Aller au contenu
OJC

VD Yeelight Controller

Recommended Posts

VD Yeelight Controller

v. 1.3.1 - 18/11/2019

 

Je vous présente le VD Yeelight Manager qui, comme son nom l'indique, permet d'intégrer les produits Yeelight avec la HC2. Pour être très précis, je ne dispose que d'une Bedside Lamp et je n'ai donc pas testé ce VD avec d'autres produits. Cependant, l'API étant générique, il n'y a pas de raisons que ça ne fonctionne pas aussi. Seule limite, je n'ai pas intégré dans le VD la gestion de la background light puisque la Bedside Lamp n'en a pas.

 

Trêve de bavardages, voici l'usine à gaz :)VD Yeelight Controller v. 1.3.1

 

5a4e78b7286d5_VDYeelightManager.thumb.png.f1da1ed1ec02b465e578a9336e96a204.png

 

Paramétrage des plus classiques, il vous suffit de renseigner l'IP de la Yeelight dans le champ IP du VD, et le port dans le champ Port du VD (c'est le port 55443 qui est utilisé par les Yeelights). Avant d'importer le VD, je vous conseille de l'ouvrir dans un éditeur de texte et de faire un rechercher/remplacer pour définir l'ID de l'icône des boutons (rechercher la chaîne "buttonIcon":1011).

 

IMPORTANT = Il faut activer le Contrôle sur le réseau local depuis l'application Yeelight, pour chaque lampe.

 

Pour pouvoir gérer une Yeelight 'presque' comme un module, il suffit d'insérer dans la scène ou le code du bouton ou du Main Loop le code suivant :

 

