import bodyParser from "body-parser"; import type { Connect, ViteDevServer } from "vite"; import type { ServerResponse } from "http"; import { OcppClient } from "./ocppClient"; import { ocppServer } from "./ocppServer"; const ocppClients = new Map(); let state = { site: { grid: { power: 0, energy: 0 }, pv: { power: 0, energy: 0 }, battery: { power: 0, soc: 0 }, }, loadpoints: [{ power: 0, energy: 0, enabled: false, status: "A" }], vehicles: [{ soc: 0, range: 0 }], hems: { relay: false, w3: false, s1: false, s2: false, w4: false }, ocpp: { clients: [] as { stationId: string; serverUrl: string; connected: boolean }[], }, }; const loggingMiddleware = ( req: Connect.IncomingMessage, _: ServerResponse, next: Connect.NextFunction ) => { console.log(`[simulator] ${req.method} ${req.url}`); next(); }; const stateApiMiddleware = ( req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction ) => { if (req.method === "POST" && req.originalUrl === "/api/state") { console.log("[simulator] POST /api/state"); // @ts-expect-error Property 'body' does not exist on type 'IncomingMessage' state = req.body; res.end(); } else if (req.method === "POST" && req.originalUrl === "/api/shutdown") { console.log("[simulator] POST /api/shutdown"); for (const client of ocppClients.values()) { client.disconnect(); } ocppClients.clear(); res.end(); process.exit(); } else if (req.originalUrl === "/api/state") { updateOcppState(); res.end(JSON.stringify(state)); } else { next(); } }; const openemsMiddleware = ( req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction ) => { const endpoints = { "/rest/channel/_sum/GridActivePower": { value: state.site.grid.power }, "/rest/channel/_sum/GridBuyActiveEnergy": { value: state.site.grid.energy }, "/rest/channel/_sum/ProductionActivePower": { value: state.site.pv.power }, "/rest/channel/_sum/ProductionActiveEnergy": { value: state.site.pv.energy }, "/rest/channel/_sum/EssDischargePower": { value: state.site.battery.power }, "/rest/channel/_sum/EssSoc": { value: state.site.battery.soc }, }; const endpoint = endpoints[req.originalUrl as keyof typeof endpoints]; if (req.method === "GET" && endpoint) { console.log("[simulator] GET", req.originalUrl); res.end(JSON.stringify(endpoint)); } else { next(); } }; const teslaloggerMiddleware = ( req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction ) => { if (req.method === "GET" && req.originalUrl && req.originalUrl.startsWith("/currentjson/")) { const idPart = req.originalUrl.split("/")[2]; const id = idPart ? parseInt(idPart) : 0; const vehicle = state.vehicles[id - 1]; if (!vehicle) { res.statusCode = 404; res.end(JSON.stringify({ error: "Vehicle not found" })); return; } const data = { battery_level: vehicle.soc, battery_range_km: vehicle.range, plugged_in: true, charging: false, odometer: 10000, is_preconditioning: false, charge_current_request: 10, }; res.end(JSON.stringify(data)); } else { next(); } }; const shellyMiddleware = ( req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction ) => { // simulate a shelly gen2 switch device api. implement power and energy if (req.originalUrl === "/shelly") { res.end(JSON.stringify({ gen: 2 })); } else if (req.originalUrl === "/rpc/Shelly.ListMethods") { res.end(JSON.stringify({ methods: ["Switch.GetStatus"] })); } else if (req.originalUrl === "/rpc/Switch.GetStatus") { res.end( JSON.stringify({ apower: state.site.pv.power, aenergy: { total: state.site.pv.energy }, }) ); } else { next(); } }; const updateOcppState = () => { state.ocpp.clients = Array.from(ocppClients.entries()).map(([stationId, client]) => ({ stationId, serverUrl: client.getServerUrl(), connected: client.isConnected(), })); }; const demoAuthMiddleware = ( _req: Connect.IncomingMessage, _res: ServerResponse, next: Connect.NextFunction ) => { // Mock login requests are now handled by the Vue app // This middleware is kept for potential future extensions next(); }; const ocppMiddleware = ( req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction ) => { if (req.method === "POST" && req.originalUrl === "/api/ocpp/connect") { console.log("[simulator] POST /api/ocpp/connect"); // @ts-expect-error Property 'body' does not exist on type 'IncomingMessage' const { stationId, serverUrl } = req.body; if (!stationId || !serverUrl) { res.statusCode = 400; res.end(JSON.stringify({ error: "stationId and serverUrl required" })); return; } if (ocppClients.has(stationId)) { res.statusCode = 400; res.end(JSON.stringify({ error: `Client ${stationId} already exists` })); return; } const client = new OcppClient(stationId, serverUrl); ocppClients.set(stationId, client); // fire-and-forget: the client retries until connected and boots itself on open client.connect(); updateOcppState(); console.log(`[simulator] OCPP client ${stationId} connecting to ${serverUrl}`); res.end(JSON.stringify({ status: "connecting", stationId })); } else if (req.method === "POST" && req.originalUrl === "/api/ocpp/server") { console.log("[simulator] POST /api/ocpp/server"); // @ts-expect-error Property 'body' does not exist on type 'IncomingMessage' const { enabled, username, password } = req.body; ocppServer.configure({ enabled: !!enabled, username, password }); res.end(JSON.stringify(ocppServer.status())); } else if (req.method === "GET" && req.originalUrl === "/api/ocpp/server") { res.end(JSON.stringify(ocppServer.status())); } else if (req.method === "POST" && req.originalUrl === "/api/ocpp/disconnect") { console.log("[simulator] POST /api/ocpp/disconnect"); // @ts-expect-error Property 'body' does not exist on type 'IncomingMessage' const { stationId } = req.body; if (!stationId) { res.statusCode = 400; res.end(JSON.stringify({ error: "stationId required" })); return; } const client = ocppClients.get(stationId); if (client) { client.disconnect(); ocppClients.delete(stationId); updateOcppState(); res.end(JSON.stringify({ status: "disconnected", stationId })); } else { res.statusCode = 404; res.end(JSON.stringify({ error: `Client ${stationId} not found` })); } } else { next(); } }; export default () => ({ name: "api", enforce: "pre", configureServer(server: ViteDevServer) { console.log("[simulator] configured"); if (server.httpServer) { ocppServer.attach(server.httpServer); } return () => { server.middlewares.use(loggingMiddleware); server.middlewares.use(bodyParser.json()); server.middlewares.use(stateApiMiddleware); server.middlewares.use(openemsMiddleware); server.middlewares.use(teslaloggerMiddleware); server.middlewares.use(shellyMiddleware); server.middlewares.use(demoAuthMiddleware); server.middlewares.use(ocppMiddleware); }; }, });