fix: Preserve HA name if set in withHomeassistant and remove analog prefix (#32654)

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
This commit is contained in:
luar123
2026-09-04 21:30:38 +02:00
committed by GitHub
co-authored by Koen Kanters
parent ac098810a1
commit bba9c027c5
4 changed files with 227 additions and 20 deletions
+15 -13
View File
@@ -141,7 +141,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
alarm_temperature_min: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-low"},
angle: {icon: "angle-acute"},
angle_axis: {icon: "angle-acute"},
apparent_temperature: {device_class: "temperature", icon: "mdi:thermometer-lines", preserve_name: true, state_class: "measurement"},
apparent_temperature: {device_class: "temperature", icon: "mdi:thermometer-lines", state_class: "measurement"},
aqi: {device_class: "aqi", state_class: "measurement"},
auto_relock_time: {entity_category: "config", icon: "mdi:timer"},
away_preset_days: {entity_category: "config", icon: "mdi:timer"},
@@ -195,7 +195,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
entity_category: "diagnostic",
state_class: "measurement",
},
dew_point: {device_class: "temperature", icon: "mdi:thermometer-water", preserve_name: true, state_class: "measurement"},
dew_point: {device_class: "temperature", icon: "mdi:thermometer-water", state_class: "measurement"},
distance: {device_class: "distance", state_class: "measurement"},
duration: {entity_category: "config", icon: "mdi:timer"},
eco2: {device_class: "volatile_organic_compounds_parts", state_class: "measurement"},
@@ -213,12 +213,13 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
formaldehyd: {state_class: "measurement"},
formaldehyde: {state_class: "measurement"},
flow: {device_class: "volume_flow_rate", state_class: "measurement"},
frequency: {device_class: "frequency", state_class: "measurement"},
gas: {device_class: "gas", state_class: "total_increasing", icon: "mdi:meter-gas"},
gas_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
gust_speed: {device_class: "wind_speed", icon: "mdi:weather-windy-variant", preserve_name: true, state_class: "measurement"},
gust_speed: {device_class: "wind_speed", icon: "mdi:weather-windy-variant", state_class: "measurement"},
hcho: {icon: "mdi:air-filter", state_class: "measurement"},
heat_stress: {icon: "mdi:weather-sunny-alert", state_class: "measurement"},
humidex: {device_class: "temperature", icon: "mdi:thermometer-alert", preserve_name: true, state_class: "measurement"},
humidex: {device_class: "temperature", icon: "mdi:thermometer-alert", state_class: "measurement"},
humidity: {device_class: "humidity", state_class: "measurement"},
humidity_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
humidity_max: {entity_category: "config", icon: "mdi:water-percent"},
@@ -318,7 +319,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
device_class: "water",
state_class: "total_increasing",
},
wind_chill: {device_class: "temperature", icon: "mdi:snowflake-thermometer", preserve_name: true, state_class: "measurement"},
wind_chill: {device_class: "temperature", icon: "mdi:snowflake-thermometer", state_class: "measurement"},
wind_direction: {icon: "mdi:compass-outline", state_class: "measurement"},
wind_speed: {device_class: "wind_speed", icon: "mdi:weather-windy", state_class: "measurement"},
x: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
@@ -396,6 +397,8 @@ const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
return feature.property;
};
const cleanName = (name: string): string => name.replace(/^(?:analog_in_|analog_out_)/, "");
const applyHomeAssistantExposeMetadata = (payload: DiscoveryEntry, homeAssistant: zhc.Expose["homeassistant"]): void => {
if (!homeAssistant) {
return;
@@ -1207,7 +1210,7 @@ export class HomeAssistant extends Extension {
command_topic_postfix: firstExpose.property,
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
...(firstExpose.value_step && {step: firstExpose.value_step}),
...NUMERIC_DISCOVERY_LOOKUP[firstExpose.name],
...NUMERIC_DISCOVERY_LOOKUP[cleanName(firstExpose.name)],
},
};
@@ -1239,7 +1242,7 @@ export class HomeAssistant extends Extension {
Object.assign(extraAttrs, {device_class: "power", state_class: "measurement"});
}
let key = firstExpose.name;
let key = cleanName(firstExpose.name);
// Home Assistant uses a different voc device_class for µg/m³ versus ppb or ppm.
if (firstExpose.name === "voc" && firstExpose.unit && ["ppb", "ppm"].includes(firstExpose.unit)) {
@@ -1487,12 +1490,11 @@ export class HomeAssistant extends Extension {
}
// Let Home Assistant generate entity name when device_class is present.
// preserve_name allows device_class and explicit name to coexist (e.g. derived sensors).
if (
entry.discovery_payload.device_class &&
entry.discovery_payload.name !== null &&
!NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.preserve_name
) {
// homeassistant.name allows device_class and explicit name to coexist (e.g. derived sensors).
// If name = null Home Assistant will use the device name as entity name.
// This is intentionally an undefined-only check to distinguish it from an null value.
if (entry.discovery_payload.device_class && firstExpose.homeassistant?.name === undefined) {
delete entry.discovery_payload.name;
}
+121 -1
View File
@@ -1046,6 +1046,62 @@ describe("Extension: Bridge", () => {
source: "generated",
description: "Automatically generated definition",
exposes: [
{
access: 5,
description: "Analog Input my_sensor_name on endpoint 1",
endpoint: "1",
homeassistant: {name: "my_sensor_name"},
label: "my_sensor_name",
name: "analog_in_temperature",
property: "analog_in_temperature_1",
type: "numeric",
unit: "°C",
},
{
access: 5,
description: "Analog Input my_sensor2_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_sensor2_name"},
label: "my_sensor2_name",
name: "analog_in_temperature",
property: "analog_in_temperature_2",
type: "numeric",
unit: "°C",
},
{
access: 7,
description: "Analog Output my_number_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_number_name"},
label: "my_number_name",
name: "analog_output",
property: "analog_output_2",
type: "numeric",
},
{
access: 5,
description: "Binary Input my_binary_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_binary_name"},
label: "my_binary_name",
name: "binary_input",
property: "binary_input_2",
type: "binary",
value_off: "OFF",
value_on: "ON",
},
{
access: 7,
description: "Binary Output my_switch_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_switch_name"},
label: "my_switch_name",
name: "binary_output",
property: "binary_output_2",
type: "binary",
value_off: "OFF",
value_on: "ON",
},
{
access: 1,
category: "diagnostic",
@@ -1120,9 +1176,17 @@ describe("Extension: Bridge", () => {
endpoints: {
"1": {
bindings: [],
clusters: {input: ["genBasic"], output: ["genBasic", "genOnOff", "genLevelCtrl", "genScenes"]},
clusters: {input: ["genBasic", "genAnalogInput"], output: ["genBasic", "genOnOff", "genLevelCtrl", "genScenes"]},
configured_reportings: [],
scenes: [],
name: "1",
},
"2": {
bindings: [],
clusters: {input: ["genBasic", "genAnalogInput", "genAnalogOutput", "genBinaryInput", "genBinaryOutput"], output: []},
configured_reportings: [],
scenes: [],
name: "2",
},
},
friendly_name: "0x0017880104e45518",
@@ -2762,6 +2826,62 @@ describe("Extension: Bridge", () => {
source: "generated",
description: "Automatically generated definition",
exposes: [
{
access: 5,
description: "Analog Input my_sensor_name on endpoint 1",
endpoint: "1",
homeassistant: {name: "my_sensor_name"},
label: "my_sensor_name",
name: "analog_in_temperature",
property: "analog_in_temperature_1",
type: "numeric",
unit: "°C",
},
{
access: 5,
description: "Analog Input my_sensor2_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_sensor2_name"},
label: "my_sensor2_name",
name: "analog_in_temperature",
property: "analog_in_temperature_2",
type: "numeric",
unit: "°C",
},
{
access: 7,
description: "Analog Output my_number_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_number_name"},
label: "my_number_name",
name: "analog_output",
property: "analog_output_2",
type: "numeric",
},
{
access: 5,
description: "Binary Input my_binary_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_binary_name"},
label: "my_binary_name",
name: "binary_input",
property: "binary_input_2",
type: "binary",
value_off: "OFF",
value_on: "ON",
},
{
access: 7,
description: "Binary Output my_switch_name on endpoint 2",
endpoint: "2",
homeassistant: {name: "my_switch_name"},
label: "my_switch_name",
name: "binary_output",
property: "binary_output_2",
type: "binary",
value_off: "OFF",
value_on: "ON",
},
{
access: 1,
category: "diagnostic",
+71
View File
@@ -1368,6 +1368,77 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover analog input on unsupported devices with device class and name", () => {
const payload = {
availability: [
{
topic: "zigbee2mqtt/bridge/state",
value_template: "{{ value_json.state }}",
},
],
default_entity_id: "sensor.0x0017880104e45518_analog_in_temperature_1",
device: {
identifiers: ["zigbee2mqtt_0x0017880104e45518"],
manufacturer: "notSupportedMfg",
model: "Automatically generated definition",
model_id: "notSupportedModelID",
name: "0x0017880104e45518",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
device_class: "temperature",
enabled_by_default: true,
name: "my_sensor_name",
object_id: "0x0017880104e45518_analog_in_temperature_1",
origin: origin,
state_class: "measurement",
state_topic: "zigbee2mqtt/0x0017880104e45518",
unique_id: "0x0017880104e45518_analog_in_temperature_1_zigbee2mqtt",
unit_of_measurement: "°C",
value_template: '{{ value_json["analog_in_temperature_1"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"homeassistant/sensor/0x0017880104e45518/analog_in_temperature_1/config",
stringify(payload),
{
retain: true,
qos: 1,
},
);
});
it("Should discover analog output on unsupported devices", () => {
const payload = {
availability: [
{
topic: "zigbee2mqtt/bridge/state",
value_template: "{{ value_json.state }}",
},
],
command_topic: "zigbee2mqtt/0x0017880104e45518/2/set/analog_output_2",
default_entity_id: "number.0x0017880104e45518_analog_output_2",
device: {
identifiers: ["zigbee2mqtt_0x0017880104e45518"],
manufacturer: "notSupportedMfg",
model: "Automatically generated definition",
model_id: "notSupportedModelID",
name: "0x0017880104e45518",
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
},
name: "my_number_name",
object_id: "0x0017880104e45518_analog_output_2",
origin: origin,
state_topic: "zigbee2mqtt/0x0017880104e45518",
unique_id: "0x0017880104e45518_analog_output_2_zigbee2mqtt",
value_template: '{{ value_json["analog_output_2"] }}',
};
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x0017880104e45518/analog_output_2/config", stringify(payload), {
qos: 1,
retain: true,
});
});
it("Should apply user configuration after converter compatibility mapping", async () => {
settings.set(["devices", "0x18fc2600000d7ae2", "homeassistant", "climate"], {
modes: ["off", "heat", "auto"],
+20 -6
View File
@@ -40,6 +40,10 @@ const CLUSTERS = {
msRelativeHumidity: Zcl.Clusters.msRelativeHumidity.ID,
msSoilMoisture: Zcl.Clusters.msSoilMoisture.ID,
msCO2: Zcl.Clusters.msCO2.ID,
genAnalogInput: Zcl.Clusters.genAnalogInput.ID,
genAnalogOutput: Zcl.Clusters.genAnalogOutput.ID,
genBinaryInput: Zcl.Clusters.genBinaryInput.ID,
genBinaryOutput: Zcl.Clusters.genBinaryOutput.ID,
};
export const CUSTOM_CLUSTERS = {
@@ -626,7 +630,7 @@ export const devices = {
{target: groups.group_1, cluster: {ID: 6, name: "genOnOff"}},
{target: groups.group_1, cluster: {ID: 6, name: "genLevelCtrl"}},
]),
new Endpoint(2, [0, 1, 3, 15, 64512], [25, 6], "0x0017880104e45517"),
new Endpoint(2, [0, 1, 3, 64512], [25, 6], "0x0017880104e45517"),
],
InterviewState.Successful,
"Battery",
@@ -637,7 +641,17 @@ export const devices = {
"0x0017880104e45518",
6536,
0,
[new Endpoint(1, [0], [0, 3, 4, 6, 8, 5], "0x0017880104e45518")],
[
new Endpoint(1, [0, 12], [0, 3, 4, 6, 8, 5], "0x0017880104e45518", [], {
genAnalogInput: {description: "my_sensor_name", applicationType: 0},
}),
new Endpoint(2, [0, 12, 13, 15, 16], [], "0x0017880104e45518", [], {
genAnalogInput: {description: "my_sensor2_name", applicationType: 0},
genAnalogOutput: {description: "my_number_name"},
genBinaryInput: {description: "my_binary_name"},
genBinaryOutput: {description: "my_switch_name"},
}),
],
InterviewState.Successful,
"Battery",
"notSupportedModelID",
@@ -928,7 +942,7 @@ export const devices = {
33901,
4476,
[
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64ax", [], {
new Endpoint(1, [0, 4, 3, 5, 10, 258, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64ax", [], {
lightingColorCtrl: {colorCapabilities: 254},
}),
],
@@ -942,7 +956,7 @@ export const devices = {
33901,
4476,
[
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {
new Endpoint(1, [0, 4, 3, 5, 10, 258, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {
seMetering: {multiplier: 1, divisor: 10000},
}),
],
@@ -958,7 +972,7 @@ export const devices = {
33901,
4476,
[
new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {
new Endpoint(1, [0, 4, 3, 5, 10, 258, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {
seMetering: {multiplier: 1, divisor: 10000},
}),
],
@@ -973,7 +987,7 @@ export const devices = {
"0x90fd9ffffe4b64ac",
33901,
4476,
[new Endpoint(1, [0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {})],
[new Endpoint(1, [0, 4, 3, 5, 10, 258, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026], [0, 3, 4, 6, 8, 5], "0x90fd9ffffe4b64aa", [], {})],
InterviewState.Successful,
"Mains (single phase)",
"qnazj70",