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;