Fix esphome binary sensors when state is missing (#95140)

* Fix esphome binary sensors when state is missing

* Fix esphome binary sensors when state is missing

* Fix esphome binary sensors when state is missing
This commit is contained in:
J. Nick Koston 2023-06-24 22:09:26 -05:00 committed by GitHub
parent c6b3d538de
commit 9eedc8a602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 125 additions and 16 deletions

View File

@ -49,10 +49,9 @@ class EsphomeBinarySensor(
# Status binary sensors indicated connected state. # Status binary sensors indicated connected state.
# So in their case what's usually _availability_ is now state # So in their case what's usually _availability_ is now state
return self._entry_data.available return self._entry_data.available
state = self._state if not self._has_state or self._state.missing_state:
if not self._has_state or state.missing_state:
return None return None
return state.state return self._state.state
@callback @callback
def _on_static_info_update(self, static_info: EntityInfo) -> None: def _on_static_info_update(self, static_info: EntityInfo) -> None:

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from asyncio import Event from asyncio import Event
from collections.abc import Callable from collections.abc import Awaitable, Callable
from typing import Any from typing import Any
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
@ -142,13 +142,30 @@ async def mock_dashboard(hass):
yield data yield data
class MockESPHomeDevice:
"""Mock an esphome device."""
def __init__(self, entry: MockConfigEntry) -> None:
"""Init the mock."""
self.entry = entry
self.state_callback: Callable[[EntityState], None]
def set_state_callback(self, state_callback: Callable[[EntityState], None]) -> None:
"""Set the state callback."""
self.state_callback = state_callback
def set_state(self, state: EntityState) -> None:
"""Mock setting state."""
self.state_callback(state)
async def _mock_generic_device_entry( async def _mock_generic_device_entry(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: APIClient, mock_client: APIClient,
mock_device_info: dict[str, Any], mock_device_info: dict[str, Any],
mock_list_entities_services: tuple[list[EntityInfo], list[UserService]], mock_list_entities_services: tuple[list[EntityInfo], list[UserService]],
states: list[EntityState], states: list[EntityState],
) -> MockConfigEntry: ) -> MockESPHomeDevice:
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={
@ -158,6 +175,7 @@ async def _mock_generic_device_entry(
}, },
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
mock_device = MockESPHomeDevice(entry)
device_info = DeviceInfo( device_info = DeviceInfo(
name="test", name="test",
@ -169,6 +187,7 @@ async def _mock_generic_device_entry(
async def _subscribe_states(callback: Callable[[EntityState], None]) -> None: async def _subscribe_states(callback: Callable[[EntityState], None]) -> None:
"""Subscribe to state.""" """Subscribe to state."""
mock_device.set_state_callback(callback)
for state in states: for state in states:
callback(state) callback(state)
@ -194,7 +213,7 @@ async def _mock_generic_device_entry(
await hass.async_block_till_done() await hass.async_block_till_done()
return entry return mock_device
@pytest.fixture @pytest.fixture
@ -205,9 +224,11 @@ async def mock_voice_assistant_entry(
"""Set up an ESPHome entry with voice assistant.""" """Set up an ESPHome entry with voice assistant."""
async def _mock_voice_assistant_entry(version: int) -> MockConfigEntry: async def _mock_voice_assistant_entry(version: int) -> MockConfigEntry:
return await _mock_generic_device_entry( return (
hass, mock_client, {"voice_assistant_version": version}, ([], []), [] await _mock_generic_device_entry(
) hass, mock_client, {"voice_assistant_version": version}, ([], []), []
)
).entry
return _mock_voice_assistant_entry return _mock_voice_assistant_entry
@ -227,8 +248,11 @@ async def mock_voice_assistant_v2_entry(mock_voice_assistant_entry) -> MockConfi
@pytest.fixture @pytest.fixture
async def mock_generic_device_entry( async def mock_generic_device_entry(
hass: HomeAssistant, hass: HomeAssistant,
) -> MockConfigEntry: ) -> Callable[
"""Set up an ESPHome entry.""" [APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockConfigEntry],
]:
"""Set up an ESPHome entry and return the MockConfigEntry."""
async def _mock_device_entry( async def _mock_device_entry(
mock_client: APIClient, mock_client: APIClient,
@ -236,8 +260,32 @@ async def mock_generic_device_entry(
user_service: list[UserService], user_service: list[UserService],
states: list[EntityState], states: list[EntityState],
) -> MockConfigEntry: ) -> MockConfigEntry:
return (
await _mock_generic_device_entry(
hass, mock_client, {}, (entity_info, user_service), states
)
).entry
return _mock_device_entry
@pytest.fixture
async def mock_esphome_device(
hass: HomeAssistant,
) -> Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
]:
"""Set up an ESPHome entry and return the MockESPHomeDevice."""
async def _mock_device(
mock_client: APIClient,
entity_info: list[EntityInfo],
user_service: list[UserService],
states: list[EntityState],
) -> MockESPHomeDevice:
return await _mock_generic_device_entry( return await _mock_generic_device_entry(
hass, mock_client, {}, (entity_info, user_service), states hass, mock_client, {}, (entity_info, user_service), states
) )
return _mock_device_entry return _mock_device

View File

@ -1,11 +1,24 @@
"""Test ESPHome binary sensors.""" """Test ESPHome binary sensors."""
from aioesphomeapi import APIClient, BinarySensorInfo, BinarySensorState from collections.abc import Awaitable, Callable
from aioesphomeapi import (
APIClient,
BinarySensorInfo,
BinarySensorState,
EntityInfo,
EntityState,
UserService,
)
import pytest import pytest
from homeassistant.components.esphome import DomainData from homeassistant.components.esphome import DomainData
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .conftest import MockESPHomeDevice
from tests.common import MockConfigEntry
async def test_assist_in_progress( async def test_assist_in_progress(
hass: HomeAssistant, hass: HomeAssistant,
@ -37,7 +50,10 @@ async def test_binary_sensor_generic_entity(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: APIClient, mock_client: APIClient,
binary_state: tuple[bool, str], binary_state: tuple[bool, str],
mock_generic_device_entry, mock_generic_device_entry: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockConfigEntry],
],
) -> None: ) -> None:
"""Test a generic binary_sensor entity.""" """Test a generic binary_sensor entity."""
entity_info = [ entity_info = [
@ -63,7 +79,12 @@ async def test_binary_sensor_generic_entity(
async def test_status_binary_sensor( async def test_status_binary_sensor(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockConfigEntry],
],
) -> None: ) -> None:
"""Test a generic binary_sensor entity.""" """Test a generic binary_sensor entity."""
entity_info = [ entity_info = [
@ -89,7 +110,12 @@ async def test_status_binary_sensor(
async def test_binary_sensor_missing_state( async def test_binary_sensor_missing_state(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockConfigEntry],
],
) -> None: ) -> None:
"""Test a generic binary_sensor that is missing state.""" """Test a generic binary_sensor that is missing state."""
entity_info = [ entity_info = [
@ -111,3 +137,39 @@ async def test_binary_sensor_missing_state(
state = hass.states.get("binary_sensor.test_my_binary_sensor") state = hass.states.get("binary_sensor.test_my_binary_sensor")
assert state is not None assert state is not None
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
async def test_binary_sensor_has_state_false(
hass: HomeAssistant,
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test a generic binary_sensor where has_state is false."""
entity_info = [
BinarySensorInfo(
object_id="mybinary_sensor",
key=1,
name="my binary_sensor",
unique_id="my_binary_sensor",
)
]
states = []
user_service = []
mock_device = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("binary_sensor.test_my_binary_sensor")
assert state is not None
assert state.state == STATE_UNKNOWN
mock_device.set_state(BinarySensorState(key=1, state=True, missing_state=False))
await hass.async_block_till_done()
state = hass.states.get("binary_sensor.test_my_binary_sensor")
assert state is not None
assert state.state == STATE_ON