gopro_wired.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. # gopro.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
  2. # This copyright was auto-generated on Wed, Sep 1, 2021 5:05:47 PM
  3. """Implements top level interface to GoPro module."""
  4. from __future__ import annotations
  5. import asyncio
  6. import logging
  7. from typing import Any, Callable, Final
  8. import requests
  9. import open_gopro.features
  10. import open_gopro.network.wifi.mdns_scanner # Imported this way for pytest monkeypatching
  11. from open_gopro.api import (
  12. BleCommands,
  13. BleSettings,
  14. BleStatuses,
  15. HttpCommands,
  16. HttpSettings,
  17. WiredApi,
  18. )
  19. from open_gopro.domain.communicator_interface import (
  20. BaseGoProCommunicator,
  21. GoProWiredInterface,
  22. Message,
  23. MessageRules,
  24. )
  25. from open_gopro.domain.exceptions import (
  26. FailedToFindDevice,
  27. GoProNotOpened,
  28. InvalidOpenGoProVersion,
  29. )
  30. from open_gopro.gopro_base import GoProBase
  31. from open_gopro.models import GoProResp, constants
  32. from open_gopro.models.constants import StatusId
  33. from open_gopro.models.types import CameraState, UpdateCb, UpdateType
  34. logger = logging.getLogger(__name__)
  35. GET_TIMEOUT: Final = 5
  36. HTTP_GET_RETRIES: Final = 5
  37. class WiredGoPro(GoProBase[WiredApi], GoProWiredInterface):
  38. """The top-level USB interface to a Wired GoPro device.
  39. See the `Open GoPro SDK <https://gopro.github.io/OpenGoPro/python_sdk>`_ for complete documentation.
  40. If a serial number is not passed when instantiating, the mDNS server will be queried to find a connected
  41. GoPro.
  42. This class also handles:
  43. - ensuring camera is ready / not encoding before transferring data
  44. It can be used via context manager:
  45. >>> async with WiredGoPro() as gopro:
  46. >>> # Send some messages now
  47. Or without:
  48. >>> gopro = WiredGoPro()
  49. >>> await gopro.open()
  50. >>> # Send some messages now
  51. Args:
  52. serial (str | None): (at least) last 3 digits of GoPro Serial number. If not set, first GoPro
  53. discovered from mDNS will be used. Defaults to None
  54. **kwargs (Any): additional keyword arguments to pass to base class
  55. """
  56. _BASE_IP: Final[str] = "172.2{}.1{}{}.51"
  57. _BASE_ENDPOINT: Final[str] = "http://{ip}:8080/"
  58. _MDNS_SERVICE_NAME: Final[str] = "_gopro-web._tcp.local."
  59. def __init__(self, serial: str | None = None, **kwargs: Any) -> None:
  60. GoProBase.__init__(self, **kwargs)
  61. GoProWiredInterface.__init__(self)
  62. self._serial = serial
  63. # We currently only support version 2.0
  64. self._wired_api = WiredApi(self)
  65. self._open = False
  66. self._poll_period = kwargs.get("poll_period", 2)
  67. self._encoding = False
  68. self._busy = False
  69. self.streaming = open_gopro.features.StreamFeature()
  70. self._loop: asyncio.AbstractEventLoop
  71. async def open(self, timeout: int = 10, retries: int = 1) -> None:
  72. """Connect to the Wired GoPro Client and prepare it for communication
  73. Args:
  74. timeout (int): time (in seconds) before considering connection a failure. Defaults to 10.
  75. retries (int): number of connection retries. Defaults to 1.
  76. Raises:
  77. InvalidOpenGoProVersion: the GoPro camera does not support the correct Open GoPro API version
  78. FailedToFindDevice: could not auto-discover GoPro via mDNS
  79. """
  80. self._loop = asyncio.get_event_loop()
  81. if not self._serial:
  82. for retry in range(1, retries + 1):
  83. try:
  84. response = await open_gopro.network.wifi.mdns_scanner.find_first_ip_addr(
  85. WiredGoPro._MDNS_SERVICE_NAME, timeout
  86. )
  87. self._serial = response.name.split(".")[0]
  88. break
  89. except FailedToFindDevice as e:
  90. if retry == retries:
  91. raise e
  92. logger.warning(f"Failed to discover GoPro. Retrying #{retry}")
  93. await self.http_command.wired_usb_control(control=constants.Toggle.ENABLE)
  94. await self.http_command.set_third_party_client_info()
  95. # Find and configure API version
  96. if (version := (await self.http_command.get_open_gopro_api_version()).data) != self.version:
  97. raise InvalidOpenGoProVersion(version)
  98. logger.info(f"Using Open GoPro API version {version}")
  99. await self.streaming.open(self._loop, self)
  100. # Wait for initial ready state
  101. await self._wait_for_state({StatusId.ENCODING: False, StatusId.BUSY: False})
  102. self._open = True
  103. async def close(self) -> None:
  104. """Gracefully close the GoPro Client connection"""
  105. @property
  106. async def is_ready(self) -> bool:
  107. """Is gopro ready to receive commands
  108. Returns:
  109. bool: yes if ready, no otherwise
  110. """
  111. current_state = (await self.http_command.get_camera_state()).data
  112. self._encoding = bool(current_state[StatusId.ENCODING])
  113. self._busy = bool(current_state[StatusId.BUSY])
  114. return not (self._encoding or self._busy)
  115. @property
  116. def identifier(self) -> str:
  117. """Unique identifier for the connected GoPro Client
  118. Raises:
  119. GoProNotOpened: serial was not passed to instantiation and IP has not yet been discovered
  120. Returns:
  121. str: identifier
  122. """
  123. if self._serial:
  124. return self._serial
  125. raise GoProNotOpened("IP address has not yet been discovered")
  126. @property
  127. def version(self) -> str:
  128. """The Open GoPro API version of the GoPro Client
  129. Only Version 2.0 is currently supported.
  130. Returns:
  131. str: string version
  132. """
  133. return self._api.version
  134. @property
  135. def http_command(self) -> HttpCommands:
  136. """Used to access the USB commands
  137. Returns:
  138. HttpCommands: the commands
  139. """
  140. return self._api.http_command
  141. @property
  142. def http_setting(self) -> HttpSettings:
  143. """Used to access the USB settings
  144. Returns:
  145. HttpSettings: the settings
  146. """
  147. return self._api.http_setting
  148. @property
  149. def ble_command(self) -> BleCommands:
  150. """Used to call the BLE commands
  151. Raises:
  152. NotImplementedError: Not valid for WiredGoPro
  153. """
  154. raise NotImplementedError
  155. @property
  156. def ble_setting(self) -> BleSettings:
  157. """Used to access the BLE settings
  158. Raises:
  159. NotImplementedError: Not valid for WiredGoPro
  160. """
  161. raise NotImplementedError
  162. @property
  163. def ble_status(self) -> BleStatuses:
  164. """Used to access the BLE statuses
  165. Raises:
  166. NotImplementedError: Not valid for WiredGoPro
  167. """
  168. raise NotImplementedError
  169. @property
  170. def is_open(self) -> bool:
  171. """Is this client ready for communication?
  172. Returns:
  173. bool: True if yes, False if no
  174. """
  175. return self._open
  176. @property
  177. def is_ble_connected(self) -> bool:
  178. """Are we connected via BLE to the GoPro device?
  179. Returns:
  180. bool: True if yes, False if no
  181. """
  182. return False
  183. @property
  184. def is_http_connected(self) -> bool:
  185. """Are we connected via Wifi to the GoPro device?
  186. Returns:
  187. bool: True if yes, False if no
  188. """
  189. return self.is_open
  190. def _register_update(
  191. self, callback: UpdateCb, update: BaseGoProCommunicator._CompositeRegisterType | UpdateType
  192. ) -> None:
  193. raise NotImplementedError
  194. def _unregister_update(
  195. self, callback: UpdateCb, update: BaseGoProCommunicator._CompositeRegisterType | UpdateType | None = None
  196. ) -> None:
  197. raise NotImplementedError
  198. def register_update(self, callback: UpdateCb, update: UpdateType) -> None:
  199. """Register for callbacks when an update occurs
  200. Args:
  201. callback (UpdateCb): callback to be notified in
  202. update (UpdateType): update to register for
  203. Raises:
  204. NotImplementedError: not yet possible
  205. """
  206. raise NotImplementedError
  207. def unregister_update(self, callback: UpdateCb, update: UpdateType | None = None) -> None:
  208. """Unregister for asynchronous update(s)
  209. Args:
  210. callback (UpdateCb): callback to stop receiving update(s) on
  211. update (UpdateType | None): updates to unsubscribe for. Defaults to None (all
  212. updates that use this callback will be unsubscribed).
  213. Raises:
  214. NotImplementedError: not yet possible
  215. """
  216. raise NotImplementedError
  217. async def configure_cohn(self, timeout: int = 60) -> bool:
  218. """Prepare Camera on the Home Network
  219. Provision if not provisioned
  220. Then wait for COHN to be connected and ready
  221. Args:
  222. timeout (int): time in seconds to wait for COHN to be ready. Defaults to 60.
  223. Returns:
  224. bool: True if success, False otherwise
  225. Raises:
  226. NotImplementedError: not yet possible
  227. """
  228. raise NotImplementedError
  229. @property
  230. async def is_cohn_provisioned(self) -> bool:
  231. """Is COHN currently provisioned?
  232. Get the current COHN status from the camera
  233. Returns:
  234. bool: True if COHN is provisioned, False otherwise
  235. Raises:
  236. NotImplementedError: not yet possible
  237. """
  238. raise NotImplementedError
  239. ##########################################################################################################
  240. # End Public API
  241. ##########################################################################################################
  242. async def _enforce_message_rules(
  243. self, wrapped: Callable, message: Message, rules: MessageRules = MessageRules(), **kwargs: Any
  244. ) -> GoProResp:
  245. # Acquire ready lock unless we are initializing or this is a Set Shutter Off command
  246. if self._should_maintain_state and self.is_open and not rules.is_fastpass(**kwargs):
  247. # Wait for not encoding and not busy
  248. logger.trace("Waiting for camera to be ready to receive messages.") # type: ignore
  249. await self._wait_for_state({StatusId.ENCODING: False, StatusId.BUSY: False})
  250. logger.trace("Camera is ready to receive messages") # type: ignore
  251. response = await wrapped(message, **kwargs)
  252. else: # Either we're not maintaining state, we're not opened yet, or this is a fastpass message
  253. response = await wrapped(message, **kwargs)
  254. # Release the lock if we acquired it
  255. if self._should_maintain_state:
  256. if response.ok:
  257. # Is there any special handling required after receiving the response?
  258. if rules.should_wait_for_encoding_start(**kwargs):
  259. logger.trace("Waiting to receive encoding started.") # type: ignore
  260. # Wait for encoding to start
  261. await self._wait_for_state({StatusId.ENCODING: True})
  262. return response
  263. async def _wait_for_state(self, check: CameraState) -> None:
  264. """Poll the current state until a variable amount of states are all equal to desired values
  265. Args:
  266. check (CameraState): dict{setting / status: value} of settings / statuses and values to wait for
  267. """
  268. while True:
  269. state = (await self.http_command.get_camera_state()).data
  270. for key, value in check.items():
  271. if state.get(key) != value:
  272. logger.trace(f"Not ready ==> {key} != {value}") # type: ignore
  273. await asyncio.sleep(self._poll_period)
  274. break # Get new state and try again
  275. else:
  276. return # Everything matches. Exit
  277. @property
  278. def _api(self) -> WiredApi:
  279. return self._wired_api
  280. @property
  281. def ip_address(self) -> str: # noqa: D102
  282. if not self._serial:
  283. raise GoProNotOpened("Serial / IP has not yet been discovered")
  284. return WiredGoPro._BASE_IP.format(*self._serial[-3:])
  285. @property
  286. def _base_url(self) -> str:
  287. """Build the base endpoint for USB commands
  288. Raises:
  289. GoProNotOpened: The GoPro serial has not yet been set / discovered
  290. Returns:
  291. str: base endpoint with URL from serial number
  292. """
  293. if not self._serial:
  294. raise GoProNotOpened("Serial / IP has not yet been discovered")
  295. return WiredGoPro._BASE_ENDPOINT.format(ip=self.ip_address)
  296. @property
  297. def _requests_session(self) -> requests.Session:
  298. return requests.Session()