[fa icon="calendar"] Publié le 25 March 2019 par Louis Kraemer


If you're reading this, chances are you either like home automation or you're a bit lazy like me. One of my roommate and I wanted, at first, to fully automate the lights of our apartment. The goal was to just walk into any room and immediately have them light up. Of course, we would code all of that ourselves.

 

At first, we were brainstorming and some of the ideas we had were a bit overkill :

     Let’s just put a camera in every room and have them detect which roommate is currently in the room by using deep learning
     We could use thermal imaging to detect the presence of a human even at night

After thinking about what emerged of our brief but intense brainstorming, we realized that those solutions were either expensive (thermal module for raspberry is about 40€ or extremely difficult to implement (deep learning would require huge sets of data). We then decided to begin by creating a node server and a frontend to manually switch on and off the light.

 

We would have to have a server, a smartphone and, of course, a smart light bulb.

 

We will use :

  • A Raspberry 3 Model B (every raspberry or computer able to run node will do)
  • A smartphone
  • A Xiaomi Yeelight RGB (it’s about 25€ on Amazon, it doesn’t need a bridge to connect to the wifi and it can interface with Alexa and Google Home if needed in the future)

 

First, let’s talk about architecture.

 

Architecture

 

For this project, we want to be able to see the state of every room (on or off) on our frontend. We’ll have to use either long polling or a duplex channel to have real-time updates. I’m not a fan of long polling and prefer to use a duplex channel which is more elegant. A lot of frameworks can allow us to have real-time communication like socket io, however, those frameworks were created to allow older devices to have seamless “duplex channel” even if they did not support a more recent standard like WebSocket. In our case, we aim the frontend to work on about five devices which are less than two years old. Let’s use WebSocket.

 

For the connection between the server and the lamp, Xiaomi let us use a TCP socket. The first idea was to manually handle that connection. We’ll later see that using node-red will help us communicate with the light without having to manage it ourselves.

 

Article (1)

 

The important point in this schema is that there will not be a classic “request-response” process. The process is divided into two subprocesses:

  • The frontend ask the server to change the state of the lamp, then the server ask the lamp to modify its state accordingly

The first subprocess ends without any response like in HTTP. The second process works that way :

  • The lamp changes its state and notifies every device connected to it of that change, our server then notify all our frontend that the light’s state changed

 

We’ll use react-native and its WebSocket integration for the frontend. For the back, I initially wanted to use node and the npm package ws to handle WebSocket but, after some discussions with a colleague, I decided to make a POC with node-red. It corresponded perfectly to what I wanted to achieve and so I kept it.

 

Code

Front

 

For the frontend code, we’ll obviously not see the whole code. I’ll focus on some parts I think are important. The full code is available on my repo.

First, we must decide on a proper way to communicate with WebSocket. We cannot just send whatever string we want to our server, we must have a clean protocol. I don’t say that what I ended up using is the best protocol but it’s one way to do it (feel free to propose improvements ! ).

Here is an example of a message sent through the WebSocket :

 

{
  endpoint: "yeelight::set",
  payload: {
    id: "test_light",
    on: true
  }
}

 

We have two key: endpoint and payload. The endpoint is built like that <entity>::<method>. So if we want to power on the light “light_test”, the entity concerned is “yeelight” and the method is “set”. In the payload we precise the id of the devices we want the state to be altered and the properties to be updated. Here is an example to get all available lights :

{
  endpoint: "yeelight::getAll",
  payload: null
}

Incoming WebSockets will have the same structure. So if the server sends us a list of all lights, it’ll send it as follow :

{
  endpoint: "yeelight::getAll",
  payload: [
     // Here are all the lights
  ]
}

For now, we only have yeelight entities connected to the server but the goal is to manage all of our IoT devices through this app.

 

Second, to structure our data flow, we’ll use Redux. It allows us to have a single source of truth inside our app. But remember that we don’t use a request/response protocol to communicate with our server. We only send a command through WebSocket and then, at any given point in time, we can receive an update on what’s happening with our light. So the main problem we are facing is: “How do we handle incoming WebSocket message ? 

 

This is where Redux-Saga appears! Redux-Saga is a library that allows our code to have an asynchronous thread side by side with our redux flow. It is described as a Redux middleware. To have a better understanding of our flow here is a diagram of a standard redux flow :

 

Article (2)

 

And here is our flow :

 

Article (3)

 

Let’s dive into the code. In the App.js file, we need to

import createSagaMiddleware from "redux-saga";

import { createStore, applyMiddleware } from "redux";

import { rootReducer } from "./src/reducers";

import { handleWs } from "./src/services/websocket";

const sagaMiddleware = createSagaMiddleware();

const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(handleWs);

Here we create the saga middleware which will be put just before the reducer. Then we give it in the second argument of createStore with the applyMiddleware method from redux. Finally, we tell redux-saga which method he’ll be executing (handleWs).

Let’s create a service which hosts all WebSocket related code. In this service, we describe the handleWs function.

function* handleWs() {
 const channel = yield call(createSocketChannel, ws);
 while (true) {
   const { endpoint, payload } = yield take(channel);
   yield put({ type: endpoint, payload });
 }
}

You remark the use of “*” after function? It means that handleWs is an ES6 generator. The generator is called only one time when the app launches. The first line call createSocketChannel with ws as an argument. ws is the WebSocket socket :

const ws = new WebSocket("ws://host:port");

And createSocketChannel is as below :

const createSocketChannel = socket => {
 return eventChannel(emit => {
   socket.onopen = () => {
     emit({ endpoint: 'WEBSOCKET_CONNECTED' });
   };

   socket.onmessage = e => {
     try {
       // Here we handle all incoming messages
       const { endpoint, payload } = JSON.parse(e.data);
       if (isNil(endpoint) || isNil(payload)) {
         throw new Error("WRONG_MESSAGE_FORMAT");
       }
       emit({ endpoint, payload });
     } catch (err) {
       console.warn(err);
     }
   };

   socket.onerror = e => {
     // an error occurred
     console.log(e.message);
   };

   socket.onclose = e => {
     // connection closed
     emit({ endpoint: 'WEBSOCKET_DISCONNECTED' });
     console.log(e.code, e.reason);
   };
   return () => {
     socket.close();
   };
 });
};

In the while loop, we use “yield take” which means that we will wait until something is emitted. After that, we use the “put” command to dispatch an action to our reducer. We don’t want that behavior to execute only one time which is why it’s in a while(true) loop.

So every time a WebSocket arrives, we emit it, our generator gets it and dispatch a corresponding action to the reducers.

 

Back

 

Let’s go over the concept of node-red. It was designed to be used in IoT. The official description is “Node-RED is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways.” Basically, the server exposes a web interface which allows you to drag and drop “nodes” who represents either devices or logical components.

I will consider that you have an up and running node red server and that you know how to add nodes to it.

The first thing I did was to search for a yeelight node and the one I ended up using this package. It contains two nodes: one input which sends data whenever the state of the light changes and one output which allows us to send a command to the bulb.

First, don’t forget to activate the developer mode.

As an example to understand you can import this flow.

Double click on each light node and enter the IP address of your light. If you click on the bug at the top of the right column you can see the debug message sent by the light. You can click on the two inject node to switch on and off the light.

My goal is not to make a complete tutorial of how I did my project step by step but more of a presentation of the solution I’m currently using and the roadmap to enhance my server and have a complete IoT solution (back + front).

First thing first, the behavior of the yeelight node was not the one I expected so I forked it and tuned it. The original can be used as a proof of concept.

For our server to be complete (light wise), we need two flows :

  • Front - back: which will interpret any request coming from the app and will decide which action to take
  • Back - front: which will receive an update from the light and broadcast them to every app connected

The back - front flow is pretty simple.

Article (4)

Basically, we have a node for the light, a node which adds an identifier (which will be used by the app to send a command to a specific bulb), a node which updates a global variable storing all lights data and, finally, a WebSocket output which broadcast to every connected device.

The front - back flow is a bit more complicated.

 

Article (5)

 

Let’s get through the different nodes.

  • WebSocket in: Listen to all incoming WebSocket calls and pass it on.
  • JSON: Parse the message to convert a string to JSON Object.
  • Router: Look the endpoint of the request and redirect the message to the corresponding node.

After that, we have three branches.

The last one is for getting info about a specific light. It’ll send all the light details to the device that initiated the WebSocket call.

The second one is for getting the main info about all lights and also sends those data to the caller.

 

Finally the first branch.

  • Extract command: Extract the payload from the input message (containing the command to send to the bulb).
  • Redirect to light: Look up the id in the payload and redirect the command to the corresponding light.
  • Weegle Room: Send the command to the light of Weegle’s room. (I added two inject node to switch on and off the light for test purposes).

 

The flows are available here so you can explore each node and understand what it’s doing. It’s my first node-red project so feel free to give advice!

 

Next Step

Front

The most important step for me is to implement the reconnection to the server. I added a popup which is displayed whenever the server connection is lost but I didn’t find yet how to rerun the redux saga properly.

The second step is cosmetic but I want to work on the design of the app: add transitions, animations, … If the app works, sure it’s great, but who wants to use an ugly app?

I’ll try to update it regularly so you can follow the progression on my repo.

 

Back

The back is another story. There are many things to update :

  • Subflows: a PR is in review right now to introduce subflows with instance parameter
  • Database: a database would allow for data persistence and (maybe later) authentication
  • Light switch: the goal here is to make a ZigBee bridge manually (with the project zigbee2mqtt)
  • Pure node server: I mainly used node-red to make a proof of concept. It's quite easy to rapidly develop a basic server. I'm also working on a pure node server as a side project. It's currently available on this repo and will be the subject of another article in the future.

 

I hope you enjoyed this article, I'll be writing more about this project in the future. If you have any recommendations, let me know in the comments below !


Liked this article? Interested in building an app with us?

Contact a React Native expert in Paris




Want to rate this article?
Submit Rating

Topics: React Native, redux, Raspberry, Node-Red