diff --git a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue index d2fc59597..f77385766 100644 --- a/assets/js/components/Config/DeviceModal/DeviceModalBase.vue +++ b/assets/js/components/Config/DeviceModal/DeviceModalBase.vue @@ -330,12 +330,11 @@ export default defineComponent({ return (this.modbus?.Choice || []) as ModbusCapability[]; }, modbusDefaults() { - const { ID, Comset, Baudrate, Port } = this.modbus || {}; return { - id: ID, - comset: Comset, - baudrate: Baudrate, - port: Port, + id: this.modbus?.ID, + comset: this.modbus?.Comset, + baudrate: this.modbus?.Baudrate, + port: this.modbus?.Port, }; }, description() { @@ -353,7 +352,6 @@ export default defineComponent({ }, apiData(): ApiData { let data: ApiData = { - ...this.modbusDefaults, ...this.values, }; if (this.values.type === ConfigType.Template && this.templateName) { @@ -743,7 +741,10 @@ export default defineComponent({ clearTimeout(this.serviceValuesTimer); } this.serviceValuesTimer = setTimeout(async () => { - this.serviceValues = await fetchServiceValues(this.templateParams, this.values); + this.serviceValues = await fetchServiceValues(this.templateParams, { + ...this.modbusDefaults, + ...this.values, + }); }, 500); }, applyServiceDefault(paramName: string) { diff --git a/assets/js/components/Config/DeviceModal/Modbus.vue b/assets/js/components/Config/DeviceModal/Modbus.vue index c7412116d..1568972aa 100644 --- a/assets/js/components/Config/DeviceModal/Modbus.vue +++ b/assets/js/components/Config/DeviceModal/Modbus.vue @@ -45,8 +45,8 @@ type="Int" class="me-2" required - :model-value="id || defaultId || 1" - @change="$emit('update:id', $event.target.value)" + :model-value="id || defaultId" + @input="$emit('update:id', $event.target.value)" />
@@ -62,7 +62,7 @@ class="me-2" required :model-value="host" - @change="$emit('update:host', $event.target.value)" + @input="$emit('update:host', $event.target.value)" /> @@ -72,8 +72,8 @@ type="Int" class="me-2 w-50" required - :model-value="port || defaultPort || 502" - @change="$emit('update:port', $event.target.value)" + :model-value="port || defaultPort" + @input="$emit('update:port', $event.target.value)" /> @@ -153,8 +153,8 @@ class="me-2 w-50" :choice="comsetOptions" required - :model-value="comset || defaultComset || '8N1'" - @change="$emit('update:comset', $event.target.value)" + :model-value="comset || defaultComset" + @input="$emit('update:comset', $event.target.value)" />
@@ -268,7 +268,15 @@ export default defineComponent({ this.setConnectionAndProtocolByModbus(newValue); } }, - connection() { + connection(newValue: MODBUS_CONNECTION, oldValue: MODBUS_CONNECTION) { + if (newValue !== oldValue) { + // Clear connection-specific parameters to ensure correct dependency group is used + if (newValue === MODBUS_CONNECTION.TCPIP) { + this.$emit("update:device", undefined); + } else if (newValue === MODBUS_CONNECTION.SERIAL) { + this.$emit("update:host", undefined); + } + } this.applyServiceDefault(); }, device(newValue: string | undefined) { diff --git a/assets/js/components/Config/DeviceModal/index.test.ts b/assets/js/components/Config/DeviceModal/index.test.ts index be912c419..27585a31a 100644 --- a/assets/js/components/Config/DeviceModal/index.test.ts +++ b/assets/js/components/Config/DeviceModal/index.test.ts @@ -21,9 +21,7 @@ describe("createServiceEndpoints", () => { const endpoints = createServiceEndpoints(params); const homeEndpoint = endpoints.find(({ name }) => name === "home")!; const powerEndpoint = endpoints.find(({ name }) => name === "power")!; - expect(homeEndpoint.dependencies).toEqual([]); expect(homeEndpoint.url({})).toBe("homes"); - expect(powerEndpoint.dependencies).toEqual(["home"]); expect(powerEndpoint.url({ home: "main" })).toBe("homes/main/sensors"); expect(powerEndpoint.url({ home: "with space" })).toBe("homes/with%20space/sensors"); expect(powerEndpoint.url({} as Record)).toBe("homes/{home}/sensors"); @@ -36,7 +34,6 @@ describe("createServiceEndpoints", () => { ]; const endpoints = createServiceEndpoints(params); const sensorEndpoint = endpoints.find(({ name }) => name === "sensor")!; - expect(sensorEndpoint.dependencies).toEqual(["home", "sensor"]); expect(sensorEndpoint.url({ home: "hq", sensor: "battery" })).toBe("homes/hq/sensors/battery"); }); @@ -51,4 +48,55 @@ describe("createServiceEndpoints", () => { "homes/{home}/sensors/{sensor}?token={token}" ); }); + + it("expands {modbus} for TCP/IP", () => { + const params = [buildParam("param", "service?address=100&{modbus}")]; + const endpoints = createServiceEndpoints(params); + + expect(endpoints[0]!.url({ host: "192.168.1.1", port: "502", id: "1" })).toBe( + "service?address=100&uri=192.168.1.1:502&id=1" + ); + }); + + it("expands {modbus} for serial", () => { + const params = [buildParam("param", "service?address=100&{modbus}")]; + const endpoints = createServiceEndpoints(params); + + expect( + endpoints[0]!.url({ device: "/dev/ttyUSB0", baudrate: "9600", comset: "8N1", id: "1" }) + ).toBe("service?address=100&device=%2Fdev%2FttyUSB0&baudrate=9600&comset=8N1&id=1"); + }); + + it("leaves {modbus} unexpanded when connection missing", () => { + const params = [buildParam("param", "service?address=100&{modbus}")]; + const endpoints = createServiceEndpoints(params); + + expect(endpoints[0]!.url({})).toBe("service?address=100&{modbus}"); + }); + + it("prefers device over host when both present", () => { + const params = [buildParam("param", "service?{modbus}")]; + const endpoints = createServiceEndpoints(params); + + expect( + endpoints[0]!.url({ + device: "/dev/ttyUSB0", + baudrate: "9600", + comset: "8N1", + host: "192.168.1.1", + port: "502", + id: "1", + }) + ).toBe("service?device=%2Fdev%2FttyUSB0&baudrate=9600&comset=8N1&id=1"); + }); + + it("treats empty strings as missing values", () => { + const params = [buildParam("sensor", "homes/{home}/sensors")]; + const endpoints = createServiceEndpoints(params); + + // Empty string should be treated as missing, leaving placeholder + expect(endpoints[0]!.url({ home: "" })).toBe("homes/{home}/sensors"); + // Non-empty value should replace placeholder + expect(endpoints[0]!.url({ home: "main" })).toBe("homes/main/sensors"); + }); }); diff --git a/assets/js/components/Config/DeviceModal/index.ts b/assets/js/components/Config/DeviceModal/index.ts index 6a5fdb93f..f1304d289 100644 --- a/assets/js/components/Config/DeviceModal/index.ts +++ b/assets/js/components/Config/DeviceModal/index.ts @@ -35,7 +35,7 @@ export type TemplateParam = { export type ParamService = { name: string; - dependencies: string[]; + service: string; url: (values: Record) => string; }; @@ -118,6 +118,22 @@ export async function loadServiceValues(path: string) { } } +// Expand {modbus} to actual connection params based on values +const expandModbus = (service: string, values: Record): string => { + if (!service.includes("{modbus}")) return service; + + if (values["device"]) { + return service.replace( + "{modbus}", + "device={device}&baudrate={baudrate}&comset={comset}&id={id}" + ); + } + if (values["host"]) { + return service.replace("{modbus}", "uri={host}:{port}&id={id}"); + } + return service; +}; + export const createServiceEndpoints = (params: TemplateParam[]): ParamService[] => { return params .map((param) => { @@ -127,7 +143,8 @@ export const createServiceEndpoints = (params: TemplateParam[]): ParamService[] const stringValues = (values: Record): Record => Object.entries(values).reduce( (acc, [key, val]) => { - if (val !== undefined && val !== null) acc[key] = String(val); + if (val !== undefined && val !== null && val !== "" && key !== "modbus") + acc[key] = String(val); return acc; }, {} as Record @@ -135,9 +152,9 @@ export const createServiceEndpoints = (params: TemplateParam[]): ParamService[] return { name: param.Name, - dependencies: extractPlaceholders(param.Service), + service: param.Service, url: (values: Record) => - replacePlaceholders(param.Service!, stringValues(values)), + replacePlaceholders(expandModbus(param.Service!, values), stringValues(values)), } as ParamService; }) .filter((endpoint): endpoint is ParamService => endpoint !== null); @@ -152,17 +169,11 @@ export const fetchServiceValues = async ( await Promise.all( endpoints.map(async (endpoint) => { - const params: Record = {}; - endpoint.dependencies.forEach((dependency) => { - if (values[dependency]) { - params[dependency] = values[dependency]; - } - }); - if (Object.keys(params).length !== endpoint.dependencies.length) { - // missing dependency values, skip + const url = endpoint.url(values); + if (extractPlaceholders(url).length > 0) { + // missing values, not all placeholders are filled return; } - const url = endpoint.url(params); const data = await loadServiceValues(url); if (data) { result[endpoint.name] = data; diff --git a/assets/js/components/Config/PropertyEntry.vue b/assets/js/components/Config/PropertyEntry.vue index b209bb7a4..6bdcf275d 100644 --- a/assets/js/components/Config/PropertyEntry.vue +++ b/assets/js/components/Config/PropertyEntry.vue @@ -10,7 +10,6 @@ -
- - {{ unitValue }} -
-
+
-
- - + + + +
+ {{ unitValue }} - × - - - -
@@ -148,7 +147,15 @@ export default { // no values if (length === 0) return false; // value selected, dont offer single same option again - if (this.value && this.serviceValues.includes(this.value)) return false; + // Convert both to strings for comparison to handle number/string type mismatches + const valueStr = String(this.value ?? ""); + if ( + this.value != null && + valueStr !== "" && + this.serviceValues.some((v) => String(v) === valueStr) + ) { + return false; + } return true; }, showClearButton() { @@ -294,7 +301,7 @@ export default { }; -