lundi 6 mars 2017

Domotique: un peu beaucoup de code lua ;-)

Bon... aujourd'hui on va parler de code (je vais tout vous mettre ;-), je vais vous partager ce que j'utilise pour mon code .lua dans le NodeMCU.

Tout d'abord je dois dire ce que je fais avec ;-)
Mon but: avoir un composant wifi utilisable avec Jeedom pouvant piloter un relais, lire la température avec un ds18b20 (utilisant le protocol "one-wire"), lire le courant consommé (utilisant une petite pince ampèremétrique), vérifier la présence avec un capteur PIR (Passive Infrared Sensor) et lire une commande comme un interrupteur. Et pour finir, j'ai décider de faire un code commun pour la maintenance.
De plus, mon but est de pouvoir mettre cela dans une boite encastrable dans un mur pour par exemple être au dessus d'un bouton existant et ainsi le commander (avec retour d'état). Je metterais des photos plus tard ;-)

Donc dans le nodeMCU, j'ai organisé ainsi :

1) Avec le init.lua qui me sert juste à lancer la config du node, la config du wifi et du "main.lc" (version compilé du .lua pour gagner de la place) avec la possibilité de faire du telnet en option pour avoir un accès remote au code du nodemcu mais c'est encore de l'expérimentale ;-) :

-- init.lua --

-- Network Variables
--list files in NODEMCU
l = file.list();
    for k,v in pairs(l) do
      --print("name:"..k..", size:"..v)
      --print(string.find(k, "_conf.lua", 1))
      if((string.find(k, "_conf.lua", 1) ~= nil) and (string.find(k, "wifi_node", 1) ~= nil))then
        -- Load conf Jeedom
        --print("load file:"..string.sub(k, 1,string.find(k, ".lua", 1)-1));
        require(string.sub(k, 1,string.find(k, ".lua", 1)-1));
      end
    end

-- Mode Variables ("telnet" or "web")
require("mode_conf"); 

-- Configure Wireless Internet
wifi.setmode(wifi.STATION);
--print('set mode=STATION (mode='..wifi.getmode()..')\n')
--print('MAC Address: ',wifi.sta.getmac())
--print('Chip ID: ',node.chipid())
--print('Heap Size: ',node.heap(),'\n')

-- Configure WiFi
wifi.sta.config(ssid,pass);

tmr.alarm(0, 1000, 1, function() 
   if wifi.sta.getip()==nil then
      print("Connect AP, Waiting...") 
   else
      print("Wifi AP connected. IP:")
      print(wifi.sta.getip())
      tmr.stop(0)
      if(telnet_server_mode == "true")then
      -- Load telnet server
        dofile("telnet.lc");
      else
      -- Load main
        dofile("main.lc");
      end
   end
end)

2) avec le configuration wifi dans un fichier dédié pour gérer plusieurs nodes par la suite :
    (moi je les mets en IP fixe à partir de ma freebox pour ne pas avoir de soucis et en fonction de leur adresse Mac) - wifi_nodeX_conf.lua (X étant le numéro de votre node sur le réseau, juste si vous voulez utiliser des points wifi différent ;-)

-- config wifi
ssid = "votre ssid à mettre ici"
pass = "et le password ici bien sur ;-)"

2 bis) avec le mode choisi (pour faire du telnet ou pas) - mode_conf.lua


telnet_server_mode = "false"; -- to activate "telnet" server when necessary (11/05/2015) because only one TCP server supported on NodeMCU firmware

3) et avant de parler du principal, voici aussi le code du telnet.lua à tester ;-) parce que encore expérimentale comme je l'ai déjà dis (qu'il faudra aussi compiler en .lc):


print("====a LUA console over wifi.==========")
print("Telnet Server starting ......")

   sv=net.createServer(net.TCP, 180)
   sv:listen(8080,   function(conn)
      print("Wifi console connected.")
   
      function s_output(str)
         if (conn~=nil)    then
            conn:send(str)
         end
      end
      node.output(s_output,0)

      conn:on("receive", function(conn, pl) 
         node.input(pl) 
         if (conn==nil)    then 
            print("conn is nil.") 
         end
      end)
      conn:on("disconnection",function(conn) 
         node.output(nil) 
      end)
   end)   
   print("Telnet Server running at :8080")
   print("===Now,Using ESPlorer and input LUA.====")

