你知道 Apple HomeKit 吗?这是一个简单的解决方案,使您的家 “聪明”。但是这也是非常昂贵的,因为每个 HomeKit-Accessory 都需要获得许可。
这个障碍可以通过 HomeBridge 来解决。该项目旨在使不兼容的设备在 HomeKit 生态系统中可用。
要配置 HomeBridge 并添加新设备,您必须手动调整 JSON 文件。在我自己和我父母的家中实施 HomeBridge 后,我发现需要一个更简单的解决方案来配置这一切。
我想出了一个提供 Node.js 网页的 HomeBridge 插件。 这个插件是由 HomeBridge 本身启动的,读取 config.json 文件,解析所有的部分,并在简单的网站上显示。该项目现在在 HomeBridge-Server 下的 GitHub 上,可以通过 npm 来安装:
npm install homebridge-server@latest -g
有关如何设置以及如何使用插件的更多信息,请参阅 Wiki。
该网站使用Twitter Bootstrap框架,并提供一个菜单栏来对 config.json
进行备份,显示 HomeBridge 的日志文件并重启系统。并且,它提供了一个可视化的方式,来调整 config.json
与不同的输入字段。您可以轻松添加或删除平台和插件。
所有可用的平台和附件都列在包含信息栏中的类型(type),给定名称(name)和所有附加字段的信息栏(info-column)中。信息栏实际上是最难的部分,因为插件需要的附加字段取决于单个插件开发人员。我认为最简单的方法是向用户提供这些信息,只是正确地输出 JSON。
这是我的第一个 Node.js 项目,所以我尽量保持它尽可能小,也许很多事情都不是完美的。我是开放的,愿意为每一个改进!
.------------.
| HomeBridge |
'-----.------'
| .------------------------.
.-------------------'--------------------. |'/' |
| HomeBridge-Server | |'/saveBridgeSettings' |
|========================================| |'/addPlatform' |
|handleRequest(req, res)....................|'/addAccessory' |
|stripEscapeCodes(chunk) | |'/savePlatformSettings' |
|saveConfig(res, backup) | |'/saveAccessorySettings'|
|reloadConfig(res) | |'/createBackup' |
|prepareConfig() | |'/showLog' |
|printAddPage(res, type, additionalInput)| |'/reboot' |
|printMainPage(res) | |default |
'----------------------------------------' '------------------------'
该插件由一个请求处理程序组成,该处理程序对该地址作出反应并调用相应的函数。所有其他的都是辅助功能,下面将详细介绍。
为了使我的开发过程更加清晰,我将一步一步地浏览整个文件。
第一部分是使得 HomeBridge 可以访问插件,并设置基本参数:
var Service, Characteristic, LastUpdate;
module.exports = function(homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
homebridge.registerPlatform("homebridge-server", "Server", Server);
}
接下来我提供 HomeBridge-plugin-manager 调用的主函数,要求文件系统访问读写 config.json
以及所需的节点 http-server
来为页面服务:
function Server(log, config) {
var self = this;
self.config = config;
self.log = log;
var fs = require('fs');
var http = require('http');
接下来,我读取并解析实际的 config.json
:
var configJSON = require(process.argv[process.argv.indexOf('-U') + 1] + '/config.json');
然后我准备两个变量的平台和两个配件。其中一个包含 JSON 对象,而另一个包含相应的字符串,以便我可以稍后在网页中替换并连接它们。
var platformsJSON = {};
var platforms = "";
var accessoriesJSON = {};
var accessories = "";
接下来,我准备 header 和 footer 变量。这些包含了 jQuery 和 Bootstrap 的资源,以及接近 Apples 字体和配色方案的视觉适应。
var bootstrap = "<link href="//maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css" rel="stylesheet"/>"
//+ "<link href="//cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.0/bootstrap3-editable/css/bootstrap-editable.css" rel="stylesheet">"
;
var font = "<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet" type="text/css"/>";
var style = "<style>h1, h2, h3, h4, h5, h6 {font-family: 'Open Sans', sans-serif;}p, div {font-family: 'Open Sans', sans-serif;} input[type='radio'], input[type='checkbox'] {line-height: normal; margin: 0;}</style>"
var header = "<html><meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/><head><title>Homebridge - Configuration</title>" + bootstrap + font + style + "</head><body style="padding-top: 70px;">";
var footer = "</body>"
//+ "<script src="//cdnjs.cloudflare.com/ajax/libs/x-editable/1.5.0/bootstrap3-editable/js/bootstrap-editable.min.js"></script>"
//+ "<script> $(document).ready(function() { $.fn.editable.defaults.mode = 'popup'; $('#username').editable(); }); </script>"
//+ "<script defer="defer" src="//code.jquery.com/jquery-ui-latest.min.js"></script>"
//+ "<script defer="defer" src="//code.jquery.com/jquery-latest.min.js"></script>"
+ "<script defer="defer" src="//maxcdn.bootstrapcdn.com/bootstrap/latest/js/bootstrap.min.js"></script>"
+ "</html>";
我已经注意到了一些资源,因为它们现在看起来很坏,所以我可以有一个完全响应的导航栏或功能的输入字段。 我认为输入字段是最重要的用途。;)
要创建一个导航栏,我实现了以下字符串:
var navBar = (function() {/*
<nav class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
<a class="navbar-brand" href="/">Homebridge - Configuration</a>
</div>
<div class="container-fluid">
<ul class="nav navbar-nav navbar-right">
<li><a href="/createBackup">Backup</a></li>
<li><a href="/showLog">Log</a></li>
<li><a href="/reboot">Reboot</a></li>
</ul>
</div>
</nav>
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];
然后我使用 html 的 table 语法以 table1
变量开始和以 table2
变量结束。这其中被用来包围即时创建的表格。我在网上发现了一个黑客行为,允许在代码中换行,以便表代码易于阅读。 hack 由一个内联函数组成,用于替换换行符以及作为注释给出的字符串中的 /*
和 */
。
var table1 = (function() {/*
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th width="15%">Type</th>
<th width="35%">Name</th>
<th width="40%">Info</th>
<th width="10%"></th>
</tr>
</thead>
<tbody>
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];
var table2 = (function() {/*
</tbody>
</table>
</div>
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];
然后我创建了最后一个重要的变量,它们将包含 HomeBridge 实例的名称(name),用户名(username)和引脚(pin)。
var bridgeName;
var bridgeUsername;
var bridgePin;
设置了所有的变量后,我开始实现所有需要的内联函数。
第一个函数是 ``stripEscapeCodes(chunk)。 这个函数接受一个带有转义码的字符串,替换它们并返回清理后的字符串。
function stripEscapeCodes(chunk) {
var receivedData = chunk.toString()
.replace(/\%40/g,'@')
.replace(/\%23/g,'#')
.replace(/\%7B/g,'{')
.replace(/\%0D/g,'')
.replace(/\%0A/g,'')
.replace(/\%2C/g,',')
.replace(/\%7D/g,'}')
.replace(/\%3A/g,':')
.replace(/\%22/g,'"')
.replace(/\+/g,' ')
.replace(/\+\+/g,'')
.replace(/\%2F/g,'/')
.replace(/\%3C/g,'<')
.replace(/\%3E/g,'>')
.replace(/\%5B/g,'[')
.replace(/\%5D/g,']');
return receivedData;
}
服务器的一个非常重要的步骤是,正确地将 JSON 内容转换为正确的 html 代码。这是由 prepareConfig()
函数提供的。主要是迭代 JSON 的内容,将每个项目添加到平台或附件字符串,并用 html 标记填充它以分隔表格列和行。
function prepareConfig() {
bridgeName = "<div class="form-group"><label for="homebridgename">Name:</label><input class="form-control" name="bridgeName" type="text" value='" + configJSON.bridge.name + "'/></div>";
bridgeUsername = "<div class="form-group"><label for="username">Username:</label><input class="form-control" name="bridgeUsername" type="text" value='" + configJSON.bridge.username + "'/></div>";
bridgePin = "<div class="form-group"><label for="pin">Pin:</label><input class="form-control" name="bridgePin" type="text" value='" + configJSON.bridge.pin + "'/></div>";
platformsJSON = configJSON.platforms;
platforms = "";
accessoriesJSON = configJSON.accessories;
accessories = "";
const wastebasket = "🗑";
const pen = "✍";
var symbolToPresent = wastebasket;
for (var id_platform in platformsJSON) {
var platformNoTypeNoName = JSON.parse(JSON.stringify(platformsJSON[id_platform]));
delete platformNoTypeNoName.platform;
delete platformNoTypeNoName.name;
var platform = platformsJSON[id_platform];
platforms = platforms + "<tr>"
+ "<td style="vertical-align:middle;">" + platform.platform + "</td>"
+ "<td style="vertical-align:middle;">" + platform.name + "</td>"
+ "<td style="vertical-align:middle;">" + (JSON.stringify(platformNoTypeNoName, null, ' ')).replace(/,/g,',<br/>') + "</td>"
+ "<td style="vertical-align:middle;"><a class="btn btn-default center-block" href='/removePlatform" + id_platform + "' style="height: 34px; line-height: 16px; vertical-align:middle;outline:none !important;"><span '="" style="font-size:25px;">" + symbolToPresent + ";</span></a>"
+ "</td></tr>";
}
for (var id_accessory in accessoriesJSON) {
var accessoryNoTypeNoName = JSON.parse(JSON.stringify(accessoriesJSON[id_accessory]));
delete accessoryNoTypeNoName.accessory;
delete accessoryNoTypeNoName.name;
var accessory = accessoriesJSON[id_accessory];
accessories = accessories + "<tr>"
+ "<td style="vertical-align:middle;">" + accessory.accessory + "</td>"
+ "<td style="vertical-align:middle;">" + accessory.name + "</td>"
+ "<td style="vertical-align:middle;">" + (JSON.stringify(accessoryNoTypeNoName, null, ' ')).replace(/,/g,',<br/>') + "</td>"
+ "<td style="vertical-align:middle;"><a class="btn btn-default center-block" href='/removeAccessory" + id_accessory + "' style="height: 34px; line-height: 16px; vertical-align:middle;outline:none !important;"><span '="" style="font-size:25px;">" + symbolToPresent + ";</span></a>"
+ "</td></tr>";
}
}
为了向用户展示添加平台或附件的能力,服务器提供了由 printAddPage(res,type,additionalInput)
函数创建的添加页面。 此功能只显示一个文本字段(texture-field ),用户可以在其中添加所需平台或附件的复制 JSON 代码。 如果页面显示为添加平台(platform)或配件(accessory)取决于用户来自哪里。该函数接受三个参数。用于 html 代码的响应变量 res,类型变量类型,简单地添加到 html 标记的字符串以及在平台和附件之间分离的 POST-link,以及额外的输入字符串 additionalInput
来提供反馈,如果 用户输入了不正确的JSON代码。
function printAddPage(res, type, additionalInput) {
res.write(header + navBar);
res.write("<div class="container">");
if(additionalInput != null) {
res.write(additionalInput);
}
res.write("<h2>Add " + type + "</h2>");
res.write("<form action='/save" + type + "Settings' enctype="application/x-www-form-urlencoded" method="post">")
res.write("<textarea class="form-control" name='" + type + "ToAdd' placeholder='{ \"" + type + "\": \"test\" }' required="" rows="10" type="text"></textarea>");
res.write("<br/>");
res.write("<div class="row">");
res.write("<div class="col-xs-offset-1 col-sm-offset-1 col-md-offset-2 col-xs-10 col-sm-9 col-md-8 text-center">");
res.write("<div class="btn-group" data-toggle="buttons">");
res.write("<input class="btn btn-default center-block" style="width:135px" type="submit" value="Save">");
res.write("<a class="btn btn-default center-block" href="/" style="width:135px">Cancel</a>");
res.write("</input></div>");
res.write("</div>");
res.write("</div></form>");
res.write("<br/>");
res.write("</div>");
res.end(footer);
}
第二个输出函数是 printMainPage(res)
,它连接预编译的变量来为主网页提供服务。用户可以与导航栏交互,保存调用 /saveBridgeSettings
处理程序的更改设置(请参阅下面的更多信息),通过调用 /addPlatform-
或 /addAccessory
处理程序添加平台和附件,并通过单击删除每个平台或设备到废纸篓里(这是早些时候,当每个项目被添加到表中)。
function printMainPage(res) {
res.write(header + navBar);
res.write("<div class="container">");
//res.write("<h1>Homebridge</h1>");
//res.write("<h2>Configuration</h2>");
res.write("<form action="/saveBridgeSettings" enctype="application/x-www-form-urlencoded" method="post">")
res.write(bridgeName + bridgeUsername + bridgePin);
res.write("<input class="btn btn-default center-block" style="width:135px" type="submit" value="Save">");
res.write("</input></form>");
res.write("<h2>Platforms</h2>");
if (0 < Object.keys(platformsJSON).length) {
res.write(table1 + platforms + table2);
} else {
res.write("No platforms installed or configured!");
}
res.write("<a class="btn btn-default center-block" href="/addPlatform" name="AddPlatform" style="width:135px">Add</a><br/>");
res.write("<h2>Accessories</h2>");
if (0 < Object.keys(accessoriesJSON).length) {
res.write(table1 + accessories + table2);
} else {
res.write("No accessories installed or configured!");
}
res.write("<a class="btn btn-default center-block" href="/addAccessory" name="AddAccessory" style="width:135px">Add</a><br/>");
res.write("</div>");
res.end(footer);
reloadConfig(res)
函数重新读取并解析 config.json
,重置变量并输出到主页面。
function reloadConfig(res) {
configJSON = require(process.argv[process.argv.indexOf('-U') + 1] + '/config.json');
prepareConfig();
printMainPage(res);
}
为了使更改永久,服务器提供 saveConfig(res,backup)
功能。这个函数使用一个响应变量 res,来打印网页上的一个标题和一个 bool 变量 backup
来决定如何保存配置。它将存储的 configJSON
转换成一个有效的字符串,替换所有空的项目,最后写入文件。
function saveConfig(res, backup) {
var newConfig = JSON.stringify(configJSON)
.replace(/\[,/g, '[')
.replace(/,null/g, '')
.replace(/null,/g, '')
.replace(/null/g, '')
.replace(/,,/g, ',')
.replace(/,\]/g, ']');
newConfig = JSON.stringify(JSON.parse(newConfig), null, 4);
if(backup != null) {
fs.writeFile(process.argv[process.argv.indexOf('-U') + 1] + '/config.json.bak', newConfig, "utf8", function (err, data) {
if (err) {
return console.log(err);
}
res.write(header + navBar);
res.write("<div class="alert alert-success alert-dismissible fade in out"><a class="close" data-dismiss="success" href="/">×</a><strong>Succes!</strong> Configuration saved!</div>");
res.end(footer);
});
} else {
res.write(header + navBar);
res.write("<div class="alert alert-danger alert-dismissible fade in out"><a class="close" data-dismiss="alert" href="/">×</a><strong>Note!</strong> Please restart Homebridge to activate your changes.</div>");
fs.writeFile(process.argv[process.argv.indexOf('-U') + 1] + '/config.json', newConfig, "utf8", reloadConfig(res));
}
}
在成功存储配置之后,该函数调用 reloadConfig(res)
函数来反映浏览器中的可视变化。这不会重新启动 HomeBridge,以便更改不会影响正在运行的系统。为了使这些改变不仅是永久性的,而且也是适用的,用户必须重新启动 HomeBridge 服务。 这可以通过重新启动服务本身或重新启动整个系统来完成。后者可以通过网页导航栏中的重新启动按钮触发。
最后一个重要的功能是 handleRequest(req,res)
功能。 此功能指示服务器如何响应不同的请求的网页。 它是作为 switch-case-statement
来实现的。
function handleRequest(req, res) {
switch (req.url) {
第一种情况是通过准备配置,并返回主页面来处理根 /
请求。
case '/':
prepareConfig();
printMainPage(res);
break;
第二种情况处理 /saveBridgeSettings
请求。 这有点复杂,因为客户端必须发 POST 请求配置来保存。该函数然后拆分接收到的字符串,并去除所有的转义码。
case '/saveBridgeSettings':
if (req.method == 'POST') {
req.on('data', function(chunk) {
var receivedData = chunk.toString();
console.log("[Homebridge-Server] received body data: " + receivedData);
var arr = receivedData.split("&");
configJSON.bridge.name = stripEscapeCodes(arr[0].replace('bridgeName=',''));
configJSON.bridge.username = arr[1].replace('bridgeUsername=','').replace(/\%3A/g,':');
configJSON.bridge.pin = arr[2].replace('bridgePin=','');
saveConfig(res);
console.log("[Homebridge-Server] Saved bridge settings.");
});
req.on('end', function(chunk) { });
} else {
console.log("[Homebridge-Server] [405] " + req.method + " to " + req.url);
}
break;
第三种情况 /addPlatform
和第四种情况 /addAccessory
用于向用户呈现单个项目的添加页面。
case '/addPlatform':
printAddPage(res, "Platform");
break;
case '/addAccessory':
printAddPage(res, "Accessory");
break;
接下来的两种情况 /savePlatformSettings
和 /saveAccessorySettings
解析给定的 JSON 代码,并将其存储在配置文件中的正确位置。如果检测到错误,则会打印带有错误代码的横幅,并将用户返回到相应的添加页面。
case '/savePlatformSettings':
if (req.method == 'POST') {
req.on('data', function(chunk) {
var receivedData = stripEscapeCodes(chunk).replace('PlatformToAdd=','');
try {
configJSON.platforms.push(JSON.parse(receivedData));
if(configJSON.platforms.length == 1) {
configJSON.platforms = JSON.parse(JSON.stringify(configJSON.platforms).replace('[,','['));
}
saveConfig(res);
console.log("[Homebridge-Server] Saved platform " + JSON.parse(receivedData).name + ".");
} catch (ex) {
res.write(header + navBar);
res.write("<div class="alert alert-danger alert-dismissible fade in out"><a class="close" data-dismiss="alert" href="/addPlatform">×</a><strong>Error!</strong> Invalid JSON-entry detected. Please verify your input!</div>");
printAddPage(res, "Platform", "{gfm-js-extract-pre-1}");
}
});
req.on('end', function(chunk) { });
} else {
console.log("[Homebridge-Server] [405] " + req.method + " to " + req.url);
}
break;
case '/saveAccessorySettings':
if (req.method == 'POST') {
req.on('data', function(chunk) {
var receivedData = stripEscapeCodes(chunk).replace('AccessoryToAdd=','');
try {
configJSON.accessories.push(JSON.parse(receivedData));
if(configJSON.accessories.length == 1) {
configJSON.accessories = JSON.parse(JSON.stringify(configJSON.accessories).replace('[,','['));
}
saveConfig(res);
console.log("[Homebridge-Server] Saved accessory " + JSON.parse(receivedData).name + ".");
} catch (ex) {
res.write(header + navBar);
res.write("<div class="alert alert-danger alert-dismissible fade in out"><a class="close" data-dismiss="alert" href="/addAccessory">×</a><strong>Error!</strong> Invalid JSON-entry detected. Please verify your input!</div>");
printAddPage(res, "Accessory", "{gfm-js-extract-pre-2}");
}
});
req.on('end', function(chunk) { });
} else {
console.log("[Homebridge-Server] [405] " + req.method + " to " + req.url);
}
break;
如果用户想要创建备份,请求将是 /createBackup
,处理程序将调用 saveConfig(res,true)
函数。
为了呈现 HomeBridge 的最新日志文件,服务将从存储中读取存储的日志文件,将其打包到代码容器中,并将客户端作为网页提供。这个网页是静态的,需要手动刷新。
case '/showLog':
logFile = require('fs');
logFile.readFile(self.config.log, 'utf8', function (err, log) {
if (err) {
return console.log(err);
}
res.write(header + navBar);
res.write("<div class="container">");
res.write("<h2>Log</h2>");
res.write("{gfm-js-extract-pre-3}");
res.write("</div>");
res.end(footer);
});
break;
要使所有更改处于活动状态,需要重新启动服务。由于有很多不同的可能性,来自动启动 HomeBridge
服务,我决定只提供重启整个系统的可能性。也许将来我可以在配置中添加另一个命令,这个命令将在这个按钮上单击。
case '/reboot':
var exec = require('child_process').exec;
var cmd = "sudo reboot";
exec(cmd, function(error, stdout, stderr) {
// command output is in stdout
});
break;
默认情况下,验证用户是否请求带有 /remove
前缀的页面。 地址的第二部分是 Platform
或 Accessory
,第三部分是按照 config.json
顺序的项目的索引。如果服务器发现给定的索引,它将从 configJSON 变量中删除,并且更改将被存储。
default:
url = req.url;
if (url.indexOf('/remove') !== -1) {
object = url.replace('/remove', '');
if (object.indexOf('Platform') !== -1) {
platform = object.replace('Platform', '');
delete configJSON.platforms[platform];
console.log("[Homebridge-Server] Removed platform " + platform + ".");
} else if (object.indexOf('Accessory') !== -1) {
accessory = object.replace('Accessory', '');
delete configJSON.accessories[accessory];
console.log("[Homebridge-Server] Removed accessory " + accessory + ".");
}
saveConfig(res);
}
};
}
为了完成程序,我最终启动服务器,让它监听定义的端口并打印服务器可到达的 IP 地址。
var server = http.createServer(handleRequest);
server.listen(self.config.port, function() {
require('dns').lookup(require('os').hostname(), function(err, add, fam) {
console.log("[Homebridge-Server] is listening on: http://%s:%s", add, self.config.port);
})
});
}
为了完成这个插件,我实现了一个空的附件回调函数,返回一个空的数组,这样服务器就不会被列为一个真正的 HomeKit 可访问的设备。
Server.prototype.accessories = function(callback) {
var self = this;
self.accessories = [];
callback(self.accessories);
}
原文链接:https://gismo141.github.io/configure-your-homebridge-2/index.html
观光\评论区