本文用到的
智能电源插座 :涂鸦智能的插座 (只有这个是需要购买的,京东价格是62元)
智能语音助手 :Google Assistant
智能任务系统 :IFTTT
文章的目标是通过自己搭建的私有云
、IFTTT
、Slack
、Google Assistant
、涂鸦智能
、图灵机器人
等等第三方服务,构建自己的“智能生活”基本的框架。
前段时间我在京东上购买了一个
涂鸦智能的插座,LifeSmart 智能家居,手机远程无线遥控开关定时wifi智能节能插座,支持小度小米小爱同学音箱。
涂鸦智能是一个把产品智能化的一个平台,从软件和硬件和云端上面提供给厂商一个一站式人工智能物联网的解决方案(确实不是涂鸦智能的广告- -.)。所以严格来说应该是“鹊起”基于涂鸦解决方案所开发出来的一个产品,基于涂鸦提供的一切规范,所有软硬云端的开发规范都可以在官网无权限限制地查看到(https://docs.tuya.com/cn/)
初试插座
我首先下载了涂鸦的官方app:Smart Life,注册登录、智能配对插座,通过手机控制插座开关,一切顺利,还支持设置定时开关和 Schedule,而且手机控制到插座的反应十分灵敏(看样子应该使用了长链接)。
破解插座
当然使用官方 app 显然是无法满足我们需求,所以查看官方文档,发现文档中的 tuya.m.device.dp.publish 接口正好可以满足我们的需求。
通过接入指南,我们可以知道接入方式提供了两种:
- MQTT,果然有使用长链接(app端默认用的应该就是长链接了)
- https,这个目前比较适合我们,暂时不管延迟怎么样,先选择用 https 来验证控制的可行性
下面有列出通用的参数:
参数名称 | 参数类型 | 是否必须 | 是否签名 | 参数描述 |
---|---|---|---|---|
a | String | 是 | 是 | API名称 |
v | String | 是 | 是 | API接口版本 |
sid | String | 否 | 是 | 用户登录授权成功后,ATOP颁发给应用的用户session |
time | String | 是 | 是 | 时间戳,格式为数字,大小到秒非毫秒,时区为标准时区,例如:1458010495。API服务端允许客户端请求最大时间误差为540分钟。 |
sign | String | 是 | 否 | API输入参数签名结果,签名算法参照下面的介绍 |
clientId | String | 是 | 是 | 用户的APPID(注各平台不一样如:ios、android、云云对接的id都不一样) |
lang | String | 否 | 是 | APP的语言,如”en”,“zh_cn”,错误信息根据语言自动翻译 |
ttid | String | 否 | 是 | APP渠道或云端渠道,如公司名,用于数据分析跟踪 |
os | String | 是 | 是 | 手机操作系统,如”Android”,“ios”,云端可以写linux或写公司名 |
上述我们无法拿到的是哪些呢?
- sid:登录后自然就能拿到了
- sign:所有参数都有了,那sign自然能算出来(文档下面有sign算法)
- clientId:这玩意就比较麻烦了,相当于一个apiKey
- ttid:这个非必须,暂时可以不管
所以只要拿到 clientId,我们就能
- 通过 tuya.m.user.email.token.get 接口拿到登录用的 token
- 通过 tuya.m.user.mobile.passwd.login 接口登录
- 然后通过 tuya.m.device.dp.publish 接口下发指令
那怎么去拿到 clientId 呢?反编译试试(大家应该都知道怎么做),class.dex 有点多,最后拿到了
- client_id:
8qp5cfk*******3mpmc3
(这个打码了) - app_secret:
g75ktcvsae8**********e95j738tawg
(同样打码)
拿到登录 token
打开 Postman
,按照文档 POST countryCode
和 mobile
可以得到 token:
{
"api"
:
"tuya.m.user.mobile.token.get"
,
"result"
:{
"exponent"
:
"3"
,
"pExponent"
:
"..."
,
"publicKey"
:
"..."
,
"token"
:
"..."
},
"status"
:
"ok"
,
"success"
:
true
}
通过 token 进行登录
{
"countryCode"
:
"86"
,
"mobile"
:
"11745678923"
,
"passwd"
:
"根据获取token接口返回的公钥对 md5(明文密码) 进行rsa加密"
,
"token"
:
"126bb7570dcae343980b0607e6b35084"
,
"ifencrypt"
:
1
}
根据登录接口的文档,密码需要使用 gettoken 接口返回的公钥对 md5之后的密码进行 rsa 加密,apk 反编译之后代码可以完全看到,直接拷贝即可
登录完之后,就可以拿到具体的 sid (sessionId)了,有了 sessionId,就可以去对插座进行下发指令。
插座下发指令
同样根据下发指令接口文档:
{
"devId"
:
"002yt001sf000000sfV3"
,
"dps"
: {
"1"
:
1
,
"2"
:
5
}
}
可以看到,POST 的数据除了 sid,还需要 devId 和 dps
dps 可以参考这里的文档:https://fchelp.cloud.alipay.com/queryArticleContent.htm?tntInstId=WRNQWLCN&articleId=89429815&helpCode=SCE_00000019
我们要控制插座开关的话,那就使用 {"1": true}
/ {"1": false}
即可
那 devId 呢?它代表我添加设备 id,那我怎么知道我刚给在 Smart Life 上添加的插座 id 呢?抓包试试(大家应该都知道),通过抓包,我们可以很容易拿到所有你绑定的 devId。
至此,我们可以通过 http 来控制插座的开关了。
搭建自己的私有云项目
我为自己的项目取名为
Angelia
,安革利亚,古希腊神话人物之一,为“消息女神”。
搭建 web 项目,新建 AngeliaController
:
@RestController
@RequestMapping
(
"/angelia"
)
@Configuration
class AngeliaController {
// ...
}
增加 Tuya 的配置文件:tuyaclient.properties
tuya.client.client_id=xxxxxx
tuya.client.app_secret=xxxxxx
tuya.client.ttid=xxxxxx
tuya.client.v=
1.0
tuya.client.base_url=https:
//a1.tuyacn.com/api.json
# smart life app 登录手机号 tuya.client.user_mobile=18511111111 tuya.client.user_country_code=86 tuya.client.user_rsa_encrypted_passwd=xxx # 设备信息 tuya.client.lang=en tuya.client.os=Android tuya.client.os_system=9 tuya.client.time_zone_id=Asia/Shanghai tuya.client.platform=Pixel tuya.client.sdk_version=2.6.4 tuya.client.app_version=3.4.3 tuya.client.app_rn_version=5.6 # 设备 id,模拟手机的 deviceId tuya.client.device_id=e507510a0288accd7****** tuya.client.imei=xxxxxx tuya.client.imsi=xxxxxx # 智能设备,可以多个:别名|id|dpid,别名|id|dpid,别名|id|dpid # dpid: 1. 开关;4.rgb;5. 档位;6. 温度;15. 红外数据 tuya.client.dev_ids=[\ {\ "aliasList": ["plug a", "plug 1", "插座a", "洗手间总电源"],\ "devId": "111222333",\ "dpId": "1"\ },\ {\ "aliasList": ["plug b", "plug 2", "插座b", "卧室总电源"],\ "devId": "12341234",\ "dpId": "1"\ },\ // ... ]
以上,除了刚刚的那些必要的参数之类,在这个配置文件里面还增加了对所有设备的配置,每个设备对应它的 dpId,devId,还有别名(控制的时候不可能直接说 “devId 为 a1d2f32a1d2f32 的插座关掉”,而是说 “关掉插座1” / “关掉洗手间总电源”等等),每个设备别名可以有多个。
好了,回到 AngeliaController
,新增一个接口:
/** * 插座控制接口 */ @Autowired lateinit var tuyaClientService: TuyaClientService @Autowired lateinit var tuyaClientProperties: TuyaClientProperties @PostMapping("/control/plug") fun controlPlug(@RequestBody request: PlugRequestVo): JSONObject { val dev = tuyaClientProperties.findDev(request.alias) return try { if (null == dev) { JsonResult.error("Device named ${request.alias} is not found") } else { tuyaClientService.controlPlug(dev.devId, request.turnOn) JsonResult.success() } } catch (e: Exception) { JsonResult.error(e.message) } } data class PlugRequestVo( val alias: String, val turnOn: Boolean )
以上,该接口接收别名(alias)和开闭状态(turnOn)两个入参。首先,通过请求的别名去 properties 查找设备,如果找到,则通过 tuyaClientService
的 controlPlug
方法来下发指令,controlPlug
的实现就是刚刚上面说的 获取登录接口-登录-下发指令
几步。
构建、部署,通过 Postman 访问 http://localhost:xxxxx/angelia/control/plug
, body 设置为 {"alias": "卧室总电源", "turnOn": true}
,插座即可打开。
我们破解了涂鸦的插座,搭建了自己的 web 服务,暴露了一个接口来控制插座的开关。这篇我们配合 IFTTT、Slack 来控制插座:
- 说 “OK Google” 唤醒 Google Assistant,然后说 “帮我打开卧室的电源”,最后插座被打开。
- 创建 Slack 机器人
Angelia
,对它发消息“帮我打开卧室的电源”,然后插座打开,Angelia
回复说 “好的,已经打开”。 - 通过 Slack 机器人
Angelia
,发送 Slash Commands,打开关闭插座。
创建自己私人的 Slack Workspace
打开 Slack,根据提示创建自己的 Slack Workspace: https://slack.com/create
比如我的 Workspace 为 https://wangjie.slack.com。
创建 Slack App
创建完毕登录之后,默认应该有 #general
、#random
等 channel,但暂时不用这两个 channel。
接下来,我们来创建一个 App。
打开 https://api.slack.com/,点击 Start Building
输入 App 名称和你要添加到的 workspace。
设置 Bot 信息
创建完毕之后,我们需要设置这个 app 的机器人相关信息,打开 app 设置页面,选择 Bot Users
:
设置机器人的名称(Display name 和 Default name)。勾选 Always Show My Bot as Online
,点击 Save Changes
。
设置 Events API
Event API 可以在各种时间发生的时候触发调用,比如 消息发送的时候、channels 改变的时候等等。
我们先回到我们的 web 服务,打开上一章创建的 AngeliaController
,新增一个处理 Event 的 api:
@PostMapping
(
"/say/at"
)
fun say(
@RequestBody
request
: BotEventRequestVo): JSONObject {
logger
.info
(
"[slack event request]request -> \n$request"
)
return
JsonResult
.success
(
"token"
to request.token,
"challenge"
to request.challenge,
"message"
to message
)
}
data class BotEventRequestVo( val challenge: String?, val token: String?, val team_id: String?, val api_app_id: String?, val event: BotEventVo?, val type: String?, val event_id: String?, val event_time: String?, val authed_users: List<String>? ) data class BotEventVo( val type: String?, val user: String?, val text: String?, val client_msg_id: String?, val ts: String?, val channel: String?, val event_ts: String?, val channel_type: String? )构建,部署到服务器。
打开 Slack App 设置页面的 Event Subscriptions
:
在 Request URL
中填写刚刚在我们 web 服务上创建的接口 http://[server ip]:xxx/angelia/say/at
,并且点击验证。
注意:这里认证的依据是,你的接口 Response 需要返回请求中的
challenge
数据就算认证成功。
然后在 Subscribe to Bot Events
中添加订阅的事件,需要增加的是 message.im
message.im
表示当你跟 bot 的私聊中产生消息的时候(有可能是你发送消息给 Bot,也有可能是 Bot 发消息给你),事件就会触发。
点击保存。
这时,当你在 Slack 中发送消息给机器人的时候,你的 web 服务端就能收到请求了。
处理事件
你的 web 服务器收到请求之后,需要对此进行处理,所以你需要去解析发的消息中的信息,然后打开/关闭对应设备(插座)的开关。完善之前的 say
接口:
@Autowired
lateinit
var
tuyaClientService:
TuyaClientService
@Autowired
lateinit
var
angeliaSlackProperties:
AngeliaSlackProperties
/** * Angelia 机器人 对话入口 */ @PostMapping("/say/at") fun say(@RequestBody request: BotEventRequestVo): JSONObject { try { val text = request.event?.text val eventType = request.event?.type if (eventType == "message" // 直接对话 || request.event.user != angeliaSlackProperties.angelia_id // angelia自己发的忽略掉 ) { val message = angeliaBotService.parseTuyaClient(text) ?: "Sorry, I can not understand." angeliaSlackService.postMessage(JSONObject().apply { this["text"] = "$message" this["channel"] = request.event.channel this["as_user"] = true }) } return JsonResult.success( "token" to request.token, "challenge" to request.challenge, "message" to "Request eventId(${request.event_id}) done." ) } catch (e: Exception) { angeliaSlackService.postMessage(JSONObject().apply { this["text"] = "Sorry! Something is wrong: ${e.message}" this["channel"] = request.event?.channel this["as_user"] = true }) return JsonResult.error(e.message) } }
上面代码的逻辑很简单:
- 首先,eventType是直接对话的(与机器人 bot 私聊),并且是我发给机器人的(机器人发给我的消息不用处理)才会去处理
- 然后通过
angeliaBotService.parseTuyaClient()
方法进行文本解析和处理 - 如果解析不出来,则返回 null,message 就是 “Sorry, I can not understand.”
- 最后返回 Response(带上 message),这里的 message 就是 Bot 发送给我的数据。
这里需要做一些 Slack 的配置 slack.properties:
#
token
for
bot
angelia.slack.bot_token=Bearer xoxb-2923xxxxxxxxxxxxxxxxxxxx
#
slack api
angelia.slack.api_base_url=https://slack.com/api
angelia.slack.api_chat_post_message=/chat.postMessage
angelia.slack.angelia_id=UCWxxxxxx
angelia.slack.bot_token
:是 Bot 发送消息到 Slack 的token,这个 token 可以在 app 设置页面的 OAuth & Permissions
中拿到
注意:是下面的那个
Bot User OAuth Access Token
,并且添加到配置文件的时候需要加上Bearer
(注意后面有个空格)
angelia.slack.api_base_url
和 angelia.slack.api_chat_post_message
是发送消息的 url,不用改动。
angelia.slack.angelia_id
表示机器人的id,可以通过在 slack 左侧选中机器人右键复制链接,path 最后部分就是 id
最后,你就能在 slack 中打开与机器人聊天框,发送“关闭插座a”来控制插座:
集成 Google Assistant 和 IFTTT
首先确保你的手机装有 Google Assistant(Google Home 先不讨论。。。是的,我没买- -)。
首先我们需要在 web 服务器端再创建如下一个接口:
/** * 插座控制接口 */ @PostMapping("/control/plug") fun controlPlug(@RequestBody request: PlugRequestVo): JSONObject { val dev = tuyaClientProperties.findDev(request.alias) return try { if (null == dev) { JsonResult.error("Device named ${request.alias} is not found") } else { tuyaClientService.controlPlug(dev.devId, request.turnOn) JsonResult.success() } } catch (e: Exception) { JsonResult.error(e.message) } } data class PlugRequestVo( val alias: String, val turnOn: Boolean )
构建部署到服务器。
然后打开IFTTT、注册(如果还没有账户)登录,创建 Applet
This:选择 Google Assistant:
That:选择 Webhook:
注意:POST 请求,在 body 中填写如上 json 数据。
关闭的 Applet 也是类似,把 body 中的 turnOn 改成 false 就可以了
最后,你就可以通过 “OK, Google” 唤醒 Google Assistant,然后说”Turn on plug a”来打开插座了。
吐槽下涂鸦的 Google Assistant
本来想直接使用 Google Assistant 的 Smart Life 的,但是一直没成功,很多人也在反映这个事情,不过貌似没什么效果(看下面这个评分,估计反映一直是被无视的- -):
使用 Slack 的 Slash Command 控制
在 web 服务中再新增两个接口用于 Slash Command:
/** * 插座控制接口,For Slack command line(slash commands) */ @PostMapping("/plug/turnon") fun plugTurnOnForCommand(@RequestBody body: String): JSONObject { return JsonResult.success("text" to plugControlForCommand(body, true)) } /** * For Slack command line(slash commands) */ @PostMapping("/plug/turnoff") fun plugTurnOffForCommand(@RequestBody body: String): JSONObject { return JsonResult.success("text" to plugControlForCommand(body, false)) } /** * For Slack command line(slash commands) control */ private fun plugControlForCommand(body: String, turnOn: Boolean): String { try { return body.split("&").map { val pair = it.split("=") Pair(pair[0], URLDecoder.decode(pair[1], "UTF-8")) }.firstOrNull { it.first == "text" }?.let { val dev = tuyaClientProperties.findContainsDev(it.second) if (null == dev) { "Sorry for failed command, Device named ${it.second} is not found." } else { tuyaClientService.controlPlug(dev.devId, turnOn) "${if (turnOn) "Turn On" else "Turn Off"} Done(${it.second})." } } ?: "Sorry for failed command, Device name required." } catch (e: Exception) { return "Sorry for failed command, ${e.message}." } }
打开 App 设置页面的 Slash Commands
,
点击 Create New Command
,
然后在聊天的输入框中就可以通过”/”显示 command 提示,选择命令,后面跟上你要执行该命令的设备别名就行了。
其它场景
还有其它很多场景可以实现。比如:
- 结合 IFTTT,当离开家门100m远的时候,自动触发 webhook,让你的私有云帮你把电源、智能们关闭。
- 实时检测你的位置和家门,如果你不在家,自动调用 Google Calendar 确定你的日程安排,如果又没有外出的安排,则通过 Slack 发送 Interactive messages 到你手机上提醒,提供按钮一键锁门。
- 小细节,晚上手机充电的时候,可以设定手机一旦充满,关闭电源,又如果手机电源掉电过快,低于90%的电量,重新自动开启电源,确保你早上起床的时候手机电量肯定在某个值之上。
- 等等等等,太多的智能场景可以去实现。
章尾
现在越来越多的厂商制作着各种各样的智能设备,但是又在自己的一亩三分地固步自封。做个插座,提供一个 app 控制下开关、定个时、做个 schedule 就是所谓的智能了,你买了我的设备就必须要用我的软硬件产品。那些需要用户花心思去考虑什么时候我该怎么的设备不是冰冷的,没有生命的么?智能是人类赋予了设备生命,掌握了“思考”的能力,现在的生活如此多元化,再牛的公司也不可能覆盖你的所有生活领域,如果买了这样的智能设备但自此被囚困在这里,我想,这才是我非智能生活的开始吧。
原文链接:https://www.cnblogs.com/tiantianbyconan/p/9714007.html
作者:天天_byconan