_y={credits='Yeelight Controller v. 1.3.0 - Copyright 2018 Olivier Meyer - GNU GPLv3',global='y_yeelight_global',item='y_yeelight_',props={power=true,bright=true,ct=true,rgb=true,hue=true,sat=true,color_mode=true,flowing=true,delayoff=true,flow_params=true,music_on=true,name=true,bg_power=true,bg_flowing=true,bg_flow_params=true,bg_ct=true,bg_lmode=true,bg_bright=true,bg_rgb=true,bg_hue=true,bg_sat=true,nl_br=true},f=fibaro,debug=function(s,m,c)s.f:debug(string.format('<span style="color:%s;">%s</span>', c, m))end,here=function(s)local h=type(s.f.getSelfId)=='nil' if h then return h,__fibaroSceneId else return h,s.f:getSelfId(),_elementID_ end end,log=function(s)local h=s:here()if h then local a=s.f:args()if a then if a[1].yeelight then s:debug(a[1].debug,a[1].color);s:debug(a[1].command,a[1].color);os.exit()end end end end,getBtn=function(s,i,n)local c,r=0,api.get('/devices/'..tostring(i))['properties']['rows'] local x=#r for a=1,x do local y=#r[a].elements for b=1,y do c=c+1 if n==r[a].elements[b].name then return c end end end s:debug('[Yeelight] Unable to locate button '..n..', check virtual device '..i,'Tomato');return nil end,build=function(s,m,p)local c='{"id":1, "method":"'..m..'", "params":['for i=1,#p do if type(p[i])=="number"then c=c..p[i]else c=c..'"'..p[i]..'"'end;if i~=#p then c=c..', 'end end;c=c..']}\r\n'return c end,load=function(s,v)local g=s.f:getGlobalValue(v)if string.len(g or '')>0 then local d=json.decode(g)if d and type(d)=='table'then return d else s:debug('[Yeelight] Unable to process data, check variable','Tomato')end else s:debug('[Yeelight] No data found!','Tomato')end return nil end,set=function(s,i,d)local g=s:load(s.global)if g[tostring(i)]then local a,b,c=s:here();g[tostring(i)].scene=a;g[tostring(i)].id=b;g[tostring(i)].button=c;g[tostring(i)].command=d;s.f:setGlobal(s.global,json.encode(g))end end,call=function(s,i,m,p)local b=s:getBtn(i,'btnTransmission')if b==nil then return nil end;local c=s:build(m,p)s:set(i,c)s.f:call(i,'pressButton',b)end,getValue=function(s,i,p,g)if s:checkP(p) then if type(g)~='table' then g=s:load(s.item..i)end if g.properties then if g.properties[p] then if g.properties[p].value then return g.properties[p].value end end end s:debug('[Yeelight] Unable to get value of '..tostring(p)..', please check variable.','Tomato');end return nil end,getModificationTime=function(s,i,p,g)if s:checkP(p) then if type(g)~='table' then g=s:load(s.item..i)end if g.properties then if g.properties[p] then if g.properties[p].modificationTime then return g.properties[p].modificationTime end end end s:debug('[Yeelight] Unable to get modification time of '..tostring(p)..', please check variable.','Tomato');end return nil end,get=function(s,i,p)local g=s:load(s.item..i);return s:getValue(i,p,g),s:getModificationTime(i,p,g)end,getStatus=function(s,i)local g=s:load(s.item..i)if g then if g.status then return g.status end end return nil end,getLastChange=function(s,i)local g=s:load(s.item..i)if g then if g.last then return g.last end end return nil end,checkP=function(s,p)if not s.props[p] then s:debug('[Yeelight] '..p..' is not an existing property!','Tomato') return false end return true end}
_y:log()

 

Ce code ci-dessus contient les fonctions suivantes :

  • _y:call(id, method, params) qui permet d'envoyer une commande à la lampe. Les différents boutons du VD permettent d'avoir un aperçu du fonctionnement. Pour aller plus loin, je vous renvoie à la documentation de l'API Yeelight : Yeelight_Inter-Operation_Spec.pdf
  • _y:get(id, property) qui fait exactement la même chose que fibaro:get()
  • _y:getValue(id, property) qui fait exactement la même chose que fibaro:getValue()
  • _y:getModificationTime(id, property) qui fait exactement la même chose que fibaro:getModificationTime()
  • _y:getStatus(id) qui retourne "online" si la lampe est connectée au réseau wifi, et "offline" dans le cas contraire
  • _y:getLastChange(id) qui retourne une table contenant les propriétés qui viennent d'être modifiées et leur nouvelle valeur

 

Les logs générés par l'utilisation de la fonction call() s'affichent dans la zone de debug de la scène (ne pas oublier pour cela la ligne _y:log() et d'autoriser suffisamment d'instances) ou dans celle du bouton / main loop du module virtuel d'où a été utilisé la fonction.

 

Pour pouvoir utiliser ces lampes comme déclencheurs de scène, le VD crée automatiquement une variable globale y_yeelight_XXX,XXX est l'ID du VD. Cette variable globale contient toutes les propriétés de la lampe et est mise à jour en temps réel.

Par exemple, pour lancer l'exécution d'une scène à l'allumage d'une Yeelight :

 

--[[
%% globals
y_yeelight_XXX
--]]

_y={credits='Yeelight Controller v. 1.3.0 - Copyright 2018 Olivier Meyer - GNU GPLv3',global='y_yeelight_global',item='y_yeelight_',props={power=true,bright=true,ct=true,rgb=true,hue=true,sat=true,color_mode=true,flowing=true,delayoff=true,flow_params=true,music_on=true,name=true,bg_power=true,bg_flowing=true,bg_flow_params=true,bg_ct=true,bg_lmode=true,bg_bright=true,bg_rgb=true,bg_hue=true,bg_sat=true,nl_br=true},f=fibaro,debug=function(s,m,c)s.f:debug(string.format('<span style="color:%s;">%s</span>', c, m))end,here=function(s)local h=type(s.f.getSelfId)=='nil' if h then return h,__fibaroSceneId else return h,s.f:getSelfId(),_elementID_ end end,log=function(s)local h=s:here()if h then local a=s.f:args()if a then if a[1].yeelight then s:debug(a[1].debug,a[1].color);s:debug(a[1].command,a[1].color);os.exit()end end end end,getBtn=function(s,i,n)local c,r=0,api.get('/devices/'..tostring(i))['properties']['rows'] local x=#r for a=1,x do local y=#r[a].elements for b=1,y do c=c+1 if n==r[a].elements[b].name then return c end end end s:debug('[Yeelight] Unable to locate button '..n..', check virtual device '..i,'Tomato');return nil end,build=function(s,m,p)local c='{"id":1, "method":"'..m..'", "params":['for i=1,#p do if type(p[i])=="number"then c=c..p[i]else c=c..'"'..p[i]..'"'end;if i~=#p then c=c..', 'end end;c=c..']}\r\n'return c end,load=function(s,v)local g=s.f:getGlobalValue(v)if string.len(g or '')>0 then local d=json.decode(g)if d and type(d)=='table'then return d else s:debug('[Yeelight] Unable to process data, check variable','Tomato')end else s:debug('[Yeelight] No data found!','Tomato')end return nil end,set=function(s,i,d)local g=s:load(s.global)if g[tostring(i)]then local a,b,c=s:here();g[tostring(i)].scene=a;g[tostring(i)].id=b;g[tostring(i)].button=c;g[tostring(i)].command=d;s.f:setGlobal(s.global,json.encode(g))end end,call=function(s,i,m,p)local b=s:getBtn(i,'btnTransmission')if b==nil then return nil end;local c=s:build(m,p)s:set(i,c)s.f:call(i,'pressButton',b)end,getValue=function(s,i,p,g)if s:checkP(p) then if type(g)~='table' then g=s:load(s.item..i)end if g.properties then if g.properties[p] then if g.properties[p].value then return g.properties[p].value end end end s:debug('[Yeelight] Unable to get value of '..tostring(p)..', please check variable.','Tomato');end return nil end,getModificationTime=function(s,i,p,g)if s:checkP(p) then if type(g)~='table' then g=s:load(s.item..i)end if g.properties then if g.properties[p] then if g.properties[p].modificationTime then return g.properties[p].modificationTime end end end s:debug('[Yeelight] Unable to get modification time of '..tostring(p)..', please check variable.','Tomato');end return nil end,get=function(s,i,p)local g=s:load(s.item..i);return s:getValue(i,p,g),s:getModificationTime(i,p,g)end,getStatus=function(s,i)local g=s:load(s.item..i)if g then if g.status then return g.status end end return nil end,getLastChange=function(s,i)local g=s:load(s.item..i)if g then if g.last then return g.last end end return nil end,checkP=function(s,p)if not s.props[p] then s:debug('[Yeelight] '..p..' is not an existing property!','Tomato') return false end return true end}
_y:log()

local function run()
  --Code à exécuter à l''allumage de la Yeelight
end

local trigger = fibaro:getSourceTrigger()
if trigger.type == "global" then
  if trigger.name == "y_yeelight_XXX" then
    if _y:getLastChange(XXX)["power"] == "on" then run() end
  end
end

 

Et voici les icônes que j'utilise pour l'état on/off de la lampe, celle qui est sur chaque bouton pour signifier l'envoi de la commande, et celle qui indique que la lampe est hors ligne :

 

5a4e7d3e16b4f_VIRTUALYeelight_ON.png.6030c37a1c008933678dac031298da3b.png5a4e7d3d5634a_VIRTUALYeelight_OFF.png.f117dc1b896c87e41b3d649ca06b6311.png5a4e7d3e74fd8_VIRTUALYeelight_SEND.png.40767a5a130de7a173a88294ba86b25e.png5a4e7d3dafdda_VIRTUALYeelight_OFFLINE.png.b077b1a348fd64b1a6878155c974207a.png

 

 

REMERCIEMENTSun grand merci à @ADN182 qui m'a fourni le VD qu'il avait commencé à développer et qui m'a fait gagner beaucoup de temps dans la compréhension de la manière d'envoyer les commandes à la lampe, vu que l'objet Net.FTcpSocket ne semble documenté nulle part (ou alors je n'ai pas trouvé :huh:). Egalement, je dois pas mal à @Krikroff et à son VD Sonos Remote puisque c'est de là que vient l'idée d'utiliser une variable globale pour faire passer les commandes depuis n'importe quelle scène jusqu'au VD pour qu'il l'envoie à la lampe. De même pour le principe du ping que j'utilise pour détecter quand la lampe est déconnectée du réseau wifi, de manière à relancer automatiquement la connexion TCP lorsqu'elle revient en ligne, ce qui était le souci de @ADN182. Et, enfin, un grand merci également à @Talwayseb pour son VD Philips Hue qui m'a grandement inspiré pour l'esthétique du VD Yeelight Manager. Et aussi à @pepite et @Lazer pour leurs tuyaux pour faire passer les debug entre un VD et un autre VD (_elementID_ !!) ou une scène (fibaro:args()).

Modifié par OJC
Publication de la v. 1.3.1
  • Like 7
  • Thanks 1
  • Upvote 2

Partager ce message


Lien à poster
Partager sur d’autres sites

Petite mise à jour avec notamment l'ajout du ModificationTime pour chaque propriété de la lampe et l'envoi des logs dans la zone de debug de la scène ou du VD d'où a été envoyée la commande.

Partager ce message


Lien à poster
Partager sur d’autres sites

Well done, toujours et encore ;-)

 

