
我家里的小米智能電器有米家、yeelight、aqara等品牌,米家有小米電視、米家空調(diào)等,yeelight有各個房間的吸頂燈,aqara有智能開關(guān)。由于米家開發(fā)者文檔顯示目前當(dāng)前僅面向企業(yè)開放,暫未面向個人開發(fā)者開放。而aqara智能開關(guān)可玩性一般(文檔可訪問這里),而yeelight智能燈可以調(diào)節(jié)燈光開關(guān)、光暗、與傳感器聯(lián)動,可玩性更高,于是我就把魔爪伸向了yeelight智能燈了。
我想做的效果是門開燈亮,門關(guān)燈滅。先來看一下效果:視頻地址。
要完成這個效果,我們需要完成兩大工作:接入門磁傳感器及編碼聯(lián)動。接下來逐一細(xì)講。
接入門磁傳感器
這部分包含控制臺操作 LoRa 門磁傳感器,控制臺操作 LoRa 網(wǎng)關(guān)、網(wǎng)關(guān)實物操作、門磁傳感器實物操作等步驟。具體操作步驟如下:
1. 控制臺操作 LoRa 門磁傳感器
1.1. 創(chuàng)建項目和產(chǎn)品
- 登錄 物聯(lián)網(wǎng)開發(fā)平臺控制臺,選擇【新建項目】。
- 在新建項目頁面,填寫項目基本信息。
- 項目名稱:輸入“LoRa樓宇傳感器演示”或其他名稱。
- 項目描述:按照實際需求填寫項目描述。
- 項目新建成功后,即可新建產(chǎn)品。
1.2. 新建產(chǎn)品
- 進(jìn)入該項目的產(chǎn)品列表頁面,單擊【新建產(chǎn)品】。
- 在新建產(chǎn)品頁面,填寫產(chǎn)品基本信息。
- 產(chǎn)品名稱輸入“LoRa門磁傳感器”或其他產(chǎn)品名稱。
- 產(chǎn)品類型選擇“門磁傳感器”。
- 認(rèn)證方式選擇“密鑰認(rèn)證”。
- 通信方式選擇“LoRaWAN”。

產(chǎn)品新建成功后,您可在產(chǎn)品列表頁查看到“LoRa門磁傳感器”。
1.3. 創(chuàng)建數(shù)據(jù)模板
選擇“門磁傳感器”類型后,自定義產(chǎn)品功能。
示例中,我們僅使用最簡單的默認(rèn)工作模式。
- 綁定 bound
- 上報周期 period
- 門窗打開狀態(tài) isOpen

