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 (
<>
{name}
Loading...
>
);
}
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 (
{name}: {loading ? 'Loading...' : last || 'No data'}
{children}
);
}
function SolarPower({end, duration}) {
const [data, loading, tickFormatter] = useSensor('solar', 'Solar', end, duration);
return (
x.actual_total + ' W'}
loading={loading}
topMargin={25}
>
v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function OutsideTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Outside', end, duration);
return (
x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
v.toFixed(1) + ' °C'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function NookTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Nook', end, duration);
return (
x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function BedroomTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Bedroom', end, duration);
return (
x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function SeedsTemperature({end, duration}) {
const [data, loading, tickFormatter] = useSensor('temperature', 'Seeds', end, duration);
return (
x.temperature_C?.toFixed(1) + ' °C'}
loading={loading}
>
v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function Thermostat({end, duration}) {
const [data, loading, tickFormatter] = useSensor('thermostat', 'Venstar', end, duration);
return (
x.spacetemp?.toFixed(1) + ' °C'}
loading={loading}
>
v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function Gas({end, duration}) {
const [data, loading, tickFormatter] = useSensor('ertscm', 'Gas', end, duration);
return (
(x.max / 1000)?.toFixed(1) + ' GJ'}
loading={loading}
>
v.toFixed(1) + ' MJ'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function Water({end, duration}) {
const [data, loading, tickFormatter] = useSensor('ertscm', 'Water', end, duration);
return (
(x.max / 1000)?.toFixed(1) + ' m³'}
loading={loading}
>
v + ' L'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function LivingRoomDust({end, duration}) {
const [data, loading, tickFormatter] = useSensor('dust', 'Living Room', end, duration);
return (
x.max_p10?.toFixed(1) + ' ug/m³'}
loading={loading}
>
v.toFixed(1) + ' ug/m³'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function LivingRoomAir({end, duration}) {
const [data, loading, tickFormatter] = useSensor('air', 'Living Room', end, duration);
return (
x.max_p10?.toFixed(1) + ' ug/m³'}
loading={loading}
>
v + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function BedroomSleep({end, duration}) {
const [data, loading, tickFormatter] = useSensor('sleep', 'Bedroom', end, duration);
return (
x.max_mag?.toFixed(1) + ' m/s²'}
loading={loading}
>
v.toFixed(1) + ' m/s²'}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function LivingRoomLux({end, duration}) {
const [data, loading, tickFormatter] = useSensor('lux', 'Living Room', end, duration);
return (
x.lux?.toFixed(1) + ' lx'}
loading={loading}
>
v.toFixed(1) + units[name]}
labelFormatter={timeStr => moment(timeStr).tz('America/Edmonton').format('ddd MMM DD h:mm A')}
separator=': '
/>
);
}
function Graphs({end, duration}) {
return (
);
}
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 (
{!!submenu &&
{submenu === 'end' &&
<>
Choose start date:
chooseEnd(x)}
/>
>
}
{submenu === 'duration' &&
<>
Choose duration:
{durations.map(x =>
)}
>
}
}
);
}
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 (
);
}
export default App;