Skip to content
Draft
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
41 changes: 40 additions & 1 deletion homeassistant/components/schlage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

from pycognito.exceptions import WarrantException
import pyschlage
import voluptuous as vol

from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, SupportsResponse
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import config_validation as cv, service

from .const import DOMAIN, SERVICE_ADD_CODE, SERVICE_DELETE_CODE, SERVICE_GET_CODES
from .coordinator import SchlageConfigEntry, SchlageDataUpdateCoordinator

PLATFORMS: list[Platform] = [
Expand All @@ -33,8 +37,43 @@ async def async_setup_entry(hass: HomeAssistant, entry: SchlageConfigEntry) -> b
hass, entry, username, pyschlage.Schlage(auth)
)
entry.runtime_data = coordinator

await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_ADD_CODE,
entity_domain=LOCK_DOMAIN,
schema={
vol.Required("name"): cv.string,
vol.Required("code"): cv.matches_regex(r"^\d{4,8}$"),
},
func=SERVICE_ADD_CODE,
)

service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_DELETE_CODE,
entity_domain=LOCK_DOMAIN,
schema={
vol.Required("name"): cv.string,
},
func=SERVICE_DELETE_CODE,
)

service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_GET_CODES,
entity_domain=LOCK_DOMAIN,
schema=None,
func=SERVICE_GET_CODES,
supports_response=SupportsResponse.ONLY,
)

return True


Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/schlage/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
LOGGER = logging.getLogger(__package__)
MANUFACTURER = "Schlage"
UPDATE_INTERVAL = timedelta(seconds=30)

SERVICE_ADD_CODE = "add_code"
SERVICE_DELETE_CODE = "delete_code"
SERVICE_GET_CODES = "get_codes"
2 changes: 1 addition & 1 deletion homeassistant/components/schlage/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def __init__(
async def _async_update_data(self) -> SchlageData:
"""Fetch the latest data from the Schlage API."""
try:
locks = await self.hass.async_add_executor_job(self.api.locks)
locks = await self.hass.async_add_executor_job(self.api.locks, True)
except NotAuthorizedError as ex:
raise ConfigEntryAuthFailed from ex
except SchlageError as ex:
Expand Down
13 changes: 13 additions & 0 deletions homeassistant/components/schlage/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"services": {
"add_code": {
"service": "mdi:key-plus"
},
"delete_code": {
"service": "mdi:key-minus"
},
"get_codes": {
"service": "mdi:table-key"
}
}
}
87 changes: 86 additions & 1 deletion homeassistant/components/schlage/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

from typing import Any

from pyschlage.code import AccessCode

from homeassistant.components.lock import LockEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, ServiceResponse, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN
from .coordinator import LockData, SchlageConfigEntry, SchlageDataUpdateCoordinator
from .entity import SchlageEntity

Expand Down Expand Up @@ -64,3 +68,84 @@ async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
await self.hass.async_add_executor_job(self._lock.unlock)
await self.coordinator.async_request_refresh()

# Door code services
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Door code services

def _validate_code_name(
self, codes: dict[str, AccessCode] | None, name: str
) -> None:
"""Validate that the code name doesn't already exist."""
if codes and any(code.name.lower() == name.lower() for code in codes.values()):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="schlage_name_exists",
)

def _validate_code_value(
self, codes: dict[str, AccessCode] | None, code: str
) -> None:
"""Validate that the code value doesn't already exist."""
if codes and any(
existing_code.code == code for existing_code in codes.values()
):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="schlage_code_exists",
)

async def add_code(self, name: str, code: str) -> None:
"""Add a lock code."""

# code is required by voluptuous, the following is just to satisfy type
# checker, it should never be None
if code is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="schlage_code_required",
) # pragma: no cover
Comment on lines +98 to +104
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# code is required by voluptuous, the following is just to satisfy type
# checker, it should never be None
if code is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="schlage_code_required",
) # pragma: no cover
if TYPE_CHECKING:
assert code is not None

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that code is str, so this code should not be needed at all.


codes = self._lock.access_codes
self._validate_code_name(codes, name)
self._validate_code_value(codes, code)

access_code = AccessCode(name=name, code=code)
await self.coordinator.hass.async_add_executor_job(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await self.coordinator.hass.async_add_executor_job(
await self.hass.async_add_executor_job(

self._lock.add_access_code, access_code
)
await self.coordinator.async_request_refresh()

async def delete_code(self, name: str) -> None:
"""Delete a lock code."""
codes = self._lock.access_codes
if not codes:
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this raise ServiceValidationError?


code_id_to_delete = next(
(
code_id
for code_id, code_data in codes.items()
if code_data.name.lower() == name.lower()
),
None,
)

if not code_id_to_delete:
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this raise ServiceValidationError?


if self._lock.access_codes:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already check if codes is empty above, so we can remove the check here

await self.coordinator.hass.async_add_executor_job(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await self.coordinator.hass.async_add_executor_job(
await self.hass.async_add_executor_job(

self._lock.access_codes[code_id_to_delete].delete
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._lock.access_codes[code_id_to_delete].delete
codes[code_id_to_delete].delete

)
await self.coordinator.async_request_refresh()

async def get_codes(self) -> ServiceResponse:
"""Get lock codes."""

if self._lock.access_codes:
return {
code: {
"name": self._lock.access_codes[code].name,
"code": self._lock.access_codes[code].code,
}
for code in self._lock.access_codes
}
return {}
37 changes: 37 additions & 0 deletions homeassistant/components/schlage/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
get_codes:
target:
entity:
domain: lock
integration: schlage

add_code:
target:
entity:
domain: lock
integration: schlage
fields:
name:
required: true
example: "Example Person"
selector:
text:
multiline: false
code:
required: true
example: "1111"
selector:
text:
multiline: false

delete_code:
target:
entity:
domain: lock
integration: schlage
fields:
name:
required: true
example: "Example Person"
selector:
text:
multiline: false
42 changes: 42 additions & 0 deletions homeassistant/components/schlage/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,48 @@
"exceptions": {
"schlage_refresh_failed": {
"message": "Failed to refresh Schlage data"
},
"schlage_code_exists": {
"message": "A PIN code with this value already exists on the lock"
},
"schlage_code_required": {
"message": "PIN code is required"
},
"schlage_name_exists": {
"message": "A PIN code with this name already exists on the lock"
},
"schlage_name_required": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unused

"message": "PIN name is required"
}
},
"services": {
"add_code": {
"name": "Add PIN code",
"description": "Add a PIN code to a lock.",
"fields": {
"name": {
"name": "PIN Name",
"description": "Name for PIN code. Must be unique to lock."
},
"code": {
"name": "PIN code",
"description": "The PIN code to add. Must be unique to lock and be between 4 and 8 digits long."
}
}
},
"delete_code": {
"name": "Delete PIN code",
"description": "Delete a PIN code from a lock.",
"fields": {
"name": {
"name": "PIN Name",
"description": "Name of PIN code to delete."
}
}
},
"get_codes": {
"name": "Get PIN codes",
"description": "Retrieve all PIN codes from the lock"
}
}
}
Loading
Loading