效果:效果视频
本文讲述到的器材:Echo Dot(当然也可以接入其他设备),极路由1S(已经开启极客模式,理论上只要能进入ssh的路由器都可以),小米网关,温湿度传感器
此文的方法是DIY一个测试版Skill,会将设备直接暴露在公网上,安全性很差,而且账户认证什么的都是写死的,只适合自己玩,而且外网接口千万不要外泄。
原文地址:http://blog.csdn.net/luhanglei/article/details/60140972
1.首先,把小米网关的“开发者模式”打开(小米网关页面→更多(三个点)→关于→狂点插件版本号),到这里网关就可以被发现且局域网控制了。
2.然后下载绿米官方的通信文档:点击这里下载通信文档
发现设备,把以下java代码放进eclipse里跑就可以看见局域网内的网关信息
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;public class Discover {public static void main(String[] args) throws IOException, InterruptedException {// TODO 自动生成的方法存根InetAddress ia = InetAddress.getByName("224.0.0.50");final MulticastSocket clientSocket = new MulticastSocket();clientSocket.joinGroup(ia);new Thread() {@Overridepublic void run() {try {listen(clientSocket);} catch (IOException e) {e.printStackTrace();}}}.start();StringBuffer sb = new StringBuffer();sb.append("{\"cmd\":\"whois\"}");DatagramPacket sendPacket = new DatagramPacket(sb.toString().getBytes(), sb.toString().length(), ia, 4321);// 1982clientSocket.send(sendPacket);}static void listen(DatagramSocket clientSocket) throws IOException {byte[] receiveData = new byte[1024];DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);clientSocket.receive(receivePacket);String response = new String(receivePacket.getData());System.out.println(response);System.exit(0);}}
应答信息里有网关的IP和端口号
发送单播信息获取设备列表,同样是java代码放进去跑,需要把其中的ip和端口号换掉,也就是把“IPIPIP”和“PORT”替换掉
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;public class Gateway {public static void main(String[] args) throws IOException, InterruptedException {// TODO 自动生成的方法存根InetAddress ia = InetAddress.getByName("IPIPIP");// final MulticastSocket clientSocket = new MulticastSocket();final DatagramSocket clientSocket = new DatagramSocket();// clientSocket.joinGroup(ia);new Thread() {@Overridepublic void run() {try {listen(clientSocket);} catch (IOException e) {// TODO 自动生成的 catch 块e.printStackTrace();}}}.start();StringBuffer sb = new StringBuffer();sb.append("{\"cmd\":\"get_id_list\"}");DatagramPacket sendPacket = new DatagramPacket(sb.toString().getBytes(), sb.toString().length(), ia, PORT);clientSocket.send(sendPacket);}static void listen(DatagramSocket clientSocket) throws IOException {byte[] receiveData = new byte[1024];DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);clientSocket.receive(receivePacket);String response = new String(receivePacket.getData());System.out.println(response);System.exit(0);}}
代码执行以后,会收到如图回复
data中有网关下所有设备的信息,通过把"{\"cmd\":\"get_id_list\"}"命令修改为"{\"cmd\":\"read\",\"sid\":\"设备的ID\"}"可以看到设备的信息,从而可以判断出设备是什么。因为此文主要通过温湿度传感器进行示例,我将继续以温湿度传感器为例子进行描述。
通过文档可以看到开发者模式的通信方式是在局域网内进行UDP通信。而亚马逊的echo dot必须要通过云服务去控制设备,并不支持在局域网内进行操作。因此我们要做的就是让网关可以在公网被访问到。把tcp进行内网穿透的软件很多,比如ngrok、frp、花生壳(内网版)等,但udp的内网穿透却不像tcp那么成熟,即便支持,也不是很稳定。我尝试过用frp进行udp内网穿透,刚运行的时候反应迅速,但过了一夜之后就失联了。由于对路由器的系统不是很了解,一直以为我的路由器在不刷机的情况下不能自由的运行程序,因为缩水版linux安装运行环境也很麻烦,没怎么尝试过。直到看到frp直接下载就能运行以后,才知道原来可以通过go语言编译成mips32le版本在极路由之类的路由器上直接运行。
于是赶紧学了一点go语言,实现了一个在路由器某个端口监听TCP,将消息通过UDP转发给网关的程序。最后工作流程如下:
Amazon Lambda →TCP→路由器内网穿透软件→我写的tcp2udp→网关,打通了一条相对稳定的通路。
我把编译后的程序传在了CSDN下载中:tcp2udp(仅限mips32le芯片的路由器,其他的如果需要可以在评论里留下平台我再编译)
首先,我在内网穿透服务新建一个到我路由器12143端口的通道。如果使用ngrok,配置如下,其他类似(最后的55555为服务提供者提供的端口号,如果自己搭的服务器,相信你知道什么意思):
tcp,127.0.0.1,12143,55555
配置完成之后,相当于服务器的55555端口和路由器的12143端口打通了。下面要做的,就是用tcp2udp把路由器的12143端口监听器来,如果有消息来,发给网关。
把文件放到存储卡里,比如我放到了存储卡里的tcp2udp文件夹下,那我就需要在路由器的shell里CD到这个文件夹,然后执行它。
打开程序所在文件夹(文件夹名字是tcp2udp,里面还有tcp2udp的程序,只是个名字,任意起)
cd /tmp/storage/mmcblk0/tcp2udp
执行(-p是端口号,-t是网关的ip和端口号,最后的&符号是添加进队列在后台运行,不至于关掉ssh后就被干掉)
./tcp2udp -p 12143 -t 192.168.199.162:9898 &
出现像上图中的样子,就是执行成功了。这时候把第二段java代码中的ip和端口号换成内网穿透服务器的地址和端口号,则也能工作。
最后一步,接入echo dot,创建skill的步骤在前两篇关于echo开发的博文中说过,这次类似。
Lambda的创建在下文中有,但触发器要用custom skill,因为smart home对于获取温湿度的接口并不支持:
点击打开链接
创建custom skill的过程下文中有介绍,自己用,则不需要使用账户,可以选不用账户
点击打开链接
我在lamda中的代码如下,已经实现了获取温度和湿度
import socket
import json
def lambda_handler(event, context):# TODO implementslots = event['request']['intent']['slots']isTemperature = 0isHumidity = 0if slots['firstval']['value']=="temperature" :isTemperature = 1elif slots['firstval']['value']=="humidity":isHumidity = 1if slots['secondval'].has_key("value"):if slots['secondval']['value']=="temperature" :isTemperature = 1elif slots['secondval']['value']=="humidity":isHumidity = 1address = ('内网穿透服务器地址', 内网穿透服务器端口) s = socket.socket() s.connect(address) s.send('{"cmd":"read","sid":"温湿度传感器ID"}')data= s.recv(1024) d = json.loads(json.loads(data)['data'])print dtemperature = float(d['temperature'])/100humidity = float(d['humidity'])/100res = {}res['version'] = "1.0"speechText = ""if (isTemperature==1) and (isHumidity==1):speechText = "The temperature is "+str(temperature)+" degree, and the humidity is "+str(humidity)+" percent."elif isTemperature==1:speechText = "The temperature is "+str(temperature)+" degree."else:speechText = "The humidity is "+str(humidity)+" percent."outputSpeech = {}outputSpeech['type'] = "PlainText"outputSpeech['text'] = speechTextcard = {}card['type'] = "Simple"card['title'] = "Smart Home"card['content'] = speechTextresp = {}resp['outputSpeech'] = outputSpeechresp['card'] = cardres['response'] = respreturn res
原文地址:http://blog.csdn.net/luhanglei/article/details/60140972