mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-10-23 22:32:40 +00:00
fix: Home Assistant: guard value templates against missing properties (#33071)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
This commit is contained in:
3 files changed
+293
-289
No files matched your search
@@ -8,13 +8,7 @@ import {stringify} from "../util/stringify";
|
||||
import utils, {assertBinaryExpose, assertEnumExpose, assertNumericExpose, isBinaryExpose, isEnumExpose, isNumericExpose} from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
interface MockProperty {
|
||||
property: string;
|
||||
value: KeyValue | string | null;
|
||||
}
|
||||
|
||||
interface DiscoveryEntry {
|
||||
mockProperties: MockProperty[];
|
||||
type: string;
|
||||
object_id: string;
|
||||
discovery_payload: KeyValue;
|
||||
@@ -22,7 +16,6 @@ interface DiscoveryEntry {
|
||||
}
|
||||
|
||||
interface Discovered {
|
||||
mockProperties: Set<MockProperty>;
|
||||
messages: {[s: string]: {payload: string; published: boolean}};
|
||||
triggers: Set<string>;
|
||||
discovered: boolean;
|
||||
@@ -399,6 +392,16 @@ const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
|
||||
|
||||
const cleanName = (name: string): string => name.replace(/^(?:analog_in_|analog_out_)/, "");
|
||||
|
||||
/**
|
||||
* Reads `property` from the state payload, guarded against it being absent, which otherwise logs
|
||||
* `Template variable warning: 'dict object' has no attribute '<property>'`. Renders an empty string
|
||||
* when absent, which the MQTT platforms ignore.
|
||||
* Check presence, not truthiness: `0`, `false` and `null` are falsy in Jinja.
|
||||
* https://github.com/Koenkk/zigbee2mqtt/issues/30086
|
||||
*/
|
||||
const stateValueTemplate = (property: string, body = `{{ value_json["${property}"] }}`): string =>
|
||||
`{% if "${property}" in value_json %}${body}{% endif %}`;
|
||||
|
||||
const applyHomeAssistantExposeMetadata = (payload: DiscoveryEntry, homeAssistant: zhc.Expose["homeassistant"]): void => {
|
||||
if (!homeAssistant) {
|
||||
return;
|
||||
@@ -611,7 +614,7 @@ export class HomeAssistant extends Extension {
|
||||
private getDiscovered(entity: Device | Group | Bridge | string | number): Discovered {
|
||||
const ID = typeof entity === "string" || typeof entity === "number" ? entity : entity.ID;
|
||||
if (!(ID in this.discovered)) {
|
||||
this.discovered[ID] = {messages: {}, triggers: new Set(), mockProperties: new Set(), discovered: false};
|
||||
this.discovered[ID] = {messages: {}, triggers: new Set(), discovered: false};
|
||||
}
|
||||
return this.discovered[ID];
|
||||
}
|
||||
@@ -645,7 +648,6 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "light",
|
||||
object_id: endpointName ? `light_${endpointName}` : "light",
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
brightness: !!hasBrightness,
|
||||
@@ -707,12 +709,11 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "switch",
|
||||
object_id: endpointName ? `switch_${endpointName}` : "switch",
|
||||
mockProperties: [{property: property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
payload_off: state.value_off,
|
||||
payload_on: state.value_on,
|
||||
value_template: `{{ value_json["${property}"] }}`,
|
||||
value_template: stateValueTemplate(property),
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpointName,
|
||||
},
|
||||
@@ -747,7 +748,6 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "climate",
|
||||
object_id: endpointName ? `climate_${endpointName}` : "climate",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
// Static
|
||||
@@ -759,7 +759,7 @@ export class HomeAssistant extends Extension {
|
||||
max_temp: primarySetpoint.value_max.toString(),
|
||||
// Temperature
|
||||
current_temperature_topic: true,
|
||||
current_temperature_template: `{{ value_json["${temperature.property}"] }}`,
|
||||
current_temperature_template: stateValueTemplate(temperature.property),
|
||||
command_topic_prefix: endpointName,
|
||||
},
|
||||
};
|
||||
@@ -773,28 +773,30 @@ export class HomeAssistant extends Extension {
|
||||
mode.values.splice(mode.values.indexOf("sleep"), 1);
|
||||
}
|
||||
discoveryEntry.discovery_payload.mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json["${mode.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.mode_state_template = stateValueTemplate(mode.property);
|
||||
discoveryEntry.discovery_payload.modes = mode.values;
|
||||
discoveryEntry.discovery_payload.mode_command_topic = true;
|
||||
}
|
||||
|
||||
const state = (firstExpose as zhc.Climate).features.find((f) => f.name === "running_state");
|
||||
if (state) {
|
||||
discoveryEntry.mockProperties.push({property: state.property, value: null});
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json["${state.property}"]] }}`;
|
||||
discoveryEntry.discovery_payload.action_template = stateValueTemplate(
|
||||
state.property,
|
||||
`{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json["${state.property}"]] }}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (heatingSetpoint && coolingSetpoint) {
|
||||
discoveryEntry.discovery_payload.temperature_low_command_topic = heatingSetpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json["${heatingSetpoint.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.temperature_low_state_template = stateValueTemplate(heatingSetpoint.property);
|
||||
discoveryEntry.discovery_payload.temperature_low_state_topic = true;
|
||||
discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json["${coolingSetpoint.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.temperature_high_state_template = stateValueTemplate(coolingSetpoint.property);
|
||||
discoveryEntry.discovery_payload.temperature_high_state_topic = true;
|
||||
} else {
|
||||
discoveryEntry.discovery_payload.temperature_command_topic = primarySetpoint.name;
|
||||
discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json["${primarySetpoint.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.temperature_state_template = stateValueTemplate(primarySetpoint.property);
|
||||
discoveryEntry.discovery_payload.temperature_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -802,7 +804,7 @@ export class HomeAssistant extends Extension {
|
||||
if (fanMode) {
|
||||
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
|
||||
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json["${fanMode.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template = stateValueTemplate(fanMode.property);
|
||||
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -810,7 +812,7 @@ export class HomeAssistant extends Extension {
|
||||
if (swingMode) {
|
||||
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
|
||||
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json["${swingMode.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template = stateValueTemplate(swingMode.property);
|
||||
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -818,7 +820,7 @@ export class HomeAssistant extends Extension {
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.preset_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = "preset";
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json["${preset.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = stateValueTemplate(preset.property);
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -829,10 +831,9 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "number",
|
||||
object_id: endpointName ? `${tempCalibration.name}_${endpointName}` : `${tempCalibration.name}`,
|
||||
mockProperties: [{property: tempCalibration.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${tempCalibration.label} ${endpointName}` : tempCalibration.label,
|
||||
value_template: `{{ value_json["${tempCalibration.property}"] }}`,
|
||||
value_template: stateValueTemplate(tempCalibration.property),
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic_postfix: tempCalibration.property,
|
||||
@@ -856,10 +857,9 @@ export class HomeAssistant extends Extension {
|
||||
if (piHeatingDemand) {
|
||||
const discoveryEntry: Partial<DiscoveryEntry> = {
|
||||
object_id: endpointName ? `${piHeatingDemand.name}_${endpointName}` : `${piHeatingDemand.name}`,
|
||||
mockProperties: [{property: piHeatingDemand.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${piHeatingDemand.label} ${endpointName}` : piHeatingDemand.label,
|
||||
value_template: `{{ value_json["${piHeatingDemand.property}"] }}`,
|
||||
value_template: stateValueTemplate(piHeatingDemand.property),
|
||||
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
|
||||
icon: "mdi:radiator",
|
||||
},
|
||||
@@ -887,10 +887,9 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: endpointName ? /* v8 ignore next */ `${piCoolingDemand.name}_${endpointName}` : `${piCoolingDemand.name}`,
|
||||
mockProperties: [{property: piCoolingDemand.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? /* v8 ignore next */ `${piCoolingDemand.label} ${endpointName}` : piCoolingDemand.label,
|
||||
value_template: `{{ value_json["${piCoolingDemand.property}"] }}`,
|
||||
value_template: stateValueTemplate(piCoolingDemand.property),
|
||||
...(piCoolingDemand.unit && {unit_of_measurement: piCoolingDemand.unit}),
|
||||
entity_category: "diagnostic",
|
||||
icon: "mdi:air-conditioner",
|
||||
@@ -909,10 +908,9 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: endpointName ? `${localTemperature.name}_${endpointName}` : `${localTemperature.name}`,
|
||||
mockProperties: [{property: localTemperature.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${localTemperature.label} ${endpointName}` : localTemperature.label,
|
||||
value_template: `{{ value_json["${localTemperature.property}"] }}`,
|
||||
value_template: stateValueTemplate(localTemperature.property),
|
||||
...(localTemperature.unit && {unit_of_measurement: localTemperature.unit}),
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
@@ -927,7 +925,7 @@ export class HomeAssistant extends Extension {
|
||||
const humidities = allExposes.filter(isNumericExpose).filter((e) => e.name === "humidity" && e.access & ACCESS_STATE);
|
||||
const currentHumidity = humidities.find((e) => e.endpoint === endpointName) ?? humidities.find((e) => e.endpoint === undefined);
|
||||
if (currentHumidity) {
|
||||
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json["${currentHumidity.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.current_humidity_template = stateValueTemplate(currentHumidity.property);
|
||||
discoveryEntry.discovery_payload.current_humidity_topic = true;
|
||||
}
|
||||
|
||||
@@ -941,13 +939,12 @@ export class HomeAssistant extends Extension {
|
||||
type: "lock",
|
||||
/* v8 ignore next */
|
||||
object_id: endpointName ? `lock_${endpointName}` : "lock",
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
discovery_payload: {
|
||||
/* v8 ignore next */
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
value_template: `{{ value_json["${state.property}"] }}`,
|
||||
value_template: stateValueTemplate(state.property),
|
||||
state_locked: state.value_on,
|
||||
state_unlocked: state.value_off,
|
||||
/* v8 ignore next */
|
||||
@@ -975,7 +972,6 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "cover",
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
object_id: endpointName ? `cover_${endpointName}` : "cover",
|
||||
discovery_payload: {
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
@@ -990,7 +986,9 @@ export class HomeAssistant extends Extension {
|
||||
// The movement direction is calculated (assumed) in this case.
|
||||
if (running) {
|
||||
assert(position, `Cover must have 'position' when it has 'running'`);
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json and value_json["${featurePropertyWithoutEndpoint(running)}"] %} {% if value_json["${featurePropertyWithoutEndpoint(position)}"] > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
const runningProperty = featurePropertyWithoutEndpoint(running);
|
||||
const positionProperty = featurePropertyWithoutEndpoint(position);
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${runningProperty}" in value_json and value_json["${runningProperty}"] and "${positionProperty}" in value_json %} {% if value_json["${positionProperty}"] > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
}
|
||||
|
||||
// If curtains have `motor_state` or `moving` property, lookup for possible
|
||||
@@ -1029,7 +1027,7 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
// If curtains do not have `running`, `motor_state` or `moving` properties.
|
||||
if (!discoveryEntry.discovery_payload.value_template) {
|
||||
discoveryEntry.discovery_payload.value_template = `{{ value_json["${featurePropertyWithoutEndpoint(state)}"] }}`;
|
||||
discoveryEntry.discovery_payload.value_template = stateValueTemplate(featurePropertyWithoutEndpoint(state));
|
||||
discoveryEntry.discovery_payload.state_open = "OPEN";
|
||||
discoveryEntry.discovery_payload.state_closed = "CLOSE";
|
||||
discoveryEntry.discovery_payload.state_stopped = "STOP";
|
||||
@@ -1044,7 +1042,7 @@ export class HomeAssistant extends Extension {
|
||||
if (position) {
|
||||
discoveryEntry.discovery_payload = {
|
||||
...discoveryEntry.discovery_payload,
|
||||
position_template: `{{ value_json["${featurePropertyWithoutEndpoint(position)}"] }}`,
|
||||
position_template: stateValueTemplate(featurePropertyWithoutEndpoint(position)),
|
||||
set_position_template: `{ "${getProperty(position)}": {{ position }} }`,
|
||||
set_position_topic: true,
|
||||
position_topic: true,
|
||||
@@ -1056,7 +1054,7 @@ export class HomeAssistant extends Extension {
|
||||
...discoveryEntry.discovery_payload,
|
||||
tilt_command_topic: true,
|
||||
tilt_status_topic: true,
|
||||
tilt_status_template: `{{ value_json["${featurePropertyWithoutEndpoint(tilt)}"] }}`,
|
||||
tilt_status_template: stateValueTemplate(featurePropertyWithoutEndpoint(tilt)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1068,7 +1066,6 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "fan",
|
||||
object_id: "fan",
|
||||
mockProperties: [{property: "fan_state", value: null}],
|
||||
discovery_payload: {
|
||||
name: null,
|
||||
state_topic: true,
|
||||
@@ -1121,31 +1118,37 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntry.discovery_payload.percentage_state_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_command_topic = "fan_mode";
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json["${modeEmulatedSpeed.property}"]] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_value_template = stateValueTemplate(
|
||||
modeEmulatedSpeed.property,
|
||||
`{{ {${percentValues}}[value_json["${modeEmulatedSpeed.property}"]] | default('None') }}`,
|
||||
);
|
||||
discoveryEntry.discovery_payload.percentage_command_template = `{{ {${percentCommands}}[value] | default('') }}`;
|
||||
discoveryEntry.discovery_payload.speed_range_min = 1;
|
||||
discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1;
|
||||
assert(presets.length !== 0);
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = "fan_mode";
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json["${modeEmulatedSpeed.property}"] if value_json["${modeEmulatedSpeed.property}"] in [${presetList}] else 'None' | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = stateValueTemplate(
|
||||
modeEmulatedSpeed.property,
|
||||
`{{ value_json["${modeEmulatedSpeed.property}"] if value_json["${modeEmulatedSpeed.property}"] in [${presetList}] else 'None' }}`,
|
||||
);
|
||||
discoveryEntry.discovery_payload.preset_modes = presets;
|
||||
|
||||
// Emulate state based on mode
|
||||
discoveryEntry.discovery_payload.state_value_template = "{{ value_json.fan_state }}";
|
||||
discoveryEntry.discovery_payload.state_value_template = stateValueTemplate("fan_state");
|
||||
discoveryEntry.discovery_payload.command_topic_postfix = "fan_state";
|
||||
/* v8 ignore start */
|
||||
} else if (nativeSpeed) {
|
||||
/* v8 ignore stop */
|
||||
discoveryEntry.discovery_payload.percentage_state_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_command_topic = "speed";
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ value_json["${nativeSpeed.property}"] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_value_template = stateValueTemplate(nativeSpeed.property);
|
||||
discoveryEntry.discovery_payload.percentage_command_template = `{{ value | default('') }}`;
|
||||
discoveryEntry.discovery_payload.speed_range_min = nativeSpeed.value_min;
|
||||
discoveryEntry.discovery_payload.speed_range_max = nativeSpeed.value_max;
|
||||
|
||||
// Speed-controlled fans generally have an onOff cluster, use that for state
|
||||
discoveryEntry.discovery_payload.state_value_template = "{{ value_json.state }}";
|
||||
discoveryEntry.discovery_payload.state_value_template = stateValueTemplate("state");
|
||||
discoveryEntry.discovery_payload.command_topic_postfix = "state";
|
||||
}
|
||||
|
||||
@@ -1163,14 +1166,15 @@ export class HomeAssistant extends Extension {
|
||||
if (firstExpose.access & ACCESS_SET) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "switch",
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
object_id: endpointName ? `switch_${firstExpose.name}_${endpointName}` : `switch_${firstExpose.name}`,
|
||||
discovery_payload: {
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template:
|
||||
value_template: stateValueTemplate(
|
||||
firstExpose.property,
|
||||
typeof firstExpose.value_on === "boolean"
|
||||
? `{% if value_json["${firstExpose.property}"] %}true{% else %}false{% endif %}`
|
||||
: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
: undefined,
|
||||
),
|
||||
payload_on: firstExpose.value_on.toString(),
|
||||
payload_off: firstExpose.value_off.toString(),
|
||||
command_topic: true,
|
||||
@@ -1185,10 +1189,9 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "binary_sensor",
|
||||
object_id: endpointName ? `${firstExpose.name}_${endpointName}` : `${firstExpose.name}`,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
value_template: stateValueTemplate(firstExpose.property),
|
||||
payload_on: firstExpose.value_on,
|
||||
payload_off: firstExpose.value_off,
|
||||
...(BINARY_DISCOVERY_LOOKUP[firstExpose.name] || {}),
|
||||
@@ -1210,10 +1213,9 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "number",
|
||||
object_id: endpointName ? `${firstExpose.name}_${endpointName}` : `${firstExpose.name}`,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
value_template: stateValueTemplate(firstExpose.property),
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
@@ -1261,10 +1263,9 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: endpointName ? `${firstExpose.name}_${endpointName}` : `${firstExpose.name}`,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
value_template: stateValueTemplate(firstExpose.property),
|
||||
enabled_by_default: !allowsSet,
|
||||
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
|
||||
...NUMERIC_DISCOVERY_LOOKUP[key],
|
||||
@@ -1303,7 +1304,6 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: "event",
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
state_topic: true,
|
||||
@@ -1318,7 +1318,7 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json["${firstExpose.property}"] }}` : undefined;
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ? stateValueTemplate(firstExpose.property) : undefined;
|
||||
|
||||
/**
|
||||
* If enum has only one item and has SET access then expose as BUTTON entity.
|
||||
@@ -1327,7 +1327,6 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: "button",
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
state_topic: false,
|
||||
@@ -1348,7 +1347,6 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: "select",
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: valueTemplate,
|
||||
@@ -1372,7 +1370,6 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: "sensor",
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: valueTemplate,
|
||||
@@ -1398,7 +1395,6 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "siren",
|
||||
object_id: endpointName ? /* v8 ignore next */ `siren_${endpointName}` : "siren",
|
||||
mockProperties: [{property: warningExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? /* v8 ignore next */ utils.capitalize(endpointName) : null,
|
||||
command_topic: true,
|
||||
@@ -1450,11 +1446,10 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: "text",
|
||||
object_id: firstExposeTyped.property,
|
||||
mockProperties: [{property: firstExposeTyped.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${firstExposeTyped.label} ${endpointName}` : firstExposeTyped.label,
|
||||
state_topic: firstExposeTyped.access & ACCESS_STATE,
|
||||
value_template: `{{ value_json["${firstExposeTyped.property}"] }}`,
|
||||
value_template: stateValueTemplate(firstExposeTyped.property),
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExposeTyped.property,
|
||||
@@ -1467,12 +1462,14 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntries.push({
|
||||
type: "sensor",
|
||||
object_id: firstExposeTyped.property,
|
||||
mockProperties: [{property: firstExposeTyped.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpointName ? `${firstExposeTyped.label} ${endpointName}` : firstExposeTyped.label,
|
||||
// Truncate text if it's too long
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/23199
|
||||
value_template: `{{ value_json["${firstExposeTyped.property}"] | default('',True) | string | truncate(254, True, '', 0) }}`,
|
||||
value_template: stateValueTemplate(
|
||||
firstExposeTyped.property,
|
||||
`{{ value_json["${firstExposeTyped.property}"] | default('',True) | string | truncate(254, True, '', 0) }}`,
|
||||
),
|
||||
...LIST_DISCOVERY_LOOKUP[firstExposeTyped.name],
|
||||
},
|
||||
});
|
||||
@@ -1573,7 +1570,10 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
await this.mqtt.publish(`${data.entity.name}/${endpoint}`, stringify(payload), {});
|
||||
// Nothing to republish when this payload carries no attribute of the endpoint.
|
||||
if (!utils.objectIsEmpty(payload)) {
|
||||
await this.mqtt.publish(`${data.entity.name}/${endpoint}`, stringify(payload), {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1684,10 +1684,9 @@ export class HomeAssistant extends Extension {
|
||||
const config: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: "last_seen",
|
||||
mockProperties: [{property: "last_seen", value: null}],
|
||||
discovery_payload: {
|
||||
name: "Last seen",
|
||||
value_template: "{{ value_json.last_seen }}",
|
||||
value_template: stateValueTemplate("last_seen"),
|
||||
icon: "mdi:clock",
|
||||
enabled_by_default: false,
|
||||
entity_category: "diagnostic",
|
||||
@@ -1707,7 +1706,6 @@ export class HomeAssistant extends Extension {
|
||||
const updateSensor: DiscoveryEntry = {
|
||||
type: "update",
|
||||
object_id: "update",
|
||||
mockProperties: [{property: "update", value: {state: null}}],
|
||||
discovery_payload: {
|
||||
name: null,
|
||||
entity_picture: "https://github.com/Koenkk/zigbee2mqtt/raw/master/images/logo.png",
|
||||
@@ -1716,7 +1714,10 @@ export class HomeAssistant extends Extension {
|
||||
entity_category: "config",
|
||||
command_topic: `${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/update`,
|
||||
payload_install: `{"id": "${entity.ieeeAddr}"}`,
|
||||
value_template: `{"latest_version":"{{ value_json['update']['latest_version'] }}","installed_version":"{{ value_json['update']['installed_version'] }}","update_percentage":{{ value_json['update'].get('progress', 'null') }},"in_progress":{{ (value_json['update']['state'] == 'updating')|lower }}}`,
|
||||
value_template: stateValueTemplate(
|
||||
"update",
|
||||
`{"latest_version":"{{ value_json['update']['latest_version'] }}","installed_version":"{{ value_json['update']['installed_version'] }}","update_percentage":{{ value_json['update'].get('progress', 'null') }},"in_progress":{{ (value_json['update'].get('state') == 'updating')|lower }}}`,
|
||||
),
|
||||
},
|
||||
};
|
||||
configs.push(updateSensor);
|
||||
@@ -1728,7 +1729,6 @@ export class HomeAssistant extends Extension {
|
||||
const sceneEntry: DiscoveryEntry = {
|
||||
type: "scene",
|
||||
object_id: `scene_${scene.id}`,
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: `${scene.name}`,
|
||||
state_topic: false,
|
||||
@@ -2042,14 +2042,6 @@ export class HomeAssistant extends Extension {
|
||||
} else {
|
||||
logger.debug(`Skipping discovery of '${topic}', already discovered`);
|
||||
}
|
||||
|
||||
/* v8 ignore start */
|
||||
if (config.mockProperties) {
|
||||
/* v8 ignore stop */
|
||||
for (const mockProperty of config.mockProperties) {
|
||||
discovered.mockProperties.add(mockProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const topic of lastDiscoveredTopics) {
|
||||
@@ -2211,12 +2203,6 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
override adjustMessageBeforePublish(entity: Device | Group | Bridge, message: KeyValue): void {
|
||||
for (const mockProperty of this.getDiscovered(entity).mockProperties) {
|
||||
if (message[mockProperty.property] === undefined) {
|
||||
message[mockProperty.property] = mockProperty.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy hue -> h, saturation -> s to make homeassistant happy
|
||||
if (message.color !== undefined) {
|
||||
if (message.color.hue !== undefined) {
|
||||
@@ -2263,7 +2249,6 @@ export class HomeAssistant extends Extension {
|
||||
const config: DiscoveryEntry = {
|
||||
type: "device_automation",
|
||||
object_id: `${key}_${value}`,
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
automation_type: "trigger",
|
||||
type: key,
|
||||
@@ -2299,7 +2284,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "binary_sensor",
|
||||
object_id: "connection_state",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Connection state",
|
||||
device_class: "connectivity",
|
||||
@@ -2315,7 +2299,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "binary_sensor",
|
||||
object_id: "restart_required",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Restart required",
|
||||
device_class: "problem",
|
||||
@@ -2333,7 +2316,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "button",
|
||||
object_id: "restart",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Restart",
|
||||
device_class: "restart",
|
||||
@@ -2347,7 +2329,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "select",
|
||||
object_id: "log_level",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Log level",
|
||||
entity_category: "config",
|
||||
@@ -2363,7 +2344,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "sensor",
|
||||
object_id: "version",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Version",
|
||||
icon: "mdi:zigbee",
|
||||
@@ -2376,7 +2356,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "sensor",
|
||||
object_id: "coordinator_version",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Coordinator version",
|
||||
icon: "mdi:chip",
|
||||
@@ -2390,7 +2369,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "sensor",
|
||||
object_id: "network_map",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Network map",
|
||||
entity_category: "diagnostic",
|
||||
@@ -2407,7 +2385,6 @@ export class HomeAssistant extends Extension {
|
||||
{
|
||||
type: "switch",
|
||||
object_id: "permit_join",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: "Permit join",
|
||||
icon: "mdi:human-greeting-proximity",
|
||||
|
||||
@@ -247,13 +247,8 @@ describe("Extension: Frontend", () => {
|
||||
"zigbee2mqtt/bulb_color",
|
||||
stringify({
|
||||
state: "ON",
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
identify: null,
|
||||
power_on_behavior: null,
|
||||
linkquality: 20,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
);
|
||||
@@ -279,13 +274,8 @@ describe("Extension: Frontend", () => {
|
||||
topic: "bulb_color",
|
||||
payload: {
|
||||
state: "ON",
|
||||
power_on_behavior: null,
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
identify: null,
|
||||
linkquality: 20,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -309,13 +299,8 @@ describe("Extension: Frontend", () => {
|
||||
payload: {
|
||||
state: "ON",
|
||||
brightness: 90,
|
||||
power_on_behavior: null,
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
identify: null,
|
||||
linkquality: 20,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -59,6 +59,17 @@ describe("Extension: HomeAssistant", () => {
|
||||
return controller.zigbee.resolveEntity(zhDeviceOrGroup)!;
|
||||
};
|
||||
|
||||
const createDevice = (exposes: zhc.Expose[], definition: object = {}): Device =>
|
||||
({
|
||||
definition,
|
||||
isDevice: (): boolean => true,
|
||||
isGroup: (): boolean => false,
|
||||
endpoint: () => undefined,
|
||||
options: {},
|
||||
exposes: (): zhc.Expose[] => exposes,
|
||||
zh: {endpoints: []},
|
||||
}) as Device;
|
||||
|
||||
beforeAll(async () => {
|
||||
const {getZigbee2MQTTVersion} = await import("../../lib/util/utils.js");
|
||||
z2m_version = (await getZigbee2MQTTVersion()).version;
|
||||
@@ -128,6 +139,115 @@ describe("Extension: HomeAssistant", () => {
|
||||
expect(duplicated).toStrictEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Home Assistant logs `Template variable warning: 'dict object' has no attribute '<property>'`
|
||||
* when a `value_template` reads a property which isn't in the payload, reported since 2021 in
|
||||
* #30086, #20949, #20494, #16571, #24386, #15116, #12825, #11848, #7361, #7139, #7073 and #6987.
|
||||
* Guards must check presence, not truthiness: `0`, `false` and `null` are falsy in Jinja, so
|
||||
* `{% if value_json["x"] %}` and `default('', True)` would drop a legitimate `0`.
|
||||
*/
|
||||
describe("value_json guards", () => {
|
||||
const findUnguardedReads = (template: string): string[] => {
|
||||
const reads = /value_json(?:\[["']([^"'\]]+)["']\]|\.([A-Za-z_]\w*))/g;
|
||||
const unguarded = new Set<string>();
|
||||
|
||||
for (const match of template.matchAll(reads)) {
|
||||
const property = match[1] ?? match[2];
|
||||
// `default(...)` also guards: Jinja's filter type-checks the `Undefined` instead of
|
||||
// stringifying it, so it doesn't log either. Only counts for the expression it's in,
|
||||
// so a second unguarded read elsewhere in the same template is still reported.
|
||||
const expression = template.slice(template.lastIndexOf("{", match.index), template.indexOf("}", match.index));
|
||||
|
||||
if (!template.includes(`"${property}" in value_json`) && !expression.includes("default(")) {
|
||||
unguarded.add(property);
|
||||
}
|
||||
}
|
||||
|
||||
return [...unguarded];
|
||||
};
|
||||
|
||||
// One walk over every zigbee-herdsman-converters definition, shared by the checks below.
|
||||
type GeneratedConfig = {type: string; object_id: string; discovery_payload: KeyValueAny};
|
||||
let generatedConfigs: GeneratedConfig[] | undefined;
|
||||
|
||||
const getGeneratedConfigs = async (): Promise<GeneratedConfig[]> => {
|
||||
if (!generatedConfigs) {
|
||||
settings.set(["advanced", "last_seen"], "ISO_8601");
|
||||
generatedConfigs = [];
|
||||
|
||||
for (const baseDefinition of await getZhcBaseDefinitions()) {
|
||||
const d = zhc.prepareDefinition(baseDefinition);
|
||||
const exposes = typeof d.exposes === "function" ? d.exposes({isDummyDevice: true}, {}) : d.exposes;
|
||||
// @ts-expect-error private
|
||||
generatedConfigs.push(...(extension.getConfigs(createDevice(exposes, d)) as GeneratedConfig[]));
|
||||
}
|
||||
}
|
||||
|
||||
return generatedConfigs;
|
||||
};
|
||||
|
||||
const templatesOf = function* (config: GeneratedConfig): Generator<[string, string]> {
|
||||
for (const [key, value] of Object.entries(config.discovery_payload)) {
|
||||
if (key.endsWith("_template") && typeof value === "string") yield [key, value];
|
||||
}
|
||||
};
|
||||
|
||||
it("Should guard every generated state template against a missing property", async () => {
|
||||
const offenders = new Set<string>();
|
||||
|
||||
for (const config of await getGeneratedConfigs()) {
|
||||
for (const [key, value] of templatesOf(config)) {
|
||||
for (const property of findUnguardedReads(value)) {
|
||||
// Carried on the expose by zigbee-herdsman-converters (`lib/zosung.js`), not built here.
|
||||
if (property === "learned_ir_timings") continue;
|
||||
|
||||
offenders.add(`${config.type}/${config.object_id} ${key} reads '${property}': ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect([...offenders]).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("Should guard the number entity built from a numeric expose with SET access", () => {
|
||||
const exposes = [new zhc.Numeric("effect_speed", zhc.access.ALL).withLabel("Effect speed").withValueMin(0).withValueMax(255)];
|
||||
// @ts-expect-error private
|
||||
const configs = extension.getConfigs(createDevice(exposes));
|
||||
const number = configs.find((config: {type: string}) => config.type === "number");
|
||||
|
||||
assert(number);
|
||||
expect(number.discovery_payload.value_template).toStrictEqual(
|
||||
'{% if "effect_speed" in value_json %}{{ value_json["effect_speed"] }}{% endif %}',
|
||||
);
|
||||
});
|
||||
|
||||
it("Should not guard with a form that drops a legitimate 0, false or null", async () => {
|
||||
const falsySensitive = new Set<string>();
|
||||
|
||||
for (const config of await getGeneratedConfigs()) {
|
||||
for (const [key, value] of templatesOf(config)) {
|
||||
// `default(x, True)` is falsy-sensitive, except on the truncating
|
||||
// text/list/composite template, which only carries strings.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/23199
|
||||
if (/default\([^)]*,\s*True\)/.test(value) && !value.includes("truncate(")) {
|
||||
falsySensitive.add(`${config.type}/${config.object_id} ${key}: ${value}`);
|
||||
}
|
||||
|
||||
// A bare `{% if value_json["x"] %}` tests truthiness; a comparison such as
|
||||
// `{% if value_json["position"] == 0 %}` doesn't, hence the closing `%}`.
|
||||
// Intended for a binary expose, but only once presence is established.
|
||||
for (const match of value.matchAll(/\{%-?\s*if\s+value_json\["([^"\]]+)"\]\s*-?%\}/g)) {
|
||||
if (!value.includes(`"${match[1]}" in value_json`)) {
|
||||
falsySensitive.add(`${config.type}/${config.object_id} ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect([...falsySensitive]).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should mark thermostat configuration toggles as config entities", () => {
|
||||
const switchExposes = [
|
||||
new zhc.Switch().withLabel("Auto lock").withState("auto_lock", false, "Enable/disable auto lock", zhc.access.STATE_SET, "AUTO", "MANUAL"),
|
||||
@@ -248,17 +368,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
});
|
||||
|
||||
it("Should apply expose-level Home Assistant discovery metadata", () => {
|
||||
const createDevice = (exposes: zhc.Expose[]): Device =>
|
||||
({
|
||||
definition: {},
|
||||
isDevice: (): boolean => true,
|
||||
isGroup: (): boolean => false,
|
||||
endpoint: () => undefined,
|
||||
options: {},
|
||||
exposes: (): zhc.Expose[] => exposes,
|
||||
zh: {endpoints: []},
|
||||
}) as Device;
|
||||
|
||||
const voltageExpose = new zhc.Numeric("voltage", zhc.access.STATE).withUnit("V");
|
||||
Object.assign(voltageExpose, {
|
||||
homeassistant: {
|
||||
@@ -282,17 +391,6 @@ describe("Extension: HomeAssistant", () => {
|
||||
});
|
||||
|
||||
it("Should set discovery name to null when expose specifies homeassistant name null", () => {
|
||||
const createDevice = (exposes: zhc.Expose[]): Device =>
|
||||
({
|
||||
definition: {},
|
||||
isDevice: (): boolean => true,
|
||||
isGroup: (): boolean => false,
|
||||
endpoint: () => undefined,
|
||||
options: {},
|
||||
exposes: (): zhc.Expose[] => exposes,
|
||||
zh: {endpoints: []},
|
||||
}) as Device;
|
||||
|
||||
const contactExpose = new zhc.Binary("contact", zhc.access.STATE, false, true).withHomeAssistant({name: null});
|
||||
|
||||
// @ts-expect-error private
|
||||
@@ -411,7 +509,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unique_id: "9_switch_zigbee2mqtt",
|
||||
group: ["0x0017880104e45542_switch_right_zigbee2mqtt"],
|
||||
origin: origin,
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
value_template: '{% if "state" in value_json %}{{ value_json["state"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
@@ -424,7 +522,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -451,7 +549,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
value_template: '{% if "humidity" in value_json %}{{ value_json["humidity"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -478,7 +576,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "hPa",
|
||||
device_class: "atmospheric_pressure",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["pressure"] }}',
|
||||
value_template: '{% if "pressure" in value_json %}{{ value_json["pressure"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_pressure",
|
||||
default_entity_id: "sensor.weather_sensor_pressure",
|
||||
@@ -505,7 +603,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "battery",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["battery"] }}',
|
||||
value_template: '{% if "battery" in value_json %}{{ value_json["battery"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_battery",
|
||||
default_entity_id: "sensor.weather_sensor_battery",
|
||||
@@ -535,7 +633,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
entity_category: "diagnostic",
|
||||
unit_of_measurement: "lqi",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["linkquality"] }}',
|
||||
value_template: '{% if "linkquality" in value_json %}{{ value_json["linkquality"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
name: "Linkquality",
|
||||
object_id: "weather_sensor_linkquality",
|
||||
@@ -577,7 +675,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "switch.wall_switch_double_left",
|
||||
unique_id: "0x0017880104e45542_switch_left_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: '{{ value_json["state_left"] }}',
|
||||
value_template: '{% if "state_left" in value_json %}{{ value_json["state_left"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_left/config", stringify(payload), {
|
||||
@@ -604,7 +702,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "switch.wall_switch_double_right",
|
||||
unique_id: "0x0017880104e45542_switch_right_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: '{{ value_json["state_right"] }}',
|
||||
value_template: '{% if "state_right" in value_json %}{{ value_json["state_right"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_right/config", stringify(payload), {
|
||||
@@ -717,7 +815,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
value_template: '{% if "humidity" in value_json %}{{ value_json["humidity"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -789,7 +887,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -815,7 +913,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
value_template: '{% if "humidity" in value_json %}{{ value_json["humidity"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -842,7 +940,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "hPa",
|
||||
device_class: "atmospheric_pressure",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["pressure"] }}',
|
||||
value_template: '{% if "pressure" in value_json %}{{ value_json["pressure"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
object_id: "weather_sensor_pressure",
|
||||
@@ -900,7 +998,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
object_id: "weather_sensor_temperature",
|
||||
@@ -930,7 +1028,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
value_template: '{% if "humidity" in value_json %}{{ value_json["humidity"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
device: {
|
||||
@@ -973,7 +1071,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -1000,7 +1098,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
value_template: '{% if "humidity" in value_json %}{{ value_json["humidity"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -1063,7 +1161,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "light.my_switch",
|
||||
unique_id: "0x0017880104e45541_light_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
value_template: '{% if "state" in value_json %}{{ value_json["state"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/light/0x0017880104e45541/light/config", stringify(payload), {
|
||||
@@ -1105,15 +1203,17 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover devices with fan", () => {
|
||||
const payload = {
|
||||
state_topic: "zigbee2mqtt/fan",
|
||||
state_value_template: "{{ value_json.fan_state }}",
|
||||
state_value_template: '{% if "fan_state" in value_json %}{{ value_json["fan_state"] }}{% endif %}',
|
||||
command_topic: "zigbee2mqtt/fan/set/fan_state",
|
||||
percentage_state_topic: "zigbee2mqtt/fan",
|
||||
percentage_command_topic: "zigbee2mqtt/fan/set/fan_mode",
|
||||
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json[\"fan_mode\"]] | default('None') }}",
|
||||
percentage_value_template:
|
||||
"{% if \"fan_mode\" in value_json %}{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json[\"fan_mode\"]] | default('None') }}{% endif %}",
|
||||
percentage_command_template: "{{ {0:'off', 1:'low', 2:'medium', 3:'high', 4:'on'}[value] | default('') }}",
|
||||
preset_mode_state_topic: "zigbee2mqtt/fan",
|
||||
preset_mode_command_topic: "zigbee2mqtt/fan/set/fan_mode",
|
||||
preset_mode_value_template: "{{ value_json[\"fan_mode\"] if value_json[\"fan_mode\"] in ['smart'] else 'None' | default('None') }}",
|
||||
preset_mode_value_template:
|
||||
'{% if "fan_mode" in value_json %}{{ value_json["fan_mode"] if value_json["fan_mode"] in [\'smart\'] else \'None\' }}{% endif %}',
|
||||
preset_modes: ["smart"],
|
||||
speed_range_min: 1,
|
||||
speed_range_max: 4,
|
||||
@@ -1181,11 +1281,11 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover devices with speed-controlled fan", () => {
|
||||
const payload = {
|
||||
state_topic: "zigbee2mqtt/fanbee",
|
||||
state_value_template: "{{ value_json.state }}",
|
||||
state_value_template: '{% if "state" in value_json %}{{ value_json["state"] }}{% endif %}',
|
||||
command_topic: "zigbee2mqtt/fanbee/set/state",
|
||||
percentage_state_topic: "zigbee2mqtt/fanbee",
|
||||
percentage_command_topic: "zigbee2mqtt/fanbee/set/speed",
|
||||
percentage_value_template: "{{ value_json[\"speed\"] | default('None') }}",
|
||||
percentage_value_template: '{% if "speed" in value_json %}{{ value_json["speed"] }}{% endif %}',
|
||||
percentage_command_template: "{{ value | default('') }}",
|
||||
speed_range_min: 1,
|
||||
speed_range_max: 254,
|
||||
@@ -1218,7 +1318,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover thermostat devices", () => {
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
"{% if \"running_state\" in value_json %}{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}{% endif %}",
|
||||
action_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
availability: [
|
||||
{
|
||||
@@ -1226,7 +1326,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
value_template: "{{ value_json.state }}",
|
||||
},
|
||||
],
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_template: '{% if "local_temperature" in value_json %}{{ value_json["local_temperature"] }}{% endif %}',
|
||||
current_temperature_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0x0017882104a44559"],
|
||||
@@ -1238,18 +1338,18 @@ describe("Extension: HomeAssistant", () => {
|
||||
},
|
||||
preset_mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/preset",
|
||||
preset_modes: ["schedule", "manual", "boost", "complex", "comfort", "eco", "away"],
|
||||
preset_mode_value_template: '{{ value_json["preset"] }}',
|
||||
preset_mode_value_template: '{% if "preset" in value_json %}{{ value_json["preset"] }}{% endif %}',
|
||||
preset_mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
max_temp: "35",
|
||||
min_temp: "5",
|
||||
mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/system_mode",
|
||||
mode_state_template: '{{ value_json["system_mode"] }}',
|
||||
mode_state_template: '{% if "system_mode" in value_json %}{{ value_json["system_mode"] }}{% endif %}',
|
||||
mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
modes: ["heat", "auto", "off"],
|
||||
name: null,
|
||||
temp_step: 0.5,
|
||||
temperature_command_topic: "zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint",
|
||||
temperature_state_template: '{{ value_json["current_heating_setpoint"] }}',
|
||||
temperature_state_template: '{% if "current_heating_setpoint" in value_json %}{{ value_json["current_heating_setpoint"] }}{% endif %}',
|
||||
temperature_state_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
temperature_unit: "C",
|
||||
object_id: "ts0601_thermostat",
|
||||
@@ -1289,7 +1389,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/thermostat",
|
||||
unique_id: "0x0017880104e45550_pi_heating_demand_zigbee2mqtt",
|
||||
unit_of_measurement: "%",
|
||||
value_template: '{{ value_json["pi_heating_demand"] }}',
|
||||
value_template: '{% if "pi_heating_demand" in value_json %}{{ value_json["pi_heating_demand"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45550/pi_heating_demand/config", stringify(payload), {
|
||||
@@ -1326,7 +1426,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
unique_id: "0x18fc2600000d7ae2_pi_heating_demand_zigbee2mqtt",
|
||||
unit_of_measurement: "%",
|
||||
value_template: '{{ value_json["pi_heating_demand"] }}',
|
||||
value_template: '{% if "pi_heating_demand" in value_json %}{{ value_json["pi_heating_demand"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x18fc2600000d7ae2/pi_heating_demand/config", stringify(payload), {
|
||||
@@ -1338,10 +1438,10 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover Bosch BTH-RA with a compatibility mapping", () => {
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
"{% if \"running_state\" in value_json %}{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}{% endif %}",
|
||||
action_topic: "zigbee2mqtt/bosch_radiator",
|
||||
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_template: '{% if "local_temperature" in value_json %}{{ value_json["local_temperature"] }}{% endif %}',
|
||||
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
|
||||
@@ -1366,7 +1466,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
temp_step: 0.5,
|
||||
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
|
||||
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
|
||||
temperature_state_template: '{% if "occupied_heating_setpoint" in value_json %}{{ value_json["occupied_heating_setpoint"] }}{% endif %}',
|
||||
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
temperature_unit: "C",
|
||||
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
|
||||
@@ -1404,7 +1504,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/0x0017880104e45518",
|
||||
unique_id: "0x0017880104e45518_analog_in_temperature_1_zigbee2mqtt",
|
||||
unit_of_measurement: "°C",
|
||||
value_template: '{{ value_json["analog_in_temperature_1"] }}',
|
||||
value_template: '{% if "analog_in_temperature_1" in value_json %}{{ value_json["analog_in_temperature_1"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
@@ -1440,7 +1540,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
state_topic: "zigbee2mqtt/0x0017880104e45518",
|
||||
unique_id: "0x0017880104e45518_analog_output_2_zigbee2mqtt",
|
||||
value_template: '{{ value_json["analog_output_2"] }}',
|
||||
value_template: '{% if "analog_output_2" in value_json %}{{ value_json["analog_output_2"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x0017880104e45518/analog_output_2/config", stringify(payload), {
|
||||
@@ -1482,10 +1582,10 @@ describe("Extension: HomeAssistant", () => {
|
||||
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
"{% if \"running_state\" in value_json %}{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}{% endif %}",
|
||||
action_topic: "zigbee2mqtt/bosch_radiator",
|
||||
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_template: '{% if "local_temperature" in value_json %}{{ value_json["local_temperature"] }}{% endif %}',
|
||||
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
|
||||
@@ -1499,7 +1599,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
max_temp: "30",
|
||||
min_temp: "5",
|
||||
mode_command_topic: "zigbee2mqtt/bosch_radiator/set/system_mode",
|
||||
mode_state_template: '{{ value_json["system_mode"] }}',
|
||||
mode_state_template: '{% if "system_mode" in value_json %}{{ value_json["system_mode"] }}{% endif %}',
|
||||
mode_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
modes: ["heat"],
|
||||
name: null,
|
||||
@@ -1508,7 +1608,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
temp_step: 0.5,
|
||||
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
|
||||
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
|
||||
temperature_state_template: '{% if "occupied_heating_setpoint" in value_json %}{{ value_json["occupied_heating_setpoint"] }}{% endif %}',
|
||||
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
temperature_unit: "C",
|
||||
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
|
||||
@@ -1550,12 +1650,12 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover Bosch BTH-RM230Z with a current_humidity attribute", () => {
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
"{% if \"running_state\" in value_json %}{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}{% endif %}",
|
||||
action_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
|
||||
current_humidity_template: '{{ value_json["humidity"] }}',
|
||||
current_humidity_template: '{% if "humidity" in value_json %}{{ value_json["humidity"] }}{% endif %}',
|
||||
current_humidity_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_template: '{% if "local_temperature" in value_json %}{{ value_json["local_temperature"] }}{% endif %}',
|
||||
current_temperature_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
default_entity_id: "climate.bosch_rm230z",
|
||||
device: {
|
||||
@@ -1580,10 +1680,12 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin,
|
||||
temp_step: 0.5,
|
||||
temperature_high_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_cooling_setpoint",
|
||||
temperature_high_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
|
||||
temperature_high_state_template:
|
||||
'{% if "occupied_cooling_setpoint" in value_json %}{{ value_json["occupied_cooling_setpoint"] }}{% endif %}',
|
||||
temperature_high_state_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
temperature_low_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_heating_setpoint",
|
||||
temperature_low_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
|
||||
temperature_low_state_template:
|
||||
'{% if "occupied_heating_setpoint" in value_json %}{{ value_json["occupied_heating_setpoint"] }}{% endif %}',
|
||||
temperature_low_state_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
temperature_unit: "C",
|
||||
unique_id: "0x18fc2600000d7ae3_climate_zigbee2mqtt",
|
||||
@@ -1614,7 +1716,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
unique_id: "0x18fc2600000d7ae3_local_temperature_zigbee2mqtt",
|
||||
unit_of_measurement: "°C",
|
||||
value_template: '{{ value_json["local_temperature"] }}',
|
||||
value_template: '{% if "local_temperature" in value_json %}{{ value_json["local_temperature"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x18fc2600000d7ae3/local_temperature/config", stringify(payload), {
|
||||
@@ -1669,7 +1771,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
expect(climate).toBeDefined();
|
||||
expect(climate!.discovery_payload).toMatchObject({
|
||||
temperature_command_topic: "occupied_cooling_setpoint",
|
||||
temperature_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
|
||||
temperature_state_template: '{% if "occupied_cooling_setpoint" in value_json %}{{ value_json["occupied_cooling_setpoint"] }}{% endif %}',
|
||||
temperature_state_topic: true,
|
||||
min_temp: "16",
|
||||
max_temp: "32",
|
||||
@@ -1687,9 +1789,9 @@ describe("Extension: HomeAssistant", () => {
|
||||
position_topic: "zigbee2mqtt/smart vent",
|
||||
set_position_topic: "zigbee2mqtt/smart vent/set",
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_template: '{% if "position" in value_json %}{{ value_json["position"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/smart vent",
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
value_template: '{% if "state" in value_json %}{{ value_json["state"] }}{% endif %}',
|
||||
state_open: "OPEN",
|
||||
state_closed: "CLOSE",
|
||||
state_stopped: "STOP",
|
||||
@@ -1726,7 +1828,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
name: "L6",
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_template: '{% if "position" in value_json %}{{ value_json["position"] }}{% endif %}',
|
||||
position_topic: "zigbee2mqtt/zigfred_plus/l6",
|
||||
set_position_template: '{ "position_l6": {{ position }} }',
|
||||
set_position_topic: "zigbee2mqtt/zigfred_plus/l6/set",
|
||||
@@ -1735,13 +1837,13 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_open: "OPEN",
|
||||
state_topic: "zigbee2mqtt/zigfred_plus/l6",
|
||||
tilt_command_topic: "zigbee2mqtt/zigfred_plus/l6/set/tilt",
|
||||
tilt_status_template: '{{ value_json["tilt"] }}',
|
||||
tilt_status_template: '{% if "tilt" in value_json %}{{ value_json["tilt"] }}{% endif %}',
|
||||
tilt_status_topic: "zigbee2mqtt/zigfred_plus/l6",
|
||||
object_id: "zigfred_plus_l6",
|
||||
default_entity_id: "cover.zigfred_plus_l6",
|
||||
unique_id: "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
value_template: '{% if "state" in value_json %}{{ value_json["state"] }}{% endif %}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config", stringify(payload), {
|
||||
@@ -1773,7 +1875,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "0xa4c138018cf95021_left",
|
||||
default_entity_id: "cover.0xa4c138018cf95021_left",
|
||||
origin: origin,
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_template: '{% if "position" in value_json %}{{ value_json["position"] }}{% endif %}',
|
||||
position_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
|
||||
set_position_template: '{ "position_left": {{ position }} }',
|
||||
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/left/set",
|
||||
@@ -1806,7 +1908,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "0xa4c138018cf95021_right",
|
||||
default_entity_id: "cover.0xa4c138018cf95021_right",
|
||||
origin: origin,
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_template: '{% if "position" in value_json %}{{ value_json["position"] }}{% endif %}',
|
||||
position_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
|
||||
set_position_template: '{ "position_right": {{ position }} }',
|
||||
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/right/set",
|
||||
@@ -1958,7 +2060,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
object_id: "weather_sensor_temperature",
|
||||
@@ -2011,7 +2113,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("Should set missing values to null", async () => {
|
||||
it("Should not add missing values to the payload", async () => {
|
||||
// Missing properties are guarded in the discovery value templates instead.
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/6987
|
||||
const device = devices.WSDCGQ11LM;
|
||||
const data = {measuredValue: -85};
|
||||
@@ -2027,11 +2130,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
await mockZHEvents.message(payload);
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/weather_sensor",
|
||||
stringify({battery: null, humidity: null, linkquality: null, pressure: null, temperature: -0.85, voltage: null}),
|
||||
{retain: false, qos: 1},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/weather_sensor", stringify({temperature: -0.85}), {retain: false, qos: 1});
|
||||
});
|
||||
|
||||
it("Should copy hue/saturtion to h/s if present", async () => {
|
||||
@@ -2047,14 +2146,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
stringify({
|
||||
color: {hue: 0, saturation: 100, h: 0, s: 100},
|
||||
color_mode: "hs",
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
identify: null,
|
||||
linkquality: null,
|
||||
state: null,
|
||||
power_on_behavior: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
);
|
||||
@@ -2073,14 +2165,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
stringify({
|
||||
color: {x: 0.4576, y: 0.41},
|
||||
color_mode: "xy",
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
identify: null,
|
||||
linkquality: null,
|
||||
state: null,
|
||||
power_on_behavior: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
);
|
||||
@@ -2097,14 +2182,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bulb_color",
|
||||
stringify({
|
||||
linkquality: null,
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
identify: null,
|
||||
state: "ON",
|
||||
power_on_behavior: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
);
|
||||
@@ -2149,7 +2228,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
enabled_by_default: true,
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -2209,25 +2288,18 @@ describe("Extension: HomeAssistant", () => {
|
||||
"zigbee2mqtt/bulb",
|
||||
stringify({
|
||||
state: "ON",
|
||||
color_options: null,
|
||||
brightness: 50,
|
||||
color_temp: 370,
|
||||
effect: null,
|
||||
identify: null,
|
||||
linkquality: 99,
|
||||
power_on_behavior: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: true, qos: 0},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/remote",
|
||||
stringify({
|
||||
action_duration: null,
|
||||
battery: null,
|
||||
brightness: 255,
|
||||
linkquality: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: true, qos: 0},
|
||||
);
|
||||
@@ -2250,30 +2322,41 @@ describe("Extension: HomeAssistant", () => {
|
||||
"zigbee2mqtt/bulb",
|
||||
stringify({
|
||||
state: "ON",
|
||||
color_options: null,
|
||||
brightness: 50,
|
||||
color_temp: 370,
|
||||
effect: null,
|
||||
identify: null,
|
||||
linkquality: 99,
|
||||
power_on_behavior: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: true, qos: 0},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/remote",
|
||||
stringify({
|
||||
action_duration: null,
|
||||
battery: null,
|
||||
brightness: 255,
|
||||
linkquality: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
update: {installed_version: -1, latest_version: -1},
|
||||
}),
|
||||
{retain: true, qos: 0},
|
||||
);
|
||||
});
|
||||
|
||||
it("Should keep the versions of an update payload that already has them", async () => {
|
||||
data.writeDefaultState({"0x000b57fffec6a5b2": {state: "ON", update: {state: "idle", installed_version: 1, latest_version: 2}}});
|
||||
// @ts-expect-error private
|
||||
extension.state.load();
|
||||
await resetExtension();
|
||||
await flushPromises();
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await mockMQTTEvents.message("homeassistant/status", "online");
|
||||
await flushPromises();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bulb",
|
||||
stringify({state: "ON", update: {state: "idle", installed_version: 1, latest_version: 2}}),
|
||||
{retain: true, qos: 0},
|
||||
);
|
||||
});
|
||||
|
||||
it("Shouldnt send all status when home assistant comes offline", async () => {
|
||||
data.writeDefaultState();
|
||||
// @ts-expect-error private
|
||||
@@ -2312,7 +2395,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
enabled_by_default: true,
|
||||
state_class: "measurement",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -2382,7 +2465,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor_renamed",
|
||||
object_id: "weather_sensor_renamed_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_renamed_temperature",
|
||||
@@ -2516,7 +2599,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor_renamed",
|
||||
object_id: "weather_sensor_renamed_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_renamed_temperature",
|
||||
@@ -2562,7 +2645,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/bulb",
|
||||
unique_id: "0x000b57fffec6a5b2_update_zigbee2mqtt",
|
||||
value_template:
|
||||
"{\"latest_version\":\"{{ value_json['update']['latest_version'] }}\",\"installed_version\":\"{{ value_json['update']['installed_version'] }}\",\"update_percentage\":{{ value_json['update'].get('progress', 'null') }},\"in_progress\":{{ (value_json['update']['state'] == 'updating')|lower }}}",
|
||||
"{% if \"update\" in value_json %}{\"latest_version\":\"{{ value_json['update']['latest_version'] }}\",\"installed_version\":\"{{ value_json['update']['installed_version'] }}\",\"update_percentage\":{{ value_json['update'].get('progress', 'null') }},\"in_progress\":{{ (value_json['update'].get('state') == 'updating')|lower }}}{% endif %}",
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/update/0x000b57fffec6a5b2/update/config", stringify(payload), {
|
||||
@@ -2605,19 +2688,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
{retain: true, qos: 1},
|
||||
);
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/button",
|
||||
stringify({
|
||||
action: "single",
|
||||
battery: null,
|
||||
identify: null,
|
||||
linkquality: null,
|
||||
voltage: null,
|
||||
power_outage_count: null,
|
||||
device_temperature: null,
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/button", stringify({action: "single"}), {retain: false, qos: 0});
|
||||
|
||||
// Should only discover it once
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
@@ -2741,30 +2812,13 @@ describe("Extension: HomeAssistant", () => {
|
||||
|
||||
await mockMQTTEvents.message("zigbee2mqtt/U202DST600ZB/l2/set", stringify({state: "ON", brightness: 20}));
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/U202DST600ZB",
|
||||
stringify({
|
||||
state_l2: "ON",
|
||||
brightness_l2: 20,
|
||||
linkquality: null,
|
||||
state_l1: null,
|
||||
effect_l1: null,
|
||||
effect_l2: null,
|
||||
power_on_behavior_l1: null,
|
||||
power_on_behavior_l2: null,
|
||||
}),
|
||||
{qos: 0, retain: false},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/U202DST600ZB/l2",
|
||||
stringify({state: "ON", brightness: 20, effect: null, power_on_behavior: null}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/U202DST600ZB/l1",
|
||||
stringify({state: null, effect: null, power_on_behavior: null}),
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/U202DST600ZB", stringify({state_l2: "ON", brightness_l2: 20}), {
|
||||
qos: 0,
|
||||
retain: false,
|
||||
});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/U202DST600ZB/l2", stringify({state: "ON", brightness: 20}), {});
|
||||
// `l1` has no attributes in this payload, so nothing is republished for it.
|
||||
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/U202DST600ZB/l1", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Shouldnt crash in onPublishEntityState on group publish", async () => {
|
||||
@@ -3100,7 +3154,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "sensor.bulb_last_seen",
|
||||
unique_id: "0x000b57fffec6a5b2_last_seen_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: "{{ value_json.last_seen }}",
|
||||
value_template: '{% if "last_seen" in value_json %}{{ value_json["last_seen"] }}{% endif %}',
|
||||
device_class: "timestamp",
|
||||
entity_category: "diagnostic",
|
||||
};
|
||||
@@ -3127,7 +3181,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
value_template: '{% if "temperature" in value_json %}{{ value_json["temperature"] }}{% endif %}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -3494,7 +3548,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
state_topic: "zigbee2mqtt/0x18fc26000000cafe",
|
||||
unique_id: "0x18fc26000000cafe_device_mode_zigbee2mqtt",
|
||||
value_template: '{{ value_json["device_mode"] }}',
|
||||
value_template: '{% if "device_mode" in value_json %}{{ value_json["device_mode"] }}{% endif %}',
|
||||
};
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/select/0x18fc26000000cafe/device_mode/config", stringify(payload), {
|
||||
retain: true,
|
||||
@@ -3525,23 +3579,11 @@ describe("Extension: HomeAssistant", () => {
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/button");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({
|
||||
action: "single",
|
||||
battery: null,
|
||||
identify: null,
|
||||
linkquality: null,
|
||||
voltage: null,
|
||||
power_outage_count: null,
|
||||
device_temperature: null,
|
||||
});
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 0, retain: false});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/button");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({
|
||||
action: "",
|
||||
battery: null,
|
||||
identify: null,
|
||||
linkquality: null,
|
||||
voltage: null,
|
||||
power_outage_count: null,
|
||||
device_temperature: null,
|
||||
});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({qos: 0, retain: false});
|
||||
expect(mockMQTTPublishAsync.mock.calls[2][0]).toStrictEqual("homeassistant/device_automation/0x0017880104e45520/action_single/config");
|
||||
|
||||
Reference in new issue
Block a user