__init__.py 34 KB

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