-
-
Notifications
You must be signed in to change notification settings - Fork 37.3k
Add new integration for AiDot #167272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Add new integration for AiDot #167272
Changes from 81 commits
c38868b
83ec82c
beda1a9
43bf742
6ea582a
a0d89a8
ac5cfdd
b338428
2e7a9e0
159b788
5ee7657
d47971b
68866f7
477146f
2af0696
87acc91
d371f97
37a0912
4879aad
45d2c34
7a4955e
0b7dc76
8db9684
1fc8901
872edb9
c70ccc5
78ddd1f
9da883f
e397c09
6d52247
bdcba8d
45dc6b3
6f0f112
5f0fa88
7035c30
07cbb8f
52c958e
25120b7
8622d37
9145bb7
46beaf9
9f47612
8ceb46e
abaff05
e722fb9
c9f2079
269ac86
18c538c
1f3ce0c
c9412be
36cce5e
2ce7d5f
be6a728
666ac1a
81503cf
c5d2347
a793bee
9834df0
e39977e
031fd10
7c3b335
1a68b98
c132bf5
b150442
568b129
0f120a2
3b6a84b
3c1ca98
224ce8d
e1ee6f7
6e42028
5950fb2
2d8d889
4386122
3025807
799ce41
65ed5e3
b7f349b
6e0e117
9f35857
d44ebe0
c47ed24
4a6522f
6be7e25
a96beaf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| """The aidot integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
|
|
||
| from .coordinator import AidotConfigEntry, AidotDeviceManagerCoordinator | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.LIGHT] | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: AidotConfigEntry) -> bool: | ||
| """Set up aidot from a config entry.""" | ||
|
|
||
| coordinator = AidotDeviceManagerCoordinator(hass, entry) | ||
| await coordinator.async_config_entry_first_refresh() | ||
| entry.runtime_data = coordinator | ||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: AidotConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| await entry.runtime_data.async_cleanup() | ||
| return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| """Config flow for Aidot integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from aidot.client import AidotClient | ||
| from aidot.const import CONF_LOGIN_INFO, DEFAULT_COUNTRY_CODE, SUPPORTED_COUNTRY_CODES | ||
| from aidot.exceptions import AidotUserOrPassIncorrect | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
| from homeassistant.const import CONF_COUNTRY_CODE, CONF_PASSWORD, CONF_USERNAME | ||
| from homeassistant.helpers import selector | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
| DATA_SCHEMA = vol.Schema( | ||
| { | ||
| vol.Required( | ||
| CONF_COUNTRY_CODE, | ||
| default=DEFAULT_COUNTRY_CODE, | ||
| ): selector.CountrySelector( | ||
| selector.CountrySelectorConfig( | ||
| countries=SUPPORTED_COUNTRY_CODES, | ||
| ) | ||
| ), | ||
| vol.Required(CONF_USERNAME): str, | ||
| vol.Required(CONF_PASSWORD): str, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| class AidotConfigFlow(ConfigFlow, domain=DOMAIN): | ||
| """Handle aidot config flow.""" | ||
|
|
||
| async def async_step_user( | ||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> ConfigFlowResult: | ||
| """Handle the initial step.""" | ||
| errors: dict[str, str] = {} | ||
| if user_input is not None: | ||
| client = AidotClient( | ||
| session=async_get_clientsession(self.hass), | ||
| country_code=user_input[CONF_COUNTRY_CODE], | ||
| username=user_input[CONF_USERNAME], | ||
| password=user_input[CONF_PASSWORD], | ||
| ) | ||
| await self.async_set_unique_id(client.get_identifier()) | ||
| self._abort_if_unique_id_configured() | ||
| try: | ||
| login_info = await client.async_post_login() | ||
| except AidotUserOrPassIncorrect: | ||
| errors["base"] = "invalid_auth" | ||
|
|
||
| if not errors: | ||
| return self.async_create_entry( | ||
| title=f"{user_input[CONF_USERNAME]} {user_input[CONF_COUNTRY_CODE]}", | ||
| data={ | ||
| CONF_LOGIN_INFO: login_info, | ||
| }, | ||
| ) | ||
|
|
||
| return self.async_show_form( | ||
| step_id="user", data_schema=DATA_SCHEMA, errors=errors | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| """Constants for the aidot integration.""" | ||
|
|
||
| DOMAIN = "aidot" |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,169 @@ | ||||||
| """Coordinator for Aidot.""" | ||||||
|
|
||||||
| from datetime import timedelta | ||||||
| import logging | ||||||
|
|
||||||
| from aidot.client import AidotClient | ||||||
| from aidot.const import ( | ||||||
| CONF_ACCESS_TOKEN, | ||||||
| CONF_AES_KEY, | ||||||
| CONF_DEVICE_LIST, | ||||||
| CONF_ID, | ||||||
| CONF_LOGIN_INFO, | ||||||
| CONF_TYPE, | ||||||
| ) | ||||||
| from aidot.device_client import DeviceClient, DeviceStatusData | ||||||
| from aidot.exceptions import AidotAuthFailed, AidotUserOrPassIncorrect | ||||||
|
|
||||||
| from homeassistant.config_entries import ConfigEntry | ||||||
| from homeassistant.core import HomeAssistant | ||||||
| from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError | ||||||
| from homeassistant.helpers import device_registry as dr | ||||||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||||||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||||||
|
|
||||||
| from .const import DOMAIN | ||||||
|
|
||||||
| type AidotConfigEntry = ConfigEntry[AidotDeviceManagerCoordinator] | ||||||
|
||||||
| _LOGGER = logging.getLogger(__name__) | ||||||
|
|
||||||
| UPDATE_DEVICE_LIST_INTERVAL = timedelta(hours=6) | ||||||
|
|
||||||
|
|
||||||
| class AidotDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceStatusData]): | ||||||
| """Class to manage Aidot data.""" | ||||||
|
|
||||||
| def __init__( | ||||||
| self, | ||||||
| hass: HomeAssistant, | ||||||
| config_entry: AidotConfigEntry, | ||||||
| device_client: DeviceClient, | ||||||
| ) -> None: | ||||||
| """Initialize coordinator.""" | ||||||
| super().__init__( | ||||||
| hass, | ||||||
| _LOGGER, | ||||||
| config_entry=config_entry, | ||||||
| name=DOMAIN, | ||||||
| update_interval=None, | ||||||
| ) | ||||||
| self.device_client = device_client | ||||||
|
|
||||||
| async def _async_setup(self) -> None: | ||||||
| """Set up the coordinator.""" | ||||||
| self.device_client.on_status_update = self._handle_status_update | ||||||
|
|
||||||
| def _handle_status_update(self, status: DeviceStatusData) -> None: | ||||||
| """Handle status callback.""" | ||||||
| self.async_set_updated_data(status) | ||||||
|
|
||||||
| async def _async_update_data(self) -> DeviceStatusData: | ||||||
| """Return current status.""" | ||||||
| return self.device_client.status | ||||||
|
|
||||||
|
|
||||||
| class AidotDeviceManagerCoordinator(DataUpdateCoordinator[None]): | ||||||
| """Class to manage fetching Aidot data.""" | ||||||
|
|
||||||
| config_entry: AidotConfigEntry | ||||||
|
|
||||||
| def __init__( | ||||||
| self, | ||||||
| hass: HomeAssistant, | ||||||
| config_entry: AidotConfigEntry, | ||||||
| ) -> None: | ||||||
| """Initialize coordinator.""" | ||||||
| super().__init__( | ||||||
| hass, | ||||||
| _LOGGER, | ||||||
| config_entry=config_entry, | ||||||
| name=DOMAIN, | ||||||
| update_interval=UPDATE_DEVICE_LIST_INTERVAL, | ||||||
| ) | ||||||
| self.client = AidotClient( | ||||||
| session=async_get_clientsession(hass), | ||||||
| token=config_entry.data[CONF_LOGIN_INFO], | ||||||
| ) | ||||||
| self.client.set_token_fresh_cb(self.token_fresh_cb) | ||||||
| self.device_coordinators: dict[str, AidotDeviceUpdateCoordinator] = {} | ||||||
| self.previous_lists: set[str] = set() | ||||||
|
|
||||||
| async def _async_setup(self) -> None: | ||||||
| """Set up the coordinator.""" | ||||||
| try: | ||||||
| await self.async_auto_login() | ||||||
| except AidotUserOrPassIncorrect as error: | ||||||
| raise ConfigEntryAuthFailed from error | ||||||
|
|
||||||
| async def _async_update_data(self) -> None: | ||||||
| """Update data async.""" | ||||||
| try: | ||||||
| data = await self.client.async_get_all_device() | ||||||
| except AidotAuthFailed as error: | ||||||
| self.token_fresh_cb() | ||||||
| raise ConfigEntryError from error | ||||||
| filter_device_list = [ | ||||||
| device | ||||||
| for device in data[CONF_DEVICE_LIST] | ||||||
| if ( | ||||||
| device[CONF_TYPE] == "light" | ||||||
|
||||||
| device[CONF_TYPE] == "light" | |
| device.get(CONF_TYPE) == "light" |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code accesses device[CONF_ID] on line 116 without checking if the key exists, but uses device.get(CONF_ID) on line 127. This is inconsistent. If a device lacks CONF_ID, line 116 will raise a KeyError. Either use consistent error handling or verify that CONF_ID is always present in devices returned from the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I asked this in the previous PR, what was the identifier again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The identifier is "{region}-{username}" (e.g. "us-user@email.com"), which uniquely identifies the user's account per region.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we have something more specific like a user ID? Do we get a token back that we can get the user ID out of?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the login response does return a user ID. However, using it as the unique_id would require calling the login API first, and the AiDot cloud only allows one active session per account — a second login would invalidate the first entry's token. By using {region}-{username} we can detect duplicates and abort before making any API calls, which protects the existing config entry from being disrupted.