小白超详细解读: 家庭留言-基于企业微信和小度TTS播报

我是一点都不懂开发的纯纯小白。入门Node-red快2个月了,遇到过各种大大小小的坑,跌跌撞撞的也学到了很多。Node-red入门貌似有点难度,但其实它可做到的远超过我们可以想到的,所以我还是一路走来。我希望更多的小白少走一些弯路,多一些成功,至少可以少一些家人的“蔑视” :smile:。下面我会尽可能详细的描述流程的重点、难点、我曾经遇到过的坑。内容很多,也会有很多错误,大神们千万不要鄙视啊。
家里有老人有小孩。老人依然是以听广播为主,小孩依然要控制手机的使用时间。直到现在,还是电话多,发信息少,有个什么事情就特别想有个留言给家人。前两天学习了大神 利用音箱TTS功能和企业微信配合实现语音留言板,晚上就给自己的小度音箱配上了留言功能。自动化终于帮助到了家人(好像之前都是自己在玩,家人都是一脸的嫌弃)。给大家分享一下我的过程。

前提

  1. 企业微信已经接入Node-red
  2. 百度TTS在hass下可调用小度的dlna播放语音信息

新增节点

node-red-contrib-map,用于将微信用户id匹配为我们自己的称呼。比如,奶奶、爷爷、帅帅、美女等等。后面会详细描述

流程简单说明

使用企业微信发出留言

  1. 企业微信用户,在注册的应用中发出留言消息(可以是文字,也可以是语音。微信插件会自动转为文字的)
  2. Node-red服务端接收消息
  3. 如果是留言,则提取留言信息,并保存下来

Node-red提取留言,并调取TTS播报

上图

image-20200415032207507
可以导入的流程

