#要说的话#
因为自己其他事情,小半年没有更新HomeAssistant自定义组件内容了,这次把一个7合一的空气质量传感器实现了,硬件搭建还是使用的亿佰特的RS485模块+M702(RS485版)传感器,这次有几点更新:
- 把亿佰特模块独立出来了;
- 把实体数据放到设备里;
- 亿佰特模块和传感器代码传到CSDN,大家可以下载;
硬件的淘宝链接:
亿佰特模块(NA111):商品详情
传感器模块(M702):商品详情
亿佰特模块有个遗憾的地方,就是没有5V的电源输出,我另外配了一个电源模块给M702供电。
#组件思路#
在亿佰特模块中进行设备的发现,模块的IP、端口配置,并使用json文件把信息保存在系统中;
在传感器模块中选择亿佰特模块中配置的设备,然后两者关联起来,
#具体实现#
亿佰特模块的发现功能在上一篇文章《HomeAssistant自定义组件学习-二》中有说明。本次只是需要把发现的设备模块给其他模块进行调用,这里我采用的是通过json配置文件进行数据配置的传递。json文件的操作方法同样在前几章中有实现,感兴趣的可以翻一下我之前的文章,或者直接下载我上传的资源,下载地址附后面。
设备模块的占用冲突,在json文件里增加used信息,其他模块选择设备时检查used字段,防止占用冲突,实现代码在传感器模块的config_flow.py中。
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
ent_data: dict[str, Any] = {}
if self._device_manager is None:
self._device_manager = EbyteDeviceManager(self.hass)
if user_input is not None:
# 如果有用户输入,则处理输入的信息
if CONFIG_USER_INPUT_EBYTE_DEVICE in user_input:
# 保存选择通讯设备,
mac: str = user_input[CONFIG_USER_INPUT_EBYTE_DEVICE]
name: str = user_input[CONF_NAME]
sd: EbyteDeviceInfo = self._device_manager.get_device_by_mac(mac)
if sd is not None:
ent_data[CONF_NAME] = name
ent_data[CONF_HOST] = sd.Ip
ent_data[CONF_PORT] = sd.Port
ent_data[CONF_MAC] = sd.MAC
dp: map[str, Any] = {}
dp[CONF_HOST] = sd.Ip
dp[CONF_MAC] = sd.MAC
dp[CONF_NAME] = name
dp[CONF_HOST] = sd.Ip
ret: ConfigFlowResult = self.async_create_entry(
title=f"AirQuality[{sd.Ip}]",
description="This is a air quality device!",
description_placeholders=dp,
options=dp,
data=ent_data,
)
# 标识选择的设备已经使用
sd.set_used(True)
sd.set_device_name(name)
sd.save_config()
return ret
_LOGGER.error(f"EbyteDeviceInfo:{mac} is None") # noqa: G004
# 如果没有用户输入,则显示选择列表
ops = []
devices: dict[str, EbyteDeviceInfo] = self._device_manager.get_all_devices()
if len(devices) > 0:
for d in devices.values():
# 通讯设备没有使用的才能给用户选择
if not d.Used:
ops.append(SelectOptionDict(value=d.MAC, label="Ebyte:" + d.Ip)) # noqa: PERF401
if len(ops) > 0:
schema = vol.Schema(
{
vol.Required(
schema=CONF_NAME,
description="Enter Device Name.",
default="Air Quality",
): str,
vol.Required(CONFIG_USER_INPUT_EBYTE_DEVICE): SelectSelector(
SelectSelectorConfig(
options=ops,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
else:
schema = vol.Schema(
{
"ERROR:": "No Unused Ebyte RS485 device, please config device in ebyte!"
}
)
else:
schema = vol.Schema({"ERROR:": "No Ebyte RS485 device founded!"})
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
由于是7合1传感器,不可能每个值 都写一个类,看了一下其他组件的实现方法,采用dict来保存值 ,传感器不同的值,通过key进行区分,所以设备类就成了这样:
"""空气传感器设备类文件."""
# from ..const import _LOGGER
from enum import StrEnum
import logging
from ...ebyte.base.base import BaseLink
from ..const import DOMAIN
_LOGGER = logging.getLogger(DOMAIN)
"""
Description:空气传感器设备操作类,实现与设备的通讯功能。获取设备的
version: 1.0.0.0
Author: Cubar
Date: 2025-06-03 15:19:17
LastEditors: hht
LastEditTime: 2025-06-03 15:19:20
"""
class AirQualityAttribute(StrEnum):
"""空气质量属性."""
attr_co2 = "attr_co2"
attr_co = "attr_co"
attr_tvoc = "attr_tvoc"
attr_pm2d5 = "attr_pm2d5"
attr_pm10 = "attr_pm10"
attr_temperature = "attr_temperature"
attr_humidity = "attr_humidity"
class AirQualityDevice:
"""空气质量传感器操作类."""
_link: BaseLink
# 查询命令
_cmd_query_status: bytes = b"\x01\x03\x00\x02\x00\x07"
# 空气质量数据
_attrs: dict[str, float]
# 属性单位
_units: dict[str, str]
# 属性名称
_names: dict[str, str]
# ICON
_icons: dict[str, str]
def __init__(self, link: BaseLink):
"""初始化函数."""
self._link = link
self._attrs = {
AirQualityAttribute.attr_co2: 0.0,
AirQualityAttribute.attr_co: 0.0,
AirQualityAttribute.attr_tvoc: 0.0,
AirQualityAttribute.attr_pm2d5: 0.0,
AirQualityAttribute.attr_pm10: 0.0,
AirQualityAttribute.attr_temperature: 0.5,
AirQualityAttribute.attr_humidity: 0.0,
}
self._units = {
AirQualityAttribute.attr_co2: "PPM",
AirQualityAttribute.attr_co: "ug",
AirQualityAttribute.attr_tvoc: "ug",
AirQualityAttribute.attr_pm2d5: "ug",
AirQualityAttribute.attr_pm10: "ug",
AirQualityAttribute.attr_temperature: "℃",
AirQualityAttribute.attr_humidity: "%RH",
}
self._names = {}
self._names[AirQualityAttribute.attr_co2] = "CO2"
self._names[AirQualityAttribute.attr_co] = "CO"
self._names[AirQualityAttribute.attr_tvoc] = "TVOC"
self._names[AirQualityAttribute.attr_pm2d5] = "PM2.5"
self._names[AirQualityAttribute.attr_pm10] = "PM10"
self._names[AirQualityAttribute.attr_temperature] = "Temperature"
self._names[AirQualityAttribute.attr_humidity] = "Humidity"
self._icons = {
AirQualityAttribute.attr_co2: "mdi:molecule-co2",
AirQualityAttribute.attr_co: "mdi:molecule-co",
AirQualityAttribute.attr_tvoc: "mdi:virus",
AirQualityAttribute.attr_pm2d5: "mdi:grain",
AirQualityAttribute.attr_pm10: "mdi:grain",
AirQualityAttribute.attr_temperature: "mdi:temperature-celsius",
AirQualityAttribute.attr_humidity: "mdi:virus",
}
@property
def CO2(self) -> float:
"""获取CO2值,单位:PPM."""
return self._attrs.get(AirQualityAttribute.attr_co2)
@property
def CO(self) -> float:
"""获取甲醛值,单位:ug."""
return self._attrs.get(AirQualityAttribute.attr_co)
@property
def TVOC(self) -> float:
"""获取TVOC值,单位:ug."""
return self._attrs.get(AirQualityAttribute.attr_tvoc)
@property
def PM25(self) -> float:
"""获取PM2.5的值,单位:ug."""
return self._attrs.get(AirQualityAttribute.attr_pm2d5)
@property
def PM10(self) -> float:
"""获取PM10值,单位:ug."""
return self._attrs.get(AirQualityAttribute.attr_pm10)
@property
def Temperature(self) -> float:
"""获取温度值,单位:摄氏度."""
return self._attrs.get(AirQualityAttribute.attr_temperature)
@property
def Humidity(self) -> float:
"""获取湿度,单位:%RH."""
return self._attrs.get(AirQualityAttribute.attr_humidity)
# CRC16位校验计算程序
def calculate_crc(self, data: bytes):
"""CRC计算函数."""
crc = 0xFFFF
for pos in data:
crc ^= pos
for _ in range(8):
if (crc & 1) != 0:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return [(crc & 0xFF), (crc >> 8) & 0xFF]
def _check_crc(self, data: bytes) -> bool:
"""校验CRC,最后两字节为CRC."""
crc = self.calculate_crc(data[0 : len(data) - 2])
if crc[0] == data[len(data) - 2] and crc[1] == data[len(data) - 1]:
return True
return False
def _anally_data(self, data: bytes) -> bool:
"""分析数据."""
raise NotImplementedError
def get_unit(self, name: str) -> str:
"""获取指定属性的单位."""
if name in self._units:
return self._units.get(name)
return "Nan"
def get_value(self, name: str) -> float:
"""获取指定属性的值."""
if name in self._attrs:
return self._attrs.get(name)
return 0
def get_name(self, name: str) -> str:
"""获取指定属性的名称."""
if name in self._names:
return self._names[name]
return "Unkown"
def get_icon(self, name: str) -> str:
"""获取ICON."""
if name in self._icons:
return self._icons[name]
return None
def update_date(self) -> None:
"""更新数据."""
try:
ret: bytes = self._link.send_data(self._cmd_query_status)
if len(ret) > 0:
if not self._anally_data(ret):
_LOGGER.error(f"err:{ret!r}") # noqa: G004
except ConnectionRefusedError:
_LOGGER.error("error: ConnectionRefusedError")
def close(self) -> None:
"""关闭链路."""
try:
self._link.close()
except ConnectionError as ce:
_LOGGER.error(f"err:{ce}") # noqa: G004
在添加实体时,通过传入key以区分。
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""实例化实体方法."""
# 获取配置中的信息
ip: str = entry.data[CONF_HOST]
mac: str = entry.data[CONF_MAC]
port: int = entry.data[CONF_PORT]
# 获取链路
link: EbyteRS485Link = static_ebyte_manager.get_link_by_mac(
mac, ip, port, 9600, 8, 1
)
# 实例化设备操作类
device: AirQualityDevice_M702 = AirQualityDevice_M702(link)
# 从系统中获取设备
deviceEntry: DeviceEntry = hass.data[DOMAIN][DEVICES].get(
entry.data.get(CONF_DEVICE_ID)
)
devs = []
# 添加数据实体
devs.append(
AirQualitySensor(AirQualityAttribute.attr_co2, device, entry, deviceEntry)
)
devs.append(
AirQualitySensor(AirQualityAttribute.attr_co, device, entry, deviceEntry)
)
devs.append(
AirQualitySensor(AirQualityAttribute.attr_tvoc, device, entry, deviceEntry)
)
devs.append(
AirQualitySensor(AirQualityAttribute.attr_pm2d5, device, entry, deviceEntry)
)
devs.append(
AirQualitySensor(AirQualityAttribute.attr_pm10, device, entry, deviceEntry)
)
devs.append(
AirQualitySensor(
AirQualityAttribute.attr_temperature, device, entry, deviceEntry
)
)
devs.append(
AirQualitySensor(AirQualityAttribute.attr_humidity, device, entry, deviceEntry)
)
# 调用添加回调,把实体添加到系统
async_add_entities(devs, True)
关键的部分来了,前两章中,我也提到过,把实体放到设备中显示,这次参考了cpuspeed组件,这个组件很简单,其实很简单,Entity中有一个DeviceInfo,只要把这个属性设置成想要附上去的设备,就可以了。具体类代码:
class AirQualitySensor(SensorEntity):
"""空气质量传感器实体类."""
_name: str
_device: AirQualityDevice
_value: float
def __init__(
self,
name: str,
device: AirQualityDevice,
entry: ConfigEntry,
device_entry: DeviceEntry,
):
"""初始化方法."""
self._name = name
self._device = device
super().__init__()
self._attr_native_unit_of_measurement = self._device.get_unit(self._name)
self._attr_name = self._device.get_name(self._name)
self._attr_unique_id = f"sensor.{DOMAIN}_{entry.data[CONF_MAC]}_{self._name}"
self.entity_id = self._attr_unique_id
# 把实体放到设备中
self._attr_device_info = DeviceInfo(
name=device_entry.name,
identifiers=device_entry.identifiers,
)
self._attr_icon = self._device.get_icon(self._name)
# _LOGGER.critical(f"name: {self._name}: entry_id:{device_entry.id}")
@property
def unit_of_measurement(self) -> str:
"""测量单位."""
return self._device.get_unit(self._name)
@property
def native_value(self) -> float:
"""返回传感器值."""
return self._device.get_value(self._name)
@property
def status(self) -> float:
"""返回传感器值."""
return self._value
async def async_update(self) -> None:
"""数据更新."""
self._device.update_date()
self._value = self._device.get_value(self._name)
self._attr_native_value = self._value
部分代码是实现图标的,现在使用的是系统默认图标,后续可以研究一下使用自己的图标。
最终显示效果:
模块资源下载地址:
亿佰特:亿佰特组件
空气质量传感器:空气质量传感器