4
0

__init__.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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 homeassistant.util import slugify
  14. from .const import (
  15. CONF_DEVICE_ID,
  16. CONF_LOCAL_KEY,
  17. CONF_POLL_ONLY,
  18. CONF_PROTOCOL_VERSION,
  19. CONF_TYPE,
  20. DOMAIN,
  21. )
  22. from .device import setup_device, async_delete_device
  23. from .helpers.device_config import get_config
  24. _LOGGER = logging.getLogger(__name__)
  25. async def async_migrate_entry(hass, entry: ConfigEntry):
  26. """Migrate to latest config format."""
  27. CONF_TYPE_AUTO = "auto"
  28. if entry.version == 1:
  29. # Removal of Auto detection.
  30. config = {**entry.data, **entry.options, "name": entry.title}
  31. if config[CONF_TYPE] == CONF_TYPE_AUTO:
  32. device = setup_device(hass, config)
  33. config[CONF_TYPE] = await device.async_inferred_type()
  34. if config[CONF_TYPE] is None:
  35. _LOGGER.error(
  36. "Unable to determine type for device %s",
  37. config[CONF_DEVICE_ID],
  38. )
  39. return False
  40. entry.data = {
  41. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  42. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  43. CONF_HOST: config[CONF_HOST],
  44. }
  45. entry.version = 2
  46. if entry.version == 2:
  47. # CONF_TYPE is not configurable, move from options to main config.
  48. config = {**entry.data, **entry.options, "name": entry.title}
  49. opts = {**entry.options}
  50. # Ensure type has been migrated. Some users are reporting errors which
  51. # suggest it was removed completely. But that is probably due to
  52. # overwriting options without CONF_TYPE.
  53. if config.get(CONF_TYPE, CONF_TYPE_AUTO) == CONF_TYPE_AUTO:
  54. device = setup_device(hass, config)
  55. config[CONF_TYPE] = await device.async_inferred_type()
  56. if config[CONF_TYPE] is None:
  57. _LOGGER.error(
  58. "Unable to determine type for device %s",
  59. config[CONF_DEVICE_ID],
  60. )
  61. return False
  62. entry.data = {
  63. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  64. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  65. CONF_HOST: config[CONF_HOST],
  66. CONF_TYPE: config[CONF_TYPE],
  67. }
  68. opts.pop(CONF_TYPE, None)
  69. entry.options = {**opts}
  70. entry.version = 3
  71. if entry.version == 3:
  72. # Migrate to filename based config_type, to avoid needing to
  73. # parse config files to find the right one.
  74. config = {**entry.data, **entry.options, "name": entry.title}
  75. config_type = get_config(config[CONF_TYPE]).config_type
  76. # Special case for kogan_switch. Consider also v2.
  77. if config_type == "smartplugv1":
  78. device = setup_device(hass, config)
  79. config_type = await device.async_inferred_type()
  80. if config_type != "smartplugv2":
  81. config_type = "smartplugv1"
  82. entry.data = {
  83. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  84. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  85. CONF_HOST: config[CONF_HOST],
  86. CONF_TYPE: config_type,
  87. }
  88. entry.version = 4
  89. if entry.version <= 5:
  90. # Migrate unique ids of existing entities to new format
  91. old_id = entry.unique_id
  92. conf_file = get_config(entry.data[CONF_TYPE])
  93. if conf_file is None:
  94. _LOGGER.error(
  95. "Configuration file for %s not found",
  96. entry.data[CONF_TYPE],
  97. )
  98. return False
  99. @callback
  100. def update_unique_id(entity_entry):
  101. """Update the unique id of an entity entry."""
  102. e = conf_file.primary_entity
  103. if e.entity != entity_entry.platform:
  104. for e in conf_file.secondary_entities():
  105. if e.entity == entity_entry.platform:
  106. break
  107. if e.entity == entity_entry.platform:
  108. new_id = e.unique_id(old_id)
  109. if new_id != old_id:
  110. _LOGGER.info(
  111. "Migrating %s unique_id %s to %s",
  112. e.entity,
  113. old_id,
  114. new_id,
  115. )
  116. return {
  117. "new_unique_id": entity_entry.unique_id.replace(
  118. old_id,
  119. new_id,
  120. )
  121. }
  122. await async_migrate_entries(hass, entry.entry_id, update_unique_id)
  123. entry.version = 6
  124. if entry.version <= 8:
  125. # Deprecated entities are removed, trim the config back to required
  126. # config only
  127. conf = {**entry.data, **entry.options}
  128. entry.data = {
  129. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  130. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  131. CONF_HOST: conf[CONF_HOST],
  132. CONF_TYPE: conf[CONF_TYPE],
  133. }
  134. entry.options = {}
  135. entry.version = 9
  136. if entry.version <= 9:
  137. # Added protocol_version, default to auto
  138. conf = {**entry.data, **entry.options}
  139. entry.data = {
  140. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  141. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  142. CONF_HOST: conf[CONF_HOST],
  143. CONF_TYPE: conf[CONF_TYPE],
  144. CONF_PROTOCOL_VERSION: "auto",
  145. }
  146. entry.options = {}
  147. entry.version = 10
  148. if entry.version <= 10:
  149. conf = entry.data | entry.options
  150. entry.data = {
  151. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  152. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  153. CONF_HOST: conf[CONF_HOST],
  154. CONF_TYPE: conf[CONF_TYPE],
  155. CONF_PROTOCOL_VERSION: "auto",
  156. CONF_POLL_ONLY: False,
  157. }
  158. entry.options = {}
  159. entry.version = 11
  160. if entry.version <= 11:
  161. # Migrate unique ids of existing entities to new format
  162. device_id = entry.unique_id
  163. conf_file = get_config(entry.data[CONF_TYPE])
  164. if conf_file is None:
  165. _LOGGER.error(
  166. "Configuration file for %s not found",
  167. entry.data[CONF_TYPE],
  168. )
  169. return False
  170. @callback
  171. def update_unique_id12(entity_entry):
  172. """Update the unique id of an entity entry."""
  173. old_id = entity_entry.unique_id
  174. platform = entity_entry.entity_id.split(".", 1)[0]
  175. e = conf_file.primary_entity
  176. if e.name:
  177. expect_id = f"{device_id}-{slugify(e.name)}"
  178. else:
  179. expect_id = device_id
  180. if e.entity != platform or expect_id != old_id:
  181. for e in conf_file.secondary_entities():
  182. if e.name:
  183. expect_id = f"{device_id}-{slugify(e.name)}"
  184. else:
  185. expect_id = device_id
  186. if e.entity == platform and expect_id == old_id:
  187. break
  188. if e.entity == platform and expect_id == old_id:
  189. new_id = e.unique_id(device_id)
  190. if new_id != old_id:
  191. _LOGGER.info(
  192. "Migrating %s unique_id %s to %s",
  193. e.entity,
  194. old_id,
  195. new_id,
  196. )
  197. return {
  198. "new_unique_id": entity_entry.unique_id.replace(
  199. old_id,
  200. new_id,
  201. )
  202. }
  203. await async_migrate_entries(hass, entry.entry_id, update_unique_id12)
  204. entry.version = 12
  205. return True
  206. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
  207. _LOGGER.debug(
  208. "Setting up entry for device: %s",
  209. entry.data[CONF_DEVICE_ID],
  210. )
  211. config = {**entry.data, **entry.options, "name": entry.title}
  212. setup_device(hass, config)
  213. device_conf = get_config(entry.data[CONF_TYPE])
  214. if device_conf is None:
  215. _LOGGER.error("Configuration file for %s not found", config[CONF_TYPE])
  216. return False
  217. entities = set()
  218. e = device_conf.primary_entity
  219. entities.add(e.entity)
  220. for e in device_conf.secondary_entities():
  221. entities.add(e.entity)
  222. await hass.config_entries.async_forward_entry_setups(entry, entities)
  223. entry.add_update_listener(async_update_entry)
  224. return True
  225. async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
  226. _LOGGER.debug("Unloading entry for device: %s", entry.data[CONF_DEVICE_ID])
  227. config = entry.data
  228. data = hass.data[DOMAIN][config[CONF_DEVICE_ID]]
  229. device_conf = get_config(config[CONF_TYPE])
  230. if device_conf is None:
  231. _LOGGER.error("Configuration file for %s not found", config[CONF_TYPE])
  232. return False
  233. entities = {}
  234. e = device_conf.primary_entity
  235. if e.config_id in data:
  236. entities[e.entity] = True
  237. for e in device_conf.secondary_entities():
  238. if e.config_id in data:
  239. entities[e.entity] = True
  240. for e in entities:
  241. await hass.config_entries.async_forward_entry_unload(entry, e)
  242. await async_delete_device(hass, config)
  243. del hass.data[DOMAIN][config[CONF_DEVICE_ID]]
  244. return True
  245. async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry):
  246. _LOGGER.debug("Updating entry for device: %s", entry.data[CONF_DEVICE_ID])
  247. await async_unload_entry(hass, entry)
  248. await async_setup_entry(hass, entry)