|
13 | 13 | import pytest |
14 | 14 |
|
15 | 15 | from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE |
| 16 | +from homeassistant.components.homekit_controller.connection import ( |
| 17 | + MAX_CHARACTERISTICS_PER_REQUEST, |
| 18 | +) |
16 | 19 | from homeassistant.components.homekit_controller.const import ( |
17 | 20 | DEBOUNCE_COOLDOWN, |
18 | 21 | DOMAIN, |
@@ -377,9 +380,15 @@ def _create_accessory(accessory: Accessory) -> Service: |
377 | 380 | state = await helper.poll_and_get_state() |
378 | 381 | assert state.state == STATE_OFF |
379 | 382 | assert mock_get_characteristics.call_count == 2 |
380 | | - # Verify everything is polled |
381 | | - assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 10), (1, 11)} |
382 | | - assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 10), (1, 11)} |
| 383 | + # Verify everything is polled (convert to set for comparison since batching changes the type) |
| 384 | + assert set(mock_get_characteristics.call_args_list[0][0][0]) == { |
| 385 | + (1, 10), |
| 386 | + (1, 11), |
| 387 | + } |
| 388 | + assert set(mock_get_characteristics.call_args_list[1][0][0]) == { |
| 389 | + (1, 10), |
| 390 | + (1, 11), |
| 391 | + } |
383 | 392 |
|
384 | 393 | # Test device goes offline |
385 | 394 | helper.pairing.available = False |
@@ -526,3 +535,84 @@ async def mock_get_characteristics( |
526 | 535 | state = hass.states.get("climate.homew") |
527 | 536 | assert state is not None |
528 | 537 | assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 |
| 538 | + |
| 539 | + |
| 540 | +async def test_characteristic_polling_batching( |
| 541 | + hass: HomeAssistant, get_next_aid: Callable[[], int] |
| 542 | +) -> None: |
| 543 | + """Test that characteristic polling is batched to MAX_CHARACTERISTICS_PER_REQUEST.""" |
| 544 | + |
| 545 | + # Create a large accessory with many characteristics (more than 49) |
| 546 | + def create_large_accessory_with_many_chars(accessory: Accessory) -> None: |
| 547 | + """Create an accessory with many characteristics to test batching.""" |
| 548 | + # Add multiple services with many characteristics each |
| 549 | + for service_num in range(10): # 10 services |
| 550 | + service = accessory.add_service( |
| 551 | + ServicesTypes.LIGHTBULB, name=f"Light {service_num}" |
| 552 | + ) |
| 553 | + # Each lightbulb service gets several characteristics |
| 554 | + service.add_char(CharacteristicsTypes.ON) |
| 555 | + service.add_char(CharacteristicsTypes.BRIGHTNESS) |
| 556 | + service.add_char(CharacteristicsTypes.HUE) |
| 557 | + service.add_char(CharacteristicsTypes.SATURATION) |
| 558 | + service.add_char(CharacteristicsTypes.COLOR_TEMPERATURE) |
| 559 | + # Set initial values |
| 560 | + for char in service.characteristics: |
| 561 | + if char.type != CharacteristicsTypes.IDENTIFY: |
| 562 | + char.value = 0 |
| 563 | + |
| 564 | + helper = await setup_test_component( |
| 565 | + hass, get_next_aid(), create_large_accessory_with_many_chars |
| 566 | + ) |
| 567 | + |
| 568 | + # Track the get_characteristics calls |
| 569 | + get_chars_calls = [] |
| 570 | + original_get_chars = helper.pairing.get_characteristics |
| 571 | + |
| 572 | + async def mock_get_characteristics(chars): |
| 573 | + """Mock get_characteristics to track batch sizes.""" |
| 574 | + get_chars_calls.append(list(chars)) |
| 575 | + return await original_get_chars(chars) |
| 576 | + |
| 577 | + # Clear any calls from setup |
| 578 | + get_chars_calls.clear() |
| 579 | + |
| 580 | + # Patch get_characteristics to track calls |
| 581 | + with mock.patch.object( |
| 582 | + helper.pairing, "get_characteristics", side_effect=mock_get_characteristics |
| 583 | + ): |
| 584 | + # Trigger an update through time_changed which simulates regular polling |
| 585 | + # time_changed expects seconds, not a datetime |
| 586 | + await time_changed(hass, 300) # 5 minutes in seconds |
| 587 | + await hass.async_block_till_done() |
| 588 | + |
| 589 | + # We created 10 lightbulb services with 5 characteristics each = 50 total |
| 590 | + # Plus any base accessory characteristics that are pollable |
| 591 | + # This should result in exactly 2 batches |
| 592 | + assert len(get_chars_calls) == 2, ( |
| 593 | + f"Should have made exactly 2 batched calls, got {len(get_chars_calls)}" |
| 594 | + ) |
| 595 | + |
| 596 | + # Check that no batch exceeded MAX_CHARACTERISTICS_PER_REQUEST |
| 597 | + for i, batch in enumerate(get_chars_calls): |
| 598 | + assert len(batch) <= MAX_CHARACTERISTICS_PER_REQUEST, ( |
| 599 | + f"Batch {i} size {len(batch)} exceeded maximum {MAX_CHARACTERISTICS_PER_REQUEST}" |
| 600 | + ) |
| 601 | + |
| 602 | + # Verify the total number of characteristics polled |
| 603 | + total_chars = sum(len(batch) for batch in get_chars_calls) |
| 604 | + # Each lightbulb has: ON, BRIGHTNESS, HUE, SATURATION, COLOR_TEMPERATURE = 5 |
| 605 | + # 10 lightbulbs = 50 characteristics |
| 606 | + assert total_chars == 50, ( |
| 607 | + f"Should have polled exactly 50 characteristics, got {total_chars}" |
| 608 | + ) |
| 609 | + |
| 610 | + # The first batch should be full (49 characteristics) |
| 611 | + assert len(get_chars_calls[0]) == 49, ( |
| 612 | + f"First batch should have exactly 49 characteristics, got {len(get_chars_calls[0])}" |
| 613 | + ) |
| 614 | + |
| 615 | + # The second batch should have exactly 1 characteristic |
| 616 | + assert len(get_chars_calls[1]) == 1, ( |
| 617 | + f"Second batch should have exactly 1 characteristic, got {len(get_chars_calls[1])}" |
| 618 | + ) |
0 commit comments