diff --git a/client/main.jsx b/client/main.jsx index cda4586..a68555b 100644 --- a/client/main.jsx +++ b/client/main.jsx @@ -3,6 +3,8 @@ import { Meteor } from 'meteor/meteor'; import ReactDOM from 'react-dom'; import App from '../imports/ui/App'; import 'bootstrap/dist/css/bootstrap.min.css'; +import startMqttObserver from "../mongo-mqtt"; +import {publish} from "../imports/api/mqttApi"; export const PlantTypesCollection = new Meteor.Collection('plantTypes'); export const SensorDataCollection = new Meteor.Collection('sensorData'); @@ -10,6 +12,8 @@ export const ActiveDeviceCollection = new Meteor.Collection('activeDevice'); export const ConfiguredDevicesCollection = new Meteor.Collection('configuredDevices'); Meteor.startup(() => { + // If this code is on the meteor server-side, publish all MongoDB collections for the client-side, + // start the mqtt-observer script to push data to the database and define the mqttPublish method to be accessible from the client-side. if(Meteor.isServer) { Meteor.publish('plantTypesCollection', function() { return PlantTypesCollection.find(); @@ -26,8 +30,17 @@ Meteor.startup(() => { Meteor.publish('configuredDevicesCollection', function() { return ConfiguredDevicesCollection.find(); }) + + startMqttObserver() + + Meteor.methods({ + 'mqttPublish'({ topic, payload }) { + publish(topic, payload) + } + }) } + // If this code is on the meteor client-side, subscribe all published MongoDB collections from the server-side and render the application. if (Meteor.isClient) { Meteor.subscribe('plantTypesCollection'); Meteor.subscribe('sensorDataCollection'); diff --git a/imports/api/espIds.js b/imports/api/espIds.js index 07360fd..7b277d7 100644 --- a/imports/api/espIds.js +++ b/imports/api/espIds.js @@ -1,5 +1,7 @@ import {SensorDataCollection} from "../../client/main"; +// Fetch all esp-IDs stored in all documents of the sensorData collection and remove duplicates, +// returning an array of esp-IDs that have sent data to the database. export function getAllEspIds() { return _.uniq(SensorDataCollection.find({}, { sort: {device_id: 1}, fields: {device_id: true} diff --git a/imports/api/mqttApi.js b/imports/api/mqttApi.js index 5b400aa..5d6703c 100644 --- a/imports/api/mqttApi.js +++ b/imports/api/mqttApi.js @@ -2,6 +2,7 @@ import { connect } from 'mqtt'; var client = connect('mqtt://mqtt.timovolkmann.de') +// If connection to the mqtt-client is not established, try to reconnect. Else publish a message to the mqtt broker. function publish(topic, message) { if (!client.connected) { client.reconnect(); diff --git a/imports/api/plantTypes.js b/imports/api/plantTypes.js index d8af609..7c7606c 100644 --- a/imports/api/plantTypes.js +++ b/imports/api/plantTypes.js @@ -1,5 +1,6 @@ import {PlantTypesCollection} from '../../client/main' +// Return an array of all plant types currently stored in the database. export function getAllPlantTypes() { const plantTypesDocuments = PlantTypesCollection.find(); diff --git a/imports/ui/Home.jsx b/imports/ui/Home.jsx index 3a4934f..6c69d2a 100644 --- a/imports/ui/Home.jsx +++ b/imports/ui/Home.jsx @@ -5,30 +5,42 @@ import { Table } from "react-bootstrap"; import Settings from "./Settings"; export default function Home() { + // Return all documents of the configuredDevices collection. const configuredDevices = useTracker(() => { return ConfiguredDevicesCollection.find().fetch(); }); + // Return the soil type of the given plant type. function getSoil(type) { const plantType = PlantTypesCollection.findOne({plantType: type}); return plantType.dirtType } + // Return the permanent wilting point of the given plant type. function getPermWilPoint(type) { const plantType = PlantTypesCollection.findOne({plantType: type}); return plantType.data.soilMoisture.pwp } + // Return the field capacity of the given plant type. function getFieldCap(type) { const plantType = PlantTypesCollection.findOne({plantType: type}); return plantType.data.soilMoisture.fc } + // Return the minimal temperature of the given plant type. function getMinTemp(type) { const plantType = PlantTypesCollection.findOne({plantType: type}); return plantType.data.temperature.minTemp } + // Return the maximal temperature of the given plant type. + function getMaxTemp(type) { + const plantType = PlantTypesCollection.findOne({plantType: type}); + return plantType.data.temperature.maxTemp + } + + // Return the minimal lux value of the given plant type. function getMinLux(type) { const plantType = PlantTypesCollection.findOne({plantType: type}); return plantType.data.light.minLux @@ -48,7 +60,8 @@ export default function Home() { Mode Permanent Wilting Point Field Capacity - Min Temperature + Min Temperature + Max Temperature Min Brightness @@ -63,7 +76,8 @@ export default function Home() { {device.mode} {getPermWilPoint(device.type) + " %"} {getFieldCap(device.type) + " %"} - {getMinTemp(device.type) + " °C"} + {getMinTemp(device.type) + " °C"} + {getMaxTemp(device.type) + " °C"} {getMinLux(device.type) + " lux"} })} diff --git a/imports/ui/Overview.jsx b/imports/ui/Overview.jsx index 924bc92..9f7b0f5 100644 --- a/imports/ui/Overview.jsx +++ b/imports/ui/Overview.jsx @@ -5,18 +5,20 @@ import {SensorDataCollection, ActiveDeviceCollection, ConfiguredDevicesCollectio import {useTracker} from 'meteor/react-meteor-data'; import { Col, Form, Row, Card, CardDeck } from "react-bootstrap"; import moment from 'moment' -import { scaleLog } from 'd3-scale'; -const scale = scaleLog().base(Math.E); export default function Overview() { + // Return the document of the currently active device in the activeDevice collection. const activeDevice = useTracker(() => { return ActiveDeviceCollection.find().fetch()[0]; }); + // Return all documents of the configuredDevices collection. const configuredDevices = useTracker(() => { return ConfiguredDevicesCollection.find().fetch(); }); + // If the activeDevice is not null or undefined, return and filter the documents of the last two days (48 Hrs) of the currently active device in the sensorData collection + // and reverse the array so that the oldest documents come first. const sensorData = useTracker(() => { if (activeDevice === null || activeDevice === undefined) { return []; @@ -33,6 +35,7 @@ export default function Overview() { } }); + // Set the selected device from the configured devices dropdown-menu, to the new currently active device. const handleChange = (e) => { if (e.target.value === "") { console.log("No device selected!"); @@ -42,11 +45,12 @@ export default function Overview() { } } + // Get the current date as string. const getLabelFromStamp = (x) => { return moment(x.timestamp).format("LLLL"); - // return moment(x.timestamp).format("HH:mm"); } + // Workaround for log(0). const brightnessMapper = (x) => { if (x.brightness < 1) { x.brightness = 0.1 @@ -54,6 +58,7 @@ export default function Overview() { return x; } + //If the data that is loaded from the database has not been fetched yet, show a loading screen. Else load the application. if ((sensorData.length <= 0)) { return ( <> @@ -77,7 +82,8 @@ export default function Overview() { -
active device: {!activeDevice === undefined && !ConfiguredDevicesCollection.findOne({ deviceId: activeDevice.deviceId }) === undefined ? ConfiguredDevicesCollection.findOne({ deviceId: activeDevice.deviceId }).alias : "No device selected"}
+
active device: {!activeDevice === undefined && !ConfiguredDevicesCollection.findOne({ deviceId: activeDevice.deviceId }) === undefined ? + ConfiguredDevicesCollection.findOne({ deviceId: activeDevice.deviceId }).alias : "No device selected"}
@@ -114,7 +120,8 @@ export default function Overview() { -
active device: {ConfiguredDevicesCollection.findOne({deviceId: activeDevice.deviceId}) === undefined ? "No device selected" : ConfiguredDevicesCollection.findOne({deviceId: activeDevice.deviceId}).alias}
+
active device: {ConfiguredDevicesCollection.findOne({deviceId: activeDevice.deviceId}) === undefined ? + "No device selected" : ConfiguredDevicesCollection.findOne({deviceId: activeDevice.deviceId}).alias}
diff --git a/imports/ui/SensorCardDeck.jsx b/imports/ui/SensorCardDeck.jsx index f281d70..a33917e 100644 --- a/imports/ui/SensorCardDeck.jsx +++ b/imports/ui/SensorCardDeck.jsx @@ -9,10 +9,12 @@ import { import {useTracker} from 'meteor/react-meteor-data'; export default function SensorCardDeck() { + // Return the document of the currently active device in the activeDevice collection. const deviceId = useTracker(() => { return ActiveDeviceCollection.find().fetch()[0]; }); + // If the deviceId is not null or undefined, return the most recent document in the sensorData collection of the currently active device. const sensorData = useTracker(() => { if (deviceId === null || deviceId === undefined) { return undefined; @@ -21,7 +23,8 @@ export default function SensorCardDeck() { } }); - const plantType = useTracker(() => { + // If the deviceId is not null or undefined, return the document in the configuredDevices collection of the currently active device. + const configurationData = useTracker(() => { if (deviceId === null || deviceId === undefined) { return undefined; } else { @@ -29,16 +32,18 @@ export default function SensorCardDeck() { } }); + // If the configurationData is not null or undefined, return the configured plant type of the currently active device. const plantTypeData = useTracker(() => { - if (plantType === null || plantType === undefined) { + if (configurationData === null || configurationData === undefined) { return undefined; } else { - return PlantTypesCollection.findOne({plantType: plantType.type}); + return PlantTypesCollection.findOne({plantType: configurationData.type}); } }); - if (sensorData.length <= 0 || plantType === undefined || deviceId === undefined || plantTypeData === undefined) { + //If the data that is loaded from the database has not been fetched yet, show a loading screen. Else load the application. + if (sensorData.length <= 0 || configurationData === undefined || deviceId === undefined || plantTypeData === undefined) { return ( diff --git a/imports/ui/Settings.jsx b/imports/ui/Settings.jsx index a40855f..77602e2 100644 --- a/imports/ui/Settings.jsx +++ b/imports/ui/Settings.jsx @@ -7,21 +7,25 @@ import {ConfiguredDevicesCollection, PlantTypesCollection} from "../../client/ma import {useTracker} from 'meteor/react-meteor-data'; export default function Settings() { + // Return all documents of the plantTypes collection. const plantTypes = useTracker(() => { return PlantTypesCollection.find(); }); const espIds = useTracker(getAllEspIds); + // Variables for the configuration of the selected device. var selectedEspId; var selectedAlias = ""; var selectedType; var selectedMode = null; + // Settings from the database for each plant type to be published to the mqtt broker. const payloadVegi = plantTypes.fetch()[0]; const payloadCacti = plantTypes.fetch()[1]; const payloadFlower = plantTypes.fetch()[2]; + // If the Name field is empty, log a warning. Else set the entered string as selectedAlias. const handleChangeAlias = (e) => { if (e.target.value === "") { console.log("Nickname cannot be empty!"); @@ -30,6 +34,7 @@ export default function Settings() { } } + // If no device is selected, log a warning. Else set the selected device as selectedEspId. const handleChangeDevice = (e) => { if (e.target.value === "") { console.log("No device selected!"); @@ -38,6 +43,7 @@ export default function Settings() { } } + // If no plant type is selected, log a warning. Else set the selected type as selectedType. const handleChangeType = (e) => { if (e.target.value === "") { console.log("No type selected!"); @@ -46,36 +52,45 @@ export default function Settings() { } } + // If no mode is selected, log a warning. Else set the selected mode as selectedMode. const handleChangeMode = (e) => { if (e.target.value === "") { - console.log("No type selected!"); + console.log("No mode selected!"); } else { selectedMode = e.target.value; } } + // Save the configured device to the database and send the payload data to the mqtt broker. const handleSendData = (e) => { var payload = ""; var nmValue = null; + var minLux = PlantTypesCollection.findOne({plantType: selectedType}).data.light.minLux; + + // Configure the payload depending on the selected plant type and the selected mode. if (selectedType === "Vegetables") {payload = JSON.stringify(payloadVegi.data.soilMoisture);} if (selectedType === "Cacti") {payload = JSON.stringify(payloadCacti.data.soilMoisture);} if (selectedType === "Flowers") {payload = JSON.stringify(payloadFlower.data.soilMoisture);} if (selectedMode === "Growth") {nmValue = 450} if (selectedMode === "Bloom") {nmValue = 700} - + // Log all data console.log("Payload: " + payload); console.log("Alias: " + selectedAlias); console.log("ID: " + selectedEspId); console.log("Type: " + selectedType); console.log("Mode: " + selectedMode); - if ((payload === "") || (nmValue === null) || (selectedEspId === undefined) || (selectedAlias === "") || (selectedType === undefined) || (selectedMode === null)) {alert("Nickname is empty or no device/type/mode selected!");} else { + // If one of the values is empty, null or undefined send an alert. Else insert a document in the configuredDevices collection with the entered data, + // or update the data if the device has already been configured. + if ((payload === "") || (nmValue === null) || (minLux === undefined) || (selectedEspId === undefined) || + (selectedAlias === "") || (selectedType === undefined) || (selectedMode === null)) {alert("Nickname is empty or no device/type/mode selected!");} else { var doc = ConfiguredDevicesCollection.findOne({deviceId: selectedEspId}); if (doc === undefined) {ConfiguredDevicesCollection.insert({deviceId: selectedEspId, alias: selectedAlias, type: selectedType, mode: selectedMode});} else { ConfiguredDevicesCollection.update({_id: doc._id}, {$set: {alias: selectedAlias, type: selectedType, mode: selectedMode}}); } + // Publish the configuration data of the selected plant type for the selected device to the mqtt broker. Meteor.call('mqttPublish', { topic: 'smartgarden/commands/' + selectedEspId + '/soil', payload: payload @@ -87,6 +102,7 @@ export default function Settings() { } }) + // Publish the irrigation and light values as true to the mqtt broker, to activate the automatic irrigation and lighting for the selected device. Meteor.call('mqttPublish', { topic: 'smartgarden/commands/' + selectedEspId + '/automatic', payload: JSON.stringify({ @@ -101,10 +117,11 @@ export default function Settings() { } }) + // Publish the minimum lux value of the selected plant type and the nanometer value of the selected mode for the selected device to the mqtt broker. Meteor.call('mqttPublish', { topic: 'smartgarden/commands/' + selectedEspId + '/light', payload: JSON.stringify({ - 'minLX': 500, + 'minLX': minLux, 'nm': nmValue }) }, (err, res) => { @@ -118,6 +135,7 @@ export default function Settings() { } + //If the data that is loaded from the database has not been fetched yet, show a loading screen. Else load the application. if ((plantTypes.length <= 0) || (espIds.length <= 0)) { return ( diff --git a/server/main.js b/server/main.js index 08fbc82..c47c58d 100644 --- a/server/main.js +++ b/server/main.js @@ -8,6 +8,8 @@ var ActiveDeviceCollection = new Meteor.Collection('activeDevice'); var ConfiguredDevicesCollection = new Meteor.Collection('configuredDevices'); Meteor.startup(() => { + // If this code is on the meteor server-side, publish all MongoDB collections for the client-side, + // start the mqtt-observer script to push data to the database and define the mqttPublish method to be accessible from the client-side. if(Meteor.isServer) { Meteor.publish('plantTypesCollection', function() { return PlantTypesCollection.find(); @@ -25,7 +27,7 @@ Meteor.startup(() => { return ConfiguredDevicesCollection.find(); }) - startMqttObserver() + //startMqttObserver() Meteor.methods({ 'mqttPublish'({ topic, payload }) { @@ -34,6 +36,7 @@ Meteor.startup(() => { }) } + // If this code is on the meteor client-side, subscribe all published MongoDB collections from the server-side and render the application. if (Meteor.isClient) { Meteor.subscribe('plantTypesCollection'); Meteor.subscribe('sensorDataCollection');