☰
hass-xiaomi-miot 多实体转换器设计指南:将 `cnhdm.airrtc.wkq01` 温控器拆分为空调、地暖、新风三个独立实体
2026/10/10 8:50:55 网站建设 项目流程
  • 物联网
  • 智能家居

【免费下载链接】hass-xiaomi-miot

Automatic integrate all Xiaomi devices to HomeAssistant via miot-spec, support Wi-Fi, BLE, ZigBee devices. 小米米家智能家居设备接入Hass集成

项目地址:https://gitcode.com/gh_mirrors/ha/hass-xiaomi-miot
点击查看免费下载

导读

cnhdm.airrtc.wkq01(新风机/暖通一体设备)通过单个 MIoTthermostat服务同时暴露空调、地暖与新风三种功能,且电源、目标温度、风速等属性语义高度重叠。本指南基于仓库中的设计规范文档 docs/superpowers/specs/2026-07-11-multi-climate-converters-design.md,完整讲解如何通过"模型级converters显式替换 + 精确 MIoT 属性选择器 + 实体唯一 ID 选项"将设备拆分为两个 Climate 实体与一个 Fan 实体,并保留共享温度 Sensor 与物理按键锁 Switch。读完本指南,你将掌握这套多实体转换器机制的配置结构、唯一 ID 判定优先级、固定 HVAC 模式(power-only Climate)的行为规则,以及与之配套的 pytest 测试策略。

一、问题背景:一个 thermostat 服务承载三种功能

cnhdm.airrtc.wkq01的 MIoT 规格中,空调、地暖和新鲜空气功能全部挂在同一个thermostat服务(SIID 2)下,且多个属性承担重叠的语义角色——例如存在多个电源属性、多个目标温度属性、多个风速属性。

在当时的全局转换器机制下,GLOBAL_CONVERTERS中的 Climate 转换器按语义名分组属性,遇到重复属性时只能"先到先得"。PR #2878 曾提议在ClimateEntity.on_init()中加入重复属性防护(第一个匹配属性胜出),但评审结论认为该方案不安全:

  • 它依赖转换器的处理顺序,结果不确定;
  • 它会静默抑制设备真实具备的功能(例如新风、地暖将无法独立控制);
  • 它仍然无法把地暖和新风暴露为独立实体。

模型级属性排除方案也被否决。最终设计目标明确:必须保留每一个可独立控制的功能,同时避免不相关属性被组合进同一个 Home Assistant 实体。本文档即该目标的批准设计(状态:Approved for implementation planning)。

二、设计目标与非目标

Goals(目标):

  • 将设备表示为三个可独立控制的实体:空调 Climate、地暖 Climate、新风 Fan;
  • 共享环境温度保留为独立 Sensor,物理控制锁保留为 Switch;
  • 通过精确的 MIoT 标识符(如prop.2.3)声明式地选择存在歧义的属性;
  • 保持既有空调实体身份不变(含用户自定义的 Entity Registry ID);
  • 为新增的父转换器与实体提供稳定、唯一、可区分的身份;
  • 未配置新机制的其他型号设备,转换器行为保持不变。

Non-Goals(非目标):

  • 不改变全局 Climate 实体的重复属性选择逻辑;
  • 不为单个型号抽取可复用的全局转换器常量;
  • 不拆分 MIoT 服务定义本身;
  • 默认不改变既有型号的实体身份。

三、设备属性映射:精确选择器 vs 语义名

3.1 属性映射总表

功能MIoT 选择器含义
空调prop.2.3电源
空调prop.2.1模式
空调prop.2.5目标温度
空调prop.2.2风速
新风prop.2.9电源
新风prop.2.7风速
地暖prop.2.10电源
地暖prop.2.8目标温度
共享环境prop.3.1当前温度(temperature)
设备控制prop.5.1物理控制锁(physical_controls_locked)

3.2 两种寻址方式的取舍

设计文档明确了一条重要规则:

三个父转换器内部使用精确选择器(prop.2.x),因为这些属性虽然同属一个服务,却对应不同的逻辑功能;而独立的 Sensor 和 Switch 使用语义属性名(temperature、physical_controls_locked),因为这两个名称本身无歧义。

