async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) cg.add_define("USE_WEBSERVER") cg.add(paren.set_port(config[CONF_PORT])) cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) cg.add_define("USE_WEBSERVER_VERSION", config[CONF_VERSION]) cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) cg.add(var.set_allow_ota(config[CONF_OTA])) if CONF_AUTH in config: cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: cg.add_define("USE_WEBSERVER_CSS_INCLUDE") path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) with open(file=path, encoding="utf-8") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: cg.add_define("USE_WEBSERVER_JS_INCLUDE") path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) with open(file=path, encoding="utf-8") as myfile: cg.add(var.set_js_include(myfile.read())) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) if CONF_LOCAL in config and config[CONF_LOCAL]: cg.add_define("USE_WEBSERVER_LOCAL")
def file_(value): import json value = string(value) path = CORE.relative_config_path(value) if CORE.vscode and (not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)): print(json.dumps({ "type": "check_file_exists", "path": path, })) data = json.loads(input()) assert data["type"] == "file_exists_response" if data["content"]: return value raise Invalid( "Could not find file '{}'. Please make sure it exists (full path: {})." "".format(path, os.path.abspath(path))) if not os.path.exists(path): raise Invalid( "Could not find file '{}'. Please make sure it exists (full path: {})." "".format(path, os.path.abspath(path))) if not os.path.isfile(path): raise Invalid("Path '{}' is not a file (full path: {})." "".format(path, os.path.abspath(path))) return value
def _process_single_config(config: dict): conf = config[CONF_SOURCE] if conf[CONF_TYPE] == TYPE_GIT: with cv.prepend_path([CONF_SOURCE]): components_dir = _process_git_config(config[CONF_SOURCE], config[CONF_REFRESH]) elif conf[CONF_TYPE] == TYPE_LOCAL: components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) else: raise NotImplementedError() if config[CONF_COMPONENTS] == "all": num_components = len(list(components_dir.glob("*/__init__.py"))) if num_components > 100: # Prevent accidentally including all components from an esphome fork/branch # In this case force the user to manually specify which components they want to include raise cv.Invalid( "This source is an ESPHome fork or branch. Please manually specify the components you want to import using the 'components' key", [CONF_COMPONENTS], ) allowed_components = None else: for i, name in enumerate(config[CONF_COMPONENTS]): expected = components_dir / name / "__init__.py" if not expected.is_file(): raise cv.Invalid( f"Could not find __init__.py file for component {name}. Please check the component is defined by this source (search path: {expected})", [CONF_COMPONENTS, i], ) allowed_components = config[CONF_COMPONENTS] loader.install_meta_finder(components_dir, allowed_components=allowed_components)
def directory(value): import json value = string(value) path = CORE.relative_config_path(value) if CORE.vscode and (not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)): print(json.dumps({ 'type': 'check_directory_exists', 'path': path, })) data = json.loads(input()) assert data['type'] == 'directory_exists_response' if data['content']: return value raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." "".format(path, os.path.abspath(path))) if not os.path.exists(path): raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." "".format(path, os.path.abspath(path))) if not os.path.isdir(path): raise Invalid("Path '{}' is not a directory (full path: {})." "".format(path, os.path.abspath(path))) return value
def to_code(config): from PIL import Image path = CORE.relative_config_path(config[CONF_FILE]) try: image = Image.open(path) except Exception as e: raise core.EsphomeError(u"Could not load image file {}: {}".format( path, e)) if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) image = image.convert('1', dither=Image.NONE) width, height = image.size if width > 500 or height > 500: _LOGGER.warning( "The image you requested is very big. Please consider using the resize " "parameter") width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] for y in range(height): for x in range(width): if image.getpixel((x, y)): continue pos = x + y * width8 data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable(config[CONF_ID], prog_arr, width, height)
def file_(value): import json from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) if CORE.vscode and (not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)): print(json.dumps({ 'type': 'check_file_exists', 'path': path, })) data = json.loads(safe_input()) assert data['type'] == 'file_exists_response' if data['content']: return value raise Invalid( u"Could not find file '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) if not os.path.exists(path): raise Invalid( u"Could not find file '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) if not os.path.isfile(path): raise Invalid(u"Path '{}' is not a file (full path: {})." u"".format(path, os.path.abspath(path))) return value
async def to_code(config): from PIL import Image path = CORE.relative_config_path(config[CONF_FILE]) try: image = Image.open(path) except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") width, height = image.size if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) width, height = image.size else: if width > 500 or height > 500: _LOGGER.warning( "The image you requested is very big. Please consider using" " the resize parameter.") dither = Image.NONE if config[ CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG if config[CONF_TYPE] == "GRAYSCALE": image = image.convert("L", dither=dither) pixels = list(image.getdata()) data = [0 for _ in range(height * width)] pos = 0 for pix in pixels: data[pos] = pix pos += 1 elif config[CONF_TYPE] == "RGB24": image = image.convert("RGB") pixels = list(image.getdata()) data = [0 for _ in range(height * width * 3)] pos = 0 for pix in pixels: data[pos] = pix[0] pos += 1 data[pos] = pix[1] pos += 1 data[pos] = pix[2] pos += 1 elif config[CONF_TYPE] == "BINARY": image = image.convert("1", dither=dither) width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] for y in range(height): for x in range(width): if image.getpixel((x, y)): continue pos = x + y * width8 data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]])
async def to_code(config): from PIL import ImageFont conf = config[CONF_FILE] if conf[CONF_TYPE] == TYPE_LOCAL: path = CORE.relative_config_path(conf[CONF_PATH]) elif conf[CONF_TYPE] == TYPE_GFONTS: path = _compute_gfonts_local_path(conf) try: font = ImageFont.truetype(str(path), config[CONF_SIZE]) except Exception as e: raise core.EsphomeError(f"Could not load truetype file {path}: {e}") ascent, descent = font.getmetrics() glyph_args = {} data = [] for glyph in config[CONF_GLYPHS]: mask = font.getmask(glyph, mode="1") _, (offset_x, offset_y) = font.font.getsize(glyph) width, height = mask.size width8 = ((width + 7) // 8) * 8 glyph_data = [0] * (height * width8 // 8) for y in range(height): for x in range(width): if not mask.getpixel((x, y)): continue pos = x + y * width8 glyph_data[pos // 8] |= 0x80 >> (pos % 8) glyph_args[glyph] = (len(data), offset_x, offset_y, width, height) data += glyph_data rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) glyph_initializer = [] for glyph in config[CONF_GLYPHS]: glyph_initializer.append( cg.StructInitializer( GlyphData, ("a_char", glyph), ( "data", cg.RawExpression( f"{str(prog_arr)} + {str(glyph_args[glyph][0])}"), ), ("offset_x", glyph_args[glyph][1]), ("offset_y", glyph_args[glyph][2]), ("width", glyph_args[glyph][3]), ("height", glyph_args[glyph][4]), )) glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer) cg.new_Pvariable(config[CONF_ID], glyphs, len(glyph_initializer), ascent, ascent + descent)
def file_(value): value = string(value) path = CORE.relative_config_path(value) if not os.path.exists(path): raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) if not os.path.isfile(path): raise Invalid(u"Path '{}' is not a file (full path: {})." u"".format(path, os.path.abspath(path))) return value
def add_includes(includes): # Add includes at the very end, so that the included files can access global variables for include in includes: path = CORE.relative_config_path(include) if os.path.isdir(path): # Directory, copy tree for p in walk_files(path): basename = os.path.relpath(p, os.path.dirname(path)) include_file(p, basename) else: # Copy file basename = os.path.basename(path) include_file(path, basename)
def to_code(config): from PIL import Image path = CORE.relative_config_path(config[CONF_FILE]) try: image = Image.open(path) except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) if config[CONF_TYPE].startswith('RGB565'): width, height = image.size image = image.convert('RGB') pixels = list(image.getdata()) data = [0 for _ in range(height * width * 2)] pos = 0 for pix in pixels: r = (pix[0] >> 3) & 0x1F g = (pix[1] >> 2) & 0x3F b = (pix[2] >> 3) & 0x1F p = (r << 11) + (g << 5) + b data[pos] = (p >> 8) & 0xFF pos += 1 data[pos] = p & 0xFF pos += 1 rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, 1) else: image = image.convert('1', dither=Image.NONE) width, height = image.size if width > 500 or height > 500: _LOGGER.warning( "The image you requested is very big. Please consider using the resize " "parameter") width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] for y in range(height): for x in range(width): if image.getpixel((x, y)): continue pos = x + y * width8 data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable(config[CONF_ID], prog_arr, width, height)
def preload_core_config(config): core_key = 'esphome' if 'esphomeyaml' in config: _LOGGER.warning("The esphomeyaml section has been renamed to esphome in 1.11.0. " "Please replace 'esphomeyaml:' in your configuration with 'esphome:'.") config[CONF_ESPHOME] = config.pop('esphomeyaml') core_key = 'esphomeyaml' if CONF_ESPHOME not in config: raise cv.RequiredFieldInvalid("required key not provided", CONF_ESPHOME) with cv.prepend_path(core_key): out = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) CORE.name = out[CONF_NAME] CORE.esp_platform = out[CONF_PLATFORM] with cv.prepend_path(core_key): out2 = PRELOAD_CONFIG_SCHEMA2(config[CONF_ESPHOME]) CORE.board = out2[CONF_BOARD] CORE.build_path = CORE.relative_config_path(out2[CONF_BUILD_PATH])
def to_code(config): from PIL import ImageFont path = CORE.relative_config_path(config[CONF_FILE]) try: font = ImageFont.truetype(path, config[CONF_SIZE]) except Exception as e: raise core.EsphomeError(f"Could not load truetype file {path}: {e}") ascent, descent = font.getmetrics() glyph_args = {} data = [] for glyph in config[CONF_GLYPHS]: mask = font.getmask(glyph, mode='1') _, (offset_x, offset_y) = font.font.getsize(glyph) width, height = mask.size width8 = ((width + 7) // 8) * 8 glyph_data = [0 for _ in range(height * width8 // 8)] # noqa: F812 for y in range(height): for x in range(width): if not mask.getpixel((x, y)): continue pos = x + y * width8 glyph_data[pos // 8] |= 0x80 >> (pos % 8) glyph_args[glyph] = (len(data), offset_x, offset_y, width, height) data += glyph_data rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) glyphs = [] for glyph in config[CONF_GLYPHS]: glyphs.append(Glyph(glyph, prog_arr, *glyph_args[glyph])) cg.new_Pvariable(config[CONF_ID], glyphs, ascent, ascent + descent)
def storage_path(): # type: () -> str return CORE.relative_config_path(".esphome", f"{CORE.config_filename}.json")
def to_code(config): from PIL import Image path = CORE.relative_config_path(config[CONF_FILE]) try: image = Image.open(path) except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") width, height = image.size frames = image.n_frames if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) width, height = image.size else: if width > 500 or height > 500: _LOGGER.warning( "The image you requested is very big. Please consider using" " the resize parameter.") if config[CONF_TYPE] == 'GRAYSCALE': data = [0 for _ in range(height * width * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert('L', dither=Image.NONE) pixels = list(frame.getdata()) for pix in pixels: data[pos] = pix pos += 1 elif config[CONF_TYPE] == 'RGB24': data = [0 for _ in range(height * width * 3 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert('RGB') pixels = list(frame.getdata()) for pix in pixels: data[pos] = pix[0] pos += 1 data[pos] = pix[1] pos += 1 data[pos] = pix[2] pos += 1 elif config[CONF_TYPE] == 'BINARY': width8 = ((width + 7) // 8) * 8 data = [0 for _ in range((height * width8 // 8) * frames)] for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert('1', dither=Image.NONE) for y in range(height): for x in range(width): if frame.getpixel((x, y)): continue pos = x + y * width8 + (height * width8 * frameIndex) data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, frames, espImage.IMAGE_TYPE[config[CONF_TYPE]])
def storage_path(): # type: () -> str return CORE.relative_config_path('.esphome', '{}.json'.format(CORE.config_filename))
async def to_code(config): from PIL import Image path = CORE.relative_config_path(config[CONF_FILE]) try: image = Image.open(path) except Exception as e: raise core.EsphomeError(f"Could not load image file {path}: {e}") width, height = image.size frames = image.n_frames if CONF_RESIZE in config: new_width_max, new_height_max = config[CONF_RESIZE] ratio = min(new_width_max / width, new_height_max / height) width, height = int(width * ratio), int(height * ratio) else: if width > 500 or height > 500: _LOGGER.warning( "The image you requested is very big. Please consider using" " the resize parameter.") if config[CONF_TYPE] == "GRAYSCALE": data = [0 for _ in range(height * width * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert("L", dither=Image.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) for pix in pixels: data[pos] = pix pos += 1 elif config[CONF_TYPE] == "RGB24": data = [0 for _ in range(height * width * 3 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert("RGB") if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) for pix in pixels: data[pos] = pix[0] pos += 1 data[pos] = pix[1] pos += 1 data[pos] = pix[2] pos += 1 elif config[CONF_TYPE] == "RGB565": data = [0 for _ in range(height * width * 2 * frames)] pos = 0 for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert("RGB") if CONF_RESIZE in config: frame = frame.resize([width, height]) pixels = list(frame.getdata()) if len(pixels) != height * width: raise core.EsphomeError( f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" ) for pix in pixels: R = pix[0] >> 3 G = pix[1] >> 2 B = pix[2] >> 3 rgb = (R << 11) | (G << 5) | B data[pos] = rgb >> 8 pos += 1 data[pos] = rgb & 255 pos += 1 elif config[CONF_TYPE] == "BINARY": width8 = ((width + 7) // 8) * 8 data = [0 for _ in range((height * width8 // 8) * frames)] for frameIndex in range(frames): image.seek(frameIndex) frame = image.convert("1", dither=Image.NONE) if CONF_RESIZE in config: frame = frame.resize([width, height]) for y in range(height): for x in range(width): if frame.getpixel((x, y)): continue pos = x + y * width8 + (height * width8 * frameIndex) data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable( config[CONF_ID], prog_arr, width, height, frames, espImage.IMAGE_TYPE[config[CONF_TYPE]], )
def write_gitignore(): path = CORE.relative_config_path(".gitignore") if not os.path.isfile(path): with open(path, "w") as f: f.write(GITIGNORE_CONTENT)
def read_relative_config_path(value): # pylint: disable=unspecified-encoding return Path(CORE.relative_config_path(value)).read_text()
def write_gitignore(): path = CORE.relative_config_path(".gitignore") if not os.path.isfile(path): with open(file=path, mode="w", encoding="utf-8") as f: f.write(GITIGNORE_CONTENT)
def preload_core_config(config, result): with cv.prepend_path(CONF_ESPHOME): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) CORE.name = conf[CONF_NAME] CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: conf[CONF_BUILD_PATH] = f".esphome/build/{CORE.name}" CORE.build_path = CORE.relative_config_path(conf[CONF_BUILD_PATH]) has_oldstyle = CONF_PLATFORM in conf newstyle_found = [key for key in TARGET_PLATFORMS if key in config] oldstyle_opts = [ CONF_ESP8266_RESTORE_FROM_FLASH, CONF_BOARD_FLASH_MODE, CONF_ARDUINO_VERSION, CONF_BOARD, ] if not has_oldstyle and not newstyle_found: raise cv.Invalid("Platform missing for core options!", [CONF_ESPHOME]) if has_oldstyle and newstyle_found: raise cv.Invalid( f"Please remove the `platform` key from the [esphome] block. You're already using the new style with the [{conf[CONF_PLATFORM]}] block", [CONF_ESPHOME, CONF_PLATFORM], ) if len(newstyle_found) > 1: raise cv.Invalid( f"Found multiple target platform blocks: {', '.join(newstyle_found)}. Only one is allowed.", [newstyle_found[0]], ) if newstyle_found: # Convert to newstyle for key in oldstyle_opts: if key in conf: raise cv.Invalid( f"Please move {key} to the [{newstyle_found[0]}] block.", [CONF_ESPHOME, key], ) if has_oldstyle: plat = conf.pop(CONF_PLATFORM) plat_conf = {} if CONF_ESP8266_RESTORE_FROM_FLASH in conf: plat_conf["restore_from_flash"] = conf.pop( CONF_ESP8266_RESTORE_FROM_FLASH) if CONF_BOARD_FLASH_MODE in conf: plat_conf[CONF_BOARD_FLASH_MODE] = conf.pop(CONF_BOARD_FLASH_MODE) if CONF_ARDUINO_VERSION in conf: plat_conf[CONF_FRAMEWORK] = {} if plat != PLATFORM_ESP8266: plat_conf[CONF_FRAMEWORK][CONF_TYPE] = "arduino" try: if conf[CONF_ARDUINO_VERSION] not in ("recommended", "latest", "dev"): cv.Version.parse(conf[CONF_ARDUINO_VERSION]) plat_conf[CONF_FRAMEWORK][CONF_VERSION] = conf.pop( CONF_ARDUINO_VERSION) except ValueError: plat_conf[CONF_FRAMEWORK][CONF_SOURCE] = conf.pop( CONF_ARDUINO_VERSION) if CONF_BOARD in conf: plat_conf[CONF_BOARD] = conf.pop(CONF_BOARD) # Insert generated target platform config to main config config[plat] = plat_conf config[CONF_ESPHOME] = conf
def read_relative_config_path(value): return Path(CORE.relative_config_path(value)).read_text()
def _process_single_config(config: dict): conf = config[CONF_SOURCE] if conf[CONF_TYPE] == TYPE_GIT: key = f"{conf[CONF_URL]}@{conf.get(CONF_REF)}" repo_dir = _compute_destination_path(key) if not repo_dir.is_dir(): cmd = ["git", "clone", "--depth=1"] if CONF_REF in conf: cmd += ["--branch", conf[CONF_REF]] cmd += [conf[CONF_URL], str(repo_dir)] ret = subprocess.run(cmd, capture_output=True, check=False) _handle_git_response(ret) else: # Check refresh needed file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") # On first clone, FETCH_HEAD does not exists if not file_timestamp.exists(): file_timestamp = Path(repo_dir / ".git" / "HEAD") age = datetime.datetime.now() - datetime.datetime.fromtimestamp( file_timestamp.stat().st_mtime ) if age.seconds > config[CONF_REFRESH].total_seconds: _LOGGER.info("Executing git pull %s", key) cmd = ["git", "pull"] ret = subprocess.run( cmd, cwd=repo_dir, capture_output=True, check=False ) _handle_git_response(ret) if (repo_dir / "esphome" / "components").is_dir(): components_dir = repo_dir / "esphome" / "components" elif (repo_dir / "components").is_dir(): components_dir = repo_dir / "components" else: raise cv.Invalid( "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder", [CONF_SOURCE], ) elif conf[CONF_TYPE] == TYPE_LOCAL: components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) else: raise NotImplementedError() if config[CONF_COMPONENTS] == "all": num_components = len(list(components_dir.glob("*/__init__.py"))) if num_components > 100: # Prevent accidentally including all components from an esphome fork/branch # In this case force the user to manually specify which components they want to include raise cv.Invalid( "This source is an ESPHome fork or branch. Please manually specify the components you want to import using the 'components' key", [CONF_COMPONENTS], ) allowed_components = None else: for i, name in enumerate(config[CONF_COMPONENTS]): expected = components_dir / name / "__init__.py" if not expected.is_file(): raise cv.Invalid( f"Could not find __init__.py file for component {name}. Please check the component is defined by this source (search path: {expected})", [CONF_COMPONENTS, i], ) allowed_components = config[CONF_COMPONENTS] loader.install_meta_finder(components_dir, allowed_components=allowed_components)