基于mqtt協(xié)議的智能家居系統(tǒng)解決方案

應用場景說明:

在經(jīng)濟高速發(fā)展的今天乒融,現(xiàn)代人對自己的生活的要求越來越高粤铭,家電設備也迅猛增加,但是日常生活中,人們不擅長對于家電的管理邮偎,而造成了大量的不必要的能耗損失蔚叨。在這樣的一種情況下棺克,如果有一套智能家居系統(tǒng)能夠管理家庭電器的狀態(tài)症概,我們也可以隨時的控制家電,這樣我們的生活效率將會有很大程度上的提高但荤。

在這樣的一個需求的大背景下罗岖,我們又對設備和設備之間,人和設備之間腹躁,進行了一番詳細的分析桑包。首先我們來看設備和設備之間的需求。

1.假設在N市的A設備的狀態(tài)發(fā)生改變纺非,我們需要遠在M市的B設備的狀態(tài)也發(fā)生改變哑了,這種需求我們稱為不同網(wǎng)段的設備間的聯(lián)動需求。

2.假設在N市的A設備的狀態(tài)發(fā)生改變烧颖,我們需要當前統(tǒng)一網(wǎng)段下的B設備也發(fā)生改變弱左,這種需求我們稱為同一網(wǎng)段的設備間的聯(lián)動需求。

3.假設主人遠行炕淮,而忘記了自己家有沒有鎖門拆火,有沒有關燈,有沒有不該開啟的電器設備關閉涂圆,這是我們需要得知家中設備運行的狀態(tài)们镜,并調(diào)控到最佳狀態(tài)。我們稱之為人機遠程調(diào)控需求润歉。

4.假設在家庭與廣域網(wǎng)斷網(wǎng)的情況下模狭,我們還可以得知和控制家庭的設備,而不是失去對于家庭設備間的控制踩衩。我們稱之為遠程失聯(lián)需求嚼鹉。

基于國內(nèi)市場的大需求和我們自行分析的小需求下,我們設計了一套滿足于以上四點控制剛性需求的智能家居控制系統(tǒng)驱富。

系統(tǒng)結構說明

我們現(xiàn)在有了上面的需求分析锚赤,這時我們就可以對系統(tǒng)進行選型和架構了,我們對于設備間的聯(lián)動需求進行分析后選擇了一種物聯(lián)網(wǎng)廣泛使用的推送消息的協(xié)議機制(mqtt),然后對它進行二次開發(fā)和封裝萌朱。下面我們就來看看系統(tǒng)的設計結構:

結構:

1.節(jié)點事件上報(publish nodeid event),這個場景用于當人在現(xiàn)場對設備的狀態(tài)進行了改變,這時策菜,該設備應該向主服務器進行通報晶疼,事件的發(fā)生酒贬,以及當前的狀態(tài),還有為了實現(xiàn)設備的熱插拔翠霍,當設備連上這套系統(tǒng)后锭吨,它便會廣播上線通知,當設備異常斷開系統(tǒng)后寒匙,會發(fā)送遺言離線通知零如,方便我們對節(jié)點事件異常進行及時的處理。

2.節(jié)點屬性上報(publish nodeid property),當人為的改變了設備后锄弱,主服務器和在線的控制端將會受到該設備節(jié)點的屬性上報通知考蕾,這個行為主要是及時的獲取設備點的狀態(tài)信息。當設備剛上線是也會進行屬性播報会宪,以便控制端熱加載設備肖卧。

3.節(jié)點方法被調(diào)用(subscribe nodeid call | publish 0 ack),當N市的A設備狀態(tài)發(fā)生改變,M市的B設備也要發(fā)生狀態(tài)的改變掸鹅,就會直接讓A去控制B設備塞帐,這時我們成A為控制器,B為執(zhí)行器巍沙。那么A就會調(diào)用控制遠程設備命令葵姥,B就會收到call命令之后執(zhí)行命令并返回一個ack以確認信息的無誤性。

4.系統(tǒng)廣播事件(subscribe 0 system),當所有的設備同時接受統(tǒng)一命令的調(diào)控時,我們?yōu)榱颂岣咝畔⑻幚淼男适褂孟到y(tǒng)廣播事件來統(tǒng)一調(diào)度句携。

