Aller au contenu
Lazer

Utilisation de net.HTTPClient() asynchrone dans une scène

Recommended Posts

Utilisation de net.HTTPClient() asynchrone

 

- dans une scène en LUA sur HC2/HC3 -

- dans un QuickApp sur HC3 -

 

 

Dans les scènes, Fibaro ne nous laisse pas le choix, dès que l'on veut faire des appels HTTP, on est obligé d'utiliser la fonction asynchronse net.HTTPClient().

La fonction Net.FHTTP() synchrone utilisée dans les VD n'est pas disponible dans les scènes.

Toutefois, l'avantage de net.HTTPClient() est d'accepter les connexions sécurisées HTTPS devenues majoritaires sur Internet.

 

Exemple de code simple pour une requête de type GET

local http = net.HTTPClient()
http:request("http://1.2.3.4/url", {
	success = function(response)
		if response.status == 200 then
			print('OK, réponse : '.. response.data)
		else
			print("Erreur : status=" .. tostring(response.status))
		end
	end,
	error = function(err)
		print("Erreur : " .. err)
	end,
	options = {
		method = 'GET'
	}
})

 

L'exemple suivant effectue une requête de type POST permettant d'envoyer des données vers le site distant. De plus, la fonction success() récupère les données de type JSON en vue d'un traitement ultérieur (notez que les données envoyées vers le site Web et les données reçues depuis le site Web sont différentes, cela dépend de l'application qui tourne sur le site) :

-- Les données à envoyer au formulaire
local myJson = {
	"couleurs": {
		[1] = "bleu",
		[2] = "blanc",
		[3] = "rouge"
	},
	"fruits": {
		[1] = "pomme",
		[2] = "banane"
	},
}
-- Appel HTTPS
local http = net.HTTPClient()
http:request("https://www.domaine.com/url", {
	success = function(response)
		if response.status == 200 then
			if response.data and response.data ~= "" then
				print('Retour : '.. response.data)
				local jsonTable = json.decode(response.data)
				-- Parcours de la table JSON
				local k, v
				for k, v in pairs(jsonTable) do
					print("key = " .. k .. " - type(v) = " .. type(v))
				end
				-- Ici la suite du code, exécuté en asynchrone, donc après la fin de l'exécution du code appelant http:request()
				-- ...
			else
				print("Error : empty response data")
			end
		else
			print("Erreur : status=" .. tostring(response.status))
		end
	end,
	error = function(err)
		print("Erreur : " .. err)
	end,
	options = {
		method = 'POST',
		timeout = 5000,
		checkCertificate = false,
		headers = {
			["content-type"] = 'application/x-www-form-urlencoded;',
			["Authorization"] = "Basic YWRtaW46cGFzc3dvcmQ=" -- username:password encodé en Base64 (admin:password)
		},
		data = json.encode(myJson)
	}
})
-- Ici la suite du code, exécuté en synchrone, donc avant l'exécution du contenu de la fonction success()
-- ...

 

On remarque dans les options que l'on peut choisir les paramètres suivants :

  • method : obligatoire : GET ou PUT ou POST ou DELETE
  • timeout : facultatif : délai d'attente en millisecondes avant échec de la requête. Peut être utile avec certains serveurs un peu trop lents à répondre. Dans le doute, inutile d'utiliser ce paramètre.
  • checkCertificate : facultatif : true ou false, permet d'ignorer les alertes de sécurité sur les certificats auto-signés (non reconnus pas une autorité de certification approuvée)
  • headers : facultatif : permet de passer le(s) en-tête(s) HTTP de son choix vers le site Web distant. Si vous ne savez pas ce qu'est un Header, c'est que vous n'avez probablement pas besoin d'envoyer de header, donc ignorez ce paramètre.
  • data : facultatif : ce sont les données à envoyer dans les formulaires POST et PUT sous forme de chaine de caractères. Donc si les données sont de type tableau JSON, il faut les encoder avec json.encode().

 

 

Asynchronisme

 

net.HTTPClient() est asynchrone, le code dans les fonctions success() et error() appelées en callback s'exécute toujours après la fin de l'exécution du thread principal.

 

Quand on commence à programmer en asynchrone, il ne faut plus jamais utiliser de fonctions synchrones comme sleep(), sous peine de comportement surprenant.

Préférer à la place l'emploi de la fonction settimout() qui est elle-même asynchrone (chercher les exemples sur le forum)

 

La bonne pratique quand on programme en asynchrone est la suivante :

