feat: add user-agent (#1804)

This commit is contained in:
Li Shuzhen
2026-09-28 13:59:57 +08:00
committed by GitHub
parent a253918896
commit 7201cf2277
6 changed files with 77 additions and 6 deletions

View File

@@ -92,6 +92,7 @@ from .miot.const import (
MIHOME_CERT_EXPIRE_MARGIN MIHOME_CERT_EXPIRE_MARGIN
) )
from .miot.miot_cloud import MIoTHttpClient, MIoTOauthClient from .miot.miot_cloud import MIoTHttpClient, MIoTOauthClient
from .miot.common import get_system_info_str
from .miot.miot_storage import MIoTStorage, MIoTCert from .miot.miot_storage import MIoTStorage, MIoTCert
from .miot.miot_mdns import MipsService from .miot.miot_mdns import MipsService
from .miot.web_pages import oauth_redirect_page from .miot.web_pages import oauth_redirect_page
@@ -175,6 +176,7 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._oauth_redirect_url_full = '' self._oauth_redirect_url_full = ''
self._miot_oauth = None self._miot_oauth = None
self._miot_http = None self._miot_http = None
self._system_info = ''
self._cc_home_info = {} self._cc_home_info = {}
self._cc_home_list_show = {} self._cc_home_list_show = {}
@@ -438,11 +440,13 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.info( _LOGGER.info(
'async_step_oauth, redirect_url: %s', 'async_step_oauth, redirect_url: %s',
self._oauth_redirect_url_full) self._oauth_redirect_url_full)
self._system_info = await get_system_info_str(self.hass)
miot_oauth = MIoTOauthClient( miot_oauth = MIoTOauthClient(
client_id=OAUTH2_CLIENT_ID, client_id=OAUTH2_CLIENT_ID,
redirect_url=self._oauth_redirect_url_full, redirect_url=self._oauth_redirect_url_full,
cloud_server=self._cloud_server, cloud_server=self._cloud_server,
uuid=self._uuid, uuid=self._uuid,
system_info=self._system_info,
loop=self._main_loop) loop=self._main_loop)
self._cc_oauth_auth_url = miot_oauth.gen_auth_url( self._cc_oauth_auth_url = miot_oauth.gen_auth_url(
redirect_url=self._oauth_redirect_url_full) redirect_url=self._oauth_redirect_url_full)
@@ -520,7 +524,9 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._miot_http = MIoTHttpClient( self._miot_http = MIoTHttpClient(
cloud_server=self._cloud_server, cloud_server=self._cloud_server,
client_id=OAUTH2_CLIENT_ID, client_id=OAUTH2_CLIENT_ID,
access_token=auth_info['access_token']) access_token=auth_info['access_token'],
uuid=self._uuid,
system_info=self._system_info)
else: else:
self._miot_http.update_http_header( self._miot_http.update_http_header(
cloud_server=self._cloud_server, cloud_server=self._cloud_server,
@@ -1275,10 +1281,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
'get_access_token, %s, %s', err, traceback.format_exc()) 'get_access_token, %s, %s', err, traceback.format_exc())
raise MIoTConfigError('get_token_error') from err raise MIoTConfigError('get_token_error') from err
# Check uid # Check uid
system_info = await get_system_info_str(self.hass)
m_http: MIoTHttpClient = MIoTHttpClient( m_http: MIoTHttpClient = MIoTHttpClient(
cloud_server=self._cloud_server, cloud_server=self._cloud_server,
client_id=OAUTH2_CLIENT_ID, client_id=OAUTH2_CLIENT_ID,
access_token=auth_info['access_token'], access_token=auth_info['access_token'],
uuid=self._entry_data['uuid'],
system_info=system_info,
loop=self._main_loop) loop=self._main_loop)
if await m_http.get_uid_async() != self._uid: if await m_http.get_uid_async() != self._uid:
raise AbortFlow('inconsistent_account') raise AbortFlow('inconsistent_account')

View File

