ptvoinfo / zigbee-configurable-firmware

PTVO firmware for CC2530, CC2531, and CC2652 Zigbee chips
https://ptvo.info/zigbee-configurable-firmware-features/
MIT License
207 stars 22 forks source link

How to convert ADC 3.3v reading to percentage? #57

Closed Bram81 closed 2 years ago

Bram81 commented 3 years ago

Hi,

First of all, thanks for your fantastic work! I've been struggeling with an issue for weeks now, so therefore my question. I've build a DIY soil moisture sensor connected to a CC2530 flashed with the ptvo firmware . The analog input is connected to p01 and the value is reported to my Domoticz installation through zigbee2mqtt. The only thing is that I can't succeed in converting the reading to a percentage, saying value x is 0% en y is 100%. Spent a lot of time googling and trying but nothing works or stops zigbee2mqtt from starting. My external converter looks like this:

`const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');

const exposes = zigbeeHerdsmanConverters.exposes; const ea = exposes.access; const e = exposes.presets; const fz = zigbeeHerdsmanConverters.fromZigbeeConverters; const tz = zigbeeHerdsmanConverters.toZigbeeConverters;

const ptvo_switch = zigbeeHerdsmanConverters.findByDevice({modelID: 'ptvo.switch'}); fz.legacy = ptvo_switch.meta.tuyaThermostatPreset;

const device = { zigbeeModel: ['Bodemvochtigheid'], model: 'Bodemvochtigheid', vendor: 'Custom devices (DiY)', description: 'Configurable firmware', fromZigbee: [fz.ignore_basic_report, fz.ptvo_switch_analog_input,], toZigbee: [tz.ptvo_switch_trigger, tz.ptvo_switch_analog_input,], exposes: [e.voltage().withAccess(ea.STATE).withValueMin(0.0).withValueMax(3.3).withEndpoint('l1'), ], meta: { multiEndpoint: true,

},
endpoint: (device) => {
    return {
        l1: 1,
    };
},

};

module.exports = device; `

Can anyone help me in my quest to solve this...? Thanks in advance!

ptvoinfo commented 3 years ago

I think you can do it in Domoticz. Not, in the converter. Otherwise, you should implement your fz.ptvo_switch_analog_input function.

Bram81 commented 3 years ago

I think you can do it in Domoticz. Not, in the converter. Otherwise, you should implement your fz.ptvo_switch_analog_input function.

Hi, Thank you for the quick reply. Unfortunately in Domoticz you can't make any custom kind of template for a device the way it can be done in HA. Could you give a short example of the way I should implement the fz.ptvo_switch_analog_input function?

ptvoinfo commented 3 years ago

Hello @Bram81,

I'm sorry about the delay with my answer. Here is the customized code of a custom converter. I've added fz.ptvo_switch_analog_input2 after fz.legacy = ptvo_switch.meta.tuyaThermostatPreset. You can find the "scale" constant in the code. This scale factor will be applied for all analog values.


fz.ptvo_switch_analog_input2 = {
   cluster: 'genAnalogInput',
   type: ['attributeReport', 'readResponse'],
   convert: (model, msg, publish, options, meta) => {
      const payload = {};
      const channel = msg.endpoint.ID;
      const name = `l${channel}`;
      const endpoint = msg.endpoint;
      const scale = 1;
      payload[name] = precisionRound(msg.data['presentValue'], 3);
      payload[name] *= scale;
      const cluster = 'genLevelCtrl';
      if (endpoint && (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster))) {
          payload['brightness_' + name] = msg.data['presentValue'];
      } else if (msg.data.hasOwnProperty('description')) {
          const data1 = msg.data['description'];
          if (data1) {
              const data2 = data1.split(',');
              const devid = data2[1];
              const unit = data2[0];
              if (devid) {
                  payload['device_' + name] = devid;
              }

              const valRaw = msg.data['presentValue'];
              if (unit) {
                  let val = precisionRound(valRaw, 1);

                  const nameLookup = {
                      'C': 'temperature',
                      '%': 'humidity',
                      'm': 'altitude',
                      'Pa': 'pressure',
                      'ppm': 'quality',
                      'psize': 'particle_size',
                      'V': 'voltage',
                      'A': 'current',
                      'Wh': 'energy',
                      'W': 'power',
                      'Hz': 'frequency',
                      'pf': 'power_factor',
                      'lx': 'illuminance_lux',
                  };

                  let nameAlt = '';
                  if (unit === 'A') {
                      if (valRaw < 1) {
                          val = precisionRound(valRaw, 3);
                      }
                  }
                  if (unit.startsWith('mcpm') || unit.startsWith('ncpm')) {
                      const num = unit.substr(4, 1);
                      nameAlt = (num === 'A')? unit.substr(0, 4) + '10': unit;
                      val = precisionRound(valRaw, 2);
                  } else {
                      nameAlt = nameLookup[unit];
                  }
                  if (nameAlt === undefined) {
                      const valueIndex = parseInt(unit, 10);
                      if (! isNaN(valueIndex)) {
                          nameAlt = 'val' + unit;
                      }
                  }

                  if (nameAlt !== undefined) {
                      payload[nameAlt + '_' + name] = val * scale;
                  }
              }
          }
      }
      return payload;
   }
};

