Full Code of Savjee/home-energy-monitor for AI

master 70fd234b305f cached
97 files
130.7 KB
37.3k tokens
95 symbols
1 requests
Download .txt
Repository: Savjee/home-energy-monitor
Branch: master
Commit: 70fd234b305f
Files: 97
Total size: 130.7 KB

Directory structure:
gitextract_udkxjzlu/

├── .github/
│   └── workflows/
│       ├── aws.yml
│       └── firmware.yml
├── .gitignore
├── 3D-designs/
│   ├── 01 - Case.stl
│   └── 02 - Top lid.stl
├── README.md
├── _stuff/
│   └── Sketch icon.sketch
├── src-app/
│   ├── .gitignore
│   ├── angular.json
│   ├── config.xml
│   ├── e2e/
│   │   ├── protractor.conf.js
│   │   ├── src/
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── app.po.ts
│   │   └── tsconfig.e2e.json
│   ├── ionic.config.json
│   ├── package.json
│   ├── resources/
│   │   ├── README.md
│   │   ├── icon.png.md5
│   │   └── splash.png.md5
│   ├── src/
│   │   ├── app/
│   │   │   ├── app-routing.module.ts
│   │   │   ├── app.component.html
│   │   │   ├── app.component.ts
│   │   │   ├── app.module.ts
│   │   │   ├── components/
│   │   │   │   ├── components.module.ts
│   │   │   │   └── loading-indicator/
│   │   │   │       ├── loading-indicator.component.html
│   │   │   │       ├── loading-indicator.component.scss
│   │   │   │       └── loading-indicator.component.ts
│   │   │   ├── services/
│   │   │   │   └── energy-service.service.ts
│   │   │   ├── tab-home/
│   │   │   │   ├── tab-home.module.ts
│   │   │   │   ├── tab-home.page.html
│   │   │   │   ├── tab-home.page.scss
│   │   │   │   └── tab-home.page.ts
│   │   │   ├── tab-readings/
│   │   │   │   ├── tab-readings.module.ts
│   │   │   │   ├── tab-readings.page.html
│   │   │   │   ├── tab-readings.page.scss
│   │   │   │   └── tab-readings.page.ts
│   │   │   ├── tab-statistics/
│   │   │   │   ├── tab-statistics.module.ts
│   │   │   │   ├── tab-statistics.page.html
│   │   │   │   ├── tab-statistics.page.scss
│   │   │   │   └── tab-statistics.page.ts
│   │   │   ├── tabs/
│   │   │   │   ├── tabs.module.ts
│   │   │   │   ├── tabs.page.html
│   │   │   │   ├── tabs.page.scss
│   │   │   │   ├── tabs.page.ts
│   │   │   │   └── tabs.router.module.ts
│   │   │   └── utils/
│   │   │       └── chart-defaults.ts
│   │   ├── environments/
│   │   │   ├── environment.prod.ts
│   │   │   └── environment.ts
│   │   ├── global.scss
│   │   ├── index.html
│   │   ├── karma.conf.js
│   │   ├── main.ts
│   │   ├── polyfills.ts
│   │   ├── test.ts
│   │   ├── theme/
│   │   │   └── variables.scss
│   │   ├── tsconfig.app.json
│   │   └── tsconfig.spec.json
│   ├── tsconfig.json
│   └── tslint.json
├── src-aws/
│   ├── core/
│   │   ├── aws-connections.js
│   │   ├── config.js
│   │   ├── helpers/
│   │   │   ├── CalculateKwh.js
│   │   │   ├── CalculateKwh.test.js
│   │   │   ├── IsNightTarif.js
│   │   │   └── IsNightTarif.test.js
│   │   └── helpers.js
│   ├── dashboard/
│   │   ├── img/
│   │   │   └── favicons/
│   │   │       ├── browserconfig.xml
│   │   │       └── site.webmanifest
│   │   ├── index.html
│   │   ├── main.css
│   │   └── main.js
│   ├── functions/
│   │   ├── cron-rotate-daily.js
│   │   └── graphql/
│   │       ├── graphql.js
│   │       └── resolvers/
│   │           ├── realtime.js
│   │           ├── stats.js
│   │           └── usageData.js
│   ├── package.json
│   ├── serverless.yml
│   └── webpack.config.js
└── src-esp32/
    ├── .gitignore
    ├── .travis.yml
    ├── certificates/
    │   └── .gitKeep
    ├── include/
    │   └── README
    ├── lib/
    │   └── README
    ├── platformio.ini
    ├── src/
    │   ├── config/
    │   │   ├── config.dist.h
    │   │   └── enums.h
    │   ├── functions/
    │   │   └── drawFunctions.h
    │   ├── main.cpp
    │   └── tasks/
    │       ├── fetch-time-from-ntp.h
    │       ├── measure-electricity.h
    │       ├── mqtt-aws.h
    │       ├── mqtt-home-assistant.h
    │       ├── updateDisplay.h
    │       ├── wifi-connection.h
    │       └── wifi-update-signalstrength.h
    └── test/
        └── README

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/aws.yml
================================================
name: aws

on:
  pull_request:
  push:
   paths:
     - 'src-aws/**'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: npm install
      run: |
        cd src-aws
        npm ci
        npm install -g serverless mocha
      env:
        CI: true
    - name: package
      run: |
        cd src-aws
        serverless package


================================================
FILE: .github/workflows/firmware.yml
================================================
name: firmware

on:
  pull_request:
  push:
    paths:
      - 'src-esp32/**'

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v1

    - name: Setup Python
      uses: actions/setup-python@master
      with:
        python-version: '3.x'

    - name: Install Platform IO
      run: |
        python -m pip install --upgrade pip
        pip install -U platformio

    - name: Build
      run: |
        cd src-esp32
        cp src/config/config.dist.h src/config/config.h
        mkdir -p certificates
        echo "a" > certificates/amazonrootca1.pem
        echo "a" > certificates/certificate.pem.crt
        echo "a" > certificates/private.pem.key
        echo "a" > certificates/public.pem.key
        platformio run
      env:
        CI: true


================================================
FILE: .gitignore
================================================
.DS_Store
src-aws/.serverless
src-esp32/.vscode
node_modules
src-esp32/certificates/*.pem
src-esp32/certificates/*.crt
src-esp32/certificates/*.key
.vscode


================================================
FILE: README.md
================================================
<p align="center">
    <a href="https://github.com/Savjee/home-energy-monitor" rel="noopener">
        <img width=200px height=200px src="https://savjee.github.io/home-energy-monitor/readme-images/logo.png" alt="Home Energy Monitor">
    </a>
</p>

<h3 align="center">Home Energy Monitor (v2)</h3>

<div align="center">

[![GitHub Issues](https://img.shields.io/github/issues/Savjee/home-energy-monitor.svg)](https://github.com/Savjee/home-energy-monitor/issues)
[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/Savjee/home-energy-monitor.svg)](https://github.com/Savjee/home-energy-monitor/pulls)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE)

**⚠️ This is a work in progress. By no means is this production ready.**
</div>

---

ESP32-based Home Energy Monitor: monitors electricity consumption of your entire house with a single CT sensor.

## Structure

This project consists out of multiple components:

| Folder            | Description         | Build status | 
| ----------------- | ------------------- | ------------ | 
| `src-app`         | Mobile app (Ionic)  | n/a |
| `src-aws`         | Serverless AWS backend + GraphQL API | ![AWS Build Status](https://github.com/Savjee/home-energy-monitor/workflows/aws/badge.svg) |
| `src-esp32`       | Firmware for the ESP32 (measuring device) | ![Firmware Build Status](https://github.com/Savjee/home-energy-monitor/workflows/firmware/badge.svg) |

(TODO: add instructions on how to deploy all of this. 😅)

## Video explanation

<div align="center">

[![IMAGE ALT TEXT HERE](https://img.youtube.com/vi/ah3ezprtgmc/0.jpg)](https://www.youtube.com/watch?v=ah3ezprtgmc)

*[https://www.youtube.com/watch?v=ah3ezprtgmc](https://www.youtube.com/watch?v=ah3ezprtgmc)*
</div>

Read 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/)

## Cloud Architecture

This is the cloud architecture that powers the energy meter and the app:

![AWS Cloud Architecture](https://savjee.github.io/home-energy-monitor/readme-images/architecture.png)

In a nutshell:
* The ESP32 has a MQTT connection with AWS IoT Core
* Every 30 seconds, 30 measurements are sent to AWS
* These measurements are stored in DynamoDB (IoT Rule)
* Once a day, all readings from the previous day are archived to S3
* A GraphQL API (hosted on Lambda) exposes the data stored in DynamoDB

## Screenshots

Web dashboard, built on top of the GraphQL API:

![Screenshot Web Dashboard](https://savjee.github.io/home-energy-monitor/readme-images/web-dashboard.png)

What is displayed on the ESP32 OLED display:

![Screenshot ESP32 OLED](https://savjee.github.io/home-energy-monitor/readme-images/esp32-oled.jpg)


## DIY Requirements

To build your own Energy Monitor you need the following hardware:

* ESP32
* CT sensor: YHDC SCT-013-030 (30A/1V)
* 10µF capacitor
* 2 resistors (between 10k-470kΩ)

Other requirements:
* AWS Account (Should be able to run in free-tier)
* Install [PlatformIO](https://platformio.org) on your system
* Drivers for your ESP32 board

Read 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/)


## Contribute

I'm happy to merge in any pull requests. Also feel free to report bugs or feature requests.

================================================
FILE: src-app/.gitignore
================================================
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore

*~
*.sw[mnpcod]
*.log
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
.vscode/
npm-debug.log*

.idea/
.ionic/
.sourcemaps/
.sass-cache/
.tmp/
.versions/
coverage/
www/
node_modules/
tmp/
temp/
platforms/
plugins/
plugins/android.json
plugins/ios.json
$RECYCLE.BIN/

.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate


================================================
FILE: src-app/angular.json
================================================
{
  "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json",
  "version": 1,
  "defaultProject": "app",
  "newProjectRoot": "projects",
  "projects": {
    "app": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {},
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "www",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              {
                "glob": "**/*",
                "input": "src/assets",
                "output": "assets"
              },
              {
                "glob": "**/*.svg",
                "input": "node_modules/ionicons/dist/ionicons/svg",
                "output": "./svg"
              }
            ],
            "styles": [
              {
                "input": "src/theme/variables.scss"
              },
              {
                "input": "src/global.scss"
              }
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                }
              ]
            },
            "ci": {
              "progress": false
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "app:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "app:build:production"
            },
            "ci": {
              "progress": false
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "app:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": [],
            "scripts": [],
            "assets": [
              {
                "glob": "favicon.ico",
                "input": "src/",
                "output": "/"
              },
              {
                "glob": "**/*",
                "input": "src/assets",
                "output": "/assets"
              }
            ]
          },
          "configurations": {
            "ci": {
              "progress": false,
              "watch": false
            }
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
            "exclude": ["**/node_modules/**"]
          }
        },
        "ionic-cordova-build": {
          "builder": "@ionic/angular-toolkit:cordova-build",
          "options": {
            "browserTarget": "app:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "app:build:production"
            }
          }
        },
        "ionic-cordova-serve": {
          "builder": "@ionic/angular-toolkit:cordova-serve",
          "options": {
            "cordovaBuildTarget": "app:ionic-cordova-build",
            "devServerTarget": "app:serve"
          },
          "configurations": {
            "production": {
              "cordovaBuildTarget": "app:ionic-cordova-build:production",
              "devServerTarget": "app:serve:production"
            }
          }
        }
      }
    },
    "app-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "app:serve"
          },
          "configurations": {
            "ci": {
              "devServerTarget": "app:serve:ci"
            }
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": ["**/node_modules/**"]
          }
        }
      }
    }
  },
  "cli": {
    "defaultCollection": "@ionic/angular-toolkit"
  },
  "schematics": {
    "@ionic/angular-toolkit:component": {
      "styleext": "scss"
    },
    "@ionic/angular-toolkit:page": {
      "styleext": "scss"
    }
  }
}


================================================
FILE: src-app/config.xml
================================================
<?xml version='1.0' encoding='utf-8'?>
<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">
    <name>Home Energy</name>
    <description>Measuring home energy usage.</description>
    <author email="hi@savjee.be" href="https://savjee.be/">Xavier Decuyper</author>
    <content src="index.html" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <preference name="ScrollEnabled" value="false" />
    <preference name="android-minSdkVersion" value="19" />
    <preference name="BackupWebStorage" value="none" />
    <preference name="SplashMaintainAspectRatio" value="true" />
    <preference name="FadeSplashScreenDuration" value="300" />
    <preference name="SplashShowOnlyFirstTime" value="false" />
    <preference name="SplashScreen" value="screen" />
    <preference name="SplashScreenDelay" value="3000" />
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
        <icon height="57" src="resources/ios/icon/icon.png" width="57" />
        <icon height="114" src="resources/ios/icon/icon@2x.png" width="114" />
        <icon height="40" src="resources/ios/icon/icon-40.png" width="40" />
        <icon height="80" src="resources/ios/icon/icon-40@2x.png" width="80" />
        <icon height="120" src="resources/ios/icon/icon-40@3x.png" width="120" />
        <icon height="50" src="resources/ios/icon/icon-50.png" width="50" />
        <icon height="100" src="resources/ios/icon/icon-50@2x.png" width="100" />
        <icon height="60" src="resources/ios/icon/icon-60.png" width="60" />
        <icon height="120" src="resources/ios/icon/icon-60@2x.png" width="120" />
        <icon height="180" src="resources/ios/icon/icon-60@3x.png" width="180" />
        <icon height="72" src="resources/ios/icon/icon-72.png" width="72" />
        <icon height="144" src="resources/ios/icon/icon-72@2x.png" width="144" />
        <icon height="76" src="resources/ios/icon/icon-76.png" width="76" />
        <icon height="152" src="resources/ios/icon/icon-76@2x.png" width="152" />
        <icon height="167" src="resources/ios/icon/icon-83.5@2x.png" width="167" />
        <icon height="29" src="resources/ios/icon/icon-small.png" width="29" />
        <icon height="58" src="resources/ios/icon/icon-small@2x.png" width="58" />
        <icon height="87" src="resources/ios/icon/icon-small@3x.png" width="87" />
        <icon height="1024" src="resources/ios/icon/icon-1024.png" width="1024" />
        <splash height="1136" src="resources/ios/splash/Default-568h@2x~iphone.png" width="640" />
        <splash height="1334" src="resources/ios/splash/Default-667h.png" width="750" />
        <splash height="2208" src="resources/ios/splash/Default-736h.png" width="1242" />
        <splash height="1242" src="resources/ios/splash/Default-Landscape-736h.png" width="2208" />
        <splash height="1536" src="resources/ios/splash/Default-Landscape@2x~ipad.png" width="2048" />
        <splash height="2048" src="resources/ios/splash/Default-Landscape@~ipadpro.png" width="2732" />
        <splash height="768" src="resources/ios/splash/Default-Landscape~ipad.png" width="1024" />
        <splash height="2048" src="resources/ios/splash/Default-Portrait@2x~ipad.png" width="1536" />
        <splash height="2732" src="resources/ios/splash/Default-Portrait@~ipadpro.png" width="2048" />
        <splash height="1024" src="resources/ios/splash/Default-Portrait~ipad.png" width="768" />
        <splash height="960" src="resources/ios/splash/Default@2x~iphone.png" width="640" />
        <splash height="480" src="resources/ios/splash/Default~iphone.png" width="320" />
        <splash height="2732" src="resources/ios/splash/Default@2x~universal~anyany.png" width="2732" />
    </platform>
    <plugin name="cordova-plugin-whitelist" spec="1.3.3" />
    <plugin name="cordova-plugin-statusbar" spec="2.4.2" />
    <plugin name="cordova-plugin-device" spec="2.0.2" />
    <plugin name="cordova-plugin-splashscreen" spec="5.0.2" />
    <plugin name="cordova-plugin-ionic-webview" spec="^3.0.0" />
    <plugin name="cordova-plugin-ionic-keyboard" spec="^2.0.5" />
    <engine name="ios" spec="4.5.5" />
</widget>


================================================
FILE: src-app/e2e/protractor.conf.js
================================================
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './src/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: require('path').join(__dirname, './tsconfig.e2e.json')
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};


================================================
FILE: src-app/e2e/src/app.e2e-spec.ts
================================================
import { AppPage } from './app.po';

describe('new App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getPageTitle()).toContain('Tab One');
  });
});


================================================
FILE: src-app/e2e/src/app.po.ts
================================================
import { browser, by, element } from 'protractor';

export class AppPage {
  navigateTo() {
    return browser.get('/');
  }

  getPageTitle() {
    return element(by.css('ion-title')).getText();
  }
}


================================================
FILE: src-app/e2e/tsconfig.e2e.json
================================================
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "module": "commonjs",
    "target": "es5",
    "types": [
      "jasmine",
      "jasminewd2",
      "node"
    ]
  }
}


================================================
FILE: src-app/ionic.config.json
================================================
{
  "name": "src-app",
  "integrations": {
    "cordova": {}
  },
  "type": "angular"
}


================================================
FILE: src-app/package.json
================================================
{
  "name": "src-app",
  "version": "0.0.1",
  "author": "Ionic Framework",
  "homepage": "https://ionicframework.com/",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/common": "^7.2.2",
    "@angular/core": "^7.2.2",
    "@angular/forms": "^7.2.2",
    "@angular/http": "^7.2.2",
    "@angular/platform-browser": "^7.2.2",
    "@angular/platform-browser-dynamic": "^7.2.2",
    "@angular/router": "^7.2.2",
    "@ionic-native/core": "^5.0.0",
    "@ionic-native/splash-screen": "^5.0.0",
    "@ionic-native/status-bar": "^5.0.0",
    "@ionic/angular": "^4.11.10",
    "cordova-ios": "4.5.5",
    "cordova-plugin-device": "^2.0.2",
    "cordova-plugin-ionic-keyboard": "^2.1.3",
    "cordova-plugin-ionic-webview": "^3.1.2",
    "cordova-plugin-splashscreen": "^5.0.2",
    "cordova-plugin-statusbar": "^2.4.2",
    "cordova-plugin-whitelist": "^1.3.3",
    "core-js": "^2.5.4",
    "highcharts": "^7.0.3",
    "rxjs": "~6.3.3",
    "zone.js": "~0.8.29"
  },
  "devDependencies": {
    "@angular-devkit/architect": "~0.12.3",
    "@angular-devkit/build-angular": "^0.803.25",
    "@angular-devkit/core": "~7.2.3",
    "@angular-devkit/schematics": "~7.2.3",
    "@angular/cli": "~7.2.3",
    "@angular/compiler": "~7.2.2",
    "@angular/compiler-cli": "~7.2.2",
    "@angular/language-service": "~7.2.2",
    "@ionic/angular-toolkit": "~1.4.0",
    "@types/chart.js": "^2.7.45",
    "@types/jasmine": "~2.8.8",
    "@types/jasminewd2": "~2.0.3",
    "@types/node": "~10.12.0",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~2.99.1",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~3.1.4",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.1",
    "karma-jasmine": "~1.1.2",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.4.0",
    "ts-node": "~8.0.0",
    "tslint": "~5.12.0",
    "typescript": "~3.1.6"
  },
  "description": "An Ionic project",
  "cordova": {
    "plugins": {
      "cordova-plugin-whitelist": {},
      "cordova-plugin-statusbar": {},
      "cordova-plugin-device": {},
      "cordova-plugin-splashscreen": {},
      "cordova-plugin-ionic-webview": {},
      "cordova-plugin-ionic-keyboard": {}
    },
    "platforms": [
      "ios"
    ]
  }
}


================================================
FILE: src-app/resources/README.md
================================================
These are Cordova resources. You can replace icon.png and splash.png and run
`ionic cordova resources` to generate custom icons and splash screens for your
app. See `ionic cordova resources --help` for details.

Cordova reference documentation:

- Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html
- Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/


================================================
FILE: src-app/resources/icon.png.md5
================================================
3cb62ec167ca6dc5a30560cac2e8caf4

================================================
FILE: src-app/resources/splash.png.md5
================================================
7108a139271bfc1712907b3291a2b454

================================================
FILE: src-app/src/app/app-routing.module.ts
================================================
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', loadChildren: './tabs/tabs.module#TabsPageModule' },
];
@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}


================================================
FILE: src-app/src/app/app.component.html
================================================
<ion-app>
  <ion-router-outlet></ion-router-outlet>
</ion-app>


================================================
FILE: src-app/src/app/app.component.ts
================================================
import { Component } from '@angular/core';

import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar
  ) {
    this.initializeApp();
  }

  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
    });
  }
}


================================================
FILE: src-app/src/app/app.module.ts
================================================
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EnergyService } from './services/energy-service.service';
import { HttpClientModule } from '@angular/common/http';
import { DecimalPipe } from '@angular/common';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [
    StatusBar,
    SplashScreen,
    EnergyService,
    DecimalPipe,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}


================================================
FILE: src-app/src/app/components/components.module.ts
================================================
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoadingIndicatorComponent } from './loading-indicator/loading-indicator.component';
import { IonicModule } from '@ionic/angular';

@NgModule({
  declarations: [
    LoadingIndicatorComponent
  ],
  exports: [
    LoadingIndicatorComponent,
  ],
  imports: [
    CommonModule,
    IonicModule,
  ]
})
export class ComponentsModule { }


================================================
FILE: src-app/src/app/components/loading-indicator/loading-indicator.component.html
================================================
<div [class.hidden]="!show">
  <ion-progress-bar type="indeterminate" ></ion-progress-bar>
</div>

================================================
FILE: src-app/src/app/components/loading-indicator/loading-indicator.component.scss
================================================
div{
    // margin-bottom: -7px;

    &.hidden{
            opacity: 0;
    }
}

================================================
FILE: src-app/src/app/components/loading-indicator/loading-indicator.component.ts
================================================
import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-loading-indicator',
  templateUrl: './loading-indicator.component.html',
  styleUrls: ['./loading-indicator.component.scss'],
})
export class LoadingIndicatorComponent implements OnInit {
  @Input() show = false;

  constructor() { }

  ngOnInit() {}

}


================================================
FILE: src-app/src/app/services/energy-service.service.ts
================================================
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ToastController } from '@ionic/angular';
import { start } from 'repl';

@Injectable({
  providedIn: 'root'
})
export class EnergyService {

  /**
   * The URL to the main GraphQL API. This will be used to make all requests.
   */
  private BASE_URL = "*** YOUR GRAPHQL ENDPOINT HERE ***";

  private pendingRequests = [];

  constructor(private http: HttpClient, private toastCtrl: ToastController) { }

  public isLoading() {
    return this.pendingRequests.length !== 0;
  }

  public async getStatistics(): Promise<any>{

    // Calculate the start and ending dates
    const startDate = new Date();
		startDate.setDate(startDate.getDate() - 31);

    // Convert these to timestamps and make them whole (no floats)
		const start = Math.floor(startDate.getTime() / 1000);
    const end = Math.ceil(Date.now() / 1000);

    // Make the request
    const data = await this.makeGraphQLRequest(`
      query{
        usageData(startDate:${start}, endDate:${end}){
          timestamp,
          dayUse,
          nightUse
        }
      }
    `);

    console.log('Fetched stats:', data);
    return data;
  }

  public async getYearlyStats(): Promise<any>{
    const startDate = new Date();
    startDate.setDate(1);
    startDate.setMonth(0);
    startDate.setHours(0);
    startDate.setMinutes(0);

    const start = Math.floor(startDate.getTime() / 1000);
    const end = Math.ceil(Date.now() / 1000);

    const data = await this.makeGraphQLRequest(`
      query{
        usageData(startDate:${start}, endDate:${end}){
          timestamp,
          dayUse,
          nightUse,
        }
      }
    `);

    console.log('yearly stats', data);

    const beginDates = [];
    const outputData = [];

    for (let i = 0; i <= 11; i++){
      startDate.setMonth(i);

      beginDates.push(
        Math.floor(startDate.getTime() / 1000)
      );
    }

    for (let i = 0; i <= 11; i++){
      const readingsInMonth = data.data.usageData.filter(
        item => item.timestamp > beginDates[i] && item.timestamp < beginDates[i + 1]
      );

      if (readingsInMonth.length === 0) {
        outputData.push(0);
        continue;
      }

      console.log(readingsInMonth);
      const total = readingsInMonth.reduce((total, currentVal) => total + currentVal.dayUse + currentVal.nightUse);
      outputData.push(total);
    }

    console.log(outputData);
  }

  public async getHomePageStatistics(all?: boolean): Promise<any>{
    const timestamp = Math.floor(Date.now() / 1000 - 60);

    let additionalQueries = '';
    if (all === true) {
      additionalQueries = `
      stats{
          always_on
          today_so_far
      }`
    }

    const data = await this.makeGraphQLRequest(`
      query{
        realtime(sinceTimestamp: ${timestamp}){
          timestamp
          reading
        },
        ${additionalQueries}
      }
    `);

    console.log('realtime', data);

    return data;
  }

  public async getReadings(since?: number): Promise<any> {
    if (!since) {
      const date = new Date();
      date.setHours(date.getHours() - 6);
      since = date.getTime();
    }

    console.log('since', since);

    const data = await this.makeGraphQLRequest(`
      query{
        realtime(sinceTimestamp: ${Math.floor(since / 1000)}){
          timestamp, reading
        }
      }`
    );

    console.log('readings', data.data.realtime);
    return data.data.realtime;
  }


  /**
   * Makes a request to the GraphQL API and returns a promise that should
   * be awaited.
   *
   * @param query The GraphQL query that should be executed
   */
  private async makeGraphQLRequest(query: string): Promise<any> {

    const req = this.http.post(
      this.BASE_URL,
      query
    ).toPromise();

    // Push the request into the array so we can show load spinners
    // in the application at various places.
    this.pendingRequests.push(req);

    req
      .then((data) => {
        return data;
      })
      .catch(async (err) => {
        console.error('Error making GraphQL request', err);

        const toast = await this.toastCtrl.create({
          message: 'Could not fetch data from server. Try again later.',
          duration: 5000,
          showCloseButton: true,
          position: 'top',
          color: 'dark'
        });

        toast.present();
      })
      .finally(() => {

        // After any request, regardless of wether it was successfull
        // or not, we have to remove it from the pendingRequests array
        // so that all loading spinners dissapear in the UI.
        this.pendingRequests.splice(
          this.pendingRequests.indexOf(req),
          1
        );
      });

    // Return the pending requests so other methods can await it.
    return req;
  }
}