在這五個控制總命令下榔幸,我們還將設計針對每種設備的控制子命令格式。從而達到既從屬分布式控制有歸屬于集中式控制系統(tǒng)务甥。

智能家居互聯(lián)的通訊協(xié)議:

1.角色定義:?

節(jié)點牡辽,設備,控制器敞临,服務器

2.主題結構:

?yqmiot/<accountid>/<receiver>/<sender>/<command>

3.消息結構:

{

receiver:?,???#?接受者nodeid

sender:?,???????#?發(fā)送者nodeid

name:?,????????#?主命令(名字有帶商定)

action:?,???????#?子命令(可為null)

callseq:?,?????#?調(diào)用序號(多次調(diào)用時確定回包對應的請求)?(非call和ack命令可以為null)

params:?,???????#?命令參數(shù)

#?seq:?,???????????#?包序號(用戶篩選重復數(shù)據(jù)包)?暫未使用

}

備注:receiver,sender,name?未來這三者在發(fā)送數(shù)據(jù)包中可能被省略态辛,因主題中已經(jīng)存在。

屬性上報(property)

-command: "property"

-params:?設備屬性?({"name":?"hello",?"status":?"正忙呢",?"yqmiot.property.nodeid":?27888})

事件上報(event)

-command: "event"

-action:?事件名?("yqmiot.event.online",?"yqmiot.event.offline")

-params:?事件參數(shù)

方法調(diào)用(call)

-command: "call"

-action:?方法名?("yqmiot.method.ping",?"yqmiot.method.test")

-callseq:?調(diào)用序號(每次調(diào)用都必須唯一)

-params:?方法參數(shù)

調(diào)用響應(ack)

-command: "ack"

-action:?call包中的action

-callseq:?call包中的seq

-params:?回應參數(shù)

其他(暫未使用)

服務器 nodeid: 0

全頻道廣播?nodeid:?0xffffffff

全服廣播?accountid:?0,?nodeid:?0

我們把通信協(xié)議搭建好了之后挺尿,就來開始構建整個系統(tǒng)奏黑,接下來就是要使用編程語言進行編程實現(xiàn)。從協(xié)議開始编矾,一步一步構建 整套系統(tǒng)的通訊層和應用層熟史,以及控制端。

系統(tǒng)的構建:

1.設備控制端的構建:

我們是基于可以運行嵌入式linux系統(tǒng)的設備窄俏,對節(jié)點進行控制蹂匹。由于linux系統(tǒng)的便利性。我們使用了python這種腳本對設備客戶端進行了編程處理凹蜈,接下來我們一步步的看限寞,被控器的客戶端構建忍啸。

1.1.引入依賴包和常用參數(shù).

# -*- encoding: utf-8 -*-

importlogging

importtime

importsys

importgetopt

importjson

frompaho.mqtt.clientimportClientasMqtt

VERSION?="1.0.1"

"""

每個設備都擁有三類特性:屬性,事件履植,方法计雌。

屬性表示設備的當前狀態(tài),比如:電力狀態(tài)玫霎,照明開關等凿滤。每當屬性發(fā)生改變就會立即上報。

事件表示設備當前發(fā)生了什么庶近,按下按鈕翁脆,電力不足警告等。

方法則是設備對外提供的操作接口拦盹,通過它可以對設備進行控制鹃祖。比如:重啟,打開照明普舆,關機等恬口。

"""

YQMIOT_OK?=0

YQMIOT_TIMEOUT?=1

YQMIOT_BROADCAST_RECEIVER?=0#?廣播接受者id

#?系統(tǒng)命令

YQMIOT_COMMAND_PROPERTY?="property"#?屬性上報

YQMIOT_COMMAND_EVENT?="event"#?事件上報

YQMIOT_COMMAND_CALL?="call"#?方法調(diào)用

YQMIOT_COMMAND_ACK?="ack"#?方法響應

#?系統(tǒng)事件

YQMIOT_EVENT_ONLINE?="yqmiot.event.online"#?上線通知

YQMIOT_EVENT_OFFLINE?="yqmiot.event.offline"#?下線通知

YQMIOT_EVENT_TEST?="yqmiot.event.test"#?按下測試按鈕

