API: generate /api/state documentation from UI types (#32431)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
c7c744f54e
commit
c3f3384bfc
44 changed files with 8757 additions and 3347 deletions
3
.github/workflows/default.yml
vendored
3
.github/workflows/default.yml
vendored
|
|
@ -176,6 +176,9 @@ jobs:
|
||||||
- name: Install
|
- name: Install
|
||||||
run: make install-ui
|
run: make install-ui
|
||||||
|
|
||||||
|
- name: OpenAPI
|
||||||
|
run: make openapi
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: make lint-ui
|
run: make lint-ui
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/documentation.yml
vendored
2
.github/workflows/documentation.yml
vendored
|
|
@ -58,7 +58,7 @@ jobs:
|
||||||
- name: Prepare OpenAPI spec
|
- name: Prepare OpenAPI spec
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ./openapi-deploy
|
mkdir -p ./openapi-deploy
|
||||||
cp ./server/openapi.yaml ./openapi-deploy/openapi.yaml
|
npx --yes @redocly/cli@2 bundle ./server/openapi.yaml -o ./openapi-deploy/openapi.yaml
|
||||||
|
|
||||||
- name: Deploy OpenAPI spec to docs repo
|
- name: Deploy OpenAPI spec to docs repo
|
||||||
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
|
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4
|
||||||
|
|
|
||||||
3
.github/workflows/openapi-validate.yml
vendored
3
.github/workflows/openapi-validate.yml
vendored
|
|
@ -8,6 +8,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "server/openapi.yaml"
|
- "server/openapi.yaml"
|
||||||
|
- "server/openapi.state.yaml"
|
||||||
- ".github/workflows/openapi-validate.yml"
|
- ".github/workflows/openapi-validate.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -22,4 +23,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version: stable
|
go-version: stable
|
||||||
- name: Validate OpenAPI spec
|
- name: Validate OpenAPI spec
|
||||||
run: go run github.com/getkin/kin-openapi/cmd/validate@v0.133.0 -- server/openapi.yaml
|
run: go run github.com/getkin/kin-openapi/cmd/validate@v0.133.0 -ext -- server/openapi.yaml
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,6 +18,7 @@ testdata/**/*
|
||||||
!assets/js/**/*.yaml
|
!assets/js/**/*.yaml
|
||||||
!package*.json
|
!package*.json
|
||||||
!evcc.dist.yaml
|
!evcc.dist.yaml
|
||||||
|
!server/openapi.state.yaml
|
||||||
!.coderabbit.yaml
|
!.coderabbit.yaml
|
||||||
!tests/**/*.evcc.yaml
|
!tests/**/*.evcc.yaml
|
||||||
!tests/**/*.tpl.yaml
|
!tests/**/*.tpl.yaml
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ RUN --mount=type=cache,target=${GOMODCACHE} go mod download
|
||||||
# install tools
|
# install tools
|
||||||
COPY Makefile .
|
COPY Makefile .
|
||||||
COPY cmd/implement/ cmd/implement/
|
COPY cmd/implement/ cmd/implement/
|
||||||
COPY cmd/openapi/ cmd/openapi/
|
|
||||||
COPY api/ api/
|
COPY api/ api/
|
||||||
RUN --mount=type=cache,target=${GOMODCACHE} make install
|
RUN --mount=type=cache,target=${GOMODCACHE} make install
|
||||||
|
|
||||||
|
|
|
||||||
5
Makefile
5
Makefile
|
|
@ -31,7 +31,7 @@ CURRDIR := $(shell pwd)
|
||||||
|
|
||||||
default:: ui build
|
default:: ui build
|
||||||
|
|
||||||
all:: clean install install-ui ui assets lint test-ui lint-ui test build
|
all:: clean install install-ui ui assets openapi lint test-ui lint-ui test build
|
||||||
|
|
||||||
clean::
|
clean::
|
||||||
rm -rf dist/
|
rm -rf dist/
|
||||||
|
|
@ -48,6 +48,9 @@ ui::
|
||||||
assets::
|
assets::
|
||||||
go generate ./...
|
go generate ./...
|
||||||
|
|
||||||
|
openapi::
|
||||||
|
vp run openapi
|
||||||
|
|
||||||
docs::
|
docs::
|
||||||
go generate github.com/evcc-io/evcc/util/templates/...
|
go generate github.com/evcc-io/evcc/util/templates/...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,7 @@ import formatter from "@/mixins/formatter";
|
||||||
import minuteTicker from "@/mixins/minuteTicker";
|
import minuteTicker from "@/mixins/minuteTicker";
|
||||||
import { optionStep, fmtEnergy } from "@/utils/energyOptions.ts";
|
import { optionStep, fmtEnergy } from "@/utils/energyOptions.ts";
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import type { CURRENCY, Vehicle } from "@/types/evcc";
|
import type { CURRENCY, Forecast, PlanStrategy, Vehicle } from "@/types/evcc";
|
||||||
import type { PlanStrategy } from "./types";
|
|
||||||
import type { Forecast } from "@/types/evcc.ts";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ChargingPlan",
|
name: "ChargingPlan",
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,17 @@ import GenericModal from "../Helper/GenericModal.vue";
|
||||||
import PlansSettings from "./PlansSettings.vue";
|
import PlansSettings from "./PlansSettings.vue";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import type {
|
import type {
|
||||||
|
CURRENCY,
|
||||||
|
Forecast,
|
||||||
PlanStrategy,
|
PlanStrategy,
|
||||||
RepeatingPlan,
|
RepeatingPlan,
|
||||||
|
SMART_COST_TYPE,
|
||||||
StaticEnergyPlan,
|
StaticEnergyPlan,
|
||||||
StaticPlan,
|
StaticPlan,
|
||||||
StaticSocPlan,
|
StaticSocPlan,
|
||||||
} from "./types";
|
UiLoadpoint,
|
||||||
import type { CURRENCY, Forecast, SMART_COST_TYPE, UiLoadpoint, Vehicle } from "@/types/evcc";
|
Vehicle,
|
||||||
|
} from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ChargingPlanModal",
|
name: "ChargingPlanModal",
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import type { PlanStrategy } from "./types";
|
import type { PlanStrategy } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ChargingPlanStrategy",
|
name: "ChargingPlanStrategy",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import PlanRepeatingSettings from "./PlanRepeatingSettings.vue";
|
||||||
import deepEqual from "@/utils/deepEqual";
|
import deepEqual from "@/utils/deepEqual";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import type { RepeatingPlan } from "./types";
|
import type { RepeatingPlan } from "@/types/evcc";
|
||||||
|
|
||||||
const DEFAULT_WEEKDAYS = [1, 2, 3, 4, 5];
|
const DEFAULT_WEEKDAYS = [1, 2, 3, 4, 5];
|
||||||
const DEFAULT_TARGET_TIME = "07:00";
|
const DEFAULT_TARGET_TIME = "07:00";
|
||||||
|
|
|
||||||
|
|
@ -81,16 +81,18 @@ import api from "@/api";
|
||||||
import deepEqual from "@/utils/deepEqual";
|
import deepEqual from "@/utils/deepEqual";
|
||||||
import { debounceLeading } from "@/utils/debounceLeading";
|
import { debounceLeading } from "@/utils/debounceLeading";
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import type { Vehicle, CURRENCY, Forecast } from "@/types/evcc";
|
|
||||||
import type {
|
import type {
|
||||||
StaticPlan,
|
CURRENCY,
|
||||||
RepeatingPlan,
|
Forecast,
|
||||||
PlanWrapper,
|
|
||||||
StaticSocPlan,
|
|
||||||
StaticEnergyPlan,
|
|
||||||
PlanResponse,
|
PlanResponse,
|
||||||
PlanStrategy,
|
PlanStrategy,
|
||||||
} from "./types";
|
PlanWrapper,
|
||||||
|
RepeatingPlan,
|
||||||
|
StaticEnergyPlan,
|
||||||
|
StaticPlan,
|
||||||
|
StaticSocPlan,
|
||||||
|
Vehicle,
|
||||||
|
} from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ChargingPlansSettings",
|
name: "ChargingPlansSettings",
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import type { PlanWrapper } from "./types";
|
import type { PlanWrapper, Tariff } from "@/types/evcc";
|
||||||
import type { Tariff } from "@/types/evcc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ChargingPlanWarnings",
|
name: "ChargingPlanWarnings",
|
||||||
|
|
|
||||||
39
assets/js/components/ChargingPlans/types.d.ts
vendored
39
assets/js/components/ChargingPlans/types.d.ts
vendored
|
|
@ -1,39 +0,0 @@
|
||||||
import type { Rate } from "@/types/evcc";
|
|
||||||
|
|
||||||
export interface RepeatingPlan {
|
|
||||||
weekdays: number[];
|
|
||||||
time: string;
|
|
||||||
tz: string; // timezone like "Europe/Berlin"
|
|
||||||
soc: number;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlanWrapper {
|
|
||||||
planId: number;
|
|
||||||
planTime: Date;
|
|
||||||
duration: number;
|
|
||||||
plan: Rate[] | null;
|
|
||||||
power: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlanResponse {
|
|
||||||
status: number;
|
|
||||||
data: PlanWrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StaticPlan = StaticSocPlan | StaticEnergyPlan;
|
|
||||||
|
|
||||||
export interface StaticSocPlan {
|
|
||||||
soc: number;
|
|
||||||
time: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StaticEnergyPlan {
|
|
||||||
energy: number;
|
|
||||||
time: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlanStrategy {
|
|
||||||
continuous: boolean;
|
|
||||||
precondition: number;
|
|
||||||
}
|
|
||||||
|
|
@ -42,9 +42,8 @@ import "chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm";
|
||||||
import { registerChartComponents, commonOptions } from "../Sessions/chartConfig";
|
import { registerChartComponents, commonOptions } from "../Sessions/chartConfig";
|
||||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||||
import colors, { lighterColor } from "@/colors";
|
import colors, { lighterColor } from "@/colors";
|
||||||
import type { CURRENCY } from "@/types/evcc";
|
import type { CURRENCY, ForecastSlot, SolarDetails, TimeseriesEntry } from "@/types/evcc";
|
||||||
import { ForecastType, highestSlotIndexByDay } from "@/utils/forecast";
|
import { ForecastType, highestSlotIndexByDay } from "@/utils/forecast";
|
||||||
import type { ForecastSlot, SolarDetails, TimeseriesEntry } from "./types";
|
|
||||||
|
|
||||||
registerChartComponents([
|
registerChartComponents([
|
||||||
BarController,
|
BarController,
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import type { CURRENCY } from "@/types/evcc";
|
import type { CURRENCY, ForecastSlot } from "@/types/evcc";
|
||||||
import type { ForecastSlot } from "./types";
|
|
||||||
import { isStaticTariff } from "@/utils/forecast";
|
import { isStaticTariff } from "@/utils/forecast";
|
||||||
|
|
||||||
const MAX_HOURS = 96;
|
const MAX_HOURS = 96;
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ import {
|
||||||
import colors, { lighterColor } from "@/colors";
|
import colors, { lighterColor } from "@/colors";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import chartMixin from "./chartMixin";
|
import chartMixin from "./chartMixin";
|
||||||
import type { CURRENCY } from "@/types/evcc";
|
import type { CURRENCY, ForecastSlot } from "@/types/evcc";
|
||||||
import type { ForecastSlot } from "./types";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "PriceChart",
|
name: "PriceChart",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import colors, { lighterColor } from "@/colors";
|
||||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||||
import chartMixin from "./chartMixin";
|
import chartMixin from "./chartMixin";
|
||||||
import { highestSlotIndexByDay } from "@/utils/forecast";
|
import { highestSlotIndexByDay } from "@/utils/forecast";
|
||||||
import type { SolarDetails, TimeseriesEntry } from "./types";
|
import type { SolarDetails, TimeseriesEntry } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "SolarChart",
|
name: "SolarChart",
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
import formatter, { POWER_UNIT } from "@/mixins/formatter";
|
||||||
import type { SolarDetails } from "./types";
|
import type { SolarDetails } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "SolarDetails",
|
name: "SolarDetails",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import {
|
||||||
import colors from "@/colors";
|
import colors from "@/colors";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import chartMixin from "./chartMixin";
|
import chartMixin from "./chartMixin";
|
||||||
import type { ForecastSlot } from "./types";
|
import type { ForecastSlot } from "@/types/evcc";
|
||||||
|
|
||||||
export type ValueChartType = "co2" | "temperature";
|
export type ValueChartType = "co2" | "temperature";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import type { ForecastSlot } from "./types";
|
import type { ForecastSlot } from "@/types/evcc";
|
||||||
import type { ValueChartType } from "./ValueChart.vue";
|
import type { ValueChartType } from "./ValueChart.vue";
|
||||||
|
|
||||||
const MAX_HOURS = 96;
|
const MAX_HOURS = 96;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as echarts from "echarts/core";
|
import * as echarts from "echarts/core";
|
||||||
import colors from "@/colors";
|
import colors from "@/colors";
|
||||||
import escapeHtml from "@/utils/escapeHtml";
|
import escapeHtml from "@/utils/escapeHtml";
|
||||||
import type { ForecastSlot } from "./types";
|
import type { ForecastSlot } from "@/types/evcc";
|
||||||
import { BarChart, LineChart } from "echarts/charts";
|
import { BarChart, LineChart } from "echarts/charts";
|
||||||
import {
|
import {
|
||||||
GridComponent,
|
GridComponent,
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
export function isForecastSlot(obj?: TimeseriesEntry | ForecastSlot): obj is ForecastSlot {
|
|
||||||
return (obj as ForecastSlot).start !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeseriesEntry {
|
|
||||||
val: number;
|
|
||||||
ts: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ForecastSlot {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnergyByDay {
|
|
||||||
energy: number;
|
|
||||||
complete: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SolarDetails {
|
|
||||||
scale?: number;
|
|
||||||
today?: EnergyByDay;
|
|
||||||
tomorrow?: EnergyByDay;
|
|
||||||
dayAfterTomorrow?: EnergyByDay;
|
|
||||||
timeseries?: TimeseriesEntry[];
|
|
||||||
}
|
|
||||||
|
|
@ -129,9 +129,9 @@ import type {
|
||||||
SMART_COST_TYPE,
|
SMART_COST_TYPE,
|
||||||
BATTERY_MODE,
|
BATTERY_MODE,
|
||||||
LoadpointUi,
|
LoadpointUi,
|
||||||
|
PlanStrategy,
|
||||||
LoadpointSuggestion,
|
LoadpointSuggestion,
|
||||||
} from "@/types/evcc";
|
} from "@/types/evcc";
|
||||||
import type { PlanStrategy } from "@/components/ChargingPlans/types";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Loadpoint",
|
name: "Loadpoint",
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,7 @@ import { defineComponent, type PropType } from "vue";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { setLoadpointLastSmartCostLimit } from "@/uiLoadpoints";
|
import { setLoadpointLastSmartCostLimit } from "@/uiLoadpoints";
|
||||||
import settings from "@/settings";
|
import settings from "@/settings";
|
||||||
import { type CURRENCY, SMART_COST_TYPE } from "@/types/evcc";
|
import { type CURRENCY, type ForecastSlot, SMART_COST_TYPE } from "@/types/evcc";
|
||||||
import { type ForecastSlot } from "../Forecast/types";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "SmartCostLimit",
|
name: "SmartCostLimit",
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
vehicleOptions(): SelectOption<string>[] {
|
vehicleOptions(): SelectOption<string>[] {
|
||||||
return this.vehicles.map((v) => ({
|
return this.vehicles.map((v) => ({
|
||||||
name: v.name,
|
name: v.name ?? "",
|
||||||
value: v.title,
|
value: v.title,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,9 @@ import {
|
||||||
type VehicleStatus,
|
type VehicleStatus,
|
||||||
type Vehicle,
|
type Vehicle,
|
||||||
type LoadpointUi,
|
type LoadpointUi,
|
||||||
|
type PlanStrategy,
|
||||||
type LoadpointSuggestion,
|
type LoadpointSuggestion,
|
||||||
} from "@/types/evcc";
|
} from "@/types/evcc";
|
||||||
import type { PlanStrategy } from "@/components/ChargingPlans/types";
|
|
||||||
import BatteryBoostButton from "../Loadpoints/BatteryBoostButton.vue";
|
import BatteryBoostButton from "../Loadpoints/BatteryBoostButton.vue";
|
||||||
import type ChargingPlanModal from "../ChargingPlans/ChargingPlanModal.vue";
|
import type ChargingPlanModal from "../ChargingPlans/ChargingPlanModal.vue";
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ForecastSlot, TimeseriesEntry, SolarDetails } from "../components/Forecast/types";
|
import type { ForecastSlot, TimeseriesEntry, SolarDetails } from "@/types/evcc";
|
||||||
import deepCopy from "./deepClone";
|
import deepCopy from "./deepClone";
|
||||||
|
|
||||||
export enum ForecastType {
|
export enum ForecastType {
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/getkin/kin-openapi/openapi3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
doc, err := openapi3.NewLoader().LoadFromFile(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("failed to load OpenAPI spec:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// omit servers
|
|
||||||
doc.Servers = nil
|
|
||||||
|
|
||||||
b, err := json.MarshalIndent(doc, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(os.Args[2], b, 0o644); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -258,7 +258,6 @@ require (
|
||||||
tool (
|
tool (
|
||||||
github.com/dmarkham/enumer
|
github.com/dmarkham/enumer
|
||||||
github.com/evcc-io/evcc/cmd/implement
|
github.com/evcc-io/evcc/cmd/implement
|
||||||
github.com/evcc-io/evcc/cmd/openapi
|
|
||||||
github.com/evcc-io/openapi-mcp/cmd/openapi-mcp
|
github.com/evcc-io/openapi-mcp/cmd/openapi-mcp
|
||||||
github.com/gokrazy/tools/cmd/gok
|
github.com/gokrazy/tools/cmd/gok
|
||||||
go.uber.org/mock/mockgen
|
go.uber.org/mock/mockgen
|
||||||
|
|
|
||||||
392
package-lock.json
generated
392
package-lock.json
generated
|
|
@ -36,6 +36,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.62.0",
|
"@playwright/test": "^1.62.0",
|
||||||
|
"@redocly/openapi-core": "^2.38.0",
|
||||||
"@storybook/vue3": "^10.4.4",
|
"@storybook/vue3": "^10.4.4",
|
||||||
"@storybook/vue3-vite": "^10.5.5",
|
"@storybook/vue3-vite": "^10.5.5",
|
||||||
"@types/body-parser": "^1.19.6",
|
"@types/body-parser": "^1.19.6",
|
||||||
|
|
@ -51,6 +52,8 @@
|
||||||
"@vue/compiler-sfc": "^3.5.38",
|
"@vue/compiler-sfc": "^3.5.38",
|
||||||
"@vue/test-utils": "^2.4.11",
|
"@vue/test-utils": "^2.4.11",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"ajv": "^8.20.0",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
"body-parser": "^2.3.0",
|
"body-parser": "^2.3.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"happy-dom": "^20.11.1",
|
"happy-dom": "^20.11.1",
|
||||||
|
|
@ -60,6 +63,7 @@
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"storybook": "^10.5.5",
|
"storybook": "^10.5.5",
|
||||||
"terser": "^5.49.0",
|
"terser": "^5.49.0",
|
||||||
|
"ts-json-schema-generator": "^2.9.0",
|
||||||
"tsx": "^4.23.1",
|
"tsx": "^4.23.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
|
||||||
|
|
@ -68,7 +72,8 @@
|
||||||
"vue-hot-reload-api": "^2.3.4",
|
"vue-hot-reload-api": "^2.3.4",
|
||||||
"vue-tsc": "^3.3.8",
|
"vue-tsc": "^3.3.8",
|
||||||
"wait-on": "^9.1.0",
|
"wait-on": "^9.1.0",
|
||||||
"ws": "^8.21.1"
|
"ws": "^8.21.1",
|
||||||
|
"yaml": "^2.9.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=26.0.0",
|
"node": ">=26.0.0",
|
||||||
|
|
@ -4032,6 +4037,98 @@
|
||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@redocly/ajv": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-l42u0of3hY98sN2A+M4qTX1O/KrpgGH32Hu9kP2GtHyD5Dfqq86PKFLe5dwaD8DEnNmlOlll2BAmeEtf0DaySg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/config": {
|
||||||
|
"version": "0.53.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.53.1.tgz",
|
||||||
|
"integrity": "sha512-aOldTWynKS3ZM6u8WkL72d94QBobITv9X6UECiZykgVjdLiAqV737bPVLGKJYPWf0Ryf3zSHgUYALjCj66BkdA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"json-schema-to-ts": "2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core": {
|
||||||
|
"version": "2.43.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-2.43.2.tgz",
|
||||||
|
"integrity": "sha512-4Z1gCrw/uiNlC0V0IsoJ0CAn4BB8mlo7xgchDZARrWqf8nBh6BDTWICAJ8tFWJxachRvNhZkBhUQwBgIBOfiYA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/ajv": "^8.18.1",
|
||||||
|
"@redocly/config": "^0.53.1",
|
||||||
|
"ajv": "npm:@redocly/ajv@8.18.1",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
|
"colorette": "^1.2.0",
|
||||||
|
"graphql": "^16.14.1",
|
||||||
|
"js-levenshtein": "^1.1.6",
|
||||||
|
"js-yaml": "^5.2.2",
|
||||||
|
"picomatch": "^4.0.4",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
|
"yaml-ast-parser": "0.0.43"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0 || >=20.19.0 <21.0.0",
|
||||||
|
"npm": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/ajv": {
|
||||||
|
"name": "@redocly/ajv",
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-Ifm/pP/tul1qmAecpbVxCBluVE32rKfjf8gYXH4xI2gCv9mRWFhJMHzkPDM4TXlxwPQYIFegymlsy8lXz7optA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/js-yaml": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
|
|
@ -4340,6 +4437,13 @@
|
||||||
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
|
"integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/kill-port": {
|
"node_modules/@types/kill-port": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/kill-port/-/kill-port-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/kill-port/-/kill-port-2.0.3.tgz",
|
||||||
|
|
@ -5656,6 +5760,41 @@
|
||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv-formats": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ajv": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz",
|
||||||
|
|
@ -6376,6 +6515,13 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
|
@ -7033,6 +7179,13 @@
|
||||||
"integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
|
"integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-unique-numbers": {
|
"node_modules/fast-unique-numbers": {
|
||||||
"version": "9.0.27",
|
"version": "9.0.27",
|
||||||
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz",
|
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz",
|
||||||
|
|
@ -7054,6 +7207,23 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
|
@ -7271,6 +7441,16 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/graphql": {
|
||||||
|
"version": "16.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz",
|
||||||
|
"integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/happy-dom": {
|
"node_modules/happy-dom": {
|
||||||
"version": "20.11.1",
|
"version": "20.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.11.1.tgz",
|
||||||
|
|
@ -7694,6 +7874,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-levenshtein": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-sdsl": {
|
"node_modules/js-sdsl": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
|
|
@ -7761,6 +7951,28 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-to-ts": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-R1JfqKqbBR4qE8UyBR56Ms30LL62/nlhoz+1UkfI/VE7p54Awu919FZ6ZUPG8zIa3XB65usPJgr1ONVncUGSaQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"@types/json-schema": "^7.0.9",
|
||||||
|
"ts-algebra": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
|
|
@ -8491,6 +8703,16 @@
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nostics": {
|
"node_modules/nostics": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
|
||||||
|
|
@ -8982,6 +9204,16 @@
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pluralize": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pngjs": {
|
"node_modules/pngjs": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
|
@ -9468,6 +9700,16 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-main-filename": {
|
"node_modules/require-main-filename": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
|
@ -9732,6 +9974,16 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
|
@ -10494,6 +10746,13 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-algebra": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-dedent": {
|
"node_modules/ts-dedent": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
|
||||||
|
|
@ -10504,6 +10763,130 @@
|
||||||
"node": ">=6.10"
|
"node": ">=6.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-json-schema-generator": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-NR5ZE108uiPtBHBJNGnhwoUaUx5vWTDJzDFG9YlRoqxPU76n+5FClRh92dcGgysbe1smRmYalM9Saj97GW1J4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"commander": "^14.0.3",
|
||||||
|
"glob": "^13.0.6",
|
||||||
|
"json5": "^2.2.3",
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"safe-stable-stringify": "^2.5.0",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-json-schema-generator": "bin/ts-json-schema-generator.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/commander": {
|
||||||
|
"version": "14.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
||||||
|
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/glob": {
|
||||||
|
"version": "13.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||||
|
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"minipass": "^7.1.3",
|
||||||
|
"path-scurry": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/lru-cache": {
|
||||||
|
"version": "11.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||||
|
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/minimatch": {
|
||||||
|
"version": "10.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||||
|
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/path-scurry": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-json-schema-generator/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/ts-map": {
|
"node_modules/ts-map": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/ts-map/-/ts-map-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/ts-map/-/ts-map-1.0.3.tgz",
|
||||||
|
|
@ -11932,6 +12315,13 @@
|
||||||
"url": "https://github.com/sponsors/eemeli"
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml-ast-parser": {
|
||||||
|
"version": "0.0.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
|
||||||
|
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "15.4.1",
|
"version": "15.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
"lint:tsc": "vue-tsc --noEmit",
|
"lint:tsc": "vue-tsc --noEmit",
|
||||||
"lint:i18n": "tsx i18n/check.ts",
|
"lint:i18n": "tsx i18n/check.ts",
|
||||||
"dev": "vp dev",
|
"dev": "vp dev",
|
||||||
|
"openapi": "tsx scripts/state-schema/index.ts",
|
||||||
"playwright": "playwright install && playwright test --ui",
|
"playwright": "playwright install && playwright test --ui",
|
||||||
"playwright:ci": "cross-env CI=true playwright test",
|
"playwright:ci": "cross-env CI=true playwright test",
|
||||||
"simulator": "vp dev tests/simulator",
|
"simulator": "vp dev tests/simulator",
|
||||||
|
|
@ -61,6 +62,7 @@
|
||||||
"repository": "github:evcc-io/evcc",
|
"repository": "github:evcc-io/evcc",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.62.0",
|
"@playwright/test": "^1.62.0",
|
||||||
|
"@redocly/openapi-core": "^2.38.0",
|
||||||
"@storybook/vue3": "^10.4.4",
|
"@storybook/vue3": "^10.4.4",
|
||||||
"@storybook/vue3-vite": "^10.5.5",
|
"@storybook/vue3-vite": "^10.5.5",
|
||||||
"@types/body-parser": "^1.19.6",
|
"@types/body-parser": "^1.19.6",
|
||||||
|
|
@ -76,6 +78,8 @@
|
||||||
"@vue/compiler-sfc": "^3.5.38",
|
"@vue/compiler-sfc": "^3.5.38",
|
||||||
"@vue/test-utils": "^2.4.11",
|
"@vue/test-utils": "^2.4.11",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"ajv": "^8.20.0",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
"body-parser": "^2.3.0",
|
"body-parser": "^2.3.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"happy-dom": "^20.11.1",
|
"happy-dom": "^20.11.1",
|
||||||
|
|
@ -85,6 +89,7 @@
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"storybook": "^10.5.5",
|
"storybook": "^10.5.5",
|
||||||
"terser": "^5.49.0",
|
"terser": "^5.49.0",
|
||||||
|
"ts-json-schema-generator": "^2.9.0",
|
||||||
"tsx": "^4.23.1",
|
"tsx": "^4.23.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.6",
|
||||||
|
|
@ -93,6 +98,7 @@
|
||||||
"vue-tsc": "^3.3.8",
|
"vue-tsc": "^3.3.8",
|
||||||
"wait-on": "^9.1.0",
|
"wait-on": "^9.1.0",
|
||||||
"ws": "^8.21.1",
|
"ws": "^8.21.1",
|
||||||
|
"yaml": "^2.9.0",
|
||||||
"vite-plus": "0.2.6"
|
"vite-plus": "0.2.6"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
|
||||||
33
scripts/state-schema/index.ts
Normal file
33
scripts/state-schema/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { buildSchemas } from "./schemas";
|
||||||
|
import { writeStateSchemas, bundleMcpJson } from "./openapi";
|
||||||
|
|
||||||
|
const command = process.argv[2] ?? "generate";
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "generate": {
|
||||||
|
const schemas = buildSchemas();
|
||||||
|
writeStateSchemas(schemas);
|
||||||
|
await bundleMcpJson();
|
||||||
|
console.log(
|
||||||
|
`${Object.keys(schemas.defs).length} state schemas → openapi.state.yaml + mcp/openapi.json`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "dump": {
|
||||||
|
console.log(JSON.stringify(buildSchemas().defs, null, 2));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "validate": {
|
||||||
|
const source = process.argv[3];
|
||||||
|
if (!source) {
|
||||||
|
console.error("usage: vp run openapi -- validate <payload.json | url>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const { validate } = await import("./validate");
|
||||||
|
if (!(await validate(source))) process.exit(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.error(`unknown command: ${command}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
38
scripts/state-schema/openapi.ts
Normal file
38
scripts/state-schema/openapi.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { parse, stringify } from "yaml";
|
||||||
|
import { bundle, createConfig } from "@redocly/openapi-core";
|
||||||
|
import type { StateSchemas } from "./schemas";
|
||||||
|
|
||||||
|
const OPENAPI_PATH = "server/openapi.yaml";
|
||||||
|
const STATE_PATH = "server/openapi.state.yaml";
|
||||||
|
const MCP_JSON_PATH = "server/mcp/openapi.json";
|
||||||
|
|
||||||
|
const HEADER = `# GENERATED FILE - DO NOT EDIT (source: assets/js/types/evcc.ts, update: make openapi)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function writeStateSchemas(schemas: StateSchemas): void {
|
||||||
|
const root = parse(readFileSync(OPENAPI_PATH, "utf8"));
|
||||||
|
const handWritten = new Set(Object.keys(root.components?.schemas ?? {}));
|
||||||
|
for (const name of Object.keys(schemas.defs)) {
|
||||||
|
if (handWritten.has(name)) {
|
||||||
|
throw new Error(`generated schema "${name}" collides with hand-written component`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc =
|
||||||
|
HEADER +
|
||||||
|
stringify(
|
||||||
|
{ components: { schemas: schemas.defs } },
|
||||||
|
{ lineWidth: 0, aliasDuplicateObjects: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
writeFileSync(STATE_PATH, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// inline the multi-file spec into the single json embedded by the MCP server
|
||||||
|
export async function bundleMcpJson(): Promise<void> {
|
||||||
|
const { bundle: result } = await bundle({ ref: OPENAPI_PATH, config: await createConfig({}) });
|
||||||
|
const doc = result.parsed as { servers?: unknown };
|
||||||
|
delete doc.servers; // mcp server sets its own url
|
||||||
|
writeFileSync(MCP_JSON_PATH, JSON.stringify(doc, null, 2) + "\n");
|
||||||
|
}
|
||||||
221
scripts/state-schema/schemas.ts
Normal file
221
scripts/state-schema/schemas.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { createGenerator } from "ts-json-schema-generator";
|
||||||
|
import type { Schema } from "ts-json-schema-generator";
|
||||||
|
|
||||||
|
export interface StateSchemas {
|
||||||
|
rootName: string;
|
||||||
|
// schema per component name, root first, rest alphabetical
|
||||||
|
defs: Record<string, Schema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enums use SCREAMING_SNAKE in the frontend, schema names follow openapi PascalCase convention
|
||||||
|
const RENAME: Record<string, string> = {
|
||||||
|
CHARGE_MODE: "ChargeMode",
|
||||||
|
BATTERY_MODE: "BatteryMode",
|
||||||
|
CURRENCY: "Currency",
|
||||||
|
CHARGER_STATUS_REASON: "ChargerStatusReason",
|
||||||
|
PHASE_ACTION: "PhaseAction",
|
||||||
|
PV_ACTION: "PvAction",
|
||||||
|
SMART_COST_TYPE: "SmartCostType",
|
||||||
|
OCPP_STATION_STATUS: "OcppConnectionStatus",
|
||||||
|
MODBUS_BAUDRATE: "ModbusBaudrate",
|
||||||
|
MODBUS_COMSET: "ModbusComset",
|
||||||
|
MODBUS_PROXY_READONLY: "ModbusProxyReadonly",
|
||||||
|
};
|
||||||
|
|
||||||
|
const VALID_NAME = /^[a-zA-Z0-9._-]+$/;
|
||||||
|
const ROOT = "State";
|
||||||
|
|
||||||
|
function buildRawSchema(): Schema {
|
||||||
|
const generator = createGenerator({
|
||||||
|
path: "assets/js/types/evcc.ts",
|
||||||
|
tsconfig: "tsconfig.json",
|
||||||
|
type: ROOT,
|
||||||
|
jsDoc: "extended",
|
||||||
|
extraTags: ["internal"],
|
||||||
|
additionalProperties: true,
|
||||||
|
topRef: false,
|
||||||
|
skipTypeCheck: true,
|
||||||
|
sortProps: false,
|
||||||
|
});
|
||||||
|
return generator.createSchema(ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnySchema = any;
|
||||||
|
|
||||||
|
function walk(node: any, visit: (schema: AnySchema) => void): void {
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
node.forEach((child) => walk(child, visit));
|
||||||
|
} else if (node && typeof node === "object") {
|
||||||
|
visit(node);
|
||||||
|
Object.values(node).forEach((child) => walk(child, visit));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove properties tagged @internal, they are UI-only and not part of the API payload
|
||||||
|
function stripInternal(schema: AnySchema): void {
|
||||||
|
walk(schema, (node) => {
|
||||||
|
if (!node.properties) return;
|
||||||
|
for (const [key, prop] of Object.entries<AnySchema>(node.properties)) {
|
||||||
|
if (prop?.internal === true) {
|
||||||
|
delete node.properties[key];
|
||||||
|
if (Array.isArray(node.required)) {
|
||||||
|
node.required = node.required.filter((r: string) => r !== key);
|
||||||
|
if (node.required.length === 0) delete node.required;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRefs(schema: AnySchema): Set<string> {
|
||||||
|
const refs = new Set<string>();
|
||||||
|
walk(schema, (node) => {
|
||||||
|
if (typeof node.$ref === "string") {
|
||||||
|
refs.add(decodeURIComponent(node.$ref.replace("#/definitions/", "")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop definitions that became unreachable after stripping @internal properties
|
||||||
|
function reachableDefs(
|
||||||
|
root: AnySchema,
|
||||||
|
defs: Record<string, AnySchema>
|
||||||
|
): Record<string, AnySchema> {
|
||||||
|
const keep: Record<string, AnySchema> = {};
|
||||||
|
const queue = [...collectRefs(root)];
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const name = queue.shift()!;
|
||||||
|
if (keep[name] || !defs[name]) continue;
|
||||||
|
keep[name] = defs[name];
|
||||||
|
queue.push(...collectRefs(defs[name]));
|
||||||
|
}
|
||||||
|
return keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert json-schema null unions to `nullable: true`, the 3.0 style used across openapi.yaml
|
||||||
|
// (kin-openapi, which validates the spec in CI, does not support 3.1 type arrays)
|
||||||
|
function normalizeNullables(schema: AnySchema): void {
|
||||||
|
walk(schema, (node) => {
|
||||||
|
if (Array.isArray(node.type) && node.type.includes("null")) {
|
||||||
|
const rest = node.type.filter((t: string) => t !== "null");
|
||||||
|
if (rest.length !== 1) throw new Error(`unsupported type union ${node.type}`);
|
||||||
|
node.type = rest[0];
|
||||||
|
node.nullable = true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(node.anyOf) && node.anyOf.some((b: AnySchema) => b.type === "null")) {
|
||||||
|
const rest = node.anyOf.filter((b: AnySchema) => b.type !== "null");
|
||||||
|
delete node.anyOf;
|
||||||
|
node.nullable = true;
|
||||||
|
if (rest.length === 1 && !rest[0].$ref) {
|
||||||
|
Object.assign(node, rest[0]);
|
||||||
|
} else {
|
||||||
|
node.allOf = rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// openapi 3.0 has no `const`, use a single-value enum
|
||||||
|
if ("const" in node) {
|
||||||
|
node.enum = [node.const];
|
||||||
|
delete node.const;
|
||||||
|
}
|
||||||
|
// openapi 3.0 uses a singular example
|
||||||
|
if (Array.isArray(node.examples)) {
|
||||||
|
node.example = node.examples[0];
|
||||||
|
delete node.examples;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// point $refs at the final openapi component names
|
||||||
|
function rewriteRefs(schema: AnySchema, finalNames: Map<string, string>): void {
|
||||||
|
walk(schema, (node) => {
|
||||||
|
if (typeof node.$ref !== "string" || node.$ref.startsWith("#/components/")) return;
|
||||||
|
const name = decodeURIComponent(node.$ref.replace("#/definitions/", ""));
|
||||||
|
const renamed = finalNames.get(name);
|
||||||
|
if (!renamed) throw new Error(`unresolved $ref "${node.$ref}"`);
|
||||||
|
node.$ref = `#/components/schemas/${renamed}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildSchemas(): StateSchemas {
|
||||||
|
const raw = buildRawSchema() as AnySchema;
|
||||||
|
const { definitions = {}, $schema: _schema, ...root } = raw;
|
||||||
|
|
||||||
|
stripInternal(root);
|
||||||
|
Object.values(definitions as Record<string, AnySchema>).forEach(stripInternal);
|
||||||
|
|
||||||
|
const defs = reachableDefs(root, definitions);
|
||||||
|
|
||||||
|
const finalNames = new Map<string, string>();
|
||||||
|
for (const name of Object.keys(defs)) {
|
||||||
|
const renamed = RENAME[name] ?? name;
|
||||||
|
if (!VALID_NAME.test(renamed)) {
|
||||||
|
throw new Error(`invalid component name "${renamed}", rename the type in evcc.ts`);
|
||||||
|
}
|
||||||
|
if ([...finalNames.values()].includes(renamed)) {
|
||||||
|
throw new Error(`duplicate component name "${renamed}" after renaming`);
|
||||||
|
}
|
||||||
|
finalNames.set(name, renamed);
|
||||||
|
}
|
||||||
|
|
||||||
|
rewriteRefs(root, finalNames);
|
||||||
|
Object.values(defs).forEach((schema) => rewriteRefs(schema, finalNames));
|
||||||
|
|
||||||
|
normalizeNullables(root);
|
||||||
|
Object.values(defs).forEach(normalizeNullables);
|
||||||
|
|
||||||
|
const result: Record<string, Schema> = { [ROOT]: root };
|
||||||
|
for (const [name, schema] of Object.entries(defs)
|
||||||
|
.map(([name, schema]) => [finalNames.get(name)!, schema] as const)
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b))) {
|
||||||
|
result[name] = schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(result);
|
||||||
|
if (serialized.includes("#/definitions/")) {
|
||||||
|
throw new Error("unrewritten $ref to #/definitions/ left in output");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordered = Object.fromEntries(
|
||||||
|
Object.entries(result).map(([name, schema]) => [name, orderKeys(schema)])
|
||||||
|
);
|
||||||
|
|
||||||
|
return { rootName: ROOT, defs: ordered };
|
||||||
|
}
|
||||||
|
|
||||||
|
const KEY_ORDER = [
|
||||||
|
"$ref",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"nullable",
|
||||||
|
"enum",
|
||||||
|
"format",
|
||||||
|
"example",
|
||||||
|
"items",
|
||||||
|
"properties",
|
||||||
|
"required",
|
||||||
|
"additionalProperties",
|
||||||
|
"anyOf",
|
||||||
|
"allOf",
|
||||||
|
];
|
||||||
|
|
||||||
|
// stable key order for readable yaml diffs, property order itself is preserved
|
||||||
|
function orderKeys(node: any): any {
|
||||||
|
if (Array.isArray(node)) return node.map(orderKeys);
|
||||||
|
if (!node || typeof node !== "object") return node;
|
||||||
|
const keys = Object.keys(node).sort((a, b) => {
|
||||||
|
const ia = KEY_ORDER.indexOf(a);
|
||||||
|
const ib = KEY_ORDER.indexOf(b);
|
||||||
|
return (ia === -1 ? KEY_ORDER.length : ia) - (ib === -1 ? KEY_ORDER.length : ib);
|
||||||
|
});
|
||||||
|
return Object.fromEntries(
|
||||||
|
keys.map((key) => [
|
||||||
|
key,
|
||||||
|
key === "properties" ? mapValues(node[key], orderKeys) : orderKeys(node[key]),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapValues(obj: Record<string, any>, fn: (v: any) => any): Record<string, any> {
|
||||||
|
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)]));
|
||||||
|
}
|
||||||
114
scripts/state-schema/validate.ts
Normal file
114
scripts/state-schema/validate.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||||
|
import addFormats from "ajv-formats";
|
||||||
|
|
||||||
|
const STATE_SCHEMAS_PATH = "server/openapi.state.yaml";
|
||||||
|
|
||||||
|
// intentionally undocumented experimental structures
|
||||||
|
const IGNORE = new Set(["$.evopt", "$.evopt-batteries"]);
|
||||||
|
|
||||||
|
type AnySchema = any;
|
||||||
|
|
||||||
|
function resolve(schema: AnySchema, schemas: Record<string, AnySchema>): AnySchema {
|
||||||
|
if (typeof schema?.$ref === "string") {
|
||||||
|
return resolve(schemas[schema.$ref.split("/").pop()!], schemas);
|
||||||
|
}
|
||||||
|
return schema ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// report payload keys that have no schema property, additionalProperties:true hides them from ajv
|
||||||
|
function coverage(
|
||||||
|
schema: AnySchema,
|
||||||
|
data: any,
|
||||||
|
path: string,
|
||||||
|
schemas: Record<string, AnySchema>,
|
||||||
|
report: Set<string>
|
||||||
|
): void {
|
||||||
|
const s = resolve(schema, schemas);
|
||||||
|
if (s.anyOf) {
|
||||||
|
if (data === null) return;
|
||||||
|
const branch = s.anyOf.find((b: AnySchema) => resolve(b, schemas).type !== "null");
|
||||||
|
if (branch) coverage(branch, data, path, schemas, report);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
if (s.items) data.forEach((item) => coverage(s.items, item, `${path}[]`, schemas, report));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data !== null && typeof data === "object") {
|
||||||
|
if (!s.properties) return;
|
||||||
|
for (const key of Object.keys(data)) {
|
||||||
|
if (s.properties[key]) {
|
||||||
|
coverage(s.properties[key], data[key], `${path}.${key}`, schemas, report);
|
||||||
|
} else if (!IGNORE.has(`${path}.${key}`)) {
|
||||||
|
report.add(`${path}.${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// openapi uses `nullable: true`, json schema expects explicit null in the type
|
||||||
|
function expandNullable(node: any): void {
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
node.forEach(expandNullable);
|
||||||
|
} else if (node && typeof node === "object") {
|
||||||
|
if (node.nullable === true) {
|
||||||
|
delete node.nullable;
|
||||||
|
if (typeof node.type === "string") {
|
||||||
|
node.type = [node.type, "null"];
|
||||||
|
} else if (Array.isArray(node.allOf)) {
|
||||||
|
node.anyOf = [...node.allOf, { type: "null" }];
|
||||||
|
delete node.allOf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.values(node).forEach(expandNullable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate a /api/state payload against the State schema in server/openapi.state.yaml
|
||||||
|
export function validateState(payload: any): { errors: string[]; undocumented: string[] } {
|
||||||
|
const doc = parse(readFileSync(STATE_SCHEMAS_PATH, "utf8"));
|
||||||
|
const schemas: Record<string, AnySchema> = doc.components.schemas;
|
||||||
|
expandNullable(schemas);
|
||||||
|
|
||||||
|
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
||||||
|
addFormats(ajv);
|
||||||
|
const validateFn = ajv.compile({
|
||||||
|
$ref: "#/components/schemas/State",
|
||||||
|
components: { schemas },
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = validateFn(payload)
|
||||||
|
? []
|
||||||
|
: (validateFn.errors ?? []).map((err) => `${err.instancePath || "/"} ${err.message}`);
|
||||||
|
|
||||||
|
const undocumented = new Set<string>();
|
||||||
|
coverage(schemas["State"], payload, "$", schemas, undocumented);
|
||||||
|
|
||||||
|
return { errors, undocumented: [...undocumented].sort() };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validate(source: string): Promise<boolean> {
|
||||||
|
let payload: any;
|
||||||
|
if (source.startsWith("http")) {
|
||||||
|
const res = await fetch(`${source.replace(/\/$/, "")}/api/state`);
|
||||||
|
payload = await res.json();
|
||||||
|
} else {
|
||||||
|
payload = JSON.parse(readFileSync(source, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors, undocumented } = validateState(payload);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
console.error(`${source}: schema violations`);
|
||||||
|
for (const err of errors.slice(0, 30)) console.error(` ${err}`);
|
||||||
|
}
|
||||||
|
if (undocumented.length > 0) {
|
||||||
|
console.log(`${source}: ${undocumented.length} undocumented keys`);
|
||||||
|
for (const key of undocumented) console.log(` ${key}`);
|
||||||
|
}
|
||||||
|
if (errors.length === 0 && undocumented.length === 0) console.log(`${source}: ok`);
|
||||||
|
|
||||||
|
return errors.length === 0;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -216,13 +216,13 @@ Directly controls the mode of all controllable batteries. evcc behavior like 'pr
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
|------|------|-------------|
|
|------|------|-------------|
|
||||||
| batteryMode | string | Battery mode |
|
| batteryMode | string | Battery operation mode. |
|
||||||
|
|
||||||
**Example call:**
|
**Example call:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
call setExternalBatteryMode {
|
call setExternalBatteryMode {
|
||||||
"batteryMode": "normal"
|
"batteryMode": "unknown"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -384,26 +384,6 @@ call setSolarAdjusted {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## getState
|
|
||||||
|
|
||||||
Returns the complete state of the system. This structure is used by the UI. It can be filtered by JQ to only return a subset of the data.
|
|
||||||
|
|
||||||
**Tags:** general
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
|------|------|-------------|
|
|
||||||
| jq | string | Filter the state with JQ |
|
|
||||||
|
|
||||||
**Example call:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
call getState {
|
|
||||||
"jq": "example"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## removeGlobalSmartCostLimit
|
## removeGlobalSmartCostLimit
|
||||||
|
|
||||||
Convenience method to remove limit for all loadpoints at once. Value is applied to each individual loadpoint.
|
Convenience method to remove limit for all loadpoints at once. Value is applied to each individual loadpoint.
|
||||||
|
|
@ -1166,6 +1146,26 @@ call updateSession {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## getState
|
||||||
|
|
||||||
|
Returns the complete state of the system. This structure is used by the UI and also published via websocket and MQTT. It can be filtered by JQ to only return a subset of the data. Note: the response mirrors the internal UI state and carries no compatibility promise. Fields may change or disappear between releases.
|
||||||
|
|
||||||
|
**Tags:** state
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| jq | string | Filter the state with JQ |
|
||||||
|
|
||||||
|
**Example call:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
call getState {
|
||||||
|
"jq": "example"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## clearCache
|
## clearCache
|
||||||
|
|
||||||
Clears all cached data. This resets all cached values from tariffs, vehicle APIs, and other components that use caching.
|
Clears all cached data. This resets all cached values from tariffs, vehicle APIs, and other components that use caching.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
//go:generate go tool openapi openapi.yaml mcp/openapi.json
|
//go:generate go tool openapi-mcp --doc mcp/openapi.md mcp/openapi.json
|
||||||
//go:generate go tool openapi-mcp --doc mcp/openapi.md openapi.yaml
|
|
||||||
|
|
|
||||||
1829
server/openapi.state.yaml
Normal file
1829
server/openapi.state.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@ info:
|
||||||
servers:
|
servers:
|
||||||
- url: https://demo.evcc.io/api
|
- url: https://demo.evcc.io/api
|
||||||
tags:
|
tags:
|
||||||
|
- name: state
|
||||||
- name: auth
|
- name: auth
|
||||||
- name: battery
|
- name: battery
|
||||||
- name: db
|
- name: db
|
||||||
|
|
@ -449,7 +450,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Mode"
|
$ref: "./openapi.state.yaml#/components/schemas/ChargeMode"
|
||||||
/loadpoints/{id}/phases/{phases}:
|
/loadpoints/{id}/phases/{phases}:
|
||||||
post:
|
post:
|
||||||
operationId: setLoadpointPhases
|
operationId: setLoadpointPhases
|
||||||
|
|
@ -523,7 +524,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/StaticEnergyPlan"
|
$ref: "./openapi.state.yaml#/components/schemas/StaticEnergyPlan"
|
||||||
/loadpoints/{id}/plan/repeating/preview/{soc}/{weekdays}/{hourMinuteTime}/{timezone}:
|
/loadpoints/{id}/plan/repeating/preview/{soc}/{weekdays}/{hourMinuteTime}/{timezone}:
|
||||||
get:
|
get:
|
||||||
operationId: previewLoadpointRepeatingPlan
|
operationId: previewLoadpointRepeatingPlan
|
||||||
|
|
@ -590,14 +591,14 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PlanStrategy"
|
$ref: "./openapi.state.yaml#/components/schemas/PlanStrategy"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success
|
description: Success
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PlanStrategy"
|
$ref: "./openapi.state.yaml#/components/schemas/PlanStrategy"
|
||||||
/loadpoints/{id}/priority/{priority}:
|
/loadpoints/{id}/priority/{priority}:
|
||||||
post:
|
post:
|
||||||
operationId: setLoadpointPriority
|
operationId: setLoadpointPriority
|
||||||
|
|
@ -947,11 +948,11 @@ paths:
|
||||||
get:
|
get:
|
||||||
operationId: getState
|
operationId: getState
|
||||||
summary: System state
|
summary: System state
|
||||||
description: "Returns the complete state of the system. This structure is used by the UI. It can be filtered by JQ to only return a subset of the data."
|
description: "Returns the complete state of the system. This structure is used by the UI and also published via websocket and MQTT. It can be filtered by JQ to only return a subset of the data. Note: the response mirrors the internal UI state and carries no compatibility promise. Fields may change or disappear between releases."
|
||||||
externalDocs:
|
externalDocs:
|
||||||
url: https://docs.evcc.io/integrations/rest-api
|
url: https://docs.evcc.io/integrations/rest-api
|
||||||
tags:
|
tags:
|
||||||
- general
|
- state
|
||||||
parameters:
|
parameters:
|
||||||
- name: jq
|
- name: jq
|
||||||
in: query
|
in: query
|
||||||
|
|
@ -971,7 +972,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/State"
|
$ref: "./openapi.state.yaml#/components/schemas/State"
|
||||||
/system/log:
|
/system/log:
|
||||||
get:
|
get:
|
||||||
operationId: getSystemLogs
|
operationId: getSystemLogs
|
||||||
|
|
@ -1164,7 +1165,7 @@ paths:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
mode:
|
mode:
|
||||||
$ref: "#/components/schemas/Mode"
|
$ref: "./openapi.state.yaml#/components/schemas/ChargeMode"
|
||||||
/vehicles/{name}/plan/repeating:
|
/vehicles/{name}/plan/repeating:
|
||||||
post:
|
post:
|
||||||
operationId: updateVehicleRepeatingPlans
|
operationId: updateVehicleRepeatingPlans
|
||||||
|
|
@ -1183,7 +1184,7 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/RepeatingPlan"
|
$ref: "./openapi.state.yaml#/components/schemas/RepeatingPlan"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success
|
description: Success
|
||||||
|
|
@ -1192,7 +1193,7 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/RepeatingPlan"
|
$ref: "./openapi.state.yaml#/components/schemas/RepeatingPlan"
|
||||||
/vehicles/{name}/plan/soc:
|
/vehicles/{name}/plan/soc:
|
||||||
delete:
|
delete:
|
||||||
operationId: deleteVehicleSocPlan
|
operationId: deleteVehicleSocPlan
|
||||||
|
|
@ -1248,14 +1249,14 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PlanStrategy"
|
$ref: "./openapi.state.yaml#/components/schemas/PlanStrategy"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success
|
description: Success
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/PlanStrategy"
|
$ref: "./openapi.state.yaml#/components/schemas/PlanStrategy"
|
||||||
/db/backup:
|
/db/backup:
|
||||||
get:
|
get:
|
||||||
operationId: downloadBackup
|
operationId: downloadBackup
|
||||||
|
|
@ -1433,16 +1434,6 @@ paths:
|
||||||
description: Invalid parameters or database offline
|
description: Invalid parameters or database offline
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
BatteryMode:
|
|
||||||
description: Battery mode
|
|
||||||
type: string
|
|
||||||
example: normal
|
|
||||||
enum:
|
|
||||||
- unknown
|
|
||||||
- normal
|
|
||||||
- hold
|
|
||||||
- charge
|
|
||||||
- holdcharge
|
|
||||||
ChangePassword:
|
ChangePassword:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -1580,14 +1571,6 @@ components:
|
||||||
- INFO
|
- INFO
|
||||||
- DEBUG
|
- DEBUG
|
||||||
- TRACE
|
- TRACE
|
||||||
Mode:
|
|
||||||
description: "Charging mode."
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- "off"
|
|
||||||
- "now"
|
|
||||||
- "minpv"
|
|
||||||
- "pv"
|
|
||||||
Odometer:
|
Odometer:
|
||||||
nullable: true
|
nullable: true
|
||||||
type: "number"
|
type: "number"
|
||||||
|
|
@ -1626,51 +1609,10 @@ components:
|
||||||
type: integer
|
type: integer
|
||||||
example: 3600
|
example: 3600
|
||||||
minimum: 0
|
minimum: 0
|
||||||
Rate:
|
|
||||||
type: object
|
|
||||||
description: A charging interval
|
|
||||||
properties:
|
|
||||||
start:
|
|
||||||
description: Start
|
|
||||||
$ref: "#/components/schemas/Timestamp"
|
|
||||||
end:
|
|
||||||
description: End
|
|
||||||
$ref: "#/components/schemas/Timestamp"
|
|
||||||
value:
|
|
||||||
description: Cost
|
|
||||||
type: number
|
|
||||||
minimum: 0
|
|
||||||
Rates:
|
Rates:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: "#/components/schemas/Rate"
|
$ref: "./openapi.state.yaml#/components/schemas/Rate"
|
||||||
PlanStrategy:
|
|
||||||
description: Charging plan strategy configuration
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
continuous:
|
|
||||||
description: "Force continuous planning"
|
|
||||||
type: boolean
|
|
||||||
precondition:
|
|
||||||
description: "Precondition duration in seconds"
|
|
||||||
type: integer
|
|
||||||
minimum: 0
|
|
||||||
RepeatingPlan:
|
|
||||||
externalDocs:
|
|
||||||
url: https://docs.evcc.io/en/features/plans#repeating-plans
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
active:
|
|
||||||
description: "Set plan active."
|
|
||||||
type: boolean
|
|
||||||
soc:
|
|
||||||
$ref: "#/components/schemas/Soc"
|
|
||||||
time:
|
|
||||||
$ref: "#/components/schemas/HourMinuteTime"
|
|
||||||
tz:
|
|
||||||
$ref: "#/components/schemas/IANATimeZone"
|
|
||||||
weekdays:
|
|
||||||
$ref: "#/components/schemas/Weekdays"
|
|
||||||
Soc:
|
Soc:
|
||||||
description: SOC in %
|
description: SOC in %
|
||||||
type: number
|
type: number
|
||||||
|
|
@ -1682,27 +1624,6 @@ components:
|
||||||
type: number
|
type: number
|
||||||
example: 50
|
example: 50
|
||||||
minimum: 0
|
minimum: 0
|
||||||
State:
|
|
||||||
description: "The actual state structure is not documented yet. Most values should be self-explanatory. Note: While the overall structure is quite stable, details may change between releases."
|
|
||||||
type: object
|
|
||||||
StaticEnergyPlan:
|
|
||||||
externalDocs:
|
|
||||||
url: https://docs.evcc.io/en/features/plans#energy-amount-plan
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
energy:
|
|
||||||
$ref: "#/components/schemas/Energy"
|
|
||||||
time:
|
|
||||||
$ref: "#/components/schemas/Timestamp"
|
|
||||||
StaticSocPlan:
|
|
||||||
externalDocs:
|
|
||||||
url: https://docs.evcc.io/en/features/plans#create-charging-plan
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
soc:
|
|
||||||
$ref: "#/components/schemas/Soc"
|
|
||||||
time:
|
|
||||||
$ref: "#/components/schemas/Timestamp"
|
|
||||||
Timestamp:
|
Timestamp:
|
||||||
description: Timestamp in RFC3339 format
|
description: Timestamp in RFC3339 format
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -1799,7 +1720,7 @@ components:
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Mode"
|
$ref: "./openapi.state.yaml#/components/schemas/ChargeMode"
|
||||||
soc:
|
soc:
|
||||||
name: soc
|
name: soc
|
||||||
description: SOC in %
|
description: SOC in %
|
||||||
|
|
@ -1861,7 +1782,7 @@ components:
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BatteryMode"
|
$ref: "./openapi.state.yaml#/components/schemas/BatteryMode"
|
||||||
costLimit:
|
costLimit:
|
||||||
name: cost
|
name: cost
|
||||||
description: Cost limit in configured currency (default EUR) or CO2 limit in g/kWh
|
description: Cost limit in configured currency (default EUR) or CO2 limit in g/kWh
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
func TestOpenAPIValidation(t *testing.T) {
|
func TestOpenAPIValidation(t *testing.T) {
|
||||||
loader := openapi3.NewLoader()
|
loader := openapi3.NewLoader()
|
||||||
|
loader.IsExternalRefsAllowed = true
|
||||||
doc, err := loader.LoadFromFile("openapi.yaml")
|
doc, err := loader.LoadFromFile("openapi.yaml")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, doc.Validate(loader.Context))
|
require.NoError(t, doc.Validate(loader.Context))
|
||||||
|
|
|
||||||
28
tests/state-api.spec.ts
Normal file
28
tests/state-api.spec.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
|
import { validateState } from "../scripts/state-schema/validate";
|
||||||
|
|
||||||
|
test.use({ baseURL: baseUrl() });
|
||||||
|
|
||||||
|
const CONFIG = "cmd/demo.yaml";
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await start(CONFIG);
|
||||||
|
});
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("/api/state matches the openapi State schema", async ({ request }) => {
|
||||||
|
// loadpoint values appear over the first update cycles, poll until stable
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const res = await request.get("/api/state");
|
||||||
|
const { errors, undocumented } = validateState(await res.json());
|
||||||
|
return [...errors, ...undocumented];
|
||||||
|
},
|
||||||
|
{ timeout: 30000 }
|
||||||
|
)
|
||||||
|
.toEqual([]);
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"include": ["env.d.ts", "assets/js/**/*", "tests/**/*"],
|
"include": ["env.d.ts", "assets/js/**/*", "tests/**/*", "scripts/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowUnreachableCode": false,
|
"allowUnreachableCode": false,
|
||||||
"allowUnusedLabels": false,
|
"allowUnusedLabels": false,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue