Working with custom "external component"

Hi all,

I am a (happy ?) owner of a mechanical ventilation Nather Optimea which has an undocumented modbus RTC interface. I am sure you get my endgoal: I want to integrate it into HA through ESPhome. Sadly my HA box is not close to this MV and to be honest I do want to develop my esphome skills and this excuse is as good as any. :nerd_face:

So through try and errors, I eventually managed to discover all the nather optimea sensors modbus registers and I am now capable to read them with a straightforward python script.

The second step plan was to base my custom “nather_optimea” component from the official pzemdc. Indeed, the pzemdc component is a modbus sensor component, how hard can it be to adapt it to the MV and integrate it with the new ESPHome “external component” feature. My optimist past self though “easy peasy”… And here I am begging for help! :sweat_smile:

What I did so far:

  1. I copy paste the pzemdc folder component (.py, .cpp .h files) to my esphome config folder (containing the YAML files) as said in the external component doc
  2. Rename everything (files and variables) from “pzemdc” to “nather_optimea”, and only leave one sensor to begin. (see source code below);
  3. Build a new esphome config file and try to compile it through the esphome GUI (last docker based version).
  4. I get the following error :

I don’t understand where I mess this up… And the most surprising is to get my component name duplicated?! Is anyone have a clue?

Thank you for your time and have a good evening!

Xavier

The test1.yaml file :

esphome:
  name: test1
  platform: ESP32
  board: nodemcu-32s

external_components:
  - source:
      type: local
      path: my_components

wifi:
  ssid: "XXXXXXX"
  password: "XXXXXXXX"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "XXXXXXXXXXXX"
    password: "XXXXXXXXXXX"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

uart:
  tx_pin: TX
  rx_pin: RX
  baud_rate: 9600

sensor:
  - platform: "nather_optimea"
    #supply_flow_rate:
    voltage:
      name: "Supply flow rate"

The sensor.py file:

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, modbus
from esphome.const import (
    CONF_SENSOR,
    CONF_VOLTAGE,
    CONF_ID,
    DEVICE_CLASS_EMPTY,
    ICON_EMPTY,
    UNIT_EMPTY,
)

AUTO_LOAD = ["modbus"]

nather_optimea_ns = cg.esphome_ns.namespace("nather_optimea")
NATHER_OPTIMEA = nather_optimea_ns.class_("NATHER_OPTIMEA", cg.PollingComponent, modbus.ModbusDevice)

CONFIG_SCHEMA = (
    cv.Schema(
        {
            cv.GenerateID(): cv.declare_id(NATHER_OPTIMEA),
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
                UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY
            )
        }
    )
    .extend(cv.polling_component_schema("60s"))
    .extend(modbus.modbus_device_schema(0x01))
)


async def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])
    await cg.register_component(var, config)
    await modbus.register_modbus_device(var, config)

    if CONF_VOLTAGE in config:
        conf = config[CONF_VOLTAGE]
        sens = await sensor.new_sensor(conf)
        cg.add(var.set_supply_flow_rate_sensor(sens))

The nather_optimea.h file:

#pragma once

#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/modbus/modbus.h"

namespace esphome {
namespace nather_optimea {

class NATHER_OPTIMEA : public PollingComponent, public modbus::ModbusDevice {
 public:
  void set_supply_flow_rate_sensor(sensor::Sensor *supply_flow_rate_sensor) { supply_flow_rate_sensor_ = supply_flow_rate_sensor; }

  void update() override;

  void on_modbus_data(const std::vector<uint8_t> &data) override;

  void dump_config() override;

 protected:
  sensor::Sensor *supply_flow_rate_sensor_;
};

}  // namespace nather_optimea
}  // namespace esphome

and the nather_optimea.cpp file

#include "nather_optimea.h"
#include "esphome/core/log.h"

namespace esphome {
namespace nather_optimea {

static const char *TAG = "nather_optimea";

static const uint8_t NATHER_OPTIMEA_CMD_READ_IN_REGISTERS = 0x04;
static const uint16_t NATHER_OPTIMEA_REGISTER_COUNT = 1;  // 1x 16-bit registers
static const uint16_t NATHER_OPTIMEA_REGISTER_SUPPLY_FLOW_RATE_ADDR = 4032

void NATHER_OPTIMEA::on_modbus_data(const std::vector<uint8_t> &data) {
  if (data.size() < 20) {
    ESP_LOGW(TAG, "Invalid size for PZEM AC!");
    return;
  }
  // Nathea optimea reg examples:
  // 4031: reg("supply_flow_rate","p1.2","Débit d'amenée",None,None,False),
  // 4032: reg("extract_flow_rate","p1.3","Débit d'extration",None,None,False),

  auto nather_optimea_get_16bit = [&](size_t i) -> uint16_t {
    return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0);
  };
  auto nather_optimea_get_32bit = [&](size_t i) -> uint32_t {
    return (uint32_t(nather_optimea_get_16bit(i + 2)) << 16) | (uint32_t(nather_optimea_get_16bit(i + 0)) << 0);
  };

  float supply_flow_rate = static_cast<float>(nather_optimea_get_16bit(0));

  ESP_LOGD(TAG, "NATHER_OPTIMEA: supply_flow_rate=%.1f m3/h", supply_flow_rate);
  if (this->supply_flow_rate_sensor_ != nullptr)
    this->supply_flow_rate_sensor_->publish_state(supply_flow_rate);
}

void NATHER_OPTIMEA::update() { this->send(NATHER_OPTIMEA_CMD_READ_IN_REGISTERS,
                                           NATHER_OPTIMEA_REGISTER_SUPPLY_FLOW_RATE_ADDR,
                                           NATHER_OPTIMEA_REGISTER_COUNT); }
void NATHER_OPTIMEA::dump_config() {
  ESP_LOGCONFIG(TAG, "NATHER_OPTIMEA:");
  ESP_LOGCONFIG(TAG, "  Address: 0x%02X", this->address_);
  LOG_SENSOR("", "Supply Flow Rate", this->supply_flow_rate_sensor_);
}

}  // namespace nather_optimea
}  // namespace esphome

Hi all,

In order to narrow down the problem, I tried to use the official pzemdc component as an external_component:

  1. I copy paste the official pzemdc component into “my_component” folder
    Capture
  2. Updated the config file to use the “custom” pzemdc component (exact copy of the official one).
  3. and tried to compile the result…

As you can see, this is exactly the same error. Is anyone has an idea?

Thank you in advance,

Xavier

The official pzemdc external component seems to work when its source is the official git repo…

I begin to think there is a bug in this new “external component” thing… What do you think?

Same experience here. This seems like a bug or incomplete documentation to me. Will continue researching.

/e: After updating esphome to the current version (1.18.0) and starting from scratch, creating also the files init.py and sensor.py inside the component’s directory it does work.

Hi crumptuous,

Thank you for sharing!

I do run ESPHome 1.18.0 (‘latest’ esphome container image) and I tried again from scratch with identical results. Can you share the content of your __init__.py and sensor.py files? My __init__.py is empty as the __init__.py from the pzemdc component.

I don’t understand what you did which fix the issue, can you detail it further?

Tomorrow, with little hope I will try to recreate a brand new esphome container… I keep you informed.

Thank you for your time and have a good evening,

Cheers

Xavier