4) et donc un main.lua (qu'il faudra compiler en .lc) où l'on retrouve le serveur web que je peux interroger en http mais aussi des commandes qui peuvent être envoyé vers jeedom :

version = "v0.0.8";

-- global variables
SendingData = false;
--

-- global functions
function sendDataToVirtual (id,state)
            if SendingData then  -- don't allow new sending data during process of sending data
                return
            else 
                SendingData = true
            end
            --print("id: "..id);
            --print("state: "..state);
            -- A simple http client -> to send last/new state to jeedom
            if(httpreq == nil) then
            --print("initial create connection"); 
              httpreq=net.createConnection(net.TCP, false);
            else
            --print("connect close"); 
              httpreq:close();
              collectgarbage();
              --print("create connection"); 
              httpreq=net.createConnection(net.TCP, false);
            end 
            httpreq:on("receive", function(httpreq, pl) 
            --print(pl) 
            end)
            --print("connect connection"); 
            httpreq:connect(80,jeedom_ip)
            httpreq:send("GET /jeedom/core/api/jeeApi.php?apikey="..api_key
            .."&type=virtual&id="..id
            .."&value="..state
            .." HTTP/1.1\r\nHost: "..jeedom_ip
            .."\r\n".."Connection: keep-alive\r\nAccept: */*\r\n\r\n")
            SendingData = false;
end

function Sleep(delay)
     local start= tmr.time();
     while tmr.time() - start < delay do end
end
--

--list files in NODEMCU
l = file.list();
    for k,v in pairs(l) do
      --print("name:"..k..", size:"..v)
      --print(string.find(k, "_conf.lua", 1))
      if((string.find(k, "_conf.lua", 1) ~= nil) and (string.find(k, "node", 1) ~= nil))then
        -- Load conf Jeedom
        --print("load file:"..string.sub(k, 1,string.find(k, ".lua", 1)-1));
        require(string.sub(k, 1,string.find(k, ".lua", 1)-1));
      end
    end
-- for check of conf:
 --print("jeedom_ip:"..jeedom_ip)
 --print("api_key:"..api_key)
 --print("jeedom_virtual_relay_id:"..jeedom_virtual_relay_id)
 --print("jeedom_virtual_pir_id:"..jeedom_virtual_pir_id)

-- led of Wemos D1 Mini on D4 (GPIO2)
if(wemos_led ~= nil) then
  gpio.mode(wemos_led, gpio.OUTPUT);
end
--

-- relay command on D1 (GPIO5)
if(relay1 ~= nil) then
  gpio.mode(relay1,gpio.OUTPUT);
end
--

-- switch 
if(switch1 ~= nil) then
  inInt = false;
  --Send initial state after reboot
  sendDataToVirtual (jeedom_virtual_relay_id, "0");
  --
  
  gpio.mode(switch1,gpio.INPUT,gpio.PULLUP);
  gpio.trig(switch1, "both", function(level)
    if inInt then                   -- don't allow interrupt in interrupt
        return
    else 
        inInt = true
    end
    tmr.delay(100000)               -- 100ms debounce
    if(gpio.read(relay1) == 0)then
      gpio.write(relay1, gpio.HIGH)
      localnewstate = 1
    else
       gpio.write(relay1, gpio.LOW)
       localnewstate = 0
    end
    sendDataToVirtual (jeedom_virtual_relay_id, localnewstate);
    inInt = false                   -- all done, allow interrupts again
 end)
end
--

-- pir 
if(pir1 ~= nil) then
  inIntMotion = false;
  gpio.mode(pir1,gpio.INT,gpio.FLOAT);
  gpio.trig(pir1, "both", function(level)
    if inIntMotion then                   -- don't allow interrupt in interrupt
        return
    else 
        inIntMotion = true
    end
    PirNewstate = gpio.read(pir1);
    tmr.delay(100000)               -- 100ms debounce

    sendDataToVirtual (jeedom_virtual_pir_id, PirNewstate);
 
    inIntMotion = false                   -- all done, allow interrupts again
 end)
--print("pir state:"..gpio.read(pir1));
end
--

-- DS18B20 reading on D4 (GPIO2)
if(OneWirePin ~= nil) then
  -- setup to read temperature
  t = require("ds18b20")
  t.setup(OneWirePin)
  --addrs = t.addrs()
  --if (addrs ~= nil) then
  --   print("Total DS18B20 sensors: "..table.getn(addrs))
  --end
end
--

-- MCP3008 library loading (D5 to D8 is allocated in all cases).
if(MCP3008 ~= nil) then
  -- setup
  require("MCP3008")
end
--

-- CTSensor library loading
if(CTSensor ~= nil) then
  -- setup
  require("CTSensor")
end
--

-- create web server
srv=net.createServer(net.TCP)
srv:listen(80,function(conn)
    conn:on("receive", function(client,request)
        local buf = "";
        local _, _, method, path, vars = string.find(request, "([A-Z]+) (.+)?(.+) HTTP");
        if(method == nil)then
            _, _, method, path = string.find(request, "([A-Z]+) (.+) HTTP");
        end
        local _GET = {}
        if (vars ~= nil)then
            for k, v in string.gmatch(vars, "(%w+)=(%w+)&*") do
                _GET[k] = v
            end
        end
        --print(_GET.pin);
        --print(_GET.state);
        -- to read GPIO
        buf = "<!DOCTYPE HTML>\n";
        buf = buf.."<html>\n";

     if(_GET.state ~= nil)then
         if((_GET.state == "relay1") and (relay1 ~=nil))then
            buf = buf.."<div>"..gpio.read(relay1).."</div>".."\n";
         elseif((_GET.state == "switch1") and (switch1 ~=nil))then
            buf = buf..gpio.read(switch1);
         elseif((_GET.state == "pir1") and (pir1 ~=nil))then
            buf = buf..gpio.read(pir1);       
         elseif((_GET.state == "temp1") and (OneWirePin ~=nil))then
            if(t == nil) then
              -- setup to read temperature
              t = require("ds18b20")
              t.setup(OneWirePin)
            end   
            temperature = t.read();
            if(temperature ~=nil)then
              buf = buf.."<div>"..temperature.."</div>".."\n";
              --buf = buf..temperature.."\n";
            end
         elseif((string.find(_GET.state,"analog") ~= nil) and (MCP3008 ~=nil))then
            channel = tonumber(string.sub(_GET.state,7));
            --print("Debug: Channel value found:"..tostring(channel));
            analogvalue = readMCP3008(channel);
            if(analogvalue ~=nil)then
                buf = buf.."<div>"..analogvalue.."</div>".."\n";
            end
         elseif((string.find(_GET.state,"power") ~= nil) and (MCP3008 ~=nil) and (CTSensor ~=nil))then
            channel = tonumber(string.sub(_GET.state,6));
            --print("Debug: Channel value found:"..tostring(channel));
            power = getPower(channel);
            if(power ~=nil)then
                buf = buf.."<div>"..power.."</div>".."\n";
            end
         end      
      end
      if(_GET.pin ~=nil)then
         -- else write GPIO
        if((_GET.pin == "wemosledon") and (wemos_led ~=nil))then
            gpio.write(wemos_led, gpio.HIGH);
            buf = buf.."1";
        elseif((_GET.pin == "wemosledoff") and (wemos_led ~=nil))then
            gpio.write(wemos_led, gpio.LOW);
            buf = buf.."0";
        elseif((_GET.pin == "relay1on") and (relay1 ~=nil))then
            gpio.write(relay1, gpio.HIGH);
            buf = buf.."1";
            newstate=gpio.read(relay1);
            sendDataToVirtual (jeedom_virtual_relay_id, newstate);
        elseif((_GET.pin == "relay1off") and (relay1 ~=nil))then
            gpio.write(relay1, gpio.LOW);
            buf = buf.."0";
            newstate=gpio.read(relay1);
            sendDataToVirtual (jeedom_virtual_relay_id, newstate);
        end
      end
        -- display web page or not 
        if(web_server_ui == "true")then
            buf = buf.."<head><meta charset=\"UTF-8\"><title>My ESP8266 "..version.."</title></head>\n";
            buf = buf.."<body>\n";
            
            -- to display full web page
            buf = buf.."<h1> ESP8266 Web Server</h1>";
            if(wemos_led ~= nil) then
                buf = buf.."<p>GPIO2 (D4) wemos led   <a href=\"?pin=wemosledoff\"><button>ON</button></a>&nbsp;<a href=\"?pin=wemosledon\"><button>OFF</button></a>";
                buf = buf.." status: "..gpio.read(wemos_led).."</p>\n";
            end
            if(relay1 ~= nil) then
                buf = buf.."<p>GPIO5 (D1) relai <a href=\"?pin=relay1on\"><button>ON</button></a>&nbsp;<a href=\"?pin=relay1off\"><button>OFF</button></a>";
                buf = buf.." status: "..gpio.read(relay1).."</p>\n";
            end    
            if(pir1 ~= nil) then
                buf = buf.."<p>GPIO4 (D2) pir";
                buf = buf.." status: "..gpio.read(pir1).."</p>\n";
            end
            if(switch1 ~= nil) then
                buf = buf.."<p>GPIO0 (D3) switch";
                buf = buf.." status: "..gpio.read(switch1).."</p>\n";
            end
            -- Just read temperature
            --print("Temperature: "..t.read().."'C")
            if(OneWirePin ~= nil) then
                buf = buf.."<p>GPIO2 (D4) OneWire";
                buf = buf.." status: "..t.read().."°C</p>\n";
            end
            if(MCP3008 ~= nil) then
                buf = buf.."<p>GPIO? (D6) MCP3008";
                buf = buf.." status: "..readMCP3008(0).."</p>\n";
                buf = buf.." status: "..readMCP3008(1).."</p>\n";
                buf = buf.." status: "..readMCP3008(2).."</p>\n";
                buf = buf.." status: "..readMCP3008(3).."</p>\n";
                buf = buf.." status: "..readMCP3008(4).."</p>\n";
                buf = buf.." status: "..readMCP3008(5).."</p>\n";
                buf = buf.." status: "..readMCP3008(6).."</p>\n";
                buf = buf.." status: "..readMCP3008(7).."</p>\n"; 
            end
            -- Don't forget to release it after use
            --t = nil
            --ds18b20 = nil
            --package.loaded["ds18b20"]=nil
            buf = buf.."</body>\n";  
        end 
        buf = buf.."</html>\n";
        
        --local _on,_off = "",""
        client:send(buf);
        client:close();
        collectgarbage();
        -- reboot capacity ;-)
        if(_GET.reboot == "on")then
               node.restart();
        end
    end)
end)

