feat: first commit

This commit is contained in:
topsworld
2024-12-10 17:36:32 +08:00
commit 838fff399b
75 changed files with 19923 additions and 0 deletions

View File

@ -0,0 +1,89 @@
# -*- 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.
Common utilities.
"""
import json
import random
from typing import Optional
import hashlib
from paho.mqtt.client import MQTTMatcher
def calc_group_id(uid: str, home_id: str) -> str:
"""Calculate the group ID based on a user ID and a home ID."""
return hashlib.sha1(
f'{uid}central_service{home_id}'.encode('utf-8')).hexdigest()[:16]
def load_json_file(json_file: str) -> dict:
"""Load a JSON file."""
with open(json_file, 'r', encoding='utf-8') as f:
return json.load(f)
def randomize_int(value: int, ratio: float) -> int:
"""Randomize an integer value."""
return int(value * (1 - ratio + random.random()*2*ratio))
class MIoTMatcher(MQTTMatcher):
"""MIoT Pub/Sub topic matcher."""
def iter_all_nodes(self) -> any:
"""Return an iterator on all nodes with their paths and contents."""
def rec(node, path):
# pylint: disable=protected-access
if node._content:
yield ('/'.join(path), node._content)
for part, child in node._children.items():
yield from rec(child, path + [part])
return rec(self._root, [])
def get(self, topic: str) -> Optional[any]:
try:
return self[topic]
except KeyError:
return None

View File

@ -0,0 +1,149 @@
# -*- 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.
Constants.
"""
DOMAIN: str = 'xiaomi_home'
DEFAULT_NAME: str = 'Xiaomi Home'
DEFAULT_NICK_NAME: str = 'Xiaomi'
MIHOME_HTTP_API_TIMEOUT: int = 30
MIHOME_MQTT_KEEPALIVE: int = 60
# seconds, 3 days
MIHOME_CERT_EXPIRE_MARGIN: int = 3600*24*3
NETWORK_REFRESH_INTERVAL: int = 30
OAUTH2_CLIENT_ID: str = '2882303761520251711'
OAUTH2_AUTH_URL: str = 'https://account.xiaomi.com/oauth2/authorize'
DEFAULT_OAUTH2_API_HOST: str = 'ha.api.io.mi.com'
# seconds, 14 days
SPEC_STD_LIB_EFFECTIVE_TIME = 3600*24*14
# seconds, 14 days
MANUFACTURER_EFFECTIVE_TIME = 3600*24*14
SUPPORTED_PLATFORMS: list = [
# 'alarm_control_panel',
'binary_sensor',
'button',
'climate',
# 'camera',
# 'conversation',
'cover',
# 'device_tracker',
'event',
'fan',
'humidifier',
'light',
# 'lock',
# 'media_player',
'notify',
'number',
# 'remote',
# 'scene',
'select',
'sensor',
'switch',
'text',
'vacuum',
'water_heater',
]
DEFAULT_CLOUD_SERVER: str = 'cn'
CLOUD_SERVERS: dict = {
'cn': '中国大陆',
'de': 'Europe',
'i2': 'India',
'ru': 'Russia',
'sg': 'Singapore',
'us': 'United States'
}
SUPPORT_CENTRAL_GATEWAY_CTRL: list = ['cn']
DEFAULT_INTEGRATION_LANGUAGE: str = 'en'
INTEGRATION_LANGUAGES = {
'zh-Hans': '简体中文',
'zh-Hant': '繁體中文',
'en': 'English',
'es': 'Español',
'ru': 'Русский',
'fr': 'Français',
'de': 'Deutsch',
'ja': '日本語'
}
DEFAULT_CTRL_MODE: str = 'auto'
# Registered in Xiaomi OAuth 2.0 Service
# DO NOT CHANGE UNLESS YOU HAVE AN ADMINISTRATOR PERMISSION
OAUTH_REDIRECT_URL: str = 'http://homeassistant.local:8123'
MIHOME_CA_CERT_STR: str = '-----BEGIN CERTIFICATE-----\n' \
'MIIBazCCAQ+gAwIBAgIEA/UKYDAMBggqhkjOPQQDAgUAMCIxEzARBgNVBAoTCk1p\n' \
'amlhIFJvb3QxCzAJBgNVBAYTAkNOMCAXDTE2MTEyMzAxMzk0NVoYDzIwNjYxMTEx\n' \
'MDEzOTQ1WjAiMRMwEQYDVQQKEwpNaWppYSBSb290MQswCQYDVQQGEwJDTjBZMBMG\n' \
'ByqGSM49AgEGCCqGSM49AwEHA0IABL71iwLa4//4VBqgRI+6xE23xpovqPCxtv96\n' \
'2VHbZij61/Ag6jmi7oZ/3Xg/3C+whglcwoUEE6KALGJ9vccV9PmjLzAtMAwGA1Ud\n' \
'EwQFMAMBAf8wHQYDVR0OBBYEFJa3onw5sblmM6n40QmyAGDI5sURMAwGCCqGSM49\n' \
'BAMCBQADSAAwRQIgchciK9h6tZmfrP8Ka6KziQ4Lv3hKfrHtAZXMHPda4IYCIQCG\n' \
'az93ggFcbrG9u2wixjx1HKW4DUA5NXZG0wWQTpJTbQ==\n' \
'-----END CERTIFICATE-----\n' \
'-----BEGIN CERTIFICATE-----\n' \
'MIIBjzCCATWgAwIBAgIBATAKBggqhkjOPQQDAjAiMRMwEQYDVQQKEwpNaWppYSBS\n' \
'b290MQswCQYDVQQGEwJDTjAgFw0yMjA2MDkxNDE0MThaGA8yMDcyMDUyNzE0MTQx\n' \
'OFowLDELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFE1JT1QgQ0VOVFJBTCBHQVRFV0FZ\n' \
'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdYrzbnp/0x/cZLZnuEDXTFf8mhj4\n' \
'CVpZPwgj9e9Ve5r3K7zvu8Jjj7JF1JjQYvEC6yhp1SzBgglnK4L8xQzdiqNQME4w\n' \
'HQYDVR0OBBYEFCf9+YBU7pXDs6K6CAQPRhlGJ+cuMB8GA1UdIwQYMBaAFJa3onw5\n' \
'sblmM6n40QmyAGDI5sURMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIh\n' \
'AKUv+c8v98vypkGMTzMwckGjjVqTef8xodsy6PhcSCq+AiA/n9mDs62hAo5zXyJy\n' \
'Bs1s7mqXPf1XgieoxIvs1MqyiA==\n' \
'-----END CERTIFICATE-----\n'
MIHOME_CA_CERT_SHA256: str = \
'8b7bf306be3632e08b0ead308249e5f2b2520dc921ad143872d5fcc7c68d6759'

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "Geräte",
"found_central_gateway": ", lokales zentrales Gateway gefunden"
},
"control_mode": {
"auto": "automatisch",
"cloud": "Cloud"
},
"room_name_rule": {
"none": "nicht synchronisieren",
"home_room": "Hausname und Raumname (Xiaomi Home Schlafzimmer)",
"room": "Raumname (Schlafzimmer)",
"home": "Hausname (Xiaomi Home)"
},
"option_status": {
"enable": "aktivieren",
"disable": "deaktivieren"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[Hinweis]** Es wurden mehrere Netzwerkkarten erkannt, die möglicherweise mit demselben Netzwerk verbunden sind. Bitte achten Sie auf die Auswahl.",
"net_unavailable": "Schnittstelle nicht verfügbar"
}
},
"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",
"invalid_device_cache": "Ungültige Gerätecache-Informationen, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen->Geräteliste aktualisieren', um den lokalen Gerätecache zu aktualisieren",
"invalid_cert_info": "Ungültiges Benutzerzertifikat, lokale zentrale Verbindung nicht verfügbar, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen', um die Authentifizierung erneut durchzuführen",
"device_cloud_error": "Fehler beim Abrufen von Geräteinformationen aus der Cloud, bitte überprüfen Sie die lokale Netzwerkverbindung",
"xiaomi_home_error_title": "Xiaomi Home-Integrationsfehler",
"xiaomi_home_error": "Fehler **{nick_name}({uid}, {cloud_server})** festgestellt, bitte betreten Sie die Optionen-Seite, um die Konfiguration erneut durchzuführen.\n\n**Fehlermeldung**: \n{message}",
"device_list_changed_title": "Xiaomi Home-Geräteliste geändert",
"device_list_changed": "Änderung der Geräteinformationen **{nick_name}({uid}, {cloud_server})** festgestellt, bitte betreten Sie die Integrations-Optionen-Seite, klicken Sie auf 'Optionen->Geräteliste aktualisieren', um den lokalen Gerätecache zu aktualisieren.\n\nAktueller Netzwerkstatus: {network_status}\n{message}\n",
"device_list_add": "\n**{count} neue Geräte:** \n{message}",
"device_list_del": "\n**{count} Geräte nicht verfügbar:** \n{message}",
"device_list_offline": "\n**{count} Geräte offline:** \n{message}",
"network_status_online": "Online",
"network_status_offline": "Offline",
"device_exec_error": "Fehler bei der Ausführung"
}
},
"error": {
"common": {
"-10000": "Unbekannter Fehler",
"-10001": "Dienst nicht verfügbar",
"-10002": "Ungültiger Parameter",
"-10003": "Unzureichende Ressourcen",
"-10004": "Interner Fehler",
"-10005": "Unzureichende Berechtigungen",
"-10006": "Ausführungszeitüberschreitung",
"-10007": "Gerät offline oder nicht vorhanden",
"-10020": "Nicht autorisiert (OAuth2)",
"-10030": "Ungültiges Token (HTTP)",
"-10040": "Ungültiges Nachrichtenformat",
"-10050": "Ungültiges Zertifikat",
"-704000000": "Unbekannter Fehler",
"-704010000": "Nicht autorisiert (Gerät wurde möglicherweise gelöscht)",
"-704014006": "Gerätebeschreibung nicht gefunden",
"-704030013": "Eigenschaft nicht lesbar",
"-704030023": "Eigenschaft nicht beschreibbar",
"-704030033": "Eigenschaft nicht abonnierbar",
"-704040002": "Dienst existiert nicht",
"-704040003": "Eigenschaft existiert nicht",
"-704040004": "Ereignis existiert nicht",
"-704040005": "Aktion existiert nicht",
"-704040999": "Funktion nicht online",
"-704042001": "Gerät existiert nicht",
"-704042011": "Gerät offline",
"-704053036": "Gerätebetrieb zeitüberschreitung",
"-704053100": "Gerät kann diese Operation im aktuellen Zustand nicht ausführen",
"-704083036": "Gerätebetrieb zeitüberschreitung",
"-704090001": "Gerät existiert nicht",
"-704220008": "Ungültige ID",
"-704220025": "Aktionsparameteranzahl stimmt nicht überein",
"-704220035": "Aktionsparameterfehler",
"-704220043": "Eigenschaftswertfehler",
"-704222034": "Aktionsrückgabewertfehler",
"-705004000": "Unbekannter Fehler",
"-705004501": "Unbekannter Fehler",
"-705201013": "Eigenschaft nicht lesbar",
"-705201015": "Aktionsausführungsfehler",
"-705201023": "Eigenschaft nicht beschreibbar",
"-705201033": "Eigenschaft nicht abonnierbar",
"-706012000": "Unbekannter Fehler",
"-706012013": "Eigenschaft nicht lesbar",
"-706012015": "Aktionsausführungsfehler",
"-706012023": "Eigenschaft nicht beschreibbar",
"-706012033": "Eigenschaft nicht abonnierbar",
"-706012043": "Eigenschaftswertfehler",
"-706014006": "Gerätebeschreibung nicht gefunden"
}
}
}

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "Devices",
"found_central_gateway": ", Found Local Central Hub Gateway"
},
"control_mode": {
"auto": "Auto",
"cloud": "Cloud"
},
"room_name_rule": {
"none": "Do not synchronize",
"home_room": "Home Name and Room Name (Xiaomi Home Bedroom)",
"room": "Room Name (Bedroom)",
"home": "Home Name (Xiaomi Home)"
},
"option_status": {
"enable": "Enable",
"disable": "Disable"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[Notice]** Multiple network cards detected that may be connected to the same network. Please pay attention to the selection.",
"net_unavailable": "Interface unavailable"
}
},
"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",
"invalid_device_cache": "Cache device information is abnormal, please enter the Xiaomi Home integration page, click 'Options->Update device list', update the local cache",
"invalid_cert_info": "Invalid user certificate, local central link will be unavailable, please enter the Xiaomi Home integration page, click 'Options' to re-authenticate",
"device_cloud_error": "An exception occurred when obtaining device information from the cloud, please check the local network connection",
"xiaomi_home_error_title": "Xiaomi Home Integration Error",
"xiaomi_home_error": "Detected **{nick_name}({uid}, {cloud_server})** error, please enter the options page to reconfigure.\n\n**Error message**: \n{message}",
"device_list_changed_title": "Xiaomi Home device list changes",
"device_list_changed": "Detected **{nick_name}({uid}, {cloud_server})** device information has changed, please enter the integration options page, click `Options->Update device list`, update local device information.\n\nCurrent network status: {network_status}\n{message}\n",
"device_list_add": "\n**{count} new devices:** \n{message}",
"device_list_del": "\n**{count} devices unavailable:** \n{message}",
"device_list_offline": "\n**{count} devices offline:** \n{message}",
"network_status_online": "Online",
"network_status_offline": "Offline",
"device_exec_error": "Execution error"
}
},
"error": {
"common": {
"-10000": "Unknown error",
"-10001": "Service unavailable",
"-10002": "Invalid parameter",
"-10003": "Insufficient resources",
"-10004": "Internal error",
"-10005": "Insufficient permissions",
"-10006": "Execution timeout",
"-10007": "Device offline or does not exist",
"-10020": "Unauthorized (OAuth2)",
"-10030": "Invalid token (HTTP)",
"-10040": "Invalid message format",
"-10050": "Invalid certificate",
"-704000000": "Unknown error",
"-704010000": "Unauthorized (device may have been deleted)",
"-704014006": "Device description not found",
"-704030013": "Property not readable",
"-704030023": "Property not writable",
"-704030033": "Property not subscribable",
"-704040002": "Service does not exist",
"-704040003": "Property does not exist",
"-704040004": "Event does not exist",
"-704040005": "Action does not exist",
"-704040999": "Feature not online",
"-704042001": "Device does not exist",
"-704042011": "Device offline",
"-704053036": "Device operation timeout",
"-704053100": "Device cannot perform this operation in the current state",
"-704083036": "Device operation timeout",
"-704090001": "Device does not exist",
"-704220008": "Invalid ID",
"-704220025": "Action parameter count mismatch",
"-704220035": "Action parameter error",
"-704220043": "Property value error",
"-704222034": "Action return value error",
"-705004000": "Unknown error",
"-705004501": "Unknown error",
"-705201013": "Property not readable",
"-705201015": "Action execution error",
"-705201023": "Property not writable",
"-705201033": "Property not subscribable",
"-706012000": "Unknown error",
"-706012013": "Property not readable",
"-706012015": "Action execution error",
"-706012023": "Property not writable",
"-706012033": "Property not subscribable",
"-706012043": "Property value error",
"-706014006": "Device description not found"
}
}
}

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "dispositivos",
"found_central_gateway": ", se encontró la puerta de enlace central local"
},
"control_mode": {
"auto": "automático",
"cloud": "nube"
},
"room_name_rule": {
"none": "no sincronizar",
"home_room": "nombre de la casa y nombre de la habitación (Xiaomi Home Dormitorio)",
"room": "nombre de la habitación (Dormitorio)",
"home": "nombre de la casa (Xiaomi Home)"
},
"option_status": {
"enable": "habilitar",
"disable": "deshabilitar"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[Aviso]** Se detectaron varias tarjetas de red que pueden estar conectadas a la misma red. Por favor, preste atención a la selección.",
"net_unavailable": "Interfaz no disponible"
}
},
"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",
"invalid_device_cache": "La información de caché del dispositivo es anormal, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones -> Actualizar lista de dispositivos' para actualizar la información del dispositivo local",
"invalid_cert_info": "Certificado de usuario inválido, la conexión del centro local no estará disponible, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones' para volver a autenticar",
"device_cloud_error": "Error al obtener la información del dispositivo desde la nube, por favor, compruebe la conexión de red local",
"xiaomi_home_error_title": "Error de integración de Xiaomi Home",
"xiaomi_home_error": "Se detectó un error en **{nick_name}({uid}, {cloud_server})**, por favor, vaya a la página de opciones para reconfigurar.\n\n**Mensaje de error**: \n{message}",
"device_list_changed_title": "Cambio en la lista de dispositivos de Xiaomi Home",
"device_list_changed": "Se detectó un cambio en la información del dispositivo **{nick_name}({uid}, {cloud_server})**, por favor, vaya a la página de integración, haga clic en 'Opciones -> Actualizar lista de dispositivos' para actualizar la información del dispositivo local.\n\nEstado actual de la red: {network_status}\n{message}\n",
"device_list_add": "\n**{count} nuevos dispositivos:** \n{message}",
"device_list_del": "\n**{count} dispositivos no disponibles:** \n{message}",
"device_list_offline": "\n**{count} dispositivos sin conexión:** \n{message}",
"network_status_online": "En línea",
"network_status_offline": "Desconectado",
"device_exec_error": "Error de ejecución"
}
},
"error": {
"common": {
"-10000": "Error desconocido",
"-10001": "Servicio no disponible",
"-10002": "Parámetro inválido",
"-10003": "Recursos insuficientes",
"-10004": "Error interno",
"-10005": "Permisos insuficientes",
"-10006": "Tiempo de ejecución agotado",
"-10007": "Dispositivo fuera de línea o no existe",
"-10020": "No autorizado (OAuth2)",
"-10030": "Token inválido (HTTP)",
"-10040": "Formato de mensaje inválido",
"-10050": "Certificado inválido",
"-704000000": "Error desconocido",
"-704010000": "No autorizado (el dispositivo puede haber sido eliminado)",
"-704014006": "Descripción del dispositivo no encontrada",
"-704030013": "Propiedad no legible",
"-704030023": "Propiedad no escribible",
"-704030033": "Propiedad no suscribible",
"-704040002": "Servicio no existe",
"-704040003": "Propiedad no existe",
"-704040004": "Evento no existe",
"-704040005": "Acción no existe",
"-704040999": "Función no en línea",
"-704042001": "Dispositivo no existe",
"-704042011": "Dispositivo fuera de línea",
"-704053036": "Tiempo de operación del dispositivo agotado",
"-704053100": "El dispositivo no puede realizar esta operación en el estado actual",
"-704083036": "Tiempo de operación del dispositivo agotado",
"-704090001": "Dispositivo no existe",
"-704220008": "ID inválido",
"-704220025": "Número de parámetros de acción no coincide",
"-704220035": "Error de parámetro de acción",
"-704220043": "Error de valor de propiedad",
"-704222034": "Error de valor de retorno de acción",
"-705004000": "Error desconocido",
"-705004501": "Error desconocido",
"-705201013": "Propiedad no legible",
"-705201015": "Error de ejecución de acción",
"-705201023": "Propiedad no escribible",
"-705201033": "Propiedad no suscribible",
"-706012000": "Error desconocido",
"-706012013": "Propiedad no legible",
"-706012015": "Error de ejecución de acción",
"-706012023": "Propiedad no escribible",
"-706012033": "Propiedad no suscribible",
"-706012043": "Error de valor de propiedad",
"-706014006": "Descripción del dispositivo no encontrada"
}
}
}

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "appareils",
"found_central_gateway": ", passerelle centrale locale trouvée"
},
"control_mode": {
"auto": "automatique",
"cloud": "cloud"
},
"room_name_rule": {
"none": "ne pas synchroniser",
"home_room": "nom de la maison et nom de la pièce (Xiaomi Home Chambre)",
"room": "nom de la pièce (Chambre)",
"home": "nom de la maison (Xiaomi Home)"
},
"option_status": {
"enable": "activer",
"disable": "désactiver"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[Remarque]** Plusieurs cartes réseau détectées qui peuvent être connectées au même réseau. Veuillez faire attention à la sélection.",
"net_unavailable": "Interface non disponible"
}
},
"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",
"invalid_device_cache": "Informations de cache de périphérique non valides, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur `Options-> Mettre à jour la liste des appareils`, pour mettre à jour les informations locales des appareils",
"invalid_cert_info": "Certificat utilisateur non valide, le lien central local ne sera pas disponible, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur \"Options\" pour vous réauthentifier",
"device_cloud_error": "Erreur lors de la récupération des informations de l'appareil à partir du cloud, veuillez vérifier la connexion réseau locale",
"xiaomi_home_error_title": "Erreur d'intégration Xiaomi Home",
"xiaomi_home_error": "Erreur détectée sur **{nick_name}({uid}, {cloud_server})**, veuillez accéder à la page d'options pour reconfigurer.\n\n**Message d'erreur**: \n{message}",
"device_list_changed_title": "Changements dans la liste des appareils Xiaomi Home",
"device_list_changed": "Changements détectés sur **{nick_name}({uid}, {cloud_server})**, veuillez accéder à la page d'intégration, cliquez sur `Options-> Mettre à jour la liste des appareils`, pour mettre à jour les informations locales des appareils.\n\nÉtat actuel du réseau : {network_status}\n{message}\n",
"device_list_add": "\n**{count} nouveaux appareils :** \n{message}",
"device_list_del": "\n**{count} appareils non disponibles :** \n{message}",
"device_list_offline": "\n**{count} appareils hors ligne :** \n{message}",
"network_status_online": "En ligne",
"network_status_offline": "Hors ligne",
"device_exec_error": "Erreur d'exécution"
}
},
"error": {
"common": {
"-10000": "Erreur inconnue",
"-10001": "Service indisponible",
"-10002": "Paramètre invalide",
"-10003": "Ressources insuffisantes",
"-10004": "Erreur interne",
"-10005": "Permissions insuffisantes",
"-10006": "Délai d'exécution dépassé",
"-10007": "Appareil hors ligne ou n'existe pas",
"-10020": "Non autorisé (OAuth2)",
"-10030": "Jeton invalide (HTTP)",
"-10040": "Format de message invalide",
"-10050": "Certificat invalide",
"-704000000": "Erreur inconnue",
"-704010000": "Non autorisé (l'appareil peut avoir été supprimé)",
"-704014006": "Description de l'appareil introuvable",
"-704030013": "Propriété non lisible",
"-704030023": "Propriété non inscriptible",
"-704030033": "Propriété non abonnable",
"-704040002": "Service n'existe pas",
"-704040003": "Propriété n'existe pas",
"-704040004": "Événement n'existe pas",
"-704040005": "Action n'existe pas",
"-704040999": "Fonction non en ligne",
"-704042001": "Appareil n'existe pas",
"-704042011": "Appareil hors ligne",
"-704053036": "Délai d'opération de l'appareil dépassé",
"-704053100": "L'appareil ne peut pas effectuer cette opération dans l'état actuel",
"-704083036": "Délai d'opération de l'appareil dépassé",
"-704090001": "Appareil n'existe pas",
"-704220008": "ID invalide",
"-704220025": "Nombre de paramètres d'action ne correspond pas",
"-704220035": "Erreur de paramètre d'action",
"-704220043": "Erreur de valeur de propriété",
"-704222034": "Erreur de valeur de retour d'action",
"-705004000": "Erreur inconnue",
"-705004501": "Erreur inconnue",
"-705201013": "Propriété non lisible",
"-705201015": "Erreur d'exécution d'action",
"-705201023": "Propriété non inscriptible",
"-705201033": "Propriété non abonnable",
"-706012000": "Erreur inconnue",
"-706012013": "Propriété non lisible",
"-706012015": "Erreur d'exécution d'action",
"-706012023": "Propriété non inscriptible",
"-706012033": "Propriété non abonnable",
"-706012043": "Erreur de valeur de propriété",
"-706014006": "Description de l'appareil introuvable"
}
}
}

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "デバイス",
"found_central_gateway": "、ローカル中央ゲートウェイが見つかりました"
},
"control_mode": {
"auto": "自動",
"cloud": "クラウド"
},
"room_name_rule": {
"none": "同期しない",
"home_room": "家の名前と部屋の名前 Xiaomi Home 寝室)",
"room": "部屋の名前(寝室)",
"home": "家の名前 Xiaomi Home"
},
"option_status": {
"enable": "有効",
"disable": "無効"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[注意]** 複数のネットワークカードが同じネットワークに接続されている可能性があります。選択に注意してください。",
"net_unavailable": "インターフェースが利用できません"
}
},
"miot": {
"client": {
"invalid_oauth_info": "認証情報が無効です。クラウドリンクは利用できません。Xiaomi Home統合ページに入り、[オプション]をクリックして再認証してください",
"invalid_device_cache": "キャッシュデバイス情報が異常です。Xiaomi Home統合ページに入り、[オプション->デバイスリストの更新]をクリックして、ローカルキャッシュを更新してください",
"invalid_cert_info": "無効なユーザー証明書です。ローカルセントラルリンクは利用できません。Xiaomi Home統合ページに入り、[オプション]をクリックして再認証してください",
"device_cloud_error": "クラウドからデバイス情報を取得する際に例外が発生しました。ローカルネットワーク接続を確認してください",
"xiaomi_home_error_title": "Xiaomi Home統合エラー",
"xiaomi_home_error": "エラーが検出されました **{nick_name}({uid}, {cloud_server})** 、オプションページに入り再構成してください。\n\n**エラーメッセージ**: \n{message}",
"device_list_changed_title": "Xiaomi Homeデバイスリストの変更",
"device_list_changed": "変更が検出されました **{nick_name}({uid}, {cloud_server})** デバイス情報が変更されました。統合オプションページに入り、`オプション->デバイスリストの更新`をクリックして、ローカルデバイス情報を更新してください。\n\n現在のネットワーク状態{network_status}\n{message}\n",
"device_list_add": "\n**{count} 新しいデバイス:** \n{message}",
"device_list_del": "\n**{count} デバイスが利用できません:** \n{message}",
"device_list_offline": "\n**{count} デバイスがオフライン:** \n{message}",
"network_status_online": "オンライン",
"network_status_offline": "オフライン",
"device_exec_error": "実行エラー"
}
},
"error": {
"common": {
"-10000": "不明なエラー",
"-10001": "サービス利用不可",
"-10002": "無効なパラメータ",
"-10003": "リソース不足",
"-10004": "内部エラー",
"-10005": "権限不足",
"-10006": "実行タイムアウト",
"-10007": "デバイスがオフラインまたは存在しない",
"-10020": "未認証OAuth2",
"-10030": "無効なトークンHTTP",
"-10040": "無効なメッセージ形式",
"-10050": "無効な証明書",
"-704000000": "不明なエラー",
"-704010000": "未認証(デバイスが削除された可能性があります)",
"-704014006": "デバイスの説明が見つかりません",
"-704030013": "プロパティが読み取れません",
"-704030023": "プロパティが書き込めません",
"-704030033": "プロパティが購読できません",
"-704040002": "サービスが存在しません",
"-704040003": "プロパティが存在しません",
"-704040004": "イベントが存在しません",
"-704040005": "アクションが存在しません",
"-704040999": "機能がオンラインではありません",
"-704042001": "デバイスが存在しません",
"-704042011": "デバイスがオフラインです",
"-704053036": "デバイス操作タイムアウト",
"-704053100": "デバイスが現在の状態でこの操作を実行できません",
"-704083036": "デバイス操作タイムアウト",
"-704090001": "デバイスが存在しません",
"-704220008": "無効なID",
"-704220025": "アクションパラメータの数が一致しません",
"-704220035": "アクションパラメータエラー",
"-704220043": "プロパティ値エラー",
"-704222034": "アクションの戻り値エラー",
"-705004000": "不明なエラー",
"-705004501": "不明なエラー",
"-705201013": "プロパティが読み取れません",
"-705201015": "アクション実行エラー",
"-705201023": "プロパティが書き込めません",
"-705201033": "プロパティが購読できません",
"-706012000": "不明なエラー",
"-706012013": "プロパティが読み取れません",
"-706012015": "アクション実行エラー",
"-706012023": "プロパティが書き込めません",
"-706012033": "プロパティが購読できません",
"-706012043": "プロパティ値エラー",
"-706014006": "デバイスの説明が見つかりません"
}
}
}

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "устройства",
"found_central_gateway": ", найден локальный центральный шлюз"
},
"control_mode": {
"auto": "автоматический",
"cloud": "облако"
},
"room_name_rule": {
"none": "не синхронизировать",
"home_room": "название дома и название комнаты (Xiaomi Home Спальня)",
"room": "название комнаты (Спальня)",
"home": "название дома (Xiaomi Home)"
},
"option_status": {
"enable": "Включить",
"disable": "Отключить"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[Уведомление]** Обнаружено несколько сетевых карт, которые могут быть подключены к одной и той же сети. Пожалуйста, обратите внимание на выбор.",
"net_unavailable": "Интерфейс недоступен"
}
},
"miot": {
"client": {
"invalid_oauth_info": "Информация об аутентификации недействительна, облако будет недоступно, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции' для повторной аутентификации",
"invalid_device_cache": "Кэш информации об устройстве ненормальный, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции->Обновить список устройств', обновите локальный кэш",
"invalid_cert_info": "Недействительный пользовательский сертификат, локальное центральное соединение будет недоступно, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции' для повторной аутентификации",
"device_cloud_error": "При получении информации об устройстве из облака произошло исключение, пожалуйста, проверьте локальное сетевое соединение",
"xiaomi_home_error_title": "Ошибка интеграции Xiaomi Home",
"xiaomi_home_error": "Обнаружена ошибка **{nick_name}({uid}, {cloud_server})**, пожалуйста, войдите на страницу опций для повторной настройки.\n\n**Сообщение об ошибке**: \n{message}",
"device_list_changed_title": "Изменения в списке устройств Xiaomi Home",
"device_list_changed": "Обнаружены изменения в информации об устройствах **{nick_name}({uid}, {cloud_server})**, пожалуйста, войдите на страницу интеграции, нажмите `Опции->Обновить список устройств`, обновите локальную информацию об устройствах.\n\nТекущий статус сети: {network_status}\n{message}\n",
"device_list_add": "\n**{count} новых устройств:** \n{message}",
"device_list_del": "\n**{count} устройств недоступно:** \n{message}",
"device_list_offline": "\n**{count} устройств недоступно:** \n{message}",
"network_status_online": "В сети",
"network_status_offline": "Не в сети",
"device_exec_error": "Ошибка выполнения"
}
},
"error": {
"common": {
"-10000": "Неизвестная ошибка",
"-10001": "Сервис недоступен",
"-10002": "Недопустимый параметр",
"-10003": "Недостаточно ресурсов",
"-10004": "Внутренняя ошибка",
"-10005": "Недостаточно прав",
"-10006": "Тайм-аут выполнения",
"-10007": "Устройство не в сети или не существует",
"-10020": "Неавторизовано (OAuth2)",
"-10030": "Недействительный токен (HTTP)",
"-10040": "Недопустимый формат сообщения",
"-10050": "Недействительный сертификат",
"-704000000": "Неизвестная ошибка",
"-704010000": "Неавторизовано (устройство могло быть удалено)",
"-704014006": "Описание устройства не найдено",
"-704030013": "Свойство не читается",
"-704030023": "Свойство не записывается",
"-704030033": "Свойство не подписывается",
"-704040002": "Сервис не существует",
"-704040003": "Свойство не существует",
"-704040004": "Событие не существует",
"-704040005": "Действие не существует",
"-704040999": "Функция не в сети",
"-704042001": "Устройство не существует",
"-704042011": "Устройство не в сети",
"-704053036": "Тайм-аут операции устройства",
"-704053100": "Устройство не может выполнить эту операцию в текущем состоянии",
"-704083036": "Тайм-аут операции устройства",
"-704090001": "Устройство не существует",
"-704220008": "Недействительный ID",
"-704220025": "Несоответствие количества параметров действия",
"-704220035": "Ошибка параметра действия",
"-704220043": "Ошибка значения свойства",
"-704222034": "Ошибка возвращаемого значения действия",
"-705004000": "Неизвестная ошибка",
"-705004501": "Неизвестная ошибка",
"-705201013": "Свойство не читается",
"-705201015": "Ошибка выполнения действия",
"-705201023": "Свойство не записывается",
"-705201033": "Свойство не подписывается",
"-706012000": "Неизвестная ошибка",
"-706012013": "Свойство не читается",
"-706012015": "Ошибка выполнения действия",
"-706012023": "Свойство не записывается",
"-706012033": "Свойство не подписывается",
"-706012043": "Ошибка значения свойства",
"-706014006": "Описание устройства не найдено"
}
}
}

