mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
add date and time service to bosch_alarm (#142243)
* add date and time service * update quality scale * add changes from review * fix issues after merge * fix icons * apply changes from review * remove list from service schema * update quality scale * update strings * Update homeassistant/components/bosch_alarm/services.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * apply changes from review * apply changes from review * Update tests/components/bosch_alarm/test_services.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * validate exception messages * use schema to validate service call * update docstring * update error message --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
7f9b454922
commit
c3fe5f012e
@ -6,14 +6,18 @@ from ssl import SSLError
|
||||
|
||||
from bosch_alarm_mode2 import Panel
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||
from .services import setup_services
|
||||
from .types import BoschAlarmConfigEntry
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
@ -22,7 +26,11 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up bosch alarm services."""
|
||||
setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
|
||||
|
@ -12,8 +12,8 @@ from homeassistant.components.alarm_control_panel import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BoschAlarmConfigEntry
|
||||
from .entity import BoschAlarmAreaEntity
|
||||
from .types import BoschAlarmConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
@ -1,6 +1,9 @@
|
||||
"""Constants for the Bosch Alarm integration."""
|
||||
|
||||
DOMAIN = "bosch_alarm"
|
||||
HISTORY_ATTR = "history"
|
||||
ATTR_HISTORY = "history"
|
||||
CONF_INSTALLER_CODE = "installer_code"
|
||||
CONF_USER_CODE = "user_code"
|
||||
ATTR_DATETIME = "datetime"
|
||||
SERVICE_SET_DATE_TIME = "set_date_time"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
|
@ -6,8 +6,8 @@ from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import BoschAlarmConfigEntry
|
||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
|
||||
from .types import BoschAlarmConfigEntry
|
||||
|
||||
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
|
||||
|
||||
|
@ -1,4 +1,9 @@
|
||||
{
|
||||
"services": {
|
||||
"set_date_time": {
|
||||
"service": "mdi:clock-edit"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"alarms_gas": {
|
||||
|
@ -13,10 +13,7 @@ rules:
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
@ -29,10 +26,7 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
|
76
homeassistant/components/bosch_alarm/services.py
Normal file
76
homeassistant/components/bosch_alarm/services.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Services for the bosch_alarm integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
||||
from .types import BoschAlarmConfigEntry
|
||||
|
||||
|
||||
def validate_datetime(value: Any) -> dt.datetime:
|
||||
"""Validate that a provided datetime is supported on a bosch alarm panel."""
|
||||
date_val = cv.datetime(value)
|
||||
if date_val.year < 2010:
|
||||
raise vol.RangeInvalid("datetime must be after 2009")
|
||||
|
||||
if date_val.year > 2037:
|
||||
raise vol.RangeInvalid("datetime must be before 2038")
|
||||
|
||||
return date_val
|
||||
|
||||
|
||||
SET_DATE_TIME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Optional(ATTR_DATETIME): validate_datetime,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the bosch alarm integration."""
|
||||
|
||||
async def async_set_panel_date(call: ServiceCall) -> None:
|
||||
"""Set the date and time on a bosch alarm panel."""
|
||||
config_entry: BoschAlarmConfigEntry | None
|
||||
value: dt.datetime = call.data.get(ATTR_DATETIME, dt_util.now())
|
||||
entry_id = call.data[ATTR_CONFIG_ENTRY_ID]
|
||||
if not (config_entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": entry_id},
|
||||
)
|
||||
if config_entry.state is not ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
panel = config_entry.runtime_data
|
||||
try:
|
||||
await panel.set_panel_date(value)
|
||||
except asyncio.InvalidStateError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
) from err
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
async_set_panel_date,
|
||||
schema=SET_DATE_TIME_SCHEMA,
|
||||
)
|
12
homeassistant/components/bosch_alarm/services.yaml
Normal file
12
homeassistant/components/bosch_alarm/services.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
set_date_time:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: bosch_alarm
|
||||
datetime:
|
||||
required: false
|
||||
example: "2025-05-10 00:00:00"
|
||||
selector:
|
||||
datetime:
|
@ -51,6 +51,18 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"integration_not_found": {
|
||||
"message": "Integration \"{target}\" not found in registry."
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded."
|
||||
},
|
||||
"connection_error": {
|
||||
"message": "Could not connect to \"{target}\"."
|
||||
},
|
||||
"unknown_error": {
|
||||
"message": "An unknown error occurred while setting the date and time on \"{target}\"."
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Could not connect to panel."
|
||||
},
|
||||
@ -61,6 +73,22 @@
|
||||
"message": "Door cannot be manipulated while it is momentarily unlocked."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_date_time": {
|
||||
"name": "Set date & time",
|
||||
"description": "Sets the date and time on the alarm panel.",
|
||||
"fields": {
|
||||
"datetime": {
|
||||
"name": "Date & time",
|
||||
"description": "The date and time to set. The time zone of the Home Assistant instance is assumed. If omitted, the current date and time is used."
|
||||
},
|
||||
"config_entry_id": {
|
||||
"name": "Config entry",
|
||||
"description": "The Bosch Alarm integration ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"panel_fault_battery_mising": {
|
||||
|
7
homeassistant/components/bosch_alarm/types.py
Normal file
7
homeassistant/components/bosch_alarm/types.py
Normal file
@ -0,0 +1,7 @@
|
||||
"""Types for the Bosch Alarm integration."""
|
||||
|
||||
from bosch_alarm_mode2 import Panel
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
type BoschAlarmConfigEntry = ConfigEntry[Panel]
|
192
tests/components/bosch_alarm/test_services.py
Normal file
192
tests/components/bosch_alarm/test_services.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Tests for Bosch Alarm component."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import datetime as dt
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bosch_alarm.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_DATETIME,
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def platforms() -> AsyncGenerator[None]:
|
||||
"""Return the platforms to be loaded for this test."""
|
||||
with patch("homeassistant.components.bosch_alarm.PLATFORMS", []):
|
||||
yield
|
||||
|
||||
|
||||
async def test_set_date_time_service(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls succeed if the service call is valid."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_DATETIME: dt_util.now(),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_panel.set_panel_date.assert_called_once()
|
||||
|
||||
|
||||
async def test_set_date_time_service_fails_bad_entity(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls fail if the service call is done for an incorrect entity."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match='Integration "bad-config_id" not found in registry',
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: "bad-config_id",
|
||||
ATTR_DATETIME: dt_util.now(),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_date_time_service_fails_bad_params(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls fail if the service call is done with incorrect params."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
with pytest.raises(
|
||||
vol.MultipleInvalid,
|
||||
match=r"Invalid datetime specified: for dictionary value @ data\['datetime'\]",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_DATETIME: "",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_date_time_service_fails_bad_year_before(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls fail if the panel fails the service call."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
with pytest.raises(
|
||||
vol.MultipleInvalid,
|
||||
match=r"datetime must be before 2038 for dictionary value @ data\['datetime'\]",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_DATETIME: dt.datetime(2038, 1, 1),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_date_time_service_fails_bad_year_after(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls fail if the panel fails the service call."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mock_panel.set_panel_date.side_effect = ValueError()
|
||||
with pytest.raises(
|
||||
vol.MultipleInvalid,
|
||||
match=r"datetime must be after 2009 for dictionary value @ data\['datetime'\]",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_DATETIME: dt.datetime(2009, 1, 1),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_date_time_service_fails_connection_error(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls fail if the panel fails the service call."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mock_panel.set_panel_date.side_effect = asyncio.InvalidStateError()
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f'Could not connect to "{mock_config_entry.title}"',
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_DATETIME: dt_util.now(),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_set_date_time_service_fails_unloaded(
|
||||
hass: HomeAssistant,
|
||||
mock_panel: AsyncMock,
|
||||
area: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that the service calls fail if the config entry is unloaded."""
|
||||
await async_setup_component(hass, DOMAIN, {})
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=f"{mock_config_entry.title} is not loaded",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_DATE_TIME,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
||||
ATTR_DATETIME: dt_util.now(),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user