Aller au contenu
Lazer

Tests d'utilisation de /api/refreshStates

Recommended Posts

Ceci n'est pas un tuto, mais plutôt un topic de travail sur l'avancement de mes tests dans l'utilisation de l'API refreshStates, son optimisation, son impact sur les performances de la box, ses limites, etc.

 

Pour rappel, refreshStates permet de récupérer en temps réel tous les événements sur la box.

A l'origine, elle a été créée par Fibaro pour les mises à jours de l'interface Web et des applications mobiles.

Mais on peut tout à fait l'utiliser dans nos codes LUA, au sein même des QuickApps, puisque ceux-ci ne disposant pas de déclencheurs (triggers) comme les Scènes, cela leur permet ainsi de simuler ces fameux triggers.

 

Actuellement, j'ai un seul QuickApp (GEA) qui utilise cette API, je vais commencer par créer plusieurs QA qui utilisent cette même API et voir comment réagit la box. Le risque probable, c'est une occupation CPU supérieure, pouvant entrainer des ralentissement, voire plantages.

 

Voici un premier bout de code, optimisé "à fond", c'est à dire que pour optimiser au maximum les performances du LUA, je n'utilise que des variables locales, l'objectif étant de limiter autant que possible l'usage des variables (et fonctions) globales, ainsi que le parcours des tables (l'appel d'une variable globale revient à parcourir la table _G), opérations très consommatrices de cycles CPU.

 

De même, le parcours de la table se fait avec for, plus rapide que ipairs(), lui même plus rapide que pairs()

Le calcul du nombre d'éléments de la table se fait avant d'entrer dans la boucle for, afin de ne pas refaire le calcul à chaque passage dans la boucle for.

 

Toutes ces optimisations LUA rendent le code moins lisible, donc je les réserve uniquement à cette boucle infinie loop(), car elle va se répéter un grand nombre de fois, à très haute fréquence. Quelques nanosecondes à chaque cycle, ça fini par faire mal mal de secondes à la fin.

Le reste du code du QuickApp (non représenté ici) sera développé de façon plus traditionnelle.

 

Pour ces tests, je commence avec un intervalle de 250 ms, soit 1/4 de seconde, ce qui me parait quasiment instantané à l'échelle humaine, et bien suffisant pour mettre à jour l'état d'un QuickApp dans nos scénarios domotiques.

Sur GEA, je tourne actuellement à 100ms et ça ne pose à priori aucun problème, je sais que d'autres personnes sur le forum sont descendues à 50 ms.

Mais je suppose que d'avoir plusieurs QA avec un intervalle de 50ms ça sera beaucoup plus stressant que 250ms, d'où mon choix de commencer mes tests avec 250 ms.
En revanche, en cas d'erreur sur la requête HTTP, j'ai mis un timeout à 5000 ms, soit 5 secondes. Je me dit que si la requête a échoué, c'est peut être parce que la box est saturée, donc attendre plusieurs secondes ne peut que faire du bien.

 

Évidemment j'utilise pcall() à chaque appel de fonction risquée, afin de protéger le code contre tout plantage, et tant pis pour le (léger) risque d'impact sur les performances.

 

Je vais lancer ce bout de code sur plusieurs QA pendant plusieurs heures, et étudier comment se comporte la box (graph CPU)

 


__TAG = "QA_REFRESHSTATES_" .. plugin.mainDeviceId

function QuickApp:onInit()
	self:trace("")
	self:trace("onInit")
	self:trace("")
end

function QuickApp:buttonLoop(event)

	local lastRefresh = 0
	local http = net.HTTPClient()
	local http_request = http.request
	local json_decode = json.decode
	local pcall = pcall
	local type = type
	local setTimeout = setTimeout
	local self_debug = self.debug

	-- Boucle d'attente d'événements instantanés
	local function loop()
		local status, err = pcall(function()
			local stat, res = http_request(http, "http://127.0.0.1:11111/api/refreshStates?last=" .. lastRefresh, {
				success = function(res)
					local status, states = pcall(function() return json_decode(res.data) end)
					if status then
						lastRefresh = states.last or 0
						local events = states.events
						local nbEvents = #(events or {})
						if nbEvents > 0 then
							self_debug(self, nbEvents)
						end
						for i = 1, nbEvents do
							local event = events[i]
							local id = event.data and event.data.id
							--if id == 123 then
								--self:debug("Event :", json.encode(event))
							--end
						end
					else
						self:error(states or "json.decode() failed")
					end
					setTimeout(loop, 250)
				end,
				error = function(res)
					self:error("Error : API refreshStates :", res)
					setTimeout(loop, 5000)
				end,
			})
		end)
		if not status then
			self:error(err)
			setTimeout(loop, 5000)
		end
	end

	loop()

