Homebridge 插件編寫

前言

Homebridge is a lightweight NodeJS server that emulates the iOS HomeKit API; 之前在linux上部署過homebridge,最近玩路由在路由器上部署了homebridge,具體怎麼部署就不說了,還是來重溫一下homebridge插件如何編寫,搭建一個Siri物聯網吧,小白不會nodejs有點憂桑!

Plugins can publish Accessories and/or Platforms. Accessories are individual devices, like a smart switch or a garage door. Platforms act like a single device but can expose a set of devices, like a house full of smart lightbulbs. 即插件會發布 Accessories 和 Platforms,Accessories是一個獨立的設備,而Platforms是這些設備所連接的同一個平臺/同一組設備

如何編寫插件

上圖可知,homebridge的作用就是連接Homekit協議和Device協議,起一個橋的作用。插件包含兩個文件pakage.json和index.js,package.json是管理依賴的,index.js是寫插件核心邏輯的。

先來看一下HomeKit協議的layout佈局:

  • Home:整棟房子的Accessory設備
  • Room:一間屋子的Accessory設備
  • Platform:一組Accessory設備
  • Accessory:獨立的Accessory設備
  • Bridge:特殊的Accessory設備,允許與不能直接與HomeKit通信的Accessory設備通信
  • Service:Service對應Accessory設備的功能,如車庫門有開門和關門服務
  • Characteristic:每個服務有一系列的Characteristic特性,每個特性有3個權限,read、write和notify,這些Characteristic特性可以在 here 裏找到

再來看一下homebridge插件的文件結構

--> mySwitch
	--> config
		--> config.json #插件配置文件
	--> plugin
		--> index.js #核心邏輯,需要自己編寫
		-->package.json #插件包管理

config.json模板如下:

{
    "bridge": {
        "name": "Homebridge",
        "username": "94:A1:A2:BE:0B:30",
        "port": 59376,
        "pin": "033-73-874"
    },
    "description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.",
    "accessories": [
        {
            "accessory": "MyDormSwitch",//index.js會調用
            "name": "燈"
        }
    ],
    "platforms": []
}

package.json模板如下:

{
  "name": "homebridge-myswitch",//必須以homebridge-開頭,index.js會調用
  "version": "1.0.0",
  "description": "this is a switch plugin",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "homebridge-plugin"
  ],
  "author": "Bayer",
  "license": "ISC",
  "engines": {
      "node": ">=0.12.0",
      "homebridge": ">=0.2.0"
  }
}
//以上字段必須都要有

第一步:API需求分析

以一個開關插件爲例,假設它需要的API有:

  • GET請求能夠返回一個布爾值表示當前開關的狀態(通過讀取switch的On characteristic )
  • POST請求包含一個布爾值表示開關的目標狀態(通過設置switch的On characteristic)

第二步:註冊Accessory設備

將mySwitch插件注入到homebridge,mySwitch是一個javascript對象

  module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;
  /* registerAccessory' three parameters is 
				      plugin-name(must begin with homebridge-* add must be defined in package.json), 
					  accessory-name(must be defined in config.js),
					  constructor-name(must be a function)*/
  homebridge.registerAccessory("homebridge-myswitch", "MyDormSwitch", mySwitch);
};

第三步:實例化Service服務

支持的service有(可以從HomeKitTypes.js文件中找到)

HomeKitTypes.js:2650: * Service "Accessory Information"
HomeKitTypes.js:2674: * Service "Air Purifier" #空氣淨化器
HomeKitTypes.js:2697: * Service "Air Quality Sensor" #空氣質量傳感器
HomeKitTypes.js:2727: * Service "Battery Service"
HomeKitTypes.js:2747: * Service "Camera RTP Stream Management"
HomeKitTypes.js:2770: * Service "Carbon Dioxide Sensor"
HomeKitTypes.js:2794: * Service "Carbon Monoxide Sensor"
HomeKitTypes.js:2818: * Service "Contact Sensor" #接觸傳感器
HomeKitTypes.js:2840: * Service "Door"
HomeKitTypes.js:2862: * Service "Doorbell"
HomeKitTypes.js:2882: * Service "Fan" #風扇
HomeKitTypes.js:2902: * Service "Fan v2"
HomeKitTypes.js:2926: * Service "Filter Maintenance"
HomeKitTypes.js:2946: * Service "Faucet"
HomeKitTypes.js:2965: * Service "Garage Door Opener"
HomeKitTypes.js:2987: * Service "Heater Cooler"
HomeKitTypes.js:3014: * Service "Humidifier Dehumidifier"
HomeKitTypes.js:3041: * Service "Humidity Sensor" #溼度傳感器
HomeKitTypes.js:3063: * Service "Irrigation System"
HomeKitTypes.js:3085: * Service "Leak Sensor"
HomeKitTypes.js:3107: * Service "Light Sensor" #光照傳感器
HomeKitTypes.js:3129: * Service "Lightbulb" #燈泡
HomeKitTypes.js:3151: * Service "Lock Management"
HomeKitTypes.js:3177: * Service "Lock Mechanism"
HomeKitTypes.js:3196: * Service "Microphone"
HomeKitTypes.js:3215: * Service "Motion Sensor"
HomeKitTypes.js:3237: * Service "Occupancy Sensor" #人體傳感器
HomeKitTypes.js:3259: * Service "Outlet"
HomeKitTypes.js:3278: * Service "Security System" #安全系統
HomeKitTypes.js:3300: * Service "Service Label"
HomeKitTypes.js:3318: * Service "Slat"
HomeKitTypes.js:3340: * Service "Smoke Sensor" #煙霧傳感器
HomeKitTypes.js:3362: * Service "Speaker"
HomeKitTypes.js:3381: * Service "Stateless Programmable Switch"
HomeKitTypes.js:3400: * Service "Switch" #開關
HomeKitTypes.js:3418: * Service "Temperature Sensor" #溫度傳感器
HomeKitTypes.js:3440: * Service "Thermostat" #恆溫器
HomeKitTypes.js:3466: * Service "Valve"
HomeKitTypes.js:3491: * Service "Window" #窗戶
HomeKitTypes.js:3513: * Service "Window Covering" #窗簾

