discovery.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. """Discover switchbot devices."""
  2. from __future__ import annotations
  3. import asyncio
  4. import logging
  5. from collections.abc import Callable
  6. import bleak
  7. from bleak.backends.device import BLEDevice
  8. from bleak.backends.scanner import AdvertisementData
  9. from .adv_parser import parse_advertisement_data
  10. from .const import DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, DEFAULT_SCAN_TIMEOUT
  11. from .models import SwitchBotAdvertisement
  12. _LOGGER = logging.getLogger(__name__)
  13. CONNECT_LOCK = asyncio.Lock()
  14. class GetSwitchbotDevices:
  15. """Scan for all Switchbot devices and return by type."""
  16. def __init__(
  17. self,
  18. interface: int = 0,
  19. callback: Callable[[SwitchBotAdvertisement], None] | None = None,
  20. ) -> None:
  21. """Get switchbot devices class constructor."""
  22. self._interface = f"hci{interface}"
  23. self._adv_data: dict[str, SwitchBotAdvertisement] = {}
  24. self._callback = callback
  25. def detection_callback(
  26. self,
  27. device: BLEDevice,
  28. advertisement_data: AdvertisementData,
  29. ) -> None:
  30. """Callback for device detection."""
  31. discovery = parse_advertisement_data(device, advertisement_data)
  32. if discovery:
  33. self._adv_data[discovery.address] = discovery
  34. if self._callback is not None:
  35. try:
  36. self._callback(discovery)
  37. except Exception:
  38. _LOGGER.exception("Error in discovery callback")
  39. async def discover(
  40. self, retry: int = DEFAULT_RETRY_COUNT, scan_timeout: int = DEFAULT_SCAN_TIMEOUT
  41. ) -> dict:
  42. """Find switchbot devices and their advertisement data."""
  43. devices = None
  44. devices = bleak.BleakScanner(
  45. detection_callback=self.detection_callback,
  46. # TODO: Find new UUIDs to filter on. For example, see
  47. # https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/4ad138bb09f0fbbfa41b152ca327a78c1d0b6ba9/devicetypes/meter.md
  48. adapter=self._interface,
  49. )
  50. async with CONNECT_LOCK:
  51. await devices.start()
  52. await asyncio.sleep(scan_timeout)
  53. await devices.stop()
  54. if devices is None:
  55. if retry < 1:
  56. _LOGGER.error(
  57. "Scanning for Switchbot devices failed. Stop trying", exc_info=True
  58. )
  59. return self._adv_data
  60. _LOGGER.warning(
  61. "Error scanning for Switchbot devices. Retrying (remaining: %d)",
  62. retry,
  63. )
  64. await asyncio.sleep(DEFAULT_RETRY_TIMEOUT)
  65. return await self.discover(retry - 1, scan_timeout)
  66. return self._adv_data
  67. async def _get_devices_by_model(
  68. self,
  69. model: str,
  70. ) -> dict:
  71. """Get switchbot devices by type."""
  72. if not self._adv_data:
  73. await self.discover()
  74. return {
  75. address: adv
  76. for address, adv in self._adv_data.items()
  77. if adv.data.get("model") == model
  78. }
  79. async def get_blind_tilts(self) -> dict[str, SwitchBotAdvertisement]:
  80. """Return all WoBlindTilt/BlindTilts devices with services data."""
  81. regular_blinds = await self._get_devices_by_model("x")
  82. pairing_blinds = await self._get_devices_by_model("X")
  83. return {**regular_blinds, **pairing_blinds}
  84. async def get_curtains(self) -> dict[str, SwitchBotAdvertisement]:
  85. """Return all WoCurtain/Curtains devices with services data."""
  86. regular_curtains = await self._get_devices_by_model("c")
  87. pairing_curtains = await self._get_devices_by_model("C")
  88. regular_curtains3 = await self._get_devices_by_model("{")
  89. pairing_curtains3 = await self._get_devices_by_model("[")
  90. return {
  91. **regular_curtains,
  92. **pairing_curtains,
  93. **regular_curtains3,
  94. **pairing_curtains3,
  95. }
  96. async def get_bots(self) -> dict[str, SwitchBotAdvertisement]:
  97. """Return all WoHand/Bot devices with services data."""
  98. return await self._get_devices_by_model("H")
  99. async def get_tempsensors(self) -> dict[str, SwitchBotAdvertisement]:
  100. """Return all WoSensorTH/Temp sensor devices with services data."""
  101. base_meters = await self._get_devices_by_model("T")
  102. plus_meters = await self._get_devices_by_model("i")
  103. io_meters = await self._get_devices_by_model("w")
  104. hub2_meters = await self._get_devices_by_model("v")
  105. hubmini_matter_meters = await self._get_devices_by_model("%")
  106. return {
  107. **base_meters,
  108. **plus_meters,
  109. **io_meters,
  110. **hub2_meters,
  111. **hubmini_matter_meters,
  112. }
  113. async def get_contactsensors(self) -> dict[str, SwitchBotAdvertisement]:
  114. """Return all WoContact/Contact sensor devices with services data."""
  115. return await self._get_devices_by_model("d")
  116. async def get_leakdetectors(self) -> dict[str, SwitchBotAdvertisement]:
  117. """Return all Leak Detectors with services data."""
  118. return await self._get_devices_by_model("&")
  119. async def get_locks(self) -> dict[str, SwitchBotAdvertisement]:
  120. """Return all WoLock/Locks devices with services data."""
  121. locks = await self._get_devices_by_model("o")
  122. lock_pros = await self._get_devices_by_model("$")
  123. return {**locks, **lock_pros}
  124. async def get_keypads(self) -> dict[str, SwitchBotAdvertisement]:
  125. """Return all WoKeypad/Keypad devices with services data."""
  126. return await self._get_devices_by_model("y")
  127. async def get_humidifiers(self) -> dict[str, SwitchBotAdvertisement]:
  128. """Return all humidifier devices with services data."""
  129. humidifiers = await self._get_devices_by_model("e")
  130. evaporative_humidifiers = await self._get_devices_by_model("#")
  131. return {**humidifiers, **evaporative_humidifiers}
  132. async def get_device_data(
  133. self, address: str
  134. ) -> dict[str, SwitchBotAdvertisement] | None:
  135. """Return data for specific device."""
  136. if not self._adv_data:
  137. await self.discover()
  138. return {
  139. device: adv
  140. for device, adv in self._adv_data.items()
  141. # MacOS uses UUIDs instead of MAC addresses
  142. if adv.data.get("address") == address
  143. }