Skip to content

Commit

Permalink
Updated vehicle display
Browse files Browse the repository at this point in the history
  • Loading branch information
virtualzone committed Apr 13, 2024
1 parent 038db55 commit 8653911
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 68 deletions.
9 changes: 9 additions & 0 deletions frontend-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"bootstrap": "^5.3.2",
"lucide-react": "^0.367.0",
"next": "14.1.0",
"react": "^18",
"react-bootstrap": "^2.10.0",
Expand Down
8 changes: 3 additions & 5 deletions frontend-node/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import Loading from "./loading";
import { Alert, Button, Container, ListGroup, Table } from "react-bootstrap";
import { useRouter } from "next/navigation";
import VehicleStatus from "./vehicle-status";

export default function PageAuthorized() {
const [vehicles, setVehicles] = useState([] as any[])
Expand Down Expand Up @@ -69,11 +70,8 @@ export default function PageAuthorized() {
<ListGroup className="mb-5">
{(vehicles as any[]).map(e => {
return (
<ListGroup.Item action={true} onClick={() => selectVehicle(e.vin)} key={e.vin}>
<strong>{e.display_name}</strong>
<br />
{e.vin}
<br />
<ListGroup.Item action={true} onClick={() => selectVehicle(e.vehicle.vin)} key={e.vehicle.vin}>
<VehicleStatus vehicle={e.vehicle} state={e.state} />
</ListGroup.Item>
)
})}
Expand Down
38 changes: 38 additions & 0 deletions frontend-node/src/app/vehicle-status.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Col, Row } from "react-bootstrap";
import { BatteryCharging as IconCharging, Sun as IconSun, Battery as IconNotCharging, UtilityPole as IconGrid, CarFront as IconCarFront, Unplug as IconUnplugged, PlugZap as IconPluggedIn } from 'lucide-react';

// @ts-ignore: Object is possibly 'null'.
export default function VehicleStatus({ vehicle, state }) {
function getChargeState(state: any) {
if (!state.pluggedIn) return <><IconUnplugged /> Disconnected</>;
if (state.chargingState === 0) return <><IconPluggedIn /> Idle</>;
if (state.chargingState === 1) return <><IconSun /> Solar-charging at {state.amps} A</>;
if (state.chargingState === 2) return <><IconGrid /> Grid-charging at {state.amps} A</>;
return <><IconNotCharging /></>;
}

function getSoCIcon(state: any) {
if (state.chargingState === 0) return <IconNotCharging />;
if (state.chargingState > 0) return <IconCharging />;
return <IconNotCharging />;
}

if (state === null) state = {};

return (
<div>
<Row>
<Col style={{ 'flex': '0 0 100px' }}>
<IconCarFront size={64} />
</Col>
<Col style={{ 'flex': '1' }}>
<h5>{vehicle.display_name}</h5>
<Row>
<Col sm={4}>{getSoCIcon(state)} {state.soc !== undefined ? state.soc + ' %' : 'SoC unknown'}</Col>
<Col sm={8}>{getChargeState(state)}</Col>
</Row>
</Col>
</Row>
</div>
)
}
98 changes: 38 additions & 60 deletions frontend-node/src/app/vehicle/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Accordion, Button, Container, Form, InputGroup, Table } from "react-boo
import { useRouter } from "next/navigation";
import { Loader as IconLoad } from 'react-feather';
import Link from "next/link";
import VehicleStatus from "../vehicle-status";

export default function PageVehicle() {
let vehicleVIN = ""
Expand Down Expand Up @@ -40,47 +41,50 @@ export default function PageVehicle() {
}
vehicleVIN = vin;
const fetchData = async () => {
await loadVehicle();
await loadVehicle(vehicleVIN);
setLoading(false);
}
fetchData();
}, [router]);

/*
const loadVehicleState = async (vin: string) => {
const json = await getAPI("/api/1/tesla/state/" + vin);
if (json) {
setVehicleState(json);
}
}
*/

const loadLatestChargingEvents = async (vin: string) => {
const json = await getAPI("/api/1/tesla/events/" + vin);
setChargingEvents(json);
}

