sensors/client/src/App.js
2022-08-05 19:05:10 +00:00

972 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { ComposedChart, Bar, Label, LineChart, ReferenceLine, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import Datetime from 'react-datetime';
import 'react-datetime/css/react-datetime.css';
import axios from 'axios';
import moment from 'moment-timezone';
import './App.css';
let tzcache = {};
const durations = [
{id: 0, len: 'Day', win: '10m', full: '10 min', delta: [1, 'days'], format: 'HH'},
{id: 1, len: 'Day', win: '1h', full: '1 hour', delta: [1, 'days'], format: 'HH'},
{id: 2, len: 'Week', win: '1h', full: '1 hour', delta: [7, 'days'], format: 'HH'},
{id: 3, len: 'Week', win: '1d', full: '1 day', delta: [7, 'days'], format: 'D'},
{id: 4, len: 'Month', win: '1d', full: '1 day', delta: [1, 'months'], format: 'D'},
{id: 5, len: 'Month', win: '7d', full: '7 day', delta: [1, 'months'], format: 'D'},
{id: 6, len: 'Year', win: '1d', full: '1 day', delta: [1, 'years'], format: 'M/D'},
{id: 7, len: 'Year', win: '30d', full: '30 day', delta: [1, 'years'], format: 'M'},
];
const units = {
'PM10': ' ug/m³',
'PM2.5': ' ug/m³',
'VOC': ' / 500',
'CO2': ' ppm',
'Energy': ' kWh',
'Power': ' W',
'Temperature': ' °C',
'Humidity': '%',
'Setpoint': ' °C',
'State': '',
'Lux': ' lx',
};
function useSensor(measurement, name, end, duration) {
const [data, setData] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
const get = async() => {
setLoading(true);
try {
const params = { end: end.unix(), duration: duration.len.toLowerCase(), window: duration.win };
const res = await axios.get(
'https://sensors-api.dns.t0.vc/history/'+measurement+'/'+name,
{ params: params },
);
setData((d) => (res.data));
setLoading(false);
} catch (error) {
console.log(error);
}
};
get();
}, [end, duration]);
const memoConvertTZ = (isDST, timeStr, format) => {
if (!timeStr) return '?';
let lookUp, result = null;
const date = timeStr.slice(5, 10);
const hours = timeStr.slice(11, 13);
const minutes = timeStr.slice(14, 16);
if (format === 'HH') {
lookUp = [isDST, hours, format];
} else {
lookUp = [isDST, date, format];
}
if (tzcache[lookUp] != undefined ) {
result = tzcache[lookUp];
} else {
result = moment(timeStr).tz('America/Edmonton').format(format);
tzcache[lookUp] = result;
}
if (format === 'HH') {
return result + ':' + minutes;
} else {
return result;
}
};
const isDST = end.tz('America/Edmonton').isDST();
const tickFormatter = (timeStr) => memoConvertTZ(isDST, timeStr, duration.format);
return [data, loading, tickFormatter];
};
function ChartContainer({name, data, lastFormatter, loading, children, topMargin}) {
topMargin = topMargin || 5;
if (!data) {
return (
<>
<h2>{name}</h2>
<p>Loading...</p>
</>
);
}
if (data.length === 0) {
return false;
}
const dataGood = (x) => !['undefined', 'null'].some(y => lastFormatter(x).includes(y));
let last = null;
if (data.length) {
const data_end = data.slice(-2);
if (dataGood(data_end[1])) {
last = lastFormatter(data_end[1]);
} else if (dataGood(data_end[0])) {
last = lastFormatter(data_end[0]);
}
}
return (
<div className='chart'>
<h2>{name}: {loading ? 'Loading...' : last || 'No data'}</h2>
<ResponsiveContainer width='100%' height={300}>
<ComposedChart syncId={1} data={data} margin={{ top: topMargin, left: 0, right: 30, bottom: 0 }}>
{children}
</ComposedChart>
</ResponsiveContainer>
</div>
);
}
function SolarPower({end, duration}) {
const [data, loading, tickFormatter] = useSensor('solar', 'Solar', end, duration);
return (
<ChartContainer
name='Solar Power'
data={data}
lastFormatter={(x) => x.actual_total + ' W'}
loading={loading}
topMargin={25}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={[0, 10]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
domain={[0, 6000]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='left' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue'>
<Label value='Midnight' offset={7} position='top' />
</ReferenceLine>
<Bar
yAxisId='right'
type='monotone'
dataKey='lifetime_energy'
name='Energy'
fill='green'
isAnimationActive={false}
/>
<Line
yAxisId='left'
type='monotone'
dataKey='actual_total'
name='Power'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function OutsideTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Outside', end, duration);
return (
<ChartContainer
name='Outside Temperature'
data={data}
lastFormatter={(x) => x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
domain={[-40, 40]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={v => v.toFixed(1) + ' °C'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine y={0} stroke='purple'>
<Label value='Freezing' offset={7} position='bottom' />
</ReferenceLine>
<ReferenceLine x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
type='monotone'
dataKey='temperature_C'
name='Temperature'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function NookTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Nook', end, duration);
return (
<ChartContainer
name='Nook Temperature'
data={data}
lastFormatter={(x) => x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={[0, 100]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
domain={[15, 30]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='left' y={0} stroke='blue'>
<Label value='Freezing' offset={7} position='bottom' />
</ReferenceLine>
<ReferenceLine yAxisId='left' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
yAxisId='left'
type='monotone'
dataKey='temperature_C'
name='Temperature'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='right'
type='monotone'
dataKey='humidity'
name='Humidity'
stroke='blue'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function BedroomTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Bedroom', end, duration);
return (
<ChartContainer
name='Bedroom Temperature'
data={data}
lastFormatter={(x) => x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={[0, 100]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
domain={[15, 30]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='left' y={0} stroke='blue'>
<Label value='Freezing' offset={7} position='bottom' />
</ReferenceLine>
<ReferenceLine yAxisId='left' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
yAxisId='left'
type='monotone'
dataKey='temperature_C'
name='Temperature'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='right'
type='monotone'
dataKey='humidity'
name='Humidity'
stroke='blue'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function SeedsTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Seeds', end, duration);
return (
<ChartContainer
name='Garden Temperature'
data={data}
lastFormatter={(x) => x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={[0, 100]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
domain={[15, 30]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='left' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
type='monotone'
dataKey='temperature_C'
yAxisId='left'
name='Temperature'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
type='monotone'
dataKey='humidity'
yAxisId='right'
name='Humidity'
stroke='blue'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function Thermostat({end, duration}) {
const [data, loading, tickFormatter] = useSensor('thermostat', 'Venstar', end, duration);
return (
<ChartContainer
name='Nook Thermostat'
data={data}
lastFormatter={(x) => x.spacetemp?.toFixed(1) + ' °C'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={[0, 6]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
domain={[12, 30]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='left' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
yAxisId='right'
type='monotone'
dataKey='state'
name='State'
stroke='green'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='left'
type='monotone'
dataKey='heattemp'
name='Setpoint'
stroke='red'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='left'
type='monotone'
dataKey='spacetemp'
name='Temperature'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function Gas({end, duration}) {
const [data, loading, tickFormatter] = useSensor('ertscm', 'Gas', end, duration);
return (
<ChartContainer
name='Gas Usage'
data={data}
lastFormatter={(x) => (x.max / 1000)?.toFixed(1) + ' GJ'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={data.length ? [data[0].consumption_data, data.slice(-1)[0].consumption_data] : [100, 0]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={v => v.toFixed(1) + ' MJ'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='right' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Bar
yAxisId='left'
type='monotone'
dataKey='delta'
name='Delta'
fill='green'
isAnimationActive={false}
/>
<Line
yAxisId='right'
type='monotone'
dataKey='max'
name='Total'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function Water({end, duration}) {
const [data, loading, tickFormatter] = useSensor('ertscm', 'Water', end, duration);
return (
<ChartContainer
name='Water Usage'
data={data}
lastFormatter={(x) => (x.max / 1000)?.toFixed(1) + ' m³'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='right'
domain={data.length ? [data[0].consumption_data, data.slice(-1)[0].consumption_data] : [100, 0]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='left'
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={v => v + ' L'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='right' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Bar
yAxisId='left'
type='monotone'
dataKey='delta'
name='Delta'
fill='green'
isAnimationActive={false}
/>
<Line
yAxisId='right'
type='monotone'
dataKey='max'
name='Total'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function LivingRoomDust({end, duration}) {
const [data, loading, tickFormatter] = useSensor('dust', 'Living Room', end, duration);
return (
<ChartContainer
name='Living Room Dust'
data={data}
lastFormatter={(x) => x.max_p10?.toFixed(1) + ' ug/m³'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
domain={[0, 20]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={v => v.toFixed(1) + ' ug/m³'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
type='monotone'
dataKey='max_p10'
name='PM10'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function LivingRoomAir({end, duration}) {
const [data, loading, tickFormatter] = useSensor('air', 'Living Room', end, duration);
return (
<ChartContainer
name='Living Room Air'
data={data}
lastFormatter={(x) => x.max_p10?.toFixed(1) + ' ug/m³'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='co2'
domain={[400, 1000]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='voc'
domain={[0, 250]}
orientation='right'
hide={true}
/>
<YAxis
yAxisId='pm'
domain={[0, 20]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='pm' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
yAxisId='pm'
type='monotone'
dataKey='max_p10'
name='PM10'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='pm'
type='monotone'
dataKey='max_p25'
name='PM2.5'
stroke='red'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='co2'
type='monotone'
dataKey='max_co2'
name='CO2'
stroke='blue'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
<Line
yAxisId='voc'
type='monotone'
dataKey='max_voc'
name='VOC'
stroke='green'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function BedroomSleep({end, duration}) {
const [data, loading, tickFormatter] = useSensor('sleep', 'Bedroom', end, duration);
return (
<ChartContainer
name='Sleep Movement'
data={data}
lastFormatter={(x) => x.max_mag?.toFixed(1) + ' m/s²'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
domain={[5, 20]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={v => v.toFixed(1) + ' m/s²'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
type='monotone'
dataKey='max_mag'
name='Movement'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function LivingRoomLux({end, duration}) {
const [data, loading, tickFormatter] = useSensor('lux', 'Living Room', end, duration);
return (
<ChartContainer
name='Living Room Lux'
data={data}
lastFormatter={(x) => x.lux?.toFixed(1) + ' lx'}
loading={loading}
>
<XAxis
dataKey='time'
minTickGap={10}
tickFormatter={tickFormatter}
/>
<YAxis
yAxisId='lux'
domain={[0, 250]}
/>
<CartesianGrid strokeDasharray='3 3'/>
<Tooltip
formatter={(v, name) => v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
<ReferenceLine yAxisId='lux' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
<Line
yAxisId='lux'
type='monotone'
dataKey='lux'
name='Lux'
stroke='black'
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
</ChartContainer>
);
}
function Graphs({end, duration}) {
return (
<div className='container'>
<SolarPower end={end} duration={duration} />
<LivingRoomDust end={end} duration={duration} />
<LivingRoomAir end={end} duration={duration} />
<OutsideTemperature end={end} duration={duration} />
<BedroomTemperature end={end} duration={duration} />
<NookTemperature end={end} duration={duration} />
<SeedsTemperature end={end} duration={duration} />
<Thermostat end={end} duration={duration} />
<Gas end={end} duration={duration} />
<Water end={end} duration={duration} />
<BedroomSleep end={end} duration={duration} />
<LivingRoomLux end={end} duration={duration} />
</div>
);
}
function Menu({duration, setDuration, end, setEnd}) {
const [submenu, setSubmenu] = useState(false);
const chooseDuration = (x) => {
setSubmenu(false);
setDuration(x);
};
const chooseEnd = (x) => {
setSubmenu(false);
const newEnd = x.add(...duration.delta);
setEnd(newEnd);
};
const chooseNow = (x) => {
setSubmenu(false);
setEnd(moment());
};
const next = () => {
setSubmenu(false);
setEnd(prevEnd => moment(prevEnd).add(...duration.delta));
}
const prev = () => {
setSubmenu(false);
setEnd(prevEnd => moment(prevEnd).subtract(...duration.delta));
}
return (
<div className='menu'>
{!!submenu &&<div className='submenu'>
{submenu === 'end' &&
<>
<div className='submenu-header'>
<h2>Choose start date:</h2>
<button onClick={() => setSubmenu(false)}>×</button>
</div>
<div className='datepicker'>
<Datetime
input={false}
timeFormat={false}
onChange={(x) => chooseEnd(x)}
/>
</div>
<button onClick={chooseNow}>Jump to Now</button>
</>
}
{submenu === 'duration' &&
<>
<div className='submenu-header'>
<h2>Choose duration:</h2>
<button onClick={() => setSubmenu(false)}>×</button>
</div>
{durations.map(x =>
<button key={x.id} onClick={() => chooseDuration(x)}>Last {x.len} / {x.full} data</button>
)}
</>
}
</div>}
<div className='menu-container'>
<button onClick={() => prev()}>&lt;</button>
<button
onClick={() => setSubmenu('end')}
className={submenu === 'end' ? 'active' : ''}
>
{moment(end).subtract(duration.delta[0], duration.delta[1]).format('ddd MMM DD')}
</button>
<button
onClick={() => setSubmenu('duration')}
className={submenu === 'duration' ? 'active' : ''}
>
{duration.len} / {duration.win}
</button>
<button onClick={() => next()}>&gt;</button>
</div>
</div>
);
}
function App() {
const [duration, setDuration] = useState(durations[0]);
const [end, setEnd] = useState(moment());
useEffect(() => {
const updateEnd = () => {
setEnd(prevEnd => moment(prevEnd).add(1, 'minutes'));
}
const interval = setInterval(updateEnd, 60000);
return () => clearInterval(interval);
}, []);
return (
<div>
<Menu
duration={duration}
setDuration={setDuration}
end={end}
setEnd={setEnd}
/>
<Graphs
end={end}
duration={duration}
/>
</div>
);
}
export default App;