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 :
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 :
First, let's talk about 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.
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 first subprocess ends without any response like in HTTP. The second process works that way :
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.
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 :
++pre>++code>{
endpoint: "yeelight::set",
payload: {
id: "test_light",
on: true
}
}++/code>++/pre>
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 :
++pre>++code>{
endpoint: "yeelight::getAll",
payload: null
}++/code>++/pre>
Incoming WebSockets will have the same structure. So if the server sends us a list of all lights, it'll send it as follow :
++pre>++code>{
endpoint: "yeelight::getAll",
payload: [
// Here are all the lights
]
}++/code>++/pre>
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 :
And here is our flow :
Let's dive into the code. In the App.js file, we need to
++pre>++code>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);++/code>++/pre>
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.
++pre>++code>function* handleWs() {
const channel = yield call(createSocketChannel, ws);
while (true) {
const { endpoint, payload } = yield take(channel);
yield put({ type: endpoint, payload });
}
}++/code>++/pre>
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 :
++pre>++code>const ws = new WebSocket("ws://host:port");++/code>++/pre>
And createSocketChannel is as below :
++pre>++code>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();
};
});
};++/code>++/pre>
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.
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 :
The back - front flow is pretty simple.
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.
Let's get through the different nodes.
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.
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!
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.
The back is another story. There are many things to update :
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 !