export interface MainStats {
  data: any;
}


================================================
FILE: src-app/src/app/tab-home/tab-home.module.ts
================================================
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { IonicModule } from '@ionic/angular';

import { TabHomePage } from './tab-home.page';
import { ComponentsModule } from '../components/components.module';

const routes: Routes = [
  {
    path: '',
    component: TabHomePage
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes),
    ComponentsModule,
  ],
  declarations: [TabHomePage]
})
export class TabHomeModule {}


================================================
FILE: src-app/src/app/tab-home/tab-home.page.html
================================================
<ion-header>
    <ion-toolbar>
      <ion-buttons slot="end">
        <ion-spinner *ngIf="false"></ion-spinner>
      </ion-buttons>
      <ion-title>Home</ion-title>
    </ion-toolbar>
  </ion-header>

  <app-loading-indicator [show]="energyService.isLoading()"></app-loading-indicator>

  <ion-content padding>


    <div class="center">
        <div class="container">
            <div class="circle current" [routerLink]="['/tabs/readings']">
              <p>
                <span class="value" *ngIf="stats.current !== '?'">{{stats.current | number:'1.0-0' }}W</span>
                <span class="value" *ngIf="stats.current === '?'">&#8961; W</span>
                <span class="label">real-time</span>
              </p>
            </div>
            <div class="circle alwayson">
                <p>
                    <span class="value" *ngIf="stats.always_on !== '?'">{{stats.always_on | number:'1.0-0' }} W</span>
                    <span class="value" *ngIf="stats.always_on === '?'">&#8961; W</span>
                    <span class="label">always-on</span>
                  </p>
            </div>
            <div class="circle today" [routerLink]="['/tabs/statistics']">
                <p>
                    <span class="value" *ngIf="stats.today_so_far !== '?'">{{stats.today_so_far | number:'1.0-1'}} kWh</span>
                    <span class="value" *ngIf="stats.today_so_far === '?'">&#8961; kWh</span>
                    <span class="label">today so far</span>
                  </p>
            </div>
        </div>
    </div>



  </ion-content>


================================================
FILE: src-app/src/app/tab-home/tab-home.page.scss
================================================
.center{
    display: flex;
    justify-content: center;
    flex-direction: column;
    text-align: center;
    height: 100%;
}
.container{
    position: relative;
    height: 455px;
    width: 340px;
    align-self: center;

    .circle{

        border-radius: 100%;
        background-blend-mode: color-burn;

        justify-content: center;
        align-items: center;
        border-radius: 100%;
        text-align: center;
        display: flex;
        flex-wrap: wrap;

        color: #fff;

        .label, .value{
            display: block;
        }
        .label{
            font-weight: 100;
            font-size: 19px;
            text-transform: lowercase;
        }

        .value{
            font-weight: 500;
        }

        &.current{
            background-color: rgba(#1D6CFF, 0.9);
            width: 300px;
            height: 300px;
            margin: 0 auto;

            .value{
                font-size: 60px;
            }
        }

        &.alwayson{
            background-color: rgba(#8440FF, 0.9);
            width: 152px;
            height: 152px;

            position:absolute;
            top: 250px;
            left: 0;

            .value{
                font-size: 30px;
            }
        }

        &.today{
            background-color: rgba(#C324FF, 0.9);
            width: 210px;
            height: 210px;

            position:absolute;
            top: 244px;
            right: 0;

            .value{
                font-size: 35px;
            }
        }
    }
}


================================================
FILE: src-app/src/app/tab-home/tab-home.page.ts
================================================
import { Component, OnInit } from '@angular/core';
import { EnergyService } from '../services/energy-service.service';

@Component({
  selector: 'app-tab-home',
  templateUrl: './tab-home.page.html',
  styleUrls: ['./tab-home.page.scss'],
})
export class TabHomePage {

  public stats = {
    current: '?',
    always_on: '?',
    today_so_far: '?',
  }


  private intervalTimer = null;
  private scheduledTimeout = null;
  private lastUpdate = null;

  constructor(public energyService: EnergyService) { }

  /**
   * Called when the page has been loaded for the very first time. Does not
   * re-fire when the page is opened a second time.
   */
  async ionViewDidLoad() {
    // When the app resumes from the background we should see if we have to
    // refresh the data or not. Same logic as when the app first boots.
    document.addEventListener("resume", async () => {
      await this.refreshDataIfNeeded();
    }, false);
  }

  /**
   * Called everytime the screen enters the view, regardless of wether it has
   * been loaded before or not.
   */
  async ionViewWillEnter() {
    await this.refreshDataIfNeeded();
  }

  /**
   * Called when the user leaves the main screen. Here we should cancel the
   * timer that automatically refreshes the screen.
   */
  async ionViewDidLeave() {
    clearInterval(this.intervalTimer);
  }

  /**
   * Determns what should happen with the background refresh operations
   * (cancel them or schedule new ones). This function should be called
   * everytime the page is opened or when the app is resumed.
   */
  private async refreshDataIfNeeded() {
    // Define how long we should wait in between refreshed (30 seconds)
    const minimumWaitTime = 30 * 1000;

    if (this.intervalTimer !== null) {
      clearInterval(this.intervalTimer);
    }

    // If we haven't updated before, we can do it straight away and not
    // care about anything else.
    if (this.lastUpdate === null) {
      await this.fetchData(true);
      this.scheduleTimer();
      return;
    }

    const updateTimeDelta = Date.now() - this.lastUpdate;

    // If we updated before but it was more then 30 seconds ago, refresh it
    // now again and don't continue executing code.
    if (updateTimeDelta > minimumWaitTime) {
      await this.fetchData();
      this.scheduleTimer();
      return;
    }

    // If we get here it means we updates less then 30 seconds but the user
    // has reopened the dashboard. Calculate how long we must wait to reach 30
    // seconds and then schedule a timer to do the refresh.
    this.scheduleTimer(minimumWaitTime - updateTimeDelta);
    return;
  }

  /**
   * Schedules a refresh action after a given delay of waitMs milliseconds.
   * Also cancels any pending timeout should there be one.
   * @param waitMs Time to wait in milliseconds
   */
  private scheduleTimer(waitMs = 0) {
    if (this.scheduledTimeout) {
      clearTimeout(this.scheduledTimeout);
    }

    this.scheduledTimeout = setTimeout(async () => {
      this.intervalTimer = setInterval(async () => {
        await this.fetchData();
      }, 30 * 1000);
    }, waitMs);

  }

  /**
   * Actually fetches data from the backend. Should not be called more then
   * once every 30 seconds because there are no new datapoints anyway.
   *
   * @param all Wether or not to fetch the "always_on" and "today_so_far"
   *            Best to set these to false if not needed (better performance)
   */
  private async fetchData(all?: boolean) {
    const realtime = await this.energyService.getHomePageStatistics(all);

    this.stats.current = realtime.data.realtime[realtime.data.realtime.length - 1].reading;

    if (all === true) {
      this.stats.always_on = realtime.data.stats.always_on;
      this.stats.today_so_far = realtime.data.stats.today_so_far;
    }

    this.lastUpdate = Date.now();
  }
}


================================================
FILE: src-app/src/app/tab-readings/tab-readings.module.ts
================================================
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { IonicModule } from '@ionic/angular';

import { TabReadingsPage } from './tab-readings.page';
import { ComponentsModule } from '../components/components.module';

const routes: Routes = [
  {
    path: '',
    component: TabReadingsPage
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes),
    ComponentsModule
  ],
  declarations: [TabReadingsPage]
})
export class TabReadingsModule {}


================================================
FILE: src-app/src/app/tab-readings/tab-readings.page.html
================================================
<ion-header>
  <ion-toolbar>
    <ion-title>Readings</ion-title>
  </ion-toolbar>

  <ion-toolbar class="datePicker">
    <ion-icon name="arrow-back" slot="start" (click)="goToYesterday()"></ion-icon>

    <ion-title>
      <ion-datetime display-format="MMM DD, YYYY" [(ngModel)]="selectedDate" (ionChange)="dateChanged()"></ion-datetime>
    </ion-title>

    <ion-icon name="arrow-forward" slot="end" (click)="goToTomorrow()" *ngIf="showForwardArrow"></ion-icon>
  </ion-toolbar>
</ion-header>

<app-loading-indicator [show]="energyService.isLoading()"></app-loading-indicator>

<ion-content [scrollX]="false" [scrollY]="false">
  <div class="graph-wrapper">
    <div #chart></div>
  </div>
</ion-content>

================================================
FILE: src-app/src/app/tab-readings/tab-readings.page.scss
================================================
.datePicker{
    ion-title{
        font-weight: normal !important;
    }
}

.graph-wrapper{
    display: flex;
    flex-direction: column;
    height: 100%;

    div{
        width: 100%;
        height: 100%;
    }
}

================================================
FILE: src-app/src/app/tab-readings/tab-readings.page.ts
================================================
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
import { EnergyService } from '../services/energy-service.service';
import * as Highcharts from 'highcharts';
import { ChartDefaults } from '../utils/chart-defaults';

@Component({
  selector: 'app-tab-readings',
  templateUrl: './tab-readings.page.html',
  styleUrls: ['./tab-readings.page.scss'],
})
export class TabReadingsPage {
  public selectedDate = new Date().toISOString();
  public todaysDate = new Date().toISOString();
  public showForwardArrow = false;

  @ViewChild('chart') private mainChartRef: ElementRef;

  // The JS timestamp of when we last updated the data in the chart
  private lastUpdated = null;

  // Data that is used to plot the chart
  private chartData = [];

  constructor(public energyService: EnergyService) { }

  public async ionViewWillEnter() {
    // If we don't have any data in memory, go out and fetch them
    if (this.chartData.length === 0) {
      await this.refreshReadings();
      return;
    }

    // If the data we have is older then 30 minutes, refresh them!
    if (this.lastUpdated < Date.now() - 30*60*1000) {
      await this.refreshReadings();
      return;
    }
  }

  /**
   * Refreshes the data in the graph. If we already have downloaded data before,
   * it only fetches new data, after the lastUpdated timestamp.
   */
  private async refreshReadings() {
    const data = await this.energyService.getReadings(this.lastUpdated);
    this.lastUpdated = Date.now();

    const filtered = data.filter(item => item.timestamp % 30 === 0);
    this.chartData = this.chartData.concat(filtered);

    this.renderChart();
  }

  /**
   * Called when the date was changed through the picker or with
   * the arrows next to it.
   */
  public dateChanged() {

    // Show the forward arrow when the selected date is not equal to
    // todays date
    this.showForwardArrow =
      this.selectedDate.substring(0, 10) !== this.todaysDate.substring(0, 10);
  }

  /**
   * Called when the user clicks on the forward arrow
   */
  public goToTomorrow() {
    this.manipulateSelectedDateBy(1);
  }

  /**
   * Called when the user clicks on the backwards arrow
   */
  public goToYesterday() {
    this.manipulateSelectedDateBy(-1);
  }

  /**
   * Responsible for fetching the required data from the API and
   * storing it in the private "chartData" field. Also calls the
   * "renderChart" function to update if necessary.
   */
  private fetchDataForDate() {

  }

  private renderChart() {
    const data = this.chartData.map(item => [item.timestamp * 1000, item.reading]);
    const values = data.map(item => item[1]);

    Highcharts.chart(this.mainChartRef.nativeElement, {
      ...ChartDefaults,
      chart: {
        type: "line",
        panning: true,
        pinchType: 'x',
        events: {
          load() {
            this.xAxis[0].setExtremes(Date.now() - 4 * 60 * 60 * 1000, Date.now())
          }
        }
      },
      xAxis: {
        type: 'datetime',
      },
      yAxis: {
        max: Math.max(...values),
        title: {
          text: null,
        }
      },
      series: [{
        type: null,
        name: 'Usage',
        color: '#8440FF',
        data: data
      }],
      tooltip: {
        valueSuffix: 'W',
        followTouchMove: false,
      },
    });
  }

  /**
   * Takes the currently selected date and increments it by a given
   * amount of days. If given a negative number it goes backwards.
   */
  private manipulateSelectedDateBy(amount: number) {
    const date = new Date(this.selectedDate);
    date.setDate(date.getDate() + amount);
    this.selectedDate = date.toISOString();
  }
}


================================================
FILE: src-app/src/app/tab-statistics/tab-statistics.module.ts
================================================
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router';

import { IonicModule } from '@ionic/angular';

import { TabStatisticsPage } from './tab-statistics.page';
import { ComponentsModule } from '../components/components.module';

const routes: Routes = [
  {
    path: '',
    component: TabStatisticsPage
  }
];

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    RouterModule.forChild(routes),
    ComponentsModule,
  ],
  declarations: [TabStatisticsPage]
})
export class TabStatisticsModule {}


================================================
FILE: src-app/src/app/tab-statistics/tab-statistics.page.html
================================================
<ion-header>
  <ion-toolbar>
    <ion-title>Statistics</ion-title>
    <ion-spinner *ngIf="energyService.isLoading()"></ion-spinner>
  </ion-toolbar>

  <ion-toolbar>
      <ion-segment value="30days" (ionChange)="segmentChanged($event)">
          <ion-segment-button value="30days">
              <ion-label>Last 30 days</ion-label>
          </ion-segment-button>
          <ion-segment-button value="12months">
              <ion-label>Last 12 months</ion-label>
          </ion-segment-button>
        </ion-segment>
  </ion-toolbar>
</ion-header>

<app-loading-indicator [show]="energyService.isLoading()"></app-loading-indicator>

<ion-content padding [class.hidden]="activeSegmentControl !== '30days'">
      <ion-card-subtitle>Breakdown per day</ion-card-subtitle>
      <div #usageChart class="chart height260"></div>

      <br>

      <ion-card-subtitle>Day vs. Night</ion-card-subtitle>
      <div #dayVsNight class="chart"></div>

      <br><br>

      <ion-card-subtitle>More</ion-card-subtitle>
      <ion-list>
          <ion-item>
            <ion-label>Daily average</ion-label>
            <ion-note slot="end">{{ moreStats.daily_average | number:'1.0-2' }} kWh</ion-note>
          </ion-item>
          <ion-item>
            <ion-label>Total last 30 days</ion-label>
            <ion-note slot="end">{{ moreStats.total_30days | number:'1.0-2' }} kWh</ion-note>
          </ion-item>
        </ion-list>