1.4. 設(shè)備數(shù)據(jù)解析
在設(shè)備開發(fā)頁面中,按需調(diào)整 設(shè)備數(shù)據(jù)解析。由于 LoRa 類資源有限設(shè)備不適合直接傳輸 JSON 格式數(shù)據(jù),使用“設(shè)備數(shù)據(jù)解析”可以將設(shè)備原始數(shù)據(jù)轉(zhuǎn)化為產(chǎn)品JSON數(shù)據(jù)。
在上行數(shù)據(jù)解析部分,javascript示例代碼如下:
function RawToProtocol(fPort, bytes) {
const COMMAND_A1_UNBOUND = 0x02;
const COMMAND_A1_BOUND = 0x03;
const COMMAND_07_FLAG_MODE = 0x05;
const COMMAND_07_LEN_MODE = 1;
const COMMAND_07_FLAG_VER = 0x90;
const COMMAND_07_LEN_VER = 3;
const COMMAND_07_FLAG_DELAY = 0x9c;
const COMMAND_07_LEN_DELAY = 2;
const COMMAND_07_FLAG_ULPRD = 0x9d;
const COMMAND_0f_ALERT_KIND = 0x20;
const COMMAND_0f_ALERT_OPEN = 2;
const COMMAND_0f_ALERT_CLOSE = 4;
var data = {
"method": "report",
"clientToken" : new Date(),
"params" : {}
};
switch (bytes[0]) {
case 0xA1:
if (bytes[1] == COMMAND_A1_UNBOUND) {
data.params.bound = 0;
} else if (bytes[1] == COMMAND_A1_BOUND) {
data.params.bound = 1;
}
break;
case 0x07:
for (let i = 1; i < bytes.length; i++) {
if (bytes[i] == COMMAND_07_FLAG_MODE) {
i += COMMAND_07_LEN_MODE;
} else if (bytes[i] == COMMAND_07_FLAG_VER) {
i += COMMAND_07_LEN_VER;
} else if (bytes[i] == COMMAND_07_FLAG_DELAY) {
i += COMMAND_07_LEN_DELAY;
} else if (bytes[i] == COMMAND_07_FLAG_ULPRD) {
data.params.period = bytes[i+1] | (bytes[i+2] << 8);
}
}
break;
case 0x00:
if (bytes[5] & 0x80) {
data.params.isOpen = 0;
} else {
data.params.isOpen = 1;
}
break;
case 0x0f:
if (bytes[1] == COMMAND_0f_ALERT_KIND) {
if (bytes[2] == COMMAND_0f_ALERT_OPEN) {
data.params.isOpen = 1;
} else if (bytes[2] == COMMAND_0f_ALERT_CLOSE) {
data.params.isOpen = 0;
}
}
break;
}
return data;
}
在下行數(shù)據(jù)解析部分,javascript示例代碼如下:
function ProtocolToRaw(obj) {
var data = new Array();
var i = 0;
data[i++] = 8;// fport=8
data[i++] = 1;// confirmed mode
for (var property in obj.params) {
if ((property == "bound") && (obj.params[property] == 1)) {
data[i++] = 0xA1;
data[i++] = 0x03;
}
if (property == "period") {
data[i++] = 0x9D;
data[i++] = obj.params[property] & 0x00FF;
data[i++] = (obj.params[property] >> 8) & 0x00FF;
}
}
return data;
}

1.5 腳本模擬測試
這里也可以使用數(shù)據(jù)解析頁面下方的模擬調(diào)試工具,如果開發(fā)更多的功能,這個模擬腳本將會提供很大幫助。
- 上行消息 - 歸屬狀態(tài) 0xA1
設(shè)備原始數(shù)據(jù)為 0xA1,0x02,我們將其轉(zhuǎn)化為數(shù)組,即上行模擬數(shù)據(jù)為:[161,2],填入設(shè)備上行數(shù)據(jù)的編輯框中。
點擊運行,即可在模擬調(diào)試界面右側(cè)看到結(jié)果。

上行消息 - 設(shè)備基礎(chǔ)參數(shù) 0x07
設(shè)備原始數(shù)據(jù):07 05 06 90 00 08 04 9c 00 00 9d 80 70 9f 13 13 31 53 30 32 30 1f 02 05 20 95
模擬測試數(shù)據(jù):[7,5,6,144,0,8,4,156,0,0,157,128,112,159,19,19,49,83,48,50,48,31,2,5,32,149]上行消息 - 周期上報數(shù)據(jù)幀 0x00
設(shè)備原始數(shù)據(jù):00 06 00 00 00 10 d5
模擬測試數(shù)據(jù):[0,6,0,0,0,16,213]上行消息 - 報警 0x0F
設(shè)備原始數(shù)據(jù):0f 20 02 20 02 00 00 10
模擬測試數(shù)據(jù):[15,32,2,32,2,0,0,16]
這條消息為開門報警消息。設(shè)備原始數(shù)據(jù):0f 20 04 20 05 00 00 90
模擬測試數(shù)據(jù):[15,32,4,32,5,0,0,144]
這條消息為關(guān)門報警消息。下行消息 - 設(shè)置上報周期(心跳周期) 0x9D
模擬測試數(shù)據(jù)如下,將其填入設(shè)備下行數(shù)據(jù)的編輯框中:
{
"params": {
"period": 300
}
}

