Compare commits
	
		
			27 Commits
		
	
	
		
			fcc81cf7e5
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a1dc088a23 | |||
| d4a19060e8 | |||
| fe9f7e81e9 | |||
| 6e07c2be85 | |||
| 03db7575fd | |||
| df7470f147 | |||
| 17a3436017 | |||
| 4bdcd49e1f | |||
| 031c1ede21 | |||
| 08e5c2e3d2 | |||
| 2c80c4b279 | |||
| 66d688291c | |||
| 49430114fe | |||
| cfd0056197 | |||
| facd8d912f | |||
| b743cbc9a5 | |||
| ace77028c4 | |||
| dd215a8acc | |||
| 90a03dc17c | |||
| e1c42c90d5 | |||
| cb8f6daccc | |||
| 822eff6c5d | |||
| ce86f1e029 | |||
| 7ed58e94d8 | |||
| 446afb939a | |||
| cae5d1c95a | |||
| fde5018da8 | 
| @@ -34,7 +34,8 @@ module.exports = { | |||||||
| 		"semi": [ | 		"semi": [ | ||||||
| 			"error", | 			"error", | ||||||
| 			"always" | 			"always" | ||||||
| 		] | 		], | ||||||
|  | 		"no-unused-vars": [1] | ||||||
| 	}, | 	}, | ||||||
| 	"settings": { | 	"settings": { | ||||||
| 		"react": { | 		"react": { | ||||||
|   | |||||||
| @@ -7,7 +7,9 @@ | |||||||
|     "@testing-library/jest-dom": "^4.2.4", |     "@testing-library/jest-dom": "^4.2.4", | ||||||
|     "@testing-library/react": "^9.3.2", |     "@testing-library/react": "^9.3.2", | ||||||
|     "@testing-library/user-event": "^7.1.2", |     "@testing-library/user-event": "^7.1.2", | ||||||
|  |     "axios": "^0.21.1", | ||||||
|     "grommet": "^2.16.2", |     "grommet": "^2.16.2", | ||||||
|  |     "grommet-icons": "^4.5.0", | ||||||
|     "polished": "^4.0.5", |     "polished": "^4.0.5", | ||||||
|     "react": "^17.0.1", |     "react": "^17.0.1", | ||||||
|     "react-dom": "^17.0.1", |     "react-dom": "^17.0.1", | ||||||
|   | |||||||
							
								
								
									
										39
									
								
								src/App.css
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								src/App.css
									
									
									
									
									
								
							| @@ -1,39 +0,0 @@ | |||||||
| .App { |  | ||||||
|   text-align: center; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .App-logo { |  | ||||||
|   height: 40vmin; |  | ||||||
|   pointer-events: none; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @media (prefers-reduced-motion: no-preference) { |  | ||||||
|   .App-logo { |  | ||||||
|     animation: App-logo-float infinite 3s ease-in-out; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .App-header { |  | ||||||
|   min-height: 100vh; |  | ||||||
|   display: flex; |  | ||||||
|   flex-direction: column; |  | ||||||
|   align-items: center; |  | ||||||
|   justify-content: center; |  | ||||||
|   font-size: calc(10px + 2vmin); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .App-link { |  | ||||||
|   color: rgb(112, 76, 182); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @keyframes App-logo-float { |  | ||||||
|   0% { |  | ||||||
|     transform: translateY(0); |  | ||||||
|   } |  | ||||||
|   50% { |  | ||||||
|     transform: translateY(10px) |  | ||||||
|   } |  | ||||||
|   100% { |  | ||||||
|     transform: translateY(0px) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										50
									
								
								src/Core.js
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								src/Core.js
									
									
									
									
									
								
							| @@ -1,32 +1,52 @@ | |||||||
| import React from "react"; | import React from "react"; | ||||||
| import { useSelector, useDispatch } from "react-redux"; | import { useSelector, useDispatch } from "react-redux"; | ||||||
|  | import { Box, Button, Clock, Text } from "grommet"; | ||||||
|  |  | ||||||
| import { setPlugin, selectCore } from "./coreSlice"; | import { setPlugin, selectCore } from "./coreSlice"; | ||||||
| import * as plugins from "./plugins"; | import { selectVoice } from "./voiceSlice"; | ||||||
|  | import VoiceBars from "./components/VoiceBars"; | ||||||
|  | import plugins from "./plugins"; | ||||||
|  |  | ||||||
| function Core() { | function Core() { | ||||||
| 	const coreState = useSelector(selectCore); | 	const coreState = useSelector(selectCore); | ||||||
|  | 	const voiceState = useSelector(selectVoice); | ||||||
| 	const dispatch = useDispatch(); | 	const dispatch = useDispatch(); | ||||||
| 	const plugin = plugins.default[coreState.plugin]; | 	const plugin = plugins[coreState.plugin]; | ||||||
| 	const props = { | 	const props = { | ||||||
| 		data: coreState.data, | 		data: coreState.data, | ||||||
| 		close: () => dispatch(setPlugin(false)) | 		close: () => dispatch(setPlugin(false)) | ||||||
| 	}; | 	}; | ||||||
| 	return <> | 	return <> | ||||||
| 		{plugin ? null : | 		<Box height="5vh" background="light-1" justify="center" direction="row"> | ||||||
| 			<> | 			{voiceState.recording ? <VoiceBars style={{height: "100%"}}/>: | ||||||
| 				<p>Welcome to Honda!</p> | 				voiceState.text === null ? null: | ||||||
| 				{Object.keys(plugins.default).map( | 					<Text style={{paddingTop: "1em"}}>{voiceState.text}</Text> | ||||||
| 					i => <p | 			} | ||||||
| 						onClick={() => dispatch(setPlugin(i))} | 		</Box> | ||||||
| 						key={i} | 		{plugin ? React.createElement(plugin, props, null) : ( | ||||||
| 					> | 			<Box | ||||||
| 						{plugins.default[i].pluginName} | 				align="center" | ||||||
| 					</p>) | 				background="light-1" | ||||||
| 				} | 				width="100vw" | ||||||
| 			</> | 				justify="center" | ||||||
|  | 				direction="column" | ||||||
|  | 				alignContent="center" | ||||||
|  | 				height="95vh" | ||||||
|  | 			> | ||||||
|  | 				<Clock size="xxlarge" /> | ||||||
|  | 				<Clock size="xxlarge" type="digital" /> | ||||||
|  | 				<Box direction="row" margin="1.5em" wrap={true}> | ||||||
|  | 					<Button onClick={() => dispatch(setPlugin("petMode"))} primary | ||||||
|  | 						label="Pet Mode" /> | ||||||
|  | 					<Button onClick={() => dispatch(setPlugin("smartHome"))} primary | ||||||
|  | 						label="Smart Home" margin="0 0 0 1em" /> | ||||||
|  | 					<Button onClick={() => dispatch(setPlugin("maps"))} primary | ||||||
|  | 						label="Maps" margin="0 0 0 1em" /> | ||||||
|  | 					<Button onClick={() => dispatch(setPlugin("marketplace"))} primary | ||||||
|  | 						label="Marketplace" margin="0 0 0 1em" /> | ||||||
|  | 				</Box> | ||||||
|  | 			</Box>) | ||||||
| 		} | 		} | ||||||
| 		{plugin ? plugin(props) :null} |  | ||||||
| 	</>; | 	</>; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import io from "socket.io-client"; | |||||||
| import { WS_BASE } from "./config"; | import { WS_BASE } from "./config"; | ||||||
| import { useDispatch } from "react-redux"; | import { useDispatch } from "react-redux"; | ||||||
| import { setData, setPlugin } from "./coreSlice"; | import { setData, setPlugin } from "./coreSlice"; | ||||||
|  | import { setRecording, setText } from "./voiceSlice"; | ||||||
|  |  | ||||||
| const WebSocketContext = createContext(null); | const WebSocketContext = createContext(null); | ||||||
|  |  | ||||||
| @@ -17,8 +18,8 @@ function WebSocketProvider({ children }) { | |||||||
| 		socket = io.connect(WS_BASE); | 		socket = io.connect(WS_BASE); | ||||||
|  |  | ||||||
| 		socket.on("switchPlugin", (payload) => { | 		socket.on("switchPlugin", (payload) => { | ||||||
|  | 			dispatch(setData(payload.data || {})); | ||||||
| 			dispatch(setPlugin(payload.plugin)); | 			dispatch(setPlugin(payload.plugin)); | ||||||
| 			dispatch(setData(payload.data)); |  | ||||||
| 			if (payload.time) { | 			if (payload.time) { | ||||||
| 				setTimeout(() => { | 				setTimeout(() => { | ||||||
| 					dispatch(setData({})); | 					dispatch(setData({})); | ||||||
| @@ -26,6 +27,20 @@ function WebSocketProvider({ children }) { | |||||||
| 				}, payload.time); | 				}, payload.time); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
|  | 		socket.on("voice:wakeword", () => { | ||||||
|  | 			dispatch(setRecording(true)); | ||||||
|  | 			dispatch(setText(null)); | ||||||
|  | 		}); | ||||||
|  | 		socket.on("voice:record_end", () => { | ||||||
|  | 			dispatch(setRecording(false)); | ||||||
|  | 			dispatch(setText(null)); | ||||||
|  | 		}); | ||||||
|  | 		socket.on("voice:utterance", payload => { | ||||||
|  | 			dispatch(setRecording(false)); | ||||||
|  | 			dispatch(setText(payload.text)); | ||||||
|  | 			setTimeout(() => dispatch(setText(null)), 3000); | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| import { configureStore } from "@reduxjs/toolkit"; | import { configureStore } from "@reduxjs/toolkit"; | ||||||
| import coreReducer from "../coreSlice"; | import coreReducer from "../coreSlice"; | ||||||
|  | import voiceReducer from "../voiceSlice"; | ||||||
|  |  | ||||||
| export default configureStore({ | export default configureStore({ | ||||||
| 	reducer: { | 	reducer: { | ||||||
| 		core: coreReducer, | 		core: coreReducer, | ||||||
|  | 		voice: voiceReducer, | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										79
									
								
								src/components/VoiceBars.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/components/VoiceBars.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | |||||||
|  | import React from "react"; | ||||||
|  |  | ||||||
|  | function VoiceBars() { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="waves" style={{transform: "rotate(180deg)", marginTop: "130px", transition: "1s"}}> | ||||||
|  | 			<svg width="100vw" fill="none" version="1.1" | ||||||
|  | 				xmlns="http://www.w3.org/2000/svg"> | ||||||
|  | 				<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%"> | ||||||
|  | 					<stop offset="0%" stopColor="#00B4DB" /> | ||||||
|  | 					<stop offset="50%" stopColor="#224488" /> | ||||||
|  | 					<stop offset="100%" stopColor="#0083B0" /> | ||||||
|  | 				</linearGradient> | ||||||
|  | 				<path | ||||||
|  | 					fill="url(#grad1)" | ||||||
|  | 					d=" | ||||||
|  | 					M0 67 | ||||||
|  | 					C 273,183 | ||||||
|  | 					822,-40 | ||||||
|  | 					1920.00,106 | ||||||
|  |  | ||||||
|  | 					V 359 | ||||||
|  | 					H 0 | ||||||
|  | 					V 67 | ||||||
|  | 					Z"> | ||||||
|  | 					<animate | ||||||
|  | 						repeatCount="indefinite" | ||||||
|  | 						fill="url(#grad1)" | ||||||
|  | 						attributeName="d" | ||||||
|  | 						dur="5s" | ||||||
|  | 						attributeType="XML" | ||||||
|  | 						values=" | ||||||
|  | 						M0 77 | ||||||
|  | 						C 473,283 | ||||||
|  | 						822,-40 | ||||||
|  | 						1920,116 | ||||||
|  |  | ||||||
|  | 						V 359 | ||||||
|  | 						H 0 | ||||||
|  | 						V 67 | ||||||
|  | 						Z; | ||||||
|  |  | ||||||
|  | 						M0 77 | ||||||
|  | 						C 473,-40 | ||||||
|  | 						1222,283 | ||||||
|  | 						1920,136 | ||||||
|  |  | ||||||
|  | 						V 359 | ||||||
|  | 						H 0 | ||||||
|  | 						V 67 | ||||||
|  | 						Z; | ||||||
|  |  | ||||||
|  | 						M0 77 | ||||||
|  | 						C 973,260 | ||||||
|  | 						1722,-53 | ||||||
|  | 						1920,120 | ||||||
|  |  | ||||||
|  | 						V 359 | ||||||
|  | 						H 0 | ||||||
|  | 						V 67 | ||||||
|  | 						Z; | ||||||
|  |  | ||||||
|  | 						M0 77 | ||||||
|  | 						C 473,283 | ||||||
|  | 						822,-40 | ||||||
|  | 						1920,116 | ||||||
|  |  | ||||||
|  | 						V 359 | ||||||
|  | 						H 0 | ||||||
|  | 						V 67 | ||||||
|  | 						Z | ||||||
|  | 						"> | ||||||
|  | 					</animate> | ||||||
|  | 				</path> | ||||||
|  | 			</svg> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default VoiceBars; | ||||||
							
								
								
									
										85
									
								
								src/plugins/Accident.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/plugins/Accident.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | import React, { useState, useEffect, useRef } from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import { Box, Button, Heading, Text } from "grommet"; | ||||||
|  | import { Aid, Impact } from "grommet-icons"; | ||||||
|  |  | ||||||
|  | import Telegram from "../utils/telegram"; | ||||||
|  | import { TG_API, TG_USERID } from "../config"; | ||||||
|  |  | ||||||
|  | function sendNotification(close) { | ||||||
|  | 	if (window.sendingAccidentAlert) | ||||||
|  | 		return; | ||||||
|  | 	window.sendingAccidentAlert = true; | ||||||
|  | 	const bot = new Telegram(TG_API); | ||||||
|  | 	bot.sendMessage(TG_USERID, "User detected in an accident. Location has been attached below."); | ||||||
|  | 	navigator.geolocation.getCurrentPosition( | ||||||
|  | 		position => { | ||||||
|  | 			window.sendingAccidentAlert = false; | ||||||
|  | 			bot.sendLocation( TG_USERID, | ||||||
|  | 				position.coords.latitude, position.coords.longitude); | ||||||
|  | 		}, | ||||||
|  | 		() => { | ||||||
|  | 			window.sendingAccidentAlert = false; | ||||||
|  | 			bot.sendMessage(TG_USERID, "Error retrieving location"); | ||||||
|  | 		} | ||||||
|  | 	); | ||||||
|  | 	setTimeout(close, 2000); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function Accident(props) { | ||||||
|  | 	const threshold = 20; | ||||||
|  | 	const [ time, setTime ] = useState(threshold); | ||||||
|  | 	const countRef = useRef(null); | ||||||
|  |  | ||||||
|  | 	function decrementTime(time) { | ||||||
|  | 		if (time === 0) { | ||||||
|  | 			clearInterval(countRef.current); | ||||||
|  | 			sendNotification(props.close); | ||||||
|  | 			return 0; | ||||||
|  | 		} | ||||||
|  | 		return time - 1; | ||||||
|  | 	} | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		countRef.current = setInterval(() => setTime(decrementTime), 1000); | ||||||
|  | 		return () => clearInterval(countRef.current); | ||||||
|  | 	}, []); | ||||||
|  |  | ||||||
|  | 	function dismiss() { | ||||||
|  | 		clearTimeout(countRef.current); | ||||||
|  | 		props.close(); | ||||||
|  | 	} | ||||||
|  | 	return ( | ||||||
|  | 		<Box | ||||||
|  | 			align="center" | ||||||
|  | 			background="light-1" | ||||||
|  | 			width="100vw" | ||||||
|  | 			justify="center" | ||||||
|  | 			direction="column" | ||||||
|  | 			alignContent="center" | ||||||
|  | 			height="95vh" | ||||||
|  | 		> | ||||||
|  | 			{ | ||||||
|  | 				time === 0 ? | ||||||
|  | 					<Aid color="plain" size="xlarge" /> | ||||||
|  | 					:<Impact color="status-critical" size="xlarge" /> | ||||||
|  | 			} | ||||||
|  | 			<Heading>{ | ||||||
|  | 				time === 0 ? | ||||||
|  | 					"Calling Ambulance!" | ||||||
|  | 					:"Accident Detected!" | ||||||
|  | 			}</Heading> | ||||||
|  | 			<Text>{ | ||||||
|  | 				time === 0 ? "Calling Ambulance": | ||||||
|  | 					`To cancel calling the ambulance, press the button below. (${time})` | ||||||
|  | 			}</Text> | ||||||
|  | 			<Button primary size="large" onClick={dismiss} | ||||||
|  | 				label="Dismiss" margin="1.5em" /> | ||||||
|  | 		</Box> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Accident.propTypes = { | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Accident; | ||||||
							
								
								
									
										35
									
								
								src/plugins/GenericPageWithIcon.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/plugins/GenericPageWithIcon.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import { Box, Button, Heading, Text } from "grommet"; | ||||||
|  |  | ||||||
|  | function GenericPageWithIcon(props) { | ||||||
|  | 	return ( | ||||||
|  | 		<Box | ||||||
|  | 			align="center" | ||||||
|  | 			background="light-1" | ||||||
|  | 			width="100vw" | ||||||
|  | 			justify="center" | ||||||
|  | 			direction="column" | ||||||
|  | 			alignContent="center" | ||||||
|  | 			height="95vh" | ||||||
|  | 			style={{ | ||||||
|  | 				padding: "0 3em", | ||||||
|  | 			}} | ||||||
|  | 		> | ||||||
|  | 			{props.icon} | ||||||
|  | 			<Heading>{props.title}</Heading> | ||||||
|  | 			<Text style={{ textAlign: "center" }}>{props.description}</Text> | ||||||
|  | 			<Button primary size="large" onClick={props.close} | ||||||
|  | 				label="Dismiss" margin="1.5em" /> | ||||||
|  | 		</Box> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | GenericPageWithIcon.propTypes = { | ||||||
|  | 	title: PropTypes.string, | ||||||
|  | 	description: PropTypes.string, | ||||||
|  | 	icon: PropTypes.node, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default GenericPageWithIcon; | ||||||
							
								
								
									
										23
									
								
								src/plugins/Manual.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/plugins/Manual.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import GenericPageWithIcon from "./GenericPageWithIcon"; | ||||||
|  | import { StatusInfo } from "grommet-icons"; | ||||||
|  |  | ||||||
|  | function Manual(props) { | ||||||
|  | 	return ( | ||||||
|  | 		<GenericPageWithIcon | ||||||
|  | 			title={props.data.title} | ||||||
|  | 			description={props.data.description} | ||||||
|  | 			icon={<StatusInfo color="plain" size="xlarge" />} | ||||||
|  | 			close={props.close} | ||||||
|  | 		/>); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Manual.propTypes = { | ||||||
|  | 	data: PropTypes.object, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Manual.pluginName = "Manual"; | ||||||
|  |  | ||||||
|  | export default Manual; | ||||||
							
								
								
									
										53
									
								
								src/plugins/Maps.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/plugins/Maps.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import React, { useState, useEffect } from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import { Box, Button } from "grommet"; | ||||||
|  |  | ||||||
|  | import { MAPS_API } from "../config"; | ||||||
|  |  | ||||||
|  | function Maps(props) { | ||||||
|  | 	const [ location, setLocation ] = useState([null, null]); | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		navigator.geolocation.getCurrentPosition( | ||||||
|  | 			position => setLocation([position.coords.latitude, position.coords.longitude]), | ||||||
|  | 			() => {} | ||||||
|  | 		); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	const mapsEmbedUrl = new URL("https://www.google.com/maps/embed/v1/view"); | ||||||
|  | 	mapsEmbedUrl.searchParams.append("key", MAPS_API); | ||||||
|  | 	mapsEmbedUrl.searchParams.append("center", `${location[0]},${location[1]}`); | ||||||
|  | 	mapsEmbedUrl.searchParams.append("zoom", 14); | ||||||
|  | 	return ( | ||||||
|  | 		<Box | ||||||
|  | 			align="center" | ||||||
|  | 			background="light-1" | ||||||
|  | 			width="100vw" | ||||||
|  | 			justify="center" | ||||||
|  | 			direction="column" | ||||||
|  | 			alignContent="center" | ||||||
|  | 			height="95vh" | ||||||
|  | 		> | ||||||
|  | 			{location[0] === null ? | ||||||
|  | 				"Loading Location": | ||||||
|  | 				<iframe | ||||||
|  | 					width="650" | ||||||
|  | 					height="300" | ||||||
|  | 					frameBorder="0" | ||||||
|  | 					style={{ border: 0 }} | ||||||
|  | 					src={mapsEmbedUrl.href}> | ||||||
|  | 				</iframe> | ||||||
|  | 			} | ||||||
|  | 			<Button primary size="large" onClick={props.close} | ||||||
|  | 				label="Dismiss" margin="1.5em" /> | ||||||
|  | 		</Box> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Maps.propTypes = { | ||||||
|  | 	data: PropTypes.object, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Maps.pluginName = "Maps"; | ||||||
|  |  | ||||||
|  | export default Maps; | ||||||
							
								
								
									
										67
									
								
								src/plugins/Marketplace.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/plugins/Marketplace.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import { Box, Button, Card, CardBody, CardFooter, CardHeader, Heading } from "grommet"; | ||||||
|  | import { InstallOption, Trash } from "grommet-icons"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function Marketplace(props) { | ||||||
|  | 	const plugins = [ | ||||||
|  | 		{ | ||||||
|  | 			name: "Maps", | ||||||
|  | 			description: "Google Maps is a mapping service. It offers satellite imagery, aerial photography, street maps, 360° interactive panoramic views of streets, real-time traffic conditions, and route planning for traveling.", | ||||||
|  | 			installed: true | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Smart Home", | ||||||
|  | 			description: "Smart Home Plugin lets you integrate with various well known smart devices allowing you to stream video from various devices.", | ||||||
|  | 			installed: true | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "Spotify", | ||||||
|  | 			description: "With Spotify, you have access to a world of music. You can listen to artists and albums, or create your own playlist of your favourite songs.", | ||||||
|  | 			installed: false | ||||||
|  | 		}, | ||||||
|  | 	]; | ||||||
|  | 	return ( | ||||||
|  | 		<Box | ||||||
|  | 			align="center" | ||||||
|  | 			background="light-1" | ||||||
|  | 			width="100vw" | ||||||
|  | 			justify="center" | ||||||
|  | 			direction="column" | ||||||
|  | 			alignContent="center" | ||||||
|  | 			height="95vh" | ||||||
|  | 		> | ||||||
|  | 			<Box wrap="wrap" direction="row"> | ||||||
|  | 				{ | ||||||
|  | 					plugins.map(i => | ||||||
|  | 						<Card key={i.name} style={{margin: "1em", width: "30%"}}> | ||||||
|  | 							<CardHeader pad="medium"><Heading>{i.name}</Heading></CardHeader> | ||||||
|  | 							<CardBody pad="medium">{i.description}</CardBody> | ||||||
|  | 							<CardFooter pad="medium" justify="center"> | ||||||
|  | 								<Button | ||||||
|  | 									label={i.installed ? "Uninstall": "Install"} | ||||||
|  | 									icon={i.installed ? | ||||||
|  | 										<Trash color="status-error" /> : | ||||||
|  | 										<InstallOption color="status-ok" /> | ||||||
|  | 									} | ||||||
|  | 								/> | ||||||
|  | 							</CardFooter> | ||||||
|  | 						</Card> | ||||||
|  | 					) | ||||||
|  | 				} | ||||||
|  | 			</Box> | ||||||
|  | 			<Button primary size="large" onClick={props.close} | ||||||
|  | 				label="Dismiss" margin="1.5em" /> | ||||||
|  | 		</Box> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Marketplace.propTypes = { | ||||||
|  | 	data: PropTypes.object, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Marketplace.pluginName = "Marketplace"; | ||||||
|  |  | ||||||
|  | export default Marketplace; | ||||||
							
								
								
									
										59
									
								
								src/plugins/PetMode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/plugins/PetMode.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | import React, { useState, useEffect } from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import GenericPageWithIcon from "./GenericPageWithIcon"; | ||||||
|  | import { Sun } from "grommet-icons"; | ||||||
|  | import axios from "axios"; | ||||||
|  |  | ||||||
|  | import Telegram from "../utils/telegram"; | ||||||
|  | import { CAR_API, TG_API, TG_USERID  } from "../config"; | ||||||
|  |  | ||||||
|  | function sendNotification(temp) { | ||||||
|  | 	if (window.sendingPetAlert) | ||||||
|  | 		return; | ||||||
|  | 	window.sendingPetAlert = true; | ||||||
|  | 	const bot = new Telegram(TG_API); | ||||||
|  | 	bot.sendMessage(TG_USERID, `The temperature in the car is ${temp}°C. Please check your child/pet.`); | ||||||
|  | 	window.sendingPetAlert = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function PetMode(props) { | ||||||
|  | 	const [ temp, setTemp ] = useState(null); | ||||||
|  | 	const [ , setNotified ] = useState(false); | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		axios.get(`${CAR_API}data/InsideTemperature`) | ||||||
|  | 			.then(resp => setTemp(resp.data.value)); | ||||||
|  | 		const id = setInterval( | ||||||
|  | 			() => axios.get(`${CAR_API}data/InsideTemperature`) | ||||||
|  | 				.then(resp => { | ||||||
|  | 					setTemp(resp.data.value); | ||||||
|  | 					if (resp.data.value > 35 || resp.data.value < 5) { | ||||||
|  | 						setNotified(notified => { | ||||||
|  | 							if (notified) | ||||||
|  | 								return true; | ||||||
|  | 							sendNotification(resp.data.value); | ||||||
|  | 							return true; | ||||||
|  | 						}); | ||||||
|  | 					} | ||||||
|  | 				}), | ||||||
|  | 			1000 | ||||||
|  | 		); | ||||||
|  | 		return () => clearInterval(id); | ||||||
|  | 	}, []); | ||||||
|  | 	if (temp === null) | ||||||
|  | 		return <></>; | ||||||
|  | 	return ( | ||||||
|  | 		<GenericPageWithIcon | ||||||
|  | 			title={temp === null ? "Fetching temperature": `${temp}°C`} | ||||||
|  | 			description={`The temperature inside is ${temp}°C. The pet / child is ${temp > 35 ? "un": ""}safe.`} | ||||||
|  | 			icon={<Sun color={temp > 35 ? "status-critical": "plain"} size="xlarge" />} | ||||||
|  | 			close={props.close} | ||||||
|  | 		/>); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | PetMode.propTypes = { | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | PetMode.pluginName = "Temperature"; | ||||||
|  |  | ||||||
|  | export default PetMode; | ||||||
							
								
								
									
										50
									
								
								src/plugins/SmartHome.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/plugins/SmartHome.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | import React, {useState, useEffect} from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import { Box, Button } from "grommet"; | ||||||
|  | import axios from "axios"; | ||||||
|  |  | ||||||
|  | import { CAMERA_URL, LIGHTS_URL } from "../config"; | ||||||
|  |  | ||||||
|  | function SmartHome(props) { | ||||||
|  | 	const [ idx, setIdx ] = useState(0); | ||||||
|  | 	useEffect(() => { | ||||||
|  | 		const id = setInterval(() => setIdx(i => i + 1), 20); | ||||||
|  | 		return () => clearInterval(id); | ||||||
|  | 	}, []); | ||||||
|  | 	function turnOn() { | ||||||
|  | 		axios.get(`${LIGHTS_URL}/on`); | ||||||
|  | 	} | ||||||
|  | 	function turnOff() { | ||||||
|  | 		axios.get(`${LIGHTS_URL}/off`); | ||||||
|  | 	} | ||||||
|  | 	return ( | ||||||
|  | 		<Box | ||||||
|  | 			align="center" | ||||||
|  | 			background="light-1" | ||||||
|  | 			width="100vw" | ||||||
|  | 			justify="center" | ||||||
|  | 			direction="column" | ||||||
|  | 			alignContent="center" | ||||||
|  | 			height="95vh" | ||||||
|  | 		> | ||||||
|  | 			<img src={`${CAMERA_URL}?id=${idx}`} /> | ||||||
|  | 			<Box direction="row" margin="1em 0"> | ||||||
|  | 				<Button primary size="large" onClick={turnOn} | ||||||
|  | 					label="Turn On Lights" margin="0 0 0 0" /> | ||||||
|  | 				<Button primary size="large" onClick={turnOff} | ||||||
|  | 					label="Turn Off Lights" margin="0 0 0 1em" /> | ||||||
|  | 			</Box> | ||||||
|  | 			<Button primary size="large" onClick={props.close} | ||||||
|  | 				label="Dismiss" margin="0" /> | ||||||
|  | 		</Box> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | SmartHome.propTypes = { | ||||||
|  | 	data: PropTypes.object, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | SmartHome.pluginName = "Smart Home"; | ||||||
|  |  | ||||||
|  | export default SmartHome; | ||||||
							
								
								
									
										23
									
								
								src/plugins/Temperature.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/plugins/Temperature.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import GenericPageWithIcon from "./GenericPageWithIcon"; | ||||||
|  | import { Sun } from "grommet-icons"; | ||||||
|  |  | ||||||
|  | function Temperature(props) { | ||||||
|  | 	return ( | ||||||
|  | 		<GenericPageWithIcon | ||||||
|  | 			title={props.data.temperature} | ||||||
|  | 			description={`The current temperature outside is ${props.data.temperature}℃`} | ||||||
|  | 			icon={<Sun color="plain" size="xlarge" />} | ||||||
|  | 			close={props.close} | ||||||
|  | 		/>); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Temperature.propTypes = { | ||||||
|  | 	data: PropTypes.object, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Temperature.pluginName = "Temperature"; | ||||||
|  |  | ||||||
|  | export default Temperature; | ||||||
							
								
								
									
										23
									
								
								src/plugins/Warning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/plugins/Warning.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import PropTypes from "prop-types"; | ||||||
|  | import GenericPageWithIcon from "./GenericPageWithIcon"; | ||||||
|  | import { Alert } from "grommet-icons"; | ||||||
|  |  | ||||||
|  | function Warning(props) { | ||||||
|  | 	return ( | ||||||
|  | 		<GenericPageWithIcon | ||||||
|  | 			title={props.data.title} | ||||||
|  | 			description={props.data.description} | ||||||
|  | 			icon={<Alert color="status-critical" size="xlarge" />} | ||||||
|  | 			close={props.close} | ||||||
|  | 		/>); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Warning.propTypes = { | ||||||
|  | 	data: PropTypes.object, | ||||||
|  | 	close: PropTypes.func | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | Warning.pluginName = "Warning"; | ||||||
|  |  | ||||||
|  | export default Warning; | ||||||
| @@ -1,6 +1,19 @@ | |||||||
| import Mpd from "./mpd"; | import Warning from "./Warning"; | ||||||
| import Weather from "./weather"; | import Manual from "./Manual"; | ||||||
|  | import Temperature from "./Temperature"; | ||||||
|  | import Accident from "./Accident"; | ||||||
|  | import PetMode from "./PetMode"; | ||||||
|  | import SmartHome from "./SmartHome"; | ||||||
|  | import Maps from "./Maps"; | ||||||
|  | import Marketplace from "./Marketplace"; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
| 	mpd: Mpd, | 	warning: Warning, | ||||||
| 	weather: Weather, | 	manual: Manual, | ||||||
|  | 	temperature: Temperature, | ||||||
|  | 	accident: Accident, | ||||||
|  | 	petMode: PetMode, | ||||||
|  | 	smartHome: SmartHome, | ||||||
|  | 	maps: Maps, | ||||||
|  | 	marketplace: Marketplace, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,9 +0,0 @@ | |||||||
| import React from "react"; |  | ||||||
|  |  | ||||||
| function Mpd() { |  | ||||||
| 	return <p> MPD Plugin </p>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| Mpd.pluginName = "Music Player"; |  | ||||||
|  |  | ||||||
| export default Mpd; |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| import React from "react"; |  | ||||||
|  |  | ||||||
| function Weather() { |  | ||||||
| 	return <p> Weather Plugin </p>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| Weather.pluginName = "Weather"; |  | ||||||
|  |  | ||||||
| export default Weather; |  | ||||||
| @@ -1,3 +1,9 @@ | |||||||
| const WS_BASE = "http://localhost:5050/"; | const WS_BASE = "http://localhost:5050/"; | ||||||
|  | const TG_API = "xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx"; | ||||||
|  | const TG_USERID = 123456789; | ||||||
|  | const CAR_API = "http://localhost:5000/"; | ||||||
|  | const CAMERA_URL = "http://path.to/still/image.jpg"; | ||||||
|  | const MAPS_API = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; | ||||||
|  | const LIGHTS_URL = "http://192.168.1.50:5000/"; | ||||||
|  |  | ||||||
| export { WS_BASE }; | export { WS_BASE, TG_API, TG_USERID, CAR_API, CAMERA_URL, MAPS_API, LIGHTS_URL }; | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								src/utils/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/utils/index.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								src/utils/telegram.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/utils/telegram.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | import axios from "axios"; | ||||||
|  |  | ||||||
|  | class Telegram { | ||||||
|  | 	constructor(token) { | ||||||
|  | 		this.axios = axios.create({ | ||||||
|  | 			baseURL: `https://api.telegram.org/bot${token}` | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sendMessage(chat_id, text) { | ||||||
|  | 		this.axios.post("/sendMessage", {chat_id, text}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	sendLocation(chat_id, latitude, longitude) { | ||||||
|  | 		this.axios.post("/sendLocation", {chat_id, latitude, longitude}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default Telegram; | ||||||
							
								
								
									
										17
									
								
								src/voiceSlice.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/voiceSlice.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | import { createSlice } from "@reduxjs/toolkit"; | ||||||
|  |  | ||||||
|  | export const voiceSlice = createSlice({ | ||||||
|  | 	name: "voice", | ||||||
|  | 	initialState: { | ||||||
|  | 		recording: false, | ||||||
|  | 		text: null | ||||||
|  | 	}, | ||||||
|  | 	reducers: { | ||||||
|  | 		setRecording: (state, action) => ({...state, recording: action.payload}), | ||||||
|  | 		setText: (state, action) => ({...state, text: action.payload}), | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export const { setRecording, setText } = voiceSlice.actions; | ||||||
|  | export const selectVoice = state => state.voice; | ||||||
|  | export default voiceSlice.reducer; | ||||||
| @@ -2590,6 +2590,13 @@ axe-core@^4.0.2: | |||||||
|   resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf" |   resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.1.tgz#70a7855888e287f7add66002211a423937063eaf" | ||||||
|   integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ== |   integrity sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ== | ||||||
|  |  | ||||||
|  | axios@^0.21.1: | ||||||
|  |   version "0.21.1" | ||||||
|  |   resolved "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" | ||||||
|  |   integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== | ||||||
|  |   dependencies: | ||||||
|  |     follow-redirects "^1.10.0" | ||||||
|  |  | ||||||
| axobject-query@^2.2.0: | axobject-query@^2.2.0: | ||||||
|   version "2.2.0" |   version "2.2.0" | ||||||
|   resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" |   resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" | ||||||
| @@ -5204,7 +5211,7 @@ flush-write-stream@^1.0.0: | |||||||
|     inherits "^2.0.3" |     inherits "^2.0.3" | ||||||
|     readable-stream "^2.3.6" |     readable-stream "^2.3.6" | ||||||
|  |  | ||||||
| follow-redirects@^1.0.0: | follow-redirects@^1.0.0, follow-redirects@^1.10.0: | ||||||
|   version "1.13.1" |   version "1.13.1" | ||||||
|   resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" |   resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" | ||||||
|   integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== |   integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user