config_flow.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import logging
  2. import voluptuous as vol
  3. from homeassistant import config_entries
  4. from homeassistant.const import CONF_HOST, CONF_NAME
  5. from homeassistant.core import HomeAssistant, callback
  6. from . import DOMAIN
  7. from .device import TuyaLocalDevice
  8. from .const import (
  9. API_PROTOCOL_VERSIONS,
  10. CONF_DEVICE_ID,
  11. CONF_LOCAL_KEY,
  12. CONF_POLL_ONLY,
  13. CONF_PROTOCOL_VERSION,
  14. CONF_TYPE,
  15. )
  16. from .helpers.device_config import get_config
  17. from .helpers.log import log_json
  18. _LOGGER = logging.getLogger(__name__)
  19. class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
  20. VERSION = 12
  21. CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
  22. device = None
  23. data = {}
  24. async def async_step_user(self, user_input=None):
  25. errors = {}
  26. devid_opts = {}
  27. host_opts = {"default": "Auto"}
  28. key_opts = {}
  29. proto_opts = {"default": "auto"}
  30. polling_opts = {"default": False}
  31. if user_input is not None:
  32. await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
  33. self._abort_if_unique_id_configured()
  34. self.device = await async_test_connection(user_input, self.hass)
  35. if self.device:
  36. self.data = user_input
  37. return await self.async_step_select_type()
  38. else:
  39. errors["base"] = "connection"
  40. devid_opts["default"] = user_input[CONF_DEVICE_ID]
  41. host_opts["default"] = user_input[CONF_HOST]
  42. key_opts["default"] = user_input[CONF_LOCAL_KEY]
  43. proto_opts["default"] = user_input[CONF_PROTOCOL_VERSION]
  44. polling_opts["default"] = user_input[CONF_POLL_ONLY]
  45. return self.async_show_form(
  46. step_id="user",
  47. data_schema=vol.Schema(
  48. {
  49. vol.Required(CONF_DEVICE_ID, **devid_opts): str,
  50. vol.Required(CONF_HOST, **host_opts): str,
  51. vol.Required(CONF_LOCAL_KEY, **key_opts): str,
  52. vol.Required(
  53. CONF_PROTOCOL_VERSION,
  54. **proto_opts,
  55. ): vol.In(["auto"] + API_PROTOCOL_VERSIONS),
  56. vol.Required(CONF_POLL_ONLY, **polling_opts): bool,
  57. }
  58. ),
  59. errors=errors,
  60. )
  61. async def async_step_select_type(self, user_input=None):
  62. if user_input is not None:
  63. self.data[CONF_TYPE] = user_input[CONF_TYPE]
  64. return await self.async_step_choose_entities()
  65. types = []
  66. best_match = 0
  67. best_matching_type = None
  68. async for type in self.device.async_possible_types():
  69. types.append(type.config_type)
  70. q = type.match_quality(self.device._get_cached_state())
  71. if q > best_match:
  72. best_match = q
  73. best_matching_type = type.config_type
  74. if best_match < 100:
  75. best_match = int(best_match)
  76. dps = self.device._get_cached_state()
  77. _LOGGER.warning(
  78. "Device matches %s with quality of %d%%. DPS: %s",
  79. best_matching_type,
  80. best_match,
  81. log_json(dps),
  82. )
  83. _LOGGER.warning(
  84. "Report this to https://github.com/make-all/tuya-local/issues/"
  85. )
  86. if types:
  87. return self.async_show_form(
  88. step_id="select_type",
  89. data_schema=vol.Schema(
  90. {
  91. vol.Required(
  92. CONF_TYPE,
  93. default=best_matching_type,
  94. ): vol.In(types),
  95. }
  96. ),
  97. )
  98. else:
  99. return self.async_abort(reason="not_supported")
  100. async def async_step_choose_entities(self, user_input=None):
  101. if user_input is not None:
  102. title = user_input[CONF_NAME]
  103. del user_input[CONF_NAME]
  104. return self.async_create_entry(
  105. title=title, data={**self.data, **user_input}
  106. )
  107. config = get_config(self.data[CONF_TYPE])
  108. schema = {vol.Required(CONF_NAME, default=config.name): str}
  109. return self.async_show_form(
  110. step_id="choose_entities",
  111. data_schema=vol.Schema(schema),
  112. )
  113. @staticmethod
  114. @callback
  115. def async_get_options_flow(config_entry):
  116. return OptionsFlowHandler(config_entry)
  117. class OptionsFlowHandler(config_entries.OptionsFlow):
  118. def __init__(self, config_entry):
  119. """Initialize options flow."""
  120. self.config_entry = config_entry
  121. async def async_step_init(self, user_input=None):
  122. return await self.async_step_user(user_input)
  123. async def async_step_user(self, user_input=None):
  124. """Manage the options."""
  125. errors = {}
  126. config = {**self.config_entry.data, **self.config_entry.options}
  127. if user_input is not None:
  128. config = {**config, **user_input}
  129. device = await async_test_connection(config, self.hass)
  130. if device:
  131. return self.async_create_entry(title="", data=user_input)
  132. else:
  133. errors["base"] = "connection"
  134. schema = {
  135. vol.Required(
  136. CONF_LOCAL_KEY,
  137. default=config.get(CONF_LOCAL_KEY, ""),
  138. ): str,
  139. vol.Required(CONF_HOST, default=config.get(CONF_HOST, "")): str,
  140. vol.Required(
  141. CONF_PROTOCOL_VERSION,
  142. default=config.get(CONF_PROTOCOL_VERSION, "auto"),
  143. ): vol.In(["auto"] + API_PROTOCOL_VERSIONS),
  144. vol.Required(
  145. CONF_POLL_ONLY, default=config.get(CONF_POLL_ONLY, False)
  146. ): bool,
  147. }
  148. cfg = get_config(config[CONF_TYPE])
  149. if cfg is None:
  150. return self.async_abort(reason="not_supported")
  151. return self.async_show_form(
  152. step_id="user",
  153. data_schema=vol.Schema(schema),
  154. errors=errors,
  155. )
  156. async def async_test_connection(config: dict, hass: HomeAssistant):
  157. domain_data = hass.data.get(DOMAIN)
  158. existing = domain_data.get(config[CONF_DEVICE_ID]) if domain_data else None
  159. if existing:
  160. existing["device"].pause()
  161. try:
  162. device = TuyaLocalDevice(
  163. "Test",
  164. config[CONF_DEVICE_ID],
  165. config[CONF_HOST],
  166. config[CONF_LOCAL_KEY],
  167. config[CONF_PROTOCOL_VERSION],
  168. hass,
  169. True,
  170. )
  171. await device.async_refresh()
  172. retval = device if device.has_returned_state else None
  173. except Exception as e:
  174. _LOGGER.warning("Connection test failed with %s %s", type(e), e)
  175. retval = None
  176. if existing:
  177. existing["device"].resume()
  178. return retval