Создание внешнего конвертера для zigbee2mqtt

Ответить
dtvims
Site Admin
Сообщения: 151
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Создание внешнего конвертера для zigbee2mqtt

Сообщение dtvims »

External converters for Zigbee2MQTT.
Основа тут: https://www.zigbee2mqtt.io/advanced/more/external_converters.html

Сперва необходимо создать новое устройство или взять готовое. Внешний конвертер используется для не поддерживаемых устройств, чтобы описать их параметры и взаимодействие с координатором.
Так или иначе все что касается конвертера отталкивается от готового устройства.

Начинается устройство с его названия и изготовителя. Эти параметры зашиты в само устройство, в кластер ZCL_CLUSTER_ID_GEN_BASIC. Свойства:
ATTRID_BASIC_MANUFACTURER_NAME - производитель;
ATTRID_BASIC_MODEL_ID - модель.

От этих параметров и будет отталкиваться zigbee2mqtt, когда будет выбирать конвертер.
На таких же данных будут основаны выборы конечных точек, кластеров, типов данных. Вообще в теории можно сделать устройство на стандартных кластерах и их свойствах, но при этом изменить их типы и смысловую нагрузку и сделать соответствующий конвертер, который будет их понимать, но стандартные обработчики этих стандартных кластеров будут сходить с ума. По этому лучше, там где это возможно, использовать стандартные конвертеры, а уже для кастомных свойств, делать свои. Для удобства работы устройства, можно и вместо стандартных конвертеров использовать свои, тогда общение с устройством перейдет на некий новый уровень.

Я создал свое устройство, для считывания показаний электросчетчика, где каждый кластер вынес в отдельную конечную точку (EndPoint), мне показалось так удобнее. Там есть базовый кластер, кластер для учета энергии, кластер учета электроэнергии и 2 кластера температуры. Один и тот же кластер нельзя поместить в одну конечную точку, по этому кластера температуры обязаны находится в разных конечных точках, все остальные кластера разные и их можно было бы объединить в одну EP, но я решил сделать все разные. Именно я так решил (это к вопросу, почему это так и от куда взялось). Датчиков температуры 2, потому и 2 кластера. Датчики ds18b20 работают по протоколу OneWire, и их можно повесить хоть какую пачку на один контакт/провод, но необходимо использовать (знать) адрес датчика, чтобы понимать с кем именно мы имеем дело, для чего везде были созданы соответствующие свойства, значения которых можно менять, хотя именно адреса датчиков собираются устройством в автоматическом режиме, но возможность редактирования позволяет их менять на другие, а также менять местами. У электросчетчика также есть 2 кастомных поля: Адрес и время оправки отчета. Адрес у счетчика Меркурий - Это его серийный номер, потому подлежит модификации. Интервал отправки отчетов задается в секундах, чтобы раз в этот период делать опросы всех устройств и отправлять отчеты (репорты). Отчет можно отправлять только если есть изменения или всегда, что делается настройками отчетов для каждого свойства. Обо всех этих настройках далее в построении своего конвертера.
dtvims
Site Admin
Сообщения: 151
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Начинаем писать конвертер (вешалка в театре)

Сообщение dtvims »

Начинается все с подключения внешних библиотек. Это для конвертера они внешние, а для zigbee2mqtt, они вполне себе внутренние.
В свое основе - это zigbee-herdsman и zigbee-herdsman-converters. В основном, конечно последняя, но я подтяну и первую.

Выглядит это как-то так:

Код: Выделить всё

const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const exposes = (zigbeeHerdsmanConverters.hasOwnProperty('exposes'))?zigbeeHerdsmanConverters.exposes:require("zigbee-herdsman-converters/lib/exposes");
const ea = exposes.access;
const e = exposes.presets;
const fz = zigbeeHerdsmanConverters.fromZigbeeConverters || zigbeeHerdsmanConverters.fromZigbee;
const tz = zigbeeHerdsmanConverters.toZigbeeConverters || zigbeeHerdsmanConverters.toZigbee;
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const {postfixWithEndpointName} = require('zigbee-herdsman-converters/lib/utils');
const {Zcl} = require('zigbee-herdsman');
Константы тут используются как простой способ вызова свойств объектов подтянутых из этих библиотек.
Функция require() возвращает все экспортируемые методы из библиотеки в виде объекта с соответствующими свойствами.
const {Zcl} = require('zigbee-herdsman'); - такая запись вытягивает только тот объект/свойство/функцию, который указан в фигурных скобках (возможен и список).

