__init__.py 38 KB

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