Then change fz.ptvo_switch_analog_input to fz.ptvo_switch_analog_input2 in fromZigbee: [...],

Bram81 commented 3 years ago

Hi, thank you very much! No worries about the delay, realy appreciate you trying to help me out. I can see what the code has to do, but I'm a complete noob when it comes to java.. Where in the code should I implement the scale, saying 1700 is 0% and 800 = 100%?

ptvoinfo commented 3 years ago

If you can show me a formula where X is a source value, I can show you where you should place it in the code above.

Bram81 commented 3 years ago

Hi, that would be great!

The formula would be ( (Vdry - voltage_l1) / (Vdry-Vwet) ) * 100 . So lets say Vdry is 1770 and Vwet is 835 and X is the source value measured, than it would be:

( (1770- X) / (1770-835) ) * 100

ptvoinfo commented 3 years ago

The updated converter with the formula:

function precisionRound(number, precision) {
    if (typeof precision === 'number') {
        const factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    } else if (typeof precision === 'object') {
        const thresholds = Object.keys(precision).map(Number).sort((a, b) => b - a);
        for (const t of thresholds) {
            if (! isNaN(t) && number >= t) {
                return precisionRound(number, precision[t]);
            }
        }
    }
    return number;
}

fz.ptvo_switch_analog_input2 = {
   cluster: 'genAnalogInput',
   type: ['attributeReport', 'readResponse'],
   convert: (model, msg, publish, options, meta) => {
      const payload = {};
      const channel = msg.endpoint.ID;
      const name = `l${channel}`;
      const endpoint = msg.endpoint;

      const value_mv = msg.data['presentValue'] * 1000;
      const value_scaled = ( (1770- value_mv ) / (1770-835) ) * 100;
      payload[name] = precisionRound(value_scaled , 3);

      const cluster = 'genLevelCtrl';
      if (endpoint && (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster))) {
          payload['brightness_' + name] = msg.data['presentValue'];
      } else if (msg.data.hasOwnProperty('description')) {
          const data1 = msg.data['description'];
          if (data1) {
              const data2 = data1.split(',');
              const devid = data2[1];
              const unit = data2[0];
              if (devid) {
                  payload['device_' + name] = devid;
              }

              const valRaw = msg.data['presentValue'];
              if (unit) {
                  let val = precisionRound(valRaw, 1);

                  const nameLookup = {
                      'C': 'temperature',
                      '%': 'humidity',
                      'm': 'altitude',
                      'Pa': 'pressure',
                      'ppm': 'quality',
                      'psize': 'particle_size',
                      'V': 'voltage',
                      'A': 'current',
                      'Wh': 'energy',
                      'W': 'power',
                      'Hz': 'frequency',
                      'pf': 'power_factor',
                      'lx': 'illuminance_lux',
                  };

                  let nameAlt = '';
                  if (unit === 'A') {
                      if (valRaw < 1) {
                          val = precisionRound(valRaw, 3);
                      }
                  }
                  if (unit.startsWith('mcpm') || unit.startsWith('ncpm')) {
                      const num = unit.substr(4, 1);
                      nameAlt = (num === 'A')? unit.substr(0, 4) + '10': unit;
                      val = precisionRound(valRaw, 2);
                  } else {
                      nameAlt = nameLookup[unit];
                  }
                  if (nameAlt === undefined) {
                      const valueIndex = parseInt(unit, 10);
                      if (! isNaN(valueIndex)) {
                          nameAlt = 'val' + unit;
                      }
                  }

                  if (nameAlt !== undefined) {
                      payload[nameAlt + '_' + name] = val;
                  }
              }
          }
      }
      return payload;
   }
};
Bram81 commented 3 years ago