end

PS : pour ce test il faut créer un bouton buttonLoop pour lancer la boucle.

 

Modifié par Lazer
  • Like 3

Partager ce message


Lien à poster
Partager sur d’autres sites

Bon voilà, j'ai lancé 4 QuickApps avec cette boucle pour la nuit, en plus de GEA, ça me fait donc 5 QA en tout qui exploitent cette API.

C'est un bon début.

 

Ce que j'ai oublié de préciser : pourquoi je fait tout ça.

 

Je ne suis pas très fan des solutions suivantes :

- Scène avec trigger, qui appelle les fonctions d'un QA

- QA qui centralise tous les refreshStates, et appelle ensuite les fonctions des autres QA

 

Car cela rend le code plus difficile à maintenir, à cause des dépendances entre Scène et QA (mais que ce passe-t-il si je change l'ID d'un QA, le nom d'une fonction, etc....).

J'ai trop galéré sur HC2 avec ce types de dépendances, auxquels on était contraints à cause des limitations des VD d'une part, et des scènes de l'autre (le coup du VD qui appelle une scène, et qui rappelle finalement un bouton de VD... l'horreur)

 

Mon objectif, c'est d'avoir des QA autosuffisants, indépendants.

Si un QA a besoin de trigger, il met en œuvre sa petite boucle, et peut vivre sa vie indépendamment des futures reconfigurations effectuées sur la box.

 

Et surtout, ça rend les QA plus facilement partageable.

Ce n'est déjà pas toujours facile de faire un tuto et d'assurer le support aux utilisateurs, mais s'il faut en plus imposer d'installer des dépendances, c'est ingérable.

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites
il y a 34 minutes, Lazer a dit :

local http = net.HTTPClient()

Dans, le loop c'est volontaire ? Plutôt que dans init

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Il n'est pas dans le loop() ;)

 

Mais bien avant !

Partager ce message


Lien à poster
Partager sur d’autres sites

Premier bilan après 1 nuit.

J'avais oublié que le backup auto se déclenchait cette nuit là, à 3h... donc un des QA n'a pas été relancé car personne pour cliquer sur le bouton (les autres étaient dans le onInit)

Mais finalement c'est pas plus mal, ça permet de constater la différence.

 

Les consommation de CPU sont issues des relevés de DomoCharts stockés dans la base SQL.

Donc avec un intervalle de 1 minute.

Et je fais une moyenne sur plusieurs heures sur des périodes pendant lesquelles je n'ai pas utilisé la box (= aucune fenêtre Web ouverte). L'objectif étant de mesurer le plus précisément possible l'impact de ces boucles refreshStates, sans perturbation extérieure.

 

Le graph suivant représente 48h.

On voit clairement mon activité d'hier soir (environ 20h à 22h), pendant laquelle j'avais plusieurs fenêtres ouvertes sur l'interface Web, des enregistrements de QA lorsque le modifie le code LUA, etc. Cela génère une charge CPU conséquente et très irrégulière.

Le backup à 3h du matin est bien visible sous forme de petit pic.

 

image.png.dcf86136e77b3fa60661e26875712341.png

PS : ne faites pas attention au maximum de 24.6%, c'était il y a quelques jours lorsque j'ai fait des tests violents de performances de bouts de codes LUA (grosses boucles de 10 millions d’itérations, et cela a abouti aux optimisations des variables/fonctions locales versus globales comme évoqué dans le 1er post)

 

Il faut savoir que mon HC3 gère très peu de modules Z-Wave, donc la majeure partie de l'activité est liée aux QuickApps (IPX800, EDRT2, GEA, Xiaomi, DomoCharts, Evénements, Eaton, Surveillance Station, Network Monitor, Kodi, HP N54L (celui là je ne l'ai pas partagé), etc). A eux seuls ils arrivent déjà à provoquer une certaine activité, qui correspond à la période n°1.

 

La période n°2, c'était hier soir, donc avec les 4 boucles refreshStates supplémentaires (en plus de celle de GEA qui est toujours présente).

La période n°3, une boucle n'a pas été redémarrée, donc on est à 3 + 1 (GEA)

 

