Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion homeassistant/components/lcn/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["pypck"],
"quality_scale": "bronze",
"requirements": ["pypck==0.9.4", "lcn-frontend==0.2.7"]
"requirements": ["pypck==0.9.5", "lcn-frontend==0.2.7"]
}
7 changes: 0 additions & 7 deletions homeassistant/components/sunricher_dali/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send

from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER
from .types import DaliCenterConfigEntry, DaliCenterData
Expand Down Expand Up @@ -47,12 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaliCenterConfigEntry) -
"You can try to delete the gateway and add it again"
) from exc

def on_online_status(dev_id: str, available: bool) -> None:
signal = f"{DOMAIN}_update_available_{dev_id}"
hass.add_job(async_dispatcher_send, hass, signal, available)

gateway.on_online_status = on_online_status

try:
devices = await gateway.discover_devices()
except DaliGatewayError as exc:
Expand Down
25 changes: 8 additions & 17 deletions homeassistant/components/sunricher_dali/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from typing import Any

from PySrDaliGateway import Device
from PySrDaliGateway import CallbackEventType, Device
from PySrDaliGateway.helper import is_light_device
from PySrDaliGateway.types import LightStatus

Expand All @@ -19,10 +19,6 @@
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN, MANUFACTURER
Expand All @@ -40,15 +36,8 @@ async def async_setup_entry(
) -> None:
"""Set up Sunricher DALI light entities from config entry."""
runtime_data = entry.runtime_data
gateway = runtime_data.gateway
devices = runtime_data.devices

def _on_light_status(dev_id: str, status: LightStatus) -> None:
signal = f"{DOMAIN}_update_{dev_id}"
hass.add_job(async_dispatcher_send, hass, signal, status)

gateway.on_light_status = _on_light_status

async_add_entities(
DaliCenterLight(device)
for device in devices
Expand Down Expand Up @@ -123,14 +112,16 @@ async def async_turn_off(self, **kwargs: Any) -> None:
async def async_added_to_hass(self) -> None:
"""Handle entity addition to Home Assistant."""

signal = f"{DOMAIN}_update_{self._attr_unique_id}"
self.async_on_remove(
async_dispatcher_connect(self.hass, signal, self._handle_device_update)
self._light.register_listener(
CallbackEventType.LIGHT_STATUS, self._handle_device_update
)
)

signal = f"{DOMAIN}_update_available_{self._attr_unique_id}"
self.async_on_remove(
async_dispatcher_connect(self.hass, signal, self._handle_availability)
self._light.register_listener(
CallbackEventType.ONLINE_STATUS, self._handle_availability
)
)

# read_status() only queues a request on the gateway and relies on the
Expand Down Expand Up @@ -187,4 +178,4 @@ def _handle_device_update(self, status: LightStatus) -> None:
):
self._attr_rgbw_color = status["rgbw_color"]