</ion-content>

<ion-content padding [class.hidden]="activeSegmentControl !== '12months'">
  <div class="full-graph-wrapper">
    <div #yearlyChart class="chart fullheight"></div>
  </div>
</ion-content>

================================================
FILE: src-app/src/app/tab-statistics/tab-statistics.page.scss
================================================
$chartHeight: 180px;

div.chart{
    width: 100%;
    height: $chartHeight;
    max-height: $chartHeight;

    &.height260{
        height: 260px;
        max-height: 260px;
    }

    &.fullheight{
        height: 100%;
        max-height: none;
        width: 100%;
    }
}

ion-content{
    &.hidden{
        display:none;
    }
}

ion-item{
    --padding-start: 0;
}

ion-segment-button{
    min-width: 120px;
}

.full-graph-wrapper{
    display: flex;
    flex-direction: column;
    height: 100%;
}

================================================
FILE: src-app/src/app/tab-statistics/tab-statistics.page.ts
================================================
import { Component, OnInit, ElementRef, ViewChild, AfterViewInit } from '@angular/core';
import { EnergyService } from '../services/energy-service.service';
import * as Highcharts from 'highcharts';
import { DecimalPipe } from '@angular/common';
import { ChartDefaults } from '../utils/chart-defaults';

@Component({
  selector: 'app-tab-statistics',
  templateUrl: './tab-statistics.page.html',
  styleUrls: ['./tab-statistics.page.scss'],
})
export class TabStatisticsPage implements OnInit, AfterViewInit {

  @ViewChild('usageChart') private usageChartRef: ElementRef;
  @ViewChild('dayVsNight') private dayVsNightChartRef: ElementRef;
  @ViewChild('yearlyChart') private yearlyChartRef: ElementRef;

  public activeSegmentControl = "30days";

  public moreStats = {
    daily_average: null,
    total_30days: null,
  }

  // Keeps track of which segments we already rendered and did all the
  // network requests for. This to prevent multiple calls to the backend.
  private segmentsRendered = {
    '30days': false,
    '12months': false,
  }

  constructor(public energyService: EnergyService, private decimalPipe: DecimalPipe) { }

  ngOnInit() {
  }

  async ngAfterViewInit() {
    await this.segment30daysWasOpened();
  }

  /**
   * Called whenever the user switches to another segment. Responsible
   * for calling the correct function based on this event.
   */
  public segmentChanged(event) {
    this.activeSegmentControl = event.detail.value;

    if (this.activeSegmentControl === '30days') {
      this.segment30daysWasOpened();
    }

    if (this.activeSegmentControl === '12months') {
      this.segmentYearlyOverviewWasOpened();
    }
  }

  /**
   * Called when the user wants to open the "Last 30 days" summary
   * segment. It fetches the required data from the server and renders
   * a few charts on the screens asynchrounously.
   */
  private async segment30daysWasOpened() {
    // If we already rendered the charts we shouldn't do it again!
    if (this.segmentsRendered["30days"]) {
      return;
    }

    // Fetch the data we need
    const data = await this.energyService.getStatistics();

    // Calculate total day/night usage
    let totalDay = 0;
    let totalNight = 0;

    for (const entry of data.data.usageData) {
      totalDay += entry.dayUse;
      totalNight += entry.nightUse;
    }

    // Simultanuously draw all our charts on screen
    await Promise.all([
      this.drawDayVsNightChart(totalDay, totalNight),
      this.drawDailyUsageChart(data),
      this.calculateMoreStats(data),
    ]);

    this.segmentsRendered["30days"] = true;
  }

  private async segmentYearlyOverviewWasOpened() {
    if (this.segmentsRendered['12months']) {
      return;
    }

    setTimeout(async () => {
      await this.drawYearlyOverviewChart();
      this.segmentsRendered["12months"] = true;
    }, 0);
  }

  private formatTimestampForChartAxis(rawTimestamp){
    const date = new Date(rawTimestamp * 1000);
    const months = ["Jan", 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    return date.getDate() + ' ' + months[date.getMonth()];
  }

  /**
   * Calculate additional statistics that are displayed as text. This is based on
   * the data given to the function (return data from the API).
   */
  private async calculateMoreStats(data) {
    const dailyTotals = data.data.usageData.map(item => item.dayUse + item.nightUse);

    const total = dailyTotals.reduce((a, b) => a + b);

    this.moreStats.daily_average = total / dailyTotals.length;
    this.moreStats.total_30days = total;
  }

  private async drawYearlyOverviewChart() {
    await this.energyService.getYearlyStats();

    Highcharts.chart(this.yearlyChartRef.nativeElement, {
      ...ChartDefaults,
      chart: {
          type: 'bar'
      },
      xAxis: {
        categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'],
      },
      yAxis: {
        visible: false,
      },
      plotOptions: {
        series: {
            color: '#8440FF',
            stacking: 'normal'
          }
      },
      series: [
        {
          type: 'bar',
          name: 'All months',
          data: [5, 3, 4, 7, 2, 0, 0, 0, 0, 0, 0, 0]
        },
      ],
    });
  }

  /**
   * Draw the pie chart that shows the difference between day and night usage.
   */
  private async drawDayVsNightChart(dayUsage: number, nightUsage: number) {
    const self = this;

    Highcharts.chart(this.dayVsNightChartRef.nativeElement, {
      ...ChartDefaults,
      chart: {
        type: 'pie'
      },
      plotOptions:{
        pie: {
          colors: ['#534B62', '#8440FF'],
          dataLabels:{
            enabled: true,
            distance: -30,
            style:{
              color: 'white'
            }
          }
        }
      },
      tooltip: {
        formatter: function(){
          return `<b>${this.point.name} usage:</b>
                  <br>${self.decimalPipe.transform(this.y, '1.1-2')} kWh`;
        }
      },
      series: [{
        type: 'pie',
        name: 'Day vs Night',
        data: [
          ['Night', nightUsage],
          ['Day', dayUsage],
        ]
      }]
    });
  }

  private async drawDailyUsageChart(data) {
    // Reference to our page instance for use inside the formatter
    // of Highcharts (arrow functions not allowed)
    const self = this;

    Highcharts.chart(this.usageChartRef.nativeElement, {
      ...ChartDefaults,
      chart: {
        type: 'column',
      },
      xAxis:{
        categories: data.data.usageData.map(el => this.formatTimestampForChartAxis(el.timestamp)),
      },
      yAxis:{
        allowDecimals: false,
        title:{
          text: null,
        }
      },
      tooltip: {
        formatter: function(){
          return `<b>${this.x} - ${this.series.name} usage:</b>
                  <br> ${self.decimalPipe.transform(this.y, '1.1-2')} kWh`;
        }
      },
      plotOptions: {
        column: {
          stacking: 'normal',
          pointPadding: 0,
          borderWidth: 0
        }
      },
      series: [
        {
          type: 'column',
          name: 'Night',
          color: '#534B62',
          data: data.data.usageData.map(el => el.nightUse),
        },
        {
          type: 'column',
          name: 'Day',
          color: '#8440FF',
          data: data.data.usageData.map(el => el.dayUse),
        },
      ]
    });
  }
}


================================================
FILE: src-app/src/app/tabs/tabs.module.ts
================================================
import { IonicModule } from '@ionic/angular';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { TabsPageRoutingModule } from './tabs.router.module';

import { TabsPage } from './tabs.page';

@NgModule({
  imports: [
    IonicModule,
    CommonModule,
    FormsModule,
    TabsPageRoutingModule
  ],
  declarations: [TabsPage]
})
export class TabsPageModule {}


================================================
FILE: src-app/src/app/tabs/tabs.page.html
================================================
<ion-tabs>

  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home">
      <ion-icon name="flash"></ion-icon>
      <ion-label>Home</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="readings">
      <ion-icon name="pulse"></ion-icon>
      <ion-label>Readings</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="statistics">
      <ion-icon name="stats"></ion-icon>
      <ion-label>Statistics</ion-label>
    </ion-tab-button>

    <!-- <ion-tab-button tab="tab3">
      <ion-icon name="more"></ion-icon>
      <ion-label>More</ion-label>
    </ion-tab-button> -->
  </ion-tab-bar>

</ion-tabs>


================================================
FILE: src-app/src/app/tabs/tabs.page.scss
================================================



================================================
FILE: src-app/src/app/tabs/tabs.page.ts
================================================
import { Component } from '@angular/core';

@Component({
  selector: 'app-tabs',
  templateUrl: 'tabs.page.html',
  styleUrls: ['tabs.page.scss']
})
export class TabsPage {}


================================================
FILE: src-app/src/app/tabs/tabs.router.module.ts
================================================
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';

const routes: Routes = [
  {
    path: 'tabs',
    component: TabsPage,
    children: [
      {
        path: 'home',
        children: [
          {
            path: '',
            loadChildren: '../tab-home/tab-home.module#TabHomeModule'
          }
        ]
      },
      {
        path: 'readings',
        children: [
          {
            path: '',
            loadChildren: '../tab-readings/tab-readings.module#TabReadingsModule'
          }
        ]
      },
      {
        path: 'statistics',
        children: [
          {
            path: '',
            loadChildren: '../tab-statistics/tab-statistics.module#TabStatisticsModule'
          }
        ]
      },
      {
        path: '',
        redirectTo: '/tabs/home',
        pathMatch: 'full'
      }
    ]
  },
  {
    path: '',
    redirectTo: '/tabs/home',
    pathMatch: 'full'
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(routes)
  ],
  exports: [RouterModule]
})
export class TabsPageRoutingModule {}


================================================
FILE: src-app/src/app/utils/chart-defaults.ts
================================================
export const ChartDefaults = {
    title: {
        text: null,
    },
    yAxis: {
        min: 0,
    },
    legend: {
        enabled: false,
    },
    credits: {
        enabled: false,
    },
}

================================================
FILE: src-app/src/environments/environment.prod.ts
================================================
export const environment = {
  production: true
};


================================================
FILE: src-app/src/environments/environment.ts
================================================
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.

export const environment = {
  production: false
};

/*
 * For easier debugging in development mode, you can import the following file
 * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
 *
 * This import should be commented out in production mode because it will have a negative impact
 * on performance if an error is thrown.
 */
// import 'zone.js/dist/zone-error';  // Included with Angular CLI.


================================================
FILE: src-app/src/global.scss
================================================
// http://ionicframework.com/docs/theming/
@import '~@ionic/angular/css/core.css';
@import '~@ionic/angular/css/normalize.css';
@import '~@ionic/angular/css/structure.css';
@import '~@ionic/angular/css/typography.css';

@import '~@ionic/angular/css/padding.css';
@import '~@ionic/angular/css/float-elements.css';
@import '~@ionic/angular/css/text-alignment.css';
@import '~@ionic/angular/css/text-transformation.css';
@import '~@ionic/angular/css/flex-utils.css';


================================================
FILE: src-app/src/index.html
================================================
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <title>Home Energy</title>

  <base href="/" />

  <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" />
  <meta name="format-detection" content="telephone=no" />
  <meta name="msapplication-tap-highlight" content="no" />

  <link rel="icon" type="image/png" href="assets/icon/favicon.png" />

  <!-- add to homescreen for ios -->
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
</head>

<body>
  <app-root></app-root>
</body>

</html>


================================================
FILE: src-app/src/karma.conf.js
================================================
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, '../coverage'),
      reports: ['html', 'lcovonly', 'text-summary'],
      fixWebpackSourcePaths: true
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false
  });
};


================================================
FILE: src-app/src/main.ts
================================================
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.log(err));


================================================
FILE: src-app/src/polyfills.ts
================================================
/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/guide/browser-support
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills.
 *  This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot
 */

// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install --save classlist.js`.

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';

/**
 * Web Animations `@angular/platform-browser/animations`
 * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
 * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
 */
// import 'web-animations-js';  // Run `npm install --save web-animations-js`.

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 * because those flags need to be set before `zone.js` being loaded, and webpack
 * will put import in the top of bundle, so user need to create a separate file
 * in this directory (for example: zone-flags.ts), and put the following flags
 * into that file, and then add the following code before importing zone.js.
 * import './zone-flags.ts';
 *
 * The flags allowed in zone-flags.ts are listed here.
 *
 * The following flags will work for all browsers.
 *
 * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
 *
 *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
 *  with the following flag, it will bypass `zone.js` patch for IE/Edge
 *
 *  (window as any).__Zone_enable_cross_context_check = true;
 *
 */

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import 'zone.js/dist/zone';  // Included with Angular CLI.


/***************************************************************************************************
 * APPLICATION IMPORTS
 */


================================================
FILE: src-app/src/test.ts
================================================
// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);


================================================
FILE: src-app/src/theme/variables.scss
================================================
// Ionic Variables and Theming. For more info, please see:
// http://ionicframework.com/docs/theming/

/** Ionic CSS Variables **/
:root {
  /** primary **/
  --ion-color-primary: #8440FF;
  --ion-color-primary-rgb: 56, 128, 255;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #3171e0;
  --ion-color-primary-tint: #4c8dff;

  /** secondary **/
  --ion-color-secondary: #0cd1e8;
  --ion-color-secondary-rgb: 12, 209, 232;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255, 255, 255;
  --ion-color-secondary-shade: #0bb8cc;
  --ion-color-secondary-tint: #24d6ea;

  /** tertiary **/
  --ion-color-tertiary: #7044ff;
  --ion-color-tertiary-rgb: 112, 68, 255;
  --ion-color-tertiary-contrast: #ffffff;
  --ion-color-tertiary-contrast-rgb: 255, 255, 255;
  --ion-color-tertiary-shade: #633ce0;
  --ion-color-tertiary-tint: #7e57ff;

  /** success **/
  --ion-color-success: #10dc60;
  --ion-color-success-rgb: 16, 220, 96;
  --ion-color-success-contrast: #ffffff;
  --ion-color-success-contrast-rgb: 255, 255, 255;
  --ion-color-success-shade: #0ec254;
  --ion-color-success-tint: #28e070;

  /** warning **/
  --ion-color-warning: #ffce00;
  --ion-color-warning-rgb: 255, 206, 0;
  --ion-color-warning-contrast: #ffffff;
  --ion-color-warning-contrast-rgb: 255, 255, 255;
  --ion-color-warning-shade: #e0b500;
  --ion-color-warning-tint: #ffd31a;

  /** danger **/
  --ion-color-danger: #f04141;
  --ion-color-danger-rgb: 245, 61, 61;
  --ion-color-danger-contrast: #ffffff;
  --ion-color-danger-contrast-rgb: 255, 255, 255;
  --ion-color-danger-shade: #d33939;
  --ion-color-danger-tint: #f25454;

  /** dark **/
  --ion-color-dark: #222428;
  --ion-color-dark-rgb: 34, 34, 34;
  --ion-color-dark-contrast: #ffffff;
  --ion-color-dark-contrast-rgb: 255, 255, 255;
  --ion-color-dark-shade: #1e2023;
  --ion-color-dark-tint: #383a3e;

  /** medium **/
  --ion-color-medium: #989aa2;
  --ion-color-medium-rgb: 152, 154, 162;
  --ion-color-medium-contrast: #ffffff;
  --ion-color-medium-contrast-rgb: 255, 255, 255;
  --ion-color-medium-shade: #86888f;
  --ion-color-medium-tint: #a2a4ab;

  /** light **/
  --ion-color-light: #f4f5f8;
  --ion-color-light-rgb: 244, 244, 244;
  --ion-color-light-contrast: #000000;
  --ion-color-light-contrast-rgb: 0, 0, 0;
  --ion-color-light-shade: #d7d8da;
  --ion-color-light-tint: #f5f6f9;
}


================================================
FILE: src-app/src/tsconfig.app.json
================================================
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ]
}


================================================
FILE: src-app/src/tsconfig.spec.json
================================================
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/spec",
    "types": [
      "jasmine",
      "node"
    ]
  },
  "files": [
    "test.ts",
    "polyfills.ts"
  ],
  "include": [
    "**/*.spec.ts",
    "**/*.d.ts"
  ]
}


================================================
FILE: src-app/tsconfig.json
================================================
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "module": "es2015",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2018",
      "dom"
    ]
  }
}


================================================
FILE: src-app/tslint.json
================================================
{
  "rulesDirectory": [
    "codelyzer"
  ],
  "rules": {
    "arrow-return-shorthand": true,
    "callable-types": true,
    "class-name": true,
    "comment-format": [
      true,
      "check-space"
    ],
    "curly": true,
    "deprecation": {
      "severity": "warn"
    },
    "eofline": true,
    "forin": true,
    "import-blacklist": [
      true,
      "rxjs/Rx"
    ],
    "import-spacing": true,
    "indent": [
      true,
      "spaces"
    ],
    "interface-over-type-literal": true,
    "label-position": true,
    "max-line-length": [
      true,
      140
    ],
    "member-access": false,
    "member-ordering": [
      true,
      {
        "order": [
          "static-field",
          "instance-field",
          "static-method",
          "instance-method"
        ]
      }
    ],
    "no-arg": true,
    "no-bitwise": true,
    "no-console": [
      true,
      "debug",
      "info",
      "time",
      "timeEnd",
      "trace"
    ],
    "no-construct": true,
    "no-debugger": true,
    "no-duplicate-super": true,
    "no-empty": false,
    "no-empty-interface": true,
    "no-eval": true,
    "no-inferrable-types": [
      true,
      "ignore-params"
    ],
    "no-misused-new": true,
    "no-non-null-assertion": true,
    "no-redundant-jsdoc": true,
    "no-shadowed-variable": true,
    "no-string-literal": false,
    "no-string-throw": true,
    "no-switch-case-fall-through": true,
    "no-trailing-whitespace": true,
    "no-unnecessary-initializer": true,
    "no-unused-expression": true,
    "no-use-before-declare": true,
    "no-var-keyword": true,
    "object-literal-sort-keys": false,
    "one-line": [
      true,
      "check-open-brace",
      "check-catch",
      "check-else",
      "check-whitespace"
    ],
    "prefer-const": true,
    "quotemark": [
      true,
      "single"
    ],
    "radix": true,
    "semicolon": [
      true,
      "always"
    ],
    "triple-equals": [
      true,
      "allow-null-check"
    ],
    "typedef-whitespace": [
      true,
      {
        "call-signature": "nospace",
        "index-signature": "nospace",
        "parameter": "nospace",
        "property-declaration": "nospace",
        "variable-declaration": "nospace"
      }
    ],
    "unified-signatures": true,
    "variable-name": false,
    "whitespace": [
      true,
      "check-branch",
      "check-decl",
      "check-operator",
      "check-separator",
      "check-type"
    ],
    "no-output-on-prefix": true,
    "use-input-property-decorator": true,
    "use-output-property-decorator": true,
    "use-host-property-decorator": true,
    "no-input-rename": true,
    "no-output-rename": true,
    "use-life-cycle-interface": true,
    "use-pipe-transform-interface": true,
    "directive-class-suffix": true
  }
}


================================================
FILE: src-aws/core/aws-connections.js
================================================
const AWS = require("aws-sdk");
module.exports.dynamoDocClient = new AWS.DynamoDB.DocumentClient({ region: "eu-west-1" });
module.exports.s3 = new AWS.S3();


================================================
FILE: src-aws/core/config.js
================================================
module.exports.config = {
	deviceName: 'xd-home-energy-monitor-2',
	
	dynamoDb: {
		table: process.env.DYNAMO_DB_TABLE,
	},

	s3: {
		bucket: process.env.S3_STORAGE_BUCKET
	}
};

================================================
FILE: src-aws/core/helpers/CalculateKwh.js
================================================
/**
 * Calculates how many kWh has been used in the given dataset.
 * Returns an object with two fields: "day" and "night" to
 * know how much was used under which tarif.
 *
 * Used to archive these statistics on a daily basis to Dynamo
 * and to show a counter in the front-end.
 *
 * Input format:
 * 	[
 * 		[timestamp, wattage],
 * 		[timestamp, wattage],
 * 		...
 * 	]
 */
