cyr-ius / aiosysbus

Manage your Livebox in Python
GNU General Public License v3.0
3 stars 7 forks source link

Mode Asyncrhone - AIOSYSBUS #39

Closed cyr-ius closed 8 months ago

cyr-ius commented 8 months ago
          Du coup, je te propose qu'on bascule nos échanges sur le repo aiosysbus.

https://github.com/cyr-ius/aiosysbus

De là , je fais une nouvelle branche que j'appel asyncio , je prépare l'api en mode asynchrone et un fichier example complet.

Ensuite , il te suffirait de lancer un python3.11 dans un venv , de faire un pip -r requirements.txt et de jouer le fichier example.py Si il ne plante pas. On est bon , on a une API opérationnelle. On pourrait ainsi l'utiliser dans l'addon Livebox de Home Assistant

Originally posted by @cyr-ius in https://github.com/cyr-ius/hass-livebox-component/issues/89#issuecomment-1879671536

cyr-ius commented 8 months ago

@brenard , cette issue pour continuer nos échanges

epenet commented 8 months ago

Concernant Asyncio, avez-vous vu ma PR #18 ?

cyr-ius commented 8 months ago

Concernant Asyncio, avez-vous vu ma PR #18 ?

Oui je l'ai vu depuis longtemps sauf que je n'ai plus de livebox pour tester. Par contre @brenard c'est proposer de faire des tests , si tu es aussi OK , on peut tester à trois.

Je préfère utiliser aiohttp au lieu de htpx , que je trouve bcp plus performant. Du coup , je viens de pousser une branche nommé asyncio. J'ai refondu tout la partie des appels vers la box en intégrant une gestion d'erreur plus fine. Je me suis aussi assuré de toujours recevoir un dictionaire en retour des appels qui ont été émis avec succès.

Il faut tester , normalement j'ai préparer un fichier d'exemple , il suffit de mettre le mot de passe et tester. Au passage , j'ai aussi implémenter une possibilité d'appeler le livebox sur un autre port et d'activer le TLS mais sans garantie.

Si vous être preneur , tout est là https://github.com/cyr-ius/aiosysbus/tree/asyncio

cyr-ius commented 8 months ago

je voudrais mocker les appels , je suis preneur de retour que peux faire le fichier example afin de construire un jeu de tests

Le fichier access.py a été remplacer par auth.py. Conserver pour comparer en cas de besoin

brenard commented 8 months ago

Premier test : échec :face_with_head_bandage:

2024-01-07 19:06:35,215 - asyncio - DEBUG - Using selector: EpollSelector
2024-01-07 19:06:35,252 - aiohttp.client - WARNING - Can not load response cookies: Illegal key '264377da/accept-language'
2024-01-07 19:06:35,282 - aiohttp.client - WARNING - Can not load response cookies: Illegal key '264377da/sessid'
2024-01-07 19:06:35,282 - aiosysbus.auth - DEBUG - Response headers: <CIMultiDictProxy('Set-Cookie': '264377da/sessid=8dHaacDun8QthmHx7k170IFf; path=/; SameSite=Strict; HttpOnly', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'TE': 'chunked', 'Transfer-Encoding': 'chunked', 'Content-Type': 'application/x-sah-ws-1-call+json', 'X-Content-Type-Options': 'nosniff')>
2024-01-07 19:06:35,283 - root - ERROR - Unexpected response , content-type incorrect (application/x-sah-ws-1-call+json)

Note: petit patch nécessaire pour avoir ce retour avec les headers de la réponse et un message d'erreur correctement formaté :

diff --git a/aiosysbus/auth.py b/aiosysbus/auth.py
index 7732fa0..2690131 100644
--- a/aiosysbus/auth.py
+++ b/aiosysbus/auth.py
@@ -174,10 +174,11 @@ class Auth:
                 "Error occurred while communicating with Livebox."
             ) from error

