__init__.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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 async_delete_device, get_device_id, setup_device
  23. from .helpers.device_config import get_config
  24. _LOGGER = logging.getLogger(__name__)
  25. NOT_FOUND = "Configuration file for %s not found"
  26. async def async_migrate_entry(hass, entry: ConfigEntry):
  27. """Migrate to latest config format."""
  28. CONF_TYPE_AUTO = "auto"
  29. if entry.version == 1:
  30. # Removal of Auto detection.
  31. config = {**entry.data, **entry.options, "name": entry.title}
  32. if config[CONF_TYPE] == CONF_TYPE_AUTO:
  33. device = setup_device(hass, config)
  34. config[CONF_TYPE] = await device.async_inferred_type()
  35. if config[CONF_TYPE] is None:
  36. _LOGGER.error(
  37. "Unable to determine type for device %s",
  38. 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. entry.version = 2
  47. if entry.version == 2:
  48. # CONF_TYPE is not configurable, move from options to main config.
  49. config = {**entry.data, **entry.options, "name": entry.title}
  50. opts = {**entry.options}
  51. # Ensure type has been migrated. Some users are reporting errors which
  52. # suggest it was removed completely. But that is probably due to
  53. # overwriting options without CONF_TYPE.
  54. if config.get(CONF_TYPE, CONF_TYPE_AUTO) == CONF_TYPE_AUTO:
  55. device = setup_device(hass, config)
  56. config[CONF_TYPE] = await device.async_inferred_type()
  57. if config[CONF_TYPE] is None:
  58. _LOGGER.error(
  59. "Unable to determine type for device %s",
  60. config[CONF_DEVICE_ID],
  61. )
  62. return False
  63. entry.data = {
  64. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  65. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  66. CONF_HOST: config[CONF_HOST],
  67. CONF_TYPE: config[CONF_TYPE],
  68. }
  69. opts.pop(CONF_TYPE, None)
  70. entry.options = {**opts}
  71. entry.version = 3
  72. if entry.version == 3:
  73. # Migrate to filename based config_type, to avoid needing to
  74. # parse config files to find the right one.
  75. config = {**entry.data, **entry.options, "name": entry.title}
  76. config_type = get_config(config[CONF_TYPE]).config_type
  77. # Special case for kogan_switch. Consider also v2.
  78. if config_type == "smartplugv1":
  79. device = setup_device(hass, config)
  80. config_type = await device.async_inferred_type()
  81. if config_type != "smartplugv2":
  82. config_type = "smartplugv1"
  83. entry.data = {
  84. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  85. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  86. CONF_HOST: config[CONF_HOST],
  87. CONF_TYPE: config_type,
  88. }
  89. entry.version = 4
  90. if entry.version <= 5:
  91. # Migrate unique ids of existing entities to new format
  92. old_id = entry.unique_id
  93. conf_file = get_config(entry.data[CONF_TYPE])
  94. if conf_file is None:
  95. _LOGGER.error(NOT_FOUND, entry.data[CONF_TYPE])
  96. return False
  97. @callback
  98. def update_unique_id(entity_entry):
  99. """Update the unique id of an entity entry."""
  100. e = conf_file.primary_entity
  101. if e.entity != entity_entry.platform:
  102. for e in conf_file.secondary_entities():
  103. if e.entity == entity_entry.platform:
  104. break
  105. if e.entity == entity_entry.platform:
  106. new_id = e.unique_id(old_id)
  107. if new_id != old_id:
  108. _LOGGER.info(
  109. "Migrating %s unique_id %s to %s",
  110. e.entity,
  111. old_id,
  112. new_id,
  113. )
  114. return {
  115. "new_unique_id": entity_entry.unique_id.replace(
  116. old_id,
  117. new_id,
  118. )
  119. }
  120. await async_migrate_entries(hass, entry.entry_id, update_unique_id)
  121. entry.version = 6
  122. if entry.version <= 8:
  123. # Deprecated entities are removed, trim the config back to required
  124. # config only
  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. }
  132. entry.options = {}
  133. entry.version = 9
  134. if entry.version <= 9:
  135. # Added protocol_version, default to auto
  136. conf = {**entry.data, **entry.options}
  137. entry.data = {
  138. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  139. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  140. CONF_HOST: conf[CONF_HOST],
  141. CONF_TYPE: conf[CONF_TYPE],
  142. CONF_PROTOCOL_VERSION: "auto",
  143. }
  144. entry.options = {}
  145. entry.version = 10
  146. if entry.version <= 10:
  147. conf = entry.data | entry.options
  148. entry.data = {
  149. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  150. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  151. CONF_HOST: conf[CONF_HOST],
  152. CONF_TYPE: conf[CONF_TYPE],
  153. CONF_PROTOCOL_VERSION: "auto",
  154. CONF_POLL_ONLY: False,
  155. }
  156. entry.options = {}
  157. entry.version = 11
  158. if entry.version <= 11:
  159. # Migrate unique ids of existing entities to new format
  160. device_id = entry.unique_id
  161. conf_file = get_config(entry.data[CONF_TYPE])
  162. if conf_file is None:
  163. _LOGGER.error(
  164. NOT_FOUND,
  165. entry.data[CONF_TYPE],
  166. )
  167. return False
  168. @callback
  169. def update_unique_id12(entity_entry):
  170. """Update the unique id of an entity entry."""
  171. old_id = entity_entry.unique_id
  172. platform = entity_entry.entity_id.split(".", 1)[0]
  173. e = conf_file.primary_entity
  174. if e.name:
  175. expect_id = f"{device_id}-{slugify(e.name)}"
  176. else:
  177. expect_id = device_id
  178. if e.entity != platform or expect_id != old_id:
  179. for e in conf_file.secondary_entities():
  180. if e.name:
  181. expect_id = f"{device_id}-{slugify(e.name)}"
  182. else:
  183. expect_id = device_id
  184. if e.entity == platform and expect_id == old_id:
  185. break
  186. if e.entity == platform and expect_id == old_id:
  187. new_id = e.unique_id(device_id)
  188. if new_id != old_id:
  189. _LOGGER.info(
  190. "Migrating %s unique_id %s to %s",
  191. e.entity,
  192. old_id,
  193. new_id,
  194. )
  195. return {
  196. "new_unique_id": entity_entry.unique_id.replace(
  197. old_id,
  198. new_id,
  199. )
  200. }
  201. await async_migrate_entries(hass, entry.entry_id, update_unique_id12)
  202. entry.version = 12
  203. if entry.version <= 12:
  204. # Migrate unique ids of existing entities to new format taking into
  205. # account device_class if name is missing.
  206. device_id = entry.unique_id
  207. conf_file = get_config(entry.data[CONF_TYPE])
  208. if conf_file is None:
  209. _LOGGER.error(
  210. NOT_FOUND,
  211. entry.data[CONF_TYPE],
  212. )
  213. return False
  214. @callback
  215. def update_unique_id13(entity_entry):
  216. """Update the unique id of an entity entry."""
  217. old_id = entity_entry.unique_id
  218. platform = entity_entry.entity_id.split(".", 1)[0]
  219. # if unique_id ends with platform name, then this may have
  220. # changed with the addition of device_class.
  221. if old_id.endswith(platform):
  222. e = conf_file.primary_entity
  223. if e.entity != platform or e.name:
  224. for e in conf_file.secondary_entities():
  225. if e.entity == platform and not e.name:
  226. break
  227. if e.entity == platform and not e.name:
  228. new_id = e.unique_id(device_id)
  229. if new_id != old_id:
  230. _LOGGER.info(
  231. "Migrating %s unique_id %s to %s",
  232. e.entity,
  233. old_id,
  234. new_id,
  235. )
  236. return {
  237. "new_unique_id": entity_entry.unique_id.replace(
  238. old_id,
  239. new_id,
  240. )
  241. }
  242. else:
  243. replacements = {
  244. "sensor_co2": "sensor_carbon_dioxide",
  245. "sensor_co": "sensor_carbon_monoxide",
  246. "sensor_pm2_5": "sensor_pm25",
  247. "sensor_pm_10": "sensor_pm10",
  248. "sensor_pm_1_0": "sensor_pm1",
  249. "sensor_pm_2_5": "sensor_pm25",
  250. "sensor_tvoc": "sensor_volatile_organic_compounds",
  251. "sensor_current_humidity": "sensor_humidity",
  252. "sensor_current_temperature": "sensor_temperature",
  253. }
  254. for suffix, new_suffix in replacements.items():
  255. if old_id.endswith(suffix):
  256. e = conf_file.primary_entity
  257. if e.entity != platform or e.name:
  258. for e in conf_file.secondary_entities():
  259. if e.entity == platform and not e.name:
  260. break
  261. if e.entity == platform and not e.name:
  262. new_id = e.unique_id(device_id)
  263. if new_id.endswith(new_suffix):
  264. _LOGGER.info(
  265. "Migrating %s unique_id %s to %s",
  266. e.entity,
  267. old_id,
  268. new_id,
  269. )
  270. return {
  271. "new_unique_id": entity_entry.unique_id.replace(
  272. old_id,
  273. new_id,
  274. )
  275. }
  276. await async_migrate_entries(hass, entry.entry_id, update_unique_id13)
  277. entry.version = 13
  278. return True
  279. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
  280. _LOGGER.debug(
  281. "Setting up entry for device: %s",
  282. get_device_id(entry.data),
  283. )
  284. config = {**entry.data, **entry.options, "name": entry.title}
  285. setup_device(hass, config)
  286. device_conf = get_config(entry.data[CONF_TYPE])
  287. if device_conf is None:
  288. _LOGGER.error(NOT_FOUND, config[CONF_TYPE])
  289. return False
  290. entities = set()
  291. e = device_conf.primary_entity
  292. entities.add(e.entity)
  293. for e in device_conf.secondary_entities():
  294. entities.add(e.entity)
  295. await hass.config_entries.async_forward_entry_setups(entry, entities)
  296. entry.add_update_listener(async_update_entry)
  297. return True
  298. async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
  299. _LOGGER.debug("Unloading entry for device: %s", get_device_id(entry.data))
  300. config = entry.data
  301. data = hass.data[DOMAIN][get_device_id(config)]
  302. device_conf = get_config(config[CONF_TYPE])
  303. if device_conf is None:
  304. _LOGGER.error(NOT_FOUND, config[CONF_TYPE])
  305. return False
  306. entities = {}
  307. e = device_conf.primary_entity
  308. if e.config_id in data:
  309. entities[e.entity] = True
  310. for e in device_conf.secondary_entities():
  311. if e.config_id in data:
  312. entities[e.entity] = True
  313. for e in entities:
  314. await hass.config_entries.async_forward_entry_unload(entry, e)
  315. await async_delete_device(hass, config)
  316. del hass.data[DOMAIN][get_device_id(config)]
  317. return True
  318. async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry):
  319. _LOGGER.debug("Updating entry for device: %s", get_device_id(entry.data))
  320. await async_unload_entry(hass, entry)
  321. await async_setup_entry(hass, entry)