module.exports.calculateKWH = function (dataset) {
	const { isNightTarif } = require('./IsNightTarif');

	const output = {
		day: 0,
		night: 0,
	};

	for(let i = 0; i < dataset.length-1; i++){
		const current = dataset[i];
		const next = dataset[i+1];

		// Seconds between the two measurements
		const seconds =
			(next[0].getTime() - current[0].getTime()) / 1000;

		// Kilowatts used between those points
		const kWh = (current[1] * seconds * (1/(60*60))) / 1000;

		if(isNightTarif(current[0])){
			output.night += kWh;
		}else{
			output.day += kWh;
		}
	}

	return output;
}

================================================
FILE: src-aws/core/helpers/CalculateKwh.test.js
================================================
const {calculateKWH} = require('./CalculateKwh');
const assert = require('assert');

describe('Calculate kWh', function() {
    it('should return 1 when consuming 1000W for 1 hour', function() {

    	// Consume 1000W for exactly 1 hour
    	const data = [
    		[new Date(1*1000), 1000],
    		[new Date(60*60*1000 +1000), 0],
    	];

    	// Should be 1kWh (at night)
      	assert.equal(calculateKWH(data).night, 1);
	});

	it('should return 0.5 when consuming 1000W for 30min', function() {

    	// Consume 1000W for exactly 30min
    	const data = [
    		[new Date(1*1000), 1000],
    		[new Date(30*60*1000 +1000), 0],
    	];

    	// Should be 1kWh (at night)
      	assert.equal(calculateKWH(data).night, 0.5);
	});
});

================================================
FILE: src-aws/core/helpers/IsNightTarif.js
================================================
/**
 * Checks if a given date object is within night tarif or not.
 * For us that is between 21:00 and 06:00 and every weekend day.
 */
module.exports.isNightTarif = function(dateObj) {
	if (typeof dateObj === 'number') {
		dateObj = new Date(dateObj * 1000);
	}

	if((dateObj.getHours() >= 21 && dateObj.getHours() <= 23) ||
		(dateObj.getHours() >= 0 && dateObj.getHours() <= 5)){
		return true;
	}

	if(dateObj.getDay() === 0 || dateObj.getDay() === 6){
		return true;
	}

	return false;
}

================================================
FILE: src-aws/core/helpers/IsNightTarif.test.js
================================================
const { isNightTarif } = require('./IsNightTarif');
const assert = require('assert');


describe('IsNightTarif', function() {
    it('should return true for night hours', function () {
        // 01/01/2019 @ 03:00 (UTC)
      	assert.equal(isNightTarif(new Date(1546311600000)), true);
    });

    it('should return false for day hours', function () {
        // 01/01/2019 @ 13:00 (UTC)
        assert.equal(isNightTarif(new Date(1546347600000)), false);
    });

    it('should return true for weekends', function () {
        // 01/05/2019  @ 13:00 (UTC) -> Saturday
        assert.equal(isNightTarif(new Date(1546693200000)), true);
    });

    it('should also work when we pass integers instead of date objects', function () {
         // 01/05/2019  @ 13:00 (UTC) -> Saturday
         assert.equal(isNightTarif(1546693200), true);
    });
});

================================================
FILE: src-aws/core/helpers.js
================================================
module.exports.getYesterdayDate = function(){
    const yesterday = new Date();
    yesterday.setHours(0);
    yesterday.setMinutes(0);
    yesterday.setSeconds(0);
    yesterday.setDate(yesterday.getDate() -1);

    const string = yesterday
    			.toISOString()
    			.substring(0,10)
    			.replace(/-/g, '');

    return {
    	dateObj: yesterday,
    	unixTimestamp: parseInt(yesterday.getTime() / 1000),
    	string: string,
    	year: string.substring(0,4),
    	month: string.substring(4,6),
    	day: string.substring(6,8)
    }
}

module.exports.getTodaysDate = function(){
	const today = new Date();
    today.setHours(0);
    today.setMinutes(0);
    today.setSeconds(0);

    const string = today
    			.toISOString()
    			.substring(0,10)
    			.replace(/-/g, '');

    return {
    	dateObj: today,
    	unixTimestamp: parseInt(today.getTime() / 1000),
    	string: string,
    	year: string.substring(0,4),
    	month: string.substring(4,6),
    	day: string.substring(6,8)
    }
}

module.exports.parseDynamoDBReadingsToJson = function(data){
	const output = [];

	for(const entry of data.Items){
		const timestamp = entry.sortkey;
		const readings = entry.readings;


		// Calculate the time of the first entry, assuming that a 
		// measurement is taken every second. We do -2 because js
		// starts counting from 0 and because the last element should
		// not be included.
		let timeForEntry = entry.sortkey - readings.length -2;

		for(const reading of readings){
			output.push({
				timestamp: timeForEntry,
				reading: reading
			});

			timeForEntry++;
		}
	}

	return output;
}

/**
 * Convert the output from DynamoDB (which is a JSON object)
 * into a string containing a CSV document with timestamp and
 * measurement column.
 */
module.exports.parseDynamoDBItemsToCSV = function(dynamoData){
	let output = 'Timestamp,Watts\n';

	const json = module.exports.parseDynamoDBReadingsToJson(dynamoData);

	for(const reading of json){
		output += reading.timestamp + ',' + reading.reading + '\n';
	}

	return output;
}

module.exports.getReadingsFromDynamoDBSince = async function(deviceId, timestamp){
	const { dynamoDocClient } = require('./aws-connections');
	const { config } = require('./config');

	const data = await dynamoDocClient.query({
       TableName : config.dynamoDb.table,
       KeyConditionExpression: '#key = :key and #sortkey > :timestamp',
       ScanIndexForward: true, // DESC order
       ConsistentRead: false,
       ExpressionAttributeNames:{
           '#key': 'primarykey',
           '#sortkey': 'sortkey',
       },
       ExpressionAttributeValues: {
           ':key': 'reading-' + deviceId,
           ':timestamp': timestamp
       },
    }).promise();

	return module.exports.parseDynamoDBReadingsToJson(data);
}

module.exports.getUsageDataFromDynamoDB = async function(deviceId, startDate, endDate){
	const { dynamoDocClient } = require('./aws-connections');
	const { config } = require('./config');

	const data = await dynamoDocClient.query({
       TableName : config.dynamoDb.table,
       KeyConditionExpression: '#key = :key and #sortkey BETWEEN :start AND :end',
       ScanIndexForward: true, // DESC order
       ConsistentRead: false,
       ExpressionAttributeNames:{
           '#key': 'primarykey',
           '#sortkey': 'sortkey',
       },
       ExpressionAttributeValues: {
           ':key': 'summary-day-' + deviceId,
           ':start': startDate,
           ':end': endDate
       },
    }).promise();

	console.log(data);
    return data.Items;
}

module.exports.writeToS3 = async function(filename, contents){
	const { s3 } = require('./aws-connections');
	const { config } = require('./config');
	const util = require('util');
	const zlib = require('zlib');
	const gzip = util.promisify(zlib.gzip);

	const compressedBody = await gzip(contents);

	return s3.putObject({
        Body: compressedBody,
        Bucket: config.s3.bucket,
        Key: filename + '.gz'
    }).promise();
}

module.exports.readFromS3 = function(filename){
	const { s3 } = require('./aws-connections');
	const { config } = require('./config');

	return s3.getObject({
        Bucket: config.s3.bucket,
        Key: filename,
    }).promise();
}

module.exports.getDatesBetween = function(startDate, endDate){
	const dateArray = [];

    let currentDate = startDate;
    while (currentDate <= endDate) {
        dateArray.push(new Date (currentDate));
        currentDate = currentDate.addDays(1);
    }

    return dateArray;
}

/**
 * Write a given object to the given table name. Returns a
 * promise that should be awaited.
 */
module.exports.writeToDynamoDB = function(tableName, object){
	const { dynamoDocClient } = require('./aws-connections');

	return dynamoDocClient.put({
        TableName: tableName,
        Item: object
    }).promise();
}


================================================
FILE: src-aws/dashboard/img/favicons/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
    <msapplication>
        <tile>
            <square150x150logo src="img/favicons/mstile-150x150.png"/>
            <TileColor>#da532c</TileColor>
        </tile>
    </msapplication>
</browserconfig>


================================================
FILE: src-aws/dashboard/img/favicons/site.webmanifest
================================================
{
    "name": "",
    "short_name": "",
    "icons": [
        {
            "src": "android-chrome-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "android-chrome-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "theme_color": "#ffffff",
    "background_color": "#ffffff",
    "display": "standalone"
}


================================================
FILE: src-aws/dashboard/index.html
================================================
<!doctype html>
<html lang="en">
<head>
  <title>Home Energy</title>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="apple-mobile-web-app-capable" content="yes">


  <link rel="apple-touch-icon" sizes="180x180" href="img/favicons/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="img/favicons/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="img/favicons/favicon-16x16.png">
  <link rel="manifest" href="img/favicons/site.webmanifest">
  <link rel="mask-icon" href="img/favicons/safari-pinned-tab.svg" color="#5bbad5">
  <link rel="shortcut icon" href="img/favicons/favicon.ico">
  <meta name="theme-color" content="#ffffff">


  <script src="https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js" integrity="sha256-XT58qJPKCsRBRq+MIcNDQ7dVh0GAa1k2r24w62z0Olk=" crossorigin="anonymous"></script>
  <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>

  <script src="https://cdn.jsdelivr.net/npm/jstat@1.7.1/dist/jstat.min.js" integrity="sha256-Rtwg0oi/KB80JyxnJGWz/zWwjIBgDchFFBnenkosAfA=" crossorigin="anonymous"></script>

  <link rel="stylesheet" href="main.css" />
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.css" integrity="sha256-NmfeKHX4FgSrBzL2BhPhzy41cHgzNYIEZyLyqf2/B30=" crossorigin="anonymous" />
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
</head>
<body>

  <nav class="navbar navbar-dark bg-dark">
    <a class="navbar-brand" href="#">
     ⚡️ Home Energy Monitor
    </a>

    <div class="spinner-grow text-light" id="loading-indicator" role="status">
      <span class="sr-only">Loading...</span>
    </div>

    <form class="form-inline">
      <button class="btn btn-outline-secondary" type="button" id="btnYesterday">Yesterday</button>
      &nbsp;
      <button class="btn btn-outline-success" type="button" id="btnToday">Today</button>
    </form>
  </nav>

  <br>

  <div class="container-fluid">
    <div class="card">
      <div class="card-header">
      <span>Readings <small id="usage-kwh">? kWh</small></span>
    <span style="float:right;">Last Reading <small id="last-reading">?</small></span>
      </div>
      <div class="card-body">
        <div id="graphdiv" style="width: 100%; height: 300px;"></div>
      </div>
    </div>
  </div>

  <br>

  <div class="container-fluid">

    <div class="row">
      <div class="col-md-6">
        <div class="card">
          <div class="card-header">
            Last 24 hours
          </div>
          <div class="card-body" style="text-align:center;">
            <div class="circle big">
              <p>Current<br>
              <span id="stats-current">? W</span></p>
            </div>
            <div class="circle">
              <p>Standby<br>
              <span id="stats-standby">? W</span></p>
            </div>
            <div class="circle">
              <p>Peak<br>
              <span id="stats-max">? W</span></p>
            </div>
            <div class="circle">
              <p>Today<br>
              <span id="stats-kwh">? kWh</span></p>
            </div>
          </div>
        </div>

        <div class="card">
          <div class="card-header">
            Standby-loss today
          </div>
          <div class="card-body" style="text-align:center;">
            <canvas id="chart-standby" height="100px"></canvas>
          </div>
        </div>
      </div>
      <div class="col-md-6">
        <div class="card">
          <div class="card-header">
            Last 30 days
          </div>
          <div class="card-body">
            <canvas id="canvas" height="300px"></canvas>
          </div>
        </div>
      </div>
    </div>
  </div>

<script type="text/javascript" src="main.js"></script>
<script type="text/javascript">
  (async function(){

    toggleLoadingIndicator(true);
    await Promise.all([
      initChart(),
      initUsageChart(),
    ]);
    toggleLoadingIndicator(false);
    
  })();
</script>
</body>
</html>

================================================
FILE: src-aws/dashboard/main.css
================================================
.dygraph-legend{
/*	left: 60px !important;
	right: 0px;
	top: -17px !important;
	width: 100%;*/
}

.circle{
	display:block;
	background-color: #1D294E;
	color: #fff;
	width: 120px;
	height: 120px;

	justify-content: center;
	align-items: center;
	border-radius: 100%;
	text-align: center;
	display: inline-flex;
}

.circle.big{
	/*width: 200px;*/
	/*height:200px;*/
}

================================================
FILE: src-aws/dashboard/main.js
================================================
const BASE_URL = '*** YOUR GRAPHQL ENDPOINT HERE ***';
let data = [];
let chart;
let animateDuration = 1500;

function toggleLoadingIndicator(visible){
	const $el = document.getElementById('loading-indicator');

	if(visible){
		$el.style.display = 'block';
	}else{
		$el.style.display = 'none';
	}
}

function formatTimestampForChartAxis(rawTimestamp){
	const date = new Date(rawTimestamp * 1000);
	const months = ["Jan", 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
	return date.getDate() + ' ' + months[date.getMonth()];
}

function fetchChartDataForDailyUsage(){
	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest();

		xhr.onload = function () {

			if (xhr.status >= 200 && xhr.status < 300) {
				const json = JSON.parse(xhr.response);

				// Process that data for chartjs

				var chartData = {
					labels: json.data.usageData.map(el => formatTimestampForChartAxis(el.timestamp)),
					datasets: [
						{
							label: 'Day',
							backgroundColor: 'rgb(54, 162, 235)',
							data: json.data.usageData.map(el => el.dayUse)
						},
						{
							label: 'Night',
							backgroundColor: 'rgb(29, 41, 81)',
							data: json.data.usageData.map(el => el.nightUse)
						},
					]
				}

				return resolve(chartData);
			} else {
				console.log('The request failed!');
				return reject();
			}
		};

		const startDate = new Date();
		startDate.setDate(startDate.getDate() - 31);

		const start = parseInt(startDate.getTime() / 1000);
		const end = parseInt(Date.now() / 1000);

		const query = `query{usageData(startDate:${start}, endDate:${end}){timestamp, dayUse, nightUse}}`;

		xhr.open('POST', BASE_URL);
		xhr.send(query);
	});
}

function fetchData(since){
	if(!since){
		const yesterday = new Date();
		yesterday.setDate(yesterday.getDate() -1);
		yesterday.setHours(yesterday.getHours() + 12);
		since = yesterday.getTime() / 1000;
	}
	
	since = parseInt(since);


	return new Promise((resolve, reject) => {
		const xhr = new XMLHttpRequest();

		xhr.onload = function () {

			if (xhr.status >= 200 && xhr.status < 300) {
				console.time("Parse JSON");
				const json = JSON.parse(xhr.response);
				console.timeEnd("Parse JSON");


				console.time("Process data");
				processData(json);
				console.timeEnd("Process data");

				return resolve();
			} else {
				console.log('The request failed!');
				return reject();
			}
		};

		const query = `query{ realtime(sinceTimestamp: ${since}){timestamp, reading} }`;

		xhr.open('POST', BASE_URL);
		xhr.send(query);
	});
}

function processData(rawData){
	if(!rawData || !rawData.data || !rawData.data.realtime){
		return;
	}

	for(const entry of rawData.data.realtime){
		const date = entry.timestamp * 1000;

		// If this entries timestamp is before the last entry
		// in our dataset, then we skip it because it's data 
		// we already have! This reduces the time it takes to
		// process all the data by a LOT!
		if(data.length > 1 && date < data[data.length -1 ][0].getTime()){
			continue;
		}

		const watts = parseFloat(entry.reading);

		data.push([
			new Date(date),
			watts,
		]);
	}

	if(chart){
		chart.updateOptions({
			file: data,
		});
	}

	// Update metrics
	const $current = document.getElementById('stats-current');
	const $todayKwh = document.getElementById('stats-kwh');
	const $standbyPower = document.getElementById('stats-standby');
	const $max = document.getElementById('stats-max');
	const $lastreading = document.getElementById('last-reading');

	var utcSeconds = rawData.data.realtime[rawData.data.realtime.length-1]["timestamp"]; // Get the latest UTC timestamp
	var d = new Date(0); // The 0 there is the key, which sets the date to the epoch
	d.setUTCSeconds(utcSeconds); // Convert epoch to local timezone
	$lastreading.innerHTML = d.toLocaleString(); //Update HTML
	const totalKwh = calculateKWH(data);

	$current.innerHTML = data[data.length-1][1] + ' W';
	$todayKwh.innerHTML = (Math.round(totalKwh * 100) / 100) + ' kWh';


	const readings = data.map(el => el[1]);
	const standbyWatts = jStat.mode(readings);
	$standbyPower.innerHTML = parseInt(standbyWatts) + ' W';
	$max.innerHTML = jStat.max(readings) + ' W';

	// Calculate total standby kWh
	const hours = (data[data.length-1][0].getTime() - data[0][0].getTime()) / 1000 / 3600;
	const standbyKwh = (standbyWatts/1000) * hours;

	initStandbyChart({
		activePower: totalKwh - (standbyKwh/1000 * hours), 
		standbyPower: standbyKwh
	});
}

function initStandbyChart({activePower, standbyPower}){
	const barChartData = {
		labels: ['Today'],
		datasets: [{
			data: [ activePower, standbyPower ],
			backgroundColor: ['rgb(54, 162, 235)', 'rgb(29, 41, 81)']
		}],
		labels: ['Active', 'Standby']
	};
	
	const ctx = document.getElementById('chart-standby').getContext('2d');
	new Chart(ctx, {
		type: 'doughnut',
		data: barChartData,
		options: {
			animation: {
				duration: animateDuration
			},
			responsive: true,
		}
	});

	animateDuration = 0;
}

/**
 * Calculates the consumed kWh based on the given
 * dataset. More accurate when interval of measurements
 * is higher.
 */
function calculateKWH(dataset){
	let total = 0;

	for(let i = 0; i < dataset.length-1; i++){
		const current = dataset[i];
		const next = dataset[i+1];

		const seconds = (next[0].getTime() - current[0].getTime()) / 1000;

		total += (current[1] * seconds * (1/(60*60))) / 1000;
	}

	return total;
}

/**
 * Calculates the min, max and used kwh based of the highlighted
 * range in the chart. If nothing was highlighted, we make a
 * complete overview
 */
function getMetricsForSelectedRange(chart, initial_draw){
	let startDate = 0;
	let endDate = Number.MAX_SAFE_INTEGER;

	if(chart.dateWindow_){
		startDate = chart.dateWindow_[0];
		endDate = chart.dateWindow_[1];
	}

	// Extract the data between start & end date
	const dataInScope = data.filter(
		el => el[0] > startDate && el[0] < endDate
	);

	return {
		usage: calculateKWH(dataInScope),
	}
}

/**
 * Is called by Dygraphs when the user has selected a range in
 * the chart. We then have to update the metrics for the newly
 * selected range.
 */
function updateMetricsForSelectedRange(chart, initial_draw){
	const metrics = getMetricsForSelectedRange(chart, initial_draw);
	const $kwh = document.getElementById('usage-kwh');
	$kwh.innerHTML = parseFloat(metrics.usage).toFixed(2) + ' kWh';
}

/**
 * Between 21:00 and 06:00 there is a special "night hour" tarif for
 * electricity. Also all hours on Saturday and Sunday. Reflect that 
 * by highlighting these areas in grey.
 */
function highlightNightHours(canvas, area, chart){
	let foundStart = false;
	let foundEnd = false;

	let startHighlight = null;
	let endHighlight = null;

	canvas.fillStyle = "#efefef";

	for(let i = 0; i < chart.file_.length; i++){
		const entry = chart.file_[i];
		const date = entry[0];

		// Assume this is also going to be our last item to highlight
		endHighlight = chart.toDomXCoord(date);

		if(foundStart === false && isNightTarif(date)){
			// We now found our start!
			foundStart = true;
			startHighlight = chart.toDomXCoord(date);
		}

		// If this entry is not night tarif, but we did find the start
		// before then this is the end!
		if(foundStart === true && isNightTarif(date) === false){
			foundEnd = true;
		}

		// If we found both, draw them!
		if(foundStart === true && foundEnd === true){
			const width = endHighlight - startHighlight;

			canvas.fillRect(startHighlight, area.y, width, area.h);

			foundStart = false;
			foundEnd = false;
			startHighlight = null;
			endHighlight = null;
		}

		i += 30;
	}

	// It could be that we found a start but not an end (in that case we're
	// actively in night hours and we should draw these as well!)
	if(foundStart && foundEnd === false){
		const lastPosition = chart.toDomXCoord(chart.file_[chart.file_.length -1][0]);
		const width = lastPosition - startHighlight;
		canvas.fillRect(startHighlight, area.y, width, area.h);
	}
}

/**
 * Checks if a given date object is within night tarif or not.
 * For us that is between 21:00 and 06:00 and every weekend day.
 */
function isNightTarif(dateObj){
	if((dateObj.getHours() >= 21 && dateObj.getHours() <= 23) ||
		(dateObj.getHours() >= 0 && dateObj.getHours() <= 5)){
		return true;
	}

	if(dateObj.getDay() === 0 || dateObj.getDay() === 6){
		return true;
	}

	return false;
}

async function initUsageChart(){
	const chartdata = await fetchChartDataForDailyUsage();
	var ctx = document.getElementById('canvas').getContext('2d');

	new Chart(ctx, {
		type: 'bar',
		data: chartdata,
		options: {
			responsive: true,
			maintainAspectRatio: false,
			scales: {
				xAxes: [{
					stacked: true,
				}],
				yAxes: [{
					stacked: true
				}]
			}
		}
	})
}

async function initChart(){

	// First fetch some data from ThingSpeak
	await fetchData();

	// Initialize the chart
	chart = new Dygraph(
	    document.getElementById("graphdiv"),
	    data,
	    {
            legend: 'always',
	    	labels: ['Timestamp', 'Watts'],
	    	underlayCallback: highlightNightHours,
	    	drawCallback: updateMetricsForSelectedRange,
	    	showRoller: true,
	    	rollPeriod: 14,
	    }
  	);

  	// Add callbacks to the buttons "yesterday" and "today"
  	document.getElementById('btnYesterday').addEventListener('click', () => {
  		const start = new Date();
  		start.setDate(start.getDate() - 1);
  		start.setHours(0);
  		start.setMinutes(0);

  		chart.updateOptions({
  			dateWindow: [start.getTime(), start.getTime() + 24*60*60*1000]
  		})
  	});

  	document.getElementById('btnToday').addEventListener('click', () => {
  		const start = new Date();
  		start.setHours(0);
  		start.setMinutes(0);

  		chart.updateOptions({
  			dateWindow: [start.getTime(), start.getTime() + 24*60*60*1000]
  		})
  	});

  // 	document.getElementById('btnGetSignature').addEventListener('click', () => {
  // 		if(chart.dateWindow_){
		// 	const startDate = chart.dateWindow_[0];
		// 	const endDate = chart.dateWindow_[1];

		// 	const filteredData = data.filter(el => el[0] < endDate && el[0] > startDate);
		// 	console.log(filteredData.map(el => '['+ el[1] + '/10000]\n').toString());
		// }
  // 	});

  	// Every 30 seconds: fetch new data from the GraphQL endpoint.
  	// Fetch new records since the last record's timestamp.
  	setInterval(async () => {
  		await fetchData(data[data.length-1][0].getTime() / 1000);
  	}, 30 * 1000);
}

================================================
FILE: src-aws/functions/cron-rotate-daily.js
================================================
'use strict';
const { dynamoDocClient } = require('../core/aws-connections');
const { config } = require('../core/config');
const { getYesterdayDate, getTodaysDate, writeToS3, writeToDynamoDB, parseDynamoDBItemsToCSV} = require('../core/helpers');
const { calculateKWH } = require('../core/helpers/CalculateKwh');

const deviceName = config.deviceName;

/**
 * Fetches all of yesterday's readings of a certain
 * device from DynamoDB.
 */
async function fetchYesterdaysData(){
	const timerLabel = '[PERF] Get history data';
    console.time(timerLabel);


    try{
        const startRange = getYesterdayDate().unixTimestamp;
        const endRange = getTodaysDate().unixTimestamp;

        const prefix = 'reading-' + deviceName;

        const data = await dynamoDocClient.query({
            TableName : config.dynamoDb.table,
            KeyConditionExpression: '#key = :key and #sortKey BETWEEN :start AND :end',
            ScanIndexForward: true, // DESC order
            ConsistentRead: false,
            ExpressionAttributeNames:{
                '#key': 'primarykey',
                '#sortKey': 'sortkey',
            },
            ExpressionAttributeValues: {
                ':key': prefix,
                ':start': startRange,
                ':end': endRange,
            },
        }).promise();

        console.timeEnd(timerLabel);
        console.log('Item count for yesterday', data.Items.length);
        return data;
    }catch(e){
        console.log('Error fetching historical data');
        console.log(e);

        // To prevent the application from crashing completely, we
        // return an valid DynamoDB result object with no entries.
        return { Items: [] };
    }
}


function calculateKwhSummary(csvData){
    // Transform the data
    const measurements = [];

    for(const line of csvData.split('\n')){
        if(line === '') continue;

        const parts = line.split(',');

        if(parts[0] === 'Timestamp') continue;

        measurements.push(
            [new Date(parseInt(parts[0]) * 1000), parseInt(parts[1])]
        );
    }

    // Calculate the usage
    return calculateKWH(measurements);
}

async function writeUsageToDynamoDB(usageObj){
    const timerLabel = '[PERF] Write daily summary to DynamoDB';
    console.time(timerLabel);

    try{
        const key = 'summary-day-' + deviceName;
        const sortkey = getYesterdayDate().unixTimestamp;

        const data = await writeToDynamoDB(config.dynamoDb.table, {
            primarykey: key,
            sortkey: sortkey,
            usage: usageObj
        });

        console.timeEnd(timerLabel);
        return data;
    }catch(e){
        console.log('Error writing daily usage to DynamoDB:');
        console.log(e);

        // To prevent the application from crashing completely, we
        // return an valid DynamoDB result object with no entries.
        return false
    }
}

module.exports.handler = async(event, context, callback) => {
	const data = await fetchYesterdaysData();

	// Convert to CSV
	const csv = parseDynamoDBItemsToCSV(data);

	const time = getYesterdayDate();

	// Write to S3
	await writeToS3(`archived-readings/${deviceName}/${time.year}/${time.month}/${time.string}.csv`, csv);

    // Calculate the kWh consumed & write it to DynamoDB
    const usageData = calculateKwhSummary(csv);
    console.log('usage data', usageData);
    await writeUsageToDynamoDB(usageData);
};

================================================
FILE: src-aws/functions/graphql/graphql.js
================================================
const { graphql, buildSchema } = require('graphql/index');
const { realtime } = require('./resolvers/realtime');
const { usageData } = require('./resolvers/usageData');
const { stats } = require('./resolvers/stats');

// Construct a schema, using GraphQL schema language
const schema = buildSchema(`
  type Query {
    usageData(startDate: Int!, endDate: Int!): [DailySummary]!

    stats: Stats!

    realtime(sinceTimestamp: Int!): [Reading]!

    readings(startDate: Int!, endDate: Int!): [Reading]!
  }

  type Stats{
    always_on: Float
    today_so_far: Float
  }

  type Reading {
    timestamp: Int!
    reading: Int!
  }

  type DailySummary{
    timestamp: Int!
    dayUse: Float!
    nightUse: Float!
  }
`);

// The root provides a resolver function for each API endpoint
const resolvers = {
  usageData: usageData,
  realtime: realtime,
  stats: stats,
};

module.exports.handler = async function(event, context, callback){
  const query = event.body;

  const response = await graphql(
    schema,
    query,
    resolvers
  );

  return {
    statusCode: 200,
    headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Credentials': true,
    },
    body: JSON.stringify(response),
  }
};