Voici les moyennes de CPU :

  • Période n°1 "activité normale" : 5.36499 %
  • Période n°2 "avec 4 refreshStates supplémentaires" : 5.80069 %
  • Période n°3 "avec 3 refreshStates supplémentaires" : 5.65430 %

 

Quelques soustractions et divisions plus tard, j'arrive à un impact moyen sur la charge CPU de 0.10 % par QA exploitant une boucle refreshStates avec un intervalle de 250 ms.

Donc très raisonnable je trouve.

 

Je vais quand même apporter un bémol, car comme je le disais, ma box gère très peu de modules Z-Wave.

Donc même si les quelques modules modules en place et les QA génèrent un certain nombre d'événements, on est loin d'une box en prod avec 100 modules Z-Wave (relevés de température, mouvement, consommation électrique, etc).

Et c'est là l'effet pervers des boucles exploitant l'API refreshStates, c'est la charge exponentielle que ça va créer.

En effet, si le nombre d'événements sur une box en prod est multiplié par 10, alors ça fait des tables 10 fois plus grosses à traiter dans chaque boucle, multiplié par le nombre de QA exploitant cette API.
Donc il conviendra de rester vigilant.

 

Dans mon cas, je prévoie les utilisations suivantes :

  • GEA : 1 boucle avec un intervalle de 100 ms
  • Porte de garage : 1 boucle avec un intervalle de 250 ms. Ce QA sera de type RollerShutter et servira à gérer ma porte de garage, qui est équipée de 2 capteurs position haut et basse (permettant de déduire un état mi-ouvert), provenant du QA GCE IPX800.
  • KLF-050 : 1 boucle avec un intervalle de 1 s (pas besoin de plus réactif). Ce QA sera de type RollerShutter et servira à gérer le module du même nom pour le pilotage d'un Velux en IO, avec un module FGS-222 et 2 boutons poussoirs.

Donc à priori seulement 3 utilisations de l'API, avec des intervalles de rafraichissement pas trop agressifs, sauf pour GEA mais il est déjà en fonctionnement et ne pose pas de souci.

 

 

Pour info, et pour ceux qui voudraient faire leur propres tests, la requête SQL sur la base de DomoCharts est toute simple :

SELECT AVG(value) FROM domocharts_cpu WHERE time BETWEEN '2021-05-07 00:00:00' AND '2021-05-07 17:10:00'

 

 

EDIT : graph en temps réel pendant que je rédigeais ce message, aucun pic anormal à signaler :)

 

image.thumb.png.245dc7c8a3d7f91aee90d12cbcb41b08.png

 

Modifié par Lazer
  • Thanks 1

Partager ce message


Lien à poster
Partager sur d’autres sites

:74:

intéressant tout ça, je vais relire le tout à tête reposée... !!

Partager ce message


Lien à poster
Partager sur d’autres sites
Le 07/05/2021 à 21:46, Lazer a dit :

Si un QA a besoin de trigger, il met en œuvre sa petite boucle, et peut vivre sa vie indépendamment des futures reconfigurations effectuées sur la box.

 

Ce que tu entends par là, c'est d'inclure cette boucle RefreshStat pour chaque QA qui souhaite tirer parti d'un "trigger" ? (Quid de la charge)

Ou est-ce que tu consolide tous tes retours d'état au sein de cette seule et unique boucle en jouant, pour gérer les interdépendance, avec des variables ?

 

ou il y a quelques chose qui m'échappe (ce qui est fort possible).

 

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Non, c'est bien la première option que j'ai en tête : inclure cette boucle RefreshStat pour chaque QA qui souhaite tirer parti d'un "trigger"

 

D'où le test de charge, pour m'assurer que ça ne plombe pas la box.

Sur ma box de production, avec 3 QA qui exploitent ce principe et beaucoup d'événements, ça ne pose aucun souci.

 

 

La 2nde option, un QA qui centralise tous les triggers, puis les redispatch vers les autres, est l'approche choisie par @jang avec son Webhook QA : https://forum.fibaro.com/topic/49113-hc3-quickapps-coding-tips-and-tricks/page/6/?tab=comments#comment-202423

Je principe est top, mais perso je ne suis pas fan, à cause de la dépendance entre les QA (maintenance plus complexe, et il devient très compliqué de partager ses propres QA avec la communauté s'il faut monter une usine à gaz pour les utiliser)

Partager ce message


Lien à poster
Partager sur d’autres sites

ok, rassurant !

Je comprend donc mieux ton test de charge now !

Je ne sais pas encore ce que je vais faire, peut-être un mixte des 2 (boucle trigger pour les QA simple et boucle dédié pour les QA plus complexe) ... à suivre :)

 

