记一次Home Assistant自定义集成开发踩坑
最近在给家里的智能马桶搞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手动配对一下才行。
希望这篇能帮到后来人。有啥问题也可以交流,我踩过的坑你们就不用再踩了。