================================================
FILE: src-aws/functions/graphql/resolvers/realtime.js
================================================
const { getReadingsFromDynamoDBSince } = require('../../../core/helpers');
const { config } = require('../../../core/config');

/**
 * Fetches the collected readings from DynamoDB.
 * 
 * To prevent the user from consuming too many read units, we limit
 * the amount of data you can request here to the last 24 hours.
 * 
 * @param  {int} sinceTimestamp 	Timestamp in ms
 */
module.exports.realtime = async ({ sinceTimestamp }) => {
    const lowestTimestampAllowed = (new Date() / 1000) - 24 * 60 * 60;

    if (sinceTimestamp && sinceTimestamp < lowestTimestampAllowed) {
        throw new Error('This endpoint can only return data from the last 24 hours');
    }

    // If no timestamp was given, return the data from the last minute
    if (!sinceTimestamp) {
        console.log('No timestamp provided, going default');
        sinceTimestamp = (new Date() / 1000) - 60;
    }

    return await getReadingsFromDynamoDBSince(config.deviceName, sinceTimestamp);
}

================================================
FILE: src-aws/functions/graphql/resolvers/stats.js
================================================
const graphqlFields = require('graphql-fields');
const { getReadingsFromDynamoDBSince, getTodaysDate } = require('../../../core/helpers');
const { calculateKWH } = require('../../../core/helpers/CalculateKwh');

const jStat = require('jStat').jStat;

const { config } = require('../../../core/config');

module.exports.stats = async ({ sinceTimestamp }, context, info) => {
    const lowestTimestampAllowed = (new Date() / 1000) - 20 * 60 * 60;
    const todayStartTimestamp = getTodaysDate().unixTimestamp;

    const requestedFields = graphqlFields(info);
    const output = {};

    const allReadings = await getReadingsFromDynamoDBSince(config.deviceName, todayStartTimestamp);

    if (requestedFields.always_on) {
        const readingsOnly = allReadings.map(el => el.reading);
        const standbyWatts = jStat.mode(readingsOnly);

        output.always_on = standbyWatts;
    }

    // TODO: If only the today_so_far field is requested, we can get away by only loading
    // the records from today, potentially saving us a lot of DynamoDB read capacity.
    if (requestedFields.today_so_far) {
        // Tranform the readings into something the calculateKWH function expects
        const input = allReadings.map(item => [new Date(item.timestamp * 1000), item.reading]);

        const usage = calculateKWH(input);
        output.today_so_far = usage.day + usage.night;
    }

    return output;
}

================================================
FILE: src-aws/functions/graphql/resolvers/usageData.js
================================================
const { getUsageDataFromDynamoDB } = require('../../../core/helpers');
const { config } = require('../../../core/config');

module.exports.usageData = async ({ startDate, endDate }) => {

    // Fetch the data from DynamoDB
    const data = await getUsageDataFromDynamoDB(
      config.deviceName, startDate, endDate
    );

    // Tanform the usage data to a format that GraphQL expects
    return data.map(el => {
      return {
        timestamp: el.sortkey,
        dayUse: el.usage.day,
        nightUse: el.usage.night,
      }
    });
}

================================================
FILE: src-aws/package.json
================================================
{
  "name": "src-aws",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "mocha \"tests\" \"./core/helpers/*.test.js\""
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "aws-sdk": "^2.612.0",
    "serverless-finch": "^2.5.2",
    "serverless-scriptable-plugin": "^1.0.5",
    "serverless-webpack": "^5.3.1",
    "webpack": "^4.41.5",
    "webpack-node-externals": "^1.7.2"
  },
  "dependencies": {
    "graphql": "^14.6.0",
    "graphql-fields": "^2.0.3",
    "jStat": "^1.8.6"
  }
}


================================================
FILE: src-aws/serverless.yml
================================================
service: xd-home-energy-monitor

provider:
  name: aws
  runtime: nodejs12.x
  stage: prod
  region: eu-west-1
  profile: serverless-personal
  memorySize: 256
  deploymentBucket:
    name: "xd-serverless-deployments"

  environment:
    TZ: Europe/Brussels
    DYNAMO_DB_TABLE: !Ref dynamoDataStore
    S3_STORAGE_BUCKET: !Ref datastoreReadings

  apiGateway:
    minimumCompressionSize: 1024

  stackTags:
    client: "xd-home-energy-monitor"

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - dynamodb:Query
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:DeleteItem
      Resource: !GetAtt [dynamoDataStore, Arn]

    - Effect: "Allow"
      Action:
        - s3:GetObject
        - s3:PutObject
      Resource: 
        - !GetAtt [datastoreReadings, Arn]
        - !Join ['', [!GetAtt [datastoreReadings, Arn], '/*']]

plugins:
  - serverless-webpack
  - serverless-finch
  - serverless-scriptable-plugin

package:
  individually: true
  exclude:
    - functions/graphql/node_modules/**
    - dashboard/**
    - tests/**

functions:
  dailyDataArchive:
    handler: functions/cron-rotate-daily.handler
    description: Archive and aggregate yesterday's data to S3 and DynamoDB
    timeout: 30
    events:
      - schedule:
          description: "Archive the data generated yesterday to S3"
          rate: cron(0 2 * * ? *)

  graphql:
    description: GraphQL endpoint to query readings from devices
    handler: functions/graphql/graphql.handler
    memorySize: 512
    package:
      include:
        - functions/graphql/node_modules/**
    events:
      - http:
          path: graphql
          method: post
          cors: true

resources:
  Description: Monitoring home energy usage over time
  Resources:

    ###
    # S3 Bucket to store daily/monthly files containing all raw measurements.
    # Used to batch data up, reduce load on DynamoDB, reduce costs and allow
    # for fasting charting of our data with Dygraphs.
    ###
    datastoreReadings:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-datastore

    ###
    # S3 Bucket to store our front-end dashboard HTML code!
    ###
    wwwBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-www

    ###
    # DynamoDB table that stores recent raw messages from the devices
    # as well as computed usage information per sensor, per day, per month.
    ###
    dynamoDataStore:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}
        AttributeDefinitions:
          - AttributeName: "primarykey"
            AttributeType: S
          - AttributeName: "sortkey"
            AttributeType: N
        KeySchema:
          - AttributeName: "primarykey"
            KeyType: HASH
          - AttributeName: "sortkey"
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TimeToLiveSpecification:
          AttributeName: ttl
          Enabled: true

    ###
    # This IoT rule takes incoming messages and stores them straight
    # into DynamoDB with the current timestamp (when we received the 
    # message) as well as a calculated TTL for the item (30 days)
    # 
    # WARNING: if we update this, the name of the rule will change
    # and then the iotPolicyForDevices should also be changed to
    # reflect it!
    ###
    iotRule:
      Type: AWS::IoT::TopicRule
      Properties:
        TopicRulePayload:
          Actions:
            - 
              DynamoDBv2:
                PutItem:
                  TableName: ${self:service}
                RoleArn: !GetAtt [iotRuleAllowDynamoWrites, Arn]
          AwsIotSqlVersion: "2016-03-23"
          Description: "Forwards incoming sensor messages to DynamoDB for analysis"
          RuleDisabled: false
          Sql: >-
            SELECT * ,
                  'reading-' + clientid() as primarykey, 
                  (timestamp() / 1000) as sortkey, 
                  ((timestamp() / 1000) + 2592000) as ttl FROM '*** YOUR AWS IOT CORE THING ARN ***' 

    ###
    # Policy that defines what each sensor is allowed to do. On basic
    # level it should be allowed to publish directly to the rule
    # topic. We also only allow a device to connect if the used
    # certificate is attached to the thing that is trying to connect.
    ###
    iotPolicyForDevices:
      Type: AWS::IoT::Policy
      Properties:
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: "Allow"
              Action:
                - "iot:Connect"
              Resource: "*"
              Condition:
                Bool:
                  "iot:Connection.Thing.IsAttached": [true]
            -
              Effect: "Allow"
              Action:
                - "iot:Publish"
              Resource: 
                - Fn::Join:
                   - ""
                   - - "arn:aws:iot:"
                     - Ref: AWS::Region
                     - ":"
                     - Ref: AWS::AccountId
                     - ":topic/$aws/rules/"
                     - Ref: iotRule

    ###
    # Role that allows the IoT Topic Rule to write items to our
    # DynamoDB table (and only that table)
    ###
    iotRuleAllowDynamoWrites:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument: 
          Version: "2012-10-17"
          Statement: 
            - 
              Effect: "Allow"
              Principal: 
                Service: 
                  - "iot.amazonaws.com"
              Action: 
                - "sts:AssumeRole"
        Path: "/"
        Policies:
          -
            PolicyName: ${self:service}-firehose-role
            PolicyDocument:
              Version: "2012-10-17"
              Statement: 
                - Effect: "Allow"
                  Action:
                    - "dynamodb:PutItem"
                  Resource: !GetAtt [dynamoDataStore, Arn]

custom:
  scriptHooks:
     before:deploy:createDeploymentArtifacts: npm run test

  client:
    bucketName: ${self:service}-www
    distributionFolder: dashboard/
    indexDocument: index.html

  webpack:
    webpackConfig: 'webpack.config.js'   # Name of webpack configuration file
    includeModules: true   # Node modules configuration for packaging
    packager: 'npm'   # Packager that will be used to package your external modules

================================================
FILE: src-aws/webpack.config.js
================================================
const slsw = require('serverless-webpack');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  mode: 'production',
  entry: slsw.lib.entries,
  target: 'node',
  externals: [nodeExternals({
  	whitelist: ['graphql', 'graphql-fields']
  })] // exclude external modules
};

================================================
FILE: src-esp32/.gitignore
================================================
.pio
.pioenvs
.piolibdeps
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
src/config/config.h

================================================
FILE: src-esp32/.travis.yml
================================================
# Continuous Integration (CI) is the practice, in software
# engineering, of merging all developer working copies with a shared mainline
# several times a day < https://docs.platformio.org/page/ci/index.html >
#
# Documentation:
#
# * Travis CI Embedded Builds with PlatformIO
#   < https://docs.travis-ci.com/user/integration/platformio/ >
#
# * PlatformIO integration with Travis CI
#   < https://docs.platformio.org/page/ci/travis.html >
#
# * User Guide for `platformio ci` command
#   < https://docs.platformio.org/page/userguide/cmd_ci.html >
#
#
# Please choose one of the following templates (proposed below) and uncomment
# it (remove "# " before each line) or use own configuration according to the
# Travis CI documentation (see above).
#


#
# Template #1: General project. Test it using existing `platformio.ini`.
#

# language: python
# python:
#     - "2.7"
#
# sudo: false
# cache:
#     directories:
#         - "~/.platformio"
#
# install:
#     - pip install -U platformio
#     - platformio update
#
# script:
#     - platformio run


#
# Template #2: The project is intended to be used as a library with examples.
#

# language: python
# python:
#     - "2.7"
#
# sudo: false
# cache:
#     directories:
#         - "~/.platformio"
#
# env:
#     - PLATFORMIO_CI_SRC=path/to/test/file.c
#     - PLATFORMIO_CI_SRC=examples/file.ino
#     - PLATFORMIO_CI_SRC=path/to/test/directory
#
# install:
#     - pip install -U platformio
#     - platformio update
#
# script:
#     - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N


================================================
FILE: src-esp32/certificates/.gitKeep
================================================


================================================
FILE: src-esp32/include/README
================================================

This directory is intended for project header files.

A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.

```src/main.c

#include "header.h"

int main (void)
{
 ...
}
```

Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.

In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.

Read more about using header files in official GCC documentation:

* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes

https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html


================================================
FILE: src-esp32/lib/README
================================================

This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.

The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").

For example, see a structure of the following two libraries `Foo` and `Bar`:

|--lib
|  |
|  |--Bar
|  |  |--docs
|  |  |--examples
|  |  |--src
|  |     |- Bar.c
|  |     |- Bar.h
|  |  |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|  |
|  |--Foo
|  |  |- Foo.c
|  |  |- Foo.h
|  |
|  |- README --> THIS FILE
|
|- platformio.ini
|--src
   |- main.c

and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>

int main (void)
{
  ...
}

```

PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.

More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html


================================================
FILE: src-esp32/platformio.ini
================================================
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32doit-devkit-v1]
platform = espressif32
board = lolin32
framework = arduino
monitor_speed = 115200

build_flags =
  -DCOMPONENT_EMBED_TXTFILES=certificates/certificate.pem.crt:certificates/private.pem.key:certificates/amazonrootca1.pem
  -DCORE_DEBUG_LEVEL=0

lib_deps =
  https://github.com/Savjee/EmonLib-esp32.git
  Adafruit GFX Library
  Adafruit SSD1306
  adafruit/Adafruit BusIO @ ^1.7.2
  MQTT
  NTPClient


================================================
FILE: src-esp32/src/config/config.dist.h
================================================
#ifndef CONFIG
#define CONFIG

/**
 * Set this to false to disable Serial logging
 */