Сами библиотеки можно найти на гитхабе, например https://github.com/Koenkk/zigbee-herdsman/, где в src будут все исходные коды на языке TypeScript, что основан на JavaScript, но более строг. Подтягивая "zigbee-herdsman" Мы по сути тянем все объекты , которые отмечены как export в файле index.ts. В примере "const {Zcl} = require('zigbee-herdsman');" вытягивается только один объект Zcl.
zigbeeHerdsmanConverters - вытягивается целиком, хотя фактически из него нужны только exposes, из которого пара подклассов и стандартны конвертеры: fz и tz. Отдельно подгружаются еще библиотека (объект) reporting и метод postfixWithEndpointName.

Также могут пригодится следующие константы:

Код: Выделить всё

//const ACCESS_STATE = 0b001, ACCESS_WRITE = 0b010, ACCESS_READ = 0b100;
const ACCESS_STATE = ea.STATE, ACCESS_WRITE = ea.SET, ACCESS_READ = ea.GET;

//const ZCL_DATATYPE_UINT16 = 0x21;
const ZCL_DATATYPE_UINT16 = Zcl.DataType.UINT16;
//const ZCL_DATATYPE_UINT32 = 0x23;
const ZCL_DATATYPE_UINT32 = Zcl.DataType.UINT32;
//const ZCL_DATATYPE_UINT48 = 0x25;
const ZCL_DATATYPE_UINT48 = Zcl.DataType.UINT48;
//const ZCL_DATATYPE_UINT64 = 0x27;
const ZCL_DATATYPE_UINT64 = Zcl.DataType.UINT64;
Следует заметить, что закоментированы цифровые значения, т.к. есть те же значения типов ZCL, что и показано в примере, как можно найти те же значения в библиотеках "zigbee-herdsman".

А вернуть в итоге необходимо будет module.exports, где exports - это и есть наш объект, который и будет конвертером.

P.s. Подключить логгер как стандартный глобальный объект получилось немного нестандартным способом. Согласно инструкции выше возникли трудности. Не получилось сразу получить нужный объект, но вышло вот таким образом:

Код: Выделить всё

const ZHCLogger = require('zigbee-herdsman-converters/lib/logger');
const logger_gl = ZHCLogger.logger;
logger_gl - такое название пришлось дать, чтобы не пересекалось с другими возможными переменными, т.о. случайно выяснилось, что некоторые стандартные описания методов, доступные в примерах в интернете, не соответствуют действительности и logger в явном виде никуда не передается (вроде как он есть с структуре meta как meta.logger), по этому можно переменную называть не logger_gl, а коротко logger. Теперь его можно использовать в любых методах, например вызывая logger_gl.info("Сообщение в лог");
dtvims
Site Admin
Сообщения: 151
Зарегистрирован: Пн авг 02, 2010 2:43 pm

Описание свойств конвертера module.exports

Сообщение dtvims »

Можно сразу создавать свойства у объекта module.exports, но как-то принят создавать отдельно объект, а потом его присваивать в module.exports.
Создадим объект device (можно назвать и по другому).

Код: Выделить всё

const device = {
    zigbeeModel: ['Mercury DTViMS'],
    model: 'Mercury DTViMS',
    description: 'Zigbee Mercury Counter',
    vendor: 'Home DIY',
    fromZigbee: [ ... ],
    toZigbee: [ ... ],
    configure: async (device, coordinatorEndpoint, configureDefinition) => { ...	},
    onEvent: async function(event) { ... }
    exposes: [ ...	],
    endpoint: (device) => { ... return { ... } },
    options: [],
    meta: {},
    definitionWithoutExtend: unknown,
	...
};

- `model`: Идентификатор модели устройства.
- `vendor`: Производитель устройства.
- `description`: Описание устройства.
- `exposes`: Массив объектов, описывающих доступные свойства устройства.
- `fromZigbee`: Массив функций для конвертации данных, поступающих от устройства.
- `toZigbee`: Массив функций для конвертации данных, отправляемых на устройство.
- `configure`: Функция, вызываемая при подключении устройства для настройки устройства.
- `onEvent`: Функция, вызываемая при получении события от устройства.
- `endpoint`: Объект (Функция), описывающий конечные точки устройства.
- `options`: Массив объектов с дополнительными опциями для устройства.
- `meta`: Объект мета настроек устройства.
- `definitionWithoutExtend`: Объект, содержащий определение устройства без расширения.

