Skip to content
Open
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
c38868b
Add new integration for aidot
s1eedz May 13, 2024
83ec82c
fix ci work run error
s1eedz Jul 9, 2024
beda1a9
fix ci work run error 2
s1eedz Jul 9, 2024
43bf742
modify the test data and code
s1eedz Jul 9, 2024
6ea582a
Modifying unit tests
s1eedz Jul 11, 2024
a0d89a8
Increase unit test coverage
s1eedz Jul 16, 2024
ac5cfdd
Increase unit test coverage
s1eedz Jul 17, 2024
b338428
Increase unit test coverage
s1eedz Oct 11, 2024
2e7a9e0
完善单元测试
HongBryan Jan 24, 2025
159b788
add owner
HongBryan Feb 6, 2025
5ee7657
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Feb 10, 2025
d47971b
Fix
HongBryan Feb 17, 2025
68866f7
Update config_flow.py
s1eedz Feb 17, 2025
477146f
fix and add quality_scale.yaml
HongBryan Feb 18, 2025
2af0696
Remove non-conforming codes
HongBryan Feb 19, 2025
87acc91
Remove VERSION and CONNECTION_CLASS from config_flow.py
HongBryan Feb 19, 2025
d371f97
use entry.runtime_data instead and fix
HongBryan Feb 19, 2025
37a0912
Fix
HongBryan Feb 20, 2025
4879aad
Fix
HongBryan Feb 24, 2025
45d2c34
Update update_data interval
HongBryan Feb 24, 2025
7a4955e
Fix
HongBryan Feb 24, 2025
0b7dc76
Fix
HongBryan Feb 25, 2025
8db9684
Fix
HongBryan Feb 26, 2025
1fc8901
Fix
HongBryan Feb 26, 2025
872edb9
Fix
HongBryan Feb 26, 2025
c70ccc5
Fix
HongBryan Feb 27, 2025
78ddd1f
Fix
HongBryan Mar 1, 2025
9da883f
Fix
HongBryan Mar 1, 2025
e397c09
Fix
HongBryan Mar 6, 2025
6d52247
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Mar 7, 2025
bdcba8d
Fix
HongBryan Mar 7, 2025
45dc6b3
Fix
HongBryan Mar 10, 2025
6f0f112
Fix
HongBryan Mar 10, 2025
5f0fa88
Fix
HongBryan Mar 10, 2025
7035c30
Fix
HongBryan Mar 11, 2025
07cbb8f
Fix
HongBryan Mar 12, 2025
52c958e
Fix
HongBryan Mar 12, 2025
25120b7
Fix
HongBryan Mar 13, 2025
8622d37
Fix
HongBryan Mar 14, 2025
9145bb7
Fix
HongBryan Mar 19, 2025
46beaf9
Fix
HongBryan Mar 28, 2025
9f47612
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Apr 1, 2025
8ceb46e
Fix
HongBryan Apr 1, 2025
abaff05
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Apr 17, 2025
e722fb9
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Apr 17, 2025
c9f2079
fix
s1eedz Jun 17, 2025
269ac86
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Jun 23, 2025
18c538c
Update homeassistant/components/aidot/coordinator.py
s1eedz Jun 27, 2025
1f3ce0c
Update homeassistant/components/aidot/strings.json
s1eedz Jun 27, 2025
c9412be
fix
s1eedz Jun 27, 2025
36cce5e
fix
s1eedz Sep 18, 2025
2ce7d5f
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Sep 18, 2025
be6a728
fix
s1eedz Sep 18, 2025
666ac1a
Merge branch 'feature/aidot-wifi-lights-improve' of https://github.co…
s1eedz Sep 18, 2025
81503cf
fix
s1eedz Sep 18, 2025
c5d2347
fix
s1eedz Sep 29, 2025
a793bee
feat
s1eedz Nov 10, 2025
9834df0
Merge remote-tracking branch 'upstream/dev' into feature/aidot-wifi-l…
s1eedz Mar 25, 2026
e39977e
feat: update aidot integration
s1eedz Apr 3, 2026
031fd10
Fix Copilot review comments
s1eedz Apr 3, 2026
7c3b335
Fix manifest: add integration_type, remove version
s1eedz Apr 3, 2026
1a68b98
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Apr 3, 2026
c132bf5
Update homeassistant/components/aidot/quality_scale.yaml
s1eedz Apr 3, 2026
b150442
Fix prettier JSON key sorting in strings.json
s1eedz Apr 3, 2026
568b129
Fix test constants and update snapshot
s1eedz Apr 3, 2026
0f120a2
Remove Chinese comment, refactor async_turn_on logic
s1eedz Apr 3, 2026
3b6a84b
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Apr 7, 2026
3c1ca98
Address joostlek review: simplify light setup, fix coordinator, updat…
s1eedz Apr 7, 2026
224ce8d
refactor: improve aidot light entity setup with cleaner diff tracking
s1eedz Apr 7, 2026
e1ee6f7
Update homeassistant/components/aidot/light.py
s1eedz Apr 7, 2026
6e42028
Update tests/components/aidot/conftest.py
s1eedz Apr 7, 2026
5950fb2
Update tests/components/aidot/conftest.py
s1eedz Apr 7, 2026
2d8d889
refactor: move DATA_SCHEMA to module-level constant in config_flow
s1eedz Apr 7, 2026
4386122
Update tests/components/aidot/test_config_flow.py
s1eedz Apr 7, 2026
3025807
Fix ruff check formatting issues
s1eedz Apr 7, 2026
799ce41
Add tests for uncovered code paths to improve coverage
s1eedz Apr 7, 2026
65ed5e3
Fix ruff formatting
s1eedz Apr 7, 2026
b7f349b
Update homeassistant/components/aidot/light.py
s1eedz Apr 7, 2026
6e0e117
Move imports to top-level to fix PLC0415
s1eedz Apr 7, 2026
9f35857
Update homeassistant/components/aidot/light.py
s1eedz Apr 7, 2026
d44ebe0
Update homeassistant/components/aidot/coordinator.py
s1eedz Apr 7, 2026
c47ed24
Simplify light setup, add assertion to device removal test
s1eedz Apr 7, 2026
4a6522f
Merge branch 'dev' into feature/aidot-wifi-lights-improve
s1eedz Apr 7, 2026
6be7e25
Address joostlek review: rename to AiDot, remove hass param, simplify…
s1eedz Apr 8, 2026
a96beaf
Fix ruff format: single-line __init__ signature
s1eedz Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions homeassistant/components/aidot/__init__.py
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)
67 changes: 67 additions & 0 deletions homeassistant/components/aidot/config_flow.py
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


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()
Comment on lines +50 to +51
Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Author

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.

Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Author

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.

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,
},
)

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,
}
)
Comment thread
s1eedz marked this conversation as resolved.
Outdated

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
3 changes: 3 additions & 0 deletions homeassistant/components/aidot/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the aidot integration."""

DOMAIN = "aidot"
173 changes: 173 additions & 0 deletions homeassistant/components/aidot/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""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.const import Platform
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]
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type alias definition using the type keyword (line 27) requires Python 3.12+. Home Assistant typically needs to support Python 3.11+, so this should use TypeAlias from typing instead: AidotConfigEntry: TypeAlias = ConfigEntry[AidotDeviceManagerCoordinator]

Copilot uses AI. Check for mistakes.
_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):
Comment thread
s1eedz marked this conversation as resolved.
Outdated
"""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.get(CONF_DEVICE_LIST)
Comment thread
s1eedz marked this conversation as resolved.
Outdated
if (
device[CONF_TYPE] == Platform.LIGHT
Comment thread
s1eedz marked this conversation as resolved.
Outdated
and CONF_AES_KEY in device
and device[CONF_AES_KEY][0] is not None
)
]
Comment thread
s1eedz marked this conversation as resolved.
Outdated

