Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Risco set_time service #139015

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions homeassistant/components/risco/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
import logging
from typing import Any

from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError
from pyrisco.common import Partition, System, Zone
Expand All @@ -22,8 +19,10 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType

from .const import (
CONF_CONCURRENCY,
Expand All @@ -35,6 +34,10 @@
TYPE_LOCAL,
)
from .coordinator import RiscoDataUpdateCoordinator, RiscoEventsDataUpdateCoordinator
from .models import LocalData
from .services import async_setup_services

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Expand All @@ -45,14 +48,6 @@
_LOGGER = logging.getLogger(__name__)


@dataclass
class LocalData:
"""A data class for local data passed to the platforms."""

system: RiscoLocal
partition_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)


def is_local(entry: ConfigEntry) -> bool:
"""Return whether the entry represents an instance with local communication."""
return entry.data.get(CONF_TYPE) == TYPE_LOCAL
Expand Down Expand Up @@ -176,3 +171,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Risco integration services."""

await async_setup_services(hass)

return True
2 changes: 2 additions & 0 deletions homeassistant/components/risco/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_CONCURRENCY: DEFAULT_CONCURRENCY,
}

SERVICE_SET_TIME = "set_time"
7 changes: 7 additions & 0 deletions homeassistant/components/risco/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"services": {
"set_time": {
"service": "mdi:clock-edit"
}
}
}
15 changes: 15 additions & 0 deletions homeassistant/components/risco/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Models for Risco integration."""

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any

from pyrisco import RiscoLocal


@dataclass
class LocalData:
"""A data class for local data passed to the platforms."""

system: RiscoLocal
partition_updates: dict[int, Callable[[], Any]] = field(default_factory=dict)
42 changes: 42 additions & 0 deletions homeassistant/components/risco/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Services for Risco integration."""

from datetime import datetime

import voluptuous as vol

from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv

from .const import DOMAIN, SERVICE_SET_TIME
from .models import LocalData

ATTR_CONF_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_TIME = "time"


async def async_setup_services(hass: HomeAssistant) -> None:
"""Create the Risco Services/Actions."""

async def _set_time(service_call: ServiceCall) -> None:
config_entry_id = service_call.data.get(ATTR_CONF_CONFIG_ENTRY_ID, None)
time = service_call.data.get(ATTR_TIME, None)

time_to_send = time
if time is None:
time_to_send = datetime.now()

local_data: LocalData = hass.data[DOMAIN][config_entry_id]

await local_data.system.set_time(time_to_send)

hass.services.async_register(
domain=DOMAIN,
service=SERVICE_SET_TIME,
schema=vol.Schema(
{
vol.Required(ATTR_CONF_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_TIME): cv.datetime,
}
),
service_func=_set_time,
)
11 changes: 11 additions & 0 deletions homeassistant/components/risco/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
set_time:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: risco
time:
required: false
selector:
datetime:
16 changes: 16 additions & 0 deletions homeassistant/components/risco/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,21 @@
"name": "Bypassed"
}
}
},
"services": {
"set_time": {
"name": "Set the alarm panel time",
"description": "Sets the time of an alarm panel.",
"fields": {
"config_entry_id": {
"name": "Config entry",
"description": "The Risco alarm panel to set the time for."
},
"time": {
"name": "Time",
"description": "The time to send to the alarm panel. Leave it empty to use the Home Assistant system time."
}
}
}
}
}
51 changes: 51 additions & 0 deletions tests/components/risco/test_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Tests for the Risco services."""

from datetime import datetime
from unittest.mock import patch

from homeassistant.components.risco import DOMAIN
from homeassistant.components.risco.const import SERVICE_SET_TIME
from homeassistant.core import HomeAssistant


async def test_set_time_service(
hass: HomeAssistant, setup_risco_local, local_config_entry
) -> None:
"""Test the set_time service."""
with patch("homeassistant.components.risco.RiscoLocal.set_time") as mock:
time_str = "2025-02-21T12:00:00"
time = datetime.fromisoformat(time_str)
data = {
"config_entry_id": local_config_entry.entry_id,
"time": time_str,
}

await hass.services.async_call(
DOMAIN, SERVICE_SET_TIME, service_data=data, blocking=True
)

mock.assert_called_once_with(time)


async def test_set_time_service_with_no_time(
hass: HomeAssistant, setup_risco_local, local_config_entry
) -> None:
"""Test the set_time service when no time is provided."""
fixed_time = datetime(2025, 2, 21, 12, 0, 0)

with patch("homeassistant.components.risco.services.datetime") as mock_datetime:
mock_datetime.now.return_value = fixed_time
mock_datetime.fromisoformat = datetime.fromisoformat

with patch(
"homeassistant.components.risco.RiscoLocal.set_time"
) as mock_set_time:
data = {
"config_entry_id": local_config_entry.entry_id,
}

await hass.services.async_call(
DOMAIN, SERVICE_SET_TIME, service_data=data, blocking=True
)

mock_set_time.assert_called_once_with(fixed_time)