test_config_flow.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. """Tests for the config flow."""
  2. import pytest
  3. import voluptuous as vol
  4. from homeassistant.const import CONF_HOST, CONF_NAME
  5. from homeassistant.data_entry_flow import FlowResultType
  6. from homeassistant.exceptions import ConfigEntryNotReady
  7. from pytest_homeassistant_custom_component.common import MockConfigEntry
  8. from custom_components.tuya_local import (
  9. async_migrate_entry,
  10. async_setup_entry,
  11. config_flow,
  12. get_device_unique_id,
  13. )
  14. from custom_components.tuya_local.const import (
  15. CONF_DEVICE_CID,
  16. CONF_DEVICE_ID,
  17. CONF_LOCAL_KEY,
  18. CONF_POLL_ONLY,
  19. CONF_PROTOCOL_VERSION,
  20. CONF_TYPE,
  21. DOMAIN,
  22. )
  23. # Designed to contain "special" characters that users constantly suspect.
  24. TESTKEY = ")<jO<@)'P1|kR$Kd"
  25. @pytest.fixture(autouse=True)
  26. def auto_enable_custom_integrations(enable_custom_integrations):
  27. yield
  28. @pytest.fixture(autouse=True)
  29. def prevent_task_creation(mocker):
  30. mocker.patch("custom_components.tuya_local.device.TuyaLocalDevice.register_entity")
  31. yield
  32. @pytest.fixture
  33. def bypass_setup(mocker):
  34. """Prevent actual setup of the integration after config flow."""
  35. mocker.patch("custom_components.tuya_local.async_setup_entry", return_value=True)
  36. yield
  37. @pytest.fixture
  38. def bypass_data_fetch(mocker):
  39. """Prevent actual data fetching from the device."""
  40. mocker.patch("tinytuya.Device.status", return_value={"1": True})
  41. yield
  42. @pytest.mark.asyncio
  43. async def test_init_entry(hass, bypass_data_fetch):
  44. """Test initialisation of the config flow."""
  45. entry = MockConfigEntry(
  46. domain=DOMAIN,
  47. version=11,
  48. title="test",
  49. data={
  50. CONF_DEVICE_ID: "deviceid",
  51. CONF_HOST: "hostname",
  52. CONF_LOCAL_KEY: TESTKEY,
  53. CONF_POLL_ONLY: False,
  54. CONF_PROTOCOL_VERSION: "auto",
  55. CONF_TYPE: "kogan_kahtp_heater",
  56. CONF_DEVICE_CID: None,
  57. },
  58. options={},
  59. )
  60. entry.add_to_hass(hass)
  61. await hass.config_entries.async_setup(entry.entry_id)
  62. await hass.async_block_till_done()
  63. assert hass.states.get("climate.test")
  64. assert hass.states.get("lock.test_child_lock")
  65. @pytest.mark.asyncio
  66. @pytest.mark.parametrize("refresh_error", [RuntimeError("boom"), None])
  67. async def test_async_setup_entry_cleans_up_failed_device(hass, mocker, refresh_error):
  68. """Failed runtime setup should not leave stale device state cached."""
  69. mock_device = mocker.MagicMock()
  70. if refresh_error is None:
  71. mock_device.async_refresh = mocker.AsyncMock()
  72. mock_device.has_returned_state = False
  73. else:
  74. mock_device.async_refresh = mocker.AsyncMock(side_effect=refresh_error)
  75. def fake_setup_device(hass, config):
  76. hass.data.setdefault(DOMAIN, {})
  77. hass.data[DOMAIN]["deviceid"] = {
  78. "device": mock_device,
  79. "tuyadevice": mock_device._api,
  80. "tuyadevicelock": mocker.MagicMock(),
  81. }
  82. return mock_device
  83. mocker.patch(
  84. "custom_components.tuya_local.setup_device", side_effect=fake_setup_device
  85. )
  86. entry = MockConfigEntry(
  87. domain=DOMAIN,
  88. version=13,
  89. minor_version=18,
  90. title="test",
  91. data={
  92. CONF_DEVICE_ID: "deviceid",
  93. CONF_HOST: "hostname",
  94. CONF_LOCAL_KEY: TESTKEY,
  95. CONF_POLL_ONLY: False,
  96. CONF_PROTOCOL_VERSION: 3.4,
  97. CONF_TYPE: "kogan_kahtp_heater",
  98. },
  99. options={},
  100. )
  101. with pytest.raises(ConfigEntryNotReady):
  102. await async_setup_entry(hass, entry)
  103. assert "deviceid" not in hass.data.get(DOMAIN, {})
  104. mock_device._api.set_socketPersistent.assert_called_with(False)
  105. @pytest.mark.asyncio
  106. async def test_migrate_entry(hass, mocker):
  107. """Test migration from old entry format."""
  108. mock_device = mocker.MagicMock()
  109. mock_device.async_inferred_type = mocker.AsyncMock(
  110. return_value="goldair_gpph_heater"
  111. )
  112. mocker.patch("custom_components.tuya_local.setup_device", return_value=mock_device)
  113. entry = MockConfigEntry(
  114. domain=DOMAIN,
  115. version=1,
  116. title="test",
  117. data={
  118. CONF_DEVICE_ID: "deviceid",
  119. CONF_HOST: "hostname",
  120. CONF_LOCAL_KEY: TESTKEY,
  121. CONF_TYPE: "auto",
  122. "climate": True,
  123. "child_lock": True,
  124. "display_light": True,
  125. },
  126. )
  127. entry.add_to_hass(hass)
  128. assert await async_migrate_entry(hass, entry)
  129. mock_device.async_inferred_type = mocker.AsyncMock(return_value=None)
  130. mock_device.reset_mock()
  131. entry = MockConfigEntry(
  132. domain=DOMAIN,
  133. version=1,
  134. title="test2",
  135. data={
  136. CONF_DEVICE_ID: "deviceid",
  137. CONF_HOST: "hostname",
  138. CONF_LOCAL_KEY: TESTKEY,
  139. CONF_TYPE: "unknown",
  140. "climate": False,
  141. },
  142. )
  143. entry.add_to_hass(hass)
  144. assert not await async_migrate_entry(hass, entry)
  145. mock_device.reset_mock()
  146. entry = MockConfigEntry(
  147. domain=DOMAIN,
  148. version=2,
  149. title="test3",
  150. data={
  151. CONF_DEVICE_ID: "deviceid",
  152. CONF_HOST: "hostname",
  153. CONF_LOCAL_KEY: TESTKEY,
  154. CONF_TYPE: "auto",
  155. },
  156. options={
  157. "climate": False,
  158. },
  159. )
  160. entry.add_to_hass(hass)
  161. assert not await async_migrate_entry(hass, entry)
  162. mock_device.async_inferred_type = mocker.AsyncMock(return_value="smartplugv1")
  163. mock_device.reset_mock()
  164. entry = MockConfigEntry(
  165. domain=DOMAIN,
  166. version=3,
  167. title="test4",
  168. data={
  169. CONF_DEVICE_ID: "deviceid",
  170. CONF_HOST: "hostname",
  171. CONF_LOCAL_KEY: TESTKEY,
  172. CONF_TYPE: "smartplugv1",
  173. },
  174. options={
  175. "switch": True,
  176. },
  177. )
  178. entry.add_to_hass(hass)
  179. assert await async_migrate_entry(hass, entry)
  180. mock_device.async_inferred_type = mocker.AsyncMock(return_value="smartplugv2")
  181. mock_device.reset_mock()
  182. entry = MockConfigEntry(
  183. domain=DOMAIN,
  184. version=3,
  185. title="test5",
  186. data={
  187. CONF_DEVICE_ID: "deviceid",
  188. CONF_HOST: "hostname",
  189. CONF_LOCAL_KEY: TESTKEY,
  190. CONF_TYPE: "smartplugv1",
  191. },
  192. options={
  193. "switch": True,
  194. },
  195. )
  196. entry.add_to_hass(hass)
  197. assert await async_migrate_entry(hass, entry)
  198. mock_device.async_inferred_type = mocker.AsyncMock(
  199. return_value="goldair_dehumidifier"
  200. )
  201. mock_device.reset_mock()
  202. entry = MockConfigEntry(
  203. domain=DOMAIN,
  204. version=4,
  205. title="test6",
  206. data={
  207. CONF_DEVICE_ID: "deviceid",
  208. CONF_HOST: "hostname",
  209. CONF_LOCAL_KEY: TESTKEY,
  210. CONF_TYPE: "goldair_dehumidifier",
  211. },
  212. options={
  213. "humidifier": True,
  214. "fan": True,
  215. "light": True,
  216. "lock": False,
  217. "switch": True,
  218. },
  219. )
  220. entry.add_to_hass(hass)
  221. assert await async_migrate_entry(hass, entry)
  222. mock_device.async_inferred_type = mocker.AsyncMock(
  223. return_value="grid_connect_usb_double_power_point"
  224. )
  225. mock_device.reset_mock()
  226. entry = MockConfigEntry(
  227. domain=DOMAIN,
  228. version=6,
  229. title="test7",
  230. data={
  231. CONF_DEVICE_ID: "deviceid",
  232. CONF_HOST: "hostname",
  233. CONF_LOCAL_KEY: TESTKEY,
  234. CONF_TYPE: "grid_connect_usb_double_power_point",
  235. },
  236. options={
  237. "switch_main_switch": True,
  238. "switch_left_outlet": True,
  239. "switch_right_outlet": True,
  240. },
  241. )
  242. entry.add_to_hass(hass)
  243. assert await async_migrate_entry(hass, entry)
  244. @pytest.mark.asyncio
  245. async def test_flow_user_init(hass, mocker):
  246. """Test the initialisation of the form in the first page of the manual config flow path."""
  247. result = await hass.config_entries.flow.async_init(
  248. DOMAIN, context={"source": "local"}
  249. )
  250. expected = {
  251. "data_schema": mocker.ANY,
  252. "description_placeholders": mocker.ANY,
  253. "errors": {},
  254. "flow_id": mocker.ANY,
  255. "handler": DOMAIN,
  256. "step_id": "local",
  257. "type": "form",
  258. "last_step": mocker.ANY,
  259. "preview": mocker.ANY,
  260. }
  261. assert expected == result
  262. # Check the schema. Simple comparison does not work since they are not
  263. # the same object
  264. try:
  265. result["data_schema"](
  266. {CONF_DEVICE_ID: "test", CONF_LOCAL_KEY: TESTKEY, CONF_HOST: "test"}
  267. )
  268. except vol.MultipleInvalid:
  269. assert False
  270. try:
  271. result["data_schema"]({CONF_DEVICE_ID: "missing_some"})
  272. assert False
  273. except vol.MultipleInvalid:
  274. pass
  275. @pytest.mark.asyncio
  276. async def test_flow_user_init_protocol_options_are_strings(hass, mocker):
  277. """Test that protocol version dropdown uses strings, not floats."""
  278. result = await hass.config_entries.flow.async_init(
  279. DOMAIN, context={"source": "local"}
  280. )
  281. schema = result["data_schema"]
  282. # Validate that string protocol versions are accepted
  283. schema(
  284. {
  285. CONF_DEVICE_ID: "test",
  286. CONF_LOCAL_KEY: TESTKEY,
  287. CONF_HOST: "test",
  288. CONF_PROTOCOL_VERSION: "3.3",
  289. CONF_POLL_ONLY: False,
  290. }
  291. )
  292. # Validate that float protocol versions are rejected
  293. with pytest.raises(vol.MultipleInvalid):
  294. schema(
  295. {
  296. CONF_DEVICE_ID: "test",
  297. CONF_LOCAL_KEY: TESTKEY,
  298. CONF_HOST: "test",
  299. CONF_PROTOCOL_VERSION: 3.3,
  300. CONF_POLL_ONLY: False,
  301. }
  302. )
  303. @pytest.mark.asyncio
  304. async def test_async_test_connection_valid(hass, mocker):
  305. """Test that device is returned when connection is valid."""
  306. mock_device = mocker.patch(
  307. "custom_components.tuya_local.config_flow.TuyaLocalDevice"
  308. )
  309. mock_instance = mocker.AsyncMock()
  310. mock_instance.has_returned_state = True
  311. mock_instance.pause = mocker.MagicMock()
  312. mock_instance.resume = mocker.MagicMock()
  313. mock_device.return_value = mock_instance
  314. hass.data[DOMAIN] = {"deviceid": {"device": mock_instance}}
  315. device = await config_flow.async_test_connection(
  316. {
  317. CONF_DEVICE_ID: "deviceid",
  318. CONF_LOCAL_KEY: TESTKEY,
  319. CONF_HOST: "hostname",
  320. CONF_PROTOCOL_VERSION: "auto",
  321. },
  322. hass,
  323. )
  324. assert device == mock_instance
  325. mock_instance.pause.assert_called_once()
  326. mock_instance.resume.assert_called_once()
  327. @pytest.mark.asyncio
  328. async def test_async_test_connection_for_subdevice_valid(hass, mocker):
  329. """Test that subdevice is returned when connection is valid."""
  330. mock_device = mocker.patch(
  331. "custom_components.tuya_local.config_flow.TuyaLocalDevice"
  332. )
  333. mock_instance = mocker.AsyncMock()
  334. mock_instance.has_returned_state = True
  335. mock_instance.pause = mocker.MagicMock()
  336. mock_instance.resume = mocker.MagicMock()
  337. mock_device.return_value = mock_instance
  338. hass.data[DOMAIN] = {"subdeviceid": {"device": mock_instance}}
  339. device = await config_flow.async_test_connection(
  340. {
  341. CONF_DEVICE_ID: "deviceid",
  342. CONF_LOCAL_KEY: TESTKEY,
  343. CONF_HOST: "hostname",
  344. CONF_PROTOCOL_VERSION: "auto",
  345. CONF_DEVICE_CID: "subdeviceid",
  346. },
  347. hass,
  348. )
  349. assert device == mock_instance
  350. mock_instance.pause.assert_called_once()
  351. mock_instance.resume.assert_called_once()
  352. @pytest.mark.asyncio
  353. async def test_async_test_connection_invalid(hass, mocker):
  354. """Test that None is returned when connection is invalid."""
  355. mock_device = mocker.patch(
  356. "custom_components.tuya_local.config_flow.TuyaLocalDevice"
  357. )
  358. mock_instance = mocker.AsyncMock()
  359. mock_instance.has_returned_state = False
  360. mock_instance._api = mocker.MagicMock()
  361. mock_device.return_value = mock_instance
  362. device = await config_flow.async_test_connection(
  363. {
  364. CONF_DEVICE_ID: "deviceid",
  365. CONF_LOCAL_KEY: TESTKEY,
  366. CONF_HOST: "hostname",
  367. CONF_PROTOCOL_VERSION: "auto",
  368. },
  369. hass,
  370. )
  371. assert device is None
  372. @pytest.mark.asyncio
  373. async def test_flow_user_init_invalid_config(hass, mocker):
  374. """Test errors populated when config is invalid."""
  375. mocker.patch(
  376. "custom_components.tuya_local.config_flow.async_test_connection",
  377. return_value=None,
  378. )
  379. flow = await hass.config_entries.flow.async_init(
  380. DOMAIN, context={"source": "local"}
  381. )
  382. result = await hass.config_entries.flow.async_configure(
  383. flow["flow_id"],
  384. user_input={
  385. CONF_DEVICE_ID: "deviceid",
  386. CONF_HOST: "hostname",
  387. CONF_LOCAL_KEY: "badkey",
  388. CONF_PROTOCOL_VERSION: "auto",
  389. CONF_POLL_ONLY: False,
  390. },
  391. )
  392. assert {"base": "connection"} == result["errors"]
  393. def setup_device_mock(mock, mocker, failure=False, devtype="test"):
  394. mock_type = mocker.MagicMock()
  395. mock_type.legacy_type = devtype
  396. mock_type.config_type = devtype
  397. mock_type.match_quality.return_value = 100
  398. mock_type.product_display_entries.return_value = [(None, None)]
  399. mock.async_possible_types = mocker.AsyncMock(
  400. return_value=[mock_type] if not failure else []
  401. )
  402. @pytest.mark.asyncio
  403. async def test_flow_user_init_data_valid(hass, mocker):
  404. """Test we advance to the next step when connection config is valid."""
  405. mock_device = mocker.MagicMock()
  406. mock_device._protocol_configured = "auto"
  407. setup_device_mock(mock_device, mocker)
  408. mocker.patch(
  409. "custom_components.tuya_local.config_flow.async_test_connection",
  410. return_value=mock_device,
  411. )
  412. flow = await hass.config_entries.flow.async_init(
  413. DOMAIN, context={"source": "local"}
  414. )
  415. result = await hass.config_entries.flow.async_configure(
  416. flow["flow_id"],
  417. user_input={
  418. CONF_DEVICE_ID: "deviceid",
  419. CONF_HOST: "hostname",
  420. CONF_LOCAL_KEY: TESTKEY,
  421. },
  422. )
  423. assert "form" == result["type"]
  424. assert "select_type" == result["step_id"]
  425. @pytest.mark.asyncio
  426. async def test_flow_select_type_init(hass, mocker):
  427. """Test the initialisation of the form in the 2nd step of the config flow."""
  428. mock_device = mocker.patch.object(config_flow.ConfigFlowHandler, "device")
  429. setup_device_mock(mock_device, mocker)
  430. result = await hass.config_entries.flow.async_init(
  431. DOMAIN, context={"source": "select_type"}
  432. )
  433. expected = {
  434. "data_schema": mocker.ANY,
  435. "description_placeholders": {"device_name": ""},
  436. "errors": None,
  437. "flow_id": mocker.ANY,
  438. "handler": DOMAIN,
  439. "step_id": "select_type",
  440. "type": "form",
  441. "last_step": mocker.ANY,
  442. "preview": mocker.ANY,
  443. }
  444. assert expected == result
  445. # Check the schema. Simple comparison does not work since they are not
  446. # the same object
  447. try:
  448. result["data_schema"]({CONF_TYPE: "test||||"})
  449. except vol.MultipleInvalid:
  450. assert False
  451. try:
  452. result["data_schema"]({CONF_TYPE: "not_test||||"})
  453. assert False
  454. except vol.MultipleInvalid:
  455. pass
  456. @pytest.mark.asyncio
  457. async def test_flow_select_type_aborts_when_no_match(hass, mocker):
  458. """Test the flow aborts when an unsupported device is used."""
  459. mock_device = mocker.patch.object(config_flow.ConfigFlowHandler, "device")
  460. setup_device_mock(mock_device, mocker, failure=True)
  461. result = await hass.config_entries.flow.async_init(
  462. DOMAIN, context={"source": "select_type"}
  463. )
  464. assert result["type"] == "abort"
  465. assert result["reason"] == "not_supported"
  466. @pytest.mark.asyncio
  467. async def test_flow_select_type_data_valid(hass, mocker):
  468. """Test the flow continues when valid data is supplied."""
  469. mock_device = mocker.patch.object(config_flow.ConfigFlowHandler, "device")
  470. setup_device_mock(mock_device, mocker, devtype="smartplugv1")
  471. flow = await hass.config_entries.flow.async_init(
  472. DOMAIN, context={"source": "select_type"}
  473. )
  474. result = await hass.config_entries.flow.async_configure(
  475. flow["flow_id"],
  476. user_input={CONF_TYPE: "smartplugv1||||"},
  477. )
  478. assert "form" == result["type"]
  479. assert "choose_entities" == result["step_id"]
  480. @pytest.mark.asyncio
  481. async def test_flow_choose_entities_init(hass, mocker):
  482. """Test the initialisation of the form in the 3rd step of the config flow."""
  483. mocker.patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "smartplugv1"})
  484. result = await hass.config_entries.flow.async_init(
  485. DOMAIN, context={"source": "choose_entities"}
  486. )
  487. expected = {
  488. "data_schema": mocker.ANY,
  489. "description_placeholders": {"device_name": ""},
  490. "errors": None,
  491. "flow_id": mocker.ANY,
  492. "handler": DOMAIN,
  493. "step_id": "choose_entities",
  494. "type": "form",
  495. "last_step": mocker.ANY,
  496. "preview": mocker.ANY,
  497. }
  498. assert expected == result
  499. # Check the schema. Simple comparison does not work since they are not
  500. # the same object
  501. try:
  502. result["data_schema"]({CONF_NAME: "test"})
  503. except vol.MultipleInvalid:
  504. assert False
  505. try:
  506. result["data_schema"]({"climate": True})
  507. assert False
  508. except vol.MultipleInvalid:
  509. pass
  510. @pytest.mark.asyncio
  511. async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup, mocker):
  512. """Test the flow ends when data is valid."""
  513. mocker.patch.dict(
  514. config_flow.ConfigFlowHandler.data,
  515. {
  516. CONF_DEVICE_ID: "deviceid",
  517. CONF_LOCAL_KEY: TESTKEY,
  518. CONF_HOST: "hostname",
  519. CONF_POLL_ONLY: False,
  520. CONF_PROTOCOL_VERSION: "auto",
  521. CONF_TYPE: "kogan_kahtp_heater",
  522. CONF_DEVICE_CID: None,
  523. },
  524. )
  525. flow = await hass.config_entries.flow.async_init(
  526. DOMAIN, context={"source": "choose_entities"}
  527. )
  528. result = await hass.config_entries.flow.async_configure(
  529. flow["flow_id"],
  530. user_input={
  531. CONF_NAME: "test",
  532. },
  533. )
  534. expected = {
  535. "version": 13,
  536. "minor_version": mocker.ANY,
  537. "context": {"source": "choose_entities"},
  538. "type": FlowResultType.CREATE_ENTRY,
  539. "flow_id": mocker.ANY,
  540. "handler": DOMAIN,
  541. "title": "test",
  542. "description": None,
  543. "description_placeholders": None,
  544. "result": mocker.ANY,
  545. "subentries": (),
  546. "options": {},
  547. "data": {
  548. CONF_DEVICE_ID: "deviceid",
  549. CONF_HOST: "hostname",
  550. CONF_LOCAL_KEY: TESTKEY,
  551. CONF_POLL_ONLY: False,
  552. CONF_PROTOCOL_VERSION: "auto",
  553. CONF_TYPE: "kogan_kahtp_heater",
  554. CONF_DEVICE_CID: None,
  555. },
  556. }
  557. assert expected == result
  558. @pytest.mark.asyncio
  559. async def test_options_flow_init(hass, bypass_data_fetch):
  560. """Test config flow options."""
  561. config_entry = MockConfigEntry(
  562. domain=DOMAIN,
  563. version=13,
  564. unique_id="uniqueid",
  565. data={
  566. CONF_DEVICE_ID: "deviceid",
  567. CONF_HOST: "hostname",
  568. CONF_LOCAL_KEY: TESTKEY,
  569. CONF_NAME: "test",
  570. CONF_POLL_ONLY: False,
  571. CONF_PROTOCOL_VERSION: "auto",
  572. CONF_TYPE: "smartplugv1",
  573. CONF_DEVICE_CID: "",
  574. },
  575. )
  576. config_entry.add_to_hass(hass)
  577. assert await hass.config_entries.async_setup(config_entry.entry_id)
  578. await hass.async_block_till_done()
  579. # show initial form
  580. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  581. assert "form" == result["type"]
  582. assert "user" == result["step_id"]
  583. assert {} == result["errors"]
  584. assert result["data_schema"](
  585. {
  586. CONF_HOST: "hostname",
  587. CONF_LOCAL_KEY: TESTKEY,
  588. }
  589. )
  590. @pytest.mark.asyncio
  591. async def test_options_flow_modifies_config(hass, bypass_setup, mocker):
  592. mock_device = mocker.MagicMock()
  593. mocker.patch(
  594. "custom_components.tuya_local.config_flow.async_test_connection",
  595. return_value=mock_device,
  596. )
  597. config_entry = MockConfigEntry(
  598. domain=DOMAIN,
  599. version=13,
  600. unique_id="uniqueid",
  601. data={
  602. CONF_DEVICE_ID: "deviceid",
  603. CONF_HOST: "hostname",
  604. CONF_LOCAL_KEY: TESTKEY,
  605. CONF_NAME: "test",
  606. CONF_POLL_ONLY: False,
  607. CONF_PROTOCOL_VERSION: "auto",
  608. CONF_TYPE: "ble_pt216_temp_humidity",
  609. CONF_DEVICE_CID: "subdeviceid",
  610. },
  611. )
  612. config_entry.add_to_hass(hass)
  613. assert await hass.config_entries.async_setup(config_entry.entry_id)
  614. await hass.async_block_till_done()
  615. # show initial form
  616. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  617. # submit updated config
  618. result = await hass.config_entries.options.async_configure(
  619. form["flow_id"],
  620. user_input={
  621. CONF_HOST: "new_hostname",
  622. CONF_LOCAL_KEY: "new_key",
  623. CONF_POLL_ONLY: False,
  624. CONF_PROTOCOL_VERSION: "3.3",
  625. },
  626. )
  627. expected = {
  628. CONF_HOST: "new_hostname",
  629. CONF_LOCAL_KEY: "new_key",
  630. CONF_POLL_ONLY: False,
  631. CONF_PROTOCOL_VERSION: 3.3,
  632. }
  633. assert "create_entry" == result["type"]
  634. assert "" == result["title"]
  635. assert expected == result["data"]
  636. @pytest.mark.asyncio
  637. async def test_options_flow_fails_when_connection_fails(
  638. hass, bypass_data_fetch, mocker
  639. ):
  640. mocker.patch(
  641. "custom_components.tuya_local.config_flow.async_test_connection",
  642. return_value=None,
  643. )
  644. config_entry = MockConfigEntry(
  645. domain=DOMAIN,
  646. version=13,
  647. unique_id="uniqueid",
  648. data={
  649. CONF_DEVICE_ID: "deviceid",
  650. CONF_HOST: "hostname",
  651. CONF_LOCAL_KEY: TESTKEY,
  652. CONF_NAME: "test",
  653. CONF_POLL_ONLY: False,
  654. CONF_PROTOCOL_VERSION: "auto",
  655. CONF_TYPE: "smartplugv1",
  656. CONF_DEVICE_CID: "",
  657. },
  658. )
  659. config_entry.add_to_hass(hass)
  660. assert await hass.config_entries.async_setup(config_entry.entry_id)
  661. await hass.async_block_till_done()
  662. # show initial form
  663. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  664. # submit updated config
  665. result = await hass.config_entries.options.async_configure(
  666. form["flow_id"],
  667. user_input={
  668. CONF_HOST: "new_hostname",
  669. CONF_LOCAL_KEY: "new_key",
  670. },
  671. )
  672. assert "form" == result["type"]
  673. assert "user" == result["step_id"]
  674. assert {"base": "connection"} == result["errors"]
  675. @pytest.mark.asyncio
  676. async def test_options_flow_fails_when_config_is_missing(hass, mocker):
  677. mock_device = mocker.MagicMock()
  678. mocker.patch(
  679. "custom_components.tuya_local.config_flow.async_test_connection",
  680. return_value=mock_device,
  681. )
  682. config_entry = MockConfigEntry(
  683. domain=DOMAIN,
  684. version=13,
  685. unique_id="uniqueid",
  686. data={
  687. CONF_DEVICE_ID: "deviceid",
  688. CONF_HOST: "hostname",
  689. CONF_LOCAL_KEY: TESTKEY,
  690. CONF_NAME: "test",
  691. CONF_POLL_ONLY: False,
  692. CONF_PROTOCOL_VERSION: "auto",
  693. CONF_TYPE: "non_existing",
  694. },
  695. )
  696. config_entry.add_to_hass(hass)
  697. await hass.config_entries.async_setup(config_entry.entry_id)
  698. await hass.async_block_till_done()
  699. # show initial form
  700. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  701. assert result["type"] == "abort"
  702. assert result["reason"] == "not_supported"
  703. def test_migration_gets_correct_device_id():
  704. """Test that migration gets the correct device id."""
  705. # Normal device
  706. entry = MockConfigEntry(
  707. domain=DOMAIN,
  708. version=1,
  709. title="test",
  710. data={
  711. CONF_DEVICE_ID: "deviceid",
  712. CONF_HOST: "hostname",
  713. CONF_LOCAL_KEY: TESTKEY,
  714. CONF_TYPE: "auto",
  715. },
  716. )
  717. assert get_device_unique_id(entry) == "deviceid"