mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-11 14:18:50 +00:00
d83085ea7f
* Update zigbee-herdsman and zigbee-shepherd-converters. * Force Aqara S2 Lock endvices (#1764) * Start on zigbee-herdsman controller refactor. * More updates. * Cleanup zapp. * updates. * Propagate adapter disconnected event. * Updates. * Initial refactor to zigbee-herdsman. * Refactor deviceReceive to zigbee-herdsman. * Rename * Refactor deviceConfigure. * Finish bridge config. * Refactor availability. * Active homeassistant extension and more refactors. * Refactor groups. * Enable soft reset. * Activate group membership * Start on tests. * Enable reporting. * Add more controller tests. * Add more tests * Fix linting error. * Data en deviceReceive tests. * Move to zigbee-herdsman-converters. * More device publish tests. * Cleanup dependencies. * Bring device publish coverage to 100. * Bring home assistant test coverage to 100. * Device configure tests. * Attempt to fix tests. * Another attempt. * Another one. * Another one. * Another. * Add wait. * Longer wait. * Debug. * Update dependencies. * Another. * Begin on availability tests. * Improve availability tests. * Complete deviceAvailability tests. * Device bind tests. * More tests. * Begin networkmap refactors. * start on networkmap tests. * Network map tests. * Add utils tests. * Logger tests. * Settings and logger tests. * Ignore some stuff for coverage and add todos. * Add remaining missing tests. * Enforce 100% test coverage. * Start on groups test and refactor entityPublish to resolveEntity * Remove joinPathStorage, not used anymore as group information is stored into zigbee-herdsman database. * Fix linting issues. * Improve tests. * Add groups. * fix group membership. * Group: log names. * Convert MQTT message to string by default. * Fix group name. * Updates. * Revert configuration.yaml. * Add new line. * Fixes. * Updates. * Fix tests. * Ignore soft reset extension.
166 lines
6.5 KiB
JavaScript
166 lines
6.5 KiB
JavaScript
const settings = require('../util/settings');
|
|
const logger = require('../util/logger');
|
|
const utils = require('../util/utils');
|
|
const debounce = require('debounce');
|
|
|
|
class DeviceReceive {
|
|
constructor(zigbee, mqtt, state, publishEntityState) {
|
|
this.zigbee = zigbee;
|
|
this.mqtt = mqtt;
|
|
this.state = state;
|
|
this.publishEntityState = publishEntityState;
|
|
this.coordinator = null;
|
|
this.elapsed = {};
|
|
this.debouncers = {};
|
|
}
|
|
|
|
async onZigbeeStarted() {
|
|
this.coordinator = await this.zigbee.getDevice({type: 'Coordinator'});
|
|
}
|
|
|
|
publishDebounce(ieeeAddr, payload, time) {
|
|
if (!this.debouncers[ieeeAddr]) {
|
|
this.debouncers[ieeeAddr] = {
|
|
payload: {},
|
|
publish: debounce(() => {
|
|
this.publishEntityState(ieeeAddr, this.debouncers[ieeeAddr].payload);
|
|
this.debouncers[ieeeAddr].payload = {};
|
|
}, time * 1000),
|
|
};
|
|
}
|
|
|
|
this.debouncers[ieeeAddr].payload = {...this.debouncers[ieeeAddr].payload, ...payload};
|
|
this.debouncers[ieeeAddr].publish();
|
|
}
|
|
|
|
canHandleEvent(type, data, mappedDevice) {
|
|
if (type !== 'message') {
|
|
return false;
|
|
}
|
|
|
|
if (data.device.ieeeAddr === this.coordinator.ieeeAddr) {
|
|
logger.debug('Ignoring message from coordinator');
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Don't handle re-transmitted Xiaomi messages.
|
|
* https://github.com/Koenkk/zigbee2mqtt/issues/1238
|
|
*
|
|
* Some Xiaomi router devices re-transmit messages from Xiaomi end devices.
|
|
* The source address of these message is set to the one of the Xiaomi router.
|
|
* Therefore it looks like if the message came from the Xiaomi router, while in
|
|
* fact it came from the end device.
|
|
* Handling these message would result in false state updates.
|
|
* The group ID attribute of these message defines the source address of the end device.
|
|
* As the same message is also received directly from the end device, it makes no sense
|
|
* to handle these messages.
|
|
*/
|
|
const hasGroupID = data.hasOwnProperty('groupID') && data.groupID != 0;
|
|
if (utils.isXiaomiDevice(data.device) && utils.isRouter(data.device) && hasGroupID) {
|
|
logger.debug('Skipping re-transmitted Xiaomi message');
|
|
return false;
|
|
}
|
|
|
|
if (data.device.modelID === null && data.device.interviewing) {
|
|
logger.debug(`Skipping message, modelID is undefined and still interviewing`);
|
|
return false;
|
|
}
|
|
|
|
if (!mappedDevice) {
|
|
logger.warn(`Received message from unsupported device with Zigbee model '${data.device.modelID}'`);
|
|
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
onZigbeeEvent(type, data, mappedDevice, settingsDevice) {
|
|
if (!this.canHandleEvent(type, data, mappedDevice)) {
|
|
return;
|
|
}
|
|
|
|
const converters = mappedDevice.fromZigbee.filter((c) => {
|
|
const type = Array.isArray(c.type) ? c.type.includes(data.type) : c.type === data.type;
|
|
return c.cluster === data.cluster && type;
|
|
});
|
|
|
|
// Check if there is an available converter
|
|
if (!converters.length) {
|
|
// Don't log readRsp messages, they are not interesting most of the time.
|
|
if (data.type !== 'readResponse') {
|
|
logger.warn(
|
|
`No converter available for '${mappedDevice.model}' with cluster '${data.cluster}' ` +
|
|
`and type '${data.type}' and data '${JSON.stringify(data.data)}'`
|
|
);
|
|
logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Convert this Zigbee message to a MQTT message.
|
|
// Get payload for the message.
|
|
// - If a payload is returned publish it to the MQTT broker
|
|
// - If NO payload is returned do nothing. This is for non-standard behaviour
|
|
// for e.g. click switches where we need to count number of clicks and detect long presses.
|
|
const publish = (payload) => {
|
|
// Add device linkquality.
|
|
if (data.hasOwnProperty('linkquality')) {
|
|
payload.linkquality = data.linkquality;
|
|
}
|
|
|
|
// Add last seen timestamp
|
|
const now = Date.now();
|
|
if (settings.get().advanced.last_seen !== 'disable') {
|
|
payload.last_seen = utils.formatDate(now, settings.get().advanced.last_seen);
|
|
}
|
|
|
|
if (settings.get().advanced.elapsed) {
|
|
if (this.elapsed[data.device.ieeeAddr]) {
|
|
payload.elapsed = now - this.elapsed[data.device.ieeeAddr];
|
|
}
|
|
|
|
this.elapsed[data.device.ieeeAddr] = now;
|
|
}
|
|
|
|
// Check if we have to debounce
|
|
if (settingsDevice && settingsDevice.hasOwnProperty('debounce')) {
|
|
this.publishDebounce(data.device.ieeeAddr, payload, settingsDevice.debounce);
|
|
} else {
|
|
this.publishEntityState(data.device.ieeeAddr, payload);
|
|
|
|
if (settings.get().homeassistant) {
|
|
/**
|
|
* Publish an empty value for click and action payload, in this way Home Assistant
|
|
* can use Home Assistant entities in automations.
|
|
* https://github.com/Koenkk/zigbee2mqtt/issues/959#issuecomment-480341347
|
|
*/
|
|
Object.keys(payload).forEach((key) => {
|
|
if (['action', 'click'].includes(key)) {
|
|
const counterPayload = {};
|
|
counterPayload[key] = '';
|
|
this.publishEntityState(data.device.ieeeAddr, counterPayload);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
let payload = {};
|
|
converters.forEach((converter) => {
|
|
const options = {...settings.get().device_options, ...settings.getDevice(data.device.ieeeAddr)};
|
|
const converted = converter.convert(mappedDevice, data, publish, options);
|
|
if (converted) {
|
|
payload = {...payload, ...converted};
|
|
}
|
|
});
|
|
|
|
if (Object.keys(payload).length) {
|
|
publish(payload);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = DeviceReceive;
|