diff --git a/wyze_sdk/api/devices/locks.py b/wyze_sdk/api/devices/locks.py index 10f1d56..bc816b4 100644 --- a/wyze_sdk/api/devices/locks.py +++ b/wyze_sdk/api/devices/locks.py @@ -183,7 +183,7 @@ def _encrypt_access_code(self, access_code: str) -> str: secret = self._ford_client().get_crypt_secret()["secret"] return CBCEncryptor(self._ford_client().WYZE_FORD_IV_HEX).encrypt(MD5Hasher().hash(secret), access_code).hex() - def create_access_code(self, device_mac: str, access_code: str, name: Optional[str], permission: Optional[LockKeyPermission] = None, periodicity: Optional[LockKeyPeriodicity] = None, **kwargs) -> WyzeResponse: + def create_access_code(self, device_mac: str, access_code: str, name: Optional[str], permission: Optional[LockKeyPermission] = None, periodicity: Optional[LockKeyPeriodicity] = None, userid: Optional[str] = None, **kwargs) -> WyzeResponse: """Creates a guest access code on a lock. :param str device_mac: The device mac. e.g. ``ABCDEF1234567890`` @@ -208,7 +208,7 @@ def create_access_code(self, device_mac: str, access_code: str, name: Optional[s permission = LockKeyPermission(type=LockKeyPermissionType.ALWAYS) uuid = Lock.parse_uuid(mac=device_mac) - return self._ford_client().add_password(uuid=uuid, password=self._encrypt_access_code(access_code=access_code), name=name, permission=permission, periodicity=periodicity, userid=self._user_id) + return self._ford_client().add_password(uuid=uuid, password=self._encrypt_access_code(access_code=access_code), name=name, permission=permission, periodicity=periodicity, userid=userid if userid is not None else self._user_id) def delete_access_code(self, device_mac: str, access_code_id: int, **kwargs) -> WyzeResponse: """Deletes an access code from a lock. @@ -244,6 +244,20 @@ def update_access_code(self, device_mac: str, access_code_id: int, access_code: uuid = Lock.parse_uuid(mac=device_mac) return self._ford_client().update_password(uuid=uuid, password_id=str(access_code_id), password=self._encrypt_access_code(access_code=access_code), name=name, permission=permission, periodicity=periodicity) + def rename_access_code(self, device_mac: str, access_code_id: int, access_code: Optional[str] = None, name: Optional[str] = None, permission: LockKeyPermission = None, periodicity: Optional[LockKeyPeriodicity] = None, **kwargs) -> WyzeResponse: + """Renames an existing access code on a lock. + + :param str device_mac: The device mac. e.g. ``ABCDEF1234567890`` + :param int access_code_id: The id of the access code to reset. + :param str name: The new name for the guest access code. + + :rtype: WyzeResponse + + :raises WyzeRequestError: if the new access code is not valid + """ + uuid = Lock.parse_uuid(mac=device_mac) + return self._ford_client().set_nickname(uuid=uuid, password_id=str(access_code_id), nickname=name) + @property def gateways(self) -> LockGatewaysClient: """Returns a lock gateway client. diff --git a/wyze_sdk/models/devices/locks.py b/wyze_sdk/models/devices/locks.py index 3447da3..cc5ae0e 100644 --- a/wyze_sdk/models/devices/locks.py +++ b/wyze_sdk/models/devices/locks.py @@ -723,7 +723,26 @@ def __init__( self._is_online = self._extract_property(prop_def=LockProps.onoff_line(), others=others) show_unknown_key_warning(self, others) - +""" +Locks are funny objects... + +The access codes that are set on locks are referred to as "passwords" +throughout the lock infrastructure. When a new code/password is created, +it is assigned some non-obivous fields: + * description: this is not actually a description, but rather used to + shuttle around a "status" or "state" of the lock in case there was + some kind of error + * name: not to be confused with the nickname or "usename", this field + is IMMUTABLE and remains stuck to the "Guest name" provided when the + code/password was created + * username: also called a "nickname", this field is the descriptive + "guest code name" that can be changed + +All of this seems to be further complicated by different endpoints using +these field names differently. For example, the GET `.../lock/v1/auth` +endpoint puts the username value in a field called name. However, the +`.../lock/v1/pwd` calls to actually control the codes/passwords does not. +""" class Lock(LockableMixin, ContactMixin, VoltageMixin, Device): type = "Lock" diff --git a/wyze_sdk/service/base.py b/wyze_sdk/service/base.py index ac39382..8119e77 100644 --- a/wyze_sdk/service/base.py +++ b/wyze_sdk/service/base.py @@ -8,7 +8,7 @@ from abc import ABCMeta from contextlib import suppress from json import dumps -from typing import Dict, Optional, Union +from typing import Any, Dict, Optional, Union from urllib.parse import urljoin import requests @@ -108,7 +108,7 @@ def _do_request( self._logger.debug(f"Failed to send a request to server: {e}") raise e - def do_post(self, url: str, headers: dict, payload: dict, params: Optional[dict] = None) -> WyzeResponse: + def do_post(self, url: str, headers: dict, payload: dict, params: Optional[dict] = None, method: Optional[Any] = 'POST') -> WyzeResponse: with requests.Session() as client: if headers is not None: # add the request-specific headers @@ -118,7 +118,7 @@ def do_post(self, url: str, headers: dict, payload: dict, params: Optional[dict] # we have to use a prepared request because the requests module # doesn't allow us to specify the separators in our json dumping # and the server expects no extra whitespace - req = client.prepare_request(requests.Request('POST', url, json=payload, params=params)) + req = client.prepare_request(requests.Request(method, url, json=payload, params=params)) self._logger.debug('unmodified prepared request') self._logger.debug(req) @@ -192,7 +192,7 @@ def api_call( POST requests. """ has_json = json is not None - if has_json and http_verb != "POST": + if has_json and http_verb != "POST" and http_verb != "PATCH" and http_verb != "PUT": msg = "JSON data can only be submitted as POST requests. GET requests should use the 'params' argument." raise WyzeRequestError(msg) @@ -200,8 +200,8 @@ def api_call( headers = headers or {} headers.update(self.headers) - if http_verb == "POST": - return self.do_post(url=api_url, headers=headers, payload=json, params=params) + if http_verb == "POST" or http_verb == "PATCH" or http_verb == "PUT": + return self.do_post(url=api_url, headers=headers, payload=json, params=params, method=http_verb) elif http_verb == "GET": return self.do_get(url=api_url, headers=headers, payload=params) diff --git a/wyze_sdk/service/ford_service.py b/wyze_sdk/service/ford_service.py index cea9d76..bfd0251 100644 --- a/wyze_sdk/service/ford_service.py +++ b/wyze_sdk/service/ford_service.py @@ -102,7 +102,7 @@ def api_call( ) -> WyzeResponse: nonce = self.request_verifier.clock.nonce() - if http_verb == "POST": + if http_verb == "POST" or http_verb == "PATCH" or http_verb == "PUT": if json is None: json = {} # this must be done here so that it will be included in the signing @@ -114,7 +114,7 @@ def api_call( "timestamp": str(nonce), }) json.update({ - "sign": self.generate_dynamic_signature(path=api_method, method="post", body=super().get_sorted_params(sorted(json.items()))), + "sign": self.generate_dynamic_signature(path=api_method, method=http_verb.lower(), body=super().get_sorted_params(sorted(json.items()))), }) elif http_verb == "GET": if params is None: @@ -233,3 +233,11 @@ def delete_password(self, *, uuid: str, password_id: str, **kwargs) -> FordRespo 'passwordid': password_id, }) return self.api_call('/openapi/lock/v1/pwd/operations/delete', http_verb="POST", json=kwargs) + + def set_nickname(self, *, uuid: str, password_id: str, nickname: str, **kwargs) -> FordResponse: + kwargs.update({ + 'uuid': uuid, + 'passwordid': password_id, + 'nickname': nickname, + }) + return self.api_call('/openapi/lock/v1/pwd/nickname', http_verb="PUT", json=kwargs)