註冊switch時涉及到的兩個service:

  • AccessoryInformation service:每個Accessory都需要廣播與設備本身相關的信息,無論其類型如何;它包含了ManufacturerModel SerialNumber等特性

    /**
     * Service "Accessory Information"
     */
    
    Service.AccessoryInformation = function(displayName, subtype) {
      Service.call(this, displayName, '0000003E-0000-1000-8000-0026BB765291', subtype);
    
      // Required Characteristics
      this.addCharacteristic(Characteristic.Identify);
      this.addCharacteristic(Characteristic.Manufacturer);
      this.addCharacteristic(Characteristic.Model);
      this.addCharacteristic(Characteristic.Name);
      this.addCharacteristic(Characteristic.SerialNumber);
      this.addCharacteristic(Characteristic.FirmwareRevision);
    
      // Optional Characteristics
      this.addOptionalCharacteristic(Characteristic.HardwareRevision);
      this.addOptionalCharacteristic(Characteristic.AccessoryFlags);
    };
    
  • Swith service:真正的開關service,switch設備具有布爾特性On

    /**
     * Service "Switch"
     */
    
    Service.Switch = function(displayName, subtype) {
      Service.call(this, displayName, '00000049-0000-1000-8000-0026BB765291', subtype);
    
      // Required Characteristics
      this.addCharacteristic(Characteristic.On);
    
      // Optional Characteristics
      this.addOptionalCharacteristic(Characteristic.Name);
    };
    
  • 每種設備的特性都可以在HomeKitTypes.js源碼中查找到

mySwitch對象的getServices原型函數中,需要實例化以上兩個service。每個service的每個characteristic 的getter和setter方法,將調用來自Homekit每個requests

function mySwitch(log, config) {
  this.log = log;
  this.name = config['name'];//get parameter from config.json
  this.informationService = new Service.AccessoryInformation();

  this.switchService = new Service.Switch(this.name);// Service.Switch means this is a plugin of Switch
  this.switchService.getCharacteristic(Characteristic.On)
              .on('get',this.getSwitchOnCharacteristic.bind(this))//getter
              .on('set',this.setSwitchOnCharacteristic.bind(this));//setter
}

mySwitch.prototype.getServices = function() {
  return [this.switchService];
}

不同於AccessoryInformation服務的特性(它是可讀的,但只能在插件初始化時設置),On特性既可以寫亦可以響應getter和setter

第四步:實例化Characteristic

可以從HomeKitTypes.js文件中找到Characteristic "On"有關代碼,可以看出On特性有一個value值,需要在setter中使用。

/**
 * Characteristic "On"
 */

Characteristic.On = function() {
  Characteristic.call(this, 'On', '00000025-0000-1000-8000-0026BB765291');
  this.setProps({
    format: Characteristic.Formats.BOOL,
    perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
  });
  this.value = this.getDefaultValue();
};

inherits(Characteristic.On, Characteristic);

Characteristic.On.UUID = '00000025-0000-1000-8000-0026BB765291';

mySwitch對象的原型函數中編寫On特性的getter和setter的邏輯。這部分涉及硬件操作,這裏硬件上使用ser2net服務將tcp接口轉爲串口設備,通過向tcp寫入不同的命令從而控制下位機的硬件開關設備舵機

//create tcp connect
var net = require('net');
var client = new net.Socket();

var onbuf =  new Buffer([0xaa,0x08,0,0,0,0x14,0,0,0,0x66],'hex');//cmd: turn on
var offbuf = new Buffer([0xaa,0x08,0,0,0,0x14,0,0,0,0x44],'hex');//cmd: turn off
var inibuf = new Buffer([0xaa,0x08,0,0,0,0x14,0,0,0,0x55],'hex');//cmd: reset

client.setEncoding('binary');
client.connect(2018,'localhost',function(){
    console.log('Connecte Server OK!');//connect ser2net
});

//實例化
mySwitch.prototype.getSwitchOnCharacteristic = function (callback) {//getter
    console.log("**************Get on Function**************");
    callback(null);
  }

