Aller au contenu

Questions de débutant en Quick Apps sur HC3


Messages recommandés

Posté(e)

J'ai testé avec ça et ça fonctionne toujours pas, bon après tout je m'y attendais :D

 

---fermeture
function QuickApp:close()
fibaro.call(279"close")
 
setTimeout(function()  if fibaro.getValue(279"power") >= "80"end5*1000))
then
fibaro.call(279"stop"---- stop 
 
  self:debug("Fermeture")
end
 
 
 
Posté(e)
Il y a 6 heures, Fredmas a dit :

 

Alors attention, la dernière fois que nous avons discuté, ce n'était pas vraiment une tempo au sens que tu l'entends. C'est un Timeout reportant le lancement d'une fonction soit externe, soit interne à répétition.

 

Là ce que tu veux, c'est mesurer une consommation, puis agir x secondes après la mesure ? Ou attendre x secondes après le mouvement pour vérifier la mesure puis agir immédiatement ?

Bref, précise stp pourquoi faire tu souhaites ce que tu appelles "tempo" :D

attendre x secondes après le mouvement pour vérifier la mesure puis agir immédiatement ?
 

Posté(e) (modifié)

Je reprends tes précédentes citations, tu veux faire un truc comme ça par exemple :

---fermeture
function QuickApp:close() -- bouton pour fermer ton volet
	fibaro.call(279, "close") -- fermeture de ton volet
	setTimeout(function() if fibaro.getValue(279, "power") >= 80 then self:stop() end, 5*1000) -- attendre 5s avant de vérifier ta consommation puis exécuter une action
end

---STOP
function QuickApp:stop() -- bouton pour arrêter ton volet
	fibaro.call(279, "stop")
	self:debug("STOP")
end

---ouverture
function QuickApp:open() -- bouton pour ouvrir ton volet
	fibaro.call(279, "open")
	self:debug("Ouverture")
end

 

Je n'ai pas vérifié l'écriture de ce que tu avais proposé comme tes fibaro.call ou fibaro.getValue.

Je te réponds pour la structure de ce que tu demandes ;)

 

Modifié par Fredmas
Posté(e) (modifié)

j'ai une erreur : [QUICKAPP452]: main.lua:21: unexpected symbol near ','

 

21-  setTimeout(function() if fibaro.getValue(279"power") >= 80 then self:stop() end5*1000)
 
J'ai essayé d'enlever d'ajouter, déplacer des virgule,  rien n'y fait
Modifié par 971jmd
Posté(e) (modifié)

Pardon j'ai oublié un "end" que tu aurais pu trouver :D

 

---fermeture
function QuickApp:close() -- bouton pour fermer ton volet
	fibaro.call(279, "close") -- fermeture de ton volet
	setTimeout(function() if fibaro.getValue(279, "power") >= 80 then self:stop() end end, 5*1000) -- attendre 5s avant de vérifier ta consommation puis exécuter une action
end

---STOP
function QuickApp:stop() -- bouton pour arrêter ton volet
	fibaro.call(279, "stop")
	self:debug("STOP")
end

---ouverture
function QuickApp:open() -- bouton pour ouvrir ton volet
	fibaro.call(279, "open")
	self:debug("Ouverture")
end

 

Le "end" du if avant le "end" de la fonction :P

Modifié par Fredmas
Posté(e) (modifié)

Si j'avais écrit comme ci-dessous je l'aurais vu. Comme quoi la structure et la forme du code compte pour t'y retrouver :P

---fermeture
function QuickApp:close() -- bouton pour fermer ton volet
	fibaro.call(279, "close") -- fermeture de ton volet
	setTimeout(function()
		if fibaro.getValue(279, "power") >= 80
		then self:stop()
		end
	end, 5*1000) -- attendre 5s avant de vérifier ta consommation puis exécuter une action
end

---STOP
function QuickApp:stop() -- bouton pour arrêter ton volet
	fibaro.call(279, "stop")
	self:debug("STOP")
end

---ouverture
function QuickApp:open() -- bouton pour ouvrir ton volet
	fibaro.call(279, "open")
	self:debug("Ouverture")
end

 

Modifié par Fredmas
Posté(e) (modifié)

j'essaye ça 

 

Le premier essai était associé au bouton fermeture

 

