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 }}
-
-
+
-
@@ -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 {
};
-