diff --git a/src/app/Graph/DataBoxes.tsx b/src/app/Graph/DataBoxes.tsx index 9f7b865..a00e539 100644 --- a/src/app/Graph/DataBoxes.tsx +++ b/src/app/Graph/DataBoxes.tsx @@ -120,8 +120,8 @@ const Consumed = function (props: { consumption: number; totalCost: number; dayC const renderPeriod = (p: config.PeriodTypes) => { switch (p) { - // case 'last-month': - // return förra månaden + case 'last-month': + return förra månaden case 'this-month': return sedan den 1e i månaden case 'rolling': @@ -138,17 +138,17 @@ const Consumed = function (props: { consumption: number; totalCost: number; dayC switch (configState.periodType) { case 'last-month': p = 'rolling' - // if (new Date().getDate() === 1) { - // p = 'rolling' - // } else { - // p = 'this-month' - // } + if (new Date().getDate() === 1) { + p = 'rolling' + } else { + p = 'this-month' + } break case 'this-month': p = 'rolling' break case 'rolling': - p = 'this-month' + p = 'last-month' break } dispatch(config.setPeriod(p)) diff --git a/src/app/Graph/GraphLoader.lib.ts b/src/app/Graph/GraphLoader.lib.ts new file mode 100644 index 0000000..858d632 --- /dev/null +++ b/src/app/Graph/GraphLoader.lib.ts @@ -0,0 +1,27 @@ +export interface Period { + from: Date + to: Date + hours: number +} + +export function getMonthIntervalFor(month: number): Period { + const now = new Date() + now.setHours(0) + now.setMinutes(0) + now.setSeconds(0) + now.setMilliseconds(0) + + // look at previous years numbers if month is ahead of current month + const year = month > now.getMonth() ? -1 : 0 + + const from = new Date(now) + from.setFullYear(now.getFullYear() + year, month, 1) + + const to = new Date(from) + to.setFullYear(now.getFullYear() + year, month + 1, 1) + + const ms = to.getTime() - from.getTime() + const hours = Math.floor(ms / 1000 / 60 / 60) + + return { from, to, hours } +} diff --git a/src/app/Graph/GraphLoader.tsx b/src/app/Graph/GraphLoader.tsx index 955dac9..3a0c804 100644 --- a/src/app/Graph/GraphLoader.tsx +++ b/src/app/Graph/GraphLoader.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react' import { match } from 'react-router' -import moment from 'moment' import * as tibber from '../../lib/tibber' import * as svk from '../../lib/svk/' import * as dataprep from '../../lib/dataprep' @@ -16,6 +15,7 @@ import { useDispatch, useSelector } from 'src/lib/hooks' import { push } from 'connected-react-router' import { DataSourceContext } from './Graphs' +import { getMonthIntervalFor } from './GraphLoader.lib' type Params = { id: string @@ -39,36 +39,49 @@ export default function GraphLoader(props: Props) { const { gridAreaCode, priceAreaCode } = props.match.params const homeId = props.match.params.id - let period: number - switch (configState.periodType) { - case 'last-month': { - const now = moment() - const start = moment().subtract(1, 'month').date(1).hour(0).minute(0).second(0) - const diff = moment.duration(now.diff(start)) - period = Math.ceil(diff.as('hours')) - break - } - case 'this-month': - period = new Date().getDate() * 24 - break - case 'rolling': - period = period = 32 * 24 - break - } - useEffect(() => { dispatch(snapshotStore.reset()) }, [dispatch]) useEffect(() => { - dispatch(tibber.getConsumption({ homeId, interval: tibber.Interval.Hourly, last: period })) + let first: number | undefined = undefined + let last: number | undefined = undefined + let after: Date | undefined = undefined + const now = new Date() + switch (configState.periodType) { + case 'last-month': { + let prevMonth = now.getMonth() - 1 + if (prevMonth < 0) prevMonth = 11 + + const period = getMonthIntervalFor(prevMonth) + after = period.from + first = period.hours + break + } + case 'this-month': { + const period = getMonthIntervalFor(now.getMonth()) + after = period.from + first = period.hours + break + } + case 'rolling': + last = last = 32 * 24 + break + } + + console.log({ after, last, first }) + + dispatch( + tibber.getConsumption({ homeId, resolution: tibber.Interval.Hourly, after, first, last }) + ) + // price is sometimes ahead by 24 hours, so we always add another period on it - dispatch(tibber.getPrice({ homeId, interval: tibber.Interval.Hourly, last: period + 24 })) + dispatch(tibber.getPrice({ homeId, resolution: tibber.Interval.Hourly, after, first, last })) dispatch(svk.getProfile({ area: gridAreaCode, period: configState.periodType })) setFirstLoad(false) - }, [dispatch, homeId, period, configState.periodType, gridAreaCode]) + }, [dispatch, homeId, configState.periodType, gridAreaCode]) const store = async () => { dispatch( diff --git a/src/lib/config.ts b/src/lib/config.ts index 1b38be2..78eef8c 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -15,8 +15,6 @@ const getInitialState = (): State => { switch (savedPeriod) { case 'last-month': - periodType = 'rolling' - break case 'this-month': case 'rolling': periodType = savedPeriod diff --git a/src/lib/dataprep.ts b/src/lib/dataprep.ts index 81460cc..dedc47d 100644 --- a/src/lib/dataprep.ts +++ b/src/lib/dataprep.ts @@ -100,7 +100,7 @@ export function aggregateDays( const prices = hours.map((hour) => hour.price) const profiles = hours.map((hour) => hour.profile) - // Skip days where we are missing data. Most likley close to our boundaries. + // Skip days where we are missing data. Most likely close to our boundaries. let valid = true for (let i = 0; i < consumptions.length; i++) { if (!consumptions[i] || !prices[i] || !profiles[i]) { diff --git a/src/lib/tibber/thunks.ts b/src/lib/tibber/thunks.ts index 7a375c4..de6291d 100644 --- a/src/lib/tibber/thunks.ts +++ b/src/lib/tibber/thunks.ts @@ -36,6 +36,10 @@ export const getHomes = createAsyncThunk('tibber/getHomes', async return result.viewer.homes }) +interface ConsumptionArgs extends RangeOptions { + homeId: string +} + interface ConsumptionResult { viewer: { home: { @@ -46,18 +50,13 @@ interface ConsumptionResult { } } -export const getConsumption = createAsyncThunk< - ConsumptionNode[], - { - homeId: string - interval: Interval - last?: number - } ->('tibber/getConsumption', async (args) => { - const result = await doRequest(`{ +export const getConsumption = createAsyncThunk( + 'tibber/getConsumption', + async (args) => { + const result = await doRequest(`{ viewer { home(id: "${args.homeId}") { - consumption(resolution: ${args.interval}, last: ${args.last || 100}) { + consumption(${rangeParameters(args)}) { nodes { from to @@ -69,11 +68,16 @@ export const getConsumption = createAsyncThunk< } }`) - if (!result.viewer.home.consumption) { - throw new Error('missing consumption data') + if (!result.viewer.home.consumption) { + throw new Error('missing consumption data') + } + return result.viewer.home.consumption.nodes } - return result.viewer.home.consumption.nodes -}) +) + +interface PriceArgs extends RangeOptions { + homeId: string +} interface PriceResult { viewer: { @@ -89,20 +93,15 @@ interface PriceResult { } } -export const getPrice = createAsyncThunk< - PriceNode[], - { - homeId: string - interval: Interval - last?: number - } ->('tibber/getPrice', async (args) => { - const result = await doRequest(`{ +export const getPrice = createAsyncThunk( + 'tibber/getPrice', + async (args) => { + const result = await doRequest(`{ viewer { home(id: "${args.homeId}") { currentSubscription{ priceInfo{ - range(resolution: ${args.interval}, last: ${args.last || 100}){ + range(${rangeParameters(args)}){ nodes{ startsAt, total, @@ -113,11 +112,12 @@ export const getPrice = createAsyncThunk< } } }`) - if (!result.viewer.home.currentSubscription.priceInfo.range) { - throw new Error('no price data found in range') + if (!result.viewer.home.currentSubscription.priceInfo.range) { + throw new Error('no price data found in range') + } + return result.viewer.home.currentSubscription.priceInfo.range.nodes } - return result.viewer.home.currentSubscription.priceInfo.range.nodes -}) +) async function doRequest(query: string) { const init: RequestInit = { @@ -144,3 +144,50 @@ async function doRequest(query: string) { interface GQLResponse { data: T } + +interface RangeOptions { + resolution: Interval + + after?: Date + before?: Date + first?: number + last?: number +} + +function rangeParameters(args: RangeOptions): string { + if (args.after && args.before) throw new Error('invalid combination: before && after') + if (args.first && args.last) throw new Error('invalid combination: last && first') + + return Object.entries(args) + .filter(([name, value]) => { + if (value === undefined) return false + // Allowlist with the args we are using as options + return ['resolution', 'after', 'before', 'first', 'last'].indexOf(name) !== -1 + }) + .map<[string, string]>(([name, value]) => { + if (value instanceof Date) { + // Before / after on a hourly resolution is > and <, not >= and <=. + // Decrease the timestamp by a millisecond to include the first or last hour we are looking for. + switch (name) { + case 'after': + value = new Date(value.getTime() - 1) + break + case 'before': + value = new Date(value.getTime() + 1) + break + } + + return [name, `"${btoa(value.toISOString())}"`] + } else if (typeof value === 'number') { + return [name, value.toFixed(0)] + } else { + value = '' + value + } + + return [name, value] + }) + .map(([name, value]) => { + return `${name}: ${value}` + }) + .join(', ') +}