记一次Home Assistant自定义集成开发踩坑

2026年03月07日1 次阅读0 人喜欢
Home Assistant蓝牙自定义集成智能家居DockerPython踩坑记录

最近在给家里的智能马桶搞Home Assistant集成,踩了不少坑。记录一下,给后来人提个醒。

背景

我家这个马桶是VCX-Knob的,支持蓝牙控制。我想把它接入Home Assistant,这样就能用语音控制了,或者设置一些自动化场景。

这玩意儿没有现成的集成,只能自己写。我用的是Docker部署的Home Assistant,不是HAOS,这点很重要。

第一个坑:Python版本兼容

一开始写的代码在本地跑得好好的,一放到HA里就报错。报的是dataclass字段顺序的问题。

python 复制代码
# Python 3.13要求dataclass字段按定义顺序使用
@dataclass
class EntityDescription:
    key: str
    name: str | None = None  # 这个默认值字段必须放在非默认值后面
    # ...其他字段

这玩意儿在Python 3.12及以下没问题,但HA现在用的是3.13,直接报错。

解决办法就是用字符串类型注解:

python 复制代码
@dataclass
class EntityDescription:
    key: str
    name: "str | None" = None  # 用字符串就不会报错了

第二个坑:服务注册时机

我想添加一些蓝牙配对的服务,让用户可以在UI里配对设备,不用SSH进去敲命令。

一开始我把服务注册放在了async_setup()里:

python 复制代码
async def async_setup(hass, config):
    hass.services.async_register(DOMAIN, "my_service", handler)
    return True

结果发现async_setup()只在YAML配置的时候才会被调用,ConfigEntry类型的集成根本不走这里。

得把服务注册放到async_setup_entry()里。但这样又有问题:如果有多个设备,服务会被重复注册。

后来想了个办法,用计数器:

python 复制代码
# 初始化计数器
async def async_setup(hass, config):
    hass.data.setdefault(DOMAIN, {})
    hass.data[DOMAIN]["entry_count"] = 0
    return True

# 在setup_entry里管理服务生命周期
async def async_setup_entry(hass, entry):
    hass.data[DOMAIN]["entry_count"] += 1
    if hass.data[DOMAIN]["entry_count"] == 1:
        await register_services(hass)
    return True

async def async_unload_entry(hass, entry):
    hass.data[DOMAIN]["entry_count"] -= 1
    if hass.data[DOMAIN]["entry_count"] == 0:
        await unregister_services(hass)
    return True

这样第一个设备加载时注册服务,最后一个设备卸载时注销服务。

第三个坑:asyncio.communicate不存在

我之前习惯写await asyncio.communicate(process),结果报错说module 'asyncio' has no attribute 'communicate'

查了半天才发现应该用process.communicate()

python 复制代码
process = await asyncio.create_subprocess_exec(...)
stdout, stderr = await process.communicate()  # 对的
# stdout, stderr = await asyncio.communicate(process)  # 错的!

低级错误,但这玩意儿在一处错了就得在所有地方改,找半天。

第四个坑:EntityCategory要用枚举

我有段代码报错:

复制代码
ValueError: entity_category must be a valid EntityCategory instance, got diagnostic

我一开始写的是字符串"diagnostic",得用枚举:

python 复制代码
from homeassistant.components.binary_sensor import EntityCategory

BinarySensorEntityDescription(
    key="connection",
    name="已连接",
    entity_category=EntityCategory.DIAGNOSTIC,  # 对的
    # entity_category="diagnostic",  # 错的!
)

第五个坑:coordinator.data可能是None

实体初始化的时候,协调器可能还没完成第一次数据更新,这时候coordinator.data是None。

我之前直接在实体的available属性里访问:

python 复制代码
@property
def available(self) -> bool:
    return self._coordinator.data.get("connected", False)

直接报AttributeError: 'NoneType' object has no attribute 'get'

得先判断:

python 复制代码
@property
def available(self) -> bool:
    if self._coordinator.data is None:
        return False
    return self._coordinator.data.get("connected", False)

第六个坑:日志调试

这是个奇葩的。我加了好多print()语句来调试,结果Docker日志里压根看不到。

才发现Docker的docker logs只捕获logging模块的输出,不捕获stdout的print()

所以调试的时候得用:

python 复制代码
import logging
_LOGGER = logging.getLogger(__name__)

_LOGGER.info("这个能看到")
print("这个看不到!")

第七个坑:嵌套函数的垃圾回收

我一开始把服务处理函数写成嵌套的:

python 复制代码
async def setup_services(hass):
    async def handler(call):
        # ...
    
    hass.services.async_register(DOMAIN, "my_service", handler)

结果发现有时候服务调用没反应。怀疑是嵌套函数被垃圾回收了。

改成了模块级函数就好了:

python 复制代码
async def _handler(hass, call):
    # ...

async def setup_services(hass):
    hass.services.async_register(
        DOMAIN, 
        "my_service", 
        lambda call: _handler(hass, call)
    )

第八个坑:Docker蓝牙权限

这个不是代码问题,但很关键。Docker里的Home Assistant要访问蓝牙,得这么启动:

bash 复制代码
docker run -d \
  --name homeassistant \
  --network=host \
  -v /run/dbus:/run/dbus:ro \
  ...
  • --network=host:蓝牙需要host网络模式
  • /run/dbus:蓝牙通信需要挂载DBus

如果只用--net=bridge或者不挂载DBus,蓝牙根本用不了。

最后

智能家居这条路还挺有意思的,但坑也多。尤其是蓝牙设备,各家协议不一样,有的还得配对,有的直接连就能用。

我这马桶的蓝牙属于需要配对才能写的类型,配对之前只能读状态,不能发命令。这个折腾了好久,最后发现是蓝牙协议层面的限制,得用bluetoothctl手动配对一下才行。

希望这篇能帮到后来人。有啥问题也可以交流,我踩过的坑你们就不用再踩了。

加载评论中...