model и vendor - в паре идентифицируют устройство и внешний конвертер. Именно по ним будет понятно, что именно данный конвертер надо использовать. Т.е. если значения, указанные тут совпадут со значениями, что вернет устройство.

configure - основное свойство - функция, с которого начинается наша конфигурация устройства, когда оно добавляется в систему. Также в zigbee2mqtt есть кнопка, позволяющая произвести реконфигурацию устройства, тогда будет вызван данный метод.
Параметры:
- `device`: Объект устройства, которое конфигурируется.
- `coordinatorEndpoint`: Объект конечной точки координатора (позволяет связать с ним наше устройство)
- `configureDefinition`: Объект описания устройства, что-то вроде this.

Функция должна настраивать конфигурацию именно устройства, хотя там можно что угодно вызывать, но обычно тут настройка текущего оборудования. Сюда входит настройка связей с координатором, настройка отчетов и доп. параметров.
Заметим, что команды, выполняемые ниже в примере, конфигурируют именно устройство, т.е. устройству будут отправлены эти команды и именно устройство запишет себе как необходимо поступать в дальнейшем, кого слушаться и кому отправлять отчеты.
Данная информация также будет отражена в интерфейсе zigbee2mqtt.

Например (комментарии по коду):

Код: Выделить всё

configure: async (device, coordinatorEndpoint) => {
// Получение объектов конечных точек устройства
        const first_endpoint = device.getEndpoint(1);
        const second_endpoint = device.getEndpoint(2);
        const third_endpoint = device.getEndpoint(3);
        const fourth_endpoint = device.getEndpoint(4);
        const fifth_endpoint = device.getEndpoint(5);
//Настройка связей с координатором (конечная точка устройства, конечная точка координатора, список кластеров, для которых устанавливается связь). 
        await reporting.bind(first_endpoint, coordinatorEndpoint, ['genBasic']);
        await reporting.bind(second_endpoint, coordinatorEndpoint, ['haElectricalMeasurement']);
        await reporting.bind(third_endpoint, coordinatorEndpoint, ['seMetering']);
        await reporting.bind(fourth_endpoint, coordinatorEndpoint, ['msTemperatureMeasurement']);
        await reporting.bind(fifth_endpoint, coordinatorEndpoint, ['msTemperatureMeasurement']);
        
//Из соответствующих свойств, можно сразу вычитать их значения (есть сомнения, что это корректно работает из данного места)
        await second_endpoint.read('haElectricalMeasurement', ['acVoltageMultiplier', 'acVoltageDivisor']);
        await second_endpoint.read('haElectricalMeasurement', ['acCurrentMultiplier', 'acCurrentDivisor']);
        await second_endpoint.read('haElectricalMeasurement', ['acPowerMultiplier', 'acPowerDivisor']);
        await third_endpoint.read('seMetering', [0xF001]); // device_address
        await third_endpoint.read('seMetering', [0xF002]); // measurement_period
        await fourth_endpoint.read('msTemperatureMeasurement', [0xF001]); // device_address
        await fifth_endpoint.read('msTemperatureMeasurement', [0xF001]); // device_address
        
//Настройка стандартных отчетов. Все стандартные отчеты можно найти в библиотеке reporting
        await reporting.rmsVoltage(second_endpoint);
        await reporting.rmsCurrent(second_endpoint);
        await reporting.activePower(second_endpoint);
// Вторым параметром можно передать объект, что позволит переопределить некоторые свойства стандартного отчета.
// В данном случае по умолчанию значение 100, а меняется на 1, у параметра reportableChange (названия свойств переопределений иные)
        await reporting.temperature(fourth_endpoint,{change: 1});
        await reporting.temperature(fifth_endpoint,{change: 1});
        
//Конфигурирование отчета без стандартных моделей
//Кластер, наименование аттрибута и его основные параметры
        await third_endpoint.configureReporting('seMetering', [{attribute: 'currentSummDelivered', minimumReportInterval: 10, maximumReportInterval: 3600, reportableChange: 1}]);
        await third_endpoint.configureReporting('seMetering', [{attribute: 'currentTier1SummDelivered', minimumReportInterval: 10, maximumReportInterval: 3600, reportableChange: 1}]);
        await third_endpoint.configureReporting('seMetering', [{attribute: 'currentTier2SummDelivered', minimumReportInterval: 10, maximumReportInterval: 3600, reportableChange: 1}]);
        await third_endpoint.configureReporting('seMetering', [{attribute: 'currentTier3SummDelivered', minimumReportInterval: 10, maximumReportInterval: 3600, reportableChange: 1}]);
        await third_endpoint.configureReporting('seMetering', [{attribute: 'currentTier4SummDelivered', minimumReportInterval: 10, maximumReportInterval: 3600, reportableChange: 1}]);
        
	},
