alors j’ai bien trouvé une façon de faire mais cela rajoute 1 bouton par valeur dans jeedom (par exemple pour PMTSD_S tu auras 4 bouton, un pour 0, 1, …) alors je ne sais pas mais ça me plait moyen et je ne trouve pas d’expose qui permette d’associer une liste dans jeedom. La solution sans rien modifier au fichier ni dans jeedom c’est de publier sur un topic de z2m mais je m’étais un peu planté lorsque je te l’avais soumis la première fois, il faut dans le topic: zigbee2mqtt/le nom de ton w100/set et dans la valeur à envoyer {"PMTSD_S": "la valeur que tu veux" . Ou alors créer une commande dans ton équipement w100 de jeezigbee de cette manière (un par PMTSD sauf le T qui est déjà géré en slider):
Si tu veux essayer l’ajout de boutons dans l’interface jeedom et me dire ce que tu en penses voici le code à mettre dans le convertisseur (redémarrer z2m après la mise à jour et faire une synchronisation dans jeezigbee):
const {Zcl} = require("zigbee-herdsman");
const fz = require("zigbee-herdsman-converters/converters/fromZigbee");
const tz = require("zigbee-herdsman-converters/converters/toZigbee");
const exposes = require("zigbee-herdsman-converters/lib/exposes");
const { logger } = require("zigbee-herdsman-converters/lib/logger");
const lumi = require("zigbee-herdsman-converters/lib/lumi");
const m = require("zigbee-herdsman-converters/lib/modernExtend");
const e = exposes.presets;
const ea = exposes.access;
const {
lumiAction,
lumiZigbeeOTA,
lumiExternalSensor,
} = lumi.modernExtend;
const NS = "zhc:lumi";
const manufacturerCode = lumi.manufacturerCode;
const W100_0844_req = {
cluster: 'manuSpecificLumi',
type: ['attributeReport', 'readResponse'],
convert: async (model, msg, publish, options, meta) => {
const attr = msg.data[65522];
if (!attr || !Buffer.isBuffer(attr)) return;
const endsWith = Buffer.from([0x08, 0x00, 0x08, 0x44]);
if (attr.slice(-4).equals(endsWith)) {
meta.logger.info(`PMTSD request detected from device ${meta.device.ieeeAddr}`);
// Retrieve PMTSD values from meta.state
const pmtsdValues = {
P: meta.state?.PMTSD_P || '0',
M: meta.state?.PMTSD_M || '0',
T: meta.state?.PMTSD_T || '15.0', // Valeur par défaut décimale
S: meta.state?.PMTSD_S || '0',
D: meta.state?.PMTSD_D || '0'
};
// Send PMTSD frame with stored values
try {
await PMTSD_to_W100.convertSet(meta.device.getEndpoint(1), 'PMTSD_to_W100', pmtsdValues, meta);
meta.logger.info(`PMTSD frame sent for ${meta.device.ieeeAddr}`);
} catch (error) {
meta.logger.error(`Failed to send PMTSD frame: ${error.message}`);
}
return { action: 'W100_PMTSD_request' };
}
},
};
const PMTSD_to_W100 = {
key: ['PMTSD_P', 'PMTSD_M', 'PMTSD_T', 'PMTSD_S', 'PMTSD_D', 'PMTSD_to_W100'],
convertSet: async (entity, key, value, meta) => {
// Retrieve current PMTSD values from meta.state
let pmtsd = {
P: meta.state?.PMTSD_P || '0',
M: meta.state?.PMTSD_M || '0',
T: meta.state?.PMTSD_T || '15.0',
S: meta.state?.PMTSD_S || '0',
D: meta.state?.PMTSD_D || '0'
};
let hasChanged = false;
// Convertir la valeur en chaîne si elle est numérique
const stringValue = value.toString();
logger.debug(`PMTSD_to_W100: Received key=${key}, value=${value}, type=${typeof value}, stringValue=${stringValue}`);
if (key === 'PMTSD_to_W100') {
// Internal call: use value as {P, M, T, S, D} object
pmtsd = { ...pmtsd, ...value };
hasChanged = true; // Force send for internal calls
} else {
// Check if the value has changed
const previousValue = pmtsd[key.replace('PMTSD_', '')];
switch (key) {
case 'PMTSD_P':
if (!['0', '1'].includes(stringValue)) {
logger.error(`Invalid PMTSD_P value: ${stringValue}, expected '0' or '1'`);
throw new Error('PMTSD_P must be "0" (on) or "1" (off)');
}
pmtsd.P = stringValue;
hasChanged = stringValue !== previousValue;
break;
case 'PMTSD_M':
if (!['0', '1', '2'].includes(stringValue)) {
logger.error(`Invalid PMTSD_M value: ${stringValue}, expected '0', '1', or '2'`);
throw new Error('PMTSD_M must be "0" (cooling), "1" (heating), or "2" (auto)');
}
pmtsd.M = stringValue;
hasChanged = stringValue !== previousValue;
break;
case 'PMTSD_T':
const numValue = parseFloat(stringValue);
if (isNaN(numValue) || numValue < 15.0 || numValue > 30.0 || (numValue * 10) % 1 !== 0) {
logger.error(`Invalid PMTSD_T value: ${stringValue}, expected 15.0 to 30.0 with 0.1 step`);
throw new Error('PMTSD_T must be a number between 15.0 and 30.0 with one decimal place');
}
pmtsd.T = numValue.toFixed(1);
hasChanged = stringValue !== previousValue;
break;
case 'PMTSD_S':
if (!['0', '1', '2', '3'].includes(stringValue)) {
logger.error(`Invalid PMTSD_S value: ${stringValue}, expected '0', '1', '2', or '3'`);
throw new Error('PMTSD_S must be "0" (auto), "1", "2", or "3"');
}
pmtsd.S = stringValue;
hasChanged = stringValue !== previousValue;
break;
case 'PMTSD_D':
if (!['0', '1'].includes(stringValue)) {
logger.error(`Invalid PMTSD_D value: ${stringValue}, expected '0' or '1'`);
throw new Error('PMTSD_D must be "0" or "1"');
}
pmtsd.D = stringValue;
hasChanged = stringValue !== previousValue;
break;
default:
logger.error(`Unrecognized key: ${key}`);
throw new Error(`Unrecognized key: ${key}`);
}
}
// Log update
logger.info(`Processed ${key}, PMTSD: ${JSON.stringify(pmtsd)}, Changed: ${hasChanged}`);
// Update state, even if no frame is sent
const stateUpdate = { state: { PMTSD_P: pmtsd.P, PMTSD_M: pmtsd.M, PMTSD_T: pmtsd.T, PMTSD_S: pmtsd.S, PMTSD_D: pmtsd.D } };
// Check if all PMTSD values are defined
const { P, M, T, S, D } = pmtsd;
if (!P || !M || !T || !S || !D) {
logger.info(`PMTSD frame not sent: missing values (P:${P}, M:${M}, T:${T}, S:${S}, D:${D})`);
return stateUpdate;
}
// Do not send frame if no value changed (except for internal calls)
if (!hasChanged) {
logger.info(`PMTSD frame not sent: no value change`);
return stateUpdate;
}
// Utiliser T tel quel avec le point décimal
const pmtsdStr = `P${P}_M${M}_T${T}_S${S}_D${D}`;
const pmtsdBytes = Array.from(pmtsdStr).map(c => c.charCodeAt(0));
const pmtsdLen = pmtsdBytes.length;
const fixedHeader = [
0xAA, 0x71, 0x1F, 0x44,
0x00, 0x00, 0x05, 0x41, 0x1C,
0x00, 0x00,
0x54, 0xEF, 0x44, 0x80, 0x71, 0x1A,
0x08, 0x00, 0x08, 0x44, pmtsdLen,
];
const counter = Math.floor(Math.random() * 256);
fixedHeader[4] = counter;
const fullPayload = [...fixedHeader, ...pmtsdBytes];
const checksum = fullPayload.reduce((sum, b) => sum + b, 0) & 0xFF;
fullPayload[5] = checksum;
// Ensure entity is an Endpoint
const endpoint = entity.getEndpoint ? entity.getEndpoint(1) : entity;
if (!endpoint || typeof endpoint.write !== 'function') {
logger.error(`Invalid endpoint for write: ${JSON.stringify(endpoint)}`);
throw new Error('Endpoint does not support write operation');
}
await endpoint.write(
64704,
{ 65522: { value: Buffer.from(fullPayload), type: 65 } },
{ manufacturerCode: 4447, disableDefaultResponse: true },
);
logger.info(`PMTSD frame sent: ${pmtsdStr}`);
return stateUpdate;
},
convertGet: async (entity, key, meta) => {
// Return persisted value from meta.state
return { [key]: meta.state?.[key] || (key === 'PMTSD_T' ? '15.0' : '0') };
},
};
const PMTSD_from_W100 = {
cluster: 'manuSpecificLumi',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const data = msg.data[65522];
if (!data || !Buffer.isBuffer(data)) return;
const endsWith = Buffer.from([0x08, 0x44]);
const idx = data.indexOf(endsWith);
if (idx === -1 || idx + 2 >= data.length) return;
const payloadLen = data[idx + 2];
const payloadStart = idx + 3;
const payloadEnd = payloadStart + payloadLen;
if (payloadEnd > data.length) return;
const payloadBytes = data.slice(payloadStart, payloadEnd);
let payloadAscii;
try {
payloadAscii = payloadBytes.toString('ascii');
} catch {
return;
}
// Log initial state for debugging
meta.logger.info(`Initial meta.state: ${JSON.stringify(meta.state)}`);
const result = {};
const stateUpdate = { state: {} };
const partsForCombined = [];
const pairs = payloadAscii.split('_');
pairs.forEach(p => {
if (p.length >= 2) {
const key = p[0].toLowerCase(); // Normaliser la clé en minuscules
const value = p.slice(1);
let newKey;
let stateKey;
let processedValue = value;
switch (key) {
case 'p':
newKey = 'PW';
stateKey = 'PMTSD_P';
break;
case 'm':
newKey = 'MW';
stateKey = 'PMTSD_M';
break;
case 't':
newKey = 'TW';
stateKey = 'PMTSD_T';
// Valider que TW est un nombre décimal avec un chiffre après la virgule
const numValue = parseFloat(value);
if (!isNaN(numValue) && numValue >= 15.0 && numValue <= 30.0 && (numValue * 10) % 1 === 0) {
processedValue = numValue.toFixed(1);
} else {
return; // Ignorer les valeurs invalides
}
break;
case 's':
newKey = 'SW';
stateKey = 'PMTSD_S';
break;
case 'd':
newKey = 'DW';
stateKey = 'PMTSD_D';
break;
default:
newKey = key.toUpperCase() + 'W';
stateKey = null;
}
result[newKey] = value; // Conserver la valeur brute pour PW, MW, TW, etc.
if (stateKey) {
stateUpdate.state[stateKey] = processedValue;
result[stateKey] = processedValue; // Publier la valeur traitée
}
partsForCombined.push(`${newKey}${value}`);
}
});
// Formater la date et l'heure
const date = new Date();
const formattedDate = date.toLocaleString('fr-FR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
}).replace(/,/, '').replace(/(\d+)\/(\d+)\/(\d+)/, '$3-$2-$1'); // Format: YYYY-MM-DD HH:mm:ss
const combinedString = partsForCombined.length
? `${formattedDate}_${partsForCombined.join('_')}`
: `${formattedDate}`;
// Log updated state for debugging
meta.logger.info(`PMTSD decoded: ${JSON.stringify(result)} from ${meta.device.ieeeAddr}`);
meta.logger.info(`Updated meta.state: ${JSON.stringify({ ...meta.state, ...stateUpdate.state })}`);
return {
...result,
PMTSD_from_W100_Data: combinedString,
...stateUpdate
};
},
};
const Thermostat_Mode = {
key: ['Thermostat_Mode'],
convertSet: async (entity, key, value, meta) => {
const deviceMac = meta.device.ieeeAddr.replace(/^0x/, '').toLowerCase();
const hubMac = '54ef4480711a';
function cleanMac(mac, expectedLen) {
const cleaned = mac.replace(/[:\-]/g, '');
if (cleaned.length !== expectedLen) {
throw new Error(`MAC address must contain ${expectedLen} hexadecimal digits`);
}
return cleaned;
}
const dev = Buffer.from(cleanMac(deviceMac, 16), 'hex');
const hub = Buffer.from(cleanMac(hubMac, 12), 'hex');
// Ensure entity is an Endpoint
const endpoint = entity.getEndpoint ? entity.getEndpoint(1) : entity;
if (!endpoint || typeof endpoint.write !== 'function') {
logger.error(`Invalid endpoint for write: ${JSON.stringify(endpoint)}`);
throw new Error('Endpoint does not support write operation');
}
let frame;
if (value === 'ON') {
const prefix = Buffer.from('aa713244', 'hex');
const messageAlea = Buffer.from([Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)]);
const zigbeeHeader = Buffer.from('02412f6891', 'hex');
const messageId = Buffer.from([Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)]);
const control = Buffer.from([0x18]);
const payloadMacs = Buffer.concat([dev, Buffer.from('0000', 'hex'), hub]);
const payloadTail = Buffer.from('08000844150a0109e7a9bae8b083e58a9f000000000001012a40', 'hex');
frame = Buffer.concat([prefix, messageAlea, zigbeeHeader, messageId, control, payloadMacs, payloadTail]);
// Log the frame for debugging
logger.info(`Thermostat_Mode ON frame: ${frame.toString('hex')}`);
await endpoint.write(
64704,
{ 65522: { value: frame, type: 0x41 } },
{ manufacturerCode: 4447, disableDefaultResponse: true },
);
} else {
const prefix = Buffer.from([
0xaa, 0x71, 0x1c, 0x44, 0x69, 0x1c,
0x04, 0x41, 0x19, 0x68, 0x91
]);
const frameId = Buffer.from([Math.floor(Math.random() * 256)]);
const seq = Buffer.from([Math.floor(Math.random() * 256)]);
const control = Buffer.from([0x18]);
frame = Buffer.concat([prefix, frameId, seq, control, dev]);
if (frame.length < 34) {
frame = Buffer.concat([frame, Buffer.alloc(34 - frame.length, 0x00)]);
}
await endpoint.write(
64704,
{ 65522: { value: frame, type: 0x41 } },
{ manufacturerCode: 4447, disableDefaultResponse: true },
);
}
logger.info(`Thermostat_Mode set to ${value}`);
return {};
},
};
module.exports = {
zigbeeModel: ["lumi.sensor_ht.agl001"],
model: "TH-S04D",
vendor: "Aqara",
description: "Climate Sensor W100",
fromZigbee: [W100_0844_req, PMTSD_from_W100],
toZigbee: [PMTSD_to_W100, Thermostat_Mode],
exposes: [
e.binary('Thermostat_Mode', ea.ALL, 'ON', 'OFF').withDescription('ON: Enables thermostat mode, buttons send encrypted payloads, and the middle line is displayed. OFF: Disables thermostat mode, buttons send actions, and the middle line is hidden.'),
e.action(['W100_PMTSD_request']).withDescription('PMTSD request sent by the W100 via the 08000844 sequence'),
e.text('PMTSD_from_W100_Data', ea.STATE).withDescription('Latest PMTSD values sent by the W100 when manually changed, formatted as "YYYY-MM-DD HH:mm:ss_Px_Mx_Tx_Sx_Dx"'),
e.enum('PMTSD_P', ea.ALL, ['0', '1']).withDescription('Value P: activates or not the operation of the thermostat => 0 = on, 1 = off'),
e.enum('PMTSD_M', ea.ALL, ['0', '1', '2']).withDescription('Value M: thermostat operating mode => 0 = cooling, 1 = heating, 2 = auto'),
e.numeric('PMTSD_T', ea.ALL).withValueMin(15.0).withValueMax(30.0).withValueStep(0.1).withUnit('°C').withDescription('Value T: setpoint temperature'),
e.enum('PMTSD_S', ea.ALL, ['0', '1', '2', '3']).withDescription('Value S: speed => 0 = auto, 1 to 3'),
e.enum('PMTSD_D', ea.ALL, ['0', '1']).withDescription('Value D: wind mode => 0 or 1'),
],
extend: [
lumiZigbeeOTA(),
m.temperature(),
m.humidity(),
lumiExternalSensor(),
m.deviceEndpoints({endpoints: {plus: 1, center: 2, minus: 3}}),
lumiAction({
actionLookup: {hold: 0, single: 1, double: 2, release: 255},
endpointNames: ["plus", "center", "minus"],
}),
m.binary({
name: "Auto_Hide_Middle_Line",
cluster: "manuSpecificLumi",
attribute: {ID: 0x0173, type: Zcl.DataType.BOOLEAN},
valueOn: [true, 0],
valueOff: [false, 1],
description: "Applies only when thermostat mode is enabled. True: Hides the middle line after 30 seconds of inactivity. False: Always displays the middle line.",
access: "ALL",
entityCategory: "config",
zigbeeCommandOptions: {manufacturerCode},
reporting: false,
}),
m.numeric({
name: "high_temperature",
valueMin: 26,
valueMax: 60,
valueStep: 0.5,
scale: 100,
unit: "°C",
cluster: "manuSpecificLumi",
attribute: {ID: 0x0167, type: Zcl.DataType.INT16},
description: "High temperature alert",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "low_temperature",
valueMin: -20,
valueMax: 20,
valueStep: 0.5,
scale: 100,
unit: "°C",
cluster: "manuSpecificLumi",
attribute: {ID: 0x0166, type: Zcl.DataType.INT16},
description: "Low temperature alert",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "high_humidity",
valueMin: 65,
valueMax: 100,
valueStep: 1,
scale: 100,
unit: "%",
cluster: "manuSpecificLumi",
attribute: {ID: 0x016e, type: Zcl.DataType.INT16},
description: "High humidity alert",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "low_humidity",
valueMin: 0,
valueMax: 30,
valueStep: 1,
scale: 100,
unit: "%",
cluster: "manuSpecificLumi",
attribute: {ID: 0x016d, type: Zcl.DataType.INT16},
description: "Low humidity alert",
zigbeeCommandOptions: {manufacturerCode},
}),
m.enumLookup({
name: "sampling",
lookup: {low: 1, standard: 2, high: 3, custom: 4},
cluster: "manuSpecificLumi",
attribute: {ID: 0x0170, type: Zcl.DataType.UINT8},
description: "Temperature and humidity sampling settings",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "period",
valueMin: 0.5,
valueMax: 600,
valueStep: 0.5,
scale: 1000,
unit: "sec",
cluster: "manuSpecificLumi",
attribute: {ID: 0x0162, type: Zcl.DataType.UINT32},
description: "Sampling period",
zigbeeCommandOptions: {manufacturerCode},
}),
m.enumLookup({
name: "temp_report_mode",
lookup: {no: 0, threshold: 1, period: 2, threshold_period: 3},
cluster: "manuSpecificLumi",
attribute: {ID: 0x0165, type: Zcl.DataType.UINT8},
description: "Temperature reporting mode",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "temp_period",
valueMin: 1,
valueMax: 10,
valueStep: 1,
scale: 1000,
unit: "sec",
cluster: "manuSpecificLumi",
attribute: {ID: 0x0163, type: Zcl.DataType.UINT32},
description: "Temperature reporting period",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "temp_threshold",
valueMin: 0.2,
valueMax: 3,
valueStep: 0.1,
scale: 100,
unit: "°C",
cluster: "manuSpecificLumi",
attribute: {ID: 0x0164, type: Zcl.DataType.UINT16},
description: "Temperature reporting threshold",
zigbeeCommandOptions: {manufacturerCode},
}),
m.enumLookup({
name: "humi_report_mode",
lookup: {no: 0, threshold: 1, period: 2, threshold_period: 3},
cluster: "manuSpecificLumi",
attribute: {ID: 0x016c, type: Zcl.DataType.UINT8},
description: "Humidity reporting mode",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "humi_period",
valueMin: 1,
valueMax: 10,
valueStep: 1,
scale: 1000,
unit: "sec",
cluster: "manuSpecificLumi",
attribute: {ID: 0x016a, type: Zcl.DataType.UINT32},
description: "Humidity reporting period",
zigbeeCommandOptions: {manufacturerCode},
}),
m.numeric({
name: "humi_threshold",
valueMin: 2,
valueMax: 10,
valueStep: 0.5,
scale: 100,
unit: "%",
cluster: "manuSpecificLumi",
attribute: {ID: 0x016b, type: Zcl.DataType.UINT16},
description: "Humidity reporting threshold",
zigbeeCommandOptions: {manufacturerCode},
}),
m.identify(),
],
};