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,129 @@ 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 == name 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+ if name and code :
89+ access_code = AccessCode (name = name , code = code )
90+ await hass .async_add_executor_job (entity ._lock .add_access_code , access_code ) # noqa: SLF001
91+ await coordinator .async_request_refresh ()
92+
93+ async def _delete_code (entity : SchlageLockEntity , calls : ServiceCall ) -> None :
94+ """Delete a lock code."""
95+ name = calls .data .get ("name" )
96+
97+ # name is required by voluptuous, the following is just to satisfy type
98+ # checker, it should never be None
99+ if name is None :
100+ raise ServiceValidationError (
101+ translation_domain = DOMAIN ,
102+ translation_key = "schlage_name_required" ,
103+ ) # pragma: no cover
104+
105+ codes = entity ._lock .access_codes # noqa: SLF001
106+ if not codes :
107+ return
108+
109+ code_id_to_delete = next (
110+ (
111+ code_id
112+ for code_id , code_data in codes .items ()
113+ if code_data .name .lower () == name .lower ()
114+ ),
115+ None ,
116+ )
117+
118+ if not code_id_to_delete :
119+ return
120+
121+ if entity ._lock .access_codes : # noqa: SLF001
122+ await hass .async_add_executor_job (
123+ entity ._lock .access_codes [code_id_to_delete ].delete # noqa: SLF001
124+ )
125+ await coordinator .async_request_refresh ()
126+
127+ async def _get_codes (
128+ entity : SchlageLockEntity , calls : ServiceCall
129+ ) -> ServiceResponse :
130+ """Get lock codes."""
131+
132+ if entity ._lock .access_codes : # noqa: SLF001
133+ return {
134+ code : {
135+ "name" : entity ._lock .access_codes [code ].name , # noqa: SLF001
136+ "code" : entity ._lock .access_codes [code ].code , # noqa: SLF001
137+ }
138+ for code in entity ._lock .access_codes # noqa: SLF001
139+ }
140+ return {}
141+
142+ platform = entity_platform .async_get_current_platform ()
143+ platform .async_register_entity_service (
144+ name = SERVICE_ADD_CODE ,
145+ schema = {
146+ vol .Required ("name" ): cv .string ,
147+ vol .Required ("code" ): cv .matches_regex (r"^\d{4,8}$" ),
148+ },
149+ func = _add_code ,
150+ )
151+
152+ platform .async_register_entity_service (
153+ name = SERVICE_DELETE_CODE ,
154+ schema = {
155+ vol .Required ("name" ): cv .string ,
156+ },
157+ func = _delete_code ,
158+ )
159+
160+ platform .async_register_entity_service (
161+ name = SERVICE_GET_CODES ,
162+ schema = None ,
163+ func = _get_codes ,
164+ supports_response = SupportsResponse .ONLY ,
165+ )
166+
32167
33168class SchlageLockEntity (SchlageEntity , LockEntity ):
34169 """Schlage lock entity."""
0 commit comments