5) Le main utilisant aussi une url avec l'IP et la clé api qui va bien mais qui est défini dans un "nodeX_conf.lua(X étant le numéro de votre node sur le réseau, pour pouvoir utiliser des confs de nodes différents ;-). Dans ce fichier on peut aussi activer ou pas des fonctionnalités en fonction de la demande et jouer sur les numéros des entrées-sorties en affectant la valeur du pin ou par 'nil' si non activé :

-- jeedom
api_key = "votre clé api jeedom à mettre ici";
jeedom_ip = "votre ip local de jeedom au format xxx.xxx.xxx.xxx, exemple: 192.168.0.100";

-- virtual jeedom device link to this node (please set to nil if not activated/used)
jeedom_virtual_relay_id = 123; 
jeedom_virtual_pir_id = 234;

-- gpio confs (please set to nil if not activated/used)
-- led of Wemos D1 Mini on D4 (GPIO2)
wemos_led = nil;
-- relay command on D1 (GPIO5)
relay1 = 1;

-- switch reading on D3 (GPIO0)
switch1 = 3;
-- pir reading on D2 (GPIO4)
pir1 = 2;
-- DS18B20 reading on D4 (GPIO2)
OneWirePin = 4;

-- MCP3008 reading (D5 to D8 is allocated in all cases)
MCP3008 = true; -- value nil or different of nil to activate the ressource loading. 
-- to access: http://192.168.0.XXX/?state=analogY where Y is the channel of MCP3008
CTSensor = true; -- value nil or different of nil to activate the ressource loading. 
-- to read CTSensor power: http://192.168.0.XXX/?state=powerY where Y is the channel of the CT Sensor.

