@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,801 @@
|
|
|
1
|
+
import { checkTarget, computeSurplus, computeTrendWindow, decideCharge, estimateSessionKwh, evKwhNeeded, EV_TARGET_BATTERY, getNightChargeDecision, getThresholds, MIN_SESSION_KWH, morningSummary, nightChargeEstimate, reconcile, shouldStopUnscheduledCharge, sunTimes, } from './logic.js';
|
|
2
|
+
/**
|
|
3
|
+
* EV charge scheduler — Emberflow registry nodes for the EV charging demo. Each
|
|
4
|
+
* node wraps a pure function from ./logic and echoes its salient inputs back
|
|
5
|
+
* through its output so downstream nodes stay attributable in the run trace.
|
|
6
|
+
* Branching nodes emit `$branch` directly (like Route/Conditional) so a flow can
|
|
7
|
+
* gate the next node on the decision without a separate Conditional.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here talks to real hardware: the vehicle / push-notification effect
|
|
10
|
+
* nodes are SIMULATED (see each PORT NOTE).
|
|
11
|
+
*/
|
|
12
|
+
// ── coercion helpers ────────────────────────────────────────────────────────
|
|
13
|
+
function num(v, fallback = 0) {
|
|
14
|
+
const n = Number(v);
|
|
15
|
+
return Number.isFinite(n) ? n : fallback;
|
|
16
|
+
}
|
|
17
|
+
function nullableNum(v) {
|
|
18
|
+
if (v === null || v === undefined || v === '')
|
|
19
|
+
return null;
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return Number.isFinite(n) ? n : null;
|
|
22
|
+
}
|
|
23
|
+
function bool(v) {
|
|
24
|
+
return v === true || v === 'true';
|
|
25
|
+
}
|
|
26
|
+
function asForecast(v) {
|
|
27
|
+
if (v && typeof v === 'object') {
|
|
28
|
+
const o = v;
|
|
29
|
+
if (o.remainingKwh !== undefined || o.strongHoursLeft !== undefined) {
|
|
30
|
+
return { remainingKwh: num(o.remainingKwh), strongHoursLeft: num(o.strongHoursLeft) };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function asReadings(v) {
|
|
36
|
+
if (!Array.isArray(v))
|
|
37
|
+
return [];
|
|
38
|
+
return v.map((r) => {
|
|
39
|
+
const o = (r ?? {});
|
|
40
|
+
return { surplus: num(o.surplus), homeSoc: num(o.homeSoc), ppv: num(o.ppv), time: o.time };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// ── live telemetry (EvLoadTickSnapshot) ─────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Reads the run's secret or throws an actionable error — live telemetry needs
|
|
46
|
+
* the EV Demo base URL and only the server runner has it.
|
|
47
|
+
*/
|
|
48
|
+
function requireSecret(ctx, name) {
|
|
49
|
+
const value = ctx.secrets[name];
|
|
50
|
+
if (!value) {
|
|
51
|
+
throw new Error(`Missing secret ${name} — select an environment that provides it (e.g. production) and run on the server. Live EV telemetry needs the EV Demo base URL; the browser and the local environment don't have ${name}.`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
/** prod and production are both treated as the live-telemetry environment. */
|
|
56
|
+
const isLivePradarEnv = (env) => env === 'prod' || env === 'production';
|
|
57
|
+
/**
|
|
58
|
+
* Maps a Growatt status + Kia vehicle status into the flow's live tick fields,
|
|
59
|
+
* homeSoc = growatt.soc, ppv/loadPower/gridExport straight off GrowattStatus
|
|
60
|
+
* (loadPower is already mapped from sd.pLocalLoad inside getGrowattStatus), and
|
|
61
|
+
* evStatus at resMsg.vehicleStatusInfo.vehicleStatus.evStatus →
|
|
62
|
+
* batteryStatus→evBattery, (batteryPlugin ?? 0) > 0→pluggedIn,
|
|
63
|
+
* batteryCharge === true→charging. Pure so it's unit-testable without fetch.
|
|
64
|
+
*/
|
|
65
|
+
export function mapTickTelemetry(growatt, kia) {
|
|
66
|
+
const ev = kia?.resMsg?.vehicleStatusInfo?.vehicleStatus?.evStatus ?? {};
|
|
67
|
+
return {
|
|
68
|
+
ppv: num(growatt?.ppv),
|
|
69
|
+
loadPower: num(growatt?.loadPower),
|
|
70
|
+
gridExport: num(growatt?.gridExport),
|
|
71
|
+
homeSoc: num(growatt?.soc),
|
|
72
|
+
evBattery: ev.batteryStatus ?? null,
|
|
73
|
+
pluggedIn: (ev.batteryPlugin ?? 0) > 0,
|
|
74
|
+
charging: ev.batteryCharge === true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** GET one EV Demo status endpoint with a bounded timeout, throwing a clear error on failure. */
|
|
78
|
+
async function fetchPradarJson(ctx, url, label) {
|
|
79
|
+
ctx.log('info', `GET ${url}`);
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new Error(`${label} status fetch failed (${url}): ${err instanceof Error ? err.message : String(err)}`);
|
|
86
|
+
}
|
|
87
|
+
if (!response.ok)
|
|
88
|
+
throw new Error(`${label} status fetch failed: HTTP ${response.status} (${url})`);
|
|
89
|
+
return (await response.json());
|
|
90
|
+
}
|
|
91
|
+
export function registerPradarNodes(registry) {
|
|
92
|
+
// ==========================================================================
|
|
93
|
+
// EV · Load Tick Snapshot (env-aware live telemetry)
|
|
94
|
+
// ==========================================================================
|
|
95
|
+
registry.register({
|
|
96
|
+
type: 'EvLoadTickSnapshot',
|
|
97
|
+
label: 'EV · Load Tick',
|
|
98
|
+
description: "Loads the 5-min tick snapshot, binding to the run's environment: in PROD it pulls LIVE telemetry from the EV Demo service — GET {EV_DEMO_BASE_URL}/api/growatt/status for solar/battery (soc→homeSoc, ppv, loadPower, gridExport) and GET {EV_DEMO_BASE_URL}/api/kia/status for the EV (resMsg.vehicleStatusInfo.vehicleStatus.evStatus → batteryStatus→evBattery, batteryPlugin>0→pluggedIn, batteryCharge===true→charging); in every other environment it passes the scenario Input snapshot through unchanged. One node, one runbook step — the environment decides the source, no branching in the document. PORT NOTE: prod pulls live Growatt solar/battery + Kia EV telemetry verbatim to the scheduler's snapshot assembly. forecast, readings and state are NOT exposed per-tick by the status endpoints (they are the scheduler's rolling in-memory state + the solar-forecast service), and `now` is the run's clock, so all four pass through from Input on BOTH paths (best-effort in prod). The /api/ev-charge/status endpoint's `state` is deliberately NOT read: its enum (idle|waiting|charging|cooldown|night_charging) does not map cleanly to the flow's idle|waiting|charging, so Input's state is authoritative.",
|
|
99
|
+
simpleDescription: 'Loads the tick snapshot — real telemetry from EV Demo, scenario input otherwise.',
|
|
100
|
+
category: 'pradar',
|
|
101
|
+
traceKind: 'http',
|
|
102
|
+
traceDetail: 'prod: GET {EV_DEMO_BASE_URL}/api/growatt/status + /api/kia/status · non-prod: scenario snapshot passthrough',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
fields: [
|
|
105
|
+
{ name: 'ppv', type: 'number', description: 'Non-prod passthrough — prod reads GrowattStatus.ppv.' },
|
|
106
|
+
{ name: 'loadPower', type: 'number', description: 'Non-prod passthrough — prod reads GrowattStatus.loadPower.' },
|
|
107
|
+
{ name: 'gridExport', type: 'number', description: 'Non-prod passthrough — prod reads GrowattStatus.gridExport.' },
|
|
108
|
+
{ name: 'homeSoc', type: 'number', description: 'Non-prod passthrough — prod reads GrowattStatus.soc.' },
|
|
109
|
+
{ name: 'evBattery', type: 'number', description: 'Non-prod passthrough — prod reads Kia evStatus.batteryStatus.' },
|
|
110
|
+
{ name: 'pluggedIn', type: 'boolean', description: 'Non-prod passthrough — prod derives from Kia evStatus.batteryPlugin.' },
|
|
111
|
+
{ name: 'charging', type: 'boolean', description: 'Non-prod passthrough — prod derives from Kia evStatus.batteryCharge.' },
|
|
112
|
+
{ name: 'forecast', type: 'object', description: 'Always from Input — not exposed per-tick by the status endpoints.' },
|
|
113
|
+
{ name: 'readings', type: 'array', description: 'Always from Input — the scheduler’s rolling window, not on the status endpoints.' },
|
|
114
|
+
{ name: 'state', type: 'enum', enumValues: ['idle', 'waiting', 'charging'], description: 'Always from Input — scheduler-internal state, not read from /api/ev-charge/status.' },
|
|
115
|
+
{ name: 'now', type: 'datetime', description: 'Always from Input — the run’s clock.' },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
outputSchema: {
|
|
119
|
+
fields: [
|
|
120
|
+
{ name: 'ppv', type: 'number' },
|
|
121
|
+
{ name: 'loadPower', type: 'number' },
|
|
122
|
+
{ name: 'gridExport', type: 'number' },
|
|
123
|
+
{ name: 'homeSoc', type: 'number' },
|
|
124
|
+
{ name: 'evBattery', type: 'number' },
|
|
125
|
+
{ name: 'pluggedIn', type: 'boolean' },
|
|
126
|
+
{ name: 'charging', type: 'boolean' },
|
|
127
|
+
{ name: 'forecast', type: 'object' },
|
|
128
|
+
{ name: 'readings', type: 'array' },
|
|
129
|
+
{ name: 'state', type: 'string' },
|
|
130
|
+
{ name: 'now', type: 'string' },
|
|
131
|
+
{ name: 'source', type: 'string' },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
}, async (ctx) => {
|
|
135
|
+
// forecast/readings/state are never on the status endpoints — they pass
|
|
136
|
+
// through from Input on both paths (best-effort in prod). `now` passes
|
|
137
|
+
// through in test (scenarios pin it for reproducibility) but is overridden
|
|
138
|
+
// with the real wall-clock in prod below — a live tick is happening NOW,
|
|
139
|
+
// so the sun-clock/night-window gates must reflect the actual moment, not
|
|
140
|
+
// whatever instant a test scenario pinned.
|
|
141
|
+
const passthrough = {
|
|
142
|
+
forecast: ctx.input.forecast,
|
|
143
|
+
readings: ctx.input.readings,
|
|
144
|
+
state: ctx.input.state,
|
|
145
|
+
now: ctx.input.now,
|
|
146
|
+
};
|
|
147
|
+
// Bind to the environment: prod pulls live telemetry from EV Demo, every
|
|
148
|
+
// other env stands in with the scenario snapshot. Downstream nodes decide
|
|
149
|
+
// identically either way, so the flow reads as one "Load Tick" step.
|
|
150
|
+
if (isLivePradarEnv(ctx.environment)) {
|
|
151
|
+
const base = requireSecret(ctx, 'EV_DEMO_BASE_URL').replace(/\/+$/, '');
|
|
152
|
+
const growatt = (await fetchPradarJson(ctx, `${base}/api/growatt/status`, 'Growatt'));
|
|
153
|
+
const kia = (await fetchPradarJson(ctx, `${base}/api/kia/status`, 'Kia'));
|
|
154
|
+
const live = mapTickTelemetry(growatt, kia);
|
|
155
|
+
ctx.log('info', `[${ctx.environment}] Live EV Demo telemetry: ppv ${Math.round(live.ppv)}W, load ${Math.round(live.loadPower)}W, ` +
|
|
156
|
+
`export ${Math.round(live.gridExport)}W, home ${live.homeSoc}%, EV ${live.evBattery ?? '—'}% ` +
|
|
157
|
+
`(${live.pluggedIn ? 'plugged in' : 'unplugged'}${live.charging ? ', charging' : ''})`);
|
|
158
|
+
return { ...live, ...passthrough, now: new Date().toISOString(), source: 'prod-pradar' };
|
|
159
|
+
}
|
|
160
|
+
// Non-prod: pass the scenario snapshot through VERBATIM (no coercion — the
|
|
161
|
+
// downstream nodes coerce as before) so existing scenarios route identically.
|
|
162
|
+
ctx.log('info', `[${ctx.environment ?? 'local'}] Tick snapshot passthrough (non-prod stand-in for the live EV Demo load)`);
|
|
163
|
+
return {
|
|
164
|
+
ppv: ctx.input.ppv,
|
|
165
|
+
loadPower: ctx.input.loadPower,
|
|
166
|
+
gridExport: ctx.input.gridExport,
|
|
167
|
+
homeSoc: ctx.input.homeSoc,
|
|
168
|
+
evBattery: ctx.input.evBattery,
|
|
169
|
+
pluggedIn: ctx.input.pluggedIn,
|
|
170
|
+
charging: ctx.input.charging,
|
|
171
|
+
...passthrough,
|
|
172
|
+
source: 'test',
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
// ==========================================================================
|
|
176
|
+
// EV · Sun Clock
|
|
177
|
+
// ==========================================================================
|
|
178
|
+
registry.register({
|
|
179
|
+
type: 'EvSunClock',
|
|
180
|
+
label: 'EV · Sun Clock',
|
|
181
|
+
description: 'Resolves sun position + time flags for an instant via suncalc. isSolarHours (between sunrise/sunset), hoursUntilSunset, and the 23:00–05:00 night window. PORT NOTE: production used the server\'s local getHours(); this port derives the wall-clock hour from UTC so scenarios pinned with a `now` (…Z) are reproducible on any machine.',
|
|
182
|
+
simpleDescription: 'Figures out sunrise, sunset, and whether it’s nighttime',
|
|
183
|
+
category: 'pradar',
|
|
184
|
+
traceKind: 'compute',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
fields: [
|
|
187
|
+
{ name: 'now', type: 'datetime', description: 'ISO instant; defaults to current time.' },
|
|
188
|
+
{ name: 'lat', type: 'number' },
|
|
189
|
+
{ name: 'lon', type: 'number' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
outputSchema: {
|
|
193
|
+
fields: [
|
|
194
|
+
{ name: 'isSolarHours', type: 'boolean' },
|
|
195
|
+
{ name: 'hoursUntilSunset', type: 'number' },
|
|
196
|
+
{ name: 'sunrise', type: 'string' },
|
|
197
|
+
{ name: 'sunset', type: 'string' },
|
|
198
|
+
{ name: 'hour', type: 'number' },
|
|
199
|
+
{ name: 'isNightWindow', type: 'boolean' },
|
|
200
|
+
{ name: 'now', type: 'string' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
}, async (ctx) => {
|
|
204
|
+
// In production the tick is happening NOW: use the real wall-clock so the
|
|
205
|
+
// solar/night gates reflect the actual moment, not a scenario's pinned
|
|
206
|
+
// `now`. In test, honour the pinned `now` for reproducible scenarios.
|
|
207
|
+
const now = isLivePradarEnv(ctx.environment)
|
|
208
|
+
? new Date().toISOString()
|
|
209
|
+
: ctx.input.now
|
|
210
|
+
? String(ctx.input.now)
|
|
211
|
+
: undefined;
|
|
212
|
+
const lat = ctx.input.lat !== undefined ? num(ctx.input.lat) : undefined;
|
|
213
|
+
const lon = ctx.input.lon !== undefined ? num(ctx.input.lon) : undefined;
|
|
214
|
+
const clock = sunTimes(now, lat, lon);
|
|
215
|
+
ctx.log('info', `Sun clock @ ${now ?? 'now'}: ${clock.isSolarHours ? 'solar hours' : 'outside solar hours'}, ` +
|
|
216
|
+
`hour ${clock.hour} (${clock.isNightWindow ? 'night window' : 'day'}), ` +
|
|
217
|
+
`${clock.hoursUntilSunset.toFixed(1)}h until sunset (${clock.sunset})`);
|
|
218
|
+
return { ...clock, now: now ?? new Date().toISOString() };
|
|
219
|
+
});
|
|
220
|
+
// ==========================================================================
|
|
221
|
+
// EV · Compute Surplus
|
|
222
|
+
// ==========================================================================
|
|
223
|
+
registry.register({
|
|
224
|
+
type: 'EvComputeSurplus',
|
|
225
|
+
label: 'EV · Compute Surplus',
|
|
226
|
+
description: 'Solar surplus watts, verbatim: grid export when exporting, else max(0, ppv − load). Reports which branch produced the figure.',
|
|
227
|
+
simpleDescription: 'Calculates how much extra solar power is available',
|
|
228
|
+
category: 'pradar',
|
|
229
|
+
traceKind: 'compute',
|
|
230
|
+
inputSchema: {
|
|
231
|
+
fields: [
|
|
232
|
+
{ name: 'ppv', type: 'number', required: true },
|
|
233
|
+
{ name: 'loadPower', type: 'number', required: true },
|
|
234
|
+
{ name: 'gridExport', type: 'number', required: true },
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
outputSchema: {
|
|
238
|
+
fields: [
|
|
239
|
+
{ name: 'surplus', type: 'number' },
|
|
240
|
+
{ name: 'branch', type: 'enum', enumValues: ['export', 'ppv-load'] },
|
|
241
|
+
{ name: 'ppv', type: 'number' },
|
|
242
|
+
{ name: 'loadPower', type: 'number' },
|
|
243
|
+
{ name: 'gridExport', type: 'number' },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
}, async (ctx) => {
|
|
247
|
+
const ppv = num(ctx.input.ppv);
|
|
248
|
+
const loadPower = num(ctx.input.loadPower);
|
|
249
|
+
const gridExport = num(ctx.input.gridExport);
|
|
250
|
+
const { surplus, branch } = computeSurplus(ppv, loadPower, gridExport);
|
|
251
|
+
ctx.log('info', branch === 'export'
|
|
252
|
+
? `Surplus ${Math.round(surplus)}W from grid export (exporting ${Math.round(gridExport)}W)`
|
|
253
|
+
: `Surplus ${Math.round(surplus)}W from ppv−load (${Math.round(ppv)}W − ${Math.round(loadPower)}W, not exporting)`);
|
|
254
|
+
return { surplus, branch, ppv, loadPower, gridExport };
|
|
255
|
+
});
|
|
256
|
+
// ==========================================================================
|
|
257
|
+
// EV · Trend Window
|
|
258
|
+
// ==========================================================================
|
|
259
|
+
registry.register({
|
|
260
|
+
type: 'EvTrendWindow',
|
|
261
|
+
label: 'EV · Trend Window',
|
|
262
|
+
description: 'Per-cycle surplus/SOC/ppv slopes across the rolling reading window + surplus stdDev and the dynamic cooldown it drives (stable <200 → 5min, volatile >800 → 15min, else 10min). Verbatim getTrend + getCooldownMs.',
|
|
263
|
+
simpleDescription: 'Tracks whether solar and battery levels are rising or falling',
|
|
264
|
+
category: 'pradar',
|
|
265
|
+
traceKind: 'compute',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
fields: [{ name: 'readings', type: 'array', required: true, description: 'Array of {surplus, homeSoc, ppv}.' }],
|
|
268
|
+
},
|
|
269
|
+
outputSchema: {
|
|
270
|
+
fields: [
|
|
271
|
+
{ name: 'surplusTrend', type: 'number' },
|
|
272
|
+
{ name: 'socTrend', type: 'number' },
|
|
273
|
+
{ name: 'ppvTrend', type: 'number' },
|
|
274
|
+
{ name: 'stdDev', type: 'number' },
|
|
275
|
+
{ name: 'cooldownMs', type: 'number' },
|
|
276
|
+
{ name: 'cooldownMin', type: 'number' },
|
|
277
|
+
{ name: 'readingCount', type: 'number' },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
}, async (ctx) => {
|
|
281
|
+
const readings = asReadings(ctx.input.readings);
|
|
282
|
+
const tw = computeTrendWindow(readings);
|
|
283
|
+
ctx.log('info', `Trend window over ${readings.length} reading(s): surplus ${tw.surplusTrend >= 0 ? '+' : ''}${Math.round(tw.surplusTrend)}W/cycle, ` +
|
|
284
|
+
`SOC ${tw.socTrend >= 0 ? '+' : ''}${tw.socTrend.toFixed(1)}%/cycle, stdDev ${Math.round(tw.stdDev)}W → cooldown ${Math.round(tw.cooldownMs / 60_000)}min`);
|
|
285
|
+
return { ...tw, cooldownMin: tw.cooldownMs / 60_000, readingCount: readings.length };
|
|
286
|
+
});
|
|
287
|
+
// ==========================================================================
|
|
288
|
+
// EV · Thresholds (branching on skip)
|
|
289
|
+
// ==========================================================================
|
|
290
|
+
registry.register({
|
|
291
|
+
type: 'EvThresholds',
|
|
292
|
+
label: 'EV · Thresholds',
|
|
293
|
+
description: 'Verbatim getThresholds: layered start/stop watt thresholds from home SOC, ppv, sun-hours-left, EV battery, forecast and trends — urgent/buffer (1200/400), flat (2000/800), predictive (≤500/200), golden hour (≤800/200), worth-it skip (MIN_SESSION_KWH 0.5 + the ≥65%/<1.5kWh gate), trend overrides (1000/600). Emits $branch skip|ok. PORT NOTE: trends are passed in (Trend Window) instead of read from module state.',
|
|
294
|
+
simpleDescription: 'Decides the power levels that should start or stop charging',
|
|
295
|
+
category: 'pradar',
|
|
296
|
+
traceKind: 'compute',
|
|
297
|
+
tags: ['branching'],
|
|
298
|
+
inputSchema: {
|
|
299
|
+
fields: [
|
|
300
|
+
{ name: 'homeSoc', type: 'number', required: true },
|
|
301
|
+
{ name: 'ppv', type: 'number', required: true },
|
|
302
|
+
{ name: 'sunHoursLeft', type: 'number', required: true },
|
|
303
|
+
{ name: 'evBattery', type: 'number' },
|
|
304
|
+
{ name: 'forecast', type: 'object' },
|
|
305
|
+
{ name: 'surplusTrend', type: 'number' },
|
|
306
|
+
{ name: 'socTrend', type: 'number' },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
outputSchema: {
|
|
310
|
+
fields: [
|
|
311
|
+
{ name: 'startW', type: 'number' },
|
|
312
|
+
{ name: 'stopW', type: 'number' },
|
|
313
|
+
{ name: 'skipReason', type: 'string' },
|
|
314
|
+
{ name: 'reasons', type: 'array' },
|
|
315
|
+
{ name: 'homeSoc', type: 'number' },
|
|
316
|
+
{ name: 'ppv', type: 'number' },
|
|
317
|
+
{ name: 'sunHoursLeft', type: 'number' },
|
|
318
|
+
{ name: 'evBattery', type: 'number' },
|
|
319
|
+
{ name: '$branch', type: 'string' },
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
}, async (ctx) => {
|
|
323
|
+
const homeSoc = num(ctx.input.homeSoc);
|
|
324
|
+
const ppv = num(ctx.input.ppv);
|
|
325
|
+
const sunHoursLeft = num(ctx.input.sunHoursLeft);
|
|
326
|
+
const evBattery = nullableNum(ctx.input.evBattery);
|
|
327
|
+
const forecast = asForecast(ctx.input.forecast);
|
|
328
|
+
const trends = {
|
|
329
|
+
surplusTrend: num(ctx.input.surplusTrend),
|
|
330
|
+
socTrend: num(ctx.input.socTrend),
|
|
331
|
+
};
|
|
332
|
+
const result = getThresholds(homeSoc, ppv, sunHoursLeft, evBattery, forecast, trends);
|
|
333
|
+
const branch = result.skipReason ? 'skip' : 'ok';
|
|
334
|
+
ctx.log('info', `Thresholds: start ${result.startW}W / stop ${result.stopW}W${result.skipReason ? ` — SKIP: ${result.skipReason}` : ''}` +
|
|
335
|
+
(result.reasons.length ? ` [${result.reasons.join('; ')}]` : ''));
|
|
336
|
+
return {
|
|
337
|
+
startW: result.startW,
|
|
338
|
+
stopW: result.stopW,
|
|
339
|
+
skipReason: result.skipReason,
|
|
340
|
+
reasons: result.reasons,
|
|
341
|
+
homeSoc,
|
|
342
|
+
ppv,
|
|
343
|
+
sunHoursLeft,
|
|
344
|
+
evBattery,
|
|
345
|
+
$branch: branch,
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
// ==========================================================================
|
|
349
|
+
// EV · Target Check (branching)
|
|
350
|
+
// ==========================================================================
|
|
351
|
+
registry.register({
|
|
352
|
+
type: 'EvTargetCheck',
|
|
353
|
+
label: 'EV · Target Check',
|
|
354
|
+
description: 'Verbatim plugged-in / at-target gate. Effective target is 90% during solar hours (gentle free AC), else 80%. Emits $branch notPlugged|atTarget|belowTarget.',
|
|
355
|
+
simpleDescription: 'Checks whether the car is plugged in and charged enough',
|
|
356
|
+
category: 'pradar',
|
|
357
|
+
traceKind: 'compute',
|
|
358
|
+
tags: ['branching'],
|
|
359
|
+
inputSchema: {
|
|
360
|
+
fields: [
|
|
361
|
+
{ name: 'pluggedIn', type: 'boolean', required: true },
|
|
362
|
+
{ name: 'evBattery', type: 'number' },
|
|
363
|
+
{ name: 'isSolarHours', type: 'boolean', required: true },
|
|
364
|
+
{ name: 'charging', type: 'boolean' },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
outputSchema: {
|
|
368
|
+
fields: [
|
|
369
|
+
{ name: 'branch', type: 'enum', enumValues: ['notPlugged', 'atTarget', 'belowTarget'] },
|
|
370
|
+
{ name: 'effectiveTarget', type: 'number' },
|
|
371
|
+
{ name: 'reason', type: 'string' },
|
|
372
|
+
{ name: 'evBattery', type: 'number' },
|
|
373
|
+
{ name: 'charging', type: 'boolean' },
|
|
374
|
+
{ name: '$branch', type: 'string' },
|
|
375
|
+
],
|
|
376
|
+
},
|
|
377
|
+
}, async (ctx) => {
|
|
378
|
+
const pluggedIn = bool(ctx.input.pluggedIn);
|
|
379
|
+
const evBattery = nullableNum(ctx.input.evBattery);
|
|
380
|
+
const isSolarHours = bool(ctx.input.isSolarHours);
|
|
381
|
+
const charging = bool(ctx.input.charging);
|
|
382
|
+
const result = checkTarget(pluggedIn, evBattery, isSolarHours);
|
|
383
|
+
ctx.log('info', `Target check → ${result.branch}: ${result.reason}`);
|
|
384
|
+
return {
|
|
385
|
+
branch: result.branch,
|
|
386
|
+
effectiveTarget: result.effectiveTarget,
|
|
387
|
+
reason: result.reason,
|
|
388
|
+
evBattery,
|
|
389
|
+
charging,
|
|
390
|
+
$branch: result.branch,
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
// ==========================================================================
|
|
394
|
+
// EV · Unscheduled Charge Guard (branching)
|
|
395
|
+
// ==========================================================================
|
|
396
|
+
registry.register({
|
|
397
|
+
type: 'EvUnscheduledGuard',
|
|
398
|
+
label: 'EV · Unscheduled Charge Guard',
|
|
399
|
+
description: 'Verbatim shouldStopUnscheduledCharge: stops a charge the scheduler did not start when start conditions are not met (skip reason, surplus below start, or home SOC below min) — but never interferes with a scheduler-owned charging/cooldown/night session. Emits $branch stop|continue. effectiveMinSoc defaults to 0 (production sets it to 0 — morning SOC near 0% is normal).',
|
|
400
|
+
simpleDescription: 'Stops an unscheduled charge when conditions no longer allow it',
|
|
401
|
+
category: 'pradar',
|
|
402
|
+
traceKind: 'compute',
|
|
403
|
+
tags: ['branching'],
|
|
404
|
+
inputSchema: {
|
|
405
|
+
fields: [
|
|
406
|
+
{ name: 'charging', type: 'boolean', required: true },
|
|
407
|
+
{ name: 'state', type: 'string', required: true },
|
|
408
|
+
{ name: 'surplus', type: 'number', required: true },
|
|
409
|
+
{ name: 'startW', type: 'number', required: true },
|
|
410
|
+
{ name: 'skipReason', type: 'string' },
|
|
411
|
+
{ name: 'homeSoc', type: 'number', required: true },
|
|
412
|
+
{ name: 'effectiveMinSoc', type: 'number' },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
outputSchema: {
|
|
416
|
+
fields: [
|
|
417
|
+
{ name: 'shouldStop', type: 'boolean' },
|
|
418
|
+
{ name: 'reason', type: 'string' },
|
|
419
|
+
{ name: 'state', type: 'string' },
|
|
420
|
+
{ name: '$branch', type: 'string' },
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
}, async (ctx) => {
|
|
424
|
+
const charging = bool(ctx.input.charging);
|
|
425
|
+
const state = String(ctx.input.state ?? 'idle');
|
|
426
|
+
const surplus = num(ctx.input.surplus);
|
|
427
|
+
const startW = num(ctx.input.startW);
|
|
428
|
+
const skipReason = ctx.input.skipReason ? String(ctx.input.skipReason) : null;
|
|
429
|
+
const homeSoc = num(ctx.input.homeSoc);
|
|
430
|
+
const effectiveMinSoc = ctx.input.effectiveMinSoc === undefined ? 0 : num(ctx.input.effectiveMinSoc);
|
|
431
|
+
const shouldStop = shouldStopUnscheduledCharge({ charging, state, surplus, startW, skipReason, homeSoc, effectiveMinSoc });
|
|
432
|
+
const reason = shouldStop
|
|
433
|
+
? skipReason || `unscheduled charge: surplus ${Math.round(surplus)}W < ${startW}W start threshold`
|
|
434
|
+
: 'no unscheduled charge to stop';
|
|
435
|
+
ctx.log('info', `Unscheduled guard → ${shouldStop ? 'STOP' : 'continue'}: ${reason}`);
|
|
436
|
+
return { shouldStop, reason, state, $branch: shouldStop ? 'stop' : 'continue' };
|
|
437
|
+
});
|
|
438
|
+
// ==========================================================================
|
|
439
|
+
// EV · Reconcile (branching)
|
|
440
|
+
// ==========================================================================
|
|
441
|
+
registry.register({
|
|
442
|
+
type: 'EvReconcile',
|
|
443
|
+
label: 'EV · Reconcile',
|
|
444
|
+
description: 'Verbatim reconcile: adopt a charge that began outside the scheduler when current rules allow it (car charging but state is idle/waiting) — otherwise hold. Emits $branch adopt|hold.',
|
|
445
|
+
simpleDescription: 'Decides whether to take over a charge that started on its own',
|
|
446
|
+
category: 'pradar',
|
|
447
|
+
traceKind: 'compute',
|
|
448
|
+
tags: ['branching'],
|
|
449
|
+
inputSchema: {
|
|
450
|
+
fields: [
|
|
451
|
+
{ name: 'charging', type: 'boolean', required: true },
|
|
452
|
+
{ name: 'state', type: 'string', required: true },
|
|
453
|
+
],
|
|
454
|
+
},
|
|
455
|
+
outputSchema: {
|
|
456
|
+
fields: [
|
|
457
|
+
{ name: 'decision', type: 'enum', enumValues: ['adopt', 'hold'] },
|
|
458
|
+
{ name: 'nextState', type: 'string' },
|
|
459
|
+
{ name: 'reason', type: 'string' },
|
|
460
|
+
{ name: 'state', type: 'string' },
|
|
461
|
+
{ name: '$branch', type: 'string' },
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
}, async (ctx) => {
|
|
465
|
+
const charging = bool(ctx.input.charging);
|
|
466
|
+
const state = String(ctx.input.state ?? 'idle');
|
|
467
|
+
const result = reconcile(charging, state);
|
|
468
|
+
ctx.log('info', `Reconcile → ${result.decision}: ${result.reason}`);
|
|
469
|
+
return { decision: result.decision, nextState: result.nextState, reason: result.reason, state, $branch: result.decision };
|
|
470
|
+
});
|
|
471
|
+
// ==========================================================================
|
|
472
|
+
// EV · Charge Decision (branching)
|
|
473
|
+
// ==========================================================================
|
|
474
|
+
registry.register({
|
|
475
|
+
type: 'EvChargeDecision',
|
|
476
|
+
label: 'EV · Charge Decision',
|
|
477
|
+
description: 'Verbatim evaluateCharge branch (idle/waiting/charging): start when surplus ≥ startW, stop when surplus < stopW, else hold. Emits $branch start|stop|hold_waiting|hold_charging. Note effectiveMinSoc/effectiveStopSoc are 0 in production so the SOC comparisons never trip.',
|
|
478
|
+
simpleDescription: 'Decides whether to start, stop, or keep charging right now',
|
|
479
|
+
category: 'pradar',
|
|
480
|
+
traceKind: 'compute',
|
|
481
|
+
tags: ['branching'],
|
|
482
|
+
inputSchema: {
|
|
483
|
+
fields: [
|
|
484
|
+
{ name: 'state', type: 'enum', enumValues: ['idle', 'waiting', 'charging'], required: true },
|
|
485
|
+
{ name: 'surplus', type: 'number', required: true },
|
|
486
|
+
{ name: 'startW', type: 'number', required: true },
|
|
487
|
+
{ name: 'stopW', type: 'number', required: true },
|
|
488
|
+
{ name: 'homeSoc', type: 'number', required: true },
|
|
489
|
+
{ name: 'skipReason', type: 'string' },
|
|
490
|
+
],
|
|
491
|
+
},
|
|
492
|
+
outputSchema: {
|
|
493
|
+
fields: [
|
|
494
|
+
{ name: 'action', type: 'enum', enumValues: ['start', 'stop', 'hold_waiting', 'hold_charging'] },
|
|
495
|
+
{ name: 'nextState', type: 'string' },
|
|
496
|
+
{ name: 'reason', type: 'string' },
|
|
497
|
+
{ name: 'surplus', type: 'number' },
|
|
498
|
+
{ name: 'startW', type: 'number' },
|
|
499
|
+
{ name: 'stopW', type: 'number' },
|
|
500
|
+
{ name: '$branch', type: 'string' },
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
}, async (ctx) => {
|
|
504
|
+
const state = String(ctx.input.state ?? 'idle');
|
|
505
|
+
const surplus = num(ctx.input.surplus);
|
|
506
|
+
const startW = num(ctx.input.startW);
|
|
507
|
+
const stopW = num(ctx.input.stopW);
|
|
508
|
+
const homeSoc = num(ctx.input.homeSoc);
|
|
509
|
+
const skipReason = ctx.input.skipReason ? String(ctx.input.skipReason) : null;
|
|
510
|
+
const result = decideCharge({ state, surplus, startW, stopW, homeSoc, skipReason });
|
|
511
|
+
ctx.log('info', `Charge decision (${state}) → ${result.action}: ${result.reason}`);
|
|
512
|
+
return {
|
|
513
|
+
action: result.action,
|
|
514
|
+
nextState: result.nextState,
|
|
515
|
+
reason: result.reason,
|
|
516
|
+
surplus,
|
|
517
|
+
startW,
|
|
518
|
+
stopW,
|
|
519
|
+
$branch: result.action,
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
// ==========================================================================
|
|
523
|
+
// EV · Night Charge Decision (branching)
|
|
524
|
+
// ==========================================================================
|
|
525
|
+
registry.register({
|
|
526
|
+
type: 'EvNightChargeDecision',
|
|
527
|
+
label: 'EV · Night Charge Decision',
|
|
528
|
+
description: 'Verbatim night-charge gate: only 23:00–05:00, only when plugged in and not already triggered today. Charges when EV < 35% (critical, any forecast) or EV < 65% with a poor tomorrow forecast (< 10kWh). Emits $branch charge|skip and sizes the session (neededKwh to 80%, hoursToCharge at 2.3kW).',
|
|
529
|
+
simpleDescription: 'Decides whether to top up the battery overnight',
|
|
530
|
+
category: 'pradar',
|
|
531
|
+
traceKind: 'compute',
|
|
532
|
+
tags: ['branching'],
|
|
533
|
+
inputSchema: {
|
|
534
|
+
fields: [
|
|
535
|
+
{ name: 'evBattery', type: 'number', required: true },
|
|
536
|
+
{ name: 'pluggedIn', type: 'boolean', required: true },
|
|
537
|
+
{ name: 'tomorrowKwh', type: 'number' },
|
|
538
|
+
{ name: 'hour', type: 'number', required: true },
|
|
539
|
+
{ name: 'alreadyTriggeredToday', type: 'boolean' },
|
|
540
|
+
],
|
|
541
|
+
},
|
|
542
|
+
outputSchema: {
|
|
543
|
+
fields: [
|
|
544
|
+
{ name: 'shouldCharge', type: 'boolean' },
|
|
545
|
+
{ name: 'factors', type: 'array' },
|
|
546
|
+
{ name: 'neededKwh', type: 'number' },
|
|
547
|
+
{ name: 'hoursToCharge', type: 'number' },
|
|
548
|
+
{ name: 'evBattery', type: 'number' },
|
|
549
|
+
{ name: 'gateReason', type: 'string' },
|
|
550
|
+
{ name: '$branch', type: 'string' },
|
|
551
|
+
],
|
|
552
|
+
},
|
|
553
|
+
}, async (ctx) => {
|
|
554
|
+
const evBattery = num(ctx.input.evBattery);
|
|
555
|
+
const pluggedIn = bool(ctx.input.pluggedIn);
|
|
556
|
+
const tomorrowKwh = nullableNum(ctx.input.tomorrowKwh);
|
|
557
|
+
const hour = num(ctx.input.hour);
|
|
558
|
+
const alreadyTriggeredToday = bool(ctx.input.alreadyTriggeredToday);
|
|
559
|
+
// Verbatim guards, in order: once/day, night window only, must be plugged in.
|
|
560
|
+
const inNightWindow = hour >= 23 || hour < 5;
|
|
561
|
+
let gateReason = null;
|
|
562
|
+
if (alreadyTriggeredToday)
|
|
563
|
+
gateReason = 'already triggered today';
|
|
564
|
+
else if (!inNightWindow)
|
|
565
|
+
gateReason = `hour ${hour} outside 23:00–05:00 night window`;
|
|
566
|
+
else if (!pluggedIn)
|
|
567
|
+
gateReason = 'not plugged in';
|
|
568
|
+
const { neededKwh, hoursToCharge } = nightChargeEstimate(evBattery);
|
|
569
|
+
if (gateReason) {
|
|
570
|
+
ctx.log('info', `Night charge → skip: ${gateReason}`);
|
|
571
|
+
return { shouldCharge: false, factors: [], neededKwh, hoursToCharge, evBattery, gateReason, $branch: 'skip' };
|
|
572
|
+
}
|
|
573
|
+
const decision = getNightChargeDecision(evBattery, tomorrowKwh);
|
|
574
|
+
const branch = decision.shouldCharge ? 'charge' : 'skip';
|
|
575
|
+
ctx.log('info', decision.shouldCharge
|
|
576
|
+
? `Night charge → CHARGE: ${decision.factors.join('; ')} (needs ${neededKwh.toFixed(1)}kWh, ~${hoursToCharge.toFixed(1)}h to ${EV_TARGET_BATTERY}%)`
|
|
577
|
+
: `Night charge → skip: EV ${evBattery}%${tomorrowKwh !== null ? `, tomorrow ${tomorrowKwh.toFixed(1)}kWh` : ''} — solar should handle it`);
|
|
578
|
+
return {
|
|
579
|
+
shouldCharge: decision.shouldCharge,
|
|
580
|
+
factors: decision.factors,
|
|
581
|
+
neededKwh,
|
|
582
|
+
hoursToCharge,
|
|
583
|
+
evBattery,
|
|
584
|
+
gateReason: null,
|
|
585
|
+
$branch: branch,
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
// ==========================================================================
|
|
589
|
+
// EV · Session Estimate
|
|
590
|
+
// ==========================================================================
|
|
591
|
+
registry.register({
|
|
592
|
+
type: 'EvSessionEstimate',
|
|
593
|
+
label: 'EV · Session Estimate',
|
|
594
|
+
description: 'Verbatim estimateSessionKwh + evKwhNeeded + the worth-it gates: deliverable kWh from remaining solar vs kWh needed to target, and whether a session clears MIN_SESSION_KWH (0.5) / the ≥65%-and-<1.5kWh guard.',
|
|
595
|
+
simpleDescription: 'Estimates whether there’s enough sun left to make charging worthwhile',
|
|
596
|
+
category: 'pradar',
|
|
597
|
+
traceKind: 'compute',
|
|
598
|
+
inputSchema: {
|
|
599
|
+
fields: [
|
|
600
|
+
{ name: 'evBattery', type: 'number', required: true },
|
|
601
|
+
{ name: 'target', type: 'number' },
|
|
602
|
+
{ name: 'ppv', type: 'number' },
|
|
603
|
+
{ name: 'surplus', type: 'number' },
|
|
604
|
+
{ name: 'sunHoursLeft', type: 'number', required: true },
|
|
605
|
+
{ name: 'forecast', type: 'object' },
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
outputSchema: {
|
|
609
|
+
fields: [
|
|
610
|
+
{ name: 'estimatedKwh', type: 'number' },
|
|
611
|
+
{ name: 'neededKwh', type: 'number' },
|
|
612
|
+
{ name: 'worthIt', type: 'boolean' },
|
|
613
|
+
{ name: 'reason', type: 'string' },
|
|
614
|
+
{ name: 'evBattery', type: 'number' },
|
|
615
|
+
{ name: 'target', type: 'number' },
|
|
616
|
+
],
|
|
617
|
+
},
|
|
618
|
+
}, async (ctx) => {
|
|
619
|
+
const evBattery = num(ctx.input.evBattery);
|
|
620
|
+
const target = ctx.input.target !== undefined ? num(ctx.input.target) : EV_TARGET_BATTERY;
|
|
621
|
+
const power = ctx.input.ppv !== undefined ? num(ctx.input.ppv) : num(ctx.input.surplus);
|
|
622
|
+
const sunHoursLeft = num(ctx.input.sunHoursLeft);
|
|
623
|
+
const forecast = asForecast(ctx.input.forecast);
|
|
624
|
+
const estimatedKwh = estimateSessionKwh(power, sunHoursLeft, forecast);
|
|
625
|
+
const neededKwh = evKwhNeeded(evBattery, target);
|
|
626
|
+
// Mirror getThresholds' worth-it gates.
|
|
627
|
+
let worthIt = true;
|
|
628
|
+
let reason = `~${estimatedKwh.toFixed(1)}kWh deliverable vs ${neededKwh.toFixed(1)}kWh needed to ${target}%`;
|
|
629
|
+
if (estimatedKwh < MIN_SESSION_KWH && neededKwh > 2) {
|
|
630
|
+
worthIt = false;
|
|
631
|
+
reason = `not worth it: ~${estimatedKwh.toFixed(1)}kWh available vs ${neededKwh.toFixed(0)}kWh needed`;
|
|
632
|
+
}
|
|
633
|
+
if (evBattery >= 65 && estimatedKwh < 1.5) {
|
|
634
|
+
worthIt = false;
|
|
635
|
+
reason = `EV at ${evBattery}%, only ~${estimatedKwh.toFixed(1)}kWh solar remaining — not worth a session`;
|
|
636
|
+
}
|
|
637
|
+
ctx.log('info', `Session estimate: ${reason} → ${worthIt ? 'worth it' : 'skip'}`);
|
|
638
|
+
return { estimatedKwh, neededKwh, worthIt, reason, evBattery, target };
|
|
639
|
+
});
|
|
640
|
+
// ==========================================================================
|
|
641
|
+
// EV · Start Charge (mutation, simulated)
|
|
642
|
+
// ==========================================================================
|
|
643
|
+
registry.register({
|
|
644
|
+
type: 'EvStartCharge',
|
|
645
|
+
label: 'EV · Start Charge',
|
|
646
|
+
description: 'SIMULATED start-charge command. Commit (config.commit true, not safe mode) returns {sent:true, command:"start"}; otherwise dry-run returns {sent:false, wouldSend:"start"}. PORT NOTE: production calls the Kia Connect API (startCharge()).',
|
|
647
|
+
simpleDescription: 'Tells the car to start charging',
|
|
648
|
+
category: 'pradar',
|
|
649
|
+
traceKind: 'http',
|
|
650
|
+
traceDetail: 'Kia Connect startCharge() — SIMULATED (no HTTP request emitted; production calls the Kia Connect API)',
|
|
651
|
+
effects: 'mutation',
|
|
652
|
+
inputSchema: {
|
|
653
|
+
fields: [{ name: 'reason', type: 'string' }],
|
|
654
|
+
},
|
|
655
|
+
configSchema: {
|
|
656
|
+
fields: [{ name: 'commit', type: 'boolean' }],
|
|
657
|
+
},
|
|
658
|
+
outputSchema: {
|
|
659
|
+
fields: [
|
|
660
|
+
{ name: 'sent', type: 'boolean' },
|
|
661
|
+
{ name: 'command', type: 'string' },
|
|
662
|
+
{ name: 'wouldSend', type: 'string' },
|
|
663
|
+
{ name: 'reason', type: 'string' },
|
|
664
|
+
],
|
|
665
|
+
},
|
|
666
|
+
}, async (ctx) => {
|
|
667
|
+
const reason = ctx.input.reason ? String(ctx.input.reason) : 'start charge';
|
|
668
|
+
const commit = ctx.config.commit === true && !ctx.safeMode;
|
|
669
|
+
if (ctx.config.commit === true && ctx.safeMode) {
|
|
670
|
+
ctx.log('warn', 'EvStartCharge: commit requested — safe mode — command suppressed');
|
|
671
|
+
}
|
|
672
|
+
if (commit) {
|
|
673
|
+
ctx.log('info', `[SIMULATED] Kia startCharge() sent — ${reason}`);
|
|
674
|
+
return { sent: true, command: 'start', reason };
|
|
675
|
+
}
|
|
676
|
+
ctx.log('info', `Dry-run: would send Kia startCharge() — ${reason}`);
|
|
677
|
+
return { sent: false, wouldSend: 'start', reason };
|
|
678
|
+
});
|
|
679
|
+
// ==========================================================================
|
|
680
|
+
// EV · Stop Charge (mutation, simulated)
|
|
681
|
+
// ==========================================================================
|
|
682
|
+
registry.register({
|
|
683
|
+
type: 'EvStopCharge',
|
|
684
|
+
label: 'EV · Stop Charge',
|
|
685
|
+
description: 'SIMULATED stop-charge command. Commit (config.commit true, not safe mode) returns {sent:true, command:"stop"}; otherwise dry-run returns {sent:false, wouldSend:"stop"}. PORT NOTE: production calls the Kia Connect API (stopCharge()).',
|
|
686
|
+
simpleDescription: 'Tells the car to stop charging',
|
|
687
|
+
category: 'pradar',
|
|
688
|
+
traceKind: 'http',
|
|
689
|
+
traceDetail: 'Kia Connect stopCharge() — SIMULATED (no HTTP request emitted; production calls the Kia Connect API)',
|
|
690
|
+
effects: 'mutation',
|
|
691
|
+
inputSchema: {
|
|
692
|
+
fields: [{ name: 'reason', type: 'string' }],
|
|
693
|
+
},
|
|
694
|
+
configSchema: {
|
|
695
|
+
fields: [{ name: 'commit', type: 'boolean' }],
|
|
696
|
+
},
|
|
697
|
+
outputSchema: {
|
|
698
|
+
fields: [
|
|
699
|
+
{ name: 'sent', type: 'boolean' },
|
|
700
|
+
{ name: 'command', type: 'string' },
|
|
701
|
+
{ name: 'wouldSend', type: 'string' },
|
|
702
|
+
{ name: 'reason', type: 'string' },
|
|
703
|
+
],
|
|
704
|
+
},
|
|
705
|
+
}, async (ctx) => {
|
|
706
|
+
const reason = ctx.input.reason ? String(ctx.input.reason) : 'stop charge';
|
|
707
|
+
const commit = ctx.config.commit === true && !ctx.safeMode;
|
|
708
|
+
if (ctx.config.commit === true && ctx.safeMode) {
|
|
709
|
+
ctx.log('warn', 'EvStopCharge: commit requested — safe mode — command suppressed');
|
|
710
|
+
}
|
|
711
|
+
if (commit) {
|
|
712
|
+
ctx.log('info', `[SIMULATED] Kia stopCharge() sent — ${reason}`);
|
|
713
|
+
return { sent: true, command: 'stop', reason };
|
|
714
|
+
}
|
|
715
|
+
ctx.log('info', `Dry-run: would send Kia stopCharge() — ${reason}`);
|
|
716
|
+
return { sent: false, wouldSend: 'stop', reason };
|
|
717
|
+
});
|
|
718
|
+
// ==========================================================================
|
|
719
|
+
// EV · Notify (mutation, simulated)
|
|
720
|
+
// ==========================================================================
|
|
721
|
+
registry.register({
|
|
722
|
+
type: 'EvNotify',
|
|
723
|
+
label: 'EV · Notify',
|
|
724
|
+
description: 'SIMULATED ntfy push. Commit (config.commit true, not safe mode) returns {sent:true}; otherwise dry-run returns {sent:false, wouldSend:{title,message,priority}}. PORT NOTE: production POSTs to ntfy.sh/$NTFY_TOPIC.',
|
|
725
|
+
simpleDescription: 'Sends a push notification',
|
|
726
|
+
category: 'pradar',
|
|
727
|
+
traceKind: 'http',
|
|
728
|
+
traceDetail: 'POST https://ntfy.sh/{NTFY_TOPIC} — SIMULATED (no HTTP request emitted; production POSTs to ntfy.sh)',
|
|
729
|
+
effects: 'mutation',
|
|
730
|
+
inputSchema: {
|
|
731
|
+
fields: [
|
|
732
|
+
{ name: 'title', type: 'string', required: true },
|
|
733
|
+
{ name: 'message', type: 'string' },
|
|
734
|
+
{ name: 'priority', type: 'number' },
|
|
735
|
+
],
|
|
736
|
+
},
|
|
737
|
+
configSchema: {
|
|
738
|
+
fields: [{ name: 'commit', type: 'boolean' }],
|
|
739
|
+
},
|
|
740
|
+
outputSchema: {
|
|
741
|
+
fields: [
|
|
742
|
+
{ name: 'sent', type: 'boolean' },
|
|
743
|
+
{ name: 'wouldSend', type: 'object' },
|
|
744
|
+
{ name: 'title', type: 'string' },
|
|
745
|
+
],
|
|
746
|
+
},
|
|
747
|
+
}, async (ctx) => {
|
|
748
|
+
const title = String(ctx.input.title ?? 'EV');
|
|
749
|
+
const message = ctx.input.message ? String(ctx.input.message) : '';
|
|
750
|
+
const priority = ctx.input.priority !== undefined ? num(ctx.input.priority, 3) : 3;
|
|
751
|
+
const commit = ctx.config.commit === true && !ctx.safeMode;
|
|
752
|
+
if (ctx.config.commit === true && ctx.safeMode) {
|
|
753
|
+
ctx.log('warn', 'EvNotify: commit requested — safe mode — push suppressed');
|
|
754
|
+
}
|
|
755
|
+
const payload = { title, message, priority };
|
|
756
|
+
if (commit) {
|
|
757
|
+
ctx.log('info', `[SIMULATED] ntfy push — ${title}: ${message}`);
|
|
758
|
+
return { sent: true, title };
|
|
759
|
+
}
|
|
760
|
+
ctx.log('info', `Dry-run: would push ntfy — ${title} (priority ${priority})`);
|
|
761
|
+
return { sent: false, wouldSend: payload, title };
|
|
762
|
+
});
|
|
763
|
+
// ==========================================================================
|
|
764
|
+
// EV · Morning Summary
|
|
765
|
+
// ==========================================================================
|
|
766
|
+
registry.register({
|
|
767
|
+
type: 'EvMorningSummary',
|
|
768
|
+
label: 'EV · Morning Summary',
|
|
769
|
+
description: 'Verbatim morning-summary outlook lines: Good when remaining solar > EV-need×1.5, Tight when > need, else Unlikely; EV at/above the 90% solar target reports "at target".',
|
|
770
|
+
simpleDescription: 'Writes a plain-English outlook for today’s charging',
|
|
771
|
+
category: 'pradar',
|
|
772
|
+
traceKind: 'compute',
|
|
773
|
+
inputSchema: {
|
|
774
|
+
fields: [
|
|
775
|
+
{ name: 'homeSoc', type: 'number' },
|
|
776
|
+
{ name: 'evBattery', type: 'number' },
|
|
777
|
+
{ name: 'pluggedIn', type: 'boolean' },
|
|
778
|
+
{ name: 'todayKwh', type: 'number' },
|
|
779
|
+
{ name: 'remainingKwh', type: 'number' },
|
|
780
|
+
{ name: 'strongHoursLeft', type: 'number' },
|
|
781
|
+
],
|
|
782
|
+
},
|
|
783
|
+
outputSchema: {
|
|
784
|
+
fields: [
|
|
785
|
+
{ name: 'lines', type: 'array' },
|
|
786
|
+
{ name: 'summary', type: 'string' },
|
|
787
|
+
{ name: 'outlook', type: 'enum', enumValues: ['good', 'tight', 'unlikely', 'at-target', 'not-applicable'] },
|
|
788
|
+
],
|
|
789
|
+
},
|
|
790
|
+
}, async (ctx) => {
|
|
791
|
+
const homeSoc = nullableNum(ctx.input.homeSoc);
|
|
792
|
+
const evBattery = nullableNum(ctx.input.evBattery);
|
|
793
|
+
const pluggedIn = bool(ctx.input.pluggedIn);
|
|
794
|
+
const todayKwh = nullableNum(ctx.input.todayKwh);
|
|
795
|
+
const remainingKwh = nullableNum(ctx.input.remainingKwh);
|
|
796
|
+
const strongHoursLeft = nullableNum(ctx.input.strongHoursLeft);
|
|
797
|
+
const summary = morningSummary(homeSoc, evBattery, pluggedIn, todayKwh, remainingKwh, strongHoursLeft);
|
|
798
|
+
ctx.log('info', `Morning summary (${summary.outlook}):\n${summary.lines.join('\n')}`);
|
|
799
|
+
return { lines: summary.lines, summary: summary.lines.join('\n'), outlook: summary.outlook };
|
|
800
|
+
});
|
|
801
|
+
}
|