Skip to content

Commit ce11464

Browse files
authored
Enable Pylutron Caseta Smart Away (home-assistant#156711)
1 parent 1ce890b commit ce11464

File tree

5 files changed

+178
-6
lines changed

5 files changed

+178
-6
lines changed

homeassistant/components/lutron_caseta/diagnostics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ async def async_get_config_entry_diagnostics(
2525
"scenes": bridge.scenes,
2626
"occupancy_groups": bridge.occupancy_groups,
2727
"areas": bridge.areas,
28+
"smart_away_state": bridge.smart_away_state,
2829
},
2930
"integration_data": {
3031
"keypad_button_names_to_leap": data.keypad_data.button_names_to_leap,

homeassistant/components/lutron_caseta/switch.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
66
from homeassistant.config_entries import ConfigEntry
77
from homeassistant.core import HomeAssistant
8+
from homeassistant.helpers.device_registry import DeviceInfo
89
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
910

10-
from .entity import LutronCasetaUpdatableEntity
11+
from .const import DOMAIN
12+
from .entity import LutronCasetaEntity, LutronCasetaUpdatableEntity
13+
from .models import LutronCasetaData
1114

1215

1316
async def async_setup_entry(
@@ -23,9 +26,14 @@ async def async_setup_entry(
2326
data = config_entry.runtime_data
2427
bridge = data.bridge
2528
switch_devices = bridge.get_devices_by_domain(SWITCH_DOMAIN)
26-
async_add_entities(
29+
entities: list[LutronCasetaLight | LutronCasetaSmartAwaySwitch] = [
2730
LutronCasetaLight(switch_device, data) for switch_device in switch_devices
28-
)
31+
]
32+
33+
if bridge.smart_away_state != "":
34+
entities.append(LutronCasetaSmartAwaySwitch(data))
35+
36+
async_add_entities(entities)
2937

3038

3139
class LutronCasetaLight(LutronCasetaUpdatableEntity, SwitchEntity):
@@ -61,3 +69,46 @@ async def async_turn_off(self, **kwargs: Any) -> None:
6169
def is_on(self) -> bool:
6270
"""Return true if device is on."""
6371
return self._device["current_state"] > 0
72+
73+
74+
class LutronCasetaSmartAwaySwitch(LutronCasetaEntity, SwitchEntity):
75+
"""Representation of Lutron Caseta Smart Away."""
76+
77+
def __init__(self, data: LutronCasetaData) -> None:
78+
"""Init a switch entity."""
79+
device = {
80+
"device_id": "smart_away",
81+
"name": "Smart Away",
82+
"type": "SmartAway",
83+
"model": "Smart Away",
84+
"area": data.bridge_device["area"],
85+
"serial": data.bridge_device["serial"],
86+
}
87+
super().__init__(device, data)
88+
self._attr_device_info = DeviceInfo(
89+
identifiers={(DOMAIN, data.bridge_device["serial"])},
90+
)
91+
self._smart_away_unique_id = f"{self._bridge_unique_id}_smart_away"
92+
93+
@property
94+
def unique_id(self) -> str:
95+
"""Return the unique ID of the smart away switch."""
96+
return self._smart_away_unique_id
97+
98+
async def async_added_to_hass(self) -> None:
99+
"""Register callbacks."""
100+
await super().async_added_to_hass()
101+
self._smartbridge.add_smart_away_subscriber(self._handle_bridge_update)
102+
103+
async def async_turn_on(self, **kwargs: Any) -> None:
104+
"""Turn Smart Away on."""
105+
await self._smartbridge.activate_smart_away()
106+
107+
async def async_turn_off(self, **kwargs: Any) -> None:
108+
"""Turn Smart Away off."""
109+
await self._smartbridge.deactivate_smart_away()
110+
111+
@property
112+
def is_on(self) -> bool:
113+
"""Return true if Smart Away is on."""
114+
return self._smartbridge.smart_away_state == "Enabled"

tests/components/lutron_caseta/__init__.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
from collections.abc import Callable
55
from typing import Any
6-
from unittest.mock import patch
6+
from unittest.mock import AsyncMock, patch
77

88
from homeassistant.components.lutron_caseta import DOMAIN
99
from homeassistant.components.lutron_caseta.const import (
@@ -90,7 +90,9 @@
9090
class MockBridge:
9191
"""Mock Lutron bridge that emulates configured connected status."""
9292

93-
def __init__(self, can_connect=True, timeout_on_connect=False) -> None:
93+
def __init__(
94+
self, can_connect=True, timeout_on_connect=False, smart_away_state=""
95+
) -> None:
9496
"""Initialize MockBridge instance with configured mock connectivity."""
9597
self.timeout_on_connect = timeout_on_connect
9698
self.can_connect = can_connect
@@ -101,6 +103,23 @@ def __init__(self, can_connect=True, timeout_on_connect=False) -> None:
101103
self.devices = self.load_devices()
102104
self.buttons = self.load_buttons()
103105
self._subscribers: dict[str, list] = {}
106+
self.smart_away_state = smart_away_state
107+
self._smart_away_subscribers = []
108+
109+
self.activate_smart_away = AsyncMock(side_effect=self._activate)
110+
self.deactivate_smart_away = AsyncMock(side_effect=self._deactivate)
111+
112+
async def _activate(self):
113+
"""Activate smart away."""
114+
self.smart_away_state = "Enabled"
115+
for callback in self._smart_away_subscribers:
116+
callback()
117+
118+
async def _deactivate(self):
119+
"""Deactivate smart away."""
120+
self.smart_away_state = "Disabled"
121+
for callback in self._smart_away_subscribers:
122+
callback()
104123

105124
async def connect(self):
106125
"""Connect the mock bridge."""
@@ -115,6 +134,10 @@ def add_subscriber(self, device_id: str, callback_):
115134
self._subscribers[device_id] = []
116135
self._subscribers[device_id].append(callback_)
117136

137+
def add_smart_away_subscriber(self, callback_):
138+
"""Add a smart away subscriber."""
139+
self._smart_away_subscribers.append(callback_)
140+
118141
def add_button_subscriber(self, button_id: str, callback_):
119142
"""Mock a listener for button presses."""
120143

@@ -354,6 +377,7 @@ async def async_setup_integration(
354377
can_connect: bool = True,
355378
timeout_during_connect: bool = False,
356379
timeout_during_configure: bool = False,
380+
smart_away_state: str = "",
357381
) -> MockConfigEntry:
358382
"""Set up a mock bridge."""
359383
if config_entry_id is None:
@@ -370,7 +394,9 @@ def create_tls_factory(
370394
if not timeout_during_connect:
371395
on_connect_callback()
372396
return mock_bridge(
373-
can_connect=can_connect, timeout_on_connect=timeout_during_configure
397+
can_connect=can_connect,
398+
timeout_on_connect=timeout_during_configure,
399+
smart_away_state=smart_away_state,
374400
)
375401

376402
with patch(

tests/components/lutron_caseta/test_diagnostics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ async def test_diagnostics(
180180
},
181181
"occupancy_groups": {},
182182
"scenes": {},
183+
"smart_away_state": "",
183184
},
184185
"entry": {
185186
"data": {"ca_certs": "", "certfile": "", "host": "1.1.1.1", "keyfile": ""},

tests/components/lutron_caseta/test_switch.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
"""Tests for the Lutron Caseta integration."""
22

3+
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
4+
from homeassistant.const import (
5+
ATTR_ENTITY_ID,
6+
SERVICE_TURN_OFF,
7+
SERVICE_TURN_ON,
8+
STATE_OFF,
9+
STATE_ON,
10+
)
311
from homeassistant.core import HomeAssistant
412
from homeassistant.helpers import entity_registry as er
513

@@ -16,3 +24,88 @@ async def test_switch_unique_id(
1624

1725
# Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID
1826
assert entity_registry.async_get(switch_entity_id).unique_id == "000004d2_803"
27+
28+
29+
async def test_smart_away_switch_setup(
30+
hass: HomeAssistant, entity_registry: er.EntityRegistry
31+
) -> None:
32+
"""Test smart away switch is created when bridge supports it."""
33+
await async_setup_integration(hass, MockBridge, smart_away_state="Disabled")
34+
35+
smart_away_entity_id = "switch.hallway_smart_away"
36+
37+
# Verify entity is registered
38+
entity_entry = entity_registry.async_get(smart_away_entity_id)
39+
assert entity_entry is not None
40+
assert entity_entry.unique_id == "000004d2_smart_away"
41+
42+
# Verify initial state is off
43+
state = hass.states.get(smart_away_entity_id)
44+
assert state is not None
45+
assert state.state == STATE_OFF
46+
47+
48+
async def test_smart_away_switch_not_created_when_not_supported(
49+
hass: HomeAssistant, entity_registry: er.EntityRegistry
50+
) -> None:
51+
"""Test smart away switch is not created when bridge doesn't support it."""
52+
53+
await async_setup_integration(hass, MockBridge)
54+
55+
smart_away_entity_id = "switch.hallway_smart_away"
56+
57+
# Verify entity is not registered
58+
entity_entry = entity_registry.async_get(smart_away_entity_id)
59+
assert entity_entry is None
60+
61+
# Verify state doesn't exist
62+
state = hass.states.get(smart_away_entity_id)
63+
assert state is None
64+
65+
66+
async def test_smart_away_turn_on(hass: HomeAssistant) -> None:
67+
"""Test turning on smart away."""
68+
69+
await async_setup_integration(hass, MockBridge, smart_away_state="Disabled")
70+
71+
smart_away_entity_id = "switch.hallway_smart_away"
72+
73+
# Verify initial state is off
74+
state = hass.states.get(smart_away_entity_id)
75+
assert state.state == STATE_OFF
76+
77+
# Turn on smart away
78+
await hass.services.async_call(
79+
SWITCH_DOMAIN,
80+
SERVICE_TURN_ON,
81+
{ATTR_ENTITY_ID: smart_away_entity_id},
82+
blocking=True,
83+
)
84+
85+
# Verify state is on
86+
state = hass.states.get(smart_away_entity_id)
87+
assert state.state == STATE_ON
88+
89+
90+
async def test_smart_away_turn_off(hass: HomeAssistant) -> None:
91+
"""Test turning off smart away."""
92+
93+
await async_setup_integration(hass, MockBridge, smart_away_state="Enabled")
94+
95+
smart_away_entity_id = "switch.hallway_smart_away"
96+
97+
# Verify initial state is off
98+
state = hass.states.get(smart_away_entity_id)
99+
assert state.state == STATE_ON
100+
101+
# Turn on smart away
102+
await hass.services.async_call(
103+
SWITCH_DOMAIN,
104+
SERVICE_TURN_OFF,
105+
{ATTR_ENTITY_ID: smart_away_entity_id},
106+
blocking=True,
107+
)
108+
109+
# Verify state is on
110+
state = hass.states.get(smart_away_entity_id)
111+
assert state.state == STATE_OFF

0 commit comments

Comments
 (0)