Après un appel à net.HTTPClient(), le code devrait se terminer le plus rapidement possible afin de laisser la main à la fonction success() appelée en callback de net.HTTPClient().

La suite du code se déroule donc dans la fonction success().

Celle-ci, à sont tour, peut faire d'autres appels à net.HTTPClient() ou settimeout() pour déclencher de nouveaux appels de fonctions en callback asynchrone.

Etc...

 

C'est la technique que j'ai employé dans mes scènes Watchdog et Yamaha MusicCast, partagées sur le forum.

 

C'est une certaine gymnastique qui n'est pas évidente au début, et oblige à revoir toute la structure de son code LUA.

 

 

 

En complément je vous invite à lire ce sujet sur la protection des requêtes http avec pcall() :

 

 

 

Modifié par Lazer
  • Like 9
  • Thanks 1

Partager ce message


Lien à poster
Partager sur d’autres sites

Ah c'est cool comme mini tuto ça... je bookmark :D

Partager ce message


Lien à poster
Partager sur d’autres sites

Ahah tu ne peux pas parler à Alexa alors hop un petit tuto

Merci c est top :-)

Envoyé de mon BND-L21 en utilisant Tapatalk

Partager ce message


Lien à poster
Partager sur d’autres sites

merci pour le tuto,

 

Petite question , si je récupère une donné avec un GET : 

 

                local etatAlyssa = jsonTable.zones[1].state

 

pour la modifier dois je utiliser la meme structure que le GET ?

 

local myJson = { jsonTable.zones[1].state = "1" }

 

 

merci

 

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Je ne comprends pas bien ta question ?!

Partager ce message


Lien à poster
Partager sur d’autres sites

désolé, pour etre plus clair, je souhaite comprendre le fonctionnent,

 

j'utilise pour récupérer des info  un GET et un element de la table :

local etatAlyssa = jsonTable.zones[1].state

 

Si je souhaite modifier la valeur de l'élément et lui attribuer la valeur 1 ou 0   =>  state = 1  

j'utilise donc la seconde parti de ton code avec POST et la valeur dans le myJson  EX :

 

local myJson = {
	"couleurs": {
		[1] = "bleu",
		[2] = "blanc",
		[3] = "rouge"
	},
	"fruits": {
		[1] = "pomme",
		[2] = "banane"
	},
}

 

donc pour mon cas je dois utiliser la même structure qu'avec GET ? ou il faut adapter comme dans ton ex avec les accolades :

 jsonTable.zones[1].state

 

jsonTable.zones[1].state

 

modif :

{"zones":[{"state":"0"}]}

 

 

 

ou peut etre qu'il n' a aucun rapport entre un GET et POST...

Modifié par Bloug

Partager ce message


Lien à poster
Partager sur d’autres sites

Euh... mais en fait tu récupère un JSON via une requête GET, tu veux modifier une valeur, et le réinjecter dans une requête POST, c'est ça ?

Partager ce message


Lien à poster
Partager sur d’autres sites

OK
Donc il faut que tu imbriques les 2 fonctions l'une dans l'autre (en vertu de ce qui a été dit... le code DOIT continuer dans la fonction success()

 

Quand tu décodes le tableau dans la première fonction success() :

				local jsonTable = json.decode(response.data)

Juste après tu modifies la valeur :

				jsonTable.zones[1].state = "1"

Puis tu donnes l'ensemble (avec json.encode()) dans les options de ton second appel http :

	options = {
		method = 'POST',
		data = json.encode(jsonTable)
	}
})

 

Partager ce message


Lien à poster
Partager sur d’autres sites

merci :) , je regarde immediatement !

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Bon après beaucoup de tests avec GET puis POST  puis qu'avec POST et une erreur 401 j'ai ajouter des éléments dans le  headers pour bloqué sur du 400 ...

Comme le souligne le grand philosophe espagnol : Mas banda ! 

j'arrêter de polluer le topic ....

 

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Partage ton code et un maximum de détail, les logs, etc

Tu peux faire ça dans ton topic d'origine.

  • Thanks 1

Partager ce message


Lien à poster
Partager sur d’autres sites

Bonsoir,

un grand merci pour ce post qui pour moi est d'une grande utilité car je ne pars de 0 sur le code...

 

J'ai passé bcp d'heures a tester de nombreuses possibilités mais j'ai toujours un message d'erreur.

 

Je cherche a envoyer une simple requête HTTP pour contourner un problème de compatibilité avec mon synology (maj DSM7)

L'idée c'est de pouvoir déclencher une action préconfigurée dans  surveillance station depuis une HC3 via une requête HTTPS

 

