Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OTA fixes (and more) #48

Merged
merged 39 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
582144a
`golbal` -> `global`
puddly May 8, 2024
8c0da38
Allow providing event name as context
puddly May 8, 2024
cba37a2
Offload on/off state computation to Core
puddly May 8, 2024
b35b624
Use a separate event for `LevelChangeEvent`
puddly May 8, 2024
aae2f25
Do not set OTA entity state now that we do not compute it
puddly May 8, 2024
a132468
Stop OTA when we fail
puddly May 8, 2024
e4bbf9e
Set up public accessors for `name` and `translation_key`
puddly May 8, 2024
befffb5
Add icon and entity category as well
puddly May 8, 2024
2745306
Add a translation key to the `identify` button
puddly May 8, 2024
08408fc
Revert "Add a translation key to the `identify` button"
puddly May 8, 2024
315dc30
Drop all simple icons, HA already uses icon translations
puddly May 9, 2024
f1aa97d
Drop `icon` entirely
puddly May 9, 2024
e5fc846
Clarify `type: ignore`s
puddly May 20, 2024
61a7f25
Add public accessors for device and state class
puddly May 20, 2024
6f4b5fb
Use parentheses for clarity
puddly May 20, 2024
4e5cc3a
Consolidate `get_entity` into `tests.common`
puddly May 22, 2024
199c609
Unwind stack frames to log correctly in ZCL handlers
puddly May 22, 2024
7d3df24
Fix cover level change event
puddly May 22, 2024
cc5f80f
Bump ruff and run `ruff-format` during pre-commit
puddly May 22, 2024
2861d17
Log when emitting a ZHA event
puddly May 22, 2024
03ff3ce
Re-add `--fix`
puddly May 22, 2024
c6d09b3
Fix most unit tests
puddly May 22, 2024
4e13807
Fix `update` platform tests
puddly May 22, 2024
76ca3a8
Fix entity ID in counter unit test
puddly May 22, 2024
d22a9c2
Consolidate entity information into `info_object`
puddly May 22, 2024
5fb9cf9
Rename `name` to `fallback_name` to clarify intent
puddly May 23, 2024
6cf7a7a
Drop `internal_name` as well
puddly May 23, 2024
9de994e
Attach `unique_id` to `Device`
puddly May 23, 2024
4290d1f
Compute the final `entity_id` only in `BaseEntity`
puddly May 23, 2024
0d66fe4
Fix unit tests
puddly May 23, 2024
a617427
Include the device unique ID in the counter unique ID
puddly May 23, 2024
e87a8c7
Ignore `_attr_translation_key` on counters
puddly May 23, 2024
d1b5288
Expose `entity_registry_enabled_default`
puddly May 24, 2024
0092aa1
Correctly construct counter unique IDs
puddly May 24, 2024
062c3d6
Mark `entity_id` for removal
puddly May 24, 2024
7a0ee62
Get rid of `entity_id`
puddly May 24, 2024
31c988c
Handle some TODOs
puddly May 24, 2024
701a896
Update dependencies
puddly Jun 3, 2024
85057c0
Drop unused dependencies
puddly Jun 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ repos:
- id: mypy

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4
rev: v0.4.4
hooks:
- id: ruff
args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"]
args: [--fix]
- id: ruff-format
10 changes: 4 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@ readme = "README.md"
license = {text = "GPL-3.0"}
requires-python = ">=3.12"
dependencies = [
"bellows==0.38.1",
"bellows==0.39.0",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.112",
"zha-quirks==0.0.116",
"zigpy-deconz==0.23.1",
"zigpy>=0.63.5",
"zigpy==0.64.0",
"zigpy-xbee==0.20.1",
"zigpy-zigate==0.12.0",
"zigpy-znp==0.12.1",
"universal-silabs-flasher==0.0.18",
"universal-silabs-flasher==0.0.20",
"pyserial-asyncio-fast==0.11",
"python-slugify==8.0.4",
"awesomeversion==24.2.0",
]

[tool.setuptools.packages.find]
Expand Down
118 changes: 53 additions & 65 deletions tests/common.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
"""Common test objects."""

import asyncio
from collections.abc import Awaitable
from collections.abc import Awaitable, Callable
import logging
from typing import Any, Optional
from unittest.mock import AsyncMock, Mock

