Building a local external component from an existing component

Hi everyone,

Sorry if my english is not that good, i am doing my best.

I am trying to build my own version of cse7761 component.

To do this, i downloaded the github component directory with its 4 files and modified them (only the c++ and h at this moment). I also renamed the directory my_cse7761 and did the same with the files and the namespace in the files.

Then i uploaded this directory and its files to esphome directory on the computer that runs home assistant.

Then i modified my sonoff powct yaml code to add that custom component but i am not the way to do it and at this point it does not work. May you help me using my custom component in taht case ?

my-sonoff-powct.yaml :

# Basic Config
# Pin	Function
# GPIO00	Push Button (HIGH = off, LOW = on)
# GPIO05	TM1621 DA
# GPIO13	Status LED (HIGH = off, LOW = on)
# GPIO15	Wifi_LED
# GPIO17	TM1621 CS
# GPIO18	TM1621 WR
# GPIO21	Relay1
# GPIO23	TM1621 RD
# GPIO25	CSE7761 Rx
# GPIO26	CSE7761 Tx
esphome:
  name: my-sonoff-powct
  friendly_name: my Sonoff POWCT
#   on_boot:
#     priority: 250.0 # Wait until WiFi is connected to allow the sensor some time to settle
#     then:
#       - if:
#           condition:
#             lambda: 'return id(v_sensor).state > 10;'
#           then:
#             - switch.turn_on: relay_1
#           else:
#             - switch.turn_off: relay_1

esp32:
  board: nodemcu-32s

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "########################################"

ota:
  - platform: esphome
    password: "###################################"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

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

captive_portal:

time:
  - platform: homeassistant
    id: homeassistant_time

external_components:
  - source: my_cse7761

uart:
  tx_pin: GPIO26
  rx_pin: GPIO25
  baud_rate: 38400
  parity: EVEN
  stop_bits: 1

# 
# sensor:
#   - platform: cse7761
sensor:
  - platform: my_cse7761
    update_interval: 2s
    current_1:
      name: Current
      id: a_sensor
      unit_of_measurement: 'A'
      accuracy_decimals: 3
      icon: mdi:current-ac
      filters:
        # Measurement divided by the PI number
        - lambda: return x / PI;
    voltage:
      name: Voltage
      id: v_sensor
      unit_of_measurement: 'V'
      icon: mdi:sine-wave
    active_power_1:
      name: Power
      id: w_sensor
      filters:
        # Measurement divided by the PI number
        - lambda: return x / PI;
      icon: mdi:flash
      on_value_range:
        - above: 4.0
          then:
            - light.turn_on: switch_led
        - below: 3.0
          then:
            - light.turn_off: switch_led

  - platform: total_daily_energy
    name: Total Daily Energy
    power_id: w_sensor
    id: kw_sensor
    unit_of_measurement: 'kWh'
    state_class: total_increasing
    device_class: energy
    accuracy_decimals: 3
    icon: mdi:lightning-bolt
    filters:
      # Multiplication factor from W to kW is 0.001
      - multiply: 0.001

  - platform: template
    name: ESP32 Internal Temp
    device_class: temperature
    unit_of_measurement: °C
    id: esp32_temp
    icon: mdi:thermometer
    lambda: return temperatureRead();

  - platform: template
    name: Power Factor
    device_class: power_factor
    id: power_factor
    icon: mdi:angle-acute
    lambda: return id(w_sensor).state / id(v_sensor).state / id(a_sensor).state;

binary_sensor:
  - platform: gpio
    pin: GPIO00
    id: reset
    internal: true
    filters:
      - invert:
      - delayed_off: 10ms
    on_click:
      - max_length: 350ms # short press to toggle the relay
        then:
          switch.toggle: relay_1
      - min_length: 360ms # long press to cycle display info
        max_length: 3s
        then:
          - if:
              condition:
                binary_sensor.is_on: page
              then:
                binary_sensor.template.publish:
                  id: page
                  state: OFF
              else:
                binary_sensor.template.publish:
                  id: page
                  state: ON
  - platform: template # this is a fake sensor to tell the screen which info to show on display
    id: page
    publish_initial_state: true
    internal: true
  - platform: template
    name: Subordinate Device
    id: subordinate_device_on
    lambda: |-
      if (isnan(id(w_sensor).state)) {
        return {};
      } else if (id(w_sensor).state > 4) {
        // Running
        return true;
      } else {
        // Not running
        return false;
      }