la j'essaye de faire en sorte que quand le volet se ferme à partir d'un smartphone par exemple, de détecte et lancée la procédure de contrôle 

 

Mais je pense qu'il manque quelque chose dans le 

function QuickApp:onInit()  pour détecter à tout moment la fermeture du volet

 


function QuickApp:auto()
if fibaro.call(279, "close") then self:consoma () end
self:debug("consoma")
end 

function QuickApp:consoma()
setTimeout(function() if fibaro.getValue(279, "power") > 100 then self:stop() end end, 2*1000)
self:debug("AUTO STOP")
end

 

Modifié par 971jmd
Posté(e)

onInit() est la première fonction exécutée au début de ton QA. Elle ne surveille pas ton QA et les événements.

Si tu veux une surveillance il faut te tourner vers :

 

 

Posté(e)

Sinon tu fais une simple boucle de surveillance avec setTimeout toutes les secondes par exemple, mais ce n’est vraiment pas très classe quand même…

Posté(e) (modifié)

@jang @Lazer and all others members of course :P, continuing to talk around a coffee for more knowledge and improvement, I understand the architecture, but why do you prefer this way to code:

local function condition1(self) return true end
local function condition2(self) return math.random(1,3) > 1 end
local function condition3(self) return false end
local function action1(self) fibaro.call(...) end
local function action2(self) fibaro.call(...) end
local function action3(self) fibaro.call(...) end

local rules = {
  {condition=condition1,action=action1},
  {condition=condition2,action=action2},
  {condition=condition3,action=action3}
}

function mainCode(self)
    for _,r in ipairs(rules) do  -- Test conditions and run actions if true
       if r.condition(self) then r.action(self) end
    end
end

 

compare to this structure of code:

local function action1(self) fibaro.call(...) end
local function action2(self) fibaro.call(...) end
local function action3(self) fibaro.call(...) end

function mainCode(self)
--------------------------------------------------
  if condition1 then action1(self) end
--------------------------------------------------
  if condition2 then action2(self) end
--------------------------------------------------
  if condition3 then action3(self) end
--------------------------------------------------
end

 

Maintenance? Performance? Something else I didn't catch? Or pure personal point of view?

Calling a function is it longer than executing directly the content in the code?

 

Modifié par Fredmas
Posté(e) (modifié)

I guess the reason is that as you define more and more conditions and actions it will stabilise over time and you will only focus on the rules

e.g.

local rules = {
  {condition=condition1,action=action1},
  {condition=condition2,action=action2},
  {condition=condition3,action=action3}
}

combining existing conditions and actions to new rules. You could have conditions that combine other conditions (AND). You could have conditions that watch if another conditions is true for a certain time. You could improve the "rule engine" to stop evaluating rules if an action returns the "break. You could add automatic logging etc.

Of course you will end up with something like GEA at the end....

The point is that the abstraction allow you to focus on the problem - home automation rules - like, ex. GEA does.

 

When the number of rules/conditions/actions are as small as in the example above the overhead may not be justified, but when you sit there with 100+ rules, you would like an abstraction that allows reuse of logic and an easy way to add features...

 

So, I don't write all my code in this style - but dealing with large complex test logic is tempting to write a rule engine - done it many times.

My other favourite is "event style" coding. Defining event handlers and posting events to drive execution between the handlers. It's really suitable in asynchronous logic (like home automation tends to be) and it integrates well my app logic with both the asynchronous net.HTTPClient:request(...) we have as well as the triggers from /refreshStates etc.

Your code becomes like a state-machine, but a bit more flexible.

 

Here is an old post from the HC2 days

https://forum.fibaro.com/topic/25214-event-based-programming/

...and it has improved a lot since then.

Modifié par jang
  • Like 2
Posté(e)

Crystal clear, thank you @jang ;)

I understand the pro/cons, so as I already moved my actions (and reduced them thanks to parameters in functions), I will think and work to move also conditions, and play will rules :P

 

About event and refresh, I still need to read again several times what @Lazer has done, and also what you are proposing to me for reading :D

I like it, but step by step :lol:

Posté(e) (modifié)

 

Le 23/11/2021 à 17:03, jang a dit :

(...)

The important thing is that your conditions need to return true/false.

(...)

 

But this way, is changing the philosophy of my code for coding conditions... :huh:

Until now I used a lot of local variables updated by conditions, and used later in actions...