1.6 創(chuàng)建測試設(shè)備
在設(shè)備調(diào)試頁面中,單擊【新建設(shè)備】,設(shè)備名為 dws001。
- DevEUI,每一個設(shè)備有一個唯一的身份識別地址 DEVEUI, 設(shè)備據(jù)此進(jìn)行 LoRaWAN 網(wǎng)絡(luò)入網(wǎng)并身份識別。 信息以二維碼的形式體現(xiàn)在產(chǎn)品外殼。
本示例中為: 8cf957e0000001e7

- AppKey,設(shè)備的密鑰。
這可查找傳感器廠家相關(guān)資料獲取。

2. 控制臺操作 LoRa 網(wǎng)關(guān)
- 登錄 物聯(lián)網(wǎng)開發(fā)平臺控制臺,選擇上面章節(jié) “控制臺操作 LoRa 節(jié)點” 中對應(yīng)的項目。
-
在左側(cè)工具列表中選擇“服務(wù)中心”中的“LoRa網(wǎng)關(guān)管理”,并在“LoRa網(wǎng)關(guān)管理”頁面中選擇“新建網(wǎng)關(guān)”。
新建網(wǎng)關(guān)
在新建網(wǎng)關(guān)頁面,填寫網(wǎng)關(guān)基本信息。
網(wǎng)關(guān)名稱,本示例中填寫 GW2。
GwEUI,為網(wǎng)關(guān)唯一ID。本例中根據(jù) RAK 網(wǎng)關(guān)產(chǎn)品背部的 MAC 地址,將6字節(jié)mac地址的中間補足0xfffe。
是否公開。選擇“是”,表示社區(qū)開發(fā)者可在社區(qū)網(wǎng)絡(luò)中看到該網(wǎng)關(guān),并可通過這個網(wǎng)關(guān)進(jìn)行LoRa節(jié)點接入。我們鼓勵開發(fā)者們公開自己的網(wǎng)關(guān),盡可能幫助到其他開發(fā)者。選擇“否”,則只有用戶自己才能看到該網(wǎng)關(guān)。

3 LoRa網(wǎng)關(guān)實物操作
3.1. 連接配置
網(wǎng)關(guān)默認(rèn)開啟Wi-Fi AP 模式,可以直接通過web配置LoRa網(wǎng)關(guān),缺省IP:192.168.230.1,SSID:RAK72xx_xxxx(網(wǎng)關(guān)MAC地址尾號,見網(wǎng)關(guān)背后)
添加網(wǎng)關(guān)上行服務(wù)器的接口地址:loragw.things.qcloud.com,端口:1700

網(wǎng)關(guān)的上行頻段默認(rèn)設(shè)置使用8087信道編號, 頻點486.3487.7Mhz共8個頻點,LoRaWAN定義的終端頻點使用一共有96個信道,部分終端入網(wǎng)時需要搜索頻段,由于頻段范圍比較廣,會存在入網(wǎng)時間長的問題,需要耐心等待。
3.2 網(wǎng)關(guān)上線確認(rèn)
網(wǎng)關(guān)配置后之后重啟,過半分鐘左右會成功接入到物聯(lián)網(wǎng)開發(fā)平臺,我們可以在控制臺的用戶網(wǎng)關(guān)頁面中看到網(wǎng)關(guān)的在線情況。

4 LoRa 門磁傳感器實物操作
4.1 傳感器復(fù)位
根據(jù) 《門磁傳感器RHF1S020DWS規(guī)格書》中的操作說明,按照章節(jié) 3.8 進(jìn)入無線固件升級, 將磁鐵貼近門磁傳感器(CE標(biāo)志面的對面),紅色LED從慢閃逐漸常亮后挪開磁鐵, 即可讓設(shè)備會自動重置復(fù)位。

