Skip to content

Commit

Permalink
Fetch monthly prices. Keep 10 days in device storage and use it if mi…
Browse files Browse the repository at this point in the history
…ssing first days in the end of the month.
  • Loading branch information
balmli committed Jan 6, 2023
1 parent d9ed33f commit b29c2f7
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 45 deletions.
27 changes: 0 additions & 27 deletions lib/NordpoolApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,6 @@ export class NordpoolApi {
return undefined;
};

/**
* Fetch monhtly average price for a month.
*
* @param aDate
* @param opts
*/
fetchMonthlyAverage = async (aDate: MomentInput, opts: NordpoolOptions): Promise<number | undefined> => {
try {
const dailyPrices = await this.fetchDailyPrices(moment(aDate), opts);
if (!dailyPrices) {
return undefined;
}

let sumPrice = 0;
let days = 0;
for (let row of dailyPrices) {
sumPrice += row.price;
days += 1;
}

return days > 0 ? sumPrice / days : 0;
} catch (err) {
this.logger.error('Fetching monthly average failed: ', err);
}
return undefined;
};

private getHourlyPrices = async (momnt: Moment, opts: NordpoolOptions): Promise<NordpoolPrices> => {
try {
const data = await http.json({
Expand Down
12 changes: 9 additions & 3 deletions lib/PriceFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export class PriceFetcher extends EventEmitter {
this.priceFetcherOptions.fetchMethod = fetchMethod;
}

setFetchMonthlyAverage(fetchMonthlyAverage: boolean) {
this.priceFetcherOptions.fetchMonthlyAverage = fetchMonthlyAverage;
}

private clearFetchData() {
if (this.fetchTimeout) {
this.device.homey.clearTimeout(this.fetchTimeout);
Expand Down Expand Up @@ -119,9 +123,11 @@ export class PriceFetcher extends EventEmitter {
}
}

const monthlyAverage = await this.pricesFetchClient.fetchMonthlyAverage(this.device, moment(), options);
this.logger.info('monthlyAverage: ', monthlyAverage);
this.emit('monthlyAverage', monthlyAverage);
if (this.priceFetcherOptions.fetchMonthlyAverage) {
const monthlyAverage = await this.pricesFetchClient.fetchMonthlyAverage(this.device, moment(), options);
this.logger.verbose('monthlyAverage: ', monthlyAverage);
this.emit('monthlyAverage', monthlyAverage);
}
} catch (err) {
this.logger.error(err);
} finally {
Expand Down
98 changes: 93 additions & 5 deletions lib/PricesFetchClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {NordpoolApi} from "./NordpoolApi";
import {NordpoolOptions, NordpoolPrices} from "./types";

const STORE_PREFIX = 'prices-';
const STORE_MONTHLY_AVG = 'sum10-';

export class PricesFetchClient {

Expand Down Expand Up @@ -111,7 +112,55 @@ export class PricesFetchClient {
aDate: Moment,
options: NordpoolOptions
): Promise<number | undefined> => {
return await this.nordpool.fetchMonthlyAverage(aDate, options);
this.logger.debug(`fetchMonthlyAverage: ${aDate.format()}, options: ${JSON.stringify(options)}`)

await this.clearMonthlyAvgStorageExcept(device, aDate);

const dailyPrices = await this.nordpool.fetchDailyPrices(aDate, options);
if (!dailyPrices) {
return undefined;
}

const dayOfMonth = aDate.date();
const startOfMonth = moment(aDate).startOf('month');
const day10th = moment(startOfMonth).add(9, 'day');
const day11th = moment(day10th).add(1, 'day');

this.logger.debug(`fetchMonthlyAverage: ${aDate.format()}, ${dayOfMonth}, ${startOfMonth}, ${day10th}, ${day11th}`)

const hasPricesFirst10days = dailyPrices
.find(p => p.startsAt.isSameOrAfter(day10th)) !== undefined;

this.logger.debug(`fetchMonthlyAverage: ${aDate.format()}, options: ${JSON.stringify(options)} => hasPricesFirst10days: ${hasPricesFirst10days}`)

if (hasPricesFirst10days) {
const sumFirst10days = dailyPrices
.filter(p => p.startsAt.isSameOrAfter(startOfMonth) && p.startsAt.isBefore(day11th))
.map(p => p.price)
.reduce((a, b) => a + b, 0);

this.logger.debug(`fetchMonthlyAverage: => sumFirst10days: ${sumFirst10days}`)

this.storeSum10DaysInStorage(device, aDate, sumFirst10days, options);
} else if (dayOfMonth >= 15) {
const sumFirst10Days = this.getSum10DaysFromStorage(device, aDate, options);
if (!!sumFirst10Days) {
const sumAfterDay10 = dailyPrices
.filter(p => p.startsAt.isAfter(day10th))
.map(p => p.price)
.reduce((a, b) => a + b, 0);

const ret2 = (sumFirst10Days + sumAfterDay10) / dayOfMonth;
this.logger.debug(`fetchMonthlyAverage: ${aDate.format()}, options: ${JSON.stringify(options)} => ${sumFirst10Days} + ${sumAfterDay10} => monthly average from storage: ${ret2}`)
return ret2;
}
}

const ret = dailyPrices
.map(p => p.price)
.reduce((a, b) => a + b, 0) / dailyPrices.length;
this.logger.debug(`fetchMonthlyAverage: ${aDate.format()}, options: ${JSON.stringify(options)} => monthly average = ${ret}`)
return ret;
}

private cachePrefix(aDate: Moment) {
Expand All @@ -130,12 +179,33 @@ export class PricesFetchClient {

private async storePricesInStorage(device: Device, aDate: Moment, prices: NordpoolPrices, options: NordpoolOptions): Promise<void> {
const key = this.cacheId(aDate, options);
await device.setStoreValue(key, prices);
await device.setStoreValue(key, prices).catch(err => this.logger.error(err));
}

private getPricesFromStorage(device: Device, aDate: Moment, options: NordpoolOptions): NordpoolPrices | undefined {
const key = this.cacheId(aDate, options);
return device.getStoreValue(key)
return device.getStoreValue(key);
}

private cachePrefixSum10Days(aDate: Moment) {
return `${STORE_MONTHLY_AVG}${aDate.format().substring(0, 7)}-`;
}

private cacheIdSum10Days(aDate: Moment, options: NordpoolOptions) {
return `${this.cachePrefixSum10Days(aDate)}${options.currency}-${options.priceArea}`;
}

private async storeSum10DaysInStorage(device: Device, aDate: Moment, sum10FirstDays: number, options: NordpoolOptions): Promise<void> {
const key = this.cacheIdSum10Days(aDate, options);
await device.setStoreValue(key, sum10FirstDays).catch(err => this.logger.error(err));
this.logger.debug(`storeSum10DaysInStorage: ${key} = ${sum10FirstDays}`)
}

private getSum10DaysFromStorage(device: Device, aDate: Moment, options: NordpoolOptions): number | undefined {
const key = this.cacheIdSum10Days(aDate, options);
const ret = device.getStoreValue(key);
this.logger.debug(`getSum10DaysFromStorage: ${key} = ${ret}`)
return ret;
}

/**
Expand All @@ -148,7 +218,7 @@ export class PricesFetchClient {
const keys = device.getStoreKeys();
for (const key of keys) {
if (key.startsWith(STORE_PREFIX)) {
await device.unsetStoreValue(key);
await device.unsetStoreValue(key).catch(err => this.logger.error(err));
}
}
}
Expand All @@ -165,7 +235,25 @@ export class PricesFetchClient {
const keys = device.getStoreKeys();
for (const key of keys) {
if (key.startsWith(STORE_PREFIX) && keepKeys.filter(kk => key.startsWith(kk)).length === 0) {
await device.unsetStoreValue(key);
await device.unsetStoreValue(key).catch(err => this.logger.error(err));
}
}
}

/**
* Clear 'day 1-10 sum price' in storage, except for specified date.
*
* @param device
* @param aDate do not clear for date
* @private
*/
private async clearMonthlyAvgStorageExcept(device: Device, aDate: Moment): Promise<any> {
const keepKey = this.cachePrefixSum10Days(aDate);
const keys = device.getStoreKeys();
for (const key of keys) {
if (key.startsWith(STORE_MONTHLY_AVG) && !key.startsWith(keepKey)) {
this.logger.debug(`clearMonthlyAvgStorageExcept: clear key: ${key}`)
await device.unsetStoreValue(key).catch(err => this.logger.error(err));
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,19 +222,22 @@ export class PriceFetcherOptions {
nextDays: number; // Number of days in the future to fetch. Default 1 (tomorrow)
nordpoolOptions?: NordpoolOptions;
fetchMethod?: PriceFetcherMethod;
fetchMonthlyAverage?: boolean; // Shall fetch monthly average ?
fetchTime?: number; // Seconds in the hour to fetch data

constructor({prevDays, nextDays, nordpoolOptions, fetchMethod, fetchTime}: {
constructor({prevDays, nextDays, nordpoolOptions, fetchMethod, fetchMonthlyAverage, fetchTime}: {
prevDays?: number,
nextDays?: number,
nordpoolOptions?: NordpoolOptions,
fetchMethod?: PriceFetcherMethod,
fetchMonthlyAverage?: boolean,
fetchTime?: number
}) {
this.prevDays = prevDays || 1;
this.nextDays = nextDays || 1;
this.nordpoolOptions = nordpoolOptions;
this.fetchMethod = fetchMethod || PriceFetcherMethod.nordpool;
this.fetchMonthlyAverage = fetchMonthlyAverage;
this.fetchTime = fetchTime;
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@balmli/homey-utility-prices",
"version": "1.4.2",
"version": "1.4.3",
"description": "Price utility for Homey apps",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -16,6 +16,7 @@
"test": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --timeout 10000 'tests/**/*.*'",
"test0": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --timeout 10000 'tests/**/currentPriceAmongNextHoursComparer_*.*'",
"test2": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --timeout 10000 'tests2/**/*.*'",
"test3": "env TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --timeout 10000 'tests2/**/fetch_monthly_average_*.*'",
"prepublishOnly": "npm run test"
},
"keywords": [
Expand Down
46 changes: 38 additions & 8 deletions tests2/fetch_monthly_average_1.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,53 @@
import {Device} from "homey";

import moment from 'moment-timezone';

import Logger from '@balmli/homey-logger';

import {Currency, NordpoolApi} from "../lib";
import {Currency, PricesFetchClient} from "../lib";

moment.tz.setDefault("Europe/Oslo");

class HomeyDevice {
storage: Map<string, any>;

constructor() {
this.storage = new Map<string, any>();
}

getStoreKeys = () => {
return Array.from(this.storage.keys());
}

setStoreValue = (key: string, value: any) => {
this.storage.set(key, value);
}

getStoreValue = (key: string) => {
return this.storage.get(key);
}

unsetStoreValue = (key: string) => {
this.storage.delete(key);
}
}

describe('Fetch monthly average', function () {

describe('Check monthly average', function () {
it('Check monthly average 1', function (done) {
const api = new NordpoolApi({
logger: new Logger({
logLevel: 3,
prefix: undefined,
})
const logger = new Logger({
logLevel: 2,
prefix: undefined,
});
const localTime = moment().startOf('day');
api.fetchMonthlyAverage(localTime, {priceArea: 'Bergen', currency: Currency.NOK})
const testDevice = new HomeyDevice();
const client = new PricesFetchClient({logger});
const localTime = moment();//.startOf('day');
// @ts-ignore
client.fetchMonthlyAverage(testDevice as Device, localTime, {
priceArea: 'Bergen',
currency: Currency.NOK
})
.then((avg) => {
//console.log(avg);
done();
Expand Down

0 comments on commit b29c2f7

Please sign in to comment.