-- web server activation (to deactivate and to win memory)
web_server_ui="false";

6) en option si la variable OneWirePin n'est pas à 'nil': le main utilise une libraire pour le ds18b20, ds18b20.lua (qu'il faudra aussi compiler en .lc)


--------------------------------------------------------------------------------
-- DS18B20 one wire module for NODEMCU
-- NODEMCU TEAM
-- LICENCE: http://opensource.org/licenses/MIT
-- Vowstar <vowstar@nodemcu.com>
-- 2015/02/14 sza2 <sza2trash@gmail.com> Fix for negative values
--------------------------------------------------------------------------------

-- Set module name as parameter of require
local modname = ...
local M = {}
_G[modname] = M
--------------------------------------------------------------------------------
-- Local used variables
--------------------------------------------------------------------------------
-- DS18B20 dq pin
local pin = nil
-- DS18B20 default pin
local defaultPin = 9
--------------------------------------------------------------------------------
-- Local used modules
--------------------------------------------------------------------------------
-- Table module
local table = table
-- String module
local string = string
-- One wire module
local ow = ow
-- Timer module
local tmr = tmr
-- Limited to local environment
setfenv(1,M)
--------------------------------------------------------------------------------
-- Implementation
--------------------------------------------------------------------------------
C = 0
F = 1
K = 2
function setup(dq)
  pin = dq
  if(pin == nil) then
    pin = defaultPin
  end
  ow.setup(pin)