4.2 下發(fā)門磁歸屬
門磁傳感器在剛復(fù)位上電的一兩分鐘會上報3條歸屬狀態(tài)的消息,我們必須在這點時間內(nèi)盡快下發(fā)歸屬命令。

由于門磁傳感器是 LoRaWAN Class A 類設(shè)備,這類設(shè)備不會立即下發(fā)數(shù)據(jù),需要在有數(shù)據(jù)上行后,服務(wù)器才會向該設(shè)備下行數(shù)據(jù)。
因此我們下發(fā)了歸屬命令,當(dāng)傳感器上報歸屬狀態(tài)消息后,即可收到我們下發(fā)的歸屬命令。這樣門磁傳感器之后才可正常使用。
注意:設(shè)備一旦歸屬之后,會再上報周期參數(shù)上來。如果看到平臺的設(shè)備屬性中沒有更新“上報周期”,那說明傳感器還未歸屬成功。此時需要重新下發(fā)歸屬命令,然后再復(fù)位傳感器。
4.3 開關(guān)門操作
根據(jù) 《門磁傳感器RHF1S020DWS規(guī)格書》 中的章節(jié) 2.10 操作說明,
磁鐵部分外殼底部有一個開槽標(biāo)識, 安裝時請務(wù)必保證開槽的一邊朝向正對傳感器一邊。 主體傳感部分和用磁鐵部分相對方位如下圖, 分別安裝在門(或窗)固定邊和活動邊,距離小于 20mm(不需緊貼) , 主體傳感器部分和磁鐵部分安裝的表面高度基本在一個平面上。

如圖所示,磁鐵開槽方向貼近傳感器位置,即觸發(fā)了一次關(guān)門。將磁鐵挪開 35 mm 以上,即可觸發(fā)開門上報。
在控制臺的設(shè)備調(diào)試頁面下方則可以看到相應(yīng)的屬性做了更新。

至此,門磁傳感器的接入便完成了。
編碼聯(lián)動
編碼聯(lián)動分兩個步驟,分別是用JS控制智能燈,以及將其放到門磁傳感器的狀態(tài)變化事件中。
JS控制智能燈
這兒先脫離門磁傳感器,用JS實現(xiàn)兩個燈輪流變光變暗的效果:視頻地址。
?由于百度谷歌都沒有JS實現(xiàn)相關(guān)的教程,于是我只能從頭開始摸索了。yeelight的開發(fā)文檔比較簡陋,而且是全英文的,社區(qū)更是提問題的多,解答問題的少,真是不太友好。

對可行性存在懷疑的我決定先?嘗試運行一下網(wǎng)站提供的mac版demo代碼,看看是不是能跑通的。為此我還安裝了xcode。結(jié)果直接運行會報如下錯誤:
error: No account for team "S4JD8QVPVY". Add a new account in the Accounts ...
解決辦法是,這兒需要換上自己的開發(fā)者賬號所在的team。

運行成功后發(fā)現(xiàn)找不到燈設(shè)備,fine,那我暫時也沒辦法了。
本著快速實現(xiàn)的想法,我逆轉(zhuǎn)了一下思路,到github上看看有沒有相關(guān)的輪子。篩選一下語言、高贊、文檔詳細(xì)的項目后,最后選擇了幾個項目嘗試跑一下,但都沒有找到燈設(shè)備。百思不得其解地吃了一個下午茶,以及搶了一輪小米有品的口罩結(jié)果被耍猴之后,不得不感嘆整個下午都被小米耍了。
此刻作為一個逆轉(zhuǎn)裁判粉絲,我也逆轉(zhuǎn)一下思路,既然這么多demo都找不到設(shè)備,但他們既然存在,存在就是合理,那是不是其實demo是能跑通的,只是我缺了哪些步驟呢?把yeelight的論壇以及官網(wǎng)翻了個片后,我注意到官網(wǎng)的“局域網(wǎng)控制”入口,那是否我要給這些燈打開局域網(wǎng)控制才能夠被發(fā)現(xiàn)呢?于是我被逼按照提示下載了他們的app來打開控制。再運行一遍mac版的demo,果然找到燈泡設(shè)備了!github上的項目也能順利運行了。