@@ -186,3 +186,19 @@ class MIoTHttp:
ev_loop = loop or asyncio.get_running_loop() ev_loop = loop or asyncio.get_running_loop()
return await ev_loop.run_in_executor( return await ev_loop.run_in_executor(
None, MIoTHttp.post, url, data, headers) None, MIoTHttp.post, url, data, headers)
async def get_system_info_str(hass) -> str:
"""Get system info string for User-Agent."""
# pylint: disable=import-outside-toplevel
from homeassistant.helpers.system_info import async_get_system_info
info = await async_get_system_info(hass)
version = info['version']
os_name = info['os_name']
os_version = info['os_version']
arch = info['arch']
installation_type = info['installation_type']
return (
f'hass/{version} ({os_name}/{os_version};'
f' {arch}; {installation_type})'
)

View File

@@ -45,7 +45,14 @@ off Xiaomi or its affiliates' products.
Constants. Constants.
""" """
import json
from pathlib import Path
DOMAIN: str = 'xiaomi_home' DOMAIN: str = 'xiaomi_home'
INTEGRATION_VERSION: str = json.loads(
(Path(__file__).parent.parent / 'manifest.json').read_text()
)['version']
DEFAULT_NAME: str = 'Xiaomi Home' DEFAULT_NAME: str = 'Xiaomi Home'
DEFAULT_NICK_NAME: str = 'Xiaomi' DEFAULT_NICK_NAME: str = 'Xiaomi'

View File

