Compare commits
111 Commits
c693d30394
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 34f21791cc | |||
| cd547a15e6 | |||
| 900e31de9d | |||
| 6d1a1e7c78 | |||
| c959321c7b | |||
| 182c42de88 | |||
| d809c33f87 | |||
| 502c18f434 | |||
| a37f446375 | |||
| 6f80297ac7 | |||
| aa3c10fab8 | |||
| 15fcc68f76 | |||
| 3ad9ec9b3d | |||
| 4a19599162 | |||
| 1003de33f2 | |||
| aa40a3b1c1 | |||
| c8b9d2b8bd | |||
| 6df3446fca | |||
| 24a65b7f79 | |||
| 2ef752dc75 | |||
| f1938509d7 | |||
| 7b07a8049b | |||
| 316ea7bf86 | |||
| 109c877e5d | |||
| e53287be96 | |||
| 9683110604 | |||
| 0c107f86b6 | |||
| 7ec2a638a2 | |||
| 53acb33a56 | |||
| 0a7f29e1d0 | |||
| 5bc64bec13 | |||
| 9807187bc7 | |||
| 18c74bedf1 | |||
| e1fcd77180 | |||
| 360252151a | |||
| dc9872ebb8 | |||
| 8cdbb94878 | |||
| de7d9e45b9 | |||
| d0a5461073 | |||
| 1a3c11b5bb | |||
| 25e57edf39 | |||
| b4b840bf9c | |||
| 84b3ca1efd | |||
| 7572520c96 | |||
| fa53b50fbf | |||
| a568bf2f57 | |||
| d9f539f314 | |||
| 72c56c8245 | |||
| 04f64a0fe4 | |||
| 7bad27402a | |||
| 3e6782529d | |||
| 25d6a8757b | |||
| b585a39dd0 | |||
| a3c7f85302 | |||
| 646ca1268e | |||
| 562c7cb6eb | |||
| cb8129cbba | |||
| 502ae2b982 | |||
| 1f744216ec | |||
| ec7fbed514 | |||
| 3d927c18ce | |||
| f309c0af00 | |||
| 7e0eddaf38 | |||
| 1875d7b4e7 | |||
| 959e1d85d0 | |||
| 2be0dd1c3d | |||
| 0708301396 | |||
| fbc15bb371 | |||
| ca3202f9b7 | |||
| 435db835e9 | |||
| 87e706c223 | |||
| 478dca185e | |||
| b295c3fef0 | |||
| 13b35e1c00 | |||
| 2adc0a9fcb | |||
| 0a02db9a8d | |||
| bdc2921bc0 | |||
| 9dd772839b | |||
| 4bc88e5ce9 | |||
| 6c7dff2d8f | |||
| 21cec132a7 | |||
| 51031e7b20 | |||
| 81880a6a0a | |||
| 44dcc1b8ad | |||
| 17b1f979a9 | |||
| 00d9ee362f | |||
| 578bed681a | |||
| 816624ec44 | |||
| 1052cf9bb9 | |||
| 3c8393b14c | |||
| d5f5e08a3c | |||
| f93e6d2323 | |||
| 24bada26a4 | |||
| 4202e1a19d | |||
| ecd1dab005 | |||
| 54e169bdd2 | |||
| 49f9ee120b | |||
| fa8f2cddb5 | |||
| 7b15b39d5f | |||
| e5dbb0af39 | |||
| e549afce96 | |||
| 45272a6242 | |||
| 61fd657952 | |||
| 88dbba168c | |||
| ba630b6fb9 | |||
| 34f0444de7 | |||
| 8abb15cdd3 | |||
| 1346171618 | |||
| e38164fd43 | |||
| b275305434 | |||
| 1d5f63f86d |
@@ -104,3 +104,4 @@ ENV/
|
||||
|
||||
settings.py
|
||||
*.csv
|
||||
.aider*
|
||||
|
||||
+124
-274
@@ -31,6 +31,7 @@ const units = {
|
||||
'Setpoint': ' °C',
|
||||
'State': '',
|
||||
'Lux': ' lx',
|
||||
'Soil': '',
|
||||
};
|
||||
|
||||
function useSensor(measurement, name, end, duration) {
|
||||
@@ -41,7 +42,8 @@ function useSensor(measurement, name, end, duration) {
|
||||
const get = async() => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = { end: end.unix(), duration: duration.len.toLowerCase(), window: duration.win };
|
||||
const api_key = localStorage.getItem('api_key', 'null');
|
||||
const params = { end: end.unix(), duration: duration.len.toLowerCase(), window: duration.win, api_key: api_key };
|
||||
const res = await axios.get(
|
||||
'https://sensors-api.dns.t0.vc/history/'+measurement+'/'+name,
|
||||
{ params: params },
|
||||
@@ -107,7 +109,7 @@ function ChartContainer({name, data, lastFormatter, loading, children, topMargin
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataGood = (x) => !['undefined', 'null'].some(y => lastFormatter(x).includes(y));
|
||||
const dataGood = (x) => !['undefined', 'null'].some(y => String(lastFormatter(x)).includes(y));
|
||||
let last = null;
|
||||
if (data.length) {
|
||||
const data_end = data.slice(-2);
|
||||
@@ -194,12 +196,12 @@ function SolarPower({end, duration}) {
|
||||
);
|
||||
}
|
||||
|
||||
function OutsideTemperature({end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('temperature', 'Outside', end, duration);
|
||||
function Temperature({name, sensorName, end, duration, yDomain, showHumidity, showFreezingLine}) {
|
||||
const [data, loading, tickFormatter] = useSensor('temperature', sensorName, end, duration);
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
name='Outside Temperature'
|
||||
name={name}
|
||||
data={data}
|
||||
lastFormatter={(x) => x.temperature_C?.toFixed(1) + ' °C'}
|
||||
loading={loading}
|
||||
@@ -209,62 +211,18 @@ function OutsideTemperature({end, duration}) {
|
||||
minTickGap={10}
|
||||
tickFormatter={tickFormatter}
|
||||
/>
|
||||
{showHumidity &&
|
||||
<YAxis
|
||||
yAxisId='right'
|
||||
domain={[0, 100]}
|
||||
orientation='right'
|
||||
hide={true}
|
||||
/>
|
||||
}
|
||||
<YAxis
|
||||
domain={[-40, 40]}
|
||||
yAxisId={showHumidity ? 'left' : undefined}
|
||||
domain={yDomain}
|
||||
/>
|
||||
<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]}
|
||||
@@ -272,14 +230,16 @@ function NookTemperature({end, duration}) {
|
||||
separator=': '
|
||||
/>
|
||||
|
||||
<ReferenceLine yAxisId='left' y={0} stroke='blue'>
|
||||
<Label value='Freezing' offset={7} position='bottom' />
|
||||
</ReferenceLine>
|
||||
{showFreezingLine &&
|
||||
<ReferenceLine yAxisId={showHumidity ? 'left' : undefined} y={0} stroke='purple'>
|
||||
<Label value='Freezing' offset={7} position='bottom' />
|
||||
</ReferenceLine>
|
||||
}
|
||||
|
||||
<ReferenceLine yAxisId='left' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
|
||||
<ReferenceLine yAxisId={showHumidity ? 'left' : undefined} x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
|
||||
|
||||
<Line
|
||||
yAxisId='left'
|
||||
yAxisId={showHumidity ? 'left' : undefined}
|
||||
type='monotone'
|
||||
dataKey='temperature_C'
|
||||
name='Temperature'
|
||||
@@ -288,207 +248,22 @@ function NookTemperature({end, duration}) {
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
|
||||
<Line
|
||||
yAxisId='right'
|
||||
type='monotone'
|
||||
dataKey='humidity'
|
||||
name='Humidity'
|
||||
stroke='blue'
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
{showHumidity &&
|
||||
<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 MiscTemperature({end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('temperature', 'Misc', end, duration);
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
name='Misc 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={[-40, 40]}
|
||||
/>
|
||||
|
||||
<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 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);
|
||||
@@ -718,12 +493,12 @@ function LivingRoomDust({end, duration}) {
|
||||
);
|
||||
}
|
||||
|
||||
function LivingRoomAir({end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('air', 'Living Room', end, duration);
|
||||
function Air({name, sensorName, end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('air', sensorName, end, duration);
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
name='Living Room Air'
|
||||
name={name}
|
||||
data={data}
|
||||
lastFormatter={(x) => x.max_p10?.toFixed(1) + ' ug/m³'}
|
||||
loading={loading}
|
||||
@@ -807,6 +582,8 @@ function LivingRoomAir({end, duration}) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function BedroomSleep({end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('sleep', 'Bedroom', end, duration);
|
||||
|
||||
@@ -847,12 +624,12 @@ function BedroomSleep({end, duration}) {
|
||||
);
|
||||
}
|
||||
|
||||
function LivingRoomLux({end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('lux', 'Living Room', end, duration);
|
||||
function Lux({name, sensorName, end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('lux', sensorName, end, duration);
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
name='Living Room Lux'
|
||||
name={name}
|
||||
data={data}
|
||||
lastFormatter={(x) => x.lux?.toFixed(1) + ' lx'}
|
||||
loading={loading}
|
||||
@@ -892,22 +669,95 @@ function LivingRoomLux({end, duration}) {
|
||||
}
|
||||
|
||||
|
||||
function Soil({name, sensorName, end, duration}) {
|
||||
const [data, loading, tickFormatter] = useSensor('soil', sensorName, end, duration);
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
name={name}
|
||||
data={data}
|
||||
lastFormatter={(x) => x.soil?.toFixed(1)}
|
||||
loading={loading}
|
||||
>
|
||||
<XAxis
|
||||
dataKey='time'
|
||||
minTickGap={10}
|
||||
tickFormatter={tickFormatter}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
yAxisId='soil'
|
||||
domain={[0, 1000]}
|
||||
/>
|
||||
|
||||
<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='soil' x={moment().tz('America/Edmonton').startOf('day').toISOString().replace('.000', '')} stroke='blue' />
|
||||
|
||||
<Line
|
||||
yAxisId='soil'
|
||||
type='monotone'
|
||||
dataKey='soil'
|
||||
name='Soil'
|
||||
stroke='black'
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function Graphs({end, duration}) {
|
||||
const api_key = localStorage.getItem('api_key', false);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const api_key = e.target[0].value;
|
||||
localStorage.setItem('api_key', api_key);
|
||||
}
|
||||
|
||||
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} />
|
||||
<MiscTemperature end={end} duration={duration} />
|
||||
<Air name='Living Room Air' sensorName='Living Room' end={end} duration={duration} />
|
||||
<Air name='Kitchen Air' sensorName='Kitchen' end={end} duration={duration} />
|
||||
<Air name='Bedroom Air' sensorName='Bedroom' end={end} duration={duration} />
|
||||
<Air name='Laundry Room Air' sensorName='Laundry Room' end={end} duration={duration} />
|
||||
<Temperature name='Outside Temperature' sensorName='Outside' end={end} duration={duration} yDomain={[-40, 40]} showHumidity={false} showFreezingLine={true} />
|
||||
<Temperature name='Bedroom Temperature' sensorName='Bedroom' end={end} duration={duration} yDomain={[15, 30]} showHumidity={true} showFreezingLine={false} />
|
||||
<Temperature name='Nook Temperature' sensorName='Nook' end={end} duration={duration} yDomain={[15, 30]} showHumidity={true} showFreezingLine={false} />
|
||||
<Temperature name='Garden Temperature' sensorName='Seeds' end={end} duration={duration} yDomain={[15, 30]} showHumidity={true} showFreezingLine={false} />
|
||||
<Temperature name='Misc Temperature' sensorName='Misc' end={end} duration={duration} yDomain={[15, 30]} showHumidity={true} showFreezingLine={false} />
|
||||
<Temperature name='Basement Temperature' sensorName='Basement' end={end} duration={duration} yDomain={[15, 30]} showHumidity={true} showFreezingLine={false} />
|
||||
<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} />
|
||||
<Lux name='Living Room Lux' sensorName='Living Room' end={end} duration={duration} />
|
||||
<Lux name='Kitchen Lux' sensorName='Kitchen' end={end} duration={duration} />
|
||||
<Lux name='Bedroom Lux' sensorName='Bedroom' end={end} duration={duration} />
|
||||
<Lux name='Laundry Room Lux' sensorName='Laundry Room' end={end} duration={duration} />
|
||||
<Soil name='Dumb Cane Soil Moisture' sensorName='Dumb Cane' end={end} duration={duration} />
|
||||
<Soil name='Kitchen Pothos Soil Moisture' sensorName='Kitchen Pothos' end={end} duration={duration} />
|
||||
<Soil name='Dracaena Soil Moisture' sensorName='Dracaena' end={end} duration={duration} />
|
||||
|
||||
{!!api_key ||
|
||||
<div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<p>
|
||||
<input placeholder='API key' />
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+4153
-3354
File diff suppressed because it is too large
Load Diff
+56
-29
@@ -1,50 +1,77 @@
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.chrome.service import Service
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.common.exceptions import NoSuchElementException, WebDriverException
|
||||
from webdriver_manager.chrome import ChromeDriverManager
|
||||
import undetected_chromedriver as uc
|
||||
|
||||
print('Sleeping 10s before loading page...')
|
||||
time.sleep(10)
|
||||
|
||||
ser = Service('/usr/lib/chromium-browser/chromedriver')
|
||||
chrome_options = Options()
|
||||
chrome_options = uc.ChromeOptions()
|
||||
chrome_options.add_argument('--headless')
|
||||
chrome_options.add_argument('start-maximized')
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
driver = webdriver.Chrome(service=ser, options=chrome_options)
|
||||
driver.get('https://sensors.dns.t0.vc')
|
||||
time.sleep(3)
|
||||
|
||||
driver.execute_script("return document.getElementsByClassName('menu')[0].remove();")
|
||||
|
||||
graphs = [
|
||||
'Solar_Power',
|
||||
'Living_Room_Air',
|
||||
'Outside_Temperature',
|
||||
'Bedroom_Temperature',
|
||||
'Nook_Temperature',
|
||||
'Misc_Temperature',
|
||||
'Nook_Thermostat',
|
||||
'Gas_Usage',
|
||||
'Water_Usage',
|
||||
'Living_Room_Lux',
|
||||
]
|
||||
try:
|
||||
driver = uc.Chrome(service=ser, options=chrome_options)
|
||||
except WebDriverException as e:
|
||||
print('Wrong chrome driver version, extracting correct version...')
|
||||
tb = traceback.format_exc()
|
||||
version_string = tb.split('Current browser version is ')[1]
|
||||
major_version = version_string.split('.')[0]
|
||||
print('Trying version:', major_version)
|
||||
|
||||
for graph in graphs:
|
||||
print('Capturing', graph, 'graph...')
|
||||
chrome_options = uc.ChromeOptions()
|
||||
chrome_options.add_argument('--headless')
|
||||
chrome_options.add_argument('start-maximized')
|
||||
chrome_options.add_argument('--no-sandbox')
|
||||
driver = uc.Chrome(service=ser, options=chrome_options, version_main=int(major_version))
|
||||
|
||||
|
||||
try:
|
||||
element = driver.find_element(By.ID, graph)
|
||||
except NoSuchElementException:
|
||||
print('Graph not found, skipping.')
|
||||
continue
|
||||
try:
|
||||
driver.get('https://sensors.dns.t0.vc')
|
||||
time.sleep(3)
|
||||
|
||||
driver.execute_script('arguments[0].scrollIntoView({block: "center"});', element)
|
||||
driver.execute_script("return document.getElementsByClassName('menu')[0].remove();")
|
||||
|
||||
with open('/home/tanner/sensors/export/data/{}.png'.format(graph), 'wb') as f:
|
||||
f.write(element.screenshot_as_png)
|
||||
graphs = [
|
||||
'Solar_Power',
|
||||
'Living_Room_Air',
|
||||
'Outside_Temperature',
|
||||
'Bedroom_Temperature',
|
||||
'Nook_Temperature',
|
||||
#'Misc_Temperature',
|
||||
'Basement_Temperature',
|
||||
'Nook_Thermostat',
|
||||
'Gas_Usage',
|
||||
'Water_Usage',
|
||||
'Living_Room_Lux',
|
||||
]
|
||||
|
||||
for graph in graphs:
|
||||
print('Capturing', graph, 'graph...')
|
||||
|
||||
try:
|
||||
element = driver.find_element(By.ID, graph)
|
||||
except NoSuchElementException:
|
||||
print('Graph not found, skipping.')
|
||||
continue
|
||||
|
||||
driver.execute_script('arguments[0].scrollIntoView({block: "center"});', element)
|
||||
|
||||
with open('/home/tanner/sensors/export/data/{}.png'.format(graph), 'wb') as f:
|
||||
f.write(element.screenshot_as_png)
|
||||
|
||||
finally:
|
||||
driver.close()
|
||||
driver.quit()
|
||||
|
||||
driver.close()
|
||||
print('done.')
|
||||
|
||||
|
||||
@@ -19,10 +19,11 @@ import json
|
||||
import time
|
||||
import requests
|
||||
from aiohttp import web, ClientSession, ClientError
|
||||
from asyncio_mqtt import Client
|
||||
import aiomqtt
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
TIMEZONE = pytz.timezone('America/Edmonton')
|
||||
import hashlib
|
||||
|
||||
app = web.Application()
|
||||
http_session = None
|
||||
@@ -33,6 +34,7 @@ solar_client = InfluxDBClient('localhost', 8086, database='solar2')
|
||||
PORT = 6903 if PROD else 6904
|
||||
|
||||
def controller_message(message):
|
||||
logging.info('Sending controller message: %s', message)
|
||||
payload = dict(home=message)
|
||||
r = requests.post('https://tbot.tannercollin.com/message', data=payload, timeout=10)
|
||||
if r.status_code == 200:
|
||||
@@ -78,8 +80,10 @@ class Sensor():
|
||||
value = {}
|
||||
prev_value = {}
|
||||
bad_keys = []
|
||||
last_update = time.time()
|
||||
last_update = None
|
||||
update_period = None
|
||||
skip_if_hasnt_changed = False
|
||||
skip_cooldown = 1.0
|
||||
|
||||
def __init__(self, id_, name):
|
||||
self.id_ = id_
|
||||
@@ -101,12 +105,33 @@ class Sensor():
|
||||
|
||||
return str(before) != str(after)
|
||||
|
||||
def check_cooldown(self):
|
||||
if self.last_update and self.skip_cooldown and time.time() - self.last_update < self.skip_cooldown:
|
||||
# ignore data point
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def log(self):
|
||||
if not self.value or not self.changed():
|
||||
if not self.value:
|
||||
return
|
||||
|
||||
if not self.changed() and self.skip_if_hasnt_changed:
|
||||
logging.debug('Skipping writing %s, data hasn\'t changed', self)
|
||||
return
|
||||
|
||||
if self.check_cooldown():
|
||||
logging.debug('Skipping writing %s, cooldown limit', self)
|
||||
return
|
||||
|
||||
data = self.value.copy()
|
||||
self.transform(data)
|
||||
|
||||
try:
|
||||
self.transform(data)
|
||||
except BaseException as e:
|
||||
logging.exception('Problem transforming sensor data: {} - {}'.format(e.__class__.__name__, str(e)))
|
||||
logging.error('Data: %s', str(data))
|
||||
return
|
||||
|
||||
for key in self.bad_keys:
|
||||
data.pop(key, None)
|
||||
@@ -123,21 +148,26 @@ class Sensor():
|
||||
'tags': {'id': self.id_, 'name': self.name},
|
||||
'fields': data,
|
||||
}
|
||||
sensors_client.write_points([point])
|
||||
|
||||
try:
|
||||
sensors_client.write_points([point])
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.exception('Error connecting to InfluxDB!')
|
||||
return
|
||||
|
||||
logging.info('Wrote %s data to InfluxDB: %s', self, data)
|
||||
|
||||
def check_update(self):
|
||||
if self.update_period:
|
||||
if self.update_period and self.last_update:
|
||||
if time.time() - self.last_update > self.update_period:
|
||||
logging.warning('Missed expected update from %s.', self)
|
||||
self.last_update = time.time()
|
||||
|
||||
def update(self, data):
|
||||
self.last_update = time.time()
|
||||
self.prev_value = self.value
|
||||
self.value = data
|
||||
self.log()
|
||||
self.last_update = time.time()
|
||||
|
||||
async def poll(self):
|
||||
return
|
||||
@@ -156,6 +186,7 @@ class ThermostatSensor(Sensor):
|
||||
'dehum_setpoint'
|
||||
]
|
||||
update_period = 300
|
||||
skip_if_hasnt_changed = True
|
||||
|
||||
def __init__(self, id_, ip, name):
|
||||
self.id_ = id_
|
||||
@@ -174,6 +205,11 @@ class ERTSCMSensor(Sensor):
|
||||
]
|
||||
update_period = 60*60
|
||||
|
||||
def transform(self, data):
|
||||
# new gas meter
|
||||
if 'Consumption' in data:
|
||||
data['consumption_data'] = data['Consumption']
|
||||
|
||||
class OwnTracksSensor(Sensor):
|
||||
type_ = 'owntracks'
|
||||
bad_keys = [
|
||||
@@ -183,6 +219,7 @@ class OwnTracksSensor(Sensor):
|
||||
'created_at',
|
||||
]
|
||||
update_period = 90
|
||||
skip_cooldown = False
|
||||
|
||||
class DustSensor(Sensor):
|
||||
type_ = 'dust'
|
||||
@@ -220,6 +257,44 @@ class SleepSensor(Sensor):
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
class SoilSensor(Sensor):
|
||||
type_ = 'soil'
|
||||
update_period = 90
|
||||
|
||||
def transform(self, data):
|
||||
for key, value in data.items():
|
||||
# what happens if you do this to a timestamp?
|
||||
try:
|
||||
data[key] = float(round(value, 1))
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
class WH51SoilSensor(Sensor):
|
||||
type_ = 'soil'
|
||||
bad_keys = [
|
||||
'model',
|
||||
'mic',
|
||||
'battery_ok',
|
||||
]
|
||||
update_period = 60*30
|
||||
|
||||
class P2ProScaleSensor(Sensor):
|
||||
type_ = 'scale'
|
||||
skip_cooldown = 60.0 * 60 * 18 # 18 hours
|
||||
|
||||
def check_cooldown(self):
|
||||
if 'weight' not in self.value:
|
||||
return False
|
||||
|
||||
skip = super().check_cooldown()
|
||||
|
||||
if skip:
|
||||
controller_message('Cooldown skipping scale weight: ' + str(self.value['weight']))
|
||||
|
||||
return skip
|
||||
|
||||
|
||||
|
||||
class SolarSensor(Sensor):
|
||||
type_ = 'solar'
|
||||
|
||||
@@ -239,7 +314,7 @@ class Acurite606TX(Sensor):
|
||||
self.offset = offset
|
||||
|
||||
def transform(self, data):
|
||||
if data['battery_ok'] != 1:
|
||||
if data.get('battery_ok', None) != 1:
|
||||
logging.error('%s battery not ok!', self)
|
||||
data['temperature_C'] = float(data['temperature_C']) + self.offset
|
||||
|
||||
@@ -254,16 +329,29 @@ class AcuRite6002RM(Sensor):
|
||||
update_period = 40
|
||||
offset = 0.0
|
||||
|
||||
def __init__(self, id_, name, offset=0.0):
|
||||
def __init__(self, id_, name, temp_offset=0.0, hum_offset=0.0):
|
||||
self.id_ = id_
|
||||
self.name = name
|
||||
self.offset = offset
|
||||
self.temp_offset = temp_offset
|
||||
self.hum_offset = hum_offset
|
||||
|
||||
def transform(self, data):
|
||||
if data['battery_ok'] != 1:
|
||||
if data.get('battery_ok', None) != 1:
|
||||
logging.error('%s battery not ok!', self)
|
||||
data['temperature_C'] = float(data['temperature_C']) + self.offset
|
||||
data['humidity'] = float(data['humidity'])
|
||||
|
||||
data['temperature_C'] = float(data['temperature_C']) + self.temp_offset
|
||||
data['humidity'] = float(data['humidity']) + self.hum_offset
|
||||
|
||||
|
||||
class QotMotionSensor(Sensor):
|
||||
type_ = 'qotmotion'
|
||||
update_period = False
|
||||
|
||||
def transform(self, data):
|
||||
split = data['data'].split(',')
|
||||
data['battery'] = int(split[0])
|
||||
data['boots'] = int(split[1])
|
||||
data['motion'] = True # useful to distinguish if I eventually add a heartbeat
|
||||
|
||||
|
||||
async def poll_sensors():
|
||||
@@ -272,7 +360,7 @@ async def poll_sensors():
|
||||
await sensor.poll()
|
||||
sensor.check_update()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def process_data(data):
|
||||
sensor = sensors.get(data['id'])
|
||||
@@ -280,8 +368,12 @@ async def process_data(data):
|
||||
sensor.update(data)
|
||||
|
||||
async def process_mqtt(message):
|
||||
text = message.payload.decode()
|
||||
topic = message.topic
|
||||
try:
|
||||
text = message.payload.decode()
|
||||
except UnicodeDecodeError:
|
||||
return
|
||||
|
||||
topic = message.topic.value
|
||||
logging.debug('MQTT topic: %s, message: %s', topic, text)
|
||||
|
||||
if topic.startswith('test'):
|
||||
@@ -299,31 +391,68 @@ async def process_mqtt(message):
|
||||
await process_data(data)
|
||||
|
||||
async def fetch_mqtt():
|
||||
async with Client('localhost') as client:
|
||||
async with client.filtered_messages('#') as messages:
|
||||
await client.subscribe('#')
|
||||
async for message in messages:
|
||||
await process_mqtt(message)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# from https://sbtinstruments.github.io/aiomqtt/reconnection.html
|
||||
# modified to make new client since their code didn't work
|
||||
# https://github.com/sbtinstruments/aiomqtt/issues/269
|
||||
while True:
|
||||
try:
|
||||
async with aiomqtt.Client('localhost') as client:
|
||||
await client.subscribe('#')
|
||||
async for message in client.messages:
|
||||
await process_mqtt(message)
|
||||
except aiomqtt.MqttError:
|
||||
logging.info('MQTT connection lost, reconnecting in 5 seconds...')
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def owntracks(request):
|
||||
data = await request.json()
|
||||
logging.debug('Web data: %s', str(data))
|
||||
try:
|
||||
data = await request.json()
|
||||
logging.debug('Web data: %s', str(data))
|
||||
|
||||
if data.get('_type', '') == 'location':
|
||||
data['id'] = data['topic'].split('/')[-1]
|
||||
data['timestamp'] = datetime.utcfromtimestamp(data['tst'])
|
||||
if 'inregions' in data:
|
||||
data['inregions'] = ','.join(data['inregions'])
|
||||
await process_data(data)
|
||||
else:
|
||||
logging.info('Not a location, skipping.')
|
||||
if data.get('_type', '') == 'location':
|
||||
data['id'] = data['topic'].split('/')[-1]
|
||||
data['timestamp'] = datetime.utcfromtimestamp(data['tst'])
|
||||
if 'inregions' in data:
|
||||
data['inregions'] = ','.join(data['inregions'])
|
||||
await process_data(data)
|
||||
else:
|
||||
logging.info('Not a location, skipping.')
|
||||
|
||||
return web.Response()
|
||||
return web.json_response({'status': 'ok'}, status=200)
|
||||
except BaseException as e:
|
||||
logging.exception('Problem with OwnTracks data: {} - {}'.format(e.__class__.__name__, str(e)))
|
||||
try:
|
||||
logging.info('Data: %s', str(data))
|
||||
except:
|
||||
pass
|
||||
return web.json_response({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
def share_sha256(measurement, share_start, share_end, api_key):
|
||||
s = f'{measurement}-{share_start}-{share_end}-{api_key}'
|
||||
return hashlib.sha256(s.encode()).hexdigest()
|
||||
|
||||
async def history(request):
|
||||
api_key = request.rel_url.query.get('api_key', '')
|
||||
authed = api_key == settings.SENSORS_API_KEY
|
||||
|
||||
measurement = request.match_info.get('measurement')
|
||||
name = request.match_info.get('name')
|
||||
|
||||
share_start = request.rel_url.query.get('shareStart', '')
|
||||
share_end = request.rel_url.query.get('shareEnd', '')
|
||||
share_sig = request.rel_url.query.get('shareSig', '')
|
||||
|
||||
share_authed = share_sig == share_sha256(measurement, share_start, share_end, settings.SENSORS_API_KEY)
|
||||
authed = authed or share_authed
|
||||
|
||||
|
||||
if not authed and measurement in ['owntracks', 'sleep']:
|
||||
return web.json_response([])
|
||||
|
||||
if name not in [x.name for x in sensors.sensors]:
|
||||
raise
|
||||
|
||||
@@ -349,6 +478,9 @@ async def history(request):
|
||||
elif duration == 'month':
|
||||
start = end - timedelta(days=30)
|
||||
window = '1d'
|
||||
elif duration == 'quarter':
|
||||
start = end - timedelta(days=90)
|
||||
window = '1d'
|
||||
elif duration == 'year':
|
||||
start = end - timedelta(days=365)
|
||||
window = '1d'
|
||||
@@ -356,7 +488,7 @@ async def history(request):
|
||||
raise
|
||||
|
||||
window = request.rel_url.query.get('window', window)
|
||||
if window not in ['10m', '1h', '1d', '7d', '30d']:
|
||||
if window not in ['1m', '3m', '10m', '30m', '1h', '2h', '1d', '7d', '30d']:
|
||||
raise
|
||||
|
||||
if name == 'Water':
|
||||
@@ -369,6 +501,13 @@ async def history(request):
|
||||
start = int(start.timestamp())
|
||||
end = int(end.timestamp())
|
||||
|
||||
if share_authed:
|
||||
if start <= int(share_start):
|
||||
start = int(share_start)
|
||||
if end >= int(share_end):
|
||||
end = int(share_end)
|
||||
|
||||
|
||||
if measurement == 'temperature':
|
||||
client = sensors_client
|
||||
q = 'select mean("temperature_C") as temperature_C, mean("humidity") as humidity from temperature where "name" = \'{}\' and time >= {}s and time < {}s group by time({}) fill(linear)'.format(name, start, end, window)
|
||||
@@ -384,6 +523,9 @@ async def history(request):
|
||||
elif measurement == 'air':
|
||||
client = sensors_client
|
||||
q = 'select max("pm10") as max_p10, max("pm25") as max_p25, max("co2") as max_co2, max("voc_idx") as max_voc from air where "name" = \'{}\' and time >= {}s and time < {}s group by time({}) fill(linear)'.format(name, start, end, window)
|
||||
elif measurement == 'soil':
|
||||
client = sensors_client
|
||||
q = 'select mean("soil") as soil from soil where "name" = \'{}\' and time >= {}s and time < {}s group by time({}) fill(linear)'.format(name, start, end, window)
|
||||
elif measurement == 'lux':
|
||||
client = sensors_client
|
||||
q = 'select mean("lux") as lux from air where "name" = \'{}\' and time >= {}s and time < {}s group by time({}) fill(linear)'.format(name, start, end, window)
|
||||
@@ -393,6 +535,9 @@ async def history(request):
|
||||
elif measurement == 'solar':
|
||||
client = solar_client
|
||||
q = 'select max("actual_total") as actual_total, last("lifetime_energy")-first("lifetime_energy") as lifetime_energy from ecu where time >= {}s and time < {}s group by time({}) fill(linear)'.format(start, end, window)
|
||||
elif measurement == 'owntracks':
|
||||
client = sensors_client
|
||||
q = 'select first("lat") as lat, first("lon") as lon from owntracks where "acc" < 100 and "name" = \'{}\' and time >= {}s and time < {}s group by time({}) fill(previous)'.format(name, start, end, window)
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -402,6 +547,99 @@ async def history(request):
|
||||
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
async def search(request):
|
||||
api_key = request.rel_url.query.get('api_key', '')
|
||||
authed = api_key == settings.SENSORS_API_KEY
|
||||
|
||||
measurement = request.match_info.get('measurement')
|
||||
name = request.match_info.get('name')
|
||||
|
||||
if not authed:
|
||||
return web.json_response([])
|
||||
|
||||
if name not in [x.name for x in sensors.sensors]:
|
||||
raise
|
||||
|
||||
if measurement != 'owntracks':
|
||||
return web.json_response({'error': 'not implemented for this measurement'}, status=400)
|
||||
|
||||
try:
|
||||
post_data = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return web.json_response({'error': 'invalid json'}, status=400)
|
||||
|
||||
params = request.rel_url.query
|
||||
logging.info('Search request: meas=%s, name=%s, params=%s, data=%s',
|
||||
measurement, name, params, post_data)
|
||||
|
||||
areas = post_data.get('areas')
|
||||
if not areas or not isinstance(areas, list):
|
||||
return web.json_response({'error': 'invalid areas format'}, status=400)
|
||||
|
||||
try:
|
||||
for area in areas:
|
||||
_ = area['southWest']['lat']
|
||||
_ = area['southWest']['lng']
|
||||
_ = area['northEast']['lat']
|
||||
_ = area['northEast']['lng']
|
||||
except (KeyError, TypeError):
|
||||
return web.json_response({'error': 'invalid area format in areas list'}, status=400)
|
||||
|
||||
client = sensors_client
|
||||
|
||||
where_clauses = []
|
||||
for area in areas:
|
||||
sw = area['southWest']
|
||||
ne = area['northEast']
|
||||
where_clauses.append(f'("lat" >= {sw["lat"]} and "lat" <= {ne["lat"]} and "lon" >= {sw["lng"]} and "lon" <= {ne["lng"]})')
|
||||
|
||||
full_where_clause = ' or '.join(where_clauses)
|
||||
|
||||
q = f'select "lat", "lon" from owntracks where "acc" < 100 and "name" = \'{name}\' and ({full_where_clause}) order by time asc'
|
||||
points = list(client.query(q).get_points())
|
||||
|
||||
ranges = []
|
||||
current_range = None
|
||||
last_point_dt = None
|
||||
# Use a 12-hour gap to distinguish between separate visits
|
||||
GAP_THRESHOLD_HOURS = 12
|
||||
|
||||
for point in points:
|
||||
point_time_str = point['time']
|
||||
if '.' in point_time_str:
|
||||
point_dt = datetime.strptime(point_time_str, '%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
else:
|
||||
point_dt = datetime.strptime(point_time_str, '%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
if current_range is None:
|
||||
current_range = {'start': point_dt, 'end': point_dt}
|
||||
else:
|
||||
time_diff_hours = (point_dt - last_point_dt).total_seconds() / 3600
|
||||
if time_diff_hours > GAP_THRESHOLD_HOURS:
|
||||
ranges.append({
|
||||
'start': int(current_range['start'].timestamp()),
|
||||
'end': int(current_range['end'].timestamp())
|
||||
})
|
||||
current_range = {'start': point_dt, 'end': point_dt}
|
||||
else:
|
||||
current_range['end'] = point_dt
|
||||
|
||||
last_point_dt = point_dt
|
||||
|
||||
if current_range is not None:
|
||||
ranges.append({
|
||||
'start': int(current_range['start'].timestamp()),
|
||||
'end': int(current_range['end'].timestamp())
|
||||
})
|
||||
|
||||
return web.json_response(ranges)
|
||||
|
||||
|
||||
async def options_handler(request):
|
||||
return web.Response()
|
||||
|
||||
|
||||
async def latest(request):
|
||||
result = dict()
|
||||
api_key = request.rel_url.query.get('api_key', '')
|
||||
@@ -411,7 +649,7 @@ async def latest(request):
|
||||
if sensor.type_ in ['solar']:
|
||||
continue
|
||||
|
||||
if not authed and sensor.type_ in ['owntracks']:
|
||||
if not authed and sensor.type_ in ['owntracks', 'sleep']:
|
||||
continue
|
||||
|
||||
q = 'select * from {} where "name" = \'{}\' order by desc limit 1'.format(sensor.type_, sensor.name)
|
||||
@@ -426,7 +664,7 @@ async def latest(request):
|
||||
return web.json_response(result)
|
||||
|
||||
async def index(request):
|
||||
return web.Response(text='hello world', content_type='text/html')
|
||||
return web.Response(text='sensors api', content_type='text/html')
|
||||
|
||||
async def run_webserver():
|
||||
#web.run_app(app, port=PORT, loop=loop)
|
||||
@@ -444,7 +682,10 @@ def task_died(future):
|
||||
logging.error('Sensors server task died!')
|
||||
else:
|
||||
logging.error('Sensors server task died! Waiting 60s and exiting...')
|
||||
controller_message('Sensors server task died! Waiting 60s and exiting...')
|
||||
try:
|
||||
controller_message('Sensors server task died! Waiting 60s and exiting...')
|
||||
except: # we want this to succeed no matter what
|
||||
pass
|
||||
time.sleep(60)
|
||||
exit()
|
||||
|
||||
@@ -452,20 +693,45 @@ if __name__ == '__main__':
|
||||
app.router.add_get('/', index)
|
||||
app.router.add_post('/owntracks', owntracks)
|
||||
app.router.add_get('/history/{measurement}/{name}', history)
|
||||
app.router.add_post('/search/{measurement}/{name}', search)
|
||||
app.router.add_route('OPTIONS', '/search/{measurement}/{name}', options_handler)
|
||||
app.router.add_get('/latest', latest)
|
||||
|
||||
# serial, name
|
||||
# API look up is done by name
|
||||
# when retiring / reassigning a serial, change it to something impossible ie. 9999
|
||||
sensors.add(ThermostatSensor('thermostat2', '192.168.69.152', 'Venstar'))
|
||||
sensors.add(ERTSCMSensor('31005493', 'Water'))
|
||||
sensors.add(ERTSCMSensor('41249312', 'Gas'))
|
||||
sensors.add(ERTSCMSensor('78628180', 'Gas'))
|
||||
sensors.add(OwnTracksSensor('owntracks1', 'OwnTracks'))
|
||||
sensors.add(AirSensor('air1', 'Living Room'))
|
||||
sensors.add(Acurite606TX('59', 'Outside'))
|
||||
sensors.add(AirSensor('air9999', 'Living Room'))
|
||||
sensors.add(AirSensor('air1', 'Laundry Room'))
|
||||
sensors.add(AirSensor('air2', 'Bedroom'))
|
||||
sensors.add(AirSensor('air3', 'Kitchen'))
|
||||
sensors.add(Acurite606TX('252', 'Outside', 0.0))
|
||||
sensors.add(AcuRite6002RM('999999', 'Seeds', 0.0)) # A
|
||||
sensors.add(AcuRite6002RM('5613', 'Misc', 0.0)) # A
|
||||
sensors.add(AcuRite6002RM('5109', 'Nook', 0.4)) # B
|
||||
sensors.add(AcuRite6002RM('11087', 'Bedroom', -0.3)) # C
|
||||
sensors.add(AcuRite6002RM('999998', 'Misc', 0.7, -1.0)) # A
|
||||
sensors.add(AcuRite6002RM('12516', 'Basement', 0.7, -1.0)) # A
|
||||
sensors.add(AcuRite6002RM('5109', 'Nook', 0.2, -1.0)) # B
|
||||
sensors.add(AcuRite6002RM('11087', 'Bedroom', -0.7, 1.0)) # C
|
||||
sensors.add(SleepSensor('sleep1', 'Bedroom'))
|
||||
sensors.add(SolarSensor('solar', 'Solar'))
|
||||
sensors.add(SoilSensor('soil1', 'Dumb Cane'))
|
||||
sensors.add(SoilSensor('soil2', 'Kitchen Pothos'))
|
||||
sensors.add(SoilSensor('soil3', 'Dracaena'))
|
||||
sensors.add(WH51SoilSensor('0fafff', 'Side Garden'))
|
||||
sensors.add(P2ProScaleSensor('scale1', 'Master Bathroom'))
|
||||
|
||||
sensors.add(QotMotionSensor('qot_dc3c', 'Bedroom'))
|
||||
sensors.add(QotMotionSensor('qot_88c3', 'Lower Stairs Hi'))
|
||||
sensors.add(QotMotionSensor('qot_7c3c', 'Theatre'))
|
||||
sensors.add(QotMotionSensor('qot_54e6', 'Lab'))
|
||||
sensors.add(QotMotionSensor('qot_10f4', 'Office'))
|
||||
sensors.add(QotMotionSensor('qot_74c3', 'Guest Bathroom'))
|
||||
sensors.add(QotMotionSensor('qot_706f', 'Nook'))
|
||||
sensors.add(QotMotionSensor('qot_8c1c', 'Kitchen S'))
|
||||
sensors.add(QotMotionSensor('qot_a83b', 'Kitchen N'))
|
||||
sensors.add(QotMotionSensor('qot_28c3', 'Side Entrance'))
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
a = loop.create_task(poll_sensors()).add_done_callback(task_died)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "mapper",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"axios": "^0.21.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-draw": "^1.0.4",
|
||||
"leaflet-polylinedecorator": "^1.6.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"react": "^18.0.0",
|
||||
"react-datetime": "^3.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-leaflet-draw": "^0.20.6",
|
||||
"react-range-slider-input": "^3.0.7",
|
||||
"react-scripts": "4.0.3",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Tanner's Mapper</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet/dist/leaflet.css"
|
||||
crossorigin=""/>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
crossorigin=""></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,290 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
max-width: 38em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
font-family: sans-serif;
|
||||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.recharts-wrapper p {
|
||||
color: initial;
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
|
||||
.menu {
|
||||
overflow: hidden;
|
||||
background-color: #333;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.time-slider {
|
||||
padding: 1em 0.5em;
|
||||
}
|
||||
|
||||
.range {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
background-color: #666;
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.submenu h2 {
|
||||
color: white;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.submenu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
max-width: 40em;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu button {
|
||||
background-color: #333;
|
||||
height: 2.5rem;
|
||||
min-width: 3rem;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.menu button:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.menu button.active {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.menu button:disabled {
|
||||
color: #ce7e7e;
|
||||
}
|
||||
|
||||
.submenu button {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.submenu-checkbox-label {
|
||||
/* Make it look like a button */
|
||||
background-color: #666;
|
||||
height: 2.5rem;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
|
||||
/* Center content */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.submenu-checkbox-label:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.submenu-checkbox-label input[type="checkbox"] {
|
||||
/* Reset default styles */
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
|
||||
/* Custom checkbox style */
|
||||
font: inherit;
|
||||
color: currentColor;
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
border: 0.1em solid currentColor;
|
||||
border-radius: 0;
|
||||
transform: translateY(-0.075em);
|
||||
|
||||
/* For the checkmark */
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
/* Fix for Firefox Mobile rendering a black background on checked state,
|
||||
especially when extensions like Dark Reader are active. */
|
||||
.submenu-checkbox-label input[type="checkbox"]:checked {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.submenu-checkbox-label input[type="checkbox"]::before {
|
||||
content: "";
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
transform: scale(0);
|
||||
transition: 120ms transform ease-in-out;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.submenu-checkbox-label input[type="checkbox"]:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.datepicker .rdtPicker {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.datepicker .rdtPicker .rdtDay.rdtDisabled {
|
||||
color: #ce7e7e !important;
|
||||
}
|
||||
|
||||
.datepicker .rdtPicker .rdtDay.rdtNew,
|
||||
.datepicker .rdtPicker .rdtDay.rdtOld {
|
||||
color: #c8c8c8;
|
||||
}
|
||||
|
||||
.datepicker th:hover,
|
||||
.datepicker td:hover {
|
||||
background-color: #999!important;
|
||||
}
|
||||
|
||||
.submenu-actions {
|
||||
padding-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.submenu-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.submenu-group span {
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.submenu-group button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.search-results-container {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-results-group-header {
|
||||
color: white;
|
||||
margin: 0.5em 0 0.25em;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.search-results-empty {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.submenu button.active {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-result-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.merge-button-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 2em;
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.merge-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
z-index: 10;
|
||||
background-color: #888;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.merge-button:hover {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.search-result-button-wrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.search-result-button-wrapper button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
+1138
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
+12099
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,4 +1,5 @@
|
||||
aiohttp==3.8.1
|
||||
aiomqtt==2.0.0
|
||||
aiosignal==1.2.0
|
||||
async-timeout==4.0.2
|
||||
asyncio-mqtt==0.11.0
|
||||
@@ -12,11 +13,10 @@ influxdb==5.3.1
|
||||
msgpack==1.0.3
|
||||
multidict==5.2.0
|
||||
paho-mqtt==1.6.1
|
||||
pkg_resources==0.0.0
|
||||
python-dateutil==2.8.2
|
||||
pytz==2021.3
|
||||
requests==2.26.0
|
||||
six==1.16.0
|
||||
typing_extensions==4.0.1
|
||||
typing-extensions==4.9.0
|
||||
urllib3==1.26.7
|
||||
yarl==1.7.2
|
||||
|
||||
Reference in New Issue
Block a user