mirror of
https://github.com/XiaoMi/ha_xiaomi_home.git
synced 2026-01-14 21:40:42 +08:00
Merge branch 'XiaoMi:main' into main
This commit is contained in:
commit
cc7cf52b3f
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v0.1.5b1
|
||||
This version will cause some Xiaomi routers that do not support access (#564) to become unavailable. You can update the device list in the configuration or delete it manually.
|
||||
### Added
|
||||
- Fan entity support direction ctrl [#556](https://github.com/XiaoMi/ha_xiaomi_home/pull/556)
|
||||
### Changed
|
||||
- Filter miwifi.* devices and xiaomi.router.rd03 [#564](https://github.com/XiaoMi/ha_xiaomi_home/pull/564)
|
||||
### Fixed
|
||||
- Fix multi ha instance login [#560](https://github.com/XiaoMi/ha_xiaomi_home/pull/560)
|
||||
- Fix fan speed [#464](https://github.com/XiaoMi/ha_xiaomi_home/pull/464)
|
||||
- The number of profile models updated from 660 to 823. [#583](https://github.com/XiaoMi/ha_xiaomi_home/pull/583)
|
||||
|
||||
## v0.1.5b0
|
||||
### Added
|
||||
- Add missing parameter state_class [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101)
|
||||
|
||||
@ -351,7 +351,7 @@ The instance code is the code of the MIoT-Spec-V2 instance, which is in the form
|
||||
```
|
||||
service:<siid> # service
|
||||
service:<siid>:property:<piid> # property
|
||||
service:<siid>:property:<piid>:valuelist:<value> # the value in value-list of a property
|
||||
service:<siid>:property:<piid>:valuelist:<index> # The index of a value in the value-list of a property
|
||||
service:<siid>:event:<eiid> # event
|
||||
service:<siid>:action:<aiid> # action
|
||||
```
|
||||
|
||||
@ -308,3 +308,43 @@ async def async_remove_entry(
|
||||
await miot_cert.remove_user_cert_async()
|
||||
await miot_cert.remove_user_key_async()
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
device_entry: device_registry.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove the device."""
|
||||
miot_client: MIoTClient = await get_miot_instance_async(
|
||||
hass=hass, entry_id=config_entry.entry_id)
|
||||
|
||||
if len(device_entry.identifiers) != 1:
|
||||
_LOGGER.error(
|
||||
'remove device failed, invalid identifiers, %s, %s',
|
||||
device_entry.id, device_entry.identifiers)
|
||||
return False
|
||||
identifiers = list(device_entry.identifiers)[0]
|
||||
if identifiers[0] != DOMAIN:
|
||||
_LOGGER.error(
|
||||
'remove device failed, invalid domain, %s, %s',
|
||||
device_entry.id, device_entry.identifiers)
|
||||
return False
|
||||
device_info = identifiers[1].split('_')
|
||||
if len(device_info) != 2:
|
||||
_LOGGER.error(
|
||||
'remove device failed, invalid device info, %s, %s',
|
||||
device_entry.id, device_entry.identifiers)
|
||||
return False
|
||||
did = device_info[1]
|
||||
if did not in miot_client.device_list:
|
||||
_LOGGER.error(
|
||||
'remove device failed, device not found, %s, %s',
|
||||
device_entry.id, device_entry.identifiers)
|
||||
return False
|
||||
# Remove device
|
||||
await miot_client.remove_device_async(did)
|
||||
device_registry.async_get(hass).async_remove_device(device_entry.id)
|
||||
_LOGGER.info(
|
||||
'remove device, %s, %s, %s', device_info[0], did, device_entry.id)
|
||||
return True
|
||||
|
||||
@ -68,6 +68,7 @@ from homeassistant.components.webhook import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.instance_id import async_get
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .miot.const import (
|
||||
@ -90,7 +91,8 @@ from .miot.miot_cloud import MIoTHttpClient, MIoTOauthClient
|
||||
from .miot.miot_storage import MIoTStorage, MIoTCert
|
||||
from .miot.miot_mdns import MipsService
|
||||
from .miot.web_pages import oauth_redirect_page
|
||||
from .miot.miot_error import MIoTConfigError, MIoTError, MIoTOauthError
|
||||
from .miot.miot_error import (
|
||||
MIoTConfigError, MIoTError, MIoTErrorCode, MIoTOauthError)
|
||||
from .miot.miot_i18n import MIoTI18n
|
||||
from .miot.miot_network import MIoTNetwork
|
||||
from .miot.miot_client import MIoTClient, get_miot_instance_async
|
||||
@ -247,6 +249,13 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input:
|
||||
self._cloud_server = user_input.get(
|
||||
'cloud_server', self._cloud_server)
|
||||
# Gen instance uuid
|
||||
ha_uuid = await async_get(self.hass)
|
||||
if not ha_uuid:
|
||||
raise AbortFlow(reason='ha_uuid_get_failed')
|
||||
self._uuid = hashlib.sha256(
|
||||
f'{ha_uuid}.{self._virtual_did}.{self._cloud_server}'.encode(
|
||||
'utf-8')).hexdigest()[:32]
|
||||
self._integration_language = user_input.get(
|
||||
'integration_language', DEFAULT_INTEGRATION_LANGUAGE)
|
||||
self._miot_i18n = MIoTI18n(
|
||||
@ -415,15 +424,17 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
miot_oauth = MIoTOauthClient(
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
redirect_url=self._oauth_redirect_url_full,
|
||||
cloud_server=self._cloud_server
|
||||
)
|
||||
state = str(secrets.randbits(64))
|
||||
self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = state
|
||||
cloud_server=self._cloud_server,
|
||||
uuid=self._uuid,
|
||||
loop=self._main_loop)
|
||||
self._cc_oauth_auth_url = miot_oauth.gen_auth_url(
|
||||
redirect_url=self._oauth_redirect_url_full, state=state)
|
||||
redirect_url=self._oauth_redirect_url_full)
|
||||
self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = (
|
||||
miot_oauth.state)
|
||||
self.hass.data[DOMAIN][self._virtual_did]['i18n'] = (
|
||||
self._miot_i18n)
|
||||
_LOGGER.info(
|
||||
'async_step_oauth, oauth_url: %s',
|
||||
self._cc_oauth_auth_url)
|
||||
'async_step_oauth, oauth_url: %s', self._cc_oauth_auth_url)
|
||||
webhook_async_unregister(
|
||||
self.hass, webhook_id=self._virtual_did)
|
||||
webhook_async_register(
|
||||
@ -498,11 +509,6 @@ class XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=auth_info['access_token'])
|
||||
self._auth_info = auth_info
|
||||
# Gen uuid
|
||||
self._uuid = hashlib.sha256(
|
||||
f'{self._virtual_did}.{auth_info["access_token"]}'.encode(
|
||||
'utf-8')
|
||||
).hexdigest()[:32]
|
||||
try:
|
||||
self._nick_name = (
|
||||
await self._miot_http.get_user_info_async() or {}
|
||||
@ -1145,15 +1151,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
async def async_step_oauth(self, user_input=None):
|
||||
try:
|
||||
if self._cc_task_oauth is None:
|
||||
state = str(secrets.randbits(64))
|
||||
self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = state
|
||||
self._miot_oauth.set_redirect_url(
|
||||
redirect_url=self._oauth_redirect_url_full)
|
||||
self._cc_oauth_auth_url = self._miot_oauth.gen_auth_url(
|
||||
redirect_url=self._oauth_redirect_url_full, state=state)
|
||||
redirect_url=self._oauth_redirect_url_full)
|
||||
self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = (
|
||||
self._miot_oauth.state)
|
||||
self.hass.data[DOMAIN][self._virtual_did]['i18n'] = (
|
||||
self._miot_i18n)
|
||||
_LOGGER.info(
|
||||
'async_step_oauth, oauth_url: %s',
|
||||
self._cc_oauth_auth_url)
|
||||
'async_step_oauth, oauth_url: %s', self._cc_oauth_auth_url)
|
||||
webhook_async_unregister(
|
||||
self.hass, webhook_id=self._virtual_did)
|
||||
webhook_async_register(
|
||||
@ -1967,29 +1972,61 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
async def _handle_oauth_webhook(hass, webhook_id, request):
|
||||
"""Webhook to handle oauth2 callback."""
|
||||
# pylint: disable=inconsistent-quotes
|
||||
i18n: MIoTI18n = hass.data[DOMAIN][webhook_id].get('i18n', None)
|
||||
try:
|
||||
data = dict(request.query)
|
||||
if data.get('code', None) is None or data.get('state', None) is None:
|
||||
raise MIoTConfigError('invalid oauth code')
|
||||
raise MIoTConfigError(
|
||||
'invalid oauth code or state',
|
||||
MIoTErrorCode.CODE_CONFIG_INVALID_INPUT)
|
||||
|
||||
if data['state'] != hass.data[DOMAIN][webhook_id]['oauth_state']:
|
||||
raise MIoTConfigError(
|
||||
f'invalid oauth state, '
|
||||
f'{hass.data[DOMAIN][webhook_id]["oauth_state"]}, '
|
||||
f'{data["state"]}')
|
||||
f'inconsistent state, '
|
||||
f'{hass.data[DOMAIN][webhook_id]["oauth_state"]}!='
|
||||
f'{data["state"]}', MIoTErrorCode.CODE_CONFIG_INVALID_STATE)
|
||||
|
||||
fut_oauth_code: asyncio.Future = hass.data[DOMAIN][webhook_id].pop(
|
||||
'fut_oauth_code', None)
|
||||
fut_oauth_code.set_result(data['code'])
|
||||
_LOGGER.info('webhook code: %s', data['code'])
|
||||
|
||||
success_trans: dict = {}
|
||||
if i18n:
|
||||
success_trans = i18n.translate(
|
||||
'oauth2.success') or {} # type: ignore
|
||||
# Delete
|
||||
del hass.data[DOMAIN][webhook_id]['oauth_state']
|
||||
del hass.data[DOMAIN][webhook_id]['i18n']
|
||||
return web.Response(
|
||||
body=oauth_redirect_page(
|
||||
hass.config.language, 'success'), content_type='text/html')
|
||||
body=await oauth_redirect_page(
|
||||
title=success_trans.get('title', 'Success'),
|
||||
content=success_trans.get(
|
||||
'content', (
|
||||
'Please close this page and return to the account '
|
||||
'authentication page to click NEXT')),
|
||||
button=success_trans.get('button', 'Close Page'),
|
||||
success=True,
|
||||
), content_type='text/html')
|
||||
|
||||
except MIoTConfigError:
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
fail_trans: dict = {}
|
||||
err_msg: str = str(err)
|
||||
if i18n:
|
||||
if isinstance(err, MIoTConfigError):
|
||||
err_msg = i18n.translate(
|
||||
f'oauth2.error_msg.{err.code.value}'
|
||||
) or err.message # type: ignore
|
||||
fail_trans = i18n.translate('oauth2.fail') or {} # type: ignore
|
||||
return web.Response(
|
||||
body=oauth_redirect_page(hass.config.language, 'fail'),
|
||||
body=await oauth_redirect_page(
|
||||
title=fail_trans.get('title', 'Authentication Failed'),
|
||||
content=str(fail_trans.get('content', (
|
||||
'{error_msg}, Please close this page and return to the '
|
||||
'account authentication page to click the authentication '
|
||||
'link again.'))).replace('{error_msg}', err_msg),
|
||||
button=fail_trans.get('button', 'Close Page'),
|
||||
success=False),
|
||||
content_type='text/html')
|
||||
|
||||
|
||||
|
||||
@ -55,7 +55,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.util.percentage import (
|
||||
percentage_to_ranged_value,
|
||||
ranged_value_to_percentage
|
||||
ranged_value_to_percentage,
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item
|
||||
)
|
||||
|
||||
from .miot.miot_spec import MIoTSpecProperty
|
||||
@ -89,10 +91,15 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
_prop_fan_level: Optional[MIoTSpecProperty]
|
||||
_prop_mode: Optional[MIoTSpecProperty]
|
||||
_prop_horizontal_swing: Optional[MIoTSpecProperty]
|
||||
_prop_wind_reverse: Optional[MIoTSpecProperty]
|
||||
_prop_wind_reverse_forward: Any
|
||||
_prop_wind_reverse_reverse: Any
|
||||
|
||||
_speed_min: Optional[int]
|
||||
_speed_max: Optional[int]
|
||||
_speed_step: Optional[int]
|
||||
_speed_min: int
|
||||
_speed_max: int
|
||||
_speed_step: int
|
||||
_speed_names: Optional[list]
|
||||
_speed_name_map: Optional[dict[int, str]]
|
||||
_mode_list: Optional[dict[Any, Any]]
|
||||
|
||||
def __init__(
|
||||
@ -101,15 +108,22 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
"""Initialize the Fan."""
|
||||
super().__init__(miot_device=miot_device, entity_data=entity_data)
|
||||
self._attr_preset_modes = []
|
||||
self._attr_current_direction = None
|
||||
self._attr_supported_features = FanEntityFeature(0)
|
||||
|
||||
self._prop_on = None
|
||||
self._prop_fan_level = None
|
||||
self._prop_mode = None
|
||||
self._prop_horizontal_swing = None
|
||||
self._prop_wind_reverse = None
|
||||
self._prop_wind_reverse_forward = None
|
||||
self._prop_wind_reverse_reverse = None
|
||||
self._speed_min = 65535
|
||||
self._speed_max = 0
|
||||
self._speed_step = 1
|
||||
self._speed_names = []
|
||||
self._speed_name_map = {}
|
||||
|
||||
self._mode_list = None
|
||||
|
||||
# properties
|
||||
@ -124,7 +138,8 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
self._speed_min = prop.value_range['min']
|
||||
self._speed_max = prop.value_range['max']
|
||||
self._speed_step = prop.value_range['step']
|
||||
self._attr_speed_count = self._speed_max - self._speed_min+1
|
||||
self._attr_speed_count = int((
|
||||
self._speed_max - self._speed_min)/self._speed_step)+1
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
self._prop_fan_level = prop
|
||||
elif (
|
||||
@ -133,10 +148,13 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
and prop.value_list
|
||||
):
|
||||
# Fan level with value-list
|
||||
for item in prop.value_list:
|
||||
self._speed_min = min(self._speed_min, item['value'])
|
||||
self._speed_max = max(self._speed_max, item['value'])
|
||||
self._attr_speed_count = self._speed_max - self._speed_min+1
|
||||
# Fan level with value-range is prior to fan level with
|
||||
# value-list when a fan has both fan level properties.
|
||||
self._speed_name_map = {
|
||||
item['value']: item['description']
|
||||
for item in prop.value_list}
|
||||
self._speed_names = list(self._speed_name_map.values())
|
||||
self._attr_speed_count = len(prop.value_list)
|
||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||
self._prop_fan_level = prop
|
||||
elif prop.name == 'mode':
|
||||
@ -156,6 +174,30 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
elif prop.name == 'horizontal-swing':
|
||||
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||
self._prop_horizontal_swing = prop
|
||||
elif prop.name == 'wind-reverse':
|
||||
if prop.format_ == 'bool':
|
||||
self._prop_wind_reverse_forward = False
|
||||
self._prop_wind_reverse_reverse = True
|
||||
elif (
|
||||
isinstance(prop.value_list, list)
|
||||
and prop.value_list
|
||||
):
|
||||
for item in prop.value_list:
|
||||
if item['name'].lower() in {'foreward'}:
|
||||
self._prop_wind_reverse_forward = item['value']
|
||||
elif item['name'].lower() in {
|
||||
'reversal', 'reverse'}:
|
||||
self._prop_wind_reverse_reverse = item['value']
|
||||
if (
|
||||
self._prop_wind_reverse_forward is None
|
||||
or self._prop_wind_reverse_reverse is None
|
||||
):
|
||||
# NOTICE: Value may be 0 or False
|
||||
_LOGGER.info(
|
||||
'invalid wind-reverse, %s', self.entity_id)
|
||||
continue
|
||||
self._attr_supported_features |= FanEntityFeature.DIRECTION
|
||||
self._prop_wind_reverse = prop
|
||||
|
||||
def __get_mode_description(self, key: int) -> Optional[str]:
|
||||
if self._mode_list is None:
|
||||
@ -182,9 +224,19 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
# percentage
|
||||
if percentage:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=int(percentage*self._attr_speed_count/100))
|
||||
if self._speed_names:
|
||||
speed = percentage_to_ordered_list_item(
|
||||
self._speed_names, percentage)
|
||||
speed_value = self.get_map_value(
|
||||
map_=self._speed_name_map, description=speed)
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level, value=speed_value)
|
||||
else:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=int(percentage_to_ranged_value(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
percentage=percentage)))
|
||||
# preset_mode
|
||||
if preset_mode:
|
||||
await self.set_property_async(
|
||||
@ -202,11 +254,19 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the percentage of the fan speed."""
|
||||
if percentage > 0:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=int(percentage_to_ranged_value(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
percentage=percentage)))
|
||||
if self._speed_names:
|
||||
speed = percentage_to_ordered_list_item(
|
||||
self._speed_names, percentage)
|
||||
speed_value = self.get_map_value(
|
||||
map_=self._speed_name_map, description=speed)
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level, value=speed_value)
|
||||
else:
|
||||
await self.set_property_async(
|
||||
prop=self._prop_fan_level,
|
||||
value=int(percentage_to_ranged_value(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
percentage=percentage)))
|
||||
if not self.is_on:
|
||||
# If the fan is off, turn it on.
|
||||
await self.set_property_async(prop=self._prop_on, value=True)
|
||||
@ -221,6 +281,14 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
"""Set the direction of the fan."""
|
||||
if not self._prop_wind_reverse:
|
||||
return
|
||||
await self.set_property_async(
|
||||
prop=self._prop_wind_reverse,
|
||||
value=(
|
||||
self._prop_wind_reverse_reverse
|
||||
if self.current_direction == 'reverse'
|
||||
else self._prop_wind_reverse_forward))
|
||||
|
||||
async def async_oscillate(self, oscillating: bool) -> None:
|
||||
"""Oscillate the fan."""
|
||||
@ -242,13 +310,28 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
key=self.get_prop_value(prop=self._prop_mode))
|
||||
if self._prop_mode else None)
|
||||
|
||||
@property
|
||||
def current_direction(self) -> Optional[str]:
|
||||
"""Return the current direction of the fan."""
|
||||
if not self._prop_wind_reverse:
|
||||
return None
|
||||
return 'reverse' if self.get_prop_value(
|
||||
prop=self._prop_wind_reverse
|
||||
) == self._prop_wind_reverse_reverse else 'forward'
|
||||
|
||||
@property
|
||||
def percentage(self) -> Optional[int]:
|
||||
"""Return the current percentage of the fan speed."""
|
||||
fan_level = self.get_prop_value(prop=self._prop_fan_level)
|
||||
return ranged_value_to_percentage(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
value=fan_level) if fan_level else None
|
||||
if fan_level is None:
|
||||
return None
|
||||
if self._speed_names:
|
||||
return ordered_list_item_to_percentage(
|
||||
self._speed_names, self._speed_name_map[fan_level])
|
||||
else:
|
||||
return ranged_value_to_percentage(
|
||||
low_high_range=(self._speed_min, self._speed_max),
|
||||
value=fan_level)
|
||||
|
||||
@property
|
||||
def oscillating(self) -> Optional[bool]:
|
||||
@ -257,8 +340,3 @@ class Fan(MIoTServiceEntity, FanEntity):
|
||||
self.get_prop_value(
|
||||
prop=self._prop_horizontal_swing)
|
||||
if self._prop_horizontal_swing else None)
|
||||
|
||||
@property
|
||||
def percentage_step(self) -> float:
|
||||
"""Return the step of the fan speed."""
|
||||
return self._speed_step
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
"cryptography",
|
||||
"psutil"
|
||||
],
|
||||
"version": "v0.1.5b0",
|
||||
"version": "v0.1.5b1",
|
||||
"zeroconf": [
|
||||
"_miot-central._tcp.local."
|
||||
]
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Schnittstelle nicht verfügbar"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Authentifizierung erfolgreich",
|
||||
"content": "Bitte schließen Sie diese Seite und kehren Sie zur Kontoauthentifizierungsseite zurück, um auf „Weiter“ zu klicken.",
|
||||
"button": "Schließen"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Authentifizierung fehlgeschlagen",
|
||||
"content": "{error_msg}, bitte schließen Sie diese Seite und kehren Sie zur Kontoauthentifizierungsseite zurück, um den Authentifizierungslink erneut zu klicken.",
|
||||
"button": "Schließen"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Ungültige Antwortparameter ('code' oder 'state' Feld ist leer)",
|
||||
"-10101": "Übergebenes 'state' Feld stimmt nicht überein"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Ungültige Authentifizierungsinformationen, Cloud-Verbindung nicht verfügbar, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen', um die Authentifizierung erneut durchzuführen",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Interface unavailable"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Authentication Successful",
|
||||
"content": "Please close this page and return to the account authentication page to click 'Next'.",
|
||||
"button": "Close"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Authentication Failed",
|
||||
"content": "{error_msg}, please close this page and return to the account authentication page to click the authentication link again.",
|
||||
"button": "Close"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Invalid response parameters ('code' or 'state' field is empty)",
|
||||
"-10101": "Passed-in 'state' field mismatch"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Authentication information is invalid, cloud link will be unavailable, please enter the Xiaomi Home integration page, click 'Options' to re-authenticate",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Interfaz no disponible"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Autenticación exitosa",
|
||||
"content": "Por favor, cierre esta página y regrese a la página de autenticación de la cuenta para hacer clic en 'Siguiente'.",
|
||||
"button": "Cerrar"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Autenticación fallida",
|
||||
"content": "{error_msg}, por favor, cierre esta página y regrese a la página de autenticación de la cuenta para hacer clic en el enlace de autenticación nuevamente.",
|
||||
"button": "Cerrar"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Parámetros de respuesta inválidos ('code' o 'state' está vacío)",
|
||||
"-10101": "El campo 'state' proporcionado no coincide"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "La información de autenticación es inválida, la conexión en la nube no estará disponible, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones' para volver a autenticar",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Interface non disponible"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Authentification réussie",
|
||||
"content": "Veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer sur 'Suivant'.",
|
||||
"button": "Fermer"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Échec de l'authentification",
|
||||
"content": "{error_msg}, veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer à nouveau sur le lien d'authentification.",
|
||||
"button": "Fermer"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Paramètres de réponse invalides ('code' ou 'state' est vide)",
|
||||
"-10101": "Le champ 'state' transmis ne correspond pas"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Informations d'authentification non valides, le lien cloud ne sera pas disponible, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur \"Options\" pour vous réauthentifier",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "インターフェースが利用できません"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "認証成功",
|
||||
"content": "このページを閉じて、アカウント認証ページに戻り、「次へ」をクリックしてください。",
|
||||
"button": "閉じる"
|
||||
},
|
||||
"fail": {
|
||||
"title": "認証失敗",
|
||||
"content": "{error_msg}、このページを閉じて、アカウント認証ページに戻り、再度認証リンクをクリックしてください。",
|
||||
"button": "閉じる"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "無効な応答パラメータ('code'または'state'フィールドが空です)",
|
||||
"-10101": "渡された'state'フィールドが一致しません"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "認証情報が無効です。クラウドリンクは利用できません。Xiaomi Home統合ページに入り、[オプション]をクリックして再認証してください",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Interface niet beschikbaar"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Authenticatie geslaagd",
|
||||
"content": "Sluit deze pagina en ga terug naar de accountauthenticatiepagina om op 'Volgende' te klikken.",
|
||||
"button": "Sluiten"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Authenticatie mislukt",
|
||||
"content": "{error_msg}, sluit deze pagina en ga terug naar de accountauthenticatiepagina om opnieuw op de authenticatielink te klikken.",
|
||||
"button": "Sluiten"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Ongeldige antwoordparameters ('code' of 'state' veld is leeg)",
|
||||
"-10101": "Doorgegeven 'state' veld komt niet overeen"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Authenticatie-informatie is ongeldig, cloudverbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Interface indisponível"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Autenticação bem-sucedida",
|
||||
"content": "Por favor, feche esta página e volte para a página de autenticação da conta para clicar em 'Próximo'.",
|
||||
"button": "Fechar"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Falha na autenticação",
|
||||
"content": "{error_msg}, por favor, feche esta página e volte para a página de autenticação da conta para clicar no link de autenticação novamente.",
|
||||
"button": "Fechar"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Parâmetros de resposta inválidos ('code' ou 'state' está vazio)",
|
||||
"-10101": "O campo 'state' fornecido não corresponde"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Informações de autenticação inválidas, a conexão com a nuvem estará indisponível. Vá para a página de integração do Xiaomi Home e clique em 'Opções' para reautenticar.",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Interface indisponível"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Autenticação bem-sucedida",
|
||||
"content": "Por favor, feche esta página e volte para a página de autenticação da conta para clicar em 'Seguinte'.",
|
||||
"button": "Fechar"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Falha na autenticação",
|
||||
"content": "{error_msg}, por favor, feche esta página e volte para a página de autenticação da conta para clicar no link de autenticação novamente.",
|
||||
"button": "Fechar"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Parâmetros de resposta inválidos ('code' ou 'state' está vazio)",
|
||||
"-10101": "O campo 'state' fornecido não corresponde"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Informações de autenticação inválidas, a conexão na nuvem ficará indisponível. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções' para autenticar novamente.",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "Интерфейс недоступен"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "Аутентификация успешна",
|
||||
"content": "Пожалуйста, закройте эту страницу и вернитесь на страницу аутентификации учетной записи, чтобы нажать 'Далее'.",
|
||||
"button": "Закрыть"
|
||||
},
|
||||
"fail": {
|
||||
"title": "Аутентификация не удалась",
|
||||
"content": "{error_msg}, пожалуйста, закройте эту страницу и вернитесь на страницу аутентификации учетной записи, чтобы снова нажать на ссылку аутентификации.",
|
||||
"button": "Закрыть"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "Недействительные параметры ответа ('code' или 'state' поле пусто)",
|
||||
"-10101": "Переданное поле 'state' не совпадает"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "Информация об аутентификации недействительна, облако будет недоступно, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции' для повторной аутентификации",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "接口不可用"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "认证成功",
|
||||
"content": "请关闭此页面,返回账号认证页面点击“下一步”",
|
||||
"button": "关闭"
|
||||
},
|
||||
"fail": {
|
||||
"title": "认证失败",
|
||||
"content": "{error_msg},请关闭此页面,返回账号认证页面重新点击认链接进行认证。",
|
||||
"button": "关闭"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "无效的响应参数(“code”或者“state”字段为空)",
|
||||
"-10101": "传入“state”字段不一致"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "认证信息失效,云端链路将不可用,请进入 Xiaomi Home 集成页面,点击“选项”重新认证",
|
||||
|
||||
@ -64,6 +64,22 @@
|
||||
"net_unavailable": "接口不可用"
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"success": {
|
||||
"title": "認證成功",
|
||||
"content": "請關閉此頁面,返回帳號認證頁面點擊“下一步”",
|
||||
"button": "關閉"
|
||||
},
|
||||
"fail": {
|
||||
"title": "認證失敗",
|
||||
"content": "{error_msg},請關閉此頁面,返回帳號認證頁面重新點擊認鏈接進行認證。",
|
||||
"button": "關閉"
|
||||
},
|
||||
"error_msg": {
|
||||
"-10100": "無效的響應參數(“code”或者“state”字段為空)",
|
||||
"-10101": "傳入的“state”字段不一致"
|
||||
}
|
||||
},
|
||||
"miot": {
|
||||
"client": {
|
||||
"invalid_oauth_info": "認證信息失效,雲端鏈路將不可用,請進入 Xiaomi Home 集成頁面,點擊“選項”重新認證",
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
ts: 1603967572
|
||||
1245.airpurifier.dl01:
|
||||
ts: 1607502661
|
||||
17216.magic_touch.d150:
|
||||
ts: 1575097876
|
||||
17216.magic_touch.d152:
|
||||
ts: 1575097876
|
||||
17216.massage.ec1266a:
|
||||
ts: 1615881124
|
||||
397.light.hallight:
|
||||
@ -56,6 +60,10 @@ bj352.airmonitor.m30:
|
||||
ts: 1686644541
|
||||
bj352.waterpuri.s100cm:
|
||||
ts: 1615795630
|
||||
bymiot.gateway.v1:
|
||||
ts: 1575097876
|
||||
bymiot.gateway.v2:
|
||||
ts: 1575097876
|
||||
cgllc.airmonitor.b1:
|
||||
ts: 1676339912
|
||||
cgllc.airmonitor.s1:
|
||||
@ -64,6 +72,8 @@ cgllc.clock.cgc1:
|
||||
ts: 1686644422
|
||||
cgllc.clock.dove:
|
||||
ts: 1619607474
|
||||
cgllc.gateway.s1:
|
||||
ts: 1575097876
|
||||
cgllc.magnet.hodor:
|
||||
ts: 1724329476
|
||||
cgllc.motion.cgpr1:
|
||||
@ -120,8 +130,14 @@ chuangmi.cateye.ipc018:
|
||||
ts: 1632735241
|
||||
chuangmi.cateye.ipc508:
|
||||
ts: 1633677521
|
||||
chuangmi.door.hmi508:
|
||||
ts: 1611733437
|
||||
chuangmi.door.hmi515:
|
||||
ts: 1640334316
|
||||
chuangmi.gateway.ipc011:
|
||||
ts: 1575097876
|
||||
chuangmi.ir.v2:
|
||||
ts: 1575097876
|
||||
chuangmi.lock.hmi501:
|
||||
ts: 1614742147
|
||||
chuangmi.lock.hmi501b01:
|
||||
@ -142,10 +158,18 @@ chuangmi.plug.v1:
|
||||
ts: 1621925183
|
||||
chuangmi.plug.v3:
|
||||
ts: 1644480255
|
||||
chuangmi.plug.vtl_v1:
|
||||
ts: 1575097876
|
||||
chuangmi.radio.v1:
|
||||
ts: 1531108800
|
||||
chuangmi.radio.v2:
|
||||
ts: 1531108800
|
||||
chuangmi.remote.h102a03:
|
||||
ts: 1575097876
|
||||
chuangmi.remote.h102c01:
|
||||
ts: 1575097876
|
||||
chuangmi.remote.v2:
|
||||
ts: 1575097876
|
||||
chunmi.cooker.eh1:
|
||||
ts: 1607339278
|
||||
chunmi.cooker.eh402:
|
||||
@ -204,6 +228,8 @@ dmaker.airfresh.t2017:
|
||||
ts: 1686731233
|
||||
dmaker.fan.p5:
|
||||
ts: 1655793784
|
||||
doco.fcb.docov001:
|
||||
ts: 1575097876
|
||||
dsm.lock.h3:
|
||||
ts: 1615283790
|
||||
dsm.lock.q3:
|
||||
@ -218,6 +244,30 @@ fawad.airrtc.fwd20011:
|
||||
ts: 1610607149
|
||||
fbs.airmonitor.pth02:
|
||||
ts: 1686644918
|
||||
fengmi.projector.fm05:
|
||||
ts: 1575097876
|
||||
fengmi.projector.fm15:
|
||||
ts: 1575097876
|
||||
fengmi.projector.fm154k:
|
||||
ts: 1575097876
|
||||
fengmi.projector.l166:
|
||||
ts: 1650352923
|
||||
fengmi.projector.l176:
|
||||
ts: 1649936204
|
||||
fengmi.projector.l246:
|
||||
ts: 1575097876
|
||||
fengmi.projector.m055:
|
||||
ts: 1652839826
|
||||
fengmi.projector.m055d:
|
||||
ts: 1654067980
|
||||
fengyu.intercom.beebird:
|
||||
ts: 1575097876
|
||||
fengyu.intercom.sharkv1:
|
||||
ts: 1575097876
|
||||
fotile.hood.emd1tmi:
|
||||
ts: 1607483642
|
||||
guoshi.other.sem01:
|
||||
ts: 1602662080
|
||||
hannto.printer.anise:
|
||||
ts: 1618989537
|
||||
hannto.printer.honey:
|
||||
@ -226,14 +276,26 @@ hannto.printer.honey1s:
|
||||
ts: 1614332725
|
||||
hfjh.fishbowl.v1:
|
||||
ts: 1615278556
|
||||
hhcc.bleflowerpot.v2:
|
||||
ts: 1575097876
|
||||
hhcc.plantmonitor.v1:
|
||||
ts: 1664163526
|
||||
hith.foot_bath.q2:
|
||||
ts: 1531108800
|
||||
hmpace.bracelet.v4:
|
||||
ts: 1575097876
|
||||
hmpace.scales.mibfs:
|
||||
ts: 1575097876
|
||||
hmpace.scales.miscale2:
|
||||
ts: 1575097876
|
||||
huohe.lock.m1:
|
||||
ts: 1635410938
|
||||
huoman.litter_box.co1:
|
||||
ts: 1687165034
|
||||
hutlon.lock.v0001:
|
||||
ts: 1634799698
|
||||
idelan.aircondition.g1:
|
||||
ts: 1575097876
|
||||
idelan.aircondition.v1:
|
||||
ts: 1614666973
|
||||
idelan.aircondition.v2:
|
||||
@ -248,14 +310,22 @@ ikea.light.led1537r6:
|
||||
ts: 1605162872
|
||||
ikea.light.led1545g12:
|
||||
ts: 1605162937
|
||||
ikea.light.led1546g12:
|
||||
ts: 1575097876
|
||||
ikea.light.led1623g12:
|
||||
ts: 1605163009
|
||||
ikea.light.led1649c5:
|
||||
ts: 1605163064
|
||||
ikea.light.led1650r5:
|
||||
ts: 1575097876
|
||||
imibar.cooker.mbihr3:
|
||||
ts: 1624620659
|
||||
imou99.camera.tp2:
|
||||
ts: 1531108800
|
||||
inovel.projector.me2:
|
||||
ts: 1575097876
|
||||
iracc.aircondition.d19:
|
||||
ts: 1609914362
|
||||
isa.camera.df3:
|
||||
ts: 1531108800
|
||||
isa.camera.hl5:
|
||||
@ -266,18 +336,34 @@ isa.camera.isc5:
|
||||
ts: 1531108800
|
||||
isa.camera.isc5c1:
|
||||
ts: 1621238175
|
||||
isa.camera.qf3:
|
||||
ts: 1575097876
|
||||
isa.cateye.hldb6:
|
||||
ts: 1575097876
|
||||
isa.magnet.dw2hl:
|
||||
ts: 1638274655
|
||||
jieman.magic_touch.js78:
|
||||
ts: 1575097876
|
||||
jiqid.mistory.ipen1:
|
||||
ts: 1575097876
|
||||
jiqid.mistory.pro:
|
||||
ts: 1531108800
|
||||
jiqid.mistory.v1:
|
||||
ts: 1531108800
|
||||
jiqid.mistudy.v2:
|
||||
ts: 1610612349
|
||||
jiqid.robot.cube:
|
||||
ts: 1575097876
|
||||
jiwu.lock.jwp01:
|
||||
ts: 1614752632
|
||||
jyaiot.cm.ccj01:
|
||||
ts: 1611824545
|
||||
k0918.toothbrush.kid01:
|
||||
ts: 1575097876
|
||||
kejia.airer.th001:
|
||||
ts: 1575097876
|
||||
ksmb.treadmill.k12:
|
||||
ts: 1575097876
|
||||
ksmb.treadmill.v1:
|
||||
ts: 1611211447
|
||||
ksmb.treadmill.v2:
|
||||
@ -390,6 +476,8 @@ loock.lock.xfvl10:
|
||||
ts: 1632814256
|
||||
loock.safe.v1:
|
||||
ts: 1619607755
|
||||
lumi.acpartner.mcn02:
|
||||
ts: 1655791626
|
||||
lumi.acpartner.v1:
|
||||
ts: 1531108800
|
||||
lumi.acpartner.v2:
|
||||
@ -462,6 +550,8 @@ lumi.lock.acn02:
|
||||
ts: 1623928631
|
||||
lumi.lock.acn03:
|
||||
ts: 1614752574
|
||||
lumi.lock.aq1:
|
||||
ts: 1612518044
|
||||
lumi.lock.bacn01:
|
||||
ts: 1614741699
|
||||
lumi.lock.bmcn02:
|
||||
@ -482,6 +572,8 @@ lumi.lock.mcn007:
|
||||
ts: 1650446757
|
||||
lumi.lock.mcn01:
|
||||
ts: 1679881881
|
||||
lumi.lock.v1:
|
||||
ts: 1575097876
|
||||
lumi.lock.wbmcn1:
|
||||
ts: 1619422072
|
||||
lumi.motion.bmgl01:
|
||||
@ -510,14 +602,20 @@ lumi.sensor_86sw1.v1:
|
||||
ts: 1609311038
|
||||
lumi.sensor_86sw2.v1:
|
||||
ts: 1608795035
|
||||
lumi.sensor_cube.aqgl01:
|
||||
ts: 1575097876
|
||||
lumi.sensor_ht.v1:
|
||||
ts: 1621239877
|
||||
lumi.sensor_magnet.aq2:
|
||||
ts: 1641112867
|
||||
lumi.sensor_magnet.v1:
|
||||
ts: 1606120416
|
||||
lumi.sensor_magnet.v2:
|
||||
ts: 1641113779
|
||||
lumi.sensor_motion.aq2:
|
||||
ts: 1676433994
|
||||
lumi.sensor_motion.v1:
|
||||
ts: 1605093075
|
||||
lumi.sensor_motion.v2:
|
||||
ts: 1672818550
|
||||
lumi.sensor_natgas.v1:
|
||||
@ -530,6 +628,8 @@ lumi.sensor_switch.aq2:
|
||||
ts: 1615256430
|
||||
lumi.sensor_switch.aq3:
|
||||
ts: 1607399487
|
||||
lumi.sensor_switch.v1:
|
||||
ts: 1606874434
|
||||
lumi.sensor_switch.v2:
|
||||
ts: 1609310683
|
||||
lumi.sensor_wleak.aq1:
|
||||
@ -574,6 +674,20 @@ miaomiaoce.sensor_ht.t1:
|
||||
ts: 1616057242
|
||||
miaomiaoce.sensor_ht.t2:
|
||||
ts: 1636603553
|
||||
miaomiaoce.thermo.t01:
|
||||
ts: 1575097876
|
||||
midea.aircondition.v1:
|
||||
ts: 1575097876
|
||||
midea.aircondition.xa1:
|
||||
ts: 1575097876
|
||||
midea.aircondition.xa2:
|
||||
ts: 1575097876
|
||||
midr.rv_mirror.m2:
|
||||
ts: 1575097876
|
||||
midr.rv_mirror.m5:
|
||||
ts: 1575097876
|
||||
midr.rv_mirror.v1:
|
||||
ts: 1575097876
|
||||
miir.aircondition.ir01:
|
||||
ts: 1531108800
|
||||
miir.aircondition.ir02:
|
||||
@ -612,6 +726,8 @@ minij.washer.v5:
|
||||
ts: 1622792196
|
||||
minij.washer.v8:
|
||||
ts: 1615777868
|
||||
minuo.tracker.lm001:
|
||||
ts: 1575097876
|
||||
miot.light.plato2:
|
||||
ts: 1685518142
|
||||
miot.light.plato3:
|
||||
@ -624,18 +740,32 @@ mmgg.feeder.snack:
|
||||
ts: 1607503182
|
||||
moyu.washer.s1hm:
|
||||
ts: 1624620888
|
||||
mrbond.airer.m0:
|
||||
ts: 1575097876
|
||||
mrbond.airer.m1pro:
|
||||
ts: 1646393746
|
||||
mrbond.airer.m1s:
|
||||
ts: 1646393874
|
||||
mrbond.airer.m1super:
|
||||
ts: 1575097876
|
||||
msj.f_washer.m1:
|
||||
ts: 1614914340
|
||||
mxiang.cateye.mdb10:
|
||||
ts: 1616140362
|
||||
mxiang.cateye.xmcatt1:
|
||||
ts: 1616140207
|
||||
nhy.airrtc.v1:
|
||||
ts: 1575097876
|
||||
ninebot.scooter.v1:
|
||||
ts: 1602662395
|
||||
ninebot.scooter.v6:
|
||||
ts: 1575097876
|
||||
nuwa.robot.minikiwi:
|
||||
ts: 1575097876
|
||||
nwt.derh.wdh318efw1:
|
||||
ts: 1611822375
|
||||
onemore.wifispeaker.sm4:
|
||||
ts: 1575097876
|
||||
opple.light.bydceiling:
|
||||
ts: 1608187619
|
||||
opple.light.fanlight:
|
||||
@ -646,6 +776,8 @@ opple.remote.5pb112:
|
||||
ts: 1627453840
|
||||
opple.remote.5pb113:
|
||||
ts: 1636599905
|
||||
orion.wifispeaker.cm1:
|
||||
ts: 1575097876
|
||||
ows.towel_w.mj1x0:
|
||||
ts: 1610604939
|
||||
philips.light.bceiling1:
|
||||
@ -696,6 +828,8 @@ pwzn.relay.apple:
|
||||
ts: 1611217196
|
||||
pwzn.relay.banana:
|
||||
ts: 1646647255
|
||||
qicyc.bike.tdp02z:
|
||||
ts: 1575097876
|
||||
qike.bhf_light.qk201801:
|
||||
ts: 1608174909
|
||||
qmi.powerstrip.v1:
|
||||
@ -726,8 +860,32 @@ roborock.vacuum.t6:
|
||||
ts: 1619423841
|
||||
rockrobo.vacuum.v1:
|
||||
ts: 1531108800
|
||||
roidmi.carairpuri.pro:
|
||||
ts: 1575097876
|
||||
roidmi.carairpuri.v1:
|
||||
ts: 1575097876
|
||||
roidmi.cleaner.f8pro:
|
||||
ts: 1575097876
|
||||
roidmi.cleaner.v1:
|
||||
ts: 1575097876
|
||||
roidmi.cleaner.v2:
|
||||
ts: 1638514177
|
||||
roidmi.cleaner.v382:
|
||||
ts: 1575097876
|
||||
roidmi.vacuum.v1:
|
||||
ts: 1575097876
|
||||
rokid.robot.me:
|
||||
ts: 1575097876
|
||||
rokid.robot.mini:
|
||||
ts: 1575097876
|
||||
rokid.robot.pebble:
|
||||
ts: 1575097876
|
||||
rokid.robot.pebble2:
|
||||
ts: 1575097876
|
||||
roome.bhf_light.yf6002:
|
||||
ts: 1531108800
|
||||
rotai.magic_touch.sx300:
|
||||
ts: 1602662578
|
||||
rotai.massage.rt5728:
|
||||
ts: 1610607000
|
||||
rotai.massage.rt5850:
|
||||
@ -738,22 +896,42 @@ rotai.massage.rt5863:
|
||||
ts: 1611827937
|
||||
rotai.massage.rt5870:
|
||||
ts: 1632376570
|
||||
runmi.suitcase.v1:
|
||||
ts: 1575097876
|
||||
scishare.coffee.s1102:
|
||||
ts: 1611824402
|
||||
shjszn.gateway.c1:
|
||||
ts: 1575097876
|
||||
shjszn.lock.c1:
|
||||
ts: 1575097876
|
||||
shjszn.lock.kx:
|
||||
ts: 1575097876
|
||||
shuii.humidifier.jsq001:
|
||||
ts: 1575097876
|
||||
shuii.humidifier.jsq002:
|
||||
ts: 1606376290
|
||||
skyrc.feeder.dfeed:
|
||||
ts: 1626082349
|
||||
skyrc.pet_waterer.fre1:
|
||||
ts: 1608186812
|
||||
smith.w_soften.cxs05ta1:
|
||||
ts: 1575097876
|
||||
smith.waterheater.cxea1:
|
||||
ts: 1611826349
|
||||
smith.waterheater.cxeb1:
|
||||
ts: 1611826388
|
||||
smith.waterpuri.jnt600:
|
||||
ts: 1531108800
|
||||
soocare.toothbrush.m1:
|
||||
ts: 1575097876
|
||||
soocare.toothbrush.m1s:
|
||||
ts: 1610611310
|
||||
soocare.toothbrush.mc1:
|
||||
ts: 1575097876
|
||||
soocare.toothbrush.t501:
|
||||
ts: 1672192586
|
||||
soocare.toothbrush.x3:
|
||||
ts: 1575097876
|
||||
sxds.pillow.pillow02:
|
||||
ts: 1611222235
|
||||
syniot.curtain.syc1:
|
||||
@ -778,6 +956,10 @@ tokit.oven.tk32pro1:
|
||||
ts: 1617002408
|
||||
tokit.pre_cooker.tkih1:
|
||||
ts: 1607410832
|
||||
trios1.bleshoes.v02:
|
||||
ts: 1602662599
|
||||
txdd.wifispeaker.x1:
|
||||
ts: 1575097876
|
||||
viomi.aircondition.v10:
|
||||
ts: 1606375041
|
||||
viomi.aircondition.v21:
|
||||
@ -830,12 +1012,16 @@ viomi.fridge.u13:
|
||||
ts: 1614667152
|
||||
viomi.fridge.u15:
|
||||
ts: 1607505693
|
||||
viomi.fridge.u17:
|
||||
ts: 1575097876
|
||||
viomi.fridge.u18:
|
||||
ts: 1614655755
|
||||
viomi.fridge.u2:
|
||||
ts: 1531108800
|
||||
viomi.fridge.u24:
|
||||
ts: 1614667214
|
||||
viomi.fridge.u25:
|
||||
ts: 1575097876
|
||||
viomi.fridge.u4:
|
||||
ts: 1614667295
|
||||
viomi.fridge.u6:
|
||||
@ -992,6 +1178,82 @@ xiaomi.aircondition.ma6:
|
||||
ts: 1721629272
|
||||
xiaomi.aircondition.ma9:
|
||||
ts: 1721629362
|
||||
xiaomi.plc.v1:
|
||||
ts: 1575097876
|
||||
xiaomi.repeater.v1:
|
||||
ts: 1575097876
|
||||
xiaomi.repeater.v2:
|
||||
ts: 1575097876
|
||||
xiaomi.repeater.v3:
|
||||
ts: 1575097876
|
||||
xiaomi.router.d01:
|
||||
ts: 1575097876
|
||||
xiaomi.router.lv1:
|
||||
ts: 1575097876
|
||||
xiaomi.router.lv3:
|
||||
ts: 1575097876
|
||||
xiaomi.router.mv1:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r2100:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3600:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3a:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3d:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3g:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3gv2:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3gv2n:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r3p:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r4:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r4a:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r4ac:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r4c:
|
||||
ts: 1575097876
|
||||
xiaomi.router.r4cm:
|
||||
ts: 1575097876
|
||||
xiaomi.router.rm1800:
|
||||
ts: 1575097876
|
||||
xiaomi.router.v1:
|
||||
ts: 1575097876
|
||||
xiaomi.router.v2:
|
||||
ts: 1575097876
|
||||
xiaomi.router.v3:
|
||||
ts: 1575097876
|
||||
xiaomi.split_tv.b1:
|
||||
ts: 1575097876
|
||||
xiaomi.split_tv.v1:
|
||||
ts: 1575097876
|
||||
xiaomi.tv.b1:
|
||||
ts: 1661248580
|
||||
xiaomi.tv.h1:
|
||||
ts: 1575097876
|
||||
xiaomi.tv.i1:
|
||||
ts: 1661248572
|
||||
xiaomi.tv.v1:
|
||||
ts: 1670811870
|
||||
xiaomi.tvbox.b1:
|
||||
ts: 1694503508
|
||||
xiaomi.tvbox.i1:
|
||||
ts: 1694503515
|
||||
xiaomi.tvbox.v1:
|
||||
ts: 1694503501
|
||||
xiaomi.watch.band1:
|
||||
ts: 1575097876
|
||||
xiaomi.watch.band1A:
|
||||
ts: 1575097876
|
||||
xiaomi.watch.band1S:
|
||||
ts: 1575097876
|
||||
xiaomi.watch.band2:
|
||||
ts: 1575097876
|
||||
xiaomi.wifispeaker.l04m:
|
||||
ts: 1658817956
|
||||
xiaomi.wifispeaker.l06a:
|
||||
@ -1012,6 +1274,10 @@ xiaomi.wifispeaker.lx5a:
|
||||
ts: 1672299577
|
||||
xiaomi.wifispeaker.s12:
|
||||
ts: 1672299594
|
||||
xiaomi.wifispeaker.v1:
|
||||
ts: 1575097876
|
||||
xiaomi.wifispeaker.v3:
|
||||
ts: 1575097876
|
||||
xiaomi.wifispeaker.x08a:
|
||||
ts: 1672818945
|
||||
xiaomi.wifispeaker.x08c:
|
||||
@ -1028,6 +1294,44 @@ xiaovv.camera.xvd5:
|
||||
ts: 1531108800
|
||||
xiaovv.camera.xvsnowman:
|
||||
ts: 1531108800
|
||||
xiaoxun.robot.v1:
|
||||
ts: 1575097876
|
||||
xiaoxun.tracker.v1:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw306:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw560:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw705:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw710a2:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw760:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw900:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.sw960:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v1:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v10:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v11:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v2:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v3:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v4:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v5:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v7:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v8:
|
||||
ts: 1575097876
|
||||
xiaoxun.watch.v9:
|
||||
ts: 1575097876
|
||||
xjx.toilet.pro:
|
||||
ts: 1615965466
|
||||
xjx.toilet.pure:
|
||||
@ -1054,6 +1358,8 @@ yeelink.bhf_light.v3:
|
||||
ts: 1608790102
|
||||
yeelink.bhf_light.v5:
|
||||
ts: 1601292562
|
||||
yeelink.gateway.v1:
|
||||
ts: 1575097876
|
||||
yeelink.light.bslamp1:
|
||||
ts: 1703120679
|
||||
yeelink.light.bslamp2:
|
||||
@ -1192,6 +1498,10 @@ yunmi.kettle.r2:
|
||||
ts: 1606372087
|
||||
yunmi.kettle.r3:
|
||||
ts: 1637309534
|
||||
yunmi.kettle.v1:
|
||||
ts: 1575097876
|
||||
yunmi.kettle.v9:
|
||||
ts: 1602662686
|
||||
yunmi.plmachine.mg2:
|
||||
ts: 1611833658
|
||||
yunmi.waterpuri.c5:
|
||||
@ -1230,18 +1540,26 @@ yunmi.waterpurifier.v2:
|
||||
ts: 1632377061
|
||||
yunmi.waterpurifier.v3:
|
||||
ts: 1611221428
|
||||
yunyi.camera.v1:
|
||||
ts: 1575097876
|
||||
yyunyi.wopener.yypy24:
|
||||
ts: 1616741966
|
||||
yyzhn.gateway.yn181126:
|
||||
ts: 1610689325
|
||||
zdeer.ajh.a8:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.a9:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.ajb:
|
||||
ts: 1608276454
|
||||
zdeer.ajh.zda10:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.zda9:
|
||||
ts: 1531108800
|
||||
zdeer.ajh.zjy:
|
||||
ts: 1531108800
|
||||
zhij.toothbrush.bv1:
|
||||
ts: 1575097876
|
||||
zhimi.aircondition.ma1:
|
||||
ts: 1615185265
|
||||
zhimi.aircondition.ma3:
|
||||
@ -1250,6 +1568,8 @@ zhimi.aircondition.ma4:
|
||||
ts: 1626334057
|
||||
zhimi.aircondition.v1:
|
||||
ts: 1610610931
|
||||
zhimi.aircondition.v2:
|
||||
ts: 1575097876
|
||||
zhimi.aircondition.va1:
|
||||
ts: 1609924720
|
||||
zhimi.aircondition.za1:
|
||||
@ -1276,8 +1596,12 @@ zhimi.airpurifier.sa2:
|
||||
ts: 1635820002
|
||||
zhimi.airpurifier.v1:
|
||||
ts: 1635855633
|
||||
zhimi.airpurifier.v2:
|
||||
ts: 1575097876
|
||||
zhimi.airpurifier.v3:
|
||||
ts: 1676339933
|
||||
zhimi.airpurifier.v5:
|
||||
ts: 1575097876
|
||||
zhimi.airpurifier.v6:
|
||||
ts: 1636978652
|
||||
zhimi.airpurifier.v7:
|
||||
@ -1318,3 +1642,5 @@ zimi.mosq.v1:
|
||||
ts: 1620728957
|
||||
zimi.powerstrip.v2:
|
||||
ts: 1620812714
|
||||
zimi.projector.v1:
|
||||
ts: 1575097876
|
||||
|
||||
@ -257,6 +257,7 @@ class MIoTClient:
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
redirect_url=self._entry_data['oauth_redirect_url'],
|
||||
cloud_server=self._cloud_server,
|
||||
uuid=self._entry_data["uuid"],
|
||||
loop=self._main_loop)
|
||||
# MIoT http client instance
|
||||
self._http = MIoTHttpClient(
|
||||
@ -356,7 +357,7 @@ class MIoTClient:
|
||||
# Cloud mips
|
||||
self._mips_cloud.unsub_mips_state(
|
||||
key=f'{self._uid}-{self._cloud_server}')
|
||||
self._mips_cloud.disconnect()
|
||||
self._mips_cloud.deinit()
|
||||
# Cancel refresh cloud devices
|
||||
if self._refresh_cloud_devices_timer:
|
||||
self._refresh_cloud_devices_timer.cancel()
|
||||
@ -369,7 +370,7 @@ class MIoTClient:
|
||||
for mips in self._mips_local.values():
|
||||
mips.on_dev_list_changed = None
|
||||
mips.unsub_mips_state(key=mips.group_id)
|
||||
mips.disconnect()
|
||||
mips.deinit()
|
||||
if self._mips_local_state_changed_timers:
|
||||
for timer_item in (
|
||||
self._mips_local_state_changed_timers.values()):
|
||||
@ -847,6 +848,30 @@ class MIoTClient:
|
||||
_LOGGER.debug('client unsub device state, %s', did)
|
||||
return True
|
||||
|
||||
async def remove_device_async(self, did: str) -> None:
|
||||
if did not in self._device_list_cache:
|
||||
return
|
||||
sub_from = self._sub_source_list.pop(did, None)
|
||||
# Unsub
|
||||
if sub_from:
|
||||
if sub_from == 'cloud':
|
||||
self._mips_cloud.unsub_prop(did=did)
|
||||
self._mips_cloud.unsub_event(did=did)
|
||||
elif sub_from == 'lan':
|
||||
self._miot_lan.unsub_prop(did=did)
|
||||
self._miot_lan.unsub_event(did=did)
|
||||
elif sub_from in self._mips_local:
|
||||
mips = self._mips_local[sub_from]
|
||||
mips.unsub_prop(did=did)
|
||||
mips.unsub_event(did=did)
|
||||
# Storage
|
||||
await self._storage.save_async(
|
||||
domain='miot_devices',
|
||||
name=f'{self._uid}_{self._cloud_server}',
|
||||
data=self._device_list_cache)
|
||||
# Update notify
|
||||
self.__request_show_devices_changed_notify()
|
||||
|
||||
def __get_exec_error_with_rc(self, rc: int) -> str:
|
||||
err_msg: str = self._i18n.translate(key=f'error.common.{rc}')
|
||||
if not err_msg:
|
||||
|
||||
@ -47,6 +47,7 @@ MIoT http client.
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@ -75,10 +76,12 @@ class MIoTOauthClient:
|
||||
_oauth_host: str
|
||||
_client_id: int
|
||||
_redirect_url: str
|
||||
_device_id: str
|
||||
_state: str
|
||||
|
||||
def __init__(
|
||||
self, client_id: str, redirect_url: str, cloud_server: str,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
) -> None:
|
||||
self._main_loop = loop or asyncio.get_running_loop()
|
||||
if client_id is None or client_id.strip() == '':
|
||||
@ -87,6 +90,8 @@ class MIoTOauthClient:
|
||||
raise MIoTOauthError('invalid redirect_url')
|
||||
if not cloud_server:
|
||||
raise MIoTOauthError('invalid cloud_server')
|
||||
if not uuid:
|
||||
raise MIoTOauthError('invalid uuid')
|
||||
|
||||
self._client_id = int(client_id)
|
||||
self._redirect_url = redirect_url
|
||||
@ -94,8 +99,15 @@ class MIoTOauthClient:
|
||||
self._oauth_host = DEFAULT_OAUTH2_API_HOST
|
||||
else:
|
||||
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
|
||||
self._device_id = f'ha.{uuid}'
|
||||
self._state = hashlib.sha1(
|
||||
f'd={self._device_id}'.encode('utf-8')).hexdigest()
|
||||
self._session = aiohttp.ClientSession(loop=self._main_loop)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
return self._state
|
||||
|
||||
async def deinit_async(self) -> None:
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
@ -132,6 +144,8 @@ class MIoTOauthClient:
|
||||
'redirect_uri': redirect_url or self._redirect_url,
|
||||
'client_id': self._client_id,
|
||||
'response_type': 'code',
|
||||
'device_id': self._device_id,
|
||||
'state': self._state
|
||||
}
|
||||
if state:
|
||||
params['state'] = state
|
||||
@ -191,6 +205,7 @@ class MIoTOauthClient:
|
||||
'client_id': self._client_id,
|
||||
'redirect_uri': self._redirect_url,
|
||||
'code': code,
|
||||
'device_id': self._device_id
|
||||
})
|
||||
|
||||
async def refresh_access_token_async(self, refresh_token: str) -> dict:
|
||||
@ -531,9 +546,18 @@ class MIoTHttpClient:
|
||||
name = device.get('name', None)
|
||||
urn = device.get('spec_type', None)
|
||||
model = device.get('model', None)
|
||||
if did is None or name is None or urn is None or model is None:
|
||||
_LOGGER.error(
|
||||
'get_device_list, cloud, invalid device, %s', device)
|
||||
if did is None or name is None:
|
||||
_LOGGER.info(
|
||||
'invalid device, cloud, %s', device)
|
||||
continue
|
||||
if urn is None or model is None:
|
||||
_LOGGER.info(
|
||||
'missing the urn|model field, cloud, %s', device)
|
||||
continue
|
||||
if did.startswith('miwifi.'):
|
||||
# The miwifi.* routers defined SPEC functions, but none of them
|
||||
# were implemented.
|
||||
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
|
||||
continue
|
||||
device_infos[did] = {
|
||||
'did': did,
|
||||
@ -634,7 +658,7 @@ class MIoTHttpClient:
|
||||
for did in dids:
|
||||
if did not in results:
|
||||
devices.pop(did, None)
|
||||
_LOGGER.error('get device info failed, %s', did)
|
||||
_LOGGER.info('get device info failed, %s', did)
|
||||
continue
|
||||
devices[did].update(results[did])
|
||||
# Whether sub devices
|
||||
|
||||
@ -72,6 +72,8 @@ class MIoTErrorCode(Enum):
|
||||
# MIoT ev error code, -10080
|
||||
# Mips service error code, -10090
|
||||
# Config flow error code, -10100
|
||||
CODE_CONFIG_INVALID_INPUT = -10100
|
||||
CODE_CONFIG_INVALID_STATE = -10101
|
||||
# Options flow error code , -10110
|
||||
# MIoT lan error code, -10120
|
||||
CODE_LAN_UNAVAILABLE = -10120
|
||||
|
||||
@ -1,324 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (C) 2024 Xiaomi Corporation.
|
||||
|
||||
The ownership and intellectual property rights of Xiaomi Home Assistant
|
||||
Integration and related Xiaomi cloud service API interface provided under this
|
||||
license, including source code and object code (collectively, "Licensed Work"),
|
||||
are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi
|
||||
hereby grants you a personal, limited, non-exclusive, non-transferable,
|
||||
non-sublicensable, and royalty-free license to reproduce, use, modify, and
|
||||
distribute the Licensed Work only for your use of Home Assistant for
|
||||
non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize
|
||||
you to use the Licensed Work for any other purpose, including but not limited
|
||||
to use Licensed Work to develop applications (APP), Web services, and other
|
||||
forms of software.
|
||||
|
||||
You may reproduce and distribute copies of the Licensed Work, with or without
|
||||
modifications, whether in source or object form, provided that you must give
|
||||
any other recipients of the Licensed Work a copy of this License and retain all
|
||||
copyright and disclaimers.
|
||||
|
||||
Xiaomi provides the Licensed Work on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied, including, without
|
||||
limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR
|
||||
OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible
|
||||
for any direct, indirect, special, incidental, or consequential damages or
|
||||
losses arising from the use or inability to use the Licensed Work.
|
||||
|
||||
Xiaomi reserves all rights not expressly granted to you in this License.
|
||||
Except for the rights expressly granted by Xiaomi under this License, Xiaomi
|
||||
does not authorize you in any form to use the trademarks, copyrights, or other
|
||||
forms of intellectual property rights of Xiaomi and its affiliates, including,
|
||||
without limitation, without obtaining other written permission from Xiaomi, you
|
||||
shall not use "Xiaomi", "Mijia" and other words related to Xiaomi or words that
|
||||
may make the public associate with Xiaomi in any form to publicize or promote
|
||||
the software or hardware devices that use the Licensed Work.
|
||||
|
||||
Xiaomi has the right to immediately terminate all your authorization under this
|
||||
License in the event:
|
||||
1. You assert patent invalidation, litigation, or other claims against patents
|
||||
or other intellectual property rights of Xiaomi or its affiliates; or,
|
||||
2. You make, have made, manufacture, sell, or offer to sell products that knock
|
||||
off Xiaomi or its affiliates' products.
|
||||
|
||||
MIoT event loop.
|
||||
"""
|
||||
import selectors
|
||||
import heapq
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Callable, TypeVar
|
||||
import logging
|
||||
import threading
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from .miot_error import MIoTEvError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TimeoutHandle = TypeVar('TimeoutHandle')
|
||||
|
||||
|
||||
class MIoTFdHandler:
|
||||
"""File descriptor handler."""
|
||||
fd: int
|
||||
read_handler: Callable[[Any], None]
|
||||
read_handler_ctx: Any
|
||||
write_handler: Callable[[Any], None]
|
||||
write_handler_ctx: Any
|
||||
|
||||
def __init__(
|
||||
self, fd: int,
|
||||
read_handler: Callable[[Any], None] = None,
|
||||
read_handler_ctx: Any = None,
|
||||
write_handler: Callable[[Any], None] = None,
|
||||
write_handler_ctx: Any = None
|
||||
) -> None:
|
||||
self.fd = fd
|
||||
self.read_handler = read_handler
|
||||
self.read_handler_ctx = read_handler_ctx
|
||||
self.write_handler = write_handler
|
||||
self.write_handler_ctx = write_handler_ctx
|
||||
|
||||
|
||||
class MIoTTimeout:
|
||||
"""Timeout handler."""
|
||||
key: TimeoutHandle
|
||||
target: int
|
||||
handler: Callable[[Any], None]
|
||||
handler_ctx: Any
|
||||
|
||||
def __init__(
|
||||
self, key: str = None, target: int = None,
|
||||
handler: Callable[[Any], None] = None,
|
||||
handler_ctx: Any = None
|
||||
) -> None:
|
||||
self.key = key
|
||||
self.target = target
|
||||
self.handler = handler
|
||||
self.handler_ctx = handler_ctx
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.target < other.target
|
||||
|
||||
|
||||
class MIoTEventLoop:
|
||||
"""MIoT event loop."""
|
||||
_poll_fd: selectors.DefaultSelector
|
||||
|
||||
_fd_handlers: dict[str, MIoTFdHandler]
|
||||
|
||||
_timer_heap: list[MIoTTimeout]
|
||||
_timer_handlers: dict[str, MIoTTimeout]
|
||||
_timer_handle_seed: int
|
||||
|
||||
# Label if the current fd handler is freed inside a read handler to
|
||||
# avoid invalid reading.
|
||||
_fd_handler_freed_in_read_handler: bool
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._poll_fd = selectors.DefaultSelector()
|
||||
self._timer_heap = []
|
||||
self._timer_handlers = {}
|
||||
self._timer_handle_seed = 1
|
||||
self._fd_handlers = {}
|
||||
self._fd_handler_freed_in_read_handler = False
|
||||
|
||||
def loop_forever(self) -> None:
|
||||
"""Run an event loop in current thread."""
|
||||
next_timeout: int
|
||||
while True:
|
||||
next_timeout = 0
|
||||
# Handle timer
|
||||
now_ms: int = self.__get_monotonic_ms
|
||||
while len(self._timer_heap) > 0:
|
||||
timer: MIoTTimeout = self._timer_heap[0]
|
||||
if timer is None:
|
||||
break
|
||||
if timer.target <= now_ms:
|
||||
heapq.heappop(self._timer_heap)
|
||||
del self._timer_handlers[timer.key]
|
||||
if timer.handler:
|
||||
timer.handler(timer.handler_ctx)
|
||||
else:
|
||||
next_timeout = timer.target-now_ms
|
||||
break
|
||||
# Are there any files to listen to
|
||||
if next_timeout == 0 and self._fd_handlers:
|
||||
next_timeout = None # None == infinite
|
||||
# Wait for timers & fds
|
||||
if next_timeout == 0:
|
||||
# Neither timer nor fds exist, exit loop
|
||||
break
|
||||
# Handle fd event
|
||||
events = self._poll_fd.select(
|
||||
timeout=next_timeout/1000.0 if next_timeout else next_timeout)
|
||||
for key, mask in events:
|
||||
fd_handler: MIoTFdHandler = key.data
|
||||
if fd_handler is None:
|
||||
continue
|
||||
self._fd_handler_freed_in_read_handler = False
|
||||
fd_key = str(id(fd_handler.fd))
|
||||
if fd_key not in self._fd_handlers:
|
||||
continue
|
||||
if (
|
||||
mask & selectors.EVENT_READ > 0
|
||||
and fd_handler.read_handler
|
||||
):
|
||||
fd_handler.read_handler(fd_handler.read_handler_ctx)
|
||||
if (
|
||||
mask & selectors.EVENT_WRITE > 0
|
||||
and self._fd_handler_freed_in_read_handler is False
|
||||
and fd_handler.write_handler
|
||||
):
|
||||
fd_handler.write_handler(fd_handler.write_handler_ctx)
|
||||
|
||||
def loop_stop(self) -> None:
|
||||
"""Stop the event loop."""
|
||||
if self._poll_fd:
|
||||
self._poll_fd.close()
|
||||
self._poll_fd = None
|
||||
self._fd_handlers = {}
|
||||
self._timer_heap = []
|
||||
self._timer_handlers = {}
|
||||
|
||||
def set_timeout(
|
||||
self, timeout_ms: int, handler: Callable[[Any], None],
|
||||
handler_ctx: Any = None
|
||||
) -> TimeoutHandle:
|
||||
"""Set a timer."""
|
||||
if timeout_ms is None or handler is None:
|
||||
raise MIoTEvError('invalid params')
|
||||
new_timeout: MIoTTimeout = MIoTTimeout()
|
||||
new_timeout.key = self.__get_next_timeout_handle
|
||||
new_timeout.target = self.__get_monotonic_ms + timeout_ms
|
||||
new_timeout.handler = handler
|
||||
new_timeout.handler_ctx = handler_ctx
|
||||
heapq.heappush(self._timer_heap, new_timeout)
|
||||
self._timer_handlers[new_timeout.key] = new_timeout
|
||||
return new_timeout.key
|
||||
|
||||
def clear_timeout(self, timer_key: TimeoutHandle) -> None:
|
||||
"""Stop and remove the timer."""
|
||||
if timer_key is None:
|
||||
return
|
||||
timer: MIoTTimeout = self._timer_handlers.pop(timer_key, None)
|
||||
if timer:
|
||||
self._timer_heap = list(self._timer_heap)
|
||||
self._timer_heap.remove(timer)
|
||||
heapq.heapify(self._timer_heap)
|
||||
|
||||
def set_read_handler(
|
||||
self, fd: int, handler: Callable[[Any], None], handler_ctx: Any = None
|
||||
) -> bool:
|
||||
"""Set a read handler for a file descriptor.
|
||||
|
||||
Returns:
|
||||
bool: True, success. False, failed.
|
||||
"""
|
||||
self.__set_handler(
|
||||
fd, is_read=True, handler=handler, handler_ctx=handler_ctx)
|
||||
|
||||
def set_write_handler(
|
||||
self, fd: int, handler: Callable[[Any], None], handler_ctx: Any = None
|
||||
) -> bool:
|
||||
"""Set a write handler for a file descriptor.
|
||||
|
||||
Returns:
|
||||
bool: True, success. False, failed.
|
||||
"""
|
||||
self.__set_handler(
|
||||
fd, is_read=False, handler=handler, handler_ctx=handler_ctx)
|
||||
|
||||
def __set_handler(
|
||||
self, fd, is_read: bool, handler: Callable[[Any], None],
|
||||
handler_ctx: Any = None
|
||||
) -> bool:
|
||||
"""Set a handler."""
|
||||
if fd is None:
|
||||
raise MIoTEvError('invalid params')
|
||||
|
||||
if not self._poll_fd:
|
||||
raise MIoTEvError('event loop not started')
|
||||
|
||||
fd_key: str = str(id(fd))
|
||||
fd_handler = self._fd_handlers.get(fd_key, None)
|
||||
|
||||
if fd_handler is None:
|
||||
fd_handler = MIoTFdHandler(fd=fd)
|
||||
fd_handler.fd = fd
|
||||
self._fd_handlers[fd_key] = fd_handler
|
||||
|
||||
read_handler_existed = fd_handler.read_handler is not None
|
||||
write_handler_existed = fd_handler.write_handler is not None
|
||||
if is_read is True:
|
||||
fd_handler.read_handler = handler
|
||||
fd_handler.read_handler_ctx = handler_ctx
|
||||
else:
|
||||
fd_handler.write_handler = handler
|
||||
fd_handler.write_handler_ctx = handler_ctx
|
||||
|
||||
if fd_handler.read_handler is None and fd_handler.write_handler is None:
|
||||
# Remove from epoll and map
|
||||
try:
|
||||
self._poll_fd.unregister(fd)
|
||||
except (KeyError, ValueError, OSError) as e:
|
||||
del e
|
||||
self._fd_handlers.pop(fd_key, None)
|
||||
# May be inside a read handler, if not, this has no effect
|
||||
self._fd_handler_freed_in_read_handler = True
|
||||
elif read_handler_existed is False and write_handler_existed is False:
|
||||
# Add to epoll
|
||||
events = 0x0
|
||||
if fd_handler.read_handler:
|
||||
events |= selectors.EVENT_READ
|
||||
if fd_handler.write_handler:
|
||||
events |= selectors.EVENT_WRITE
|
||||
try:
|
||||
self._poll_fd.register(fd, events=events, data=fd_handler)
|
||||
except (KeyError, ValueError, OSError) as e:
|
||||
_LOGGER.error(
|
||||
'%s, register fd, error, %s, %s, %s, %s, %s',
|
||||
threading.current_thread().name,
|
||||
'read' if is_read else 'write',
|
||||
fd_key, handler, e, traceback.format_exc())
|
||||
self._fd_handlers.pop(fd_key, None)
|
||||
return False
|
||||
elif (
|
||||
read_handler_existed != (fd_handler.read_handler is not None)
|
||||
or write_handler_existed != (fd_handler.write_handler is not None)
|
||||
):
|
||||
# Modify epoll
|
||||
events = 0x0
|
||||
if fd_handler.read_handler:
|
||||
events |= selectors.EVENT_READ
|
||||
if fd_handler.write_handler:
|
||||
events |= selectors.EVENT_WRITE
|
||||
try:
|
||||
self._poll_fd.modify(fd, events=events, data=fd_handler)
|
||||
except (KeyError, ValueError, OSError) as e:
|
||||
_LOGGER.error(
|
||||
'%s, modify fd, error, %s, %s, %s, %s, %s',
|
||||
threading.current_thread().name,
|
||||
'read' if is_read else 'write',
|
||||
fd_key, handler, e, traceback.format_exc())
|
||||
self._fd_handlers.pop(fd_key, None)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def __get_next_timeout_handle(self) -> str:
|
||||
# Get next timeout handle, that is not larger than the maximum
|
||||
# value of UINT64 type.
|
||||
self._timer_handle_seed += 1
|
||||
# uint64 max
|
||||
self._timer_handle_seed %= 0xFFFFFFFFFFFFFFFF
|
||||
return str(self._timer_handle_seed)
|
||||
|
||||
@property
|
||||
def __get_monotonic_ms(self) -> int:
|
||||
"""Get monotonic ms timestamp."""
|
||||
return int(time.monotonic()*1000)
|
||||
@ -48,7 +48,7 @@ MIoT internationalization translation.
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
# pylint: disable=relative-beyond-top-level
|
||||
from .common import load_json_file
|
||||
@ -98,7 +98,7 @@ class MIoTI18n:
|
||||
|
||||
def translate(
|
||||
self, key: str, replace: Optional[dict[str, str]] = None
|
||||
) -> str | dict | None:
|
||||
) -> Union[str, dict, None]:
|
||||
result = self._data
|
||||
for item in key.split('.'):
|
||||
if item not in result:
|
||||
|
||||
@ -381,7 +381,8 @@ class _MIoTLanDevice:
|
||||
_MIoTLanDeviceState(state.value+1))
|
||||
# Fast ping
|
||||
if self._if_name is None:
|
||||
_LOGGER.error('if_name is Not set for device, %s', self.did)
|
||||
_LOGGER.error(
|
||||
'if_name is Not set for device, %s', self.did)
|
||||
return
|
||||
if self.ip is None:
|
||||
_LOGGER.error('ip is Not set for device, %s', self.did)
|
||||
@ -419,10 +420,10 @@ class _MIoTLanDevice:
|
||||
self.online = True
|
||||
else:
|
||||
_LOGGER.info('unstable device detected, %s', self.did)
|
||||
self._online_offline_timer = \
|
||||
self._online_offline_timer = (
|
||||
self._manager.internal_loop.call_later(
|
||||
self.NETWORK_UNSTABLE_RESUME_TH,
|
||||
self.__online_resume_handler)
|
||||
self.__online_resume_handler))
|
||||
|
||||
def __online_resume_handler(self) -> None:
|
||||
_LOGGER.info('unstable resume threshold past, %s', self.did)
|
||||
@ -508,9 +509,9 @@ class MIoTLan:
|
||||
key='miot_lan', group_id='*',
|
||||
handler=self.__on_mips_service_change)
|
||||
self._enable_subscribe = enable_subscribe
|
||||
self._virtual_did = str(virtual_did) \
|
||||
if (virtual_did is not None) \
|
||||
else str(secrets.randbits(64))
|
||||
self._virtual_did = (
|
||||
str(virtual_did) if (virtual_did is not None)
|
||||
else str(secrets.randbits(64)))
|
||||
# Init socket probe message
|
||||
probe_bytes = bytearray(self.OT_PROBE_LEN)
|
||||
probe_bytes[:20] = (
|
||||
@ -948,7 +949,7 @@ class MIoTLan:
|
||||
|
||||
# The following methods SHOULD ONLY be called in the internal loop
|
||||
|
||||
def ping(self, if_name: str | None, target_ip: str) -> None:
|
||||
def ping(self, if_name: Optional[str], target_ip: str) -> None:
|
||||
if not target_ip:
|
||||
return
|
||||
self.__sendto(
|
||||
@ -964,7 +965,7 @@ class MIoTLan:
|
||||
) -> None:
|
||||
if timeout_ms and not handler:
|
||||
raise ValueError('handler is required when timeout_ms is set')
|
||||
device: _MIoTLanDevice | None = self._lan_devices.get(did)
|
||||
device: Optional[_MIoTLanDevice] = self._lan_devices.get(did)
|
||||
if not device:
|
||||
raise ValueError('invalid device')
|
||||
if not device.cipher:
|
||||
@ -1232,7 +1233,7 @@ class MIoTLan:
|
||||
return
|
||||
# Keep alive message
|
||||
did: str = str(struct.unpack('>Q', data[4:12])[0])
|
||||
device: _MIoTLanDevice | None = self._lan_devices.get(did)
|
||||
device: Optional[_MIoTLanDevice] = self._lan_devices.get(did)
|
||||
if not device:
|
||||
return
|
||||
timestamp: int = struct.unpack('>I', data[12:16])[0]
|
||||
@ -1272,8 +1273,8 @@ class MIoTLan:
|
||||
_LOGGER.warning('invalid message, no id, %s, %s', did, msg)
|
||||
return
|
||||
# Reply
|
||||
req: _MIoTLanRequestData | None = \
|
||||
self._pending_requests.pop(msg['id'], None)
|
||||
req: Optional[_MIoTLanRequestData] = (
|
||||
self._pending_requests.pop(msg['id'], None))
|
||||
if req:
|
||||
if req.timeout:
|
||||
req.timeout.cancel()
|
||||
@ -1334,7 +1335,7 @@ class MIoTLan:
|
||||
return False
|
||||
|
||||
def __sendto(
|
||||
self, if_name: str | None, data: bytes, address: str, port: int
|
||||
self, if_name: Optional[str], data: bytes, address: str, port: int
|
||||
) -> None:
|
||||
if if_name is None:
|
||||
# Broadcast
|
||||
@ -1356,7 +1357,7 @@ class MIoTLan:
|
||||
try:
|
||||
# Scan devices
|
||||
self.ping(if_name=None, target_ip='255.255.255.255')
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
except Exception as err: # pylint: disable=broad-exception-caught
|
||||
# Ignore any exceptions to avoid blocking the loop
|
||||
_LOGGER.error('ping device error, %s', err)
|
||||
pass
|
||||
|
||||
@ -117,7 +117,7 @@ class MipsServiceData:
|
||||
self.type = service_info.type
|
||||
self.server = service_info.server or ''
|
||||
# Parse profile
|
||||
self.did = str(int.from_bytes(self.profile_bin[1:9]))
|
||||
self.did = str(int.from_bytes(self.profile_bin[1:9], byteorder='big'))
|
||||
self.group_id = binascii.hexlify(
|
||||
self.profile_bin[9:17][::-1]).decode('utf-8')
|
||||
self.role = int(self.profile_bin[20] >> 4)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,136 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="https://cdn.web-global.fds.api.mi-img.com/mcfe--mi-account/static/favicon_new.ico">
|
||||
<link as="style"
|
||||
href="https://font.sec.miui.com/font/css?family=MiSans:300,400,500,600,700:Chinese_Simplify,Chinese_Traditional,Latin&display=swap"
|
||||
rel="preload">
|
||||
<title>TITLE_PLACEHOLDER</title>
|
||||
<style>
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: MiSans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
background: rgb(255 255 255 / 5%);
|
||||
width: 360px;
|
||||
padding: 40 45;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 20px 50px 0 hsla(0, 0%, 64%, .1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo-frame {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-frame {
|
||||
margin: 20px 0 20px 0;
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.content-frame {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 20px;
|
||||
background-color: #ff5c00;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="frame">
|
||||
<!-- XIAOMI LOGO-->
|
||||
<div class="logo-frame">
|
||||
<svg width="50" height="50" viewBox="0 0 193 193" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>编组</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<polygon id="path-1"
|
||||
points="1.78097075e-14 0.000125324675 192.540685 0.000125324675 192.540685 192.540058 1.78097075e-14 192.540058">
|
||||
</polygon>
|
||||
</defs>
|
||||
<g id="\u9875\u9762-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="\u7F16\u7EC4">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Clip-2"></g>
|
||||
<path
|
||||
d="M172.473071,20.1164903 C154.306633,2.02148701 128.188344,-1.78097075e-14 96.2706558,-1.78097075e-14 C64.312237,-1.78097075e-14 38.155724,2.0452987 19.9974318,20.1872987 C1.84352597,38.3261656 1.78097075e-14,64.4406948 1.78097075e-14,96.3640227 C1.78097075e-14,128.286724 1.84352597,154.415039 20.0049513,172.556412 C38.1638701,190.704052 64.3141169,192.540058 96.2706558,192.540058 C128.225942,192.540058 154.376815,190.704052 172.53636,172.556412 C190.694653,154.409399 192.540685,128.286724 192.540685,96.3640227 C192.540685,64.3999643 190.672721,38.2553571 172.473071,20.1164903"
|
||||
id="Fill-1" fill="#FF6900" mask="url(#mask-2)"></path>
|
||||
<path
|
||||
d="M89.1841721,131.948836 C89.1841721,132.594885 88.640263,133.130648 87.9779221,133.130648 L71.5585097,133.130648 C70.8848896,133.130648 70.338474,132.594885 70.338474,131.948836 L70.338474,89.0100961 C70.338474,88.3584078 70.8848896,87.8251513 71.5585097,87.8251513 L87.9779221,87.8251513 C88.640263,87.8251513 89.1841721,88.3584078 89.1841721,89.0100961 L89.1841721,131.948836 Z"
|
||||
id="Fill-3" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
<path
|
||||
d="M121.332896,131.948836 C121.332896,132.594885 120.786481,133.130648 120.121633,133.130648 L104.492393,133.130648 C103.821906,133.130648 103.275491,132.594885 103.275491,131.948836 L103.275491,131.788421 L103.275491,94.9022357 C103.259198,88.4342292 102.889491,81.7863818 99.5502146,78.445226 C96.6790263,75.5652649 91.3251562,74.9054305 85.7557276,74.7669468 L57.4242049,74.7669468 C56.7555977,74.7669468 56.2154484,75.3045896 56.2154484,75.9512649 L56.2154484,128.074424 L56.2154484,131.948836 C56.2154484,132.594885 55.6640198,133.130648 54.9954127,133.130648 L39.3555198,133.130648 C38.6875393,133.130648 38.1498964,132.594885 38.1498964,131.948836 L38.1498964,60.5996188 C38.1498964,59.9447974 38.6875393,59.4121675 39.3555198,59.4121675 L84.4786692,59.4121675 C96.2717211,59.4121675 108.599909,59.9498104 114.680036,66.0380831 C120.786481,72.1533006 121.332896,84.4595571 121.332896,96.2657682 L121.332896,131.948836 Z"
|
||||
id="Fill-5" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
<path
|
||||
d="M153.53056,131.948836 C153.53056,132.594885 152.978505,133.130648 152.316164,133.130648 L136.678778,133.130648 C136.010797,133.130648 135.467515,132.594885 135.467515,131.948836 L135.467515,60.5996188 C135.467515,59.9447974 136.010797,59.4121675 136.678778,59.4121675 L152.316164,59.4121675 C152.978505,59.4121675 153.53056,59.9447974 153.53056,60.5996188 L153.53056,131.948836 Z"
|
||||
id="Fill-7" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- TITLE -->
|
||||
<div class="title-frame">
|
||||
<a id="titleArea">TITLE_PLACEHOLDER</a>
|
||||
</div>
|
||||
<!-- CONTENT -->
|
||||
<div class="content-frame">
|
||||
<a id="contentArea">CONTENT_PLACEHOLDER</a>
|
||||
</div>
|
||||
<!-- BUTTON -->
|
||||
<button onClick="window.close();" id="buttonArea">BUTTON_PLACEHOLDER</button>
|
||||
</div>
|
||||
<script>
|
||||
if (STATUS_PLACEHOLDER) {
|
||||
window.opener = null;
|
||||
window.open('', '_self');
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -59,5 +59,10 @@
|
||||
"1",
|
||||
"5"
|
||||
]
|
||||
},
|
||||
"urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03": {
|
||||
"services": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -289,7 +289,7 @@ SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
|
||||
}
|
||||
},
|
||||
'optional': {
|
||||
'properties': {'mode', 'horizontal-swing'}
|
||||
'properties': {'mode', 'horizontal-swing', 'wind-reverse'}
|
||||
},
|
||||
'entity': 'fan'
|
||||
},
|
||||
@ -439,6 +439,14 @@ SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
|
||||
'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR
|
||||
}
|
||||
},
|
||||
'power': {
|
||||
'device_class': SensorDeviceClass.POWER,
|
||||
'entity': 'sensor',
|
||||
'optional': {
|
||||
'state_class': SensorStateClass.MEASUREMENT,
|
||||
'unit_of_measurement': UnitOfPower.WATT
|
||||
}
|
||||
},
|
||||
'total-battery': {
|
||||
'device_class': SensorDeviceClass.ENERGY,
|
||||
'entity': 'sensor',
|
||||
|
||||
@ -46,237 +46,31 @@ off Xiaomi or its affiliates' products.
|
||||
MIoT redirect web pages.
|
||||
"""
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
def oauth_redirect_page(lang: str, status: str) -> str:
|
||||
_template = ''
|
||||
|
||||
|
||||
def _load_page_template():
|
||||
path = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
'resource/oauth_redirect_page.html')
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
global _template
|
||||
_template = f.read()
|
||||
|
||||
|
||||
async def oauth_redirect_page(
|
||||
title: str, content: str, button: str, success: bool
|
||||
) -> str:
|
||||
"""Return oauth redirect page."""
|
||||
return '''
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="https://cdn.web-global.fds.api.mi-img.com/mcfe--mi-account/static/favicon_new.ico">
|
||||
<link as="style"
|
||||
href="https://font.sec.miui.com/font/css?family=MiSans:300,400,500,600,700:Chinese_Simplify,Chinese_Traditional,Latin&display=swap"
|
||||
rel="preload">
|
||||
<title></title>
|
||||
<style>
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: MiSans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.frame {
|
||||
background: rgb(255 255 255 / 5%);
|
||||
width: 360px;
|
||||
padding: 40 45;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 20px 50px 0 hsla(0, 0%, 64%, .1);
|
||||
text-align: center;
|
||||
}
|
||||
.logo-frame {
|
||||
text-align: center;
|
||||
}
|
||||
.title-frame {
|
||||
margin: 20px 0 20px 0;
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.content-frame {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
button {
|
||||
margin-top: 20px;
|
||||
background-color: #ff5c00;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<!-- XIAOMI LOGO-->
|
||||
<div class="logo-frame">
|
||||
<svg width="50" height="50" viewBox="0 0 193 193" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"><title>编组</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<polygon id="path-1"
|
||||
points="1.78097075e-14 0.000125324675 192.540685 0.000125324675 192.540685 192.540058 1.78097075e-14 192.540058"></polygon>
|
||||
</defs>
|
||||
<g id="\u9875\u9762-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="\u7F16\u7EC4">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="Clip-2"></g>
|
||||
<path d="M172.473071,20.1164903 C154.306633,2.02148701 128.188344,-1.78097075e-14 96.2706558,-1.78097075e-14 C64.312237,-1.78097075e-14 38.155724,2.0452987 19.9974318,20.1872987 C1.84352597,38.3261656 1.78097075e-14,64.4406948 1.78097075e-14,96.3640227 C1.78097075e-14,128.286724 1.84352597,154.415039 20.0049513,172.556412 C38.1638701,190.704052 64.3141169,192.540058 96.2706558,192.540058 C128.225942,192.540058 154.376815,190.704052 172.53636,172.556412 C190.694653,154.409399 192.540685,128.286724 192.540685,96.3640227 C192.540685,64.3999643 190.672721,38.2553571 172.473071,20.1164903"
|
||||
id="Fill-1" fill="#FF6900" mask="url(#mask-2)"></path>
|
||||
<path d="M89.1841721,131.948836 C89.1841721,132.594885 88.640263,133.130648 87.9779221,133.130648 L71.5585097,133.130648 C70.8848896,133.130648 70.338474,132.594885 70.338474,131.948836 L70.338474,89.0100961 C70.338474,88.3584078 70.8848896,87.8251513 71.5585097,87.8251513 L87.9779221,87.8251513 C88.640263,87.8251513 89.1841721,88.3584078 89.1841721,89.0100961 L89.1841721,131.948836 Z"
|
||||
id="Fill-3" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
<path d="M121.332896,131.948836 C121.332896,132.594885 120.786481,133.130648 120.121633,133.130648 L104.492393,133.130648 C103.821906,133.130648 103.275491,132.594885 103.275491,131.948836 L103.275491,131.788421 L103.275491,94.9022357 C103.259198,88.4342292 102.889491,81.7863818 99.5502146,78.445226 C96.6790263,75.5652649 91.3251562,74.9054305 85.7557276,74.7669468 L57.4242049,74.7669468 C56.7555977,74.7669468 56.2154484,75.3045896 56.2154484,75.9512649 L56.2154484,128.074424 L56.2154484,131.948836 C56.2154484,132.594885 55.6640198,133.130648 54.9954127,133.130648 L39.3555198,133.130648 C38.6875393,133.130648 38.1498964,132.594885 38.1498964,131.948836 L38.1498964,60.5996188 C38.1498964,59.9447974 38.6875393,59.4121675 39.3555198,59.4121675 L84.4786692,59.4121675 C96.2717211,59.4121675 108.599909,59.9498104 114.680036,66.0380831 C120.786481,72.1533006 121.332896,84.4595571 121.332896,96.2657682 L121.332896,131.948836 Z"
|
||||
id="Fill-5" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
<path d="M153.53056,131.948836 C153.53056,132.594885 152.978505,133.130648 152.316164,133.130648 L136.678778,133.130648 C136.010797,133.130648 135.467515,132.594885 135.467515,131.948836 L135.467515,60.5996188 C135.467515,59.9447974 136.010797,59.4121675 136.678778,59.4121675 L152.316164,59.4121675 C152.978505,59.4121675 153.53056,59.9447974 153.53056,60.5996188 L153.53056,131.948836 Z"
|
||||
id="Fill-7" fill="#FFFFFF" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- TITLE -->
|
||||
<div class="title-frame">
|
||||
<a id="titleArea"></a>
|
||||
</div>
|
||||
<!-- CONTENT -->
|
||||
<div class="content-frame">
|
||||
<a id="contentArea"></a>
|
||||
</div>
|
||||
<!-- BUTTON -->
|
||||
<button onClick="window.close();" id="buttonArea"></button>
|
||||
</div>
|
||||
<script>
|
||||
// get language (user language -> system language)
|
||||
const locale = (localStorage.getItem('selectedLanguage')?? "''' + lang + '''").replaceAll('"','');
|
||||
const language = locale.includes("-") ? locale.substring(0, locale.indexOf("-")).trim() : locale;
|
||||
const status = "''' + status + '''";
|
||||
console.log(locale);
|
||||
// translation
|
||||
let translation = {
|
||||
zh: {
|
||||
success: {
|
||||
title: "认证完成",
|
||||
content: "请关闭此页面,返回账号认证页面点击“下一步”",
|
||||
button: "关闭页面"
|
||||
},
|
||||
fail: {
|
||||
title: "认证失败",
|
||||
content: "请关闭此页面,返回账号认证页面重新点击认链接进行认证。",
|
||||
button: "关闭页面"
|
||||
}
|
||||
},
|
||||
'zh-Hant': {
|
||||
success: {
|
||||
title: "認證完成",
|
||||
content: "請關閉此頁面,返回帳號認證頁面點擊「下一步」。",
|
||||
button: "關閉頁面"
|
||||
},
|
||||
fail: {
|
||||
title: "認證失敗",
|
||||
content: "請關閉此頁面,返回帳號認證頁面重新點擊認鏈接進行認證。",
|
||||
button: "關閉頁面"
|
||||
}
|
||||
},
|
||||
en: {
|
||||
success: {
|
||||
title: "Authentication Completed",
|
||||
content: "Please close this page and return to the account authentication page to click NEXT",
|
||||
button: "Close Page"
|
||||
},
|
||||
fail: {
|
||||
title: "Authentication Failed",
|
||||
content: "Please close this page and return to the account authentication page to click the authentication link again.",
|
||||
button: "Close Page"
|
||||
}
|
||||
},
|
||||
fr: {
|
||||
success: {
|
||||
title: "Authentification Terminée",
|
||||
content: "Veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer sur « SUIVANT »",
|
||||
button: "Fermer la page"
|
||||
},
|
||||
fail: {
|
||||
title: "Échec de l'Authentification",
|
||||
content: "Veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer de nouveau sur le lien d'authentification.",
|
||||
button: "Fermer la page"
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
success: {
|
||||
title: "Подтверждение завершено",
|
||||
content: "Пожалуйста, закройте эту страницу, вернитесь на страницу аутентификации учетной записи и нажмите кнопку «Далее».",
|
||||
button: "Закрыть страницу"
|
||||
},
|
||||
fail: {
|
||||
title: "Ошибка аутентификации",
|
||||
content: "Пожалуйста, закройте эту страницу, вернитесь на страницу аутентификации учетной записи и повторите процесс аутентификации, щелкнув ссылку.",
|
||||
button: "Закрыть страницу"
|
||||
}
|
||||
},
|
||||
de: {
|
||||
success: {
|
||||
title: "Authentifizierung abgeschlossen",
|
||||
content: "Bitte schließen Sie diese Seite, kehren Sie zur Kontobestätigungsseite zurück und klicken Sie auf „WEITER“.",
|
||||
button: "Seite schließen"
|
||||
},
|
||||
fail: {
|
||||
title: "Authentifizierung fehlgeschlagen",
|
||||
content: "Bitte schließen Sie diese Seite, kehren Sie zur Kontobestätigungsseite zurück und wiederholen Sie den Authentifizierungsprozess, indem Sie auf den Link klicken.",
|
||||
button: "Seite schließen"
|
||||
}
|
||||
},
|
||||
es: {
|
||||
success: {
|
||||
title: "Autenticación completada",
|
||||
content: "Por favor, cierre esta página, regrese a la página de autenticación de la cuenta y haga clic en 'SIGUIENTE'.",
|
||||
button: "Cerrar página"
|
||||
},
|
||||
fail: {
|
||||
title: "Error de autenticación",
|
||||
content: "Por favor, cierre esta página, regrese a la página de autenticación de la cuenta y vuelva a hacer clic en el enlace de autenticación.",
|
||||
button: "Cerrar página"
|
||||
}
|
||||
},
|
||||
ja: {
|
||||
success: {
|
||||
title: "認証完了",
|
||||
content: "このページを閉じて、アカウント認証ページに戻り、「次」をクリックしてください。",
|
||||
button: "ページを閉じる"
|
||||
},
|
||||
fail: {
|
||||
title: "認証失敗",
|
||||
content: "このページを閉じて、アカウント認証ページに戻り、認証リンクを再度クリックしてください。",
|
||||
button: "ページを閉じる"
|
||||
}
|
||||
}
|
||||
}
|
||||
// insert translate into page / match order: locale > language > english
|
||||
document.title = translation[locale]?.[status]?.title ?? translation[language]?.[status]?.title ?? translation["en"]?.[status]?.title;
|
||||
document.getElementById("titleArea").innerText = translation[locale]?.[status]?.title ?? translation[language]?.[status]?.title ?? translation["en"]?.[status]?.title;
|
||||
document.getElementById("contentArea").innerText = translation[locale]?.[status]?.content ?? translation[language]?.[status]?.content ?? translation["en"]?.[status]?.content;
|
||||
document.getElementById("buttonArea").innerText = translation[locale]?.[status]?.button ?? translation[language]?.[status]?.button ?? translation["en"]?.[status]?.button;
|
||||
window.opener=null;
|
||||
window.open('','_self');
|
||||
window.close();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
if _template == '':
|
||||
await asyncio.get_running_loop().run_in_executor(
|
||||
None, _load_page_template)
|
||||
web_page = _template.replace('TITLE_PLACEHOLDER', title)
|
||||
web_page = web_page.replace('CONTENT_PLACEHOLDER', content)
|
||||
web_page = web_page.replace('BUTTON_PLACEHOLDER', button)
|
||||
web_page = web_page.replace(
|
||||
'STATUS_PLACEHOLDER', 'true' if success else 'false')
|
||||
return web_page
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Fehler beim Abrufen der Home Assistant-UUID.",
|
||||
"network_connect_error": "Konfiguration fehlgeschlagen. Netzwerkverbindung fehlgeschlagen. Überprüfen Sie die Netzwerkkonfiguration des Geräts.",
|
||||
"already_configured": "Dieser Benutzer hat die Konfiguration bereits abgeschlossen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Konfiguration zu ändern.",
|
||||
"invalid_auth_info": "Authentifizierungsinformationen sind abgelaufen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \"Konfiguration\", um die Authentifizierung erneut durchzuführen.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Unable to reach Xiaomi MQTT Broker address, please check network configuration."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Failed to get Home Assistant UUID.",
|
||||
"network_connect_error": "Configuration failed. The network connection is abnormal. Please check the equipment network configuration.",
|
||||
"already_configured": "Configuration for this user is already completed. Please go to the integration page and click the CONFIGURE button for modifications.",
|
||||
"invalid_auth_info": "Authentication information has expired. Please go to the integration page and click the CONFIGURE button to re-authenticate.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "No se puede acceder a la dirección del Broker MQTT de Xiaomi, por favor verifique la configuración de la red."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Error al obtener el UUID de Home Assistant.",
|
||||
"network_connect_error": "La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.",
|
||||
"already_configured": "Esta cuenta ya ha finalizado la configuración. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para modificar la configuración.",
|
||||
"invalid_auth_info": "La información de autorización ha caducado. Ingrese a la página de integración y haga clic en el botón \"Configurar\" para volver a autenticarse.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Impossible d'atteindre l'adresse du Broker MQTT de Xiaomi, veuillez vérifier la configuration réseau."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Échec de l'obtention de l'UUID de Home Assistant.",
|
||||
"network_connect_error": "La configuration a échoué. Erreur de connexion réseau. Veuillez vérifier la configuration du réseau de l'appareil.",
|
||||
"already_configured": "Cet utilisateur a déjà terminé la configuration. Veuillez accéder à la page d'intégration et cliquer sur le bouton \"Configurer\" pour modifier la configuration.",
|
||||
"invalid_auth_info": "Les informations d'authentification ont expiré. Veuillez accéder à la page d'intégration et cliquer sur le bouton \"Configurer\" pour vous authentifier à nouveau.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Xiaomi MQTT ブローカーアドレスにアクセスできません。ネットワーク設定を確認してください。"
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Home Assistant インスタンスIDを取得できませんでした。",
|
||||
"network_connect_error": "設定に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク設定を確認してください。",
|
||||
"already_configured": "このユーザーはすでに設定が完了しています。統合ページにアクセスして、「設定」ボタンをクリックして設定を変更してください。",
|
||||
"invalid_auth_info": "認証情報が期限切れになりました。統合ページにアクセスして、「設定」ボタンをクリックして再度認証してください。",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Mislukt bij het ophalen van Home Assistant UUID.",
|
||||
"network_connect_error": "Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.",
|
||||
"already_configured": "Configuratie voor deze gebruiker is al voltooid. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om wijzigingen aan te brengen.",
|
||||
"invalid_auth_info": "Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Falha ao obter o UUID do Home Assistant.",
|
||||
"network_connect_error": "Configuração falhou. A conexão de rede está anormal. Verifique a configuração de rede do equipamento.",
|
||||
"already_configured": "A configuração para este usuário já foi concluída. Vá para a página de integrações e clique no botão CONFIGURAR para modificações.",
|
||||
"invalid_auth_info": "As informações de autenticação expiraram. Vá para a página de integrações e clique em CONFIGURAR para reautenticar.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Não foi possível obter o UUID do Home Assistant.",
|
||||
"network_connect_error": "A configuração falhou. A ligação de rede é anormal. Verifique a configuração de rede do equipamento.",
|
||||
"already_configured": "A configuração para este utilizador já foi concluída. Vá à página de integrações e clique em CONFIGURAR para efetuar alterações.",
|
||||
"invalid_auth_info": "A informação de autenticação expirou. Vá à página de integrações e clique em CONFIGURAR para reautenticar.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети."
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "Не удалось получить UUID Home Assistant.",
|
||||
"network_connect_error": "Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.",
|
||||
"already_configured": "Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.",
|
||||
"invalid_auth_info": "Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "无法访问小米 MQTT Broker 地址,请检查网络配置。"
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "获取 Home Assistant UUID 失败。",
|
||||
"network_connect_error": "配置失败。网络连接异常,请检查设备网络配置。",
|
||||
"already_configured": "该用户已配置完成。请进入集成页面,点击“配置”按钮修改配置。",
|
||||
"invalid_auth_info": "认证信息已过期。请进入集成页面,点击“配置”按钮重新认证。",
|
||||
|
||||
@ -90,6 +90,7 @@
|
||||
"unreachable_mqtt_broker": "無法訪問小米 MQTT Broker 地址,請檢查網絡配置。"
|
||||
},
|
||||
"abort": {
|
||||
"ha_uuid_get_failed": "獲取 Home Assistant UUID 失敗。",
|
||||
"network_connect_error": "配置失敗。網絡連接異常,請檢查設備網絡配置。",
|
||||
"already_configured": "該用戶已配置完成。請進入集成頁面,點擊“配置”按鈕修改配置。",
|
||||
"invalid_auth_info": "認證信息已過期。請進入集成頁面,點擊“配置”按鈕重新認證。",
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test rule format."""
|
||||
import json
|
||||
import logging
|
||||
from os import listdir, path
|
||||
from typing import Optional
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
TRANS_RELATIVE_PATH: str = path.join(
|
||||
ROOT_PATH, '../custom_components/xiaomi_home/translations')
|
||||
@ -27,10 +30,10 @@ def load_json_file(file_path: str) -> Optional[dict]:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return json.load(file)
|
||||
except FileNotFoundError:
|
||||
print(file_path, 'is not found.')
|
||||
_LOGGER.info('%s is not found.', file_path,)
|
||||
return None
|
||||
except json.JSONDecodeError:
|
||||
print(file_path, 'is not a valid JSON file.')
|
||||
_LOGGER.info('%s is not a valid JSON file.', file_path)
|
||||
return None
|
||||
|
||||
|
||||
@ -44,10 +47,10 @@ def load_yaml_file(file_path: str) -> Optional[dict]:
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
return yaml.safe_load(file)
|
||||
except FileNotFoundError:
|
||||
print(file_path, 'is not found.')
|
||||
_LOGGER.info('%s is not found.', file_path)
|
||||
return None
|
||||
except yaml.YAMLError:
|
||||
print(file_path, 'is not a valid YAML file.')
|
||||
_LOGGER.info('%s, is not a valid YAML file.', file_path)
|
||||
return None
|
||||
|
||||
|
||||
@ -116,37 +119,43 @@ def bool_trans(d: dict) -> bool:
|
||||
return False
|
||||
default_trans: dict = d['translate'].pop('default')
|
||||
if not default_trans:
|
||||
print('default trans is empty')
|
||||
_LOGGER.info('default trans is empty')
|
||||
return False
|
||||
default_keys: set[str] = set(default_trans.keys())
|
||||
for key, trans in d['translate'].items():
|
||||
trans_keys: set[str] = set(trans.keys())
|
||||
if set(trans.keys()) != default_keys:
|
||||
print('bool trans inconsistent', key, default_keys, trans_keys)
|
||||
_LOGGER.info(
|
||||
'bool trans inconsistent, %s, %s, %s',
|
||||
key, default_keys, trans_keys)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def compare_dict_structure(dict1: dict, dict2: dict) -> bool:
|
||||
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
|
||||
print('invalid type')
|
||||
_LOGGER.info('invalid type')
|
||||
return False
|
||||
if dict1.keys() != dict2.keys():
|
||||
print('inconsistent key values, ', dict1.keys(), dict2.keys())
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, %s, %s', dict1.keys(), dict2.keys())
|
||||
return False
|
||||
for key in dict1:
|
||||
if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
|
||||
if not compare_dict_structure(dict1[key], dict2[key]):
|
||||
print('inconsistent key values, dict, ', key)
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, dict, %s', key)
|
||||
return False
|
||||
elif isinstance(dict1[key], list) and isinstance(dict2[key], list):
|
||||
if not all(
|
||||
isinstance(i, type(j))
|
||||
for i, j in zip(dict1[key], dict2[key])):
|
||||
print('inconsistent key values, list, ', key)
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, list, %s', key)
|
||||
return False
|
||||
elif not isinstance(dict1[key], type(dict2[key])):
|
||||
print('inconsistent key values, type, ', key)
|
||||
_LOGGER.info(
|
||||
'inconsistent key values, type, %s', key)
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -239,7 +248,8 @@ def test_miot_lang_integrity():
|
||||
compare_dict: dict = load_json_file(
|
||||
path.join(TRANS_RELATIVE_PATH, name))
|
||||
if not compare_dict_structure(default_dict, compare_dict):
|
||||
print('compare_dict_structure failed /translations, ', name)
|
||||
_LOGGER.info(
|
||||
'compare_dict_structure failed /translations, %s', name)
|
||||
assert False
|
||||
# Check i18n files structure
|
||||
default_dict = load_json_file(
|
||||
@ -248,7 +258,8 @@ def test_miot_lang_integrity():
|
||||
compare_dict: dict = load_json_file(
|
||||
path.join(MIOT_I18N_RELATIVE_PATH, name))
|
||||
if not compare_dict_structure(default_dict, compare_dict):
|
||||
print('compare_dict_structure failed /miot/i18n, ', name)
|
||||
_LOGGER.info(
|
||||
'compare_dict_structure failed /miot/i18n, %s', name)
|
||||
assert False
|
||||
|
||||
|
||||
@ -284,10 +295,10 @@ def test_miot_data_sort():
|
||||
def test_sort_spec_data():
|
||||
sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)
|
||||
save_json_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data)
|
||||
print(SPEC_BOOL_TRANS_FILE, 'formatted.')
|
||||
_LOGGER.info('%s formatted.', SPEC_BOOL_TRANS_FILE)
|
||||
sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)
|
||||
save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)
|
||||
print(SPEC_MULTI_LANG_FILE, 'formatted.')
|
||||
_LOGGER.info('%s formatted.', SPEC_MULTI_LANG_FILE)
|
||||
sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)
|
||||
save_json_file(file_path=SPEC_FILTER_FILE, data=sort_data)
|
||||
print(SPEC_FILTER_FILE, 'formatted.')
|
||||
_LOGGER.info('%s formatted.', SPEC_FILTER_FILE)
|
||||
|
||||
@ -1,16 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Pytest fixtures."""
|
||||
import logging
|
||||
import random
|
||||
import shutil
|
||||
import pytest
|
||||
from os import path, makedirs
|
||||
from uuid import uuid4
|
||||
|
||||
TEST_ROOT_PATH: str = path.dirname(path.abspath(__file__))
|
||||
TEST_FILES_PATH: str = path.join(TEST_ROOT_PATH, 'miot')
|
||||
TEST_CACHE_PATH: str = path.join(TEST_ROOT_PATH, 'test_cache')
|
||||
TEST_OAUTH2_REDIRECT_URL: str = 'http://homeassistant.local:8123'
|
||||
TEST_LANG: str = 'zh-Hans'
|
||||
TEST_UID: str = '123456789'
|
||||
TEST_CLOUD_SERVER: str = 'cn'
|
||||
|
||||
DOMAIN_CLOUD_CACHE: str = 'cloud_cache'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def set_logger():
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
_LOGGER.info('set logger, %s', logger)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def load_py_file():
|
||||
@ -20,10 +41,10 @@ def load_py_file():
|
||||
'const.py',
|
||||
'miot_cloud.py',
|
||||
'miot_error.py',
|
||||
'miot_ev.py',
|
||||
'miot_i18n.py',
|
||||
'miot_lan.py',
|
||||
'miot_mdns.py',
|
||||
'miot_mips.py',
|
||||
'miot_network.py',
|
||||
'miot_spec.py',
|
||||
'miot_storage.py']
|
||||
@ -35,31 +56,35 @@ def load_py_file():
|
||||
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot',
|
||||
file_name),
|
||||
path.join(TEST_FILES_PATH, file_name))
|
||||
print('\nloaded test py files, ', file_list)
|
||||
_LOGGER.info('\nloaded test py files, %s', file_list)
|
||||
# Copy spec files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/specs'),
|
||||
dst=path.join(TEST_FILES_PATH, 'specs'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded spec test folder, specs')
|
||||
_LOGGER.info('loaded spec test folder, specs')
|
||||
# Copy lan files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'),
|
||||
dst=path.join(TEST_FILES_PATH, 'lan'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded lan test folder, lan')
|
||||
_LOGGER.info('loaded lan test folder, lan')
|
||||
# Copy i18n files to test folder
|
||||
shutil.copytree(
|
||||
src=path.join(
|
||||
TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n'),
|
||||
dst=path.join(TEST_FILES_PATH, 'i18n'),
|
||||
dirs_exist_ok=True)
|
||||
print('loaded i18n test folder, i18n')
|
||||
_LOGGER.info('loaded i18n test folder, i18n')
|
||||
|
||||
yield
|
||||
|
||||
# NOTICE: All test files and data (tokens, device information, etc.) will
|
||||
# be deleted after the test is completed. For some test cases that
|
||||
# require caching data, you can comment out the following code.
|
||||
|
||||
if path.exists(TEST_FILES_PATH):
|
||||
shutil.rmtree(TEST_FILES_PATH)
|
||||
print('\nremoved test files, ', TEST_FILES_PATH)
|
||||
@ -80,6 +105,11 @@ def test_cache_path() -> str:
|
||||
return TEST_CACHE_PATH
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_oauth2_redirect_url() -> str:
|
||||
return TEST_OAUTH2_REDIRECT_URL
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_lang() -> str:
|
||||
return TEST_LANG
|
||||
@ -90,6 +120,53 @@ def test_uid() -> str:
|
||||
return TEST_UID
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_random_did() -> str:
|
||||
# Gen random did
|
||||
return str(random.getrandbits(64))
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_uuid() -> str:
|
||||
# Gen uuid
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_cloud_server() -> str:
|
||||
return TEST_CLOUD_SERVER
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_domain_cloud_cache() -> str:
|
||||
return DOMAIN_CLOUD_CACHE
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_name_oauth2_info() -> str:
|
||||
return f'{TEST_CLOUD_SERVER}_oauth2_info'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_name_uid() -> str:
|
||||
return f'{TEST_CLOUD_SERVER}_uid'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_name_uuid() -> str:
|
||||
return f'{TEST_CLOUD_SERVER}_uuid'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_name_rd_did() -> str:
|
||||
return f'{TEST_CLOUD_SERVER}_rd_did'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_name_homes() -> str:
|
||||
return f'{TEST_CLOUD_SERVER}_homes'
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def test_name_devices() -> str:
|
||||
return f'{TEST_CLOUD_SERVER}_devices'
|
||||
|
||||
587
test/test_cloud.py
Executable file
587
test/test_cloud.py
Executable file
@ -0,0 +1,587 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_cloud.py."""
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import webbrowser
|
||||
import pytest
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_oauth_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_oauth2_redirect_url: str,
|
||||
test_uuid: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_uuid: str
|
||||
) -> dict:
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTOauthClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
local_uuid = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uuid, type_=str)
|
||||
uuid = str(local_uuid or test_uuid)
|
||||
_LOGGER.info('uuid: %s', uuid)
|
||||
miot_oauth = MIoTOauthClient(
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
redirect_url=test_oauth2_redirect_url,
|
||||
cloud_server=test_cloud_server,
|
||||
uuid=uuid)
|
||||
|
||||
oauth_info = None
|
||||
load_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
if (
|
||||
isinstance(load_info, dict)
|
||||
and 'access_token' in load_info
|
||||
and 'expires_ts' in load_info
|
||||
and load_info['expires_ts'] > int(time.time())
|
||||
):
|
||||
_LOGGER.info('load oauth info, %s', load_info)
|
||||
oauth_info = load_info
|
||||
if oauth_info is None:
|
||||
# gen oauth url
|
||||
auth_url: str = miot_oauth.gen_auth_url()
|
||||
assert isinstance(auth_url, str)
|
||||
_LOGGER.info('auth url: %s', auth_url)
|
||||
# get code
|
||||
webbrowser.open(auth_url)
|
||||
code: str = input('input code: ')
|
||||
assert code is not None
|
||||
# get access_token
|
||||
res_obj = await miot_oauth.get_access_token_async(code=code)
|
||||
assert res_obj is not None
|
||||
oauth_info = res_obj
|
||||
_LOGGER.info('get_access_token result: %s', res_obj)
|
||||
rc = await miot_storage.save_async(
|
||||
test_domain_cloud_cache, test_name_oauth2_info, oauth_info)
|
||||
assert rc
|
||||
_LOGGER.info('save oauth info')
|
||||
rc = await miot_storage.save_async(
|
||||
test_domain_cloud_cache, test_name_uuid, uuid)
|
||||
assert rc
|
||||
_LOGGER.info('save uuid')
|
||||
|
||||
access_token = oauth_info.get('access_token', None)
|
||||
assert isinstance(access_token, str)
|
||||
_LOGGER.info('access_token: %s', access_token)
|
||||
refresh_token = oauth_info.get('refresh_token', None)
|
||||
assert isinstance(refresh_token, str)
|
||||
_LOGGER.info('refresh_token: %s', refresh_token)
|
||||
|
||||
await miot_oauth.deinit_async()
|
||||
return oauth_info
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency(on=['test_miot_oauth_async'])
|
||||
async def test_miot_oauth_refresh_token(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_oauth2_redirect_url: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_uuid: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTOauthClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
uuid = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uuid, type_=str)
|
||||
assert isinstance(uuid, str)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict)
|
||||
assert 'access_token' in oauth_info
|
||||
assert 'refresh_token' in oauth_info
|
||||
assert 'expires_ts' in oauth_info
|
||||
remaining_time = oauth_info['expires_ts'] - int(time.time())
|
||||
_LOGGER.info('token remaining valid time: %ss', remaining_time)
|
||||
# Refresh token
|
||||
miot_oauth = MIoTOauthClient(
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
redirect_url=test_oauth2_redirect_url,
|
||||
cloud_server=test_cloud_server,
|
||||
uuid=uuid)
|
||||
refresh_token = oauth_info.get('refresh_token', None)
|
||||
assert refresh_token
|
||||
update_info = await miot_oauth.refresh_access_token_async(
|
||||
refresh_token=refresh_token)
|
||||
assert update_info
|
||||
assert 'access_token' in update_info
|
||||
assert 'refresh_token' in update_info
|
||||
assert 'expires_ts' in update_info
|
||||
remaining_time = update_info['expires_ts'] - int(time.time())
|
||||
assert remaining_time > 0
|
||||
_LOGGER.info('refresh token, remaining valid time: %ss', remaining_time)
|
||||
# Save oauth2 info
|
||||
rc = await miot_storage.save_async(
|
||||
test_domain_cloud_cache, test_name_oauth2_info, update_info)
|
||||
assert rc
|
||||
_LOGGER.info('refresh token success, %s', update_info)
|
||||
|
||||
await miot_oauth.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_nickname_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Get nickname
|
||||
user_info = await miot_http.get_user_info_async()
|
||||
assert isinstance(user_info, dict) and 'miliaoNick' in user_info
|
||||
nickname = user_info['miliaoNick']
|
||||
_LOGGER.info('your nickname: %s', nickname)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_uid_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_uid: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
uid = await miot_http.get_uid_async()
|
||||
assert isinstance(uid, str)
|
||||
_LOGGER.info('your uid: %s', uid)
|
||||
# Save uid
|
||||
rc = await miot_storage.save_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uid, data=uid)
|
||||
assert rc
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_homeinfos_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_uid: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Get homeinfos
|
||||
homeinfos = await miot_http.get_homeinfos_async()
|
||||
assert isinstance(homeinfos, dict)
|
||||
assert 'uid' in homeinfos and isinstance(homeinfos['uid'], str)
|
||||
assert 'home_list' in homeinfos and isinstance(
|
||||
homeinfos['home_list'], dict)
|
||||
assert 'share_home_list' in homeinfos and isinstance(
|
||||
homeinfos['share_home_list'], dict)
|
||||
# Get uid
|
||||
uid = homeinfos.get('uid', '')
|
||||
# Compare uid with uid in storage
|
||||
uid2 = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uid, type_=str)
|
||||
assert uid == uid2
|
||||
_LOGGER.info('your uid: %s', uid)
|
||||
# Get homes
|
||||
home_list = homeinfos.get('home_list', {})
|
||||
_LOGGER.info('your home_list: ,%s', home_list)
|
||||
# Get share homes
|
||||
share_home_list = homeinfos.get('share_home_list', {})
|
||||
_LOGGER.info('your share_home_list: %s', share_home_list)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_devices_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_uid: str,
|
||||
test_name_homes: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Get devices
|
||||
devices = await miot_http.get_devices_async()
|
||||
assert isinstance(devices, dict)
|
||||
assert 'uid' in devices and isinstance(devices['uid'], str)
|
||||
assert 'homes' in devices and isinstance(devices['homes'], dict)
|
||||
assert 'devices' in devices and isinstance(devices['devices'], dict)
|
||||
# Compare uid with uid in storage
|
||||
uid = devices.get('uid', '')
|
||||
uid2 = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uid, type_=str)
|
||||
assert uid == uid2
|
||||
_LOGGER.info('your uid: %s', uid)
|
||||
# Get homes
|
||||
homes = devices['homes']
|
||||
_LOGGER.info('your homes: %s', homes)
|
||||
# Get devices
|
||||
devices = devices['devices']
|
||||
_LOGGER.info('your devices count: %s', len(devices))
|
||||
# Storage homes and devices
|
||||
rc = await miot_storage.save_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_homes, data=homes)
|
||||
assert rc
|
||||
rc = await miot_storage.save_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, data=devices)
|
||||
assert rc
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_devices_with_dids_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Load devices
|
||||
local_devices = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)
|
||||
assert isinstance(local_devices, dict)
|
||||
did_list = list(local_devices.keys())
|
||||
assert len(did_list) > 0
|
||||
# Get device with dids
|
||||
test_list = did_list[:6]
|
||||
devices_info = await miot_http.get_devices_with_dids_async(
|
||||
dids=test_list)
|
||||
assert isinstance(devices_info, dict)
|
||||
_LOGGER.info('test did list, %s, %s', len(test_list), test_list)
|
||||
_LOGGER.info(
|
||||
'test result: %s, %s', len(devices_info), list(devices_info.keys()))
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_miot_cloud_get_cert(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_random_did: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_uid: str,
|
||||
test_name_rd_did: str
|
||||
):
|
||||
"""
|
||||
NOTICE: Currently, only certificate acquisition in the CN region is
|
||||
supported.
|
||||
"""
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTCert, MIoTStorage
|
||||
|
||||
if test_cloud_server.lower() != 'cn':
|
||||
_LOGGER.info('only support CN region')
|
||||
return
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
uid = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uid, type_=str)
|
||||
assert isinstance(uid, str)
|
||||
_LOGGER.info('your uid: %s', uid)
|
||||
random_did = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_rd_did, type_=str)
|
||||
if not random_did:
|
||||
random_did = test_random_did
|
||||
rc = await miot_storage.save_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_rd_did,
|
||||
data=random_did)
|
||||
assert rc
|
||||
assert isinstance(random_did, str)
|
||||
_LOGGER.info('your random_did: %s', random_did)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict)
|
||||
assert 'access_token' in oauth_info
|
||||
access_token = oauth_info['access_token']
|
||||
|
||||
# Get certificates
|
||||
miot_cert = MIoTCert(storage=miot_storage, uid=uid, cloud_server='CN')
|
||||
assert await miot_cert.verify_ca_cert_async(), 'invalid ca cert'
|
||||
remaining_time: int = await miot_cert.user_cert_remaining_time_async()
|
||||
if remaining_time > 0:
|
||||
_LOGGER.info(
|
||||
'user cert is valid, remaining time, %ss', remaining_time)
|
||||
_LOGGER.info((
|
||||
'if you want to obtain it again, please delete the '
|
||||
'key, csr, and cert files in %s.'), test_cache_path)
|
||||
return
|
||||
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server,
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=access_token)
|
||||
|
||||
user_key = miot_cert.gen_user_key()
|
||||
assert isinstance(user_key, str)
|
||||
_LOGGER.info('user_key str, %s', user_key)
|
||||
user_csr = miot_cert.gen_user_csr(user_key=user_key, did=random_did)
|
||||
assert isinstance(user_csr, str)
|
||||
_LOGGER.info('user_csr str, %s', user_csr)
|
||||
cert_str = await miot_http.get_central_cert_async(csr=user_csr)
|
||||
assert isinstance(cert_str, str)
|
||||
_LOGGER.info('user_cert str, %s', cert_str)
|
||||
rc = await miot_cert.update_user_key_async(key=user_key)
|
||||
assert rc
|
||||
rc = await miot_cert.update_user_cert_async(cert=cert_str)
|
||||
assert rc
|
||||
# verify user certificates
|
||||
remaining_time = await miot_cert.user_cert_remaining_time_async(
|
||||
cert_data=cert_str.encode('utf-8'), did=random_did)
|
||||
assert remaining_time > 0
|
||||
_LOGGER.info('user cert remaining time, %ss', remaining_time)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_prop_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Load devices
|
||||
local_devices = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)
|
||||
assert isinstance(local_devices, dict)
|
||||
did_list = list(local_devices.keys())
|
||||
assert len(did_list) > 0
|
||||
# Get prop
|
||||
test_list = did_list[:6]
|
||||
for did in test_list:
|
||||
prop_value = await miot_http.get_prop_async(did=did, siid=2, piid=1)
|
||||
device_name = local_devices[did]['name']
|
||||
_LOGGER.info('%s(%s), prop.2.1: %s', device_name, did, prop_value)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_get_props_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Load devices
|
||||
local_devices = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)
|
||||
assert isinstance(local_devices, dict)
|
||||
did_list = list(local_devices.keys())
|
||||
assert len(did_list) > 0
|
||||
# Get props
|
||||
test_list = did_list[:6]
|
||||
prop_values = await miot_http.get_props_async(params=[
|
||||
{'did': did, 'siid': 2, 'piid': 1} for did in test_list])
|
||||
|
||||
_LOGGER.info('test did list, %s, %s', len(test_list), test_list)
|
||||
_LOGGER.info('test result, %s, %s', len(prop_values), prop_values)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='skip danger operation')
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_set_prop_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
"""
|
||||
WARNING: This test case will control the actual device and is not enabled
|
||||
by default. You can uncomment @pytest.mark.skip to enable it.
|
||||
"""
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Load devices
|
||||
local_devices = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)
|
||||
assert isinstance(local_devices, dict)
|
||||
assert len(local_devices) > 0
|
||||
# Set prop
|
||||
# Find central hub gateway, control its indicator light switch
|
||||
# You can replace it with the device you want to control.
|
||||
test_did = ''
|
||||
for did, dev in local_devices.items():
|
||||
if dev['model'] == 'xiaomi.gateway.hub1':
|
||||
test_did = did
|
||||
break
|
||||
assert test_did != '', 'no central hub gateway found'
|
||||
result = await miot_http.set_prop_async(params=[{
|
||||
'did': test_did, 'siid': 3, 'piid': 1, 'value': False}])
|
||||
_LOGGER.info('test did, %s, prop.3.1=False -> %s', test_did, result)
|
||||
await asyncio.sleep(1)
|
||||
result = await miot_http.set_prop_async(params=[{
|
||||
'did': test_did, 'siid': 3, 'piid': 1, 'value': True}])
|
||||
_LOGGER.info('test did, %s, prop.3.1=True -> %s', test_did, result)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.skip(reason='skip danger operation')
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.dependency()
|
||||
async def test_miot_cloud_action_async(
|
||||
test_cache_path: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
"""
|
||||
WARNING: This test case will control the actual device and is not enabled
|
||||
by default. You can uncomment @pytest.mark.skip to enable it.
|
||||
"""
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
from miot.miot_storage import MIoTStorage
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=oauth_info['access_token'])
|
||||
|
||||
# Load devices
|
||||
local_devices = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)
|
||||
assert isinstance(local_devices, dict)
|
||||
assert len(local_devices) > 0
|
||||
# Action
|
||||
# Find central hub gateway, trigger its virtual events
|
||||
# You can replace it with the device you want to control.
|
||||
test_did = ''
|
||||
for did, dev in local_devices.items():
|
||||
if dev['model'] == 'xiaomi.gateway.hub1':
|
||||
test_did = did
|
||||
break
|
||||
assert test_did != '', 'no central hub gateway found'
|
||||
result = await miot_http.action_async(
|
||||
did=test_did, siid=4, aiid=1,
|
||||
in_list=[{'piid': 1, 'value': 'hello world.'}])
|
||||
_LOGGER.info('test did, %s, action.4.1 -> %s', test_did, result)
|
||||
|
||||
await miot_http.deinit_async()
|
||||
@ -18,7 +18,7 @@ def test_miot_matcher():
|
||||
if not matcher.get(topic=f'test/+/{l2}'):
|
||||
matcher[f'test/+/{l2}'] = f'test/+/{l2}'
|
||||
# Match
|
||||
match_result: list[(str, dict)] = list(matcher.iter_all_nodes())
|
||||
match_result: list[str] = list(matcher.iter_all_nodes())
|
||||
assert len(match_result) == 120
|
||||
match_result: list[str] = list(matcher.iter_match(topic='test/1/1'))
|
||||
assert len(match_result) == 3
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_ev.py."""
|
||||
import os
|
||||
import pytest
|
||||
|
||||
# pylint: disable=import-outside-toplevel, disable=unused-argument
|
||||
|
||||
|
||||
@pytest.mark.github
|
||||
def test_mev_timer_and_fd():
|
||||
from miot.miot_ev import MIoTEventLoop, TimeoutHandle
|
||||
|
||||
mev = MIoTEventLoop()
|
||||
assert mev
|
||||
event_fd: os.eventfd = os.eventfd(0, os.O_NONBLOCK)
|
||||
assert event_fd
|
||||
timer4: TimeoutHandle = None
|
||||
|
||||
def event_handler(event_fd):
|
||||
value: int = os.eventfd_read(event_fd)
|
||||
if value == 1:
|
||||
mev.clear_timeout(timer4)
|
||||
print('cancel timer4')
|
||||
elif value == 2:
|
||||
print('event write twice in a row')
|
||||
elif value == 3:
|
||||
mev.set_read_handler(event_fd, None, None)
|
||||
os.close(event_fd)
|
||||
event_fd = None
|
||||
print('close event fd')
|
||||
|
||||
def timer1_handler(event_fd):
|
||||
os.eventfd_write(event_fd, 1)
|
||||
|
||||
def timer2_handler(event_fd):
|
||||
os.eventfd_write(event_fd, 1)
|
||||
os.eventfd_write(event_fd, 1)
|
||||
|
||||
def timer3_handler(event_fd):
|
||||
os.eventfd_write(event_fd, 3)
|
||||
|
||||
def timer4_handler(event_fd):
|
||||
raise ValueError('unreachable code')
|
||||
|
||||
mev.set_read_handler(
|
||||
event_fd, event_handler, event_fd)
|
||||
|
||||
mev.set_timeout(500, timer1_handler, event_fd)
|
||||
mev.set_timeout(1000, timer2_handler, event_fd)
|
||||
mev.set_timeout(1500, timer3_handler, event_fd)
|
||||
timer4 = mev.set_timeout(2000, timer4_handler, event_fd)
|
||||
|
||||
mev.loop_forever()
|
||||
# Loop will exit when there are no timers or fd handlers.
|
||||
mev.loop_stop()
|
||||
@ -1,11 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_lan.py."""
|
||||
import logging
|
||||
from typing import Any
|
||||
import pytest
|
||||
import asyncio
|
||||
from zeroconf import IPVersion
|
||||
from zeroconf.asyncio import AsyncZeroconf
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@ -67,7 +70,7 @@ async def test_lan_async(test_devices: dict):
|
||||
|
||||
miot_network = MIoTNetwork()
|
||||
await miot_network.init_async()
|
||||
print('miot_network, ', miot_network.network_info)
|
||||
_LOGGER.info('miot_network, %s', miot_network.network_info)
|
||||
mips_service = MipsService(
|
||||
aiozc=AsyncZeroconf(ip_version=IPVersion.V4Only))
|
||||
await mips_service.init_async()
|
||||
@ -81,7 +84,7 @@ async def test_lan_async(test_devices: dict):
|
||||
await miot_lan.vote_for_lan_ctrl_async(key='test', vote=True)
|
||||
|
||||
async def device_state_change(did: str, state: dict, ctx: Any):
|
||||
print('device state change, ', did, state)
|
||||
_LOGGER.info('device state change, %s, %s', did, state)
|
||||
if did != test_did:
|
||||
return
|
||||
if (
|
||||
@ -91,10 +94,10 @@ async def test_lan_async(test_devices: dict):
|
||||
# Test sub prop
|
||||
miot_lan.sub_prop(
|
||||
did=did, siid=3, piid=1, handler=lambda msg, ctx:
|
||||
print(f'sub prop.3.1 msg, {did}={msg}'))
|
||||
_LOGGER.info('sub prop.3.1 msg, %s=%s', did, msg))
|
||||
miot_lan.sub_prop(
|
||||
did=did, handler=lambda msg, ctx:
|
||||
print(f'sub all device msg, {did}={msg}'))
|
||||
_LOGGER.info('sub all device msg, %s=%s', did, msg))
|
||||
evt_push_available.set()
|
||||
else:
|
||||
# miot_lan.unsub_prop(did=did, siid=3, piid=1)
|
||||
@ -102,7 +105,7 @@ async def test_lan_async(test_devices: dict):
|
||||
evt_push_unavailable.set()
|
||||
|
||||
async def lan_state_change(state: bool):
|
||||
print('lan state change, ', state)
|
||||
_LOGGER.info('lan state change, %s', state)
|
||||
if not state:
|
||||
return
|
||||
miot_lan.update_devices(devices={
|
||||
|
||||
@ -1,28 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_mdns.py."""
|
||||
import asyncio
|
||||
import logging
|
||||
import pytest
|
||||
from zeroconf import IPVersion
|
||||
from zeroconf.asyncio import AsyncZeroconf
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_loop_async():
|
||||
from miot.miot_mdns import MipsService, MipsServiceData, MipsServiceState
|
||||
from miot.miot_mdns import MipsService, MipsServiceState
|
||||
|
||||
async def on_service_state_change(
|
||||
group_id: str, state: MipsServiceState, data: MipsServiceData):
|
||||
print(
|
||||
group_id: str, state: MipsServiceState, data: dict):
|
||||
_LOGGER.info(
|
||||
'on_service_state_change, %s, %s, %s', group_id, state, data)
|
||||
|
||||
async with AsyncZeroconf(ip_version=IPVersion.V4Only) as aiozc:
|
||||
mips_service = MipsService(aiozc)
|
||||
mips_service.sub_service_change('test', '*', on_service_state_change)
|
||||
await mips_service.init_async()
|
||||
# Wait for service to discover
|
||||
await asyncio.sleep(3)
|
||||
services_detail = mips_service.get_services()
|
||||
print('get all service, ', services_detail.keys())
|
||||
_LOGGER.info('get all service, %s', list(services_detail.keys()))
|
||||
for name, data in services_detail.items():
|
||||
print(
|
||||
'\tinfo, ', name, data['did'], data['addresses'], data['port'])
|
||||
_LOGGER.info(
|
||||
'\tinfo, %s, %s, %s, %s',
|
||||
name, data['did'], data['addresses'], data['port'])
|
||||
await mips_service.deinit_async()
|
||||
|
||||
264
test/test_mips.py
Normal file
264
test/test_mips.py
Normal file
@ -0,0 +1,264 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_mips.py.
|
||||
NOTICE: When running this test case, you need to run test_cloud.py first to
|
||||
obtain the token and certificate information, and at the same time avoid data
|
||||
deletion.
|
||||
"""
|
||||
import ipaddress
|
||||
from typing import Any, Tuple
|
||||
import pytest
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable = import-outside-toplevel, unused-argument
|
||||
|
||||
@pytest.mark.parametrize('central_info', [
|
||||
('<Group id>', 'Gateway did', 'Gateway ip', 8883),
|
||||
])
|
||||
@pytest.mark.asyncio
|
||||
async def test_mips_local_async(
|
||||
test_cache_path: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_uid: str,
|
||||
test_name_rd_did: str,
|
||||
central_info: Tuple[str, str, str, int]
|
||||
):
|
||||
"""
|
||||
NOTICE:
|
||||
- Mips local is used to connect to the central gateway and is only
|
||||
supported in the Chinese mainland region.
|
||||
- Before running this test case, you need to run test_mdns.py first to
|
||||
obtain the group_id, did, ip, and port of the hub, and then fill in this
|
||||
information in the parametrize. you can enter multiple central connection
|
||||
information items for separate tests.
|
||||
- This test case requires running test_cloud.py first to obtain the
|
||||
central connection certificate.
|
||||
- This test case will control the indicator light switch of the central
|
||||
gateway.
|
||||
"""
|
||||
from miot.miot_storage import MIoTStorage, MIoTCert
|
||||
from miot.miot_mips import MipsLocalClient
|
||||
|
||||
central_group_id: str = central_info[0]
|
||||
assert isinstance(central_group_id, str)
|
||||
central_did: str = central_info[1]
|
||||
assert central_did.isdigit()
|
||||
central_ip: str = central_info[2]
|
||||
assert ipaddress.ip_address(central_ip)
|
||||
central_port: int = central_info[3]
|
||||
assert isinstance(central_port, int)
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
uid = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uid, type_=str)
|
||||
assert isinstance(uid, str)
|
||||
random_did = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_rd_did, type_=str)
|
||||
assert isinstance(random_did, str)
|
||||
miot_cert = MIoTCert(storage=miot_storage, uid=uid, cloud_server='CN')
|
||||
assert miot_cert.ca_file
|
||||
assert miot_cert.cert_file
|
||||
assert miot_cert.key_file
|
||||
_LOGGER.info(
|
||||
'cert info, %s, %s, %s', miot_cert.ca_file, miot_cert.cert_file,
|
||||
miot_cert.key_file)
|
||||
|
||||
mips_local = MipsLocalClient(
|
||||
did=random_did,
|
||||
host=central_ip,
|
||||
group_id=central_group_id,
|
||||
ca_file=miot_cert.ca_file,
|
||||
cert_file=miot_cert.cert_file,
|
||||
key_file=miot_cert.key_file,
|
||||
port=central_port,
|
||||
home_name='mips local test')
|
||||
mips_local.enable_logger(logger=_LOGGER)
|
||||
mips_local.enable_mqtt_logger(logger=_LOGGER)
|
||||
|
||||
async def on_mips_state_changed_async(key: str, state: bool):
|
||||
_LOGGER.info('on mips state changed, %s, %s', key, state)
|
||||
|
||||
async def on_dev_list_changed_async(
|
||||
mips: MipsLocalClient, did_list: list[str]
|
||||
):
|
||||
_LOGGER.info('dev list changed, %s', did_list)
|
||||
|
||||
def on_prop_changed(payload: dict, ctx: Any):
|
||||
_LOGGER.info('prop changed, %s=%s', ctx, payload)
|
||||
|
||||
def on_event_occurred(payload: dict, ctx: Any):
|
||||
_LOGGER.info('event occurred, %s=%s', ctx, payload)
|
||||
|
||||
# Reg mips state
|
||||
mips_local.sub_mips_state(
|
||||
key='mips_local', handler=on_mips_state_changed_async)
|
||||
mips_local.on_dev_list_changed = on_dev_list_changed_async
|
||||
# Connect
|
||||
await mips_local.connect_async()
|
||||
await asyncio.sleep(0.5)
|
||||
# Get device list
|
||||
device_list = await mips_local.get_dev_list_async()
|
||||
assert isinstance(device_list, dict)
|
||||
_LOGGER.info(
|
||||
'get_dev_list, %d, %s', len(device_list), list(device_list.keys()))
|
||||
# Sub Prop
|
||||
mips_local.sub_prop(
|
||||
did=central_did, handler=on_prop_changed,
|
||||
handler_ctx=f'{central_did}.*')
|
||||
# Sub Event
|
||||
mips_local.sub_event(
|
||||
did=central_did, handler=on_event_occurred,
|
||||
handler_ctx=f'{central_did}.*')
|
||||
# Get/set prop
|
||||
test_siid = 3
|
||||
test_piid = 1
|
||||
# mips_local.sub_prop(
|
||||
# did=central_did, siid=test_siid, piid=test_piid,
|
||||
# handler=on_prop_changed,
|
||||
# handler_ctx=f'{central_did}.{test_siid}.{test_piid}')
|
||||
result1 = await mips_local.get_prop_async(
|
||||
did=central_did, siid=test_siid, piid=test_piid)
|
||||
assert isinstance(result1, bool)
|
||||
_LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result1)
|
||||
result2 = await mips_local.set_prop_async(
|
||||
did=central_did, siid=test_siid, piid=test_piid, value=not result1)
|
||||
_LOGGER.info(
|
||||
'set prop.%s.%s=%s, result=%s',
|
||||
test_siid, test_piid, not result1, result2)
|
||||
assert isinstance(result2, dict)
|
||||
result3 = await mips_local.get_prop_async(
|
||||
did=central_did, siid=test_siid, piid=test_piid)
|
||||
assert isinstance(result3, bool)
|
||||
_LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result3)
|
||||
# Action
|
||||
test_siid = 4
|
||||
test_aiid = 1
|
||||
in_list = [{'piid': 1, 'value': 'hello world.'}]
|
||||
result4 = await mips_local.action_async(
|
||||
did=central_did, siid=test_siid, aiid=test_aiid,
|
||||
in_list=in_list)
|
||||
assert isinstance(result4, dict)
|
||||
_LOGGER.info(
|
||||
'action.%s.%s=%s, result=%s', test_siid, test_piid, in_list, result4)
|
||||
# Disconnect
|
||||
await mips_local.disconnect_async()
|
||||
await mips_local.deinit_async()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mips_cloud_async(
|
||||
test_cache_path: str,
|
||||
test_name_uuid: str,
|
||||
test_cloud_server: str,
|
||||
test_domain_cloud_cache: str,
|
||||
test_name_oauth2_info: str,
|
||||
test_name_devices: str
|
||||
):
|
||||
"""
|
||||
NOTICE:
|
||||
- This test case requires running test_cloud.py first to obtain the
|
||||
central connection certificate.
|
||||
- This test case will control the indicator light switch of the central
|
||||
gateway.
|
||||
"""
|
||||
from miot.const import OAUTH2_CLIENT_ID
|
||||
from miot.miot_storage import MIoTStorage
|
||||
from miot.miot_mips import MipsCloudClient
|
||||
from miot.miot_cloud import MIoTHttpClient
|
||||
|
||||
miot_storage = MIoTStorage(test_cache_path)
|
||||
uuid = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_uuid, type_=str)
|
||||
assert isinstance(uuid, str)
|
||||
oauth_info = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)
|
||||
assert isinstance(oauth_info, dict) and 'access_token' in oauth_info
|
||||
access_token = oauth_info['access_token']
|
||||
_LOGGER.info('connect info, %s, %s', uuid, access_token)
|
||||
mips_cloud = MipsCloudClient(
|
||||
uuid=uuid,
|
||||
cloud_server=test_cloud_server,
|
||||
app_id=OAUTH2_CLIENT_ID,
|
||||
token=access_token)
|
||||
mips_cloud.enable_logger(logger=_LOGGER)
|
||||
mips_cloud.enable_mqtt_logger(logger=_LOGGER)
|
||||
miot_http = MIoTHttpClient(
|
||||
cloud_server=test_cloud_server,
|
||||
client_id=OAUTH2_CLIENT_ID,
|
||||
access_token=access_token)
|
||||
|
||||
async def on_mips_state_changed_async(key: str, state: bool):
|
||||
_LOGGER.info('on mips state changed, %s, %s', key, state)
|
||||
|
||||
def on_prop_changed(payload: dict, ctx: Any):
|
||||
_LOGGER.info('prop changed, %s=%s', ctx, payload)
|
||||
|
||||
def on_event_occurred(payload: dict, ctx: Any):
|
||||
_LOGGER.info('event occurred, %s=%s', ctx, payload)
|
||||
|
||||
await mips_cloud.connect_async()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Sub mips state
|
||||
mips_cloud.sub_mips_state(
|
||||
key='mips_cloud', handler=on_mips_state_changed_async)
|
||||
# Load devices
|
||||
local_devices = await miot_storage.load_async(
|
||||
domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)
|
||||
assert isinstance(local_devices, dict)
|
||||
central_did = ''
|
||||
for did, info in local_devices.items():
|
||||
if info['model'] != 'xiaomi.gateway.hub1':
|
||||
continue
|
||||
central_did = did
|
||||
break
|
||||
if central_did:
|
||||
# Sub Prop
|
||||
mips_cloud.sub_prop(
|
||||
did=central_did, handler=on_prop_changed,
|
||||
handler_ctx=f'{central_did}.*')
|
||||
# Sub Event
|
||||
mips_cloud.sub_event(
|
||||
did=central_did, handler=on_event_occurred,
|
||||
handler_ctx=f'{central_did}.*')
|
||||
# Get/set prop
|
||||
test_siid = 3
|
||||
test_piid = 1
|
||||
# mips_cloud.sub_prop(
|
||||
# did=central_did, siid=test_siid, piid=test_piid,
|
||||
# handler=on_prop_changed,
|
||||
# handler_ctx=f'{central_did}.{test_siid}.{test_piid}')
|
||||
result1 = await miot_http.get_prop_async(
|
||||
did=central_did, siid=test_siid, piid=test_piid)
|
||||
assert isinstance(result1, bool)
|
||||
_LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result1)
|
||||
result2 = await miot_http.set_prop_async(params=[{
|
||||
'did': central_did, 'siid': test_siid, 'piid': test_piid,
|
||||
'value': not result1}])
|
||||
_LOGGER.info(
|
||||
'set prop.%s.%s=%s, result=%s',
|
||||
test_siid, test_piid, not result1, result2)
|
||||
assert isinstance(result2, list)
|
||||
result3 = await miot_http.get_prop_async(
|
||||
did=central_did, siid=test_siid, piid=test_piid)
|
||||
assert isinstance(result3, bool)
|
||||
_LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result3)
|
||||
# Action
|
||||
test_siid = 4
|
||||
test_aiid = 1
|
||||
in_list = [{'piid': 1, 'value': 'hello world.'}]
|
||||
result4 = await miot_http.action_async(
|
||||
did=central_did, siid=test_siid, aiid=test_aiid,
|
||||
in_list=in_list)
|
||||
assert isinstance(result4, dict)
|
||||
_LOGGER.info(
|
||||
'action.%s.%s=%s, result=%s',
|
||||
test_siid, test_piid, in_list, result4)
|
||||
await asyncio.sleep(1)
|
||||
# Disconnect
|
||||
await mips_cloud.disconnect_async()
|
||||
await mips_cloud.deinit_async()
|
||||
await miot_http.deinit_async()
|
||||
@ -1,8 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_network.py."""
|
||||
import logging
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@ -12,16 +15,16 @@ async def test_network_monitor_loop_async():
|
||||
miot_net = MIoTNetwork()
|
||||
|
||||
async def on_network_status_changed(status: bool):
|
||||
print(f'on_network_status_changed, {status}')
|
||||
_LOGGER.info('on_network_status_changed, %s', status)
|
||||
miot_net.sub_network_status(key='test', handler=on_network_status_changed)
|
||||
|
||||
async def on_network_info_changed(
|
||||
status: InterfaceStatus, info: NetworkInfo):
|
||||
print(f'on_network_info_changed, {status}, {info}')
|
||||
_LOGGER.info('on_network_info_changed, %s, %s', status, info)
|
||||
miot_net.sub_network_info(key='test', handler=on_network_info_changed)
|
||||
|
||||
await miot_net.init_async(3)
|
||||
await miot_net.init_async()
|
||||
await asyncio.sleep(3)
|
||||
print(f'net status: {miot_net.network_status}')
|
||||
print(f'net info: {miot_net.network_info}')
|
||||
_LOGGER.info('net status: %s', miot_net.network_status)
|
||||
_LOGGER.info('net info: %s', miot_net.network_info)
|
||||
await miot_net.deinit_async()
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_spec.py."""
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from urllib.request import Request, urlopen
|
||||
import pytest
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@ -79,10 +82,10 @@ async def test_spec_random_parse_async(test_cache_path, test_lang):
|
||||
storage = MIoTStorage(test_cache_path)
|
||||
spec_parser = MIoTSpecParser(lang=test_lang, storage=storage)
|
||||
await spec_parser.init_async()
|
||||
start_ts: int = time.time()*1000
|
||||
start_ts = time.time()*1000
|
||||
for index in test_urn_index:
|
||||
urn: str = test_urns[int(index)]
|
||||
result = await spec_parser.parse(urn=urn, skip_cache=True)
|
||||
assert result is not None
|
||||
end_ts: int = time.time()*1000
|
||||
print(f'takes time, {test_count}, {end_ts-start_ts}')
|
||||
end_ts = time.time()*1000
|
||||
_LOGGER.info('takes time, %s, %s', test_count, end_ts-start_ts)
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit test for miot_storage.py."""
|
||||
import asyncio
|
||||
import logging
|
||||
from os import path
|
||||
import pytest
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument
|
||||
|
||||
|
||||
@ -101,7 +104,7 @@ async def test_multi_task_load_async(test_cache_path):
|
||||
for _ in range(task_count):
|
||||
task_list.append(asyncio.create_task(storage.load_async(
|
||||
domain=test_domain, name=name, type_=dict)))
|
||||
print(f'\ntask count, {len(task_list)}')
|
||||
_LOGGER.info('task count, %s', len(task_list))
|
||||
result: list = await asyncio.gather(*task_list)
|
||||
assert None not in result
|
||||
|
||||
@ -178,28 +181,28 @@ async def test_user_config_async(
|
||||
config=config_update, replace=True)
|
||||
assert (config_replace := await storage.load_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server)) == config_update
|
||||
print('replace result, ', config_replace)
|
||||
_LOGGER.info('replace result, %s', config_replace)
|
||||
# Test query
|
||||
query_keys = list(config_base.keys())
|
||||
print('query keys, ', query_keys)
|
||||
_LOGGER.info('query keys, %s', query_keys)
|
||||
query_result = await storage.load_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server, keys=query_keys)
|
||||
print('query result 1, ', query_result)
|
||||
_LOGGER.info('query result 1, %s', query_result)
|
||||
assert await storage.update_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server,
|
||||
config=config_base, replace=True)
|
||||
query_result = await storage.load_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server, keys=query_keys)
|
||||
print('query result 2, ', query_result)
|
||||
_LOGGER.info('query result 2, %s', query_result)
|
||||
query_result = await storage.load_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server)
|
||||
print('query result all, ', query_result)
|
||||
_LOGGER.info('query result all, %s', query_result)
|
||||
# Remove config
|
||||
assert await storage.update_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server, config=None)
|
||||
query_result = await storage.load_user_config_async(
|
||||
uid=test_uid, cloud_server=test_cloud_server)
|
||||
print('remove result, ', query_result)
|
||||
_LOGGER.info('remove result, %s', query_result)
|
||||
# Remove domain
|
||||
assert await storage.remove_domain_async(domain='miot_config')
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user