mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Add Button for TotalConnect (#114530)
* add button for totalconnect * test button for totalconnect * change to zone.can_be_bypassed * Update homeassistant/components/totalconnect/button.py Co-authored-by: Jan-Philipp Benecke <github@bnck.me> * Update homeassistant/components/totalconnect/button.py Co-authored-by: Jan-Philipp Benecke <github@bnck.me> * Update homeassistant/components/totalconnect/button.py Co-authored-by: Jan-Philipp Benecke <github@bnck.me> * remove unused logging * Update homeassistant/components/totalconnect/button.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/totalconnect/button.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * fix button and test * Revert "bump total_connect_client to 2023.12.1" This reverts commit 189b7dcd89cf3cc8309dacc92ba47927cfbbdef3. * bump total_connect_client to 2023.12.1 * use ZoneEntity for Bypass button * use LocationEntity for PanelButton * fix typing * add translation_key for panel buttons * mock clear_bypass instead of disarm * use paramaterize * use snapshot * sentence case in strings * remove un-needed stuff * Update homeassistant/components/totalconnect/button.py * Apply suggestions from code review * Fix --------- Co-authored-by: Jan-Philipp Benecke <github@bnck.me> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
0425b7aa6d
commit
8153ff78bf
@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR]
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON]
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
101
homeassistant/components/totalconnect/button.py
Normal file
101
homeassistant/components/totalconnect/button.py
Normal file
@ -0,0 +1,101 @@
|
||||
"""Interfaces with TotalConnect buttons."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from total_connect_client.location import TotalConnectLocation
|
||||
from total_connect_client.zone import TotalConnectZone
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import TotalConnectDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .entity import TotalConnectLocationEntity, TotalConnectZoneEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class TotalConnectButtonEntityDescription(ButtonEntityDescription):
|
||||
"""TotalConnect button description."""
|
||||
|
||||
press_fn: Callable[[TotalConnectLocation], None]
|
||||
|
||||
|
||||
PANEL_BUTTONS: tuple[TotalConnectButtonEntityDescription, ...] = (
|
||||
TotalConnectButtonEntityDescription(
|
||||
key="clear_bypass",
|
||||
translation_key="clear_bypass",
|
||||
press_fn=lambda location: location.clear_bypass(),
|
||||
),
|
||||
TotalConnectButtonEntityDescription(
|
||||
key="bypass_all",
|
||||
translation_key="bypass_all",
|
||||
press_fn=lambda location: location.zone_bypass_all(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up TotalConnect buttons based on a config entry."""
|
||||
buttons: list = []
|
||||
coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
for location_id, location in coordinator.client.locations.items():
|
||||
buttons.extend(
|
||||
TotalConnectPanelButton(coordinator, location, description)
|
||||
for description in PANEL_BUTTONS
|
||||
)
|
||||
|
||||
buttons.extend(
|
||||
TotalConnectZoneBypassButton(coordinator, zone, location_id)
|
||||
for zone in location.zones.values()
|
||||
if zone.can_be_bypassed
|
||||
)
|
||||
|
||||
async_add_entities(buttons)
|
||||
|
||||
|
||||
class TotalConnectZoneBypassButton(TotalConnectZoneEntity, ButtonEntity):
|
||||
"""Represent a TotalConnect zone bypass button."""
|
||||
|
||||
_attr_translation_key = "bypass"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TotalConnectDataUpdateCoordinator,
|
||||
zone: TotalConnectZone,
|
||||
location_id: str,
|
||||
) -> None:
|
||||
"""Initialize the TotalConnect status."""
|
||||
super().__init__(coordinator, zone, location_id, "bypass")
|
||||
|
||||
def press(self) -> None:
|
||||
"""Press the bypass button."""
|
||||
self._zone.bypass()
|
||||
|
||||
|
||||
class TotalConnectPanelButton(TotalConnectLocationEntity, ButtonEntity):
|
||||
"""Generic TotalConnect panel button."""
|
||||
|
||||
entity_description: TotalConnectButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TotalConnectDataUpdateCoordinator,
|
||||
location: TotalConnectLocation,
|
||||
entity_description: TotalConnectButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the TotalConnect button."""
|
||||
super().__init__(coordinator, location)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{location.location_id}_{entity_description.key}"
|
||||
|
||||
def press(self) -> None:
|
||||
"""Press the button."""
|
||||
self.entity_description.press_fn(self._location)
|
@ -55,6 +55,17 @@
|
||||
"partition": {
|
||||
"name": "Partition {partition_id}"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"clear_bypass": {
|
||||
"name": "Clear bypass"
|
||||
},
|
||||
"bypass_all": {
|
||||
"name": "Bypass all"
|
||||
},
|
||||
"bypass": {
|
||||
"name": "Bypass"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +206,17 @@ ZONE_7 = {
|
||||
"CanBeBypassed": 0,
|
||||
}
|
||||
|
||||
# ZoneType security that cannot be bypassed is a Button on the alarm panel
|
||||
ZONE_8 = {
|
||||
"ZoneID": 8,
|
||||
"ZoneDescription": "Button",
|
||||
"ZoneStatus": ZoneStatus.FAULT,
|
||||
"ZoneTypeId": ZoneType.SECURITY,
|
||||
"PartitionId": "1",
|
||||
"CanBeBypassed": 0,
|
||||
}
|
||||
|
||||
|
||||
ZONE_INFO = [ZONE_NORMAL, ZONE_2, ZONE_3, ZONE_4, ZONE_5, ZONE_6, ZONE_7]
|
||||
ZONES = {"ZoneInfo": ZONE_INFO}
|
||||
|
||||
@ -318,6 +329,14 @@ RESPONSE_USER_CODE_INVALID = {
|
||||
"ResultData": "testing user code invalid",
|
||||
}
|
||||
RESPONSE_SUCCESS = {"ResultCode": ResultCode.SUCCESS.value}
|
||||
RESPONSE_ZONE_BYPASS_SUCCESS = {
|
||||
"ResultCode": ResultCode.SUCCESS.value,
|
||||
"ResultData": "None",
|
||||
}
|
||||
RESPONSE_ZONE_BYPASS_FAILURE = {
|
||||
"ResultCode": ResultCode.FAILED_TO_BYPASS_ZONE.value,
|
||||
"ResultData": "None",
|
||||
}
|
||||
|
||||
USERNAME = "username@me.com"
|
||||
PASSWORD = "password"
|
||||
|
277
tests/components/totalconnect/snapshots/test_button.ambr
Normal file
277
tests/components/totalconnect/snapshots/test_button.ambr
Normal file
@ -0,0 +1,277 @@
|
||||
# serializer version: 1
|
||||
# name: test_entity_registry[button.fire_bypass-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.fire_bypass',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bypass',
|
||||
'platform': 'totalconnect',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bypass',
|
||||
'unique_id': '123456_2_bypass',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.fire_bypass-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Fire Bypass',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.fire_bypass',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.gas_bypass-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.gas_bypass',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bypass',
|
||||
'platform': 'totalconnect',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bypass',
|
||||
'unique_id': '123456_3_bypass',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.gas_bypass-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Gas Bypass',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.gas_bypass',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.motion_bypass-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.motion_bypass',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bypass',
|
||||
'platform': 'totalconnect',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bypass',
|
||||
'unique_id': '123456_4_bypass',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.motion_bypass-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Motion Bypass',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.motion_bypass',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.security_bypass-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.security_bypass',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bypass',
|
||||
'platform': 'totalconnect',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bypass',
|
||||
'unique_id': '123456_1_bypass',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.security_bypass-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Security Bypass',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.security_bypass',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.test_bypass_all-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.test_bypass_all',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bypass all',
|
||||
'platform': 'totalconnect',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bypass_all',
|
||||
'unique_id': '123456_bypass_all',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.test_bypass_all-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'test Bypass all',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.test_bypass_all',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.test_clear_bypass-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': None,
|
||||
'entity_id': 'button.test_clear_bypass',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Clear bypass',
|
||||
'platform': 'totalconnect',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'clear_bypass',
|
||||
'unique_id': '123456_clear_bypass',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entity_registry[button.test_clear_bypass-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'test Clear bypass',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.test_clear_bypass',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
78
tests/components/totalconnect/test_button.py
Normal file
78
tests/components/totalconnect/test_button.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Tests for the TotalConnect buttons."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
from total_connect_client.exceptions import FailedToBypassZone
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON, SERVICE_PRESS
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import (
|
||||
RESPONSE_ZONE_BYPASS_FAILURE,
|
||||
RESPONSE_ZONE_BYPASS_SUCCESS,
|
||||
TOTALCONNECT_REQUEST,
|
||||
setup_platform,
|
||||
)
|
||||
|
||||
from tests.common import snapshot_platform
|
||||
|
||||
ZONE_BYPASS_ID = "button.security_bypass"
|
||||
PANEL_CLEAR_ID = "button.test_clear_bypass"
|
||||
PANEL_BYPASS_ID = "button.test_bypass_all"
|
||||
|
||||
|
||||
async def test_entity_registry(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test the button is registered in entity registry."""
|
||||
entry = await setup_platform(hass, BUTTON)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entity_id", [ZONE_BYPASS_ID, PANEL_BYPASS_ID])
|
||||
async def test_bypass_button(hass: HomeAssistant, entity_id: str) -> None:
|
||||
"""Test pushing a bypass button."""
|
||||
responses = [RESPONSE_ZONE_BYPASS_FAILURE, RESPONSE_ZONE_BYPASS_SUCCESS]
|
||||
await setup_platform(hass, BUTTON)
|
||||
with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request:
|
||||
# try to bypass, but fails
|
||||
with pytest.raises(FailedToBypassZone):
|
||||
await hass.services.async_call(
|
||||
domain=BUTTON,
|
||||
service=SERVICE_PRESS,
|
||||
service_data={ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_request.call_count == 1
|
||||
|
||||
# try to bypass, works this time
|
||||
await hass.services.async_call(
|
||||
domain=BUTTON,
|
||||
service=SERVICE_PRESS,
|
||||
service_data={ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_request.call_count == 2
|
||||
|
||||
|
||||
async def test_clear_button(hass: HomeAssistant) -> None:
|
||||
"""Test pushing the clear bypass button."""
|
||||
data = {ATTR_ENTITY_ID: PANEL_CLEAR_ID}
|
||||
await setup_platform(hass, BUTTON)
|
||||
TOTALCONNECT_REQUEST = (
|
||||
"total_connect_client.location.TotalConnectLocation.clear_bypass"
|
||||
)
|
||||
|
||||
with patch(TOTALCONNECT_REQUEST) as mock_request:
|
||||
await hass.services.async_call(
|
||||
domain=BUTTON,
|
||||
service=SERVICE_PRESS,
|
||||
service_data=data,
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_request.call_count == 1
|
Loading…
x
Reference in New Issue
Block a user