从源码实现看,core/device.py 的init_converters()对子属性(converters列表)的解析逻辑是:当props条目中包含.时走self.spec.get_properties(p, ...)精确查找(可匹配全局唯一属性),否则走service.get_properties(p, ...)按服务内语义名查找——这正与设计文档中的寻址规则一一对应。

四、转换器选择语义:模型级converters显式替换

4.1 新增配置键与核心逻辑

在Device.init_converters()中新增可选的模型定制键converters:

custom_converters = self.custom_config('converters') base_converters = ( GLOBAL_CONVERTERS if custom_converters is None else custom_converters ) appends = self.custom_config_list('append_converters') or [] for cfg in [*base_converters, *appends]: ...

该逻辑已在 core/device.py 中落地实现:init_converters()首先加入InfoConverter并分发设备信息,随后按上述规则选取基础转换器集合,再拼接append_converters。

4.2 三种配置形态的行为表

模型配置实际处理的转换器定义
converters缺失依次处理GLOBAL_CONVERTERS,再处理append_converters
converters: []跳过全局转换器,仅处理append_converters
非空converters用模型converters替换全局转换器,再处理append_converters

关键语义:这是显式替换机制,而非合并——模型条目不会与GLOBAL_CONVERTERS融合。由于缺失该键时保持原有全局行为,所有未配置该键的既有型号不受影响。

4.3 Info 实体诊断数据的净化

模型级converters定义中包含 Python 转换器类与内部配置结构,不适合 Home Assistant 状态序列化或 recorder 持久化。因此InfoConv.decode()在构造负载前,需要从复制出的customizes中剔除这些键:

customizes = {**device.customizes} customizes.pop('append_converters', None) customizes.pop('converters', None) customizes.pop('extend_miot_specs', None)

该实现已存在于 core/converters.py:InfoConv.decode()在payload.update(...)前完成上述pop,随后通过'converters': [c.full_name for c in device.converters](见 core/converters.py)在顶层诊断字段中保留有效的运行时转换器全名——既保留有用诊断,又不暴露类对象、不重复声明式配置。

五、实体身份:full_name 去重与唯一 ID 优先级

5.1 父转换器去重

父转换器去重已基于conv.full_name实现(见 core/device.py 的add_converter与find_converter)。空调转换器沿用由属性派生的默认属性名;地暖与新风转换器通过kwargs.attr显式指定,因此父转换器全名稳定且互不相同:

climate.floor_heating fan.fresh_air

5.2 实体键与唯一 ID 的冲突点

实体创建存在另一个碰撞点:convert_unique_id()原先对每个MiotServiceConv都返回服务 IID。由于空调与地暖两个 Climate 转换器同属 SIID 2 的thermostat服务,若不处理,两者将获得相同的实体键与 HA 唯一 ID。

设计在既有 fallback 之前插入两个可选唯一 ID 选项:

def convert_unique_id(conv): if uid := conv.option.get('unique_id'): return uid if conv.option.get('use_unique_attr'): return conv.attr # Existing fallback behavior remains unchanged. service = getattr(conv, 'service', None) if isinstance(conv, MiotServiceConv) and isinstance(service, MiotService): return service.iid ...

该实现已落地于 core/hass_entity.py 的convert_unique_id()。优先级如下:

option.unique_id > option.use_unique_attr > existing fallback

三条规则要点:

  • kwargs.attr只决定转换器属性及其full_name,本身不改变实体身份;
  • option.unique_id提供显式、稳定的实体标识,优先级最高;
  • option.use_unique_attr: true使用conv.attr作为实体标识与建议实体 ID 后缀,避免在已配置稳定属性时重复指定;
  • 两者都未设置时,MiotServiceConv维持原有按服务 IID 生成身份的行为。

空调 Climate刻意保留原有服务 IID fallback(身份不变);地暖与新风设置稳定逻辑属性并开启use_unique_attr。

5.3 固定显示名与翻译键

option.name提供实体的固定英文显示名。设置后:

  • _attr_name被替换为指定名称;
  • _attr_translation_key被清除,避免 Home Assistant 将thermostat翻译覆盖配置名;
  • 配置名不做本地化;如需更多语言,须在代码中使用翻译键。

