How to build an ambilight system for your monitor
July 23, 2021
I recently got a robotic hover the Eufy Robovac 30C which has been really ood at keeping the house clean however theres no current integrations with Home Assistant which is the platform we use to automate our home. Intersetingly the Eufy Robovac 30C has wifi and can be controlled by Alexa so it must have an undocumented API that can control it with the correct acount credentials. So I went about looking to see if I could revese engineer this. Starting by downloading and decompiling the eufy home apk I searched throught the code for
this revealed that robovac is actually controled using Tuya Cloud, this is a IoT cloud company that provides SDK's software and cloud services for IoT manfacturers they actually provide the software that runs on the devices as well and define the interfaces that your app and device uses. After a quick googling for Tuya API I discovered this project https://github.com/codetheweb/tuyapi which provides an API to interact with Tuya controlled devices given that it is written in node I wondered if someone had already created a node-red plugin for it as I already have node-red intergated with home assistant and found https://flows.nodered.org/node/node-red-contrib-tuya-smart this would allow me to create a node red flow that controlled Robovac if I could get therobovac
anddevice id
out of the eufy app.device key
to the rescue, I ranmitmproxy
installed the CA on my iphone and setup the wifi to route traffic through the proxy on my phone, then opened the app and refreshed my device list and boom, themitmproxy
anddevice id
appeared in the requests to Tuya. This allowed me to setup the Tuya nodes and replay a payload I'd captured from the app. Robovac started moving!device key
Form this point integrating I'd integrated with node-red but not with Home Assistant, I wanted full integration with home assistant, it turns out that there's a MQTT vaccum component which is great because node-red has excellent MQTT nodes :). I wired up the MQTT nodes to allow two way communication between Robovac and Home Assistant and added the MQTT vaccum to the UI and it worked :). I was able to discover all the modes Robovac supported and the required payload values through the decompiled code (included below). If the node red flow would be helpful to you I've included it as well. (You'll need to add your own
anddevice id
)device key
Using this node red flow :)
[{"id":"b2af6835.04f4b","type":"tab","label":"Maintanance","disabled":false,"info":""},{"id":"8a2a6da3.4d81f","type":"tuya-smart","z":"b2af6835.04f4b","deviceName":"Robovac","deviceIp":"192.168.0.45","deviceId":"06868586bcddc28a2c3e","deviceKey":"041b2bec7697df22","request":"{\"schema\": true}","pollingInterval":10,"x":994.5,"y":449,"wires":[["7857a445.cd9d34","7ee46beb.b6f76c"]]},{"id":"7857a445.cd9d34","type":"debug","z":"b2af6835.04f4b","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1060.5,"y":285,"wires":[]},{"id":"abb430ff.469ef8","type":"inject","z":"b2af6835.04f4b","name":"","topic":"","payload":"{ \"set\": \"auto\", \"dpsIndex\": 5}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":554.5,"y":296,"wires":[["8a2a6da3.4d81f"]]},{"id":"87131128.f8a3e","type":"inject","z":"b2af6835.04f4b","name":"","topic":"","payload":"{ \"set\": \"goHome\", \"dpsIndex\": 101}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":552.5,"y":253,"wires":[["8a2a6da3.4d81f"]]},{"id":"f6eca8ab.b542b","type":"mqtt out","z":"b2af6835.04f4b","name":"","topic":"vacuum/state","qos":"1","retain":"true","broker":"703de63b.4d8bb","x":1515.5,"y":444,"wires":[]},{"id":"7e3706e5.c1694","type":"template","z":"b2af6835.04f4b","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"battery_level\": {{payload.data.dps.104}},\n \"docked\": {{payload.data.dps.1}},\n \"cleaning\": {{payload.data.dps.2}},\n \"charging\": {{payload.charging}},\n \"fan_speed\": \"{{payload.data.dps.102}}\",\n \"error\": \"{{payload.data.dps.106}}\"\n}","output":"json","x":1316.5,"y":446,"wires":[["f6eca8ab.b542b","7857a445.cd9d34"]]},{"id":"7ee46beb.b6f76c","type":"change","z":"b2af6835.04f4b","name":"isCharging","rules":[{"t":"set","p":"payload.charging","pt":"msg","to":"$lookup(payload.data.dps, \"15\") = \"Charging\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1153.5,"y":448,"wires":[["7e3706e5.c1694"]]},{"id":"70d3356e.678304","type":"mqtt in","z":"b2af6835.04f4b","name":"","topic":"vacuum/command","qos":"2","broker":"703de63b.4d8bb","x":95,"y":378,"wires":[["d0bc6ce1.a49508"]]},{"id":"d0bc6ce1.a49508","type":"switch","z":"b2af6835.04f4b","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"turn_on","vt":"str"},{"t":"eq","v":"turn_off","vt":"str"},{"t":"eq","v":"return_to_base","vt":"str"},{"t":"eq","v":"start_pause","vt":"str"},{"t":"eq","v":"stop","vt":"str"},{"t":"eq","v":"locate","vt":"str"},{"t":"eq","v":"clean_spot","vt":"str"}],"checkall":"true","repair":false,"outputs":7,"x":287.5,"y":426,"wires":[["fbc32636.61b168"],["11f949e2.cb7916"],["11f949e2.cb7916"],["2ea933bf.b0a794"],["11f949e2.cb7916"],["c361bdcb.ed7be8"],["c8744c3.b97f43"]]},{"id":"c643188d.9380d","type":"mqtt in","z":"b2af6835.04f4b","name":"","topic":"vacuum/set_fan_speed","qos":"2","broker":"703de63b.4d8bb","x":110,"y":604,"wires":[["3f773570.d199e2"]]},{"id":"fbc32636.61b168","type":"change","z":"b2af6835.04f4b","name":"Auto","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"set\": \"auto\", \"dpsIndex\": 5}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":423.5,"y":367,"wires":[["8a2a6da3.4d81f"]]},{"id":"11f949e2.cb7916","type":"change","z":"b2af6835.04f4b","name":"Off","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"set\": \"goHome\", \"dpsIndex\": 101}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":425,"y":404,"wires":[["8a2a6da3.4d81f"]]},{"id":"3001c938.ac74b6","type":"change","z":"b2af6835.04f4b","name":"Start","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"set\": \"auto\", \"dpsIndex\": 5}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":690,"y":487,"wires":[["8a2a6da3.4d81f"]]},{"id":"2ea933bf.b0a794","type":"api-current-state","z":"b2af6835.04f4b","name":"Get state","server":"59580a28.4a5194","halt_if":"","override_topic":true,"override_payload":true,"override_data":true,"entity_id":"vacuum.robovac","x":427.5,"y":439,"wires":[["b272ab13.4a24b"]]},{"id":"967d16e2.18a1","type":"change","z":"b2af6835.04f4b","name":"Pause","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"set\": false, \"dpsIndex\": 2}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":688,"y":449,"wires":[["8a2a6da3.4d81f"]]},{"id":"b272ab13.4a24b","type":"switch","z":"b2af6835.04f4b","name":"","property":"payload.cleaning","propertyType":"msg","rules":[{"t":"eq","v":"true","vt":"str"},{"t":"eq","v":"false","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":557.5,"y":442,"wires":[["967d16e2.18a1"],["3001c938.ac74b6"]]},{"id":"3f773570.d199e2","type":"template","z":"b2af6835.04f4b","name":"Set Speed","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{ \"set\": \"{{payload}}\", \"dpsIndex\": 102}","output":"json","x":686.5,"y":600,"wires":[["8a2a6da3.4d81f"]]},{"id":"c361bdcb.ed7be8","type":"change","z":"b2af6835.04f4b","name":"Locate","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"set\": true, \"dpsIndex\": 103}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":425,"y":474,"wires":[["8a2a6da3.4d81f"]]},{"id":"c8744c3.b97f43","type":"change","z":"b2af6835.04f4b","name":"Spot clean","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"set\": \"spot\", \"dpsIndex\": 5}","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":445,"y":513,"wires":[["8a2a6da3.4d81f"]]},{"id":"703de63b.4d8bb","type":"mqtt-broker","z":"","name":"Mosquitto","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"59580a28.4a5194","type":"server","z":"","name":"Home Assistant"}]
Decompiled Eufy app code:
public class RobovacTuyaController extends TuyaBaseController {public static final int CLEANER_SPEED_BOOSTED = 1;public static final int CLEANER_SPEED_CARPET_BOOST = 4;public static final int CLEANER_SPEED_INSTENCE = 2;public static final int CLEANER_SPEED_SILENT = 3;public static final int CLEANER_SPEED_STANDARD = 0;public static final String ROBOVAC_CLEAN_MODE_DPS_ID_5 = "5";public static final String ROBOVAC_DIRECTION_DPS_ID_3 = "3";public static final String ROBOVAC_ELECTRIC_DPS_ID_104 = "104";public static final String ROBOVAC_ERROR_ALARM_DPS_ID_106 = "106";public static final String ROBOVAC_FIND_ROBOVAC_DPS_ID_103 = "103";public static final String ROBOVAC_GO_HOME_DPS_ID_101 = "101";public static final String ROBOVAC_MODE_STATUS_DPS_ID_15 = "15";public static final String ROBOVAC_PLAY_OR_PAUSE_DPS_ID_2 = "2";public static final String ROBOVAC_ROBOVAC_SPEED_DPS_ID_102 = "102";private TuyaRobovacStatusBean roboVacStatusBean;private C3232b robovacStatus;protected Map initControllers() {return null;}protected C1646e specifyEmptyController() {return null;}public TuyaRobovacStatusBean getRoboVacStatusBean() {return this.roboVacStatusBean;}public void setRoboVacStatusBean(TuyaRobovacStatusBean tuyaRobovacStatusBean) {this.roboVacStatusBean = tuyaRobovacStatusBean;}public C3232b getRobovacStatus() {return this.robovacStatus;}public void setRobovacStatus(C3232b c3232b) {this.robovacStatus = c3232b;}public RobovacTuyaController(String str, String str2) {super(str, str2);}public void speed(int i, C1647f c1647f) {Log.d("tuya", "speed cmd send...");HashMap hashMap = new HashMap();Object obj = "";if (i == 0) {obj = "Standard";} else if (i == 4) {obj = "Boost_IQ";} else if (i == 1) {obj = "Max";}hashMap.put("102", obj);doSend(hashMap, c1647f, "speed");}public void playOrPause(boolean z, C1647f c1647f) {HashMap hashMap = new HashMap();if (z) {hashMap.put(ROBOVAC_CLEAN_MODE_DPS_ID_5, "auto");} else {hashMap.put("2", Boolean.valueOf(z));}doSend(hashMap, c1647f, "playOrPause");}public void auto(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put(ROBOVAC_CLEAN_MODE_DPS_ID_5, "auto");doSend(hashMap, c1647f, "auto");}public void sm(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put(ROBOVAC_CLEAN_MODE_DPS_ID_5, "SmallRoom");doSend(hashMap, c1647f, "sm");}public void edge(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put(ROBOVAC_CLEAN_MODE_DPS_ID_5, "Edge");doSend(hashMap, c1647f, "edge");}public void spot(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put(ROBOVAC_CLEAN_MODE_DPS_ID_5, "Spot");doSend(hashMap, c1647f, "spot");}public void findMe(boolean z, C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put("103", Boolean.valueOf(z));doSend(hashMap, c1647f, "findMe");}public void turnLeft(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put("3", "left");doSend(hashMap, c1647f, "turnLeft");}public void turnRight(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put("3", "right");doSend(hashMap, c1647f, "turnRight");}public void forward(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put("3", "forward");doSend(hashMap, c1647f, "forward");}public void backward(C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put("3", "back");doSend(hashMap, c1647f, "backward");}public void goHome(boolean z, C1647f c1647f) {HashMap hashMap = new HashMap();hashMap.put("101", Boolean.valueOf(z));doSend(hashMap, c1647f, "goHome");}public String toString() {StringBuilder stringBuilder = new StringBuilder();stringBuilder.append(super.toString());stringBuilder.append(", RobovacTuyaController{roboVacStatusBean=");stringBuilder.append(this.roboVacStatusBean);stringBuilder.append(", robovacStatus=");stringBuilder.append(this.robovacStatus);stringBuilder.append('}');return stringBuilder.toString();}}