En clair la HC3 remonte un mouvement PIR et pousse la requête HTTPS qui déclenche l'action préconfigurée dans surveillance station.

 

le requête est la suivante :

https://ADRESSEipSYNOLOGY/webapi/entry.cgi?api=SYNO.SurveillanceStation.ExternalEvent&method="Trigger"&version=1&eventId=1&eventName="This is external event1"&account="{account}"&password="{password}"

 

la requête fonctionne dans Firefox, j'ai le bon retour "success" et l'action préconfigurée fonctionne mais via ma scene lua j'ai trs une erreur.

la dernière en date ;

[01.03.2021] [22:32:59] [ERROR] [SCENE96]: (load):2: ')' expected near 'Trigger'

 

ma scene lua ;

 

en déclaration j'ai ;

{
  conditions = { {
      id = 216,
      isTrigger = true,
      operator = "==",
      property = "value",
      type = "device",
      value = true
    } },
  operator = "all"
}

 

en action j'ai ;

local http = net.HTTPClient()
http:request("https://ADRESSEipSYNOLOGY/webapi/entry.cgi?api=SYNO.SurveillanceStation.ExternalEvent&method="Trigger"&version=1&eventId=1&eventName="This is external event1"&account="MONLOGIN"&password="MONPASSWORD"", {
	success = function(response)
		if response.status == 200 then
			fibaro:debug('OK, réponse : '.. response.data)
		else
			fibaro:debug("Erreur : status=" .. tostring(response.status))
		end
	end,
	error = function(err)
		fibaro:debug("Erreur : " .. err)
	end,
	options = {
		method = 'GET'
	}
})

 

si quelqu'un pouvais m'aider ca serai vraiment cool car je ne m'en sort pas.

J'imagine bien que ca doit paraitre hyper simple mais je ne suis vraiment pas doué.

 

Merci à vous !

 

 

Modifié par jucom
modif post lua

Partager ce message


Lien à poster
Partager sur d’autres sites

Pour coller ton code dans le forum, il faut utiliser les balises </> dans la barre au dessus de la zone d'édition :

 

image.png.7fbb126ee5b1b3ceb3c0efe06853c57b.png

 

Puis choisir LUA dans la liste pour obtenir la coloration du code :

 

image.png.2aaa82f59fd7b5e0c9e0fa9002c3ff25.png

 

 

Car en l'état, c'est illisible, et il manque les numéros de ligne pour se repérer par rapport aux erreurs qui te sont remontées.

 

 

Par ailleurs, sans même tenter de lire ton code, il y a un caractère étrange qui me saute aux yeux.

Pas sûr que ça soit normal, et en tout cas l'interpréteur LUA n'en voudra pas si tu l'as collé tel quel sur la box :

 

image.png.24e9a033aca32fe69ec614ed9aeae9e8.png

 

Partager ce message


Lien à poster
Partager sur d’autres sites

ok sorry, c'est noté.

j'ai modif mon post.

Partager ce message


Lien à poster
Partager sur d’autres sites

je commence a comprendre que le problème c'est la requête HTTPS en elle même car elle contient des " un peu partout.

Elle n'est pas prise en compte dans son intégralité et du coup le reste de la requête génère des erreurs.

je continu a chercher mais je ne suis pas certain d’être capable de faire en sorte que net.HTTPClient() puisse répondre à mon besoin si tenté que ça soit possible.

je vais créer un post à part à part pour ne pas polluer ici.

Partager ce message


Lien à poster
Partager sur d’autres sites

Soit un QA contenant x fonctions avec des appel HTTP différents
Ces fonctions étant appelées (aléatoire) par d'autres QA

Qu'elle est la bonne écriture le type 1 ou le type 2  ( ou aucune des 2 )

Je n'ai pas trouvé le net.HTTPClient() dans NetWork Monitor !!

 

QA Ecriture type 1
-------------------
http = net.HTTPClient()
QuickApp:fonction1 ()
	http:request(
	  code...
	)
end
QuickApp:fonction2 ()
	http:request(
	  code... 
	)
end
QuickApp:fonction2 ()
	http:request(
	  code... 
	)
end
QA Ecriture type 2
---------------------
QuickApp:fonction1 ()
	http = net.HTTPClient()
	http:request(
	  code ...
	)
end
QuickApp:fonction2 ()
	http = net.HTTPClient()
	http:request(
	  code... 
	)
end
QuickApp:fonction2 ()
	http = net.HTTPClient()
	http:request(
	  code...
	)