Hi, thanks a million! It seems there is something missing in the code. My log shows:

error 2021-10-31 14:27:26: Failed to call 'Receive' 'onZigbeeEvent' (ReferenceError: precisionRound is not defined at Object.convert (/opt/zigbee2mqtt/lib/util/externally-loaded.js:23:15) at /opt/zigbee2mqtt/lib/extension/receive.js:159:41 at Array.forEach () at Receive.onZigbeeEvent (/opt/zigbee2mqtt/lib/extension/receive.js:158:20) at Controller.callExtensionMethod (/opt/zigbee2mqtt/lib/controller.js:386:44))

Does that make any sense to you?

Bram81 commented 3 years ago

The entire converter files now is as follows:

const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');

const exposes = zigbeeHerdsmanConverters.exposes; const ea = exposes.access; const e = exposes.presets; const fz = zigbeeHerdsmanConverters.fromZigbeeConverters; const tz = zigbeeHerdsmanConverters.toZigbeeConverters;

const ptvo_switch = zigbeeHerdsmanConverters.findByDevice({modelID: 'ptvo.switch'}); fz.legacy = ptvo_switch.meta.tuyaThermostatPreset;

fz.ptvo_switch_analog_input2 = { cluster: 'genAnalogInput', type: ['attributeReport', 'readResponse'], convert: (model, msg, publish, options, meta) => { const payload = {}; const channel = msg.endpoint.ID; const name = l${channel}; const endpoint = msg.endpoint;

  const value_mv = msg.data['presentValue'] * 1000;
  const value_scaled = ( (1770- value_mv ) / (1770-835) ) * 100;
  payload[name] = precisionRound(value_scaled , 3);

  const cluster = 'genLevelCtrl';
  if (endpoint && (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster))) {
      payload['brightness_' + name] = msg.data['presentValue'];
  } else if (msg.data.hasOwnProperty('description')) {
      const data1 = msg.data['description'];
      if (data1) {
          const data2 = data1.split(',');
          const devid = data2[1];
          const unit = data2[0];
          if (devid) {
              payload['device_' + name] = devid;
          }

          const valRaw = msg.data['presentValue'];
          if (unit) {
              let val = precisionRound(valRaw, 1);

              const nameLookup = {
                  'C': 'temperature',
                  '%': 'humidity',
                  'm': 'altitude',
                  'Pa': 'pressure',
                  'ppm': 'quality',
                  'psize': 'particle_size',
                  'V': 'voltage',
                  'A': 'current',
                  'Wh': 'energy',
                  'W': 'power',
                  'Hz': 'frequency',
                  'pf': 'power_factor',
                  'lx': 'illuminance_lux',
              };

              let nameAlt = '';
              if (unit === 'A') {
                  if (valRaw < 1) {
                      val = precisionRound(valRaw, 3);
                  }
              }
              if (unit.startsWith('mcpm') || unit.startsWith('ncpm')) {
                  const num = unit.substr(4, 1);
                  nameAlt = (num === 'A')? unit.substr(0, 4) + '10': unit;
                  val = precisionRound(valRaw, 2);
              } else {
                  nameAlt = nameLookup[unit];
              }
              if (nameAlt === undefined) {
                  const valueIndex = parseInt(unit, 10);
                  if (! isNaN(valueIndex)) {
                      nameAlt = 'val' + unit;
                  }
              }

              if (nameAlt !== undefined) {
                  payload[nameAlt + '_' + name] = val;
              }
          }
      }
  }
  return payload;

} };

