CEC-2291 Remote Commands (#194)

This commit is contained in:
arpanetus
2022-09-07 23:21:57 +06:00
committed by GitHub
parent 153c6bdcf7
commit aa5a1e20e0
19 changed files with 1187 additions and 148 deletions

View File

@@ -4,19 +4,24 @@
"private": true,
"dependencies": {
"@datadog/browser-logs": "^3.11.0",
"@date-io/date-fns": "1.x",
"@date-io/moment": "1.x",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/pickers": "^3.3.10",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.26.1",
"clsx": "^1.1.1",
"date-fns": "^2.29.2",
"email-validator": "^2.0.4",
"env-cmd": "^10.1.0",
"leaflet": "^1.8.0",
"material-ui-dropzone": "^3.5.0",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-leaflet": "^3.2.5",

View File

@@ -6873,6 +6873,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
class="MuiTouchRipple-root"
/>
</button>
<button
aria-controls="tabpanel-5"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-5"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Remote Commands
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
<span
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
@@ -7036,6 +7054,13 @@ exports[`App Route /vehicle-status authenticated 1`] = `
id="tabpanel-4"
role="tabpanel"
/>
<div
aria-labelledby="tab-5"
class="makeStyles-fullWidth-0"
hidden=""
id="tabpanel-5"
role="tabpanel"
/>
</div>
</main>
</main>

View File

@@ -0,0 +1,26 @@
import useStyles from "../../useStyles";
import clsx from "clsx";
import Typography from "@material-ui/core/Typography";
import SendCommand from "../../Controls/SendCommand";
import PropTypes from "prop-types";
import {VehicleProvider} from "../../Contexts/VehicleContext";
const RemoteCommandsTab = (props) => {
const { vin } = props;
const classes = useStyles();
return (
<div className={clsx(classes.paper, classes.tableSize)}>
<Typography variant="h6">Vehicle Commands</Typography>
<VehicleProvider>
<SendCommand vins={[vin]}></SendCommand>
</VehicleProvider>
</div>
)
}
RemoteCommandsTab.propTypes = {
vin: PropTypes.string,
}
export default RemoteCommandsTab

View File

@@ -0,0 +1,40 @@
jest.mock("../../Contexts/VehicleContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../Contexts/UserContext");
jest.mock("@material-ui/core/utils/unstable_useId", () =>
jest.fn().mockReturnValue("mui-test-id")
);
import {render, waitFor} from "@testing-library/react";
import {StatusProvider} from "../../Contexts/StatusContext";
import {setToken, UserProvider} from "../../Contexts/UserContext";
import RemoteCommandsTab from "./RemoteCommandsTab";
import addSnapshotSerializer from "../../../utils/snapshot";
import {TEST_AUTH_OBJECT} from "../../../utils/testing";
import React from "react";
const renderRemoteCommandsTab = async () => {
const { container } = render(
<StatusProvider>
<UserProvider>
<RemoteCommandsTab vin="TESTVIN1234567890" />
</UserProvider>
</StatusProvider>
);
await waitFor(() => {
/* render */
});
return container;
};
describe("RemoteCommandsTab", () => {
beforeAll(() => {
addSnapshotSerializer(expect);
});
it("Render", async () => {
setToken(TEST_AUTH_OBJECT);
const container = await renderRemoteCommandsTab();
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,173 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RemoteCommandsTab Render 1`] = `
<div>
<div
data-testid="mocked-statusprovider"
>
<div
data-testid="mocked-userprovider"
>
<div
class="makeStyles-paper-0 makeStyles-tableSize-0"
>
<h6
class="MuiTypography-root MuiTypography-h6"
>
Vehicle Commands
</h6>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="makeStyles-paper-0"
style="margin-top: 20px;"
>
<div
class="MuiFormControl-root makeStyles-formControl-0"
>
<label
class="MuiFormLabel-root MuiInputLabel-root makeStyles-whiteBackground-0 MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-marginDense MuiInputLabel-outlined MuiFormLabel-filled"
data-shrink="true"
for="send-command"
>
Command
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
>
<select
aria-invalid="false"
class="MuiSelect-root MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
id="send-command"
name="send-command"
>
<option
value="doors_lock"
>
Lock doors
</option>
<option
value="doors_unlock"
>
Unlock doors
</option>
<option
value="vent_windows"
>
Vent windows
</option>
<option
value="close_windows"
>
Close windows
</option>
<option
value="flash_headlights"
>
Flash headlights
</option>
<option
value="trunk_close"
>
Close trunk
</option>
<option
value="alert"
>
Alert
</option>
<option
value="precondition"
>
Precondition
</option>
<option
value="california_mode"
>
California mode
</option>
<option
value="trunk_open"
>
Open trunk
</option>
<option
value="temp_cabin"
>
Set cabin temperature(°C)
</option>
<option
value="defrost"
>
Defrost
</option>
<option
value="driver_seat_preheat"
>
Driver seat preheat
</option>
<option
value="passenger_seat_preheat"
>
Preheat passenger seat
</option>
<option
value="steering_wheel_preheat"
>
Preheat Steering wheel
</option>
<option
value="charging"
>
Charging
</option>
</select>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-0 MuiOutlinedInput-notchedOutline"
style="padding-left: 8px;"
>
<legend
class="PrivateNotchedOutline-legend-0"
style="width: 0.01px;"
>
<span>
</span>
</legend>
</fieldset>
</div>
</div>
<button
aria-label="send command"
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Send
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@@ -119,6 +119,24 @@ exports[`CarStatus Render 1`] = `
class="MuiTouchRipple-root"
/>
</button>
<button
aria-controls="tabpanel-5"
aria-selected="false"
class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit"
id="tab-5"
role="tab"
tabindex="-1"
type="button"
>
<span
class="MuiTab-wrapper"
>
Remote Commands
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
<span
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
@@ -228,6 +246,13 @@ exports[`CarStatus Render 1`] = `
id="tabpanel-4"
role="tabpanel"
/>
<div
aria-labelledby="tab-5"
class="makeStyles-fullWidth-0"
hidden=""
id="tabpanel-5"
role="tabpanel"
/>
</div>
</div>
</div>

View File

@@ -12,6 +12,7 @@ import TabPanel from "../../Controls/TabPanel";
import { useStatusContext } from "../../Contexts/StatusContext";
import useStyles from "../../useStyles";
import CANSignalsTab from "./CANSignalsTab";
import RemoteCommandsTab from "./RemoteCommandsTab";
const tabHashes = ["details", "updates", "filters"];
@@ -64,6 +65,7 @@ const CarStatus = () => {
<Tab label="CAN Filters" {...tabProps(2)} />
<Tab label="Digital Twin" {...tabProps(3)} />
<Tab label="CAN Signals" {...tabProps(4)} />
<Tab label="Remote Commands" {...tabProps(5)} />
</Tabs>
</Box>
@@ -86,6 +88,10 @@ const CarStatus = () => {
<TabPanel value={tabIndex} index={4} className={classes.fullWidth}>
<CANSignalsTab vin={vin} />
</TabPanel>
<TabPanel value={tabIndex} index={5} className={classes.fullWidth}>
<RemoteCommandsTab vin={vin} />
</TabPanel>
</div>
);
};

View File

@@ -164,10 +164,10 @@ export const VehicleProvider = ({ children }) => {
}
};
const sendCommand = async (vins, command, parameters, token) => {
const sendCommand = async (vins, command, token) => {
try {
setBusy(true);
const result = await api.sendCommand(vins, command, parameters, token);
const result = await api.sendCommand(vins, command, token);
if (result.error)
throw new Error(`Send command error. ${result.message}`);
return result;

View File

@@ -317,6 +317,55 @@ describe("VehicleContext", () => {
checkBaseResults("", "false");
});
});
describe("sendCommand", () => {
beforeEach(async () => {
const TestComp = () => {
const {busy, sendCommand} = useVehicleContext();
const { message, setMessage } = useStatusContext();
const sendC = async (vin, command) => {
try {
await sendCommand(vin, command);
} catch (e) {
setMessage(e.message);
}
};
return (
<>
<div data-testid="error">{message}</div>
<div data-testid="busy">{busy.toString()}</div>
<button data-testid="sendCommandNullVin" onClick={() => sendC(null, {command:"doors_lock"})} />
<button data-testid="sendCommandNonexistentVin" onClick={() => sendC(["11111111111111111"], {command:"doors_lock"})} />
<button
data-testid="sendCommandVin"
onClick={() => sendC("3C4PDCBG0ET127145", {command:"doors_lock"})}
/>
<button
data-testid="sendCommandWrongCommand"
onClick={() => sendC("3C4PDCBG0ET127145", {command:"noSuchCommand"})}
/>
</>
);}
render(
<StatusProvider>
<VehicleProvider>
<TestComp />
</VehicleProvider>
</StatusProvider>
);
});
afterEach(() => {
cleanup();
});
it("initial state", () => {
checkBaseResults("", "false");
});
})
});
const expectedFilters = [

View File

@@ -0,0 +1,251 @@
import {Grid, Slider, Switch} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
const tempMarks = [...[{value: 0, label: "Off"}, {value: 1, label: "On"},],
...Array.from({length: 26}, (_, i) => {
return {value: i + 2, label: `${i + 15}°C`}
})];
const valuetext = (value) => {
return `${value}`;
}
const highMidLowOffMarks = [{
value: 3, label: "High",
}, {
value: 2, label: "Mid",
}, {
value: 1, label: "Low",
}, {
value: 0, label: "Off",
},]
const precondMarks = [
{value: 0, label: "Battery"},
{value: 1, label: "All"},
{value: 2, label: "Climate"},
{value: 3, label: "Stop"},
]
const labelFunc = (marks) => (value) => marks.find(mark => mark.value === value)?.label;
const labelRemoved = (marks) => Array.from(marks).map((mark) => {
return {value: mark.value}
})
const Commands = [
{value: "doors_lock", label: "Lock doors"},
{value: "doors_unlock", label: "Unlock doors"},
{value: "vent_windows", label: "Vent windows"},
{value: "close_windows", label: "Close windows"},
{value: "flash_headlights", label: "Flash headlights"},
{value: "trunk_close", label: "Close trunk"},
{value: "alert", label: "Alert"},
{
value: "precondition", label: "Precondition", params: {
dataFunc: (val, handleValChange) => <span>
<Typography id="precondition-slider" gutterBottom>
Set driver seat preheat
</Typography>
<Slider
valueLabelFormat={labelFunc(precondMarks)}
getAriaValueText={labelFunc(precondMarks)}
defaultValue={0}
value={val}
aria-labelledby="precondition-slider"
valueLabelDisplay="auto"
step={null}
min={0}
max={3}
marks={precondMarks}
size="small"
onChange={handleValChange}
/>
</span>,
}
},
{
value: "california_mode", label: "California mode", params: {
dataFunc: (val, handleValChange) => {
if (typeof val !== "boolean") {
val = false
}
return <span>
<Typography id="california_mode-slider" gutterBottom>
Set California mode
</Typography>
<Typography component="div">
<Grid component="label" container alignItems="center" spacing={1}>
<Grid item>Off</Grid>
<Grid item>
<Switch checked={val} onChange={handleValChange} name="checkedC"/>
</Grid>
<Grid item>On</Grid>
</Grid>
</Typography>
</span>
},
},
}, {
value: "trunk_open", label: "Open trunk", params: {
dataFunc: (val, handleValChange) => <span>
<Typography id="trunk_open-slider" gutterBottom>
Set trunk's openness level
</Typography>
<Slider
defaultValue={1}
value={val}
getAriaValueText={valuetext}
aria-label="Open trunk"
aria-labelledby="trunk_open-slider"
valueLabelDisplay="auto"
step={1}
marks
size="small"
min={1}
max={5}
onChange={handleValChange}
/>
</span>,
}
}, {
value: "temp_cabin", label: "Set cabin temperature(°C)", params: {
dataFunc: (val, handleValChange) => {
return <span>
<Typography id="temp_cabin-slider" gutterBottom>
Set cabin temperature
</Typography>
<Slider
valueLabelFormat={labelFunc(tempMarks)}
getAriaValueText={labelFunc(tempMarks)}
defaultValue={0}
value={val}
aria-labelledby="temp_cabin-slider"
valueLabelDisplay="auto"
step={1}
min={0}
max={27}
marks={labelRemoved(tempMarks)}
size="small"
onChange={handleValChange}
/>
</span>
}
}
}, {
value: "defrost", label: "Defrost", params: {
dataFunc: (val, handleValChange) => {
if (typeof val !== "boolean") {
val = false
}
return <span>
<Typography id="defrost-slider" gutterBottom>
Set defrost
</Typography>
<Typography component="div">
<Grid component="label" container alignItems="center" spacing={1}>
<Grid item>Off</Grid>
<Grid item>
<Switch checked={val} onChange={handleValChange} name="checkedC"/>
</Grid>
<Grid item>On</Grid>
</Grid>
</Typography>
</span>
},
},
}, {
value: "driver_seat_preheat", label: "Driver seat preheat", params: {
dataFunc: (val, handleValChange) => <span>
<Typography id="driver_seat_preheat-slider" gutterBottom>
Set driver seat preheat
</Typography>
<Slider
valueLabelFormat={labelFunc(highMidLowOffMarks)}
getAriaValueText={labelFunc(highMidLowOffMarks)}
defaultValue={0}
value={val}
aria-labelledby="driver_seat_preheat-slider"
valueLabelDisplay="auto"
step={null}
min={0}
max={3}
marks={highMidLowOffMarks}
size="small"
onChange={handleValChange}
/>
</span>,
}
}, {
value: "passenger_seat_preheat", label: "Preheat passenger seat", params: {
dataFunc: (val, handleValChange) => <span>
<Typography id="passenger_seat_preheat-slider" gutterBottom>
Set passenger seat preheat
</Typography>
<Slider
valueLabelFormat={labelFunc(highMidLowOffMarks)}
getAriaValueText={labelFunc(highMidLowOffMarks)}
defaultValue={0}
value={val}
aria-labelledby="passenger_seat_preheat-slider"
valueLabelDisplay="auto"
step={null}
min={0}
max={3}
marks={highMidLowOffMarks}
size="small"
onChange={handleValChange}
/>
</span>,
}
}, {
value: "steering_wheel_preheat", label: "Preheat Steering wheel", params: {
dataFunc: (val, handleValChange) => {
if (typeof val !== "boolean") {
val = false
}
return <span>
<Typography id="steering_wheel_preheat-slider" gutterBottom>
Set steering wheel preheat on/off
</Typography>
<Typography component="div">
<Grid component="label" container alignItems="center" spacing={1}>
<Grid item>Off</Grid>
<Grid item>
<Switch checked={val} onChange={handleValChange} name="checkedC"/>
</Grid>
<Grid item>On</Grid>
</Grid>
</Typography>
</span>
},
},
}, {
value: "charging", label: "Charging", params: {
dataFunc: (val, handleValChange) => {
if (typeof val !== "boolean") {
val = false
}
return <span>
<Typography id="charging-slider" gutterBottom>
Set charging on/off
</Typography>
<Typography component="div">
<Grid component="label" container alignItems="center" spacing={1}>
<Grid item>Off</Grid>
<Grid item>
<Switch checked={val} onChange={handleValChange} name="checkedC"/>
</Grid>
<Grid item>On</Grid>
</Grid>
</Typography>
</span>
},
},
}]
export default Commands;

View File

@@ -0,0 +1,73 @@
import PropTypes from "prop-types";
import {FormControl} from "@material-ui/core";
import useStyles from "../../useStyles";
import {MuiPickersUtilsProvider} from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
export const Dates = (
{
startDateFunc,
endDateFunc,
startDate,
handleStartChange,
endDate,
handleEndChange,
}) => {
if (startDateFunc && endDateFunc) {
return (<MuiPickersUtilsProvider utils={DateFnsUtils}>
{startDateFunc(startDate, handleStartChange)}
{endDateFunc(endDate, handleEndChange, startDate)}
</MuiPickersUtilsProvider>);
}
return null;
}
Dates.propTypes = {
startDateFunc: PropTypes.func,
endDateFunc: PropTypes.func,
startDate: PropTypes.instanceOf(Date),
handleStartChange: PropTypes.func,
endDate: PropTypes.instanceOf(Date),
handleEndChange: PropTypes.func
}
export const Parameters = (props) => {
const {params} = props;
const classes = useStyles();
if (params === null || params === undefined || params === "") {
return null;
}
const {data, handleDataChange} = props;
const {startDate, handleStartChange} = props;
const {endDate, handleEndChange} = props;
return (
<FormControl size="small" className={classes.formControl}>
<div style={{width: "300px", marginTop: "1em"}}>
{params.dataFunc(data, handleDataChange)}
</div>
<Dates
startDateFunc={params.startDateFunc}
endDateFunc={params.endDateFunc}
startDate={startDate}
handleStartChange={handleStartChange}
endDate={endDate}
handleEndChange={handleEndChange}
></Dates>
</FormControl>
)
}
Parameters.propTypes = {
params: PropTypes.any,
data: PropTypes.any,
handleDataChange: PropTypes.func,
startDate: PropTypes.instanceOf(Date),
handleStartChange: PropTypes.func,
endDate: PropTypes.instanceOf(Date),
handleEndChange: PropTypes.func
};

View File

@@ -0,0 +1,140 @@
jest.mock("@material-ui/pickers/MuiPickersUtilsProvider")
import {render, waitFor} from "@testing-library/react";
import {Dates, Parameters} from "./Parameters";
import addSnapshotSerializer from "../../../utils/snapshot";
const date = Date.parse("2011-10-10T14:48:00")
const renderDates = async (empty) => {
if (empty) {
await waitFor(() => {
/* render */
});
const { container } = render(<Dates startDateFunc={null} endDateFunc={null}/>)
return container;
}
const [start, setStart] = [date, (_) => {}];
const [end, setEnd] = [date, (_) => {}];
const handleStartChange = (val) => {
setStart(val)
}
const handleEndChange = (val) => {
setEnd(val)
}
await waitFor(() => {
/* render */
});
const {container} = render(
<div>
<Dates
startDateFunc={(val, handleValChange) => <div>{val.toString()}</div>}
endDateFunc={(val, handleValChange, prevVal) => <div>{val.toString()}, {prevVal.toString()}</div>}
startDate={start}
endDate={end}
handleStartChange={handleStartChange}
handleEndChange={handleEndChange}
/>
</div>
)
return container
}
describe("Dates", () => {
beforeAll(() => {
addSnapshotSerializer(expect);
})
it("Render empty", async () => {
const container = await renderDates(true);
expect(container).toMatchSnapshot();
})
it("Render filled", async () => {
const container = await renderDates(false);
expect(container).toMatchSnapshot();
})
})
const renderState = {
EMPTY: 0,
JUST_DATA: 1,
WITH_DATE: 2,
}
const renderParameters = async (rs) => {
await waitFor(() => {
/* render */
});
if (rs===renderState.EMPTY) {
const { container } = render(<Parameters/>)
return container;
}
const params = {dataFunc:(val, handleValChange) => <div>val.toString()</div>}
const [data, handleDataChange] = [true, (_)=>{}];
if (rs===renderState.JUST_DATA) {
const { container } = render(<Parameters
params={params}
data={data}
handleDataChange={handleDataChange}
/>)
return container;
}
params.startDateFunc = (val, handleValChange) => (<div>val.toString()</div>)
params.endDateFunc = (val, handleValChange, prevVal) => (<div>val.toString(), prevVal.toString()</div>)
const [start, handleStartChange] = [date, (_)=>{}];
const [end, handleEndChange] = [date, (_)=>{}];
if (rs===renderState.WITH_DATE) {
const {container} = render(<Parameters
params={params}
data={data}
handleDataChange={handleDataChange}
start={start}
handleStartChange={handleStartChange}
end={end}
handleEndChange={handleEndChange}
/>
)
return container
}
}
describe("Params", () => {
beforeAll(() => {
addSnapshotSerializer(expect);
})
it("Render empty", async () => {
const container = await renderParameters(renderState.EMPTY);
expect(container).toMatchSnapshot();
})
it("Render just data", async () => {
const container = await renderDates(renderState.JUST_DATA);
expect(container).toMatchSnapshot();
})
it("Render with date", async () => {
const container = await renderDates(renderState.WITH_DATE);
expect(container).toMatchSnapshot();
})
})

View File

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Dates Render empty 1`] = `<div />`;
exports[`Dates Render filled 1`] = `
<div>
<div>
<div>
1318258080000
</div>
<div>
1318258080000
,
1318258080000
</div>
</div>
</div>
`;
exports[`Params Render empty 1`] = `<div />`;
exports[`Params Render just data 1`] = `<div />`;
exports[`Params Render with date 1`] = `<div />`;

