diff --git a/package.json b/package.json index 19e98ed..11daec5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 3bc6f9c..e2a4fa3 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -6873,6 +6873,24 @@ exports[`App Route /vehicle-status authenticated 1`] = ` class="MuiTouchRipple-root" /> + + diff --git a/src/components/Cars/Status/RemoteCommandsTab.jsx b/src/components/Cars/Status/RemoteCommandsTab.jsx new file mode 100644 index 0000000..819e8a0 --- /dev/null +++ b/src/components/Cars/Status/RemoteCommandsTab.jsx @@ -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 ( +
+ Vehicle Commands + + + +
+ ) +} + +RemoteCommandsTab.propTypes = { + vin: PropTypes.string, +} + +export default RemoteCommandsTab \ No newline at end of file diff --git a/src/components/Cars/Status/RemoteCommandsTab.test.jsx b/src/components/Cars/Status/RemoteCommandsTab.test.jsx new file mode 100644 index 0000000..c1d7882 --- /dev/null +++ b/src/components/Cars/Status/RemoteCommandsTab.test.jsx @@ -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( + + + + + + ); + await waitFor(() => { + /* render */ + }); + return container; +}; + +describe("RemoteCommandsTab", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + setToken(TEST_AUTH_OBJECT); + const container = await renderRemoteCommandsTab(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/Cars/Status/__snapshots__/RemoteCommandsTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/RemoteCommandsTab.test.jsx.snap new file mode 100644 index 0000000..b6bfec5 --- /dev/null +++ b/src/components/Cars/Status/__snapshots__/RemoteCommandsTab.test.jsx.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RemoteCommandsTab Render 1`] = ` +
+
+
+
+
+ Vehicle Commands +
+
+
+
+ +
+ + + +
+
+ +
+
+
+
+
+
+`; diff --git a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap index a42716e..d6855a8 100644 --- a/src/components/Cars/Status/__snapshots__/index.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/index.test.jsx.snap @@ -119,6 +119,24 @@ exports[`CarStatus Render 1`] = ` class="MuiTouchRipple-root" /> + + diff --git a/src/components/Cars/Status/index.jsx b/src/components/Cars/Status/index.jsx index 97f6122..0943eb7 100644 --- a/src/components/Cars/Status/index.jsx +++ b/src/components/Cars/Status/index.jsx @@ -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 = () => { + @@ -86,6 +88,10 @@ const CarStatus = () => { + + + + ); }; diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index d6aac72..82d9c2f 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -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; diff --git a/src/components/Contexts/VehicleContext.test.jsx b/src/components/Contexts/VehicleContext.test.jsx index c371cfe..b97cf25 100644 --- a/src/components/Contexts/VehicleContext.test.jsx +++ b/src/components/Contexts/VehicleContext.test.jsx @@ -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 ( + <> +
{message}
+
{busy.toString()}
+ ); }; diff --git a/src/components/Controls/SendCommand/sanitize.js b/src/components/Controls/SendCommand/sanitize.js new file mode 100644 index 0000000..45560e5 --- /dev/null +++ b/src/components/Controls/SendCommand/sanitize.js @@ -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 +} diff --git a/src/components/Controls/SendCommand/sanitize.test.js b/src/components/Controls/SendCommand/sanitize.test.js new file mode 100644 index 0000000..4e748f6 --- /dev/null +++ b/src/components/Controls/SendCommand/sanitize.test.js @@ -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") + } + }) +}) \ No newline at end of file diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index ea3d879..b66976c 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -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) => { diff --git a/src/services/commands.js b/src/services/commands.js index ac1c305..53806da 100644 --- a/src/services/commands.js +++ b/src/services/commands.js @@ -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; diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index d57c1d0..d99c1d2 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -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)