end

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Pas sûr de moi, mais à priori utiliser une seule variable HTTPClient pour l'ensemble du QuickApp est valable, donc l'option n°1.

Cela dit l'option 2 est valable aussi, mais tu vas redéfinir un nouvel objet à chaque fois, donc ça prend du temps à initialiser, ça consomme plus de RAM, etc. De mémoire il me semble que c'est cette dernières option que j'utilise dans Network Monitor (mais c'est le seul, tous mes autres QuickApps ont un seul objet HTTPClient global)

 

Cependant, attention il y a une (grosse) erreur dans ton code dans l'option 2
Tu définis ta variable ainsi au sein de chaque fonction :

http = net.HTTPClient()

Donc... http est global !

Si bien qu'à chaque appel de l'une des autres fonctions, tu redéfinies... la même variable ! Bref, du travail inutile.

 

Autre chose, de façon générale, il faut éviter les variables globales :

- on ne maitrise pas leur portée, elle peuvent entrer en conflit avec d'autres variables du même nom mais dont la portée serait locale... soit dans une fonction, une boucle imbriquée, ou bien carrément dans un autre fichier (puisque les QA peuvent maintenant accueillir plusieurs fichiers LUA)

- cela consomme plus de RAM, car une variable globale n'est jamais libérée par le Garbage Collector (par définition, elle doit rester tout le temps accessible par tout le monde au sein du programme.... sauf si on la détruit explicitement avec http = nil, ce que personne ne pense à faire)

- et enfin, l'accès aux variables globales est plus lent (les variables globales sont stockées dans un super tableau _G que LUA doit parcourir pour trouver la bonne entrée)

Donc la bonne syntaxe dans chaque fonction pour l'option 2 serait :

local http = net.HTTPClient()

 

De façon similaire, dans la première option, tu as aussi défini une variable globale.

Il vaudrait mieux la stocker dans le QuickApp, lors de son initialisation :

QuickApp:onInit()
	self.http = net.HTTPClient()
end

Et on appelle ensuite self.http à chaque fois que l'on veut s'en servir.

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Le code c'était un squelete un peu simpliste 

OUI l'idée c'est de le mettre comme tu le montre et d'utiliser self.http dans les fonctions

QuickApp:onInit()
	self.http = net.HTTPClient()
end

mais j'avais un doute avec des appels concurrents et aléatoires sur les fonctions du QA 

Le = nil a l'époque des pertes de memoire en HC2 j'en avait mis de partout mais si on declare dans le init cela ne me semble plus utile dans un QA

Ce qui m'a fait poser la question c'est la lecture de l'exemple de la doc fibaro QA ou il disent : 

 

-- An example of a GET inquiry
-- self.http must have been previously created by net.HTTP
function QuickApp:getQuote()

 

Le net.HTTP est bien en dehors de la fonction alors que moi je le m'étais dans chaque fonction

 

 

 

 

Partager ce message


Lien à poster
Partager sur d’autres sites

mais que ce passerait-il si 2 méthodes de ce QA sont appelées "simultanément" avec la variable http déclarée dans le onInit() ?

Modifié par jjacques68

Partager ce message


Lien à poster
Partager sur d’autres sites

Je me pose la même question sur des appels simultanés 

 

Sur la HC2, j'avais une scene avec  http=net.HTTPClient dans chacune des fonctions et http=nill en fin de fonction

La scène est activée à chaque appel et pour des appels simultanés il y avait donc plusieurs occurrences

Cela fonctionne depuis plusieurs années ( sans soucis de mémoire )

 

Mais avec un QA ?? Je fais faire des essais

Modifié par henri-allauch

Partager ce message


Lien à poster
Partager sur d’autres sites

L'avis de @Lazer  est le bon

J'ai fait un QA-1 avec net.HTTPClient dans le Init  et trois fonctions d'appel HTTP ( le serveur php immédiatement le renvoi le texte émis )

un bouton de test lance 5 fois les 3 fonctions - > OK

J'ai fait un deuxième QA-2 avec un bouton de TEST

il appelle 10 fois les trois fonctions du QA-1 - > OK

Rien perdu pas d'erreur 

Meme essais avec une réponse décalée de 4s ( pour simuler un delais de traitement du serveur ) 

Rien de perdu pas d'erreur

 

Je pense que c'est bon

j'ai mis les QA le php et les résultats en fichier zip joint

Test.zip

Modifié par henri-allauch

Partager ce message


Lien à poster
Partager sur d’autres sites

Merci pour ton test qui confirme que l'option n°1, c'est à dire la variable self.http unique dans tout le QuickApp est la meilleure.

 

