Merge branch 'dev'

This commit is contained in:
Koen Kanters
2022-09-01 08:37:54 +02:00
9 changed files with 648 additions and 459 deletions
+8 -2
View File
@@ -292,6 +292,7 @@ export default class Groups extends Extension {
groupKey, deviceKey, skipDisableReporting, resolvedEntityEndpoint,
} = parsed;
const message = utils.parseJSON(data.message, data.message);
let changedGroups: Group[] = [];
const responseData: KeyValue = {device: deviceKey};
if (groupKey) {
@@ -320,6 +321,7 @@ export default class Groups extends Extension {
logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`);
await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh);
settings.addDeviceToGroup(resolvedEntityGroup.ID.toString(), keys);
changedGroups.push(resolvedEntityGroup);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
@@ -333,6 +335,7 @@ export default class Groups extends Extension {
logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`);
await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh);
settings.removeDeviceFromGroup(resolvedEntityGroup.ID.toString(), keys);
changedGroups.push(resolvedEntityGroup);
/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
@@ -344,6 +347,7 @@ export default class Groups extends Extension {
}
} else { // remove_all
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
changedGroups = this.zigbee.groups().filter((g) => g.zh.members.includes(resolvedEntityEndpoint));
await resolvedEntityEndpoint.removeFromAllGroups();
for (const settingsGroup of settings.getGroups()) {
settings.removeDeviceFromGroup(settingsGroup.ID.toString(), keys);
@@ -372,8 +376,10 @@ export default class Groups extends Extension {
if (error) {
logger.error(error);
} else {
this.eventBus.emitGroupMembersChanged({
group: resolvedEntityGroup, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting});
for (const group of changedGroups) {
this.eventBus.emitGroupMembersChanged({
group, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting});
}
}
}
}
+53 -58
View File
@@ -375,6 +375,7 @@ export default class HomeAssistant extends Extension {
const tilt = exposes.find((expose) => expose.features.find((e) => e.name === 'tilt'));
const motorState = definitionExposes?.find((e) => e.type === 'enum' && e.name === 'motor_state' &&
e.access === ACCESS_STATE);
const running = definitionExposes?.find((e) => e.type === 'binary' && e.name === 'running');
const discoveryEntry: DiscoveryEntry = {
type: 'cover',
@@ -382,48 +383,38 @@ export default class HomeAssistant extends Extension {
object_id: endpoint ? `cover_${endpoint}` : 'cover',
discovery_payload: {
command_topic_prefix: endpoint,
command_topic: true,
state_topic: true,
},
};
// For covers only supporting tilt don't discover the command/state_topic, otherwise
// HA does not correctly reflect the state
// - https://github.com/home-assistant/core/issues/51793
// - https://github.com/Koenkk/zigbee-herdsman-converters/pull/2663
if (!tilt || (tilt && position)) {
discoveryEntry.discovery_payload.command_topic = true;
discoveryEntry.discovery_payload.state_topic = !position;
discoveryEntry.discovery_payload.command_topic_prefix = endpoint;
// For curtains that have `motor_state` lookup a possible state names and make this
// available for discovery. If the curtains only support the `running` value,
// then we use it anyway. The movement direction is calculated (assumed) in this case.
if (motorState) {
const openingLookup = ['opening', 'open', 'forward', 'up', 'rising'];
const closingLookup = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining'];
const stoppedLookup = ['stopped', 'stop', 'pause', 'paused'];
// For curtains that have `motor_state` lookup a possible state names and make this
// available for discovery. If the curtains only support the `running` value,
// then we use it anyway. The movement direction is calculated (assumed) in this case.
if (motorState) {
const openingLookup = ['opening', 'open', 'forward', 'up', 'rising'];
const closingLookup = ['closing', 'close', 'backward', 'back', 'reverse', 'down', 'declining'];
const stoppedLookup = ['stopped', 'stop', 'pause', 'paused'];
const openingState = motorState.values.find((s) => openingLookup.includes(s.toLowerCase()));
const closingState = motorState.values.find((s) => closingLookup.includes(s.toLowerCase()));
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toLowerCase()));
const openingState = motorState.values.find((s) => openingLookup.includes(s.toLowerCase()));
const closingState = motorState.values.find((s) => closingLookup.includes(s.toLowerCase()));
const stoppedState = motorState.values.find((s) => stoppedLookup.includes(s.toLowerCase()));
if (openingState && closingState && stoppedState) {
discoveryEntry.discovery_payload.state_topic = true;
discoveryEntry.discovery_payload.state_opening = openingState;
discoveryEntry.discovery_payload.state_closing = closingState;
discoveryEntry.discovery_payload.state_stopped = stoppedState;
discoveryEntry.discovery_payload.value_template = `{% if not value_json.motor_state %} ` +
`${stoppedState} {% else %} {{ value_json.motor_state }} {% endif %}`;
}
} else {
const running = definitionExposes?.find((e) => e.type === 'binary' && e.name === 'running');
if (running) {
discoveryEntry.discovery_payload.state_topic = true;
discoveryEntry.discovery_payload.value_template = `{% if not value_json.running %} ` +
`stopped {% else %} {% if value_json.position > 0 %} closing {% else %} ` +
`opening {% endif %} {% endif %}`;
}
if (openingState && closingState && stoppedState) {
discoveryEntry.discovery_payload.state_opening = openingState;
discoveryEntry.discovery_payload.state_closing = closingState;
discoveryEntry.discovery_payload.state_stopped = stoppedState;
discoveryEntry.discovery_payload.value_template = `{% if not value_json.motor_state %} ` +
`${stoppedState} {% else %} {{ value_json.motor_state }} {% endif %}`;
}
} else if (running) {
discoveryEntry.discovery_payload.value_template = `{% if not value_json.running %} ` +
`stopped {% else %} {% if value_json.position > 0 %} closing {% else %} ` +
`opening {% endif %} {% endif %}`;
} else {
discoveryEntry.discovery_payload.value_template = `{{ value_json.state }}`;
discoveryEntry.discovery_payload.state_open = 'OPEN';
discoveryEntry.discovery_payload.state_closed = 'CLOSE';
}
if (!position && !tilt) {
@@ -529,6 +520,7 @@ export default class HomeAssistant extends Extension {
garage_door_contact: {device_class: 'garage_door', payload_on: false, payload_off: true},
eco_mode: {entity_category: 'config', icon: 'mdi:leaf'},
expose_pin: {entity_category: 'config', icon: 'mdi:pin'},
flip_indicator_light: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
gas: {device_class: 'gas'},
invert_cover: {entity_category: 'config', icon: 'mdi:arrow-left-right'},
led_disabled_night: {entity_category: 'config', icon: 'mdi:led-off'},
@@ -802,39 +794,42 @@ export default class HomeAssistant extends Extension {
week: {entity_category: 'config', icon: 'mdi:calendar-clock'},
};
const valueTemplate = firstExpose.access & ACCESS_STATE ?
`{{ value_json.${firstExpose.property} }}` : undefined;
if (firstExpose.access & ACCESS_STATE) {
discoveryEntries.push({
type: 'sensor',
object_id: firstExpose.property,
mockProperties: [{property: firstExpose.property, value: null}],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
value_template: valueTemplate,
enabled_by_default: !(firstExpose.access & ACCESS_SET),
...lookup[firstExpose.name],
},
});
}
/**
* If enum attribute has SET access then expose as SELECT entity too.
* Note: currently both sensor and select are discovered, this is to avoid
* breaking changes for sensors already existing in HA (legacy).
*/
if ((firstExpose.access & ACCESS_SET)) {
discoveryEntries.push({
type: 'select',
object_id: firstExpose.property,
mockProperties: [{property: firstExpose.property, value: null}],
discovery_payload: {
value_template: `{{ value_json.${firstExpose.property} }}`,
state_topic: true,
command_topic_prefix: endpoint,
command_topic: true,
command_topic_postfix: firstExpose.property,
options: firstExpose.values.map((v) => v.toString()),
...lookup[firstExpose.name],
},
});
}
/**
* If enum attribute has SET access then expose as SELECT entity too.
* Note: currently both sensor and select are discovered, this is to avoid
* breaking changes for sensors already existing in HA (legacy).
*/
if ((firstExpose.access & ACCESS_SET)) {
discoveryEntries.push({
type: 'select',
object_id: firstExpose.property,
mockProperties: [], // Already mocked above in case access STATE is supported
discovery_payload: {
value_template: valueTemplate,
state_topic: !!(firstExpose.access & ACCESS_STATE),
command_topic_prefix: endpoint,
command_topic: true,
command_topic_postfix: firstExpose.property,
options: firstExpose.values.map((v) => v.toString()),
...lookup[firstExpose.name],
},
});
}
} else if (firstExpose.type === 'text' || firstExpose.type === 'composite') {
if (firstExpose.access & ACCESS_STATE) {
+2 -3
View File
@@ -98,10 +98,9 @@ export default class MQTT {
}, utils.seconds(10));
logger.info('Connected to MQTT server');
await this.publishStateOnline();
if (this.initialConnect) {
await this.publishStateOnline();
} else {
if (!this.initialConnect) {
this.republishRetainedTimer = setTimeout(() => {
// Republish retained messages in case MQTT broker does not persist them.
// https://github.com/Koenkk/zigbee2mqtt/issues/9629
+558 -374
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "1.27.0",
"version": "1.27.0-dev",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"repository": {
@@ -47,7 +47,7 @@
"humanize-duration": "^3.27.2",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.0",
"jszip": "^3.10.1",
"mkdir-recursive": "^0.4.0",
"moment": "^2.29.4",
"mqtt": "4.3.7",
@@ -60,29 +60,29 @@
"winston-syslog": "^2.6.0",
"winston-transport": "^4.5.0",
"ws": "^8.8.1",
"zigbee-herdsman": "0.14.46",
"zigbee-herdsman-converters": "14.0.583",
"zigbee2mqtt-frontend": "0.6.107"
"zigbee-herdsman": "0.14.51",
"zigbee-herdsman-converters": "14.0.610",
"zigbee2mqtt-frontend": "0.6.110"
},
"devDependencies": {
"@babel/core": "^7.18.9",
"@babel/plugin-proposal-decorators": "^7.18.9",
"@babel/preset-env": "^7.18.9",
"@babel/core": "^7.18.10",
"@babel/plugin-proposal-decorators": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@types/debounce": "^1.2.1",
"@types/finalhandler": "^1.1.1",
"@types/humanize-duration": "^3.27.1",
"@types/jest": "^28.1.6",
"@types/jest": "^28.1.7",
"@types/js-yaml": "^4.0.5",
"@types/object-assign-deep": "^0.4.0",
"@types/rimraf": "^3.0.2",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"babel-jest": "^28.1.3",
"eslint": "^8.20.0",
"eslint": "^8.22.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-jest": "^26.7.0",
"eslint-plugin-jest": "^26.8.5",
"jest": "^28.1.3",
"tmp": "^0.2.1",
"typescript": "^4.7.4"
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -651,7 +651,7 @@ describe('Controller', () => {
MQTT.events['connect']();
await flushPromises();
jest.runOnlyPendingTimers();
expect(MQTT.publish).toHaveBeenCalledTimes(12);
expect(MQTT.publish).toHaveBeenCalledTimes(13);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
});
@@ -662,7 +662,8 @@ describe('Controller', () => {
await flushPromises();
await MQTT.events.message('zigbee2mqtt/bridge/state', 'online');
jest.runOnlyPendingTimers();
expect(MQTT.publish).toHaveBeenCalledTimes(0);
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/state', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
});
it('Should prevent any message being published with retain flag when force_disable_retain is set', async () => {
+4
View File
@@ -811,6 +811,10 @@ describe('HomeAssistant extension', () => {
set_position_topic: 'zigbee2mqtt/smart vent/set',
set_position_template: '{ "position": {{ position }} }',
position_template: '{{ value_json.position }}',
state_topic: 'zigbee2mqtt/smart vent',
value_template: `{{ value_json.state }}`,
state_open: 'OPEN',
state_closed: 'CLOSE',
json_attributes_topic: 'zigbee2mqtt/smart vent',
name: 'smart vent',
unique_id: '0x0017880104e45551_cover_zigbee2mqtt',
+6 -6
View File
@@ -84,7 +84,7 @@ describe('Receive', () => {
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20}), {retain: false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 10, tilt: 20, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function));
// Inverted
MQTT.publish.mockClear();
@@ -92,7 +92,7 @@ describe('Receive', () => {
await zigbeeHerdsman.events.message({data: {currentPositionLiftPercentage: 90, currentPositionTiltPercentage: 80}, cluster: 'closuresWindowCovering', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80}), {retain: false, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/J1_cover', stringify({position: 90, tilt: 80, state: 'OPEN'}), {retain: false, qos: 0}, expect.any(Function));
});
it('Should allow to disable the legacy integration', async () => {
@@ -242,15 +242,15 @@ describe('Receive', () => {
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false});
});
it('Should handle a zigbee message with voltage 3010', async () => {
it('Should handle a zigbee message with voltage 2990', async () => {
const device = zigbeeHerdsman.devices.WXKG02LM_rev1;
const data = {'65281': {'1': 3010}}
const data = {'65281': {'1': 2990}}
const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10};
await zigbeeHerdsman.events.message(payload);
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 46, voltage: 3010});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 93, voltage: 2990});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});
@@ -262,7 +262,7 @@ describe('Receive', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(1);
expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor');
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 56, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, 'device_temperature': 19, 'power_outage_count': 34});
expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, "illuminance_lux": 381, 'voltage': 3045, 'device_temperature': 19, 'power_outage_count': 34});
expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false});
});