end

function addrs()
  setup(pin)
  tbl = {}
  ow.reset_search(pin)
  repeat
    addr = ow.search(pin)
    if(addr ~= nil) then
      table.insert(tbl, addr)
    end
    tmr.wdclr()
  until (addr == nil)
  ow.reset_search(pin)
  return tbl
end

function readNumber(addr, unit)
  result = nil
  setup(pin)
  flag = false
  if(addr == nil) then
    ow.reset_search(pin)
    count = 0
    repeat
      count = count + 1
      addr = ow.search(pin)
      tmr.wdclr()
    until((addr ~= nil) or (count > 100))
    ow.reset_search(pin)
  end
  if(addr == nil) then
    return result
  end
  crc = ow.crc8(string.sub(addr,1,7))
  if (crc == addr:byte(8)) then
    if ((addr:byte(1) == 0x10) or (addr:byte(1) == 0x28)) then
      -- print("Device is a DS18S20 family device.")
      ow.reset(pin)
      ow.select(pin, addr)
      ow.write(pin, 0x44, 1)
      -- tmr.delay(1000000)
      present = ow.reset(pin)
      ow.select(pin, addr)
      ow.write(pin,0xBE,1)
      -- print("P="..present)
      data = nil
      data = string.char(ow.read(pin))
      for i = 1, 8 do
        data = data .. string.char(ow.read(pin))
      end
      -- print(data:byte(1,9))
      crc = ow.crc8(string.sub(data,1,8))
      -- print("CRC="..crc)
      if (crc == data:byte(9)) then
        t = (data:byte(1) + data:byte(2) * 256)
        if (t > 32767) then
          t = t - 65536
        end

        if (addr:byte(1) == 0x28) then
          t = t * 625  -- DS18B20, 4 fractional bits
        else
          t = t * 5000 -- DS18S20, 1 fractional bit
        end

        if(unit == nil or unit == 'C') then
          -- do nothing
        elseif(unit == 'F') then
          t = t * 1.8 + 320000
        elseif(unit == 'K') then
          t = t + 2731500
        else
          return nil
        end
        t = t / 10000
        return t
      end
      tmr.wdclr()
    else
    -- print("Device family is not recognized.")
    end
  else
  -- print("CRC is not valid!")
  end
  return result
end

function read(addr, unit)
  t = readNumber(addr, unit)
  if (t == nil) then
    return nil
  else
    return t
  end
end

-- Return module table
return M

