"""COAP implementation.""" from __future__ import annotations from functools import wraps import json import logging import subprocess from time import time from typing import TYPE_CHECKING from ..command import Command from ..error import ClientError, RequestError, RequestTimeout, ServerError from ..gateway import Gateway _LOGGER = logging.getLogger(__name__) CLIENT_ERROR_PREFIX = "4." SERVER_ERROR_PREFIX = "5." class APIFactory: """APIFactory.""" def __init__(self, host, psk_id="pytradfri", psk=None, timeout=10): """Create object of class.""" self._host = host self._psk_id = psk_id self._psk = psk self._timeout = timeout # seconds @property def psk(self): """Return psk.""" return self._psk @psk.setter def psk(self, value): """Set psk.""" self._psk = value def _base_command(self, method: str) -> list[str]: """Return base command.""" return [ "coap-client", "-u", self._psk_id, "-k", self._psk, "-v", "0", "-m", method, ] def _execute(self, api_command, *, timeout=None): """Execute the command.""" if api_command.observe: self._observe(api_command) return None method = api_command.method path = api_command.path data = api_command.data parse_json = api_command.parse_json url = api_command.url(self._host) proc_timeout = self._timeout if timeout is not None: proc_timeout = timeout command = self._base_command(method) kwargs = { "stderr": subprocess.DEVNULL, "timeout": proc_timeout, "universal_newlines": True, } if data is not None: kwargs["input"] = json.dumps(data) command.append("-f") command.append("-") _LOGGER.debug("Executing %s %s %s: %s", self._host, method, path, data) else: _LOGGER.debug("Executing %s %s %s", self._host, method, path) command.append(url) try: return_value = subprocess.check_output(command, **kwargs) except subprocess.TimeoutExpired: raise RequestTimeout() from None except subprocess.CalledProcessError as exc: msg = f"Error executing request: {exc}" raise RequestError(msg) from None api_command.result = _process_output(return_value, parse_json) return api_command.result def request(self, api_commands, *, timeout=None): """Make a request. Timeout is in seconds.""" if not isinstance(api_commands, list): return self._execute(api_commands, timeout=timeout) command_results = [] for api_command in api_commands: result = self._execute(api_command, timeout=timeout) command_results.append(result) return command_results def _observe(self, api_command: Command) -> None: """Observe an endpoint.""" path = api_command.path duration = api_command.observe_duration if duration <= 0: raise ValueError("Observation duration has to be greater than 0.") url = api_command.url(self._host) err_callback = api_command.err_callback command = self._base_command("get") + [ "-s", str(duration), "-B", str(duration), url, ] try: proc = subprocess.Popen( # pylint: disable=consider-using-with command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=True, ) except subprocess.CalledProcessError as exc: msg = f"Error executing request: {exc}" raise RequestError(msg) from None output = "" open_obj = 0 start = time() def read_stdout() -> str: """Read from stdout.""" if TYPE_CHECKING: assert proc.stdout return proc.stdout.read(1) for data in iter(read_stdout, ""): if data == "\n": _LOGGER.debug( "Observing stopped for %s after %.1fs", path, time() - start ) if err_callback: err_callback(RequestError("Observing stopped.")) break if data == "{": open_obj += 1 elif data == "}": open_obj -= 1 output += data if open_obj == 0: api_command.result = _process_output(output) output = "" def generate_psk(self, security_key): """Generate and set a psk from the security key.""" if not self._psk: # Backup the real identity. existing_psk_id = self._psk_id # Set the default identity and security key for generation. self._psk_id = "Client_identity" self._psk = security_key # Ask the Gateway to generate the psk for the identity. self._psk = self.request(Gateway().generate_psk(existing_psk_id)) # Restore the real identity. self._psk_id = existing_psk_id return self._psk def _process_output(output, parse_json=True): """Process output.""" output = output.strip() _LOGGER.debug("Received: %s", output) if not output: return None if "decrypt_verify" in output: raise RequestError( "Please compile coap-client without debug output. See " "instructions at " "https://github.com/ggravlingen/pytradfri#installation" ) if output.startswith(CLIENT_ERROR_PREFIX): raise ClientError(output) if output.startswith(SERVER_ERROR_PREFIX): raise ServerError(output) if not parse_json: return output return json.loads(output) def retry_timeout(api, retries=3): """Retry API call when a timeout occurs.""" @wraps(api) def retry_api(*args, **kwargs): """Retrying API.""" for i in range(1, retries + 1): try: return api(*args, **kwargs) except RequestTimeout: if i == retries: raise return None return retry_api