Объект meta - доп. настройки, варианты которых можно поискать тут: zigbee-herdsman-converters/src/lib/types.ts в interface DefinitionMeta.

Метод onEvent используется редко. Пример описания функции:

Код: Выделить всё

async function(event) => {
	// Код для обработки событий от устройства
	// event.type и event.data - основные свойства
}
Вообще я нашел еще несколько вариантов описания данного метода, но не нашел реального подтверждения, что так правильно. Эти варианты ниже НЕ работают! Возможно это устаревшие вызовы.

Код: Выделить всё

async function(type, data, device, settings, state) => {
	// Код для обработки событий от устройства
}
async function(type, data, device, publish, options, meta) => {
	// Код для обработки событий от устройства
}
async function(type, data, device, endpoints, logger) => {
	// Код для обработки событий от устройства
}
Единственные события, которое мне удалось поймать для своего устройства - это событие, когда event.type = "stop" и "start", при перезапуске zigbee2mqtt, хотя событий должно быть больше.
В последствии выяснилось, что вызывается также событие "deviceOptionsChanged" при изменении options устройства ("Настройки (Особые)" - см. далее), но при этом возможно появление события "start" (возможно случайное совпадение).

exposes - Массив со свойствами устройства. Описывает какие свойства будут отображаться у устройства в интерфейсе zigbee2mqtt.
options - Аналогичный массив, но относится к дополнительным параметрам "Настройки (Особые)".
Например:

Код: Выделить всё

exposes: [
// Есть стандартные поля (свойства), как тут для кластера haElectricalMeasurement
        e.power(),
        e.current(), 
        e.voltage(), 
// Нестандартные, которые можно создавать на свое усмотрение. Хотя Для температуры, есть стандартный, тут используется универсальный способ
        e.numeric('temperature', ACCESS_STATE).withUnit('°C').withDescription('Measured temperature value'), 
        e.text('device_address_temp', ACCESS_STATE | ACCESS_WRITE | ACCESS_READ).withDescription('Device Address temp'), 
// temperature_out - это стандартное свойство температуры, но в преобразовании ему было дано новое имя, в отличии от стандартного temperature
// По другому имени, ему можно дать свое описание и другие свойства, чтобы идентичные параметры можно было отличить в интерфейсе
// Аналогично можно сделать добавив в конце метод withEndpoint(StrNameEP)
// exposes.numeric('temperature', ACCESS_STATE).withUnit('°C').withDescription('Measured temperature value').withEndpoint('l1'), 
// Тут свойство имеет имя temperature, но его полное имя будет temperature_l1 - добавится имя точки, такое и будет отличие
// Но для этого заранее необходимо завести алиасы для EP в свойстве endpoint
        e.numeric('temperature_out', ACCESS_STATE).withUnit('°C').withDescription('Measured temperature out value'), 
        e.text('device_address_temp_out', ACCESS_STATE | ACCESS_WRITE | ACCESS_READ).withDescription('Device Address temp out'), 
        e.numeric('energy_t0', ACCESS_STATE).withUnit('kWh').withDescription('Energy on tariff all'), 
        e.numeric('energy_t1', ACCESS_STATE).withUnit('kWh').withDescription('Energy on tariff 1'), 
        e.numeric('energy_t2', ACCESS_STATE).withUnit('kWh').withDescription('Energy on tariff 2'), 
        e.text('device_address', ACCESS_STATE | ACCESS_WRITE | ACCESS_READ).withDescription('Device Address'), 
        e.numeric('measurement_period', ACCESS_STATE | ACCESS_WRITE | ACCESS_READ).withUnit('sec').withDescription('Measurement Period').withValueMin(10).withValueMax(600)
	],