7) en option si la variable MCP3008 est égale à 'true': le main utilise une libraire pour le MCP3008 (convertiseur analogique/numérique), MCP3008.lua (qu'il faudra aussi compiler en .lc)


MISO = 6           --> D6
MOSI =  7         --> D7
CLK = 5            --> D5
CS = 8              --> D8

-- Pin Initialization
gpio.mode(CS, gpio.OUTPUT)
gpio.mode(CLK, gpio.OUTPUT)
gpio.mode(MOSI, gpio.OUTPUT)
gpio.mode(MISO, gpio.INPUT)

-- Function to read MCP3008
function readMCP3008(adc_ch)
   
   if (tonumber(adc_ch) >=0) and (tonumber(adc_ch) < 8) then
      -- MCP3008 has eight channels 0-8

      gpio.write(CS,gpio.HIGH)
       tmr.delay(5)  
        
       gpio.write(CLK, gpio.LOW)  
          gpio.write(CS, gpio.LOW)      -->Activate the chip 
       tmr.delay(1)                  -->1us Delay
      
       commandout = adc_ch
       commandout=bit.bor(commandout, 0x18) 
        commandout=bit.lshift(commandout, 3)
        for i=1,5 do
          if bit.band(commandout,0x80) >0 then
               gpio.write(MOSI, gpio.HIGH)    
          else   
               gpio.write(MOSI, gpio.LOW) 
           end   
            commandout=bit.lshift(commandout,1)
            
            gpio.write(CLK, gpio.HIGH)
            tmr.delay(1)
            gpio.write(CLK, gpio.LOW)
            tmr.delay(1)       
      end
      adcout = 0
      for i=1,12 do
            gpio.write(CLK, gpio.HIGH)
            tmr.delay(1)  
            gpio.write(CLK, gpio.LOW)
            tmr.delay(1)  
             adcout = bit.lshift(adcout,1);
             if gpio.read(MISO)>0 then
                 adcout = bit.bor(adcout, 0x1)
            end
        end 
        gpio.write(CS, gpio.HIGH)
        adcout = bit.rshift(adcout,1)     
        return adcout
     else 
        return -1
     end
end


8) en option  si la variable CTSencor égale à 'true': le main utilise une libraire pour le capteur de courant (convertiseur analogique/numérique), CTSensor.lua (qu'il faudra aussi compiler en .lc)


BURDEN_R = 22.9     --> Resistance in Ohms of Burden Resistor
CT_RATIO  = 2000.0  --> Ratio of Primary:Secondary coils/current
AC_VOLTAGE = 230.0  --> AC Supply Voltage
NUM_SAMPLES = 100  --> Number of samples to take
supplyVoltage = 3.30 --> Get current Vcc voltage (this is applied to AREF on the MCP3008)
 
-- Calculate apparent power from current sensor
function getPower(MCP3008_channel)
  -- Calculate the ratio of ADC step to measured current
  ratio = (CT_RATIO / BURDEN_R) * (supplyVoltage / 1024)
  Irms = 0
  sample = 0
  filtered = 0
  squared = 0
  sum = 0

  -- For 10bit ADC offset = 512 (i.e. half of 1024)
  offset = 512
  
 for i=1,NUM_SAMPLES do
    sample = readMCP3008(MCP3008_channel)
    -- Digital low pass filter extracts the 2.5 V or 1.65 V dc offset,
    --  then subtract this - signal is now centered on 0 counts.
    offset = (offset + (sample - offset) / 1024)
    filtered = sample - offset
    squared = filtered * filtered
    sum = sum + squared
 end
  
  -- Calculate average RMS current
  Irms = ratio * math.sqrt(sum / NUM_SAMPLES)
  
  -- Return output power in KWatts.
  return ((AC_VOLTAGE * Irms)/1000);
end

Bon c'est tout, désolé, cela fait beaucoup pour une première fois mais j'ai préféré faire ainsi et tout vous mettre. A vous de prendre que ce qui est utile pour vous ;-)

Je vous parlerais de hard ;-) plus tard mais manque de temps cela sera pour une prochaine fois :-(
Je peux juste dire pour 'teaser' que j'en suis dans les 15 € pour mon "IoT" multi-capteurs !



Aucun commentaire :

Enregistrer un commentaire