diff --git a/homeassistant/components/lutron_caseta/diagnostics.py b/homeassistant/components/lutron_caseta/diagnostics.py index 02763b14247..1e37b65782e 100644 --- a/homeassistant/components/lutron_caseta/diagnostics.py +++ b/homeassistant/components/lutron_caseta/diagnostics.py @@ -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, diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index b71ccf4bfa8..e8f2d2e0f0b 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -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" diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index 03b78b1e44e..28738445fc6 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -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( diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index 45229918578..759321bae30 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -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": ""}, diff --git a/tests/components/lutron_caseta/test_switch.py b/tests/components/lutron_caseta/test_switch.py index c38305ec26b..f8a092734bb 100644 --- a/tests/components/lutron_caseta/test_switch.py +++ b/tests/components/lutron_caseta/test_switch.py @@ -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