coned-fetch

This commit is contained in:
2021-09-04 19:28:46 -04:00
parent c3cab2e4f1
commit b1b8b7606f
17 changed files with 5465 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
import chalk from "chalk";
import pupeteer from "puppeteer";
function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export class ConEd {
browser: pupeteer.Browser | undefined;
refresh_interval_min = 15;
db_store: ((raw_data: any) => Promise<any>) | undefined;
public constructor(values: Partial<ConEd>) {
Object.assign(this, values);
if (!this.valid_config()) {
return;
}
}
valid_config(): boolean {
const req_config = ["EMAIL", "PASSWORD", "MFA_SECRET", "ACCOUNT_UUID", "METER_NUM"];
return req_config.every((config) => {
if (!process.env[config]) {
console.log(chalk.red(`${config} is not set in the .env config! Exiting...`));
return false;
}
return true;
});
}
async init(): Promise<void> {
this.browser = await pupeteer.launch({
defaultViewport: { width: 1920, height: 1080 },
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
}
async login(): Promise<void> {
const page = await this.browser!.newPage();
// Login to ConEd website
await page.goto("https://www.coned.com/en/login");
await page.type("#form-login-email", process.env.EMAIL!);
await page.type("#form-login-password", process.env.PASSWORD!);
await page.click("#form-login-remember-me");
await page.click(".submit-button");
// Wait for login to authenticate
await sleep(5000);
await page.screenshot({ path: "meter0.png" });
// Enter in 2 factor auth code (see README for details)
await page.type("#form-login-mfa-code", process.env.MFA_SECRET!);
await page.click(".js-login-new-device-form .button");
// Wait for authentication to complete
await page.waitForNavigation();
await sleep(5000);
await page.screenshot({ path: "meter1.png" });
}
async read_api(): Promise<any> {
return new Promise(async (resolve) => {
// Access the API using your newly acquired authentication cookies!
const api_page = await this.browser!.newPage();
const api_url = `https://cned.opower.com/ei/edge/apis/cws-real-time-ami-v1/cws/cned/accounts/${process.env.ACCOUNT_UUID}/meters/${process.env.METER_NUM}/usage`;
await api_page.goto(api_url);
await api_page.screenshot({ path: "meter2.png" });
const data_elem = await api_page.$("pre");
const text_data = await api_page.evaluate((el: HTMLElement) => el.textContent, data_elem);
const raw_data = JSON.parse(text_data!);
await api_page.close();
resolve(raw_data);
});
}
async fetch_once(): Promise<void> {
await this.init();
await this.login();
let raw_data = await this.read_api();
let attempt = 1;
while ("error" in raw_data && attempt < 5) {
console.log(chalk.yellow("Failed to fetch data from API:", raw_data["error"]["details"]));
raw_data = await this.read_api();
await sleep(15000);
attempt++;
}
if (attempt < 5) {
console.log(chalk.green("Successfully retrieved API data!"));
this.db_store!(raw_data);
} else {
console.log(chalk.red("Failed to retrieve API data:", raw_data["error"]));
}
await this.browser?.close();
}
monitor(interval_min = 15): void {
this.fetch_once();
setInterval(() => {
this.fetch_once();
}, interval_min * 60 * 1000);
}
}
+144
View File
@@ -0,0 +1,144 @@
// Import config from .env
// eslint-disable-next-line
require("dotenv").config();
import { ConEd } from "./coned";
import { connect } from "trilogy";
import chalk from "chalk";
import fs from "fs";
import moment from "moment";
import fastify from "fastify";
import axios from "axios";
const output_dir = "out";
if (!fs.existsSync(output_dir)) {
fs.mkdirSync(output_dir);
}
async function main() {
const db = connect(`./${output_dir}/storage.db`);
process.on("exit", function () {
db.close();
console.log(chalk.green("Successfully closed DB connection!"));
});
const energySchema = {
id: "increments",
startTime: { type: Date, unique: true },
endTime: { type: Date, unique: true },
energy: { type: Number },
};
type EnergyEntry = {
id: number;
startTime: Date;
endTime: Date;
energy: number;
};
const energyModel = await db.model<EnergyEntry>("energy", energySchema);
let latest_read_startTime = new Date(0);
async function update_sensor(latest_read: any) {
if (latest_read.startTime < latest_read_startTime) return;
latest_read_startTime = latest_read.startTime;
axios.post("http://supervisor/core/api/states/sensor.coned_energy", {
state: latest_read.value,
attributes: { unit_of_measurement: "kWh", friendly_name: "ConEd Energy Usage", device_class: "energy", state_class: "measurement", last_reset: latest_read.startTime },
}, {
headers: {"Authorization": `Bearer ${process.env.SUPERVISOR_TOKEN}`, "Content-Type": "application/json"}
});
console.log(chalk.green(`Sent sensor update: ${latest_read.value} kWh @ ${moment(latest_read.startTime).format("LLL")}`));
}
async function db_store(raw_data: any) {
const filtered_data = raw_data.reads.filter((row: any) => {
return row.value !== null && row.value !== undefined;
})
const latest_read = filtered_data[0];
update_sensor(latest_read);
for (const row of filtered_data) {
await energyModel.updateOrCreate({ startTime: new Date(row.startTime), endTime: new Date(row.endTime) }, { energy: row.value }).catch((e) => {
console.log(chalk.red(`Err: ${e}`));
});
}
console.log(chalk.green("Database Updated!"));
console.log(chalk.green("Database Closed!"));
}
const c = new ConEd({ db_store });
c.monitor();
const server = fastify({
//logger: true,
});
// Declare a route
server.get<{
Querystring: { h: number };
}>("/", (request, reply) => {
console.log(request.query);
let hour_lookback = 24;
if (request.query.h) {
if (!!Number(request.query.h)) {
hour_lookback = Number(request.query.h);
} else {
reply.code(400).type("text/plain").send("Err 400: Invalid Request");
return;
}
}
const d = new Date();
d.setHours(d.getHours() - hour_lookback);
energyModel.find([["startTime", ">", d]]).then((raw_data) => {
reply.send({ data: raw_data });
});
});
server.get<{
Querystring: { n: number };
}>("/latest", (request, reply) => {
console.log(request.query);
let n_results = 64;
if (request.query.n) {
if (!!Number(request.query.n)) {
n_results = Number(request.query.n);
} else {
reply.code(400).type("text/plain").send("Err 400: Invalid Request");
return;
}
}
energyModel.find({}, { limit: n_results, order: ["startTime", "desc"] }).then((raw_data) => {
function to_delta_string(dt: Date): string {
const delta_min = Math.abs(dt.getTime() - new Date().getTime()) / 6e4;
return delta_min < 60 ? `${delta_min.toFixed(0)}min` : `${(delta_min / 60).toPrecision(2)}hr`;
}
//Create array of energy readings starting with the most recent
const readings = Object.values(raw_data).map((row: EnergyEntry) => row.energy);
//Time of earliest read in list
const earliest_dt: Date = Object.values(raw_data)[raw_data.length - 1].startTime;
const earliest_delta = to_delta_string(earliest_dt);
//Time of peak (most recent if tie)
const peak_index = readings.indexOf(Math.max.apply(null, readings));
const peak = Object.values(raw_data)[peak_index].energy;
const peak_dt = Object.values(raw_data)[peak_index].startTime;
const peak_delta = to_delta_string(peak_dt);
//Time of latest read in list
const latest_dt: Date = Object.values(raw_data)[0].startTime;
const latest_delta = to_delta_string(latest_dt);
const resp_data = { earliest_delta, latest_delta, peak, peak_delta, readings };
reply.send(resp_data);
});
});
// Run the server!
server.listen(3000, "0.0.0.0", function (err, address) {
if (err) {
server.log.error(err);
process.exit(1);
}
server.log.info(`server listening on ${address}`);
});
}
main();