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:
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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": ""},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user