HomeAssistant自定义组件学习-【三】

#要说的话#

因为自己其他事情,小半年没有更新HomeAssistant自定义组件内容了,这次把一个7合一的空气质量传感器实现了,硬件搭建还是使用的亿佰特的RS485模块+M702(RS485版)传感器,这次有几点更新:

  1. 把亿佰特模块独立出来了;
  2. 把实体数据放到设备里;
  3. 亿佰特模块和传感器代码传到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

部分代码是实现图标的,现在使用的是系统默认图标,后续可以研究一下使用自己的图标。

最终显示效果:

模块资源下载地址:

亿佰特:亿佰特组件

空气质量传感器:空气质量传感器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值