diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index df4689268cf..28c43d8df46 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -44,7 +44,10 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Async setup hass config entry.""" - hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + TUYA_HA_TUYA_MAP: {}, + TUYA_HA_DEVICES: set(), + } # Project type has been renamed to auth type in the upstream Tuya IoT SDK. # This migrates existing config entries to reflect that name change. @@ -54,6 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_update_entry(entry, data=data) success = await _init_tuya_sdk(hass, entry) + + if not success: + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return bool(success) @@ -143,7 +153,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] ) - hass.data.pop(DOMAIN) + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) return unload diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 8fffed3cd9f..bcde364ae1b 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -6,7 +6,6 @@ from typing import Any from tuya_iot import AuthType, TuyaOpenAPI import voluptuous as vol -from voluptuous.schema_builder import UNDEFINED from homeassistant import config_entries @@ -18,11 +17,10 @@ from .const import ( CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, - CONF_REGION, CONF_USERNAME, DOMAIN, SMARTLIFE_APP, - TUYA_REGIONS, + TUYA_COUNTRIES, TUYA_RESPONSE_CODE, TUYA_RESPONSE_MSG, TUYA_RESPONSE_PLATFROM_URL, @@ -42,14 +40,20 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Try login.""" response = {} + country = [ + country + for country in TUYA_COUNTRIES + if country.name == user_input[CONF_COUNTRY_CODE] + ][0] + data = { - CONF_ENDPOINT: TUYA_REGIONS[user_input[CONF_REGION]], + CONF_ENDPOINT: country.endpoint, CONF_AUTH_TYPE: AuthType.CUSTOM, CONF_ACCESS_ID: user_input[CONF_ACCESS_ID], CONF_ACCESS_SECRET: user_input[CONF_ACCESS_SECRET], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_COUNTRY_CODE: user_input[CONF_REGION], + CONF_COUNTRY_CODE: country.country_code, } for app_type in ("", TUYA_SMART_APP, SMARTLIFE_APP): @@ -109,29 +113,32 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): TUYA_RESPONSE_MSG: response.get(TUYA_RESPONSE_MSG), } - def _schema_default(key: str) -> str | UNDEFINED: - if not user_input: - return UNDEFINED - return user_input[key] + if user_input is None: + user_input = {} return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( - CONF_REGION, default=_schema_default(CONF_REGION) - ): vol.In(TUYA_REGIONS.keys()), + CONF_COUNTRY_CODE, + default=user_input.get(CONF_COUNTRY_CODE, "United States"), + ): vol.In( + # We don't pass a dict {code:name} because country codes can be duplicate. + [country.name for country in TUYA_COUNTRIES] + ), vol.Required( - CONF_ACCESS_ID, default=_schema_default(CONF_ACCESS_ID) + CONF_ACCESS_ID, default=user_input.get(CONF_ACCESS_ID, "") ): str, vol.Required( - CONF_ACCESS_SECRET, default=_schema_default(CONF_ACCESS_SECRET) + CONF_ACCESS_SECRET, + default=user_input.get(CONF_ACCESS_SECRET, ""), ): str, vol.Required( - CONF_USERNAME, default=_schema_default(CONF_USERNAME) + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") ): str, vol.Required( - CONF_PASSWORD, default=_schema_default(CONF_PASSWORD) + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") ): str, } ), diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 7c6440d7e48..44b66b576e3 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,4 +1,5 @@ """Constants for the Tuya integration.""" +from dataclasses import dataclass DOMAIN = "tuya" @@ -9,7 +10,6 @@ CONF_ACCESS_ID = "access_id" CONF_ACCESS_SECRET = "access_secret" CONF_USERNAME = "username" CONF_PASSWORD = "password" -CONF_REGION = "region" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" @@ -31,13 +31,265 @@ TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -TUYA_REGIONS = { - "America": "https://openapi.tuyaus.com", - "China": "https://openapi.tuyacn.com", - "Eastern America": "https://openapi-ueaz.tuyaus.com", - "Europe": "https://openapi.tuyaeu.com", - "India": "https://openapi.tuyain.com", - "Western Europe": "https://openapi-weaz.tuyaeu.com", -} +ENDPOINT_AMERICA = "https://openapi.tuyaus.com" +ENDPOINT_CHINA = "https://openapi.tuyacn.com" +ENDPOINT_EASTERN_AMERICA = "https://openapi-ueaz.tuyaus.com" +ENDPOINT_EUROPE = "https://openapi.tuyaeu.com" +ENDPOINT_INDIA = "https://openapi.tuyain.com" +ENDPOINT_WESTERN_EUROPE = "https://openapi-weaz.tuyaeu.com" PLATFORMS = ["climate", "fan", "light", "scene", "switch"] + + +@dataclass +class Country: + """Describe a supported country.""" + + name: str + country_code: str + endpoint: str = ENDPOINT_AMERICA + + +# https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb#title-4-China%20Data%20Center +TUYA_COUNTRIES = [ + Country("Afghanistan", "93"), + Country("Albania", "355"), + Country("Algeria", "213"), + Country("American Samoa", "1-684"), + Country("Andorra", "376"), + Country("Angola", "244"), + Country("Anguilla", "1-264"), + Country("Antarctica", "672"), + Country("Antigua and Barbuda", "1-268"), + Country("Argentina", "54", ENDPOINT_EUROPE), + Country("Armenia", "374"), + Country("Aruba", "297"), + Country("Australia", "61"), + Country("Austria", "43", ENDPOINT_EUROPE), + Country("Azerbaijan", "994"), + Country("Bahamas", "1-242"), + Country("Bahrain", "973"), + Country("Bangladesh", "880"), + Country("Barbados", "1-246"), + Country("Belarus", "375"), + Country("Belgium", "32", ENDPOINT_EUROPE), + Country("Belize", "501"), + Country("Benin", "229"), + Country("Bermuda", "1-441"), + Country("Bhutan", "975"), + Country("Bolivia", "591"), + Country("Bosnia and Herzegovina", "387"), + Country("Botswana", "267"), + Country("Brazil", "55", ENDPOINT_EUROPE), + Country("British Indian Ocean Territory", "246"), + Country("British Virgin Islands", "1-284"), + Country("Brunei", "673"), + Country("Bulgaria", "359"), + Country("Burkina Faso", "226"), + Country("Burundi", "257"), + Country("Cambodia", "855"), + Country("Cameroon", "237"), + Country("Canada", "1", ENDPOINT_AMERICA), + Country("Cape Verde", "238"), + Country("Cayman Islands", "1-345"), + Country("Central African Republic", "236"), + Country("Chad", "235"), + Country("Chile", "56"), + Country("China", "86", ENDPOINT_CHINA), + Country("Christmas Island", "61"), + Country("Cocos Islands", "61"), + Country("Colombia", "57"), + Country("Comoros", "269"), + Country("Cook Islands", "682"), + Country("Costa Rica", "506"), + Country("Croatia", "385", ENDPOINT_EUROPE), + Country("Cuba", "53"), + Country("Curacao", "599"), + Country("Cyprus", "357", ENDPOINT_EUROPE), + Country("Czech Republic", "420", ENDPOINT_EUROPE), + Country("Democratic Republic of the Congo", "243"), + Country("Denmark", "45", ENDPOINT_EUROPE), + Country("Djibouti", "253"), + Country("Dominica", "1-767"), + Country("Dominican Republic", "1-809"), + Country("East Timor", "670"), + Country("Ecuador", "593"), + Country("Egypt", "20"), + Country("El Salvador", "503"), + Country("Equatorial Guinea", "240"), + Country("Eritrea", "291"), + Country("Estonia", "372", ENDPOINT_EUROPE), + Country("Ethiopia", "251"), + Country("Falkland Islands", "500"), + Country("Faroe Islands", "298"), + Country("Fiji", "679"), + Country("Finland", "358", ENDPOINT_EUROPE), + Country("France", "33", ENDPOINT_EUROPE), + Country("French Polynesia", "689"), + Country("Gabon", "241"), + Country("Gambia", "220"), + Country("Georgia", "995"), + Country("Germany", "49", ENDPOINT_EUROPE), + Country("Ghana", "233"), + Country("Gibraltar", "350"), + Country("Greece", "30", ENDPOINT_EUROPE), + Country("Greenland", "299"), + Country("Grenada", "1-473"), + Country("Guam", "1-671"), + Country("Guatemala", "502"), + Country("Guernsey", "44-1481"), + Country("Guinea", "224"), + Country("Guinea-Bissau", "245"), + Country("Guyana", "592"), + Country("Haiti", "509"), + Country("Honduras", "504"), + Country("Hong Kong", "852"), + Country("Hungary", "36", ENDPOINT_EUROPE), + Country("Iceland", "354", ENDPOINT_EUROPE), + Country("India", "91", ENDPOINT_INDIA), + Country("Indonesia", "62"), + Country("Iran", "98"), + Country("Iraq", "964"), + Country("Ireland", "353", ENDPOINT_EUROPE), + Country("Isle of Man", "44-1624"), + Country("Israel", "972"), + Country("Italy", "39", ENDPOINT_EUROPE), + Country("Ivory Coast", "225"), + Country("Jamaica", "1-876"), + Country("Japan", "81", ENDPOINT_EUROPE), + Country("Jersey", "44-1534"), + Country("Jordan", "962"), + Country("Kazakhstan", "7"), + Country("Kenya", "254"), + Country("Kiribati", "686"), + Country("Kosovo", "383"), + Country("Kuwait", "965"), + Country("Kyrgyzstan", "996"), + Country("Laos", "856"), + Country("Latvia", "371", ENDPOINT_EUROPE), + Country("Lebanon", "961"), + Country("Lesotho", "266"), + Country("Liberia", "231"), + Country("Libya", "218"), + Country("Liechtenstein", "423", ENDPOINT_EUROPE), + Country("Lithuania", "370", ENDPOINT_EUROPE), + Country("Luxembourg", "352", ENDPOINT_EUROPE), + Country("Macau", "853"), + Country("Macedonia", "389"), + Country("Madagascar", "261"), + Country("Malawi", "265"), + Country("Malaysia", "60"), + Country("Maldives", "960"), + Country("Mali", "223"), + Country("Malta", "356", ENDPOINT_EUROPE), + Country("Marshall Islands", "692"), + Country("Mauritania", "222"), + Country("Mauritius", "230"), + Country("Mayotte", "262"), + Country("Mexico", "52"), + Country("Micronesia", "691"), + Country("Moldova", "373"), + Country("Monaco", "377"), + Country("Mongolia", "976"), + Country("Montenegro", "382"), + Country("Montserrat", "1-664"), + Country("Morocco", "212"), + Country("Mozambique", "258"), + Country("Myanmar", "95"), + Country("Namibia", "264"), + Country("Nauru", "674"), + Country("Nepal", "977"), + Country("Netherlands", "31", ENDPOINT_EUROPE), + Country("Netherlands Antilles", "599"), + Country("New Caledonia", "687"), + Country("New Zealand", "64"), + Country("Nicaragua", "505"), + Country("Niger", "227"), + Country("Nigeria", "234"), + Country("Niue", "683"), + Country("North Korea", "850"), + Country("Northern Mariana Islands", "1-670"), + Country("Norway", "47"), + Country("Oman", "968"), + Country("Pakistan", "92"), + Country("Palau", "680"), + Country("Palestine", "970"), + Country("Panama", "507"), + Country("Papua New Guinea", "675"), + Country("Paraguay", "595"), + Country("Peru", "51"), + Country("Philippines", "63"), + Country("Pitcairn", "64"), + Country("Poland", "48", ENDPOINT_EUROPE), + Country("Portugal", "351", ENDPOINT_EUROPE), + Country("Puerto Rico", "1-787, 1-939"), + Country("Qatar", "974"), + Country("Republic of the Congo", "242"), + Country("Reunion", "262"), + Country("Romania", "40", ENDPOINT_EUROPE), + Country("Russia", "7", ENDPOINT_EUROPE), + Country("Rwanda", "250"), + Country("Saint Barthelemy", "590"), + Country("Saint Helena", "290"), + Country("Saint Kitts and Nevis", "1-869"), + Country("Saint Lucia", "1-758"), + Country("Saint Martin", "590"), + Country("Saint Pierre and Miquelon", "508"), + Country("Saint Vincent and the Grenadines", "1-784"), + Country("Samoa", "685"), + Country("San Marino", "378"), + Country("Sao Tome and Principe", "239"), + Country("Saudi Arabia", "966"), + Country("Senegal", "221"), + Country("Serbia", "381"), + Country("Seychelles", "248"), + Country("Sierra Leone", "232"), + Country("Singapore", "65"), + Country("Sint Maarten", "1-721"), + Country("Slovakia", "421", ENDPOINT_EUROPE), + Country("Slovenia", "386", ENDPOINT_EUROPE), + Country("Solomon Islands", "677"), + Country("Somalia", "252"), + Country("South Africa", "27"), + Country("South Korea", "82"), + Country("South Sudan", "211"), + Country("Spain", "34", ENDPOINT_EUROPE), + Country("Sri Lanka", "94"), + Country("Sudan", "249"), + Country("Suriname", "597"), + Country("Svalbard and Jan Mayen", "47", ENDPOINT_EUROPE), + Country("Swaziland", "268"), + Country("Sweden", "46", ENDPOINT_EUROPE), + Country("Switzerland", "41"), + Country("Syria", "963"), + Country("Taiwan", "886"), + Country("Tajikistan", "992"), + Country("Tanzania", "255"), + Country("Thailand", "66"), + Country("Togo", "228"), + Country("Tokelau", "690"), + Country("Tonga", "676"), + Country("Trinidad and Tobago", "1-868"), + Country("Tunisia", "216"), + Country("Turkey", "90"), + Country("Turkmenistan", "993"), + Country("Turks and Caicos Islands", "1-649"), + Country("Tuvalu", "688"), + Country("U.S. Virgin Islands", "1-340"), + Country("Uganda", "256"), + Country("Ukraine", "380"), + Country("United Arab Emirates", "971"), + Country("United Kingdom", "44", ENDPOINT_EUROPE), + Country("United States", "1", ENDPOINT_AMERICA), + Country("Uruguay", "598"), + Country("Uzbekistan", "998"), + Country("Vanuatu", "678"), + Country("Vatican", "379"), + Country("Venezuela", "58"), + Country("Vietnam", "84"), + Country("Wallis and Futuna", "681"), + Country("Western Sahara", "212"), + Country("Yemen", "967"), + Country("Zambia", "260"), + Country("Zimbabwe", "263"), +] diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 044c068ac9c..0bb59615e6e 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -4,7 +4,7 @@ "user": { "description": "Enter your Tuya credentials", "data": { - "region": "Region", + "country_code": "Country", "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "username": "Account", @@ -17,4 +17,4 @@ "login_error": "Login error ({code}): {msg}" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 4b4b9a6d1dd..e69872fd309 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, "error": { "invalid_auth": "Invalid authentication", "login_error": "Login error ({code}): {msg}" }, - "flow_title": "Tuya configuration", "step": { - "login": { - "data": { - "access_id": "Access ID", - "access_secret": "Access Secret", - "country_code": "Country Code", - "endpoint": "Availability Zone", - "password": "Password", - "tuya_app_type": "Mobile App", - "username": "Account" - }, - "description": "Enter your Tuya credential", - "title": "Tuya" - }, "user": { "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", - "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", + "country_code": "Country", "password": "Password", - "platform": "The app where your account is registered", - "region": "Region", - "tuya_project_type": "Tuya cloud project type", "username": "Account" }, - "description": "Enter your Tuya credentials", - "title": "Tuya Integration" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Failed to connect" - }, - "error": { - "dev_multi_type": "Multiple selected devices to configure must be of the same type", - "dev_not_config": "Device type not configurable", - "dev_not_found": "Device not found" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Brightness range used by device", - "curr_temp_divider": "Current Temperature value divider (0 = use default)", - "max_kelvin": "Max color temperature supported in kelvin", - "max_temp": "Max target temperature (use min and max = 0 for default)", - "min_kelvin": "Min color temperature supported in kelvin", - "min_temp": "Min target temperature (use min and max = 0 for default)", - "set_temp_divided": "Use divided Temperature value for set temperature command", - "support_color": "Force color support", - "temp_divider": "Temperature values divider (0 = use default)", - "temp_step_override": "Target Temperature step", - "tuya_max_coltemp": "Max color temperature reported by device", - "unit_of_measurement": "Temperature unit used by device" - }, - "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", - "title": "Configure Tuya Device" - }, - "init": { - "data": { - "discovery_interval": "Discovery device polling interval in seconds", - "list_devices": "Select the devices to configure or leave empty to save configuration", - "query_device": "Select device that will use query method for faster status update", - "query_interval": "Query device polling interval in seconds" - }, - "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", - "title": "Configure Tuya Options" + "description": "Enter your Tuya credentials" } } } diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 745bcfde661..a5aee459bd7 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -12,13 +12,14 @@ from homeassistant.components.tuya.const import ( CONF_ACCESS_SECRET, CONF_APP_TYPE, CONF_AUTH_TYPE, + CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, - CONF_REGION, CONF_USERNAME, DOMAIN, + ENDPOINT_INDIA, SMARTLIFE_APP, - TUYA_REGIONS, + TUYA_COUNTRIES, TUYA_SMART_APP, ) from homeassistant.core import HomeAssistant @@ -26,15 +27,15 @@ from homeassistant.core import HomeAssistant MOCK_SMART_HOME_PROJECT_TYPE = 0 MOCK_INDUSTRY_PROJECT_TYPE = 1 -MOCK_REGION = "Europe" +MOCK_COUNTRY = "India" MOCK_ACCESS_ID = "myAccessId" MOCK_ACCESS_SECRET = "myAccessSecret" MOCK_USERNAME = "myUsername" MOCK_PASSWORD = "myPassword" -MOCK_ENDPOINT = "https://openapi-ueaz.tuyaus.com" +MOCK_ENDPOINT = ENDPOINT_INDIA TUYA_INPUT_DATA = { - CONF_REGION: MOCK_REGION, + CONF_COUNTRY_CODE: MOCK_COUNTRY, CONF_ACCESS_ID: MOCK_ACCESS_ID, CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, CONF_USERNAME: MOCK_USERNAME, @@ -92,16 +93,18 @@ async def test_user_flow( ) await hass.async_block_till_done() + country = [country for country in TUYA_COUNTRIES if country.name == MOCK_COUNTRY][0] + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == MOCK_USERNAME assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD - assert result["data"][CONF_ENDPOINT] == MOCK_ENDPOINT - assert result["data"][CONF_ENDPOINT] != TUYA_REGIONS[TUYA_INPUT_DATA[CONF_REGION]] + assert result["data"][CONF_ENDPOINT] == country.endpoint assert result["data"][CONF_APP_TYPE] == app_type assert result["data"][CONF_AUTH_TYPE] == project_type + assert result["data"][CONF_COUNTRY_CODE] == country.country_code assert not result["result"].unique_id