Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: speed-controlled fans #8188

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/converters/fromZigbee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ const converters1 = {
}
},
} satisfies Fz.Converter,
fan_speed: {
cluster: 'genLevelCtrl',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
if (msg.data.currentLevel !== undefined) {
const property = postfixWithEndpointName('fan_speed', msg, model, meta);
return {[property]: msg.data['currentLevel']};
}
},
} satisfies Fz.Converter,
thermostat: {
cluster: 'hvacThermostat',
type: ['attributeReport', 'readResponse'],
Expand Down
16 changes: 16 additions & 0 deletions src/converters/toZigbee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,6 +1554,22 @@ const converters2 = {
await entity.read('hvacFanCtrl', ['fanMode']);
},
} satisfies Tz.Converter,
fan_speed: {
key: ['fan_speed'],
convertSet: async (entity, key, value, meta) => {
utils.assertNumber(value);
await entity.command(
'genLevelCtrl',
'moveToLevelWithOnOff',
{level: value.toString(), transtime: 0},
utils.getOptions(meta.mapped, entity),
);
return {state: {fan_speed: value}, readAfterWriteTime: 0};
},
convertGet: async (entity, key, meta) => {
await entity.read('genLevelCtrl', ['currentLevel']);
},
} satisfies Tz.Converter,
thermostat_local_temperature: {
key: ['local_temperature'],
convertGet: async (entity, key, meta) => {
Expand Down
11 changes: 11 additions & 0 deletions src/devices/custom_devices_diy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,17 @@ const definitions: DefinitionWithExtend[] = [
}),
],
},
{
zigbeeModel: ['FanBee1', 'Fanbox2'],
model: 'FanBee',
vendor: 'Lorenz Brun',
description: 'Fan with valve',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide a link to this device? Note to self; after this merge Koenkk/zigbee2mqtt#24483

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a custom device built by me. I'll eventually open-source the design files and firmware, but right now there is no public docs or anything for it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For those type of devices, you can use the external converters (there is no point of adding it to the list of officially supported devices if nobody can get it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, but for the tests we need a device which exercises this behavior. There is an Inovelli fan which looks like it could also use this fan control model but as I do not own one of them I didn't feel comfortable changing it over to the new model (also this is a breaking change for anyone using the MQTT API).

I do intend on making this device publicly available.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do intend on making this device publicly available.

Good, then I will merge it once this is done.

fromZigbee: [fz.on_off, fz.fan_speed],
toZigbee: [tz.on_off, tz.fan_speed],
exposes: [
e.fan().withSpeed(),
],
},
];

export default definitions;
Expand Down
9 changes: 8 additions & 1 deletion src/lib/exposes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,14 +560,21 @@ export class Fan extends Base {
constructor() {
super();
this.type = 'fan';
this.addFeature(new Binary('state', access.ALL, 'ON', 'OFF').withDescription('On/off state of this fan').withProperty('fan_state'));
}

withModes(modes: string[], access = a.ALL) {
this.addFeature(new Binary('state', access, 'ON', 'OFF').withDescription('On/off state of this fan').withProperty('fan_state'));
this.addFeature(new Enum('mode', access, modes).withProperty('fan_mode').withDescription('Mode of this fan'));
return this;
}

withSpeed(minSpeed = 1, maxSpeed = 254, access = a.ALL) {
this.addFeature(new Binary('state', access, 'ON', 'OFF').withDescription('On/off state of this fan').withProperty('state'));
assert(this.features.findIndex(f => f.name === 'mode') === -1, 'Fan can only be either mode or speed-controlled, not both');
this.addFeature(new Numeric('speed', access).withProperty('fan_speed').withValueMin(minSpeed).withValueMax(maxSpeed).withDescription('Speed of this fan'));
return this;
}

clone(): Fan {
const clone = new Fan();
this.copy(clone);
Expand Down
Loading