Petit ce VD tu vois tout sur ton smartphone ;-) :2:

Partager ce message


Lien à poster
Partager sur d’autres sites

Dernière petite modification : il est désormais possible de modifier le VD à loisir (ajouter ou supprimer des boutons, des étiquettes...) en fonction des besoins et de la lampe (par exemple, l'ampoule blanche n'a pas besoin de tout ce qui concerne la couleur), et ce sans avoir à s'inquiéter du changement de numérotation des boutons Connexion TCP et Transmission :).

 

Pour ceux que ça intéresse, voici la fonction qui permet au VD de se débrouiller tout seul pour savoir où appuyer (fonction générique pouvant être reprise telle qu'elle dans n'importe quel VD ou scène pourvu qu'on ait besoin de faire un pressButton) :

 

function getBtn(id, name)
  count,rows = 0, api.get("/devices/"..tostring(id))["properties"]["rows"]
  for a, b in pairs(rows) do 
    for c, d in pairs(b) do 
      if c == "elements" then 
        for e, f in pairs(d) do 
          for g, h in pairs(f) do 
            if g == "name" then 
              count = count + 1 
              if h == n then 
                return count
              end 
            end 
          end 
        end 
      end 
    end 
  end 
  fibaro:debug("Unable to locate button "..n..", check virtual device "..id)
  return nil
end

Et la version minifiée :

getBtn=function(i,n)x,r=0,api.get("/devices/"..tostring(i))["properties"]["rows"];for a,b in pairs(r)do for c,d in pairs(b)do if c == "elements" then for e,f in pairs(d)do for g,h in pairs(f)do if g=="name" then x=x+1 if h==n then return x end end end end end end end fibaro:debug("Unable to locate button "..n..", check virtual device "..i);return nil end

 

Modifié par OJC
  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

Si je peux me permettre, par souci d'optimisation de la performance du code, il faut éviter l'utilisation de la fonction "pairs" qui est très lente. Elle a l'avantage de simplifier l'écriture et la compréhension, mais elle est peu performante, On trouve des benchmarks sur le net.

Donc il vaut mieux privilégier l'emploi d'une simple boucle "for".

 

Bon après, c'est surtout vrai si on appelle souvent ce code. Si il est peu utilisé, c'est pas ça qui changera les perfs de la box.

 

Merci pour ce bout de code en tout cas :)

 

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

