python
This commit is contained in:
297
custom_components/ttlock/services.py
Normal file
297
custom_components/ttlock/services.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""Services for ttlock integration."""
|
||||
|
||||
from datetime import time
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ENABLED, WEEKDAYS
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.dt import as_utc
|
||||
|
||||
from .const import (
|
||||
CONF_ALL_DAY,
|
||||
CONF_AUTO_UNLOCK,
|
||||
CONF_END_TIME,
|
||||
CONF_SECONDS,
|
||||
CONF_START_TIME,
|
||||
CONF_WEEK_DAYS,
|
||||
DOMAIN,
|
||||
SVC_CLEANUP_PASSCODES,
|
||||
SVC_CONFIG_AUTOLOCK,
|
||||
SVC_CONFIG_PASSAGE_MODE,
|
||||
SVC_CREATE_PASSCODE,
|
||||
SVC_LIST_PASSCODES,
|
||||
SVC_LIST_RECORDS,
|
||||
)
|
||||
from .coordinator import LockUpdateCoordinator, coordinator_for
|
||||
from .models import AddPasscodeConfig, OnOff, PassageModeConfig
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_LIST_RECORDS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("start_date"): cv.datetime,
|
||||
vol.Optional("end_date"): cv.datetime,
|
||||
vol.Optional("page_size", default=50): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=200)
|
||||
),
|
||||
vol.Optional("page_no", default=1): vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Services:
|
||||
"""Wraps service handlers."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the service singleton."""
|
||||
self.hass = hass
|
||||
|
||||
def register(self) -> None:
|
||||
"""Register services for ttlock integration."""
|
||||
|
||||
self.hass.services.register(
|
||||
DOMAIN,
|
||||
SVC_CONFIG_PASSAGE_MODE,
|
||||
self.handle_configure_passage_mode,
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_AUTO_UNLOCK, default=False): cv.boolean,
|
||||
vol.Optional(CONF_ALL_DAY, default=False): cv.boolean,
|
||||
vol.Optional(CONF_START_TIME, default=time()): cv.time,
|
||||
vol.Optional(CONF_END_TIME, default=time()): cv.time,
|
||||
vol.Optional(CONF_WEEK_DAYS, default=WEEKDAYS): cv.weekdays,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.hass.services.register(
|
||||
DOMAIN,
|
||||
SVC_CREATE_PASSCODE,
|
||||
self.handle_create_passcode,
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required("passcode_name"): cv.string,
|
||||
vol.Required("passcode"): cv.string,
|
||||
vol.Required("start_time", default=time()): cv.datetime,
|
||||
vol.Required("end_time", default=time()): cv.datetime,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.hass.services.register(
|
||||
DOMAIN,
|
||||
SVC_CLEANUP_PASSCODES,
|
||||
self.handle_cleanup_passcodes,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
self.hass.services.register(
|
||||
DOMAIN,
|
||||
SVC_LIST_PASSCODES,
|
||||
self.handle_list_passcodes,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
self.hass.services.register(
|
||||
DOMAIN,
|
||||
SVC_LIST_RECORDS,
|
||||
self.handle_list_records,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional("start_date"): cv.datetime,
|
||||
vol.Optional("end_date"): cv.datetime,
|
||||
vol.Optional("page_size", default=50): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=200)
|
||||
),
|
||||
vol.Optional("page_no", default=1): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
self.hass.services.register(
|
||||
DOMAIN,
|
||||
SVC_CONFIG_AUTOLOCK,
|
||||
self.handle_configure_autolock,
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(CONF_ENABLED): cv.boolean,
|
||||
vol.Optional(CONF_SECONDS): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=60)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def _get_coordinators(self, call: ServiceCall) -> dict[str, LockUpdateCoordinator]:
|
||||
"""Get coordinators for the requested entities.
|
||||
|
||||
Returns a dictionary mapping entity_ids to their coordinators.
|
||||
Filters out any entity_ids that don't have associated coordinators.
|
||||
|
||||
Args:
|
||||
call: The service call containing entity_ids
|
||||
|
||||
Returns:
|
||||
A dictionary where keys are entity_ids and values are their corresponding
|
||||
LockUpdateCoordinators. Only includes entries where a coordinator exists.
|
||||
"""
|
||||
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
||||
if entity_ids:
|
||||
return {
|
||||
entity_id: coordinator
|
||||
for entity_id in entity_ids
|
||||
if (coordinator := coordinator_for(self.hass, entity_id))
|
||||
}
|
||||
return {}
|
||||
|
||||
async def handle_list_passcodes(self, call: ServiceCall) -> ServiceResponse:
|
||||
"""List all passcodes for the selected locks."""
|
||||
passcodes = {}
|
||||
|
||||
for entity_id, coordinator in self._get_coordinators(call).items():
|
||||
codes = await coordinator.api.list_passcodes(coordinator.lock_id)
|
||||
passcodes[entity_id] = [
|
||||
{
|
||||
"name": code.name,
|
||||
"passcode": code.passcode,
|
||||
"type": code.type.name,
|
||||
"start_date": code.start_date,
|
||||
"end_date": code.end_date,
|
||||
"expired": code.expired,
|
||||
}
|
||||
for code in codes
|
||||
]
|
||||
|
||||
return {"passcodes": passcodes}
|
||||
|
||||
async def handle_configure_passage_mode(self, call: ServiceCall):
|
||||
"""Enable passage mode for the given entities."""
|
||||
start_time = call.data.get(CONF_START_TIME)
|
||||
end_time = call.data.get(CONF_END_TIME)
|
||||
days = [WEEKDAYS.index(day) + 1 for day in call.data.get(CONF_WEEK_DAYS)]
|
||||
|
||||
config = PassageModeConfig(
|
||||
passageMode=OnOff.on if call.data.get(CONF_ENABLED) else OnOff.off,
|
||||
autoUnlock=OnOff.on if call.data.get(CONF_AUTO_UNLOCK) else OnOff.off,
|
||||
isAllDay=OnOff.on if call.data.get(CONF_ALL_DAY) else OnOff.off,
|
||||
startDate=start_time.hour * 60 + start_time.minute,
|
||||
endDate=end_time.hour * 60 + end_time.minute,
|
||||
weekDays=days,
|
||||
)
|
||||
|
||||
for _entity_id, coordinator in self._get_coordinators(call).items():
|
||||
if await coordinator.api.set_passage_mode(coordinator.lock_id, config):
|
||||
coordinator.data.passage_mode_config = config
|
||||
coordinator.async_update_listeners()
|
||||
|
||||
async def handle_create_passcode(self, call: ServiceCall):
|
||||
"""Create a new passcode for the given entities."""
|
||||
|
||||
start_time_val = call.data.get("start_time")
|
||||
start_time_utc = as_utc(start_time_val)
|
||||
start_time = int(start_time_utc.timestamp() * 1000)
|
||||
|
||||
end_time_val = call.data.get("end_time")
|
||||
end_time_utc = as_utc(end_time_val)
|
||||
end_time = int(end_time_utc.timestamp() * 1000)
|
||||
|
||||
config = AddPasscodeConfig(
|
||||
passcode=call.data.get("passcode"),
|
||||
passcodeName=call.data.get("passcode_name"),
|
||||
startDate=start_time,
|
||||
endDate=end_time,
|
||||
)
|
||||
|
||||
for _entity_id, coordinator in self._get_coordinators(call).items():
|
||||
await coordinator.api.add_passcode(coordinator.lock_id, config)
|
||||
|
||||
async def handle_cleanup_passcodes(self, call: ServiceCall) -> ServiceResponse:
|
||||
"""Clean up expired passcodes for the given entities."""
|
||||
removed = {}
|
||||
|
||||
for entity_id, coordinator in self._get_coordinators(call).items():
|
||||
removed_for_lock = []
|
||||
codes = await coordinator.api.list_passcodes(coordinator.lock_id)
|
||||
for code in codes:
|
||||
if code.expired:
|
||||
if await coordinator.api.delete_passcode(
|
||||
coordinator.lock_id, code.id
|
||||
):
|
||||
removed_for_lock.append(code.name)
|
||||
if removed_for_lock:
|
||||
removed[entity_id] = removed_for_lock
|
||||
|
||||
return {"removed": removed}
|
||||
|
||||
async def handle_configure_autolock(self, call: ServiceCall):
|
||||
"""Set the autolock seconds."""
|
||||
|
||||
if call.data.get(CONF_ENABLED):
|
||||
seconds = call.data.get(CONF_SECONDS) or 10
|
||||
else:
|
||||
seconds = 0
|
||||
|
||||
for coordinator in self._get_coordinators(call).values():
|
||||
if await coordinator.api.set_auto_lock(coordinator.lock_id, seconds):
|
||||
coordinator.data.auto_lock_seconds = seconds
|
||||
coordinator.async_update_listeners()
|
||||
|
||||
async def handle_list_records(self, call: ServiceCall) -> ServiceResponse:
|
||||
"""List records for the selected locks."""
|
||||
records = {}
|
||||
params = {}
|
||||
|
||||
# Convert datetime parameters to millisecond timestamps if provided
|
||||
if start_date := call.data.get("start_date"):
|
||||
params["start_date"] = int(as_utc(start_date).timestamp() * 1000)
|
||||
if end_date := call.data.get("end_date"):
|
||||
params["end_date"] = int(as_utc(end_date).timestamp() * 1000)
|
||||
|
||||
# Get pagination values from call data with defaults
|
||||
params["page_no"] = call.data.get("page_no", 1)
|
||||
params["page_size"] = min(call.data.get("page_size", 50), 200)
|
||||
|
||||
for entity_id, coordinator in self._get_coordinators(call).items():
|
||||
lock_records = await coordinator.api.get_lock_records(
|
||||
coordinator.lock_id, **params
|
||||
)
|
||||
records[entity_id] = [
|
||||
{
|
||||
"id": record.id,
|
||||
"lock_id": record.lock_id,
|
||||
"record_type": record.record_type.name,
|
||||
"success": record.success,
|
||||
"username": record.username,
|
||||
"keyboard_pwd": record.keyboard_pwd,
|
||||
"lock_date": record.lock_date,
|
||||
"server_date": record.server_date,
|
||||
}
|
||||
for record in lock_records
|
||||
]
|
||||
|
||||
return {"records": records}
|
||||
Reference in New Issue
Block a user