[](https://github.com/Savjee/home-energy-monitor/issues)
[](https://github.com/Savjee/home-energy-monitor/pulls)
[](/LICENSE)
**⚠️ This is a work in progress. By no means is this production ready.**
---
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 |  |
| `src-esp32` | Firmware for the ESP32 (measuring device) |  |
(TODO: add instructions on how to deploy all of this. 😅)
## Video explanation
[](https://www.youtube.com/watch?v=ah3ezprtgmc)
*[https://www.youtube.com/watch?v=ah3ezprtgmc](https://www.youtube.com/watch?v=ah3ezprtgmc)*
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:

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:

What is displayed on the ESP32 OLED display:

## 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
================================================
Home EnergyMeasuring home energy usage.Xavier Decuyper
================================================
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
================================================
================================================
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
================================================
================================================
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{
// 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{
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{
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 {
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 {
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
================================================
Home
{{stats.today_so_far | number:'1.0-1'}} kWh⌁ kWhtoday so far
================================================
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
================================================
Readings
================================================
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
================================================
StatisticsLast 30 daysLast 12 monthsBreakdown per day Day vs. Night