@Lazer Chef, oui Chef !

 

getBtn=function(i,n)
  r = api.get("/devices/"..tostring(i))["properties"]["rows"]
  for a=1,#r do
    for b=1,#r[a].elements do
      if n == r[a].elements[b].name then return a+b end
    end 
  end 
  return nil
end

getBtn=function(i,n)r=api.get("/devices/"..tostring(i))["properties"]["rows"] for a=1,#r do for b=1,#r[a].elements do if n == r[a].elements[b].name then return a+b end end end return nil end

 

Modifié par OJC

Partager ce message


Lien à poster
Partager sur d’autres sites

Je peux faire le chieur ? :P

Tu comptes le nombre d'éléments de tes tableaux à chaque passage dans la boucle, tant qu'à optimiser il veut mieux stocker dans une variable locale avant d'entrer dans la boucle.

Partager ce message


Lien à poster
Partager sur d’autres sites

J'ai du mal à saisir ?

 

EDIT = Comme ça ? cf post précédent :)

Modifié par OJC

Partager ce message


Lien à poster
Partager sur d’autres sites

Comme ceci, non testé :

getBtn=function(i,n)
  local c,r = 0, api.get("/devices/"..tostring(i))["properties"]["rows"]
  local nbr = #r
  for a=1, nbr do
    local nbra = #r[a].elements
    for b=1, nbra do
      c=c+1
      if n == r[a].elements[b].name then return c end
    end 
  end 
  return nil