delete_lists = self.previous_lists - (
current_lists := {device[CONF_ID] for device in filter_device_list}
)
Copy link

Copilot AI Apr 7, 2026

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.

Copilot uses AI. Check for mistakes.

for dev_id in delete_lists:
if dev_id in self.device_coordinators:
del self.device_coordinators[dev_id]
if delete_lists:
self._purge_deleted_lists()
self.previous_lists = current_lists

for device in filter_device_list:
dev_id = device.get(CONF_ID)
if dev_id not in self.device_coordinators:
device_client = self.client.get_device_client(device)
device_coordinator = AidotDeviceUpdateCoordinator(
self.hass, self.config_entry, device_client
)
await device_coordinator.async_config_entry_first_refresh()
self.device_coordinators[dev_id] = device_coordinator

async def async_cleanup(self) -> None:
"""Perform cleanup actions."""
await self.client.async_cleanup()

def token_fresh_cb(self) -> None:
"""Update token."""
self.hass.config_entries.async_update_entry(
self.config_entry, data={CONF_LOGIN_INFO: self.client.login_info.copy()}
)

async def async_auto_login(self) -> None:
"""Async auto login."""
if self.client.login_info.get(CONF_ACCESS_TOKEN) is None:
try:
await self.client.async_post_login()
except AidotUserOrPassIncorrect as error:
raise AidotUserOrPassIncorrect from error
Comment thread
s1eedz marked this conversation as resolved.
Outdated
Comment thread
s1eedz marked this conversation as resolved.
Outdated

def _purge_deleted_lists(self) -> None:
"""Purge device entries of deleted lists."""

device_reg = dr.async_get(self.hass)
identifiers = {
(
DOMAIN,
f"{device_coordinator.device_client.info.dev_id}",
)
for device_coordinator in self.device_coordinators.values()
}
for device in dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id
):
if not set(device.identifiers) & identifiers:
_LOGGER.debug("Removing obsolete device entry %s", device.name)
device_reg.async_update_device(
device.id, remove_config_entry_id=self.config_entry.entry_id
)
Loading
Loading