Пример для options аналогичен, но для него есть предопределенные опции в exposes.

Код: Выделить всё

options: [exposes.options.invert_cover()],
endpoint - Тут метод возвращающий список конечных точек, вернее их алиасов
Например:

Код: Выделить всё

endpoint: (device) => {
        return {
            'l1': 1, 'l2': 2
        };
    },
Лично мне не понравилась данная возможность, хотя некоторые конфигурации выглядели бы удобнее и интуитивно понятнее, но в интерфейсе zigbee2mqtt, в разделе EndPoint они отображаются вместе со стандартными номерами конечных точек. Выбор при этом свойств работает только по стандартным номерам EP, а по их алисам выдает ошибку, что для меня вызывает лишь недоумения. Возможно, если бы алиасы работали бы в интерфейсе также корректно, то такой негативно реакции уже не было бы.

fromZigbee и toZigbee - основные свойства описания конвертера. Они являются массивами объектов содержащих описания методы конвертеров.
fromZigbee - тут помещаются обработчики по кластерам, которые обрабатываю сообщения приходящие от устройства. Именно тут можно поместить значения в свойства которые настраивались в exposes. Например, создается свойство итогового объекта "temperature_out", где имя как константа (фиксированное имя) или для задания имени можно использовать метод "postfixWithEndpointName('temperature', msg, model, meta)", который к имени "temperature", добавит значение EP или алиас EP, если в этом есть необходимость. Кстати, аналогичное преобразование имени будет произведено в exposes, у полей, потому там необходимо именно базовые названия давать "e.numeric('temperature' ...)", а там будет дополнительно автоматом выполнено преобразование "postfixWithEndpointName". Можно использовать такой автоматический формат или фиксированный - на усмотрение разработчика (см. примеры реализованных проектов, коих много).
toZigbee - содержит объекты для описания механизмов отправки данных на устройство. Интересно, что там есть 2 метода один для записи данных в Устройств, а второй - это запрос на чтение данных из устройство. В последнем случае смысл в том что устройство само значение не пришлет и его необходимо у него запросить, вот именно этот запрос и отправляется, а ответ уже придет от устройства и будет обработан преобразованием из fromZigbee.
Тут используются названия свойств, которые также определялись в exposes. Внутри метода обработчика уже в зависимости от свойства выбирается конечная точка, кластер и свойство согласно протоколу z-stack и туда отправляется необходимое значения.
Тут есть множество нюансов, потому необходимо остановится детальнее именно на этих свойствах далее.

definitionWithoutExtend - свойство не имеющее реальных примеров. Можно предположить, что в нем можно дать описание устройству, чтобы оно определялось как поддерживаемое, но при этом использовало только стандартные кластера z-stack, что в целом возможно для стандартных устройств типа одиночного датчика температуры. Однако даже для последнего таких примеров реального использования найдено не было. Вероятно был задел на такую возможность, но оно не нашло своего клиента.
dtvims
Site Admin
Сообщения: 151
Зарегистрирован: Пн авг 02, 2010 2:43 pm

fromZigbee - обработка сообщений от устройства

Сообщение dtvims »

Объект fromZigbee содержит коллекцию преобразователей, которые занимаются переводом сообщений от Zigbee-устройств в полезный формат, пригодный для дальнейшей обработки в Zigbee2MQTT. Он устроен как словарь, где ключом выступает название преобразователя, а значением — определение процедуры преобразования.

- `cluster`: Имя Zigbee-кластера, из которого поступают сообщения.
- `type`: Массив типов сообщений, которые поддерживает преобразователь (например, attributes_report, command, raw и т.д.).
- `convert`: Метод, осуществляющий преобразование данных из Zigbee-формата в удобный формат MQTT.

Метод convert имеет формат:

Код: Выделить всё

convert(model, message, publish, options, meta) => {}
- `model`: Объект, представляющий модель устройства (см. выше).
- `message`: Сообщение, пришедшее от MQTT-брокера. message.data - непосредственно содержит данные по свойствам.
- `publish`: Функция для обратной публикации сообщений в MQTT (может понадобиться для обратной связи).
- `options`: Дополнительные параметры конфигурации.
- `meta`: Объект с мета-данными, связанными с сообщением (например, источник, направление, идентификатор сообщения и т.д.).

