44
55from typing import Any
66
7+ from pyschlage .code import AccessCode
8+ import voluptuous as vol
9+
710from homeassistant .components .lock import LockEntity
8- from homeassistant .core import HomeAssistant , callback
11+ from homeassistant .core import (
12+ HomeAssistant ,
13+ ServiceCall ,
14+ ServiceResponse ,
15+ SupportsResponse ,
16+ callback ,
17+ )
18+ from homeassistant .exceptions import ServiceValidationError
19+ from homeassistant .helpers import config_validation as cv , entity_platform
920from homeassistant .helpers .entity_platform import AddConfigEntryEntitiesCallback
1021
22+ from .const import DOMAIN , SERVICE_ADD_CODE , SERVICE_DELETE_CODE , SERVICE_GET_CODES
1123from .coordinator import LockData , SchlageConfigEntry , SchlageDataUpdateCoordinator
1224from .entity import SchlageEntity
1325
@@ -29,6 +41,128 @@ def _add_new_locks(locks: dict[str, LockData]) -> None:
2941 _add_new_locks (coordinator .data .locks )
3042 coordinator .new_locks_callbacks .append (_add_new_locks )
3143
44+ # Custom services
45+ def _validate_code_name (codes : dict [str , AccessCode ] | None , name : str ) -> None :
46+ """Validate that the code name doesn't already exist."""
47+ if codes and any (code .name .lower () == name .lower () for code in codes .values ()):
48+ raise ServiceValidationError (
49+ translation_domain = DOMAIN ,
50+ translation_key = "schlage_name_exists" ,
51+ )
52+
53+ def _validate_code_value (codes : dict [str , AccessCode ] | None , code : str ) -> None :
54+ """Validate that the code value doesn't already exist."""
55+ if codes and any (
56+ existing_code .code == code for existing_code in codes .values ()
57+ ):
58+ raise ServiceValidationError (
59+ translation_domain = DOMAIN ,
60+ translation_key = "schlage_code_exists" ,
61+ )
62+
63+ async def _add_code (entity : SchlageLockEntity , calls : ServiceCall ) -> None :
64+ """Add a lock code."""
65+ name = calls .data .get ("name" )
66+ code = calls .data .get ("code" )
67+
68+ # name is required by voluptuous, the following is just to satisfy type
69+ # checker, it should never be None
70+ if name is None :
71+ raise ServiceValidationError (
72+ translation_domain = DOMAIN ,
73+ translation_key = "schlage_name_required" ,
74+ ) # pragma: no cover
75+
76+ # code is required by voluptuous, the following is just to satisfy type
77+ # checker, it should never be None
78+ if code is None :
79+ raise ServiceValidationError (
80+ translation_domain = DOMAIN ,
81+ translation_key = "schlage_code_required" ,
82+ ) # pragma: no cover
83+
84+ codes = entity ._lock .access_codes # noqa: SLF001
85+ _validate_code_name (codes , name )
86+ _validate_code_value (codes , code )
87+
88+ access_code = AccessCode (name = name , code = code )
89+ await hass .async_add_executor_job (entity ._lock .add_access_code , access_code ) # noqa: SLF001
90+ await coordinator .async_request_refresh ()
91+
92+ async def _delete_code (entity : SchlageLockEntity , calls : ServiceCall ) -> None :
93+ """Delete a lock code."""
94+ name = calls .data .get ("name" )
95+
96+ # name is required by voluptuous, the following is just to satisfy type
97+ # checker, it should never be None
98+ if name is None :
99+ raise ServiceValidationError (
100+ translation_domain = DOMAIN ,
101+ translation_key = "schlage_name_required" ,
102+ ) # pragma: no cover
103+
104+ codes = entity ._lock .access_codes # noqa: SLF001
105+ if not codes :
106+ return
107+
108+ code_id_to_delete = next (
109+ (
110+ code_id
111+ for code_id , code_data in codes .items ()
112+ if code_data .name .lower () == name .lower ()
113+ ),
114+ None ,
115+ )
116+
117+ if not code_id_to_delete :
118+ return
119+
120+ if entity ._lock .access_codes : # noqa: SLF001
121+ await hass .async_add_executor_job (
122+ entity ._lock .access_codes [code_id_to_delete ].delete # noqa: SLF001
123+ )
124+ await coordinator .async_request_refresh ()
125+
126+ async def _get_codes (
127+ entity : SchlageLockEntity , calls : ServiceCall
128+ ) -> ServiceResponse :
129+ """Get lock codes."""
130+
131+ if entity ._lock .access_codes : # noqa: SLF001
132+ return {
133+ code : {
134+ "name" : entity ._lock .access_codes [code ].name , # noqa: SLF001
135+ "code" : entity ._lock .access_codes [code ].code , # noqa: SLF001
136+ }
137+ for code in entity ._lock .access_codes # noqa: SLF001
138+ }
139+ return {}
140+
141+ platform = entity_platform .async_get_current_platform ()
142+ platform .async_register_entity_service (
143+ name = SERVICE_ADD_CODE ,
144+ schema = {
145+ vol .Required ("name" ): cv .string ,
146+ vol .Required ("code" ): cv .matches_regex (r"^\d{4,8}$" ),
147+ },
148+ func = _add_code ,
149+ )
150+
151+ platform .async_register_entity_service (
152+ name = SERVICE_DELETE_CODE ,
153+ schema = {
154+ vol .Required ("name" ): cv .string ,
155+ },
156+ func = _delete_code ,
157+ )
158+
159+ platform .async_register_entity_service (
160+ name = SERVICE_GET_CODES ,
161+ schema = None ,
162+ func = _get_codes ,
163+ supports_response = SupportsResponse .ONLY ,
164+ )
165+
32166
33167class SchlageLockEntity (SchlageEntity , LockEntity ):
34168 """Schlage lock entity."""
0 commit comments