View File

@ -0,0 +1,95 @@
{
"config": {
"other": {
"devices": "个设备",
"found_central_gateway": ",发现本地中枢网关"
},
"control_mode": {
"auto": "自动",
"cloud": "云端"
},
"room_name_rule": {
"none": "不同步",
"home_room": "家庭名 和 房间名 (米家 卧室)",
"room": "房间名 (卧室)",
"home": "家庭名 (米家)"
},
"option_status": {
"enable": "启用",
"disable": "禁用"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[提示]** 检测到多个网卡可能连接同一个网络,请注意选择。",
"net_unavailable": "接口不可用"
}
},
"miot": {
"client": {
"invalid_oauth_info": "认证信息失效,云端链路将不可用,请进入 Xiaomi Home 集成页面,点击“选项”重新认证",
"invalid_device_cache": "缓存设备信息异常,请进入 Xiaomi Home 集成页面,点击`选项->更新设备列表`,更新本地设备信息",
"invalid_cert_info": "无效的用户证书,本地中枢链路将不可用,请进入 Xiaomi Home 集成页面,点击“选项”重新认证",
"device_cloud_error": "从云端获取设备信息异常,请检查本地网络连接",
"xiaomi_home_error_title": "Xiaomi Home 集成错误",
"xiaomi_home_error": "检测到 **{nick_name}({uid}, {cloud_server})** 出现错误,请进入选项页面重新配置。\n\n**错误信息**: \n{message}",
"device_list_changed_title": "Xiaomi Home设备列表变化",
"device_list_changed": "检测到 **{nick_name}({uid}, {cloud_server})** 设备信息发生变化,请进入集成选项页面,点击`选项->更新设备列表`,更新本地设备信息。\n\n当前网络状态{network_status}\n{message}\n",
"device_list_add": "\n**{count} 个新增设备**: \n{message}",
"device_list_del": "\n**{count} 个设备不可用**: \n{message}",
"device_list_offline": "\n**{count} 个设备离线**: \n{message}",
"network_status_online": "在线",
"network_status_offline": "离线",
"device_exec_error": "执行错误"
}
},
"error": {
"common": {
"-10000": "未知错误",
"-10001": "服务不可用",
"-10002": "参数无效",
"-10003": "资源不足",
"-10004": "内部错误",
"-10005": "权限不足",
"-10006": "执行超时",
"-10007": "设备离线或者不存在",
"-10020": "未授权OAuth2",
"-10030": "无效的tokenHTTP",
"-10040": "无效的消息格式",
"-10050": "无效的证书",
"-704000000": "未知错误",
"-704010000": "未授权(设备可能被删除)",
"-704014006": "没找到设备描述",
"-704030013": "Property不可读",
"-704030023": "Property不可写",
"-704030033": "Property不可订阅",
"-704040002": "Service不存在",
"-704040003": "Property不存在",
"-704040004": "Event不存在",
"-704040005": "Action不存在",
"-704040999": "功能未上线",
"-704042001": "Device不存在",
"-704042011": "设备离线",
"-704053036": "设备操作超时",
"-704053100": "设备在当前状态下无法执行此操作",
"-704083036": "设备操作超时",
"-704090001": "Device不存在",
"-704220008": "无效的ID",
"-704220025": "Action参数个数不匹配",
"-704220035": "Action参数错误",
"-704220043": "Property值错误",
"-704222034": "Action返回值错误",
"-705004000": "未知错误",
"-705004501": "未知错误",
"-705201013": "Property不可读",
"-705201015": "Action执行错误",
"-705201023": "Property不可写",
"-705201033": "Property不可订阅",
"-706012000": "未知错误",
"-706012013": "Property不可读",
"-706012015": "Action执行错误",
"-706012023": "Property不可写",
"-706012033": "Property不可订阅",
"-706012043": "Property值错误",
"-706014006": "没找到设备描述"
}
}
}

