mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +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 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.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
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.device_registry import CONNECTION_NETWORK_MAC
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
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] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.ALARM_CONTROL_PANEL,
|
Platform.ALARM_CONTROL_PANEL,
|
||||||
@ -22,7 +26,11 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.SWITCH,
|
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:
|
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.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import BoschAlarmConfigEntry
|
|
||||||
from .entity import BoschAlarmAreaEntity
|
from .entity import BoschAlarmAreaEntity
|
||||||
|
from .types import BoschAlarmConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
"""Constants for the Bosch Alarm integration."""
|
"""Constants for the Bosch Alarm integration."""
|
||||||
|
|
||||||
DOMAIN = "bosch_alarm"
|
DOMAIN = "bosch_alarm"
|
||||||
HISTORY_ATTR = "history"
|
ATTR_HISTORY = "history"
|
||||||
CONF_INSTALLER_CODE = "installer_code"
|
CONF_INSTALLER_CODE = "installer_code"
|
||||||
CONF_USER_CODE = "user_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.const import CONF_PASSWORD
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import BoschAlarmConfigEntry
|
|
||||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
|
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
|
||||||
|
from .types import BoschAlarmConfigEntry
|
||||||
|
|
||||||
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
|
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
|
||||||
|
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
{
|
{
|
||||||
|
"services": {
|
||||||
|
"set_date_time": {
|
||||||
|
"service": "mdi:clock-edit"
|
||||||
|
}
|
||||||
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"alarms_gas": {
|
"alarms_gas": {
|
||||||
|
@ -13,10 +13,7 @@ rules:
|
|||||||
config-flow-test-coverage: done
|
config-flow-test-coverage: done
|
||||||
config-flow: done
|
config-flow: done
|
||||||
dependency-transparency: done
|
dependency-transparency: done
|
||||||
docs-actions:
|
docs-actions: done
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
No custom actions are defined.
|
|
||||||
docs-high-level-description: done
|
docs-high-level-description: done
|
||||||
docs-installation-instructions: done
|
docs-installation-instructions: done
|
||||||
docs-removal-instructions: done
|
docs-removal-instructions: done
|
||||||
@ -29,10 +26,7 @@ rules:
|
|||||||
unique-config-entry: done
|
unique-config-entry: done
|
||||||
|
|
||||||
# Silver
|
# Silver
|
||||||
action-exceptions:
|
action-exceptions: done
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
No custom actions are defined.
|
|
||||||
config-entry-unloading: done
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters: todo
|
docs-configuration-parameters: todo
|
||||||
docs-installation-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": {
|
"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": {
|
"cannot_connect": {
|
||||||
"message": "Could not connect to panel."
|
"message": "Could not connect to panel."
|
||||||
},
|
},
|
||||||
@ -61,6 +73,22 @@
|
|||||||
"message": "Door cannot be manipulated while it is momentarily unlocked."
|
"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": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"panel_fault_battery_mising": {
|
"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