当同时设置option.use_unique_attr: true时,建议实体 ID 由conv.attr生成,而非底层服务/属性标识。既有空调 Climate 不设置option.name、不启用use_unique_attr,因此保留服务派生的Thermostat显示名、thermostat翻译键,以及由thermostat服务派生的实体 ID——这正是兼容"用户已重命名实体 ID"的关键。

六、固定 HVAC 模式:power-only Climate 的option.hvac_mode

6.1 适用条件与行为表

option.hvac_mode为只有电源转换器、没有真实模式转换器的 Climate 实体定义固定运行模式。配置值通过现有_hvac_modes映射解析,从而保持对应HVACAction一致(映射初始化见 custom_components/xiaomi_miot/climate.py,选项读取与_power_hvac_mode设置见 climate.py)。

以option.hvac_mode: 'heat'为例,完整行为:

电源状态HVAC 模式HVAC 动作
offHVACMode.OFFHVACAction.OFF
onHVACMode.HEATHVACAction.HEATING

支持的 HVAC 模式恰好只有OFF与HEAT。选择 Heat 或调用开启时,只写入实体被分配的电源属性;设置目标温度时只写入其被分配的目标温度属性。

6.2 条件性语义

  • 仅当存在电源转换器且无真实模式转换器时生效;
  • 真实模式转换器始终优先:一旦存在,option.hvac_mode对支持的模式、状态、动作与写入全部失效;
  • 未配置该选项的 power-only Climate 保留既有OFF/AUTO行为。

6.3 状态更新的电源驱动不变量

  • 负载包含电源属性时,其值确定性地设置is_on、HVAC 模式与 HVAC 动作,与之前取值无关;
  • 重复的电源值具有幂等性,会重新断言完整的对应状态;
  • 负载不包含电源属性时,不得改变is_on、HVAC 模式或动作;目标温度与当前温度仍独立更新;
  • 同时包含电源与温度的负载,在一次调用中同时更新两组状态。

6.4 明确不引入 RestoreEntity

设计不为ClimateEntity增加RestoreEntity行为:设置/重载之后,由设备返回的首次电源更新确立固定 HVAC 状态(该约束同样写入测试策略,见后文)。

七、模型配置:cnhdm.airrtc.wkq01完整定制

以下配置为设计文档批准的完整模型定制(源码中已落地于 core/device_customizes.py 的DEVICE_CUSTOMIZES['cnhdm.airrtc.wkq01']):

'cnhdm.airrtc.wkq01': { 'converters': [ { 'class': MiotClimateConv, 'services': ['thermostat'], 'kwargs': { 'main_props': ['prop.2.1'], }, 'converters': [ {'props': ['prop.2.1'], 'desc': True}, {'props': ['prop.2.2'], 'desc': True}, {'props': ['prop.2.3']}, {'props': ['prop.2.5']}, {'props': ['prop.3.1']}, ], }, { 'class': MiotClimateConv, 'services': ['thermostat'], 'kwargs': { 'attr': 'floor_heating', 'main_props': ['prop.2.8'], 'option': { 'name': 'Floor Heating', 'use_unique_attr': True, 'hvac_mode': 'heat', }, }, 'converters': [ {'props': ['prop.2.8']}, {'props': ['prop.2.10']}, {'props': ['prop.3.1']}, ], }, { 'class': MiotFanConv, 'services': ['thermostat'], 'kwargs': { 'attr': 'fresh_air', 'main_props': ['prop.2.9'], 'option': { 'name': 'Fresh Air', 'use_unique_attr': True, }, }, 'converters': [ {'props': ['prop.2.7'], 'desc': True}, {'props': ['prop.2.9']}, ], }, ], 'sensor_properties': 'temperature', 'switch_properties': 'physical_controls_locked', }

从源码看,init_converters()对每个cfg的处理流程为:先按services查找thermostat服务并创建父转换器(MiotClimateConv/MiotFanConv),再遍历converters子列表逐条按精确选择器解析属性并挂接到父转换器的attrs(core/device.py)。此外,Device在get_spec()中调用init_converters()之前,会先通过extend_miot_specs扩展本地规格(core/device.py),确保精确选择器解析到正确的属性对象。

