Send SMS messages via Huawei LTE modem API (tested with E3372)

There are many scripts written here, but there is no system, you can get confused … Is it possible to ask the authors to put this chaos in order?
As far as I understand, there are ready-made functions:

:global tokenParser - main API parser
:global sendSMS - function send SMS
:global delSMS - function delete SMS
:global ASCIItoCP1252toUTF8 (or ASCIItoCP12XXtoUTF8) - recoding function (depends on which national alphabet is used)
… + script for enumerating messages in a loop with sending SMS in some way … (e-mail, telegram, sms, etc…)

As I see it, it is necessary to make a library of functions that would be able to:

  • send an arbitrary SMS,
  • get a list of incoming SMS,
  • get the Nth SMS from this list into a variable,
  • get a list of sent SMS,
  • get the Nth SMS from the list of sent SMS into a variable,
  • delete an arbitrary SMS by number from the list of incoming/outgoing SMS

Based on this, you can do whatever you want. Maybe someone will take it and make it “turnkey”?

Hi @pkt,

Starting from this: 1 for example.

How can I do to execute an action if is >0.

I have started like this:


[admin@MikroTik] > :put ([$recvSMS]->“data”)

<?xml version="1.0" encoding="UTF-8"?> 1 0 40000 +34XXXXXXXXX Lorem ipsum 2023-02-08 16:44:19 0 0 1

{
:global tokenParser
:global recvSMS
:local xmlSmsList ([$recvSMS]->"data")
:put [($tokenParser->"getTag") source=$xmlSmsList tag="Count"]
}
Output:  1

thanks.

You can not demand that the information in the thread is ordered to your liking, especially when no one has been able to solve the Mikrotik compatibility with the Huawei API, until the comrade @pkt arrived and thanks to his help the light was made (he is a very busy person and helps when he can).

Chaos? of course, right now the posts are the result of tests and more tests until it is finally working as it should. You should read from the beginning to find out everything.

Anyway, I’m going to try to sort out the content of the main functions and script.

By the way, this thread is NOT MINE, I’m just trying to understand and collaborate.

I also found out that I could not have reflashed my modem, since it turned out that the API is available only if the user password for entering the WEB interface settings is not set. As soon as I reset the password on my old firmware, these Mikrotik SMS functions worked. I wonder if it is possible to work with SMS with a “secure” API - there should be the possibility of authorization to access the API …

Yes, I understand all the difficulties and the amount of work done. Many thanks to all authors. Let’s hope that someday we will get a good library of functions.

Collection of Huawei API scripts for use with MikroTik
(Tested on a Huawei E3372h-320 modem)

Token Parser Library
(Main Script base)

# TOKEN PARSER LIBRARY
# v1.0.0 pkt

# :put [($tokenParser->"getTag") source=$xml tag="SessionInfo"]

# ($tokenParser->"getBetween")
#  get delimited value
#    source - source string
#    fromTok - (optional) text AFTER this token (or from source beginning) will be returned
#    toTok - (optional) text BEFORE this token (or until source finish) will be returned
#    startPos - (optional) start position (default 0 = beginning)
#  returns an array with fields data and pos
# 
# ($tokenParser->"getTag")
#  get value for XML tag
#    source - xml string
#    tag - tag which value is to be returned
#    startPos - (optional, text index) start position (if not specified, it will search from the beginning of the string)
#  returns the tag content
# 
# ($tokenParser->"getTagDetailed")
#  get value and end position for XML tag
#    source - xml string
#    tag - tag which value is to be returned
#    startPos - (optional, text index) start position (if not specified, it will search from the beginning of the string)
#  returns an array with fields "data" (tag content) and "pos" (where tag ends)
# 
# ($tokenParser->"getTagList")
#  get a list with the content of each appearance of tag
#    source - xml string
#    tag - tag which value is to be returned
#  returns an array with tag contents
# 
# ($tokenParser->"forEachTag")
#  incremental parser that calls the callback with the content of each appearance of tag
#    source - xml string
#    tag - tag which value is to be returned
#    callback - callback (with param content) to be called for each appearance of tag
#    callbackArgs - callback will be called passing this value in param args


:global tokenParser ({})

