Home Assistant 核心架构与实现深度解析
基于源码版本 2026.7.0.dev0,Python 3.14.2+
目录
1. 项目总体结构
1.1 顶层目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| E:\pyproject\core\
├── homeassistant/ # 核心源代码主目录
│ ├── core.py # HomeAssistant 主类、EventBus、StateMachine、ServiceRegistry
│ ├── config_entries.py # ConfigEntry、ConfigFlow、ConfigEntries 管理器
│ ├── bootstrap.py # 启动引导,分阶段加载
│ ├── loader.py # Integration 类,manifest 解析,自定义集成管理
│ ├── setup.py # async_setup_component,依赖解析
│ ├── data_entry_flow.py # FlowHandler/FlowManager 基类
│ ├── config.py # 配置文件处理
│ ├── const.py # 全局常量、Platform 枚举
│ ├── runner.py # 运行器,事件循环设置
│ ├── __main__.py # CLI 入口
│ ├── auth/ # 认证系统
│ ├── components/ # 所有内置集成(2000+个)
│ ├── generated/ # 自动生成的代码(蓝牙、DHCP、SSDP 数据等)
│ ├── helpers/ # 辅助模块(entity、entity_registry、template 等)
│ └── util/ # 工具模块
├── tests/ # 测试代码
├── custom_components/ # 自定义集成目录
└── pyproject.toml # 项目配置
|
1.2 集成组织方式
所有集成都以子目录形式存放在 homeassistant/components/ 下,目录名即域名。每个集成至少包含:
1
2
3
4
5
6
7
8
| components/<domain>/
├── manifest.json # 集成清单(必需)
├── __init__.py # 集成入口
├── config_flow.py # 配置流(可选,支持 UI 配置时需要)
├── light.py # 向 light 域提供实体(可选平台文件)
├── sensor.py # 向 sensor 域提供实体(可选平台文件)
├── strings.json # 前端字符串
└── services.yaml # 服务定义
|
1.3 manifest.json 关键字段
1
2
3
4
5
6
7
8
9
10
11
12
| {
"domain": "hue",
"name": "Philips Hue",
"integration_type": "hub",
"config_flow": true,
"dependencies": [],
"after_dependencies": [],
"requirements": ["aiohue==4.8.1"],
"iot_class": "local_push",
"zeroconf": ["_hue._tcp.local."],
"codeowners": ["@marcelveldt"]
}
|
| 字段 | 说明 |
|---|
domain | 集成域名,必须等于目录名 |
name | 人类可读名称 |
integration_type | entity(平台型)、hub(集线器型)、system(系统型)、virtual(虚拟型)、helper(辅助型)、device(设备型) |
config_flow | 是否支持 UI 配置流 |
dependencies | 前置依赖集成(必须先加载) |
after_dependencies | 后置依赖(仅影响加载顺序,不强制加载) |
requirements | pip 依赖包 |
iot_class | local_push、local_polling、cloud_push、cloud_polling、assumed_state |
zeroconf/homekit/ssdp/dhcp/usb/bluetooth | 自动发现配置 |
2. 核心类与基类体系
2.1 HomeAssistant 类
源码: homeassistant/core.py
HomeAssistant 是整个系统的根对象,几乎一切皆通过它访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class HomeAssistant:
def __init__(self, config_dir: str):
self.data = HassDict() # 全局数据存储字典
self.loop = asyncio.get_running_loop() # 事件循环
self.bus = EventBus(self) # 事件总线
self.services = ServiceRegistry(self) # 服务注册表
self.states = StateMachine(self.bus, self.loop) # 状态机
self.config = Config(self, config_dir) # 配置
self.config_entries: ConfigEntries # 配置条目管理器
self.auth: AuthManager # 认证管理器
self.state: CoreState = CoreState.not_running
self.exit_code: int = 0
self.timeout = TimeoutManager()
self.import_executor = InterruptibleThreadPoolExecutor(...)
|
核心方法:
| 方法 | 说明 |
|---|
async_run() | 主入口,设置信号处理,等待停止 |
async_start() | 发射 EVENT_HOMEASSISTANT_START → CoreState.running → 发射 EVENT_HOMEASSISTANT_STARTED |
async_create_task() | 创建异步任务 |
async_add_job() / async_add_executor_job() | 添加任务到事件循环或线程池 |
CoreState 生命周期:
1
| NOT_RUNNING → STARTING → RUNNING → STOPPING → FINAL_WRITE → STOPPED
|
2.2 EventBus — 事件总线
源码: homeassistant/core.py
事件驱动架构的核心,所有组件间通信都通过事件总线。
1
2
3
| class EventBus:
def async_listen(self, event_type, listener) -> Callable: # 注册监听器
def async_fire(self, event_type, event_data) -> None: # 发射事件
|
关键事件:
EVENT_HOMEASSISTANT_START — HA 开始运行EVENT_HOMEASSISTANT_STARTED — HA 完全启动EVENT_HOMEASSISTANT_STOP — HA 停止EVENT_STATE_CHANGED — 状态变更EVENT_CALL_SERVICE — 服务调用EVENT_COMPONENT_LOADED — 组件加载完成
2.3 StateMachine — 状态机
源码: homeassistant/core.py
管理所有实体的状态,状态以 (entity_id, state, attributes) 三元组存储。
1
2
3
4
5
| class StateMachine:
def async_set(self, entity_id, state, attributes) -> None # 设置状态
def async_get(self, entity_id) -> State | None # 获取状态
def async_all(self) -> list[State] # 获取所有状态
def async_remove(self, entity_id) -> bool # 移除状态
|
状态变更会触发 EVENT_STATE_CHANGED 事件。
2.4 ServiceRegistry — 服务注册表
源码: homeassistant/core.py
管理所有已注册的服务,两层字典结构 _services[domain][service_name]。
1
2
3
4
| class ServiceRegistry:
def async_register(self, domain, service, service_func, schema) -> None
def async_call(self, domain, service, service_data, blocking) -> Any
def async_remove(self, domain, service) -> None
|
详见第 7 节。
2.5 Entity 类 — 实体基类
源码: homeassistant/helpers/entity.py
所有实体的抽象基类,使用 ABCCachedProperties 元类缓存高频属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| class Entity(metaclass=ABCCachedProperties, cached_properties=...):
entity_id: str = None
hass: HomeAssistant = None
platform: EntityPlatform = None
entity_description: EntityDescription
# 核心属性
@property
def state(self) -> StateType # 实体状态值
@property
def capability_attributes(self) -> dict # 能力属性
@property
def name(self) -> str | None # 名称
@property
def icon(self) -> str | None # 图标
@property
def device_class(self) -> str | None # 设备类
@property
def unit_of_measurement(self) -> str | None # 计量单位
@property
def supported_features(self) -> int # 支持的特性标志位
@property
def available(self) -> bool # 是否可用
@property
def should_poll(self) -> bool # 是否需要轮询(默认 True)
# 核心方法
def async_write_ha_state(self) -> None # 写入状态到状态机(推荐)
def async_update_ha_state(self, force_refresh) -> None # 更新状态
def async_device_update(self) -> None # 从设备拉取最新数据
def async_added_to_hass(self) -> None # 添加到 HA 后的钩子
def async_will_remove_from_hass(self) -> None # 从 HA 移除前的钩子
def async_on_remove(self, func) -> None # 注册移除时的清理回调
|
关键属性设置模式:Entity 大量使用 _attr_* 类属性来提供默认值,子类可以直接设置类属性而不需要定义 property:
1
2
3
| class MyEntity(Entity):
_attr_should_poll = False # 等价于 should_poll = False
_attr_icon = "mdi:lightbulb" # 等价于 icon = "mdi:lightbulb"
|
2.6 ToggleEntity — 开关实体基类
源码: homeassistant/helpers/entity.py
继承自 Entity,添加开/关语义。
1
2
3
4
5
6
7
8
9
10
11
12
| class ToggleEntity(Entity):
_attr_is_on: bool | None = None
@property
@final
def state(self) -> Literal["on", "off"] | None:
if (is_on := self.is_on) is None: return None
return STATE_ON if is_on else STATE_OFF
async def async_turn_on(self, **kwargs) -> None: ... # 子类必须实现
async def async_turn_off(self, **kwargs) -> None: ... # 子类必须实现
async def async_toggle(self, **kwargs) -> None: ... # 默认实现调用 turn_on/off
|
2.7 ConfigEntry 类 — 配置条目
源码: homeassistant/config_entries.py
每个用户配置的集成实例对应一个 ConfigEntry,是 Config Entry 驱动架构的核心数据结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class ConfigEntry[_DataT = Any]:
entry_id: str # 唯一 ID (ULID)
domain: str # 所属集成域名
title: str # 显示标题
data: MappingProxyType # 配置数据(如 host, api_key)
runtime_data: _DataT # 运行时数据(泛型,集成可自定义类型)
options: MappingProxyType # 用户选项
unique_id: str | None # 去重 ID
state: ConfigEntryState # 状态
version: int # 配置迁移版本
minor_version: int # 配置迁移次版本号
source: str # 来源: user, bluetooth, dhcp, zeroconf, ...
subentries: MappingProxyType[str, ConfigSubentry] # 子条目
disabled_by: ConfigEntryDisabler | None
|
ConfigEntryState 生命周期:
1
2
3
4
5
6
| NOT_LOADED → SETUP_IN_PROGRESS → LOADED
→ SETUP_ERROR
→ SETUP_RETRY
→ MIGRATION_ERROR
LOADED → UNLOAD_IN_PROGRESS → NOT_LOADED
→ FAILED_UNLOAD
|
runtime_data 模式:集成通过泛型参数声明运行时数据类型:
1
2
3
4
5
6
7
| type HueConfigEntry = ConfigEntry[HueBridge]
# 在 async_setup_entry 中:
entry.runtime_data = bridge
# 在平台文件中:
bridge = config_entry.runtime_data # 自动具有 HueBridge 类型
|
2.8 Integration 类 — 集成定义
源码: homeassistant/loader.py
代表一个已解析的集成,包含 manifest 信息和模块引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class Integration:
domain: str
name: str
integration_type: str
dependencies: list[str]
after_dependencies: list[str]
requirements: list[str]
disabled: str | None
manifest: dict
@classmethod
def resolve_from_root(cls, hass, root_module, domain) -> Integration | None
async def async_get_component(self) -> ModuleType # 获取 __init__.py 模块
async def async_get_platform(self, platform) -> ModuleType # 获取平台模块
async def resolve_dependencies(self) -> bool | None
|
关键数据键:
DATA_COMPONENTS — 已加载的组件模块缓存DATA_INTEGRATIONS — 已解析的 Integration 对象缓存DATA_CUSTOM_COMPONENTS — 自定义集成缓存
3. 启动流程详解
3.1 完整启动链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| __main__.py::main()
├── validate_python() # 校验 Python 版本
├── validate_os() # 校验操作系统
├── ensure_config_path() # 确保配置目录存在
├── restore_backup() # 检查是否需要从备份恢复
└── runner.run(runtime_config)
├── HassEventLoopPolicy 设置 # 自定义事件循环策略
│ ├── 64 workers 线程池
│ ├── 调试模式
│ └── loop.time = monotonic
├── asyncio.new_event_loop()
└── loop.run_until_complete(setup_and_run_hass())
├── bootstrap.async_setup_hass()
│ ├── create_hass()
│ │ ├── core.HomeAssistant(config_dir)
│ │ │ ├── EventBus
│ │ │ ├── ServiceRegistry
│ │ │ └── StateMachine
│ │ ├── loader.async_setup(hass)
│ │ └── async_enable_logging()
│ ├── conf_util.async_ensure_config_exists()
│ ├── conf_util.process_ha_config_upgrade()
│ ├── conf_util.async_hass_config_yaml()
│ └── async_from_config_dict(config_dict, hass)
│ ├── ConfigEntries(hass, config)
│ ├── loader.async_get_custom_components()
│ ├── async_load_base_functionality()
│ ├── async_setup_component("homeassistant")
│ ├── async_setup_component("persistent_notification")
│ ├── async_process_ha_core_config()
│ └── _async_set_up_integrations()
│ ├── 解析依赖关系
│ ├── Stage 0: 核心基础设施
│ ├── Stage 1: 发现和云服务
│ └── Stage 2: 所有其余集成
└── hass.async_run()
├── hass.async_start()
│ ├── CoreState → STARTING
│ ├── fire EVENT_HOMEASSISTANT_START
│ ├── CoreState → RUNNING
│ └── fire EVENT_HOMEASSISTANT_STARTED
└── await _stopped.wait()
|
3.2 启动阶段
Stage 0 — 核心基础设施(分多个子阶段):
| 子阶段 | 集成 | 说明 |
|---|
| logging, http deps | isal, logger, network, system_log, sentry | 日志和网络基础 |
| labs | labs | 实验性功能 |
| frontend | frontend | 前端 UI |
| recorder | recorder | 状态历史记录 |
| debugger | debugpy | 调试器 |
| zeroconf | zeroconf | mDNS 发现 |
Stage 1 — 发现和云服务(超时 120s):
- bluetooth, dhcp, ssdp, usb — 发现集成
- mqtt_eventstream, cloud, hassio
Stage 2 — 所有其余集成(超时 300s)
3.3 async_load_base_functionality — 注册表并行加载
启动时并行加载以下核心模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| await asyncio.gather(
entity.async_setup(hass), # 实体源
frame.async_setup(hass), # 帧追踪
template.async_setup(hass), # 模板引擎
translation.async_setup(hass), # 翻译
device_registry.async_load(), # 设备注册表
area_registry.async_load(), # 区域注册表
entity_registry.async_load(), # 实体注册表
floor_registry.async_load(), # 楼层注册表
issue_registry.async_load(), # 问题注册表
label_registry.async_load(), # 标签注册表
restore_state.async_load(), # 状态恢复
hass.config_entries.async_initialize(), # 配置条目
...
)
|
3.4 恢复模式
如果核心集成(如 frontend)加载失败或配置解析出错:
- 停止当前 hass 实例
- 重新创建 hass 实例
- 仅加载最小集:
backup、cloud、frontend + http 配置
4. 集成加载机制
4.1 async_setup_component 完整流程
源码: homeassistant/setup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| async_setup_component(hass, domain, config)
│
├── 1. 检查 hass.config.components — 已加载则返回 True
├── 2. 检查 _DATA_SETUP — 正在加载则 await 已有 Future
├── 3. 创建新 Future 注册到 _DATA_SETUP
│
└── 4. 调用 _async_setup_component(hass, domain, config)
│
├── 阶段1: 获取 Integration 定义
│ └── loader.async_get_integration(hass, domain)
│ ├── 先查 DATA_INTEGRATIONS 缓存
│ ├── 内置集成: Integration.resolve_from_root(hass, components, domain)
│ └── 自定义集成: 从 custom_components 包解析
│
├── 阶段2: 检查集成是否被禁用
│
├── 阶段3: 预加载翻译(异步启动,稍后等待)
│
├── 阶段4: 解析依赖关系
│ └── integration.resolve_dependencies()
│ 检查依赖是否存在 + 循环依赖检测
│
├── 阶段5: 处理依赖和需求
│ └── async_process_deps_reqs(hass, config, integration)
│ ├── _async_process_dependencies() — 并行加载 dependencies + after_dependencies
│ │ ├── dependencies: 强制加载(async_setup_component)
│ │ └── after_dependencies: 仅等待已计划安装的依赖
│ └── requirements.async_get_integration_with_requirements() — 安装 pip 包
│
├── 阶段6: 导入组件模块
│ └── integration.async_get_component() → importlib.import_module()
│
├── 阶段7: 校验配置
│ ├── conf_util.async_process_component_config() — 用 CONFIG_SCHEMA 校验
│ ├── conf_util.async_handle_component_errors()
│ └── conf_util.async_drop_config_annotations()
│
├── 阶段8: 执行安装
│ ├── 如果有 component.async_setup → task = component.async_setup(hass, config)
│ ├── 否则如果有 component.setup → 线程池执行 component.setup(hass, config)
│ ├── 否则如果没有 async_setup_entry → 记录错误返回 False
│ ├── async_timeout(SLOW_SETUP_MAX_WAIT=300s) 等待结果
│ └── 等待翻译加载完成
│
├── 阶段9: 等待 Config Flow 导入完成
│ └── hass.config_entries.flow.async_wait_import_flow_initialized(domain)
│
└── 阶段10: 设置 Config Entry + 标记完成
├── hass.config.components.add(domain) ← 先标记,防止死锁
├── 并行安装所有 ConfigEntry:
│ await asyncio.gather(*(
│ entry.async_setup_locked(hass, integration=integration)
│ for entry in hass.config_entries.async_entries(domain)
│ ))
└── 触发 EVENT_COMPONENT_LOADED 事件
|
4.2 依赖解析细节
_async_process_dependencies() 的行为:
dependencies(前置依赖):强制加载。如果依赖未安装,调用 async_setup_component 安装。after_dependencies(后置依赖):仅等待已计划安装的依赖,不主动触发安装,避免死锁。
1
2
3
4
5
6
7
8
9
10
| # 伪代码
for dep in integration.dependencies:
if dep not in setup and dep not in components:
create_task(async_setup_component(hass, dep, config)) # 强制安装
for dep in integration.after_dependencies:
if dep in setup_done_tasks: # 只等待已计划的
wait_tasks.append(setup_done_tasks[dep])
await asyncio.gather(*all_tasks)
|
4.3 ConfigEntry 的 setup 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| entry.async_setup_locked(hass, integration=integration)
│
├── 检查 entry.state (必须是 NOT_LOADED)
├── entry.state = SETUP_IN_PROGRESS
│
└── component.async_setup_entry(hass, entry)
│ ← 集成 __init__.py 中定义的 async_setup_entry 函数
│
├── 初始化连接/API
├── 创建运行时对象(如 HueBridge)
├── entry.runtime_data = 运行时对象
├── hass.config_entries.async_forward_entry_setups(entry, platforms)
│ └── 转发到各平台设置
└── 注册更新监听器等
|
4.4 错误处理与重试
| 错误类型 | 处理方式 |
|---|
IntegrationNotFound | 记录错误 + 创建 repair issue |
| 集成被禁用 | 记录错误 + 持久通知 |
| 依赖解析失败 | 记录错误 |
ImportError | 记录错误 + 持久通知 |
| 配置校验失败 | 记录错误 + 持久通知 |
TimeoutError(超 300 秒) | 记录错误 |
PlatformNotReady | 线性递增退避重试(基础等待 30s,min(tries,6)*30 秒后重试) |
ConfigEntryNotReady | 标记为 SETUP_RETRY,自动重试 |
5. 配置流(ConfigFlow)机制
5.1 类继承链
1
2
3
4
| FlowHandler (data_entry_flow.py)
└── ConfigEntryBaseFlow (config_entries.py)
├── ConfigFlow — 新设备配置流程
└── OptionsFlow — 已配置设备的选项流程
|
5.2 FlowResultType — 流程结果类型
| 类型 | 说明 |
|---|
FORM | 显示表单,等待用户输入 |
CREATE_ENTRY | 创建条目,流程成功结束 |
ABORT | 中止流程 |
EXTERNAL_STEP | 外部步骤(如 OAuth 跳转) |
EXTERNAL_STEP_DONE | 外部步骤完成 |
SHOW_PROGRESS | 显示进度 |
SHOW_PROGRESS_DONE | 进度完成 |
MENU | 显示导航菜单 |
5.3 FlowHandler 基类核心方法
1
2
3
4
5
6
7
8
9
10
| class FlowHandler:
# 结果返回方法
def async_show_form(self, *, step_id, data_schema, errors, ...) → FORM
def async_create_entry(self, *, title, data, ...) → CREATE_ENTRY
def async_abort(self, *, reason, ...) → ABORT
def async_external_step(self, *, step_id, url, ...) → EXTERNAL_STEP
def async_external_step_done(self, *, next_step_id) → EXTERNAL_STEP_DONE
def async_show_progress(self, *, step_id, progress_action, progress_task, ...) → SHOW_PROGRESS
def async_show_progress_done(self, *, next_step_id) → SHOW_PROGRESS_DONE
def async_show_menu(self, *, step_id, menu_options, ...) → MENU
|
5.4 FlowManager 核心流程
1
2
3
4
5
6
7
8
9
10
11
12
13
| FlowManager.async_init(handler, *, context, data)
│
├── async_create_flow(handler, context, data) → 创建 FlowHandler 实例
├── 分配 flow_id (UUID)
├── 注册到 _progress 索引
└── _async_handle_step(flow, flow.init_step, data)
│ ← init_step 默认为 "init",但 ConfigFlow 中被覆盖为 context["source"]
│
├── getattr(flow, f"async_step_{step_id}")(user_input)
│ ← 反射调用步骤方法
│
├── 如果结果在 FLOW_NOT_COMPLETE_STEPS 中 → 设置 cur_step,流程继续
└── 否则 → async_finish_flow(),流程结束
|
5.5 ConfigFlow 类详解
源码: homeassistant/config_entries.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| class ConfigFlow(ConfigEntryBaseFlow):
# 自动注册:子类声明 domain 时自动注册到 HANDLERS 注册表
def __init_subclass__(cls, *, domain=None, **kwargs):
if domain is not None:
HANDLERS.register(domain)(cls)
# unique_id 管理
async def async_set_unique_id(self, unique_id, *, raise_on_progress=True) -> ConfigEntry | None
def _abort_if_unique_id_configured(self, updates=None, *, error="already_configured") -> None
# 发现步骤方法(全部默认委托到 _async_step_discovery_without_unique_id → async_step_user)
async def async_step_user(self, user_input) # 用户手动添加
async def async_step_bluetooth(self, discovery_info) # 蓝牙发现
async def async_step_dhcp(self, discovery_info) # DHCP 发现
async def async_step_zeroconf(self, discovery_info) # mDNS/Zeroconf 发现
async def async_step_homekit(self, discovery_info) # HomeKit 发现
async def async_step_hassio(self, discovery_info) # Hass.io 发现
async def async_step_ssdp(self, discovery_info) # SSDP 发现
async def async_step_usb(self, discovery_info) # USB 发现
async def async_step_mqtt(self, discovery_info) # MQTT 发现
async def async_step_integration_discovery(self, discovery_info) # 集成发现
async def async_step_import(self, import_data) # YAML 导入
async def async_step_ignore(self, user_input) # 忽略发现
async def async_step_reauth(self, user_input) # 重新认证
async def async_step_reconfigure(self, user_input) # 重新配置
# 选项流支持
@staticmethod
def async_get_options_flow(config_entry) -> OptionsFlow # 子类覆盖以支持选项
# 结果方法
def async_create_entry(self, *, title, data, options, ...) -> ConfigFlowResult
def async_abort(self, *, reason, next_flow, ...) -> ConfigFlowResult
def async_update_and_abort(self, entry, *, unique_id, title, data, ...) -> ConfigFlowResult
def async_update_reload_and_abort(self, entry, ...) -> ConfigFlowResult
|
5.6 配置流程完整示例
用户手动添加集成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 1. 用户点击"添加集成"
↓
2. ConfigEntriesFlowManager.async_init("my_integration", context={"source": "user"})
↓
3. async_create_flow() → 实例化 MyConfigFlow, 设置 init_step="user"
↓
4. _async_handle_step(flow, "user", None)
↓
5. MyConfigFlow.async_step_user(None)
→ 返回 async_show_form(step_id="user", data_schema=SCHEMA)
↓
6. 用户填写表单,前端调用 async_configure(flow_id, user_input)
↓
7. Schema 校验 user_input
↓
8. _async_handle_step(flow, "user", validated_input)
↓
9. MyConfigFlow.async_step_user(validated_input)
→ 调用 async_set_unique_id(device_id)
→ 返回 async_create_entry(title="My Device", data={...})
↓
10. async_finish_flow() → 创建 ConfigEntry → async_add() → 完成
|
自动发现流程:
1
2
3
4
5
6
7
| 1. 蓝牙发现设备
→ async_init("my_integration", context={"source": "bluetooth"}, data=BluetoothServiceInfoBleak(...))
↓
2. init_step="bluetooth" → 调用 async_step_bluetooth(discovery_info)
↓
3. 如果未覆盖: 默认实现调用 _async_step_discovery_without_unique_id() → async_step_user()
如果已覆盖: 通常先 async_set_unique_id(),再返回表单或直接创建条目
|
5.7 OptionsFlow — 选项流
1
2
3
4
5
6
7
| class OptionsFlow(ConfigEntryBaseFlow):
@property
def config_entry(self) -> ConfigEntry: ... # 关联的 ConfigEntry
# 常用变体
class OptionsFlowWithReload(OptionsFlow):
automatic_reload: bool = True # 选项变更后自动重新加载 ConfigEntry
|
集成通过在 ConfigFlow 中覆盖 async_get_options_flow 支持选项流:
1
2
3
4
5
| class MyConfigFlow(ConfigFlow, domain="my_integration"):
@staticmethod
@callback
def async_get_options_flow(config_entry) -> OptionsFlow:
return MyOptionsFlow(config_entry)
|
6. 实体(Entity)体系
6.1 Entity 生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| 创建实体实例
│
↓ EntityPlatform.async_add_entities() → _async_add_entity()
│
├── entity.add_to_platform_start()
│ ├── 设置 entity.hass = hass
│ ├── 设置 entity.platform = entity_platform
│ └── 设置 parallel_updates 信号量
│
├── 可选: entity.async_device_update() (update_before_add=True 时)
│
├── 生成/查找 entity_id
│ ├── 有 unique_id → 通过 entity_registry 查找或创建
│ └── 无 unique_id → 根据 suggested_object_id 生成
│
├── hass.states.async_reserve(entity_id) 防止并发冲突
│
├── 注册移除回调
│
└── entity.add_to_platform_finish()
├── 调用 async_internal_added_to_hass()
├── 调用 async_added_to_hass() ← 子类覆盖的钩子
└── async_write_ha_state() 写入初始状态
|
6.2 状态更新机制
1
2
3
4
5
6
7
8
| # 推荐方式 — 立即写入状态
entity.async_write_ha_state()
# 传统方式 — 先更新再写入
entity.async_update_ha_state(force_refresh=False)
# 从设备拉取最新数据
entity.async_device_update()
|
async_write_ha_state() 的内部流程:
- 收集实体的 state、capability_attributes、state_attributes
- 构造
State 对象 - 调用
hass.states.async_set_internal(entity_id, state, attr, force_update, context, state_info, time_now) - 触发
EVENT_STATE_CHANGED 事件
6.3 平台特定实体类
LightEntity
继承链: Entity → ToggleEntity → LightEntity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class LightEntity(ToggleEntity):
# 核心属性
brightness: int | None # 亮度 0..255
color_mode: ColorMode | None # 当前颜色模式
supported_color_modes: set[ColorMode] | None # 支持的颜色模式
hs_color: tuple[float, float] | None # HS 颜色
rgb_color: tuple[int, int, int] | None # RGB 颜色
color_temp_kelvin: int | None # 色温(开尔文)
min_color_temp_kelvin: int # 最小色温
max_color_temp_kelvin: int # 最大色温
effect: str | None # 当前效果
effect_list: list[str] | None # 效果列表
# 核心方法
async def async_turn_on(self, **kwargs) # 开灯
async def async_turn_off(self, **kwargs) # 关灯
async def async_toggle(self, **kwargs) # 切换(重写自 ToggleEntity)
# 色彩模式 ColorMode
# ONOFF, BRIGHTNESS, HS, XY, RGB, RGBW, RGBWW, COLOR_TEMP, WHITE, UNKNOWN
# 特性标志 LightEntityFeature (IntFlag)
# EFFECT = 4, FLASH = 8, TRANSITION = 32
|
async_setup 模式:
1
2
3
4
5
6
7
8
9
10
11
| # light/__init__.py
async def async_setup(hass, config):
component = EntityComponent[LightEntity](_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
# 注册实体服务
component.async_register_entity_service(SERVICE_TURN_ON, ..., async_handle_light_on_service)
component.async_register_entity_service(SERVICE_TURN_OFF, ..., async_handle_light_off_service)
component.async_register_entity_service(SERVICE_TOGGLE, ..., async_handle_toggle_service)
async def async_setup_entry(hass, entry):
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
SensorEntity
继承链: Entity → SensorEntity
1
2
3
4
5
6
7
8
9
10
11
| class SensorEntity(Entity):
native_value: StateType | date | datetime | Decimal # 传感器原始值
native_unit_of_measurement: str | None # 原始计量单位
device_class: SensorDeviceClass # 设备类
state_class: SensorStateClass # 状态类 (MEASUREMENT/TOTAL/TOTAL_INCREASING)
suggested_display_precision: int | None # 建议显示精度
suggested_unit_of_measurement: str | None # 建议显示单位
options: list[str] | None # 枚举选项
# state 属性是 @final,不可重写
# 内部自动处理: 单位转换、精度、校验
|
SensorEntityDescription 额外字段:device_class, native_unit_of_measurement, options, state_class, suggested_display_precision, suggested_unit_of_measurement
BinarySensorEntity
继承链: Entity → BinarySensorEntity
1
2
3
4
5
| class BinarySensorEntity(Entity):
is_on: bool | None # 核心属性
device_class: BinarySensorDeviceClass # 设备类 (battery, motion, door, ...)
# state 属性是 @final,根据 is_on 返回 "on" 或 "off"
|
这是最简单的实体类之一,只需实现 is_on。
6.4 EntityDescription — 实体描述模式
用于将实体属性从子类移到描述对象,减少子类数量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from homeassistant.helpers.entity import EntityDescription
# 定义描述
SENSOR_DESCRIPTION = SensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement="°C",
state_class=SensorStateClass.MEASUREMENT,
)
# 使用描述
class MySensor(SensorEntity):
def __init__(self, description):
self.entity_description = description
self._attr_unique_id = f"{description.key}_sensor"
self._native_value = None
@property
def native_value(self):
return self._native_value
|
6.5 CoordinatorEntity — 协调器实体
用于配合 DataUpdateCoordinator 实现轮询式更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator
coordinator = DataUpdateCoordinator(
hass, LOGGER,
update_method=async_fetch_data,
update_interval=timedelta(seconds=30),
)
class MyEntity(CoordinatorEntity, LightEntity):
def __init__(self, coordinator, light_data):
super().__init__(coordinator)
self.light_data = light_data
@property
def brightness(self):
return self.coordinator.data[self.light_data.id]["brightness"]
# CoordinatorEntity 自动在 coordinator 刷新时调用 async_write_ha_state()
|
7. 服务(Service)机制
7.1 服务注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # 域级服务注册
hass.services.async_register(
domain="light",
service="turn_on",
service_func=async_handle_turn_on,
schema=TURN_ON_SCHEMA,
)
# 实体服务注册(更常用)
component.async_register_entity_service(
SERVICE_TURN_ON,
TURN_ON_SCHEMA,
"async_turn_on", # 字符串,映射到实体方法名
)
# 管理员服务注册
async_register_admin_service(hass, domain, service, service_func, schema)
|
7.2 服务调用流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 用户/自动化调用 light.turn_on(entity_id="light.living_room", brightness=255)
│
├── ServiceRegistry.async_call("light", "turn_on", service_data)
│ ├── 查找 handler = _services["light"]["turn_on"]
│ ├── 校验 return_response 与 supports_response
│ ├── Schema 校验 service_data
│ ├── 创建 ServiceCall 对象
│ ├── bus.fire(EVENT_CALL_SERVICE)
│ └── 执行服务
│
└── 实体服务调用链:
entity_service_call(hass, entities, service_func, call)
├── _resolve_entity_service_call_entities() # 解析目标实体
│ ├── 根据 entity_id/area_id/device_id 解析
│ ├── 过滤不可用的实体
│ ├── 过滤不满足 required_features 的实体
│ └── 过滤不满足 entity_device_classes 的实体
│
└── _async_handle_entity_calls([(entity, _handle_single_entity_call(...))])
├── 单实体: entity.async_request_call(coro)
├── 多实体: asyncio.gather 并发调用
└── _handle_single_entity_call()
├── func 是字符串 → getattr(entity, func)(**data)
│ 如 entity.async_turn_on(brightness=255)
└── func 是 HassJob → hass.async_run_hass_job(func, entity, data)
|
7.3 批量实体服务
async_register_batched_entity_service 注册的服务将所有匹配实体一次性传给服务函数:
1
2
3
4
5
| component.async_register_batched_entity_service(
SERVICE_GET_FORECASTS,
GET_FORECASTS_SCHEMA,
async_get_forecasts, # func(entities, service_call)
)
|
8.1 EntityComponent — 域级编排器
源码: homeassistant/helpers/entity_component.py
EntityComponent 是域级组件(如 light、sensor)的核心编排器,管理该域下所有平台和实体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| class EntityComponent[_EntityT = Entity]:
def __init__(self, logger, domain, hass, scan_interval):
# 创建根平台(与域同名的 "catch-all" 平台)
self._platforms: dict[str | tuple, EntityPlatform] = {}
self._entities: dict[str, Entity] # 引用根平台的 domain_entities
hass.data[DATA_INSTANCES][domain] = self # 全局注册
async def async_setup(self, config):
# 遍历配置中的平台,为每个平台创建异步任务
async def async_setup_entry(self, config_entry):
# 导入平台模块,创建 EntityPlatform,调用 EntityPlatform.async_setup_entry()
async def async_unload_entry(self, config_entry):
# 弹出平台,调用 EntityPlatform.async_reset()
async_register_entity_service(...) # 注册逐实体服务
async_register_batched_entity_service(...) # 注册批量服务
|
源码: homeassistant/helpers/entity_platform.py
每个 EntityPlatform 实例负责一种集成在一种域下的所有实体。
1
2
3
4
5
6
7
| class EntityPlatform:
def __init__(self, *, hass, logger, domain, platform_name, platform, scan_interval, entity_namespace):
self.entities: dict[str, Entity] = {} # 当前平台的实体
self.domain_entities: dict[str, Entity] # 按域索引的所有实体
self.domain_platform_entities: dict[str, Entity] # 按(域,平台名)索引的实体
self.config_entry: ConfigEntry | None = None
self._setup_complete: bool = False
|
两条设置路径:
1
2
3
4
5
6
7
8
9
| 路径 A: YAML 配置
EntityPlatform.async_setup(platform_config, discovery_info)
├── 检查平台模块是否有 async_setup_platform / setup_platform
└── 调用 platform.async_setup_platform(hass, config, async_add_entities, discovery_info)
路径 B: ConfigEntry
EntityPlatform.async_setup_entry(config_entry)
├── 保存 self.config_entry = config_entry
└── 调用 platform.async_setup_entry(hass, config_entry, async_add_entities)
|
共享核心 _async_setup_platform():
- 设置
current_platform 上下文变量 - 加载翻译
- 设置慢启动警告(10秒)
- 执行 setup awaitable(60秒超时)
- 等待所有实体添加任务完成
- 异常处理:
PlatformNotReady → 线性递增退避重试(min(tries,6)*30 秒) - 成功:
hass.config.components.add(full_name), _setup_complete = True
8.3 实体添加流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| 集成代码调用 async_add_entities(entities)
│ ← 这是 EntityPlatform 传给平台的回调
│
├── _async_schedule_add_entities() / _async_schedule_add_entities_for_entry()
│ └── 创建 task → async_add_entities()
│
└── async_add_entities(new_entities, update_before_add, *, config_subentry_id=None)
├── 验证 config_subentry_id
├── update_before_add=True → _async_add_and_update_entities() (并发)
└── update_before_add=False → _async_add_entities() (顺序)
│
└── 逐个调用 _async_add_entity(entity, ...)
├── entity.add_to_platform_start() # 设置 hass/platform 引用
├── 可选: entity.async_device_update()
├── 生成/查找 entity_id
│ ├── 有 unique_id → entity_registry 查找/创建
│ └── 无 unique_id → suggested_object_id 生成
├── 禁用检查 → 已禁用则跳过
├── 注册到三个字典:
│ self.entities[entity_id] = entity
│ self.domain_entities[entity_id] = entity
│ self.domain_platform_entities[entity_id] = entity
├── hass.states.async_reserve(entity_id)
└── entity.add_to_platform_finish()
├── async_internal_added_to_hass()
├── async_added_to_hass() ← 子类钩子
└── async_write_ha_state()
|
8.4 轮询机制
当有 should_poll=True 的实体时,EntityPlatform 启动轮询定时器:
1
2
3
4
5
6
7
8
9
10
| _async_handle_interval_callback()
├── 重新调度自身(scan_interval 秒后)
└── 创建后台任务 → _async_update_entity_states()
├── 少量实体: 顺序更新
└── 多量实体: 并发 gather 更新
并行度控制: PARALLEL_UPDATES 常量
├── 平台模块可定义 PARALLEL_UPDATES 常量
├── 未定义且有同步 update 方法 → 默认为 1
└── 设为 0 则不限制并行
|
8.5 架构层次总览
1
2
3
4
5
6
7
8
9
10
| HomeAssistant (hass)
└─ EntityComponent (如 "light")
│ 管理 domain 级别的逻辑
│
├─ EntityPlatform (platform_name="light") ← 根平台,catch-all
├─ EntityPlatform (platform_name="hue") ← hue 集成的 light 平台
├─ EntityPlatform (platform_name="tplink") ← tplink 集成的 light 平台
└─ EntityPlatform (platform_name="zwave") ← zwave 集成的 light 平台
│
└─ entities: {entity_id: Entity} ← 该平台管理的所有实体
|
hass.data 中的关键数据结构:
| Key | 类型 | 说明 |
|---|
DATA_ENTITY_PLATFORM | dict[str, list[EntityPlatform]] | 按集成名索引的所有 EntityPlatform |
DATA_DOMAIN_ENTITIES | dict[str, dict[str, Entity]] | 按域索引的所有实体 |
DATA_DOMAIN_PLATFORM_ENTITIES | dict[tuple, dict[str, Entity]] | 按(域, 平台名)索引的实体 |
DATA_INSTANCES | dict[str, EntityComponent] | 按域索引的 EntityComponent 实例 |
9. 集成实现完整示例:Hue
9.1 整体架构
1
2
3
4
5
| 用户发现/配置桥接器
→ ConfigFlow 创建 ConfigEntry
→ async_setup_entry 创建 HueBridge 实例
→ HueBridge 初始化 API 并转发平台设置
→ 各平台 (light/sensor/...) 的 async_setup_entry 创建实体
|
9.2 __init__.py — 集成入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| DOMAIN = "hue"
type HueConfigEntry = ConfigEntry[HueBridge]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_setup_services(hass) # 注册全局服务
return True
async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
await check_migration(hass, entry)
bridge = HueBridge(hass, entry)
if not await bridge.async_initialize_bridge():
return False
# unique_id 修正 & 设备注册
return True
async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
return await entry.runtime_data.async_reset()
|
9.3 bridge.py — 桥接器管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| class HueBridge:
def __init__(self, hass, config_entry):
self.config_entry = config_entry
if self.api_version == 1:
self.api = HueBridgeV1(self.host, app_key, session)
else:
self.api = HueBridgeV2(self.host, app_key)
self.config_entry.runtime_data = self # 自注册
async def async_initialize_bridge(self) -> bool:
# 1. 连接 API
try:
async with asyncio.timeout(10):
await self.api.initialize()
except Unauthorized:
create_config_flow(self.hass, self.host) # 重新触发配对
return False
except (TimeoutError, ...):
raise ConfigEntryNotReady(...) # HA 会自动重试
# 2. 根据版本转发平台设置
if self.api_version == 1:
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS_v1
) # [BINARY_SENSOR, LIGHT, SENSOR]
else:
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS_v2
) # [BINARY_SENSOR, EVENT, LIGHT, SCENE, SENSOR, SWITCH]
# 3. 注册选项更新监听器
self.reset_jobs.append(
self.config_entry.add_update_listener(_update_listener)
)
return True
async def async_reset(self):
# 卸载所有平台 + 清理
while self.reset_jobs:
self.reset_jobs.pop()()
unload_success = await self.hass.config_entries.async_unload_platforms(
self.config_entry, PLATFORMS_v1 if ... else PLATFORMS_v2
)
if unload_success:
delattr(self.config_entry, "runtime_data")
return unload_success
|
9.4 config_flow.py — 配置流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class HueFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_user(self, user_input=None):
# 展示桥接器列表或手动输入
if user_input is None:
bridges = await discover_nupnp()
return self.async_show_form(step_id="init", ...)
async def async_step_link(self, user_input=None):
# 请求用户按 Link 按钮
if user_input is None:
return self.async_show_form(step_id="link")
try:
app_key = await create_app_key(bridge.host, ...)
except LinkButtonNotPressed:
errors["base"] = "register_failed"
return self.async_create_entry(
title=f"Hue Bridge {bridge.id}",
data={CONF_HOST: bridge.host, CONF_API_KEY: app_key, CONF_API_VERSION: 2},
)
# 三种发现入口
async def async_step_zeroconf(self, discovery_info): ... # mDNS 发现
async def async_step_homekit(self, discovery_info): ... # HomeKit 发现
async def async_step_import(self, import_data): ... # YAML 导入
|
9.5 light.py — 平台文件
1
2
3
4
5
6
| async def async_setup_entry(hass, config_entry, async_add_entities):
bridge = config_entry.runtime_data
if bridge.api_version == 1:
await setup_entry_v1(hass, config_entry, async_add_entities)
else:
await setup_entry_v2(hass, config_entry, async_add_entities)
|
V1 路径 — 轮询模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| async def setup_entry_v1(hass, config_entry, async_add_entities):
bridge = config_entry.runtime_data
# 创建 DataUpdateCoordinator(5秒轮询)
coordinator = DataUpdateCoordinator(
hass, LOGGER, name="light",
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
update_interval=timedelta(seconds=5),
)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise PlatformNotReady
# 创建实体
async_add_entities(HueLight(coordinator, light) for light in bridge.api.lights.values())
class HueLight(CoordinatorEntity, LightEntity):
_attr_should_poll = False # 由 Coordinator 管理更新
async def async_turn_on(self, **kwargs):
await bridge.async_request_call(self.light.set_state, **command)
|
V2 路径 — 事件驱动模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| async def setup_entry_v2(hass, config_entry, async_add_entities):
bridge = config_entry.runtime_data
controller = bridge.api.lights
# 立即添加所有当前灯
async_add_entities(make_light_entity(light) for light in controller)
# 注册事件监听器,动态添加新灯
config_entry.async_on_unload(
controller.subscribe(async_add_light, event_filter=EventType.RESOURCE_ADDED)
)
class HueLight(HueBaseEntity, LightEntity):
_attr_should_poll = False # V2 事件驱动,不需要轮询
class HueBaseEntity(Entity):
_attr_should_poll = False
async def async_added_to_hass(self):
# 订阅资源更新和删除事件
self.async_on_remove(
self.controller.subscribe(
self._handle_event, self.resource.id,
(EventType.RESOURCE_UPDATED, EventType.RESOURCE_DELETED),
)
)
def _handle_event(self, event_type, resource):
if event_type == EventType.RESOURCE_DELETED:
# 自动移除实体
else:
self.async_write_ha_state() # 推送状态更新
|
9.6 完整流程图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| ┌─────────────────────────────────────────────────────────────────┐
│ 1. 发现阶段 (config_flow.py) │
│ │
│ Zeroconf/HomeKit/手动 → HueFlowHandler │
│ → async_step_link() 请求用户按 Link 按钮 │
│ → create_app_key() 获取 API 密钥 │
│ → async_create_entry(data={host, api_key, api_version}) │
└───────────────────────────┬─────────────────────────────────────┘
│ 创建 ConfigEntry
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. 集成初始化 (__init__.py) │
│ │
│ async_setup_entry(hass, entry): │
│ → bridge = HueBridge(hass, entry) │
│ → bridge.async_initialize_bridge() │
│ → api.initialize() 连接 API │
│ → async_forward_entry_setups() 转发到各平台 │
│ → entry.runtime_data = bridge │
└───────────────────────────┬─────────────────────────────────────┘
│ 转发到平台
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. 平台初始化 (light.py → v1/light.py 或 v2/light.py) │
│ │
│ V1: DataUpdateCoordinator(5秒轮询) → HueLight(CoordinatorEntity)│
│ V2: 事件驱动 → HueLight(HueBaseEntity) → async_write_ha_state │
└───────────────────────────┬─────────────────────────────────────┤
│ 实体运行
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. 实体运行时 │
│ │
│ V1: Coordinator 定时轮询 → CoordinatorEntity 自动更新状态 │
│ V2: SSE 事件推送 → HueBaseEntity._handle_event → async_write │
│ 控制: bridge.async_request_call(api_method, **params) │
│ 卸载: bridge.async_reset() → unload_platforms → 清理 │
└─────────────────────────────────────────────────────────────────┘
|
10. 如何实现自己的集成
10.1 最小集成结构
1
2
3
4
| custom_components/my_integration/
├── manifest.json
├── __init__.py
└── sensor.py # 向 sensor 域提供实体(可选)
|
10.2 manifest.json
1
2
3
4
5
6
7
8
9
10
11
| {
"domain": "my_integration",
"name": "My Integration",
"codeowners": ["@your_github"],
"config_flow": true,
"documentation": "https://www.example.com",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["my-library==1.0.0"],
"zeroconf": ["_myservice._tcp.local."]
}
|
10.3 __init__.py — 集成入口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import Platform
DOMAIN = "my_integration"
type MyConfigEntry = ConfigEntry[MyRuntimeData]
PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
"""从 ConfigEntry 设置集成。"""
# 1. 初始化运行时数据
runtime_data = MyRuntimeData(hass, entry)
entry.runtime_data = runtime_data
# 2. 初始化连接
try:
await runtime_data.async_connect()
except ConnectionError as err:
raise ConfigEntryNotReady(f"Connection failed: {err}") from err
# 3. 转发平台设置
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# 4. 注册选项更新监听器
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
async def async_unload_entry(hass: HomeAssistant, entry: MyConfigEntry) -> bool:
"""卸载 ConfigEntry。"""
# 卸载所有平台
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
await entry.runtime_data.async_disconnect()
return unload_ok
async def _async_update_listener(hass: HomeAssistant, entry: MyConfigEntry) -> None:
"""选项变更时重新加载。"""
await hass.config_entries.async_reload(entry.entry_id)
|
10.4 config_flow.py — 配置流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| from homeassistant import config_entries
from homeassistant.core import callback
import voluptuous as vol
from .const import DOMAIN, CONF_HOST, CONF_API_KEY
class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""My Integration 配置流。"""
VERSION = 1
async def async_step_user(self, user_input=None):
"""用户手动配置步骤。"""
errors = {}
if user_input is not None:
# 验证连接
try:
await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
else:
# 设置唯一 ID 防止重复配置
await self.async_set_unique_id(user_input[CONF_HOST])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"My Device ({user_input[CONF_HOST]})",
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_HOST): str,
vol.Required(CONF_API_KEY): str,
}),
errors=errors,
)
async def async_step_zeroconf(self, discovery_info):
"""Zeroconf 自动发现步骤。"""
host = discovery_info.host
await self.async_set_unique_id(host)
self._abort_if_unique_id_configured()
# 展示确认表单
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_HOST, default=host): str,
vol.Required(CONF_API_KEY): str,
}),
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""选项流。"""
return MyOptionsFlow(config_entry)
class MyOptionsFlow(config_entries.OptionsFlowWithReload):
"""选项流。"""
def __init__(self, config_entry):
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
if user_input is not None:
return self.async_create_entry(data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
vol.Required("poll_interval", default=30): int,
}),
)
|
10.5 sensor.py — 向 sensor 域提供实体(CoordinatorEntity 模式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""设置传感器平台。"""
runtime_data = config_entry.runtime_data
# 创建协调器(轮询式)
coordinator = DataUpdateCoordinator(
hass,
__name__,
update_method=runtime_data.async_fetch_data,
update_interval=timedelta(seconds=30),
)
await coordinator.async_config_entry_first_refresh()
# 添加实体
async_add_entities(
MySensor(coordinator, description)
for description in SENSORS
)
SENSORS = [
SensorEntityDescription(
key="temperature",
name="Temperature",
native_unit_of_measurement="°C",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
]
class MySensor(CoordinatorEntity, SensorEntity):
"""My Integration 传感器实体。"""
_attr_has_entity_name = True
def __init__(self, coordinator, description):
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = description.key
@property
def native_value(self):
"""返回传感器值。"""
return self.coordinator.data.get(self.entity_description.key)
|
10.6 从 YAML 配置加载集成
前面展示的集成仅支持 ConfigFlow(UI 配置),但自定义集成也可以支持从 configuration.yaml 加载。这种模式下,用户在 YAML 中声明集成配置,HA 解析后调用集成的 async_setup 函数。
10.6.1 两种加载路径对比
| 特性 | ConfigFlow 路径 | YAML 路径 |
|---|
| 入口函数 | async_setup_entry(hass, entry) | async_setup(hass, config) |
| 配置来源 | UI 交互创建的 ConfigEntry | configuration.yaml 中的 YAML 配置 |
| 平台设置方式 | async_forward_entry_setups(entry, platforms) | 通过 EntityComponent.async_setup(config) 自动遍历 |
| 卸载函数 | async_unload_entry(hass, entry) | 无(HA 停止时自动清理) |
| 运行时数据 | entry.runtime_data | hass.data[DOMAIN] |
| 配置校验 | ConfigFlow 中的 Schema | CONFIG_SCHEMA / PLATFORM_SCHEMA |
10.6.2 仅 YAML 配置的集成
最简单的方式 — 集成只从 YAML 加载,不需要 ConfigFlow:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| # __init__.py
import voluptuous as vol
from homeassistant.core import HomeAssistant
from homeassistant.const import Platform
import homeassistant.helpers.config_validation as cv
DOMAIN = "my_integration"
PLATFORMS = [Platform.SENSOR]
# 定义 YAML 配置 Schema
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required("host"): str,
vol.Required("api_key"): cv.string,
vol.Optional("poll_interval", default=30): int,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""从 YAML 配置设置集成。"""
conf = config[DOMAIN]
# 存储配置到 hass.data,供平台文件使用
hass.data[DOMAIN] = {
"host": conf["host"],
"api_key": conf["api_key"],
"poll_interval": conf["poll_interval"],
}
# 创建 EntityComponent 并设置平台
component = EntityComponent(_LOGGER, DOMAIN, hass)
await component.async_setup(config)
return True
|
对应的平台文件(sensor.py)需要实现 async_setup_platform(传统方式):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # sensor.py
from homeassistant.components.sensor import SensorEntity
from homeassistant.core import HomeAssistant
async def async_setup_platform(
hass: HomeAssistant,
config: dict, # 平台级 YAML 配置(来自 configuration.yaml 中 my_integration: 下的 platform: sensor 块)
async_add_entities, # 添加实体的回调
discovery_info=None, # 发现信息(如果有)
) -> None:
"""从 YAML 配置设置传感器平台。"""
data = hass.data[DOMAIN] # 获取集成级配置
async_add_entities([
MySensor(data["host"], data["api_key"], "temperature"),
MySensor(data["host"], data["api_key"], "humidity"),
])
class MySensor(SensorEntity):
"""YAML 模式的传感器。"""
def __init__(self, host, api_key, sensor_type):
self._host = host
self._api_key = api_key
self._sensor_type = sensor_type
self._attr_unique_id = f"{host}_{sensor_type}"
self._attr_name = f"My {sensor_type}"
self._native_value = None
@property
def native_value(self):
return self._native_value
async def async_update(self):
"""EntityPlatform 轮询时调用此方法。"""
self._native_value = await fetch_value(self._host, self._api_key, self._sensor_type)
|
对应的 configuration.yaml:
1
2
3
4
| my_integration:
host: "192.168.1.100"
api_key: "abc123"
poll_interval: 30
|
10.6.3 同时支持 YAML 和 ConfigFlow
许多内置集成同时支持两种加载方式。通常的做法是:
- YAML 配置通过
async_setup 处理,并在其中将配置导入为 ConfigEntry - ConfigFlow 直接创建 ConfigEntry
- 核心逻辑统一由
async_setup_entry 处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
| # __init__.py
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import Platform
import homeassistant.helpers.config_validation as cv
DOMAIN = "my_integration"
PLATFORMS = [Platform.SENSOR]
# YAML 配置 Schema — 使用 config_entry_only_config_schema 可声明仅支持 Config Entry
# CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) # 仅 ConfigFlow
#
# 同时支持 YAML 和 ConfigFlow 时:
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required("host"): str,
vol.Required("api_key"): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""从 YAML 配置设置集成,并将配置导入为 ConfigEntry。"""
if DOMAIN not in config:
return True # 没有此集成的 YAML 配置,可能通过 ConfigFlow 配置
conf = config[DOMAIN]
# 检查是否已有同配置的 ConfigEntry(避免重复导入)
for entry in hass.config_entries.async_entries(DOMAIN):
if entry.source == SOURCE_IMPORT and entry.data.get("host") == conf["host"]:
return True # 已导入过
# 通过 ConfigFlow 导入 YAML 配置
# 这是 HA 生产代码中 YAML → ConfigEntry 导入的标准模式
# 参见:sleepiq, yeelight, lutron_caseta, sun, thread 等集成
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=conf,
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""从 ConfigEntry 设置集成(YAML 导入和 ConfigFlow 统一走此路径)。"""
# 1. 初始化运行时数据
runtime_data = MyRuntimeData(hass, entry)
entry.runtime_data = runtime_data
# 2. 初始化连接
try:
await runtime_data.async_connect()
except ConnectionError as err:
raise ConfigEntryNotReady(f"Connection failed: {err}") from err
# 3. 转发平台设置
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""卸载 ConfigEntry。"""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
await entry.runtime_data.async_disconnect()
return unload_ok
|
对应的 ConfigFlow 中增加 import 步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # config_flow.py
class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_import(self, import_data):
"""从 YAML 配置导入。"""
host = import_data["host"]
await self.async_set_unique_id(host)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"My Device ({host})",
data=import_data,
)
async def async_step_user(self, user_input=None):
# ... 用户手动配置步骤(同 10.4 节)
|
同时,平台文件需要同时实现 async_setup_platform(YAML 路径)和 async_setup_entry(ConfigEntry 路径):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| # sensor.py
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""ConfigEntry 路径设置传感器。"""
runtime_data = config_entry.runtime_data
async_add_entities([
MySensor(runtime_data, "temperature"),
MySensor(runtime_data, "humidity"),
])
async def async_setup_platform(
hass: HomeAssistant,
config: dict,
async_add_entities: AddEntitiesCallback,
discovery_info=None,
) -> None:
"""YAML 路径设置传感器(通常不再需要,因为 YAML 会导入为 ConfigEntry)。"""
# 如果使用了 YAML → ConfigEntry 导入模式,此函数通常不会被执行
# 但保留兼容性时,可以:
data = hass.data[DOMAIN]
async_add_entities([
MySensorLegacy(data["host"], data["api_key"], "temperature"),
])
|
某些集成(特别是 hub 型集成如 mqtt、hue)支持在 YAML 中为每个平台分别配置:
1
2
3
4
5
6
7
8
9
10
| # configuration.yaml
light:
- platform: my_integration
host: "192.168.1.100"
name: "Living Room Light"
sensor:
- platform: my_integration
host: "192.168.1.100"
sensor_type: "temperature"
|
此时平台文件需要定义 PLATFORM_SCHEMA:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # sensor.py
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.core import HomeAssistant
PLATFORM_SCHEMA = vol.Schema(
{
vol.Required("host"): str,
vol.Required("sensor_type"): str,
vol.Optional("name"): str,
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup_platform(
hass: HomeAssistant,
config: dict, # 已经过 PLATFORM_SCHEMA 校验的平台配置
async_add_entities,
discovery_info=None,
) -> None:
host = config["host"]
sensor_type = config["sensor_type"]
name = config.get("name", f"My {sensor_type}")
async_add_entities([MySensor(host, sensor_type, name)])
|
注意:PLATFORM_SCHEMA 在 sensor.py 等平台文件中定义,而 CONFIG_SCHEMA 在 __init__.py 中定义。两者的区别:
| Schema | 定义位置 | 校验范围 | 用途 |
|---|
CONFIG_SCHEMA | __init__.py | 整个集成的 YAML 配置块 | 集成级配置(host, api_key 等) |
PLATFORM_SCHEMA | 平台文件(sensor.py 等) | 每个平台条目的 YAML 配置 | 平台级配置(sensor_type, name 等) |
10.6.5 YAML 加载与 ConfigFlow 加载的内部流程差异
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| YAML 路径:
bootstrap._async_set_up_integrations()
→ 解析 configuration.yaml
→ async_setup_component("my_integration", config)
→ _async_setup_component()
→ integration.async_get_component() 导入 __init__.py
→ conf_util.async_process_component_config() 用 CONFIG_SCHEMA 校验
→ component.async_setup(hass, processed_config) ← 调用集成的 async_setup
→ EntityComponent.async_setup(config)
→ config_per_platform() 遍历各平台配置
→ async_setup_platform("sensor", platform_config)
→ 导入 sensor.py
→ EntityPlatform.async_setup(platform_config)
→ platform.async_setup_platform(hass, config, async_add_entities)
ConfigFlow 路径:
用户在 UI 中添加集成
→ ConfigEntriesFlowManager.async_init()
→ ConfigFlow → async_create_entry()
→ ConfigEntry 创建
→ _async_setup_component() 阶段 10
→ entry.async_setup_locked(hass)
→ component.async_setup_entry(hass, entry) ← 调用集成的 async_setup_entry
→ async_forward_entry_setups(entry, ["sensor"])
→ EntityComponent.async_setup_entry(entry)
→ 导入 sensor.py
→ EntityPlatform.async_setup_entry(config_entry)
→ platform.async_setup_entry(hass, config_entry, async_add_entities)
|
易混淆概念:discovery.async_load_platform vs hass.config_entries.flow.async_init
| hass.config_entries.flow.async_init | discovery.async_load_platform |
|---|
| 用途 | 启动 ConfigFlow,创建 ConfigEntry | 平台发现:动态将平台加载到另一个域 |
| YAML 导入 | ✅ 标准做法(66+ 个内置集成使用) | ❌ 不用于 YAML 导入 |
| 典型场景 | YAML → ConfigEntry 转换、UI 配置 | 集成动态加载自己的 notify 平台等 |
| 是否创建 ConfigEntry | 是 | 否,完全绕过 ConfigEntry |
| 源码实例 | sleepiq、yeelight、sun、thread | html5(notify)、template(多平台) |
async_load_platform 的工作原理:发送 EVENT_LOAD_PLATFORM dispatcher 信号 → EntityComponent 监听到信号 → 调用 _async_component_platform_discovered → 最终调用 async_setup_platform。它是一个平台发现机制,与 ConfigEntry 无关。
10.6.6 当前推荐做法
Home Assistant 社区的趋势是优先使用 ConfigFlow,YAML 配置正逐步被淘汰。对于新集成:
- 推荐:仅支持 ConfigFlow,使用
cv.config_entry_only_config_schema(DOMAIN) 声明不支持 YAML - 兼容:同时支持 YAML 和 ConfigFlow,YAML 配置通过
async_step_import 导入为 ConfigEntry - 不推荐:仅支持 YAML 配置(新集成不应采用)
1
2
3
4
| # 仅支持 ConfigFlow(推荐)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
# 不支持 YAML 配置(manifest.json 中设置 "config_flow": true)
|
10.7 不使用 Coordinator 的 SensorEntity 实现模式
前面 10.5 节展示了最常见的 CoordinatorEntity + SensorEntity 模式——通过 DataUpdateCoordinator 定时拉取数据,实体自动跟随刷新。然而,并非所有传感器都需要 Coordinator。许多内置集成只继承 SensorEntity 本身,根据数据来源的不同,采用两种截然不同的更新策略:
| 策略 | 适用场景 | 关键机制 | 典型集成 |
|---|
| 轮询模式 | 本地可定时请求的数据源 | async_update() + should_poll=True(默认) | moon |
| 推送模式 | 数据由外部事件推送或本地计算 | should_poll=False + async_write_ha_state() | sun |
10.7.1 轮询模式 — 纯 SensorEntity + async_update
当传感器需要定时从某个数据源拉取数据,且数据量较小、更新逻辑简单时,可以直接继承 SensorEntity,通过重写 async_update() 方法实现轮询更新。这种方式不需要 DataUpdateCoordinator,EntityPlatform 的轮询定时器会定期调用 async_update(),而 DataUpdateCoordinator 提供的错误重试、多实体共享刷新等能力在这种简单场景下并不必要。
源码实例:Moon 集成 (homeassistant/components/moon/)
Moon 集成追踪月相,数据来源是纯计算(astral.moon.phase()),无需外部 API,一个传感器就足够。
manifest.json — 注意 iot_class 为 calculated(计算型,不是 local_polling):
1
2
3
4
5
6
7
8
| {
"domain": "moon",
"name": "Moon",
"config_flow": true,
"integration_type": "service",
"iot_class": "calculated",
"single_config_entry": true
}
|
__init__.py — 极简入口,只转发平台设置:
1
2
3
4
5
6
7
8
9
10
| from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
config_flow.py — 极简配置流,不需要任何配置参数:
1
2
3
4
5
6
7
8
9
10
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from .const import DEFAULT_NAME, DOMAIN
class MoonConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
if user_input is not None:
return self.async_create_entry(title=DEFAULT_NAME, data={})
return self.async_show_form(step_id="user")
|
sensor.py — 核心实现,只继承 SensorEntity,不使用 CoordinatorEntity:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| from astral import moon
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
# 月相枚举值
STATE_FIRST_QUARTER = "first_quarter"
STATE_FULL_MOON = "full_moon"
STATE_LAST_QUARTER = "last_quarter"
STATE_NEW_MOON = "new_moon"
STATE_WANING_CRESCENT = "waning_crescent"
STATE_WANING_GIBBOUS = "waning_gibbous"
STATE_WAXING_CRESCENT = "waxing_crescent"
STATE_WAXING_GIBBOUS = "waxing_gibbous"
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
async_add_entities([MoonSensorEntity(entry)], True)
class MoonSensorEntity(SensorEntity):
"""Representation of a Moon sensor."""
_attr_has_entity_name = True
_attr_device_class = SensorDeviceClass.ENUM
_attr_options = [
STATE_NEW_MOON,
STATE_WAXING_CRESCENT,
STATE_FIRST_QUARTER,
STATE_WAXING_GIBBOUS,
STATE_FULL_MOON,
STATE_WANING_GIBBOUS,
STATE_LAST_QUARTER,
STATE_WANING_CRESCENT,
]
_attr_translation_key = "phase"
def __init__(self, entry: ConfigEntry) -> None:
self._attr_unique_id = entry.entry_id
self._attr_device_info = DeviceInfo(
name="Moon",
identifiers={(DOMAIN, entry.entry_id)},
entry_type=DeviceEntryType.SERVICE,
)
async def async_update(self) -> None:
"""Get the time and updates the states."""
today = dt_util.now().date()
state = moon.phase(today)
if state < 0.5 or state > 27.5:
self._attr_native_value = STATE_NEW_MOON
elif state < 6.5:
self._attr_native_value = STATE_WAXING_CRESCENT
elif state < 7.5:
self._attr_native_value = STATE_FIRST_QUARTER
elif state < 13.5:
self._attr_native_value = STATE_WAXING_GIBBOUS
elif state < 14.5:
self._attr_native_value = STATE_FULL_MOON
elif state < 20.5:
self._attr_native_value = STATE_WANING_GIBBOUS
elif state < 21.5:
self._attr_native_value = STATE_LAST_QUARTER
else:
self._attr_native_value = STATE_WANING_CRESCENT
|
关键要点:
should_poll 默认为 True:SensorEntity 默认就是轮询模式,不需要显式设置。EntityPlatform 会按 scan_interval 定时调用 async_update()。
async_update() 方法:这是轮询模式下 Entity 获取数据的入口。每次轮询时,EntityPlatform 先调用 async_device_update() → async_update(),然后自动调用 async_write_ha_state() 将更新写入状态机。因此 async_update() 中只需要更新 _attr_* 属性,不需要手动调用 async_write_ha_state()。
_attr_* 类属性:Moon 传感器大量使用 _attr_* 设置静态属性(_attr_has_entity_name、_attr_device_class、_attr_options、_attr_translation_key),在 async_update() 中通过 self._attr_native_value = ... 动态更新值。这是 HA 推荐的写法——避免定义过多的 property。
async_add_entities([MoonSensorEntity(entry)], True):第二个参数 True 表示 update_before_add,即添加实体前先调用一次 async_update(),确保实体有初始值。
何时选择此模式而非 CoordinatorEntity:
- 数据源是纯计算或本地文件读取(无网络请求)
- 只有一个或少数几个实体,不需要共享刷新逻辑
- 不需要 Coordinator 提供的错误重试、刷新状态追踪等能力
- 更新逻辑简单,不需要在多个实体间共享同一数据快照
10.7.2 推送模式 — SensorEntity + dispatcher + async_write_ha_state
当数据由外部事件推送(如 MQTT 消息、WebSocket 通知、定时事件变化)而非需要主动轮询时,应采用推送模式。核心思路是:should_poll = False(不轮询)+ 在事件回调中更新属性并调用 async_write_ha_state() 主动推送状态变更。
源码实例:Sun 集成 (homeassistant/components/sun/)
Sun 集成的传感器追踪太阳位置(方位角、仰角)和下一个日出/日落时间。太阳位置数据由 Sun 实体自行计算,根据太阳相位以不同间隔更新,通过 dispatcher 信号通知传感器刷新。
__init__.py — 入口创建 Sun 实体并注册为 runtime_data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from .entity import Sun, SunConfigEntry
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config) -> bool:
if not hass.config_entries.async_entries(DOMAIN):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config,
)
)
return True
async def async_setup_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
sun = Sun(hass)
component = EntityComponent[Sun](_LOGGER, DOMAIN, hass)
await component.async_add_entities([sun])
entry.runtime_data = sun
entry.async_on_unload(sun.remove_listeners)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: SunConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.async_remove()
return unload_ok
|
entity.py — Sun 实体自行计算太阳位置,并通过 dispatcher 信号推送更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| from homeassistant.helpers.dispatcher import async_dispatcher_send
SIGNAL_POSITION_CHANGED = "sun_position_changed"
SIGNAL_EVENTS_CHANGED = "sun_events_changed"
class Sun(Entity):
"""Representation of the Sun — 核心实体,自行计算并推送。"""
_attr_name = "Sun"
entity_id = "sun.sun"
# ... 初始化、位置计算等方法 ...
@callback
def update_events(self, now=None):
"""计算下一个日出/日落等事件,完成后发送信号。"""
# ... 计算逻辑 ...
async_dispatcher_send(self.hass, SIGNAL_EVENTS_CHANGED) # ← 通知传感器
# 设置定时器,在下一个事件时间再次调用 update_events
self._update_events_listener = event.async_track_point_in_utc_time(
self.hass, self.update_events, self._next_change
)
@callback
def update_sun_position(self, now=None):
"""计算当前太阳方位角和仰角,完成后发送信号。"""
self.solar_azimuth = round(
self.location.solar_azimuth(utc_point_in_time, self.elevation), 2
)
self.solar_elevation = round(
self.location.solar_elevation(utc_point_in_time, self.elevation), 2
)
self.async_write_ha_state() # ← Sun 实体自身也推送状态
async_dispatcher_send(self.hass, SIGNAL_POSITION_CHANGED) # ← 通知传感器
# 根据太阳相位设置不同间隔的定时器
delta = _PHASE_UPDATES[self.phase]
self._update_sun_position_listener = event.async_track_point_in_utc_time(
self.hass, self.update_sun_position, utc_point_in_time + delta
)
|
sensor.py — 传感器只继承 SensorEntity,通过 dispatcher 监听信号,收到信号时调用 async_write_ha_state():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| from dataclasses import dataclass
from homeassistant.components.sensor import (
SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass,
)
from homeassistant.const import DEGREE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED
from .entity import Sun, SunConfigEntry
# 自定义 EntityDescription,增加 value_fn 和 signal 字段
@dataclass(kw_only=True, frozen=True)
class SunSensorEntityDescription(SensorEntityDescription):
"""Describes a Sun sensor entity."""
value_fn: Callable[[Sun], StateType | datetime] # ← 从 Sun 实体获取值的函数
signal: str # ← 监听的 dispatcher 信号名
# 定义所有传感器的描述
SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = (
SunSensorEntityDescription(
key="next_rising",
device_class=SensorDeviceClass.TIMESTAMP,
translation_key="next_rising",
value_fn=lambda data: data.next_rising,
signal=SIGNAL_EVENTS_CHANGED,
),
SunSensorEntityDescription(
key="solar_elevation",
state_class=SensorStateClass.MEASUREMENT,
translation_key="solar_elevation",
value_fn=lambda data: data.solar_elevation,
native_unit_of_measurement=DEGREE,
signal=SIGNAL_POSITION_CHANGED,
entity_registry_enabled_default=False,
),
# ... 更多传感器描述 ...
)
async def async_setup_entry(
hass: HomeAssistant, entry: SunConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
sun = entry.runtime_data
async_add_entities(
[SunSensor(sun, description, entry.entry_id) for description in SENSOR_TYPES]
)
class SunSensor(SensorEntity):
"""Representation of a Sun Sensor — 纯推送模式。"""
_attr_has_entity_name = True
_attr_should_poll = False # ← 关键:不轮询!
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: SunSensorEntityDescription
def __init__(self, sun, entity_description, entry_id) -> None:
self.entity_description = entity_description
self._attr_unique_id = f"{entry_id}-{entity_description.key}"
self.sun = sun
@property
def native_value(self):
"""Return value of sensor — 从 Sun 实体直接读取计算好的值。"""
return self.entity_description.value_fn(self.sun)
async def async_added_to_hass(self) -> None:
"""Register signal listener when added to hass."""
await super().async_added_to_hass()
# ← 关键:监听 dispatcher 信号,收到信号时调用 async_write_ha_state()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
self.entity_description.signal,
self.async_write_ha_state,
)
)
|
关键要点:
_attr_should_poll = False:这是推送模式的核心标志。设置后,EntityPlatform 不会定时调用 async_update(),实体必须自己负责推送状态更新。
async_dispatcher_connect:在 async_added_to_hass() 中订阅 dispatcher 信号。当 Sun 实体计算完新数据后,发送 SIGNAL_POSITION_CHANGED 或 SIGNAL_EVENTS_CHANGED 信号,所有订阅该信号的 SunSensor 实体收到通知后调用 async_write_ha_state(),将最新的 native_value(通过 value_fn(self.sun) 从 Sun 实体获取)写入状态机。
value_fn + EntityDescription 模式:Sun 集成巧妙地在 SensorEntityDescription 中增加了 value_fn 字段,让每个传感器描述自带一个从 Sun 实体提取值的函数。这样一来,8 个传感器只需一个 SunSensor 类,通过不同的 entity_description 配置即可——避免了 8 个子类的冗余。
async_on_remove:在 async_added_to_hass() 中用 async_on_remove() 包裹信号订阅的取消逻辑,确保实体被移除时自动取消订阅,不会造成内存泄漏。
native_value property:推送模式中,native_value 不从 Coordinator 的 self.coordinator.data 中取值,而是直接从运行时对象(self.sun)读取。因为运行时对象已经通过定时器更新了数据,传感器只需在收到信号时反映最新值即可。
推送模式的其他常见实现方式:
dispatcher 信号是 HA 内部通信的一种方式,推送模式还有其他常见的事件监听手段:
| 监听方式 | 适用场景 | 注册方法 | 清理方法 |
|---|
| dispatcher 信号 | 同一集成内部通信 | async_dispatcher_connect(hass, signal, callback) | async_on_remove() 包裹 |
| EventBus 事件 | 监听 HA 全局事件(如状态变化) | hass.bus.async_listen(event_type, callback) | async_on_remove() 包裹 |
| state_change 事件 | 监听其他实体状态变化 | async_track_state_change_event(hass, [entity_ids], callback) | async_on_remove() 包裹 |
| MQTT 订阅 | 接收 MQTT 消息推送 | mqtt_subscription.async_subscribe_topics(hass, ...) | async_on_remove() 包裹 |
| WebSocket 回调 | 接收设备推送数据 | 在 API 客户端中注册回调 | 连接关闭时自动清理 |
所有监听方式的共同模式:在 async_added_to_hass() 中注册,在 async_on_remove() 或 async_will_remove_from_hass() 中取消。回调中更新 _attr_* 属性,然后调用 async_write_ha_state()。
何时选择推送模式而非 CoordinatorEntity:
- 数据源是事件驱动(设备主动推送状态、MQTT 消息、定时事件变化)
- 实体可以从已有的运行时对象中直接读取值,不需要独立的拉取逻辑
- 需要更细粒度的更新控制(如 Sun 根据不同太阳相位以不同间隔更新)
- 一个数据源驱动多个传感器,但数据已经在别处计算好(如 Sun 实体)
10.8 关键实现要点
runtime_data 模式:使用 ConfigEntry[MyData] 泛型,在 async_setup_entry 中设置 entry.runtime_data,平台通过 config_entry.runtime_data 获取,类型安全。
ConfigEntryNotReady:连接失败时抛出此异常,HA 会自动重试设置。
async_forward_entry_setups:在集成入口的 async_setup_entry 中调用,将配置条目转发到各平台。
async_unload_platforms:在 async_unload_entry 中调用,卸载所有平台。
async_on_unload:注册卸载时的清理回调,如取消订阅、关闭连接。
DataUpdateCoordinator:轮询式更新的推荐方式,自动处理轮询间隔、错误重试、状态更新。适合多个实体共享同一数据源的场景。
CoordinatorEntity:与 Coordinator 配合的实体基类,自动在 Coordinator 刷新时更新状态。适合需要共享刷新逻辑的轮询式集成。
纯 SensorEntity 模式:简单传感器可以直接继承 SensorEntity,通过 async_update() 实现轮询(如 Moon),或通过 should_poll=False + async_write_ha_state() 实现推送(如 Sun)。不需要 Coordinator 时不应强加 Coordinator。
EntityDescription:将实体属性从子类移到描述对象,支持一个实体类多个实例。配合 value_fn 可以进一步简化多传感器场景。
should_poll:事件驱动集成的实体应设 _attr_should_poll = False,通过 async_write_ha_state() 主动推送更新;轮询式集成保持默认 True,由 EntityPlatform 定时调用 async_update()。
YAML 与 ConfigFlow 共存:通过 SOURCE_IMPORT 将 YAML 配置导入为 ConfigEntry,统一由 async_setup_entry 处理,避免维护两套逻辑。
11. 蓝图(Blueprint)机制
蓝图(Blueprint)是 Home Assistant 提供的一种配置复用机制——用户可以将自动化(automation)或脚本(script)的模板定义为一个蓝图,然后多次使用该蓝图,每次只需提供不同的输入参数(如不同的传感器、灯光实体)。蓝图让社区能够分享可复用的自动化模板,降低配置门槛。
11.1 蓝图概述与使用方法
什么是蓝图?
蓝图本质上是一个包含 blueprint: 元数据头的 YAML 文件,其中定义了:
- 元数据:蓝图名称、描述、所属域(automation 或 script)、作者、最低 HA 版本要求等
- 输入定义(
input:):蓝图的可配置参数,每个输入可以指定名称、描述、默认值和选择器(selector) - 配置模板:触发器、条件、动作(automation)或序列(script)——其中引用输入的地方用
!input <input_name> 标记
用户使用蓝图创建自动化/脚本时,只需填写蓝图定义的输入参数,HA 会自动将 !input 替换为用户提供的实际值,生成完整的配置。
使用蓝图的流程
- 导入蓝图:在 Settings → Automations & Scenes → Blueprints 页面,点击 “Import Blueprint”,输入蓝图 URL(支持社区论坛、GitHub 文件、GitHub Gist、HA 官网、任意 URL)
- 创建自动化:选择已导入的蓝图,点击 “Create Automation”,在编辑器中填写输入参数
- 保存运行:保存后,自动化使用
use_blueprint: 配置,HA 在每次加载时自动替换输入生成完整配置
蓝图文件存储位置
蓝图 YAML 文件存储在配置目录下的 blueprints/ 子目录中:
1
2
3
4
5
6
7
8
9
10
11
12
13
| config/
├── blueprints/
│ ├── automation/ # 自动化蓝图
│ │ ├── homeassistant/ # HA 内置示例(首次启动自动复制)
│ │ │ └── motion_light.yaml
│ │ ├── community_user/ # 从社区论坛导入
│ │ │ └── some_blueprint.yaml
│ │ └── github_user/ # 从 GitHub 导入
│ │ └── another_blueprint.yaml
│ └── script/ # 脚本蓝图
│ ├── homeassistant/
│ │ └── confirmable_notification.yaml
│ └── ...
|
11.2 蓝图 YAML 格式与自定义蓝图
蓝图 YAML 结构
一个自动化蓝图的完整 YAML 格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| blueprint:
name: Motion-activated Light # 必需:蓝图名称
description: Turn on a light when motion is detected. # 可选:描述
domain: automation # 必需:所属域(automation 或 script)
source_url: https://github.com/... # 可选:蓝图来源 URL
author: Home Assistant # 可选:作者
homeassistant: # 可选:HA 版本要求
min_version: 2024.1.0
input: # 可选:输入参数定义
motion_entity: # 输入名(与 !input 对应)
name: Motion Sensor # 输入的显示名称
selector: # 选择器(控制前端输入控件)
entity:
filter:
- device_class: motion
domain: binary_sensor
light_target:
name: Light
selector:
target:
entity:
domain: light
no_motion_wait:
name: Wait time
description: Time to leave the light on after last motion.
default: 120 # 默认值
selector:
number:
min: 0
max: 3600
unit_of_measurement: seconds
# 以下是自动化配置模板,使用 !input 引用输入参数
mode: restart
max_exceeded: silent
triggers:
- trigger: state
entity_id: !input motion_entity # ← 替换为用户提供的传感器实体
from: "off"
to: "on"
actions:
- action: light.turn_on
target: !input light_target # ← 替换为用户提供的灯光目标
- wait_for_trigger:
trigger: state
entity_id: !input motion_entity
from: "on"
to: "off"
- delay: !input no_motion_wait # ← 替换为用户提供的等待时间
- action: light.turn_off
target: !input light_target
|
!input 是 YAML 自定义标签,在解析时被转换为 Input 对象(annotatedyaml.Input)。Input 是一个仅包含 name 字段的 dataclass:
1
2
3
| @dataclass(slots=True, frozen=True)
class Input:
name: str
|
当 YAML Loader 遇到 !input motion_entity 时,调用 Input.from_node(loader, node),创建一个 Input(name="motion_entity") 对象,暂存于 YAML 数据结构中。这个对象不是最终值——它是一个"占位符",将在蓝图替换阶段被替换为用户提供的实际值。
输入定义的详细格式
每个输入可以有以下字段:
| 字段 | 必需 | 说明 |
|---|
name | 可选 | 输入的显示名称(前端表单标签) |
description | 可选 | 输入的详细说明 |
default | 可选 | 默认值(用户未填时使用) |
selector | 可选 | 选择器定义(控制前端输入控件类型和过滤条件) |
选择器类型(常用的):
| 选择器 | 前端控件 | 适用输入类型 |
|---|
entity | 实体选择器 | 选择特定实体 |
target | 目标选择器 | 选择实体/设备/区域的组合 |
device | 设备选择器 | 选择特定设备 |
number | 数字滑块/输入 | 选择数值 |
text | 文本输入框 | 输入文本 |
boolean | 开关 | 选择 true/false |
select | 下拉选择 | 选择预设选项 |
time | 时间选择器 | 选择时间 |
date | 日期选择器 | 选择日期 |
action | 动作序列编辑器 | 定义一组动作 |
addon | Add-on 选择器 | 选择 Hass.io Add-on |
area | 区域选择器 | 选择区域 |
蓝图支持将输入参数分组显示,通过嵌套的 input: 字段实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| blueprint:
input:
notification_section:
name: Notification Settings
icon: mdi:bell
description: Configure notification options
collapsed: true # 可折叠
input:
notify_device:
name: Device to notify
selector:
device:
filter:
integration: mobile_app
message:
name: Message
selector:
text:
action_section:
name: Action Settings
input:
confirm_action:
name: Confirmation Action
default: []
selector:
action:
|
分组输入在引用时直接用内部 input 名称(如 !input notify_device),而不是用分组名。HA 在校验时会检查所有 !input 引用的名称是否都有对应的输入定义,分组内的 key 会被展平到同一层。
使用蓝图创建自动化时的 YAML 格式
用户通过蓝图创建自动化时,生成的 YAML 配置使用 use_blueprint: 字段:
1
2
3
4
5
6
7
8
9
10
| # automations.yaml
- id: "1681234567"
alias: "Motion Light - Living Room"
use_blueprint:
path: homeassistant/motion_light.yaml # 蓝图文件路径
input:
motion_entity: binary_sensor.living_room_motion # 用户提供的实际值
light_target:
entity_id: light.living_room
no_motion_wait: 180
|
HA 加载此自动化时,会读取蓝图文件,将所有 !input 替换为 input: 中的实际值,生成完整的自动化配置。
11.3 蓝图源码解析:导入、校验与替换
核心类结构
蓝图机制涉及以下核心类,分布在 homeassistant/components/blueprint/ 和 annotatedyaml 包中:
| 类/文件 | 位置 | 说明 |
|---|
Blueprint | models.py | 蓝图数据模型,包含元数据和校验逻辑 |
BlueprintInputs | models.py | 蓝图输入数据,负责输入校验和替换 |
DomainBlueprints | models.py | 域级蓝图管理器,负责加载/存储蓝图文件 |
Input | annotatedyaml/objects.py | YAML !input 标记的数据类(占位符) |
substitute() | annotatedyaml/input.py | 递归替换 YAML 数据中的 Input 对象 |
extract_inputs() | annotatedyaml/input.py | 递归提取 YAML 数据中所有 Input 引用名 |
BLUEPRINT_SCHEMA | schemas.py | 蓝图元数据的 Voluptuous 校验 Schema |
BLUEPRINT_INSTANCE_FIELDS | schemas.py | use_blueprint: 字段的校验 Schema |
蓝图导入流程
源码: homeassistant/components/blueprint/importer.py
用户在前端点击 “Import Blueprint” 时,通过 WebSocket 调用 ws_import_blueprint,触发以下流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| 前端输入 URL → websocket_api: blueprint/import
│
├── importer.fetch_blueprint_from_url(hass, url)
│ │
│ ├── 尝试 5 种导入函数(按顺序):
│ │ 1. fetch_blueprint_from_community_post → 解析论坛帖子 JSON,提取 YAML 代码块
│ │ 2. fetch_blueprint_from_github_url → 转换 GitHub URL 为 raw.githubusercontent.com URL,下载 YAML
│ │ 3. fetch_blueprint_from_github_gist_url → 调用 GitHub Gist API,提取 .yaml 文件
│ │ 4. fetch_blueprint_from_website_url → 下载 HA 官网的 YAML 文件
│ │ 5. fetch_blueprint_from_generic_url → 下载任意 URL 的 YAML 文件
│ │
│ │ 每种函数如果 URL 不匹配其模式,抛出 UnsupportedUrl,被 suppress
│ │ 继续尝试下一种。5 种都不匹配则抛出 "Unsupported URL" 错误
│ │
│ ├── yaml_util.parse_yaml(raw_yaml) → 解析 YAML 数据
│ │ 此阶段 !input 标签被解析为 Input 对象(占位符)
│ │
│ ├── Blueprint(data, schema=BLUEPRINT_SCHEMA) → 校验蓝图元数据
│ │ 校验: name, domain, input 定义, min_version 等
│ │ 校验: 所有 !input 引用的名称是否在 input 定义中存在
│ │ → extract_inputs(data) 扫描整个数据,收集所有 Input.name
│ │ → 与 blueprint.inputs 对比,缺失的抛出 InvalidBlueprint
│ │
│ └── 返回 ImportedBlueprint(suggested_filename, raw_data, blueprint)
│
├── blueprint.update_metadata(source_url=url) → 记录来源 URL
│
└── 返回给前端: metadata, raw_data, suggested_filename, validation_errors
前端显示预览 → 用户确认 → websocket_api: blueprint/save
→ DomainBlueprints.async_add_blueprint()
→ 写入 YAML 文件到 config/blueprints/<domain>/<path>
→ 存入内存缓存 _blueprints[path] = blueprint
|
社区论坛导入的细节:HA 从论坛帖子中提取 YAML 代码块时,会解析帖子的 HTML 内容,查找 <code class="lang-yaml"> 或 <code class="lang-auto"> 标签,对 YAML 代码块进行 HTML 反转义(html.unescape)后解析。只接受标记为 YAML 或 auto 语法类型的代码块。
GitHub URL 转换:https://github.com/<repo>/blob/<path> 被自动转换为 https://raw.githubusercontent.com/<repo>/<path>,以获取原始 YAML 内容而非 HTML 页面。
蓝图校验流程
Blueprint.__init__ 在构造时执行两轮校验:
第一轮:Voluptuous Schema 校验
1
2
3
| class Blueprint:
def __init__(self, data, *, path, expected_domain, schema):
data = self.data = schema(data) # ← BLUEPRINT_SCHEMA 校验
|
BLUEPRINT_SCHEMA 校验以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| BLUEPRINT_SCHEMA = vol.Schema({
vol.Required(CONF_BLUEPRINT): vol.Schema({
vol.Required(CONF_NAME): str, # 蓝图名称
vol.Optional(CONF_DESCRIPTION): str, # 描述
vol.Required(CONF_DOMAIN): str, # 所属域(automation/script)
vol.Optional(CONF_SOURCE_URL): cv.url, # 来源 URL
vol.Optional(CONF_AUTHOR): str, # 作者
vol.Optional(CONF_HOMEASSISTANT): { # HA 版本约束
vol.Optional(CONF_MIN_VERSION): version_validator # 格式: X.Y.Z
},
vol.Optional(CONF_INPUT, default=dict): vol.All( # 输入定义
{str: vol.Any(None, BLUEPRINT_INPUT_SCHEMA, BLUEPRINT_INPUT_SECTION_SCHEMA)},
unique_input_validator, # 禁止重复 input key
),
}),
}, extra=vol.ALLOW_EXTRA) # ← 允许自动化/脚本配置通过(不做域级校验)
|
注意 extra=vol.ALLOW_EXTRA:蓝图 Schema 只校验 blueprint: 元数据部分,自动化/脚本的配置内容(triggers、actions 等)不在蓝图导入时校验——它们在后续由域级 Schema(PLATFORM_SCHEMA)校验。
第二轮:!input 引用完整性校验
1
2
3
4
5
6
| class Blueprint:
def __init__(self, data, ...):
# ...
missing = yaml_util.extract_inputs(data) - set(self.inputs)
if missing:
raise InvalidBlueprint(..., f"Missing input definition for {', '.join(missing)}")
|
extract_inputs() 递归遍历整个 YAML 数据树,收集所有 Input 对象的 name。然后与蓝图 input: 定义中展平后的所有 key 对比——如果 !input 引用了未定义的输入名,抛出 InvalidBlueprint。
蓝图替换流程 — 核心机制
当自动化使用 use_blueprint: 引用蓝图时,HA 需要将蓝图模板中的所有 !input 占位符替换为用户提供的实际值。这是蓝图机制的核心。
源码路径:automation/config.py::_async_validate_config_item → blueprint/models.py::BlueprintInputs.async_substitute → annotatedyaml/input.py::substitute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| 自动化配置包含 use_blueprint:
│
├── config.py 检测到 blueprint.is_blueprint_instance_config(config) == True
│ 即配置中有 "use_blueprint" 键
│
├── blueprints.async_inputs_from_config(config)
│ │
│ ├── BLUEPRINT_INSTANCE_FIELDS(config) → 校验 use_blueprint 结构
│ │ 校验: path 必须是 .yaml 后缀的合法路径
│ │ 校验: input 必须是 dict
│ │
│ ├── blueprints.async_get_blueprint(bp_conf["path"])
│ │ │ 加载蓝图文件 → 解析 YAML → Blueprint 对象
│ │ │ 缓存:已加载的蓝图存入 _blueprints[path]
│ │ │ 并发保护:asyncio.Lock 防止重复加载
│ │
│ └── BlueprintInputs(blueprint, config_with_inputs)
│ │
│ ├── inputs.validate() → 校验输入完整性
│ │ 缺失必需输入(无 default 且用户未提供) → MissingInput
│ │
│ └── inputs.inputs_with_default → 合并用户输入和默认值
│ 用户未提供的输入 → 使用蓝图 input 定义中的 default
│
├── blueprint_inputs.async_substitute() → 生成完整自动化配置
│ │
│ ├── yaml_util.substitute(blueprint.data, inputs_with_default)
│ │ │ 递归遍历 blueprint.data(整个 YAML 数据树)
│ │ │ 遇到 Input 对象 → 用 inputs_with_default[input.name] 替换
│ │ │ 遇到 list → 递归替换每个元素
│ │ │ 遇到 dict → 递归替换每个值(key 不替换)
│ │ │ 遇到其他类型 → 保持不变
│ │ │ 未定义的 Input → 抛出 UndefinedSubstitution
│ │
│ ├── combined = {**processed, **config_with_inputs}
│ │ 合并蓝图替换后的配置 + 用户配置中可能额外添加的字段
│ │
│ ├── combined.pop("use_blueprint") # 移除蓝图引用
│ ├── combined.pop("blueprint") # 移除蓝图元数据
│ │
│ └── 返回: 纯自动化配置(无蓝图标记)
│
└── PLATFORM_SCHEMA(config) → 对替换后的配置做域级校验
校验 triggers、conditions、actions 等
|
substitute() 的核心实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # annotatedyaml/input.py
def substitute(obj: Any, substitutions: dict[str, Any]) -> Any:
"""递归替换 YAML 数据中的 Input 对象。"""
if isinstance(obj, Input):
# 找到占位符 → 用用户提供的值替换
if obj.name not in substitutions:
raise UndefinedSubstitution(obj.name)
return substitutions[obj.name]
if isinstance(obj, list):
return [substitute(val, substitutions) for val in obj]
if isinstance(obj, dict):
return {key: substitute(val, substitutions) for key, val in obj.items()}
return obj # 原始值(字符串、数字等)保持不变
|
关键点:substitute() 是一个深度递归函数,会遍历整个 YAML 数据树的所有层级。这意味着 !input 可以出现在 YAML 的任何位置——不仅限于实体 ID 或参数值,还可以出现在字典值、列表元素、嵌套结构中。例如 target: !input light_target 会被替换为一个完整的 target 配置块(如 {entity_id: light.living_room})。
蓝图替换的可视化示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # 蓝图文件 motion_light.yaml(包含 !input 占位符)
triggers:
- trigger: state
entity_id: !input motion_entity ← Input("motion_entity")
from: "off"
to: "on"
actions:
- action: light.turn_on
target: !input light_target ← Input("light_target")
- delay: !input no_motion_wait ← Input("no_motion_wait")
# 用户自动化配置(use_blueprint + input)
use_blueprint:
path: homeassistant/motion_light.yaml
input:
motion_entity: binary_sensor.hall_motion
light_target: {entity_id: light.hall}
no_motion_wait: 300
# substitute() 替换后生成的完整配置
triggers:
- trigger: state
entity_id: binary_sensor.hall_motion ← 替换完成
from: "off"
to: "on"
actions:
- action: light.turn_on
target: {entity_id: light.hall} ← 替换完成
- delay: 300 ← 替换完成
|
11.4 蓝图与自动化/脚本的集成
自动化集成中的蓝图集成
源码: homeassistant/components/automation/helpers.py
自动化集成在 async_setup 中注册蓝图域:
1
2
3
4
5
6
| # automation/__init__.py
async def async_setup(hass, config):
# ...
async_get_blueprints(hass) # 注册 automation 域的 DomainBlueprints
# 首次启动时自动复制内置蓝图到 config/blueprints/automation/
hass.async_create_task(async_get_blueprints(hass).async_populate())
|
async_get_blueprints() 使用 @singleton 装饰器,确保全局只创建一个 DomainBlueprints 实例:
1
2
3
4
5
6
7
8
9
10
11
12
| # automation/helpers.py
@singleton(DATA_BLUEPRINTS)
@callback
def async_get_blueprints(hass) -> blueprint.DomainBlueprints:
return blueprint.DomainBlueprints(
hass,
DOMAIN, # "automation"
LOGGER,
_blueprint_in_use, # 检查蓝图是否被自动化引用
_reload_blueprint_automations, # 重载引用蓝图的自动化
AUTOMATION_BLUEPRINT_SCHEMA, # 专用蓝图 Schema
)
|
配置校验中的蓝图处理
源码: homeassistant/components/automation/config.py::_async_validate_config_item
自动化配置校验时,如果检测到 use_blueprint: 键,执行蓝图替换后再校验:
1
2
3
4
5
6
7
8
9
| if blueprint.is_blueprint_instance_config(config):
# 1. 从 DomainBlueprints 加载蓝图
blueprint_inputs = await blueprints.async_inputs_from_config(config)
# 2. 替换 !input 占位符
config = blueprint_inputs.async_substitute()
# 3. 对替换后的完整配置做域级校验
validated_config = PLATFORM_SCHEMA(config)
|
蓝图替换后的配置与手动编写的自动化配置完全一致——HA 内部不再区分"蓝图自动化"和"手动自动化",它们走同样的校验和执行流程。
脚本集成中的蓝图集成
脚本(script)集成同样支持蓝图,使用类似的模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # script 集成的蓝图 Schema
AUTOMATION_BLUEPRINT_SCHEMA → BLUEPRINT_SCHEMA(通用)
SCRIPT_BLUEPRINT_SCHEMA → BLUEPRINT_SCHEMA(通用)
# script 使用蓝图时的 YAML
script:
my_notify:
use_blueprint:
path: homeassistant/confirmable_notification.yaml
input:
notify_device: <device_id>
title: "确认操作"
message: "是否执行此操作?"
|
当前蓝图仅支持 automation 和 script 两个域,但 DomainBlueprints 的设计是通用的——任何域都可以注册蓝图支持。
蓝图的更新与重载
当蓝图文件被更新(重新导入或手动编辑 YAML)时:
1
2
3
4
5
6
7
8
9
| DomainBlueprints.async_add_blueprint(blueprint, path, allow_override=True)
│
├── 覆盖现有 YAML 文件
├── 更新内存缓存
│
└── 如果覆盖了已有蓝图 → _reload_blueprint_consumers()
│ 对于 automation → 调用 automation.reload 服务
│ 所有引用该蓝图的自动化将被重新加载
│ → 重新执行蓝图替换 → 生成更新后的配置
|
蓝图的"Take Control"(接管)功能
前端提供的 “Take Control” 功能实际上调用了 blueprint/substitute WebSocket 命令:
1
2
3
4
| # websocket_api.py::ws_substitute_blueprint
blueprint_config = {"use_blueprint": {"path": msg["path"], "input": msg["input"]}}
blueprint_inputs = await domain_blueprints.async_inputs_from_config(blueprint_config)
config = blueprint_inputs.async_substitute() # 生成完整配置
|
前端拿到替换后的完整配置后,将其保存为普通自动化(不含 use_blueprint: 字段)。从此该自动化与蓝图脱离关系,用户可以自由编辑——但也失去了随蓝图更新自动同步的能力。
蓝图机制的整体架构图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| ┌───────────────────────────────────────────────────────────────────┐
│ 1. 蓝图导入 (importer.py + websocket_api.py) │
│ │
│ URL → fetch_blueprint_from_url() → 解析 YAML → Blueprint 对象 │
│ → 校验 BLUEPRINT_SCHEMA + !input 完整性 │
│ → DomainBlueprints.async_add_blueprint() │
│ → 写入 config/blueprints/<domain>/<path>.yaml │
│ → 存入内存缓存 │
└───────────────────────────┬───────────────────────────────────────┘
│ 蓝图文件存储在磁盘
▼
┌───────────────────────────────────────────────────────────────────┐
│ 2. 自动化/脚本配置加载 (config.py) │
│ │
│ 检测 use_blueprint: → DomainBlueprints.async_inputs_from_config()│
│ → 加载蓝图 Blueprint 对象 → BlueprintInputs │
│ → 合并用户输入 + 默认值 → inputs_with_default │
│ → substitute(blueprint.data, inputs_with_default) │
│ → 递归替换所有 !input Input 对象为实际值 │
│ → 移除 blueprint: 和 use_blueprint: → 纯自动化/脚本配置 │
│ → PLATFORM_SCHEMA 校验 → 正常自动化/脚本流程 │
└───────────────────────────┬───────────────────────────────────────┘
│ 替换后的配置与手动配置无异
▼
┌───────────────────────────────────────────────────────────────────┐
│ 3. 运行时 (automation/__init__.py) │
│ │
│ 替换后的自动化配置 → AutomationEntity │
│ 保留 raw_blueprint_inputs 用于追踪和 Trace │
│ referenced_blueprint 属性 → 路径字符串 │
│ 蓝图更新时 → reload 服务 → 重新执行蓝图替换 │
└───────────────────────────────────────────────────────────────────┘
|
12. 关键设计模式总结
12.1 架构模式
| 模式 | 实现 |
|---|
| 事件驱动 | EventBus + StateMachine,所有组件间通信通过事件 |
| 分层架构 | 入口层 → 核心层 → 加载层 → 实体层 → 集成层 |
| 泛型约束 | EntityComponent[LightEntity], ConfigEntry[HueBridge] |
| 注册表模式 | ServiceRegistry, EntityRegistry, DeviceRegistry, HANDLERS 等 |
| 观察者模式 | EventBus 监听器, Coordinator 订阅, ConfigEntry 更新监听 |
12.2 加载模式
| 模式 | 实现 |
|---|
| 分阶段启动 | Stage 0/1/2,基础设施先行 |
| 并行加载 | asyncio.gather 并行设置 ConfigEntry 和平台 |
| 依赖注入 | hass.data 全局字典 + DATA_INSTANCES |
| 惰性加载 | 自定义集成按需解析,平台模块按需导入 |
| 线性递增退避重试 | PlatformNotReady → min(tries,6)*30 秒递增等待(30, 60, 90, … 180s) |
12.3 实体模式
| 模式 | 实现 | 典型集成 |
|---|
| 轮询式(纯 Entity) | should_poll=True + async_update() | moon |
| 协调器式 | DataUpdateCoordinator + CoordinatorEntity | hue (v1) |
| 推送式(纯 Entity) | should_poll=False + dispatcher/事件监听 + async_write_ha_state() | sun |
| 描述模式 | EntityDescription + value_fn 将属性从子类移到描述对象 | sun |
| attr 模式* | 类属性默认值,减少 property 定义 | moon |
12.4 集成模式
| 模式 | 实现 |
|---|
| runtime_data 模式 | entry.runtime_data 存储运行时对象,类型安全 |
| 平台转发模式 | async_forward_entry_setups 将设置传播到各平台 |
| 配置流模式 | ConfigFlow + 步骤方法,支持 UI 配置 |
| 蓝图模式 | !input 占位符 + substitute() 递归替换,配置模板化复用 |
| 统一错误处理 | async_request_call 包装 API 调用,统一异常转换 |
| 选项 = 重载 | 选项变更触发 async_reload,重新初始化整个集成 |
12.5 @final 保护
以下关键属性/方法标记为 @final,子类不可覆盖:
LightEntity.state_attributes — 灯光状态序列化逻辑SensorEntity.state — 包含单位转换、精度处理的完整校验SensorEntity.unit_of_measurement — 单位转换管道BinarySensorEntity.state — 从 is_on 自动派生ToggleEntity.state — 从 is_on 自动派生
附录:关键源码文件索引
| 文件 | 行数 | 说明 |
|---|
homeassistant/core.py | 2878 | HomeAssistant 主类、EventBus、StateMachine、ServiceRegistry |
homeassistant/config_entries.py | 4180 | ConfigEntry、ConfigFlow、ConfigEntries 管理器 |
homeassistant/bootstrap.py | 1084 | 启动引导 |
homeassistant/loader.py | 1789 | Integration 类、manifest 解析 |
homeassistant/setup.py | 842 | async_setup_component |
homeassistant/data_entry_flow.py | 940 | FlowHandler/FlowManager 基类 |
homeassistant/helpers/entity.py | 1780 | Entity 基类 |
homeassistant/helpers/entity_platform.py | 1348 | EntityPlatform |
homeassistant/helpers/entity_component.py | 398 | EntityComponent |
homeassistant/helpers/service.py | 1408 | 服务辅助函数 |
homeassistant/const.py | 1011 | 全局常量 |
homeassistant/components/blueprint/models.py | 385 | Blueprint、BlueprintInputs、DomainBlueprints |
homeassistant/components/blueprint/importer.py | 288 | 蓝图 URL 导入逻辑(论坛/GitHub/Gist/官网/通用) |
homeassistant/components/blueprint/schemas.py | 151 | BLUEPRINT_SCHEMA、BLUEPRINT_INSTANCE_FIELDS |
homeassistant/components/blueprint/websocket_api.py | 274 | 蓝图 WebSocket API(导入/保存/删除/替换) |
homeassistant/components/automation/config.py | 338 | 自动化配置校验(含蓝图替换流程) |
homeassistant/components/automation/helpers.py | 40 | 自动化蓝图 DomainBlueprints 注册 |
annotatedyaml/input.py | ~60 | Input 类、substitute()、extract_inputs() |
homeassistant/runner.py | 330 | 运行器 |