Skip to content

Commit

Permalink
Merge pull request #72 from Necroneco/tilt
Browse files Browse the repository at this point in the history
Add support for tilt cover
  • Loading branch information
rxwen authored May 27, 2024
2 parents 226f544 + e6a81b8 commit 30ac828
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 81 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.zh-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 17 additions & 16 deletions custom_components/terncy/core/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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")
Expand Down Expand Up @@ -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",
Expand All @@ -648,14 +649,14 @@ 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
else:
entity._attr_name = name

entity.set_available(online)
entity.update_state([{"attr": "on", "value": scene_data["on"]}])
entity.update_state(init_states)

# endregion
26 changes: 21 additions & 5 deletions custom_components/terncy/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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."""

Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
4 changes: 3 additions & 1 deletion custom_components/terncy/hass/add_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
7 changes: 6 additions & 1 deletion custom_components/terncy/hass/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,6 +37,7 @@ def __init__(
gateway: "TerncyGateway",
eid: str,
description: TerncyEntityDescription,
init_states: list[AttrValue],
):
self.gateway = gateway
self.eid = eid
Expand Down
58 changes: 24 additions & 34 deletions custom_components/terncy/hass/entity_descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -40,6 +44,8 @@
class TerncyEntityDescription(EntityDescription):
PLATFORM: Platform = None

has_entity_name: bool = True

sub_key: str | None = None
"""用作 unique_id 的后缀。"""

Expand All @@ -53,9 +59,6 @@ class TerncyEntityDescription(EntityDescription):
required_attrs: list[str] | None = None
"""需要的属性,如果没有这些属性,就不创建实体"""

disabled_attrs: list[str] | None = None
"""不应存在的属性,如果有任一属性,就不创建实体"""


# region Binary Sensor

Expand All @@ -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}
Expand All @@ -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


Expand All @@ -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


Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
11 changes: 9 additions & 2 deletions custom_components/terncy/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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
Expand Down
Loading

0 comments on commit 30ac828

Please sign in to comment.