const device = { zigbeeModel: ['Bodemvochtigheid'], model: 'Bodemvochtigheid', vendor: 'Custom devices (DiY)', description: 'Configurable firmware', fromZigbee: [fz.ignore_basic_report, fz.ptvo_switch_analog_input2,], toZigbee: [tz.ptvo_switch_trigger, tz.ptvo_switch_analog_input,], exposes: [e.humidity().withAccess(ea.STATE).withValueMin(0.0).withValueMax(3.3).withEndpoint('l1'), ], meta: { multiEndpoint: true,

}, endpoint: (device) => { return { l1: 1, }; }, };

module.exports = device;

ptvoinfo commented 3 years ago

I've copied the corresponding function from z2m to the converter.

function precisionRound(number, precision) {
    if (typeof precision === 'number') {
        const factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    } else if (typeof precision === 'object') {
        const thresholds = Object.keys(precision).map(Number).sort((a, b) => b - a);
        for (const t of thresholds) {
            if (! isNaN(t) && number >= t) {
                return precisionRound(number, precision[t]);
            }
        }
    }
    return number;
}

fz.ptvo_switch_analog_input2 = {
   cluster: 'genAnalogInput',
   type: ['attributeReport', 'readResponse'],
   convert: (model, msg, publish, options, meta) => {
      const payload = {};
      const channel = msg.endpoint.ID;
      const name = `l${channel}`;
      const endpoint = msg.endpoint;

      const value_mv = msg.data['presentValue'] * 1000;
      const value_scaled = ( (1770- value_mv ) / (1770-835) ) * 100;
      payload[name] = precisionRound(value_scaled , 3);

      const cluster = 'genLevelCtrl';
      if (endpoint && (endpoint.supportsInputCluster(cluster) || endpoint.supportsOutputCluster(cluster))) {
          payload['brightness_' + name] = msg.data['presentValue'];
      } else if (msg.data.hasOwnProperty('description')) {
          const data1 = msg.data['description'];
          if (data1) {
              const data2 = data1.split(',');
              const devid = data2[1];
              const unit = data2[0];
              if (devid) {
                  payload['device_' + name] = devid;
              }

              const valRaw = msg.data['presentValue'];
              if (unit) {
                  let val = precisionRound(valRaw, 1);

                  const nameLookup = {
                      'C': 'temperature',
                      '%': 'humidity',
                      'm': 'altitude',
                      'Pa': 'pressure',
                      'ppm': 'quality',
                      'psize': 'particle_size',
                      'V': 'voltage',
                      'A': 'current',
                      'Wh': 'energy',
                      'W': 'power',
                      'Hz': 'frequency',
                      'pf': 'power_factor',
                      'lx': 'illuminance_lux',
                  };

                  let nameAlt = '';
                  if (unit === 'A') {
                      if (valRaw < 1) {
                          val = precisionRound(valRaw, 3);
                      }
                  }
                  if (unit.startsWith('mcpm') || unit.startsWith('ncpm')) {
                      const num = unit.substr(4, 1);
                      nameAlt = (num === 'A')? unit.substr(0, 4) + '10': unit;
                      val = precisionRound(valRaw, 2);
                  } else {
                      nameAlt = nameLookup[unit];
                  }
                  if (nameAlt === undefined) {
                      const valueIndex = parseInt(unit, 10);
                      if (! isNaN(valueIndex)) {
                          nameAlt = 'val' + unit;
                      }
                  }

                  if (nameAlt !== undefined) {
                      payload[nameAlt + '_' + name] = val;
                  }
              }
          }
      }
      return payload;
   }
};
Bram81 commented 3 years ago

Hi, cheers for that! Now the log shows both the calculated and the measured value, but the only value I get in Domoticz is the measured one. I figured I have to tell it to send the other value, but after trying almost everthing which made some sort of sense to me I can't get it to work..

Log shows:

info 2021-11-01 13:29:09: MQTT publish: topic 'zigbee2mqtt/0x00124b001adf71f2', payload '{"l1":113.679,"linkquality":84,"voltage_l1":703}'

I would need the 113.679 but 703 is being sent..

Bram81 commented 2 years ago

Funny, I've just managed to get this working a couple of days ago. Turned out I had to expose both humidity and voltage to get the right value in Domoticz. Working like a charm now, thanks!