__init__.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. """
  2. Platform for Tuya WiFi-connected devices.
  3. Based on nikrolls/homeassistant-goldair-climate for Goldair branded devices.
  4. Based on sean6541/tuya-homeassistant for service call logic, and TarxBoy's
  5. investigation into Goldair's tuyapi statuses
  6. https://github.com/codetheweb/tuyapi/issues/31.
  7. """
  8. import logging
  9. from homeassistant.config_entries import ConfigEntry
  10. from homeassistant.const import CONF_HOST
  11. from homeassistant.core import HomeAssistant, callback
  12. from homeassistant.helpers.entity_registry import async_migrate_entries
  13. from .const import (
  14. CONF_DEVICE_ID,
  15. CONF_LOCAL_KEY,
  16. CONF_PROTOCOL_VERSION,
  17. CONF_TYPE,
  18. DOMAIN,
  19. )
  20. from .device import setup_device, async_delete_device
  21. from .helpers.device_config import get_config
  22. _LOGGER = logging.getLogger(__name__)
  23. async def async_migrate_entry(hass, entry: ConfigEntry):
  24. """Migrate to latest config format."""
  25. CONF_TYPE_AUTO = "auto"
  26. if entry.version == 1:
  27. # Removal of Auto detection.
  28. config = {**entry.data, **entry.options, "name": entry.title}
  29. if config[CONF_TYPE] == CONF_TYPE_AUTO:
  30. device = setup_device(hass, config)
  31. config[CONF_TYPE] = await device.async_inferred_type()
  32. if config[CONF_TYPE] is None:
  33. _LOGGER.error(
  34. f"Unable to determine type for device {config[CONF_DEVICE_ID]}."
  35. )
  36. return False
  37. entry.data = {
  38. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  39. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  40. CONF_HOST: config[CONF_HOST],
  41. }
  42. entry.version = 2
  43. if entry.version == 2:
  44. # CONF_TYPE is not configurable, move it from options to the main config.
  45. config = {**entry.data, **entry.options, "name": entry.title}
  46. opts = {**entry.options}
  47. # Ensure type has been migrated. Some users are reporting errors which
  48. # suggest it was removed completely. But that is probably due to
  49. # overwriting options without CONF_TYPE.
  50. if config.get(CONF_TYPE, CONF_TYPE_AUTO) == CONF_TYPE_AUTO:
  51. device = setup_device(hass, config)
  52. config[CONF_TYPE] = await device.async_inferred_type()
  53. if config[CONF_TYPE] is None:
  54. _LOGGER.error(
  55. f"Unable to determine type for device {config[CONF_DEVICE_ID]}."
  56. )
  57. return False
  58. entry.data = {
  59. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  60. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  61. CONF_HOST: config[CONF_HOST],
  62. CONF_TYPE: config[CONF_TYPE],
  63. }
  64. opts.pop(CONF_TYPE, None)
  65. entry.options = {**opts}
  66. entry.version = 3
  67. if entry.version == 3:
  68. # Migrate to filename based config_type, to avoid needing to
  69. # parse config files to find the right one.
  70. config = {**entry.data, **entry.options, "name": entry.title}
  71. config_type = get_config(config[CONF_TYPE]).config_type
  72. # Special case for kogan_switch. Consider also v2.
  73. if config_type == "smartplugv1":
  74. device = setup_device(hass, config)
  75. config_type = await device.async_inferred_type()
  76. if config_type != "smartplugv2":
  77. config_type = "smartplugv1"
  78. entry.data = {
  79. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  80. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  81. CONF_HOST: config[CONF_HOST],
  82. CONF_TYPE: config_type,
  83. }
  84. entry.version = 4
  85. if entry.version <= 5:
  86. # Migrate unique ids of existing entities to new format
  87. old_id = entry.unique_id
  88. conf_file = get_config(entry.data[CONF_TYPE])
  89. if conf_file is None:
  90. _LOGGER.error(f"Configuration file for {entry.data[CONF_TYPE]} not found.")
  91. return False
  92. @callback
  93. def update_unique_id(entity_entry):
  94. """Update the unique id of an entity entry."""
  95. e = conf_file.primary_entity
  96. if e.entity != entity_entry.platform:
  97. for e in conf_file.secondary_entities():
  98. if e.entity == entity_entry.platform:
  99. break
  100. if e.entity == entity_entry.platform:
  101. new_id = e.unique_id(old_id)
  102. if new_id != old_id:
  103. _LOGGER.info(
  104. f"Migrating {e.entity} unique_id {old_id} to {new_id}."
  105. )
  106. return {
  107. "new_unique_id": entity_entry.unique_id.replace(old_id, new_id)
  108. }
  109. await async_migrate_entries(hass, entry.entry_id, update_unique_id)
  110. entry.version = 6
  111. if entry.version <= 8:
  112. # Deprecated entities are removed, trim the config back to required
  113. # config only
  114. conf = {**entry.data, **entry.options}
  115. entry.data = {
  116. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  117. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  118. CONF_HOST: conf[CONF_HOST],
  119. CONF_TYPE: conf[CONF_TYPE],
  120. }
  121. entry.options = {}
  122. entry.version = 9
  123. if entry.version <= 9:
  124. # Added protocol_version, default to auto
  125. conf = {**entry.data, **entry.options}
  126. entry.data = {
  127. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  128. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  129. CONF_HOST: conf[CONF_HOST],
  130. CONF_TYPE: conf[CONF_TYPE],
  131. CONF_PROTOCOL_VERSION: "auto",
  132. }
  133. entry.options = {}
  134. entry.version = 10
  135. return True
  136. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
  137. _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
  138. config = {**entry.data, **entry.options, "name": entry.title}
  139. setup_device(hass, config)
  140. device_conf = get_config(entry.data[CONF_TYPE])
  141. if device_conf is None:
  142. _LOGGER.error(f"Configuration file for {config[CONF_TYPE]} not found.")
  143. return False
  144. entities = set()
  145. e = device_conf.primary_entity
  146. entities.add(e.entity)
  147. for e in device_conf.secondary_entities():
  148. entities.add(e.entity)
  149. await hass.config_entries.async_forward_entry_setups(entry, entities)
  150. entry.add_update_listener(async_update_entry)
  151. return True
  152. async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
  153. _LOGGER.debug(f"Unloading entry for device: {entry.data[CONF_DEVICE_ID]}")
  154. config = entry.data
  155. data = hass.data[DOMAIN][config[CONF_DEVICE_ID]]
  156. device_conf = get_config(config[CONF_TYPE])
  157. if device_conf is None:
  158. _LOGGER.error(f"Configuration file for {config[CONF_TYPE]} not found.")
  159. return False
  160. entities = {}
  161. e = device_conf.primary_entity
  162. if e.config_id in data:
  163. entities[e.entity] = True
  164. for e in device_conf.secondary_entities():
  165. if e.config_id in data:
  166. entities[e.entity] = True
  167. for e in entities:
  168. await hass.config_entries.async_forward_entry_unload(entry, e)
  169. await async_delete_device(hass, config)
  170. del hass.data[DOMAIN][config[CONF_DEVICE_ID]]
  171. return True
  172. async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry):
  173. _LOGGER.debug(f"Updating entry for device: {entry.data[CONF_DEVICE_ID]}")
  174. await async_unload_entry(hass, entry)
  175. await async_setup_entry(hass, entry)