Skip to content

Commit bb141ca

Browse files
committed
Feat: Add services for managing Schlage door codes
This commit introduces three new services for the Schlage integration: - `add_code`: Allows users to add a new access code to a Schlage lock. - `delete_code`: Enables users to delete an existing access code from a Schlage lock. - `get_codes`: Retrieves all access codes associated with a Schlage lock. Signed-off-by: Andrew Grimberg <[email protected]>
1 parent a50b035 commit bb141ca

File tree

8 files changed

+560
-10
lines changed

8 files changed

+560
-10
lines changed

homeassistant/components/schlage/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SchlageConfigEntry) -> b
3333
hass, entry, username, pyschlage.Schlage(auth)
3434
)
3535
entry.runtime_data = coordinator
36+
3637
await coordinator.async_config_entry_first_refresh()
3738
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
3839
return True

homeassistant/components/schlage/const.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@
77
LOGGER = logging.getLogger(__package__)
88
MANUFACTURER = "Schlage"
99
UPDATE_INTERVAL = timedelta(seconds=30)
10+
11+
SERVICE_ADD_CODE = "add_code"
12+
SERVICE_DELETE_CODE = "delete_code"
13+
SERVICE_GET_CODES = "get_codes"

homeassistant/components/schlage/coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(
6565
async def _async_update_data(self) -> SchlageData:
6666
"""Fetch the latest data from the Schlage API."""
6767
try:
68-
locks = await self.hass.async_add_executor_job(self.api.locks)
68+
locks = await self.hass.async_add_executor_job(self.api.locks, True)
6969
except NotAuthorizedError as ex:
7070
raise ConfigEntryAuthFailed from ex
7171
except SchlageError as ex:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"services": {
3+
"add_code": {
4+
"service": "mdi:key-plus"
5+
},
6+
"delete_code": {
7+
"service": "mdi:key-minus"
8+
},
9+
"get_codes": {
10+
"service": "mdi:table-key"
11+
}
12+
}
13+
}

homeassistant/components/schlage/lock.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,22 @@
44

55
from typing import Any
66

7+
from pyschlage.code import AccessCode
8+
import voluptuous as vol
9+
710
from 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
920
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1021

22+
from .const import DOMAIN, SERVICE_ADD_CODE, SERVICE_DELETE_CODE, SERVICE_GET_CODES
1123
from .coordinator import LockData, SchlageConfigEntry, SchlageDataUpdateCoordinator
1224
from .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

33168
class SchlageLockEntity(SchlageEntity, LockEntity):
34169
"""Schlage lock entity."""
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
get_codes:
2+
fields:
3+
entity_id:
4+
required: true
5+
selector:
6+
entity:
7+
filter:
8+
domain: lock
9+
integration: schlage
10+
multiple: true
11+
12+
add_code:
13+
fields:
14+
entity_id:
15+
required: true
16+
selector:
17+
entity:
18+
filter:
19+
domain: lock
20+
integration: schlage
21+
multiple: true
22+
name:
23+
required: true
24+
example: "Example Person"
25+
selector:
26+
text:
27+
multiline: false
28+
code:
29+
required: true
30+
example: "1111"
31+
selector:
32+
text:
33+
multiline: false
34+
35+
delete_code:
36+
fields:
37+
entity_id:
38+
required: true
39+
selector:
40+
entity:
41+
filter:
42+
domain: lock
43+
integration: schlage
44+
multiple: true
45+
name:
46+
required: true
47+
example: "Example Person"
48+
selector:
49+
text:
50+
multiline: false

homeassistant/components/schlage/strings.json

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
"name": "Keypad disabled"
3232
}
3333
},
34+
"switch": {
35+
"beeper": {
36+
"name": "Keypress Beep"
37+
},
38+
"lock_and_leave": {
39+
"name": "1-Touch Locking"
40+
}
41+
},
3442
"select": {
3543
"auto_lock_time": {
3644
"name": "Auto-lock time",
@@ -45,19 +53,67 @@
4553
"300": "5 minutes"
4654
}
4755
}
48-
},
49-
"switch": {
50-
"beeper": {
51-
"name": "Keypress Beep"
52-
},
53-
"lock_and_leave": {
54-
"name": "1-Touch Locking"
55-
}
5656
}
5757
},
5858
"exceptions": {
5959
"schlage_refresh_failed": {
6060
"message": "Failed to refresh Schlage data"
61+
},
62+
"schlage_code_exists": {
63+
"message": "PIN code already exists"
64+
},
65+
"schlage_code_required": {
66+
"message": "PIN code is required"
67+
},
68+
"schlage_name_exists": {
69+
"message": "PIN name already exists"
70+
},
71+
"schlage_name_required": {
72+
"message": "PIN name is required"
73+
}
74+
},
75+
"services": {
76+
"add_code": {
77+
"name": "Add PIN code",
78+
"description": "Add a PIN code to a lock.",
79+
"fields": {
80+
"entity_id": {
81+
"name": "Lock",
82+
"description": "The lock to add the PIN code to."
83+
},
84+
"name": {
85+
"name": "PIN Name",
86+
"description": "Name for PIN code. Must be unique to lock."
87+
},
88+
"code": {
89+
"name": "PIN code",
90+
"description": "The PIN code to add. Must be unique to lock and be between 4 and 8 digits long."
91+
}
92+
}
93+
},
94+
"delete_code": {
95+
"name": "Delete PIN code",
96+
"description": "Delete a PIN code from a lock.",
97+
"fields": {
98+
"entity_id": {
99+
"name": "Lock",
100+
"description": "The lock to delete the PIN code from."
101+
},
102+
"name": {
103+
"name": "PIN Name",
104+
"description": "Name of PIN code to delete."
105+
}
106+
}
107+
},
108+
"get_codes": {
109+
"name": "Get PIN codes",
110+
"description": "Retrieve all PIN codes from the lock",
111+
"fields": {
112+
"entity_id": {
113+
"name": "Lock",
114+
"description": "The lock to retrieve PIN codes from."
115+
}
116+
}
61117
}
62118
}
63119
}

0 commit comments

Comments
 (0)