MikroTik

MikroTik integration #

MikroTik API library #

Creating api user #

On the MikroTik router go to System > Users.

Open the Groups tab and create a new group named api with read and api access rights.

Open the Users tab and create a new user named api and set the group to api.

For improved security you specify LogicMachine IP in the Allowed Address list.

User library #

Go to Scripting > User libraries and create a new user library called mikrotik. Copy/paste the following code into the scripting editor.

local bnot, band, bor = bit.bnot, bit.band, bit.bor
local lshift, rshift = bit.lshift, bit.rshift
local char = string.char

local socket = require('socket')

local DEFAULT_PORT = 8728
local DEFAULT_TIMEOUT = 5

local function byte(int, idx)
  local shifted = rshift(int, 8 * idx)
  return band(shifted, 0xff)
end

local function parseword(word)
  local _, pos = word:find('.=')

  if not pos then
    return '_type', word
  end

  local tag = word:sub(2, pos - 1)
  local value = word:sub(pos + 1)

  return tag, value
end

local function encodelength(l)
  if l < 0x80 then
    return char(l)
  elseif l < 0x4000 then
    local l = bor(l, 0x8000)
    return
      char(byte(l, 1)) ..
      char(byte(l, 0))
  elseif l < 0x200000 then
    local l = bor(l, 0xC00000)
    return
      char(byte(l, 2)) ..
      char(byte(l, 1)) ..
      char(byte(l, 0))
  elseif l < 0x10000000 then
    local l = bor(l, 0xE0000000)
    return
      char(byte(l, 3)) ..
      char(byte(l, 2)) ..
      char(byte(l, 1)) ..
      char(byte(l, 0))
  else
    return
      '\xF0' ..
      char(byte(l, 3)) ..
      char(byte(l, 2)) ..
      char(byte(l, 1)) ..
      char(byte(l, 0))
  end
end

local _M = {}
_M.__index = _M

function _M.new(host, port, timeout)
  local mtk = {}
  setmetatable(mtk, _M)

  local sock = socket.tcp()

  sock:settimeout(timeout or DEFAULT_TIMEOUT)

  local res, err = sock:connect(host, port or DEFAULT_PORT)
  if not res then
    return nil, err
  end

  mtk.sock = sock

  return mtk
end

function _M:close()
  if self.sock then
    self.sock:close()
    self.sock = nil
  end
end

function _M:readbyte()
  local b = assert(self.sock:receive(1))
  return b:byte(1)
end

function _M:readlength()
  local l = self:readbyte()
  if band(l, 0x80) == 0x00 then
    return l
  elseif band(l, 0xc0) == 0x80 then
    l = band(l, bnot(0xc0))

    return
      lshift(l, 8) +
      self:readbyte()
  elseif band(l, 0xe0) == 0xc0 then
    l = band(l, bnot(0xc0))

    return
      lshift(l, 16) +
      lshift(self:readbyte(), 8) +
      self:readbyte()
  elseif band(l, 0xf0) == 0xe0 then
    l = band(l, bnot(0xf0))

    return
      lshift(l, 24) +
      lshift(self:readbyte(), 16) +
      lshift(self:readbyte(), 8) +
      self:readbyte()
  elseif band(l, 0xf8) == 0xf0 then
    return
      lshift(self:readbyte(), 24) +
      lshift(self:readbyte(), 16) +
      lshift(self:readbyte(), 8) +
      self:readbyte()
  end
end