八、实体行为细则

8.1 空调 Climate(身份保持既有)

  • 电源:prop.2.3
  • 模式:prop.2.1
  • 目标温度:prop.2.5
  • 风速:prop.2.2
  • 当前温度:prop.3.1
  • 实体身份:既有服务 IID fallback

其支持的 HVAC 模式继续由模式属性的值列表派生(对应 climate.py 中prop.in_list(['mode'])分支对_hvac_modes的填充逻辑)。

8.2 地暖 Climate(固定 power-only 行为)

  • 电源:prop.2.10
  • 目标温度:prop.2.8
  • 当前温度:prop.3.1
  • 转换器属性:floor_heating
  • 实体身份:floor_heating(源自显式转换器属性)
  • 建议实体 ID 后缀:floor_heating
  • 固定显示名:Floor Heating

该转换器无真实模式属性,因此option.hvac_mode: 'heat'应用前述固定 power-only 行为。开启地暖或选择 Heat 仅写prop.2.10,设置目标温度仅写prop.2.8。

8.3 新风 Fan(有序列表映射)

  • 电源:prop.2.9
  • 风速:prop.2.7
  • 转换器属性:fresh_air
  • 实体身份:fresh_air(源自显式转换器属性)
  • 建议实体 ID 后缀:fresh_air
  • 固定显示名:Fresh Air

风速属性是三值枚举:

原始值描述
1Low
2Medium
3High

既有 Fan 有序列表转换(ordered_list_item_to_percentage)将描述映射为 Home Assistant 百分比,并将百分比反向映射为原始 MIoT 值。实体报告三档速度,支持FanEntityFeature.SET_SPEED。

写入行为取决于当前电源状态:

  • 开启状态下调整百分比,仅写prop.2.7;
  • 关闭状态下设置正百分比,同时写prop.2.9 = True与对应prop.2.7值;
  • 将百分比设为 0,仅写prop.2.9 = False;
  • 不带百分比调用开启,仅写prop.2.9 = True;
  • 调用关闭,仅写prop.2.9 = False。

任何新风操作都不得写入空调的电源或风速属性prop.2.3、prop.2.2——这是保证三实体互不干扰的硬约束。

8.4 独立实体

  • sensor_properties: 'temperature'通过既有语义查找路径创建共享当前温度 Sensor(即prop.3.1);
  • switch_properties: 'physical_controls_locked'通过既有语义查找路径创建物理控制锁 Switch(即prop.5.1)。

共享温度同时喂给两个 Climate 实体是有意为之:Climate 需要当前温度,而独立 Sensor 保留了设备原有功能特征,二者并不冲突。

九、兼容性保障

  • 未配置converters的型号继续处理GLOBAL_CONVERTERS后再处理append_converters;
  • 既有append_converters定制语义保持不变;
  • 既有转换器与实体唯一 ID 不变,除非转换器显式设置unique_id或启用use_unique_attr;
  • 该型号的既有空调实体保留基于服务 IID 的身份,升级与重载后用户自定义的 Entity Registry 实体 ID 依然生效;
  • 该变更不引入全局属性顺序优先级,也不会抑制其他设备的重复属性;
  • 因为该型号跳过了全局转换器,未来 MIoT 规格新增内容必须先显式评审该模型定制及其 checked-in fixture,之后才可能暴露新实体。

十、测试策略:首个聚焦的 pytest 基础设施

10.1 测试目录结构

本变更随设计引入仓库首个聚焦的 pytest 基础设施:

requirements_test.txt tests/ ├── conftest.py ├── fixtures/ │ └── cnhdm.airrtc.wkq01.json ├── test_converter_options.py └── test_cnhdm_airrtc_wkq01.py
  • requirements_test.txt提供 pytest 与 Home Assistant 自定义组件测试支持(fixtures 依赖);
  • tests/conftest.py仅包含这些测试所需的共享 HA 与集成 fixtures;
  • tests/fixtures/cnhdm.airrtc.wkq01.json是固定的本地 MIoT 规格 fixture,测试不拉取在线规格,保证确定性;
  • tests/test_converter_options.py覆盖框架级转换器替换、身份、命名与 Info 诊断行为;
  • tests/test_cnhdm_airrtc_wkq01.py覆盖型号专属的实体分组、运行时行为与实体注册表迁移生命周期。