#define DEBUG true

/**
 * The name of this device (as defined in the AWS IOT console).
 * Also used to set the hostname on the network
 */
#define DEVICE_NAME "*****YOUR AWS IOT DEVICE NAME******"

/**
 * ADC input pin that is used to read out the CT sensor
 */
#define ADC_INPUT 36

/**
 * The voltage of your home, used to calculate the wattage.
 * Try setting this as accurately as possible.
 */
#define HOME_VOLTAGE 245.0

/**
 * WiFi credentials
 */
#define WIFI_NETWORK "****** YOUR WIFI NETWORK NAME *******"
#define WIFI_PASSWORD "****** YOUR WIFI PASSWORD *******"

/**
 * Timeout for the WiFi connection. When this is reached,
 * the ESP goes into deep sleep for 30seconds to try and
 * recover.
 */
#define WIFI_TIMEOUT 20000 // 20 seconds

/**
 * How long should we wait after a failed WiFi connection
 * before trying to set one up again.
 */
#define WIFI_RECOVER_TIME_MS 20000 // 20 seconds

/**
 * Dimensions of the OLED display attached to the ESP
 */
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

/**
 * Force Emonlib to assume a 3.3V supply to the CT sensor
 */
#define emonTxV3 1


/**
 * Local measurements
 */
#define LOCAL_MEASUREMENTS 30


/**
 * The MQTT endpoint of the service we should connect to and receive messages
 * from.
 */
#define AWS_ENABLED true
#define AWS_IOT_ENDPOINT "**** YOUR AWS IOT ENDPOINT ****"
#define AWS_IOT_TOPIC "**** YOUR AWS IOT RULE ARN ****"

#define MQTT_CONNECT_DELAY 200
#define MQTT_CONNECT_TIMEOUT 20000 // 20 seconds


/**
 * Syncing time with an NTP server
 */
#define NTP_TIME_SYNC_ENABLED true
#define NTP_SERVER "pool.ntp.org"
#define NTP_OFFSET_SECONDS 3600
#define NTP_UPDATE_INTERVAL_MS 60000

/**
 * Wether or not you want to enable Home Assistant integration
 */
#define HA_ENABLED false
#define HA_ADDRESS "*** YOUR HOME ASSISTANT IP ADDRESSS ***"
#define HA_PORT 8883
#define HA_USER "*** MQTT USER ***"
#define HA_PASSWORD "*** MQTT PASSWORD ***"

// Check which core Arduino is running on. This is done because updating the 
// display only works from the Arduino core.
#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif

#endif

================================================
FILE: src-esp32/src/config/enums.h
================================================
#ifndef ENUMS
#define ENUMS

#include <Arduino.h>

// The state in which the device can be. This mainly affects what
// is drawn on the display.
enum DEVICE_STATE {
  CONNECTING_WIFI,
  CONNECTING_AWS,
  FETCHING_TIME,
  UP,
};

// Place to store all the variables that need to be displayed.
// All other functions should update these!
struct DisplayValues {
  double watt;
  double amps;
  int8_t wifi_strength;
  DEVICE_STATE currentState;
  String time;
};

#if DEBUG == true
  #define serial_print(x)  Serial.print (x)
  #define serial_println(x)  Serial.println (x)
#else
  #define serial_print(x)
  #define serial_println(x)
#endif

#endif

================================================
FILE: src-esp32/src/functions/drawFunctions.h
================================================
#ifndef DRAW_FUNCTIONS
#define DRAW_FUNCTIONS

#include <WiFi.h>
#include <Adafruit_SSD1306.h>
#include "../config/enums.h"
#include "../config/config.h"

extern Adafruit_SSD1306 display;
extern DisplayValues gDisplayValues;
extern unsigned char measureIndex;

void drawTime(){
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print(gDisplayValues.time);
}

void drawSignalStrength(){
  const byte X = 51;
  const byte X_SPACING = 2;

  // Draw the four base rectangles
  display.fillRect(X, 8-2, 1, 2, WHITE); // Bar 1
  display.fillRect(X + X_SPACING, 8-2, 1, 2, WHITE); // Bar 2
  display.fillRect(X + X_SPACING*2, 8-2, 1, 2, WHITE); // Bar 3
  display.fillRect(X + X_SPACING*3, 8-2, 1, 2, WHITE); // Bar 4

  // Draw bar 2
  if(gDisplayValues.wifi_strength > -70){
    display.fillRect(X+X_SPACING, 8-4, 1, 4, WHITE);
  }

  // Draw bar 3
  if(gDisplayValues.wifi_strength > -60){
    display.fillRect(X+X_SPACING*2, 8-6, 1, 6, WHITE);
  }

  // Draw bar 4
  if(gDisplayValues.wifi_strength >= -50){
    display.fillRect(X+X_SPACING*3, 8-8, 1, 8, WHITE);
  }
}

void drawMeasurementProgress(){
  const byte Y = SCREEN_WIDTH - 20;
  display.drawRect(0, Y, measureIndex*2, 2, WHITE);
}

/**
 * The screen that is displayed when the ESP has just booted
 * and is connecting to WiFi & AWS.
 */
void drawBootscreen(){
  byte X = 14;
  byte Y = 70;
  byte WIDTH = 6;
  byte MAX_HEIGHT = 35;
  byte HEIGHT_STEP = 10;
  byte X_SPACING = 10;

  display.fillRect(X              , Y, WIDTH, MAX_HEIGHT - HEIGHT_STEP*3, WHITE);
  display.fillRect(X + X_SPACING  , Y - HEIGHT_STEP, WIDTH, MAX_HEIGHT - HEIGHT_STEP*2, WHITE);
  display.fillRect(X + X_SPACING*2, Y - HEIGHT_STEP*2, WIDTH, MAX_HEIGHT - HEIGHT_STEP, WHITE);
  display.fillRect(X + X_SPACING*3, Y - HEIGHT_STEP*3, WIDTH, MAX_HEIGHT, WHITE);

  display.setTextSize(1);
  display.setCursor(0, Y + MAX_HEIGHT / 2);
  display.println("Connecting");

  if(gDisplayValues.currentState == CONNECTING_WIFI){
    display.println("   WiFi");
  }

  if(gDisplayValues.currentState == CONNECTING_AWS){
    display.println("   AWS");
  }
}

/**
 * Draw the current amps & watts in the middle of the display.
 */
void drawAmpsWatts(){

  String watts = String(gDisplayValues.watt, 0);
  String amps = String(gDisplayValues.amps, 2);
  
  String lblWatts = "Watt";
  String lblAmps = "Amps";

  const int startY = 30;

  // Calculate how wide (pixels) the text will be once rendered.
  // Each character = 6 pixels, with font size 2, that is 12 pixels.
  // -1 because of the spacing between letters (last one doesn't)
  int widthAmps = (amps.length() * 12) -1;
  int widthLblAmps = lblAmps.length() * 6 - 1;

  int widthWatts = watts.length() * 12 - 1;
  int widthLblWatts = lblWatts.length() * 6 -1;

  display.setTextSize(2);
  display.setCursor((SCREEN_HEIGHT - widthAmps) / 2, startY);
  display.print(amps);

  display.setTextSize(1);
  display.setCursor((SCREEN_HEIGHT - widthLblAmps) / 2, startY + 15);
  display.print(lblAmps);

  display.setTextSize(2);
  display.setCursor((SCREEN_HEIGHT - widthWatts) / 2, startY + 40);
  display.print(watts);

  display.setTextSize(1);
  display.setCursor((SCREEN_HEIGHT - widthLblWatts) / 2, startY + 60);
  display.print(lblWatts);
}

#endif

================================================
FILE: src-esp32/src/main.cpp
================================================
#include <Arduino.h>
#include "EmonLib.h"
#include "WiFi.h"
#include <driver/adc.h>
#include "config/config.h"
#include "config/enums.h"
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#include "tasks/updateDisplay.h"
#include "tasks/fetch-time-from-ntp.h"
#include "tasks/mqtt-aws.h"
#include "tasks/wifi-connection.h"
#include "tasks/wifi-update-signalstrength.h"
#include "tasks/measure-electricity.h"
#include "tasks/mqtt-home-assistant.h"

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
DisplayValues gDisplayValues;
EnergyMonitor emon1;

// Place to store local measurements before sending them off to AWS
unsigned short measurements[LOCAL_MEASUREMENTS];
unsigned char measureIndex = 0;

void setup()
{
  #if DEBUG == true
    Serial.begin(115200);
  #endif 

  // Setup the ADC
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
  analogReadResolution(ADC_BITS);
  pinMode(ADC_INPUT, INPUT);

  // i2c for the OLED panel
  Wire.begin(5, 4); 

  // Initialize the display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false, false)) {
    serial_println(F("SSD1306 allocation failed"));
    delay(10*1000);
    ESP.restart();
  }

  // Init the display
  display.clearDisplay();
  display.setRotation(3);
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setTextWrap(false);

  // Initialize emon library
  emon1.current(ADC_INPUT, 30);

  // ----------------------------------------------------------------
  // TASK: Connect to WiFi & keep the connection alive.
  // ----------------------------------------------------------------
  xTaskCreatePinnedToCore(
    keepWiFiAlive,
    "keepWiFiAlive",  // Task name
    5000,            // Stack size (bytes)
    NULL,             // Parameter
    1,                // Task priority
    NULL,             // Task handle
    ARDUINO_RUNNING_CORE
  );

  // ----------------------------------------------------------------
  // TASK: Connect to AWS & keep the connection alive.
  // ----------------------------------------------------------------
  #if AWS_ENABLED == true
    xTaskCreate(
      keepAWSConnectionAlive,
      "MQTT-AWS",      // Task name
      5000,            // Stack size (bytes)
      NULL,             // Parameter
      5,                // Task priority
      NULL              // Task handle
    );
  #endif

  // ----------------------------------------------------------------
  // TASK: Update the display every second
  //       This is pinned to the same core as Arduino
  //       because it would otherwise corrupt the OLED
  // ----------------------------------------------------------------
  xTaskCreatePinnedToCore(
    updateDisplay,
    "UpdateDisplay",  // Task name
    10000,            // Stack size (bytes)
    NULL,             // Parameter
    3,                // Task priority
    NULL,             // Task handle
    ARDUINO_RUNNING_CORE
  );

  // ----------------------------------------------------------------
  // Task: measure electricity consumption ;)
  // ----------------------------------------------------------------
  xTaskCreate(
    measureElectricity,
    "Measure electricity",  // Task name
    5000,                  // Stack size (bytes)
    NULL,                   // Parameter
    4,                      // Task priority
    NULL                    // Task handle
  );

  // ----------------------------------------------------------------
  // TASK: update time from NTP server.
  // ----------------------------------------------------------------
  #if NTP_TIME_SYNC_ENABLED == true
    xTaskCreate(
      fetchTimeFromNTP,
      "Update NTP time",
      5000,            // Stack size (bytes)
      NULL,             // Parameter
      1,                // Task priority
      NULL              // Task handle
    );
  #endif

  // ----------------------------------------------------------------
  // TASK: update WiFi signal strength
  // ----------------------------------------------------------------
  xTaskCreate(
    updateWiFiSignalStrength,
    "Update WiFi strength",
    1000,             // Stack size (bytes)
    NULL,             // Parameter
    2,                // Task priority
    NULL              // Task handle
  );

  #if HA_ENABLED == true
    xTaskCreate(
      HADiscovery,
      "MQTT-HA Discovery",  // Task name
      5000,                // Stack size (bytes)
      NULL,                 // Parameter
      5,                    // Task priority
      NULL                  // Task handle
    );

    xTaskCreate(
      keepHAConnectionAlive,
      "MQTT-HA Connect",
      5000,
      NULL,
      4,
      NULL
    );
  #endif
}

void loop()
{
  vTaskDelay(10000 / portTICK_PERIOD_MS);
}

================================================
FILE: src-esp32/src/tasks/fetch-time-from-ntp.h
================================================
#ifndef TASK_FETCH_TIME_NTP
#define TASK_FETCH_TIME_NTP

#if NTP_TIME_SYNC_ENABLED == true
    #include <Arduino.h>
    #include <WiFi.h>
    #include <NTPClient.h>
    #include <WiFiUdp.h>
    #include <NTPClient.h>
    #include "../config/enums.h"

    extern void reconnectWifiIfNeeded();
    extern DisplayValues gDisplayValues;

    WiFiUDP ntpUDP;

    // TODO: this does not take timezones into account! Only UTC for now.
    NTPClient timeClient(ntpUDP, NTP_SERVER, NTP_OFFSET_SECONDS, NTP_UPDATE_INTERVAL_MS);

    void fetchTimeFromNTP(void * parameter){
        for(;;){
            if(!WiFi.isConnected()){
                vTaskDelay(10*1000 / portTICK_PERIOD_MS);
                continue;
            }

            serial_println("[NTP] Updating...");
            timeClient.update();

            String timestring = timeClient.getFormattedTime();
            short tIndex = timestring.indexOf("T");
            gDisplayValues.time = timestring.substring(tIndex + 1, timestring.length() -3);
            
            serial_println("[NTP] Done");

            // Sleep for a minute before checking again
            vTaskDelay(NTP_UPDATE_INTERVAL_MS / portTICK_PERIOD_MS);
        }
    }
#endif
#endif


================================================
FILE: src-esp32/src/tasks/measure-electricity.h
================================================
#ifndef TASK_MEASURE_ELECTRICITY
#define TASK_MEASURE_ELECTRICITY

#include <Arduino.h>
#include "EmonLib.h"

#include "../config/config.h"
#include "../config/enums.h"
#include "mqtt-aws.h"
#include "mqtt-home-assistant.h"

extern DisplayValues gDisplayValues;
extern EnergyMonitor emon1;
extern unsigned short measurements[];
extern unsigned char measureIndex;

void measureElectricity(void * parameter)
{
    for(;;){
      serial_println("[ENERGY] Measuring...");
      long start = millis();

      double amps = emon1.calcIrms(1480);
      double watts = amps * HOME_VOLTAGE;

      gDisplayValues.amps = amps;
      gDisplayValues.watt = watts;

      measurements[measureIndex] = watts;
      measureIndex++;

      if(measureIndex == LOCAL_MEASUREMENTS){
          #if AWS_ENABLED == true
            xTaskCreate(
              uploadMeasurementsToAWS,
              "Upload measurements to AWS",
              10000,             // Stack size (bytes)
              NULL,             // Parameter
              5,                // Task priority
              NULL              // Task handle
            );
          #endif

          #if HA_ENABLED == true
            xTaskCreate(
              sendEnergyToHA,
              "HA-MQTT Upload",
              10000,             // Stack size (bytes)
              NULL,             // Parameter
              5,                // Task priority
              NULL              // Task handle
            );
          #endif

          measureIndex = 0;
      }

      long end = millis();

      // Schedule the task to run again in 1 second (while
      // taking into account how long measurement took)
      vTaskDelay((1000-(end-start)) / portTICK_PERIOD_MS);
    }    
}

#endif


================================================
FILE: src-esp32/src/tasks/mqtt-aws.h
================================================
#ifndef TASK_MQTT_AWS
#define TASK_MQTT_AWS

#if AWS_ENABLED == true
    #include <Arduino.h>
    #include <WiFiClientSecure.h>
    #include <MQTTClient.h>
    #include "../config/config.h"

    extern unsigned short measurements[];

    #define AWS_MAX_MSG_SIZE_BYTES 300

    WiFiClientSecure AWS_net;
    MQTTClient AWS_mqtt = MQTTClient(AWS_MAX_MSG_SIZE_BYTES);

    extern const uint8_t aws_root_ca_pem_start[] asm("_binary_certificates_amazonrootca1_pem_start");
    extern const uint8_t aws_root_ca_pem_end[] asm("_binary_certificates_amazonrootca1_pem_end");

    extern const uint8_t certificate_pem_crt_start[] asm("_binary_certificates_certificate_pem_crt_start");
    extern const uint8_t certificate_pem_crt_end[] asm("_binary_certificates_certificate_pem_crt_end");

    extern const uint8_t private_pem_key_start[] asm("_binary_certificates_private_pem_key_start");
    extern const uint8_t private_pem_key_end[] asm("_binary_certificates_private_pem_key_end");

    void keepAWSConnectionAlive(void * parameter){
        for(;;){
            if(AWS_mqtt.connected()){
                AWS_mqtt.loop();
                vTaskDelay(500 / portTICK_PERIOD_MS);
                continue;
            }

            if(!WiFi.isConnected()){
                vTaskDelay(1000 / portTICK_PERIOD_MS);
                continue;
            }

            // Configure certificates
            AWS_net.setCACert((const char *) aws_root_ca_pem_start);
            AWS_net.setCertificate((const char *) certificate_pem_crt_start);
            AWS_net.setPrivateKey((const char *) private_pem_key_start);

            serial_println(F("[MQTT] Connecting to AWS..."));
            AWS_mqtt.begin(AWS_IOT_ENDPOINT, 8883, AWS_net);

            long startAttemptTime = millis();
        
            while (!AWS_mqtt.connect(DEVICE_NAME, HA_USER, HA_PASSWORD) &&
                    millis() - startAttemptTime < MQTT_CONNECT_TIMEOUT)
            {
                vTaskDelay(MQTT_CONNECT_DELAY);
            }

            if(!AWS_mqtt.connected()){
                serial_println(F("[MQTT] AWS connection timeout. Retry in 30s."));
                vTaskDelay(30000 / portTICK_PERIOD_MS);
            }

            serial_println(F("[MQTT] AWS Connected!"));
        }
    }

    /**
     * TASK: Upload measurements to AWS. This only works when there are enough
     * local measurements. It's called by the measurement function.
     */
    void uploadMeasurementsToAWS(void * parameter){
        if(!WiFi.isConnected() || !AWS_mqtt.connected()){
            serial_println("[MQTT] AWS: no connection. Discarding data..");
            vTaskDelete(NULL);
        }

        char msg[AWS_MAX_MSG_SIZE_BYTES];
        strcpy(msg, "{\"readings\":[");

        for (short i = 0; i < LOCAL_MEASUREMENTS-1; i++){
            strcat(msg, String(measurements[i]).c_str());
            strcat(msg, ",");
        }

        strcat(msg, String(measurements[LOCAL_MEASUREMENTS-1]).c_str());
        strcat(msg, "]}");
            
        serial_print("[MQTT] AWS publish: ");
        serial_println(msg);
        AWS_mqtt.publish(AWS_IOT_TOPIC, msg);

        // Task is done!
        vTaskDelete(NULL);
    }
#endif
#endif


================================================
FILE: src-esp32/src/tasks/mqtt-home-assistant.h
================================================
#ifndef TASK_HOME_ASSISTANT
#define TASK_HOME_ASSISTANT