end

J'en ai profité pour déclarer toutes les variables en local, cela évite d'interférer avec les éventuelles variables du reste du script LUA de l'utilisateur qui intègrera ce code.

  • Like 2

Partager ce message


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

for a=1, nbr do

J'ignorais cette subtilité, tu veux dire que la ligne ci-dessus est exécuter autant de fois que la valeur nbr. Je supposais qu'elle n'était exécutée qu'une fois et que c'était uniquement le contenu de la boucle qui était exécuté autant de fois que la valeur nbr. :huh:

 

Il y énormément de codes sur le forum qui sont écrit comme ça :

for a=1, #r do

 

Modifié par MAM78

Partager ce message


Lien à poster
Partager sur d’autres sites

Oui tout à fait, la ligne for elle-même est interprétée à chaque passage.

Bien sur qu'il y a énormément de code écrit ainsi, tout comme il y a énormément de code avec la fonction pair().

Le propre d'un langage de programmation, c'est aussi de nous faciliter la vie avec des raccourcis, mais ces raccourcis sont rarement performants au sens du temps d'exécution machine.

Si on pousse plus loin encore le raisonnement, un appel de fonction, c'est très long, il faut placer les paramètres sur la pile, décaler le pointeur d'exécution du programme, etc. D'un point de vue performance, un code linéaire est donc plus performant qu'un code bien structuré avec de multiples fonctions. Sauf qu'à un moment donné, on est quand même obligé d'utiliser les fonctions si on veut que le développeur, un simple humain, puisse s'y retrouver dans la structure du code. De plus, les fonctions permettent de factoriser le code, donc de minimiser l'usage mémoire. Il a longtemps été reproché à Microsoft d'être trop gourmand en RAM.... donc les fonctions, faut les utiliser !

  • Like 1

Partager ce message


Lien à poster
Partager sur d’autres sites

Merci pour ce petit rappel des basiques de la programmation :D

Partager ce message


Lien à poster
Partager sur d’autres sites

Cela dit, je parle là d'optimisation "atomiques", il existe ainsi plein de petits trucs et astuces à savoir....

 

MAIS on gagne là des pouillèmes de micro-secondes, souvent le temps perdu par un code l'est par une mauvaise logique, et il est parfois plus efficace de restructurer son code différemment que de se prendre la tête sur les optimisations atomiques.

Partager ce message


Lien à poster
Partager sur d’autres sites

Et une lampe wifi à côté de la tête toute là nuit ?
Je suis pas trop parano mais j ai pris l habitude de mettre l iPhone en avion la nuit.

Partager ce message


Lien à poster
Partager sur d’autres sites

Un p'tit WallPlug fait l'affaire :)

Partager ce message


Lien à poster
Partager sur d’autres sites