@@ -59,7 +59,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
# pylint: disable=relative-beyond-top-level # pylint: disable=relative-beyond-top-level
from .common import MIoTMatcher, slugify_did from .common import MIoTMatcher, get_system_info_str, slugify_did
from .const import ( from .const import (
DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN, DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,
MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL, MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,
@@ -189,6 +189,7 @@ class MIoTClient:
storage: MIoTStorage, storage: MIoTStorage,
mips_service: MipsService, mips_service: MipsService,
miot_lan: MIoTLan, miot_lan: MIoTLan,
system_info: str = '',
loop: Optional[asyncio.AbstractEventLoop] = None) -> None: loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
# MUST run in a running event loop # MUST run in a running event loop
self._main_loop = loop or asyncio.get_running_loop() self._main_loop = loop or asyncio.get_running_loop()
@@ -207,6 +208,7 @@ class MIoTClient:
self._entry_data = entry_data self._entry_data = entry_data
self._uid = entry_data['uid'] self._uid = entry_data['uid']
self._cloud_server = entry_data['cloud_server'] self._cloud_server = entry_data['cloud_server']
self._system_info = system_info
self._ctrl_mode = CtrlMode.load( self._ctrl_mode = CtrlMode.load(
entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)) entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE))
self._network = network self._network = network
@@ -285,12 +287,15 @@ class MIoTClient:
redirect_url=self._entry_data['oauth_redirect_url'], redirect_url=self._entry_data['oauth_redirect_url'],
cloud_server=self._cloud_server, cloud_server=self._cloud_server,
uuid=self._entry_data["uuid"], uuid=self._entry_data["uuid"],
system_info=self._system_info,
loop=self._main_loop) loop=self._main_loop)
# MIoT http client instance # MIoT http client instance
self._http = MIoTHttpClient( self._http = MIoTHttpClient(
cloud_server=self._cloud_server, cloud_server=self._cloud_server,
client_id=OAUTH2_CLIENT_ID, client_id=OAUTH2_CLIENT_ID,
access_token=self._user_config['auth_info']['access_token'], access_token=self._user_config['auth_info']['access_token'],
uuid=self._entry_data['uuid'],
system_info=self._system_info,
loop=self._main_loop) loop=self._main_loop)
# MIoT cert client # MIoT cert client
self._cert = MIoTCert( self._cert = MIoTCert(
@@ -2041,6 +2046,7 @@ async def get_miot_instance_async(
hass.data[DOMAIN]['miot_lan'] = miot_lan hass.data[DOMAIN]['miot_lan'] = miot_lan
_LOGGER.info('create miot_lan instance') _LOGGER.info('create miot_lan instance')
# MIoT client # MIoT client
system_info = await get_system_info_str(hass)
miot_client = MIoTClient( miot_client = MIoTClient(
entry_id=entry_id, entry_id=entry_id,
entry_data=entry_data, entry_data=entry_data,
@@ -2048,6 +2054,7 @@ async def get_miot_instance_async(
storage=storage, storage=storage,
mips_service=mips_service, mips_service=mips_service,
miot_lan=miot_lan, miot_lan=miot_lan,
system_info=system_info,
loop=loop loop=loop
) )
miot_client.persistent_notify = persistent_notify miot_client.persistent_notify = persistent_notify

View File

@@ -61,6 +61,7 @@ from .common import calc_group_id
from .const import ( from .const import (
UNSUPPORTED_MODELS, UNSUPPORTED_MODELS,
DEFAULT_OAUTH2_API_HOST, DEFAULT_OAUTH2_API_HOST,
INTEGRATION_VERSION,
MIHOME_HTTP_API_TIMEOUT, MIHOME_HTTP_API_TIMEOUT,
OAUTH2_AUTH_URL) OAUTH2_AUTH_URL)
from .miot_error import MIoTErrorCode, MIoTHttpError, MIoTOauthError from .miot_error import MIoTErrorCode, MIoTHttpError, MIoTOauthError
@@ -78,11 +79,13 @@ class MIoTOauthClient:
_client_id: int _client_id: int
_redirect_url: str _redirect_url: str
_device_id: str _device_id: str
_user_agent: str
_state: str _state: str
def __init__( def __init__(
self, client_id: str, redirect_url: str, cloud_server: str, self, client_id: str, redirect_url: str, cloud_server: str,
uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None uuid: str, system_info: str = '',
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None: ) -> None:
self._main_loop = loop or asyncio.get_running_loop() self._main_loop = loop or asyncio.get_running_loop()
if client_id is None or client_id.strip() == '': if client_id is None or client_id.strip() == '':
@@ -101,6 +104,10 @@ class MIoTOauthClient:
else: else:
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}' self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
self._device_id = f'ha.{uuid}' self._device_id = f'ha.{uuid}'
self._user_agent = (
f'ha_xiaomi_home/{INTEGRATION_VERSION}'
f' {system_info}'
f' client/{self._device_id}')
self._state = hashlib.sha1( self._state = hashlib.sha1(
f'd={self._device_id}'.encode('utf-8')).hexdigest() f'd={self._device_id}'.encode('utf-8')).hexdigest()
self._session = aiohttp.ClientSession(loop=self._main_loop) self._session = aiohttp.ClientSession(loop=self._main_loop)
@@ -161,7 +168,10 @@ class MIoTOauthClient:
http_res = await self._session.get( http_res = await self._session.get(
url=f'https://{self._oauth_host}/app/v2/ha/oauth/get_token', url=f'https://{self._oauth_host}/app/v2/ha/oauth/get_token',
params={'data': json.dumps(data)}, params={'data': json.dumps(data)},
headers={'content-type': 'application/x-www-form-urlencoded'}, headers={
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': self._user_agent,
},
timeout=MIHOME_HTTP_API_TIMEOUT timeout=MIHOME_HTTP_API_TIMEOUT
) )
if http_res.status == 401: if http_res.status == 401:
@@ -239,12 +249,14 @@ class MIoTHttpClient:
_base_url: str _base_url: str
_client_id: str _client_id: str
_access_token: str _access_token: str
_user_agent: str
_get_prop_timer: Optional[asyncio.TimerHandle] _get_prop_timer: Optional[asyncio.TimerHandle]
_get_prop_list: dict[str, dict] _get_prop_list: dict[str, dict]
def __init__( def __init__(
self, cloud_server: str, client_id: str, access_token: str, self, cloud_server: str, client_id: str, access_token: str,
uuid: str, system_info: str = '',
loop: Optional[asyncio.AbstractEventLoop] = None loop: Optional[asyncio.AbstractEventLoop] = None
) -> None: ) -> None:
self._main_loop = loop or asyncio.get_running_loop() self._main_loop = loop or asyncio.get_running_loop()
@@ -260,9 +272,14 @@ class MIoTHttpClient:
not isinstance(cloud_server, str) not isinstance(cloud_server, str)
or not isinstance(client_id, str) or not isinstance(client_id, str)
or not isinstance(access_token, str) or not isinstance(access_token, str)
or not isinstance(uuid, str)
): ):
raise MIoTHttpError('invalid params') raise MIoTHttpError('invalid params')
self._user_agent = (
f'ha_xiaomi_home/{INTEGRATION_VERSION}'
f' {system_info}'
f' client/ha.{uuid}')
self.update_http_header( self.update_http_header(
cloud_server=cloud_server, client_id=client_id, cloud_server=cloud_server, client_id=client_id,
access_token=access_token) access_token=access_token)
@@ -303,6 +320,7 @@ class MIoTHttpClient:
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': f'Bearer{self._access_token}', 'Authorization': f'Bearer{self._access_token}',
'X-Client-AppId': self._client_id, 'X-Client-AppId': self._client_id,
'User-Agent': self._user_agent,
} }
# pylint: disable=unused-private-member # pylint: disable=unused-private-member
@@ -367,7 +385,10 @@ class MIoTHttpClient:
url='https://open.account.xiaomi.com/user/profile', url='https://open.account.xiaomi.com/user/profile',
params={ params={
'clientId': self._client_id, 'token': self._access_token}, 'clientId': self._client_id, 'token': self._access_token},
headers={'content-type': 'application/x-www-form-urlencoded'}, headers={
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': self._user_agent,
},
timeout=MIHOME_HTTP_API_TIMEOUT timeout=MIHOME_HTTP_API_TIMEOUT
) )

