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(', ')
+}