:set ($tokenParser->"getBetween") do={ # get delimited value
  # source - source string
  # fromTok - (optional) text AFTER this token (or from source beginning) will be returned
  # toTok - (optional) text BEFORE this token (or until source finish) will be returned
  # startPos - (optional) start position (default 0 = beginning)
  
  # returns an array with fields data and pos
  # if fromTok and/or toTok are specified and neither of them appear in source, empty string "" will be returned as data

  # based on function getBetween by CuriousKiwi, modified by pkt

  :local posStart
  if ([:len $startPos] = 0) do={
    :set posStart -1
  } else={
    :set posStart ($startPos-1)
  }

  :local found true
  :local data 

  :local resultStart
  :if ([:len $fromTok] > 0) do={
    :set resultStart [:find $source $fromTok $posStart]
    :if ([:len $resultStart] = 0) do={ # start token not found
      :set found false
      :set data ""
    }
    :set resultStart ($resultStart + [:len $fromTok])
  } else={
    :set resultStart 0
  }

  :local resultEnd
  :if (found = true && [:len $toTok] > 0) do={
    :set resultEnd [:find $source $toTok ($resultStart-1)]
    :if ([:len $resultEnd] = 0) do={ # end token not found
      :set found false
      :set data ""
    }
  } else={
    :set resultEnd [:len $source]
  }

  :if ($found = true) do={ :set data [:pick $source $resultStart $resultEnd] }

  :return { data=$data; pos=$resultEnd }
}

:set ($tokenParser->"getTag") do={ # get value for XML tag
  # source - xml string
  # tag - tag which value is to be returned
  # startPos - (optional, text index) start position (if not specified, it will search from the beginning of the string)

  # returns the tag content

  :global tokenParser
  :return ([($tokenParser->"getBetween") source=$source fromTok=("<$tag>") toTok=("</$tag>") startPos=$startPos]->"data")
}

:set ($tokenParser->"getTagDetailed") do={ # get value and end position for XML tag
  # source - xml string
  # tag - tag which value is to be returned
  # startPos - (optional, text index) start position (if not specified, it will search from the beginning of the string)

  # returns an array with fields "data" (tag content) and "pos" (where tag ends)

  :global tokenParser
  :return [($tokenParser->"getBetween") source=$source fromTok=("<$tag>") toTok=("</$tag>") startPos=$startPos]
}

:set ($tokenParser->"getTagList") do={ # get a list with the content of each appearance of tag
  # source - xml string
  # tag - tag which value is to be returned

  # returns an array with tag contents

  :global tokenParser

  :local result ({})
  :local doneTags false
  :local startPos 0

  :do {
    :local tagContent [($tokenParser->"getTagDetailed") source=$source tag=$tag startPos=$startPos]

    :local content ($tagContent->"data")
    :if ($content != "") do={
      :set ($result->[:len $result]) $content

      # advance start pos to search for next tag
      :set startPos ($tagContent->"pos")
    } else={
      :set doneTags true
    }
  } while=($doneTags = false)

  :return $result
}

:set ($tokenParser->"forEachTag") do={ # incremental parser that calls the callback with the content of each appearance of tag
  # source - xml string
  # tag - tag which value is to be returned
  # callback - callback (with param content) to be called for each appearance of tag
  # callbackArgs - callback will be called passing this value in param args

  :global tokenParser

  :local doneTags false
  :local startPos 0

  :do {
    :local tagContent [($tokenParser->"getTagDetailed") source=$source tag=$tag startPos=$startPos]

    :local content ($tagContent->"data")
    :if ($content != "") do={
      [$callback tagContent=$content args=$callbackArgs]

      # advance start pos to search for next tag
      :set startPos ($tagContent->"pos")
    } else={
      :set doneTags true
    }
  } while=($doneTags = false)
}

Function to get a list of SMS messages

:global recvSMS do={
  :local lteIP "192.168.8.1"

  :global tokenParser

  # get SessionID and Token via LTE modem API
  :local urlSesTokInfo "http://$lteIP/api/webserver/SesTokInfo"
  :local api [/tool fetch $urlSesTokInfo output=user as-value]
  :local apiData  ($api->"data")

  # parse SessionID and Token from API session data 
  :local apiSessionID [($tokenParser->"getTag") source=$apiData tag="SesInfo"]
  :local apiToken [($tokenParser->"getTag") source=$apiData tag="TokInfo"]

  # header and data config
  :local apiHead "Content-Type:text/xml,Cookie: $apiSessionID,__RequestVerificationToken:$apiToken"
  :local recvData "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><PageIndex>1</PageIndex><ReadCount>20</ReadCount><BoxType>1</BoxType><SortType>0</SortType><Ascending>0</Ascending><UnreadPreferred>1</UnreadPreferred></request>"

  # recv SMS via LTE modem API with fetch
  :return [/tool fetch http-method=post http-header-field=$apiHead url="http://$lteIP/api/sms/sms-list" http-data=$recvData output=user as-value]
}

