Skip to content

Commit 460eb9f

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 460eb9f

File tree

8 files changed

+529
-10
lines changed

8 files changed

+529
-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: 108 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,101 @@ 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+
async def _add_code(entity: SchlageLockEntity, calls: ServiceCall) -> None:
46+
"""Add a lock code."""
47+
name = calls.data.get("name")
48+
code = calls.data.get("code")
49+
50+
codes = entity._lock.access_codes # noqa: SLF001
51+
if codes:
52+
for existing_code in codes.values():
53+
if existing_code.name == name:
54+
raise ServiceValidationError(
55+
translation_domain=DOMAIN,
56+
translation_key="schlage_name_exists",
57+
)
58+
if existing_code.code == code:
59+
raise ServiceValidationError(
60+
translation_domain=DOMAIN,
61+
translation_key="schlage_code_exists",
62+
)
63+
if name and code:
64+
access_code = AccessCode(name=name, code=code)
65+
await hass.async_add_executor_job(entity._lock.add_access_code, access_code) # noqa: SLF001
66+
await coordinator.async_request_refresh()
67+
68+
async def _delete_code(entity: SchlageLockEntity, calls: ServiceCall) -> None:
69+
"""Delete a lock code."""
70+
name = calls.data.get("name")
71+
72+
# name is required by voluptions, the following is just to satisfy type
73+
# checker, it should never be None
74+
if name is None:
75+
raise ServiceValidationError(
76+
translation_domain=DOMAIN,
77+
translation_key="schlage_name_required",
78+
) # pragma: no cover
79+
80+
codes = entity._lock.access_codes # noqa: SLF001
81+
if not codes:
82+
return
83+
84+
code_id_to_delete = None
85+
for code_id, code_data in codes.items():
86+
if code_data.name.lower() == name.lower():
87+
code_id_to_delete = code_id
88+
break
89+
90+
if not code_id_to_delete:
91+
return
92+
93+
if entity._lock.access_codes: # noqa: SLF001
94+
await hass.async_add_executor_job(
95+
entity._lock.access_codes[code_id_to_delete].delete # noqa: SLF001
96+
)
97+
await coordinator.async_request_refresh()
98+
99+
async def _get_codes(
100+
entity: SchlageLockEntity, calls: ServiceCall
101+
) -> ServiceResponse:
102+
"""Get lock codes."""
103+
104+
if entity._lock.access_codes: # noqa: SLF001
105+
return {
106+
code: {
107+
"name": entity._lock.access_codes[code].name, # noqa: SLF001
108+
"code": entity._lock.access_codes[code].code, # noqa: SLF001
109+
}
110+
for code in entity._lock.access_codes # noqa: SLF001
111+
}
112+
return {}
113+
114+
platform = entity_platform.async_get_current_platform()
115+
platform.async_register_entity_service(
116+
name=SERVICE_ADD_CODE,
117+
schema={
118+
vol.Required("name"): cv.string,
119+
vol.Required("code"): cv.matches_regex(r"^\d{4,8}$"),
120+
},
121+
func=_add_code,
122+
)
123+
124+
platform.async_register_entity_service(
125+
name=SERVICE_DELETE_CODE,
126+
schema={
127+
vol.Required("name"): cv.string,
128+
},
129+
func=_delete_code,
130+
)
131+
132+
platform.async_register_entity_service(
133+
name=SERVICE_GET_CODES,
134+
schema=None,
135+
func=_get_codes,
136+
supports_response=SupportsResponse.ONLY,
137+
)
138+
32139

33140
class SchlageLockEntity(SchlageEntity, LockEntity):
34141
"""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: 61 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,64 @@
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_name_exists": {
66+
"message": "PIN name already exists"
67+
},
68+
"schlage_name_required": {
69+
"message": "PIN name is required"
70+
}
71+
},
72+
"services": {
73+
"add_code": {
74+
"name": "Add PIN code",
75+
"description": "Add a PIN code to a lock.",
76+
"fields": {
77+
"entity_id": {
78+
"name": "Lock",
79+
"description": "The lock to add the PIN code to."
80+
},
81+
"name": {
82+
"name": "PIN Name",
83+
"description": "Name for PIN code. Must be unique to lock."
84+
},
85+
"code": {
86+
"name": "PIN code",
87+
"description": "The PIN code to add. Must be unique to lock and be between 4 and 8 digits long."
88+
}
89+
}
90+
},
91+
"delete_code": {
92+
"name": "Delete PIN code",
93+
"description": "Delete a PIN code from a lock.",
94+
"fields": {
95+
"entity_id": {
96+
"name": "Lock",
97+
"description": "The lock to delete the PIN code from."
98+
},
99+
"name": {
100+
"name": "PIN Name",
101+
"description": "Name of PIN code to delete."
102+
}
103+
}
104+
},
105+
"get_codes": {
106+
"name": "Get PIN codes",
107+
"description": "Retrieve all PIN codes from the lock",
108+
"fields": {
109+
"entity_id": {
110+
"name": "Lock",
111+
"description": "The lock to retrieve PIN codes from."
112+
}
113+
}
61114
}
62115
}
63116
}

0 commit comments

Comments
 (0)