CI 中新增 pytest 任务(.github/workflows/validate.yml),仅针对当前稳定版 Home Assistant 环境运行;既有 stable/dev/2023.7 配置验证矩阵保持不变,新单元测试任务不扩展该兼容性矩阵。基础设施保持最小化、聚焦于本转换器设计,不引入覆盖率工具、多版本 pytest 矩阵或无关集成行为的测试。

10.2 关键验证点

转换器选择:验证无converters键时获得全局+追加转换器;converters: []跳过全局但处理追加;非空converters替换全局但仍处理追加;Info 实体的customizes状态属性中省略模型级converters;Info 顶层converters字段仍列出有效运行时转换器全名。

唯一 ID:验证option.unique_id覆盖use_unique_attr与服务 IID fallback;use_unique_attr: true在无显式 ID 时返回conv.attr并以其生成建议实体 ID;未设置任何选项的MiotServiceConv仍用服务 IID;显式kwargs.attr产生稳定、互异的转换器全名;该型号的两个 Climate 与一个 Fan 实体键、HA 唯一 ID 均互不相同。

实体注册表迁移:纯身份单元测试与注册表生命周期测试分离;生命周期测试必须通过 HA 的 Entity Platform 添加实体以真正触达 Entity Registry(仅 mockasync_add_entities不够)。以既有空调身份预置注册表:

platform: xiaomi_miot unique ID: <device unique ID>-2 entity ID: climate.living_room_ac

加载新模型配置后验证:空调 Climate 复用既有注册条目、保留climate.living_room_ac;地暖唯一 ID 为<device unique ID>-floor_heating、建议后缀floor_heating;新风唯一 ID 为<device unique ID>-fresh_air、建议后缀fresh_air;注册表中恰好两个 Climate + 一个 Fan;不产生重复 thermostat/空调实体;服务派生的Thermostat显示名与thermostat翻译键不影响既有注册条目的复用。卸载并重载配置条目后,三个唯一 ID 与实体 ID 全部保持不变、实体计数不增加、不出现_2/_3碰撞后缀、用户重命名的climate.living_room_ac仍指向原注册条目。

固定 HVAC 模式:地暖 Climate 恰好声明OFF与HEAT;HEAT → OFF → HEAT序列确定性地映射为HEAT/HEATING、OFF/OFF、HEAT/HEATING;重复True/重复False电源更新幂等并重断言完整模式/动作状态;仅含目标温度的负载更新温度而保留当前is_on、模式与动作(通电与断电两种状态均验证);含电源与温度的负载在同一次调用中更新两组状态;选择 Heat 与调用开启仅写prop.2.10;设置地暖目标温度仅写prop.2.8;存在真实模式转换器时忽略option.hvac_mode;无该选项的 power-only Climate 保留OFF/AUTO行为;不引入/不测试新的 RestoreEntity 行为——设置或重载后,首次电源更新确立正确的固定模式与动作。

新风 Fan:原始值1/2/3经 Low/Medium/High 解码为有序列表百分比;代表性百分比反向编码为原始值1/2/3(断言不重复实现 HA 的百分比取整算法);speed_count == 3且支持SET_SPEED;开启时改百分比仅写prop.2.7;关闭时设正百分比同时写prop.2.9 = True与对应prop.2.7;百分比置零与关闭仅写prop.2.9 = False;无百分比开启仅写prop.2.9 = True;任何新风操作不写prop.2.2/prop.2.3。

模型映射(以固定 fixture 为准):完整获批实体集合恰好为:

域实体
buttonInfo
climateThermostat
climateFloor Heating
fanFresh Air
sensorTemperature
switchPhysical Controls Locked