Function to delete SMS messages (Inbox)

:global delSMS do={
  :local lteIP "192.168.8.1"

  :global tokenParser

  # get SessionID and Token via LTE modem API
  :local urlSesTokInfo "http://$lteIP/api/webserver/SesTokInfo"
  :local api [/tool fetch $urlSesTokInfo output=user as-value]
  :local apiData  ($api->"data")

  # parse SessionID and Token from API session data 
  :local apiSessionID [($tokenParser->"getTag") source=$apiData tag="SesInfo"]
  :local apiToken [($tokenParser->"getTag") source=$apiData tag="TokInfo"]

  # header and data config
  :local apiHead "Content-Type:text/xml,Cookie: $apiSessionID,__RequestVerificationToken:$apiToken"
  :local delData "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><Index>$index</Index></request>"

  # delete SMS via LTE modem API with fetch
  :return [/tool fetch http-method=post http-header-field=$apiHead url="http://$lteIP/api/sms/delete-sms" http-data=$delData output=user as-value]
}

Script to delete SMS messages (Sentbox)

:global recvSMSsentbox do={
  :local lteIP "192.168.8.1"

  :global tokenParser

  # get SessionID and Token via LTE modem API
  :local urlSesTokInfo "http://$lteIP/api/webserver/SesTokInfo"
  :local api [/tool fetch $urlSesTokInfo output=user as-value]
  :local apiData  ($api->"data")

  # parse SessionID and Token from API session data 
  :local apiSessionID [($tokenParser->"getTag") source=$apiData tag="SesInfo"]
  :local apiToken [($tokenParser->"getTag") source=$apiData tag="TokInfo"]

  # header and data config
  :local apiHead "Content-Type:text/xml,Cookie: $apiSessionID,__RequestVerificationToken:$apiToken"
  :local recvData "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><PageIndex>1</PageIndex><ReadCount>20</ReadCount><BoxType>2</BoxType><SortType>0</SortType><Ascending>0</Ascending><UnreadPreferred>1</UnreadPreferred></request>"

  # delete SMS via LTE modem API with fetch
  :return [/tool fetch http-method=post http-header-field=$apiHead url="http://$lteIP/api/sms/sms-list" http-data=$recvData output=user as-value]
}

:global tokenParser
:global delSMS
:local xmlSmsList ([$recvSMSsentbox]->"data")
:local smsList [($tokenParser->"getTagList") source=$xmlSmsList tag="Message"]
:local smsCount [:tonum [($tokenParser->"getTag") source=$xmlSmsList tag="Count"]]

:if ($smsCount > 0) do={

:delay 5s
:foreach tagContent in=$smsList do={
  :local index [($tokenParser->"getTag") source=$tagContent tag="Index"]
  [$delSMS index=$index]
}
}

Function to send SMS messages

:global sendSMS do={
	# Example:	
	# :put [$sendSMS lteIP="192.168.8.1" phone="+34XXXXXXXXX" sms="test sms via lte api"]

    :local lteIP "192.168.8.1"

    :global tokenParser

    # get SessionID and Token via LTE modem API
    :local urlSesTokInfo "http://$lteIP/api/webserver/SesTokInfo"
    :local api [/tool fetch $urlSesTokInfo output=user as-value]
    :local apiData  ($api->"data")
      
    # parse SessionID and Token from API session data 
    :local apiSessionID [($tokenParser->"getTag") source=$apiData tag="SesInfo"]
    :local apiToken [($tokenParser->"getTag") source=$apiData tag="TokInfo"]

	# header and data config
	:local apiHead "Content-Type:text/xml,Cookie: $apiSessionID,__RequestVerificationToken:$apiToken"
	:local sendData "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><Index>-1</Index><Phones><Phone>$phone</Phone></Phones><Sca></Sca><Content>$sms</Content><Length>-1</Length><Reserved>1</Reserved><Date>-1</Date></request>"

	# send SMS via LTE modem API with fetch
    :return [/tool fetch http-method=post http-header-field=$apiHead url="http://$lteIP/api/sms/send-sms" http-data=$sendData output=user as-value]
}

