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": [ | ||||
| 			"error", | ||||
| 			"always" | ||||
| 		] | ||||
| 		], | ||||
| 		"no-unused-vars": [1] | ||||
| 	}, | ||||
| 	"settings": { | ||||
| 		"react": { | ||||
|   | ||||
| @@ -7,7 +7,9 @@ | ||||
|     "@testing-library/jest-dom": "^4.2.4", | ||||
|     "@testing-library/react": "^9.3.2", | ||||
|     "@testing-library/user-event": "^7.1.2", | ||||
|     "axios": "^0.21.1", | ||||
|     "grommet": "^2.16.2", | ||||
|     "grommet-icons": "^4.5.0", | ||||
|     "polished": "^4.0.5", | ||||
|     "react": "^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 { useSelector, useDispatch } from "react-redux"; | ||||
| import { Box, Button, Clock, Text } from "grommet"; | ||||
|  | ||||
| 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() { | ||||
| 	const coreState = useSelector(selectCore); | ||||
| 	const voiceState = useSelector(selectVoice); | ||||
| 	const dispatch = useDispatch(); | ||||
| 	const plugin = plugins.default[coreState.plugin]; | ||||
| 	const plugin = plugins[coreState.plugin]; | ||||
| 	const props = { | ||||
| 		data: coreState.data, | ||||
| 		close: () => dispatch(setPlugin(false)) | ||||
| 	}; | ||||
| 	return <> | ||||
| 		{plugin ? null : | ||||
| 			<> | ||||
| 				<p>Welcome to Honda!</p> | ||||
| 				{Object.keys(plugins.default).map( | ||||
| 					i => <p | ||||
| 						onClick={() => dispatch(setPlugin(i))} | ||||
| 						key={i} | ||||
| 					> | ||||
| 						{plugins.default[i].pluginName} | ||||
| 					</p>) | ||||
| 				} | ||||
| 			</> | ||||
| 		<Box height="5vh" background="light-1" justify="center" direction="row"> | ||||
| 			{voiceState.recording ? <VoiceBars style={{height: "100%"}}/>: | ||||
| 				voiceState.text === null ? null: | ||||
| 					<Text style={{paddingTop: "1em"}}>{voiceState.text}</Text> | ||||
| 			} | ||||
| 		</Box> | ||||
| 		{plugin ? React.createElement(plugin, props, null) : ( | ||||
| 			<Box | ||||
| 				align="center" | ||||
| 				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 { useDispatch } from "react-redux"; | ||||
| import { setData, setPlugin } from "./coreSlice"; | ||||
| import { setRecording, setText } from "./voiceSlice"; | ||||
|  | ||||
| const WebSocketContext = createContext(null); | ||||
|  | ||||
| @@ -17,8 +18,8 @@ function WebSocketProvider({ children }) { | ||||
| 		socket = io.connect(WS_BASE); | ||||
|  | ||||
| 		socket.on("switchPlugin", (payload) => { | ||||
| 			dispatch(setData(payload.data || {})); | ||||
| 			dispatch(setPlugin(payload.plugin)); | ||||
| 			dispatch(setData(payload.data)); | ||||
| 			if (payload.time) { | ||||
| 				setTimeout(() => { | ||||
| 					dispatch(setData({})); | ||||
| @@ -26,6 +27,20 @@ function WebSocketProvider({ children }) { | ||||
| 				}, 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 ( | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { configureStore } from "@reduxjs/toolkit"; | ||||
| import coreReducer from "../coreSlice"; | ||||
| import voiceReducer from "../voiceSlice"; | ||||
|  | ||||
| export default configureStore({ | ||||
| 	reducer: { | ||||
| 		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 Weather from "./weather"; | ||||
| import Warning from "./Warning"; | ||||
| 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 { | ||||
| 	mpd: Mpd, | ||||
| 	weather: Weather, | ||||
| 	warning: Warning, | ||||
| 	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 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" | ||||
|   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: | ||||
|   version "2.2.0" | ||||
|   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" | ||||
|     readable-stream "^2.3.6" | ||||
|  | ||||
| follow-redirects@^1.0.0: | ||||
| follow-redirects@^1.0.0, follow-redirects@^1.10.0: | ||||
|   version "1.13.1" | ||||
|   resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7" | ||||
|   integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg== | ||||
|   | ||||
		Reference in New Issue
	
	Block a user