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, "scenes": bridge.scenes,
"occupancy_groups": bridge.occupancy_groups, "occupancy_groups": bridge.occupancy_groups,
"areas": bridge.areas, "areas": bridge.areas,
"smart_away_state": bridge.smart_away_state,
}, },
"integration_data": { "integration_data": {
"keypad_button_names_to_leap": data.keypad_data.button_names_to_leap, "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.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback 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( async def async_setup_entry(
@@ -23,9 +26,14 @@ async def async_setup_entry(
data = config_entry.runtime_data data = config_entry.runtime_data
bridge = data.bridge bridge = data.bridge
switch_devices = bridge.get_devices_by_domain(SWITCH_DOMAIN) 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 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): class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
@@ -61,3 +69,46 @@ class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if device is on.""" """Return true if device is on."""
return self._device["current_state"] > 0 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 import asyncio
from collections.abc import Callable from collections.abc import Callable
from typing import Any 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 import DOMAIN
from homeassistant.components.lutron_caseta.const import ( from homeassistant.components.lutron_caseta.const import (
@@ -90,7 +90,9 @@ _LEAP_DEVICE_TYPES = {
class MockBridge: class MockBridge:
"""Mock Lutron bridge that emulates configured connected status.""" """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.""" """Initialize MockBridge instance with configured mock connectivity."""
self.timeout_on_connect = timeout_on_connect self.timeout_on_connect = timeout_on_connect
self.can_connect = can_connect self.can_connect = can_connect
@@ -101,6 +103,23 @@ class MockBridge:
self.devices = self.load_devices() self.devices = self.load_devices()
self.buttons = self.load_buttons() self.buttons = self.load_buttons()
self._subscribers: dict[str, list] = {} 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): async def connect(self):
"""Connect the mock bridge.""" """Connect the mock bridge."""
@@ -115,6 +134,10 @@ class MockBridge:
self._subscribers[device_id] = [] self._subscribers[device_id] = []
self._subscribers[device_id].append(callback_) 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_): def add_button_subscriber(self, button_id: str, callback_):
"""Mock a listener for button presses.""" """Mock a listener for button presses."""
@@ -354,6 +377,7 @@ async def async_setup_integration(
can_connect: bool = True, can_connect: bool = True,
timeout_during_connect: bool = False, timeout_during_connect: bool = False,
timeout_during_configure: bool = False, timeout_during_configure: bool = False,
smart_away_state: str = "",
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up a mock bridge.""" """Set up a mock bridge."""
if config_entry_id is None: if config_entry_id is None:
@@ -370,7 +394,9 @@ async def async_setup_integration(
if not timeout_during_connect: if not timeout_during_connect:
on_connect_callback() on_connect_callback()
return mock_bridge( 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( with patch(

View File

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

View File

@@ -1,5 +1,13 @@
"""Tests for the Lutron Caseta integration.""" """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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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 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" 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