diff --git a/package-lock.json b/package-lock.json index 9ba2294..8e27041 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@rneui/base": "^4.0.0-rc.7", "@rneui/themed": "^4.0.0-rc.7", "expo": "~48.0.6", + "expo-sqlite": "~11.1.1", "expo-status-bar": "~1.4.4", "g": "^2.0.1", "immer": "^10.0.1", @@ -31,7 +32,8 @@ "react-native-screens": "~3.20.0", "react-navigation-stack": "^2.10.4", "react-redux": "^8.0.5", - "redux": "^4.2.1" + "redux": "^4.2.1", + "yup": "^1.2.0" }, "devDependencies": { "@babel/core": "^7.20.0" @@ -2821,6 +2823,18 @@ "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-13.0.0.tgz", "integrity": "sha512-TI+l71+5aSKnShYclFa14Kum+hQMZ86b95SH6tQUG3qZEmLTarvWpKwqtTwQKqvlJSJrpFiSFu3eCuZokY6zWA==" }, + "node_modules/@expo/websql": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@expo/websql/-/websql-1.0.1.tgz", + "integrity": "sha512-H9/t1V7XXyKC343FJz/LwaVBfDhs6IqhDtSYWpt8LNSQDVjf5NvVJLc5wp+KCpRidZx8+0+YeHJN45HOXmqjFA==", + "dependencies": { + "argsarray": "^0.0.1", + "immediate": "^3.2.2", + "noop-fn": "^1.0.0", + "pouchdb-collections": "^1.0.1", + "tiny-queue": "^0.2.1" + } + }, "node_modules/@expo/xcpretty": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.2.2.tgz", @@ -5214,6 +5228,11 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/argsarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/argsarray/-/argsarray-0.0.1.tgz", + "integrity": "sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg==" + }, "node_modules/arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -7195,6 +7214,17 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-sqlite": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-11.1.1.tgz", + "integrity": "sha512-93KQ4Bc4+xQF2nOZC2jcJ+d0K1ooQSEYganLukw4Sp2LuxUtBsPXYTyOeisSxQSMMaz9nHiTqag8DEC1MiB7Xw==", + "dependencies": { + "@expo/websql": "^1.0.1" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-status-bar": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.4.4.tgz", @@ -7986,6 +8016,11 @@ "node": ">=4.0" } }, + "node_modules/immediate": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", + "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" + }, "node_modules/immer": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.1.tgz", @@ -10807,6 +10842,11 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/noop-fn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/noop-fn/-/noop-fn-1.0.0.tgz", + "integrity": "sha512-pQ8vODlgXt2e7A3mIbFDlizkr46r75V+BJxVAyat8Jl7YmI513gG5cfyRL0FedKraoZ+VAouI1h4/IWpus5pcQ==" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11424,6 +11464,11 @@ "node": ">=0.10.0" } }, + "node_modules/pouchdb-collections": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz", + "integrity": "sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg==" + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -11532,6 +11577,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -13568,6 +13618,16 @@ "xtend": "~4.0.1" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, + "node_modules/tiny-queue": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.1.tgz", + "integrity": "sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -13647,6 +13707,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14326,6 +14391,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", + "integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 457334b..9809c80 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@rneui/base": "^4.0.0-rc.7", "@rneui/themed": "^4.0.0-rc.7", "expo": "~48.0.6", + "expo-sqlite": "~11.1.1", "expo-status-bar": "~1.4.4", "g": "^2.0.1", "immer": "^10.0.1", @@ -32,7 +33,8 @@ "react-native-screens": "~3.20.0", "react-navigation-stack": "^2.10.4", "react-redux": "^8.0.5", - "redux": "^4.2.1" + "redux": "^4.2.1", + "yup": "^1.2.0" }, "devDependencies": { "@babel/core": "^7.20.0" diff --git a/src/App.js b/src/App.js index 868546e..887b415 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,7 @@ import HomeScreen from './screens/Home'; import App_2 from './screens/Home/src/components/mqttfile'; import store from './redux/store'; import { Provider } from 'react-redux'; +import PlantsScreen from './screens/PlantsScreen'; const Tab = createBottomTabNavigator(); @@ -19,6 +20,7 @@ const App = () => { + diff --git a/src/redux/reducers.js b/src/redux/reducers.js index f887d49..0ff7125 100644 --- a/src/redux/reducers.js +++ b/src/redux/reducers.js @@ -1,13 +1,16 @@ import { ADD_SENSOR_DATA } from './actions'; const initialState = { - sensorValue: '{"humidity": 0, "temperature": 0, "pressure": 0}', + sensorValue: { + humidity: 0, + temperature: 0, + pressure: 0, + }, }; const sensorReducer = (state = initialState, action) => { switch (action.type) { case ADD_SENSOR_DATA: - console.log('sensorReducer is sending ' + action.payload); return { sensorValue: action.payload, }; diff --git a/src/screens/Home/index.js b/src/screens/Home/index.js index 46833f1..51390e5 100644 --- a/src/screens/Home/index.js +++ b/src/screens/Home/index.js @@ -1,10 +1,21 @@ import React from 'react'; -import { View, Text, Button, StyleSheet, ScrollView } from 'react-native'; +import { + View, + Text, + Button, + StyleSheet, + ScrollView, + TouchableWithoutFeedback, + Keyboard, + KeyboardAvoidingView, + Platform, +} from 'react-native'; import Sensor from '../../shared/Sensor'; import ReduxTest from './src/components/ReduxTest'; +import Database from './src/components/Database'; const styles = StyleSheet.create({ container: { @@ -16,15 +27,19 @@ const styles = StyleSheet.create({ const HomeScreen = () => { return ( - - - - - - - - - + + + + + + + + + + ); }; diff --git a/src/screens/Home/src/components/Database.js b/src/screens/Home/src/components/Database.js new file mode 100644 index 0000000..dcbc3be --- /dev/null +++ b/src/screens/Home/src/components/Database.js @@ -0,0 +1,225 @@ +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + ScrollView, + TextInput, + Button, +} from 'react-native'; +import { object, string, number, setLocale } from 'yup'; + +import * as SQLite from 'expo-sqlite'; +const db = SQLite.openDatabase('db.plantDb'); // returns Database object + +export default class Database extends React.Component { + constructor(props) { + super(props); + this.state = { + data: null, + name: null, + lowerLimit: null, + upperLimit: null, + recLowerLimit: null, + recUpperLimit: null, + validationError: null, + }; + + // Check if the plants table exists if not create it + db.transaction((tx) => { + tx.executeSql( + 'CREATE TABLE IF NOT EXISTS plants (id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, LowerLimit INT, UpperLimit INT, RecLowerLimit INT, RecUpperLimit INT)' + ); + }); + + this.fetchData(); + } + + /** + * Fetches data from database and stores it in state. + */ + fetchData = () => { + db.transaction((tx) => { + // sending 4 arguments in executeSql + tx.executeSql( + 'SELECT * FROM plants', + null, // passing sql query and parameters:null + // success callback which sends two things Transaction object and ResultSet Object + (txObj, { rows: { _array } }) => { + this.setState({ ...this.state, data: _array }); + console.log(_array); + }, + // failure callback which sends two things Transaction object and Error + (txObj, error) => console.log('Error ', error) + ); // end executeSQL + }); // end transaction + }; + + /** + * Validates form input using rules in mySchema. Inserts into plants table if successful. Otherwise, sets this.state.validationError. + */ + handleSubmit = async () => { + await mySchema + .validate(this.state) + .then(() => { + this.setState({ ...this.state, validationError: null }); + db.transaction((tx) => { + tx.executeSql( + 'INSERT INTO plants (Name, LowerLimit, UpperLimit, RecLowerLimit, RecUpperLimit) VALUES (?, ?, ?, ?, ?)', + [ + this.state.name, + this.state.lowerLimit, + this.state.upperLimit, + this.state.recLowerLimit, + this.state.recUpperLimit, + ], + this.fetchData(), + (txObj, err) => console.log('Error ', err) + ); + }); + }) + .catch((err) => { + this.setState({ ...this.state, validationError: err }); + console.log(err); + }); + }; + + handleClear = () => { + db.transaction((tx) => { + tx.executeSql('DELETE FROM plants'); + }); + this.setState({ + data: null, + name: null, + lowerLimit: null, + upperLimit: null, + recLowerLimit: null, + recUpperLimit: null, + }); + }; + + render() { + return ( + + Plant Name + this.setState({ name: newData })} + /> + Temperature Lower Limit + this.setState({ lowerLimit: newData })} + /> + Temperature Upper Limit + this.setState({ upperLimit: newData })} + /> + Temperature Recommended Lower Limit + this.setState({ recLowerLimit: newData })} + /> + Temperature Recommended Upper Limit + this.setState({ recUpperLimit: newData })} + /> + + + {this.state.validationError !== null + ? this.state.validationError.message + : ''} + + + + + + + {JSON.stringify(this.state.data)} + + ); + } +} + +// Set shared error messages +setLocale({ + mixed: { + required: 'All fields are required', + }, +}); + +// Validation schema for Yup +const mySchema = object({ + name: string().required(), + lowerLimit: number().required().typeError('Lower limit must be a number'), + upperLimit: number().required().typeError('Upper limit must be a number'), + recLowerLimit: number() + .required() + .when( + 'lowerLimit', + (lowerLimit, recLowerLimit) => + lowerLimit && + recLowerLimit.min( + lowerLimit, + 'Recommended lower limit must be greater than lower limit' + ) + ) + .when( + 'recUpperLimit', + (recUpperLimit, recLowerLimit) => + recUpperLimit && + recLowerLimit.max( + recUpperLimit, + 'Recommended lower limit must be less than recommended upper limit' + ) + ) + .typeError('Recommended lower limit must be a number'), + recUpperLimit: number() + .required() + .when( + 'upperLimit', + (upperLimit, recUpperLimit) => + upperLimit && + recUpperLimit.max( + upperLimit, + 'Recommended upper limit must be less than upper limit' + ) + ) + .typeError('Recommended upper limit must be a number'), +}); + +const styles = { + container: { + flex: 1, + marginTop: 20, + }, + textInput: { + height: 40, + margin: 5, + padding: 2, + borderWidth: 2, + minWidth: 300, + }, + text: { + maxWidth: 300, + }, + errorText: { + color: 'red', + maxWidth: 300, + }, +}; diff --git a/src/screens/Home/src/components/ReduxTest.js b/src/screens/Home/src/components/ReduxTest.js index 3fa8813..86ac485 100644 --- a/src/screens/Home/src/components/ReduxTest.js +++ b/src/screens/Home/src/components/ReduxTest.js @@ -9,15 +9,23 @@ export default function ReduxTest() { const dispatch = useDispatch(); const handlePress = () => { + let tempVal = parseInt(Math.random() * 100); + let presVal = parseInt(Math.random() * 100); + let humVal = parseInt(Math.random() * 100); + dispatch({ type: ADD_SENSOR_DATA, - payload: '{ "temperature": 500, "humidity": 70 }', + payload: { + temperature: tempVal, + pressure: presVal, + humidity: humVal, + }, }); }; return ( - Hello + Press me! ); } @@ -27,6 +35,9 @@ const styles = StyleSheet.create({ alignItems: 'center', width: 50, height: 50, - backgroundColor: 'blue', + backgroundColor: 'black', + }, + text: { + color: 'white', }, }); diff --git a/src/screens/PlantsScreen.js b/src/screens/PlantsScreen.js new file mode 100644 index 0000000..ee53716 --- /dev/null +++ b/src/screens/PlantsScreen.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { + View, + Text, + Button, + StyleSheet, + ScrollView, + TouchableWithoutFeedback, + Keyboard, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import Database from './Home/src/components/Database'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +const PlantsScreen = () => { + return ( + + + + + + + + ); +}; + +export default PlantsScreen; diff --git a/src/shared/Sensor.js b/src/shared/Sensor.js index 21c082b..fba7aa5 100644 --- a/src/shared/Sensor.js +++ b/src/shared/Sensor.js @@ -11,12 +11,18 @@ export default function Sensor({ initial, label, dataType }) { const value = useSelector((state) => state.sensors.sensorValue); // Get the sensor value from the MQTT sensor in the Redux state setTimeout(() => { - for (const [key, val] of Object.entries(JSON.parse(value))) { + // Convert JSON string to JSON object + let sensorData = value; + if (typeof sensorData === 'string') { + sensorData = JSON.parse(sensorData); + } + + for (const [key, val] of Object.entries(sensorData)) { if (key === dataType) { setSensorValue(Number(val)); } } - }, 5000); + }, 2000); useEffect(() => { // Update indicator styling based on sensor value