mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-04 02:51:44 +00:00
fix: Home Assistant: support speed-controlled fans (#24483)
This commit is contained in:
@@ -872,15 +872,17 @@ export default class HomeAssistant extends Extension {
|
||||
discovery_payload: {
|
||||
name: null,
|
||||
state_topic: true,
|
||||
state_value_template: '{{ value_json.fan_state }}',
|
||||
command_topic: true,
|
||||
command_topic_postfix: 'fan_state',
|
||||
},
|
||||
};
|
||||
|
||||
const speed = (firstExpose as zhc.Fan).features.filter(isEnumExpose).find((e) => e.name === 'mode');
|
||||
const modeEmulatedSpeed = (firstExpose as zhc.Fan).features.filter(isEnumExpose).find((e) => e.name === 'mode');
|
||||
const nativeSpeed = (firstExpose as zhc.Fan).features.filter(isNumericExpose).find((e) => e.name === 'speed');
|
||||
|
||||
if (speed) {
|
||||
// Exactly one mode needs to be active (logical xor)
|
||||
assert(!modeEmulatedSpeed != !nativeSpeed, 'Fans need to be either mode- or speed-controlled');
|
||||
|
||||
if (modeEmulatedSpeed) {
|
||||
// A fan entity in Home Assistant 2021.3 and above may have a speed,
|
||||
// controlled by a percentage from 1 to 100, and/or non-speed presets.
|
||||
// The MQTT Fan integration allows the speed percentage to be mapped
|
||||
@@ -894,9 +896,9 @@ export default class HomeAssistant extends Extension {
|
||||
// ZCL. This supports a generic ZCL HVAC Fan Control fan. "Off" is
|
||||
// always a valid speed.
|
||||
let speeds = ['off'].concat(
|
||||
['low', 'medium', 'high', '1', '2', '3', '4', '5', '6', '7', '8', '9'].filter((s) => speed.values.includes(s)),
|
||||
['low', 'medium', 'high', '1', '2', '3', '4', '5', '6', '7', '8', '9'].filter((s) => modeEmulatedSpeed.values.includes(s)),
|
||||
);
|
||||
let presets = ['on', 'auto', 'smart'].filter((s) => speed.values.includes(s));
|
||||
let presets = ['on', 'auto', 'smart'].filter((s) => modeEmulatedSpeed.values.includes(s));
|
||||
|
||||
if (['99432'].includes(definition!.model)) {
|
||||
// The Hampton Bay 99432 fan implements 4 speeds using the ZCL
|
||||
@@ -908,22 +910,37 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
const allowed = [...speeds, ...presets];
|
||||
speed.values.forEach((s) => assert(allowed.includes(s.toString())));
|
||||
modeEmulatedSpeed.values.forEach((s) => assert(allowed.includes(s.toString())));
|
||||
const percentValues = speeds.map((s, i) => `'${s}':${i}`).join(', ');
|
||||
const percentCommands = speeds.map((s, i) => `${i}:'${s}'`).join(', ');
|
||||
const presetList = presets.map((s) => `'${s}'`).join(', ');
|
||||
|
||||
discoveryEntry.discovery_payload.percentage_state_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_command_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json.${speed.property}] | default('None') }}`;
|
||||
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_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.${speed.property} if value_json.${speed.property} in [${presetList}] else 'None' | default('None') }}`;
|
||||
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_modes = presets;
|
||||
|
||||
// Emulate state based on mode
|
||||
discoveryEntry.discovery_payload.state_value_template = '{{ value_json.fan_state }}';
|
||||
discoveryEntry.discovery_payload.command_topic_postfix = 'fan_state';
|
||||
} else if (nativeSpeed) {
|
||||
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_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.command_topic_postfix = 'state';
|
||||
}
|
||||
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
@@ -1629,7 +1646,7 @@ export default class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
if (payload.percentage_command_topic) {
|
||||
payload.percentage_command_topic = `${baseTopic}/${commandTopicPrefix}set/fan_mode`;
|
||||
payload.percentage_command_topic = `${baseTopic}/${commandTopicPrefix}set/${payload.percentage_command_topic}`;
|
||||
}
|
||||
|
||||
if (payload.preset_mode_state_topic) {
|
||||
|
||||
@@ -142,6 +142,7 @@ describe('Extension: Bridge', () => {
|
||||
retain: false,
|
||||
},
|
||||
'0x000b57fffec6a5b7': {friendly_name: 'bulb_2', retain: false},
|
||||
'0x00124b00cfcf3298': {friendly_name: 'fanbee', retain: true},
|
||||
'0x0017880104a44559': {friendly_name: 'J1_cover'},
|
||||
'0x0017880104e43559': {friendly_name: 'U202DST600ZB'},
|
||||
'0x0017880104e44559': {friendly_name: '3157100_thermostat'},
|
||||
|
||||
@@ -881,6 +881,42 @@ describe('Extension: HomeAssistant', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Should discover devices with speed-controlled fan', async () => {
|
||||
const payload = {
|
||||
state_topic: 'zigbee2mqtt/fanbee',
|
||||
state_value_template: '{{ value_json.state }}',
|
||||
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_command_template: "{{ value | default('') }}",
|
||||
speed_range_min: 1,
|
||||
speed_range_max: 254,
|
||||
name: null,
|
||||
object_id: 'fanbee',
|
||||
unique_id: '0x00124b00cfcf3298_fan_zigbee2mqtt',
|
||||
origin: origin,
|
||||
device: {
|
||||
identifiers: ['zigbee2mqtt_0x00124b00cfcf3298'],
|
||||
name: 'fanbee',
|
||||
model: 'Fan with valve',
|
||||
model_id: 'FanBee',
|
||||
manufacturer: 'Lorenz Brun',
|
||||
via_device: 'zigbee2mqtt_bridge_0x00124b00120144ae',
|
||||
},
|
||||
availability: [
|
||||
{
|
||||
topic: 'zigbee2mqtt/bridge/state',
|
||||
value_template: '{{ value_json.state }}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const idx = mockMQTTPublishAsync.mock.calls.findIndex((c) => c[0] === 'homeassistant/fan/0x00124b00cfcf3298/fan/config');
|
||||
expect(idx).not.toBe(-1);
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[idx][1])).toStrictEqual(payload);
|
||||
});
|
||||
|
||||
it('Should discover thermostat devices', async () => {
|
||||
const payload = {
|
||||
action_template:
|
||||
|
||||
@@ -205,6 +205,10 @@ export const DEFAULT_CONFIGURATION = {
|
||||
'0x000b57cdfec6a5b3': {
|
||||
friendly_name: 'hue_twilight',
|
||||
},
|
||||
'0x00124b00cfcf3298': {
|
||||
friendly_name: 'fanbee',
|
||||
retain: true,
|
||||
},
|
||||
},
|
||||
groups: {
|
||||
1: {
|
||||
|
||||
@@ -1136,6 +1136,16 @@ export const devices = {
|
||||
undefined,
|
||||
CUSTOM_CLUSTERS,
|
||||
),
|
||||
fanbee: new Device(
|
||||
'Router',
|
||||
'0x00124b00cfcf3298',
|
||||
18129,
|
||||
0xfff1,
|
||||
[new Endpoint(8, [0, 3, 4, 5, 6, 8], []), new Endpoint(242, [], [33])],
|
||||
true,
|
||||
'DC Source',
|
||||
'FanBee1',
|
||||
),
|
||||
};
|
||||
|
||||
export const mockController = {
|
||||
|
||||
Reference in New Issue
Block a user