function _M:send(...)
  local message = { ... }

  for i, word in ipairs(message) do
    message[ i ] = encodelength(#word) .. word
  end

  message[ #message + 1 ] = '\0'

  return self.sock:send(table.concat(message))
end

function _M:readword()
  local stat, len = pcall(self.readlength, self)

  if not stat then
    return nil, 'timeout'
  end

  if len == 0 then
    return nil, 'done'
  end

  return self.sock:receive(len)
end

function _M:read()
  local sentence = {}

  while true do
    local word, err = self:readword()

    if not word then
      if err == 'done' then
        return sentence
      else
        return nil, err
      end
    end

    local tag, value = parseword(word)
    sentence[ tag ] = value
  end

  return sentence
end

function _M:readlist(...)
  local res, err, list

  res, err = self:send(...)
  if not res then
    return nil, err
  end

  list = {}

  while true do
    res, err = self:read()
    if res then
      if res._type == '!done' then
        break
      elseif res._type == '!trap' then
        return nil, res.message or 'read failed'
      else
        list[ #list + 1 ] = res
      end
    else
      return nil, err
    end
  end

  return list
end

function _M:login(user, pass)
  self:send('/login', '=name=' .. user, '=password=' .. pass)

  local res, err = self:read()
  if res then
    if res._type == '!done' then
      return true
    else
      return nil, res.message or 'login failed'
    end
  else
    return nil, err
  end
end

return _M

Example #

Get a list of currently connected WiFi clients.

Change ip, user and password as needed.

ip = '192.168.1.1'
user = 'api'
password = '123456'

mt, err = require('user.mikrotik').new(ip)

if mt then
  res, err = mt:login(user, password) -- user api, pass 123456

  if res then
    -- get wireless registration table
    res, err = mt:readlist('/interface/wireless/registration-table/print')
    log(res)
  else
    log('login failed', tostring(err))
  end

  mt:close()
else
  log('connect failed', tostring(err))
end

Further assistance can be found in this forum thread

Activate LogicMachine scene from MikroTik #

This example runs the I am home scene when the router detects that your phone is connected to the router WiFi.

MikroTik scripting and LogicMachine Remote services are used for this.

Check your WiFi device MAC #

Check your phone or your family member’s phone MAC addresses. In Android you can see it in Settings > About phone > Status . In iOS you can see it in Settings > General > About

Enable Remote service on LogicMachine #

Go to System config > Services > Remote services

scene_on_mikrotik logicmachine

WiFi client detection script on Mikrotik RouterOS #

Go to System > Scheduler and add the following script. It will check the existence of two MAC addresses and activate the group address 1/1/1 on LogicMachine with IP 192.168.1.13 (in this example login=remote, password=11111111). In case one of the WiFi devices is connected to the Mikrotik router, 1 will be sent to 1/1/1. If both are disconnected – 0 is sent to 1/1/1. You can also adjust the Scheduler interval, how often you want the script to check the WiFi registration table.

{
  :local exists [/int wire reg find mac-address="78:02:F8:7E:96:01"];
  :local exists2 [/int wire reg find mac-address="B0:E2:35:CF:46:48"];
  :if ($exists!="" or $exists2!="") do={
     /tool fetch url="http://remote:11111111@192.168.1.13/scada-remote" http-data="m=json&r=grp&fn=write&alias=1/1/1&value=1" http-method=post;
   }  else={
     /tool fetch url="http://remote:11111111@192.168.1.13/scada-remote" http-data="m=json&r=grp&fn=write&alias=1/1/1&value=0" http-method=post;
   }
}

scene_on_mikrotik mikrotik

Event-based script for scene group address #

Add event-based script for I am home group address 1/1/1. On each received telegram to this group address, the script will activate groups 1/1/2 and 1/1/3 if both of the following conditions are true:

  • if the previous value for 1/1/1 was false (the scene haven't already been run)

  • the scene has not been executed in the past 5 hours

value = event.getvalue()
prev_value = storage.get('prev_value')

if value then
  if not prev_value then
    off_time = storage.get('last_off_time', 0)
    delta = os.time() - off_time

    if delta > (5 * 60 * 60) then
      grp.write('1/1/2', true)
      grp.write('1/1/3', true)
    end
  end
else
  storage.set('last_off_time', os.time())
end

storage.set('prev_value', value)