mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-09-20 03:34:36 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user