[{"id":"fe499b9e.c8b658","type":"function","z":"907025e5.b57f38","name":"检查是否有留言","func":"//获取未读留言记录\na = flow.get(\"TTS_Message\");\n//销毁留言记录\nflow.set(\"TTS_Message\",[]);\n\nif(a.length == 0){\n    a = \"1\"\n}\n\n//b = a.length;\nmsg.payload = a;\nreturn msg;","outputs":1,"noerr":0,"x":690,"y":420,"wires":[["3dee51cc.9b16be"]]},{"id":"7fe04832.a874f8","type":"bizwechat-input","z":"907025e5.b57f38","name":"","bizwechat":"283f0f13.5e3b1","x":480,"y":320,"wires":[["74eee6e1.2e24e8"]]},{"id":"e5af16c8.275c38","type":"api-call-service","z":"907025e5.b57f38","name":"语音播报","server":"bab09590.5967f8","version":1,"debugenabled":true,"service_domain":"tts","service":"baidu_say","entityId":"","data":"","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1210,"y":480,"wires":[[]]},{"id":"18c92705.f3a099","type":"function","z":"907025e5.b57f38","name":"tts播报数据","func":"// 获取上一个流程的msg信息,构建服务百度tts服务的数据\n\na = msg.payload;\nmsg.payload = {};\nmsg.payload.data = {\n    \"entity_id\": \"media_player.xiao_du_zhi_neng_yin_xiang_2811\",\n    \"message\":a,\n    \"cache\":\"false\"}\nreturn msg;","outputs":1,"noerr":0,"x":1200,"y":420,"wires":[["e5af16c8.275c38"]]},{"id":"94a284d3.f4f4c8","type":"inject","z":"907025e5.b57f38","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":490,"y":420,"wires":[["fe499b9e.c8b658"]]},{"id":"4ca03b82.4b1c44","type":"map-map","z":"907025e5.b57f38","name":"","config":"4ab28474.5e22ac","in":"payload.FromUserName","inType":"msg","inLhsOrRhs":"lhs","out":"payload.FromUserName","outType":"msg","outLhsOrRhs":"rhs","caseInsensitive":true,"forwardIfNoMatch":true,"defaultIfNoMatch":"Unknown: {{{payload.FromUserName}}}","x":860,"y":400,"wires":[["39513c91.058ba4"]]},{"id":"3dee51cc.9b16be","type":"split","z":"907025e5.b57f38","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":860,"y":360,"wires":[["4ca03b82.4b1c44"]]},{"id":"39513c91.058ba4","type":"join","z":"907025e5.b57f38","name":"","mode":"auto","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":860,"y":440,"wires":[["fddeeb60.c450b8"]]},{"id":"74eee6e1.2e24e8","type":"function","z":"907025e5.b57f38","name":"保存留言","func":"/**\n 时间戳\n 对Date的扩展,将 Date 转化为指定格式的String\n 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,\n 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)\n*/\nDate.prototype.Format = function(fmt){ //author: meizz\n    var o = {\n        \"M+\" : this.getMonth()+1,                 //月份\n        \"d+\" : this.getDate(),                    //日\n        \"h+\" : this.getHours(),                   //小时\n        \"m+\" : this.getMinutes(),                 //分\n        \"s+\" : this.getSeconds(),                 //秒\n        \"q+\" : Math.floor((this.getMonth()+3)/3), //季度\n        \"S\"  : this.getMilliseconds()             //毫秒\n    };\n    if(/(y+)/.test(fmt))\n    fmt=fmt.replace(RegExp.$1, (this.getFullYear()+\"\").substr(4 - RegExp.$1.length));\n    for(var k in o)\n    if(new RegExp(\"(\"+ k +\")\").test(fmt))\n    fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : ((\"00\"+ o[k]).substr((\"\"+ o[k]).length)));\n    return fmt;\n};\n\nvar FromUserName = \"\"  //msg.message.FromUserName;\n\n//判断是打开企业微信后并发送了消息\nif(msg.message.MsgType != \"event\"){\n    //判断是有内容的消息\n    if(msg.message.AsrContent || msg.message.Content){\n        var message = \"\"\n        //如果是语音消息\n        if(msg.message.AsrContent) message = msg.message.AsrContent[0];\n        //如果是文本消息\n        if(msg.message.Content) message = msg.message.Content;\n        //获取未读的消息记录\n        var Message = flow.get(\"TTS_Message\") || [];\n        //统计消息数量\n        var i = Message.length;\n        //增加新消息\n        Message[i] = {};\n        Message[i]['FromUserName'] = msg.message.FromUserName;\n        Message[i]['CreateTime'] = new Date().Format(\"MM月dd日hh点mm分\");\n        Message[i]['Content'] = message;\n        //保存\n        flow.set(\"TTS_Message\", Message);\n        msg.payload = Message;\n        return msg;\n    }\n}\n\n","outputs":1,"noerr":0,"x":690,"y":320,"wires":[[]]},{"id":"fddeeb60.c450b8","type":"function","z":"907025e5.b57f38","name":"读取所有人留言","func":"//获取未读留言记录\nvar TTS = \"您当前没有留言消息\"\nif(msg.payload == 1){\n    msg.payload = TTS;\n    return msg;\n}\nvar Message = msg.payload;\n\n//计算留言数量\nvar i = Message.length;\nvar TTS = \"\";\n//如果存在未读留言\nif(i>0){\n    //构造TTS内容\n    var message= \"\";\n    for(var a=0;a<i;a++){\n        message += `${Message[a]['FromUserName']}于${Message[a]['CreateTime']}留言说:${Message[a]['Content']}。`;\n    }\n    TTS = \"您当前共有\"+ i +\"条未读消息:\" + message;\n}\nmsg.payload = TTS;\nreturn msg;","outputs":1,"noerr":0,"x":1010,"y":420,"wires":[["18c92705.f3a099"]]},{"id":"56f943c0.3b1bec","type":"comment","z":"907025e5.b57f38","name":"留言","info":"参考:https://bbs.iobroker.cn/t/topic/940\n企业微信用户可以发出消息;\n系统判断是否关键字;\n如果是关键字,则转向相应流程;\n如果不是关键字,则视为留言,并保存下来;\n\n触发,播报消息。现在没有对指定人进行限制。消息播报流程已经脱离了微信端。所以,是由node-red存储、播报、控制等\n\n后续进一步增强,\n参考:https://bbs.iobroker.cn/t/topic/941","x":400,"y":280,"wires":[]},{"id":"283f0f13.5e3b1","type":"bizwechat-configurator","z":"","name":"","port":"3001","corpid":"1345","agentid":"100002","corpsecret":"joagpjg","url":"http://yuming:13001","token":"jouppg","aeskey":"ajougpu[pughohtout","client_id":"jouaugut","client_secret":"ouaugphoheogout"},{"id":"bab09590.5967f8","type":"server","z":"","name":"Home Assistant","legacy":false,"addon":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true},{"id":"4ab28474.5e22ac","type":"map-config","z":"","name":"微信用户名","lhsName":"lhs","rhsName":"rhs","mappings":[{"lhs":"yaoyuan","rhs":"姚远"}]}]

详细说明

  1. 微信服务端
    请参阅大神的node-red-contrib-bizwechat(企业微信版本的pushbear),以及视频教程 基于企业微信的一对多消息送达服务-完美替代pushbear
    这里只说与微信接收端的一些注意


    有些同学,企业微信配置好了,可以由应用向企业微信发消息,一切正常。但是“服务端”就是收不到任何消息。那么百分之八十,你是下面这个接收端口有问题了。比如配置企业微信之后,你又改变了路由器的端口映射,改变了nginx的反代配置,等等。如果你是像我一样是在docker下安装的node-red,那么在docker中,要把你的端口暴露出来。我的语句是
    docker run -itd -p 1880:1880 -p 3001:3001 -v /home/pi/docker/node-red-data:/data --restart=always --name mynodered nodered/node-red:latest-minimal
    -p 3001:3001,就可以把3001端口暴露出来了。还有些同学,可以接受文字信息,但语音有问题,那多半是百度TTS这个key有问题。去百度看看就知道了。

  2. 保存服务端的配置


    这里多说几句。

  • debug节点,调试利器

调试的时候,在有疑问的环节增加debug节点。这是node-red最优秀的能力之一,让我们可以看见发生了什么。
image

  • 怎么看大神的取数语句
    image

看调试信息
image
前面提到,我在大神的保存留言function内改了一点东西,就是这样取得数。
从“保存留言”输出的信息可以看到,我一共有5条未读信息,它就以数组的方式按顺序保存下来了。要学习数组怎么使用,大神这段很典型,让我受益匪浅

如果你在“保存留言”看到了我这样的信息,那就是留言已经保存成功了。后面就是怎么取出留言并播放的事情了。

  1. 触发留言播放流程
    在调试的时候,一般这里就放一个inject节点。之后,应用的时候,往往这里是某个自动化的输出。

  2. “检查是否有留言”节点
    大神的流程是一个原型。我在收听留言的时候总听不太懂是谁给我留言了。所以,我增加了一个微信用户id转名称的环节。在这里,就要把大神的节点拆改一下。我的目的就是:

  • 取出保存的留言消息,准备在中途修改它

  • 如果没有消息,也要告诉后面处理的流程,别处理了,这里啥也没有。所以,我加了一个的标识 “a = ‘1’ ”,让这个节点的输出为“1”。
    image

  • 中间加了一个给发信人重新赋值的环节
    map节点
    map节点


    map的详细匹配内容
    image
    split和join节点:在匹配前和匹配后,将数组拆分,再合并

拆分
image
合并
image

  1. 读取所有人留言

TTS语音播报

我使用的是小度play音箱,带有dlna功能。为了省事,我直接调用了hass的百度TTS服务。在调用服务前,需要先构造好服务的data。

tts播报数据

先观察一下前端节点给过来的信息结构是什么样子的。它是msg.payload下面带了一条文字信息。
image
构造tts的data信息

hass中配置小度dlna

在hass的configuration.yaml文件中,有这两段配置就可以了

  • 自动发现dlna
    image
  • 配置百度语音识别的TTS
    image
  • 然后你在hass主页面看到了
    image
  • 看看能不能TTS发声

在Node-red“语音播报节点”配置与hass的链接

OK了,

  1. 打开你手机的企业微信,进入你配置和node-red集成应用的那个对话,发出留言。看看Node-red有反应吗
  2. 然后出发一下留言收听的流程,看看音箱播报了吗 :smiley:

一点启发

  1. 在留言流程的“服务端”和“保存留言”中间,可以插入一个switch节点,先判断一下这个信息是不是留言,可能是你发出的查询命令或是控制灯开关的命令呢。所以,企业微信是可以控制智能家居的。
  2. 根据“服务端”的用户名id,可以识别出谁发的信息,也可以识别出信息是发给谁的。这样就有点意思了。只是我还没有搞清楚,怎么做到在应用的对话中,发给特定人 :innocent:
  3. map节点。我之所以没有使用change进行转移,还是看中了map的匹配方法。它可以应用到很多地方。我使用Node-red就是为了不编程,能用节点就用节点处理。所以,这个map节点很重要。
  4. 如何触发留言收听流程。这个很有意思,不同的家庭、不同的已有自动化流程、不同的需求,都可以触发这个流程。这也是Node-red灵活性的优势。你想怎么配就怎么配,你想什么时候配,画条线就搞定了。我是家里搞了个“一键控”,10块钱买了个无线门铃,把那个按钮放在床头,睡觉就是关灯开夜灯,天黑负责开关灯,然后就是听留言、听温湿度、听气象,很方便的
  5. 真正的留言不是这么简单的,这里只是一个原型。真正的留言,要有来留言提醒,听过了也可以保留一段时间重新听。如果能知道留言是发给谁的,最好是这个人能够听到这个留言。还有,留言多长时间没有听到,可以发送到接收人的手机里保存。仅仅是为了保存,不是每个人都能听到留言,也不是每个人都注意到了手机信息,但我给你一个永久查证的机会 :smiley:

唧唧歪歪说了这么多,希望能给像我一样的小白们有所帮助

顶楼主,写的很详细,也很不错!!适用于像我这样有浓厚兴趣,缺没有功底的人

不能导入。错误。能不能打包一下附件

不能导入啊,报的啥错误呢?可能是里面有个“node-red-contrib-map”需要安装一下的。TTS流程.txt (6.6 KB)