from slugify import slugify
import zigpy.types as t
import zigpy.zcl
import zigpy.zcl.foundation as zcl_f

from zha.application import Platform
from zha.application.gateway import Gateway
from zha.application.platforms import PlatformEntity
from zha.application.platforms import BaseEntity, GroupEntity, PlatformEntity
from zha.zigbee.device import Device
from zha.zigbee.group import Group

Expand Down Expand Up @@ -167,12 +166,15 @@ def reset_clusters(clusters: list[zigpy.zcl.Cluster]) -> None:
cluster.write_attributes.reset_mock()


def find_entity(device: Device, platform: Platform) -> Optional[PlatformEntity]:
def find_entity(device: Device, platform: Platform) -> PlatformEntity:
"""Find an entity for the specified platform on the given device."""
for entity in device.platform_entities.values():
if platform == entity.PLATFORM:
return entity
return None

raise KeyError(
f"No entity found for platform {platform!r} on device {device}: {device.platform_entities}"
)


def mock_coro(
Expand All @@ -187,71 +189,57 @@ def mock_coro(
return fut


def find_entity_id(
domain: str, zha_device: Device, qualifier: Optional[str] = None
) -> Optional[str]:
"""Find the entity id under the testing.
def get_group_entity(
group: Group,
platform: Platform,
entity_type: type[BaseEntity] = BaseEntity,
qualifier: str | None = None,
) -> GroupEntity:
"""Get the first entity of the specified platform on the given group."""
for entity in group.group_entities.values():
if platform != entity.PLATFORM:
continue

This is used to get the entity id in order to get the state from the state
machine so that we can test state changes.
"""
entities = find_entity_ids(domain, zha_device)
if not entities:
return None
if qualifier:
for entity_id in entities:
if qualifier in entity_id:
return entity_id
return None
else:
return entities[0]
if not isinstance(entity, entity_type):
continue

if qualifier is not None and qualifier not in entity.info_object.unique_id:
continue

def find_entity_ids(
domain: str, zha_device: Device, omit: Optional[list[str]] = None
) -> list[str]:
"""Find the entity ids under the testing.
return entity

This is used to get the entity id in order to get the state from the state
machine so that we can test state changes.
"""
ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]])
head = f"{domain}.{slugify(f'{zha_device.name} {ieeetail}', separator='_')}"

entity_ids = [
f"{entity.PLATFORM}.{slugify(entity.name, separator='_')}"
for entity in zha_device.platform_entities.values()
]

matches = []
res = []
for entity_id in entity_ids:
if entity_id.startswith(head):
matches.append(entity_id)

if omit:
for entity_id in matches:
skip = False
for o in omit:
if o in entity_id:
skip = True
break
if not skip:
res.append(entity_id)
else:
res = matches
return res
raise KeyError(
f"No {entity_type} entity found for platform {platform!r} on group {group}: {group.group_entities}"
)


def async_find_group_entity_id(domain: str, group: Group) -> Optional[str]:
"""Find the group entity id under test."""
entity_id = f"{domain}.{group.name.lower().replace(' ','_')}"
def get_entity(
device: Device,
platform: Platform,
entity_type: type[BaseEntity] = BaseEntity,
exact_entity_type: type[BaseEntity] | None = None,
qualifier: str | None = None,
qualifier_func: Callable[[BaseEntity], bool] = lambda e: True,
) -> PlatformEntity:
"""Get the first entity of the specified platform on the given device."""
for entity in device.platform_entities.values():
if platform != entity.PLATFORM:
continue

if not isinstance(entity, entity_type):
continue

if exact_entity_type is not None and type(entity) is not exact_entity_type:
continue

entity_ids = [
f"{entity.PLATFORM}.{slugify(entity.name, separator='_')}"
for entity in group.group_entities.values()
]
if qualifier is not None and qualifier not in entity.info_object.unique_id:
continue

if entity_id in entity_ids:
return entity_id
return None
if not qualifier_func(entity):
continue

return entity

raise KeyError(
f"No {entity_type} entity found for platform {platform!r} on device {device}: {device.platform_entities}"
)
3 changes: 1 addition & 2 deletions tests/test_alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
from zigpy.zcl.clusters import security
import zigpy.zcl.foundation as zcl_f