#if HA_ENABLED == true

    #include <Arduino.h>
    #include <WiFiClientSecure.h>
    #include <MQTTClient.h>
    #include "../config/config.h"

    WiFiClientSecure HA_net;
    MQTTClient HA_mqtt(1024);

    extern unsigned short measurements[];

    const char* PROGMEM HA_discovery_msg = "{"
            "\"name\":\"" DEVICE_NAME "\","
            "\"device_class\":\"power\","
            "\"unit_of_measurement\":\"W\","
            "\"icon\":\"mdi:transmission-tower\","
            "\"state_topic\":\"homeassistant/sensor/" DEVICE_NAME "/state\","
            "\"value_template\":\"{{ value_json.power}}\","
            "\"device\": {"
                "\"name\":\"" DEVICE_NAME "\","
                "\"sw_version\":\"2.0\","
                "\"model\":\"HW V2\","
                "\"manufacturer\":\"Xavier Decuyper\","
                "\"identifiers\":[\"" DEVICE_NAME "\"]"
            "}"
        "}";

    /**
     * Established a connection to Home Assistant MQTT broker.
     * 
     * This task should run continously. It will check if an
     * MQTT connection is active and if so, will sleep for 1
     * minute. If not, a new connection will be established.
     */
    void keepHAConnectionAlive(void * parameter){
        for(;;){
            // When we are connected, loop the MQTT client and sleep for 0,5s
            if(HA_mqtt.connected()){
                HA_mqtt.loop();
                vTaskDelay(250 / portTICK_PERIOD_MS);
                continue;
            }

            if(!WiFi.isConnected()){
                vTaskDelay(1000 / portTICK_PERIOD_MS);
                continue;
            }

            serial_println(F("[MQTT] Connecting to HA..."));
            HA_mqtt.begin(HA_ADDRESS, HA_PORT, HA_net);

            long startAttemptTime = millis();
        
            while (!HA_mqtt.connect(DEVICE_NAME, HA_USER, HA_PASSWORD) &&
                    millis() - startAttemptTime < MQTT_CONNECT_TIMEOUT)
            {
                vTaskDelay(MQTT_CONNECT_DELAY / portTICK_PERIOD_MS);
            }

            if(!HA_mqtt.connected()){
                serial_println(F("[MQTT] HA connection failed. Waiting 30s.."));
                vTaskDelay(30000 / portTICK_PERIOD_MS);
            }

            serial_println(F("[MQTT] HA Connected!"));
        }
    }

    /**
     * TASK: Every 15 minutes we send Home Assistant a discovery message
     *       so that the energy monitor shows up in the device registry.
     */
    void HADiscovery(void * parameter){
        for(;;){
            if(!HA_mqtt.connected()){
                serial_println("[MQTT] HA: no MQTT connection.");
                vTaskDelay(30 * 1000 / portTICK_PERIOD_MS);
                continue;
            }

            serial_println("[MQTT] HA sending auto discovery");
            HA_mqtt.publish("homeassistant/sensor/" DEVICE_NAME "/config", HA_discovery_msg);
            vTaskDelay(15 * 60 * 1000 / portTICK_PERIOD_MS);
        }
    }

    void sendEnergyToHA(void * parameter){
        if(!HA_mqtt.connected()){
        serial_println("[MQTT] Can't send to HA without MQTT. Abort.");
        vTaskDelete(NULL);
        }

        char msg[30];
        strcpy(msg, "{\"power\":");
            strcat(msg, String(measurements[LOCAL_MEASUREMENTS-1]).c_str());
        strcat(msg, "}");

        serial_print("[MQTT] HA publish: ");
        serial_println(msg);

        HA_mqtt.publish("homeassistant/sensor/" DEVICE_NAME "/state", msg);

        // Task is done!
        vTaskDelete(NULL);
    }
#endif
#endif


================================================
FILE: src-esp32/src/tasks/updateDisplay.h
================================================
#ifndef TASK_UPDATE_DISPLAY
#define TASK_UPDATE_DISPLAY

#include <Arduino.h>
#include <Adafruit_SSD1306.h>
#include "functions/drawFunctions.h"
#include "../config/config.h"

extern Adafruit_SSD1306 display;
extern DisplayValues gDisplayValues;

/**
 * Metafunction that takes care of drawing all the different
 * parts of the display (or not if it's turned off).
 */
void updateDisplay(void * parameter){
  for (;;){
    serial_println(F("[LCD] Updating..."));
    display.clearDisplay();

    if(gDisplayValues.currentState == CONNECTING_WIFI || 
        gDisplayValues.currentState == CONNECTING_AWS){
      drawBootscreen();
    }
    
    if(gDisplayValues.currentState == UP){
      drawTime();
      drawSignalStrength();
      drawAmpsWatts();
      drawMeasurementProgress();
    }

    display.display();

    // Sleep for 2 seconds, then update display again!
    vTaskDelay(2000 / portTICK_PERIOD_MS);
  }
}

#endif


================================================
FILE: src-esp32/src/tasks/wifi-connection.h
================================================
#ifndef TASK_WIFI_CONNECTION
#define TASK_WIFI_CONNECTION

#include <Arduino.h>
#include "WiFi.h"
#include "../config/enums.h"
#include "../config/config.h"

extern DisplayValues gDisplayValues;
extern void goToDeepSleep();

/**
 * Task: monitor the WiFi connection and keep it alive!
 * 
 * When a WiFi connection is established, this task will check it every 10 seconds 
 * to make sure it's still alive.
 * 
 * If not, a reconnect is attempted. If this fails to finish within the timeout,
 * the ESP32 is send to deep sleep in an attempt to recover from this.
 */
void keepWiFiAlive(void * parameter){
    for(;;){
        if(WiFi.status() == WL_CONNECTED){
            vTaskDelay(10000 / portTICK_PERIOD_MS);
            continue;
        }

        serial_println(F("[WIFI] Connecting"));
        gDisplayValues.currentState = CONNECTING_WIFI;

        WiFi.mode(WIFI_STA);
        WiFi.setHostname(DEVICE_NAME);
        WiFi.begin(WIFI_NETWORK, WIFI_PASSWORD);

        unsigned long startAttemptTime = millis();

        // Keep looping while we're not connected and haven't reached the timeout
        while (WiFi.status() != WL_CONNECTED && 
                millis() - startAttemptTime < WIFI_TIMEOUT){}

        // Make sure that we're actually connected, otherwise go to deep sleep
        if(WiFi.status() != WL_CONNECTED){
            serial_println(F("[WIFI] FAILED"));
            vTaskDelay(WIFI_RECOVER_TIME_MS / portTICK_PERIOD_MS);
        }

        serial_print(F("[WIFI] Connected: "));
        serial_println(WiFi.localIP());
        gDisplayValues.currentState = UP;
    }
}

#endif


================================================
FILE: src-esp32/src/tasks/wifi-update-signalstrength.h
================================================
#ifndef TASK_UPDATE_WIFI_SIGNAL
#define TASK_UPDATE_WIFI_SIGNAL

#include <Arduino.h>
#include "WiFi.h"
#include "../config/enums.h"

extern DisplayValues gDisplayValues;

/**
 * TASK: Get the current WiFi signal strength and write it to the
 * displayValues so it can be shown by the updateDisplay task
 */
void updateWiFiSignalStrength(void * parameter){
  for(;;){
    if(WiFi.isConnected()){
      serial_println(F("[WIFI] Updating signal strength..."));
      gDisplayValues.wifi_strength = WiFi.RSSI();
    }

    // Sleep for 10 seconds
    vTaskDelay(10000 / portTICK_PERIOD_MS);
  }
}

#endif

================================================
FILE: src-esp32/test/README
================================================

This directory is intended for PIO Unit Testing and project tests.

Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.

More information about PIO Unit Testing:
- https://docs.platformio.org/page/plus/unit-testing.html
Download .txt
gitextract_udkxjzlu/

├── .github/
│   └── workflows/
│       ├── aws.yml
│       └── firmware.yml
├── .gitignore
├── 3D-designs/
│   ├── 01 - Case.stl
│   └── 02 - Top lid.stl
├── README.md
├── _stuff/
│   └── Sketch icon.sketch
├── src-app/
│   ├── .gitignore
│   ├── angular.json
│   ├── config.xml
│   ├── e2e/
│   │   ├── protractor.conf.js
│   │   ├── src/
│   │   │   ├── app.e2e-spec.ts
│   │   │   └── app.po.ts
│   │   └── tsconfig.e2e.json
│   ├── ionic.config.json
│   ├── package.json
│   ├── resources/
│   │   ├── README.md
│   │   ├── icon.png.md5
│   │   └── splash.png.md5
│   ├── src/
│   │   ├── app/
│   │   │   ├── app-routing.module.ts
│   │   │   ├── app.component.html
│   │   │   ├── app.component.ts
│   │   │   ├── app.module.ts
│   │   │   ├── components/
│   │   │   │   ├── components.module.ts
│   │   │   │   └── loading-indicator/
│   │   │   │       ├── loading-indicator.component.html
│   │   │   │       ├── loading-indicator.component.scss
│   │   │   │       └── loading-indicator.component.ts
│   │   │   ├── services/
│   │   │   │   └── energy-service.service.ts
│   │   │   ├── tab-home/
│   │   │   │   ├── tab-home.module.ts
│   │   │   │   ├── tab-home.page.html
│   │   │   │   ├── tab-home.page.scss
│   │   │   │   └── tab-home.page.ts
│   │   │   ├── tab-readings/
│   │   │   │   ├── tab-readings.module.ts
│   │   │   │   ├── tab-readings.page.html
│   │   │   │   ├── tab-readings.page.scss
│   │   │   │   └── tab-readings.page.ts
│   │   │   ├── tab-statistics/
│   │   │   │   ├── tab-statistics.module.ts
│   │   │   │   ├── tab-statistics.page.html
│   │   │   │   ├── tab-statistics.page.scss
│   │   │   │   └── tab-statistics.page.ts
│   │   │   ├── tabs/
│   │   │   │   ├── tabs.module.ts
│   │   │   │   ├── tabs.page.html
│   │   │   │   ├── tabs.page.scss
│   │   │   │   ├── tabs.page.ts
│   │   │   │   └── tabs.router.module.ts
│   │   │   └── utils/
│   │   │       └── chart-defaults.ts
│   │   ├── environments/
│   │   │   ├── environment.prod.ts
│   │   │   └── environment.ts
│   │   ├── global.scss
│   │   ├── index.html
│   │   ├── karma.conf.js
│   │   ├── main.ts
│   │   ├── polyfills.ts
│   │   ├── test.ts
│   │   ├── theme/
│   │   │   └── variables.scss
│   │   ├── tsconfig.app.json
│   │   └── tsconfig.spec.json
│   ├── tsconfig.json
│   └── tslint.json
├── src-aws/
│   ├── core/
│   │   ├── aws-connections.js
│   │   ├── config.js
│   │   ├── helpers/
│   │   │   ├── CalculateKwh.js
│   │   │   ├── CalculateKwh.test.js
│   │   │   ├── IsNightTarif.js
│   │   │   └── IsNightTarif.test.js
│   │   └── helpers.js
│   ├── dashboard/
│   │   ├── img/
│   │   │   └── favicons/
│   │   │       ├── browserconfig.xml
│   │   │       └── site.webmanifest
│   │   ├── index.html
│   │   ├── main.css
│   │   └── main.js
│   ├── functions/
│   │   ├── cron-rotate-daily.js
│   │   └── graphql/
│   │       ├── graphql.js
│   │       └── resolvers/
│   │           ├── realtime.js
│   │           ├── stats.js
│   │           └── usageData.js
│   ├── package.json
│   ├── serverless.yml
│   └── webpack.config.js
└── src-esp32/
    ├── .gitignore
    ├── .travis.yml
    ├── certificates/
    │   └── .gitKeep
    ├── include/
    │   └── README
    ├── lib/
    │   └── README
    ├── platformio.ini
    ├── src/
    │   ├── config/
    │   │   ├── config.dist.h
    │   │   └── enums.h
    │   ├── functions/
    │   │   └── drawFunctions.h
    │   ├── main.cpp
    │   └── tasks/
    │       ├── fetch-time-from-ntp.h
    │       ├── measure-electricity.h
    │       ├── mqtt-aws.h
    │       ├── mqtt-home-assistant.h
    │       ├── updateDisplay.h
    │       ├── wifi-connection.h
    │       └── wifi-update-signalstrength.h
    └── test/
        └── README
Download .txt
SYMBOL INDEX (95 symbols across 30 files)

