esphome/esphome/components/libretiny/generate_components.py

330 lines
9.7 KiB
Python

# Copyright (c) Kuba Szczodrzyński 2023-06-01.
# pylint: skip-file
# flake8: noqa
import json
import re
from pathlib import Path
from black import FileMode, format_str
from ltchiptool import Board, Family
from ltchiptool.util.lvm import LVM
BASE_CODE_INIT = """
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
# For custom pin validators, put validate_pin() or validate_usage()
# in gpio.py file in this directory.
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
# in schema.py file in this directory.
from esphome import pins
from esphome.components import libretiny
from esphome.components.libretiny.const import (
COMPONENT_{COMPONENT},
KEY_COMPONENT_DATA,
KEY_LIBRETINY,
LibreTinyComponent,
)
from esphome.core import CORE
{IMPORTS}
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"]
COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_{COMPONENT},
boards={COMPONENT}_BOARDS,
board_pins={COMPONENT}_BOARD_PINS,
pin_validation={PIN_VALIDATION},
usage_validation={USAGE_VALIDATION},
)
def _set_core_data(config):
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
CONFIG_SCHEMA = {SCHEMA}
PIN_SCHEMA = {PIN_SCHEMA}
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
"""
BASE_CODE_BOARDS = """
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
from esphome.components.libretiny.const import {FAMILIES}
{COMPONENT}_BOARDS = {BOARDS_JSON}
{COMPONENT}_BOARD_PINS = {PINS_JSON}
BOARDS = {COMPONENT}_BOARDS
"""
# variable names in component extension code
VAR_SCHEMA = "COMPONENT_SCHEMA"
VAR_PIN_SCHEMA = "COMPONENT_PIN_SCHEMA"
VAR_GPIO_PIN = "validate_pin"
VAR_GPIO_USAGE = "validate_usage"
# lines for code snippets
SCHEMA_BASE = "libretiny.BASE_SCHEMA"
SCHEMA_EXTRA = f"libretiny.BASE_SCHEMA.extend({VAR_SCHEMA})"
PIN_SCHEMA_BASE = "libretiny.gpio.BASE_PIN_SCHEMA"
PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})"
# supported root components
COMPONENT_MAP = {
"rtl87xx": "realtek-amb",
"bk72xx": "beken-72xx",
}
def subst(code: str, key: str, value: str) -> str:
return code.replace(f"{{{key}}}", value)
def subst_all(code: str, value: str) -> str:
return re.sub(r"{.+?}", value, code)
def subst_many(code: str, *templates: tuple[str, str]) -> str:
while True:
prev_code = code
for key, value in templates:
code = subst(code, key, value)
if code == prev_code:
break
return code
def check_base_code(code: str) -> None:
code = subst_all(code, "DUMMY")
formatted = format_str(code, mode=FileMode())
if code.strip() != formatted.strip():
print(formatted)
raise RuntimeError("Base code is not formatted properly")
def write_component_code(
component_dir: Path,
component: str,
) -> None:
code = BASE_CODE_INIT
gpio_py = component_dir.joinpath("gpio.py")
schema_py = component_dir.joinpath("schema.py")
init_py = component_dir.joinpath("__init__.py")
# gather all imports
imports = {
"gpio": set(),
"schema": set(),
"boards": {"{COMPONENT}_BOARDS", "{COMPONENT}_BOARD_PINS"},
}
# substitution values
values = dict(
COMPONENT=component.upper(),
COMPONENT_LOWER=component.lower(),
SCHEMA=SCHEMA_BASE,
PIN_SCHEMA=PIN_SCHEMA_BASE,
PIN_VALIDATION="None",
USAGE_VALIDATION="None",
)
# parse gpio.py file to find custom validators
if gpio_py.is_file():
gpio_code = gpio_py.read_text()
if VAR_GPIO_PIN in gpio_code:
values["PIN_VALIDATION"] = VAR_GPIO_PIN
imports["gpio"].add(VAR_GPIO_PIN)
# parse schema.py file to find schema extension
if schema_py.is_file():
schema_code = schema_py.read_text()
if VAR_SCHEMA in schema_code:
values["SCHEMA"] = SCHEMA_EXTRA
imports["schema"].add(VAR_SCHEMA)
if VAR_PIN_SCHEMA in schema_code:
values["PIN_SCHEMA"] = PIN_SCHEMA_EXTRA
imports["schema"].add(VAR_PIN_SCHEMA)
# add import lines if needed
import_lines = "\n".join(
f"from .{m} import {', '.join(sorted(v))}" for m, v in imports.items() if v
)
code = subst_many(
code,
("IMPORTS", import_lines),
*values.items(),
)
# format with black
code = format_str(code, mode=FileMode())
# write back to file
init_py.write_text(code)
def write_component_boards(
component_dir: Path,
component: str,
boards: list[Board],
) -> list[Family]:
code = BASE_CODE_BOARDS
variants_dir = Path(LVM.path(), "boards", "variants")
boards_py = component_dir.joinpath("boards.py")
pin_regex = r"#define PIN_(\w+)\s+(\d+)"
pin_number_regex = r"0*(\d+)$"
# families to import
families = set()
# found root families
root_families = []
# substitution values
values = dict(
COMPONENT=component.upper(),
)
# resulting JSON objects
boards_json = {}
pins_json = {}
# go through all boards found for this root family
for board in boards:
family = "FAMILY_" + board.family.short_name
boards_json[board.name] = {
"name": board.title,
"family": family,
}
families.add(family)
if board.family not in root_families:
root_families.append(board.family)
board_h = variants_dir.joinpath(f"{board.name}.h")
board_code = board_h.read_text()
board_pins = {}
for match in re.finditer(pin_regex, board_code):
pin_name = match[1]
pin_value = match[2]
board_pins[pin_name] = int(pin_value)
# trim leading zeroes in GPIO numbers
pin_name = re.sub(pin_number_regex, r"\1", pin_name)
board_pins[pin_name] = int(pin_value)
pins_json[board.name] = board_pins
# make the JSONs format as non-inline
boards_json = json.dumps(boards_json).replace("}", ",}")
pins_json = json.dumps(pins_json).replace("}", ",}")
# remove quotes from family constants
for family in families:
boards_json = boards_json.replace(f'"{family}"', family)
code = subst_many(
code,
("FAMILIES", ", ".join(sorted(families))),
("BOARDS_JSON", boards_json),
("PINS_JSON", pins_json),
*values.items(),
)
# format with black
code = format_str(code, mode=FileMode())
# write back to file
boards_py.write_text(code)
return root_families
def write_const(
components_dir: Path,
components: set[str],
families: dict[str, str],
) -> None:
const_py = components_dir.joinpath("libretiny").joinpath("const.py")
if not const_py.is_file():
raise FileNotFoundError(const_py)
code = const_py.read_text()
components = sorted(components)
v2f = families
families = sorted(families)
# regex for finding the component list block
comp_regex = r"(# COMPONENTS.+?\n)(.*?)(\n# COMPONENTS)"
# build component constants
comp_str = "\n".join(f'COMPONENT_{f} = "{f.lower()}"' for f in components)
# replace the 2nd regex group only
repl = lambda m: m.group(1) + comp_str + m.group(3)
code = re.sub(comp_regex, repl, code, flags=re.DOTALL | re.MULTILINE)
# regex for finding the family list block
fam_regex = r"(# FAMILIES.+?\n)(.*?)(\n# FAMILIES)"
# build family constants
fam_defs = "\n".join(f'FAMILY_{v} = "{v}"' for v in families)
fam_list = ", ".join(f"FAMILY_{v}" for v in families)
fam_friendly = ", ".join(f'FAMILY_{v}: "{v}"' for v in families)
fam_component = ", ".join(f"FAMILY_{v}: COMPONENT_{v2f[v]}" for v in families)
fam_lines = [
fam_defs,
"FAMILIES = [",
fam_list,
",]",
"FAMILY_FRIENDLY = {",
fam_friendly,
",}",
"FAMILY_COMPONENT = {",
fam_component,
",}",
]
var_str = "\n".join(fam_lines)
# replace the 2nd regex group only
repl = lambda m: m.group(1) + var_str + m.group(3)
code = re.sub(fam_regex, repl, code, flags=re.DOTALL | re.MULTILINE)
# format with black
code = format_str(code, mode=FileMode())
# write back to file
const_py.write_text(code)
if __name__ == "__main__":
# safety check if code is properly formatted
check_base_code(BASE_CODE_INIT)
# list all boards from ltchiptool
components_dir = Path(__file__).parent.parent
boards = [Board(b) for b in Board.get_list()]
# keep track of all supported root- and chip-families
components = set()
families = {}
# loop through supported components
for component, family_name in COMPONENT_MAP.items():
family = Family.get(name=family_name)
# make family component directory
component_dir = components_dir.joinpath(component)
component_dir.mkdir(exist_ok=True)
# filter boards list
family_boards = [b for b in boards if family in b.family.inheritance]
# write __init__.py
write_component_code(component_dir, component)
# write boards.py
component_families = write_component_boards(
component_dir, component, family_boards
)
# store current root component name
components.add(component.upper())
# add all chip families
for family in component_families:
families[family.short_name] = component.upper()
# update libretiny/const.py
write_const(components_dir, components, families)