#?系統(tǒng)屬性

YQMIOT_PROPERTY_NODEID?="yqmiot.property.nodeid"#?節(jié)點id號

YQMIOT_PROPERTY_ACCOUNTID?="yqmiot.property.accountid"#?節(jié)點所在賬號id(頻道id)頻道隔離

YQMIOT_PROPERTY_MODEL?="yqmiot.property.model"#?設備所屬類型

YQMIOT_PROPERTY_VERSION?="yqmiot.property.version"#?設備所屬固件版本號

#?系統(tǒng)方法

YQMIOT_METHOD_PING?="yqmiot.method.ping"#?ping連通測試

YQMIOT_METHOD_TEST?="yqmiot.method.test"#?方法調(diào)用測試

logging.basicConfig(level=logging.DEBUG,

format='[%(asctime)s]?%(levelname)s?%(message)s',

datefmt='%Y-%m-%d?%H:%M:%S')

root?=?logging.getLogger()

root.setLevel(logging.NOTSET)

1.2.mqtt通訊層的基本封裝

classMqttClient(object):

"""Mqtt通訊封裝"""

def__init__(self,address):

ifnotisinstance(address,tuple)?orlen(address)?!=2:

raiseValueError("Invalid?address.")

defon_connect(client,userdata,flags,rc):

self.handleConnected()

defon_message(client,userdata,msg):

self.handleMessage(msg.topic,?msg.payload)

self.client?=?Mqtt()

self.address?=?address

self.client.on_connect?=?on_connect

self.client.on_message?=?on_message

defhandleConnected(self):

pass

defhandleMessage(self,topic,payload):

pass

defpublish(self,topic,payload=None,qos=0,retain=False):

self.client.publish(topic,?payload,?qos,?retain)

defsubscribe(self,topic,qos=0):

self.client.subscribe(topic,?qos)

defstart(self):

self.client.connect_async(self.address[0],self.address[1])

self.client.loop_start()

defstop(self):

self.client.loop_stop()

defusername_pw_set(self,username,password=None):

self.client.username_pw_set(username,?password)

defwill_set(self,topic,payload=None,qos=0,retain=False):

self.client.will_set(topic,?payload,?qos,?retain)

1.3.家居互聯(lián)通訊層封裝

classYqmiotBase(MqttClient):

"""月球貓互聯(lián)通訊基類"""

def__init__(self,address,accountid,nodeid,authkey=None,username=None,password=None):

"""username和password是mqtt賬號密碼。"""

super(YqmiotBase,self).__init__(address)

self.username?=?username

self.password?=?password

self.accountid?=?accountid

self.nodeid?=?nodeid

self.authkey?=?authkey#TODO

self.callMethodInfo?=?{}#

self.callMethodTimeout?=10*1000#?方法調(diào)用超時時間TODO處理多線程問題沼侣。調(diào)用超時

self.callseq?=0

ifself.accountid?<=0orself.nodeid?<=0:

raiseValueError("Invalid?accountid?or?nodeid.")

defhandleConnected(self):

super(YqmiotBase,self).handleConnected()

#?偵聽發(fā)送給自己的消息

topic?="yqmiot/{self.accountid}/{self.nodeid}/#".format(self=self)

self.subscribe(topic)

defhandleMessage(self,topic,payload):

super(YqmiotBase,self).handleMessage(topic,?payload)

try:

prefix,?account,?receiver,?sender,?command?=?topic.split("/")

account?=int(account)

receiver?=int(receiver)

sender?=int(sender)

except:

logging.error("Invalid?topic.?{}".format(topic))

return

#?if?prefix?!=?"yqmiot"?\

#?????or?account?!=?self.accountid?\

#?????or?receiver?!=?self.nodeid:?#TODO處理廣播

#?????logging.error("It's?not?my?topic.?{}".format(topic))

#?????return

try:

payload?=?json.loads(payload)

except:

logging.error("Invalid?payload.?{}".format(payload))

return

cmd?=?Command(

name=?command,

action=?payload.get("action"),

receiver=?receiver,

sender=?sender,

callseq=?payload.get("callseq"),

params=?payload.get("params"))

try:

self.handleCommand(cmd)