FILE: src-app/e2e/protractor.conf.js
  method onPrepare (line 22) | onPrepare() {

FILE: src-app/e2e/src/app.po.ts
  class AppPage (line 3) | class AppPage {
    method navigateTo (line 4) | navigateTo() {
    method getPageTitle (line 8) | getPageTitle() {

FILE: src-app/src/app/app-routing.module.ts
  class AppRoutingModule (line 13) | class AppRoutingModule {}

FILE: src-app/src/app/app.component.ts
  class AppComponent (line 11) | class AppComponent {
    method constructor (line 12) | constructor(
    method initializeApp (line 20) | initializeApp() {

FILE: src-app/src/app/app.module.ts
  class AppModule (line 28) | class AppModule {}

FILE: src-app/src/app/components/components.module.ts
  class ComponentsModule (line 18) | class ComponentsModule { }

FILE: src-app/src/app/components/loading-indicator/loading-indicator.component.ts
  class LoadingIndicatorComponent (line 8) | class LoadingIndicatorComponent implements OnInit {
    method constructor (line 11) | constructor() { }
    method ngOnInit (line 13) | ngOnInit() {}

FILE: src-app/src/app/services/energy-service.service.ts
  class EnergyService (line 9) | class EnergyService {
    method constructor (line 18) | constructor(private http: HttpClient, private toastCtrl: ToastControll...
    method isLoading (line 20) | public isLoading() {
    method getStatistics (line 24) | public async getStatistics(): Promise<any>{
    method getYearlyStats (line 49) | public async getYearlyStats(): Promise<any>{
    method getHomePageStatistics (line 100) | public async getHomePageStatistics(all?: boolean): Promise<any>{
    method getReadings (line 127) | public async getReadings(since?: number): Promise<any> {
    method makeGraphQLRequest (line 155) | private async makeGraphQLRequest(query: string): Promise<any> {
  type MainStats (line 199) | interface MainStats {

FILE: src-app/src/app/tab-home/tab-home.module.ts
  class TabHomeModule (line 28) | class TabHomeModule {}

FILE: src-app/src/app/tab-home/tab-home.page.ts
  class TabHomePage (line 9) | class TabHomePage {
    method constructor (line 22) | constructor(public energyService: EnergyService) { }
    method ionViewDidLoad (line 28) | async ionViewDidLoad() {
    method ionViewWillEnter (line 40) | async ionViewWillEnter() {
    method ionViewDidLeave (line 48) | async ionViewDidLeave() {
    method refreshDataIfNeeded (line 57) | private async refreshDataIfNeeded() {
    method scheduleTimer (line 95) | private scheduleTimer(waitMs = 0) {
    method fetchData (line 115) | private async fetchData(all?: boolean) {

FILE: src-app/src/app/tab-readings/tab-readings.module.ts
  class TabReadingsModule (line 28) | class TabReadingsModule {}

FILE: src-app/src/app/tab-readings/tab-readings.page.ts
  class TabReadingsPage (line 11) | class TabReadingsPage {
    method constructor (line 24) | constructor(public energyService: EnergyService) { }
    method ionViewWillEnter (line 26) | public async ionViewWillEnter() {
    method refreshReadings (line 44) | private async refreshReadings() {
    method dateChanged (line 58) | public dateChanged() {
    method goToTomorrow (line 69) | public goToTomorrow() {
    method goToYesterday (line 76) | public goToYesterday() {
    method fetchDataForDate (line 85) | private fetchDataForDate() {
    method renderChart (line 89) | private renderChart() {
    method manipulateSelectedDateBy (line 131) | private manipulateSelectedDateBy(amount: number) {

FILE: src-app/src/app/tab-statistics/tab-statistics.module.ts
  class TabStatisticsModule (line 28) | class TabStatisticsModule {}

FILE: src-app/src/app/tab-statistics/tab-statistics.page.ts
  class TabStatisticsPage (line 12) | class TabStatisticsPage implements OnInit, AfterViewInit {
    method constructor (line 32) | constructor(public energyService: EnergyService, private decimalPipe: ...
    method ngOnInit (line 34) | ngOnInit() {
    method ngAfterViewInit (line 37) | async ngAfterViewInit() {
    method segmentChanged (line 45) | public segmentChanged(event) {
    method segment30daysWasOpened (line 62) | private async segment30daysWasOpened() {
    method segmentYearlyOverviewWasOpened (line 90) | private async segmentYearlyOverviewWasOpened() {
    method formatTimestampForChartAxis (line 101) | private formatTimestampForChartAxis(rawTimestamp){
    method calculateMoreStats (line 111) | private async calculateMoreStats(data) {
    method drawYearlyOverviewChart (line 120) | private async drawYearlyOverviewChart() {
    method drawDayVsNightChart (line 153) | private async drawDayVsNightChart(dayUsage: number, nightUsage: number) {
    method drawDailyUsageChart (line 190) | private async drawDailyUsageChart(data) {

FILE: src-app/src/app/tabs/tabs.module.ts
  class TabsPageModule (line 19) | class TabsPageModule {}

FILE: src-app/src/app/tabs/tabs.page.ts
  class TabsPage (line 8) | class TabsPage {}

FILE: src-app/src/app/tabs/tabs.router.module.ts
  class TabsPageRoutingModule (line 57) | class TabsPageRoutingModule {}

FILE: src-aws/core/aws-connections.js
  constant AWS (line 1) | const AWS = require("aws-sdk");

FILE: src-aws/dashboard/main.js
  constant BASE_URL (line 1) | const BASE_URL = '*** YOUR GRAPHQL ENDPOINT HERE ***';
  function toggleLoadingIndicator (line 6) | function toggleLoadingIndicator(visible){
  function formatTimestampForChartAxis (line 16) | function formatTimestampForChartAxis(rawTimestamp){
  function fetchChartDataForDailyUsage (line 22) | function fetchChartDataForDailyUsage(){
  function fetchData (line 69) | function fetchData(since){
  function processData (line 109) | function processData(rawData){
  function initStandbyChart (line 171) | function initStandbyChart({activePower, standbyPower}){
  function calculateKWH (line 201) | function calculateKWH(dataset){
  function getMetricsForSelectedRange (line 221) | function getMetricsForSelectedRange(chart, initial_draw){
  function updateMetricsForSelectedRange (line 245) | function updateMetricsForSelectedRange(chart, initial_draw){
  function highlightNightHours (line 256) | function highlightNightHours(canvas, area, chart){
  function isNightTarif (line 312) | function isNightTarif(dateObj){
  function initUsageChart (line 325) | async function initUsageChart(){
  function initChart (line 347) | async function initChart(){

FILE: src-aws/functions/cron-rotate-daily.js
  function fetchYesterdaysData (line 13) | async function fetchYesterdaysData(){
  function calculateKwhSummary (line 54) | function calculateKwhSummary(csvData){
  function writeUsageToDynamoDB (line 74) | async function writeUsageToDynamoDB(usageObj){

FILE: src-esp32/src/config/enums.h
  type DEVICE_STATE (line 8) | enum DEVICE_STATE {
  type DisplayValues (line 17) | struct DisplayValues {

FILE: src-esp32/src/functions/drawFunctions.h
  function drawTime (line 13) | void drawTime(){
  function drawSignalStrength (line 19) | void drawSignalStrength(){
  function drawMeasurementProgress (line 45) | void drawMeasurementProgress(){
  function drawBootscreen (line 54) | void drawBootscreen(){
  function drawAmpsWatts (line 83) | void drawAmpsWatts(){

FILE: src-esp32/src/main.cpp
  function setup (line 27) | void setup()
  function loop (line 159) | void loop()

FILE: src-esp32/src/tasks/fetch-time-from-ntp.h
  function fetchTimeFromNTP (line 20) | void fetchTimeFromNTP(void * parameter){

FILE: src-esp32/src/tasks/measure-electricity.h
  function measureElectricity (line 17) | void measureElectricity(void * parameter)

FILE: src-esp32/src/tasks/mqtt-aws.h
  function keepAWSConnectionAlive (line 26) | void keepAWSConnectionAlive(void * parameter){
  function uploadMeasurementsToAWS (line 68) | void uploadMeasurementsToAWS(void * parameter){

FILE: src-esp32/src/tasks/mqtt-home-assistant.h
  function keepHAConnectionAlive (line 39) | void keepHAConnectionAlive(void * parameter){
  function HADiscovery (line 77) | void HADiscovery(void * parameter){
  function sendEnergyToHA (line 91) | void sendEnergyToHA(void * parameter){

FILE: src-esp32/src/tasks/updateDisplay.h
  function updateDisplay (line 16) | void updateDisplay(void * parameter){

FILE: src-esp32/src/tasks/wifi-connection.h
  function keepWiFiAlive (line 21) | void keepWiFiAlive(void * parameter){

FILE: src-esp32/src/tasks/wifi-update-signalstrength.h
  function updateWiFiSignalStrength (line 14) | void updateWiFiSignalStrength(void * parameter){
Condensed preview — 97 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (148K chars).
[
  {
    "path": ".github/workflows/aws.yml",
    "chars": 578,
    "preview": "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    str"
  },
  {
    "path": ".github/workflows/firmware.yml",
    "chars": 788,
    "preview": "name: firmware\n\non:\n  pull_request:\n  push:\n    paths:\n      - 'src-esp32/**'\n\njobs:\n  build:\n    runs-on: ubuntu-latest"
  },
  {
    "path": ".gitignore",
    "chars": 156,
    "preview": ".DS_Store\nsrc-aws/.serverless\nsrc-esp32/.vscode\nnode_modules\nsrc-esp32/certificates/*.pem\nsrc-esp32/certificates/*.crt\ns"
  },
  {
    "path": "README.md",
    "chars": 3435,
    "preview": "<p align=\"center\">\n    <a href=\"https://github.com/Savjee/home-energy-monitor\" rel=\"noopener\">\n        <img width=200px "
  },
  {
    "path": "src-app/.gitignore",
    "chars": 433,
    "preview": "# Specifies intentionally untracked files to ignore when using Git\n# http://git-scm.com/docs/gitignore\n\n*~\n*.sw[mnpcod]\n"
  },
  {
    "path": "src-app/angular.json",
    "chars": 5455,
    "preview": "{\n  \"$schema\": \"./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json\",\n  \"version\": 1,\n  \"defaultProj"
  },
  {
    "path": "src-app/config.xml",
    "chars": 4437,
    "preview": "<?xml version='1.0' encoding='utf-8'?>\n<widget id=\"be.savjee.homeenergy\" version=\"0.0.1\" xmlns=\"http://www.w3.org/ns/wid"
  },
  {
    "path": "src-app/e2e/protractor.conf.js",
    "chars": 753,
    "preview": "// Protractor configuration file, see link for more information\n// https://github.com/angular/protractor/blob/master/lib"
  },
  {
    "path": "src-app/e2e/src/app.e2e-spec.ts",
    "chars": 275,
    "preview": "import { AppPage } from './app.po';\n\ndescribe('new App', () => {\n  let page: AppPage;\n\n  beforeEach(() => {\n    page = n"
  },
  {
    "path": "src-app/e2e/src/app.po.ts",
    "chars": 202,
    "preview": "import { browser, by, element } from 'protractor';\n\nexport class AppPage {\n  navigateTo() {\n    return browser.get('/');"
  },
  {
    "path": "src-app/e2e/tsconfig.e2e.json",
    "chars": 214,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/app\",\n    \"module\": \"commonjs\",\n    "
  },
  {
    "path": "src-app/ionic.config.json",
    "chars": 88,
    "preview": "{\n  \"name\": \"src-app\",\n  \"integrations\": {\n    \"cordova\": {}\n  },\n  \"type\": \"angular\"\n}\n"
  },
  {
    "path": "src-app/package.json",
    "chars": 2385,
    "preview": "{\n  \"name\": \"src-app\",\n  \"version\": \"0.0.1\",\n  \"author\": \"Ionic Framework\",\n  \"homepage\": \"https://ionicframework.com/\","
  },
  {
    "path": "src-app/resources/README.md",
    "chars": 419,
    "preview": "These are Cordova resources. You can replace icon.png and splash.png and run\n`ionic cordova resources` to generate custo"
  },
  {
    "path": "src-app/resources/icon.png.md5",
    "chars": 32,
    "preview": "3cb62ec167ca6dc5a30560cac2e8caf4"
  },
  {
    "path": "src-app/resources/splash.png.md5",
    "chars": 32,
    "preview": "7108a139271bfc1712907b3291a2b454"
  },
  {
    "path": "src-app/src/app/app-routing.module.ts",
    "chars": 381,
    "preview": "import { NgModule } from '@angular/core';\nimport { PreloadAllModules, RouterModule, Routes } from '@angular/router';\n\nco"
  },
  {
    "path": "src-app/src/app/app.component.html",
    "chars": 63,
    "preview": "<ion-app>\n  <ion-router-outlet></ion-router-outlet>\n</ion-app>\n"
  },
  {
    "path": "src-app/src/app/app.component.ts",
    "chars": 613,
    "preview": "import { Component } from '@angular/core';\n\nimport { Platform } from '@ionic/angular';\nimport { SplashScreen } from '@io"
  },
  {
    "path": "src-app/src/app/app.module.ts",
    "chars": 988,
    "preview": "import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\nimport { RouteReuse"
  },
  {
    "path": "src-app/src/app/components/components.module.ts",
    "chars": 433,
    "preview": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { LoadingIndicatorCompo"
  },
  {
    "path": "src-app/src/app/components/loading-indicator/loading-indicator.component.html",
    "chars": 97,
    "preview": "<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",
    "chars": 79,
    "preview": "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",
    "chars": 343,
    "preview": "import { Component, OnInit, Input } from '@angular/core';\n\n@Component({\n  selector: 'app-loading-indicator',\n  templateU"
  },
  {
    "path": "src-app/src/app/services/energy-service.service.ts",
    "chars": 4880,
    "preview": "import { Injectable } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { ToastController "
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.module.ts",
    "chars": 636,
    "preview": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.page.html",
    "chars": 1581,
    "preview": "<ion-header>\n    <ion-toolbar>\n      <ion-buttons slot=\"end\">\n        <ion-spinner *ngIf=\"false\"></ion-spinner>\n      </"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.page.scss",
    "chars": 1540,
    "preview": ".center{\n    display: flex;\n    justify-content: center;\n    flex-direction: column;\n    text-align: center;\n    height:"
  },
  {
    "path": "src-app/src/app/tab-home/tab-home.page.ts",
    "chars": 3848,
    "preview": "import { Component, OnInit } from '@angular/core';\nimport { EnergyService } from '../services/energy-service.service';\n\n"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.module.ts",
    "chars": 655,
    "preview": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.page.html",
    "chars": 707,
    "preview": "<ion-header>\n  <ion-toolbar>\n    <ion-title>Readings</ion-title>\n  </ion-toolbar>\n\n  <ion-toolbar class=\"datePicker\">\n  "
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.page.scss",
    "chars": 218,
    "preview": ".datePicker{\n    ion-title{\n        font-weight: normal !important;\n    }\n}\n\n.graph-wrapper{\n    display: flex;\n    flex"
  },
  {
    "path": "src-app/src/app/tab-readings/tab-readings.page.ts",
    "chars": 3672,
    "preview": "import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';\nimport { EnergyService } from '../services/ene"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.module.ts",
    "chars": 666,
    "preview": "import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.page.html",
    "chars": 1629,
    "preview": "<ion-header>\n  <ion-toolbar>\n    <ion-title>Statistics</ion-title>\n    <ion-spinner *ngIf=\"energyService.isLoading()\"></"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.page.scss",
    "chars": 504,
    "preview": "$chartHeight: 180px;\n\ndiv.chart{\n    width: 100%;\n    height: $chartHeight;\n    max-height: $chartHeight;\n\n    &.height2"
  },
  {
    "path": "src-app/src/app/tab-statistics/tab-statistics.page.ts",
    "chars": 6440,
    "preview": "import { Component, OnInit, ElementRef, ViewChild, AfterViewInit } from '@angular/core';\nimport { EnergyService } from '"
  },
  {
    "path": "src-app/src/app/tabs/tabs.module.ts",
    "chars": 456,
    "preview": "import { IonicModule } from '@ionic/angular';\nimport { NgModule } from '@angular/core';\nimport { CommonModule } from '@a"
  },
  {
    "path": "src-app/src/app/tabs/tabs.page.html",
    "chars": 619,
    "preview": "<ion-tabs>\n\n  <ion-tab-bar slot=\"bottom\">\n    <ion-tab-button tab=\"home\">\n      <ion-icon name=\"flash\"></ion-icon>\n     "
  },
  {
    "path": "src-app/src/app/tabs/tabs.page.scss",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "src-app/src/app/tabs/tabs.page.ts",
    "chars": 174,
    "preview": "import { Component } from '@angular/core';\n\n@Component({\n  selector: 'app-tabs',\n  templateUrl: 'tabs.page.html',\n  styl"
  },
  {
    "path": "src-app/src/app/tabs/tabs.router.module.ts",
    "chars": 1136,
    "preview": "import { NgModule } from '@angular/core';\nimport { RouterModule, Routes } from '@angular/router';\nimport { TabsPage } fr"
  },
  {
    "path": "src-app/src/app/utils/chart-defaults.ts",
    "chars": 199,
    "preview": "export const ChartDefaults = {\n    title: {\n        text: null,\n    },\n    yAxis: {\n        min: 0,\n    },\n    legend: {"
  },
  {
    "path": "src-app/src/environments/environment.prod.ts",
    "chars": 51,
    "preview": "export const environment = {\n  production: true\n};\n"
  },
  {
    "path": "src-app/src/environments/environment.ts",
    "chars": 662,
    "preview": "// This file can be replaced during build by using the `fileReplacements` array.\n// `ng build --prod` replaces `environm"
  },
  {
    "path": "src-app/src/global.scss",
    "chars": 464,
    "preview": "// http://ionicframework.com/docs/theming/\n@import '~@ionic/angular/css/core.css';\n@import '~@ionic/angular/css/normaliz"
  },
  {
    "path": "src-app/src/index.html",
    "chars": 686,
    "preview": "<!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 "
  },
  {
    "path": "src-app/src/karma.conf.js",
    "chars": 981,
    "preview": "// Karma configuration file, see link for more information\n// https://karma-runner.github.io/1.0/config/configuration-fi"
  },
  {
    "path": "src-app/src/main.ts",
    "chars": 370,
    "preview": "import { enableProdMode } from '@angular/core';\nimport { platformBrowserDynamic } from '@angular/platform-browser-dynami"
  },
  {
    "path": "src-app/src/polyfills.ts",
    "chars": 3571,
    "preview": "/**\n * This file includes polyfills needed by Angular and is loaded before the app.\n * You can add your own extra polyfi"
  },
  {
    "path": "src-app/src/test.ts",
    "chars": 642,
    "preview": "// This file is required by karma.conf.js and loads recursively all the .spec and framework files\n\nimport 'zone.js/dist/"
  },
  {
    "path": "src-app/src/theme/variables.scss",
    "chars": 2434,
    "preview": "// Ionic Variables and Theming. For more info, please see:\n// http://ionicframework.com/docs/theming/\n\n/** Ionic CSS Var"
  },
  {
    "path": "src-app/src/tsconfig.app.json",
    "chars": 166,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/app\",\n    \"types\": []\n  },\n  \"exclud"
  },
  {
    "path": "src-app/src/tsconfig.spec.json",
    "chars": 256,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../out-tsc/spec\",\n    \"types\": [\n      \"jasmine"
  },
  {
    "path": "src-app/tsconfig.json",
    "chars": 435,
    "preview": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"outDir\": \"./dist/out-tsc\",\n    \"sourceMap\":"
  },
  {
    "path": "src-app/tslint.json",
    "chars": 2788,
    "preview": "{\n  \"rulesDirectory\": [\n    \"codelyzer\"\n  ],\n  \"rules\": {\n    \"arrow-return-shorthand\": true,\n    \"callable-types\": true"
  },
  {
    "path": "src-aws/core/aws-connections.js",
    "chars": 157,
    "preview": "const AWS = require(\"aws-sdk\");\nmodule.exports.dynamoDocClient = new AWS.DynamoDB.DocumentClient({ region: \"eu-west-1\" }"
  },
  {
    "path": "src-aws/core/config.js",
    "chars": 177,
    "preview": "module.exports.config = {\n\tdeviceName: 'xd-home-energy-monitor-2',\n\t\n\tdynamoDb: {\n\t\ttable: process.env.DYNAMO_DB_TABLE,\n"
  },
  {
    "path": "src-aws/core/helpers/CalculateKwh.js",
    "chars": 961,
    "preview": "/**\n * Calculates how many kWh has been used in the given dataset.\n * Returns an object with two fields: \"day\" and \"nigh"
  },
  {
    "path": "src-aws/core/helpers/CalculateKwh.test.js",
    "chars": 731,
    "preview": "const {calculateKWH} = require('./CalculateKwh');\nconst assert = require('assert');\n\ndescribe('Calculate kWh', function("
  },
  {
    "path": "src-aws/core/helpers/IsNightTarif.js",
    "chars": 492,
    "preview": "/**\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 w"
  },
  {
    "path": "src-aws/core/helpers/IsNightTarif.test.js",
    "chars": 851,
    "preview": "const { isNightTarif } = require('./IsNightTarif');\nconst assert = require('assert');\n\n\ndescribe('IsNightTarif', functio"
  },
  {
    "path": "src-aws/core/helpers.js",
    "chars": 4825,
    "preview": "module.exports.getYesterdayDate = function(){\n    const yesterday = new Date();\n    yesterday.setHours(0);\n    yesterday"
  },
  {
    "path": "src-aws/dashboard/img/favicons/browserconfig.xml",
    "chars": 258,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo"
  },
  {
    "path": "src-aws/dashboard/img/favicons/site.webmanifest",
    "chars": 424,
    "preview": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"android-chrome-192x192.png\",\n      "
  },
  {
    "path": "src-aws/dashboard/index.html",
    "chars": 4365,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n  <title>Home Energy</title>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" co"
  },
  {
    "path": "src-aws/dashboard/main.css",
    "chars": 367,
    "preview": ".dygraph-legend{\n/*\tleft: 60px !important;\n\tright: 0px;\n\ttop: -17px !important;\n\twidth: 100%;*/\n}\n\n.circle{\n\tdisplay:blo"
  },
  {
    "path": "src-aws/dashboard/main.js",
    "chars": 10400,
    "preview": "const BASE_URL = '*** YOUR GRAPHQL ENDPOINT HERE ***';\nlet data = [];\nlet chart;\nlet animateDuration = 1500;\n\nfunction t"
  },
  {
    "path": "src-aws/functions/cron-rotate-daily.js",
    "chars": 3429,
    "preview": "'use strict';\nconst { dynamoDocClient } = require('../core/aws-connections');\nconst { config } = require('../core/config"
  },
  {
    "path": "src-aws/functions/graphql/graphql.js",
    "chars": 1278,
    "preview": "const { graphql, buildSchema } = require('graphql/index');\nconst { realtime } = require('./resolvers/realtime');\nconst {"
  },
  {
    "path": "src-aws/functions/graphql/resolvers/realtime.js",
    "chars": 967,
    "preview": "const { getReadingsFromDynamoDBSince } = require('../../../core/helpers');\nconst { config } = require('../../../core/con"
  },
  {
    "path": "src-aws/functions/graphql/resolvers/stats.js",
    "chars": 1408,
    "preview": "const graphqlFields = require('graphql-fields');\nconst { getReadingsFromDynamoDBSince, getTodaysDate } = require('../../"
  },
  {
    "path": "src-aws/functions/graphql/resolvers/usageData.js",
    "chars": 543,
    "preview": "const { getUsageDataFromDynamoDB } = require('../../../core/helpers');\nconst { config } = require('../../../core/config'"
  },
  {
    "path": "src-aws/package.json",
    "chars": 589,
    "preview": "{\n  \"name\": \"src-aws\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"directories\": {\n    \"test\": \""
  },
  {
    "path": "src-aws/serverless.yml",
    "chars": 6459,
    "preview": "service: xd-home-energy-monitor\n\nprovider:\n  name: aws\n  runtime: nodejs12.x\n  stage: prod\n  region: eu-west-1\n  profile"
  },
  {
    "path": "src-aws/webpack.config.js",
    "chars": 298,
    "preview": "const slsw = require('serverless-webpack');\nconst nodeExternals = require('webpack-node-externals');\n\nmodule.exports = {"
  },
  {
    "path": "src-esp32/.gitignore",
    "chars": 121,
    "preview": ".pio\n.pioenvs\n.piolibdeps\n.vscode/.browse.c_cpp.db*\n.vscode/c_cpp_properties.json\n.vscode/launch.json\nsrc/config/config."
  },
  {
    "path": "src-esp32/.travis.yml",
    "chars": 1557,
    "preview": "# Continuous Integration (CI) is the practice, in software\n# engineering, of merging all developer working copies with a"
  },
  {
    "path": "src-esp32/certificates/.gitKeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src-esp32/include/README",
    "chars": 1386,
    "preview": "\nThis directory is intended for project header files.\n\nA header file is a file containing C declarations and macro defin"
  },
  {
    "path": "src-esp32/lib/README",
    "chars": 1037,
    "preview": "\nThis directory is intended for project specific (private) libraries.\nPlatformIO will compile them to static libraries a"
  },
  {
    "path": "src-esp32/platformio.ini",
    "chars": 787,
    "preview": "; PlatformIO Project Configuration File\n;\n;   Build options: build flags, source filter\n;   Upload options: custom uploa"
  },
  {
    "path": "src-esp32/src/config/config.dist.h",
    "chars": 2247,
    "preview": "#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 nam"
  },
  {
    "path": "src-esp32/src/config/enums.h",
    "chars": 645,
    "preview": "#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// "
  },
  {
    "path": "src-esp32/src/functions/drawFunctions.h",
    "chars": 3242,
    "preview": "#ifndef DRAW_FUNCTIONS\n#define DRAW_FUNCTIONS\n\n#include <WiFi.h>\n#include <Adafruit_SSD1306.h>\n#include \"../config/enums"
  },
  {
    "path": "src-esp32/src/main.cpp",
    "chars": 4727,
    "preview": "#include <Arduino.h>\n#include \"EmonLib.h\"\n#include \"WiFi.h\"\n#include <driver/adc.h>\n#include \"config/config.h\"\n#include "
  },
  {
    "path": "src-esp32/src/tasks/fetch-time-from-ntp.h",
    "chars": 1219,
    "preview": "#ifndef TASK_FETCH_TIME_NTP\n#define TASK_FETCH_TIME_NTP\n\n#if NTP_TIME_SYNC_ENABLED == true\n    #include <Arduino.h>\n    "
  },
  {
    "path": "src-esp32/src/tasks/measure-electricity.h",
    "chars": 1743,
    "preview": "#ifndef TASK_MEASURE_ELECTRICITY\n#define TASK_MEASURE_ELECTRICITY\n\n#include <Arduino.h>\n#include \"EmonLib.h\"\n\n#include \""
  },
  {
    "path": "src-esp32/src/tasks/mqtt-aws.h",
    "chars": 3211,
    "preview": "#ifndef TASK_MQTT_AWS\n#define TASK_MQTT_AWS\n\n#if AWS_ENABLED == true\n    #include <Arduino.h>\n    #include <WiFiClientSe"
  },
  {
    "path": "src-esp32/src/tasks/mqtt-home-assistant.h",
    "chars": 3596,
    "preview": "#ifndef TASK_HOME_ASSISTANT\n#define TASK_HOME_ASSISTANT\n\n#if HA_ENABLED == true\n\n    #include <Arduino.h>\n    #include <"
  },
  {
    "path": "src-esp32/src/tasks/updateDisplay.h",
    "chars": 929,
    "preview": "#ifndef TASK_UPDATE_DISPLAY\n#define TASK_UPDATE_DISPLAY\n\n#include <Arduino.h>\n#include <Adafruit_SSD1306.h>\n#include \"fu"
  },
  {
    "path": "src-esp32/src/tasks/wifi-connection.h",
    "chars": 1607,
    "preview": "#ifndef TASK_WIFI_CONNECTION\n#define TASK_WIFI_CONNECTION\n\n#include <Arduino.h>\n#include \"WiFi.h\"\n#include \"../config/en"
  },
  {
    "path": "src-esp32/src/tasks/wifi-update-signalstrength.h",
    "chars": 601,
    "preview": "#ifndef TASK_UPDATE_WIFI_SIGNAL\n#define TASK_UPDATE_WIFI_SIGNAL\n\n#include <Arduino.h>\n#include \"WiFi.h\"\n#include \"../con"
  },
  {
    "path": "src-esp32/test/README",
    "chars": 490,
    "preview": "\nThis directory is intended for PIO Unit Testing and project tests.\n\nUnit Testing is a software testing method by which "
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the Savjee/home-energy-monitor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 97 files (130.7 KB), approximately 37.3k tokens, and a symbol index with 95 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!