Refactor Overview page, make Settings page to home and add warning when temp or soil values are critical.

This commit is contained in:
Andrés Uribe Stengel 2020-07-23 17:59:36 +02:00
parent 52a5752db7
commit 8da0b54d2b
7 changed files with 250 additions and 160 deletions

View File

@ -1,6 +1,6 @@
import {SensorDataCollection} from "../../client/main"; import {SensorDataCollection} from "../../client/main";
export function getAllEspNames() { export function getAllEspIds() {
return _.uniq(SensorDataCollection.find({}, { return _.uniq(SensorDataCollection.find({}, {
sort: {device_id: 1}, fields: {device_id: true} sort: {device_id: 1}, fields: {device_id: true}
}).fetch().map(function (x) { }).fetch().map(function (x) {

View File

@ -2,7 +2,6 @@ import React, { Suspense } from 'react'
import NavBar from './NavigationBar' import NavBar from './NavigationBar'
import Home from './Home' import Home from './Home'
import Overview from './Overview' import Overview from './Overview'
import Settings from './Settings'
import About from './About' import About from './About'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
@ -20,9 +19,6 @@ function App() {
<Route path="/overview"> <Route path="/overview">
<div className="p-3"><Overview></Overview></div> <div className="p-3"><Overview></Overview></div>
</Route> </Route>
<Route path="/settings">
<div className="p-3"><Settings></Settings></div>
</Route>
<Route path="/about" component={About}></Route> <Route path="/about" component={About}></Route>
</Switch> </Switch>
</Suspense> </Suspense>

View File

@ -1,11 +1,13 @@
import React from 'react' import React from 'react'
import {CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts'; import {
import SensorCardDeck from './SensorCardDeck' SensorDataCollection,
import {SensorDataCollection, ActiveDeviceCollection} from "../../client/main"; ActiveDeviceCollection,
ConfiguredDevicesCollection,
PlantTypesCollection
} from "../../client/main";
import {useTracker} from 'meteor/react-meteor-data'; import {useTracker} from 'meteor/react-meteor-data';
import { Col, Form, Row, Card, CardDeck } from "react-bootstrap"; import { Card, CardDeck, Table } from "react-bootstrap";
import { getAllEspNames } from "../api/espNames"; import Settings from "./Settings";
import moment from 'moment'
export default function Home() { export default function Home() {
const deviceId = useTracker(() => { const deviceId = useTracker(() => {
@ -23,19 +25,36 @@ export default function Home() {
} }
}); });
const handleChange = (e) => { const configuredDevices = useTracker(() => {
if (e.target.value === "") { return ConfiguredDevicesCollection.find().fetch();
console.log("No device selected!"); });
} else {
var doc = ActiveDeviceCollection.findOne({deviceId: deviceId.deviceId}); function getSoil(type) {
ActiveDeviceCollection.update({_id: doc._id}, {$set: {deviceId: e.target.value}}); const plantType = PlantTypesCollection.findOne({plantType: type});
} return plantType.dirtType
} }
const getLabelFromStamp = (x) => { function getPermWilPoint(type) {
return moment(x.timestamp).format("HH:mm"); const plantType = PlantTypesCollection.findOne({plantType: type});
return plantType.data.soilMoisture.pwp
} }
function getFieldCap(type) {
const plantType = PlantTypesCollection.findOne({plantType: type});
return plantType.data.soilMoisture.fc
}
function getMinTemp(type) {
const plantType = PlantTypesCollection.findOne({plantType: type});
return plantType.data.temperature.minTemp
}
function getMinLux(type) {
const plantType = PlantTypesCollection.findOne({plantType: type});
return plantType.data.light.minLux
}
if ((sensorData.length <= 0)) { if ((sensorData.length <= 0)) {
return ( return (
<CardDeck> <CardDeck>
@ -50,84 +69,41 @@ export default function Home() {
} else { } else {
return ( return (
<> <>
<Table striped bordered hover responsive>
<thead>
<tr key={'head'}>
<th key={'#'}>#</th>
<th key={'nickname'}>Device Nickname</th>
<th key={'id'}>Device ID</th>
<th key={'type'}>Plant Type</th>
<th key={'soil'}>Soil Type</th>
<th key={'pwp'}>Permanent Wilting Point</th>
<th key={'fc'}>Field Capacity</th>
<th key={'temp'}>Min Temperature</th>
<th key={'brightness'}>Min Brightness</th>
</tr>
</thead>
<tbody>
{configuredDevices.map((device, index) => {
return <tr key={'body' + index}>
<td key={'#-' + index}>{index}</td>
<td key={'nickname-' + index}>{device.alias}</td>
<td key={'id-' + index}>{device.deviceId}</td>
<td key={'type-' + index}>{device.type}</td>
<td key={'soil-' + index}>{getSoil(device.type)}</td>
<td key={'pwp-' + index}>{getPermWilPoint(device.type) + " %"}</td>
<td key={'fc-' + index}>{getFieldCap(device.type) + " %"}</td>
<td key={'temp-' + index}>{getMinTemp(device.type) + " °C"}</td>
<td key={'brightness-' + index}>{getMinLux(device.type) + " lux"}</td>
</tr>
})}
</tbody>
</Table>
<br></br> <br></br>
<Row> <h4>If your device is not visible in the table above, configure it here below.</h4>
<Col xs lg="2"> <br></br>
<h4>Devices:</h4> <br></br>
</Col> <Settings></Settings>
</Row>
<Row>
<Col xs lg="2">
<Form>
<Form.Group>
<Form.Control as="select" type="text" onChange={handleChange}>
<option></option>
{getAllEspNames().map((espName, index) => {
return <option key={index} value={espName}>{espName}</option>
})}
</Form.Control>
</Form.Group>
</Form>
</Col>
<Col>
<h6>active device: {deviceId.deviceId}</h6>
</Col>
</Row>
<SensorCardDeck/>
<Row>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.temperature !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="temperature" stroke="#10b5de"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.humidity !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="humidity" stroke="#ff6f00"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
</Row>
<Row>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.brightness !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="brightness" stroke="#ffd500"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.moisture !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="moisture" stroke="#1c4399"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
</Row>
</> </>
) )
} }

View File

@ -24,7 +24,6 @@ export default function NavigationBar() {
<Navbar.Collapse id="responsive-navbar-nav"> <Navbar.Collapse id="responsive-navbar-nav">
<Nav className="mr-auto"> <Nav className="mr-auto">
<Nav.Link href="/overview">Overview</Nav.Link> <Nav.Link href="/overview">Overview</Nav.Link>
<Nav.Link href="/settings">Settings</Nav.Link>
<Nav.Link href="/about">About</Nav.Link> <Nav.Link href="/about">About</Nav.Link>
{ {
/* /*

View File

@ -1,23 +1,21 @@
import React from 'react' import React from 'react'
import { import {CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from 'recharts';
SensorDataCollection, import SensorCardDeck from './SensorCardDeck'
ActiveDeviceCollection, import {SensorDataCollection, ActiveDeviceCollection, ConfiguredDevicesCollection} from "../../client/main";
ConfiguredDevicesCollection,
PlantTypesCollection
} from "../../client/main";
import {useTracker} from 'meteor/react-meteor-data'; import {useTracker} from 'meteor/react-meteor-data';
import { Card, CardDeck, Table } from "react-bootstrap"; import { Col, Form, Row, Card, CardDeck } from "react-bootstrap";
import moment from 'moment'
export default function Overview() { export default function Overview() {
const deviceId = useTracker(() => { const activeDevice = useTracker(() => {
return ActiveDeviceCollection.find().fetch()[0]; return ActiveDeviceCollection.find().fetch()[0];
}); });
const sensorData = useTracker(() => { const sensorData = useTracker(() => {
if (deviceId === null || deviceId === undefined) { if (activeDevice === null || activeDevice === undefined) {
return []; return [];
} else { } else {
return SensorDataCollection.find({device_id: deviceId.deviceId}, { return SensorDataCollection.find({device_id: activeDevice.deviceId}, {
sort: {timestamp: -1}, sort: {timestamp: -1},
limit: 61 limit: 61
}).fetch().reverse(); }).fetch().reverse();
@ -28,11 +26,18 @@ export default function Overview() {
return ConfiguredDevicesCollection.find().fetch(); return ConfiguredDevicesCollection.find().fetch();
}); });
function getDirt(type) { const handleChange = (e) => {
const plantType = PlantTypesCollection.findOne({plantType: type}); if (e.target.value === "") {
return plantType.dirtType console.log("No device selected!");
} else {
var doc = ActiveDeviceCollection.findOne({deviceId: activeDevice.deviceId});
ActiveDeviceCollection.update({_id: doc._id}, {$set: {deviceId: e.target.value}});
}
} }
const getLabelFromStamp = (x) => {
return moment(x.timestamp).format("HH:mm");
}
if ((sensorData.length <= 0)) { if ((sensorData.length <= 0)) {
return ( return (
@ -48,28 +53,84 @@ export default function Overview() {
} else { } else {
return ( return (
<> <>
<Table striped bordered hover responsive>
<thead>
<tr key={'head'}>
<th key={'#'}>#</th>
<th key={'name'}>Device Name</th>
<th key={'type'}>Plant Type</th>
<th key={'dirt'}>Dirt Type</th>
</tr>
</thead>
<tbody>
{configuredDevices.map((device, index) => {
return <tr key={'body' + index}>
<td key={'#-' + index}>{index}</td>
<td key={'name-' + index}>{device.deviceId}</td>
<td key={'type-' + index}>{device.type}</td>
<td key={'dirt-' + index}>{getDirt(device.type)}</td>
</tr>
})}
</tbody>
</Table>
<br></br> <br></br>
<h4>If your device is not visible, go to Settings to configure it first.</h4> <Row>
<Col xs lg="2">
<h4>Devices:</h4>
</Col>
</Row>
<Row>
<Col xs lg="2">
<Form>
<Form.Group>
<Form.Control as="select" type="text" onChange={handleChange}>
<option></option>
{configuredDevices.map((devices, index) => {
return <option key={index} value={devices.deviceId}>{devices.alias}</option>
})}
</Form.Control>
</Form.Group>
</Form>
</Col>
<Col>
<h6>active device: {ConfiguredDevicesCollection.findOne({deviceId: activeDevice.deviceId}).alias}</h6>
</Col>
</Row>
<SensorCardDeck/>
<Row>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.temperature !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="temperature" stroke="#10b5de"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.humidity !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="humidity" stroke="#ff6f00"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
</Row>
<Row>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.brightness !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="brightness" stroke="#ffd500"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
<Col>
<ResponsiveContainer width='100%' height={325}>
<LineChart data={sensorData.filter(el => el.moisture !== null)} margin={{top: 50, right: 50, bottom: 20, left: 5}}>
<Line type="monotone" dataKey="moisture" stroke="#1c4399"/>
<CartesianGrid stroke="#ccc" strokeDasharray="5 5"/>
<XAxis dataKey={getLabelFromStamp}/>
<YAxis/>
<Tooltip/>
<Legend/>
</LineChart>
</ResponsiveContainer>
</Col>
</Row>
</> </>
) )
} }

View File

@ -1,6 +1,11 @@
import React from 'react' import React from 'react'
import { Card, CardDeck } from 'react-bootstrap'; import { Card, CardDeck } from 'react-bootstrap';
import {ActiveDeviceCollection, SensorDataCollection} from "../../client/main"; import {
ActiveDeviceCollection,
ConfiguredDevicesCollection,
PlantTypesCollection,
SensorDataCollection
} from "../../client/main";
import {useTracker} from 'meteor/react-meteor-data'; import {useTracker} from 'meteor/react-meteor-data';
export default function SensorCardDeck() { export default function SensorCardDeck() {
@ -12,6 +17,14 @@ export default function SensorCardDeck() {
return SensorDataCollection.find({ device_id: deviceId.deviceId }, { sort: { timestamp: -1 }, limit: 1 }).fetch(); return SensorDataCollection.find({ device_id: deviceId.deviceId }, { sort: { timestamp: -1 }, limit: 1 }).fetch();
}); });
const plantType = useTracker(() => {
return ConfiguredDevicesCollection.findOne({ deviceId: deviceId.deviceId }).type;
});
const plantTypeData = useTracker(() => {
return PlantTypesCollection.findOne({ plantType: plantType });
});
if (sensorData.length <= 0) { if (sensorData.length <= 0) {
return ( return (
@ -27,12 +40,19 @@ export default function SensorCardDeck() {
} else { } else {
return ( return (
<CardDeck> <CardDeck>
<Card> {sensorData[0].temperature < plantTypeData.data.temperature.minTemp ?
<Card.Body> <Card border={'danger'}>
<Card.Title>Temperature</Card.Title> <Card.Body>
<Card.Text>{sensorData[0].temperature} °C</Card.Text> <Card.Title>Temperature</Card.Title>
</Card.Body> <Card.Text>{sensorData[0].temperature} °C</Card.Text>
</Card> </Card.Body>
</Card> :
<Card>
<Card.Body>
<Card.Title>Temperature</Card.Title>
<Card.Text>{sensorData[0].temperature} °C</Card.Text>
</Card.Body>
</Card>}
<Card> <Card>
<Card.Body> <Card.Body>
<Card.Title>Humidity</Card.Title> <Card.Title>Humidity</Card.Title>
@ -45,12 +65,19 @@ export default function SensorCardDeck() {
<Card.Text>{sensorData[0].brightness} lux</Card.Text> <Card.Text>{sensorData[0].brightness} lux</Card.Text>
</Card.Body> </Card.Body>
</Card> </Card>
<Card> {sensorData[0].moisture > plantTypeData.data.soilMoisture.sat ?
<Card.Body> <Card border={'danger'}>
<Card.Title>Moisture</Card.Title> <Card.Body>
<Card.Text>{sensorData[0].moisture} %</Card.Text> <Card.Title>Moisture</Card.Title>
</Card.Body> <Card.Text>{sensorData[0].moisture} %</Card.Text>
</Card> </Card.Body>
</Card> :
<Card>
<Card.Body>
<Card.Title>Moisture</Card.Title>
<Card.Text>{sensorData[0].moisture} %</Card.Text>
</Card.Body>
</Card>}
</CardDeck> </CardDeck>
) )
} }

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {Col, Form, Row, Card, CardDeck, Button, Container} from "react-bootstrap"; import {Col, Form, Row, Card, CardDeck, Button, Container} from "react-bootstrap";
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import {getAllEspNames} from "../api/espNames"; import {getAllEspIds} from "../api/espIds";
import {getAllPlantTypes} from "../api/plantTypes"; import {getAllPlantTypes} from "../api/plantTypes";
import {ConfiguredDevicesCollection, PlantTypesCollection, ActiveDeviceCollection, SensorDataCollection} from "../../client/main"; import {ConfiguredDevicesCollection, PlantTypesCollection, ActiveDeviceCollection, SensorDataCollection} from "../../client/main";
import {useTracker} from 'meteor/react-meteor-data'; import {useTracker} from 'meteor/react-meteor-data';
@ -27,18 +27,27 @@ export default function Settings() {
return PlantTypesCollection.find(); return PlantTypesCollection.find();
}); });
var selectedEspName; var selectedEspId;
var selectedAlias = "";
var selectedType; var selectedType;
const payloadVegi = plantTypes.fetch()[0]; const payloadVegi = plantTypes.fetch()[0];
const payloadCacti = plantTypes.fetch()[1]; const payloadCacti = plantTypes.fetch()[1];
const payloadFlower = plantTypes.fetch()[2]; const payloadFlower = plantTypes.fetch()[2];
const handleChangeName = (e) => { const handleChangeAlias = (e) => {
if (e.target.value === "") {
console.log("Nickname cannot be empty!");
} else {
selectedAlias = e.target.value;
}
}
const handleChangeDevice = (e) => {
if (e.target.value === "") { if (e.target.value === "") {
console.log("No device selected!"); console.log("No device selected!");
} else { } else {
selectedEspName = e.target.value; selectedEspId = e.target.value;
} }
} }
@ -50,16 +59,35 @@ export default function Settings() {
} }
} }
const handleTest = (e) => { const handleSendData = (e) => {
var payload = ""; var payload = "";
if (selectedType === "Vegetables") {payload = JSON.stringify(payloadVegi.data.soilMoisture);} if (selectedType === "Vegetables") {payload = JSON.stringify(payloadVegi.data.soilMoisture);}
if (selectedType === "Cacti") {payload = JSON.stringify(payloadCacti.data.soilMoisture);} if (selectedType === "Cacti") {payload = JSON.stringify(payloadCacti.data.soilMoisture);}
if (selectedType === "Flowers") {payload = JSON.stringify(payloadFlower.data.soilMoisture);} if (selectedType === "Flowers") {payload = JSON.stringify(payloadFlower.data.soilMoisture);}
if ((payload === "") || (selectedEspName === undefined) || (selectedType === undefined)) {alert("No device or type selected!");} else { var doc = ConfiguredDevicesCollection.findOne({deviceId: selectedEspId});
if (doc === undefined) {ConfiguredDevicesCollection.insert({deviceId: selectedEspId, alias: selectedEspId, type: selectedType});} else {
ConfiguredDevicesCollection.update({_id: doc._id}, {$set: {alias: selectedAlias, type: selectedType}});
}
if ((payload === "") || (selectedEspId === undefined) || (selectedAlias === "") || (selectedType === undefined)) {alert("Nickname is empty or no device/type selected!");} else {
Meteor.call('mqttPublish', { Meteor.call('mqttPublish', {
topic: 'smartgarden/commands/' + selectedEspName + '/soil', topic: 'smartgarden/commands/' + selectedEspId + '/soil',
payload: payload payload: payload
}, (err, res) => {
if (err) {
alert(err);
} else {
console.log('success')
}
})
Meteor.call('mqttPublish', {
topic: 'smartgarden/commands/' + selectedEspId + '/automatic',
payload: JSON.stringify({
'irrigation': "true",
'light': 'true'
})
}, (err, res) => { }, (err, res) => {
if (err) { if (err) {
alert(err); alert(err);
@ -67,11 +95,6 @@ export default function Settings() {
alert('success') alert('success')
} }
}) })
var doc = ConfiguredDevicesCollection.findOne({deviceId: selectedEspName});
if (doc === undefined) {ConfiguredDevicesCollection.insert({deviceId: selectedEspName, type: selectedType});} else {
ConfiguredDevicesCollection.update({_id: doc._id}, {$set: {type: selectedType}});
}
} }
} }
@ -105,9 +128,17 @@ export default function Settings() {
<Form> <Form>
<Form.Group> <Form.Group>
<Form.Label>Name:</Form.Label> <Form.Label>Name:</Form.Label>
<Form.Control as="select" type="text" onChange={handleChangeName}> <Form.Control
type="text"
id="alias"
placeholder="Enter a device nickname..."
onChange={handleChangeAlias}/>
</Form.Group>
<Form.Group>
<Form.Label>Device:</Form.Label>
<Form.Control as="select" type="text" onChange={handleChangeDevice}>
<option></option> <option></option>
{getAllEspNames().map((espName, index) => { {getAllEspIds().map((espName, index) => {
return <option key={index} value={espName}>{espName}</option> return <option key={index} value={espName}>{espName}</option>
})} })}
</Form.Control> </Form.Control>
@ -121,7 +152,7 @@ export default function Settings() {
})} })}
</Form.Control> </Form.Control>
</Form.Group> </Form.Group>
<Button type={"test"} onClick={handleTest} >Send</Button> <Button type={"sendData"} onClick={handleSendData} >Send</Button>
</Form> </Form>
</Container> </Container>