View File

@@ -4,7 +4,7 @@ import logging
import random import random
import shutil import shutil
import pytest import pytest
from os import path, makedirs from os import path, makedirs, remove
from uuid import uuid4 from uuid import uuid4
TEST_ROOT_PATH: str = path.dirname(path.abspath(__file__)) TEST_ROOT_PATH: str = path.dirname(path.abspath(__file__))
@@ -57,6 +57,12 @@ def load_py_file():
file_name), file_name),
path.join(TEST_FILES_PATH, file_name)) path.join(TEST_FILES_PATH, file_name))
_LOGGER.info('\nloaded test py files, %s', file_list) _LOGGER.info('\nloaded test py files, %s', file_list)
# Copy manifest.json to test folder (parent of miot/)
shutil.copyfile(
path.join(
TEST_ROOT_PATH, '../custom_components/xiaomi_home/manifest.json'),
path.join(TEST_ROOT_PATH, 'manifest.json'))
_LOGGER.info('loaded manifest.json')
# Copy spec files to test folder # Copy spec files to test folder
shutil.copytree( shutil.copytree(
src=path.join( src=path.join(
@@ -89,6 +95,11 @@ def load_py_file():
shutil.rmtree(TEST_FILES_PATH) shutil.rmtree(TEST_FILES_PATH)
print('\nremoved test files, ', TEST_FILES_PATH) print('\nremoved test files, ', TEST_FILES_PATH)
manifest_path = path.join(TEST_ROOT_PATH, 'manifest.json')
if path.exists(manifest_path):
remove(manifest_path)
print('removed test manifest.json, ', manifest_path)
if path.exists(TEST_CACHE_PATH): if path.exists(TEST_CACHE_PATH):
shutil.rmtree(TEST_CACHE_PATH) shutil.rmtree(TEST_CACHE_PATH)
print('removed test cache, ', TEST_CACHE_PATH) print('removed test cache, ', TEST_CACHE_PATH)