[
  {
    "path": ".github/workflows/aws.yml",
    "content": "name: aws\n\non:\n  pull_request:\n  push:\n   paths:\n     - 'src-aws/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [12.x]\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v1\n      with:\n        node-version: ${{ matrix.node-version }}\n    - name: npm install\n      run: |\n        cd src-aws\n        npm ci\n        npm install -g serverless mocha\n      env:\n        CI: true\n    - name: package\n      run: |\n        cd src-aws\n        serverless package\n"
  },
  {
    "path": ".github/workflows/firmware.yml",
    "content": "name: firmware\n\non:\n  pull_request:\n  push:\n    paths:\n      - 'src-esp32/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v1\n\n    - name: Setup Python\n      uses: actions/setup-python@master\n      with:\n        python-version: '3.x'\n\n    - name: Install Platform IO\n      run: |\n        python -m pip install --upgrade pip\n        pip install -U platformio\n\n    - name: Build\n      run: |\n        cd src-esp32\n        cp src/config/config.dist.h src/config/config.h\n        mkdir -p certificates\n        echo \"a\" > certificates/amazonrootca1.pem\n        echo \"a\" > certificates/certificate.pem.crt\n        echo \"a\" > certificates/private.pem.key\n        echo \"a\" > certificates/public.pem.key\n        platformio run\n      env:\n        CI: true\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nsrc-aws/.serverless\nsrc-esp32/.vscode\nnode_modules\nsrc-esp32/certificates/*.pem\nsrc-esp32/certificates/*.crt\nsrc-esp32/certificates/*.key\n.vscode\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <a href=\"https://github.com/Savjee/home-energy-monitor\" rel=\"noopener\">\n        <img width=200px height=200px src=\"https://savjee.github.io/home-energy-monitor/readme-images/logo.png\" alt=\"Home Energy Monitor\">\n    </a>\n</p>\n\n<h3 align=\"center\">Home Energy Monitor (v2)</h3>\n\n<div align=\"center\">\n\n[![GitHub Issues](https://img.shields.io/github/issues/Savjee/home-energy-monitor.svg)](https://github.com/Savjee/home-energy-monitor/issues)\n[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/Savjee/home-energy-monitor.svg)](https://github.com/Savjee/home-energy-monitor/pulls)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE)\n\n**⚠️ This is a work in progress. By no means is this production ready.**\n</div>\n\n---\n\nESP32-based Home Energy Monitor: monitors electricity consumption of your entire house with a single CT sensor.\n\n## Structure\n\nThis project consists out of multiple components:\n\n| Folder            | Description         | Build status | \n| ----------------- | ------------------- | ------------ | \n| `src-app`         | Mobile app (Ionic)  | n/a |\n| `src-aws`         | Serverless AWS backend + GraphQL API | ![AWS Build Status](https://github.com/Savjee/home-energy-monitor/workflows/aws/badge.svg) |\n| `src-esp32`       | Firmware for the ESP32 (measuring device) | ![Firmware Build Status](https://github.com/Savjee/home-energy-monitor/workflows/firmware/badge.svg) |\n\n(TODO: add instructions on how to deploy all of this. 😅)\n\n## Video explanation\n\n<div align=\"center\">\n\n[![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/ah3ezprtgmc/0.jpg)](https://www.youtube.com/watch?v=ah3ezprtgmc)\n\n*[https://www.youtube.com/watch?v=ah3ezprtgmc](https://www.youtube.com/watch?v=ah3ezprtgmc)*\n</div>\n\nRead my blog post for more instructions: [https://savjee.be/2019/07/Home-Energy-Monitor-ESP32-CT-Sensor-Emonlib/](https://savjee.be/2019/07/Home-Energy-Monitor-ESP32-CT-Sensor-Emonlib/)\n\n## Cloud Architecture\n\nThis is the cloud architecture that powers the energy meter and the app:\n\n![AWS Cloud Architecture](https://savjee.github.io/home-energy-monitor/readme-images/architecture.png)\n\nIn a nutshell:\n* The ESP32 has a MQTT connection with AWS IoT Core\n* Every 30 seconds, 30 measurements are sent to AWS\n* These measurements are stored in DynamoDB (IoT Rule)\n* Once a day, all readings from the previous day are archived to S3\n* A GraphQL API (hosted on Lambda) exposes the data stored in DynamoDB\n\n## Screenshots\n\nWeb dashboard, built on top of the GraphQL API:\n\n![Screenshot Web Dashboard](https://savjee.github.io/home-energy-monitor/readme-images/web-dashboard.png)\n\nWhat is displayed on the ESP32 OLED display:\n\n![Screenshot ESP32 OLED](https://savjee.github.io/home-energy-monitor/readme-images/esp32-oled.jpg)\n\n\n## DIY Requirements\n\nTo build your own Energy Monitor you need the following hardware:\n\n* ESP32\n* CT sensor: YHDC SCT-013-030 (30A/1V)\n* 10µF capacitor\n* 2 resistors (between 10k-470kΩ)\n\nOther requirements:\n* AWS Account (Should be able to run in free-tier)\n* Install [PlatformIO](https://platformio.org) on your system\n* Drivers for your ESP32 board\n\nRead my blog post for more instructions: [https://savjee.be/2019/07/Home-Energy-Monitor-ESP32-CT-Sensor-Emonlib/](https://savjee.be/2019/07/Home-Energy-Monitor-ESP32-CT-Sensor-Emonlib/)\n\n\n## Contribute\n\nI'm happy to merge in any pull requests. Also feel free to report bugs or feature requests."
  },
  {
    "path": "src-app/.gitignore",
    "content": "# Specifies intentionally untracked files to ignore when using Git\n# http://git-scm.com/docs/gitignore\n\n*~\n*.sw[mnpcod]\n*.log\n*.tmp\n*.tmp.*\nlog.txt\n*.sublime-project\n*.sublime-workspace\n.vscode/\nnpm-debug.log*\n\n.idea/\n.ionic/\n.sourcemaps/\n.sass-cache/\n.tmp/\n.versions/\ncoverage/\nwww/\nnode_modules/\ntmp/\ntemp/\nplatforms/\nplugins/\nplugins/android.json\nplugins/ios.json\n$RECYCLE.BIN/\n\n.DS_Store\nThumbs.db\nUserInterfaceState.xcuserstate\n"
  },
  {
    "path": "src-app/angular.json",
    "content": "{\n  \"$schema\": \"./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json\",\n  \"version\": 1,\n  \"defaultProject\": \"app\",\n  \"newProjectRoot\": \"projects\",\n  \"projects\": {\n    \"app\": {\n      \"root\": \"\",\n      \"sourceRoot\": \"src\",\n      \"projectType\": \"application\",\n      \"prefix\": \"app\",\n      \"schematics\": {},\n      \"architect\": {\n        \"build\": {\n          \"builder\": \"@angular-devkit/build-angular:browser\",\n          \"options\": {\n            \"outputPath\": \"www\",\n            \"index\": \"src/index.html\",\n            \"main\": \"src/main.ts\",\n            \"polyfills\": \"src/polyfills.ts\",\n            \"tsConfig\": \"src/tsconfig.app.json\",\n            \"assets\": [\n              {\n                \"glob\": \"**/*\",\n                \"input\": \"src/assets\",\n                \"output\": \"assets\"\n              },\n              {\n                \"glob\": \"**/*.svg\",\n                \"input\": \"node_modules/ionicons/dist/ionicons/svg\",\n                \"output\": \"./svg\"\n              }\n            ],\n            \"styles\": [\n              {\n                \"input\": \"src/theme/variables.scss\"\n              },\n              {\n                \"input\": \"src/global.scss\"\n              }\n            ],\n            \"scripts\": []\n          },\n          \"configurations\": {\n            \"production\": {\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"src/environments/environment.ts\",\n                  \"with\": \"src/environments/environment.prod.ts\"\n                }\n              ],\n              \"optimization\": true,\n              \"outputHashing\": \"all\",\n              \"sourceMap\": false,\n              \"extractCss\": true,\n              \"namedChunks\": false,\n              \"aot\": true,\n              \"extractLicenses\": true,\n              \"vendorChunk\": false,\n              \"buildOptimizer\": true,\n              \"budgets\": [\n                {\n                  \"type\": \"initial\",\n                  \"maximumWarning\": \"2mb\",\n                  \"maximumError\": \"5mb\"\n                }\n              ]\n            },\n            \"ci\": {\n              \"progress\": false\n            }\n          }\n        },\n        \"serve\": {\n          \"builder\": \"@angular-devkit/build-angular:dev-server\",\n          \"options\": {\n            \"browserTarget\": \"app:build\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"browserTarget\": \"app:build:production\"\n            },\n            \"ci\": {\n              \"progress\": false\n            }\n          }\n        },\n        \"extract-i18n\": {\n          \"builder\": \"@angular-devkit/build-angular:extract-i18n\",\n          \"options\": {\n            \"browserTarget\": \"app:build\"\n          }\n        },\n        \"test\": {\n          \"builder\": \"@angular-devkit/build-angular:karma\",\n          \"options\": {\n            \"main\": \"src/test.ts\",\n            \"polyfills\": \"src/polyfills.ts\",\n            \"tsConfig\": \"src/tsconfig.spec.json\",\n            \"karmaConfig\": \"src/karma.conf.js\",\n            \"styles\": [],\n            \"scripts\": [],\n            \"assets\": [\n              {\n                \"glob\": \"favicon.ico\",\n                \"input\": \"src/\",\n                \"output\": \"/\"\n              },\n              {\n                \"glob\": \"**/*\",\n                \"input\": \"src/assets\",\n                \"output\": \"/assets\"\n              }\n            ]\n          },\n          \"configurations\": {\n            \"ci\": {\n              \"progress\": false,\n              \"watch\": false\n            }\n          }\n        },\n        \"lint\": {\n          \"builder\": \"@angular-devkit/build-angular:tslint\",\n          \"options\": {\n            \"tsConfig\": [\"src/tsconfig.app.json\", \"src/tsconfig.spec.json\"],\n            \"exclude\": [\"**/node_modules/**\"]\n          }\n        },\n        \"ionic-cordova-build\": {\n          \"builder\": \"@ionic/angular-toolkit:cordova-build\",\n          \"options\": {\n            \"browserTarget\": \"app:build\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"browserTarget\": \"app:build:production\"\n            }\n          }\n        },\n        \"ionic-cordova-serve\": {\n          \"builder\": \"@ionic/angular-toolkit:cordova-serve\",\n          \"options\": {\n            \"cordovaBuildTarget\": \"app:ionic-cordova-build\",\n            \"devServerTarget\": \"app:serve\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"cordovaBuildTarget\": \"app:ionic-cordova-build:production\",\n              \"devServerTarget\": \"app:serve:production\"\n            }\n          }\n        }\n      }\n    },\n    \"app-e2e\": {\n      \"root\": \"e2e/\",\n      \"projectType\": \"application\",\n      \"architect\": {\n        \"e2e\": {\n          \"builder\": \"@angular-devkit/build-angular:protractor\",\n          \"options\": {\n            \"protractorConfig\": \"e2e/protractor.conf.js\",\n            \"devServerTarget\": \"app:serve\"\n          },\n          \"configurations\": {\n            \"ci\": {\n              \"devServerTarget\": \"app:serve:ci\"\n            }\n          }\n        },\n        \"lint\": {\n          \"builder\": \"@angular-devkit/build-angular:tslint\",\n          \"options\": {\n            \"tsConfig\": \"e2e/tsconfig.e2e.json\",\n            \"exclude\": [\"**/node_modules/**\"]\n          }\n        }\n      }\n    }\n  },\n  \"cli\": {\n    \"defaultCollection\": \"@ionic/angular-toolkit\"\n  },\n  \"schematics\": {\n    \"@ionic/angular-toolkit:component\": {\n      \"styleext\": \"scss\"\n    },\n    \"@ionic/angular-toolkit:page\": {\n      \"styleext\": \"scss\"\n    }\n  }\n}\n"
  },
  {
    "path": "src-app/config.xml",
    "content": "<?xml version='1.0' encoding='utf-8'?>\n<widget id=\"be.savjee.homeenergy\" version=\"0.0.1\" xmlns=\"http://www.w3.org/ns/widgets\" xmlns:cdv=\"http://cordova.apache.org/ns/1.0\">\n    <name>Home Energy</name>\n    <description>Measuring home energy usage.</description>\n    <author email=\"hi@savjee.be\" href=\"https://savjee.be/\">Xavier Decuyper</author>\n    <content src=\"index.html\" />\n    <access origin=\"*\" />\n    <allow-intent href=\"http://*/*\" />\n    <allow-intent href=\"https://*/*\" />\n    <allow-intent href=\"tel:*\" />\n    <allow-intent href=\"sms:*\" />\n    <allow-intent href=\"mailto:*\" />\n    <allow-intent href=\"geo:*\" />\n    <preference name=\"ScrollEnabled\" value=\"false\" />\n    <preference name=\"android-minSdkVersion\" value=\"19\" />\n    <preference name=\"BackupWebStorage\" value=\"none\" />\n    <preference name=\"SplashMaintainAspectRatio\" value=\"true\" />\n    <preference name=\"FadeSplashScreenDuration\" value=\"300\" />\n    <preference name=\"SplashShowOnlyFirstTime\" value=\"false\" />\n    <preference name=\"SplashScreen\" value=\"screen\" />\n    <preference name=\"SplashScreenDelay\" value=\"3000\" />\n    <platform name=\"ios\">\n        <allow-intent href=\"itms:*\" />\n        <allow-intent href=\"itms-apps:*\" />\n        <icon height=\"57\" src=\"resources/ios/icon/icon.png\" width=\"57\" />\n        <icon height=\"114\" src=\"resources/ios/icon/icon@2x.png\" width=\"114\" />\n        <icon height=\"40\" src=\"resources/ios/icon/icon-40.png\" width=\"40\" />\n        <icon height=\"80\" src=\"resources/ios/icon/icon-40@2x.png\" width=\"80\" />\n        <icon height=\"120\" src=\"resources/ios/icon/icon-40@3x.png\" width=\"120\" />\n        <icon height=\"50\" src=\"resources/ios/icon/icon-50.png\" width=\"50\" />\n        <icon height=\"100\" src=\"resources/ios/icon/icon-50@2x.png\" width=\"100\" />\n        <icon height=\"60\" src=\"resources/ios/icon/icon-60.png\" width=\"60\" />\n        <icon height=\"120\" src=\"resources/ios/icon/icon-60@2x.png\" width=\"120\" />\n        <icon height=\"180\" src=\"resources/ios/icon/icon-60@3x.png\" width=\"180\" />\n        <icon height=\"72\" src=\"resources/ios/icon/icon-72.png\" width=\"72\" />\n        <icon height=\"144\" src=\"resources/ios/icon/icon-72@2x.png\" width=\"144\" />\n        <icon height=\"76\" src=\"resources/ios/icon/icon-76.png\" width=\"76\" />\n        <icon height=\"152\" src=\"resources/ios/icon/icon-76@2x.png\" width=\"152\" />\n        <icon height=\"167\" src=\"resources/ios/icon/icon-83.5@2x.png\" width=\"167\" />\n        <icon height=\"29\" src=\"resources/ios/icon/icon-small.png\" width=\"29\" />\n        <icon height=\"58\" src=\"resources/ios/icon/icon-small@2x.png\" width=\"58\" />\n        <icon height=\"87\" src=\"resources/ios/icon/icon-small@3x.png\" width=\"87\" />\n        <icon height=\"1024\" src=\"resources/ios/icon/icon-1024.png\" width=\"1024\" />\n        <splash height=\"1136\" src=\"resources/ios/splash/Default-568h@2x~iphone.png\" width=\"640\" />\n        <splash height=\"1334\" src=\"resources/ios/splash/Default-667h.png\" width=\"750\" />\n        <splash height=\"2208\" src=\"resources/ios/splash/Default-736h.png\" width=\"1242\" />\n        <splash height=\"1242\" src=\"resources/ios/splash/Default-Landscape-736h.png\" width=\"2208\" />\n        <splash height=\"1536\" src=\"resources/ios/splash/Default-Landscape@2x~ipad.png\" width=\"2048\" />\n        <splash height=\"2048\" src=\"resources/ios/splash/Default-Landscape@~ipadpro.png\" width=\"2732\" />\n        <splash height=\"768\" src=\"resources/ios/splash/Default-Landscape~ipad.png\" width=\"1024\" />\n        <splash height=\"2048\" src=\"resources/ios/splash/Default-Portrait@2x~ipad.png\" width=\"1536\" />\n        <splash height=\"2732\" src=\"resources/ios/splash/Default-Portrait@~ipadpro.png\" width=\"2048\" />\n        <splash height=\"1024\" src=\"resources/ios/splash/Default-Portrait~ipad.png\" width=\"768\" />\n        <splash height=\"960\" src=\"resources/ios/splash/Default@2x~iphone.png\" width=\"640\" />\n        <splash height=\"480\" src=\"resources/ios/splash/Default~iphone.png\" width=\"320\" />\n        <splash height=\"2732\" src=\"resources/ios/splash/Default@2x~universal~anyany.png\" width=\"2732\" />\n    </platform>\n    <plugin name=\"cordova-plugin-whitelist\" spec=\"1.3.3\" />\n    <plugin name=\"cordova-plugin-statusbar\" spec=\"2.4.2\" />\n    <plugin name=\"cordova-plugin-device\" spec=\"2.0.2\" />\n    <plugin name=\"cordova-plugin-splashscreen\" spec=\"5.0.2\" />\n    <plugin name=\"cordova-plugin-ionic-webview\" spec=\"^3.0.0\" />\n    <plugin name=\"cordova-plugin-ionic-keyboard\" spec=\"^2.0.5\" />\n    <engine name=\"ios\" spec=\"4.5.5\" />\n</widget>\n"
  },
  {
    "path": "src-app/e2e/protractor.conf.js",
    "content": "// Protractor configuration file, see link for more information\n// https://github.com/angular/protractor/blob/master/lib/config.ts\n\nconst { SpecReporter } = require('jasmine-spec-reporter');\n\nexports.config = {\n  allScriptsTimeout: 11000,\n  specs: [\n    './src/**/*.e2e-spec.ts'\n  ],\n  capabilities: {\n    'browserName': 'chrome'\n  },\n  directConnect: true,\n  baseUrl: 'http://localhost:4200/',\n  framework: 'jasmine',\n  jasmineNodeOpts: {\n    showColors: true,\n    defaultTimeoutInterval: 30000,\n    print: function() {}\n  },\n  onPrepare() {\n    require('ts-node').register({\n      project: require('path').join(__dirname, './tsconfig.e2e.json')\n    });\n    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));\n  }\n};\n"
  },
  {
    "path": "src-app/e2e/src/app.e2e-spec.ts",
    "content": "import { AppPage } from './app.po';\n\ndescribe('new App', () => {\n  let page: AppPage;\n\n  beforeEach(() => {\n    page = new AppPage();\n  });\n\n  it('should display welcome message', () => {\n    page.navigateTo();\n    expect(page.getPageTitle()).toContain('Tab One');\n  });\n});\n"
  },
  {
    "path": "src-app/e2e/src/app.po.ts",
    "content": "import { browser, by, element } from 'protractor';\n\nexport class AppPage {\n  navigateTo() {\n    return browser.get('/');\n  }\n\n  getPageTitle() {\n    return element(by.css('ion-title')).getText();\n  }\n}\n"
  },
  {
    "path": "src-app/e2e/tsconfig.e2e.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/app\",\n    \"module\": \"commonjs\",\n    \"target\": \"es5\",\n    \"types\": [\n      \"jasmine\",\n      \"jasminewd2\",\n      \"node\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src-app/ionic.config.json",
    "content": "{\n  \"name\": \"src-app\",\n  \"integrations\": {\n    \"cordova\": {}\n  },\n  \"type\": \"angular\"\n}\n"
  },
  {
    "path": "src-app/package.json",
    "content": "{\n  \"name\": \"src-app\",\n  \"version\": \"0.0.1\",\n  \"author\": \"Ionic Framework\",\n  \"homepage\": \"https://ionicframework.com/\",\n  \"scripts\": {\n    \"ng\": \"ng\",\n    \"start\": \"ng serve\",\n    \"build\": \"ng build\",\n    \"test\": \"ng test\",\n    \"lint\": \"ng lint\",\n    \"e2e\": \"ng e2e\"\n  },\n  \"private\": true,\n  \"dependencies\": {\n    \"@angular/common\": \"^7.2.2\",\n    \"@angular/core\": \"^7.2.2\",\n    \"@angular/forms\": \"^7.2.2\",\n    \"@angular/http\": \"^7.2.2\",\n    \"@angular/platform-browser\": \"^7.2.2\",\n    \"@angular/platform-browser-dynamic\": \"^7.2.2\",\n    \"@angular/router\": \"^7.2.2\",\n    \"@ionic-native/core\": \"^5.0.0\",\n    \"@ionic-native/splash-screen\": \"^5.0.0\",\n    \"@ionic-native/status-bar\": \"^5.0.0\",\n    \"@ionic/angular\": \"^4.11.10\",\n    \"cordova-ios\": \"4.5.5\",\n    \"cordova-plugin-device\": \"^2.0.2\",\n    \"cordova-plugin-ionic-keyboard\": \"^2.1.3\",\n    \"cordova-plugin-ionic-webview\": \"^3.1.2\",\n    \"cordova-plugin-splashscreen\": \"^5.0.2\",\n    \"cordova-plugin-statusbar\": \"^2.4.2\",\n    \"cordova-plugin-whitelist\": \"^1.3.3\",\n    \"core-js\": \"^2.5.4\",\n    \"highcharts\": \"^7.0.3\",\n    \"rxjs\": \"~6.3.3\",\n    \"zone.js\": \"~0.8.29\"\n  },\n  \"devDependencies\": {\n    \"@angular-devkit/architect\": \"~0.12.3\",\n    \"@angular-devkit/build-angular\": \"^0.803.25\",\n    \"@angular-devkit/core\": \"~7.2.3\",\n    \"@angular-devkit/schematics\": \"~7.2.3\",\n    \"@angular/cli\": \"~7.2.3\",\n    \"@angular/compiler\": \"~7.2.2\",\n    \"@angular/compiler-cli\": \"~7.2.2\",\n    \"@angular/language-service\": \"~7.2.2\",\n    \"@ionic/angular-toolkit\": \"~1.4.0\",\n    \"@types/chart.js\": \"^2.7.45\",\n    \"@types/jasmine\": \"~2.8.8\",\n    \"@types/jasminewd2\": \"~2.0.3\",\n    \"@types/node\": \"~10.12.0\",\n    \"codelyzer\": \"~4.5.0\",\n    \"jasmine-core\": \"~2.99.1\",\n    \"jasmine-spec-reporter\": \"~4.2.1\",\n    \"karma\": \"~3.1.4\",\n    \"karma-chrome-launcher\": \"~2.2.0\",\n    \"karma-coverage-istanbul-reporter\": \"~2.0.1\",\n    \"karma-jasmine\": \"~1.1.2\",\n    \"karma-jasmine-html-reporter\": \"^0.2.2\",\n    \"protractor\": \"~5.4.0\",\n    \"ts-node\": \"~8.0.0\",\n    \"tslint\": \"~5.12.0\",\n    \"typescript\": \"~3.1.6\"\n  },\n  \"description\": \"An Ionic project\",\n  \"cordova\": {\n    \"plugins\": {\n      \"cordova-plugin-whitelist\": {},\n      \"cordova-plugin-statusbar\": {},\n      \"cordova-plugin-device\": {},\n      \"cordova-plugin-splashscreen\": {},\n      \"cordova-plugin-ionic-webview\": {},\n      \"cordova-plugin-ionic-keyboard\": {}\n    },\n    \"platforms\": [\n      \"ios\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src-app/resources/README.md",
    "content": "These are Cordova resources. You can replace icon.png and splash.png and run\n`ionic cordova resources` to generate custom icons and splash screens for your\napp. See `ionic cordova resources --help` for details.\n\nCordova reference documentation:\n\n- Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html\n- Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/\n"
  },
  {
    "path": "src-app/resources/icon.png.md5",
    "content": "3cb62ec167ca6dc5a30560cac2e8caf4"
  },
  {
    "path": "src-app/resources/splash.png.md5",
    "content": "7108a139271bfc1712907b3291a2b454"
  },
  {
    "path": "src-app/src/app/app-routing.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { PreloadAllModules, RouterModule, Routes } from '@angular/router';\n\nconst routes: Routes = [\n  { path: '', loadChildren: './tabs/tabs.module#TabsPageModule' },\n];\n@NgModule({\n  imports: [\n    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })\n  ],\n  exports: [RouterModule]\n})\nexport class AppRoutingModule {}\n"
  },
  {
    "path": "src-app/src/app/app.component.html",
    "content": "<ion-app>\n  <ion-router-outlet></ion-router-outlet>\n</ion-app>\n"
  },
  {
    "path": "src-app/src/app/app.component.ts",
    "content": "import { Component } from '@angular/core';\n\nimport { Platform } from '@ionic/angular';\nimport { SplashScreen } from '@ionic-native/splash-screen/ngx';\nimport { StatusBar } from '@ionic-native/status-bar/ngx';\n\n@Component({\n  selector: 'app-root',\n  templateUrl: 'app.component.html'\n})\nexport class AppComponent {\n  constructor(\n    private platform: Platform,\n    private splashScreen: SplashScreen,\n    private statusBar: StatusBar\n  ) {\n    this.initializeApp();\n  }\n\n  initializeApp() {\n    this.platform.ready().then(() => {\n      this.statusBar.styleDefault();\n      this.splashScreen.hide();\n    });\n  }\n}\n"
  },
  {
    "path": "src-app/src/app/app.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\nimport { RouteReuseStrategy } from '@angular/router';\n\nimport { IonicModule, IonicRouteStrategy } from '@ionic/angular';\nimport { SplashScreen } from '@ionic-native/splash-screen/ngx';\nimport { StatusBar } from '@ionic-native/status-bar/ngx';\n\nimport { AppRoutingModule } from './app-routing.module';\nimport { AppComponent } from './app.component';\nimport { EnergyService } from './services/energy-service.service';\nimport { HttpClientModule } from '@angular/common/http';\nimport { DecimalPipe } from '@angular/common';\n\n@NgModule({\n  declarations: [AppComponent],\n  entryComponents: [],\n  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],\n  providers: [\n    StatusBar,\n    SplashScreen,\n    EnergyService,\n    DecimalPipe,\n    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }\n  ],\n  bootstrap: [AppComponent]\n})\nexport class AppModule {}\n"
  },
  {
    "path": "src-app/src/app/components/components.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { LoadingIndicatorComponent } from './loading-indicator/loading-indicator.component';\nimport { IonicModule } from '@ionic/angular';\n\n@NgModule({\n  declarations: [\n    LoadingIndicatorComponent\n  ],\n  exports: [\n    LoadingIndicatorComponent,\n  ],\n  imports: [\n    CommonModule,\n    IonicModule,\n  ]\n})\nexport class ComponentsModule { }\n"
  },
  {
    "path": "src-app/src/app/components/loading-indicator/loading-indicator.component.html",
    "content": "<div [class.hidden]=\"!show\">\n  <ion-progress-bar type=\"indeterminate\" ></ion-progress-bar>\n</div>"
  },
  {
    "path": "src-app/src/app/components/loading-indicator/loading-indicator.component.scss",
    "content": "div{\n    // margin-bottom: -7px;\n\n    &.hidden{\n            opacity: 0;\n    }\n}"
  },
  {
    "path": "src-app/src/app/components/loading-indicator/loading-indicator.component.ts",
    "content": "import { Component, OnInit, Input } from '@angular/core';\n\n@Component({\n  selector: 'app-loading-indicator',\n  templateUrl: './loading-indicator.component.html',\n  styleUrls: ['./loading-indicator.component.scss'],\n})\nexport class LoadingIndicatorComponent implements OnInit {\n  @Input() show = false;\n\n  constructor() { }\n\n  ngOnInit() {}\n\n}\n"
  },
  {
    "path": "src-app/src/app/services/energy-service.service.ts",
    "content": "import { Injectable } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { ToastController } from '@ionic/angular';\nimport { start } from 'repl';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class EnergyService {\n\n  /**\n   * The URL to the main GraphQL API. This will be used to make all requests.\n   */\n  private BASE_URL = \"*** YOUR GRAPHQL ENDPOINT HERE ***\";\n\n  private pendingRequests = [];\n\n  constructor(private http: HttpClient, private toastCtrl: ToastController) { }\n\n  public isLoading() {\n    return this.pendingRequests.length !== 0;\n  }\n\n  public async getStatistics(): Promise<any>{\n\n    // Calculate the start and ending dates\n    const startDate = new Date();\n\t\tstartDate.setDate(startDate.getDate() - 31);\n\n    // Convert these to timestamps and make them whole (no floats)\n\t\tconst start = Math.floor(startDate.getTime() / 1000);\n    const end = Math.ceil(Date.now() / 1000);\n\n    // Make the request\n    const data = await this.makeGraphQLRequest(`\n      query{\n        usageData(startDate:${start}, endDate:${end}){\n          timestamp,\n          dayUse,\n          nightUse\n        }\n      }\n    `);\n\n    console.log('Fetched stats:', data);\n    return data;\n  }\n\n  public async getYearlyStats(): Promise<any>{\n    const startDate = new Date();\n    startDate.setDate(1);\n    startDate.setMonth(0);\n    startDate.setHours(0);\n    startDate.setMinutes(0);\n\n    const start = Math.floor(startDate.getTime() / 1000);\n    const end = Math.ceil(Date.now() / 1000);\n\n    const data = await this.makeGraphQLRequest(`\n      query{\n        usageData(startDate:${start}, endDate:${end}){\n          timestamp,\n          dayUse,\n          nightUse,\n        }\n      }\n    `);\n\n    console.log('yearly stats', data);\n\n    const beginDates = [];\n    const outputData = [];\n\n    for (let i = 0; i <= 11; i++){\n      startDate.setMonth(i);\n\n      beginDates.push(\n        Math.floor(startDate.getTime() / 1000)\n      );\n    }\n\n    for (let i = 0; i <= 11; i++){\n      const readingsInMonth = data.data.usageData.filter(\n        item => item.timestamp > beginDates[i] && item.timestamp < beginDates[i + 1]\n      );\n\n      if (readingsInMonth.length === 0) {\n        outputData.push(0);\n        continue;\n      }\n\n      console.log(readingsInMonth);\n      const total = readingsInMonth.reduce((total, currentVal) => total + currentVal.dayUse + currentVal.nightUse);\n      outputData.push(total);\n    }\n\n    console.log(outputData);\n  }\n\n  public async getHomePageStatistics(all?: boolean): Promise<any>{\n    const timestamp = Math.floor(Date.now() / 1000 - 60);\n\n    let additionalQueries = '';\n    if (all === true) {\n      additionalQueries = `\n      stats{\n          always_on\n          today_so_far\n      }`\n    }\n\n    const data = await this.makeGraphQLRequest(`\n      query{\n        realtime(sinceTimestamp: ${timestamp}){\n          timestamp\n          reading\n        },\n        ${additionalQueries}\n      }\n    `);\n\n    console.log('realtime', data);\n\n    return data;\n  }\n\n  public async getReadings(since?: number): Promise<any> {\n    if (!since) {\n      const date = new Date();\n      date.setHours(date.getHours() - 6);\n      since = date.getTime();\n    }\n\n    console.log('since', since);\n\n    const data = await this.makeGraphQLRequest(`\n      query{\n        realtime(sinceTimestamp: ${Math.floor(since / 1000)}){\n          timestamp, reading\n        }\n      }`\n    );\n\n    console.log('readings', data.data.realtime);\n    return data.data.realtime;\n  }\n\n\n  /**\n   * Makes a request to the GraphQL API and returns a promise that should\n   * be awaited.\n   *\n   * @param query The GraphQL query that should be executed\n   */\n  private async makeGraphQLRequest(query: string): Promise<any> {\n\n    const req = this.http.post(\n      this.BASE_URL,\n      query\n    ).toPromise();\n\n    // Push the request into the array so we can show load spinners\n    // in the application at various places.\n    this.pendingRequests.push(req);\n\n    req\n      .then((data) => {\n        return data;\n      })\n      .catch(async (err) => {\n        console.error('Error making GraphQL request', err);\n\n        const toast = await this.toastCtrl.create({\n          message: 'Could not fetch data from server. Try again later.',\n          duration: 5000,\n          showCloseButton: true,\n          position: 'top',\n          color: 'dark'\n        });\n\n        toast.present();\n      })\n      .finally(() => {\n\n        // After any request, regardless of wether it was successfull\n        // or not, we have to remove it from the pendingRequests array\n        // so that all loading spinners dissapear in the UI.\n        this.pendingRequests.splice(\n          this.pendingRequests.indexOf(req),\n          1\n        );\n      });\n\n    // Return the pending requests so other methods can await it.\n    return req;\n  }\n}\n\nexport interface MainStats {\n  data: any;\n}\n"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { Routes, RouterModule } from '@angular/router';\n\nimport { IonicModule } from '@ionic/angular';\n\nimport { TabHomePage } from './tab-home.page';\nimport { ComponentsModule } from '../components/components.module';\n\nconst routes: Routes = [\n  {\n    path: '',\n    component: TabHomePage\n  }\n];\n\n@NgModule({\n  imports: [\n    CommonModule,\n    FormsModule,\n    IonicModule,\n    RouterModule.forChild(routes),\n    ComponentsModule,\n  ],\n  declarations: [TabHomePage]\n})\nexport class TabHomeModule {}\n"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.page.html",
    "content": "<ion-header>\n    <ion-toolbar>\n      <ion-buttons slot=\"end\">\n        <ion-spinner *ngIf=\"false\"></ion-spinner>\n      </ion-buttons>\n      <ion-title>Home</ion-title>\n    </ion-toolbar>\n  </ion-header>\n\n  <app-loading-indicator [show]=\"energyService.isLoading()\"></app-loading-indicator>\n\n  <ion-content padding>\n\n\n    <div class=\"center\">\n        <div class=\"container\">\n            <div class=\"circle current\" [routerLink]=\"['/tabs/readings']\">\n              <p>\n                <span class=\"value\" *ngIf=\"stats.current !== '?'\">{{stats.current | number:'1.0-0' }}W</span>\n                <span class=\"value\" *ngIf=\"stats.current === '?'\">&#8961; W</span>\n                <span class=\"label\">real-time</span>\n              </p>\n            </div>\n            <div class=\"circle alwayson\">\n                <p>\n                    <span class=\"value\" *ngIf=\"stats.always_on !== '?'\">{{stats.always_on | number:'1.0-0' }} W</span>\n                    <span class=\"value\" *ngIf=\"stats.always_on === '?'\">&#8961; W</span>\n                    <span class=\"label\">always-on</span>\n                  </p>\n            </div>\n            <div class=\"circle today\" [routerLink]=\"['/tabs/statistics']\">\n                <p>\n                    <span class=\"value\" *ngIf=\"stats.today_so_far !== '?'\">{{stats.today_so_far | number:'1.0-1'}} kWh</span>\n                    <span class=\"value\" *ngIf=\"stats.today_so_far === '?'\">&#8961; kWh</span>\n                    <span class=\"label\">today so far</span>\n                  </p>\n            </div>\n        </div>\n    </div>\n\n\n\n  </ion-content>\n"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.page.scss",
    "content": ".center{\n    display: flex;\n    justify-content: center;\n    flex-direction: column;\n    text-align: center;\n    height: 100%;\n}\n.container{\n    position: relative;\n    height: 455px;\n    width: 340px;\n    align-self: center;\n\n    .circle{\n\n        border-radius: 100%;\n        background-blend-mode: color-burn;\n\n        justify-content: center;\n        align-items: center;\n        border-radius: 100%;\n        text-align: center;\n        display: flex;\n        flex-wrap: wrap;\n\n        color: #fff;\n\n        .label, .value{\n            display: block;\n        }\n        .label{\n            font-weight: 100;\n            font-size: 19px;\n            text-transform: lowercase;\n        }\n\n        .value{\n            font-weight: 500;\n        }\n\n        &.current{\n            background-color: rgba(#1D6CFF, 0.9);\n            width: 300px;\n            height: 300px;\n            margin: 0 auto;\n\n            .value{\n                font-size: 60px;\n            }\n        }\n\n        &.alwayson{\n            background-color: rgba(#8440FF, 0.9);\n            width: 152px;\n            height: 152px;\n\n            position:absolute;\n            top: 250px;\n            left: 0;\n\n            .value{\n                font-size: 30px;\n            }\n        }\n\n        &.today{\n            background-color: rgba(#C324FF, 0.9);\n            width: 210px;\n            height: 210px;\n\n            position:absolute;\n            top: 244px;\n            right: 0;\n\n            .value{\n                font-size: 35px;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.page.ts",
    "content": "import { Component, OnInit } from '@angular/core';\nimport { EnergyService } from '../services/energy-service.service';\n\n@Component({\n  selector: 'app-tab-home',\n  templateUrl: './tab-home.page.html',\n  styleUrls: ['./tab-home.page.scss'],\n})\nexport class TabHomePage {\n\n  public stats = {\n    current: '?',\n    always_on: '?',\n    today_so_far: '?',\n  }\n\n\n  private intervalTimer = null;\n  private scheduledTimeout = null;\n  private lastUpdate = null;\n\n  constructor(public energyService: EnergyService) { }\n\n  /**\n   * Called when the page has been loaded for the very first time. Does not\n   * re-fire when the page is opened a second time.\n   */\n  async ionViewDidLoad() {\n    // When the app resumes from the background we should see if we have to\n    // refresh the data or not. Same logic as when the app first boots.\n    document.addEventListener(\"resume\", async () => {\n      await this.refreshDataIfNeeded();\n    }, false);\n  }\n\n  /**\n   * Called everytime the screen enters the view, regardless of wether it has\n   * been loaded before or not.\n   */\n  async ionViewWillEnter() {\n    await this.refreshDataIfNeeded();\n  }\n\n  /**\n   * Called when the user leaves the main screen. Here we should cancel the\n   * timer that automatically refreshes the screen.\n   */\n  async ionViewDidLeave() {\n    clearInterval(this.intervalTimer);\n  }\n\n  /**\n   * Determns what should happen with the background refresh operations\n   * (cancel them or schedule new ones). This function should be called\n   * everytime the page is opened or when the app is resumed.\n   */\n  private async refreshDataIfNeeded() {\n    // Define how long we should wait in between refreshed (30 seconds)\n    const minimumWaitTime = 30 * 1000;\n\n    if (this.intervalTimer !== null) {\n      clearInterval(this.intervalTimer);\n    }\n\n    // If we haven't updated before, we can do it straight away and not\n    // care about anything else.\n    if (this.lastUpdate === null) {\n      await this.fetchData(true);\n      this.scheduleTimer();\n      return;\n    }\n\n    const updateTimeDelta = Date.now() - this.lastUpdate;\n\n    // If we updated before but it was more then 30 seconds ago, refresh it\n    // now again and don't continue executing code.\n    if (updateTimeDelta > minimumWaitTime) {\n      await this.fetchData();\n      this.scheduleTimer();\n      return;\n    }\n\n    // If we get here it means we updates less then 30 seconds but the user\n    // has reopened the dashboard. Calculate how long we must wait to reach 30\n    // seconds and then schedule a timer to do the refresh.\n    this.scheduleTimer(minimumWaitTime - updateTimeDelta);\n    return;\n  }\n\n  /**\n   * Schedules a refresh action after a given delay of waitMs milliseconds.\n   * Also cancels any pending timeout should there be one.\n   * @param waitMs Time to wait in milliseconds\n   */\n  private scheduleTimer(waitMs = 0) {\n    if (this.scheduledTimeout) {\n      clearTimeout(this.scheduledTimeout);\n    }\n\n    this.scheduledTimeout = setTimeout(async () => {\n      this.intervalTimer = setInterval(async () => {\n        await this.fetchData();\n      }, 30 * 1000);\n    }, waitMs);\n\n  }\n\n  /**\n   * Actually fetches data from the backend. Should not be called more then\n   * once every 30 seconds because there are no new datapoints anyway.\n   *\n   * @param all Wether or not to fetch the \"always_on\" and \"today_so_far\"\n   *            Best to set these to false if not needed (better performance)\n   */\n  private async fetchData(all?: boolean) {\n    const realtime = await this.energyService.getHomePageStatistics(all);\n\n    this.stats.current = realtime.data.realtime[realtime.data.realtime.length - 1].reading;\n\n    if (all === true) {\n      this.stats.always_on = realtime.data.stats.always_on;\n      this.stats.today_so_far = realtime.data.stats.today_so_far;\n    }\n\n    this.lastUpdate = Date.now();\n  }\n}\n"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { Routes, RouterModule } from '@angular/router';\n\nimport { IonicModule } from '@ionic/angular';\n\nimport { TabReadingsPage } from './tab-readings.page';\nimport { ComponentsModule } from '../components/components.module';\n\nconst routes: Routes = [\n  {\n    path: '',\n    component: TabReadingsPage\n  }\n];\n\n@NgModule({\n  imports: [\n    CommonModule,\n    FormsModule,\n    IonicModule,\n    RouterModule.forChild(routes),\n    ComponentsModule\n  ],\n  declarations: [TabReadingsPage]\n})\nexport class TabReadingsModule {}\n"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.page.html",
    "content": "<ion-header>\n  <ion-toolbar>\n    <ion-title>Readings</ion-title>\n  </ion-toolbar>\n\n  <ion-toolbar class=\"datePicker\">\n    <ion-icon name=\"arrow-back\" slot=\"start\" (click)=\"goToYesterday()\"></ion-icon>\n\n    <ion-title>\n      <ion-datetime display-format=\"MMM DD, YYYY\" [(ngModel)]=\"selectedDate\" (ionChange)=\"dateChanged()\"></ion-datetime>\n    </ion-title>\n\n    <ion-icon name=\"arrow-forward\" slot=\"end\" (click)=\"goToTomorrow()\" *ngIf=\"showForwardArrow\"></ion-icon>\n  </ion-toolbar>\n</ion-header>\n\n<app-loading-indicator [show]=\"energyService.isLoading()\"></app-loading-indicator>\n\n<ion-content [scrollX]=\"false\" [scrollY]=\"false\">\n  <div class=\"graph-wrapper\">\n    <div #chart></div>\n  </div>\n</ion-content>"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.page.scss",
    "content": ".datePicker{\n    ion-title{\n        font-weight: normal !important;\n    }\n}\n\n.graph-wrapper{\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n\n    div{\n        width: 100%;\n        height: 100%;\n    }\n}"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.page.ts",
    "content": "import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';\nimport { EnergyService } from '../services/energy-service.service';\nimport * as Highcharts from 'highcharts';\nimport { ChartDefaults } from '../utils/chart-defaults';\n\n@Component({\n  selector: 'app-tab-readings',\n  templateUrl: './tab-readings.page.html',\n  styleUrls: ['./tab-readings.page.scss'],\n})\nexport class TabReadingsPage {\n  public selectedDate = new Date().toISOString();\n  public todaysDate = new Date().toISOString();\n  public showForwardArrow = false;\n\n  @ViewChild('chart') private mainChartRef: ElementRef;\n\n  // The JS timestamp of when we last updated the data in the chart\n  private lastUpdated = null;\n\n  // Data that is used to plot the chart\n  private chartData = [];\n\n  constructor(public energyService: EnergyService) { }\n\n  public async ionViewWillEnter() {\n    // If we don't have any data in memory, go out and fetch them\n    if (this.chartData.length === 0) {\n      await this.refreshReadings();\n      return;\n    }\n\n    // If the data we have is older then 30 minutes, refresh them!\n    if (this.lastUpdated < Date.now() - 30*60*1000) {\n      await this.refreshReadings();\n      return;\n    }\n  }\n\n  /**\n   * Refreshes the data in the graph. If we already have downloaded data before,\n   * it only fetches new data, after the lastUpdated timestamp.\n   */\n  private async refreshReadings() {\n    const data = await this.energyService.getReadings(this.lastUpdated);\n    this.lastUpdated = Date.now();\n\n    const filtered = data.filter(item => item.timestamp % 30 === 0);\n    this.chartData = this.chartData.concat(filtered);\n\n    this.renderChart();\n  }\n\n  /**\n   * Called when the date was changed through the picker or with\n   * the arrows next to it.\n   */\n  public dateChanged() {\n\n    // Show the forward arrow when the selected date is not equal to\n    // todays date\n    this.showForwardArrow =\n      this.selectedDate.substring(0, 10) !== this.todaysDate.substring(0, 10);\n  }\n\n  /**\n   * Called when the user clicks on the forward arrow\n   */\n  public goToTomorrow() {\n    this.manipulateSelectedDateBy(1);\n  }\n\n  /**\n   * Called when the user clicks on the backwards arrow\n   */\n  public goToYesterday() {\n    this.manipulateSelectedDateBy(-1);\n  }\n\n  /**\n   * Responsible for fetching the required data from the API and\n   * storing it in the private \"chartData\" field. Also calls the\n   * \"renderChart\" function to update if necessary.\n   */\n  private fetchDataForDate() {\n\n  }\n\n  private renderChart() {\n    const data = this.chartData.map(item => [item.timestamp * 1000, item.reading]);\n    const values = data.map(item => item[1]);\n\n    Highcharts.chart(this.mainChartRef.nativeElement, {\n      ...ChartDefaults,\n      chart: {\n        type: \"line\",\n        panning: true,\n        pinchType: 'x',\n        events: {\n          load() {\n            this.xAxis[0].setExtremes(Date.now() - 4 * 60 * 60 * 1000, Date.now())\n          }\n        }\n      },\n      xAxis: {\n        type: 'datetime',\n      },\n      yAxis: {\n        max: Math.max(...values),\n        title: {\n          text: null,\n        }\n      },\n      series: [{\n        type: null,\n        name: 'Usage',\n        color: '#8440FF',\n        data: data\n      }],\n      tooltip: {\n        valueSuffix: 'W',\n        followTouchMove: false,\n      },\n    });\n  }\n\n  /**\n   * Takes the currently selected date and increments it by a given\n   * amount of days. If given a negative number it goes backwards.\n   */\n  private manipulateSelectedDateBy(amount: number) {\n    const date = new Date(this.selectedDate);\n    date.setDate(date.getDate() + amount);\n    this.selectedDate = date.toISOString();\n  }\n}\n"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { Routes, RouterModule } from '@angular/router';\n\nimport { IonicModule } from '@ionic/angular';\n\nimport { TabStatisticsPage } from './tab-statistics.page';\nimport { ComponentsModule } from '../components/components.module';\n\nconst routes: Routes = [\n  {\n    path: '',\n    component: TabStatisticsPage\n  }\n];\n\n@NgModule({\n  imports: [\n    CommonModule,\n    FormsModule,\n    IonicModule,\n    RouterModule.forChild(routes),\n    ComponentsModule,\n  ],\n  declarations: [TabStatisticsPage]\n})\nexport class TabStatisticsModule {}\n"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.page.html",
    "content": "<ion-header>\n  <ion-toolbar>\n    <ion-title>Statistics</ion-title>\n    <ion-spinner *ngIf=\"energyService.isLoading()\"></ion-spinner>\n  </ion-toolbar>\n\n  <ion-toolbar>\n      <ion-segment value=\"30days\" (ionChange)=\"segmentChanged($event)\">\n          <ion-segment-button value=\"30days\">\n              <ion-label>Last 30 days</ion-label>\n          </ion-segment-button>\n          <ion-segment-button value=\"12months\">\n              <ion-label>Last 12 months</ion-label>\n          </ion-segment-button>\n        </ion-segment>\n  </ion-toolbar>\n</ion-header>\n\n<app-loading-indicator [show]=\"energyService.isLoading()\"></app-loading-indicator>\n\n<ion-content padding [class.hidden]=\"activeSegmentControl !== '30days'\">\n      <ion-card-subtitle>Breakdown per day</ion-card-subtitle>\n      <div #usageChart class=\"chart height260\"></div>\n\n      <br>\n\n      <ion-card-subtitle>Day vs. Night</ion-card-subtitle>\n      <div #dayVsNight class=\"chart\"></div>\n\n      <br><br>\n\n      <ion-card-subtitle>More</ion-card-subtitle>\n      <ion-list>\n          <ion-item>\n            <ion-label>Daily average</ion-label>\n            <ion-note slot=\"end\">{{ moreStats.daily_average | number:'1.0-2' }} kWh</ion-note>\n          </ion-item>\n          <ion-item>\n            <ion-label>Total last 30 days</ion-label>\n            <ion-note slot=\"end\">{{ moreStats.total_30days | number:'1.0-2' }} kWh</ion-note>\n          </ion-item>\n        </ion-list>\n</ion-content>\n\n<ion-content padding [class.hidden]=\"activeSegmentControl !== '12months'\">\n  <div class=\"full-graph-wrapper\">\n    <div #yearlyChart class=\"chart fullheight\"></div>\n  </div>\n</ion-content>"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.page.scss",
    "content": "$chartHeight: 180px;\n\ndiv.chart{\n    width: 100%;\n    height: $chartHeight;\n    max-height: $chartHeight;\n\n    &.height260{\n        height: 260px;\n        max-height: 260px;\n    }\n\n    &.fullheight{\n        height: 100%;\n        max-height: none;\n        width: 100%;\n    }\n}\n\nion-content{\n    &.hidden{\n        display:none;\n    }\n}\n\nion-item{\n    --padding-start: 0;\n}\n\nion-segment-button{\n    min-width: 120px;\n}\n\n.full-graph-wrapper{\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n}"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.page.ts",
    "content": "import { Component, OnInit, ElementRef, ViewChild, AfterViewInit } from '@angular/core';\nimport { EnergyService } from '../services/energy-service.service';\nimport * as Highcharts from 'highcharts';\nimport { DecimalPipe } from '@angular/common';\nimport { ChartDefaults } from '../utils/chart-defaults';\n\n@Component({\n  selector: 'app-tab-statistics',\n  templateUrl: './tab-statistics.page.html',\n  styleUrls: ['./tab-statistics.page.scss'],\n})\nexport class TabStatisticsPage implements OnInit, AfterViewInit {\n\n  @ViewChild('usageChart') private usageChartRef: ElementRef;\n  @ViewChild('dayVsNight') private dayVsNightChartRef: ElementRef;\n  @ViewChild('yearlyChart') private yearlyChartRef: ElementRef;\n\n  public activeSegmentControl = \"30days\";\n\n  public moreStats = {\n    daily_average: null,\n    total_30days: null,\n  }\n\n  // Keeps track of which segments we already rendered and did all the\n  // network requests for. This to prevent multiple calls to the backend.\n  private segmentsRendered = {\n    '30days': false,\n    '12months': false,\n  }\n\n  constructor(public energyService: EnergyService, private decimalPipe: DecimalPipe) { }\n\n  ngOnInit() {\n  }\n\n  async ngAfterViewInit() {\n    await this.segment30daysWasOpened();\n  }\n\n  /**\n   * Called whenever the user switches to another segment. Responsible\n   * for calling the correct function based on this event.\n   */\n  public segmentChanged(event) {\n    this.activeSegmentControl = event.detail.value;\n\n    if (this.activeSegmentControl === '30days') {\n      this.segment30daysWasOpened();\n    }\n\n    if (this.activeSegmentControl === '12months') {\n      this.segmentYearlyOverviewWasOpened();\n    }\n  }\n\n  /**\n   * Called when the user wants to open the \"Last 30 days\" summary\n   * segment. It fetches the required data from the server and renders\n   * a few charts on the screens asynchrounously.\n   */\n  private async segment30daysWasOpened() {\n    // If we already rendered the charts we shouldn't do it again!\n    if (this.segmentsRendered[\"30days\"]) {\n      return;\n    }\n\n    // Fetch the data we need\n    const data = await this.energyService.getStatistics();\n\n    // Calculate total day/night usage\n    let totalDay = 0;\n    let totalNight = 0;\n\n    for (const entry of data.data.usageData) {\n      totalDay += entry.dayUse;\n      totalNight += entry.nightUse;\n    }\n\n    // Simultanuously draw all our charts on screen\n    await Promise.all([\n      this.drawDayVsNightChart(totalDay, totalNight),\n      this.drawDailyUsageChart(data),\n      this.calculateMoreStats(data),\n    ]);\n\n    this.segmentsRendered[\"30days\"] = true;\n  }\n\n  private async segmentYearlyOverviewWasOpened() {\n    if (this.segmentsRendered['12months']) {\n      return;\n    }\n\n    setTimeout(async () => {\n      await this.drawYearlyOverviewChart();\n      this.segmentsRendered[\"12months\"] = true;\n    }, 0);\n  }\n\n  private formatTimestampForChartAxis(rawTimestamp){\n    const date = new Date(rawTimestamp * 1000);\n    const months = [\"Jan\", 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n    return date.getDate() + ' ' + months[date.getMonth()];\n  }\n\n  /**\n   * Calculate additional statistics that are displayed as text. This is based on\n   * the data given to the function (return data from the API).\n   */\n  private async calculateMoreStats(data) {\n    const dailyTotals = data.data.usageData.map(item => item.dayUse + item.nightUse);\n\n    const total = dailyTotals.reduce((a, b) => a + b);\n\n    this.moreStats.daily_average = total / dailyTotals.length;\n    this.moreStats.total_30days = total;\n  }\n\n  private async drawYearlyOverviewChart() {\n    await this.energyService.getYearlyStats();\n\n    Highcharts.chart(this.yearlyChartRef.nativeElement, {\n      ...ChartDefaults,\n      chart: {\n          type: 'bar'\n      },\n      xAxis: {\n        categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'],\n      },\n      yAxis: {\n        visible: false,\n      },\n      plotOptions: {\n        series: {\n            color: '#8440FF',\n            stacking: 'normal'\n          }\n      },\n      series: [\n        {\n          type: 'bar',\n          name: 'All months',\n          data: [5, 3, 4, 7, 2, 0, 0, 0, 0, 0, 0, 0]\n        },\n      ],\n    });\n  }\n\n  /**\n   * Draw the pie chart that shows the difference between day and night usage.\n   */\n  private async drawDayVsNightChart(dayUsage: number, nightUsage: number) {\n    const self = this;\n\n    Highcharts.chart(this.dayVsNightChartRef.nativeElement, {\n      ...ChartDefaults,\n      chart: {\n        type: 'pie'\n      },\n      plotOptions:{\n        pie: {\n          colors: ['#534B62', '#8440FF'],\n          dataLabels:{\n            enabled: true,\n            distance: -30,\n            style:{\n              color: 'white'\n            }\n          }\n        }\n      },\n      tooltip: {\n        formatter: function(){\n          return `<b>${this.point.name} usage:</b>\n                  <br>${self.decimalPipe.transform(this.y, '1.1-2')} kWh`;\n        }\n      },\n      series: [{\n        type: 'pie',\n        name: 'Day vs Night',\n        data: [\n          ['Night', nightUsage],\n          ['Day', dayUsage],\n        ]\n      }]\n    });\n  }\n\n  private async drawDailyUsageChart(data) {\n    // Reference to our page instance for use inside the formatter\n    // of Highcharts (arrow functions not allowed)\n    const self = this;\n\n    Highcharts.chart(this.usageChartRef.nativeElement, {\n      ...ChartDefaults,\n      chart: {\n        type: 'column',\n      },\n      xAxis:{\n        categories: data.data.usageData.map(el => this.formatTimestampForChartAxis(el.timestamp)),\n      },\n      yAxis:{\n        allowDecimals: false,\n        title:{\n          text: null,\n        }\n      },\n      tooltip: {\n        formatter: function(){\n          return `<b>${this.x} - ${this.series.name} usage:</b>\n                  <br> ${self.decimalPipe.transform(this.y, '1.1-2')} kWh`;\n        }\n      },\n      plotOptions: {\n        column: {\n          stacking: 'normal',\n          pointPadding: 0,\n          borderWidth: 0\n        }\n      },\n      series: [\n        {\n          type: 'column',\n          name: 'Night',\n          color: '#534B62',\n          data: data.data.usageData.map(el => el.nightUse),\n        },\n        {\n          type: 'column',\n          name: 'Day',\n          color: '#8440FF',\n          data: data.data.usageData.map(el => el.dayUse),\n        },\n      ]\n    });\n  }\n}\n"
  },
  {
    "path": "src-app/src/app/tabs/tabs.module.ts",
    "content": "import { IonicModule } from '@ionic/angular';\nimport { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n\nimport { TabsPageRoutingModule } from './tabs.router.module';\n\nimport { TabsPage } from './tabs.page';\n\n@NgModule({\n  imports: [\n    IonicModule,\n    CommonModule,\n    FormsModule,\n    TabsPageRoutingModule\n  ],\n  declarations: [TabsPage]\n})\nexport class TabsPageModule {}\n"
  },
  {
    "path": "src-app/src/app/tabs/tabs.page.html",
    "content": "<ion-tabs>\n\n  <ion-tab-bar slot=\"bottom\">\n    <ion-tab-button tab=\"home\">\n      <ion-icon name=\"flash\"></ion-icon>\n      <ion-label>Home</ion-label>\n    </ion-tab-button>\n\n    <ion-tab-button tab=\"readings\">\n      <ion-icon name=\"pulse\"></ion-icon>\n      <ion-label>Readings</ion-label>\n    </ion-tab-button>\n\n    <ion-tab-button tab=\"statistics\">\n      <ion-icon name=\"stats\"></ion-icon>\n      <ion-label>Statistics</ion-label>\n    </ion-tab-button>\n\n    <!-- <ion-tab-button tab=\"tab3\">\n      <ion-icon name=\"more\"></ion-icon>\n      <ion-label>More</ion-label>\n    </ion-tab-button> -->\n  </ion-tab-bar>\n\n</ion-tabs>\n"
  },
  {
    "path": "src-app/src/app/tabs/tabs.page.scss",
    "content": "\n"
  },
  {
    "path": "src-app/src/app/tabs/tabs.page.ts",
    "content": "import { Component } from '@angular/core';\n\n@Component({\n  selector: 'app-tabs',\n  templateUrl: 'tabs.page.html',\n  styleUrls: ['tabs.page.scss']\n})\nexport class TabsPage {}\n"
  },
  {
    "path": "src-app/src/app/tabs/tabs.router.module.ts",
    "content": "import { NgModule } from '@angular/core';\nimport { RouterModule, Routes } from '@angular/router';\nimport { TabsPage } from './tabs.page';\n\nconst routes: Routes = [\n  {\n    path: 'tabs',\n    component: TabsPage,\n    children: [\n      {\n        path: 'home',\n        children: [\n          {\n            path: '',\n            loadChildren: '../tab-home/tab-home.module#TabHomeModule'\n          }\n        ]\n      },\n      {\n        path: 'readings',\n        children: [\n          {\n            path: '',\n            loadChildren: '../tab-readings/tab-readings.module#TabReadingsModule'\n          }\n        ]\n      },\n      {\n        path: 'statistics',\n        children: [\n          {\n            path: '',\n            loadChildren: '../tab-statistics/tab-statistics.module#TabStatisticsModule'\n          }\n        ]\n      },\n      {\n        path: '',\n        redirectTo: '/tabs/home',\n        pathMatch: 'full'\n      }\n    ]\n  },\n  {\n    path: '',\n    redirectTo: '/tabs/home',\n    pathMatch: 'full'\n  }\n];\n\n@NgModule({\n  imports: [\n    RouterModule.forChild(routes)\n  ],\n  exports: [RouterModule]\n})\nexport class TabsPageRoutingModule {}\n"
  },
  {
    "path": "src-app/src/app/utils/chart-defaults.ts",
    "content": "export const ChartDefaults = {\n    title: {\n        text: null,\n    },\n    yAxis: {\n        min: 0,\n    },\n    legend: {\n        enabled: false,\n    },\n    credits: {\n        enabled: false,\n    },\n}"
  },
  {
    "path": "src-app/src/environments/environment.prod.ts",
    "content": "export const environment = {\n  production: true\n};\n"
  },
  {
    "path": "src-app/src/environments/environment.ts",
    "content": "// This file can be replaced during build by using the `fileReplacements` array.\n// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.\n// The list of file replacements can be found in `angular.json`.\n\nexport const environment = {\n  production: false\n};\n\n/*\n * For easier debugging in development mode, you can import the following file\n * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.\n *\n * This import should be commented out in production mode because it will have a negative impact\n * on performance if an error is thrown.\n */\n// import 'zone.js/dist/zone-error';  // Included with Angular CLI.\n"
  },
  {
    "path": "src-app/src/global.scss",
    "content": "// http://ionicframework.com/docs/theming/\n@import '~@ionic/angular/css/core.css';\n@import '~@ionic/angular/css/normalize.css';\n@import '~@ionic/angular/css/structure.css';\n@import '~@ionic/angular/css/typography.css';\n\n@import '~@ionic/angular/css/padding.css';\n@import '~@ionic/angular/css/float-elements.css';\n@import '~@ionic/angular/css/text-alignment.css';\n@import '~@ionic/angular/css/text-transformation.css';\n@import '~@ionic/angular/css/flex-utils.css';\n"
  },
  {
    "path": "src-app/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\" />\n  <title>Home Energy</title>\n\n  <base href=\"/\" />\n\n  <meta name=\"viewport\" content=\"viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n  <meta name=\"format-detection\" content=\"telephone=no\" />\n  <meta name=\"msapplication-tap-highlight\" content=\"no\" />\n\n  <link rel=\"icon\" type=\"image/png\" href=\"assets/icon/favicon.png\" />\n\n  <!-- add to homescreen for ios -->\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\" />\n</head>\n\n<body>\n  <app-root></app-root>\n</body>\n\n</html>\n"
  },
  {
    "path": "src-app/src/karma.conf.js",
    "content": "// Karma configuration file, see link for more information\n// https://karma-runner.github.io/1.0/config/configuration-file.html\n\nmodule.exports = function (config) {\n  config.set({\n    basePath: '',\n    frameworks: ['jasmine', '@angular-devkit/build-angular'],\n    plugins: [\n      require('karma-jasmine'),\n      require('karma-chrome-launcher'),\n      require('karma-jasmine-html-reporter'),\n      require('karma-coverage-istanbul-reporter'),\n      require('@angular-devkit/build-angular/plugins/karma')\n    ],\n    client: {\n      clearContext: false // leave Jasmine Spec Runner output visible in browser\n    },\n    coverageIstanbulReporter: {\n      dir: require('path').join(__dirname, '../coverage'),\n      reports: ['html', 'lcovonly', 'text-summary'],\n      fixWebpackSourcePaths: true\n    },\n    reporters: ['progress', 'kjhtml'],\n    port: 9876,\n    colors: true,\n    logLevel: config.LOG_INFO,\n    autoWatch: true,\n    browsers: ['Chrome'],\n    singleRun: false\n  });\n};\n"
  },
  {
    "path": "src-app/src/main.ts",
    "content": "import { enableProdMode } from '@angular/core';\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynamic';\n\nimport { AppModule } from './app/app.module';\nimport { environment } from './environments/environment';\n\nif (environment.production) {\n  enableProdMode();\n}\n\nplatformBrowserDynamic().bootstrapModule(AppModule)\n  .catch(err => console.log(err));\n"
  },
  {
    "path": "src-app/src/polyfills.ts",
    "content": "/**\n * This file includes polyfills needed by Angular and is loaded before the app.\n * You can add your own extra polyfills to this file.\n *\n * This file is divided into 2 sections:\n *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.\n *   2. Application imports. Files imported after ZoneJS that should be loaded before your main\n *      file.\n *\n * The current setup is for so-called \"evergreen\" browsers; the last versions of browsers that\n * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),\n * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.\n *\n * Learn more in https://angular.io/guide/browser-support\n */\n\n/***************************************************************************************************\n * BROWSER POLYFILLS\n */\n\n/** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills.\n *  This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot\n */\n\n// import 'core-js/es6/symbol';\n// import 'core-js/es6/object';\n// import 'core-js/es6/function';\n// import 'core-js/es6/parse-int';\n// import 'core-js/es6/parse-float';\n// import 'core-js/es6/number';\n// import 'core-js/es6/math';\n// import 'core-js/es6/string';\n// import 'core-js/es6/date';\n// import 'core-js/es6/array';\n// import 'core-js/es6/regexp';\n// import 'core-js/es6/map';\n// import 'core-js/es6/weak-map';\n// import 'core-js/es6/set';\n\n/** IE10 and IE11 requires the following for NgClass support on SVG elements */\n// import 'classlist.js';  // Run `npm install --save classlist.js`.\n\n/** IE10 and IE11 requires the following for the Reflect API. */\n// import 'core-js/es6/reflect';\n\n/**\n * Web Animations `@angular/platform-browser/animations`\n * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.\n * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).\n */\n// import 'web-animations-js';  // Run `npm install --save web-animations-js`.\n\n/**\n * By default, zone.js will patch all possible macroTask and DomEvents\n * user can disable parts of macroTask/DomEvents patch by setting following flags\n * because those flags need to be set before `zone.js` being loaded, and webpack\n * will put import in the top of bundle, so user need to create a separate file\n * in this directory (for example: zone-flags.ts), and put the following flags\n * into that file, and then add the following code before importing zone.js.\n * import './zone-flags.ts';\n *\n * The flags allowed in zone-flags.ts are listed here.\n *\n * The following flags will work for all browsers.\n *\n * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame\n * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick\n * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames\n *\n *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js\n *  with the following flag, it will bypass `zone.js` patch for IE/Edge\n *\n *  (window as any).__Zone_enable_cross_context_check = true;\n *\n */\n\n/***************************************************************************************************\n * Zone JS is required by default for Angular itself.\n */\nimport 'zone.js/dist/zone';  // Included with Angular CLI.\n\n\n/***************************************************************************************************\n * APPLICATION IMPORTS\n */\n"
  },
  {
    "path": "src-app/src/test.ts",
    "content": "// This file is required by karma.conf.js and loads recursively all the .spec and framework files\n\nimport 'zone.js/dist/zone-testing';\nimport { getTestBed } from '@angular/core/testing';\nimport {\n  BrowserDynamicTestingModule,\n  platformBrowserDynamicTesting\n} from '@angular/platform-browser-dynamic/testing';\n\ndeclare const require: any;\n\n// First, initialize the Angular testing environment.\ngetTestBed().initTestEnvironment(\n  BrowserDynamicTestingModule,\n  platformBrowserDynamicTesting()\n);\n// Then we find all the tests.\nconst context = require.context('./', true, /\\.spec\\.ts$/);\n// And load the modules.\ncontext.keys().map(context);\n"
  },
  {
    "path": "src-app/src/theme/variables.scss",
    "content": "// Ionic Variables and Theming. For more info, please see:\n// http://ionicframework.com/docs/theming/\n\n/** Ionic CSS Variables **/\n:root {\n  /** primary **/\n  --ion-color-primary: #8440FF;\n  --ion-color-primary-rgb: 56, 128, 255;\n  --ion-color-primary-contrast: #ffffff;\n  --ion-color-primary-contrast-rgb: 255, 255, 255;\n  --ion-color-primary-shade: #3171e0;\n  --ion-color-primary-tint: #4c8dff;\n\n  /** secondary **/\n  --ion-color-secondary: #0cd1e8;\n  --ion-color-secondary-rgb: 12, 209, 232;\n  --ion-color-secondary-contrast: #ffffff;\n  --ion-color-secondary-contrast-rgb: 255, 255, 255;\n  --ion-color-secondary-shade: #0bb8cc;\n  --ion-color-secondary-tint: #24d6ea;\n\n  /** tertiary **/\n  --ion-color-tertiary: #7044ff;\n  --ion-color-tertiary-rgb: 112, 68, 255;\n  --ion-color-tertiary-contrast: #ffffff;\n  --ion-color-tertiary-contrast-rgb: 255, 255, 255;\n  --ion-color-tertiary-shade: #633ce0;\n  --ion-color-tertiary-tint: #7e57ff;\n\n  /** success **/\n  --ion-color-success: #10dc60;\n  --ion-color-success-rgb: 16, 220, 96;\n  --ion-color-success-contrast: #ffffff;\n  --ion-color-success-contrast-rgb: 255, 255, 255;\n  --ion-color-success-shade: #0ec254;\n  --ion-color-success-tint: #28e070;\n\n  /** warning **/\n  --ion-color-warning: #ffce00;\n  --ion-color-warning-rgb: 255, 206, 0;\n  --ion-color-warning-contrast: #ffffff;\n  --ion-color-warning-contrast-rgb: 255, 255, 255;\n  --ion-color-warning-shade: #e0b500;\n  --ion-color-warning-tint: #ffd31a;\n\n  /** danger **/\n  --ion-color-danger: #f04141;\n  --ion-color-danger-rgb: 245, 61, 61;\n  --ion-color-danger-contrast: #ffffff;\n  --ion-color-danger-contrast-rgb: 255, 255, 255;\n  --ion-color-danger-shade: #d33939;\n  --ion-color-danger-tint: #f25454;\n\n  /** dark **/\n  --ion-color-dark: #222428;\n  --ion-color-dark-rgb: 34, 34, 34;\n  --ion-color-dark-contrast: #ffffff;\n  --ion-color-dark-contrast-rgb: 255, 255, 255;\n  --ion-color-dark-shade: #1e2023;\n  --ion-color-dark-tint: #383a3e;\n\n  /** medium **/\n  --ion-color-medium: #989aa2;\n  --ion-color-medium-rgb: 152, 154, 162;\n  --ion-color-medium-contrast: #ffffff;\n  --ion-color-medium-contrast-rgb: 255, 255, 255;\n  --ion-color-medium-shade: #86888f;\n  --ion-color-medium-tint: #a2a4ab;\n\n  /** light **/\n  --ion-color-light: #f4f5f8;\n  --ion-color-light-rgb: 244, 244, 244;\n  --ion-color-light-contrast: #000000;\n  --ion-color-light-contrast-rgb: 0, 0, 0;\n  --ion-color-light-shade: #d7d8da;\n  --ion-color-light-tint: #f5f6f9;\n}\n"
  },
  {
    "path": "src-app/src/tsconfig.app.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/app\",\n    \"types\": []\n  },\n  \"exclude\": [\n    \"test.ts\",\n    \"**/*.spec.ts\"\n  ]\n}\n"
  },
  {
    "path": "src-app/src/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/spec\",\n    \"types\": [\n      \"jasmine\",\n      \"node\"\n    ]\n  },\n  \"files\": [\n    \"test.ts\",\n    \"polyfills.ts\"\n  ],\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "src-app/tsconfig.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"outDir\": \"./dist/out-tsc\",\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"module\": \"es2015\",\n    \"moduleResolution\": \"node\",\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"importHelpers\": true,\n    \"target\": \"es5\",\n    \"typeRoots\": [\n      \"node_modules/@types\"\n    ],\n    \"lib\": [\n      \"es2018\",\n      \"dom\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src-app/tslint.json",
    "content": "{\n  \"rulesDirectory\": [\n    \"codelyzer\"\n  ],\n  \"rules\": {\n    \"arrow-return-shorthand\": true,\n    \"callable-types\": true,\n    \"class-name\": true,\n    \"comment-format\": [\n      true,\n      \"check-space\"\n    ],\n    \"curly\": true,\n    \"deprecation\": {\n      \"severity\": \"warn\"\n    },\n    \"eofline\": true,\n    \"forin\": true,\n    \"import-blacklist\": [\n      true,\n      \"rxjs/Rx\"\n    ],\n    \"import-spacing\": true,\n    \"indent\": [\n      true,\n      \"spaces\"\n    ],\n    \"interface-over-type-literal\": true,\n    \"label-position\": true,\n    \"max-line-length\": [\n      true,\n      140\n    ],\n    \"member-access\": false,\n    \"member-ordering\": [\n      true,\n      {\n        \"order\": [\n          \"static-field\",\n          \"instance-field\",\n          \"static-method\",\n          \"instance-method\"\n        ]\n      }\n    ],\n    \"no-arg\": true,\n    \"no-bitwise\": true,\n    \"no-console\": [\n      true,\n      \"debug\",\n      \"info\",\n      \"time\",\n      \"timeEnd\",\n      \"trace\"\n    ],\n    \"no-construct\": true,\n    \"no-debugger\": true,\n    \"no-duplicate-super\": true,\n    \"no-empty\": false,\n    \"no-empty-interface\": true,\n    \"no-eval\": true,\n    \"no-inferrable-types\": [\n      true,\n      \"ignore-params\"\n    ],\n    \"no-misused-new\": true,\n    \"no-non-null-assertion\": true,\n    \"no-redundant-jsdoc\": true,\n    \"no-shadowed-variable\": true,\n    \"no-string-literal\": false,\n    \"no-string-throw\": true,\n    \"no-switch-case-fall-through\": true,\n    \"no-trailing-whitespace\": true,\n    \"no-unnecessary-initializer\": true,\n    \"no-unused-expression\": true,\n    \"no-use-before-declare\": true,\n    \"no-var-keyword\": true,\n    \"object-literal-sort-keys\": false,\n    \"one-line\": [\n      true,\n      \"check-open-brace\",\n      \"check-catch\",\n      \"check-else\",\n      \"check-whitespace\"\n    ],\n    \"prefer-const\": true,\n    \"quotemark\": [\n      true,\n      \"single\"\n    ],\n    \"radix\": true,\n    \"semicolon\": [\n      true,\n      \"always\"\n    ],\n    \"triple-equals\": [\n      true,\n      \"allow-null-check\"\n    ],\n    \"typedef-whitespace\": [\n      true,\n      {\n        \"call-signature\": \"nospace\",\n        \"index-signature\": \"nospace\",\n        \"parameter\": \"nospace\",\n        \"property-declaration\": \"nospace\",\n        \"variable-declaration\": \"nospace\"\n      }\n    ],\n    \"unified-signatures\": true,\n    \"variable-name\": false,\n    \"whitespace\": [\n      true,\n      \"check-branch\",\n      \"check-decl\",\n      \"check-operator\",\n      \"check-separator\",\n      \"check-type\"\n    ],\n    \"no-output-on-prefix\": true,\n    \"use-input-property-decorator\": true,\n    \"use-output-property-decorator\": true,\n    \"use-host-property-decorator\": true,\n    \"no-input-rename\": true,\n    \"no-output-rename\": true,\n    \"use-life-cycle-interface\": true,\n    \"use-pipe-transform-interface\": true,\n    \"directive-class-suffix\": true\n  }\n}\n"
  },
  {
    "path": "src-aws/core/aws-connections.js",
    "content": "const AWS = require(\"aws-sdk\");\nmodule.exports.dynamoDocClient = new AWS.DynamoDB.DocumentClient({ region: \"eu-west-1\" });\nmodule.exports.s3 = new AWS.S3();\n"
  },
  {
    "path": "src-aws/core/config.js",
    "content": "module.exports.config = {\n\tdeviceName: 'xd-home-energy-monitor-2',\n\t\n\tdynamoDb: {\n\t\ttable: process.env.DYNAMO_DB_TABLE,\n\t},\n\n\ts3: {\n\t\tbucket: process.env.S3_STORAGE_BUCKET\n\t}\n};"
  },
  {
    "path": "src-aws/core/helpers/CalculateKwh.js",
    "content": "/**\n * Calculates how many kWh has been used in the given dataset.\n * Returns an object with two fields: \"day\" and \"night\" to\n * know how much was used under which tarif.\n *\n * Used to archive these statistics on a daily basis to Dynamo\n * and to show a counter in the front-end.\n *\n * Input format:\n * \t[\n * \t\t[timestamp, wattage],\n * \t\t[timestamp, wattage],\n * \t\t...\n * \t]\n */\nmodule.exports.calculateKWH = function (dataset) {\n\tconst { isNightTarif } = require('./IsNightTarif');\n\n\tconst output = {\n\t\tday: 0,\n\t\tnight: 0,\n\t};\n\n\tfor(let i = 0; i < dataset.length-1; i++){\n\t\tconst current = dataset[i];\n\t\tconst next = dataset[i+1];\n\n\t\t// Seconds between the two measurements\n\t\tconst seconds =\n\t\t\t(next[0].getTime() - current[0].getTime()) / 1000;\n\n\t\t// Kilowatts used between those points\n\t\tconst kWh = (current[1] * seconds * (1/(60*60))) / 1000;\n\n\t\tif(isNightTarif(current[0])){\n\t\t\toutput.night += kWh;\n\t\t}else{\n\t\t\toutput.day += kWh;\n\t\t}\n\t}\n\n\treturn output;\n}"
  },
  {
    "path": "src-aws/core/helpers/CalculateKwh.test.js",
    "content": "const {calculateKWH} = require('./CalculateKwh');\nconst assert = require('assert');\n\ndescribe('Calculate kWh', function() {\n    it('should return 1 when consuming 1000W for 1 hour', function() {\n\n    \t// Consume 1000W for exactly 1 hour\n    \tconst data = [\n    \t\t[new Date(1*1000), 1000],\n    \t\t[new Date(60*60*1000 +1000), 0],\n    \t];\n\n    \t// Should be 1kWh (at night)\n      \tassert.equal(calculateKWH(data).night, 1);\n\t});\n\n\tit('should return 0.5 when consuming 1000W for 30min', function() {\n\n    \t// Consume 1000W for exactly 30min\n    \tconst data = [\n    \t\t[new Date(1*1000), 1000],\n    \t\t[new Date(30*60*1000 +1000), 0],\n    \t];\n\n    \t// Should be 1kWh (at night)\n      \tassert.equal(calculateKWH(data).night, 0.5);\n\t});\n});"
  },
  {
    "path": "src-aws/core/helpers/IsNightTarif.js",
    "content": "/**\n * Checks if a given date object is within night tarif or not.\n * For us that is between 21:00 and 06:00 and every weekend day.\n */\nmodule.exports.isNightTarif = function(dateObj) {\n\tif (typeof dateObj === 'number') {\n\t\tdateObj = new Date(dateObj * 1000);\n\t}\n\n\tif((dateObj.getHours() >= 21 && dateObj.getHours() <= 23) ||\n\t\t(dateObj.getHours() >= 0 && dateObj.getHours() <= 5)){\n\t\treturn true;\n\t}\n\n\tif(dateObj.getDay() === 0 || dateObj.getDay() === 6){\n\t\treturn true;\n\t}\n\n\treturn false;\n}"
  },
  {
    "path": "src-aws/core/helpers/IsNightTarif.test.js",
    "content": "const { isNightTarif } = require('./IsNightTarif');\nconst assert = require('assert');\n\n\ndescribe('IsNightTarif', function() {\n    it('should return true for night hours', function () {\n        // 01/01/2019 @ 03:00 (UTC)\n      \tassert.equal(isNightTarif(new Date(1546311600000)), true);\n    });\n\n    it('should return false for day hours', function () {\n        // 01/01/2019 @ 13:00 (UTC)\n        assert.equal(isNightTarif(new Date(1546347600000)), false);\n    });\n\n    it('should return true for weekends', function () {\n        // 01/05/2019  @ 13:00 (UTC) -> Saturday\n        assert.equal(isNightTarif(new Date(1546693200000)), true);\n    });\n\n    it('should also work when we pass integers instead of date objects', function () {\n         // 01/05/2019  @ 13:00 (UTC) -> Saturday\n         assert.equal(isNightTarif(1546693200), true);\n    });\n});"
  },
  {
    "path": "src-aws/core/helpers.js",
    "content": "module.exports.getYesterdayDate = function(){\n    const yesterday = new Date();\n    yesterday.setHours(0);\n    yesterday.setMinutes(0);\n    yesterday.setSeconds(0);\n    yesterday.setDate(yesterday.getDate() -1);\n\n    const string = yesterday\n    \t\t\t.toISOString()\n    \t\t\t.substring(0,10)\n    \t\t\t.replace(/-/g, '');\n\n    return {\n    \tdateObj: yesterday,\n    \tunixTimestamp: parseInt(yesterday.getTime() / 1000),\n    \tstring: string,\n    \tyear: string.substring(0,4),\n    \tmonth: string.substring(4,6),\n    \tday: string.substring(6,8)\n    }\n}\n\nmodule.exports.getTodaysDate = function(){\n\tconst today = new Date();\n    today.setHours(0);\n    today.setMinutes(0);\n    today.setSeconds(0);\n\n    const string = today\n    \t\t\t.toISOString()\n    \t\t\t.substring(0,10)\n    \t\t\t.replace(/-/g, '');\n\n    return {\n    \tdateObj: today,\n    \tunixTimestamp: parseInt(today.getTime() / 1000),\n    \tstring: string,\n    \tyear: string.substring(0,4),\n    \tmonth: string.substring(4,6),\n    \tday: string.substring(6,8)\n    }\n}\n\nmodule.exports.parseDynamoDBReadingsToJson = function(data){\n\tconst output = [];\n\n\tfor(const entry of data.Items){\n\t\tconst timestamp = entry.sortkey;\n\t\tconst readings = entry.readings;\n\n\n\t\t// Calculate the time of the first entry, assuming that a \n\t\t// measurement is taken every second. We do -2 because js\n\t\t// starts counting from 0 and because the last element should\n\t\t// not be included.\n\t\tlet timeForEntry = entry.sortkey - readings.length -2;\n\n\t\tfor(const reading of readings){\n\t\t\toutput.push({\n\t\t\t\ttimestamp: timeForEntry,\n\t\t\t\treading: reading\n\t\t\t});\n\n\t\t\ttimeForEntry++;\n\t\t}\n\t}\n\n\treturn output;\n}\n\n/**\n * Convert the output from DynamoDB (which is a JSON object)\n * into a string containing a CSV document with timestamp and\n * measurement column.\n */\nmodule.exports.parseDynamoDBItemsToCSV = function(dynamoData){\n\tlet output = 'Timestamp,Watts\\n';\n\n\tconst json = module.exports.parseDynamoDBReadingsToJson(dynamoData);\n\n\tfor(const reading of json){\n\t\toutput += reading.timestamp + ',' + reading.reading + '\\n';\n\t}\n\n\treturn output;\n}\n\nmodule.exports.getReadingsFromDynamoDBSince = async function(deviceId, timestamp){\n\tconst { dynamoDocClient } = require('./aws-connections');\n\tconst { config } = require('./config');\n\n\tconst data = await dynamoDocClient.query({\n       TableName : config.dynamoDb.table,\n       KeyConditionExpression: '#key = :key and #sortkey > :timestamp',\n       ScanIndexForward: true, // DESC order\n       ConsistentRead: false,\n       ExpressionAttributeNames:{\n           '#key': 'primarykey',\n           '#sortkey': 'sortkey',\n       },\n       ExpressionAttributeValues: {\n           ':key': 'reading-' + deviceId,\n           ':timestamp': timestamp\n       },\n    }).promise();\n\n\treturn module.exports.parseDynamoDBReadingsToJson(data);\n}\n\nmodule.exports.getUsageDataFromDynamoDB = async function(deviceId, startDate, endDate){\n\tconst { dynamoDocClient } = require('./aws-connections');\n\tconst { config } = require('./config');\n\n\tconst data = await dynamoDocClient.query({\n       TableName : config.dynamoDb.table,\n       KeyConditionExpression: '#key = :key and #sortkey BETWEEN :start AND :end',\n       ScanIndexForward: true, // DESC order\n       ConsistentRead: false,\n       ExpressionAttributeNames:{\n           '#key': 'primarykey',\n           '#sortkey': 'sortkey',\n       },\n       ExpressionAttributeValues: {\n           ':key': 'summary-day-' + deviceId,\n           ':start': startDate,\n           ':end': endDate\n       },\n    }).promise();\n\n\tconsole.log(data);\n    return data.Items;\n}\n\nmodule.exports.writeToS3 = async function(filename, contents){\n\tconst { s3 } = require('./aws-connections');\n\tconst { config } = require('./config');\n\tconst util = require('util');\n\tconst zlib = require('zlib');\n\tconst gzip = util.promisify(zlib.gzip);\n\n\tconst compressedBody = await gzip(contents);\n\n\treturn s3.putObject({\n        Body: compressedBody,\n        Bucket: config.s3.bucket,\n        Key: filename + '.gz'\n    }).promise();\n}\n\nmodule.exports.readFromS3 = function(filename){\n\tconst { s3 } = require('./aws-connections');\n\tconst { config } = require('./config');\n\n\treturn s3.getObject({\n        Bucket: config.s3.bucket,\n        Key: filename,\n    }).promise();\n}\n\nmodule.exports.getDatesBetween = function(startDate, endDate){\n\tconst dateArray = [];\n\n    let currentDate = startDate;\n    while (currentDate <= endDate) {\n        dateArray.push(new Date (currentDate));\n        currentDate = currentDate.addDays(1);\n    }\n\n    return dateArray;\n}\n\n/**\n * Write a given object to the given table name. Returns a\n * promise that should be awaited.\n */\nmodule.exports.writeToDynamoDB = function(tableName, object){\n\tconst { dynamoDocClient } = require('./aws-connections');\n\n\treturn dynamoDocClient.put({\n        TableName: tableName,\n        Item: object\n    }).promise();\n}\n"
  },
  {
    "path": "src-aws/dashboard/img/favicons/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"img/favicons/mstile-150x150.png\"/>\n            <TileColor>#da532c</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "src-aws/dashboard/img/favicons/site.webmanifest",
    "content": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "src-aws/dashboard/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Home Energy</title>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n\n\n  <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"img/favicons/apple-touch-icon.png\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"img/favicons/favicon-32x32.png\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"img/favicons/favicon-16x16.png\">\n  <link rel=\"manifest\" href=\"img/favicons/site.webmanifest\">\n  <link rel=\"mask-icon\" href=\"img/favicons/safari-pinned-tab.svg\" color=\"#5bbad5\">\n  <link rel=\"shortcut icon\" href=\"img/favicons/favicon.ico\">\n  <meta name=\"theme-color\" content=\"#ffffff\">\n\n\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js\" integrity=\"sha256-XT58qJPKCsRBRq+MIcNDQ7dVh0GAa1k2r24w62z0Olk=\" crossorigin=\"anonymous\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js\" integrity=\"sha256-MZo5XY1Ah7Z2Aui4/alkfeiq3CopMdV/bbkc/Sh41+s=\" crossorigin=\"anonymous\"></script>\n\n  <script src=\"https://cdn.jsdelivr.net/npm/jstat@1.7.1/dist/jstat.min.js\" integrity=\"sha256-Rtwg0oi/KB80JyxnJGWz/zWwjIBgDchFFBnenkosAfA=\" crossorigin=\"anonymous\"></script>\n\n  <link rel=\"stylesheet\" href=\"main.css\" />\n  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.css\" integrity=\"sha256-NmfeKHX4FgSrBzL2BhPhzy41cHgzNYIEZyLyqf2/B30=\" crossorigin=\"anonymous\" />\n  <link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css\" integrity=\"sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS\" crossorigin=\"anonymous\">\n</head>\n<body>\n\n  <nav class=\"navbar navbar-dark bg-dark\">\n    <a class=\"navbar-brand\" href=\"#\">\n     ⚡️ Home Energy Monitor\n    </a>\n\n    <div class=\"spinner-grow text-light\" id=\"loading-indicator\" role=\"status\">\n      <span class=\"sr-only\">Loading...</span>\n    </div>\n\n    <form class=\"form-inline\">\n      <button class=\"btn btn-outline-secondary\" type=\"button\" id=\"btnYesterday\">Yesterday</button>\n      &nbsp;\n      <button class=\"btn btn-outline-success\" type=\"button\" id=\"btnToday\">Today</button>\n    </form>\n  </nav>\n\n  <br>\n\n  <div class=\"container-fluid\">\n    <div class=\"card\">\n      <div class=\"card-header\">\n      <span>Readings <small id=\"usage-kwh\">? kWh</small></span>\n    <span style=\"float:right;\">Last Reading <small id=\"last-reading\">?</small></span>\n      </div>\n      <div class=\"card-body\">\n        <div id=\"graphdiv\" style=\"width: 100%; height: 300px;\"></div>\n      </div>\n    </div>\n  </div>\n\n  <br>\n\n  <div class=\"container-fluid\">\n\n    <div class=\"row\">\n      <div class=\"col-md-6\">\n        <div class=\"card\">\n          <div class=\"card-header\">\n            Last 24 hours\n          </div>\n          <div class=\"card-body\" style=\"text-align:center;\">\n            <div class=\"circle big\">\n              <p>Current<br>\n              <span id=\"stats-current\">? W</span></p>\n            </div>\n            <div class=\"circle\">\n              <p>Standby<br>\n              <span id=\"stats-standby\">? W</span></p>\n            </div>\n            <div class=\"circle\">\n              <p>Peak<br>\n              <span id=\"stats-max\">? W</span></p>\n            </div>\n            <div class=\"circle\">\n              <p>Today<br>\n              <span id=\"stats-kwh\">? kWh</span></p>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"card\">\n          <div class=\"card-header\">\n            Standby-loss today\n          </div>\n          <div class=\"card-body\" style=\"text-align:center;\">\n            <canvas id=\"chart-standby\" height=\"100px\"></canvas>\n          </div>\n        </div>\n      </div>\n      <div class=\"col-md-6\">\n        <div class=\"card\">\n          <div class=\"card-header\">\n            Last 30 days\n          </div>\n          <div class=\"card-body\">\n            <canvas id=\"canvas\" height=\"300px\"></canvas>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n\n<script type=\"text/javascript\" src=\"main.js\"></script>\n<script type=\"text/javascript\">\n  (async function(){\n\n    toggleLoadingIndicator(true);\n    await Promise.all([\n      initChart(),\n      initUsageChart(),\n    ]);\n    toggleLoadingIndicator(false);\n    \n  })();\n</script>\n</body>\n</html>"
  },
  {
    "path": "src-aws/dashboard/main.css",
    "content": ".dygraph-legend{\n/*\tleft: 60px !important;\n\tright: 0px;\n\ttop: -17px !important;\n\twidth: 100%;*/\n}\n\n.circle{\n\tdisplay:block;\n\tbackground-color: #1D294E;\n\tcolor: #fff;\n\twidth: 120px;\n\theight: 120px;\n\n\tjustify-content: center;\n\talign-items: center;\n\tborder-radius: 100%;\n\ttext-align: center;\n\tdisplay: inline-flex;\n}\n\n.circle.big{\n\t/*width: 200px;*/\n\t/*height:200px;*/\n}"
  },
  {
    "path": "src-aws/dashboard/main.js",
    "content": "const BASE_URL = '*** YOUR GRAPHQL ENDPOINT HERE ***';\nlet data = [];\nlet chart;\nlet animateDuration = 1500;\n\nfunction toggleLoadingIndicator(visible){\n\tconst $el = document.getElementById('loading-indicator');\n\n\tif(visible){\n\t\t$el.style.display = 'block';\n\t}else{\n\t\t$el.style.display = 'none';\n\t}\n}\n\nfunction formatTimestampForChartAxis(rawTimestamp){\n\tconst date = new Date(rawTimestamp * 1000);\n\tconst months = [\"Jan\", 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\treturn date.getDate() + ' ' + months[date.getMonth()];\n}\n\nfunction fetchChartDataForDailyUsage(){\n\treturn new Promise((resolve, reject) => {\n\t\tconst xhr = new XMLHttpRequest();\n\n\t\txhr.onload = function () {\n\n\t\t\tif (xhr.status >= 200 && xhr.status < 300) {\n\t\t\t\tconst json = JSON.parse(xhr.response);\n\n\t\t\t\t// Process that data for chartjs\n\n\t\t\t\tvar chartData = {\n\t\t\t\t\tlabels: json.data.usageData.map(el => formatTimestampForChartAxis(el.timestamp)),\n\t\t\t\t\tdatasets: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlabel: 'Day',\n\t\t\t\t\t\t\tbackgroundColor: 'rgb(54, 162, 235)',\n\t\t\t\t\t\t\tdata: json.data.usageData.map(el => el.dayUse)\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlabel: 'Night',\n\t\t\t\t\t\t\tbackgroundColor: 'rgb(29, 41, 81)',\n\t\t\t\t\t\t\tdata: json.data.usageData.map(el => el.nightUse)\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t}\n\n\t\t\t\treturn resolve(chartData);\n\t\t\t} else {\n\t\t\t\tconsole.log('The request failed!');\n\t\t\t\treturn reject();\n\t\t\t}\n\t\t};\n\n\t\tconst startDate = new Date();\n\t\tstartDate.setDate(startDate.getDate() - 31);\n\n\t\tconst start = parseInt(startDate.getTime() / 1000);\n\t\tconst end = parseInt(Date.now() / 1000);\n\n\t\tconst query = `query{usageData(startDate:${start}, endDate:${end}){timestamp, dayUse, nightUse}}`;\n\n\t\txhr.open('POST', BASE_URL);\n\t\txhr.send(query);\n\t});\n}\n\nfunction fetchData(since){\n\tif(!since){\n\t\tconst yesterday = new Date();\n\t\tyesterday.setDate(yesterday.getDate() -1);\n\t\tyesterday.setHours(yesterday.getHours() + 12);\n\t\tsince = yesterday.getTime() / 1000;\n\t}\n\t\n\tsince = parseInt(since);\n\n\n\treturn new Promise((resolve, reject) => {\n\t\tconst xhr = new XMLHttpRequest();\n\n\t\txhr.onload = function () {\n\n\t\t\tif (xhr.status >= 200 && xhr.status < 300) {\n\t\t\t\tconsole.time(\"Parse JSON\");\n\t\t\t\tconst json = JSON.parse(xhr.response);\n\t\t\t\tconsole.timeEnd(\"Parse JSON\");\n\n\n\t\t\t\tconsole.time(\"Process data\");\n\t\t\t\tprocessData(json);\n\t\t\t\tconsole.timeEnd(\"Process data\");\n\n\t\t\t\treturn resolve();\n\t\t\t} else {\n\t\t\t\tconsole.log('The request failed!');\n\t\t\t\treturn reject();\n\t\t\t}\n\t\t};\n\n\t\tconst query = `query{ realtime(sinceTimestamp: ${since}){timestamp, reading} }`;\n\n\t\txhr.open('POST', BASE_URL);\n\t\txhr.send(query);\n\t});\n}\n\nfunction processData(rawData){\n\tif(!rawData || !rawData.data || !rawData.data.realtime){\n\t\treturn;\n\t}\n\n\tfor(const entry of rawData.data.realtime){\n\t\tconst date = entry.timestamp * 1000;\n\n\t\t// If this entries timestamp is before the last entry\n\t\t// in our dataset, then we skip it because it's data \n\t\t// we already have! This reduces the time it takes to\n\t\t// process all the data by a LOT!\n\t\tif(data.length > 1 && date < data[data.length -1 ][0].getTime()){\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst watts = parseFloat(entry.reading);\n\n\t\tdata.push([\n\t\t\tnew Date(date),\n\t\t\twatts,\n\t\t]);\n\t}\n\n\tif(chart){\n\t\tchart.updateOptions({\n\t\t\tfile: data,\n\t\t});\n\t}\n\n\t// Update metrics\n\tconst $current = document.getElementById('stats-current');\n\tconst $todayKwh = document.getElementById('stats-kwh');\n\tconst $standbyPower = document.getElementById('stats-standby');\n\tconst $max = document.getElementById('stats-max');\n\tconst $lastreading = document.getElementById('last-reading');\n\n\tvar utcSeconds = rawData.data.realtime[rawData.data.realtime.length-1][\"timestamp\"]; // Get the latest UTC timestamp\n\tvar d = new Date(0); // The 0 there is the key, which sets the date to the epoch\n\td.setUTCSeconds(utcSeconds); // Convert epoch to local timezone\n\t$lastreading.innerHTML = d.toLocaleString(); //Update HTML\n\tconst totalKwh = calculateKWH(data);\n\n\t$current.innerHTML = data[data.length-1][1] + ' W';\n\t$todayKwh.innerHTML = (Math.round(totalKwh * 100) / 100) + ' kWh';\n\n\n\tconst readings = data.map(el => el[1]);\n\tconst standbyWatts = jStat.mode(readings);\n\t$standbyPower.innerHTML = parseInt(standbyWatts) + ' W';\n\t$max.innerHTML = jStat.max(readings) + ' W';\n\n\t// Calculate total standby kWh\n\tconst hours = (data[data.length-1][0].getTime() - data[0][0].getTime()) / 1000 / 3600;\n\tconst standbyKwh = (standbyWatts/1000) * hours;\n\n\tinitStandbyChart({\n\t\tactivePower: totalKwh - (standbyKwh/1000 * hours), \n\t\tstandbyPower: standbyKwh\n\t});\n}\n\nfunction initStandbyChart({activePower, standbyPower}){\n\tconst barChartData = {\n\t\tlabels: ['Today'],\n\t\tdatasets: [{\n\t\t\tdata: [ activePower, standbyPower ],\n\t\t\tbackgroundColor: ['rgb(54, 162, 235)', 'rgb(29, 41, 81)']\n\t\t}],\n\t\tlabels: ['Active', 'Standby']\n\t};\n\t\n\tconst ctx = document.getElementById('chart-standby').getContext('2d');\n\tnew Chart(ctx, {\n\t\ttype: 'doughnut',\n\t\tdata: barChartData,\n\t\toptions: {\n\t\t\tanimation: {\n\t\t\t\tduration: animateDuration\n\t\t\t},\n\t\t\tresponsive: true,\n\t\t}\n\t});\n\n\tanimateDuration = 0;\n}\n\n/**\n * Calculates the consumed kWh based on the given\n * dataset. More accurate when interval of measurements\n * is higher.\n */\nfunction calculateKWH(dataset){\n\tlet total = 0;\n\n\tfor(let i = 0; i < dataset.length-1; i++){\n\t\tconst current = dataset[i];\n\t\tconst next = dataset[i+1];\n\n\t\tconst seconds = (next[0].getTime() - current[0].getTime()) / 1000;\n\n\t\ttotal += (current[1] * seconds * (1/(60*60))) / 1000;\n\t}\n\n\treturn total;\n}\n\n/**\n * Calculates the min, max and used kwh based of the highlighted\n * range in the chart. If nothing was highlighted, we make a\n * complete overview\n */\nfunction getMetricsForSelectedRange(chart, initial_draw){\n\tlet startDate = 0;\n\tlet endDate = Number.MAX_SAFE_INTEGER;\n\n\tif(chart.dateWindow_){\n\t\tstartDate = chart.dateWindow_[0];\n\t\tendDate = chart.dateWindow_[1];\n\t}\n\n\t// Extract the data between start & end date\n\tconst dataInScope = data.filter(\n\t\tel => el[0] > startDate && el[0] < endDate\n\t);\n\n\treturn {\n\t\tusage: calculateKWH(dataInScope),\n\t}\n}\n\n/**\n * Is called by Dygraphs when the user has selected a range in\n * the chart. We then have to update the metrics for the newly\n * selected range.\n */\nfunction updateMetricsForSelectedRange(chart, initial_draw){\n\tconst metrics = getMetricsForSelectedRange(chart, initial_draw);\n\tconst $kwh = document.getElementById('usage-kwh');\n\t$kwh.innerHTML = parseFloat(metrics.usage).toFixed(2) + ' kWh';\n}\n\n/**\n * Between 21:00 and 06:00 there is a special \"night hour\" tarif for\n * electricity. Also all hours on Saturday and Sunday. Reflect that \n * by highlighting these areas in grey.\n */\nfunction highlightNightHours(canvas, area, chart){\n\tlet foundStart = false;\n\tlet foundEnd = false;\n\n\tlet startHighlight = null;\n\tlet endHighlight = null;\n\n\tcanvas.fillStyle = \"#efefef\";\n\n\tfor(let i = 0; i < chart.file_.length; i++){\n\t\tconst entry = chart.file_[i];\n\t\tconst date = entry[0];\n\n\t\t// Assume this is also going to be our last item to highlight\n\t\tendHighlight = chart.toDomXCoord(date);\n\n\t\tif(foundStart === false && isNightTarif(date)){\n\t\t\t// We now found our start!\n\t\t\tfoundStart = true;\n\t\t\tstartHighlight = chart.toDomXCoord(date);\n\t\t}\n\n\t\t// If this entry is not night tarif, but we did find the start\n\t\t// before then this is the end!\n\t\tif(foundStart === true && isNightTarif(date) === false){\n\t\t\tfoundEnd = true;\n\t\t}\n\n\t\t// If we found both, draw them!\n\t\tif(foundStart === true && foundEnd === true){\n\t\t\tconst width = endHighlight - startHighlight;\n\n\t\t\tcanvas.fillRect(startHighlight, area.y, width, area.h);\n\n\t\t\tfoundStart = false;\n\t\t\tfoundEnd = false;\n\t\t\tstartHighlight = null;\n\t\t\tendHighlight = null;\n\t\t}\n\n\t\ti += 30;\n\t}\n\n\t// It could be that we found a start but not an end (in that case we're\n\t// actively in night hours and we should draw these as well!)\n\tif(foundStart && foundEnd === false){\n\t\tconst lastPosition = chart.toDomXCoord(chart.file_[chart.file_.length -1][0]);\n\t\tconst width = lastPosition - startHighlight;\n\t\tcanvas.fillRect(startHighlight, area.y, width, area.h);\n\t}\n}\n\n/**\n * Checks if a given date object is within night tarif or not.\n * For us that is between 21:00 and 06:00 and every weekend day.\n */\nfunction isNightTarif(dateObj){\n\tif((dateObj.getHours() >= 21 && dateObj.getHours() <= 23) ||\n\t\t(dateObj.getHours() >= 0 && dateObj.getHours() <= 5)){\n\t\treturn true;\n\t}\n\n\tif(dateObj.getDay() === 0 || dateObj.getDay() === 6){\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nasync function initUsageChart(){\n\tconst chartdata = await fetchChartDataForDailyUsage();\n\tvar ctx = document.getElementById('canvas').getContext('2d');\n\n\tnew Chart(ctx, {\n\t\ttype: 'bar',\n\t\tdata: chartdata,\n\t\toptions: {\n\t\t\tresponsive: true,\n\t\t\tmaintainAspectRatio: false,\n\t\t\tscales: {\n\t\t\t\txAxes: [{\n\t\t\t\t\tstacked: true,\n\t\t\t\t}],\n\t\t\t\tyAxes: [{\n\t\t\t\t\tstacked: true\n\t\t\t\t}]\n\t\t\t}\n\t\t}\n\t})\n}\n\nasync function initChart(){\n\n\t// First fetch some data from ThingSpeak\n\tawait fetchData();\n\n\t// Initialize the chart\n\tchart = new Dygraph(\n\t    document.getElementById(\"graphdiv\"),\n\t    data,\n\t    {\n            legend: 'always',\n\t    \tlabels: ['Timestamp', 'Watts'],\n\t    \tunderlayCallback: highlightNightHours,\n\t    \tdrawCallback: updateMetricsForSelectedRange,\n\t    \tshowRoller: true,\n\t    \trollPeriod: 14,\n\t    }\n  \t);\n\n  \t// Add callbacks to the buttons \"yesterday\" and \"today\"\n  \tdocument.getElementById('btnYesterday').addEventListener('click', () => {\n  \t\tconst start = new Date();\n  \t\tstart.setDate(start.getDate() - 1);\n  \t\tstart.setHours(0);\n  \t\tstart.setMinutes(0);\n\n  \t\tchart.updateOptions({\n  \t\t\tdateWindow: [start.getTime(), start.getTime() + 24*60*60*1000]\n  \t\t})\n  \t});\n\n  \tdocument.getElementById('btnToday').addEventListener('click', () => {\n  \t\tconst start = new Date();\n  \t\tstart.setHours(0);\n  \t\tstart.setMinutes(0);\n\n  \t\tchart.updateOptions({\n  \t\t\tdateWindow: [start.getTime(), start.getTime() + 24*60*60*1000]\n  \t\t})\n  \t});\n\n  // \tdocument.getElementById('btnGetSignature').addEventListener('click', () => {\n  // \t\tif(chart.dateWindow_){\n\t\t// \tconst startDate = chart.dateWindow_[0];\n\t\t// \tconst endDate = chart.dateWindow_[1];\n\n\t\t// \tconst filteredData = data.filter(el => el[0] < endDate && el[0] > startDate);\n\t\t// \tconsole.log(filteredData.map(el => '['+ el[1] + '/10000]\\n').toString());\n\t\t// }\n  // \t});\n\n  \t// Every 30 seconds: fetch new data from the GraphQL endpoint.\n  \t// Fetch new records since the last record's timestamp.\n  \tsetInterval(async () => {\n  \t\tawait fetchData(data[data.length-1][0].getTime() / 1000);\n  \t}, 30 * 1000);\n}"
  },
  {
    "path": "src-aws/functions/cron-rotate-daily.js",
    "content": "'use strict';\nconst { dynamoDocClient } = require('../core/aws-connections');\nconst { config } = require('../core/config');\nconst { getYesterdayDate, getTodaysDate, writeToS3, writeToDynamoDB, parseDynamoDBItemsToCSV} = require('../core/helpers');\nconst { calculateKWH } = require('../core/helpers/CalculateKwh');\n\nconst deviceName = config.deviceName;\n\n/**\n * Fetches all of yesterday's readings of a certain\n * device from DynamoDB.\n */\nasync function fetchYesterdaysData(){\n\tconst timerLabel = '[PERF] Get history data';\n    console.time(timerLabel);\n\n\n    try{\n        const startRange = getYesterdayDate().unixTimestamp;\n        const endRange = getTodaysDate().unixTimestamp;\n\n        const prefix = 'reading-' + deviceName;\n\n        const data = await dynamoDocClient.query({\n            TableName : config.dynamoDb.table,\n            KeyConditionExpression: '#key = :key and #sortKey BETWEEN :start AND :end',\n            ScanIndexForward: true, // DESC order\n            ConsistentRead: false,\n            ExpressionAttributeNames:{\n                '#key': 'primarykey',\n                '#sortKey': 'sortkey',\n            },\n            ExpressionAttributeValues: {\n                ':key': prefix,\n                ':start': startRange,\n                ':end': endRange,\n            },\n        }).promise();\n\n        console.timeEnd(timerLabel);\n        console.log('Item count for yesterday', data.Items.length);\n        return data;\n    }catch(e){\n        console.log('Error fetching historical data');\n        console.log(e);\n\n        // To prevent the application from crashing completely, we\n        // return an valid DynamoDB result object with no entries.\n        return { Items: [] };\n    }\n}\n\n\nfunction calculateKwhSummary(csvData){\n    // Transform the data\n    const measurements = [];\n\n    for(const line of csvData.split('\\n')){\n        if(line === '') continue;\n\n        const parts = line.split(',');\n\n        if(parts[0] === 'Timestamp') continue;\n\n        measurements.push(\n            [new Date(parseInt(parts[0]) * 1000), parseInt(parts[1])]\n        );\n    }\n\n    // Calculate the usage\n    return calculateKWH(measurements);\n}\n\nasync function writeUsageToDynamoDB(usageObj){\n    const timerLabel = '[PERF] Write daily summary to DynamoDB';\n    console.time(timerLabel);\n\n    try{\n        const key = 'summary-day-' + deviceName;\n        const sortkey = getYesterdayDate().unixTimestamp;\n\n        const data = await writeToDynamoDB(config.dynamoDb.table, {\n            primarykey: key,\n            sortkey: sortkey,\n            usage: usageObj\n        });\n\n        console.timeEnd(timerLabel);\n        return data;\n    }catch(e){\n        console.log('Error writing daily usage to DynamoDB:');\n        console.log(e);\n\n        // To prevent the application from crashing completely, we\n        // return an valid DynamoDB result object with no entries.\n        return false\n    }\n}\n\nmodule.exports.handler = async(event, context, callback) => {\n\tconst data = await fetchYesterdaysData();\n\n\t// Convert to CSV\n\tconst csv = parseDynamoDBItemsToCSV(data);\n\n\tconst time = getYesterdayDate();\n\n\t// Write to S3\n\tawait writeToS3(`archived-readings/${deviceName}/${time.year}/${time.month}/${time.string}.csv`, csv);\n\n    // Calculate the kWh consumed & write it to DynamoDB\n    const usageData = calculateKwhSummary(csv);\n    console.log('usage data', usageData);\n    await writeUsageToDynamoDB(usageData);\n};"
  },
  {
    "path": "src-aws/functions/graphql/graphql.js",
    "content": "const { graphql, buildSchema } = require('graphql/index');\nconst { realtime } = require('./resolvers/realtime');\nconst { usageData } = require('./resolvers/usageData');\nconst { stats } = require('./resolvers/stats');\n\n// Construct a schema, using GraphQL schema language\nconst schema = buildSchema(`\n  type Query {\n    usageData(startDate: Int!, endDate: Int!): [DailySummary]!\n\n    stats: Stats!\n\n    realtime(sinceTimestamp: Int!): [Reading]!\n\n    readings(startDate: Int!, endDate: Int!): [Reading]!\n  }\n\n  type Stats{\n    always_on: Float\n    today_so_far: Float\n  }\n\n  type Reading {\n    timestamp: Int!\n    reading: Int!\n  }\n\n  type DailySummary{\n    timestamp: Int!\n    dayUse: Float!\n    nightUse: Float!\n  }\n`);\n\n// The root provides a resolver function for each API endpoint\nconst resolvers = {\n  usageData: usageData,\n  realtime: realtime,\n  stats: stats,\n};\n\nmodule.exports.handler = async function(event, context, callback){\n  const query = event.body;\n\n  const response = await graphql(\n    schema,\n    query,\n    resolvers\n  );\n\n  return {\n    statusCode: 200,\n    headers: {\n        'Content-Type': 'application/json',\n        'Access-Control-Allow-Origin': '*',\n        'Access-Control-Allow-Credentials': true,\n    },\n    body: JSON.stringify(response),\n  }\n};"
  },
  {
    "path": "src-aws/functions/graphql/resolvers/realtime.js",
    "content": "const { getReadingsFromDynamoDBSince } = require('../../../core/helpers');\nconst { config } = require('../../../core/config');\n\n/**\n * Fetches the collected readings from DynamoDB.\n * \n * To prevent the user from consuming too many read units, we limit\n * the amount of data you can request here to the last 24 hours.\n * \n * @param  {int} sinceTimestamp \tTimestamp in ms\n */\nmodule.exports.realtime = async ({ sinceTimestamp }) => {\n    const lowestTimestampAllowed = (new Date() / 1000) - 24 * 60 * 60;\n\n    if (sinceTimestamp && sinceTimestamp < lowestTimestampAllowed) {\n        throw new Error('This endpoint can only return data from the last 24 hours');\n    }\n\n    // If no timestamp was given, return the data from the last minute\n    if (!sinceTimestamp) {\n        console.log('No timestamp provided, going default');\n        sinceTimestamp = (new Date() / 1000) - 60;\n    }\n\n    return await getReadingsFromDynamoDBSince(config.deviceName, sinceTimestamp);\n}"
  },
  {
    "path": "src-aws/functions/graphql/resolvers/stats.js",
    "content": "const graphqlFields = require('graphql-fields');\nconst { getReadingsFromDynamoDBSince, getTodaysDate } = require('../../../core/helpers');\nconst { calculateKWH } = require('../../../core/helpers/CalculateKwh');\n\nconst jStat = require('jStat').jStat;\n\nconst { config } = require('../../../core/config');\n\nmodule.exports.stats = async ({ sinceTimestamp }, context, info) => {\n    const lowestTimestampAllowed = (new Date() / 1000) - 20 * 60 * 60;\n    const todayStartTimestamp = getTodaysDate().unixTimestamp;\n\n    const requestedFields = graphqlFields(info);\n    const output = {};\n\n    const allReadings = await getReadingsFromDynamoDBSince(config.deviceName, todayStartTimestamp);\n\n    if (requestedFields.always_on) {\n        const readingsOnly = allReadings.map(el => el.reading);\n        const standbyWatts = jStat.mode(readingsOnly);\n\n        output.always_on = standbyWatts;\n    }\n\n    // TODO: If only the today_so_far field is requested, we can get away by only loading\n    // the records from today, potentially saving us a lot of DynamoDB read capacity.\n    if (requestedFields.today_so_far) {\n        // Tranform the readings into something the calculateKWH function expects\n        const input = allReadings.map(item => [new Date(item.timestamp * 1000), item.reading]);\n\n        const usage = calculateKWH(input);\n        output.today_so_far = usage.day + usage.night;\n    }\n\n    return output;\n}"
  },
  {
    "path": "src-aws/functions/graphql/resolvers/usageData.js",
    "content": "const { getUsageDataFromDynamoDB } = require('../../../core/helpers');\nconst { config } = require('../../../core/config');\n\nmodule.exports.usageData = async ({ startDate, endDate }) => {\n\n    // Fetch the data from DynamoDB\n    const data = await getUsageDataFromDynamoDB(\n      config.deviceName, startDate, endDate\n    );\n\n    // Tanform the usage data to a format that GraphQL expects\n    return data.map(el => {\n      return {\n        timestamp: el.sortkey,\n        dayUse: el.usage.day,\n        nightUse: el.usage.night,\n      }\n    });\n}"
  },
  {
    "path": "src-aws/package.json",
    "content": "{\n  \"name\": \"src-aws\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"directories\": {\n    \"test\": \"tests\"\n  },\n  \"scripts\": {\n    \"test\": \"mocha \\\"tests\\\" \\\"./core/helpers/*.test.js\\\"\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"aws-sdk\": \"^2.612.0\",\n    \"serverless-finch\": \"^2.5.2\",\n    \"serverless-scriptable-plugin\": \"^1.0.5\",\n    \"serverless-webpack\": \"^5.3.1\",\n    \"webpack\": \"^4.41.5\",\n    \"webpack-node-externals\": \"^1.7.2\"\n  },\n  \"dependencies\": {\n    \"graphql\": \"^14.6.0\",\n    \"graphql-fields\": \"^2.0.3\",\n    \"jStat\": \"^1.8.6\"\n  }\n}\n"
  },
  {
    "path": "src-aws/serverless.yml",
    "content": "service: xd-home-energy-monitor\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: prod\n  region: eu-west-1\n  profile: serverless-personal\n  memorySize: 256\n  deploymentBucket:\n    name: \"xd-serverless-deployments\"\n\n  environment:\n    TZ: Europe/Brussels\n    DYNAMO_DB_TABLE: !Ref dynamoDataStore\n    S3_STORAGE_BUCKET: !Ref datastoreReadings\n\n  apiGateway:\n    minimumCompressionSize: 1024\n\n  stackTags:\n    client: \"xd-home-energy-monitor\"\n\n  iamRoleStatements:\n    - Effect: \"Allow\"\n      Action:\n        - dynamodb:Query\n        - dynamodb:GetItem\n        - dynamodb:PutItem\n        - dynamodb:DeleteItem\n      Resource: !GetAtt [dynamoDataStore, Arn]\n\n    - Effect: \"Allow\"\n      Action:\n        - s3:GetObject\n        - s3:PutObject\n      Resource: \n        - !GetAtt [datastoreReadings, Arn]\n        - !Join ['', [!GetAtt [datastoreReadings, Arn], '/*']]\n\nplugins:\n  - serverless-webpack\n  - serverless-finch\n  - serverless-scriptable-plugin\n\npackage:\n  individually: true\n  exclude:\n    - functions/graphql/node_modules/**\n    - dashboard/**\n    - tests/**\n\nfunctions:\n  dailyDataArchive:\n    handler: functions/cron-rotate-daily.handler\n    description: Archive and aggregate yesterday's data to S3 and DynamoDB\n    timeout: 30\n    events:\n      - schedule:\n          description: \"Archive the data generated yesterday to S3\"\n          rate: cron(0 2 * * ? *)\n\n  graphql:\n    description: GraphQL endpoint to query readings from devices\n    handler: functions/graphql/graphql.handler\n    memorySize: 512\n    package:\n      include:\n        - functions/graphql/node_modules/**\n    events:\n      - http:\n          path: graphql\n          method: post\n          cors: true\n\nresources:\n  Description: Monitoring home energy usage over time\n  Resources:\n\n    ###\n    # S3 Bucket to store daily/monthly files containing all raw measurements.\n    # Used to batch data up, reduce load on DynamoDB, reduce costs and allow\n    # for fasting charting of our data with Dygraphs.\n    ###\n    datastoreReadings:\n      Type: AWS::S3::Bucket\n      Properties:\n        BucketName: ${self:service}-datastore\n\n    ###\n    # S3 Bucket to store our front-end dashboard HTML code!\n    ###\n    wwwBucket:\n      Type: AWS::S3::Bucket\n      Properties:\n        BucketName: ${self:service}-www\n\n    ###\n    # DynamoDB table that stores recent raw messages from the devices\n    # as well as computed usage information per sensor, per day, per month.\n    ###\n    dynamoDataStore:\n      Type: AWS::DynamoDB::Table\n      Properties:\n        TableName: ${self:service}\n        AttributeDefinitions:\n          - AttributeName: \"primarykey\"\n            AttributeType: S\n          - AttributeName: \"sortkey\"\n            AttributeType: N\n        KeySchema:\n          - AttributeName: \"primarykey\"\n            KeyType: HASH\n          - AttributeName: \"sortkey\"\n            KeyType: RANGE\n        ProvisionedThroughput:\n          ReadCapacityUnits: 1\n          WriteCapacityUnits: 1\n        TimeToLiveSpecification:\n          AttributeName: ttl\n          Enabled: true\n\n    ###\n    # This IoT rule takes incoming messages and stores them straight\n    # into DynamoDB with the current timestamp (when we received the \n    # message) as well as a calculated TTL for the item (30 days)\n    # \n    # WARNING: if we update this, the name of the rule will change\n    # and then the iotPolicyForDevices should also be changed to\n    # reflect it!\n    ###\n    iotRule:\n      Type: AWS::IoT::TopicRule\n      Properties:\n        TopicRulePayload:\n          Actions:\n            - \n              DynamoDBv2:\n                PutItem:\n                  TableName: ${self:service}\n                RoleArn: !GetAtt [iotRuleAllowDynamoWrites, Arn]\n          AwsIotSqlVersion: \"2016-03-23\"\n          Description: \"Forwards incoming sensor messages to DynamoDB for analysis\"\n          RuleDisabled: false\n          Sql: >-\n            SELECT * ,\n                  'reading-' + clientid() as primarykey, \n                  (timestamp() / 1000) as sortkey, \n                  ((timestamp() / 1000) + 2592000) as ttl FROM '*** YOUR AWS IOT CORE THING ARN ***' \n\n    ###\n    # Policy that defines what each sensor is allowed to do. On basic\n    # level it should be allowed to publish directly to the rule\n    # topic. We also only allow a device to connect if the used\n    # certificate is attached to the thing that is trying to connect.\n    ###\n    iotPolicyForDevices:\n      Type: AWS::IoT::Policy\n      Properties:\n        PolicyDocument:\n          Version: \"2012-10-17\"\n          Statement:\n            -\n              Effect: \"Allow\"\n              Action:\n                - \"iot:Connect\"\n              Resource: \"*\"\n              Condition:\n                Bool:\n                  \"iot:Connection.Thing.IsAttached\": [true]\n            -\n              Effect: \"Allow\"\n              Action:\n                - \"iot:Publish\"\n              Resource: \n                - Fn::Join:\n                   - \"\"\n                   - - \"arn:aws:iot:\"\n                     - Ref: AWS::Region\n                     - \":\"\n                     - Ref: AWS::AccountId\n                     - \":topic/$aws/rules/\"\n                     - Ref: iotRule\n\n    ###\n    # Role that allows the IoT Topic Rule to write items to our\n    # DynamoDB table (and only that table)\n    ###\n    iotRuleAllowDynamoWrites:\n      Type: AWS::IAM::Role\n      Properties:\n        AssumeRolePolicyDocument: \n          Version: \"2012-10-17\"\n          Statement: \n            - \n              Effect: \"Allow\"\n              Principal: \n                Service: \n                  - \"iot.amazonaws.com\"\n              Action: \n                - \"sts:AssumeRole\"\n        Path: \"/\"\n        Policies:\n          -\n            PolicyName: ${self:service}-firehose-role\n            PolicyDocument:\n              Version: \"2012-10-17\"\n              Statement: \n                - Effect: \"Allow\"\n                  Action:\n                    - \"dynamodb:PutItem\"\n                  Resource: !GetAtt [dynamoDataStore, Arn]\n\ncustom:\n  scriptHooks:\n     before:deploy:createDeploymentArtifacts: npm run test\n\n  client:\n    bucketName: ${self:service}-www\n    distributionFolder: dashboard/\n    indexDocument: index.html\n\n  webpack:\n    webpackConfig: 'webpack.config.js'   # Name of webpack configuration file\n    includeModules: true   # Node modules configuration for packaging\n    packager: 'npm'   # Packager that will be used to package your external modules"
  },
  {
    "path": "src-aws/webpack.config.js",
    "content": "const slsw = require('serverless-webpack');\nconst nodeExternals = require('webpack-node-externals');\n\nmodule.exports = {\n  mode: 'production',\n  entry: slsw.lib.entries,\n  target: 'node',\n  externals: [nodeExternals({\n  \twhitelist: ['graphql', 'graphql-fields']\n  })] // exclude external modules\n};"
  },
  {
    "path": "src-esp32/.gitignore",
    "content": ".pio\n.pioenvs\n.piolibdeps\n.vscode/.browse.c_cpp.db*\n.vscode/c_cpp_properties.json\n.vscode/launch.json\nsrc/config/config.h"
  },
  {
    "path": "src-esp32/.travis.yml",
    "content": "# Continuous Integration (CI) is the practice, in software\n# engineering, of merging all developer working copies with a shared mainline\n# several times a day < https://docs.platformio.org/page/ci/index.html >\n#\n# Documentation:\n#\n# * Travis CI Embedded Builds with PlatformIO\n#   < https://docs.travis-ci.com/user/integration/platformio/ >\n#\n# * PlatformIO integration with Travis CI\n#   < https://docs.platformio.org/page/ci/travis.html >\n#\n# * User Guide for `platformio ci` command\n#   < https://docs.platformio.org/page/userguide/cmd_ci.html >\n#\n#\n# Please choose one of the following templates (proposed below) and uncomment\n# it (remove \"# \" before each line) or use own configuration according to the\n# Travis CI documentation (see above).\n#\n\n\n#\n# Template #1: General project. Test it using existing `platformio.ini`.\n#\n\n# language: python\n# python:\n#     - \"2.7\"\n#\n# sudo: false\n# cache:\n#     directories:\n#         - \"~/.platformio\"\n#\n# install:\n#     - pip install -U platformio\n#     - platformio update\n#\n# script:\n#     - platformio run\n\n\n#\n# Template #2: The project is intended to be used as a library with examples.\n#\n\n# language: python\n# python:\n#     - \"2.7\"\n#\n# sudo: false\n# cache:\n#     directories:\n#         - \"~/.platformio\"\n#\n# env:\n#     - PLATFORMIO_CI_SRC=path/to/test/file.c\n#     - PLATFORMIO_CI_SRC=examples/file.ino\n#     - PLATFORMIO_CI_SRC=path/to/test/directory\n#\n# install:\n#     - pip install -U platformio\n#     - platformio update\n#\n# script:\n#     - platformio ci --lib=\".\" --board=ID_1 --board=ID_2 --board=ID_N\n"
  },
  {
    "path": "src-esp32/certificates/.gitKeep",
    "content": ""
  },
  {
    "path": "src-esp32/include/README",
    "content": "\nThis directory is intended for project header files.\n\nA header file is a file containing C declarations and macro definitions\nto be shared between several project source files. You request the use of a\nheader file in your project source file (C, C++, etc) located in `src` folder\nby including it, with the C preprocessing directive `#include'.\n\n```src/main.c\n\n#include \"header.h\"\n\nint main (void)\n{\n ...\n}\n```\n\nIncluding a header file produces the same results as copying the header file\ninto each source file that needs it. Such copying would be time-consuming\nand error-prone. With a header file, the related declarations appear\nin only one place. If they need to be changed, they can be changed in one\nplace, and programs that include the header file will automatically use the\nnew version when next recompiled. The header file eliminates the labor of\nfinding and changing all the copies as well as the risk that a failure to\nfind one copy will result in inconsistencies within a program.\n\nIn C, the usual convention is to give header files names that end with `.h'.\nIt is most portable to use only letters, digits, dashes, and underscores in\nheader file names, and at most one dot.\n\nRead more about using header files in official GCC documentation:\n\n* Include Syntax\n* Include Operation\n* Once-Only Headers\n* Computed Includes\n\nhttps://gcc.gnu.org/onlinedocs/cpp/Header-Files.html\n"
  },
  {
    "path": "src-esp32/lib/README",
    "content": "\nThis directory is intended for project specific (private) libraries.\nPlatformIO will compile them to static libraries and link into executable file.\n\nThe source code of each library should be placed in a an own separate directory\n(\"lib/your_library_name/[here are source files]\").\n\nFor example, see a structure of the following two libraries `Foo` and `Bar`:\n\n|--lib\n|  |\n|  |--Bar\n|  |  |--docs\n|  |  |--examples\n|  |  |--src\n|  |     |- Bar.c\n|  |     |- Bar.h\n|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html\n|  |\n|  |--Foo\n|  |  |- Foo.c\n|  |  |- Foo.h\n|  |\n|  |- README --> THIS FILE\n|\n|- platformio.ini\n|--src\n   |- main.c\n\nand a contents of `src/main.c`:\n```\n#include <Foo.h>\n#include <Bar.h>\n\nint main (void)\n{\n  ...\n}\n\n```\n\nPlatformIO Library Dependency Finder will find automatically dependent\nlibraries scanning project source files.\n\nMore information about PlatformIO Library Dependency Finder\n- https://docs.platformio.org/page/librarymanager/ldf.html\n"
  },
  {
    "path": "src-esp32/platformio.ini",
    "content": "; PlatformIO Project Configuration File\n;\n;   Build options: build flags, source filter\n;   Upload options: custom upload port, speed and extra flags\n;   Library options: dependencies, extra library storages\n;   Advanced options: extra scripting\n;\n; Please visit documentation for the other options and examples\n; https://docs.platformio.org/page/projectconf.html\n\n[env:esp32doit-devkit-v1]\nplatform = espressif32\nboard = lolin32\nframework = arduino\nmonitor_speed = 115200\n\nbuild_flags =\n  -DCOMPONENT_EMBED_TXTFILES=certificates/certificate.pem.crt:certificates/private.pem.key:certificates/amazonrootca1.pem\n  -DCORE_DEBUG_LEVEL=0\n\nlib_deps =\n  https://github.com/Savjee/EmonLib-esp32.git\n  Adafruit GFX Library\n  Adafruit SSD1306\n  adafruit/Adafruit BusIO @ ^1.7.2\n  MQTT\n  NTPClient\n"
  },
  {
    "path": "src-esp32/src/config/config.dist.h",
    "content": "#ifndef CONFIG\n#define CONFIG\n\n/**\n * Set this to false to disable Serial logging\n */\n#define DEBUG true\n\n/**\n * The name of this device (as defined in the AWS IOT console).\n * Also used to set the hostname on the network\n */\n#define DEVICE_NAME \"*****YOUR AWS IOT DEVICE NAME******\"\n\n/**\n * ADC input pin that is used to read out the CT sensor\n */\n#define ADC_INPUT 36\n\n/**\n * The voltage of your home, used to calculate the wattage.\n * Try setting this as accurately as possible.\n */\n#define HOME_VOLTAGE 245.0\n\n/**\n * WiFi credentials\n */\n#define WIFI_NETWORK \"****** YOUR WIFI NETWORK NAME *******\"\n#define WIFI_PASSWORD \"****** YOUR WIFI PASSWORD *******\"\n\n/**\n * Timeout for the WiFi connection. When this is reached,\n * the ESP goes into deep sleep for 30seconds to try and\n * recover.\n */\n#define WIFI_TIMEOUT 20000 // 20 seconds\n\n/**\n * How long should we wait after a failed WiFi connection\n * before trying to set one up again.\n */\n#define WIFI_RECOVER_TIME_MS 20000 // 20 seconds\n\n/**\n * Dimensions of the OLED display attached to the ESP\n */\n#define SCREEN_WIDTH 128\n#define SCREEN_HEIGHT 64\n\n/**\n * Force Emonlib to assume a 3.3V supply to the CT sensor\n */\n#define emonTxV3 1\n\n\n/**\n * Local measurements\n */\n#define LOCAL_MEASUREMENTS 30\n\n\n/**\n * The MQTT endpoint of the service we should connect to and receive messages\n * from.\n */\n#define AWS_ENABLED true\n#define AWS_IOT_ENDPOINT \"**** YOUR AWS IOT ENDPOINT ****\"\n#define AWS_IOT_TOPIC \"**** YOUR AWS IOT RULE ARN ****\"\n\n#define MQTT_CONNECT_DELAY 200\n#define MQTT_CONNECT_TIMEOUT 20000 // 20 seconds\n\n\n/**\n * Syncing time with an NTP server\n */\n#define NTP_TIME_SYNC_ENABLED true\n#define NTP_SERVER \"pool.ntp.org\"\n#define NTP_OFFSET_SECONDS 3600\n#define NTP_UPDATE_INTERVAL_MS 60000\n\n/**\n * Wether or not you want to enable Home Assistant integration\n */\n#define HA_ENABLED false\n#define HA_ADDRESS \"*** YOUR HOME ASSISTANT IP ADDRESSS ***\"\n#define HA_PORT 8883\n#define HA_USER \"*** MQTT USER ***\"\n#define HA_PASSWORD \"*** MQTT PASSWORD ***\"\n\n// Check which core Arduino is running on. This is done because updating the \n// display only works from the Arduino core.\n#if CONFIG_FREERTOS_UNICORE\n#define ARDUINO_RUNNING_CORE 0\n#else\n#define ARDUINO_RUNNING_CORE 1\n#endif\n\n#endif"
  },
  {
    "path": "src-esp32/src/config/enums.h",
    "content": "#ifndef ENUMS\n#define ENUMS\n\n#include <Arduino.h>\n\n// The state in which the device can be. This mainly affects what\n// is drawn on the display.\nenum DEVICE_STATE {\n  CONNECTING_WIFI,\n  CONNECTING_AWS,\n  FETCHING_TIME,\n  UP,\n};\n\n// Place to store all the variables that need to be displayed.\n// All other functions should update these!\nstruct DisplayValues {\n  double watt;\n  double amps;\n  int8_t wifi_strength;\n  DEVICE_STATE currentState;\n  String time;\n};\n\n#if DEBUG == true\n  #define serial_print(x)  Serial.print (x)\n  #define serial_println(x)  Serial.println (x)\n#else\n  #define serial_print(x)\n  #define serial_println(x)\n#endif\n\n#endif"
  },
  {
    "path": "src-esp32/src/functions/drawFunctions.h",
    "content": "#ifndef DRAW_FUNCTIONS\n#define DRAW_FUNCTIONS\n\n#include <WiFi.h>\n#include <Adafruit_SSD1306.h>\n#include \"../config/enums.h\"\n#include \"../config/config.h\"\n\nextern Adafruit_SSD1306 display;\nextern DisplayValues gDisplayValues;\nextern unsigned char measureIndex;\n\nvoid drawTime(){\n  display.setTextSize(1);\n  display.setCursor(0, 0);\n  display.print(gDisplayValues.time);\n}\n\nvoid drawSignalStrength(){\n  const byte X = 51;\n  const byte X_SPACING = 2;\n\n  // Draw the four base rectangles\n  display.fillRect(X, 8-2, 1, 2, WHITE); // Bar 1\n  display.fillRect(X + X_SPACING, 8-2, 1, 2, WHITE); // Bar 2\n  display.fillRect(X + X_SPACING*2, 8-2, 1, 2, WHITE); // Bar 3\n  display.fillRect(X + X_SPACING*3, 8-2, 1, 2, WHITE); // Bar 4\n\n  // Draw bar 2\n  if(gDisplayValues.wifi_strength > -70){\n    display.fillRect(X+X_SPACING, 8-4, 1, 4, WHITE);\n  }\n\n  // Draw bar 3\n  if(gDisplayValues.wifi_strength > -60){\n    display.fillRect(X+X_SPACING*2, 8-6, 1, 6, WHITE);\n  }\n\n  // Draw bar 4\n  if(gDisplayValues.wifi_strength >= -50){\n    display.fillRect(X+X_SPACING*3, 8-8, 1, 8, WHITE);\n  }\n}\n\nvoid drawMeasurementProgress(){\n  const byte Y = SCREEN_WIDTH - 20;\n  display.drawRect(0, Y, measureIndex*2, 2, WHITE);\n}\n\n/**\n * The screen that is displayed when the ESP has just booted\n * and is connecting to WiFi & AWS.\n */\nvoid drawBootscreen(){\n  byte X = 14;\n  byte Y = 70;\n  byte WIDTH = 6;\n  byte MAX_HEIGHT = 35;\n  byte HEIGHT_STEP = 10;\n  byte X_SPACING = 10;\n\n  display.fillRect(X              , Y, WIDTH, MAX_HEIGHT - HEIGHT_STEP*3, WHITE);\n  display.fillRect(X + X_SPACING  , Y - HEIGHT_STEP, WIDTH, MAX_HEIGHT - HEIGHT_STEP*2, WHITE);\n  display.fillRect(X + X_SPACING*2, Y - HEIGHT_STEP*2, WIDTH, MAX_HEIGHT - HEIGHT_STEP, WHITE);\n  display.fillRect(X + X_SPACING*3, Y - HEIGHT_STEP*3, WIDTH, MAX_HEIGHT, WHITE);\n\n  display.setTextSize(1);\n  display.setCursor(0, Y + MAX_HEIGHT / 2);\n  display.println(\"Connecting\");\n\n  if(gDisplayValues.currentState == CONNECTING_WIFI){\n    display.println(\"   WiFi\");\n  }\n\n  if(gDisplayValues.currentState == CONNECTING_AWS){\n    display.println(\"   AWS\");\n  }\n}\n\n/**\n * Draw the current amps & watts in the middle of the display.\n */\nvoid drawAmpsWatts(){\n\n  String watts = String(gDisplayValues.watt, 0);\n  String amps = String(gDisplayValues.amps, 2);\n  \n  String lblWatts = \"Watt\";\n  String lblAmps = \"Amps\";\n\n  const int startY = 30;\n\n  // Calculate how wide (pixels) the text will be once rendered.\n  // Each character = 6 pixels, with font size 2, that is 12 pixels.\n  // -1 because of the spacing between letters (last one doesn't)\n  int widthAmps = (amps.length() * 12) -1;\n  int widthLblAmps = lblAmps.length() * 6 - 1;\n\n  int widthWatts = watts.length() * 12 - 1;\n  int widthLblWatts = lblWatts.length() * 6 -1;\n\n  display.setTextSize(2);\n  display.setCursor((SCREEN_HEIGHT - widthAmps) / 2, startY);\n  display.print(amps);\n\n  display.setTextSize(1);\n  display.setCursor((SCREEN_HEIGHT - widthLblAmps) / 2, startY + 15);\n  display.print(lblAmps);\n\n  display.setTextSize(2);\n  display.setCursor((SCREEN_HEIGHT - widthWatts) / 2, startY + 40);\n  display.print(watts);\n\n  display.setTextSize(1);\n  display.setCursor((SCREEN_HEIGHT - widthLblWatts) / 2, startY + 60);\n  display.print(lblWatts);\n}\n\n#endif"
  },
  {
    "path": "src-esp32/src/main.cpp",
    "content": "#include <Arduino.h>\n#include \"EmonLib.h\"\n#include \"WiFi.h\"\n#include <driver/adc.h>\n#include \"config/config.h\"\n#include \"config/enums.h\"\n#include <Wire.h>\n#include <Adafruit_GFX.h>\n#include <Adafruit_SSD1306.h>\n\n#include \"tasks/updateDisplay.h\"\n#include \"tasks/fetch-time-from-ntp.h\"\n#include \"tasks/mqtt-aws.h\"\n#include \"tasks/wifi-connection.h\"\n#include \"tasks/wifi-update-signalstrength.h\"\n#include \"tasks/measure-electricity.h\"\n#include \"tasks/mqtt-home-assistant.h\"\n\nAdafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);\nDisplayValues gDisplayValues;\nEnergyMonitor emon1;\n\n// Place to store local measurements before sending them off to AWS\nunsigned short measurements[LOCAL_MEASUREMENTS];\nunsigned char measureIndex = 0;\n\nvoid setup()\n{\n  #if DEBUG == true\n    Serial.begin(115200);\n  #endif \n\n  // Setup the ADC\n  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);\n  analogReadResolution(ADC_BITS);\n  pinMode(ADC_INPUT, INPUT);\n\n  // i2c for the OLED panel\n  Wire.begin(5, 4); \n\n  // Initialize the display\n  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false, false)) {\n    serial_println(F(\"SSD1306 allocation failed\"));\n    delay(10*1000);\n    ESP.restart();\n  }\n\n  // Init the display\n  display.clearDisplay();\n  display.setRotation(3);\n  display.setTextSize(1);\n  display.setTextColor(WHITE);\n  display.setTextWrap(false);\n\n  // Initialize emon library\n  emon1.current(ADC_INPUT, 30);\n\n  // ----------------------------------------------------------------\n  // TASK: Connect to WiFi & keep the connection alive.\n  // ----------------------------------------------------------------\n  xTaskCreatePinnedToCore(\n    keepWiFiAlive,\n    \"keepWiFiAlive\",  // Task name\n    5000,            // Stack size (bytes)\n    NULL,             // Parameter\n    1,                // Task priority\n    NULL,             // Task handle\n    ARDUINO_RUNNING_CORE\n  );\n\n  // ----------------------------------------------------------------\n  // TASK: Connect to AWS & keep the connection alive.\n  // ----------------------------------------------------------------\n  #if AWS_ENABLED == true\n    xTaskCreate(\n      keepAWSConnectionAlive,\n      \"MQTT-AWS\",      // Task name\n      5000,            // Stack size (bytes)\n      NULL,             // Parameter\n      5,                // Task priority\n      NULL              // Task handle\n    );\n  #endif\n\n  // ----------------------------------------------------------------\n  // TASK: Update the display every second\n  //       This is pinned to the same core as Arduino\n  //       because it would otherwise corrupt the OLED\n  // ----------------------------------------------------------------\n  xTaskCreatePinnedToCore(\n    updateDisplay,\n    \"UpdateDisplay\",  // Task name\n    10000,            // Stack size (bytes)\n    NULL,             // Parameter\n    3,                // Task priority\n    NULL,             // Task handle\n    ARDUINO_RUNNING_CORE\n  );\n\n  // ----------------------------------------------------------------\n  // Task: measure electricity consumption ;)\n  // ----------------------------------------------------------------\n  xTaskCreate(\n    measureElectricity,\n    \"Measure electricity\",  // Task name\n    5000,                  // Stack size (bytes)\n    NULL,                   // Parameter\n    4,                      // Task priority\n    NULL                    // Task handle\n  );\n\n  // ----------------------------------------------------------------\n  // TASK: update time from NTP server.\n  // ----------------------------------------------------------------\n  #if NTP_TIME_SYNC_ENABLED == true\n    xTaskCreate(\n      fetchTimeFromNTP,\n      \"Update NTP time\",\n      5000,            // Stack size (bytes)\n      NULL,             // Parameter\n      1,                // Task priority\n      NULL              // Task handle\n    );\n  #endif\n\n  // ----------------------------------------------------------------\n  // TASK: update WiFi signal strength\n  // ----------------------------------------------------------------\n  xTaskCreate(\n    updateWiFiSignalStrength,\n    \"Update WiFi strength\",\n    1000,             // Stack size (bytes)\n    NULL,             // Parameter\n    2,                // Task priority\n    NULL              // Task handle\n  );\n\n  #if HA_ENABLED == true\n    xTaskCreate(\n      HADiscovery,\n      \"MQTT-HA Discovery\",  // Task name\n      5000,                // Stack size (bytes)\n      NULL,                 // Parameter\n      5,                    // Task priority\n      NULL                  // Task handle\n    );\n\n    xTaskCreate(\n      keepHAConnectionAlive,\n      \"MQTT-HA Connect\",\n      5000,\n      NULL,\n      4,\n      NULL\n    );\n  #endif\n}\n\nvoid loop()\n{\n  vTaskDelay(10000 / portTICK_PERIOD_MS);\n}"
  },
  {
    "path": "src-esp32/src/tasks/fetch-time-from-ntp.h",
    "content": "#ifndef TASK_FETCH_TIME_NTP\n#define TASK_FETCH_TIME_NTP\n\n#if NTP_TIME_SYNC_ENABLED == true\n    #include <Arduino.h>\n    #include <WiFi.h>\n    #include <NTPClient.h>\n    #include <WiFiUdp.h>\n    #include <NTPClient.h>\n    #include \"../config/enums.h\"\n\n    extern void reconnectWifiIfNeeded();\n    extern DisplayValues gDisplayValues;\n\n    WiFiUDP ntpUDP;\n\n    // TODO: this does not take timezones into account! Only UTC for now.\n    NTPClient timeClient(ntpUDP, NTP_SERVER, NTP_OFFSET_SECONDS, NTP_UPDATE_INTERVAL_MS);\n\n    void fetchTimeFromNTP(void * parameter){\n        for(;;){\n            if(!WiFi.isConnected()){\n                vTaskDelay(10*1000 / portTICK_PERIOD_MS);\n                continue;\n            }\n\n            serial_println(\"[NTP] Updating...\");\n            timeClient.update();\n\n            String timestring = timeClient.getFormattedTime();\n            short tIndex = timestring.indexOf(\"T\");\n            gDisplayValues.time = timestring.substring(tIndex + 1, timestring.length() -3);\n            \n            serial_println(\"[NTP] Done\");\n\n            // Sleep for a minute before checking again\n            vTaskDelay(NTP_UPDATE_INTERVAL_MS / portTICK_PERIOD_MS);\n        }\n    }\n#endif\n#endif\n"
  },
  {
    "path": "src-esp32/src/tasks/measure-electricity.h",
    "content": "#ifndef TASK_MEASURE_ELECTRICITY\n#define TASK_MEASURE_ELECTRICITY\n\n#include <Arduino.h>\n#include \"EmonLib.h\"\n\n#include \"../config/config.h\"\n#include \"../config/enums.h\"\n#include \"mqtt-aws.h\"\n#include \"mqtt-home-assistant.h\"\n\nextern DisplayValues gDisplayValues;\nextern EnergyMonitor emon1;\nextern unsigned short measurements[];\nextern unsigned char measureIndex;\n\nvoid measureElectricity(void * parameter)\n{\n    for(;;){\n      serial_println(\"[ENERGY] Measuring...\");\n      long start = millis();\n\n      double amps = emon1.calcIrms(1480);\n      double watts = amps * HOME_VOLTAGE;\n\n      gDisplayValues.amps = amps;\n      gDisplayValues.watt = watts;\n\n      measurements[measureIndex] = watts;\n      measureIndex++;\n\n      if(measureIndex == LOCAL_MEASUREMENTS){\n          #if AWS_ENABLED == true\n            xTaskCreate(\n              uploadMeasurementsToAWS,\n              \"Upload measurements to AWS\",\n              10000,             // Stack size (bytes)\n              NULL,             // Parameter\n              5,                // Task priority\n              NULL              // Task handle\n            );\n          #endif\n\n          #if HA_ENABLED == true\n            xTaskCreate(\n              sendEnergyToHA,\n              \"HA-MQTT Upload\",\n              10000,             // Stack size (bytes)\n              NULL,             // Parameter\n              5,                // Task priority\n              NULL              // Task handle\n            );\n          #endif\n\n          measureIndex = 0;\n      }\n\n      long end = millis();\n\n      // Schedule the task to run again in 1 second (while\n      // taking into account how long measurement took)\n      vTaskDelay((1000-(end-start)) / portTICK_PERIOD_MS);\n    }    \n}\n\n#endif\n"
  },
  {
    "path": "src-esp32/src/tasks/mqtt-aws.h",
    "content": "#ifndef TASK_MQTT_AWS\n#define TASK_MQTT_AWS\n\n#if AWS_ENABLED == true\n    #include <Arduino.h>\n    #include <WiFiClientSecure.h>\n    #include <MQTTClient.h>\n    #include \"../config/config.h\"\n\n    extern unsigned short measurements[];\n\n    #define AWS_MAX_MSG_SIZE_BYTES 300\n\n    WiFiClientSecure AWS_net;\n    MQTTClient AWS_mqtt = MQTTClient(AWS_MAX_MSG_SIZE_BYTES);\n\n    extern const uint8_t aws_root_ca_pem_start[] asm(\"_binary_certificates_amazonrootca1_pem_start\");\n    extern const uint8_t aws_root_ca_pem_end[] asm(\"_binary_certificates_amazonrootca1_pem_end\");\n\n    extern const uint8_t certificate_pem_crt_start[] asm(\"_binary_certificates_certificate_pem_crt_start\");\n    extern const uint8_t certificate_pem_crt_end[] asm(\"_binary_certificates_certificate_pem_crt_end\");\n\n    extern const uint8_t private_pem_key_start[] asm(\"_binary_certificates_private_pem_key_start\");\n    extern const uint8_t private_pem_key_end[] asm(\"_binary_certificates_private_pem_key_end\");\n\n    void keepAWSConnectionAlive(void * parameter){\n        for(;;){\n            if(AWS_mqtt.connected()){\n                AWS_mqtt.loop();\n                vTaskDelay(500 / portTICK_PERIOD_MS);\n                continue;\n            }\n\n            if(!WiFi.isConnected()){\n                vTaskDelay(1000 / portTICK_PERIOD_MS);\n                continue;\n            }\n\n            // Configure certificates\n            AWS_net.setCACert((const char *) aws_root_ca_pem_start);\n            AWS_net.setCertificate((const char *) certificate_pem_crt_start);\n            AWS_net.setPrivateKey((const char *) private_pem_key_start);\n\n            serial_println(F(\"[MQTT] Connecting to AWS...\"));\n            AWS_mqtt.begin(AWS_IOT_ENDPOINT, 8883, AWS_net);\n\n            long startAttemptTime = millis();\n        \n            while (!AWS_mqtt.connect(DEVICE_NAME, HA_USER, HA_PASSWORD) &&\n                    millis() - startAttemptTime < MQTT_CONNECT_TIMEOUT)\n            {\n                vTaskDelay(MQTT_CONNECT_DELAY);\n            }\n\n            if(!AWS_mqtt.connected()){\n                serial_println(F(\"[MQTT] AWS connection timeout. Retry in 30s.\"));\n                vTaskDelay(30000 / portTICK_PERIOD_MS);\n            }\n\n            serial_println(F(\"[MQTT] AWS Connected!\"));\n        }\n    }\n\n    /**\n     * TASK: Upload measurements to AWS. This only works when there are enough\n     * local measurements. It's called by the measurement function.\n     */\n    void uploadMeasurementsToAWS(void * parameter){\n        if(!WiFi.isConnected() || !AWS_mqtt.connected()){\n            serial_println(\"[MQTT] AWS: no connection. Discarding data..\");\n            vTaskDelete(NULL);\n        }\n\n        char msg[AWS_MAX_MSG_SIZE_BYTES];\n        strcpy(msg, \"{\\\"readings\\\":[\");\n\n        for (short i = 0; i < LOCAL_MEASUREMENTS-1; i++){\n            strcat(msg, String(measurements[i]).c_str());\n            strcat(msg, \",\");\n        }\n\n        strcat(msg, String(measurements[LOCAL_MEASUREMENTS-1]).c_str());\n        strcat(msg, \"]}\");\n            \n        serial_print(\"[MQTT] AWS publish: \");\n        serial_println(msg);\n        AWS_mqtt.publish(AWS_IOT_TOPIC, msg);\n\n        // Task is done!\n        vTaskDelete(NULL);\n    }\n#endif\n#endif\n"
  },
  {
    "path": "src-esp32/src/tasks/mqtt-home-assistant.h",
    "content": "#ifndef TASK_HOME_ASSISTANT\n#define TASK_HOME_ASSISTANT\n\n#if HA_ENABLED == true\n\n    #include <Arduino.h>\n    #include <WiFiClientSecure.h>\n    #include <MQTTClient.h>\n    #include \"../config/config.h\"\n\n    WiFiClientSecure HA_net;\n    MQTTClient HA_mqtt(1024);\n\n    extern unsigned short measurements[];\n\n    const char* PROGMEM HA_discovery_msg = \"{\"\n            \"\\\"name\\\":\\\"\" DEVICE_NAME \"\\\",\"\n            \"\\\"device_class\\\":\\\"power\\\",\"\n            \"\\\"unit_of_measurement\\\":\\\"W\\\",\"\n            \"\\\"icon\\\":\\\"mdi:transmission-tower\\\",\"\n            \"\\\"state_topic\\\":\\\"homeassistant/sensor/\" DEVICE_NAME \"/state\\\",\"\n            \"\\\"value_template\\\":\\\"{{ value_json.power}}\\\",\"\n            \"\\\"device\\\": {\"\n                \"\\\"name\\\":\\\"\" DEVICE_NAME \"\\\",\"\n                \"\\\"sw_version\\\":\\\"2.0\\\",\"\n                \"\\\"model\\\":\\\"HW V2\\\",\"\n                \"\\\"manufacturer\\\":\\\"Xavier Decuyper\\\",\"\n                \"\\\"identifiers\\\":[\\\"\" DEVICE_NAME \"\\\"]\"\n            \"}\"\n        \"}\";\n\n    /**\n     * Established a connection to Home Assistant MQTT broker.\n     * \n     * This task should run continously. It will check if an\n     * MQTT connection is active and if so, will sleep for 1\n     * minute. If not, a new connection will be established.\n     */\n    void keepHAConnectionAlive(void * parameter){\n        for(;;){\n            // When we are connected, loop the MQTT client and sleep for 0,5s\n            if(HA_mqtt.connected()){\n                HA_mqtt.loop();\n                vTaskDelay(250 / portTICK_PERIOD_MS);\n                continue;\n            }\n\n            if(!WiFi.isConnected()){\n                vTaskDelay(1000 / portTICK_PERIOD_MS);\n                continue;\n            }\n\n            serial_println(F(\"[MQTT] Connecting to HA...\"));\n            HA_mqtt.begin(HA_ADDRESS, HA_PORT, HA_net);\n\n            long startAttemptTime = millis();\n        \n            while (!HA_mqtt.connect(DEVICE_NAME, HA_USER, HA_PASSWORD) &&\n                    millis() - startAttemptTime < MQTT_CONNECT_TIMEOUT)\n            {\n                vTaskDelay(MQTT_CONNECT_DELAY / portTICK_PERIOD_MS);\n            }\n\n            if(!HA_mqtt.connected()){\n                serial_println(F(\"[MQTT] HA connection failed. Waiting 30s..\"));\n                vTaskDelay(30000 / portTICK_PERIOD_MS);\n            }\n\n            serial_println(F(\"[MQTT] HA Connected!\"));\n        }\n    }\n\n    /**\n     * TASK: Every 15 minutes we send Home Assistant a discovery message\n     *       so that the energy monitor shows up in the device registry.\n     */\n    void HADiscovery(void * parameter){\n        for(;;){\n            if(!HA_mqtt.connected()){\n                serial_println(\"[MQTT] HA: no MQTT connection.\");\n                vTaskDelay(30 * 1000 / portTICK_PERIOD_MS);\n                continue;\n            }\n\n            serial_println(\"[MQTT] HA sending auto discovery\");\n            HA_mqtt.publish(\"homeassistant/sensor/\" DEVICE_NAME \"/config\", HA_discovery_msg);\n            vTaskDelay(15 * 60 * 1000 / portTICK_PERIOD_MS);\n        }\n    }\n\n    void sendEnergyToHA(void * parameter){\n        if(!HA_mqtt.connected()){\n        serial_println(\"[MQTT] Can't send to HA without MQTT. Abort.\");\n        vTaskDelete(NULL);\n        }\n\n        char msg[30];\n        strcpy(msg, \"{\\\"power\\\":\");\n            strcat(msg, String(measurements[LOCAL_MEASUREMENTS-1]).c_str());\n        strcat(msg, \"}\");\n\n        serial_print(\"[MQTT] HA publish: \");\n        serial_println(msg);\n\n        HA_mqtt.publish(\"homeassistant/sensor/\" DEVICE_NAME \"/state\", msg);\n\n        // Task is done!\n        vTaskDelete(NULL);\n    }\n#endif\n#endif\n"
  },
  {
    "path": "src-esp32/src/tasks/updateDisplay.h",
    "content": "#ifndef TASK_UPDATE_DISPLAY\n#define TASK_UPDATE_DISPLAY\n\n#include <Arduino.h>\n#include <Adafruit_SSD1306.h>\n#include \"functions/drawFunctions.h\"\n#include \"../config/config.h\"\n\nextern Adafruit_SSD1306 display;\nextern DisplayValues gDisplayValues;\n\n/**\n * Metafunction that takes care of drawing all the different\n * parts of the display (or not if it's turned off).\n */\nvoid updateDisplay(void * parameter){\n  for (;;){\n    serial_println(F(\"[LCD] Updating...\"));\n    display.clearDisplay();\n\n    if(gDisplayValues.currentState == CONNECTING_WIFI || \n        gDisplayValues.currentState == CONNECTING_AWS){\n      drawBootscreen();\n    }\n    \n    if(gDisplayValues.currentState == UP){\n      drawTime();\n      drawSignalStrength();\n      drawAmpsWatts();\n      drawMeasurementProgress();\n    }\n\n    display.display();\n\n    // Sleep for 2 seconds, then update display again!\n    vTaskDelay(2000 / portTICK_PERIOD_MS);\n  }\n}\n\n#endif\n"
  },
  {
    "path": "src-esp32/src/tasks/wifi-connection.h",
    "content": "#ifndef TASK_WIFI_CONNECTION\n#define TASK_WIFI_CONNECTION\n\n#include <Arduino.h>\n#include \"WiFi.h\"\n#include \"../config/enums.h\"\n#include \"../config/config.h\"\n\nextern DisplayValues gDisplayValues;\nextern void goToDeepSleep();\n\n/**\n * Task: monitor the WiFi connection and keep it alive!\n * \n * When a WiFi connection is established, this task will check it every 10 seconds \n * to make sure it's still alive.\n * \n * If not, a reconnect is attempted. If this fails to finish within the timeout,\n * the ESP32 is send to deep sleep in an attempt to recover from this.\n */\nvoid keepWiFiAlive(void * parameter){\n    for(;;){\n        if(WiFi.status() == WL_CONNECTED){\n            vTaskDelay(10000 / portTICK_PERIOD_MS);\n            continue;\n        }\n\n        serial_println(F(\"[WIFI] Connecting\"));\n        gDisplayValues.currentState = CONNECTING_WIFI;\n\n        WiFi.mode(WIFI_STA);\n        WiFi.setHostname(DEVICE_NAME);\n        WiFi.begin(WIFI_NETWORK, WIFI_PASSWORD);\n\n        unsigned long startAttemptTime = millis();\n\n        // Keep looping while we're not connected and haven't reached the timeout\n        while (WiFi.status() != WL_CONNECTED && \n                millis() - startAttemptTime < WIFI_TIMEOUT){}\n\n        // Make sure that we're actually connected, otherwise go to deep sleep\n        if(WiFi.status() != WL_CONNECTED){\n            serial_println(F(\"[WIFI] FAILED\"));\n            vTaskDelay(WIFI_RECOVER_TIME_MS / portTICK_PERIOD_MS);\n        }\n\n        serial_print(F(\"[WIFI] Connected: \"));\n        serial_println(WiFi.localIP());\n        gDisplayValues.currentState = UP;\n    }\n}\n\n#endif\n"
  },
  {
    "path": "src-esp32/src/tasks/wifi-update-signalstrength.h",
    "content": "#ifndef TASK_UPDATE_WIFI_SIGNAL\n#define TASK_UPDATE_WIFI_SIGNAL\n\n#include <Arduino.h>\n#include \"WiFi.h\"\n#include \"../config/enums.h\"\n\nextern DisplayValues gDisplayValues;\n\n/**\n * TASK: Get the current WiFi signal strength and write it to the\n * displayValues so it can be shown by the updateDisplay task\n */\nvoid updateWiFiSignalStrength(void * parameter){\n  for(;;){\n    if(WiFi.isConnected()){\n      serial_println(F(\"[WIFI] Updating signal strength...\"));\n      gDisplayValues.wifi_strength = WiFi.RSSI();\n    }\n\n    // Sleep for 10 seconds\n    vTaskDelay(10000 / portTICK_PERIOD_MS);\n  }\n}\n\n#endif"
  },
  {
    "path": "src-esp32/test/README",
    "content": "\nThis directory is intended for PIO Unit Testing and project tests.\n\nUnit Testing is a software testing method by which individual units of\nsource code, sets of one or more MCU program modules together with associated\ncontrol data, usage procedures, and operating procedures, are tested to\ndetermine whether they are fit for use. Unit testing finds problems early\nin the development cycle.\n\nMore information about PIO Unit Testing:\n- https://docs.platformio.org/page/plus/unit-testing.html\n"
  }
]