Compare commits
29 Commits
c5a475f9c5
...
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 | |||
| fcc81cf7e5 | |||
| 459d334732 |
@@ -34,7 +34,8 @@ module.exports = {
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
],
|
||||
"no-unused-vars": [1]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
||||
src/config.js
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import React from "react";
|
||||
import { Grommet } from "grommet";
|
||||
import Core from "./Core";
|
||||
import WebSocketProvider from "./WebSocket";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Grommet plain>
|
||||
<Core />
|
||||
</Grommet>
|
||||
<WebSocketProvider>
|
||||
<Grommet plain>
|
||||
<Core />
|
||||
</Grommet>
|
||||
</WebSocketProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
54
src/Core.js
54
src/Core.js
@@ -1,35 +1,51 @@
|
||||
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();
|
||||
console.log(plugins.default);
|
||||
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>)
|
||||
}
|
||||
</>
|
||||
}
|
||||
{plugin ?
|
||||
React.cloneElement(plugin(), props)
|
||||
:null
|
||||
<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>)
|
||||
}
|
||||
</>;
|
||||
}
|
||||
|
||||
57
src/WebSocket.js
Normal file
57
src/WebSocket.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { createContext } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
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);
|
||||
|
||||
export { WebSocketContext };
|
||||
|
||||
function WebSocketProvider({ children }) {
|
||||
let socket;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
if (!socket) {
|
||||
socket = io.connect(WS_BASE);
|
||||
|
||||
socket.on("switchPlugin", (payload) => {
|
||||
dispatch(setData(payload.data || {}));
|
||||
dispatch(setPlugin(payload.plugin));
|
||||
if (payload.time) {
|
||||
setTimeout(() => {
|
||||
dispatch(setData({}));
|
||||
dispatch(setPlugin(false));
|
||||
}, 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 (
|
||||
<WebSocketContext.Provider value={socket}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
WebSocketProvider.propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
export default WebSocketProvider;
|
||||
@@ -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;
|
||||
9
src/sample.config.js
Normal file
9
src/sample.config.js
Normal file
@@ -0,0 +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, 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