Функция возвращает объект со свойствами, которые разработчик определил в преобразователе.

Код: Выделить всё

const result = {};
result.deviceParam = "Sample Value";
return result;
В приведенном примере создается произвольное свойство, куда помещается произвольное значение. Это свойство добавиться к параметрам, которые система сможет подтянуть автоматом без учета данного конвертера.
Не всегда устройство присылает данные по всем свойствам, а значит сперва необходимо проверить, пришло ли значение свойства:

Код: Выделить всё

if (msg.data.hasOwnProperty('currentTier1SummDelivered')){
	result.mySummDelivered = msg.data['currentTier1SummDelivered'];
}
Данные присваиваются в результирующую переменную, название которой выбрано разработчиком, и потом может отображаться или менять в exposes используя именно это имя (mySummDelivered).
В "msg.data['PropertyName']" (PropertyName - имя свойства согласно z-stack) данные "сырые", т.е. там может быть значение не того типа, которое ожидается и иногда требует дополнительного преобразования. Устройство может быть реализовано очень разными способами и для нормального использования может потребоваться доп преобразование, например, корректировка на значение погрешности или преобразование типа из массива в число и любое другое.

Код: Выделить всё

if (msg.data.hasOwnProperty('currentTier1SummDelivered')) {
  const data = msg.data['currentTier1SummDelivered'];
  if (Array.isArray(data))
    result.energy_t1 = data[1] / 100
  else
    result.energy_t1 =(data & 0xFFFFFFFF) / 100;
  energy_all += result.energy_t1;
}
В данном случае currentTier1SummDelivered имеет тип uint48. Стандартным типом является uint32. В более ранних версиях Zigbee2MQTT 64-х разрядные числа, ну и любые, что имеют разрядность более 32-х, делились на две части, верхнюю и нижнюю. По сути это был массив из 2-х значений. По этому, для поддержки старой версии, есть проверка на массив. Ввиду особенностей устройства используется значение только нижней части числа.
В более новых версиях Zigbee2MQTT была добавлена поддержка BigInt и старый вариант с массивом перестал поддерживаться. Тут, конечно, получилось неудачно с поддержкой устройств. Устройства входящие в код "zigbee-herdsman-converters" были справлены вместе с этой доработкой, а вот пользовательские конвертеры пострадали. Теперь в data сразу стало значение uint64. Опять же, ввиду особенностей устройства, значение дополнительно преобразовано маской в 48 бит. Поскольку данный код из проекта для счетчика Меркурий, то там хоть используется счетчиком 8 бит для представления подсчитанных киловатт, но сделано это так, что реальное значение поместилось бы и в uint32, потому преобразование делается по маске, и 100-е части - это части одного киловатта, потому деление превращает значение в тип float и отображается в интерфейсе с сотыми частями.
Можно добавить также преобразование "result.energy_all = energy_all.toFixed(3);", что сделает фиксированное число после запятой, но теперь - это уже будет строковое значение. С другой стороны, данное значение нужно только отображать. JavaScript автоматически преобразует типы, т.о. даже если значение будет преобразовано в строку, а затем снова где-то будет использовано как число, то преобразование произойдет автоматически, но необходимо понимать, что арифметическая операция сложении может работать и со строками, и можно получить результатом сложение строк, а не чисел, т.е. результат может оказаться иным, чем ожидается.

Кастомные свойства не имеют названия, но доступны по коду:

Код: Выделить всё

if (msg.data.hasOwnProperty(0xF001)) {
  result[postfixWithEndpointName('device_address', msg, model, meta)] = msg.data[0xF001];
}
postfixWithEndpointName - преобразует имя в понятное Zigbee2MQTT, на случай корректировок с учетом конечных точек. Может быть полезно, когда есть много свойств с одинаковым именем в разных конечных точках, чтобы была возможность их различать. Или можно давать фиксированное имя, но придется проверять номер конечной точки, например через свойство "msg.endpoint.ID", значение которого и будет номер конечной точки.
Режим со многими EP включается настройкой "meta: {"multiEndpoint":true}", см. конфигурацию device.
Например, описание пары конвертеров:

Код: Выделить всё

