mirror of
https://github.com/home-assistant/core.git
synced 2026-09-05 04:51:05 +01:00
Only add a Lutron Caseta battery sensor where there is a battery (#180545)
This commit is contained in:
@@ -1,18 +1,25 @@
|
||||
"""Support for Lutron Caseta Occupancy/Vacancy/Battery Sensors."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from typing import Any, override
|
||||
|
||||
from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED, BridgeResponseError
|
||||
from pylutron_caseta import (
|
||||
OCCUPANCY_GROUP_OCCUPIED,
|
||||
BridgeDisconnectedError,
|
||||
BridgeResponseError,
|
||||
)
|
||||
from pylutron_caseta.smartbridge import Smartbridge
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||
from homeassistant.const import ATTR_SUGGESTED_AREA, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -44,13 +51,41 @@ async def async_setup_entry(
|
||||
LutronOccupancySensor(hass, occupancy_group, data)
|
||||
for occupancy_group in occupancy_groups.values()
|
||||
)
|
||||
async_add_entities(
|
||||
(
|
||||
LutronCasetaBatterySensor(hass, device, data)
|
||||
for device in bridge.get_devices_by_domain(COVER_DOMAIN)
|
||||
),
|
||||
update_before_add=True,
|
||||
battery_sensors = [
|
||||
LutronCasetaBatterySensor(hass, cover, data)
|
||||
for cover in bridge.get_devices_by_domain(COVER_DOMAIN)
|
||||
]
|
||||
reports_battery = await asyncio.gather(
|
||||
*(
|
||||
_async_reports_battery(bridge, sensor.device_id)
|
||||
for sensor in battery_sensors
|
||||
)
|
||||
)
|
||||
entity_registry = er.async_get(hass)
|
||||
sensors_to_add: list[LutronCasetaBatterySensor] = []
|
||||
for sensor, has_battery in zip(battery_sensors, reports_battery, strict=True):
|
||||
if has_battery:
|
||||
sensors_to_add.append(sensor)
|
||||
elif entity_id := entity_registry.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, sensor.unique_id
|
||||
):
|
||||
# Earlier releases created this for every cover
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async_add_entities(sensors_to_add, update_before_add=True)
|
||||
|
||||
|
||||
async def _async_reports_battery(bridge: Smartbridge, device_id: str) -> bool:
|
||||
"""Return whether the bridge reports a battery for the device.
|
||||
|
||||
A shade wired for power answers without a battery status, and nothing in
|
||||
the device data the bridge caches tells the two apart. A cover the bridge
|
||||
could not answer for keeps its sensor, so nothing is removed over a hiccup.
|
||||
"""
|
||||
try:
|
||||
return await bridge.get_battery_status(device_id) is not None
|
||||
except BridgeResponseError, BridgeDisconnectedError, TimeoutError:
|
||||
return True
|
||||
|
||||
|
||||
class LutronOccupancySensor(LutronCasetaEntity, BinarySensorEntity):
|
||||
@@ -149,7 +184,7 @@ class LutronCasetaBatterySensor(LutronCasetaEntity, BinarySensorEntity):
|
||||
"""Fetch the latest battery status from the bridge."""
|
||||
try:
|
||||
status = await self._smartbridge.get_battery_status(self.device_id)
|
||||
except BridgeResponseError:
|
||||
except BridgeResponseError, BridgeDisconnectedError, TimeoutError:
|
||||
self._attr_is_on = None
|
||||
return
|
||||
normalized_status = status.strip().casefold() if status else None
|
||||
|
||||
@@ -4,9 +4,14 @@ from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pylutron_caseta import BridgeResponseError
|
||||
from pylutron_caseta import BridgeDisconnectedError, BridgeResponseError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.lutron_caseta import DOMAIN
|
||||
from homeassistant.components.lutron_caseta.binary_sensor import SCAN_INTERVAL
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -88,6 +93,9 @@ async def test_battery_sensor_updates_on_schedule(
|
||||
await async_setup_integration(hass, factory)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Setup asks once to find out whether the shade has a battery at all
|
||||
instance.get_battery_status.reset_mock()
|
||||
|
||||
binary_sensor_entity_id = (
|
||||
"binary_sensor.basement_bedroom_basement_bedroom_left_shade_battery"
|
||||
)
|
||||
@@ -107,7 +115,7 @@ async def test_battery_sensor_updates_on_schedule(
|
||||
updated_state = hass.states.get(binary_sensor_entity_id)
|
||||
assert updated_state is not None
|
||||
assert updated_state.state == STATE_ON
|
||||
assert instance.get_battery_status.await_count == 2
|
||||
assert instance.get_battery_status.await_count == 1
|
||||
instance.get_battery_status.assert_awaited_with("802")
|
||||
|
||||
instance.battery_statuses["802"] = "Unknown"
|
||||
@@ -118,7 +126,86 @@ async def test_battery_sensor_updates_on_schedule(
|
||||
unknown_state = hass.states.get(binary_sensor_entity_id)
|
||||
assert unknown_state is not None
|
||||
assert unknown_state.state == STATE_UNKNOWN
|
||||
assert instance.get_battery_status.await_count == 3
|
||||
assert instance.get_battery_status.await_count == 2
|
||||
|
||||
|
||||
async def test_no_battery_sensor_for_a_shade_wired_for_power(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test a shade the bridge reports no battery for gets no battery sensor."""
|
||||
instance = MockBridge()
|
||||
instance.battery_statuses = {}
|
||||
|
||||
def factory(*args: Any, **kwargs: Any) -> MockBridge:
|
||||
"""Return the mock bridge instance."""
|
||||
return instance
|
||||
|
||||
await async_setup_integration(hass, factory)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get(
|
||||
"binary_sensor.basement_bedroom_basement_bedroom_left_shade_battery"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_battery_sensor_removed_when_the_shade_has_no_battery(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a battery sensor from an earlier release is removed."""
|
||||
instance = MockBridge()
|
||||
instance.battery_statuses = {}
|
||||
|
||||
def factory(*args: Any, **kwargs: Any) -> MockBridge:
|
||||
"""Return the mock bridge instance."""
|
||||
return instance
|
||||
|
||||
entity_registry.async_get_or_create(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"000004d2_802_battery",
|
||||
suggested_object_id="basement_bedroom_basement_bedroom_left_shade_battery",
|
||||
)
|
||||
|
||||
await async_setup_integration(hass, factory)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, "000004d2_802_battery"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[BridgeDisconnectedError(), TimeoutError],
|
||||
)
|
||||
async def test_battery_sensor_kept_when_the_bridge_cannot_answer(
|
||||
hass: HomeAssistant,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
"""Test a cover keeps its battery sensor when the bridge cannot be asked."""
|
||||
instance = MockBridge()
|
||||
instance.get_battery_status = AsyncMock(side_effect=error)
|
||||
|
||||
def factory(*args: Any, **kwargs: Any) -> MockBridge:
|
||||
"""Return the mock bridge instance."""
|
||||
return instance
|
||||
|
||||
await async_setup_integration(hass, factory)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get(
|
||||
"binary_sensor.basement_bedroom_basement_bedroom_left_shade_battery"
|
||||
)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
async def test_battery_sensor_handles_bridge_response_error(
|
||||
|
||||
Reference in New Issue
Block a user