Good news is that it should decrease the number of local variables.

Bad news is that it should increase the number of local conditions.

 

Modifié par Fredmas
Posté(e) (modifié)

Sorry for this long post in English in a French forum. However, I don't trust Google to translate my non-native English into French ;-)

Anyway, it's the 'beginner's questions' and we are discussing how to structure our programs.

Week-end reading for everyone interested in writing their own rule system – or just structure programs in a more “declarative” way. 

 

condition_1 => action_1
condition_2 => action_2
    :
condition_n => action_n

If you have this way of programming, you typically need to test your conditions in a loop that runs every x second

 

local function loop()
 
    if condition_1 then action_1 end
    if condition_2 then action_2 end
        :
    if condition_n then action_n end
    
    setTimeout(loop,100*30) -- check every 30s
end
loop()

 

Ex. In this case checking every 30s. This is the way standard GEA rules behaves.

However, GEA has another model with the -1 trigger, that reacts directly on external events, like devices changing states etc., and  then run the loop checking the rules. That way rules can respond immediately to ex. sensors being breached.

 

We can add that immediate check too

local function loop()
 
    if fibaro.getValue(88,'value')==true then fibaro.call(99,'turnOff') end
    if condition_2 then action_2 end
        :
    if condition_n then action_n end
    
    setTimeout(loop,100*30) -- check every 30s
end
loop()
 
local function check_sensor()
   if fibaro.getValue(88,'value')==true then 
     loop() 
   end
   setTimeout(check_sensor,1000*1)  -- check every second
end
check_sensor()

 

We have a loop that runs every 30s and checks all our "rules".

We have another function, check_sensor, that runs every second and if the sensor is breached it calls the main loop that checks all rules. The ‘loop’ function still carries out the action when the light is on. This gives us the flexibility to check everything every 30s but also to react immediately when a sensor is breached.

 

We can improve this. Now we check the sensor in both loops (fibaro.getValue). We have already detected that the sensor was breached so we can tell the main loop what has happened.

 

local function loop(event)
 
    if event=='sensor_88_breached' then fibaro.call(99,'turnOff') end
    if condition_2 then action_2 end
        :
    if condition_n then action_n end
    
    setTimeout(loop,100*30) -- check every 30s
end
loop()
 
local function check_sensor()
   if fibaro.getValue(88,'value')==true then 
     loop('sensor_88_breached') 
   end
   setTimeout(check_sensor,1000*1)  -- check every second
end
check_sensor()

 

 Let's make the check_sensor code be more generic and check more devices

 

local function rule_checker(event)
 
    if event.type=='device' and event.id==88 and event.value==true then fibaro.call(99,'turnOff') end
    if condition_2 then action_2 end
        :
    if condition_n then action_n end
    
    setTimeout(function() rule_checker({type='loop'}) end,100*30) -- check every 30s