View File

@@ -1,31 +1,32 @@
import React, { useEffect, useState } from "react";
import React, {useEffect, useState} from "react";
import PropTypes from "prop-types";
import { FormControl, IconButton, InputLabel, Select } from "@material-ui/core";
import SendIcon from "@material-ui/icons/Send";
import {Button, FormControl, InputLabel, Select} from "@material-ui/core";
import { useVehicleContext } from "../../Contexts/VehicleContext";
import commands from "../../../services/commands";
import {useVehicleContext} from "../../Contexts/VehicleContext";
import commands from "./Commands";
import useStyles from "../../useStyles";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { logger } from "../../../services/monitoring";
import {useUserContext} from "../../Contexts/UserContext";
import {useStatusContext} from "../../Contexts/StatusContext";
import {logger} from "../../../services/monitoring";
import {Parameters} from "./Parameters";
import clsx from "clsx";
import {sanitize} from "./sanitize";
const SendCommand = ({ vins }) => {
const SendCommand = ({vins}) => {
const classes = useStyles();
const { sendCommand, busy } = useVehicleContext();
const {busy, sendCommand} = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
idToken: {jwtToken: token},
},
} = useUserContext();
const NoParameters = {
value: "",
label: "None",
};
const { setMessage } = useStatusContext();
const {setMessage} = useStatusContext();
const [command, setCommand] = useState("");
const [parameters, setParameters] = useState([NoParameters]);
const [parameter, setParameter] = useState("");
const [parameters, setParameters] = useState(null);
const [data, setData] = useState(null);
const [startTime, setStartTime] = useState(null)
const [endTime, setEndTime] = useState(null)
const changeCommandHandler = (e) => {
selectCommand(e.target.value);
};
@@ -34,16 +35,27 @@ const SendCommand = ({ vins }) => {
const params = getParameters(cmd);
setCommand(cmd);
setParameters(params);
setParameter(params[0].value);
setData(null);
setStartTime(null);
setEndTime(null);
};
const changeParametersHandler = (e) => {
setParameter(e.target.value);
const handleDataChange = (_, value) => {
setData(value);
};
const clickHandler = async (e) => {
const handleStartChange = (date, _) => {
setStartTime(date);
}
const handleEndChange = (date, _) => {
setEndTime(date)
}
const clickHandler = async (_) => {
try {
await sendCommand(vins, command, parameter, token);
const cmd = sanitize({command: command, data: data, start: startTime, end: endTime});
await sendCommand(vins, cmd, token);
if (vins.length === 1) {
setMessage(`Sent command to ${vins[0]}`);
} else {
@@ -59,13 +71,13 @@ const SendCommand = ({ vins }) => {
for (let i = 0, len = commands.length; i < len; i += 1) {
const item = commands[i];
if (item.value === cmd) {
if (!item.parameters) {
if (!item.params) {
break;
}
return item.parameters;
return item.params;
}
}
return [NoParameters];
return null;
};
useEffect(() => {
@@ -75,9 +87,9 @@ const SendCommand = ({ vins }) => {
}, []);
return (
<div className={classes.form} style={{ marginTop: 20 }}>
<div className={clsx(classes.paper)} style={{marginTop: 20}}>
<FormControl
className={classes.formControlInline}
className={classes.formControl}
variant="outlined"
size="small"
>
@@ -101,45 +113,27 @@ const SendCommand = ({ vins }) => {
))}
</Select>
</FormControl>
<FormControl
className={classes.formControlInline}
variant="outlined"
size="small"
>
<InputLabel
htmlFor="send-parameter"
className={classes.whiteBackground}
>
Parameter
</InputLabel>
<Select
native
value={parameter}
variant="outlined"
inputProps={{
name: "send-parameter",
id: "send-parameter",
}}
onChange={changeParametersHandler}
disabled={parameters.length === 0}
>
{parameters.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</Select>
</FormControl>
<IconButton
color="primary"
<Parameters
params={parameters}
data={data}
handleDataChange={handleDataChange}
startDate={startTime}
handleStartChange={handleStartChange}
endDate={endTime}
handleEndChange={handleEndChange}
/>
<Button
type="submit"
aria-label="send command"
component="span"
onClick={clickHandler}
size="small"
disabled={busy || vins.length === 0}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={clickHandler}
>
<SendIcon fontSize="large" />
</IconButton>
{busy ? "Sending..." : "Send"}
</Button>
</div>
);
};

View File

@@ -0,0 +1,115 @@
export const onOff = {
false: "off",
true: "on",
}
export const onOffTemp = {
0: "off",
1: "on",
...Object.fromEntries(Array.from({length: 26}, (_, i) => [i+2, `${i+15}`]))
}
export const precond = {
0: "battery",
1: "all",
2: "climate",
3: "stop"
}
export const hmol = {
0: "off",
1: "low",
2: "mid",
3: "high"
}
export const emptyCommands = [
"doors_lock", "doors_unlock", "vent_windows",
"close_windows", "trunk_close", "flash_headlights",
"alert"
]
export const onOffCommands = ["california_mode", "steering_wheel_preheat", "defrost", "charging"]
export const hmolCommands = ["passenger_seat_preheat", "driver_seat_preheat"]
const removeIfFieldsAreEmpty = (cmd, fields) => {
for (const field of fields) {
if (cmd[field] == null) {
delete cmd[field]
}
}
return cmd
}
const tempCabinPeriodConvert = (cmd) => {
if (cmd.data == null || typeof cmd.data !== "number" || cmd.data < 0 || cmd.data > 27) {
cmd.data = 0
}
cmd.data = onOffTemp[cmd.data]
return cmd
}
const onOffConvert = (cmd) => {
if (cmd.data == null || cmd.data !== true) {
cmd.data = false
}
cmd.data = onOff[cmd.data]
return cmd
}
const precondConvert = cmd => {
if (cmd.data == null || typeof cmd.data !== "number" || cmd.data < 0 || cmd.data > 3) {
cmd.data = 0
}
cmd.data = precond[cmd.data]
return cmd
}
const hmolConvert = (cmd) => {
if (cmd.data == null || typeof cmd.data !== "number" || cmd.data < 0 || cmd.data > 3) {
cmd.data = 0
}
cmd.data = hmol[cmd.data]
return cmd
}
const trunkOpenConvert = (cmd) => {
if (cmd.data == null || typeof cmd.data !== "number" || cmd.data < 1 || cmd.data > 5) {
cmd.data = 1
}
cmd.data = cmd.data.toString()
return cmd
}
export const sanitize = (cmd) => {
cmd = removeIfFieldsAreEmpty(cmd, ["data", "start", "end"])
if (onOffCommands.includes(cmd.command)) {
cmd = onOffConvert(cmd)
} else if (hmolCommands.includes(cmd.command)) {
cmd = hmolConvert(cmd)
} else if (cmd.command === "precondition") {
cmd = precondConvert(cmd)
} else if (cmd.command === "trunk_open") {
cmd = trunkOpenConvert(cmd)
} else if (cmd.command === "temp_cabin") {
cmd = tempCabinPeriodConvert(cmd)
} else {
delete cmd.data;
}
return cmd
}

View File

@@ -0,0 +1,150 @@
import {emptyCommands, hmol, hmolCommands, onOff, onOffCommands, onOffTemp, precond, sanitize,} from "./sanitize";
const randomValues = [null, undefined, "someString", 33]
describe("Sanitize test", () => {
it("empty commands", () => {
for (const command of emptyCommands) {
const cmd = sanitize({command})
expect(cmd.command).toEqual(command)
expect("data" in cmd).toEqual(false);
expect("start" in cmd).toEqual(false);
expect("end" in cmd).toEqual(false);
}
})
it("on-off commands with proper values", () => {
for (const command of onOffCommands) {
for (const data of [false, true]) {
const cmd = sanitize({command, data})
expect(cmd.data).toEqual(onOff[data])
expect(cmd.command).toEqual(command)
expect("start" in cmd).toEqual(false);
expect("end" in cmd).toEqual(false);
}
}
})
it("on-off commands with dirty values", () => {
for (const command of onOffCommands) {
const cmd = {command}
for (const rVal of randomValues) {
if (rVal !== undefined) {
cmd.data = rVal
}
const res = sanitize(cmd)
expect(res.data).toEqual("off")
expect(res.command).toEqual(command)
expect("start" in res).toEqual(false);
expect("end" in res).toEqual(false);
}
}
})
it("high-mid-low-off with proper values", () => {
for (const command of hmolCommands) {
for (const data of [0,1,2,3]) {
const cmd = sanitize({command, data})
expect(cmd.data).toEqual(hmol[data])
expect(cmd.command).toEqual(command)
expect("start" in cmd).toEqual(false);
expect("end" in cmd).toEqual(false);
}
}
})
it("precondition with proper values", () => {
for (const data of [0,1,2,3]) {
const cmd = sanitize({command:"precondition", data})
expect(cmd.data).toEqual(precond[data])
expect(cmd.command).toEqual("precondition")
expect("start" in cmd).toEqual(false);
expect("end" in cmd).toEqual(false);
}
})
it("precondition with wrong values", () => {
const cmd = {command:"precondition"}
for (const rVal of randomValues) {
if (rVal !== undefined) {
cmd.data = rVal
}
const res = sanitize(cmd)
expect(res.data).toEqual("battery")
expect(res.command).toEqual("precondition")
expect("start" in res).toEqual(false);
expect("end" in res).toEqual(false);
}
})
it("high-mid-low-off with wrong values", () => {
for (const command of hmolCommands) {
const cmd = {command}
for (const rVal of randomValues) {
if (rVal !== undefined) {
cmd.data = rVal
}
const res = sanitize(cmd)
expect(res.data).toEqual("off")
expect(res.command).toEqual(command)
expect("start" in res).toEqual(false);
expect("end" in res).toEqual(false);
}
}
})
it("open trunk", () => {
const cmd = {command: "trunk_open"}
for (let i = 1; i <= 5; i++) {
cmd.data = i
const res = sanitize(cmd)
expect(res.data).toEqual(i.toString())
expect(res.command).toEqual("trunk_open")
expect("start" in res).toEqual(false);
expect("end" in res).toEqual(false);
}
})
it("open trunk with wrong values", () => {
const cmd = {command: "trunk_open"}
for (const rVal of randomValues) {
if (rVal !== undefined) {
cmd.data = rVal
}
const res = sanitize(cmd)
expect(res.data).toEqual("1")
expect(res.command).toEqual("trunk_open")
expect("start" in res).toEqual(false);
expect("end" in res).toEqual(false);
}
})
it("cabin temp with period with proper values", () => {
for (let i = 0; i <= 27; i++) {
const res = sanitize({
command: "temp_cabin",
data: i,
start: new Date(),
end: new Date(),
})
expect(res.command).toEqual("temp_cabin")
expect(res.data).toEqual(onOffTemp[i])
expect(typeof res.start).toEqual("object")
expect(typeof res.end).toEqual("object")
}
})
})

View File

@@ -94,11 +94,10 @@ const vehiclesAPI = {
data: [2021, 2022],
};
},
sendCommand: async (vin, command, parameters) => {
sendCommand: async (vin, command) => {
return {
vin,
command,
parameters,
};
},
updateVehicle: async (vin, vehicle) => {

View File

@@ -1,81 +1,26 @@
const Locks = [
const Commands = [
{value: "doors_lock", label: "Lock doors"},
{value: "doors_unlock", label: "Unlock doors"},
{value: "vent_windows", label: "Vent windows"},
{value: "close_windows", label: "Close windows"},
{value: "california_mode", label: "California mode"},
{value: "trunk_open", label: "Open trunk"},
{value: "trunk_close", label: "Close trunk "},
{value: "flash_headlights", label: "Flash headlights"},
{value: "alert", label: "Alert"},
{value: "temp_cabin", label: "Set cabin temperature"},
{
value: "right_front",
label: "Front right door",
},{
value: "left_front",
label: "Front left door",
},{
value: "right_rear",
label: "Rear right door",
},{
value: "left_rear",
label: "Rear left door",
},{
value: "trunk",
label: "Trunk",
value: "temp_cabin",
label: "Set cabin temperature for period",
params: {
data: ""
},
},
];
const Windows = [
{
value: "right_front",
label: "Front right window",
},{
value: "left_front",
label: "Front left window",
},{
value: "right_rear",
label: "Rear right window",
},{
value: "left_rear",
label: "Rear left window",
},
];
const Commands = [{
value: "lock",
label: "Lock door",
parameters: Locks,
},
{
value: "unlock",
label: "Unlock door",
parameters: Locks,
},{
value: "open",
label: "Open window",
parameters: Windows,
},
{
value: "close",
label: "Close window",
parameters: Windows,
},
{
value: "ecu",
label: "ECU Versions",
},
{
value: "log",
label: "Log level",
parameters: [
{
value: "info",
label: "Info",
},
{
value: "debug",
label: "Debug",
},
{
value: "trace",
label: "Trace",
},
],
},{
value: "headlights",
label: "Flash headlights",
}];
{value: "defrost", label: "Defrost"},
{value: "driver_seat_preheat", label: "Driver seat preheat"},
{value: "passenger_seat_preheat", label: "Preheat passenger seat"},
{value: "steering_wheel_preheat", label: "Preheat Steering wheel"},
{value: "precondition", label: "Precondition"},
{value: "charging", label: "Charging"}]
export default Commands;

View File

@@ -125,7 +125,7 @@ const vehiclesAPI = {
.then(fetchRespHandler)
.catch(errorHandler),
sendCommand: async (vins, command, parameters, token) =>
sendCommand: async (vins, command, token) =>
fetch(`${API_ENDPOINT}/vehiclecommand`, {
method: "POST",
headers: Object.assign(
@@ -134,8 +134,7 @@ const vehiclesAPI = {
),
body: JSON.stringify({
vins,
command,
parameters,
...command,
}),
})
.then(fetchRespHandler)