const fz_local = {
    se_metering: {
        cluster: 'seMetering',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            let energy_all = 0;
            if (msg.data.hasOwnProperty('currentSummDelivered')) {
				result.energy_sum = propertyval(msg,'currentSummDelivered');
            }
            if (msg.data.hasOwnProperty('currentTier1SummDelivered')) {
				result.energy_t1 = propertyval(msg,'currentTier1SummDelivered');
                energy_all += result.energy_t1;
            }
            if (msg.data.hasOwnProperty('currentTier2SummDelivered')) {
                result.energy_t2 = propertyval(msg,'currentTier2SummDelivered');
                energy_all += result.energy_t2;
                }
            if (msg.data.hasOwnProperty('currentTier3SummDelivered')) {
                result.energy_t3 = propertyval(msg,'currentTier3SummDelivered');
                energy_all += result.energy_t3;
            }
            if (msg.data.hasOwnProperty('currentTier4SummDelivered')) {
                result.energy_t4 = propertyval(msg,'currentTier4SummDelivered');
                energy_all += result.energy_t4;
            }
            
            if (msg.data.hasOwnProperty(0xF001)) {
                result[postfixWithEndpointName('device_address', msg, model, meta)] = msg.data[0xF001];
            }
            if (msg.data.hasOwnProperty(0xF002)) {
                result[postfixWithEndpointName('measurement_period', msg, model, meta)] = msg.data[0xF002];
            }
            result.energy_all = energy_all.toFixed(2);
            return result;
        },
    }, 
    temperature:{
        cluster: 'msTemperatureMeasurement',
        type: ['attributeReport', 'readResponse'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            if(msg.endpoint.ID == 4){
                if (msg.data.hasOwnProperty('measuredValue')) {
				    result.temperature = msg.data['measuredValue'];
                }
                if (msg.data.hasOwnProperty(0xF001)) {
                    result[postfixWithEndpointName('device_address_temp', msg, model, meta)] = msg.data[0xF001];
                }
            }
            if(msg.endpoint.ID == 5){
                if (msg.data.hasOwnProperty('measuredValue')) {
				    result.temperature_out = msg.data['measuredValue'];
                }
                if (msg.data.hasOwnProperty(0xF001)) {
                    result[postfixWithEndpointName('device_address_temp_out', msg, model, meta)] = msg.data[0xF001];
                }
            }
            
            return result;
        },
    }
};
В примере есть 2 датчика температуры, они используют один и тот же кластер, потому используют разные конечные точки. Чтобы разместить значения в разных свойствах, есть проверка на номер конечной точки. Метод "postfixWithEndpointName" в данном случае лишний, но ничем не мешает, т.к. алиасы для конечных точек в проекте не используются, да и конфликтов с именами нигде не возникает. По сути метод "postfixWithEndpointName" в данном случае название свойства не меняет.

Результат должен быть помещен в соответствующее свойство описания устройства:

Код: Выделить всё

const device = {
    zigbeeModel: ['Mercury DTViMS'],
    model: 'Mercury DTViMS',
    description: 'Zigbee Mercury Counter',
    vendor: 'Home DIY',
    fromZigbee: [fz.electrical_measurement, fz_local.se_metering, fz_local.temperature],
...
}
Тут fz - это объект со стандартными конвертерами. Из него взят стандартный конвертер "fz.electrical_measurement". Можно было бы и для температуры взять стандартный конвертер, но тогда не было бы интуитивно понятно что за датчики температуры используются, а задумка в том, что один внутренний (в комнате), а другой внешний (на улице). В целом можно было бы добавить алиасы для конечных точек и 5-ю EP назвать out, а 4-ю назвать in, тогда их имена автоматически добавили бы к себе эти алиасы и получились бы эти свойства с необходимыми понятными названиями, тогда можно было бы использовать стандартный конвертер. Но в примере используются еще кастомные свойства, где помещен адрес датчика ds18b20 в 64-х битном формате (это будет важно для toZigbee). Также в данном случае не учитывается поддержка старых версий Zigbee2MQTT, где 64-х разрядное число было представлено как массив.
fz_local - уже объект с пользовательскими конвертерами. Название может быть любым, но для понятности обычно называется именно так. Для использования, нужно передать как в примере в массив по одному значению в каждый элемент массива. Повторно одни и тот же конвертер добавлять не надо, даже если один кластер используется несколько раз.
Ответить