+        _LOGGER.debug("Response headers: %s", response.headers)
         content_type = response.headers.get("Content-Type", "")
         if content_type not in CONTENT_TYPES:
             raise UnexpectedResponse(
-                "Unexpected response , content-type incorrect (%s)", content_type
+                f"Unexpected response , content-type incorrect ({content_type})"
             )

         if (response.status // 100) in [4, 5]:
cyr-ius commented 8 months ago

J ai corrigé, le retour. Après dans l ancien code je vérifiais pas le retour du content-type. De ce coté , si le contrôle de retour est trop exigeant on pourra le bypasser. En attendant j ai corrigé et ajouter le content type dans le tableau CONTENT-TYPE Pas hésiter à faire un PR, je les validerais sur la branche asyncio A tester

epenet commented 8 months ago

Sur mon poste, il faut ajouter le content-type "application/json; charset=UTF-8":

--- a/aiosysbus/auth.py
+++ b/aiosysbus/auth.py
@@ -24,6 +24,7 @@ CONTENT_TYPES = [
     "application/x-sah-ws-1-call+json",
     "application/x-sah-ws-1-call+json; charset=UTF-8",
     "application/json",
+    "application/json; charset=UTF-8",
 ]

Une fois le content-type ajouté, j'ai moi aussi le problème avec les cookies qui sont refusés.

2024-01-08 08:12:58,074 - aiohttp.client - WARNING - Can not load response cookies: Illegal key 'e2c29097/accept-language'
2024-01-08 08:12:58,074 - aiosysbus.auth - DEBUG - Challenge headers: <CIMultiDictProxy('Cache-Control': 'must-revalidate', 'Set-Cookie': 'e2c29097/accept-language=; path=/; SameSite=Strict', 'Etag': 'e2c29097', 'X-Frame-Options': 'SAMEORIGIN', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'X-Content-Type-Options': 'nosniff', 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'must-revalidate', 'Content-Length': '1449')>

On dirait que aiohttp n'aime pas le "/" dans le nom du cookie.

epenet commented 8 months ago

OK, donc il y a plusieurs soucis (en plus du CONTENT_TYPES):

Tout d'abord, aiohttp n'accepte pas le "/" dans le nom du cookie donc ça pose problème. J'ai réussi à bypass avec le code suivant en haut de "example.py"

import sys

if "http" in sys.modules:
    raise ImportError("Crawler must be imported before http module")
import http.cookies

http.cookies._is_legal_key = lambda _: True

Deuxième problème, par défaut, le CookieJar fournit par aiohttp n'accepte pas les adresses IP et il faut passer unsafe=True dans le constructeur:

        self._session = session
        if not self._session:
            jar = CookieJar(unsafe=True)
            self._session = ClientSession(cookie_jar=jar)

Troisième problème - si on cherche à générer un deuxième token (ça semble être toujours le cas actuellement) la livebox renvoie un Set-Cookie pour vider/recréer le cookie sessid que aiohttp ne comprends pas. Il vaut mieux vider le CookieJar c'est que la réponse de la livebox vide et recrée dans la même header le cookie sessid:

2024-01-08 10:14:03,757 - aiosysbus.auth - DEBUG - RESPONSE HEADERS: <CIMultiDictProxy('Set-Cookie': 'e2c29097/sessid=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; SameSite=Strict', 'Set-Cookie': 'e2c29097/sessid=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/; SameSite=Strict', 'Set-Cookie': 'e2c29097/sessid=Ah+xeWOLvJxMo6e+mN2SvSVy; path=/; SameSite=Strict; HttpOnly', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'TE': 'chunked', 'Transfer-Encoding': 'chunked', 'Content-Type': 'application/x-sah-ws-1-call+json', 'X-Content-Type-Options': 'nosniff')>
        # Get challenge from API
        self.session.cookie_jar.clear_domain(str(self.base_url))
        await self._async_get_challenge(self.base_url)
cyr-ius commented 8 months ago

Ok , vous avez fait un super taff. C est là qu on voit que maintenir le code sans livebox c est pas simple Vous êtes tombé dans le truc que je redoutais. le cookie du challenge de départ qui fonctionne tout seul dans le mode requests et touch'y sur aiohttp . Merci @epenet pour avoir persévéré à vouloir garder aiohttp d autre aurait changer de package

Du coup, j ai mergé dans le code dans l ordre proposer par @brenard 40,43,45,46 . Aucun soucis , pas de conflit. A le lecture je vois pas de coquille. Maintenant je vous laisse tester la bête

cyr-ius commented 8 months ago

La petite classe IgnoreIllegalKeyFilter pour intercepté le warning de aiohttp, c le MustHave. Celle là, je l avais pas. Ca rigole plus.🤘

cyr-ius commented 8 months ago

@epenet ,@brenard , pouvez vous me dire si cela fonctionne ? J'ai mergé votre code.

brenard commented 8 months ago

@epenet ,@brenard , pouvez vous me dire si cela fonctionne ? J'ai mergé votre code.

Je viens de tester et ça fonctionne comme attendu.

Reste que j'ai commenté quelques trucs dans ton fichier de tests qui marchait pas, soit parce que la Livebox retournait une erreur, soit parce que je voyais pas quelle requête ou quel test tu cherchais à faire. J'ai systématiquement mis l'erreur que j'avais en commentaire avec mon analyse. Si tu peut faire le tour sur celles-ci et m'en dire plus, peut-être qu'on pourra en corriger quelques unes.

PS : si je comprends bien, la finalité initiale que tu visais, c'est d'utilisé ce mode async dans ton extension Home Assistant pour pouvoir se passer du bridge (ou du moins d'y faire des appels asynchrones sans "adapteur") ? Tu as besoin d'aide sur ce point ?

cyr-ius commented 8 months ago

Effectivement c'est l'objectif. Dans mon fichier d'exemple , j'ai toujours eu quelques requêtes inopérantes car je ne maitriaisais pas les paramètres attendus. Faire du reverse fait que j'ai pas toujours su déterminer comment appeler certains API. J'ai regarder le fichier d'exemple et effectivement je penses que pour celles ou tu as noté des erreurs , l'api attend des paramètres que nous passons pas. Pour l'exception de la ligne 76. Mon exemple sortait simplement quelques éléments de la variable "hosts" de la ligne 66 et j'iterais sur celle-ci . Mais il est bien possible qu'il faille utilisé l'exemple de la ligne 63 pour que l'itération de ligne 87 fonctionne.

Du coup , je vais pousser notre nouvelle version Aiosysbus dans pypi pour effectivement refondre l'addon HA car depuis on a fait un bond côté HA sur la façon de coder l'addon et cela sera certainement plus efficient.

Du coup, comme on a forké aiosysbus avec une branch asyncio. Je vais forké le dépot cyr-ius/hass-livebox-component avec une branch asyncio.

Recoder l'ensemble me pose pas de soucis, après comme d'habitude il faudra tester le nouveau module et proposer des PR si cela ne fonctionne pas. Donc aucun pbl , pour coder à plusieurs.

Je pensais effectivement me débarrser complètement du bridge. Garder un coordinateur qui récupère ce qu'il a besoin et ensuite injecter cela dans les différentes entités en s'appuyant sur la classe SensorDescription pour être plus efficace. Tout en essayant de garder la retro compatibilité pour éviter ques ls gens ne voyent apparaitrent des trucs nouveaux ou casser dans HA

cyr-ius commented 8 months ago

Merci pour votre contribution.