@xdelivered/emberflow 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EV charge scheduler — pure decision logic for a demo solar/EV charging flow.
|
|
3
|
+
* Every decision is a pure function so an Emberflow flow can drive it
|
|
4
|
+
* deterministically (constants, formulas, thresholds, priorities, edge cases).
|
|
5
|
+
*
|
|
6
|
+
* Two notes on shape:
|
|
7
|
+
* 1. `getThresholds` takes trends as an explicit parameter rather than reading
|
|
8
|
+
* a module-level rolling-reading window.
|
|
9
|
+
* 2. Time-dependent helpers take a `now` ISO string (default: current time) and
|
|
10
|
+
* resolve the wall-clock HOUR from UTC (`getUTCHours`) so scenarios pinned
|
|
11
|
+
* with a `now` (…Z) are reproducible on any machine. Sunrise/sunset come
|
|
12
|
+
* from suncalc as absolute UTC instants, so `isSolarHours` /
|
|
13
|
+
* `hoursUntilSunset` are machine-independent.
|
|
14
|
+
*/
|
|
15
|
+
export declare const LAT = 52.52;
|
|
16
|
+
export declare const LON = 13.405;
|
|
17
|
+
export declare const EV_TARGET_BATTERY = 80;
|
|
18
|
+
export declare const EV_SOLAR_TARGET = 90;
|
|
19
|
+
export declare const EV_CAPACITY_KWH = 77.4;
|
|
20
|
+
export declare const EV_CHARGE_RATE_KW = 2.3;
|
|
21
|
+
export declare const EV_LOW_BATTERY = 50;
|
|
22
|
+
export declare const EV_NIGHT_CHARGE_BELOW = 35;
|
|
23
|
+
export declare const START_SURPLUS_W = 1500;
|
|
24
|
+
export declare const STOP_SURPLUS_W = 500;
|
|
25
|
+
export declare const START_MIN_SOC = 30;
|
|
26
|
+
export declare const STOP_MIN_SOC = 20;
|
|
27
|
+
export declare const COOLDOWN_MS: number;
|
|
28
|
+
export declare const PREDICTIVE_BATTERY_SOC = 90;
|
|
29
|
+
export declare const PREDICTIVE_MIN_PPV = 2000;
|
|
30
|
+
export declare const SUNSET_HOURS_THRESHOLD = 2;
|
|
31
|
+
export declare const GOLDEN_HOUR_START_W = 800;
|
|
32
|
+
export declare const GOLDEN_HOUR_STOP_W = 200;
|
|
33
|
+
export declare const MIN_SESSION_KWH = 0.5;
|
|
34
|
+
export declare const TREND_WINDOW = 6;
|
|
35
|
+
export type EvChargeState = 'idle' | 'waiting' | 'charging' | 'cooldown' | 'night_charging';
|
|
36
|
+
export interface Reading {
|
|
37
|
+
time?: number;
|
|
38
|
+
surplus: number;
|
|
39
|
+
homeSoc: number;
|
|
40
|
+
ppv: number;
|
|
41
|
+
}
|
|
42
|
+
export interface Forecast {
|
|
43
|
+
remainingKwh: number;
|
|
44
|
+
strongHoursLeft: number;
|
|
45
|
+
}
|
|
46
|
+
export interface SunClock {
|
|
47
|
+
isSolarHours: boolean;
|
|
48
|
+
hoursUntilSunset: number;
|
|
49
|
+
sunrise: string;
|
|
50
|
+
sunset: string;
|
|
51
|
+
/** UTC wall-clock hour (0–23). */
|
|
52
|
+
hour: number;
|
|
53
|
+
/** True inside the 23:00–05:00 night-charge window. */
|
|
54
|
+
isNightWindow: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** Sun position + derived time flags for the given instant/location. */
|
|
57
|
+
export declare function sunTimes(now?: string, lat?: number, lon?: number): SunClock;
|
|
58
|
+
export interface SurplusResult {
|
|
59
|
+
surplus: number;
|
|
60
|
+
/** 'export' when grid export is positive, else 'ppv-load'. */
|
|
61
|
+
branch: 'export' | 'ppv-load';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Verbatim surplus calc (evaluateCharge):
|
|
65
|
+
* surplus = gridExport > 0 ? gridExport : max(0, ppv - loadPower)
|
|
66
|
+
*/
|
|
67
|
+
export declare function computeSurplus(ppv: number, loadPower: number, gridExport: number): SurplusResult;
|
|
68
|
+
/** How many kWh the EV needs to reach target (verbatim evKwhNeeded). */
|
|
69
|
+
export declare function evKwhNeeded(currentPct: number, target?: number): number;
|
|
70
|
+
/**
|
|
71
|
+
* Estimate deliverable kWh from remaining solar (verbatim estimateSessionKwh).
|
|
72
|
+
* With a forecast: remaining kWh capped by charge rate over the hours left.
|
|
73
|
+
* Without: assume current surplus sustains for the remaining sun hours.
|
|
74
|
+
*/
|
|
75
|
+
export declare function estimateSessionKwh(surplusW: number, sunHoursLeft: number, forecast: Forecast | null): number;
|
|
76
|
+
/** Per-cycle slope of `key` across the reading window (verbatim getTrend). */
|
|
77
|
+
export declare function getTrend(readings: Reading[], key: 'surplus' | 'homeSoc' | 'ppv'): number;
|
|
78
|
+
export interface TrendWindow {
|
|
79
|
+
surplusTrend: number;
|
|
80
|
+
socTrend: number;
|
|
81
|
+
ppvTrend: number;
|
|
82
|
+
stdDev: number;
|
|
83
|
+
cooldownMs: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Dynamic cooldown from surplus volatility (verbatim getCooldownMs):
|
|
87
|
+
* < 3 readings → default 10 min
|
|
88
|
+
* stdDev < 200 → stable → 5 min
|
|
89
|
+
* stdDev > 800 → volatile → 15 min
|
|
90
|
+
* else → default 10 min
|
|
91
|
+
*/
|
|
92
|
+
export declare function getCooldownMs(readings: Reading[]): number;
|
|
93
|
+
/** stdDev of surplus across the window (0 for <3 readings, mirroring the cooldown gate). */
|
|
94
|
+
export declare function surplusStdDev(readings: Reading[]): number;
|
|
95
|
+
/** Bundle the trend-window derivations one Trend Window node emits. */
|
|
96
|
+
export declare function computeTrendWindow(readings: Reading[]): TrendWindow;
|
|
97
|
+
export interface Trends {
|
|
98
|
+
surplusTrend: number;
|
|
99
|
+
socTrend: number;
|
|
100
|
+
}
|
|
101
|
+
export interface ThresholdResult {
|
|
102
|
+
startW: number;
|
|
103
|
+
stopW: number;
|
|
104
|
+
skipReason: string | null;
|
|
105
|
+
reasons: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Verbatim port of getThresholds. The ONLY change: `trends` (surplus + home-SOC
|
|
109
|
+
* per-cycle slopes) is passed in explicitly instead of read from module state
|
|
110
|
+
* via `getTrend(...)`. Defaults to zero slopes (no trend override), matching a
|
|
111
|
+
* cold start with fewer than two readings.
|
|
112
|
+
*/
|
|
113
|
+
export declare function getThresholds(homeSoc: number, ppv: number, sunHoursLeft: number, evBattery: number | null, forecast: Forecast | null, trends?: Trends): ThresholdResult;
|
|
114
|
+
export interface UnscheduledGuardInput {
|
|
115
|
+
charging: boolean;
|
|
116
|
+
state: EvChargeState;
|
|
117
|
+
surplus: number;
|
|
118
|
+
startW: number;
|
|
119
|
+
skipReason: string | null;
|
|
120
|
+
homeSoc: number;
|
|
121
|
+
effectiveMinSoc: number;
|
|
122
|
+
}
|
|
123
|
+
export declare function shouldStopUnscheduledCharge({ charging, state, surplus, startW, skipReason, homeSoc, effectiveMinSoc, }: UnscheduledGuardInput): boolean;
|
|
124
|
+
export interface NightChargeDecision {
|
|
125
|
+
shouldCharge: boolean;
|
|
126
|
+
factors: string[];
|
|
127
|
+
}
|
|
128
|
+
export declare function getNightChargeDecision(evBattery: number, tomorrowKwh: number | null): NightChargeDecision;
|
|
129
|
+
export type ChargeAction = 'start' | 'stop' | 'hold_waiting' | 'hold_charging';
|
|
130
|
+
export interface ChargeDecisionInput {
|
|
131
|
+
state: 'idle' | 'waiting' | 'charging';
|
|
132
|
+
surplus: number;
|
|
133
|
+
startW: number;
|
|
134
|
+
stopW: number;
|
|
135
|
+
homeSoc: number;
|
|
136
|
+
skipReason: string | null;
|
|
137
|
+
}
|
|
138
|
+
export interface ChargeDecisionResult {
|
|
139
|
+
action: ChargeAction;
|
|
140
|
+
nextState: EvChargeState;
|
|
141
|
+
reason: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Verbatim port of evaluateCharge's idle/waiting/charging branch (lines
|
|
145
|
+
* 629–705). effectiveMinSoc and effectiveStopSoc are both 0 in production
|
|
146
|
+
* (morning home SOC near 0% is normal after overnight discharge — the surplus
|
|
147
|
+
* threshold already prevents grid import), so the SOC comparisons never trip
|
|
148
|
+
* here; they are preserved verbatim for fidelity.
|
|
149
|
+
*/
|
|
150
|
+
export declare function decideCharge({ state, surplus, startW, stopW, homeSoc, skipReason, }: ChargeDecisionInput): ChargeDecisionResult;
|
|
151
|
+
export type TargetBranch = 'notPlugged' | 'atTarget' | 'belowTarget';
|
|
152
|
+
export interface TargetCheckResult {
|
|
153
|
+
branch: TargetBranch;
|
|
154
|
+
effectiveTarget: number;
|
|
155
|
+
reason: string;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Verbatim port of the plugged-in / at-target gate. Effective target is the
|
|
159
|
+
* solar target (90%) during solar hours, else the grid target (80%).
|
|
160
|
+
*/
|
|
161
|
+
export declare function checkTarget(pluggedIn: boolean, evBattery: number | null, isSolarHours: boolean): TargetCheckResult;
|
|
162
|
+
export type ReconcileDecision = 'adopt' | 'hold';
|
|
163
|
+
export interface ReconcileResult {
|
|
164
|
+
decision: ReconcileDecision;
|
|
165
|
+
nextState: EvChargeState;
|
|
166
|
+
reason: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Verbatim port: if the car is charging but the scheduler isn't in a
|
|
170
|
+
* charging/night/cooldown state, and current rules allow it, adopt the session.
|
|
171
|
+
*/
|
|
172
|
+
export declare function reconcile(charging: boolean, state: EvChargeState): ReconcileResult;
|
|
173
|
+
export interface NightChargeEstimate {
|
|
174
|
+
neededKwh: number;
|
|
175
|
+
hoursToCharge: number;
|
|
176
|
+
}
|
|
177
|
+
/** kWh needed to reach the grid target + hours at the home charge rate (verbatim). */
|
|
178
|
+
export declare function nightChargeEstimate(evBattery: number): NightChargeEstimate;
|
|
179
|
+
export interface MorningSummary {
|
|
180
|
+
lines: string[];
|
|
181
|
+
outlook: 'good' | 'tight' | 'unlikely' | 'at-target' | 'not-applicable';
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Verbatim port of the morning-summary outlook lines. `remainingKwh` is today's
|
|
185
|
+
* remaining solar forecast; `evBattery`/`pluggedIn` come from the Kia snapshot.
|
|
186
|
+
* Outlook thresholds: Good when remaining > needed×1.5, Tight when > needed,
|
|
187
|
+
* else Unlikely; EV at/above the solar target reports "at target".
|
|
188
|
+
*/
|
|
189
|
+
export declare function morningSummary(homeSoc: number | null, evBattery: number | null, pluggedIn: boolean, todayKwh: number | null, remainingKwh: number | null, strongHoursLeft: number | null): MorningSummary;
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import * as SunCalc from 'suncalc';
|
|
2
|
+
/**
|
|
3
|
+
* EV charge scheduler — pure decision logic for a demo solar/EV charging flow.
|
|
4
|
+
* Every decision is a pure function so an Emberflow flow can drive it
|
|
5
|
+
* deterministically (constants, formulas, thresholds, priorities, edge cases).
|
|
6
|
+
*
|
|
7
|
+
* Two notes on shape:
|
|
8
|
+
* 1. `getThresholds` takes trends as an explicit parameter rather than reading
|
|
9
|
+
* a module-level rolling-reading window.
|
|
10
|
+
* 2. Time-dependent helpers take a `now` ISO string (default: current time) and
|
|
11
|
+
* resolve the wall-clock HOUR from UTC (`getUTCHours`) so scenarios pinned
|
|
12
|
+
* with a `now` (…Z) are reproducible on any machine. Sunrise/sunset come
|
|
13
|
+
* from suncalc as absolute UTC instants, so `isSolarHours` /
|
|
14
|
+
* `hoursUntilSunset` are machine-independent.
|
|
15
|
+
*/
|
|
16
|
+
// Demo location (Berlin). Overridable per call.
|
|
17
|
+
export const LAT = 52.52;
|
|
18
|
+
export const LON = 13.405;
|
|
19
|
+
// EV constants (verbatim)
|
|
20
|
+
export const EV_TARGET_BATTERY = 80; // Target for night/grid charging
|
|
21
|
+
export const EV_SOLAR_TARGET = 90; // Target for solar excess charging (AC is gentle, free energy)
|
|
22
|
+
export const EV_CAPACITY_KWH = 77.4; // EV6 Long Range usable capacity
|
|
23
|
+
export const EV_CHARGE_RATE_KW = 2.3; // Home EVSE charge rate (10A single phase)
|
|
24
|
+
export const EV_LOW_BATTERY = 50; // Below this, EV gets priority over home battery
|
|
25
|
+
export const EV_NIGHT_CHARGE_BELOW = 35; // Below this at night, force a grid charge to 80%
|
|
26
|
+
// Thresholds (watts) — reactive mode (verbatim)
|
|
27
|
+
export const START_SURPLUS_W = 1500;
|
|
28
|
+
export const STOP_SURPLUS_W = 500;
|
|
29
|
+
export const START_MIN_SOC = 30;
|
|
30
|
+
export const STOP_MIN_SOC = 20;
|
|
31
|
+
export const COOLDOWN_MS = 10 * 60_000;
|
|
32
|
+
// Predictive thresholds (verbatim)
|
|
33
|
+
export const PREDICTIVE_BATTERY_SOC = 90;
|
|
34
|
+
export const PREDICTIVE_MIN_PPV = 2000;
|
|
35
|
+
export const SUNSET_HOURS_THRESHOLD = 2;
|
|
36
|
+
export const GOLDEN_HOUR_START_W = 800;
|
|
37
|
+
export const GOLDEN_HOUR_STOP_W = 200;
|
|
38
|
+
// Worth-it calculation: minimum kWh a session must deliver to justify the wake +
|
|
39
|
+
// command overhead (~13 min at 2.3kW — below this it's not worth the wear).
|
|
40
|
+
export const MIN_SESSION_KWH = 0.5;
|
|
41
|
+
// Trend tracking — rolling window of last N readings (verbatim).
|
|
42
|
+
export const TREND_WINDOW = 6;
|
|
43
|
+
function resolveNow(now) {
|
|
44
|
+
return now ? new Date(now) : new Date();
|
|
45
|
+
}
|
|
46
|
+
/** Sun position + derived time flags for the given instant/location. */
|
|
47
|
+
export function sunTimes(now, lat = LAT, lon = LON) {
|
|
48
|
+
const d = resolveNow(now);
|
|
49
|
+
const times = SunCalc.getTimes(d, lat, lon);
|
|
50
|
+
// @types/suncalc types sunrise/sunset as possibly null (polar edge cases);
|
|
51
|
+
// for this latitude they are always present. Fall back to the instant itself.
|
|
52
|
+
const sunrise = times.sunrise ?? d;
|
|
53
|
+
const sunset = times.sunset ?? d;
|
|
54
|
+
const isSolarHours = d > sunrise && d < sunset;
|
|
55
|
+
const hoursUntilSunset = Math.max(0, (sunset.getTime() - d.getTime()) / 3_600_000);
|
|
56
|
+
const hour = d.getUTCHours();
|
|
57
|
+
// Verbatim night gate: production returns early "outside night" when
|
|
58
|
+
// `hour >= 5 && hour < 23`, i.e. the night window is hour >= 23 || hour < 5.
|
|
59
|
+
const isNightWindow = hour >= 23 || hour < 5;
|
|
60
|
+
return {
|
|
61
|
+
isSolarHours,
|
|
62
|
+
hoursUntilSunset,
|
|
63
|
+
sunrise: sunrise.toISOString(),
|
|
64
|
+
sunset: sunset.toISOString(),
|
|
65
|
+
hour,
|
|
66
|
+
isNightWindow,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Verbatim surplus calc (evaluateCharge):
|
|
71
|
+
* surplus = gridExport > 0 ? gridExport : max(0, ppv - loadPower)
|
|
72
|
+
*/
|
|
73
|
+
export function computeSurplus(ppv, loadPower, gridExport) {
|
|
74
|
+
if (gridExport > 0)
|
|
75
|
+
return { surplus: gridExport, branch: 'export' };
|
|
76
|
+
return { surplus: Math.max(0, ppv - loadPower), branch: 'ppv-load' };
|
|
77
|
+
}
|
|
78
|
+
/** How many kWh the EV needs to reach target (verbatim evKwhNeeded). */
|
|
79
|
+
export function evKwhNeeded(currentPct, target = EV_TARGET_BATTERY) {
|
|
80
|
+
const pctNeeded = target - currentPct;
|
|
81
|
+
if (pctNeeded <= 0)
|
|
82
|
+
return 0;
|
|
83
|
+
return (pctNeeded / 100) * EV_CAPACITY_KWH;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Estimate deliverable kWh from remaining solar (verbatim estimateSessionKwh).
|
|
87
|
+
* With a forecast: remaining kWh capped by charge rate over the hours left.
|
|
88
|
+
* Without: assume current surplus sustains for the remaining sun hours.
|
|
89
|
+
*/
|
|
90
|
+
export function estimateSessionKwh(surplusW, sunHoursLeft, forecast) {
|
|
91
|
+
if (forecast) {
|
|
92
|
+
return Math.min(forecast.remainingKwh, EV_CHARGE_RATE_KW * sunHoursLeft);
|
|
93
|
+
}
|
|
94
|
+
return (Math.min(surplusW, EV_CHARGE_RATE_KW * 1000) / 1000) * sunHoursLeft;
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Trends + dynamic cooldown (verbatim getTrend / getCooldownMs)
|
|
98
|
+
// ============================================================================
|
|
99
|
+
/** Per-cycle slope of `key` across the reading window (verbatim getTrend). */
|
|
100
|
+
export function getTrend(readings, key) {
|
|
101
|
+
if (readings.length < 2)
|
|
102
|
+
return 0;
|
|
103
|
+
const first = readings[0];
|
|
104
|
+
const last = readings[readings.length - 1];
|
|
105
|
+
const cycles = readings.length - 1;
|
|
106
|
+
return (last[key] - first[key]) / cycles;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Dynamic cooldown from surplus volatility (verbatim getCooldownMs):
|
|
110
|
+
* < 3 readings → default 10 min
|
|
111
|
+
* stdDev < 200 → stable → 5 min
|
|
112
|
+
* stdDev > 800 → volatile → 15 min
|
|
113
|
+
* else → default 10 min
|
|
114
|
+
*/
|
|
115
|
+
export function getCooldownMs(readings) {
|
|
116
|
+
if (readings.length < 3)
|
|
117
|
+
return COOLDOWN_MS;
|
|
118
|
+
const surpluses = readings.map((r) => r.surplus);
|
|
119
|
+
const mean = surpluses.reduce((a, b) => a + b, 0) / surpluses.length;
|
|
120
|
+
const variance = surpluses.reduce((sum, s) => sum + (s - mean) ** 2, 0) / surpluses.length;
|
|
121
|
+
const stdDev = Math.sqrt(variance);
|
|
122
|
+
if (stdDev < 200)
|
|
123
|
+
return 5 * 60_000;
|
|
124
|
+
if (stdDev > 800)
|
|
125
|
+
return 15 * 60_000;
|
|
126
|
+
return COOLDOWN_MS;
|
|
127
|
+
}
|
|
128
|
+
/** stdDev of surplus across the window (0 for <3 readings, mirroring the cooldown gate). */
|
|
129
|
+
export function surplusStdDev(readings) {
|
|
130
|
+
if (readings.length < 1)
|
|
131
|
+
return 0;
|
|
132
|
+
const surpluses = readings.map((r) => r.surplus);
|
|
133
|
+
const mean = surpluses.reduce((a, b) => a + b, 0) / surpluses.length;
|
|
134
|
+
const variance = surpluses.reduce((sum, s) => sum + (s - mean) ** 2, 0) / surpluses.length;
|
|
135
|
+
return Math.sqrt(variance);
|
|
136
|
+
}
|
|
137
|
+
/** Bundle the trend-window derivations one Trend Window node emits. */
|
|
138
|
+
export function computeTrendWindow(readings) {
|
|
139
|
+
return {
|
|
140
|
+
surplusTrend: getTrend(readings, 'surplus'),
|
|
141
|
+
socTrend: getTrend(readings, 'homeSoc'),
|
|
142
|
+
ppvTrend: getTrend(readings, 'ppv'),
|
|
143
|
+
stdDev: surplusStdDev(readings),
|
|
144
|
+
cooldownMs: getCooldownMs(readings),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Verbatim port of getThresholds. The ONLY change: `trends` (surplus + home-SOC
|
|
149
|
+
* per-cycle slopes) is passed in explicitly instead of read from module state
|
|
150
|
+
* via `getTrend(...)`. Defaults to zero slopes (no trend override), matching a
|
|
151
|
+
* cold start with fewer than two readings.
|
|
152
|
+
*/
|
|
153
|
+
export function getThresholds(homeSoc, ppv, sunHoursLeft, evBattery, forecast, trends = { surplusTrend: 0, socTrend: 0 }) {
|
|
154
|
+
const reasons = [];
|
|
155
|
+
// Default thresholds must cover the EV charge rate (2.3kW) so we don't
|
|
156
|
+
// primarily import from grid. Home battery can buffer the small shortfall.
|
|
157
|
+
let startW = 2000;
|
|
158
|
+
let stopW = 800;
|
|
159
|
+
let skipReason = null;
|
|
160
|
+
const evUrgent = evBattery !== null && evBattery < EV_LOW_BATTERY;
|
|
161
|
+
const homeHasBuffer = homeSoc >= 30;
|
|
162
|
+
if (evUrgent) {
|
|
163
|
+
startW = 1200;
|
|
164
|
+
stopW = 400;
|
|
165
|
+
reasons.push(`ev urgent: ${evBattery}% < ${EV_LOW_BATTERY}%, accepting some grid`);
|
|
166
|
+
}
|
|
167
|
+
else if (homeHasBuffer) {
|
|
168
|
+
startW = 1200;
|
|
169
|
+
stopW = 400;
|
|
170
|
+
reasons.push(`home ${homeSoc}% has buffer — EV priority, home tops up EV shortfall`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
reasons.push(`home ${homeSoc}% flat — need ${startW}W solar to avoid grid import`);
|
|
174
|
+
}
|
|
175
|
+
// --- Predictive: battery nearly full + strong production → export imminent ---
|
|
176
|
+
if (homeSoc >= PREDICTIVE_BATTERY_SOC && ppv >= PREDICTIVE_MIN_PPV) {
|
|
177
|
+
startW = Math.min(startW, 500);
|
|
178
|
+
stopW = Math.min(stopW, 200);
|
|
179
|
+
reasons.push(`predictive: home SOC ${homeSoc}% + ppv ${Math.round(ppv)}W → export imminent`);
|
|
180
|
+
}
|
|
181
|
+
// --- Golden hour: sun setting soon, capture remaining solar ---
|
|
182
|
+
if (sunHoursLeft > 0 && sunHoursLeft <= SUNSET_HOURS_THRESHOLD && !skipReason) {
|
|
183
|
+
startW = Math.min(startW, GOLDEN_HOUR_START_W);
|
|
184
|
+
stopW = Math.min(stopW, GOLDEN_HOUR_STOP_W);
|
|
185
|
+
reasons.push(`golden hour: ${sunHoursLeft.toFixed(1)}h until sunset`);
|
|
186
|
+
}
|
|
187
|
+
// --- Worth-it check: is the remaining solar enough to justify a session? ---
|
|
188
|
+
const inBonusZone = evBattery !== null && evBattery >= EV_TARGET_BATTERY && evBattery < EV_SOLAR_TARGET;
|
|
189
|
+
if (evBattery !== null && forecast && !skipReason) {
|
|
190
|
+
const estimatedKwh = estimateSessionKwh(ppv, sunHoursLeft, forecast);
|
|
191
|
+
const target = inBonusZone ? EV_SOLAR_TARGET : EV_TARGET_BATTERY;
|
|
192
|
+
const needed = evKwhNeeded(evBattery, target);
|
|
193
|
+
if (estimatedKwh < MIN_SESSION_KWH && needed > 2) {
|
|
194
|
+
skipReason = `not worth it: ~${estimatedKwh.toFixed(1)}kWh available vs ${needed.toFixed(0)}kWh needed`;
|
|
195
|
+
reasons.push(skipReason);
|
|
196
|
+
}
|
|
197
|
+
if (evBattery >= 65 && estimatedKwh < 1.5) {
|
|
198
|
+
skipReason = `EV at ${evBattery}%, only ~${estimatedKwh.toFixed(1)}kWh solar remaining — not worth a session`;
|
|
199
|
+
reasons.push(skipReason);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// --- Trend-aware: surplus rising fast → be more aggressive ---
|
|
203
|
+
const surplusTrend = trends.surplusTrend;
|
|
204
|
+
if (surplusTrend > 200) {
|
|
205
|
+
startW = Math.min(startW, 1000);
|
|
206
|
+
reasons.push(`trend: surplus rising ${Math.round(surplusTrend)}W/cycle`);
|
|
207
|
+
}
|
|
208
|
+
// --- SOC climbing fast → battery about to fill ---
|
|
209
|
+
const socTrend = trends.socTrend;
|
|
210
|
+
if (socTrend > 2 && homeSoc > 75) {
|
|
211
|
+
startW = Math.min(startW, 600);
|
|
212
|
+
reasons.push(`trend: home SOC rising ${socTrend.toFixed(1)}%/cycle from ${homeSoc}%`);
|
|
213
|
+
}
|
|
214
|
+
return { startW, stopW, skipReason, reasons };
|
|
215
|
+
}
|
|
216
|
+
export function shouldStopUnscheduledCharge({ charging, state, surplus, startW, skipReason, homeSoc, effectiveMinSoc, }) {
|
|
217
|
+
if (!charging)
|
|
218
|
+
return false;
|
|
219
|
+
if (state === 'charging' || state === 'cooldown' || state === 'night_charging')
|
|
220
|
+
return false;
|
|
221
|
+
if (skipReason)
|
|
222
|
+
return true;
|
|
223
|
+
if (surplus < startW)
|
|
224
|
+
return true;
|
|
225
|
+
if (homeSoc < effectiveMinSoc)
|
|
226
|
+
return true;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
export function getNightChargeDecision(evBattery, tomorrowKwh) {
|
|
230
|
+
const factors = [];
|
|
231
|
+
const criticallyLow = evBattery < EV_NIGHT_CHARGE_BELOW;
|
|
232
|
+
const poorForecast = tomorrowKwh !== null && tomorrowKwh < 10; // < 10kWh expected = poor day
|
|
233
|
+
const evModerate = evBattery < 65;
|
|
234
|
+
if (criticallyLow) {
|
|
235
|
+
factors.push(`EV critically low at ${evBattery}% (< ${EV_NIGHT_CHARGE_BELOW}%)`);
|
|
236
|
+
}
|
|
237
|
+
else if (evModerate && poorForecast) {
|
|
238
|
+
factors.push(`EV at ${evBattery}% with poor forecast tomorrow (${tomorrowKwh?.toFixed(1)}kWh)`);
|
|
239
|
+
}
|
|
240
|
+
return { shouldCharge: factors.length > 0, factors };
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Verbatim port of evaluateCharge's idle/waiting/charging branch (lines
|
|
244
|
+
* 629–705). effectiveMinSoc and effectiveStopSoc are both 0 in production
|
|
245
|
+
* (morning home SOC near 0% is normal after overnight discharge — the surplus
|
|
246
|
+
* threshold already prevents grid import), so the SOC comparisons never trip
|
|
247
|
+
* here; they are preserved verbatim for fidelity.
|
|
248
|
+
*/
|
|
249
|
+
export function decideCharge({ state, surplus, startW, stopW, homeSoc, skipReason, }) {
|
|
250
|
+
const effectiveMinSoc = 0;
|
|
251
|
+
const effectiveStopSoc = 0;
|
|
252
|
+
if (state === 'idle' || state === 'waiting') {
|
|
253
|
+
if (skipReason) {
|
|
254
|
+
return { action: 'hold_waiting', nextState: 'waiting', reason: skipReason };
|
|
255
|
+
}
|
|
256
|
+
if (surplus >= startW && homeSoc >= effectiveMinSoc) {
|
|
257
|
+
return {
|
|
258
|
+
action: 'start',
|
|
259
|
+
nextState: 'cooldown',
|
|
260
|
+
reason: `surplus ${Math.round(surplus)}W ≥ ${startW}W threshold, starting charge`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (surplus < startW) {
|
|
264
|
+
return {
|
|
265
|
+
action: 'hold_waiting',
|
|
266
|
+
nextState: 'waiting',
|
|
267
|
+
reason: `surplus ${Math.round(surplus)}W < ${startW}W threshold`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
action: 'hold_waiting',
|
|
272
|
+
nextState: 'waiting',
|
|
273
|
+
reason: `home SOC ${homeSoc}% < ${effectiveMinSoc}% threshold`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// state === 'charging'
|
|
277
|
+
if (surplus < stopW || homeSoc < effectiveStopSoc) {
|
|
278
|
+
const reason = surplus < stopW
|
|
279
|
+
? `surplus ${Math.round(surplus)}W < ${stopW}W, stopping charge`
|
|
280
|
+
: `home SOC ${homeSoc}% < ${effectiveStopSoc}%, stopping charge`;
|
|
281
|
+
return { action: 'stop', nextState: 'cooldown', reason };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
action: 'hold_charging',
|
|
285
|
+
nextState: 'charging',
|
|
286
|
+
reason: `charging — surplus ${Math.round(surplus)}W, home SOC ${homeSoc}%`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Verbatim port of the plugged-in / at-target gate. Effective target is the
|
|
291
|
+
* solar target (90%) during solar hours, else the grid target (80%).
|
|
292
|
+
*/
|
|
293
|
+
export function checkTarget(pluggedIn, evBattery, isSolarHours) {
|
|
294
|
+
const effectiveTarget = isSolarHours ? EV_SOLAR_TARGET : EV_TARGET_BATTERY;
|
|
295
|
+
if (!pluggedIn) {
|
|
296
|
+
return { branch: 'notPlugged', effectiveTarget, reason: 'not plugged in' };
|
|
297
|
+
}
|
|
298
|
+
if (evBattery !== null && evBattery >= effectiveTarget) {
|
|
299
|
+
return {
|
|
300
|
+
branch: 'atTarget',
|
|
301
|
+
effectiveTarget,
|
|
302
|
+
reason: `battery at ${evBattery}% (target ${effectiveTarget}%)`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
branch: 'belowTarget',
|
|
307
|
+
effectiveTarget,
|
|
308
|
+
reason: `battery at ${evBattery ?? '?'}% below target ${effectiveTarget}%`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Verbatim port: if the car is charging but the scheduler isn't in a
|
|
313
|
+
* charging/night/cooldown state, and current rules allow it, adopt the session.
|
|
314
|
+
*/
|
|
315
|
+
export function reconcile(charging, state) {
|
|
316
|
+
if (charging && state !== 'charging' && state !== 'night_charging' && state !== 'cooldown') {
|
|
317
|
+
return {
|
|
318
|
+
decision: 'adopt',
|
|
319
|
+
nextState: 'charging',
|
|
320
|
+
reason: 'reconciled: car was already charging and scheduler conditions are met',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return { decision: 'hold', nextState: state, reason: 'no reconciliation needed' };
|
|
324
|
+
}
|
|
325
|
+
/** kWh needed to reach the grid target + hours at the home charge rate (verbatim). */
|
|
326
|
+
export function nightChargeEstimate(evBattery) {
|
|
327
|
+
const neededKwh = evKwhNeeded(evBattery); // target = EV_TARGET_BATTERY (80)
|
|
328
|
+
return { neededKwh, hoursToCharge: neededKwh / EV_CHARGE_RATE_KW };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Verbatim port of the morning-summary outlook lines. `remainingKwh` is today's
|
|
332
|
+
* remaining solar forecast; `evBattery`/`pluggedIn` come from the Kia snapshot.
|
|
333
|
+
* Outlook thresholds: Good when remaining > needed×1.5, Tight when > needed,
|
|
334
|
+
* else Unlikely; EV at/above the solar target reports "at target".
|
|
335
|
+
*/
|
|
336
|
+
export function morningSummary(homeSoc, evBattery, pluggedIn, todayKwh, remainingKwh, strongHoursLeft) {
|
|
337
|
+
const homeStr = homeSoc ?? '?';
|
|
338
|
+
const evStr = evBattery ?? '?';
|
|
339
|
+
const plugged = pluggedIn ? 'plugged in' : 'not plugged in';
|
|
340
|
+
const todayStr = todayKwh != null ? todayKwh.toFixed(1) : '?';
|
|
341
|
+
const strongStr = strongHoursLeft ?? '?';
|
|
342
|
+
const remainStr = remainingKwh != null ? remainingKwh.toFixed(1) : '?';
|
|
343
|
+
const lines = [
|
|
344
|
+
`Home battery: ${homeStr}%`,
|
|
345
|
+
`EV: ${evStr}% (${plugged})`,
|
|
346
|
+
`Solar forecast: ${todayStr}kWh today (${remainStr}kWh remaining, ${strongStr}h strong sun)`,
|
|
347
|
+
];
|
|
348
|
+
let outlook = 'not-applicable';
|
|
349
|
+
if (pluggedIn && typeof evBattery === 'number' && evBattery < EV_SOLAR_TARGET) {
|
|
350
|
+
const needed = evKwhNeeded(evBattery, EV_SOLAR_TARGET);
|
|
351
|
+
const remaining = remainingKwh ?? 0;
|
|
352
|
+
if (remaining > needed * 1.5) {
|
|
353
|
+
outlook = 'good';
|
|
354
|
+
lines.push(`Outlook: Good — ${remaining.toFixed(1)}kWh forecast should cover ${needed.toFixed(1)}kWh EV needs`);
|
|
355
|
+
}
|
|
356
|
+
else if (remaining > needed) {
|
|
357
|
+
outlook = 'tight';
|
|
358
|
+
lines.push(`Outlook: Tight — ${remaining.toFixed(1)}kWh forecast vs ${needed.toFixed(1)}kWh EV needs (home battery gets priority)`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
outlook = 'unlikely';
|
|
362
|
+
lines.push(`Outlook: Unlikely to fully charge EV today (${remaining.toFixed(1)}kWh forecast vs ${needed.toFixed(1)}kWh needed)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (typeof evBattery === 'number' && evBattery >= EV_SOLAR_TARGET) {
|
|
366
|
+
outlook = 'at-target';
|
|
367
|
+
lines.push(`EV already at target (${EV_SOLAR_TARGET}%) — no charging needed`);
|
|
368
|
+
}
|
|
369
|
+
return { lines, outlook };
|
|
370
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { NodeRegistry } from '../../engine/index.js';
|
|
2
|
+
/** Shape of {EV_DEMO_BASE_URL}/api/growatt/status we consume (inverter status endpoint). */
|
|
3
|
+
export interface GrowattStatusJson {
|
|
4
|
+
soc?: number;
|
|
5
|
+
ppv?: number;
|
|
6
|
+
loadPower?: number;
|
|
7
|
+
gridExport?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Shape of {EV_DEMO_BASE_URL}/api/vehicle/status we consume. */
|
|
10
|
+
export interface KiaStatusJson {
|
|
11
|
+
resMsg?: {
|
|
12
|
+
vehicleStatusInfo?: {
|
|
13
|
+
vehicleStatus?: {
|
|
14
|
+
evStatus?: {
|
|
15
|
+
batteryStatus?: number;
|
|
16
|
+
batteryPlugin?: number;
|
|
17
|
+
batteryCharge?: boolean;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** The live tick fields the scheduler derives before running the decision logic. */
|
|
24
|
+
export interface EvTickLiveFields {
|
|
25
|
+
ppv: number;
|
|
26
|
+
loadPower: number;
|
|
27
|
+
gridExport: number;
|
|
28
|
+
homeSoc: number;
|
|
29
|
+
evBattery: number | null;
|
|
30
|
+
pluggedIn: boolean;
|
|
31
|
+
charging: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Maps a Growatt status + Kia vehicle status into the flow's live tick fields,
|
|
35
|
+
* homeSoc = growatt.soc, ppv/loadPower/gridExport straight off GrowattStatus
|
|
36
|
+
* (loadPower is already mapped from sd.pLocalLoad inside getGrowattStatus), and
|
|
37
|
+
* evStatus at resMsg.vehicleStatusInfo.vehicleStatus.evStatus →
|
|
38
|
+
* batteryStatus→evBattery, (batteryPlugin ?? 0) > 0→pluggedIn,
|
|
39
|
+
* batteryCharge === true→charging. Pure so it's unit-testable without fetch.
|
|
40
|
+
*/
|
|
41
|
+
export declare function mapTickTelemetry(growatt: GrowattStatusJson | undefined, kia: KiaStatusJson | undefined): EvTickLiveFields;
|
|
42
|
+
export declare function registerPradarNodes(registry: NodeRegistry): void;
|