CEC-2291 Remote Commands (#194)
This commit is contained in:
@@ -4,19 +4,24 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@datadog/browser-logs": "^3.11.0",
|
"@datadog/browser-logs": "^3.11.0",
|
||||||
|
"@date-io/date-fns": "1.x",
|
||||||
|
"@date-io/moment": "1.x",
|
||||||
"@emotion/react": "^11.9.0",
|
"@emotion/react": "^11.9.0",
|
||||||
"@emotion/styled": "^11.8.1",
|
"@emotion/styled": "^11.8.1",
|
||||||
"@material-ui/core": "^4.12.4",
|
"@material-ui/core": "^4.12.4",
|
||||||
"@material-ui/icons": "^4.11.3",
|
"@material-ui/icons": "^4.11.3",
|
||||||
|
"@material-ui/pickers": "^3.3.10",
|
||||||
"@testing-library/jest-dom": "^5.16.4",
|
"@testing-library/jest-dom": "^5.16.4",
|
||||||
"@testing-library/react": "^12.1.4",
|
"@testing-library/react": "^12.1.4",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
|
"date-fns": "^2.29.2",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"leaflet": "^1.8.0",
|
"leaflet": "^1.8.0",
|
||||||
"material-ui-dropzone": "^3.5.0",
|
"material-ui-dropzone": "^3.5.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-leaflet": "^3.2.5",
|
"react-leaflet": "^3.2.5",
|
||||||
|
|||||||
@@ -6873,6 +6873,24 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
/>
|
/>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
||||||
@@ -7036,6 +7054,13 @@ exports[`App Route /vehicle-status authenticated 1`] = `
|
|||||||
id="tabpanel-4"
|
id="tabpanel-4"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="tab-5"
|
||||||
|
class="makeStyles-fullWidth-0"
|
||||||
|
hidden=""
|
||||||
|
id="tabpanel-5"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
26
src/components/Cars/Status/RemoteCommandsTab.jsx
Normal file
26
src/components/Cars/Status/RemoteCommandsTab.jsx
Normal 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
|
||||||
40
src/components/Cars/Status/RemoteCommandsTab.test.jsx
Normal file
40
src/components/Cars/Status/RemoteCommandsTab.test.jsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
@@ -119,6 +119,24 @@ exports[`CarStatus Render 1`] = `
|
|||||||
class="MuiTouchRipple-root"
|
class="MuiTouchRipple-root"
|
||||||
/>
|
/>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
class="PrivateTabIndicator-root-0 PrivateTabIndicator-colorSecondary-0 MuiTabs-indicator"
|
||||||
@@ -228,6 +246,13 @@ exports[`CarStatus Render 1`] = `
|
|||||||
id="tabpanel-4"
|
id="tabpanel-4"
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
aria-labelledby="tab-5"
|
||||||
|
class="makeStyles-fullWidth-0"
|
||||||
|
hidden=""
|
||||||
|
id="tabpanel-5"
|
||||||
|
role="tabpanel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TabPanel from "../../Controls/TabPanel";
|
|||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
import CANSignalsTab from "./CANSignalsTab";
|
import CANSignalsTab from "./CANSignalsTab";
|
||||||
|
import RemoteCommandsTab from "./RemoteCommandsTab";
|
||||||
|
|
||||||
const tabHashes = ["details", "updates", "filters"];
|
const tabHashes = ["details", "updates", "filters"];
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ const CarStatus = () => {
|
|||||||
<Tab label="CAN Filters" {...tabProps(2)} />
|
<Tab label="CAN Filters" {...tabProps(2)} />
|
||||||
<Tab label="Digital Twin" {...tabProps(3)} />
|
<Tab label="Digital Twin" {...tabProps(3)} />
|
||||||
<Tab label="CAN Signals" {...tabProps(4)} />
|
<Tab label="CAN Signals" {...tabProps(4)} />
|
||||||
|
<Tab label="Remote Commands" {...tabProps(5)} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -86,6 +88,10 @@ const CarStatus = () => {
|
|||||||
<TabPanel value={tabIndex} index={4} className={classes.fullWidth}>
|
<TabPanel value={tabIndex} index={4} className={classes.fullWidth}>
|
||||||
<CANSignalsTab vin={vin} />
|
<CANSignalsTab vin={vin} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabIndex} index={5} className={classes.fullWidth}>
|
||||||
|
<RemoteCommandsTab vin={vin} />
|
||||||
|
</TabPanel>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -164,10 +164,10 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendCommand = async (vins, command, parameters, token) => {
|
const sendCommand = async (vins, command, token) => {
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
const result = await api.sendCommand(vins, command, parameters, token);
|
const result = await api.sendCommand(vins, command, token);
|
||||||
if (result.error)
|
if (result.error)
|
||||||
throw new Error(`Send command error. ${result.message}`);
|
throw new Error(`Send command error. ${result.message}`);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -317,6 +317,55 @@ describe("VehicleContext", () => {
|
|||||||
checkBaseResults("", "false");
|
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 = [
|
const expectedFilters = [
|
||||||
|
|||||||
251
src/components/Controls/SendCommand/Commands.jsx
Normal file
251
src/components/Controls/SendCommand/Commands.jsx
Normal 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;
|
||||||
73
src/components/Controls/SendCommand/Parameters.jsx
Normal file
73
src/components/Controls/SendCommand/Parameters.jsx
Normal 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
|
||||||
|
};
|
||||||
140
src/components/Controls/SendCommand/Parameters.test.jsx
Normal file
140
src/components/Controls/SendCommand/Parameters.test.jsx
Normal 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();
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 />`;
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { FormControl, IconButton, InputLabel, Select } from "@material-ui/core";
|
import {Button, FormControl, InputLabel, Select} from "@material-ui/core";
|
||||||
import SendIcon from "@material-ui/icons/Send";
|
|
||||||
|
|
||||||
import { useVehicleContext } from "../../Contexts/VehicleContext";
|
import {useVehicleContext} from "../../Contexts/VehicleContext";
|
||||||
import commands from "../../../services/commands";
|
import commands from "./Commands";
|
||||||
import useStyles from "../../useStyles";
|
import useStyles from "../../useStyles";
|
||||||
import { useUserContext } from "../../Contexts/UserContext";
|
import {useUserContext} from "../../Contexts/UserContext";
|
||||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
import {useStatusContext} from "../../Contexts/StatusContext";
|
||||||
import { logger } from "../../../services/monitoring";
|
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 classes = useStyles();
|
||||||
const { sendCommand, busy } = useVehicleContext();
|
const {busy, sendCommand} = useVehicleContext();
|
||||||
const {
|
const {
|
||||||
token: {
|
token: {
|
||||||
idToken: { jwtToken: token },
|
idToken: {jwtToken: token},
|
||||||
},
|
},
|
||||||
} = useUserContext();
|
} = useUserContext();
|
||||||
const NoParameters = {
|
const {setMessage} = useStatusContext();
|
||||||
value: "",
|
|
||||||
label: "None",
|
|
||||||
};
|
|
||||||
const { setMessage } = useStatusContext();
|
|
||||||
const [command, setCommand] = useState("");
|
const [command, setCommand] = useState("");
|
||||||
const [parameters, setParameters] = useState([NoParameters]);
|
const [parameters, setParameters] = useState(null);
|
||||||
const [parameter, setParameter] = useState("");
|
const [data, setData] = useState(null);
|
||||||
|
const [startTime, setStartTime] = useState(null)
|
||||||
|
const [endTime, setEndTime] = useState(null)
|
||||||
|
|
||||||
const changeCommandHandler = (e) => {
|
const changeCommandHandler = (e) => {
|
||||||
selectCommand(e.target.value);
|
selectCommand(e.target.value);
|
||||||
};
|
};
|
||||||
@@ -34,16 +35,27 @@ const SendCommand = ({ vins }) => {
|
|||||||
const params = getParameters(cmd);
|
const params = getParameters(cmd);
|
||||||
setCommand(cmd);
|
setCommand(cmd);
|
||||||
setParameters(params);
|
setParameters(params);
|
||||||
setParameter(params[0].value);
|
setData(null);
|
||||||
|
setStartTime(null);
|
||||||
|
setEndTime(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeParametersHandler = (e) => {
|
const handleDataChange = (_, value) => {
|
||||||
setParameter(e.target.value);
|
setData(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickHandler = async (e) => {
|
const handleStartChange = (date, _) => {
|
||||||
|
setStartTime(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEndChange = (date, _) => {
|
||||||
|
setEndTime(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickHandler = async (_) => {
|
||||||
try {
|
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) {
|
if (vins.length === 1) {
|
||||||
setMessage(`Sent command to ${vins[0]}`);
|
setMessage(`Sent command to ${vins[0]}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -59,13 +71,13 @@ const SendCommand = ({ vins }) => {
|
|||||||
for (let i = 0, len = commands.length; i < len; i += 1) {
|
for (let i = 0, len = commands.length; i < len; i += 1) {
|
||||||
const item = commands[i];
|
const item = commands[i];
|
||||||
if (item.value === cmd) {
|
if (item.value === cmd) {
|
||||||
if (!item.parameters) {
|
if (!item.params) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return item.parameters;
|
return item.params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [NoParameters];
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -75,9 +87,9 @@ const SendCommand = ({ vins }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.form} style={{ marginTop: 20 }}>
|
<div className={clsx(classes.paper)} style={{marginTop: 20}}>
|
||||||
<FormControl
|
<FormControl
|
||||||
className={classes.formControlInline}
|
className={classes.formControl}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -101,45 +113,27 @@ const SendCommand = ({ vins }) => {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl
|
<Parameters
|
||||||
className={classes.formControlInline}
|
params={parameters}
|
||||||
variant="outlined"
|
data={data}
|
||||||
size="small"
|
handleDataChange={handleDataChange}
|
||||||
>
|
startDate={startTime}
|
||||||
<InputLabel
|
handleStartChange={handleStartChange}
|
||||||
htmlFor="send-parameter"
|
endDate={endTime}
|
||||||
className={classes.whiteBackground}
|
handleEndChange={handleEndChange}
|
||||||
>
|
/>
|
||||||
Parameter
|
<Button
|
||||||
</InputLabel>
|
type="submit"
|
||||||
<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"
|
|
||||||
aria-label="send command"
|
aria-label="send command"
|
||||||
component="span"
|
|
||||||
onClick={clickHandler}
|
|
||||||
size="small"
|
|
||||||
disabled={busy || vins.length === 0}
|
disabled={busy || vins.length === 0}
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
className={classes.submit}
|
||||||
|
onClick={clickHandler}
|
||||||
>
|
>
|
||||||
<SendIcon fontSize="large" />
|
{busy ? "Sending..." : "Send"}
|
||||||
</IconButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
115
src/components/Controls/SendCommand/sanitize.js
Normal file
115
src/components/Controls/SendCommand/sanitize.js
Normal 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
|
||||||
|
}
|
||||||
150
src/components/Controls/SendCommand/sanitize.test.js
Normal file
150
src/components/Controls/SendCommand/sanitize.test.js
Normal 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -94,11 +94,10 @@ const vehiclesAPI = {
|
|||||||
data: [2021, 2022],
|
data: [2021, 2022],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
sendCommand: async (vin, command, parameters) => {
|
sendCommand: async (vin, command) => {
|
||||||
return {
|
return {
|
||||||
vin,
|
vin,
|
||||||
command,
|
command,
|
||||||
parameters,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateVehicle: async (vin, vehicle) => {
|
updateVehicle: async (vin, vehicle) => {
|
||||||
|
|||||||
@@ -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",
|
value: "temp_cabin",
|
||||||
label: "Front right door",
|
label: "Set cabin temperature for period",
|
||||||
},{
|
params: {
|
||||||
value: "left_front",
|
data: ""
|
||||||
label: "Front left door",
|
|
||||||
},{
|
|
||||||
value: "right_rear",
|
|
||||||
label: "Rear right door",
|
|
||||||
},{
|
|
||||||
value: "left_rear",
|
|
||||||
label: "Rear left door",
|
|
||||||
},{
|
|
||||||
value: "trunk",
|
|
||||||
label: "Trunk",
|
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
];
|
{value: "defrost", label: "Defrost"},
|
||||||
|
{value: "driver_seat_preheat", label: "Driver seat preheat"},
|
||||||
const Commands = [{
|
{value: "passenger_seat_preheat", label: "Preheat passenger seat"},
|
||||||
value: "lock",
|
{value: "steering_wheel_preheat", label: "Preheat Steering wheel"},
|
||||||
label: "Lock door",
|
{value: "precondition", label: "Precondition"},
|
||||||
parameters: Locks,
|
{value: "charging", label: "Charging"}]
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
}];
|
|
||||||
|
|
||||||
export default Commands;
|
export default Commands;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ const vehiclesAPI = {
|
|||||||
.then(fetchRespHandler)
|
.then(fetchRespHandler)
|
||||||
.catch(errorHandler),
|
.catch(errorHandler),
|
||||||
|
|
||||||
sendCommand: async (vins, command, parameters, token) =>
|
sendCommand: async (vins, command, token) =>
|
||||||
fetch(`${API_ENDPOINT}/vehiclecommand`, {
|
fetch(`${API_ENDPOINT}/vehiclecommand`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: Object.assign(
|
headers: Object.assign(
|
||||||
@@ -134,8 +134,7 @@ const vehiclesAPI = {
|
|||||||
),
|
),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
vins,
|
vins,
|
||||||
command,
|
...command,
|
||||||
parameters,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(fetchRespHandler)
|
.then(fetchRespHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user