from tests.conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from zha.application.gateway import Gateway
from zha.application.platforms.alarm_control_panel import AlarmControlPanel
from zha.application.platforms.alarm_control_panel.const import AlarmState
from zha.zigbee.device import Device

from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE

_LOGGER = logging.getLogger(__name__)


Expand Down
5 changes: 2 additions & 3 deletions tests/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@
import zigpy.profiles.zha
from zigpy.zcl.clusters import general, measurement, security

from tests.common import find_entity, send_attributes_report, update_attribute_cache
from tests.conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from zha.application import Platform
from zha.application.gateway import Gateway
from zha.application.platforms import PlatformEntity
from zha.application.platforms.binary_sensor import IASZone, Occupancy
from zha.zigbee.device import Device

from .common import find_entity, send_attributes_report, update_attribute_cache
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE

DEVICE_IAS = {
1: {
SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
Expand Down
33 changes: 8 additions & 25 deletions tests/test_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from unittest.mock import call, patch

import pytest
from slugify import slugify
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
Expand All @@ -24,7 +23,7 @@
from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
import zigpy.zcl.foundation as zcl_f

from tests.common import find_entity, find_entity_id, mock_coro, update_attribute_cache
from tests.common import get_entity, mock_coro, update_attribute_cache
from tests.conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from zha.application import Platform
from zha.application.gateway import Gateway
Expand Down Expand Up @@ -120,8 +119,7 @@ async def test_button(

zha_device, cluster = contact_sensor
assert cluster is not None
entity: PlatformEntity = find_entity(zha_device, Platform.BUTTON) # type: ignore
assert entity is not None
entity: PlatformEntity = get_entity(zha_device, Platform.BUTTON)
assert isinstance(entity, Button)
assert entity.PLATFORM == Platform.BUTTON

Expand All @@ -137,15 +135,6 @@ async def test_button(
assert cluster.request.call_args[0][3] == 5 # duration in seconds


def get_entity(zha_dev: Device, entity_id: str) -> PlatformEntity:
"""Get entity."""
entities = {
entity.PLATFORM + "." + slugify(entity.name, separator="_"): entity
for entity in zha_dev.platform_entities.values()
}
return entities[entity_id]


async def test_frost_unlock(
zha_gateway: Gateway,
tuya_water_valve: tuple[Device, general.Identify], # pylint: disable=redefined-outer-name
Expand All @@ -154,11 +143,9 @@ async def test_frost_unlock(

zha_device, cluster = tuya_water_valve
assert cluster is not None
entity_id = find_entity_id(
Platform.BUTTON, zha_device, qualifier="reset_frost_lock"
entity: PlatformEntity = get_entity(
zha_device, platform=Platform.BUTTON, entity_type=WriteAttributeButton
)
entity: PlatformEntity = get_entity(zha_device, entity_id)
assert entity is not None
assert isinstance(entity, WriteAttributeButton)

assert entity._attr_device_class == ButtonDeviceClass.RESTART
Expand Down Expand Up @@ -258,10 +245,7 @@ async def test_quirks_command_button(

zha_device, cluster = custom_button_device
assert cluster is not None
entity_id = find_entity_id(Platform.BUTTON, zha_device)
entity: PlatformEntity = get_entity(zha_device, entity_id)
assert isinstance(entity, Button)
assert entity is not None
entity: PlatformEntity = get_entity(zha_device, platform=Platform.BUTTON)

with patch(
"zigpy.zcl.Cluster.request",
Expand All @@ -283,10 +267,9 @@ async def test_quirks_write_attr_button(

zha_device, cluster = custom_button_device
assert cluster is not None
entity_id = find_entity_id(Platform.BUTTON, zha_device, qualifier="feed")
entity: PlatformEntity = get_entity(zha_device, entity_id)
assert isinstance(entity, WriteAttributeButton)
assert entity is not None
entity: PlatformEntity = get_entity(
zha_device, platform=Platform.BUTTON, entity_type=WriteAttributeButton
)

assert cluster.get(cluster.AttributeDefs.feed.name) == 0

Expand Down
Loading
Loading