__init__.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  1. """
  2. Integration 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.exceptions import ConfigEntryNotReady
  13. from homeassistant.helpers.entity_registry import (
  14. async_get as async_get_entity_registry,
  15. )
  16. from homeassistant.helpers.entity_registry import async_migrate_entries
  17. from homeassistant.util import slugify
  18. from .const import (
  19. CONF_DEVICE_CID,
  20. CONF_DEVICE_ID,
  21. CONF_LOCAL_KEY,
  22. CONF_POLL_ONLY,
  23. CONF_PROTOCOL_VERSION,
  24. CONF_TYPE,
  25. DOMAIN,
  26. )
  27. from .device import async_delete_device, get_device_id, setup_device
  28. from .helpers.device_config import get_config
  29. from .helpers.discovery import async_start_discovery, async_stop_discovery
  30. from .services import async_setup_services
  31. _LOGGER = logging.getLogger(__name__)
  32. NOT_FOUND = "Configuration file for %s not found"
  33. def replace_unique_ids(entity_entry, device_id, conf_file, replacements):
  34. """Update the unique id of an entry based on a table of replacements."""
  35. old_id = entity_entry.unique_id
  36. platform = entity_entry.entity_id.split(".", 1)[0]
  37. for suffix, new_suffix in replacements.items():
  38. if old_id.endswith(suffix):
  39. # If the entity still exists in the config, do not migrate
  40. for e in conf_file.all_entities():
  41. if e.unique_id(device_id) == old_id:
  42. return None
  43. for e in conf_file.all_entities():
  44. new_id = e.unique_id(device_id)
  45. if e.entity == platform and not e.name and new_id.endswith(new_suffix):
  46. break
  47. if e.entity == platform and not e.name and new_id.endswith(new_suffix):
  48. _LOGGER.info(
  49. "Migrating %s unique_id %s to %s",
  50. e.entity,
  51. old_id,
  52. new_id,
  53. )
  54. return {
  55. "new_unique_id": entity_entry.unique_id.replace(
  56. old_id,
  57. new_id,
  58. )
  59. }
  60. def get_device_unique_id(entry: ConfigEntry):
  61. """Get the unique id for the device from the config entry."""
  62. return (
  63. entry.unique_id
  64. or entry.data.get(CONF_DEVICE_CID)
  65. or entry.data.get(CONF_DEVICE_ID)
  66. )
  67. def cleanup_failed_device(hass: HomeAssistant, device_id: str):
  68. """Drop cached device objects left behind by failed setup."""
  69. domain_data = hass.data.get(DOMAIN, {})
  70. stale = domain_data.pop(device_id, None)
  71. if not stale:
  72. return
  73. api = stale.get("tuyadevice")
  74. if api:
  75. api.set_socketPersistent(False)
  76. if api.parent:
  77. api.parent.set_socketPersistent(False)
  78. async def async_migrate_entry(hass, entry: ConfigEntry):
  79. """Migrate to latest config format."""
  80. CONF_TYPE_AUTO = "auto"
  81. if entry.version == 1:
  82. # Removal of Auto detection.
  83. config = {**entry.data, **entry.options, "name": entry.title}
  84. if config[CONF_TYPE] == CONF_TYPE_AUTO:
  85. device = await hass.async_add_executor_job(
  86. setup_device,
  87. hass,
  88. config,
  89. )
  90. config[CONF_TYPE] = await device.async_inferred_type()
  91. if config[CONF_TYPE] is None:
  92. _LOGGER.error(
  93. "Unable to determine type for device %s",
  94. config[CONF_DEVICE_ID],
  95. )
  96. return False
  97. hass.config_entries.async_update_entry(
  98. entry,
  99. data={
  100. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  101. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  102. CONF_HOST: config[CONF_HOST],
  103. },
  104. version=2,
  105. )
  106. if entry.version == 2:
  107. # CONF_TYPE is not configurable, move from options to main config.
  108. config = {**entry.data, **entry.options, "name": entry.title}
  109. opts = {**entry.options}
  110. # Ensure type has been migrated. Some users are reporting errors which
  111. # suggest it was removed completely. But that is probably due to
  112. # overwriting options without CONF_TYPE.
  113. if config.get(CONF_TYPE, CONF_TYPE_AUTO) == CONF_TYPE_AUTO:
  114. device = await hass.async_add_executor_job(
  115. setup_device,
  116. hass,
  117. config,
  118. )
  119. config[CONF_TYPE] = await device.async_inferred_type()
  120. if config[CONF_TYPE] is None:
  121. _LOGGER.error(
  122. "Unable to determine type for device %s",
  123. config[CONF_DEVICE_ID],
  124. )
  125. return False
  126. opts.pop(CONF_TYPE, None)
  127. hass.config_entries.async_update_entry(
  128. entry,
  129. data={
  130. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  131. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  132. CONF_HOST: config[CONF_HOST],
  133. CONF_TYPE: config[CONF_TYPE],
  134. },
  135. options={**opts},
  136. version=3,
  137. )
  138. if entry.version == 3:
  139. # Migrate to filename based config_type, to avoid needing to
  140. # parse config files to find the right one.
  141. config = {**entry.data, **entry.options, "name": entry.title}
  142. config_yaml = await hass.async_add_executor_job(
  143. get_config,
  144. config[CONF_TYPE],
  145. )
  146. config_type = config_yaml.config_type
  147. # Special case for kogan_switch. Consider also v2.
  148. if config_type == "smartplugv1":
  149. device = await hass.async_add_executor_job(
  150. setup_device,
  151. hass,
  152. config,
  153. )
  154. config_type = await device.async_inferred_type()
  155. if config_type != "smartplugv2":
  156. config_type = "smartplugv1"
  157. hass.config_entries.async_update_entry(
  158. entry,
  159. data={
  160. CONF_DEVICE_ID: config[CONF_DEVICE_ID],
  161. CONF_LOCAL_KEY: config[CONF_LOCAL_KEY],
  162. CONF_HOST: config[CONF_HOST],
  163. CONF_TYPE: config_type,
  164. },
  165. version=4,
  166. )
  167. if entry.version <= 5:
  168. # Migrate unique ids of existing entities to new format
  169. old_id = get_device_unique_id(entry)
  170. conf_file = await hass.async_add_executor_job(
  171. get_config,
  172. entry.data[CONF_TYPE],
  173. )
  174. if conf_file is None:
  175. _LOGGER.error(NOT_FOUND, entry.data[CONF_TYPE])
  176. return False
  177. @callback
  178. def update_unique_id(entity_entry):
  179. """Update the unique id of an entity entry."""
  180. for e in conf_file.all_entities():
  181. if e.entity == entity_entry.platform:
  182. break
  183. if e.entity == entity_entry.platform:
  184. new_id = e.unique_id(old_id)
  185. if new_id != old_id:
  186. _LOGGER.info(
  187. "Migrating %s unique_id %s to %s",
  188. e.entity,
  189. old_id,
  190. new_id,
  191. )
  192. return {
  193. "new_unique_id": entity_entry.unique_id.replace(
  194. old_id,
  195. new_id,
  196. )
  197. }
  198. await async_migrate_entries(hass, entry.entry_id, update_unique_id)
  199. hass.config_entries.async_update_entry(entry, version=6)
  200. if entry.version <= 8:
  201. # Deprecated entities are removed, trim the config back to required
  202. # config only
  203. conf = {**entry.data, **entry.options}
  204. hass.config_entries.async_update_entry(
  205. entry,
  206. data={
  207. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  208. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  209. CONF_HOST: conf[CONF_HOST],
  210. CONF_TYPE: conf[CONF_TYPE],
  211. },
  212. options={},
  213. version=9,
  214. )
  215. if entry.version <= 9:
  216. # Added protocol_version, default to auto
  217. conf = {**entry.data, **entry.options}
  218. hass.config_entries.async_update_entry(
  219. entry,
  220. data={
  221. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  222. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  223. CONF_HOST: conf[CONF_HOST],
  224. CONF_TYPE: conf[CONF_TYPE],
  225. CONF_PROTOCOL_VERSION: "auto",
  226. },
  227. options={},
  228. version=10,
  229. )
  230. if entry.version <= 10:
  231. conf = entry.data | entry.options
  232. hass.config_entries.async_update_entry(
  233. entry,
  234. data={
  235. CONF_DEVICE_ID: conf[CONF_DEVICE_ID],
  236. CONF_LOCAL_KEY: conf[CONF_LOCAL_KEY],
  237. CONF_HOST: conf[CONF_HOST],
  238. CONF_TYPE: conf[CONF_TYPE],
  239. CONF_PROTOCOL_VERSION: conf[CONF_PROTOCOL_VERSION],
  240. CONF_POLL_ONLY: False,
  241. },
  242. options={},
  243. version=11,
  244. )
  245. if entry.version <= 11:
  246. # Migrate unique ids of existing entities to new format
  247. device_id = get_device_unique_id(entry)
  248. conf_file = await hass.async_add_executor_job(
  249. get_config,
  250. entry.data[CONF_TYPE],
  251. )
  252. if conf_file is None:
  253. _LOGGER.error(
  254. NOT_FOUND,
  255. entry.data[CONF_TYPE],
  256. )
  257. return False
  258. @callback
  259. def update_unique_id12(entity_entry):
  260. """Update the unique id of an entity entry."""
  261. old_id = entity_entry.unique_id
  262. platform = entity_entry.entity_id.split(".", 1)[0]
  263. for e in conf_file.all_entities():
  264. if e.name:
  265. expect_id = f"{device_id}-{slugify(e.name)}"
  266. else:
  267. expect_id = device_id
  268. if e.entity == platform and expect_id == old_id:
  269. break
  270. if e.entity == platform and expect_id == old_id:
  271. new_id = e.unique_id(device_id)
  272. if new_id != old_id:
  273. _LOGGER.info(
  274. "Migrating %s unique_id %s to %s",
  275. e.entity,
  276. old_id,
  277. new_id,
  278. )
  279. return {
  280. "new_unique_id": entity_entry.unique_id.replace(
  281. old_id,
  282. new_id,
  283. )
  284. }
  285. await async_migrate_entries(hass, entry.entry_id, update_unique_id12)
  286. hass.config_entries.async_update_entry(entry, version=12)
  287. if entry.version <= 12:
  288. # Migrate unique ids of existing entities to new format taking into
  289. # account device_class if name is missing.
  290. device_id = get_device_unique_id(entry)
  291. conf_file = await hass.async_add_executor_job(
  292. get_config,
  293. entry.data[CONF_TYPE],
  294. )
  295. if conf_file is None:
  296. _LOGGER.error(
  297. NOT_FOUND,
  298. entry.data[CONF_TYPE],
  299. )
  300. return False
  301. @callback
  302. def update_unique_id13(entity_entry):
  303. """Update the unique id of an entity entry."""
  304. old_id = entity_entry.unique_id
  305. platform = entity_entry.entity_id.split(".", 1)[0]
  306. # if unique_id ends with platform name, then this may have
  307. # changed with the addition of device_class.
  308. if old_id.endswith(platform):
  309. for e in conf_file.all_entities():
  310. if e.entity == platform and not e.name:
  311. break
  312. if e.entity == platform and not e.name:
  313. new_id = e.unique_id(device_id)
  314. if new_id != old_id:
  315. _LOGGER.info(
  316. "Migrating %s unique_id %s to %s",
  317. e.entity,
  318. old_id,
  319. new_id,
  320. )
  321. return {
  322. "new_unique_id": entity_entry.unique_id.replace(
  323. old_id,
  324. new_id,
  325. )
  326. }
  327. else:
  328. replacements = {
  329. "sensor_co2": "sensor_carbon_dioxide",
  330. "sensor_co": "sensor_carbon_monoxide",
  331. "sensor_pm2_5": "sensor_pm25",
  332. "sensor_pm_10": "sensor_pm10",
  333. "sensor_pm_1_0": "sensor_pm1",
  334. "sensor_pm_2_5": "sensor_pm25",
  335. "sensor_tvoc": "sensor_volatile_organic_compounds",
  336. "sensor_current_humidity": "sensor_humidity",
  337. "sensor_current_temperature": "sensor_temperature",
  338. }
  339. return replace_unique_ids(
  340. entity_entry, device_id, conf_file, replacements
  341. )
  342. await async_migrate_entries(hass, entry.entry_id, update_unique_id13)
  343. hass.config_entries.async_update_entry(entry, version=13)
  344. if entry.version == 13 and entry.minor_version < 2:
  345. # Migrate unique ids of existing entities to new id taking into
  346. # account translation_key, and standardising naming
  347. device_id = get_device_unique_id(entry)
  348. conf_file = await hass.async_add_executor_job(
  349. get_config,
  350. entry.data[CONF_TYPE],
  351. )
  352. if conf_file is None:
  353. _LOGGER.error(
  354. NOT_FOUND,
  355. entry.data[CONF_TYPE],
  356. )
  357. return False
  358. @callback
  359. def update_unique_id13_2(entity_entry):
  360. """Update the unique id of an entity entry."""
  361. # Standardistion of entity naming to use translation_key
  362. replacements = {
  363. # special meaning of None to handle _full and _empty variants
  364. "binary_sensor_tank": None,
  365. "binary_sensor_tank_full_or_missing": "binary_sensor_tank_full",
  366. "binary_sensor_water_tank_full": "binary_sensor_tank_full",
  367. "binary_sensor_low_water": "binary_sensor_tank_empty",
  368. "binary_sensor_water_tank_empty": "binary_sensor_tank_empty",
  369. "binary_sensor_fault": "binary_sensor_problem",
  370. "binary_sensor_error": "binary_sensor_problem",
  371. "binary_sensor_fault_alarm": "binary_sensor_problem",
  372. "binary_sensor_errors": "binary_sensor_problem",
  373. "binary_sensor_defrosting": "binary_sensor_defrost",
  374. "binary_sensor_anti_frost": "binary_sensor_defrost",
  375. "binary_sensor_anti_freeze": "binary_sensor_defrost",
  376. "binary_sensor_low_battery": "binary_sensor_battery",
  377. "binary_sensor_low_battery_alarm": "binary_sensor_battery",
  378. "select_temperature_units": "select_temperature_unit",
  379. "select_display_temperature_unit": "select_temperature_unit",
  380. "select_display_unit": "select_temperature_unit",
  381. "select_display_units": "select_temperature_unit",
  382. "select_temperature_display_units": "select_temperature_unit",
  383. "switch_defrost": "switch_anti_frost",
  384. "switch_frost_protection": "switch_anti_frost",
  385. }
  386. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  387. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_2)
  388. hass.config_entries.async_update_entry(entry, minor_version=2)
  389. if entry.version == 13 and entry.minor_version < 3:
  390. # Migrate unique ids of existing entities to new id taking into
  391. # account translation_key, and standardising naming
  392. device_id = get_device_unique_id(entry)
  393. conf_file = await hass.async_add_executor_job(
  394. get_config,
  395. entry.data[CONF_TYPE],
  396. )
  397. if conf_file is None:
  398. _LOGGER.error(
  399. NOT_FOUND,
  400. entry.data[CONF_TYPE],
  401. )
  402. return False
  403. @callback
  404. def update_unique_id13_3(entity_entry):
  405. """Update the unique id of an entity entry."""
  406. # Standardistion of entity naming to use translation_key
  407. replacements = {
  408. "light_front_display": "light_display",
  409. "light_lcd_brightness": "light_display",
  410. "light_coal_bed": "light_logs",
  411. "light_ember": "light_embers",
  412. "light_led_indicator": "light_indicator",
  413. "light_status_indicator": "light_indicator",
  414. "light_indicator_light": "light_indicator",
  415. "light_indicators": "light_indicator",
  416. "light_night_light": "light_nightlight",
  417. "number_tiemout_period": "number_timeout_period",
  418. "sensor_remaining_time": "sensor_time_remaining",
  419. "sensor_timer_remain": "sensor_time_remaining",
  420. "sensor_timer": "sensor_time_remaining",
  421. "sensor_timer_countdown": "sensor_time_remaining",
  422. "sensor_timer_remaining": "sensor_time_remaining",
  423. "sensor_time_left": "sensor_time_remaining",
  424. "sensor_timer_minutes_left": "sensor_time_remaining",
  425. "sensor_timer_time_left": "sensor_time_remaining",
  426. "sensor_auto_shutoff_time_remaining": "sensor_time_remaining",
  427. "sensor_warm_time_remaining": "sensor_time_remaining",
  428. "sensor_run_time_remaining": "sensor_time_remaining",
  429. "switch_ioniser": "switch_ionizer",
  430. "switch_run_uv_cycle": "switch_uv_sterilization",
  431. "switch_uv_light": "switch_uv_sterilization",
  432. "switch_ihealth": "switch_uv_sterilization",
  433. "switch_uv_lamp": "switch_uv_sterilization",
  434. "switch_anti_freeze": "switch_anti_frost",
  435. }
  436. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  437. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_3)
  438. hass.config_entries.async_update_entry(entry, minor_version=3)
  439. if entry.version == 13 and entry.minor_version < 4:
  440. conf = entry.data | entry.options
  441. conf_type = conf[CONF_TYPE]
  442. # Duolicate config removal - make sure the correct one is used
  443. if conf_type == "ble-yl01_waterquality_tester":
  444. conf_type = "ble_yl01_watertester"
  445. hass.config_entries.async_update_entry(
  446. entry,
  447. data={**entry.data, CONF_TYPE: conf_type},
  448. options={**entry.options},
  449. minor_version=4,
  450. )
  451. if entry.version == 13 and entry.minor_version < 5:
  452. # Migrate unique ids of existing entities to new id taking into
  453. # account translation_key, and standardising naming
  454. device_id = get_device_unique_id(entry)
  455. conf_file = await hass.async_add_executor_job(
  456. get_config,
  457. entry.data[CONF_TYPE],
  458. )
  459. if conf_file is None:
  460. _LOGGER.error(
  461. NOT_FOUND,
  462. entry.data[CONF_TYPE],
  463. )
  464. return False
  465. @callback
  466. def update_unique_id13_5(entity_entry):
  467. """Update the unique id of an entity entry."""
  468. # Standardistion of entity naming to use translation_key
  469. replacements = {
  470. "number_countdown": "number_timer",
  471. "select_countdown": "select_timer",
  472. "sensor_countdown": "sensor_time_remaining",
  473. "sensor_countdown_timer": "sensor_time_remaining",
  474. "fan": "fan_aroma_diffuser",
  475. }
  476. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  477. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_5)
  478. if entry.version == 13 and entry.minor_version < 6:
  479. # Migrate unique ids of existing entities to new id taking into
  480. # account translation_key, and standardising naming
  481. device_id = get_device_unique_id(entry)
  482. conf_file = await hass.async_add_executor_job(
  483. get_config,
  484. entry.data[CONF_TYPE],
  485. )
  486. if conf_file is None:
  487. _LOGGER.error(
  488. NOT_FOUND,
  489. entry.data[CONF_TYPE],
  490. )
  491. return False
  492. @callback
  493. def update_unique_id13_6(entity_entry):
  494. """Update the unique id of an entity entry."""
  495. # Standardistion of entity naming to use translation_key
  496. replacements = {
  497. "switch_sleep_mode": "switch_sleep",
  498. "switch_sleep_timer": "switch_sleep",
  499. "select_voice_language": "select_language",
  500. "select_restore_power_state": "select_initial_state",
  501. "select_power_on_state": "select_initial_state",
  502. "select_restart_status": "select_initial_state",
  503. "select_poweron_status": "select_initial_state",
  504. "select_mop_mode": "select_mopping",
  505. "select_mop_control": "select_mopping",
  506. "select_water_setting": "select_mopping",
  507. "select_water_mode": "select_mopping",
  508. "select_water_control": "select_mopping",
  509. "select_water_tank": "select_mopping",
  510. "light_light": "light",
  511. "light_lights": "light",
  512. }
  513. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  514. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_6)
  515. hass.config_entries.async_update_entry(entry, minor_version=6)
  516. if entry.version == 13 and entry.minor_version < 7:
  517. # Migrate unique ids of existing entities to new id taking into
  518. # account translation_key, and standardising naming
  519. device_id = get_device_unique_id(entry)
  520. conf_file = await hass.async_add_executor_job(
  521. get_config,
  522. entry.data[CONF_TYPE],
  523. )
  524. if conf_file is None:
  525. _LOGGER.error(
  526. NOT_FOUND,
  527. entry.data[CONF_TYPE],
  528. )
  529. return False
  530. @callback
  531. def update_unique_id13_7(entity_entry):
  532. """Update the unique id of an entity entry."""
  533. # Standardistion of entity naming to use translation_key
  534. replacements = {
  535. "sensor_charger_state": "sensor_status",
  536. }
  537. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  538. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_7)
  539. hass.config_entries.async_update_entry(entry, minor_version=7)
  540. if entry.version == 13 and entry.minor_version < 8:
  541. # Migrate unique ids of existing entities to new id taking into
  542. # account translation_key, and standardising naming
  543. device_id = get_device_unique_id(entry)
  544. conf_file = await hass.async_add_executor_job(
  545. get_config,
  546. entry.data[CONF_TYPE],
  547. )
  548. if conf_file is None:
  549. _LOGGER.error(
  550. NOT_FOUND,
  551. entry.data[CONF_TYPE],
  552. )
  553. return False
  554. @callback
  555. def update_unique_id13_8(entity_entry):
  556. """Update the unique id of an entity entry."""
  557. # Standardistion of entity naming to use translation_key
  558. replacements = {
  559. "sensor_forward_energy": "sensor_energy_consumed",
  560. "sensor_total_forward_energy": "sensor_energy_consumed",
  561. "sensor_energy_in": "sensor_energy_consumed",
  562. "sensor_reverse_energy": "sensor_energy_produced",
  563. "sensor_energy_out": "sensor_energy_produced",
  564. "sensor_forward_energy_a": "sensor_energy_consumed_a",
  565. "sensor_reverse_energy_a": "sensor_energy_produced_a",
  566. "sensor_forward_energy_b": "sensor_energy_consumed_b",
  567. "sensor_reverse_energy_b": "sensor_energy_produced_b",
  568. "sensor_energy_consumption_a": "sensor_energy_consumed_a",
  569. "sensor_energy_consumption_b": "sensor_energy_consumed_b",
  570. "number_timer_switch_1": "number_timer_1",
  571. "number_timer_switch_2": "number_timer_2",
  572. "number_timer_switch_3": "number_timer_3",
  573. "number_timer_switch_4": "number_timer_4",
  574. "number_timer_switch_5": "number_timer_5",
  575. "number_timer_socket_1": "number_timer_1",
  576. "number_timer_socket_2": "number_timer_2",
  577. "number_timer_socket_3": "number_timer_3",
  578. "number_timer_outlet_1": "number_timer_1",
  579. "number_timer_outlet_2": "number_timer_2",
  580. "number_timer_outlet_3": "number_timer_3",
  581. "number_timer_gang_1": "number_timer_1",
  582. "number_timer_gang_2": "number_timer_2",
  583. "number_timer_gang_3": "number_timer_3",
  584. }
  585. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  586. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_8)
  587. hass.config_entries.async_update_entry(entry, minor_version=8)
  588. if entry.version == 13 and entry.minor_version < 9:
  589. # Migrate unique ids of existing entities to new id taking into
  590. # account translation_key, and standardising naming
  591. device_id = get_device_unique_id(entry)
  592. conf_file = await hass.async_add_executor_job(
  593. get_config,
  594. entry.data[CONF_TYPE],
  595. )
  596. if conf_file is None:
  597. _LOGGER.error(
  598. NOT_FOUND,
  599. entry.data[CONF_TYPE],
  600. )
  601. return False
  602. @callback
  603. def update_unique_id13_9(entity_entry):
  604. """Update the unique id of an entity entry."""
  605. # Standardistion of entity naming to use translation_key
  606. replacements = {
  607. "number_delay_time": "number_delay",
  608. }
  609. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  610. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_9)
  611. hass.config_entries.async_update_entry(entry, minor_version=9)
  612. if entry.version == 13 and entry.minor_version < 10:
  613. # Migrate unique ids of existing entities to new id taking into
  614. # account translation_key, and standardising naming
  615. device_id = get_device_unique_id(entry)
  616. conf_file = await hass.async_add_executor_job(
  617. get_config,
  618. entry.data[CONF_TYPE],
  619. )
  620. if conf_file is None:
  621. _LOGGER.error(
  622. NOT_FOUND,
  623. entry.data[CONF_TYPE],
  624. )
  625. return False
  626. @callback
  627. def update_unique_id13_10(entity_entry):
  628. """Update the unique id of an entity entry."""
  629. # Standardistion of entity naming to use translation_key
  630. replacements = {
  631. "switch_disturb_switch": "switch_do_not_disturb",
  632. "number_temperature_correction": "number_temperature_calibration",
  633. "number_calibration_offset": "number_temperature_calibration",
  634. "number_high_temperature_limit": "number_maximum_temperature",
  635. "number_low_temperature_limit": "number_minimum_temperature",
  636. }
  637. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  638. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_10)
  639. hass.config_entries.async_update_entry(entry, minor_version=10)
  640. if entry.version == 13 and entry.minor_version < 12:
  641. # at some point HA stopped populating unique_id for device entries,
  642. # and our migrations have been using the wrong value. Migration
  643. # to correct that
  644. unique_id = entry.unique_id
  645. device_id = get_device_unique_id(entry)
  646. if unique_id is None:
  647. hass.config_entries.async_update_entry(
  648. entry,
  649. unique_id=device_id,
  650. )
  651. @callback
  652. def update_unique_id3_12(entity_entry):
  653. """Update the unique id of badly migrated entities."""
  654. old_id = entity_entry.unique_id
  655. if old_id.startswith("None-"):
  656. # new_id = f"{device_id}-{old_id[5:]}"
  657. # return {
  658. # "new_unique_id": new_id,
  659. # }
  660. # Rather than update, delete the bogus entities, as HA will
  661. # recreate them correctly, and maybe already has so duplicates
  662. # could result if they are migrated.
  663. registry = async_get_entity_registry(hass)
  664. registry.async_remove(entity_entry.entity_id)
  665. await async_migrate_entries(hass, entry.entry_id, update_unique_id3_12)
  666. hass.config_entries.async_update_entry(entry, minor_version=12)
  667. if entry.version == 13 and entry.minor_version < 13:
  668. # Migrate unique ids of existing entities to new id taking into
  669. # account translation_key, and standardising naming
  670. device_id = get_device_unique_id(entry)
  671. conf_file = await hass.async_add_executor_job(
  672. get_config,
  673. entry.data[CONF_TYPE],
  674. )
  675. if conf_file is None:
  676. _LOGGER.error(
  677. NOT_FOUND,
  678. entry.data[CONF_TYPE],
  679. )
  680. return False
  681. @callback
  682. def update_unique_id13_13(entity_entry):
  683. """Update the unique id of an entity entry."""
  684. # Standardistion of entity naming to use translation_key
  685. replacements = {
  686. "switch_flip": "switch_flip_image",
  687. "number_feed": "number_manual_feed",
  688. "number_feed_portions": "number_manual_feed",
  689. "number_manual_amount": "number_manual_feed",
  690. }
  691. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  692. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_13)
  693. hass.config_entries.async_update_entry(entry, minor_version=13)
  694. # 13.14 was botched, so repeat as 13.15
  695. if entry.version == 13 and entry.minor_version < 15:
  696. # Migrate unique ids of existing entities to new id taking into
  697. # account translation_key, and standardising naming
  698. device_id = get_device_unique_id(entry)
  699. conf_file = await hass.async_add_executor_job(
  700. get_config,
  701. entry.data[CONF_TYPE],
  702. )
  703. if conf_file is None:
  704. _LOGGER.error(
  705. NOT_FOUND,
  706. entry.data[CONF_TYPE],
  707. )
  708. return False
  709. @callback
  710. def update_unique_id13_15(entity_entry):
  711. """Update the unique id of an entity entry."""
  712. # Standardistion of entity naming to use translation_key
  713. replacements = {
  714. "sensor_filter_lifetime": "sensor_filter_life",
  715. "sensor_filter": "sensor_filter_life",
  716. "sensor_filter_days": "sensor_filter_life",
  717. "sensor_filter_remaining": "sensor_filter_life",
  718. "sensor_filter_remain": "sensor_filter_life",
  719. "sensor_filter_replacement": "sensor_filter_life",
  720. "sensor_filter_days_left": "sensor_filter_life",
  721. "sensor_filter_left_days": "sensor_filter_life",
  722. "sensor_filter_left": "sensor_filter_life",
  723. "sensor_filter_hours_left": "sensor_filter_life",
  724. "sensor_replace_filter_in": "sensor_filter_life",
  725. "sensor_filter_change_due": "sensor_filter_life",
  726. "sensor_filter_usage": "sensor_filter_life",
  727. "sensor_filter_used": "sensor_filter_life",
  728. "sensor_filter_time": "sensor_filter_life",
  729. "button_reset_filter": "button_filter_reset",
  730. "button_replace_filter": "button_filter_reset",
  731. "button_filter_changed": "button_filter_reset",
  732. "button_filter_replaced": "button_filter_reset",
  733. "select_motion_detection": "select_motion_sensitivity",
  734. "select_motion_distance": "select_motion_sensitivity",
  735. }
  736. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  737. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_15)
  738. hass.config_entries.async_update_entry(entry, minor_version=15)
  739. if entry.version == 13 and entry.minor_version < 16:
  740. # Migrate unique ids of existing entities to new id taking into
  741. # account translation_key, and standardising naming
  742. device_id = get_device_unique_id(entry)
  743. conf_file = await hass.async_add_executor_job(
  744. get_config,
  745. entry.data[CONF_TYPE],
  746. )
  747. if conf_file is None:
  748. _LOGGER.error(
  749. NOT_FOUND,
  750. entry.data[CONF_TYPE],
  751. )
  752. return False
  753. @callback
  754. def update_unique_id13_16(entity_entry):
  755. """Update the unique id of an entity entry."""
  756. # Standardistion of entity naming to use translation_key
  757. replacements = {
  758. "switch_beep": "switch_sound",
  759. "switch_mute": "switch_sound",
  760. "switch_muffling": "switch_sound",
  761. "switch_mute_sound": "switch_sound",
  762. "switch_mute_voice": "switch_sound",
  763. }
  764. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  765. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_16)
  766. hass.config_entries.async_update_entry(entry, minor_version=16)
  767. if entry.version == 13 and entry.minor_version < 17:
  768. # Migrate unique ids of existing entities to new id taking into
  769. # account translation_key, and standardising naming
  770. device_id = get_device_unique_id(entry)
  771. conf_file = await hass.async_add_executor_job(
  772. get_config,
  773. entry.data[CONF_TYPE],
  774. )
  775. if conf_file is None:
  776. _LOGGER.error(
  777. NOT_FOUND,
  778. entry.data[CONF_TYPE],
  779. )
  780. return False
  781. @callback
  782. def update_unique_id13_17(entity_entry):
  783. """Update the unique id of an entity entry."""
  784. # Standardistion of entity naming to use translation_key
  785. replacements = {
  786. "sensor_light_intensity": "sensor_illuminance",
  787. "sensor_rain": "sensor_precipitation",
  788. "sensor_rainfall_rate": "sensor_precipitation_intensity",
  789. "text_regular_schedule": "text_schedule",
  790. "text_program": "text_schedule",
  791. "text_program_data": "text_schedule",
  792. "text_weekly_program": "text_schedule",
  793. "text_weekprogram": "text_schedule",
  794. }
  795. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  796. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_17)
  797. hass.config_entries.async_update_entry(entry, minor_version=17)
  798. if entry.version == 13 and entry.minor_version < 18:
  799. # Migrate unique ids of existing entities to new id taking into
  800. # account translation_key, and standardising naming
  801. device_id = get_device_unique_id(entry)
  802. conf_file = await hass.async_add_executor_job(
  803. get_config,
  804. entry.data[CONF_TYPE],
  805. )
  806. if conf_file is None:
  807. _LOGGER.error(
  808. NOT_FOUND,
  809. entry.data[CONF_TYPE],
  810. )
  811. return False
  812. @callback
  813. def update_unique_id13_18(entity_entry):
  814. """Update the unique id of an entity entry."""
  815. # Standardistion of entity naming to use translation_key
  816. replacements = {
  817. "sensor_air_temperature": "sensor_ambient_temperature",
  818. "sensor_current_temperature": "sensor_temperature",
  819. }
  820. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  821. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_18)
  822. hass.config_entries.async_update_entry(entry, minor_version=18)
  823. if entry.version == 13 and entry.minor_version < 19:
  824. # Migrate unique ids of existing entities to new id taking into
  825. # account translation_key, and standardising naming
  826. device_id = get_device_unique_id(entry)
  827. conf_file = await hass.async_add_executor_job(
  828. get_config,
  829. entry.data[CONF_TYPE],
  830. )
  831. if conf_file is None:
  832. _LOGGER.error(
  833. NOT_FOUND,
  834. entry.data[CONF_TYPE],
  835. )
  836. return False
  837. @callback
  838. def update_unique_id13_19(entity_entry):
  839. """Update the unique id of an entity entry."""
  840. # Standardistion of entity naming to use translation_key
  841. replacements = {
  842. "fan_fan_with_presets": "fan_air_purifier",
  843. "fan_purifier": "fan_air_purifier",
  844. "fan": "fan_air_purifier",
  845. "light_ambient": "light_ambient_light",
  846. "number_snooze_time": "number_snooze_duration",
  847. "select_display": "select_display_brightness",
  848. "select_snooze_type": "select_snooze_action",
  849. "switch_internet_time": "switch_network_time",
  850. }
  851. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  852. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_19)
  853. hass.config_entries.async_update_entry(entry, minor_version=19)
  854. if entry.version == 13 and entry.minor_version < 20:
  855. # Migrate unique ids of existing entities to new id taking into
  856. # account translation_key, and standardising naming
  857. device_id = get_device_unique_id(entry)
  858. conf_file = await hass.async_add_executor_job(
  859. get_config,
  860. entry.data[CONF_TYPE],
  861. )
  862. if conf_file is None:
  863. _LOGGER.error(
  864. NOT_FOUND,
  865. entry.data[CONF_TYPE],
  866. )
  867. return False
  868. @callback
  869. def update_unique_id13_20(entity_entry):
  870. """Update the unique id of an entity entry."""
  871. # Standardistion of entity naming to use translation_key
  872. replacements = {
  873. "select_nightvision": "select_night_vision",
  874. "switch_timer_set": "switch_timer",
  875. "switch_timer_start": "switch_timer",
  876. "select_record_mode": "select_recording_mode",
  877. "switch_osd_watermark": "switch_watermark",
  878. "switch_timestamp": "switch_watermark",
  879. }
  880. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  881. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_20)
  882. hass.config_entries.async_update_entry(entry, minor_version=20)
  883. if entry.version == 13 and entry.minor_version < 21:
  884. # Migrate unique ids of existing entities to new id taking into
  885. # account translation_key, and standardising naming
  886. device_id = get_device_unique_id(entry)
  887. conf_file = await hass.async_add_executor_job(
  888. get_config,
  889. entry.data[CONF_TYPE],
  890. )
  891. if conf_file is None:
  892. _LOGGER.error(
  893. NOT_FOUND,
  894. entry.data[CONF_TYPE],
  895. )
  896. return False
  897. @callback
  898. def update_unique_id13_21(entity_entry):
  899. """Update the unique id of an entity entry."""
  900. # Standardistion of entity naming to use translation_key
  901. replacements = {
  902. "switch_motion_enable": "switch_motion_detection",
  903. "switch_motion_sensing": "switch_motion_detection",
  904. "swtich_motion_notification": "switch_motion_detection",
  905. }
  906. return replace_unique_ids(entity_entry, device_id, conf_file, replacements)
  907. await async_migrate_entries(hass, entry.entry_id, update_unique_id13_21)
  908. hass.config_entries.async_update_entry(entry, minor_version=21)
  909. return True
  910. async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
  911. device_id = get_device_id(entry.data)
  912. _LOGGER.debug(
  913. "Setting up entry for device: %s",
  914. device_id,
  915. )
  916. # Start background LAN rediscovery so a device that changes IP (e.g. after a
  917. # DHCP lease change) is relocated and reconnected without manual
  918. # reconfiguration. Safe to call repeatedly; only one sweeper is started.
  919. await async_start_discovery(hass)
  920. config = {**entry.data, **entry.options, "name": entry.title}
  921. try:
  922. device = await hass.async_add_executor_job(setup_device, hass, config)
  923. await device.async_refresh()
  924. except Exception as e:
  925. cleanup_failed_device(hass, device_id)
  926. raise ConfigEntryNotReady("tuya-local device not ready") from e
  927. if not device.has_returned_state:
  928. cleanup_failed_device(hass, device_id)
  929. raise ConfigEntryNotReady("tuya-local device offline")
  930. device_conf = await hass.async_add_executor_job(
  931. get_config,
  932. entry.data[CONF_TYPE],
  933. )
  934. if device_conf is None:
  935. _LOGGER.error(NOT_FOUND, config[CONF_TYPE])
  936. return False
  937. entities = set()
  938. for e in device_conf.all_entities():
  939. entities.add(e.entity)
  940. await hass.config_entries.async_forward_entry_setups(entry, entities)
  941. await async_setup_services(hass, entities)
  942. entry.add_update_listener(async_update_entry)
  943. return True
  944. async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
  945. device_id = get_device_id(entry.data)
  946. _LOGGER.debug("Unloading entry for device: %s", device_id)
  947. config = entry.data
  948. domain_data = hass.data.get(DOMAIN, {})
  949. data = domain_data.get(device_id)
  950. if data is None:
  951. await async_delete_device(hass, config)
  952. return True
  953. device_conf = await hass.async_add_executor_job(
  954. get_config,
  955. config[CONF_TYPE],
  956. )
  957. if device_conf is None:
  958. _LOGGER.error(NOT_FOUND, config[CONF_TYPE])
  959. return False
  960. entities = {}
  961. for e in device_conf.all_entities():
  962. if e.config_id in data:
  963. entities[e.entity] = True
  964. for e in entities:
  965. await hass.config_entries.async_forward_entry_unload(entry, e)
  966. await async_delete_device(hass, config)
  967. domain_data.pop(device_id, None)
  968. # Stop the shared rediscovery sweeper once the last device is gone.
  969. remaining = [
  970. e
  971. for e in hass.config_entries.async_entries(DOMAIN)
  972. if e.entry_id != entry.entry_id
  973. ]
  974. if not remaining:
  975. async_stop_discovery(hass)
  976. return True
  977. async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry):
  978. _LOGGER.debug("Updating entry for device: %s", get_device_id(entry.data))
  979. await async_unload_entry(hass, entry)
  980. await async_setup_entry(hass, entry)