Je publie une nouvelle version, m'étant rendu compte que dans certains cas, le VD détectait bien la remise sous tension de la lampe mais ne relançait pas la connexion pour en recevoir les informations de mise à jour.

J'en ai profité pour nettoyer un peu le code et virer la fonction _y:help() qui au final prenait surtout beaucoup de place pour pas grand chose.

Et le VD s'occupe aussi de nettoyer tout seul les variables globales lorsqu'un VD est supprimé ou en cas de changement d'ID. J'ai à cette occasion découvert que si on peut faire un api.delete dans une scène, ce n'est pas prévu pour les VD :mellow: où il faut passer directement par Net.FHttp() pour pouvoir supprimer une variable globale...

Partager ce message


Lien à poster
Partager sur d’autres sites

Je tombe sur ce post tardivement, voyant qu'on parle d'optimisation :

 

getBtn=function(i,n)
  local c,r = 0, api.get("/devices/"..tostring(i))["properties"]["rows"]
  local nbr = #r
  for a=1, nbr do
    local nbra = #r[a].elements
    for b=1, nbra do
      c=c+1
      if n == r[a].elements[b].name then return c end
    end 
  end 
  return nil
end

A quoi sert "c" ... pourquoi ne pas retourner directement "b" :D

 

Ok, :98:

Partager ce message


Lien à poster
Partager sur d’autres sites

LOL :13:

Magnifique exemple de ce que je disais précédemment : " il est parfois plus efficace de restructurer son code différemment "

 

J'avoue que je n'ai même pas cherché à comprendre ce que faisait le code.... :rolleyes:

  • Haha 1

Partager ce message


Lien à poster
Partager sur d’autres sites

@Steven Parce que la valeur de a correspond au numéro de la ligne sur le VD, et celle de b correspond au numéro de l'élément au sein de cette ligne, d'où il suit qu'elle est réinitialisée à chaque itération de a et donc que retourner cette valeur de b comme résultat de la fonction ne correspond pas à ce qui est attendu.

 

Sinon, pas trop froid, dehors ?

Partager ce message


Lien à poster
Partager sur d’autres sites

C'est pas faux :P

 

Oui, je me les cailles comme on dit vulgairement... température réelle -7, ressentie -13.

Je suis en train du brûler du "pellet" comme jamais. C'est dans des moments comme celui-ci qu'on regrette de ne pas avoir un autre mode de chauffage. :(

 

Après en ce qui concerne l'optimisation, perso, ma devise est : "préféré un code qui fait son travail à un code parfait qui ne fait rien". :16:

Partager ce message


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

préféré un code qui fait son travail à un code parfait qui ne fait rien"

Et moi je fais des codes qui ne font rien et non parfait, la galère non ? :60:

Partager ce message


Lien à poster
Partager sur d’autres sites

Même pas vrai ... patate. :D

  • Like 1
  • Haha 1

Partager ce message


Lien à poster
Partager sur d’autres sites

bonjour, je test ce VD avec un Yeelight Smart LED Bulb et un Yeelight Lightstrip qui semblent ok avec l'open API mais le VD ne marche pas. J'ai mis l'ip et le port dans le VD mais il me met "état : hors ligne" alors que dans l'app Yeelight j'ai bien le contrôle des deux... une idée ? 

 

Quelqu'un a t-il essayé avec ce genre de produit et cela est-il fonctionnel chez quelqu'un ?

 

Sur quel serveur les avez-vous enregistrer ? (singapour, chine, allemagne ?)

Modifié par Eliah

Partager ce message


Lien à poster
Partager sur d’autres sites

Bonjour @Eliah

Qu'est-ce que tu as dans le debug du main loop ?

Partager ce message


Lien à poster
Partager sur d’autres sites

@Eliah Tu as activé le contrôle local dans l'application Yeelight (pour chaque lampe) ? C'est indispensable pour pouvoir commander les lampes depuis le réseau local.

Partager ce message


Lien à poster
Partager sur d’autres sites

×