4
0

__init__.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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_LIGHT,
  16. CONF_LOCAL_KEY,
  17. CONF_LOCK,
  18. CONF_TYPE,
  19. DOMAIN,
  20. )
  21. from .device import setup_device, delete_device
  22. from .helpers.device_config import get_config
  23. _LOGGER = logging.getLogger(__name__)
  24. async def async_migrate_entry(hass, entry: ConfigEntry):
  25. """Migrate to latest config format."""
  26. CONF_TYPE_AUTO = "auto"
  27. CONF_DISPLAY_LIGHT = "display_light"
  28. CONF_CHILD_LOCK = "child_lock"
  29. if entry.version == 1:
  30. # Removal of Auto detection.
  31. config = {**entry.data, **entry.options, "name": entry.title}
  32. opts = {**entry.options}
  33. if config[CONF_TYPE] == CONF_TYPE_AUTO:
  34. device = setup_device(hass, config)
  35. config[CONF_TYPE] = await device.async_inferred_type()
  36. if config[CONF_TYPE] is None:
  37. _LOGGER.error(
  38. f"Unable to determine type for device {config[CONF_DEVICE_ID]}."
  39. )
  40. return False
  41. entry.data = {
  42. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  43. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  44. CONF_HOST: config[CONF_HOST],
  45. }
  46. if CONF_CHILD_LOCK in config:
  47. opts.pop(CONF_CHILD_LOCK, False)
  48. opts[CONF_LOCK] = config[CONF_CHILD_LOCK]
  49. if CONF_DISPLAY_LIGHT in config:
  50. opts.pop(CONF_DISPLAY_LIGHT, False)
  51. opts[CONF_LIGHT] = config[CONF_DISPLAY_LIGHT]
  52. entry.options = {**opts}
  53. entry.version = 2
  54. if entry.version == 2:
  55. # CONF_TYPE is not configurable, move it from options to the main config.
  56. config = {**entry.data, **entry.options, "name": entry.title}
  57. opts = {**entry.options}
  58. # Ensure type has been migrated. Some users are reporting errors which
  59. # suggest it was removed completely. But that is probably due to
  60. # overwriting options without CONF_TYPE.
  61. if config.get(CONF_TYPE, CONF_TYPE_AUTO) == CONF_TYPE_AUTO:
  62. device = setup_device(hass, config)
  63. config[CONF_TYPE] = await device.async_inferred_type()
  64. if config[CONF_TYPE] is None:
  65. _LOGGER.error(
  66. f"Unable to determine type for device {config[CONF_DEVICE_ID]}."
  67. )
  68. return False
  69. entry.data = {
  70. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  71. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  72. CONF_HOST: config[CONF_HOST],
  73. CONF_TYPE: config[CONF_TYPE],
  74. }
  75. opts.pop(CONF_TYPE, None)
  76. entry.options = {**opts}
  77. entry.version = 3
  78. if entry.version == 3:
  79. # Migrate to filename based config_type, to avoid needing to
  80. # parse config files to find the right one.
  81. config = {**entry.data, **entry.options, "name": entry.title}
  82. config_type = get_config(config[CONF_TYPE]).config_type
  83. # Special case for kogan_switch. Consider also v2.
  84. if config_type == "smartplugv1":
  85. device = setup_device(hass, config)
  86. config_type = await device.async_inferred_type()
  87. if config_type != "smartplugv2":
  88. config_type = "smartplugv1"
  89. entry.data = {
  90. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  91. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  92. CONF_HOST: config[CONF_HOST],
  93. CONF_TYPE: config_type,
  94. }
  95. entry.version = 4
  96. if entry.version == 4:
  97. # Migrate indexes to entity id rather than type, to allow for multiple
  98. # entities of the same type for a device.
  99. config = {**entry.data, **entry.options, "name": entry.title}
  100. devcfg = get_config(config[CONF_TYPE])
  101. opts = {**entry.options}
  102. newopts = {**opts}
  103. entry.data = {
  104. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  105. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  106. CONF_HOST: config[CONF_HOST],
  107. CONF_TYPE: config[CONF_TYPE],
  108. }
  109. e = devcfg.primary_entity
  110. if e.config_id != e.entity:
  111. newopts.pop(e.entity, None)
  112. newopts[e.config_id] = opts.get(e.entity, False)
  113. for e in devcfg.secondary_entities():
  114. if e.config_id != e.entity:
  115. newopts.pop(e.entity, None)
  116. newopts[e.config_id] = opts.get(e.entity, False)
  117. entry.options = {**newopts}
  118. entry.version = 5
  119. if entry.version == 5:
  120. # Migrate unique ids of existing entities to new format
  121. old_id = entry.unique_id
  122. conf_file = get_config(entry.data[CONF_TYPE])
  123. if conf_file is None:
  124. _LOGGER.error(f"Configuration file for {entry.data[CONF_TYPE]} not found.")
  125. return False
  126. @callback
  127. def update_unique_id(entity_entry):
  128. """Update the unique id of an entity entry."""
  129. e = conf_file.primary_entity
  130. if e.entity != entity_entry.platform:
  131. for e in conf_file.secondary_entities():
  132. if e.entity == entity_entry.platform:
  133. break
  134. if e.entity == entity_entry.platform:
  135. new_id = e.unique_id(old_id)
  136. if new_id != old_id:
  137. _LOGGER.info(
  138. f"Migrating {e.entity} unique_id {old_id} to {new_id}."
  139. )
  140. return {
  141. "new_unique_id": entity_entry.unique_id.replace(old_id, new_id)
  142. }
  143. await async_migrate_entries(hass, entry.entry_id, update_unique_id)
  144. entry.version = 6
  145. if entry.version == 6:
  146. # Migrate some entity names to make them consistent for translations
  147. opts = {**entry.data, **entry.options}
  148. newopts = {**entry.options}
  149. master = opts.get("switch_main_switch")
  150. if master is not None:
  151. newopts.pop("switch_main_switch", None)
  152. newopts["switch_master"] = master
  153. outlet1 = opts.get("switch_left_outlet")
  154. outlet2 = opts.get("switch_right_outlet")
  155. outlet1 = opts.get("switch_wall_switch_1") if outlet1 is None else outlet1
  156. outlet2 = opts.get("switch_wall_switch_2") if outlet2 is None else outlet2
  157. if outlet1 is not None:
  158. newopts.pop("switch_left_outlet", None)
  159. newopts.pop("switch_wall_switch_1", None)
  160. newopts["switch_outlet_1"] = outlet1
  161. if outlet2 is not None:
  162. newopts.pop("switch_right_outlet", None)
  163. newopts.pop("switch_wall_switch_2", None)
  164. newopts["switch_outlet_2"] = outlet2
  165. entry.options = {**newopts}
  166. entry.version = 7
  167. return True
  168. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
  169. _LOGGER.debug(f"Setting up entry for device: {entry.data[CONF_DEVICE_ID]}")
  170. config = {**entry.data, **entry.options, "name": entry.title}
  171. setup_device(hass, config)
  172. device_conf = get_config(entry.data[CONF_TYPE])
  173. if device_conf is None:
  174. _LOGGER.error(f"Configuration file for {config[CONF_TYPE]} not found.")
  175. return False
  176. entities = {}
  177. e = device_conf.primary_entity
  178. if config.get(e.config_id, False):
  179. entities[e.entity] = True
  180. for e in device_conf.secondary_entities():
  181. if config.get(e.config_id, False):
  182. entities[e.entity] = True
  183. for e in entities:
  184. hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, e))
  185. entry.add_update_listener(async_update_entry)
  186. return True
  187. async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
  188. _LOGGER.debug(f"Unloading entry for device: {entry.data[CONF_DEVICE_ID]}")
  189. config = entry.data
  190. data = hass.data[DOMAIN][config[CONF_DEVICE_ID]]
  191. device_conf = get_config(config[CONF_TYPE])
  192. if device_conf is None:
  193. _LOGGER.error(f"Configuration file for {config[CONF_TYPE]} not found.")
  194. return False
  195. entities = {}
  196. e = device_conf.primary_entity
  197. if e.config_id in data:
  198. entities[e.entity] = True
  199. for e in device_conf.secondary_entities():
  200. if e.config_id in data:
  201. entities[e.entity] = True
  202. for e in entities:
  203. await hass.config_entries.async_forward_entry_unload(entry, e)
  204. delete_device(hass, config)
  205. del hass.data[DOMAIN][config[CONF_DEVICE_ID]]
  206. return True
  207. async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry):
  208. _LOGGER.debug(f"Updating entry for device: {entry.data[CONF_DEVICE_ID]}")
  209. await async_unload_entry(hass, entry)
  210. await async_setup_entry(hass, entry)