On ne maitrise pas ce qui se passe à l'intérieur de la librairie net.HTTPClient, codée en C, et visiblement elle gère parfaitement les requêtes simultanées :60:

 

C'était un peu le doute que j'avais eu en portant Network Monitor sur HC3, et par précaution j'ai choisi de créer une variable http pour chaque serveur à monitorer.

Visiblement c'est inutile, et je pourrais utiliser une variable self.http unique.

Bon après c'est pas pour le peu de mémoire que consomme ce QuickApp que ça va changer grand chose...

 

Il y a 17 heures, henri-allauch a dit :

Le = nil a l'époque des pertes de memoire en HC2 j'en avait mis de partout mais si on declare dans le init cela ne me semble plus utile dans un QA 

Disons que c'est toujours utile car ça permet de libérer la mémoire... mais les QuickApps étant stables par nature, ce xx = nil n'est pas indispensable comme il l'était pour les VD sur HC2 (cela dit on n'a jamais trop su pourquoi ils plantaient, jusqu'à ce que Fibaro corrige le bug)

 

Plus en détail :

  • Pour une variable locale, il est inutile de forcer sa libération avec xxx = nil à la fin du bloc de code dans laquelle elle a été déclarée, car le Garbage collector se chargera de faire la ménage tout seul
  • Pour une variable du QuickApp self.xxx, la question ne se pose pas, car si on crée une variable dans le QA, c'est qu'on veut la garder pendant toute la durée de vie du QA, donc aucune raison de la supprimer (sinon on aurait choisit une variable locale)
  • Pour une variable globale, dans ce cas, je vois 2 cas de figure :
    • soit on a sciemment choisi qu'elle soit globale pour être accessible par tout le monde (le QuickApp et les autres objets dans d'éventuels autres fichiers LUA.... mais c'est une mauvaise pratique
    • soit on a involontairement défini la variable en local pour un usage ponctuel, et dans ce cas il faut manuellement penser à la supprimer avec xxx = nil, sinon elle restera allouée en mémoire jusqu'à la fin de l'exécution du programme (dans le cas d'un QuickApp => jamais (ou bien redémarrage du QA, reboot de la box)) et le garbage collector ne pourra jamais la supprimer

Enfin, pensez que quand on veut passer une variable entre 2 fonctions, la meilleure solution est souvent le passage par paramètre.

A ce sujet, quand on passe une string ou un number en paramètre, c'est une copie.

Alors que quand on passe une table, c'est une référence... si bien que la fonction peut modifier la table originale en ne pensant modifier que sa propre table... méfiance, parfois le comportement est voulu, parfois pas !

 

Il y a 17 heures, jjacques68 a dit :

mais que ce passerait-il si 2 méthodes de ce QA sont appelées "simultanément" avec la variable http déclarée dans le onInit() ?

Impossible, comme dit hier sur un autre topic, les QA sont mono-threadés, le code ne peut jamais s'exécuter simultanément.

Il y a une file d'attente, et à chaque fois qu'on appelle un QA (un bouton, une fonction, une action, un événement, un settimout dans le cas d'une boucle, etc), tout cela est mis en file d'attente, et exécuté en série.... cf la discussion sur l'asynchronisme

 

Et franchement, je trouve ça mieux ainsi.

Pour avoir déjà programmé en multi-thread, c'est vraiment chaud, le contenu d'une variable peut être modifié entre 2 lignes consécutives de code, si un autre thread se déroule au même moment. Il faut gérer des mutex, etc, une gymnastique pas évidente.

 

Il y a 7 heures, henri-allauch a dit :

Sur la HC2, j'avais une scene avec  http=net.HTTPClient dans chacune des fonctions et http=nill en fin de fonction

La scène est activée à chaque appel et pour des appels simultanés il y avait donc plusieurs occurrences

En fait c'est différent, sur HC2 chaque instance de scène s'exécutait dans un processus (au sens Linux du terme) différent, c'est à dire un programme totalement séparé, avec son propre espace mémoire, avec sa propre copie du code, des variables, de la pile, etc.

Du coup on avait une étanchéité totale entre les instances de scènes, il était impossible pour elles de partager les mêmes variables (sauf à passer par des variables globales)

 

Sur HC3, chaque QuickApp est un processus (mono-threadé donc), et chaque scène est un processus aussi (toujours mono-threadé)

Et Fibaro, au moins jusqu'au firrmware actuel, interdit le démarrage de plusieurs "instances", c'est à dire plusieurs processus Linux.

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

×