同时验证:恰好两个 Climate 父转换器与一个 Fan 父转换器;每个父级attrs只含其声明属性;子属性转换器保持domain=None且不创建为独立实体;模型converters替换GLOBAL_CONVERTERS后 Info 转换器仍存在;语义temperature恰好创建一个 Sensor、physical_controls_locked恰好创建一个 Switch;无残留全局 thermostat 转换器产生额外 Climate/Fan;function服务的属性(含周程数据)不因副作用而暴露;空调实体保留旧唯一 ID 与服务派生实体 ID;空调父级使用Thermostat显示名与thermostat翻译键,地暖/新风使用固定名并清除服务翻译键;地暖/新风使用floor_heating/fresh_air作为建议实体 ID 后缀;各实体写入仅指向其被分配的电/模式/温度/风速属性。

测试不得比较新旧完整转换器列表:旧 Climate 分组正是被替换的缺陷,上文六实体集合才是 checked-in fixture 的权威预期结果。

该测试设计已部分在仓库中实现,例如 tests/test_cnhdm_airrtc_wkq01.py 已断言两个MiotClimateConv、一个MiotFanConv、父级full_name(climate.floor_heating、fan.fresh_air)、各父级attrs的精确属性集合,以及子属性转换器domain is None且 Info 转换器仍然存在——与上文设计逐条对应。

十一、实施范围:涉及文件一览

本变更的预期实现文件(设计文档"Implementation Scope"章节):

文件职责
custom_components/xiaomi_miot/core/device.py选择模型converters或GLOBAL_CONVERTERS,再追加append_converters
custom_components/xiaomi_miot/core/converters.py从 Info 实体的customizes状态属性中剔除模型级converters定义,同时保留有效转换器名
custom_components/xiaomi_miot/core/hass_entity.py应用唯一 ID 选项优先级;启用use_unique_attr时以conv.attr作为建议实体 ID 后缀;应用option.name固定显示名并清除服务翻译键
custom_components/xiaomi_miot/climate.py仅对 power-only Climate 应用option.hvac_mode;由_hvac_modes映射派生支持模式、通电状态与 HVAC Action;电源存在时确定性更新模式/动作、无关部分负载时予以保留;保持真实模式转换器优先级与默认OFF/AUTO行为
custom_components/xiaomi_miot/core/device_customizes.py添加该型号专属转换器定义与语义 Sensor/Switch 属性
.github/workflows/validate.yml新增使用当前稳定版 HA 测试环境的 pytest 任务
requirements_test.txt声明最小 pytest 与 HA 自定义组件测试依赖
tests/conftest.py提供共享 HA 与集成 fixtures
tests/fixtures/cnhdm.airrtc.wkq01.json提供确定性的本地 MIoT 规格供模型测试
tests/test_converter_options.py测试转换器替换、身份选项、命名与 Info 诊断
tests/test_cnhdm_airrtc_wkq01.py测试型号专属转换器分组与实体行为

本设计不包含全局转换器抽取或无关的 Climate 重构。

结语

cnhdm.airrtc.wkq01多实体转换器设计给出了一条可复用的技术路线:当单个 MIoT 服务承载多种相互独立的功能、且属性语义重叠时,通过在模型定制中显式替换converters、以精确属性选择器(prop.siid.piid)声明式划分职责、辅以use_unique_attr/unique_id消解实体身份冲突、并用option.hvac_mode为 power-only Climate 固化运行模式,即可在完全不触碰全局逻辑的前提下,把一个物理设备优雅地建模为多个边界清晰的 Home Assistant 实体。该设计已被仓库源码完整落地,并配有一整套确定性测试基础设施(固定本地 MIoT fixture + 实体注册表迁移生命周期测试),可作为后续同类"一服务多功能"设备的接入蓝本。

  • 物联网
  • 智能家居

【免费下载链接】hass-xiaomi-miot

Automatic integrate all Xiaomi devices to HomeAssistant via miot-spec, support Wi-Fi, BLE, ZigBee devices. 小米米家智能家居设备接入Hass集成

项目地址:https://gitcode.com/gh_mirrors/ha/hass-xiaomi-miot
点击查看免费下载
上一篇:NoneBot2 消息处理实战指南:Message 消息序列、MessageSegment 消息段与消息模板深度解析
下一篇:KMS智能激活:Windows和Office批量授权的终极解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询