mySwitch.prototype.setSwitchOnCharacteristic = function (on,callback) {//setter
    console.log("**************Set on Function:"+ on + "**************");

    if(on)
      client.write(onbuf);
    else
      client.write(offbuf);

    setTimeout(function(){
	    client.write(inibuf);},500);//500ms Rest init statue
    
    callback(null);
}

第五步:啓動homebridge

可以使用homebridge -help查看如何啓用,這裏寫一個常用Debug命令:DEBUG=* homebridge -D -U ./config/ -P ./plugin/,若要設置爲開機自啓,將命令行DEBUG=* homebridge -D -U ./config/ -P ./plugin/ >/dev/null添加到rc.local文件即可

附錄一:開關插件

//mySwitch完整index.js代碼
var Service, Characteristic;
var net = require('net');
var client = new net.Socket();

var onbuf =  new Buffer([0xaa,0x08,0,0,0,0x14,0,0,0,0x66],'hex');//cmd: turn on
var offbuf = new Buffer([0xaa,0x08,0,0,0,0x14,0,0,0,0x44],'hex');//cmd: turn off
var inibuf = new Buffer([0xaa,0x08,0,0,0,0x14,0,0,0,0x55],'hex');//cmd: reset

client.setEncoding('binary');
client.connect(2018,'localhost',function(){
    console.log('Connecte Server OK!');//connect ser2net
});

module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;
  /* registerAccessory' three parameters is 
				          plugin-name(must begin with homebridge-* add must be defined in package.json), 
					  accessory-name(must be defined in config.js),
					  constructor-name(must be a function)*/
  homebridge.registerAccessory("homebridge-myswitch", "MyDormSwitch", mySwitch);
};
 
function mySwitch(log, config) {
  this.log = log;
  this.name = config['name'];//get parameter from config.json
  this.informationService = new Service.AccessoryInformation();

  this.switchService = new Service.Switch(this.name);// Service.Switch means this is a plugin of Switch
  this.switchService.getCharacteristic(Characteristic.On)
              .on('get',this.getSwitchOnCharacteristic.bind(this))
              .on('set',this.setSwitchOnCharacteristic.bind(this));
}

mySwitch.prototype.getServices = function() {
  return [this.switchService];
}

mySwitch.prototype.getSwitchOnCharacteristic = function (callback) {
    console.log("**************Get on Function**************");
    callback(null);
  }

mySwitch.prototype.setSwitchOnCharacteristic = function (on,callback) {
    console.log("**************Set on Function:"+ on + "**************");

    if(on)
      client.write(onbuf);
    else
      client.write(offbuf);

    setTimeout(function(){
	    client.write(inibuf);},500);//500ms Rest init statue
    
    callback(null);
}

附錄二:溫度傳感器插件

//mySensor完整index.js代碼
var Service, Characteristic;
var net = require('net');
var client = new net.Socket();
var that;

module.exports = function (homebridge) {
  Service = homebridge.hap.Service;
  Characteristic = homebridge.hap.Characteristic;
  homebridge.registerAccessory("homebridge-mysensor", "MyDepartTemp", mySensor);
};

function mySensor(log, config) {
  this.log = log;
  this.name = config['name'];
  this.informationService = new Service.AccessoryInformation();

  this.service = new Service.TemperatureSensor(this.name);
  this.service.getCharacteristic(Characteristic.CurrentTemperature)
      .on('get', this.getState.bind(this));
  
  that = this;
}

mySensor.prototype.getServices = function() {
  return [this.service];
}

mySensor.prototype.getState = function(callback) {  
    callback(null, 25.0);//default value
    return;
}

client.setEncoding('binary');
client.connect(2018,'localhost',function() {
    console.log('Connecte Server OK!');//connect ser2net
});

client.on('data',function(data) {
  console.log('from server:'+ data);
  that.service.getCharacteristic(Characteristic.CurrentTemperature).updateValue(data);//update
});

client.on('error',function(error){
  console.log('error:'+ error);
  client.destory();
});

備註:

如果從iPhone家庭終端刪除了設備,想再次連接該設備時,先刪除persist和accessories(homebridge自動生成的)文件,再運行homebridge。最後附上一個python的server代碼,可以替代ser2net,通過調用系統命令操作硬件設備舵機

#coding=utf-8
import socket
import os
server = socket.socket()
server.bind(('localhost',6969))
server.listen(5)
print("start----")
while True:
    conn,addr = server.accept()
    print(conn,addr)
    print("*****")
    while True:
        data = conn.recv(1024)
        if not data:
            print("client has lost")
            break
        else:
            print("data",data)
            param = data.decode()
            if 'false' == param:
                print("false")
                os.system('./pwm.sh 1135000')
            else:
                print("true")
                os.system('./pwm.sh 2240000')
        conn.send(data.upper())
server.close()

參考鏈接

Github homebridge
npm中已發佈的插件
如何開發homebridge插件
HomeBridge教程 v 0.0.3
樹莓派3B+ 智能家居HomeKit
使用homebridge-mqtt對接設備到HomeKit
How To Make Siri your Perfect Home Companion With Devices not Supported by Apple Homekit

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章