except:

logging.error("Error?processing?command.?{}".format(topic))

return

defsendCommand(self,cmd):

ifcmd:

try:

accountid?=self.accountid

receiver?=?cmd.receiverifcmd.receiver?!=NoneelseYQMIOT_BROADCAST_RECEIVER#?默認接受者是服務器

sender?=self.nodeid

name?=?cmd.name

action?=?cmd.action

callseq?=?cmd.callseq

params?=?cmd.paramsifcmd.params?!=Noneelse{}

topic?="yqmiot/{}/{}/{}/{}".format(accountid,?receiver,?sender,?name)

payload?=?{"action":?cmd.action,"callseq":?callseq,"params":?params}

self.publish(topic,?json.dumps(payload))

exceptException,?e:

logging.error("Error?sending?command."+str(e))

else:

logging.error("Invalid?cmd.")

defhandleCommand(self,cmd):

ifcmd.name?==?YQMIOT_COMMAND_CALL:

self.handleCommandCall(cmd)

elifcmd.name?==?YQMIOT_COMMAND_ACK:

callseq?=?cmd.callseq

ifcallseq?inself.callMethodInfo:

info?=self.callMethodInfo.pop(callseq)

cmd.action?=?info["action"]

cmd.time?=?millis()?-?info["time"]

self.handleCommandAck(cmd)

else:

logging.error("Drop?unknown?command.")

else:

logging.error("Command?not?supported.")

defhandleCommandCall(self,cmd):

ifcmd.action?==?YQMIOT_METHOD_PING:

self.handleCommandCallPing(cmd)

else:

logging.warn("Could?not?find?method.")

defhandleCommandAck(self,cmd):

ifcmd.action?==?YQMIOT_METHOD_PING:

self.handleCommandCallPingAck(cmd)

defcallMethod(self,receiver,action,params=None):

ifreceiver?and?receiver?!=?YQMIOT_BROADCAST_RECEIVER?and?action:

try:

self.callseq?+=1

cmd?=?Command(

name=?YQMIOT_COMMAND_CALL,

action=?action,

receiver=?receiver,

callseq=self.callseq,

params=?params)

self.callMethodInfo[cmd.callseq]?=?{"action":?action,"callseq":?cmd.callseq,"time":?millis()}

self.sendCommand(cmd)

except:

logging.error("Error?calling?remote?action.")

else:

logging.error("Remote?action?parameter?is?incorrect.")

defcallMethodPing(self,receiver):

self.callMethod(receiver,?YQMIOT_METHOD_PING)

defhandleCommandCallPing(self,cmd):

self.sendCommand(cmd.reply())

defhandleCommandCallPingAck(self,cmd):

pass

1.4.互聯(lián)客戶端封裝

classYqmiotClient(YqmiotBase):

"""月球貓互聯(lián)客戶端

屬性定時上報

屬性變更上報

事件上報

處理方法調(diào)用祖能,并回包"""

defstart(self):

#?離線通知

topic?="yqmiot/{}/{}/{}/{}".format(self.accountid,?YQMIOT_BROADCAST_RECEIVER,self.nodeid,?YQMIOT_COMMAND_EVENT)

payload?=?{"action":?YQMIOT_EVENT_OFFLINE}

self.will_set(topic,?json.dumps(payload))

super(YqmiotClient,self).start()

defhandleConnected(self):

super(YqmiotClient,self).handleConnected()

logging.info("Connect?server?successfully.")

#?上線通知

self.reportEvent(YQMIOT_EVENT_ONLINE)

#TODO推送下線遺言

defreportProperty(self,params):

"""屬性上報

params(dict)?設備屬性集"""

ifisinstance(params,dict):

try:

cmd?=?Command(

name=?YQMIOT_COMMAND_PROPERTY,

receiver=?YQMIOT_BROADCAST_RECEIVER,

params=?params)

self.sendCommand(cmd)

except:

logging.error("An?error?occurred?while?reporting?the?property.")

else:

raiseTypeError("Incorrect?params?type.")

defreportEvent(self,action,params=None):

"""事件上報

action?事件名

params?參數(shù)"""

ifaction:

try:

cmd?=?Command(

name=?YQMIOT_COMMAND_EVENT,

action=?action,

receiver=?YQMIOT_BROADCAST_RECEIVER,

params=?params)

self.sendCommand(cmd)

except:

logging.error("An?error?occurred?while?reporting?the?event.")

else:

raiseTypeError("Incorrect?action?type.")

1.5.家居系統(tǒng)互聯(lián)控制器封裝

classYqmiotController(YqmiotBase):

"""

月球貓互聯(lián)控制器

"""

#?訂閱廣播消息

defhandleConnected(self):

super(YqmiotController,self).handleConnected()

logging.info("Connect?server?successfully.")

#?偵聽設備上報

topic?="yqmiot/{self.accountid}/0/#".format(self=self)

self.subscribe(topic)

defhandleCommand(self,cmd):

ifcmd.name?==?YQMIOT_COMMAND_PROPERTY:

self.handleCommandProperty(cmd)

elifcmd.name?==?YQMIOT_COMMAND_EVENT:

self.handleCommandEvent(cmd)

else:

super(YqmiotController,self).handleCommand(cmd)

defhandleCommandProperty(self,cmd):

print"設備?{}?上報屬性:{}".format(cmd.sender,?cmd.params)

defhandleCommandEvent(self,cmd):

print"設備?{}?上報事件:{}?參數(shù):{}".format(cmd.sender,?cmd.action,?cmd.params)


到這里為止,我們的控制系統(tǒng)的客戶端已經(jīng)封裝完畢蛾洛,但是這才剛剛起步养铸,我們有了客戶端,那我們還需要遠程控制器轧膘,我們?yōu)榱撕啽闫鹨娛褂昧藈eb終端的方案钞螟。來進行對設備客戶端的控制,由于代碼量很大我這里就簡要的介紹一下谎碍。

在控制端中主要使用的mqtt推送協(xié)議鳞滨,然后轉(zhuǎn)換成socket以便實時控制。因為我們的技術棧使用的是vuejs蟆淀,大家如果不了解可以先去了解了解拯啦,這是一種以數(shù)據(jù)為驅(qū)動的web解決方案,告別了傳統(tǒng)的dom節(jié)點控制熔任。使得運行速度和性能得到了很大的提升褒链。我們在控制得到socket數(shù)據(jù)后,然后進行分發(fā)進入各種控制器疑苔,分別管理不同數(shù)據(jù)和業(yè)務邏輯的實現(xiàn)以及數(shù)據(jù)的調(diào)配甫匹。

實踐效果:


設備列表


設備詳情

好下面我們就來看看最后達到的控制效果吧!


智能家居控制系統(tǒng)測試視頻



智能家居互聯(lián)




項目地址:https://github.com/yqmiot

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市兵迅,隨后出現(xiàn)的幾起案子哀墓,更是在濱河造成了極大的恐慌,老刑警劉巖喷兼,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異后雷,居然都是意外死亡季惯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門臀突,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勉抓,“玉大人,你說我怎么就攤上這事候学∨航睿” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵梳码,是天一觀的道長隐圾。 經(jīng)常有香客問我,道長掰茶,這世上最難降的妖魔是什么暇藏? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮濒蒋,結果婚禮上盐碱,老公的妹妹穿的比我還像新娘。我一直安慰自己沪伙,他們只是感情好瓮顽,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著围橡,像睡著了一般暖混。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上某饰,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天儒恋,我揣著相機與錄音,去河邊找鬼黔漂。 笑死诫尽,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的炬守。 我是一名探鬼主播牧嫉,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了酣藻?” 一聲冷哼從身側響起曹洽,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辽剧,沒想到半個月后送淆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡怕轿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年偷崩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撞羽。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡阐斜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诀紊,到底是詐尸還是另有隱情谒出,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布邻奠,位于F島的核電站笤喳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏碌宴。R本人自食惡果不足惜莉测,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唧喉。 院中可真熱鬧捣卤,春花似錦、人聲如沸八孝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽干跛。三九已至子姜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間楼入,已是汗流浹背哥捕。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嘉熊,地道東北人遥赚。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像阐肤,于是被迫代替她去往敵國和親凫佛。 傳聞我的和親對象是個殘疾皇子讲坎,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容