self.async_write_ha_state()
self.schedule_update_ha_state()
2 changes: 1 addition & 1 deletion homeassistant/components/sunricher_dali/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sunricher_dali",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["PySrDaliGateway==0.13.1"]
"requirements": ["PySrDaliGateway==0.16.2"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/sunricher_dali/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ rules:
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
test-coverage: done

# Gold
devices: done
Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/unifi/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ def async_client_uptime_value_fn(hub: UnifiHub, client: Client) -> datetime:
return dt_util.utc_from_timestamp(float(client.uptime))


@callback
def async_wired_client_allowed_fn(hub: UnifiHub, obj_id: str) -> bool:
"""Check if client is wired and allowed."""
client = hub.api.clients[obj_id]
if not client.is_wired or client.wired_rate_mbps <= 0:
return False
return True


@callback
def async_wlan_client_value_fn(hub: UnifiHub, wlan: Wlan) -> int:
"""Calculate the amount of clients connected to a wlan."""
Expand Down Expand Up @@ -407,6 +416,23 @@ class UnifiSensorEntityDescription[HandlerT: APIHandler, ApiItemT: ApiItem](
unique_id_fn=lambda hub, obj_id: f"tx-{obj_id}",
value_fn=async_client_tx_value_fn,
),
UnifiSensorEntityDescription[Clients, Client](
key="Wired client speed",
translation_key="wired_client_link_speed",
device_class=SensorDeviceClass.DATA_RATE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
entity_registry_enabled_default=False,
allowed_fn=async_wired_client_allowed_fn,
api_handler_fn=lambda api: api.clients,
device_info_fn=async_client_device_info_fn,
is_connected_fn=async_client_is_connected_fn,
name_fn=lambda _: "Link speed",
object_fn=lambda api, obj_id: api.clients[obj_id],
unique_id_fn=lambda hub, obj_id: f"wired_speed-{obj_id}",
value_fn=lambda hub, client: client.wired_rate_mbps,
),
UnifiSensorEntityDescription[Ports, Port](
key="PoE port power sensor",
device_class=SensorDeviceClass.POWER,
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/unifi/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
"provisioning": "Provisioning",
"upgrading": "Upgrading"
}
},
"wired_client_link_speed": {
"name": "Link speed"
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions tests/components/sunricher_dali/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
"""Tests for the Sunricher Sunricher DALI integration."""

from collections.abc import Callable
from unittest.mock import MagicMock

from PySrDaliGateway import CallbackEventType


def find_device_listener(
device: MagicMock, event_type: CallbackEventType
) -> Callable[..., None]:
"""Find the registered listener callback for a specific device and event type."""
for call in device.register_listener.call_args_list:
if call[0][0] == event_type:
return call[0][1]
raise AssertionError(
f"Listener for event type {event_type} not found on device {device.dev_id}"
)
1 change: 1 addition & 0 deletions tests/components/sunricher_dali/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def _create_mock_device(
device.turn_on = MagicMock()
device.turn_off = MagicMock()
device.read_status = MagicMock()
device.register_listener = MagicMock(return_value=lambda: None)
return device


Expand Down
63 changes: 45 additions & 18 deletions tests/components/sunricher_dali/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any
from unittest.mock import MagicMock, patch

from PySrDaliGateway import CallbackEventType
import pytest

from homeassistant.components.light import (
Expand All @@ -15,6 +16,8 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er

from . import find_device_listener

from tests.common import MockConfigEntry, SnapshotAssertion, snapshot_platform

TEST_DIMMER_ENTITY_ID = "light.dimmer_0000_02"
Expand All @@ -24,13 +27,20 @@
TEST_RGBW_DEVICE_ID = "01040000056A242121110E"


def _dispatch_status(
gateway: MagicMock, device_id: str, status: dict[str, Any]
def _trigger_light_status_callback(
device: MagicMock, device_id: str, status: dict[str, Any]
) -> None:
"""Invoke the status callback registered on the gateway mock."""
callback = gateway.on_light_status
assert callable(callback)
callback(device_id, status)
"""Trigger the light status callbacks registered on the device mock."""
callback = find_device_listener(device, CallbackEventType.LIGHT_STATUS)
callback(status)


def _trigger_availability_callback(
device: MagicMock, device_id: str, available: bool
) -> None:
"""Trigger the availability callbacks registered on the device mock."""
callback = find_device_listener(device, CallbackEventType.ONLINE_STATUS)
callback(available)


@pytest.fixture
Expand Down Expand Up @@ -133,26 +143,25 @@ async def test_turn_on_with_brightness(
)


async def test_dispatcher_connection(
async def test_callback_registration(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_devices: list[MagicMock],
mock_gateway: MagicMock,
) -> None:
"""Test that dispatcher signals are properly connected."""
entity_entry = er.async_get(hass).async_get(TEST_DIMMER_ENTITY_ID)
assert entity_entry is not None

state = hass.states.get(TEST_DIMMER_ENTITY_ID)
assert state is not None
"""Test that callbacks are properly registered and triggered."""
state_before = hass.states.get(TEST_DIMMER_ENTITY_ID)
assert state_before is not None

status_update: dict[str, Any] = {"is_on": True, "brightness": 128}

_dispatch_status(mock_gateway, TEST_DIMMER_DEVICE_ID, status_update)
_trigger_light_status_callback(
mock_devices[0], TEST_DIMMER_DEVICE_ID, status_update
)
await hass.async_block_till_done()

state_after = hass.states.get(TEST_DIMMER_ENTITY_ID)
assert state_after is not None
assert state_after.state == "on"
assert state_after.attributes.get("brightness") == 128


@pytest.mark.parametrize(
Expand All @@ -168,10 +177,28 @@ async def test_dispatcher_connection(
async def test_status_updates(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_gateway: MagicMock,
mock_devices: list[MagicMock],
device_id: str,
status_update: dict[str, Any],
) -> None:
"""Test various status updates for different device types."""
_dispatch_status(mock_gateway, device_id, status_update)
device = next(d for d in mock_devices if d.dev_id == device_id)
_trigger_light_status_callback(device, device_id, status_update)
await hass.async_block_till_done()


async def test_device_availability(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_devices: list[MagicMock],
) -> None:
"""Test device availability changes."""
_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, False)
await hass.async_block_till_done()
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
assert state.state == "unavailable"

_trigger_availability_callback(mock_devices[0], TEST_DIMMER_DEVICE_ID, True)
await hass.async_block_till_done()
assert (state := hass.states.get(TEST_DIMMER_ENTITY_ID))
assert state.state != "unavailable"
56 changes: 56 additions & 0 deletions tests/components/unifi/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,62 @@
'state': '0',
})
# ---
# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_link_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.wired_client_link_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.DATA_RATE: 'data_rate'>,
'original_icon': None,
'original_name': 'Link speed',
'platform': 'unifi',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'wired_client_link_speed',
'unique_id': 'wired_speed-00:00:00:00:00:01',
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
})
# ---
# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_link_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'data_rate',
'friendly_name': 'Wired client Link speed',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfDataRate.MEGABITS_PER_SECOND: 'Mbit/s'>,
}),
'context': <ANY>,
'entity_id': 'sensor.wired_client_link_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1000',
})
# ---
# name: test_entity_and_device_data[wlan_payload0-device_payload0-client_payload0-config_entry_options0][sensor.wired_client_rx-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand Down
Loading
Loading