Merci en tous cas pour le partage, ça fait bien avancer les choses.

 

Sincèrement.

 

 

Partager ce message


Lien à poster
Partager sur d’autres sites

J'ai partagé ce soir la nouvelle version du QA Événements, avec quelques commentaires ici :

 

Comme vous pourrez le lire, lors de mes tests (= usage normal de la box), j'ai constaté que ce QA pouvait consommer jusqu'à 50 Mo de RAM, et plus de 1% de CPU.

Ce qui est énorme comparé à tous mes autres QA, et même à GEA avec ses 200 règles et 4000 lignes de codes, qui est de loin le QA le plus complexe qui tourne sur ma box (GEA fait des pointes à 6 Mo de RAM seulement)

 

A comparer avec le sujet dont il est question ici, à savoir l'API refreshStates, qui s'est avéré finalement moins consommatrice qu'initialement craint.

 

En effet, même si l'exploitation de cette API impose un polling régulier et très fréquent (100 ms, soit 10 fois par secondes), à chaque boucle elle récupère soit une table vide, soit une petite table (un seul, ou quelques événements), qu'il est finalement très rapide de parcourir, comparer, analyser, traiter.

 

A l'inverse, le QA événements va interroger une autre API (/api/events/history), dans laquelle on retrouve sensiblement les mêmes informations (c'est à dire que les 2 API sont très bavarde, on y voit passer tout ce qui se passe sur la box).

Mais il le fait à intervalle plus espacé (60 secondes), et surtout il doit collecter suffisamment de lignes pour remplir les 35 labels, elle se retrouve à devoir traiter de grosses tables... ce qui est finalement beaucoup plus consommateur de CPU et de RAM.

 

Bref, il semble que l'API refreshStates soit vraiment une très bonne solution pour traiter les événements de la box, en vue d'un traitement en (quasi) temps-réel.

Mangez-en, c'est du tout bon :D

 

 

PS : En écrivant ce blah blah, il m'est venu à l'esprit qu'il serait finalement peut être plus judicieux de réécrire intégralement le code du QA événement pour exploiter l'API refreshStates, avec un potentiel énorme bénéfice sur les performances..... à méditer pour plus tard.

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

je me pose une question "philosophique" :

qu'est-il plus intéressant (performance, fonctionnalit, ....) entre

  1. l'API Refresh state programmée à 250 ms
  2. un main loop (qui tourne également toutes les 250ms) pour interroger le status d'une liste de devices ?

Partager ce message


Lien à poster
Partager sur d’autres sites

Je ne sais pas.... je pense qu'il faut que tu le testes, comme j'ai fait ici, pour comparer.

Selon le nombre de devices dans la liste, le résultat sera différent.

C'est pour quel besoin ?

Partager ce message


Lien à poster
Partager sur d’autres sites

d'après la box, j'ai 97 appareils z-wave

Actuellement, pour la gestion du chauffage, j'ai de la config dans GEA et dans un QA.

Afin de réduire les dépendances, je souhaite tout rammer dans le QA (car il fait déjà ce que GEA ne sait pas facilement faire).

Et comme dans GEA j'ai des actions qui ne sont faites qu'en fonction du changement d'état d'un appareil, je crois que le refreshState serait plus approprié, mais je me posait la question car il collectait les changements d'état (en fait, uniquement le changement de la propriété Status ou de n'importe quelle propriété ?) de tous les appareils, ou mon QA ne vérifierait qu'un nombre limité de devices ? La question reste donc ouverte ...

Partager ce message


Lien à poster
Partager sur d’autres sites