Script to get a list of SMS messages from Inbox, forward them by email and SMS, then delete them.

:global tokenParser
:global recvSMS
:global delSMS
:global sendSMS
:local xmlSmsList ([$recvSMS]->"data")
:local smsList [($tokenParser->"getTagList") source=$xmlSmsList tag="Message"]
:local smsCount [:tonum [($tokenParser->"getTag") source=$xmlSmsList tag="Count"]]

:if ($smsCount > 0) do={

:foreach tagContent in=$smsList do={

  :local index [($tokenParser->"getTag") source=$tagContent tag="Index"]
  :local date [($tokenParser->"getTag") source=$tagContent tag="Date"]
  :local phone [($tokenParser->"getTag") source=$tagContent tag="Phone"]
  :local content [($tokenParser->"getTag") source=$tagContent tag="Content"]
  :local read ([($tokenParser->"getTag") source=$tagContent tag="Smstat"] = 1)

  :if ($content != "") do={
    :put "$index $read $date $phone $content"    
    /tool e-mail send to=user@mail.com subject="SMS $phone" body="$index $read $date $phone $content"    
    :execute [$sendSMS lteIP="192.168.8.1" phone="+34XXXXXXXXX" sms="$phone $content"]
  }
}

:delay 5s
:foreach tagContent in=$smsList do={
  :local index [($tokenParser->"getTag") source=$tagContent tag="Index"]
  [$delSMS index=$index]
}
}

TIP:

Change DHCP server IP address range (modem) to be able to configure a single IP for the Mikrotik DHCP client.

  1. Run the following script to create the global variable dhcpSMS.
  2. From the Terminal type:
:put [$dhcpSMS lteIP="192.168.8.1" startIP="192.168.8.100" endIP="192.168.8.100"]
  1. Done! you have now changed the IP address range.

Script contents:

:global dhcpSMS do={
   
    # Example:	
    # :put [$dhcpSMS lteIP="192.168.8.1" startIP="192.168.8.100" endIP="192.168.8.100"]
    
    :global tokenParser

    # get SessionID and Token via LTE modem API
    :local urlSesTokInfo "http://$lteIP/api/webserver/SesTokInfo"
    :local api [/tool fetch $urlSesTokInfo output=user as-value]
    :local apiData  ($api->"data")
      
    # parse SessionID and Token from API session data 
    :local apiSessionID [($tokenParser->"getTag") source=$apiData tag="SesInfo"]
    :local apiToken [($tokenParser->"getTag") source=$apiData tag="TokInfo"]

    # header and data config
    :local apiHead "Content-Type:text/xml,Cookie: $apiSessionID,__RequestVerificationToken:$apiToken"
    :local dhcpData "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><DnsStatus>1</DnsStatus><DhcpStartIPAddress>$startIP</DhcpStartIPAddress><DhcpIPAddress>192.168.8.1</DhcpIPAddress><accessipaddress></accessipaddress><homeurl>hi.link</homeurl><DhcpStatus>1</DhcpStatus><DhcpLanNetmask>255.255.255.0</DhcpLanNetmask><SecondaryDns>192.168.8.1</SecondaryDns><PrimaryDns>192.168.8.1</PrimaryDns><DhcpEndIPAddress>$endIP</DhcpEndIPAddress><DhcpLeaseTime>86400</DhcpLeaseTime></request>"
    
    # change IP address range with fetch
    :return [/tool fetch http-method=post http-header-field=$apiHead url="http://$lteIP/api/dhcp/settings" http-data=$dhcpData output=user as-value]
}

Enjoy!.

AFAIK, user-password authentication in HiLink returns access tokens in the headers of the http response and /tool/fetch has no way to get them, so it’s actually no possible to use this type of authentication with RoS scripts.

I have suggested a new feature to Mikrotik to enhance /tool/fetch to return also response headers: SUP-107232.



Let’s hope that someday we will get a good library of functions.

I quote myself:

I am working (in my very limited spare time) in a more powerful library for HiLink access in RouterOS scripts that will try to solve these problems, call a function when new sms arrives, and even implements automatic execution of RoS scripts when receiving sms with :cmd, as Mikrotik’s own SMS support do.

I think this will work even if Count tag is empty or non-existent:

:global tokenParser
:global recvSMS