const loadVehicle = async () => {
const json = await getAPI("/api/1/tesla/my_vehicles");
(json as any[]).forEach(e => {
if (e.vin === vehicleVIN) {
setChargingEnabled(e.enabled);
setTargetSoC(e.target_soc);
setMaxAmps(e.max_amps);
setNumPhases(e.num_phases);
setChargeOnSurplus(e.surplus_charging);
setMinSurplus(e.min_surplus);
setMinChargetime(e.min_chargetime);
setChargeOnTibber(e.lowcost_charging);
setMaxPrice(e.max_price);
setGridProvider(e.gridProvider);
setGridStrategy(e.gridStrategy);
setDepartDays([...e.departDays].map(i => Number(i)));
setDepartTime(e.departTime);
setTibberToken(e.tibber_token);
loadVehicleState(e.vin);
loadLatestChargingEvents(e.vin);
setVehicle(e);
}
});
const setVehicleDetails = (e: any) => {
setChargingEnabled(e.enabled);
setTargetSoC(e.target_soc);
setMaxAmps(e.max_amps);
setNumPhases(e.num_phases);
setChargeOnSurplus(e.surplus_charging);
setMinSurplus(e.min_surplus);
setMinChargetime(e.min_chargetime);
setChargeOnTibber(e.lowcost_charging);
setMaxPrice(e.max_price);
setGridProvider(e.gridProvider);
setGridStrategy(e.gridStrategy);
setDepartDays([...e.departDays].map(i => Number(i)));
setDepartTime(e.departTime);
setTibberToken(e.tibber_token);
}

const loadVehicle = async (vin: string) => {
const e = await getAPI("/api/1/tesla/my_vehicle/" + vin);
setVehicleDetails(e.vehicle);
setVehicleState(e.state);
//loadVehicleState(e.vin);
loadLatestChargingEvents(e.vehicle.vin);
setVehicle(e.vehicle);
}

function saveVehicle() {
Expand All @@ -103,7 +107,7 @@ export default function PageVehicle() {
"tibber_token": tibberToken
};
await putAPI("/api/1/tesla/vehicle_update/" + vehicle.vin, payload);
await loadVehicle();
await loadVehicle(vehicle.vin);
setSavingVehicle(false);
}
fetchData();
Expand Down Expand Up @@ -371,34 +375,6 @@ export default function PageVehicle() {
</Accordion.Body>
</Accordion.Item>
);
let accordionState = <></>;
accordionState = (
<Accordion.Item eventKey="2">
<Accordion.Header>Vehicle State</Accordion.Header>
<Accordion.Body>
<Table>
<tbody>
<tr>
<td>Plugged In</td>
<td>{vehicleState.pluggedIn ? 'Yes' : 'No'}</td>
</tr>
<tr>
<td>Charging State</td>
<td>{getChargeStateText(vehicleState.chargingState)}</td>
</tr>
<tr>
<td>SoC</td>
<td>{vehicleState.soc ? vehicleState.soc + ' %' : 'Unknown'}</td>
</tr>
<tr>
<td>Amps</td>
<td>{vehicleState.amps !== undefined ? vehicleState.amps + ' A' : 'Unknown'}</td>
</tr>
</tbody>
</Table>
</Accordion.Body>
</Accordion.Item>
);
let accordionManualControl = (
<Accordion.Item eventKey="5">
<Accordion.Header>Test Drive</Accordion.Header>
Expand All @@ -419,19 +395,21 @@ export default function PageVehicle() {
);
return (
<Container fluid="sm" className="pt-5 container-max-width min-height">
<h2 className="pb-3">{vehicle.display_name}</h2>
<p>VIN: {vehicle.vin}</p>
<p>Before chargebot.io can control your vehicle's charging process, you need to set up the virtual key:</p>
<p><a href="https://tesla.com/_ak/chargebot.io" target="_blank">Set Up Virtual Key</a></p>
<br />
<Accordion defaultActiveKey={'2'} flush={true}>
{accordionState}
<VehicleStatus state={vehicleState} vehicle={vehicle} />
<Accordion defaultActiveKey={'0'} flush={true} style={{ 'marginTop': '50px' }}>
<Accordion.Item eventKey="0">
<Accordion.Header>Charging Preferences</Accordion.Header>
<Accordion.Body>
{chargePrefs}
</Accordion.Body>
</Accordion.Item>
<Accordion.Item eventKey="1">
<Accordion.Header>Setup</Accordion.Header>
<Accordion.Body>
<p>Before chargebot.io can control your vehicle's charging process, you need to set up the virtual key:</p>
<p><a href="https://tesla.com/_ak/chargebot.io" target="_blank">Set Up Virtual Key</a></p>
</Accordion.Body>
</Accordion.Item>
{accordionChargingEvents}
{accordionManualControl}
<Accordion.Item eventKey="99">
Expand Down
3 changes: 3 additions & 0 deletions node/charge-controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func NewChargeController() *ChargeController {
}

func (c *ChargeController) Init() {
if GetConfig().DemoMode {
return
}
c.Ticker = time.NewTicker(time.Minute * 1)
go func() {
for {
Expand Down
4 changes: 4 additions & 0 deletions node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type Config struct {
TelegramToken string
TelegramChatID string
PlugStateAutodetection bool
InitDBOnly bool
DemoMode bool
}

var _configInstance *Config
Expand Down Expand Up @@ -56,6 +58,8 @@ func (c *Config) ReadConfig() {
c.TelegramToken = c.getEnv("TELEGRAM_TOKEN", "")
c.TelegramChatID = c.getEnv("TELEGRAM_CHAT_ID", "")
c.PlugStateAutodetection = (c.getEnv("PLUG_AUTODETECT", "1") == "1")
c.InitDBOnly = (c.getEnv("INIT_DB_ONLY", "0") == "1")
c.DemoMode = (c.getEnv("DEMO_MODE", "0") == "1")
}

func (c *Config) Print() {
Expand Down
5 changes: 4 additions & 1 deletion node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func sanityCheck() {
}

if err := PingCommandServer(); err != nil {
log.Panicf("Could not ping command server: %s\n", err.Error())
log.Panicf("Could not ping command server: %s - check if TOKEN and PASSWORD are correct, get yours at https://chargebot.io\n", err.Error())
}

log.Println("Sanity check completed.")
Expand All @@ -59,6 +59,9 @@ func main() {
GetConfig().ReadConfig()
GetDB().Connect()
GetDB().InitDBStructure()
if GetConfig().InitDBOnly {
return
}
sanityCheck()

TeslaAPIInstance = &TeslaAPIProxy{}
Expand Down
8 changes: 7 additions & 1 deletion node/run.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
#!/bin/sh
PORT=8081 DEV_PROXY=1 CRYPT_KEY=12345678901234567890123456789012 TOKEN=f8f78ed5-204e-4e5b-a0cf-ceac819c3b2d PASSWORD=z2fxvdzMEOrqi1cB CMD_ENDPOINT="http://localhost:8080/api/1/user/{token}" TELEMETRY_ENDPOINT=ws://localhost:8080/api/1/user/{token}/ws go run `ls *.go | grep -v _test.go`
if command -v sqlite3 -version &> /dev/null
then
rm -f /tmp/chargebot_node.db
INIT_DB_ONLY=1 go run `ls *.go | grep -v _test.go`
sqlite3 /tmp/chargebot_node.db "insert into vehicles (vin, display_name, enabled, target_soc, max_amps, surplus_charging, min_surplus, min_chargetime, lowcost_charging, max_price, tibber_token) values ('5YJXCBE24KF152671', 'Model Y', 0, 0, 0, 0, 0, 0, 0, 0, '')"
fi
DEMO_MODE=1 PORT=8081 DEV_PROXY=1 CRYPT_KEY=12345678901234567890123456789012 TOKEN=f8f78ed5-204e-4e5b-a0cf-ceac819c3b2d PASSWORD=z2fxvdzMEOrqi1cB CMD_ENDPOINT="http://localhost:8080/api/1/user/{token}" TELEMETRY_ENDPOINT=ws://localhost:8080/api/1/user/{token}/ws go run `ls *.go | grep -v _test.go`
35 changes: 34 additions & 1 deletion node/tesla-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import (
. "github.com/virtualzone/chargebot/goshared"
)

type VehicleWithState struct {
Vehicle *Vehicle `json:"vehicle"`
State *VehicleState `json:"state"`
}

type TeslaRouter struct {
}

func (router *TeslaRouter) SetupRoutes(s *mux.Router) {
s.HandleFunc("/vehicles", router.listVehicles).Methods("GET")
s.HandleFunc("/my_vehicles", router.myVehicles).Methods("GET")
s.HandleFunc("/my_vehicle/{vin}", router.myVehicleByVIN).Methods("GET")
s.HandleFunc("/vehicle_add/{vin}", router.addVehicle).Methods("POST")
s.HandleFunc("/vehicle_update/{vin}", router.updateVehicle).Methods("PUT")
s.HandleFunc("/vehicle_delete/{vin}", router.deleteVehicle).Methods("DELETE")
Expand All @@ -35,8 +41,35 @@ func (router *TeslaRouter) listVehicles(w http.ResponseWriter, r *http.Request)
}

func (router *TeslaRouter) myVehicles(w http.ResponseWriter, r *http.Request) {
res := []VehicleWithState{}
list := GetDB().GetVehicles()
SendJSON(w, list)
for _, v := range list {
s := GetDB().GetVehicleState(v.VIN)
item := VehicleWithState{
Vehicle: v,
State: s,
}
res = append(res, item)
}
SendJSON(w, res)
}

func (router *TeslaRouter) myVehicleByVIN(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
vin := vars["vin"]

v := GetDB().GetVehicleByVIN(vin)
if v == nil {
SendNotFound(w)
return
}

s := GetDB().GetVehicleState(v.VIN)
item := VehicleWithState{
Vehicle: v,
State: s,
}
SendJSON(w, item)
}

func (router *TeslaRouter) addVehicle(w http.ResponseWriter, r *http.Request) {
Expand Down
2 changes: 2 additions & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Config struct {
AuthRolesPath string
AuthFieldEmail string
AuthFieldUsername string
InitDBOnly bool
}

var _configInstance *Config
Expand Down Expand Up @@ -73,6 +74,7 @@ func (c *Config) ReadConfig() {
c.AuthRolesPath = c.getEnv("AUTH_ROLES_PATH", "resource_access.portfolio-test.roles")
c.AuthFieldEmail = c.getEnv("AUTH_FIELD_EMAIL", "email")
c.AuthFieldUsername = c.getEnv("AUTH_FIELD_USERNAME", "preferred_username")
c.InitDBOnly = (c.getEnv("INIT_DB_ONLY", "0") == "1")
}

func (c *Config) Print() {
Expand Down
3 changes: 3 additions & 0 deletions server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ func main() {
GetDB().ResetDBStructure()
}
GetDB().InitDBStructure()
if GetConfig().InitDBOnly {
return
}
GetOIDCProvider().Init()

TeslaAPIInstance = &TeslaAPIImpl{}
Expand Down
7 changes: 7 additions & 0 deletions server/run.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
#!/bin/sh
if command -v sqlite3 -version &> /dev/null
then
rm -f /tmp/chargebot.db
INIT_DB_ONLY=1 go run `ls *.go | grep -v _test.go`
sqlite3 /tmp/chargebot.db "insert into users (id, tesla_user_id) values ('8da4de32-8829-4a99-b5fe-2103e25be03b', 'bb1e0a53-a914-49a2-a939-b533eb05663a')"
sqlite3 /tmp/chargebot.db "insert into api_tokens (token, user_id, passhash) values ('f8f78ed5-204e-4e5b-a0cf-ceac819c3b2d', '8da4de32-8829-4a99-b5fe-2103e25be03b', '56dca395afc313da732f78a8e2ef4059ac2260441c47f43f32642c732c19b814')"
fi
DOMAIN=localhost:8080 DEV_PROXY=1 go run `ls *.go | grep -v _test.go`

0 comments on commit 8653911

Please sign in to comment.