test_config_flow.py 22 KB

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