External Component issue: Component * cannot be loaded via YAML (no CONFIG_SCHEMA)

ESPHome External Component development is like shooting rays into a Black Box to find atoms.

I’ve successfully created an external component for the Adafruit NeoTrellis keypad, at least the key part of it as that derived rather nicely using the internal matrix_keypad as an example. It exposes the ‘matrix_keypad’ component and a ‘binary_sensor’ (sub?) component for each key. Now I want to do the same for the light behind each key. Creating my light.neotrellis_keypad sub-component seemed a fairly straightforward matter of massaging the binary_sensor, this time deriving from light.rgb. The whole system with the configuration Python files is an undocumented Black Box. The ‘binary_sensor’ and files are in a subdirectory below the ‘neotrellis_keypad’ folder, which lies beneath the ‘my_components’ folder. Lacking any formal connection to these sub-components, I can only assume they are found by looking through the directory tree and their Python config files are discovered. This works for the binary_sensor sub-component, but not for the light. Using the internal components as examples, it seems that you can name the Python file as either __init__.py or, for example, light.py. But in trying to resolve the errors when installing my device YAML file I do get different results:

with __init__.py:

Platform not found: ‘light.neotrellis_keypad’.

with light.py:

Component light.neotrellis_keypad cannot be loaded via YAML (no CONFIG_SCHEMA).

Here’s the entire Python config file, which does define CONFIG_SCHEMA:

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import rgb, light, output
from esphome.const import CONF_ID, CONF_KEY, CONF_ROW, CONF_COL, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_OUTPUT_ID
from .. import NeoTrellisKeypad, neotrellis_keypad_ns, CONF_KEYPAD_ID

DEPENDENCIES = ["neotrellis_keypad"]

NeoTrellisKeypadLight = neotrellis_keypad_ns.class_(
    "NeoTrellisKeypadLight", rgb.RGBLightOuput
)

def check_button(obj):
    if CONF_ROW in obj or CONF_COL in obj:
        if CONF_KEY in obj:
            raise cv.Invalid("You can't provide both a key and a position")
        if CONF_ROW not in obj:
            raise cv.Invalid("Missing row")
        if CONF_COL not in obj:
            raise cv.Invalid("Missing col")
        if obj[CONF_ROW] < 0 or obj[CONF_ROW] > 3:
            raise cv.Invalid("row must be 0 to 3")
        if obj[CONF_COL] < 0 or obj[CONF_COL] > 3:
            raise cv.Invalid("col must be 0 to 3")
    elif CONF_KEY not in obj:
        raise cv.Invalid("Missing key or position")
    elif len(obj[CONF_KEY]) != 1:
        raise cv.Invalid("Key must be one character")
    return obj

CONFIG_SCHEMA = cv.All(
    light.RGB_LIGHT_SCHEMA.extend(
        {
            cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(NeoTrellisKeypadLight),
            cv.Optional(CONF_RED): cv.use_id(output.FloatOutput),
            cv.Optional(CONF_GREEN): cv.use_id(output.FloatOutput),
            cv.Optional(CONF_BLUE): cv.use_id(output.FloatOutput),
            cv.GenerateID(CONF_KEYPAD_ID): cv.use_id(NeoTrellisKeypad),
            cv.Optional(CONF_ROW): cv.int_,
            cv.Optional(CONF_COL): cv.int_,
            cv.Optional(CONF_KEY): cv.string,
        }
    ),
    check_button,
)

async def to_code(config):
    if CONF_KEY in config:
        var = cg.new_Pvariable(config[CONF_ID], config[CONF_KEY][0])
    else:
        var = cg.new_Pvariable(config[CONF_ID], config[CONF_ROW], config[CONF_COL])
    await light.register_light(var, config)
    neotrellis_keypad = await cg.get_variable(config[CONF_KEYPAD_ID])
    cg.add(var.set_pad(neotrellis_keypad))
    if CONF_RED in config:
        red = await cg.get_variable(config[CONF_RED])
        cg.add(var.set_red(red))
    if CONF_GREEN in config:
        green = await cg.get_variable(config[CONF_GREEN])
        cg.add(var.set_green(green))
    if CONF_BLUE in config:
        blue = await cg.get_variable(config[CONF_BLUE])
        cg.add(var.set_blue(blue))

And here’s the part in the YAML that produces the error at the light platform line:

neotrellis_keypad:
  id: mykeypad
  keys: "PONMLKJIHGFEDCBA"
  on_key:
    - lambda: ESP_LOGI("KEY", "key %d pressed", x);

binary_sensor:
  - platform: neotrellis_keypad
    name: "KeyA"
    id: keyA
    key: A

light:
  - platform: neotrellis_keypad
    name: "LightA"
    id: lightA
    key: A

The device installs and works fine without the light component. There’s obviously something I’m not seeing keeping it from connecting to the light sub-component. Or perhaps an error in the Python code that I’m not seeing and is not being reported?

Consider this a cry for HELP!

It is not a black box, since all the code is available to peruse (but it might as well be, since it is not easy to figure out).

The dunder one is probably what you want, but it sounds like you have a circular reference. Your component depends on itself.

You need to provide the whole folder structure you are using to make it clearer what you are doing.

Here’s the relevant folder structure below config\esphome:
Clipboard Image

I don’t get the ‘dunder one’ comment and if you see a ‘circular reference’ I’d be pleased to hear the detail.

LOL. Yes, I guess the Black Box metaphor sounds a bit harsh. I am truly amazed at the power of ESPHome and the ability to do so much with so little code. But after perusing for a couple of days I have to admit to needing help to figure out which little bit I’ve gotten wrong. Given the keypad and binary sensor function perfectly well, I do expect some simple answer will expose my ignorance regarding the light configuration.

BTW: My continued perusal has revealed that I’ve probably used the wrong light base class. But I don’t think the process has even tried loading my __init__.py file yet.

Thanks in advance…

dunder is double under bar, i. e. __ init __.py

Your file structure seems to indicate you have one component “neotrellis_keypad” .

You have three dunder inits in the component that define the composite component. In the one you showed, you say it depends on “neotrellis_keypad”, but it is part of the definition of “neotrellis_keypad”. That looks like a circular dependency to me. I have found that the error messages in these type of cases are less than helpful.

I have found little helpful information on creating external components. There is no theory of operation, no guidelines for good design. There are some examples (all the components in esphome) but no indication if what they are doing is a current best practice or just what someone thought was a good enough idea at the time they wrote it. So I agree it is a challenge to get something working and likely nearly impossible to make something that is great.

After further experimentation I’ve discovered that the INSTALL process does not provide ANY error message information about errors encountered in the init Python code. This would obviously be VERY helpful. It apparently just ignores any errors and continues as if the component does not exist. I was able to reduce the code to the bare essentials to be valid, eliminating the “Platform not found” error and producing normal validation errors as the YAML references component options not yet restored to the schema. At least that confirms the Python code is being used.

The critical adjustment to the process seems to be adding small increments to the Python code to more easily isolate, identify, and correct errors introduced. So I’ll limp along with this process.

Is there some way to validate the Python file with meaningful error messages using the browser interface, perhaps using the Terminal?

I’m hesitant to clone the entire esphome project and setup a Linux IDE environment just for this, especially as I’m a Windows development expert but have no competence in Linux.