display:
  platform: tm1621
  id: tm1621_display
  cs_pin: GPIO17
  data_pin: GPIO05
  read_pin: GPIO23
  write_pin: GPIO18
  lambda: |-
    if (id(page).state) {
      it.display_voltage(true);
      it.display_kwh(false);
      it.printf(0, "%.1f", id(v_sensor).state);
      it.printf(1, "%.1f", id(a_sensor).state);
    } else {  
      it.display_voltage(false);
      it.display_kwh(true);
      it.printf(0, "%.1f", id(kw_sensor).state);
      it.printf(1, "%.1f", id(w_sensor).state);
    }

output:
  - platform: ledc
    id: led
    pin:
      number: GPIO13
      inverted: False

switch:
  - platform: gpio
    name: Relay
    pin: GPIO21
    id: relay_1
    restore_mode: RESTORE_DEFAULT_OFF
    on_turn_on:
      - delay: 500ms
      - light.turn_on: switch_led
    on_turn_off:
      - delay: 500ms
      - light.turn_off: switch_led

light:
  - platform: monochromatic
    id: switch_led
    output: led
    internal: True
  - platform: status_led
    id: wifi_status_led
    internal: True
    pin:
      number: GPIO15
      inverted: True

and the error message :

INFO ESPHome 2025.10.2
INFO Reading configuration /config/esphome/my-sonoff-powct.yaml...
ERROR Unexpected exception while reading configuration:
Traceback (most recent call last):
  File "/usr/local/bin/esphome", line 10, in <module>
    sys.exit(main())
             ^^^^^^
  File "/esphome/esphome/__main__.py", line 1387, in main
    return run_esphome(sys.argv)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/__main__.py", line 1362, in run_esphome
    config = read_config(
             ^^^^^^^^^^^^
  File "/esphome/esphome/config.py", line 1238, in read_config
    res = load_config(command_line_substitutions, skip_external_update)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/config.py", line 1095, in load_config
    return _load_config(command_line_substitutions, skip_external_update)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/config.py", line 1083, in _load_config
    return validate_config(config, command_line_substitutions, skip_external_update)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/config.py", line 974, in validate_config
    target_platform = core_config.preload_core_config(config, result)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/core/config.py", line 323, in preload_core_config
    if _is_target_platform(domain):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/core/config.py", line 284, in _is_target_platform
    return get_component(name, True).is_target_platform
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/loader.py", line 223, in get_component
    return _lookup_module(domain, exception)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/esphome/esphome/loader.py", line 199, in _lookup_module
    module = importlib.import_module(f"esphome.components.{domain}")
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 999, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/config/esphome/my_cse7761/sensor.py", line 33, in <module>
    cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
                               ^^^^^^^^^^^^^^^^^^^^
AttributeError: partially initialized module 'esphome.components.sensor' has no attribute 'sensor_schema' (most likely due to a circular import)

Likely the folder (source) shouldn’t have same name with files inside


.

I don’t think this is the problem. I just try to rename the files and change the include in the cpp and still the same error on line 33 in python file.

I also tried with an original cse7761 directory clone (without changing any code line, but changing the filenames and the namespace) and got the same error.

There is something I don’t understand, but I don’t know what ant then I don’t know what to seach for.

The external component documentation is quite small… Just telling how to include it. Is there a bigger reference documentation ? I don’t find anyone.

OK, I just understand what you just tell me and in fact I didn’t see that there where a global my_components directory containing several my_comp_1…etc… components directories. I thought the several components directories were directly in the esphome directory.

That was my mistake : thank you :slight_smile:

The folder source must be in a global folder component.

In my-sonoff-powct.yaml, I changed :

external_components:
  - source: my_cse7761

with :

external_components:
  - source: my_components
    components: [my_cse7761]

and placed my_cse7761 directory in a my_components directory itself in the esphome directory. Then it worked (at least i get c++ errors so it find the component).