1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Enable Pylutron Caseta Smart Away (#156711)

This commit is contained in:
omrishiv
2025-11-23 07:41:14 -08:00
committed by GitHub
parent 1ce890b105
commit ce1146492e
5 changed files with 178 additions and 6 deletions

View File

@@ -25,6 +25,7 @@ async def async_get_config_entry_diagnostics(
"scenes": bridge.scenes,
"occupancy_groups": bridge.occupancy_groups,
"areas": bridge.areas,
"smart_away_state": bridge.smart_away_state,
},
"integration_data": {
"keypad_button_names_to_leap": data.keypad_data.button_names_to_leap,

View File

@@ -5,9 +5,12 @@ from typing import Any
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import LutronCasetaUpdatableEntity
from .const import DOMAIN
from .entity import LutronCasetaEntity, LutronCasetaUpdatableEntity
from .models import LutronCasetaData
async def async_setup_entry(
@@ -23,9 +26,14 @@ async def async_setup_entry(
data = config_entry.runtime_data
bridge = data.bridge
switch_devices = bridge.get_devices_by_domain(SWITCH_DOMAIN)
async_add_entities(
entities: list[LutronCasetaLight | LutronCasetaSmartAwaySwitch] = [
LutronCasetaLight(switch_device, data) for switch_device in switch_devices
)
]
if bridge.smart_away_state != "":
entities.append(LutronCasetaSmartAwaySwitch(data))
async_add_entities(entities)
class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
@@ -61,3 +69,46 @@ class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
def is_on(self) -> bool:
"""Return true if device is on."""
return self._device["current_state"] > 0
class LutronCasetaSmartAwaySwitch(LutronCasetaEntity, SwitchEntity):
"""Representation of Lutron Caseta Smart Away."""
def __init__(self, data: LutronCasetaData) -> None:
"""Init a switch entity."""
device = {
"device_id": "smart_away",
"name": "Smart Away",
"type": "SmartAway",
"model": "Smart Away",
"area": data.bridge_device["area"],
"serial": data.bridge_device["serial"],
}
super().__init__(device, data)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, data.bridge_device["serial"])},
)
self._smart_away_unique_id = f"{self._bridge_unique_id}_smart_away"
@property
def unique_id(self) -> str:
"""Return the unique ID of the smart away switch."""
return self._smart_away_unique_id
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
await super().async_added_to_hass()
self._smartbridge.add_smart_away_subscriber(self._handle_bridge_update)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn Smart Away on."""
await self._smartbridge.activate_smart_away()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn Smart Away off."""
await self._smartbridge.deactivate_smart_away()
@property
def is_on(self) -> bool:
"""Return true if Smart Away is on."""
return self._smartbridge.smart_away_state == "Enabled"

View File

@@ -3,7 +3,7 @@
import asyncio
from collections.abc import Callable
from typing import Any
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from homeassistant.components.lutron_caseta import DOMAIN
from homeassistant.components.lutron_caseta.const import (
@@ -90,7 +90,9 @@ _LEAP_DEVICE_TYPES = {
class MockBridge:
"""Mock Lutron bridge that emulates configured connected status."""
def __init__(self, can_connect=True, timeout_on_connect=False) -> None:
def __init__(
self, can_connect=True, timeout_on_connect=False, smart_away_state=""
) -> None:
"""Initialize MockBridge instance with configured mock connectivity."""
self.timeout_on_connect = timeout_on_connect
self.can_connect = can_connect
@@ -101,6 +103,23 @@ class MockBridge:
self.devices = self.load_devices()
self.buttons = self.load_buttons()
self._subscribers: dict[str, list] = {}
self.smart_away_state = smart_away_state
self._smart_away_subscribers = []
self.activate_smart_away = AsyncMock(side_effect=self._activate)
self.deactivate_smart_away = AsyncMock(side_effect=self._deactivate)
async def _activate(self):
"""Activate smart away."""
self.smart_away_state = "Enabled"
for callback in self._smart_away_subscribers:
callback()
async def _deactivate(self):
"""Deactivate smart away."""
self.smart_away_state = "Disabled"
for callback in self._smart_away_subscribers:
callback()
async def connect(self):
"""Connect the mock bridge."""
@@ -115,6 +134,10 @@ class MockBridge:
self._subscribers[device_id] = []
self._subscribers[device_id].append(callback_)
def add_smart_away_subscriber(self, callback_):
"""Add a smart away subscriber."""
self._smart_away_subscribers.append(callback_)
def add_button_subscriber(self, button_id: str, callback_):
"""Mock a listener for button presses."""
@@ -354,6 +377,7 @@ async def async_setup_integration(
can_connect: bool = True,
timeout_during_connect: bool = False,
timeout_during_configure: bool = False,
smart_away_state: str = "",
) -> MockConfigEntry:
"""Set up a mock bridge."""
if config_entry_id is None:
@@ -370,7 +394,9 @@ async def async_setup_integration(
if not timeout_during_connect:
on_connect_callback()
return mock_bridge(
can_connect=can_connect, timeout_on_connect=timeout_during_configure
can_connect=can_connect,
timeout_on_connect=timeout_during_configure,
smart_away_state=smart_away_state,
)
with patch(

View File

@@ -180,6 +180,7 @@ async def test_diagnostics(
},
"occupancy_groups": {},
"scenes": {},
"smart_away_state": "",
},
"entry": {
"data": {"ca_certs": "", "certfile": "", "host": "1.1.1.1", "keyfile": ""},

View File

@@ -1,5 +1,13 @@
"""Tests for the Lutron Caseta integration."""
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -16,3 +24,88 @@ async def test_switch_unique_id(
# Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID
assert entity_registry.async_get(switch_entity_id).unique_id == "000004d2_803"
async def test_smart_away_switch_setup(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test smart away switch is created when bridge supports it."""
await async_setup_integration(hass, MockBridge, smart_away_state="Disabled")
smart_away_entity_id = "switch.hallway_smart_away"
# Verify entity is registered
entity_entry = entity_registry.async_get(smart_away_entity_id)
assert entity_entry is not None
assert entity_entry.unique_id == "000004d2_smart_away"
# Verify initial state is off
state = hass.states.get(smart_away_entity_id)
assert state is not None
assert state.state == STATE_OFF
async def test_smart_away_switch_not_created_when_not_supported(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test smart away switch is not created when bridge doesn't support it."""
await async_setup_integration(hass, MockBridge)
smart_away_entity_id = "switch.hallway_smart_away"
# Verify entity is not registered
entity_entry = entity_registry.async_get(smart_away_entity_id)
assert entity_entry is None
# Verify state doesn't exist
state = hass.states.get(smart_away_entity_id)
assert state is None
async def test_smart_away_turn_on(hass: HomeAssistant) -> None:
"""Test turning on smart away."""
await async_setup_integration(hass, MockBridge, smart_away_state="Disabled")
smart_away_entity_id = "switch.hallway_smart_away"
# Verify initial state is off
state = hass.states.get(smart_away_entity_id)
assert state.state == STATE_OFF
# Turn on smart away
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: smart_away_entity_id},
blocking=True,
)
# Verify state is on
state = hass.states.get(smart_away_entity_id)
assert state.state == STATE_ON
async def test_smart_away_turn_off(hass: HomeAssistant) -> None:
"""Test turning off smart away."""
await async_setup_integration(hass, MockBridge, smart_away_state="Enabled")
smart_away_entity_id = "switch.hallway_smart_away"
# Verify initial state is off
state = hass.states.get(smart_away_entity_id)
assert state.state == STATE_ON
# Turn on smart away
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: smart_away_entity_id},
blocking=True,
)
# Verify state is on
state = hass.states.get(smart_away_entity_id)
assert state.state == STATE_OFF