diff --git a/README.md b/README.md index ffc2bbd..6d7dc5c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ terncy custome component for homeassistant ## Component Information - ha_iot_class: Local Push -- ha_release: '2022.11.0' +- ha_release: '2022.12.0' - ha_config_flow: true - ha_domain: terncy diff --git a/README.zh-cn.md b/README.zh-cn.md index eaf5402..c259240 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -10,7 +10,7 @@ ## 插件信息 - ha_iot_class: Local Push -- home assistant 版本要求: '2022.11.0' +- home assistant 版本要求: '2022.12.0' - ha_config_flow: true - ha_domain: terncy diff --git a/custom_components/terncy/core/gateway.py b/custom_components/terncy/core/gateway.py index 90eca6a..b557ae4 100644 --- a/custom_components/terncy/core/gateway.py +++ b/custom_components/terncy/core/gateway.py @@ -11,15 +11,22 @@ CONF_TOKEN, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + MAJOR_VERSION, + MINOR_VERSION, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, CONNECTION_ZIGBEE, - DeviceInfo, format_mac, ) + +if (MAJOR_VERSION, MINOR_VERSION) >= (2023, 9): + from homeassistant.helpers.device_registry import DeviceInfo +else: + from homeassistant.helpers.entity import DeviceInfo + from homeassistant.helpers.typing import UNDEFINED from terncy import Terncy from terncy.event import Connected, Disconnected, EventMessage @@ -517,7 +524,7 @@ def setup_device(self, device_data: PhysicalDeviceData, svc_list: list[SvcData]) if svc_room := svc.get("room"): if svc_room_name := self.room_data.get(svc_room): suggested_area = svc_room_name - attrs = [a['attr'] for a in attributes] + attrs = [a["attr"] for a in attributes] descriptions = [ description for description in PROFILES.get(profile) @@ -526,15 +533,6 @@ def setup_device(self, device_data: PhysicalDeviceData, svc_list: list[SvcData]) or set(description.required_attrs).issubset(attrs) ) ] - descriptions = [ - description - for description in descriptions - if ( - not description.disabled_attrs - or not set(description.disabled_attrs).issubset(attrs) - ) - ] - if len(descriptions) > 0: identifiers = {(DOMAIN, eid)} device_registry.async_get_or_create( @@ -551,8 +549,10 @@ def setup_device(self, device_data: PhysicalDeviceData, svc_list: list[SvcData]) ) self.add_device(eid, device) for description in descriptions: - entity = create_entity(self, eid, description) - entity._attr_device_info = DeviceInfo(identifiers=identifiers) + entity = create_entity(self, eid, description, attributes) + entity._attr_device_info = DeviceInfo( + identifiers=identifiers + ) ha_add_entity(self.hass, self.config_entry, entity) device.entities.append(entity) else: @@ -575,7 +575,7 @@ async def async_refresh_devices(self): _LOGGER.debug("[%s] Fetching data...", self.unique_id) # room - lang = self.hass.config.language + lang = self.hass.config.language # HA>=2022.12 default_rooms = DEFAULT_ROOMS.get(lang, DEFAULT_ROOMS.get("en")) try: rooms: list[RoomData] = await self._fetch_data("room") @@ -640,6 +640,7 @@ def setup_scene(self, scene_data: SceneData): online = scene_data.get("online", True) entity = self.scenes.get(scene_id) + init_states = [{"attr": "on", "value": scene_data["on"]}] if not entity: description = TerncySwitchDescription( key="scene", @@ -648,7 +649,7 @@ def setup_scene(self, scene_data: SceneData): unique_id_prefix=self.unique_id, # scene_id不是uuid形式的,加个网关id作前缀 ) identifiers = {(DOMAIN, f"{self.unique_id}_scenes")} - entity = create_entity(self, scene_id, description) + entity = create_entity(self, scene_id, description, init_states) entity._attr_device_info = DeviceInfo(identifiers=identifiers) ha_add_entity(self.hass, self.config_entry, entity) self.scenes[scene_id] = entity @@ -656,6 +657,6 @@ def setup_scene(self, scene_data: SceneData): entity._attr_name = name entity.set_available(online) - entity.update_state([{"attr": "on", "value": scene_data["on"]}]) + entity.update_state(init_states) # endregion diff --git a/custom_components/terncy/cover.py b/custom_components/terncy/cover.py index 69b9ba4..025eb0d 100644 --- a/custom_components/terncy/cover.py +++ b/custom_components/terncy/cover.py @@ -13,6 +13,7 @@ from .hass.entity import TerncyEntity from .hass.entity_descriptions import TerncyCoverDescription +from .types import AttrValue from .utils import get_attr_value _LOGGER = logging.getLogger(__name__) @@ -29,6 +30,22 @@ async def async_setup_entry( K_TILT_ANGLE = "tiltAngle" +def get_tilt_angle(attrs: list[AttrValue]) -> int | None: + tilt_angle = get_attr_value(attrs, K_TILT_ANGLE) + if tilt_angle is not None and -90 <= tilt_angle <= 90: + return tilt_angle + return None + + +def _create_entity( + gateway, eid: str, description: TerncyCoverDescription, init_states: list[AttrValue] +): + if get_tilt_angle(init_states) is not None: + return TerncyTiltCover(gateway, eid, description, init_states) + else: + return TerncyCover(gateway, eid, description, init_states) + + class TerncyCover(TerncyEntity, CoverEntity): """Represents a Terncy Cover.""" @@ -41,7 +58,7 @@ class TerncyCover(TerncyEntity, CoverEntity): | CoverEntityFeature.STOP ) - def update_state(self, attrs): + def update_state(self, attrs: list[AttrValue]): # _LOGGER.debug("%s <= %s", self.eid, attrs) if (value := get_attr_value(attrs, K_CURTAIN_PERCENT)) is not None: self._attr_current_cover_position = value @@ -106,9 +123,9 @@ def current_cover_tilt_position(self) -> int | None: return None return 100 - round(abs(self._tilt_angle) / 0.9) - def update_state(self, attrs): + def update_state(self, attrs: list[AttrValue]): # _LOGGER.debug("[%s] <= %s", self.unique_id, attrs) - if (tilt_angle := get_attr_value(attrs, K_TILT_ANGLE)) is not None: + if (tilt_angle := get_tilt_angle(attrs)) is not None: self._tilt_angle = tilt_angle super().update_state(attrs) @@ -141,5 +158,4 @@ async def async_stop_cover_tilt(self, **kwargs) -> None: await self.api.set_attribute(self.eid, K_CURTAIN_MOTOR_STATUS, 0) -TerncyEntity.NEW["cover"] = TerncyCover -TerncyEntity.NEW[f"cover.key.tilt"] = TerncyTiltCover +TerncyEntity.NEW["cover"] = _create_entity diff --git a/custom_components/terncy/hass/add_entities.py b/custom_components/terncy/hass/add_entities.py index 3fcf473..5a63395 100644 --- a/custom_components/terncy/hass/add_entities.py +++ b/custom_components/terncy/hass/add_entities.py @@ -8,6 +8,7 @@ from .entity import TerncyEntity from .entity_descriptions import TerncyEntityDescription from ..const import DOMAIN +from ..types import AttrValue if TYPE_CHECKING: from ..core.gateway import TerncyGateway @@ -19,13 +20,14 @@ def create_entity( gateway: "TerncyGateway", eid: str, description: TerncyEntityDescription, + init_states: list[AttrValue], # 初始状态,用于判断设备支持多少特性。 ) -> TerncyEntity: domain = str(description.PLATFORM) cls = ( # fmt: off TerncyEntity.NEW.get(f"{domain}.key.{description.key}") or TerncyEntity.NEW.get(domain) ) - return cls(gateway, eid, description) + return cls(gateway, eid, description, init_states) def ha_add_entity(hass: HomeAssistant, config_entry: ConfigEntry, entity: TerncyEntity): diff --git a/custom_components/terncy/hass/entity.py b/custom_components/terncy/hass/entity.py index 99e9375..52a61e3 100644 --- a/custom_components/terncy/hass/entity.py +++ b/custom_components/terncy/hass/entity.py @@ -22,7 +22,11 @@ class TerncyEntity(Entity): ADD: dict[str, AddEntitiesCallback] = {} # key: config_entry_id.domain NEW: dict[ - str, Callable[["TerncyGateway", str, TerncyEntityDescription], "TerncyEntity"] + str, + Callable[ + ["TerncyGateway", str, TerncyEntityDescription, list[AttrValue]], + "TerncyEntity", + ], ] = {} # key: domain.key or domain entity_description: TerncyEntityDescription @@ -33,6 +37,7 @@ def __init__( gateway: "TerncyGateway", eid: str, description: TerncyEntityDescription, + init_states: list[AttrValue], ): self.gateway = gateway self.eid = eid diff --git a/custom_components/terncy/hass/entity_descriptions.py b/custom_components/terncy/hass/entity_descriptions.py index 0dee352..ea8d05d 100644 --- a/custom_components/terncy/hass/entity_descriptions.py +++ b/custom_components/terncy/hass/entity_descriptions.py @@ -6,7 +6,6 @@ from homeassistant.components.binary_sensor import BinarySensorEntityDescription from homeassistant.components.climate import ClimateEntityDescription from homeassistant.components.cover import CoverEntityDescription -from homeassistant.components.event import EventDeviceClass, EventEntityDescription from homeassistant.components.light import ( ColorMode, LightEntityDescription, @@ -19,18 +18,23 @@ ) from homeassistant.components.switch import SwitchEntityDescription from homeassistant.const import ( - EntityCategory, LIGHT_LUX, MAJOR_VERSION, + MINOR_VERSION, PERCENTAGE, Platform, UnitOfTemperature, ) + +if (MAJOR_VERSION, MINOR_VERSION) >= (2023, 3): + from homeassistant.const import EntityCategory +else: + from homeassistant.helpers.entity import EntityCategory + from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.typing import StateType, UndefinedType -from ..const import EVENT_ENTITY_BUTTON_EVENTS - +from ..const import EVENT_ENTITY_BUTTON_EVENTS, HAS_EVENT_PLATFORM # https://developers.home-assistant.io/blog/2023/12/11/entity-description-changes FROZEN_ENTITY_DESCRIPTION = MAJOR_VERSION >= 2024 @@ -40,6 +44,8 @@ class TerncyEntityDescription(EntityDescription): PLATFORM: Platform = None + has_entity_name: bool = True + sub_key: str | None = None """用作 unique_id 的后缀。""" @@ -53,9 +59,6 @@ class TerncyEntityDescription(EntityDescription): required_attrs: list[str] | None = None """需要的属性,如果没有这些属性,就不创建实体""" - disabled_attrs: list[str] | None = None - """不应存在的属性,如果有任一属性,就不创建实体""" - # region Binary Sensor @@ -65,7 +68,6 @@ class TerncyBinarySensorDescription( TerncyEntityDescription, BinarySensorEntityDescription ): PLATFORM: Platform = Platform.BINARY_SENSOR - has_entity_name: bool = True value_attr: str = "" value_map: dict[int, bool] = field( default_factory=lambda: {4: True, 3: True, 2: True, 1: True, 0: False} @@ -80,7 +82,6 @@ class TerncyBinarySensorDescription( @dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) class TerncyClimateDescription(TerncyEntityDescription, ClimateEntityDescription): PLATFORM: Platform = Platform.CLIMATE - has_entity_name: bool = True name: str | UndefinedType | None = None @@ -92,15 +93,6 @@ class TerncyClimateDescription(TerncyEntityDescription, ClimateEntityDescription @dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) class TerncyCoverDescription(TerncyEntityDescription, CoverEntityDescription): PLATFORM: Platform = Platform.COVER - has_entity_name: bool = True - name: str | UndefinedType | None = None - - -@dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) -class TerncyTiltCoverDescription(TerncyEntityDescription, CoverEntityDescription): - key: str = "tilt" - PLATFORM: Platform = Platform.COVER - has_entity_name: bool = True name: str | UndefinedType | None = None @@ -109,21 +101,22 @@ class TerncyTiltCoverDescription(TerncyEntityDescription, CoverEntityDescription # region Event -@dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) -class TerncyEventDescription(TerncyEntityDescription, EventEntityDescription): - PLATFORM: Platform = Platform.EVENT - has_entity_name: bool = True +if HAS_EVENT_PLATFORM: + from homeassistant.components.event import EventDeviceClass, EventEntityDescription + @dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) + class TerncyEventDescription(TerncyEntityDescription, EventEntityDescription): + PLATFORM: Platform = Platform.EVENT -@dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) -class TerncyButtonDescription(TerncyEventDescription): - key: str = "event_button" - sub_key: str = "button" - device_class: EventDeviceClass = EventDeviceClass.BUTTON - translation_key: str = "button" - event_types: list[str] = field( - default_factory=lambda: list(EVENT_ENTITY_BUTTON_EVENTS) - ) + @dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) + class TerncyButtonDescription(TerncyEventDescription): + key: str = "event_button" + sub_key: str = "button" + device_class: EventDeviceClass = EventDeviceClass.BUTTON + translation_key: str = "button" + event_types: list[str] = field( + default_factory=lambda: list(EVENT_ENTITY_BUTTON_EVENTS) + ) # endregion @@ -135,7 +128,6 @@ class TerncyButtonDescription(TerncyEventDescription): class TerncyLightDescription(TerncyEntityDescription, LightEntityDescription): key: str = "light" PLATFORM: Platform = Platform.LIGHT - has_entity_name: bool = True name: str | UndefinedType | None = None color_mode: ColorMode | None = None supported_color_modes: set[ColorMode] | None = None @@ -150,7 +142,6 @@ class TerncyLightDescription(TerncyEntityDescription, LightEntityDescription): @dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) class TerncySensorDescription(TerncyEntityDescription, SensorEntityDescription): PLATFORM: Platform = Platform.SENSOR - has_entity_name: bool = True value_attr: str = "" value_fn: Callable[[Any], StateType | date | datetime | Decimal] = lambda x: x @@ -214,7 +205,6 @@ class BatteryDescription(TerncySensorDescription): @dataclass(frozen=FROZEN_ENTITY_DESCRIPTION, kw_only=True) class TerncySwitchDescription(TerncyEntityDescription, SwitchEntityDescription): PLATFORM: Platform = Platform.SWITCH - has_entity_name: bool = True value_attr: str = "on" invert_state: bool = False diff --git a/custom_components/terncy/light.py b/custom_components/terncy/light.py index 1711c2a..33b7adf 100644 --- a/custom_components/terncy/light.py +++ b/custom_components/terncy/light.py @@ -16,6 +16,7 @@ from .hass.entity import TerncyEntity from .hass.entity_descriptions import TerncyLightDescription +from .types import AttrValue from .utils import get_attr_value _LOGGER = logging.getLogger(__name__) @@ -41,8 +42,14 @@ class TerncyLight(TerncyEntity, LightEntity): _attr_supported_color_modes: set[ColorMode] | set[str] | None _attr_supported_features: LightEntityFeature - def __init__(self, gateway, eid: str, description: TerncyLightDescription): - super().__init__(gateway, eid, description) + def __init__( + self, + gateway, + eid: str, + description: TerncyLightDescription, + init_states: list[AttrValue], + ): + super().__init__(gateway, eid, description, init_states) self._attr_brightness = 0 self._attr_color_mode = description.color_mode self._attr_color_temp = 0 diff --git a/custom_components/terncy/profiles/before_2023_7.py b/custom_components/terncy/profiles/before_2023_7.py index a0d11fb..33d42a2 100644 --- a/custom_components/terncy/profiles/before_2023_7.py +++ b/custom_components/terncy/profiles/before_2023_7.py @@ -15,7 +15,12 @@ from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.light import ColorMode from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.helpers.entity import EntityCategory # <2023.3 +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION + +if (MAJOR_VERSION, MINOR_VERSION) >= (2023, 3): + from homeassistant.const import EntityCategory +else: + from homeassistant.helpers.entity import EntityCategory from ..const import ( PROFILE_AC_UNIT_MACHINE, @@ -64,9 +69,6 @@ KEY_DISABLE_RELAY, KEY_WALL_SWITCH, ) -from ..cover import ( - ATTR_TILT_POSITION, -) PROFILES: dict[int, list[TerncyEntityDescription]] = { PROFILE_PIR: [ @@ -155,12 +157,6 @@ TerncyCoverDescription( key="cover", device_class=CoverDeviceClass.CURTAIN, - disabled_attrs = [ATTR_TILT_POSITION], - ), - TerncyCoverDescription( - key="cover", - device_class=CoverDeviceClass.CURTAIN, - required_attrs = [ATTR_TILT_POSITION], ), ], PROFILE_YAN_BUTTON: [ diff --git a/custom_components/terncy/profiles/profiles.py b/custom_components/terncy/profiles/profiles.py index 4fda286..b6e5375 100644 --- a/custom_components/terncy/profiles/profiles.py +++ b/custom_components/terncy/profiles/profiles.py @@ -57,9 +57,6 @@ KEY_DISABLE_RELAY, KEY_WALL_SWITCH, ) -from ..cover import ( - ATTR_TILT_POSITION, -) PROFILES: dict[int, list[TerncyEntityDescription]] = { PROFILE_PIR: [ @@ -150,12 +147,6 @@ TerncyCoverDescription( key="cover", device_class=CoverDeviceClass.CURTAIN, - disabled_attrs = [ATTR_TILT_POSITION], - ), - TerncyCoverDescription( - key="cover", - device_class=CoverDeviceClass.CURTAIN, - required_attrs = [ATTR_TILT_POSITION], ), ], PROFILE_YAN_BUTTON: [ diff --git a/hacs.json b/hacs.json index 2a1f588..f6749fb 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { "name": "Terncy", "render_readme": true, - "homeassistant": "2022.11.0" + "homeassistant": "2022.12.0" } \ No newline at end of file