:local xmlSmsList ([$recvSMS]->"data")

:local smsCount [:tonum [($tokenParser->"getTag") source=$xmlSmsList tag="Count"]]

:if ($smsCount > 0) do={
  ...
}

Many thanks to diamuxin for “ordering” the developments so far and pkt for his great contribution! We look forward to your more powerful library.

I still have questions:

  1. what sign can be used to understand whether the API firmware supports or not to write a “check” function whether a particular modem has an API in the firmware or not. and
  2. If access to the WEB is password protected, then the above functions (I only checked on sendSMS) work, but do nothing - in particular, SMS is not sent. Is it possible to somehow get a sign that password authorization is enabled in order to inform RouterOS (in the log, for example) that the functions are not possible and it should remove the password

By the way, Huawei E3372h-320 modem is much worse than the earlier model E3372h-153. Modification 320 does not have an SD slot, does not have the ability to connect external antennas and does not have the ability to change the firmware to alternative ones.

You are totally wrong (and more if you haven’t tried it), it is a newer model than the previous one and it is improved in LTE frequencies, allowing Cat6 with speeds up to 150/50 Mb/s.

It does have space for 2 antennas, but I don’t need them (95% coverage with my operator).

It doesn’t have a SD slot (only older models do) but I don’t need it either.

As for the firmware, it is not necessary to modify it (or turn it into a slow modem) as it is 100% compatible with the Huawei API and for me it is enough.

It is not a question of whether a device IS BETTER OR WORSE if not that it meets my needs, for me it is enough.

I advise you that instead of demanding so much or asking meaningless questions, you get down to work, investigate and contribute something interesting to the forum.

Thank you very much, that’s exactly what I needed.
– I have already updated the script collection.

BR.

Can someone explain procedure of deploy script and using it. I am little confuse how to get any of this script work in practice.
like:
create script
name script
run script
output result.

I didn’t mean to offend you or “offend your modem”. And my contribution to the scripts is here:

https://forummikrotik.ru/viewtopic.php?f=14&t=13947

Now I am busy writing my own Telegram Notifier and Runner system, which differs from the existing works. I am also writing a universal function FuncAS, capable of sending messages through different systems and hardware (you can read here https://habr.com/ru/post/646663/, it will just be replenished with your Huawey sendSMS) and I am working on the SSH-exec library. All this takes time, and I’m just a doctor… I see no reason for me to get into your library “SMS Huawey” You will do fine there without me.
You have already written many wonderful scripts and the community is very grateful to you for this.

And E3372h-320 is still worse than E3372h-153 (at least for those who like to break protection and make their own firmware for modems). Modification 320 is not stitched, so we have less interest in it and the price is two times lower compared to modification 153.
I also have an E3372h-320. As for the speeds, maybe it’s faster, we really don’t have Cat6 speeds.
Most of all, we value the router modem E8372 with wifi and native Huawey firmware. There are very few new ones left and they cost us about $ 200. But for our purposes, as I understand, they are not suitable, because they wrote above that it is impossible to access the API of such a modem from the Router OS.

No comments.

..

Exactly :slight_smile:

By the way, the smsSend function sends SMS, putting them in the list of sent messages with a wrong date (I have all the messages sent via the API dated December 31, 1969. If the messages from the WEB interface are sent correctly).

It works fine for me.


[admin@MikroTik] > $sendSMS lteIP="192.168.8.1" phone="+34XXXXXXXXX" sms="Lorem Ipsum Epsilon"

# SMS Outbox:

<?xml version="1.0" encoding="UTF-8"?>
<response>
	<Count>1</Count>
	<Messages>
		<Message>
			<Smstat>3</Smstat>
			<Index>40000</Index>
			<Phone>+34XXXXXXXXX</Phone>
			<Content>Lorem Ipsum Epsilon</Content>
			<Date>2023-02-09 20:58:45</Date>  # OK
			<Sca></Sca>
			<SaveType>3</SaveType>
			<Priority>4</Priority>
			<SmsType>1</SmsType>
		</Message>
	</Messages>
</response>

..

no, I have shows 23:59:59 on 31.12.1969 in all sent SMS

there are 153 modifications with different firmware on three modems. I’ll try to unsubscribe on 320

Have you looked at the WEB interface itself ?

Hi @pkt !

Did you manage to create the library you mentioned in the post?
Have you made progress on this issue?

BR.