end
rule_checker({type='loop')
 
local myDevices = { 88, 99, 101, 120, 444, 55, 6 } -- ID of devices to check
local myDeviceValues = { } -- Starts empty but will store last values of our devices
 
local function check_devices()
  for _,id in ipairs(myDevices) do
    local value = fibaro.getValue(id,'value')   -- Fetch device value
    if myDeviceValues[id]~=value then           -- Has the value changed?
      myDeviceValues[id]=value                  -- Remember new value
      rule_checker({type='device', id=id, value=value}) -- Call our rule checker with new value
    end
  end         
end
  
setInterval(check_devices,1000*1)               -- check devices every second

 

First our 'loop' function is renamed to 'rule_checker' and takes one argument 'event'

'event' is a Lua table with at least a key that is 'type'. When rule_checker calls itself every 30s it sends the argument {type='loop'} to itself. This is to make sure that there is always an argument 'event' that we can check against.

 

Our check_sensor has become check_devices,  and checks multiple devices if they have changed state and then calls our check rules with an argument (event) that is of type 'device' with information what deviceId it was and what new value it has:

{type='device', id=<id of device>, value=<new device value>}

 

 Now our rule checks if the type of event was 'device' and the id was 88, and in that case turns off device 99.

 Note that the rule will not act every 30s as it now requires the event type is 'device' - not 'loop'

 

 We could keep both rules if that would make sense.

    if event.type=='device' and event.id==88 and event.value==true then fibaro.call(99,'turnOff') end
    if event.type=='loop' and fibaro.getValue(88,'value')==true then fibaro.call(99,'turnOff') end

The observant reader will have discovered a bug. Every time check_devices calls 'rule_checker' it will start a new loop as rule_checker ends with a call to itself (the 'setTimeout'). Well,that will be fixed in the next example.

 

Now let’s make one more abstraction of this code. Sending the event to our rule_checker should be a function - 'post'

  

local post -- Forward declaration...
  
local function rule_checker(event)
 
    if event.type=='device' and event.id==88 and event.value==true then fibaro.call(99,'turnOff') end
    if condition_2 then action_2 end
        :
    if condition_n then action_n end
    
end
 
local myDevices = { 88, 99, 101, 120, 444, 55, 6 } -- ID of devices to check
local myDeviceValues = { } -- Starts empty but will store last values of our devices
 
local function check_devices()
  for _,id in ipairs(myDevices) do
    local value = fibaro.getValue(id,'value')   -- Fetch device value
    if myDeviceValues[id]~=value then           -- Has the value changed?
      myDeviceValues[id]=value                  -- Remember new value
      post({type='device', id=id, value=value}) -- Post event
    end
  end
end
 
function post(event) rule_checker(event) end    -- Posting an event means calling check rules with event
 
setInterval(check_devices,1000*1)                        -- Check devices every second
setInterval(function() post({type='loop'}) end,1000*30)  -- Post 'loop' event every 30s               

 

Instead of the rule_checker calling itself with setTimeout, we have a separate setInterval, "posting" the 'loop' event every 30s to the rule_checker function. The abstraction is a bit better, and we integrate periodic and immediate rule checks.

 

The next steps is to abstract the rule_checker function. it's just checking a number of rules in sequential order. Let’s break that apart in 2 steps.

 

local rules = {
    device = function(even) 
        if event.id==88 and event.value==true then fibaro.call(99,'turnOff') end
    end,
    
    <type_2> = function(event)
       action_2
    end,
    
    <type_n> = function(event)
       action_n
    end,
    
end

 

We make it a Lua table with the event type as key, associated to a function carrying out the action.

Our 'post' function then becomes

 

function post(event) 
    if rules[event.type] then           -- Do we have a rule for this type ?
      local action = rules[event.type]  -- Then get the action
      -- action(event)                        -- ..and call it
      setTimeout(function() action(event) end,0) -- Lets call the action with setTimeout instead of calling it directly...
    end
end   

 

We can also add rules to the table like this

 

local rules = {}
local function addRule(eventType,action) rules[eventType]=action end
  
addRule('device', function(even) 
    if event.id==88 and event.value==true then fibaro.call(99,'turnOff') end
  end)
    
addRule('loop',function(event)
    action_2
  end)

    

There is a big problem with the approach to store rules using the event type as key.

We can only have one rule for each key, because keys are unique in a Lua table (or we overwrite the old value)

  

Instead of using the type as key we can just store them in an array and let post search for matching keys

  

local function addRule(eventType,action) table.insert(rules,{type=eventType,action=action}) end
function post(event)
   for _,rule in ipairs(rules) do
     if event.type==rule.type then 
       setTimeout(function() rule.action(event) end,0)
     end
   end
end

  

This means that we can define rules like this

  

addRule('device', function(even) 
    if event.id==88 and event.value==true then fibaro.call(99,'turnOff') end
  end)
 
addRule('device', function(even) 
    if event.id==101 and event.value==true then fibaro.call(99,'turnOff') end
  end)
  
addRule('loop',function(event)
    action_2
  end)
  
addRule('loop',function(event)
    action_n
  end)

  

If we have a 'device' event, both rules will run, checking if it was 88 or 101 that was breached.

A drawback with this is that we run both device rules even though we know that if one match the other will not. And we have to do the if event.id == .. test in both.

We can do better.

  

Our post functions now only look at the 'type' key of the event. Instead it could look at all fields in the event.

  

local function addRule(event,action) table.insert(rules,{event=event,action=action}) end
  
local function match(event1,event2)
    for key,value in ipairs(event1)
      if event2[key]~=value then return false end -- If a key in event1 is not the same as in event2, no match - return false
    end
    return true -- All keys matched, return true
end
  
function post(event)
   for _,rule in ipairs(rules) do
     if match(rule.event,event.type) then 
        setTimeout(function() rule.action(event) end,0)
     end
   end
end

  

Now when we call addRule to add a rule we provide the whole event it should match for the action to be called.

  

addRule({type='device',id=88, value=true}, function(even) 
    fibaro.call(99,'turnOff') 
  end)
 
addRule({type='device',id=101, value=true}, function(even) 
    fibaro.call(99,'turnOff') 
  end)
  
addRule({type='loop'},function(event)
    action_2
  end)
  
 addRule({type='loop'},function(event)
    action_n
  end)

  

The 'post' function may not be the most efficient as it needs to look through all the rules and see if they match. Don't  worry about that now (we can make it much more efficient). Instead enjoy the abstraction :-)

  

Let’s improve the 'post' function with one more feature.

  

local function postEvent(event)
    for _,rule in ipairs(rules) do
      if match(rule.event,event.type) then 
        rule.action(event)
      end
    end
end
 
function post(event,delay) return setTimeout(function() postEvent(event) end,1000*(delay or 0)) end

  

'post' can now delay the invocation of the matching rules. (If we don't specify a delay it becomes zero and is invoked immediately)

  

Why is that a nice feature?

 

local rules = {}
local function addRule(event,action) table.insert(rules,{event=event,action=action}) end
  
local function match(event1,event2)
  for key,value in ipairs(event1)
    if event2[key]~=value then return false end -- If a key in event1 is not the same as in event2, no match - return false
  end
  return true -- All keys matched
end
  
local function postEvent(event)
  for _,rule in ipairs(rules) do
    if match(rule.event,event.type) then 
      rule.action(event)
    end
  end
end
function post(event,delay) return setTimeout(function() postEvent(event) end,1000*(delay or 0)) end
  
addRule({type='device',id=88, value=true}, function(even) 
    fibaro.call(99,'turnOff') 
  end)
 
addRule({type='device',id=101, value=true}, function(even) 
    fibaro.call(99,'turnOff') 
  end)
  
addRule({type='start'},function(event) 
    post({type='check_devices', interval=event.check_interval})  -- Start checking devices every second   
    post({type='loop', interval=event.loop_interval})            -- Start period 'loop'
  end)
  
addRule({type='loop'},function(event)  -- 'loop' event respost then'loop' event to get a periodic loop
    post(event,event.interval)           -- and we delay it with the interval value specified in the event
  end)
  
addRule({type='loop'},function(event)   -- Do something every interval
    print("Periodic check")
  end)
 
local myDevices = { 88, 99, 101, 120, 444, 55, 6 } -- ID of devices to check
local myDeviceValues = { } -- Starts empty but will store last values of our devices
  
addRule({type='check_devices'},function(event)
   for _,id in ipairs(myDevices) do
      local value = fibaro.getValue(id,'value')   -- Fetch device value
      if myDeviceValues[id]~=value then           -- Has the value changed?
         myDeviceValues[id]=value                  -- Remember new value
         post({type='device', id=id, value=value}) -- Post event
      end
    end
    post(event,event.interval)                    -- Loop and check devices
  end)
 
        
post({type='start', check_interval=1, loop_interval=30}) - Post 'start' event to get things going...             

(This is the "whole" system)

It's cool, because we have a 'start' rule that invokes another rules ('loop' and 'check_devices') by posting events.

The 'loop' rule then just reposts the event after a delay to create a periodic loop.

Then we can have other rules that trigger on the 'loop' event and carries out some actions.

  

We have abstracted away all setInterval's as "looping" is done with rules that repost events they trigger on (check_devices and loop).

 

We also see, that events can carry “arguments”, like the interval value that the loops use to decide how much to delay their posts to themselves.

  

So, this becomes a powerful abstraction model where our logic is expressed in rules and events, and where posting events connects our rules into a logic flow.

  

Another classic example. 

Assume that we want a rule that turns on a light (99) if a sensor (88) is breached and then  turns it off if the sensor has been safe for 60s.

For this case we need one more function, ‘cancel’, that allow us to cancel an event posted in the future - in case we change our mind…

  

local function cancel(timer) 
   if timer~=nil then clearTimeout(timer) end  -- cancel event, e.g. the setTimeout that will post the event
   return nil 
end
  
addRule({type='device', id=88, value=true},function(event)
   timer=cancel(timer)       -- Cancel timer, if there were a timer running
   fibaro.call(99,'turnOn')  -- Turn on light
  end)
 
addRule({type='device', id=88, value=false},function(event)
   timer = post({type='turnOff', id=99},60) -- Post event in 60s that will tuen off light
  end)
 
addRule({type='turnOff'},function(event)
    timer=nil
    fibaro.call(event.id,'turnOn')    -- Turn off light
  end)

This takes care of the resting of the interval if the sensor is breached while the timer is counting down.

 

...This is my all-time favourite model how to structure my programs.... especially when they becomes large and the logic complex and there are several parallell things going on. (and it integrates well with other asynchronous functions like net.HTTPClient requests.)

Modifié par jang
  • Like 3
  • Thanks 1
Posté(e)

Thank you so much @jang for your altruism and this big time you took to share and explain to us :13:

 

I read everything during my lunch time, but I need to read again several time in a calm position for better understanding :P Let's see this week-end with a long coffee.

Thanks again for this nice training ;)

Posté(e) (modifié)

Bonjour à tous,

 

Ca y est ! Comme je l'avais annoncé (il y a plusieurs mois quand même) je me lance dans les Quick Apps !

Je vais commencer comme @Fredmas par un QA basé sur une boucle similaire au trigger "cron" dans les scénarii.

Ce QA sera dédié à la mise à jour de conditions météo, dans un premier temps.

 

Je viens de lire (religieusement) tout ce développement - et je me rends compte que cela va être ardu à appliquer...

Je vais quand même procéder de même, c'est à dire par commencer à écrire une boucle simple et tester ce qui a été dit.

 

Il va me falloir du temps (ça tombe bien, je me suis éclaté une côte sur un fauteuil d'aéroport... ce qui me bloque un petit peu) B)

 

 

Modifié par Sowliny
Posté(e)

Super @Sowliny :D

Comme tu l'as vu, ce topic est plus à considérer comme un café philo du débutant passionné que du "faites-le pour moi" svp :2: C'est d'ailleurs pour ça que nous poussons parfois certaines discussions/compréhension :P

 

En tout cas grâce à ce topic et les membres, partant de zéro en QA (et en LUA) cela m'a permis de progresser tout en comprenant ce que je faisais afin d'être autonome en maintenance et évolutions.

Et si ça t'intéresse de savoir (sinon c'est pareil puisque tu ne peux pas me répondre :lol:), désormais je n'ai plus de variables globales (dans l'onglet Fibaro/Paramètres/Général/Variables), je n'ai plus de scènes (enfin une ou deux mais que pour Siri sur iPhone), uniquement des QA et quasiment aucune variable persistante dans l'onglet des QA.

Un QA (qui évolue sans arrêt) pour gérer les automatismes principaux de la maison.

Un QA météo (multi-météo puisqu'il fonctionne avec WeatherBit et OpenWeather avec un bouton pour choisir) pour récolter des informations envoyées aux autres QA pour des modifications d'automatismes.

Un QA que je démarre pour gérer les extérieurs et la piscine.

Un QA annexe probablement pour les trucs à venir, qui ne sont pas indispensables et qui me permettra de jouer sans crasher le reste qui est stable.

J'aurais pu tout faire dans le même facilement, mais pour l'instant je préfère séparer pour ne pas tout craquer en cas de crash du QA.

 

Après, je n'ai pas une super grosse installation de fou avec 95 modules non plus, mais il parait que ce n'est pas la taille qui compte :lol:

Posté(e)
Il y a 3 heures, Sowliny a dit :

Il va me falloir du temps (ça tombe bien, je me suis éclaté une côte sur un fauteuil d'aéroport... ce qui me bloque un petit peu) B)

De bonnes lectures et le cerveau qui fume en perspective alors :2:

Posté(e)
il y a 3 minutes, Fredmas a dit :

Après, je n'ai pas une super grosse installation de fou avec 95 modules non plus, mais il parait que ce n'est pas la taille qui compte :lol:

C'est la quantité qui compte :D
 

  • Haha 1
Posté(e) (modifié)

Je n'ose pas te répondre : "c'est ce que me dit ta femme plusieurs fois par jour", alors remplace "ta femme" par celle de mon voisin :2:

Modifié par Fredmas
  • Like 1
×
×
  • Créer...