沒想到要運行成功一個demo,還需要經(jīng)歷探索、推理、逆轉(zhuǎn)思路的過程,yeelight燈的開發(fā)入門真是趣味滿滿!建議官方出一個文檔,讓人按著步驟去做吧。這方面aqara的文檔就做得好多了。
對比試用幾個github庫后,我最終選擇了jamesblanksby的庫來開發(fā),雖然現(xiàn)在已經(jīng)不再維護(hù)了。
實現(xiàn)如下:
先安裝庫:
npm i --save node-yeelight
再編寫代碼:
var Yeelight = require('node-yeelight');
var y = new Yeelight;
y.on('ready', function() {
console.log('ready');
y.discover();
});
y.on('deviceadded', function(device) {
console.log('device added');
y.connect(device);
});
var deviceList = [];
y.on('deviceconnected', function(device) {
console.log('device connected');
deviceList.push(device);
var state = true;
setTimeout(function(){
setInterval(function() {
// 設(shè)置燈泡開關(guān)及過度時間
// y.setPower(device, state, 2500);
state = state ? false : true;
// 設(shè)置燈泡亮度
y.setBrightness(
device, // device object
state?100:1, // brightness percentage (1-100)
800 // transition speed in ms
);
}, 1000);
},(deviceList.length-1)*1000);
});
y.listen();
以上代碼實現(xiàn)的是兩個燈輪流變光變暗,即開始的視頻效果。
將控制代碼放到門磁傳感器的狀態(tài)變化事件中
參考騰訊云api調(diào)試工具實現(xiàn)代碼如下:
//騰訊云傳感器初始化
const tencentcloud = require("tencentcloud-sdk-nodejs");
const IotexplorerClient = tencentcloud.iotexplorer.XXXX.Client;
const models = tencentcloud.iotexplorer.XXXX.Models;
const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;
let cred = new Credential("XXXXX", "XXXXXX");
let httpProfile = new HttpProfile();
httpProfile.endpoint = "iotexplorer.tencentcloudapi.com";
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;
let client = new IotexplorerClient(cred, "ap-guangzhou", clientProfile);
let req = new models.DescribeDeviceDataRequest();
let params = '{"ProductId":"XXXX","DeviceName":"dev001"}'
req.from_json_string(params);
//yeelight 初始化
var Yeelight = require('node-yeelight');
var y = new Yeelight;
y.on('ready', function() {
console.log('ready');
y.discover();
});
y.on('deviceadded', function(device) {
console.log('device added');
y.connect(device);
});
var deviceList = [];
y.on('deviceconnected', function(device) {
console.log('device connected');
deviceList.push(device);
});
y.listen();
// 監(jiān)聽門窗傳感器
var isOpen;
var interval = setInterval(function(){
client.DescribeDeviceData(req, function(errMsg, response) {
if (errMsg) {
console.log(errMsg);
clearInterval(interval);
return;
}
// console.log(response.to_json_string());
var data = JSON.parse(response.Data);
if(data.isOpen.Value != isOpen){
isOpen = data.isOpen.Value;
let isOpenStr = isOpen == 1 ? "open":"close";
console.log(`the door is ${isOpenStr}`)
deviceList.forEach((device)=>{
y.setPower(device, isOpen == 1, 100);
});
}
});
},500);
至此總算實現(xiàn)了門開燈開,門關(guān)燈關(guān)的效果了。可惜的是這個智能燈得是局域網(wǎng)控制,就是要時刻開著本地服務(wù)器來控制,有點不太方便了,如果能提供云控制的辦法就好了。
在遇到新的技術(shù)難題時,我們要嘗試從多個角度去分析應(yīng)對,一條路走不通就換個思路走,在解決的過程中逐漸理解問題所在。另外還要懂得用輪子,彎路會少走很多,效率也會提高很多。