View File

@ -0,0 +1,97 @@
{
"config": {
"other": {
"devices": "個設備",
"found_central_gateway": ",發現本地中樞網關"
},
"control_mode": {
"auto": "自動",
"cloud": "雲端"
},
"room_name_rule": {
"none": "不同步",
"home_room": "家庭名 和 房間名 (米家 臥室)",
"room": "房間名 (臥室)",
"home": "家庭名 (米家)"
},
"option_status": {
"enable": "啟用",
"disable": "禁用"
},
"lan_ctrl_config": {
"notice_net_dup": "\r\n**[提示]** 檢測到多個網卡可能連接同一個網絡,請注意選擇。",
"net_unavailable": "接口不可用"
}
},
"miot": {
"client": {
"invalid_oauth_info": "認證信息失效,雲端鏈路將不可用,請進入 Xiaomi Home 集成頁面,點擊“選項”重新認證",
"invalid_device_cache": "緩存設備信息異常,請進入 Xiaomi Home 集成頁面,點擊`選項->更新設備列表`,更新本地設備信息",
"invalid_cert_info": "無效的用戶證書,本地中樞鏈路將不可用,請進入 Xiaomi Home 集成頁面,點擊“選項”重新認證",
"device_cloud_error": "從雲端獲取設備信息異常,請檢查本地網絡連接",
"xiaomi_home_error_title": "Xiaomi Home 集成錯誤",
"xiaomi_home_error": "檢測到 **{nick_name}({uid}, {cloud_server})** 出現錯誤,請進入選項頁面重新配置。\n\n**錯誤信息**: \n{message}",
"device_list_changed_title": "Xiaomi Home設備列表變化",
"device_list_changed": "檢測到 **{nick_name}({uid}, {cloud_server})** 設備信息發生變化,請進入集成選項頁面,點擊`選項->更新設備列表`,更新本地設備信息。\n\n當前網絡狀態{network_status}\n{message}\n",
"device_list_add": "\n**{count} 個新增設備:** \n{message}",
"device_list_del": "\n**{count} 個設備不可用:** \n{message}",
"device_list_offline": "\n**{count} 個設備離線:** \n{message}",
"network_status_online": "在線",
"network_status_offline": "離線",
"device_exec_error": "執行錯誤"
}
},
"error": {
"common": {
"-1": "未知錯誤",
"-10000": "未知錯誤",
"-10001": "服務不可用",
"-10002": "無效參數",
"-10003": "資源不足",
"-10004": "內部錯誤",
"-10005": "權限不足",
"-10006": "執行超時",
"-10007": "設備離線或者不存在",
"-10020": "無效的消息格式"
},
"gw": {},
"lan": {},
"cloud": {
"-704000000": "未知錯誤",
"-704010000": "未授權(設備可能被刪除)",
"-704014006": "沒找到設備描述",
"-704030013": "Property不可讀",
"-704030023": "Property不可寫",
"-704030033": "Property不可訂閱",
"-704040002": "Service不存在",
"-704040003": "Property不存在",
"-704040004": "Event不存在",
"-704040005": "Action不存在",
"-704040999": "功能未上線",
"-704042001": "Device不存在",
"-704042011": "設備離線",
"-704053036": "設備操作超時",
"-704053100": "設備在當前狀態下無法執行此操作",
"-704083036": "設備操作超時",
"-704090001": "Device不存在",
"-704220008": "無效的ID",
"-704220025": "Action參數個數不匹配",
"-704220035": "Action參數錯誤",
"-704220043": "Property值錯誤",
"-704222034": "Action返回值錯誤",
"-705004000": "未知錯誤",
"-705004501": "未知錯誤",
"-705201013": "Property不可讀",
"-705201015": "Action執行錯誤",
"-705201023": "Property不可寫",
"-705201033": "Property不可訂閱",
"-706012000": "未知錯誤",
"-706012013": "Property不可讀",
"-706012015": "Action執行錯誤",
"-706012023": "Property不可寫",
"-706012033": "Property不可訂閱",
"-706012043": "Property值錯誤",
"-706014006": "沒找到設備描述"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,809 @@
# -*- 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 http client.
"""
import asyncio
import base64
import json
import logging
import re
import time
from functools import partial
from typing import Optional
from urllib.parse import urlencode
import requests
from .common import calc_group_id
from .const import (
DEFAULT_OAUTH2_API_HOST,
MIHOME_HTTP_API_TIMEOUT,
OAUTH2_AUTH_URL)
from .miot_error import MIoTErrorCode, MIoTHttpError, MIoTOauthError
_LOGGER = logging.getLogger(__name__)
TOKEN_EXPIRES_TS_RATIO = 0.7
class MIoTOauthClient:
"""oauth agent url, default: product env."""
_main_loop: asyncio.AbstractEventLoop = None
_oauth_host: str = None
_client_id: int
_redirect_url: str
def __init__(
self, client_id: str, redirect_url: str, cloud_server: 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() == '':
raise MIoTOauthError('invalid client_id')
if not redirect_url:
raise MIoTOauthError('invalid redirect_url')
if not cloud_server:
raise MIoTOauthError('invalid cloud_server')
self._client_id = int(client_id)
self._redirect_url = redirect_url
if cloud_server == 'cn':
self._oauth_host = DEFAULT_OAUTH2_API_HOST
else:
self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
async def __call_async(self, func):
return await self._main_loop.run_in_executor(executor=None, func=func)
def set_redirect_url(self, redirect_url: str) -> None:
if not isinstance(redirect_url, str) or redirect_url.strip() == '':
raise MIoTOauthError('invalid redirect_url')
self._redirect_url = redirect_url
def gen_auth_url(
self,
redirect_url: Optional[str] = None,
state: Optional[str] = None,
scope: Optional[list] = None,
skip_confirm: Optional[bool] = False,
) -> str:
"""get auth url
Args:
redirect_url
state
scope (list, optional):
开放数据接口权限 ID可以传递多个用空格分隔具体值可以参考开放
[数据接口权限列表](https://dev.mi.com/distribute/doc/details?pId=1518).
Defaults to None.\n
skip_confirm (bool, optional):
默认值为true授权有效期内的用户在已登录情况下不显示授权页面直接通过。
如果需要用户每次手动授权设置为false. Defaults to True.\n
Returns:
str: _description_
"""
params: dict = {
'redirect_uri': redirect_url or self._redirect_url,
'client_id': self._client_id,
'response_type': 'code',
}
if state:
params['state'] = state
if scope:
params['scope'] = ' '.join(scope).strip()
params['skip_confirm'] = skip_confirm
encoded_params = urlencode(params)
return f'{OAUTH2_AUTH_URL}?{encoded_params}'
def _get_token(self, data) -> dict:
http_res = requests.get(
url=f'https://{self._oauth_host}/app/v2/ha/oauth/get_token',
params={'data': json.dumps(data)},
headers={'content-type': 'application/x-www-form-urlencoded'},
timeout=MIHOME_HTTP_API_TIMEOUT
)
if http_res.status_code == 401:
raise MIoTOauthError(
'unauthorized(401)', MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED)
if http_res.status_code != 200:
raise MIoTOauthError(
f'invalid http status code, {http_res.status_code}')
res_obj = http_res.json()
if (
not res_obj
or res_obj.get('code', None) != 0
or 'result' not in res_obj
or not all(
key in res_obj['result']
for key in ['access_token', 'refresh_token', 'expires_in'])
):
raise MIoTOauthError(f'invalid http response, {http_res.text}')
return {
**res_obj['result'],
'expires_ts': int(
time.time() +
(res_obj['result'].get('expires_in', 0)*TOKEN_EXPIRES_TS_RATIO))
}
def get_access_token(self, code: str) -> dict:
"""get access token by authorization code
Args:
code (str): auth code
Returns:
str: _description_
"""
if not isinstance(code, str):
raise MIoTOauthError('invalid code')
return self._get_token(data={
'client_id': self._client_id,
'redirect_uri': self._redirect_url,
'code': code,
})
async def get_access_token_async(self, code: str) -> dict:
return await self.__call_async(partial(self.get_access_token, code))
def refresh_access_token(self, refresh_token: str) -> dict:
"""get access token by refresh token.
Args:
refresh_token (str): refresh_token
Returns:
str: _description_
"""
if not isinstance(refresh_token, str):
raise MIoTOauthError('invalid refresh_token')
return self._get_token(data={
'client_id': self._client_id,
'redirect_uri': self._redirect_url,
'refresh_token': refresh_token,
})
async def refresh_access_token_async(self, refresh_token: str) -> dict:
return await self.__call_async(
partial(self.refresh_access_token, refresh_token))
class MIoTHttpClient:
"""MIoT http client."""
GET_PROP_AGGREGATE_INTERVAL: float = 0.2
GET_PROP_MAX_REQ_COUNT = 150
_main_loop: asyncio.AbstractEventLoop
_host: str
_base_url: str
_client_id: str
_access_token: str
_get_prop_timer: asyncio.TimerHandle
_get_prop_list: dict[str, dict[str, asyncio.Future | str | bool]]
def __init__(
self, cloud_server: str, client_id: str, access_token: str,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
self._host = None
self._base_url = None
self._client_id = None
self._access_token = None
self._get_prop_timer: asyncio.TimerHandle = None
self._get_prop_list = {}
if (
not isinstance(cloud_server, str)
or not isinstance(client_id, str)
or not isinstance(access_token, str)
):
raise MIoTHttpError('invalid params')
self.update_http_header(
cloud_server=cloud_server, client_id=client_id,
access_token=access_token)
async def __call_async(self, func) -> any:
if self._main_loop is None:
raise MIoTHttpError('miot http, un-support async methods')
return await self._main_loop.run_in_executor(executor=None, func=func)
def update_http_header(
self, cloud_server: Optional[str] = None,
client_id: Optional[str] = None,
access_token: Optional[str] = None
) -> None:
if isinstance(cloud_server, str):
if cloud_server == 'cn':
self._host = DEFAULT_OAUTH2_API_HOST
else:
self._host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'
self._base_url = f'https://{self._host}'
if isinstance(client_id, str):
self._client_id = client_id
if isinstance(access_token, str):
self._access_token = access_token
@property
def __api_session(self) -> requests.Session:
session = requests.Session()
session.headers.update({
'Host': self._host,
'X-Client-BizId': 'haapi',
'Content-Type': 'application/json',
'Authorization': f'Bearer{self._access_token}',
'X-Client-AppId': self._client_id,
})
return session
def mihome_api_get(
self, url_path: str, params: dict,
timeout: int = MIHOME_HTTP_API_TIMEOUT
) -> dict:
http_res = None
with self.__api_session as session:
http_res = session.get(
url=f'{self._base_url}{url_path}',
params=params,
timeout=timeout)
if http_res.status_code == 401:
raise MIoTHttpError(
'mihome api get failed, unauthorized(401)',
MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN)
if http_res.status_code != 200:
raise MIoTHttpError(
f'mihome api get failed, {http_res.status_code}, '
f'{url_path}, {params}')
res_obj: dict = http_res.json()
if res_obj.get('code', None) != 0:
raise MIoTHttpError(
f'invalid response code, {res_obj.get("code",None)}, '
f'{res_obj.get("message","")}')
_LOGGER.debug(
'mihome api get, %s%s, %s -> %s',
self._base_url, url_path, params, res_obj)
return res_obj
def mihome_api_post(
self, url_path: str, data: dict,
timeout: int = MIHOME_HTTP_API_TIMEOUT
) -> dict:
encoded_data = None
if data:
encoded_data = json.dumps(data).encode('utf-8')
http_res = None
with self.__api_session as session:
http_res = session.post(
url=f'{self._base_url}{url_path}',
data=encoded_data,
timeout=timeout)
if http_res.status_code == 401:
raise MIoTHttpError(
'mihome api get failed, unauthorized(401)',
MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN)
if http_res.status_code != 200:
raise MIoTHttpError(
f'mihome api post failed, {http_res.status_code}, '
f'{url_path}, {data}')
res_obj: dict = http_res.json()
if res_obj.get('code', None) != 0:
raise MIoTHttpError(
f'invalid response code, {res_obj.get("code",None)}, '
f'{res_obj.get("message","")}')
_LOGGER.debug(
'mihome api post, %s%s, %s -> %s',
self._base_url, url_path, data, res_obj)
return res_obj
def get_user_info(self) -> dict:
http_res = requests.get(
url='https://open.account.xiaomi.com/user/profile',
params={'clientId': self._client_id,
'token': self._access_token},
headers={'content-type': 'application/x-www-form-urlencoded'},
timeout=MIHOME_HTTP_API_TIMEOUT
)
res_obj = http_res.json()
if (
not res_obj
or res_obj.get('code', None) != 0
or 'data' not in res_obj
or 'miliaoNick' not in res_obj['data']
):
raise MIoTOauthError(f'invalid http response, {http_res.text}')
return res_obj['data']
async def get_user_info_async(self) -> dict:
return await self.__call_async(partial(self.get_user_info))
def get_central_cert(self, csr: str) -> Optional[str]:
if not isinstance(csr, str):
raise MIoTHttpError('invalid params')
res_obj: dict = self.mihome_api_post(
url_path='/app/v2/ha/oauth/get_central_crt',
data={
'csr': str(base64.b64encode(csr.encode('utf-8')), 'utf-8')
}
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
cert: str = res_obj['result'].get('cert', None)
if not isinstance(cert, str):
raise MIoTHttpError('invalid cert')
return cert
async def get_central_cert_async(self, csr: str) -> Optional[str]:
return await self.__call_async(partial(self.get_central_cert, csr))
def __get_dev_room_page(self, max_id: str = None) -> dict:
res_obj = self.mihome_api_post(
url_path='/app/v2/homeroom/get_dev_room_page',
data={
'start_id': max_id,
'limit': 150,
},
)
if 'result' not in res_obj and 'info' not in res_obj['result']:
raise MIoTHttpError('invalid response result')
home_list: dict = {}
for home in res_obj['result']['info']:
if 'id' not in home:
_LOGGER.error(
'get dev room page error, invalid home, %s', home)
continue
home_list[str(home['id'])] = {'dids': home.get(
'dids', None) or [], 'room_info': {}}
for room in home.get('roomlist', []):
if 'id' not in room:
_LOGGER.error(
'get dev room page error, invalid room, %s', room)
continue
home_list[str(home['id'])]['room_info'][str(room['id'])] = {
'dids': room.get('dids', None) or []}
if (
res_obj['result'].get('has_more', False)
and isinstance(res_obj['result'].get('max_id', None), str)
):
next_list = self.__get_dev_room_page(
max_id=res_obj['result']['max_id'])
for home_id, info in next_list.items():
home_list.setdefault(home_id, {'dids': [], 'room_info': {}})
home_list[home_id]['dids'].extend(info['dids'])
for room_id, info in info['room_info'].items():
home_list[home_id]['room_info'].setdefault(
room_id, {'dids': []})
home_list[home_id]['room_info'][room_id]['dids'].extend(
info['dids'])
return home_list
def get_homeinfos(self) -> dict:
res_obj = self.mihome_api_post(
url_path='/app/v2/homeroom/gethome',
data={
'limit': 150,
'fetch_share': True,
'fetch_share_dev': True,
'plat_form': 0,
'app_ver': 9,
},
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
uid: str = None
home_infos: dict = {}
for device_source in ['homelist', 'share_home_list']:
home_infos.setdefault(device_source, {})
for home in res_obj['result'].get(device_source, []):
if (
'id' not in home
or 'name' not in home
or 'roomlist' not in home
):
continue
if uid is None and device_source == 'homelist':
uid = str(home['uid'])
home_infos[device_source][home['id']] = {
'home_id': home['id'],
'home_name': home['name'],
'city_id': home.get('city_id', None),
'longitude': home.get('longitude', None),
'latitude': home.get('latitude', None),
'address': home.get('address', None),
'dids': home.get('dids', []),
'room_info': {
room['id']: {
'room_id': room['id'],
'room_name': room['name'],
'dids': room.get('dids', [])
}
for room in home.get('roomlist', [])
},
'group_id': calc_group_id(
uid=home['uid'], home_id=home['id']),
'uid': str(home['uid'])
}
home_infos['uid'] = uid
if (
res_obj['result'].get('has_more', False)
and isinstance(res_obj['result'].get('max_id', None), str)
):
more_list = self.__get_dev_room_page(
max_id=res_obj['result']['max_id'])
for home_id, info in more_list.items():
if home_id not in home_infos['homelist']:
_LOGGER.info('unknown home, %s, %s', home_id, info)
continue
home_infos['homelist'][home_id]['dids'].extend(info['dids'])
for room_id, info in info['room_info'].items():
home_infos['homelist'][home_id]['room_info'].setdefault(
room_id, {'dids': []})
home_infos['homelist'][home_id]['room_info'][
room_id]['dids'].extend(info['dids'])
return {
'uid': uid,
'home_list': home_infos.get('homelist', {}),
'share_home_list': home_infos.get('share_home_list', [])
}
async def get_homeinfos_async(self) -> dict:
return await self.__call_async(self.get_homeinfos)
def get_uid(self) -> str:
return self.get_homeinfos().get('uid', None)
async def get_uid_async(self) -> str:
return (await self.get_homeinfos_async()).get('uid', None)
def __get_device_list_page(
self, dids: list[str], start_did: str = None
) -> dict[str, dict]:
req_data: dict = {
'limit': 200,
'get_split_device': True,
'dids': dids
}
if start_did:
req_data['start_did'] = start_did
device_infos: dict = {}
res_obj = self.mihome_api_post(
url_path='/app/v2/home/device_list_page',
data=req_data
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
res_obj = res_obj['result']
for device in res_obj.get('list', []) or []:
did = device.get('did', None)
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)
continue
device_infos[did] = {
'did': did,
'uid': device.get('uid', None),
'name': name,
'urn': urn,
'model': model,
'connect_type': device.get('pid', -1),
'token': device.get('token', None),
'online': device.get('isOnline', False),
'icon': device.get('icon', None),
'parent_id': device.get('parent_id', None),
'manufacturer': model.split('.')[0],
# 2: xiao-ai, 1: general speaker
'voice_ctrl': device.get('voice_ctrl', 0),
'rssi': device.get('rssi', None),
'owner': device.get('owner', None),
'pid': device.get('pid', None),
'local_ip': device.get('local_ip', None),
'ssid': device.get('ssid', None),
'bssid': device.get('bssid', None),
'order_time': device.get('orderTime', 0),
'fw_version': device.get('extra', {}).get(
'fw_version', 'unknown'),
}
if isinstance(device.get('extra', None), dict) and device['extra']:
device_infos[did]['fw_version'] = device['extra'].get(
'fw_version', None)
device_infos[did]['mcu_version'] = device['extra'].get(
'mcu_version', None)
device_infos[did]['platform'] = device['extra'].get(
'platform', None)
next_start_did = res_obj.get('next_start_did', None)
if res_obj.get('has_more', False) and next_start_did:
device_infos.update(self.__get_device_list_page(
dids=dids, start_did=next_start_did))
return device_infos
async def get_devices_with_dids_async(
self, dids: list[str]
) -> dict[str, dict]:
results: list[dict[str, dict]] = await asyncio.gather(
*[self.__call_async(
partial(self.__get_device_list_page, dids[index:index+150]))
for index in range(0, len(dids), 150)])
devices = {}
for result in results:
if result is None:
return None
devices.update(result)
return devices
async def get_devices_async(
self, home_ids: list[str] = None
) -> dict[str, dict]:
homeinfos = await self.get_homeinfos_async()
homes: dict[str, dict[str, any]] = {}
devices: dict[str, dict] = {}
for device_type in ['home_list', 'share_home_list']:
homes.setdefault(device_type, {})
for home_id, home_info in (homeinfos.get(
device_type, None) or {}).items():
if isinstance(home_ids, list) and home_id not in home_ids:
continue
homes[device_type].setdefault(
home_id, {
'home_name': home_info['home_name'],
'uid': home_info['uid'],
'group_id': home_info['group_id'],
'room_info': {}
})
devices.update({did: {
'home_id': home_id,
'home_name': home_info['home_name'],
'room_id': home_id,
'room_name': home_info['home_name'],
'group_id': home_info['group_id']
} for did in home_info.get('dids', [])})
for room_id, room_info in home_info.get('room_info').items():
homes[device_type][home_id]['room_info'][
room_id] = room_info['room_name']
devices.update({
did: {
'home_id': home_id,
'home_name': home_info['home_name'],
'room_id': room_id,
'room_name': room_info['room_name'],
'group_id': home_info['group_id']
} for did in room_info.get('dids', [])})
dids = sorted(list(devices.keys()))
results: dict[str, dict] = await self.get_devices_with_dids_async(
dids=dids)
for did in dids:
if did not in results:
devices.pop(did, None)
_LOGGER.error('get device info failed, %s', did)
continue
devices[did].update(results[did])
# Whether sub devices
match_str = re.search(r'\.s\d+$', did)
if not match_str:
continue
device = devices.pop(did, None)
parent_did = did.replace(match_str.group(), '')
if parent_did in devices:
devices[parent_did].setdefault('sub_devices', {})
devices[parent_did]['sub_devices'][match_str.group()[
1:]] = device
else:
_LOGGER.error(
'unknown sub devices, %s, %s', did, parent_did)
return {
'uid': homeinfos['uid'],
'homes': homes,
'devices': devices
}
def get_props(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1},
{"did": "xxxxxx", "siid": 2, "piid": 2}]
"""
res_obj = self.mihome_api_post(
url_path='/app/v2/miotspec/prop/get',
data={
'datasource': 1,
'params': params
},
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def get_props_async(self, params: list) -> list:
return await self.__call_async(partial(self.get_props, params))
def get_prop(self, did: str, siid: int, piid: int) -> any:
results = self.get_props(
params=[{'did': did, 'siid': siid, 'piid': piid}])
if not results:
return None
result = results[0]
if 'value' not in result:
return None
return result['value']
async def __get_prop_handler(self) -> bool:
props_req: set[str] = set()
props_buffer: list[dict] = []
for key, item in self._get_prop_list.items():
if item.get('tag', False):
continue
# NOTICE: max req prop
if len(props_req) >= self.GET_PROP_MAX_REQ_COUNT:
break
item['tag'] = True
props_buffer.append(item['param'])
props_req.add(key)
if not props_buffer:
_LOGGER.error('get prop error, empty request list')
return False
results = await self.__call_async(partial(self.get_props, props_buffer))
for result in results:
if not all(
key in result for key in ['did', 'siid', 'piid', 'value']):
continue
key = f'{result["did"]}.{result["siid"]}.{result["piid"]}'
prop_obj = self._get_prop_list.pop(key, None)
if prop_obj is None:
_LOGGER.error('get prop error, key not exists, %s', result)
continue
prop_obj['fut'].set_result(result['value'])
props_req.remove(key)
for key in props_req:
prop_obj = self._get_prop_list.pop(key, None)
if prop_obj is None:
continue
prop_obj['fut'].set_result(None)
if props_req:
_LOGGER.error(
'get prop from cloud failed, %s, %s', len(key), props_req)
if self._get_prop_list:
self._get_prop_timer = self._main_loop.call_later(
self.GET_PROP_AGGREGATE_INTERVAL,
lambda: self._main_loop.create_task(
self.__get_prop_handler()))
else:
self._get_prop_timer = None
return True
async def get_prop_async(
self, did: str, siid: int, piid: int, immediately: bool = False
) -> any:
if immediately:
return await self.__call_async(
partial(self.get_prop, did, siid, piid))
key: str = f'{did}.{siid}.{piid}'
prop_obj = self._get_prop_list.get(key, None)
if prop_obj:
return await prop_obj['fut']
fut = self._main_loop.create_future()
self._get_prop_list[key] = {
'param': {'did': did, 'siid': siid, 'piid': piid},
'fut': fut
}
if self._get_prop_timer is None:
self._get_prop_timer = self._main_loop.call_later(
self.GET_PROP_AGGREGATE_INTERVAL,
lambda: self._main_loop.create_task(
self.__get_prop_handler()))
return await fut
def set_prop(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
"""
res_obj = self.mihome_api_post(
url_path='/app/v2/miotspec/prop/set',
data={
'params': params
},
timeout=15
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def set_prop_async(self, params: list) -> list:
"""
params = [{"did": "xxxx", "siid": 2, "piid": 1, "value": False}]
"""
return await self.__call_async(partial(self.set_prop, params))
def action(
self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict:
"""
params = {"did": "xxxx", "siid": 2, "aiid": 1, "in": []}
"""
# NOTICE: Non-standard action param
res_obj = self.mihome_api_post(
url_path='/app/v2/miotspec/action',
data={
'params': {
'did': did,
'siid': siid,
'aiid': aiid,
'in': [item['value'] for item in in_list]}
},
timeout=15
)
if 'result' not in res_obj:
raise MIoTHttpError('invalid response result')
return res_obj['result']
async def action_async(
self, did: str, siid: int, aiid: int, in_list: list[dict]
) -> dict:
return await self.__call_async(
partial(self.action, did, siid, aiid, in_list))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
# -*- 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 error code and exception.
"""
from enum import Enum
class MIoTErrorCode(Enum):
"""MIoT error code."""
# Base error code
CODE_UNKNOWN = -10000
CODE_UNAVAILABLE = -10001
CODE_INVALID_PARAMS = -10002
CODE_RESOURCE_ERROR = -10003
CODE_INTERNAL_ERROR = -10004
CODE_UNAUTHORIZED_ACCESS = -10005
CODE_TIMEOUT = -10006
# OAuth error code
CODE_OAUTH_UNAUTHORIZED = -10020
# Http error code
CODE_HTTP_INVALID_ACCESS_TOKEN = -10030
# MIoT mips error code
CODE_MIPS_INVALID_RESULT = -10040
# MIoT cert error code
CODE_CERT_INVALID_CERT = -10050
# MIoT spec error code, -10060
# MIoT storage error code, -10070
# MIoT ev error code, -10080
# Mips service error code, -10090
# Config flow error code, -10100
# Options flow error code , -10110
# MIoT lan error code, -10120
class MIoTError(Exception):
"""MIoT error."""
code: MIoTErrorCode
message: any
def __init__(
self, message: any, code: MIoTErrorCode = MIoTErrorCode.CODE_UNKNOWN
) -> None:
self.message = message
self.code = code
super().__init__(self.message)
def to_str(self) -> str:
return f'{{"code":{self.code.value},"message":"{self.message}"}}'
def to_dict(self) -> dict:
return {"code": self.code.value, "message": self.message}
class MIoTOauthError(MIoTError):
...
class MIoTHttpError(MIoTError):
...
class MIoTMipsError(MIoTError):
...
class MIoTDeviceError(MIoTError):
...
class MIoTSpecError(MIoTError):
...
class MIoTStorageError(MIoTError):
...
class MIoTCertError(MIoTError):
...
class MIoTClientError(MIoTError):
...
class MIoTEvError(MIoTError):
...
class MipsServiceError(MIoTError):
...
class MIoTConfigError(MIoTError):
...
class MIoTOptionsError(MIoTError):
...

View File

@ -0,0 +1,320 @@
# -*- 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 Callable, TypeVar
import logging
import threading
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')
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)

View File

@ -0,0 +1,109 @@
# -*- 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 internationalization translation.
"""
import asyncio
import logging
import os
from typing import Optional
from .common import load_json_file
_LOGGER = logging.getLogger(__name__)
class MIoTI18n:
"""MIoT Internationalization Translation.
Translate by Copilot, which does not guarantee the accuracy of the
translation. If there is a problem with the translation, please submit
the ISSUE feedback. After the review, we will modify it as soon as possible.
"""
_main_loop: asyncio.AbstractEventLoop
_lang: str
_data: dict
def __init__(
self, lang: str, loop: Optional[asyncio.AbstractEventLoop]
) -> None:
self._main_loop = loop or asyncio.get_event_loop()
self._lang = lang
self._data = None
async def init_async(self) -> None:
if self._data:
return
data = None
self._data = {}
try:
data = await self._main_loop.run_in_executor(
None, load_json_file,
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
f'i18n/{self._lang}.json'))
except Exception as err: # pylint: disable=broad-exception-caught
_LOGGER.error('load file error, %s', err)
return
# Check if the file is a valid JSON file
if not isinstance(data, dict):
_LOGGER.error('valid file, %s', data)
return
self._data = data
async def deinit_async(self) -> None:
self._data = None
def translate(
self, key: str, replace: Optional[dict[str, str]] = None
) -> str | dict | None:
result = self._data
for item in key.split('.'):
if item not in result:
return None
result = result[item]
if isinstance(result, str) and replace:
for k, v in replace.items():
result = result.replace('{'+k+'}', str(v))
return result or None

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,283 @@
# -*- 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 central hub gateway service discovery.
"""
import asyncio
import base64
import binascii
import copy
from enum import Enum
from typing import Callable, Optional
import logging
from zeroconf import (
DNSQuestionType,
IPVersion,
ServiceStateChange,
Zeroconf)
from zeroconf.asyncio import (
AsyncServiceInfo,
AsyncZeroconf,
AsyncServiceBrowser)
from .miot_error import MipsServiceError
_LOGGER = logging.getLogger(__name__)
MIPS_MDNS_TYPE = '_miot-central._tcp.local.'
MIPS_MDNS_REQUEST_TIMEOUT_MS = 5000
MIPS_MDNS_UPDATE_INTERVAL_S = 600
class MipsServiceState(Enum):
ADDED = 1
REMOVED = 2
UPDATED = 3
class MipsServiceData:
"""Mips service data."""
profile: str
profile_bin: bytes
name: str
addresses: list[str]
port: int
type: str
server: str
did: str
group_id: str
role: int
suite_mqtt: bool
def __init__(self, service_info: AsyncServiceInfo) -> None:
if service_info is None:
raise MipsServiceError('invalid params')
properties = service_info.decoded_properties
if properties is None:
raise MipsServiceError('invalid service properties')
self.profile = properties.get('profile', None)
if self.profile is None:
raise MipsServiceError('invalid service profile')
self.profile_bin = base64.b64decode(self.profile)
self.name = service_info.name
self.addresses = service_info.parsed_addresses(
version=IPVersion.V4Only)
if not self.addresses:
raise MipsServiceError('invalid addresses')
self.addresses.sort()
self.port = service_info.port
self.type = service_info.type
self.server = service_info.server
# Parse profile
self.did = str(int.from_bytes(self.profile_bin[1:9]))
self.group_id = binascii.hexlify(
self.profile_bin[9:17][::-1]).decode('utf-8')
self.role = int(self.profile_bin[20] >> 4)
self.suite_mqtt = ((self.profile_bin[22] >> 1) & 0x01) == 0x01
def valid_service(self) -> bool:
if self.role != 1:
return False
return self.suite_mqtt
def to_dict(self) -> dict:
return {
'name': self.name,
'addresses': self.addresses,
'port': self.port,
'type': self.type,
'server': self.server,
'did': self.did,
'group_id': self.group_id,
'role': self.role,
'suite_mqtt': self.suite_mqtt
}
def __str__(self) -> str:
return str(self.to_dict())
class MipsService:
"""MIPS service discovery."""
_aiozc: AsyncZeroconf
_main_loop: asyncio.AbstractEventLoop
_aio_browser: AsyncServiceBrowser
_services: dict[str, dict]
# key = (key, group_id)
_sub_list: dict[(str, str), Callable[[
str, MipsServiceState, dict], asyncio.Future]]
def __init__(
self, aiozc: AsyncZeroconf,
loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._aiozc = aiozc
self._main_loop = loop or asyncio.get_running_loop()
self._aio_browser = None
self._services = {}
self._sub_list = {}
async def init_async(self) -> None:
await self._aiozc.zeroconf.async_wait_for_start()
self._aio_browser = AsyncServiceBrowser(
zeroconf=self._aiozc.zeroconf,
type_=MIPS_MDNS_TYPE,
handlers=[self.__on_service_state_change],
question_type=DNSQuestionType.QM)
async def deinit_async(self) -> None:
await self._aio_browser.async_cancel()
self._services = {}
self._sub_list = {}
def get_services(self, group_id: Optional[str] = None) -> dict[str, dict]:
"""get mips services.
Args:
group_id (str, optional): _description_. Defaults to None.
Returns: {
[group_id:str]: {
"name": str,
"addresses": list[str],
"port": number,
"type": str,
"server": str,
"version": int,
"did": str,
"group_id": str,
"role": int,
"suite_mqtt": bool
}
}
"""
if group_id:
if group_id not in self._services:
return {}
return {group_id: copy.deepcopy(self._services[group_id])}
return copy.deepcopy(self._services)
def sub_service_change(
self, key: str, group_id: str,
handler: Callable[[str, MipsServiceState, dict], asyncio.Future]
) -> None:
if key is None or group_id is None or handler is None:
raise MipsServiceError('invalid params')
self._sub_list[(key, group_id)] = handler
def unsub_service_change(self, key: str) -> None:
if key is None:
return
for keys in list(self._sub_list.keys()):
if key == keys[0]:
self._sub_list.pop(keys, None)
def __on_service_state_change(
self, zeroconf: Zeroconf, service_type: str, name: str,
state_change: ServiceStateChange
) -> None:
_LOGGER.debug(
'mips service state changed, %s, %s, %s',
state_change, name, service_type)
if state_change is ServiceStateChange.Removed:
for item in list(self._services.values()):
if item['name'] != name:
continue
service_data = self._services.pop(item['group_id'], None)
self.__call_service_change(
state=MipsServiceState.REMOVED, data=service_data)
return
self._main_loop.create_task(
self.__request_service_info_async(zeroconf, service_type, name))
async def __request_service_info_async(
self, zeroconf: Zeroconf, service_type: str, name: str
) -> None:
info = AsyncServiceInfo(service_type, name)
await info.async_request(
zeroconf, MIPS_MDNS_REQUEST_TIMEOUT_MS,
question_type=DNSQuestionType.QU)
try:
service_data = MipsServiceData(info)
if not service_data.valid_service():
raise MipsServiceError(
'no primary role, no support mqtt connection')
if service_data.group_id in self._services:
# Update mips service
buffer_data = self._services[service_data.group_id]
if (
service_data.did != buffer_data['did']
or service_data.addresses != buffer_data['addresses']
or service_data.port != buffer_data['port']
):
self._services[service_data.group_id].update(
service_data.to_dict())
self.__call_service_change(
state=MipsServiceState.UPDATED,
data=service_data.to_dict())
else:
# Add mips service
self._services[service_data.group_id] = service_data.to_dict()
self.__call_service_change(
state=MipsServiceState.ADDED,
data=self._services[service_data.group_id])
except MipsServiceError as error:
_LOGGER.error('invalid mips service, %s, %s', error, info)
def __call_service_change(
self, state: MipsServiceState, data: dict = None
) -> None:
_LOGGER.info('call service change, %s, %s', state, data)
for keys in list(self._sub_list.keys()):
if keys[1] in [data['group_id'], '*']:
self._main_loop.create_task(
self._sub_list[keys](data['group_id'], state, data))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,295 @@
# -*- 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 network utilities.
"""
import asyncio
import logging
import platform
import socket
from dataclasses import dataclass
from enum import Enum, auto
import subprocess
from typing import Callable, Optional
import psutil
import ipaddress
_LOGGER = logging.getLogger(__name__)
class InterfaceStatus(Enum):
"""Interface status."""
ADD = 0
UPDATE = auto()
REMOVE = auto()
@dataclass
class NetworkInfo:
"""Network information."""
name: str
ip: str
netmask: str
net_seg: str
class MIoTNetwork:
"""MIoT network utilities."""
PING_ADDRESS_LIST = [
'1.2.4.8', # CNNIC sDNS
'8.8.8.8', # Google Public DNS
'233.5.5.5', # AliDNS
'1.1.1.1', # Cloudflare DNS
'114.114.114.114', # 114 DNS
'208.67.222.222', # OpenDNS
'9.9.9.9', # Quad9 DNS
]
_main_loop: asyncio.AbstractEventLoop
_refresh_interval: int
_refresh_task: asyncio.Task
_refresh_timer: asyncio.TimerHandle
_network_status: bool
_network_info: dict[str, NetworkInfo]
_sub_list_network_status: dict[str, Callable[[bool], asyncio.Future]]
_sub_list_network_info: dict[str, Callable[[
InterfaceStatus, NetworkInfo], asyncio.Future]]
_ping_address_priority: int
_done_event: asyncio.Event
def __init__(
self, loop: Optional[asyncio.AbstractEventLoop] = None
) -> None:
self._main_loop = loop or asyncio.get_running_loop()
self._refresh_interval = None
self._refresh_task = None
self._refresh_timer = None
self._network_status = False
self._network_info = {}
self._sub_list_network_status = {}
self._sub_list_network_info = {}
self._ping_address_priority = 0
self._done_event = asyncio.Event()
@property
def network_status(self) -> bool:
return self._network_status
@property
def network_info(self) -> dict[str, NetworkInfo]:
return self._network_info
async def deinit_async(self) -> None:
if self._refresh_task:
self._refresh_task.cancel()
self._refresh_task = None
if self._refresh_timer:
self._refresh_timer.cancel()
self._refresh_timer = None
self._refresh_interval = None
self._network_status = False
self._network_info.clear()
self._sub_list_network_status.clear()
self._sub_list_network_info.clear()
self._done_event.clear()
def sub_network_status(
self, key: str, handler: Callable[[bool], asyncio.Future]
) -> None:
self._sub_list_network_status[key] = handler
def unsub_network_status(self, key: str) -> None:
self._sub_list_network_status.pop(key, None)
def sub_network_info(
self, key: str,
handler: Callable[[InterfaceStatus, NetworkInfo], asyncio.Future]
) -> None:
self._sub_list_network_info[key] = handler
def unsub_network_info(self, key: str) -> None:
self._sub_list_network_info.pop(key, None)
async def init_async(self, refresh_interval: int = 30) -> bool:
self._refresh_interval = refresh_interval
self.__refresh_timer_handler()
# MUST get network info before starting
return await self._done_event.wait()
async def refresh_async(self) -> None:
self.__refresh_timer_handler()
async def get_network_status_async(self, timeout: int = 6) -> bool:
return await self._main_loop.run_in_executor(
None, self.__get_network_status, False, timeout)
async def get_network_info_async(self) -> dict[str, NetworkInfo]:
return await self._main_loop.run_in_executor(
None, self.__get_network_info)
def __calc_network_address(self, ip: str, netmask: str) -> str:
return str(ipaddress.IPv4Network(
f'{ip}/{netmask}', strict=False).network_address)
def __ping(
self, address: Optional[str] = None, timeout: int = 6
) -> bool:
param = '-n' if platform.system().lower() == 'windows' else '-c'
command = ['ping', param, '1', address]
try:
output = subprocess.run(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True, timeout=timeout)
return output.returncode == 0
except Exception: # pylint: disable=broad-exception-caught
return False
def __get_network_status(
self, with_retry: bool = True, timeout: int = 6
) -> bool:
if self._ping_address_priority >= len(self.PING_ADDRESS_LIST):
self._ping_address_priority = 0
if self.__ping(
self.PING_ADDRESS_LIST[self._ping_address_priority], timeout):
return True
if not with_retry:
return False
for index in range(len(self.PING_ADDRESS_LIST)):
if index == self._ping_address_priority:
continue
if self.__ping(self.PING_ADDRESS_LIST[index], timeout):
self._ping_address_priority = index
return True
return False
def __get_network_info(self) -> dict[str, NetworkInfo]:
interfaces = psutil.net_if_addrs()
results: dict[str, NetworkInfo] = {}
for name, addresses in interfaces.items():
# Skip hassio and docker* interface
if name == 'hassio' or name.startswith('docker'):
continue
for address in addresses:
if (
address.family != socket.AF_INET
or not address.address
or not address.netmask
):
continue
# skip lo interface
if address.address == '127.0.0.1':
continue
results[name] = NetworkInfo(
name=name,
ip=address.address,
netmask=address.netmask,
net_seg=self.__calc_network_address(
address.address, address.netmask))
return results
def __call_network_info_change(
self, status: InterfaceStatus, info: NetworkInfo
) -> None:
for handler in self._sub_list_network_info.values():
self._main_loop.create_task(handler(status, info))
async def __update_status_and_info_async(self, timeout: int = 6) -> None:
try:
status: bool = await self._main_loop.run_in_executor(
None, self.__get_network_status, timeout)
infos = await self._main_loop.run_in_executor(
None, self.__get_network_info)
if self._network_status != status:
for handler in self._sub_list_network_status.values():
self._main_loop.create_task(handler(status))
self._network_status = status
for name in list(self._network_info.keys()):
info = infos.pop(name, None)
if info:
# Update
if (
info.ip != self._network_info[name].ip
or info.netmask != self._network_info[name].netmask
):
self._network_info[name] = info
self.__call_network_info_change(
InterfaceStatus.UPDATE, info)
else:
# Remove
self.__call_network_info_change(
InterfaceStatus.REMOVE,
self._network_info.pop(name, None))
# Add
for name, info in infos.items():
self._network_info[name] = info
self.__call_network_info_change(InterfaceStatus.ADD, info)
if not self._done_event.is_set():
self._done_event.set()
except asyncio.CancelledError:
_LOGGER.error('update_status_and_info task was cancelled')
def __refresh_timer_handler(self) -> None:
if self._refresh_timer:
self._refresh_timer.cancel()
self._refresh_timer = None
if self._refresh_task is None or self._refresh_task.done():
self._refresh_task = self._main_loop.create_task(
self.__update_status_and_info_async())
self._refresh_timer = self._main_loop.call_later(
self._refresh_interval, self.__refresh_timer_handler)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,235 @@
{
"data": {
"urn:miot-spec-v2:property:air-cooler:000000EB": "open_close",
"urn:miot-spec-v2:property:alarm:00000012": "open_close",
"urn:miot-spec-v2:property:anion:00000025": "open_close",
"urn:miot-spec-v2:property:auto-cleanup:00000124": "open_close",
"urn:miot-spec-v2:property:auto-deodorization:00000125": "open_close",
"urn:miot-spec-v2:property:auto-keep-warm:0000002B": "open_close",
"urn:miot-spec-v2:property:automatic-feeding:000000F0": "open_close",
"urn:miot-spec-v2:property:blow:000000CD": "open_close",
"urn:miot-spec-v2:property:deodorization:000000C6": "open_close",
"urn:miot-spec-v2:property:dns-auto-mode:000000DC": "open_close",
"urn:miot-spec-v2:property:current-physical-control-lock:00000099": "open_close",
"urn:miot-spec-v2:property:dryer:00000027": "open_close",
"urn:miot-spec-v2:property:eco:00000024": "open_close",
"urn:miot-spec-v2:property:glimmer-full-color:00000089": "open_close",
"urn:miot-spec-v2:property:guard-mode:000000B6": "open_close",
"urn:miot-spec-v2:property:heater:00000026": "open_close",
"urn:miot-spec-v2:property:heating:000000C7": "open_close",
"urn:miot-spec-v2:property:horizontal-swing:00000017": "open_close",
"urn:miot-spec-v2:property:hot-water-recirculation:0000011C": "open_close",
"urn:miot-spec-v2:property:image-distortion-correction:0000010F": "open_close",
"urn:miot-spec-v2:property:mute:00000040": "open_close",
"urn:miot-spec-v2:property:motion-detection:00000056": "open_close",
"urn:miot-spec-v2:property:motion-tracking:0000008A": "open_close",
"urn:miot-spec-v2:property:off-delay:00000053": "open_close",
"urn:miot-spec-v2:property:on:00000006": "open_close",
"urn:miot-spec-v2:property:physical-controls-locked:0000001D": "open_close",
"urn:miot-spec-v2:property:preheat:00000103": "open_close",
"urn:miot-spec-v2:property:sleep-aid-mode:0000010B": "open_close",
"urn:miot-spec-v2:property:sleep-mode:00000028": "open_close",
"urn:miot-spec-v2:property:soft-wind:000000CF": "open_close",
"urn:miot-spec-v2:property:speed-control:000000E8": "open_close",
"urn:miot-spec-v2:property:time-watermark:00000087": "open_close",
"urn:miot-spec-v2:property:un-straight-blowing:00000100": "open_close",
"urn:miot-spec-v2:property:uv:00000029": "open_close",
"urn:miot-spec-v2:property:valve-switch:000000FE": "open_close",
"urn:miot-spec-v2:property:ventilation:000000CE": "open_close",
"urn:miot-spec-v2:property:vertical-swing:00000018": "open_close",
"urn:miot-spec-v2:property:wake-up-mode:00000107": "open_close",
"urn:miot-spec-v2:property:water-pump:000000F2": "open_close",
"urn:miot-spec-v2:property:watering:000000CC": "open_close",
"urn:miot-spec-v2:property:wdr-mode:00000088": "open_close",
"urn:miot-spec-v2:property:wet:0000002A": "open_close",
"urn:miot-spec-v2:property:wifi-band-combine:000000E0": "open_close",
"urn:miot-spec-v2:property:anti-fake:00000130": "yes_no",
"urn:miot-spec-v2:property:arrhythmia:000000B4": "yes_no",
"urn:miot-spec-v2:property:card-insertion-state:00000106": "yes_no",
"urn:miot-spec-v2:property:delay:0000014F": "yes_no",
"urn:miot-spec-v2:property:driving-status:000000B9": "yes_no",
"urn:miot-spec-v2:property:local-storage:0000011E": "yes_no",
"urn:miot-spec-v2:property:motor-reverse:00000072": "yes_no",
"urn:miot-spec-v2:property:plasma:00000132": "yes_no",
"urn:miot-spec-v2:property:seating-state:000000B8": "yes_no",
"urn:miot-spec-v2:property:silent-execution:000000FB": "yes_no",
"urn:miot-spec-v2:property:snore-state:0000012A": "yes_no",
"urn:miot-spec-v2:property:submersion-state:0000007E": "yes_no",
"urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3": "yes_no",
"urn:miot-spec-v2:property:wind-reverse:00000117": "yes_no",
"urn:miot-spec-v2:property:motion-state:0000007D": "motion_state",
"urn:miot-spec-v2:property:contact-state:0000007C": "contact_state"
},
"translate": {
"default": {
"zh-Hans": {
"true": "真",
"false": "假"
},
"zh-Hant": {
"true": "真",
"false": "假"
},
"en": {
"true": "True",
"false": "False"
},
"de": {
"true": "Wahr",
"false": "Falsch"
},
"es": {
"true": "Verdadero",
"false": "Falso"
},
"fr": {
"true": "Vrai",
"false": "Faux"
},
"ru": {
"true": "Истина",
"false": "Ложь"
},
"ja": {
"true": "真",
"false": "偽"
}
},
"open_close": {
"zh-Hans": {
"true": "开启",
"false": "关闭"
},
"zh-Hant": {
"true": "開啟",
"false": "關閉"
},
"en": {
"true": "Open",
"false": "Close"
},
"de": {
"true": "Öffnen",
"false": "Schließen"
},
"es": {
"true": "Abierto",
"false": "Cerrado"
},
"fr": {
"true": "Ouvert",
"false": "Fermer"
},
"ru": {
"true": "Открыть",
"false": "Закрыть"
},
"ja": {
"true": "開く",
"false": "閉じる"
}
},
"yes_no": {
"zh-Hans": {
"true": "是",
"false": "否"
},
"zh-Hant": {
"true": "是",
"false": "否"
},
"en": {
"true": "Yes",
"false": "No"
},
"de": {
"true": "Ja",
"false": "Nein"
},
"es": {
"true": "Sí",
"false": "No"
},
"fr": {
"true": "Oui",
"false": "Non"
},
"ru": {
"true": "Да",
"false": "Нет"
},
"ja": {
"true": "はい",
"false": "いいえ"
}
},
"motion_state": {
"zh-Hans": {
"true": "有人",
"false": "无人"
},
"zh-Hant": {
"true": "有人",
"false": "無人"
},
"en": {
"true": "Motion Detected",
"false": "No Motion Detected"
},
"de": {
"true": "Bewegung erkannt",
"false": "Keine Bewegung erkannt"
},
"es": {
"true": "Movimiento detectado",
"false": "No se detecta movimiento"
},
"fr": {
"true": "Mouvement détecté",
"false": "Aucun mouvement détecté"
},
"ru": {
"true": "Обнаружено движение",
"false": "Движение не обнаружено"
},
"ja": {
"true": "動きを検知",
"false": "動きが検出されません"
}
},
"contact_state": {
"zh-Hans": {
"true": "接触",
"false": "分离"
},
"zh-Hant": {
"true": "接觸",
"false": "分離"
},
"en": {
"true": "Contact",
"false": "No Contact"
},
"de": {
"true": "Kontakt",
"false": "Kein Kontakt"
},
"es": {
"true": "Contacto",
"false": "Sin contacto"
},
"fr": {
"true": "Contact",
"false": "Pas de contact"
},
"ru": {
"true": "Контакт",
"false": "Нет контакта"
},
"ja": {
"true": "接触",
"false": "非接触"
}
}
}
}

View File

@ -0,0 +1,158 @@
{
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"en": {
"service:001": "Device Information",
"service:001:property:003": "Device ID",
"service:001:property:005": "Serial Number (SN)",
"service:002": "Gateway",
"service:002:event:001": "Network Changed",
"service:002:event:002": "Network Changed",
"service:002:property:001": "Access Method",
"service:002:property:001:valuelist:000": "Wired",
"service:002:property:001:valuelist:001": "5G Wireless",
"service:002:property:001:valuelist:002": "2.4G Wireless",
"service:002:property:002": "IP Address",
"service:002:property:003": "WiFi Network Name",
"service:002:property:004": "Current Time",
"service:002:property:005": "DHCP Server MAC Address",
"service:003": "Indicator Light",
"service:003:property:001": "Switch",
"service:004": "Virtual Service",
"service:004:action:001": "Generate Virtual Event",
"service:004:event:001": "Virtual Event Occurred",
"service:004:property:001": "Event Name"
},
"es": {
"service:001": "Información del dispositivo",
"service:001:property:003": "ID del dispositivo",
"service:001:property:005": "Número de serie (SN)",
"service:002": "Puerta de enlace",
"service:002:event:001": "Cambio de red",
"service:002:event:002": "Cambio de red",
"service:002:property:001": "Método de acceso",
"service:002:property:001:valuelist:000": "Cableado",
"service:002:property:001:valuelist:001": "5G inalámbrico",
"service:002:property:001:valuelist:002": "2.4G inalámbrico",
"service:002:property:002": "Dirección IP",
"service:002:property:003": "Nombre de red WiFi",
"service:002:property:004": "Hora actual",
"service:002:property:005": "Dirección MAC del servidor DHCP",
"service:003": "Luz indicadora",
"service:003:property:001": "Interruptor",
"service:004": "Servicio virtual",
"service:004:action:001": "Generar evento virtual",
"service:004:event:001": "Ocurrió un evento virtual",
"service:004:property:001": "Nombre del evento"
},
"fr": {
"service:001": "Informations sur l'appareil",
"service:001:property:003": "ID de l'appareil",
"service:001:property:005": "Numéro de série (SN)",
"service:002": "Passerelle",
"service:002:event:001": "Changement de réseau",
"service:002:event:002": "Changement de réseau",
"service:002:property:001": "Méthode d'accès",
"service:002:property:001:valuelist:000": "Câblé",
"service:002:property:001:valuelist:001": "Sans fil 5G",
"service:002:property:001:valuelist:002": "Sans fil 2.4G",
"service:002:property:002": "Adresse IP",
"service:002:property:003": "Nom du réseau WiFi",
"service:002:property:004": "Heure actuelle",
"service:002:property:005": "Adresse MAC du serveur DHCP",
"service:003": "Voyant lumineux",
"service:003:property:001": "Interrupteur",
"service:004": "Service virtuel",
"service:004:action:001": "Générer un événement virtuel",
"service:004:event:001": "Événement virtuel survenu",
"service:004:property:001": "Nom de l'événement"
},
"ru": {
"service:001": "Информация об устройстве",
"service:001:property:003": "ID устройства",
"service:001:property:005": "Серийный номер (SN)",
"service:002": "Шлюз",
"service:002:event:001": "Сеть изменена",
"service:002:event:002": "Сеть изменена",
"service:002:property:001": "Метод доступа",
"service:002:property:001:valuelist:000": "Проводной",
"service:002:property:001:valuelist:001": "5G Беспроводной",
"service:002:property:001:valuelist:002": "2.4G Беспроводной",
"service:002:property:002": "IP Адрес",
"service:002:property:003": "Название WiFi сети",
"service:002:property:004": "Текущее время",
"service:002:property:005": "MAC адрес DHCP сервера",
"service:003": "Световой индикатор",
"service:003:property:001": "Переключатель",
"service:004": "Виртуальная служба",
"service:004:action:001": "Создать виртуальное событие",
"service:004:event:001": "Произошло виртуальное событие",
"service:004:property:001": "Название события"
},
"de": {
"service:001": "Geräteinformationen",
"service:001:property:003": "Geräte-ID",
"service:001:property:005": "Seriennummer (SN)",
"service:002": "Gateway",
"service:002:event:001": "Netzwerk geändert",
"service:002:event:002": "Netzwerk geändert",
"service:002:property:001": "Zugriffsmethode",
"service:002:property:001:valuelist:000": "Kabelgebunden",
"service:002:property:001:valuelist:001": "5G Drahtlos",
"service:002:property:001:valuelist:002": "2.4G Drahtlos",
"service:002:property:002": "IP-Adresse",
"service:002:property:003": "WiFi-Netzwerkname",
"service:002:property:004": "Aktuelle Zeit",
"service:002:property:005": "DHCP-Server-MAC-Adresse",
"service:003": "Anzeigelampe",
"service:003:property:001": "Schalter",
"service:004": "Virtueller Dienst",
"service:004:action:001": "Virtuelles Ereignis erzeugen",
"service:004:event:001": "Virtuelles Ereignis aufgetreten",
"service:004:property:001": "Ereignisname"
},
"ja": {
"service:001": "デバイス情報",
"service:001:property:003": "デバイスID",
"service:001:property:005": "シリアル番号 (SN)",
"service:002": "ゲートウェイ",
"service:002:event:001": "ネットワークが変更されました",
"service:002:event:002": "ネットワークが変更されました",
"service:002:property:001": "アクセス方法",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G ワイヤレス",
"service:002:property:001:valuelist:002": "2.4G ワイヤレス",
"service:002:property:002": "IPアドレス",
"service:002:property:003": "WiFiネットワーク名",
"service:002:property:004": "現在の時間",
"service:002:property:005": "DHCPサーバーMACアドレス",
"service:003": "インジケータライト",
"service:003:property:001": "スイッチ",
"service:004": "バーチャルサービス",
"service:004:action:001": "バーチャルイベントを生成",
"service:004:event:001": "バーチャルイベントが発生しました",
"service:004:property:001": "イベント名"
},
"zh-Hant": {
"service:001": "設備信息",
"service:001:property:003": "設備ID",
"service:001:property:005": "序號 (SN)",
"service:002": "網關",
"service:002:event:001": "網路發生變化",
"service:002:event:002": "網路發生變化",
"service:002:property:001": "接入方式",
"service:002:property:001:valuelist:000": "有線",
"service:002:property:001:valuelist:001": "5G 無線",
"service:002:property:001:valuelist:002": "2.4G 無線",
"service:002:property:002": "IP地址",
"service:002:property:003": "WiFi網路名稱",
"service:002:property:004": "當前時間",
"service:002:property:005": "DHCP伺服器MAC地址",
"service:003": "指示燈",
"service:003:property:001": "開關",
"service:004": "虛擬服務",
"service:004:action:001": "產生虛擬事件",
"service:004:event:001": "虛擬事件發生",
"service:004:property:001": "事件名稱"
}
}
}

View File

@ -0,0 +1,63 @@
{
"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1": {
"services": [
"5"
]
},
"urn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01": {
"services": [
"4",
"7",
"8"
],
"properties": [
"5.1"
]
},
"urn:miot-spec-v2:device:light:0000A001:philips-strip3": {
"services": [
"1",
"3"
],
"properties": [
"2.2"
]
},
"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1": {
"events": [
"2.1"
]
},
"urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1": {
"services": [
"1",
"5"
]
},
"urn:miot-spec-v2:device:light:0000A001:yeelink-color2": {
"properties": [
"3.*",
"2.5"
]
},
"urn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2": {
"services": [
"3"
]
},
"urn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3": {
"services": [
"3"
]
},
"urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4": {
"services": [
"10"
],
"properties": [
"9.*",
"13.*",
"15.*"
]
}
}

View File

@ -0,0 +1,392 @@
# -*- 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.
Conversion rules of MIoT-Spec-V2 instance to Home Assistant entity.
"""
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.event import EventDeviceClass
# pylint: disable=pointless-string-statement
"""SPEC_DEVICE_TRANS_MAP
{
'<device instance name>':{
'required':{
'<service instance name>':{
'required':{
'properties': {
'<property instance name>': set<property access: str>
},
'events': set<event instance name: str>,
'actions': set<action instance name: str>
},
'optional':{
'properties': set<property instance name: str>,
'events': set<event instance name: str>,
'actions': set<action instance name: str>
}
}
},
'optional':{
'<service instance name>':{
'required':{
'properties': {
'<property instance name>': set<property access: str>
},
'events': set<event instance name: str>,
'actions': set<action instance name: str>
},
'optional':{
'properties': set<property instance name: str>,
'events': set<event instance name: str>,
'actions': set<action instance name: str>
}
}
},
'entity': str
}
}
"""
SPEC_DEVICE_TRANS_MAP: dict[str, dict | str] = {
'humidifier': {
'required': {
'humidifier': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {'mode', 'target-humidity'}
}
}
},
'optional': {
'environment': {
'required': {
'properties': {
'relative-humidity': {'read', 'write'}
}
}
}
},
'entity': 'humidifier'
},
'dehumidifier': {
'required': {
'dehumidifier': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {'mode', 'target-humidity'}
}
},
},
'optional': {
'environment': {
'required': {
'properties': {
'relative-humidity': {'read', 'write'}
}
}
}
},
'entity': 'dehumidifier'
},
'vacuum': {
'required': {
'vacuum': {
'required': {
'actions': {'start-sweep', 'stop-sweeping'},
},
'optional': {
'properties': {'status', 'fan-level'},
'actions': {
'pause-sweeping',
'continue-sweep',
'stop-and-gocharge'
}
},
}
},
'optional': {
'identify': {
'required': {
'actions': {'identify'}
}
},
'battery': {
'required': {
'properties': {
'battery-level': {'read'}
},
}
},
},
'entity': 'vacuum'
},
'air-conditioner': {
'required': {
'air-conditioner': {
'required': {
'properties': {
'on': {'read', 'write'},
'mode': {'read', 'write'},
'target-temperature': {'read', 'write'}
}
},
'optional': {
'properties': {'target-humidity'}
},
}
},
'optional': {
'fan-control': {
'required': {},
'optional': {
'properties': {
'on',
'fan-level',
'horizontal-swing',
'vertical-swing'}}
},
'environment': {
'required': {},
'optional': {
'properties': {'temperature', 'relative-humidity'}
}
},
'air-condition-outlet-matching': {
'required': {},
'optional': {
'properties': {'ac-state'}
}
}
},
'entity': 'climate'
},
'air-condition-outlet': 'air-conditioner'
}
"""SPEC_SERVICE_TRANS_MAP
{
'<service instance name>':{
'required':{
'properties': {
'<property instance name>': set<property access: str>
},
'events': set<event instance name: str>,
'actions': set<action instance name: str>
},
'optional':{
'properties': set<property instance name: str>,
'events': set<event instance name: str>,
'actions': set<action instance name: str>
},
'entity': str
}
}
"""
SPEC_SERVICE_TRANS_MAP: dict[str, dict | str] = {
'light': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {
'mode', 'brightness', 'color', 'color-temperature'
}
},
'entity': 'light'
},
'indicator-light': 'light',
'ambient-light': 'light',
'night-light': 'light',
'white-light': 'light',
'fan': {
'required': {
'properties': {
'on': {'read', 'write'},
'fan-level': {'read', 'write'}
}
},
'optional': {
'properties': {'mode', 'horizontal-swing'}
},
'entity': 'fan'
},
'fan-control': 'fan',
'ceiling-fan': 'fan',
'water-heater': {
'required': {
'properties': {
'on': {'read', 'write'}
}
},
'optional': {
'properties': {'on', 'temperature', 'target-temperature', 'mode'}
},
'entity': 'water_heater'
},
'curtain': {
'required': {
'properties': {
'motor-control': {'write'}
}
},
'optional': {
'properties': {
'motor-control', 'status', 'current-position', 'target-position'
}
},
'entity': 'cover'
},
'window-opener': 'curtain'
}
"""SPEC_PROP_TRANS_MAP
{
'entities':{
'<entity name>':{
'format': set<str>,
'access': set<str>
}
},
'properties': {
'<property instance name>':{
'device_class': str,
'entity': str
}
}
}
"""
SPEC_PROP_TRANS_MAP: dict[str, dict | str] = {
'entities': {
'sensor': {
'format': {'int', 'float'},
'access': {'read'}
},
'switch': {
'format': {'bool'},
'access': {'read', 'write'}
}
},
'properties': {
'temperature': {
'device_class': SensorDeviceClass.TEMPERATURE,
'entity': 'sensor'
},
'relative-humidity': {
'device_class': SensorDeviceClass.HUMIDITY,
'entity': 'sensor'
},
'air-quality-index': {
'device_class': SensorDeviceClass.AQI,
'entity': 'sensor'
},
'pm2.5-density': {
'device_class': SensorDeviceClass.PM25,
'entity': 'sensor'
},
'pm10-density': {
'device_class': SensorDeviceClass.PM10,
'entity': 'sensor'
},
'pm1': {
'device_class': SensorDeviceClass.PM1,
'entity': 'sensor'
},
'atmospheric-pressure': {
'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,
'entity': 'sensor'
},
'tvoc-density': {
'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
'entity': 'sensor'
},
'voc-density': 'tvoc-density',
'battery-level': {
'device_class': SensorDeviceClass.BATTERY,
'entity': 'sensor'
},
'voltage': {
'device_class': SensorDeviceClass.VOLTAGE,
'entity': 'sensor'
},
'illumination': {
'device_class': SensorDeviceClass.ILLUMINANCE,
'entity': 'sensor'
},
'no-one-determine-time': {
'device_class': SensorDeviceClass.DURATION,
'entity': 'sensor'
},
'has-someone-duration': 'no-one-determine-time',
'no-one-duration': 'no-one-determine-time'
}
}
"""SPEC_EVENT_TRANS_MAP
{
'<event instance name>': str
}
"""
SPEC_EVENT_TRANS_MAP: dict[str, str] = {
'click': EventDeviceClass.BUTTON,
'double-click': EventDeviceClass.BUTTON,
'long-press': EventDeviceClass.BUTTON,
'motion-detected': EventDeviceClass.MOTION,
'no-motion': EventDeviceClass.MOTION,
'doorbell-ring': EventDeviceClass.DOORBELL
}
SPEC_ACTION_TRANS_MAP = {
}

View File

@ -0,0 +1,281 @@
# -*- 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 redirect web pages.
"""
def oauth_redirect_page(lang: str, status: str) -> 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&amp;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>
'''