Si ton QA de gestion chauffage exploite l'API refreshStates, ça me semble tout indiqué.
L'API en question remonte tous les événements, donc il te suffit de trier sur les devices / propriétés qui t'intéressent (value, batterylevel, power, energy, etc... c'est toi qui choisis)
En fait exactement ce que fait déjà GEA (il utilise une boucle refreshStates), mais dans un QA dédié, ainsi la gestion de ton chauffage devient indépendante de GEA, donc plus résiliente.

 

Partager ce message


Lien à poster
Partager sur d’autres sites

mon QA chauffage n'exploite pas encore cette API.

Il va falloir que je prenne mon courage à 2 mains pour m'y pencher (et comprendre ton code) sérieusement.

 Quand de je fais du reverse enginering, je ne comptrnds pas

local id = event.data and event.data.id

j'aurais compris :

local id = event.data.id

 

idem pour :

--self:debug("Event :", json.encode(event))

il faut que j'essaie pour voir ce que ça donne

 

Partager ce message


Lien à poster
Partager sur d’autres sites

Ce sont des "astuces" LUA.
Si event.data n'est pas défini, alors...

local id = event.data.id

...va planter.

 

Donc ...

local id = event.data and event.data.id

... permet de protéger l'exécution de la commande, puisqu'on affecte event.data.id à la variable id seulement si event.data existe

 

 

Ensuite, le debug commenté... c'est juste un débug commenté !

A remplacer par du code utile.

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

ok, c'est un debug commenté, mais je ne connais pas avec plusieurs paramètres. Ce que je connais c'est:

self:debug("toto".."tata)

et c'est quoi selft_debug ?

self_debug(self, nbEvents)

aussi, quelle est la structure de 

event.data

idem que le json auquel il se rapporte ?

event.data.id
event.data.name
...
event.data.properties.heatingThermostatSetpoint
...

 

Partager ce message


Lien à poster
Partager sur d’autres sites
Il y a 12 heures, jojo a dit :

ok, c'est un debug commenté, mais je ne connais pas avec plusieurs paramètres. Ce que je connais c'est:


self:debug("toto".."tata)

C'est pareil, les fonctions Fibaro debug/trace/warning/error peuvent prendre plusieurs paramètres, elles font automatiquement une concaténation des chaines de caractères, avec ajout d'un espace entre chaque.

 

Il y a 12 heures, jojo a dit :

et c'est quoi selft_debug ? 


self_debug(self, nbEvents)

Relis le code, tu verras que self_debug est une variable initialisée au debut, avant de rentrer dans la boucle infinie.
Ce sont des micro-optimisations, toutes les fonctions appelées régulièrement sont ainsi stockées dans une variable locale, accessible plus rapidement qu'une variable globale.
On a eu pas mal de discussions sur le forum à ce sujet, l'appel d'une variable globale (donc une fonction, puisqu'une fonction est une variable en LUA) prend du temps car le compilateur doit parcourir la super-table _G à la recherche de l'élément désiré. Et c'est même encore pire quand on recherche self.debug, car il faut également parcourir les sous-tables (self dans le cas présent)

 

Il y a 12 heures, jojo a dit :

aussi, quelle est la structure de  


event.data

A toi de le découvrir en l'affichant à l'écran (dans un self.debug par exemple)

Son contenu diffère en fonction de l'événement remonté.

 

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

En effet, c'est une méga optimisation indispensable lorsque le QA tourne 4x/sec.

et pour voir la structure du retour, il fallait juste décommenter ton exelple ! trop simple :60:

Partager ce message


Lien à poster
Partager sur d’autres sites
Il y a 11 heures, jojo a dit :

Indeed, it is a mega optimization essential when the QA runs 4x/sec.

and to see the structure of the return, you just had to uncomment your example! too simple :60:

I wouldn't call it a mega optimization...

If it would run million of times / sec I would agree... but not for 4 times per second...

Of course it's not the monthly energy savings that are of importance here but to make sure we can complete the 4 calls per seconds and still do all other stuff...

...then 0.26 microseconds is more like a micro optimization...

local n,t0=100000

a = {
    b = function() end
}

c = function() end

local d = function() end
print("----------")
t0 = os.clock()
for i=1,n do a.b() end  
local v0 = (os.clock()-t0)*1000
print(string.format("Time '%s' = %0.6f",'a',v0/n))

t0 = os.clock()
for i=1,n do c() end
local v1 = (os.clock()-t0)*1000 
print(string.format("Time '%s' = %0.6f",'c',v1/n))

t0 = os.clock()
for i=1,n do d() end 
local v2 = (os.clock()-t0)*1000
print(string.format("Time '%s' = %0.6f",'d',v2/n))

local save = (v0-v2)/n
printf("Saving %0.6f milliseconds per call, using local fun vs global table fun",save)
printf("Saving %0.3f seconds per month, using 4 calls / second",30*24*3600*4*save/1000)

...and the answer is 

[19.04.2023] [07:27:56] [DEBUG] [QUICKAPP1090]: Saving 0.000264 milliseconds per call, using local fun vs global table fun
[19.04.2023] [07:27:56] [DEBUG] [QUICKAPP1090]: Saving 2.734 seconds per month, using 4 calls / second

 

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

×