HomeKit和ioBroker让我们在家交朋友


毫无疑问,Apple iOS仍然是最受欢迎的移动操作系统之一,这意味着现代自动化系统应该能够集成到该生态系统中并提供互操作性。 这正是Homekit框架的设计目的,它使您可以使用iPhone / iPad / iWatch屏幕以及最近的Mac(macOS Mojave)中的智能设备。


大多数自动化系统(我不喜欢市场名称“ smart home”)早已包含用于与Homekit集成的模块,但是即使是受过培训的用户也不能总是只想出如何在Home(或Eve)应用程序中使用其设备的方法。


今天,我将告诉您如何在ioBroker系统(这是一个开放和免费的自动化系统)中进行这些操作。 但是为了不愚蠢地给出所有许多设备示例,我想解释一些原理并展示方法,知道哪种方法后,您可以轻松实现其他示例。


“知道一些原则很容易弥补对某些事实的无知。”
克劳德·阿德里安·赫尔维蒂乌斯


ioBroker。 驱动程序,设备和状态


首先,我想解释一下ioBroker系统中的设备及其显示方式。


让我提醒您,ioBroker系统是模块化的,扩展模块称为驱动程序(或适配器)。 驱动程序是具有某些设备或一组设备的集成模块,由一个共同的功能,协议或制造商联合在一起,因此可以将一个或多个设备“拖动”到ioBroker系统中。 另一个功能是能够创建同一驱动程序的多个实例,但在任何设置上都不同。


但是每种设备都是独特而独特的,具有不同的特性和功能。 基于此,ioBroker主要不关注设备本身,而是关注其状态表示的特征。 状态是一个内部ioBroker对象,它接受并存储一个值。 可以考虑状态的同义词:符号,属性,特征,特性,事件。 条件的示例:“温度”,“亮度级别”,“电池级别”,“开机标志”,“错误标志”,“按下标志”,“两次按下标志”等。 因此,每个设备由许多不同的状态表示。


对象结构


状态可以分为信息性状态(显示设备信息)和可变状态(状态可以由用户或脚本更改)并将这些更改发送到设备。 因此,当设备上发生某些更改时(该数据以状态显示,并且状态从ioBroker更改(由用户或脚本)更改),该设备会收到有关更改的信号,并且必须做出相应的响应(取决于设备本身和驱动程序)作品)。


所有设备状态都组合成一个状态树(注册表)。 它们首先按设备分组(在某些情况下,仍使用通道),然后按驱动程序实例分组。


MQTT协议主题的概念很容易适合这种状态树。 这样,您可以连接支持MQTT协议的其他设备或第三方系统。 安装MQTT驱动程序就足够了-相应的分支将出现在状态树中。


并且有各种各样的在线服务可以提供有用的信息和/或控制其他设备(例如汽车警报器)。 与这些服务交互的结果也表示为一组状态。


状态树


总体而言,ioBroker中的设备由表征设备并允许与其交互的一组状态表示。


家庭套件 配件,服务和规格


现在转到Homekit。 在此应用设备的分类,其功能和特性。


Homekit设备类别


附件相当于物理设备。 附件具有用于将其分配给特定组的类别


服务等同于附件具有的功能。 一个配件可以提供多种服务。


服务表明设备的功能:灯,电池,按钮,空气质量传感器,门,空气滤清器,照相机..


服务决定显示器,设备的行为和特性集。


特征等同于表征服务的属性。 这些特性决定了设备是否已开启,灯泡的亮度级别或按钮被按下了多少次。 一项服务可以具有许多特征。


对象结构


使用Homekit的应用程序会读取附件的服务和特征,然后显示并允许您通过用户界面更改特征中的值。 更改后的值被发送到Homekit设备以应用它们,并且分别从Homekit设备发送特性值,并从设备侧面发送一些更改。


总体而言,HomeKit中的设备似乎是具有一系列服务和功能的附件。


耶卡 我们加入概念


要使用Homekit,ioBroker使用Yahka驱动程序( 安装前必须安装其他模块 )-知名库的附加组件 https://github.com/KhaosT/HAP-NodeJS ,它还构建了流行的HomeBridge项目。 该库旨在创建虚拟网关/网桥,以在HomeKit中提供一组虚拟设备。 相应地配置虚拟设备和服务,设置特征值,我们将在Homekit和Home应用程序中获得完成的设备,还可以要求Siri对其进行管理。


Yahka驱动程序仅用于配置附件,向其中添加服务并指示特性(HomeKit)和条件(ioBroker)的符合性。


但是首先,在安装后,您需要配置网关并将其放入Home应用程序。 配置完成后,添加到网关的所有设备将自动添加到家庭。 为此,请指定“设备名称”(最好仅指定拉丁字母)并记住密码(或设置您自己的密码)。


网关设置


我们转到Home应用程序并添加一个新附件。



现在让我们开始讨论设备。 如果ioBroker中设备的状态集显然与HomeKit中的服务和功能集相对应,那么一切都会很好。 如果状态中的值适合于特性的值,那就更好了。 但是通常情况并非如此,您必须提出不同寻常的对接方式。 我将在下面讨论其中的一些选项,您将不得不自己实现所有其他选项,“以图像和样式”。


为了方便起见,我创建了一个文档 ,其中包含服务和类型的转换以及特征的可能值。 使用的所有类型和服务都对应于HAP-NodeJS库


温度传感器


这是最简单的示例-您所要做的就是让一个状态包含温度的数值。 它可以从任何地方获取:传感器或Internet服务(天气)。

您需要添加“传感器”类别的设备,并将“温度传感器”服务添加到该设备,并为此服务命名。 此服务具有5个特征,其中对我们而言最重要的是CurrentTemperature。


配件温度计


温度传感器服务


在CurrentTemperature特性中指示与温度相对应的状态名称就足够了。


也可以在此处添加HumiditySensor湿度服务,并且将在Homekit中创建一个单独的附件图标。


湿度传感器服务


保存,就完成了。 现在,您可以转到Siri,并询问她有关温度和湿度的信息。



与Siri对话



电瓶


另一个简单的服务。 它的诀窍是可以将其添加到几乎所有附件中。 添加BatteryService服务,并在BatteryLevel特性中指示包含电池电量百分比的状态。 之后,费用数据将出现在有关设备的其他数据中。


电池服务


您可以立即设置“低电量”的标志(特征StatusLowBattery),如果指定状态的值等于1,则相应的图标将显示在设备图像上。


但是,如果您没有这种状态,但想查看低电量图标,该怎么办? 您需要手动或使用脚本创建此状态,并在特征中指示创建的状态。


现在只剩下在此状态下正确设置值为true了。 为此,我们将使用脚本-当电池电量达到30%时,它将设置为true。


createState(""); on({id: "zigbee.0.00158d0001f41725.battery", change: "ne"}, function (obj) {   var value = obj.state.val; setState("javascript.0.", (value <= 30)); }); 

第一次运行后,脚本将创建一个状态,并且可以在特征中选择它。



此标志将显示在附件的图像上



和设备详细信息


台灯


灯泡不同-明亮,温暖,红色。 有4种情况:


  • 简单-由开和关控制
  • 可调光-也由亮度级别控制
  • 通过温度-可以控制发光的温度
  • 颜色-您可以控制发光的颜色

对于每种情况,灯泡服务都有一个对应的特征:


  • 开-开/关
  • 亮度-亮度等级
  • 色相-阴影
  • 饱和度-饱和度
  • ColorTemperature-色温

在简单的情况下,在“打开”特性中,我们指示负责打开和关闭的状态。



如果灯泡是可调光的,那么我们还会用亮度级别指示状态。



除了分配正确的状态外,观察可接受值的间隔也很重要!


例如:在某些情况下,负责灯泡亮度的状态可以采用0到255之间的值,但是在Homekit中,这些值限制为0到100之间的间隔。在这种情况下,可以使用Yahka驱动程序转换函数 。 函数“ level255”仅将值0..255的间隔转换为间隔0..100(反之亦然)。


如果您的灯是彩色的,但使用的颜色是RGB,可能会出现以下困难。 它可以是三种不同的状态,也可以是一个数字(或字符串)。 在这种情况下,您将需要从一种RGB颜色空间转换为另一种XYB空间(HomeKit使用此空间)或XY平面。


为此,您需要创建2个新状态(色相和饱和度),然后将RGB状态的值转换为2个状态,反之亦然。


颜色的结果脚本是
 //       createState("Hue"); createState("Sat"); //      RGB- on({id: "Hue", ack: false, change: 'any'}, function (obj) {  var hue = parseInt(obj.state.val);  var sat = parseInt(getState('Sat').val);  var res = hsvToRgb(hue, sat, 100);  setRGB(parseInt(res[0]), parseInt(res[1]), parseInt(res[2])); }); //    RGB- function setRGB(r, g, b){  var val = ('00'+r.toString(16)).slice(-2)+('00'+g.toString(16)).slice(-2)+('00'+b.toString(16)).slice(-2);  // RGB-   setState('zigbee.0.00124b0014d016ab.color', val, false); } //   HSV   RGB function hsvToRgb(h, s, v) {  var r, g, b;  var i;  var f, p, q, t;   h = Math.max(0, Math.min(360, h));  s = Math.max(0, Math.min(100, s));  v = Math.max(0, Math.min(100, v));  s /= 100;  v /= 100;   if(s == 0) {      r = g = b = v;      return [          Math.round(r * 255),          Math.round(g * 255),          Math.round(b * 255)      ];  }   h /= 60;  i = Math.floor(h);  f = h - i;  p = v * (1 - s);  q = v * (1 - s * f);  t = v * (1 - s * (1 - f));   switch(i) {      case 0:          r = v;          g = t;          b = p;          break;       case 1:          r = q;          g = v;          b = p;          break;       case 2:          r = p;          g = v;          b = t;          break;       case 3:          r = p;          g = q;          b = v;          break;       case 4:          r = t;          g = p;          b = v;          break;       default: // case 5:          r = v;          g = p;          b = q;  }   return [      Math.round(r * 255),      Math.round(g * 255),      Math.round(b * 255)  ]; } 

色温可以更轻松地完成-如果已知灯泡可用值的范围,则可以将其转换为HomeKit可用的间隔(通过scaleInt函数)。


灯泡服务



更深的灯



温控器


温控器-维持设定温度的设备(Thermostat服务)。 因此,恒温器的主要特性是所需温度(目标温度)。 除了设定温度外,还可以指示当前温度(CurrentTemperature),本质上是信息性的(因为设备仅从传感器读取温度)。


从Home应用程序中,可在恒温器中设置目标温度并监视当前温度。 在我的恒温器(Zont)中,只有这两种状态-它们可以通过服务云API获得。


为了在HomeKit中显示设备的美观,我必须添加几个常数:当前加热状态为活动(1),目标加热状态为自动(3)。


恒温器服务



温度选择


盖茨


有了车库门(GarageDoorOpener服务),一切都比使用温控器棘手。


在可用特征中,门具有目标状态(TargetDoorState),这表明我们希望门“打开”或“关闭”。 但是,您还需要正确显示门的当前状态(CurrentDoorState):它们是打开还是关闭,或者它们是打开还是关闭?


在我的情况下,通过ioBroker中的mqtt打开了具有多个信息状态的门:


  • 门打开的迹象(OB)
  • 门移动的迹象(左)

车库门控制状态


由于这些状态,您可以计算门的当前状态:


  • 如果没有OB和DV,则门关闭
  • 如果没有OB,但是有DV,则门会打开
  • 如果有一个OB而没有DV,则门是打开的
  • 如果有一个OB和一个DV,则门关闭

要发送信号以打开和关闭门,我有两个单独的状态(可以用一个状态进行管理,但我有两个状态),它们通过mqtt向门控制控制器发送一条消息:


  • 开启信号
  • 关闭信号

要发送信号,您需要模拟一个“单击”按钮:将值设置为true,不久后将其重置为false。 在这方面,为了与HomeKit集成,必须创建另一个状态-“门的目标状态”,当更改时,将发送相应的信号。


可以通过目标状态(即目标将要达到的目标)来代替打开门的标志:


  • 如果CA是“关闭的”并且没有DV,则门是关闭的
  • 如果CA被“关闭”并且有DV,则门将打开
  • 如果CA是“开放的”并且没有DV,则门是开放的
  • 如果CA“打开”并且有DV,则门关闭

我们还将创建一个单独的状态“当前门状态”,并根据符号的值和目标状态将其填充到脚本中。


车库门的状态更改脚本
 createState("gate_0.current"); //   createState("gate_0.target"); //   //    0,   300 on({id: "mqtt.0.gate.gpio.13", ack: false, val: 1}, function (obj) {  setStateDelayed("mqtt.0.gate.gpio.13", 0,  300); }); on({id: "mqtt.0.gate.gpio.12", ack: false, val: 1}, function (obj) {  setStateDelayed("mqtt.0.gate.gpio.12", 0,  300); }); //     on({id: "mqtt.0.gate.is_open", ack: false, val: 1}, function (obj) {  // ""  setState("javascript.0.gate_0.current", 0, true); }); on({id: "mqtt.0.gate.is_open", ack: false, val: 0}, function (obj) {  // ""  setState("javascript.0.gate_0.current", 1, true); }); //    - ,      on({id: "javascript.0.gate_0.target", ack: false, val: 0}, function (obj) {  setState("mqtt.0.gate.gpio.12", 1); }); //    - ,      on({id: "javascript.0.gate_0.target", ack: false, val: 1}, function (obj) {  setState("mqtt.0.gate.gpio.13", 1); }); on({id: "mqtt.0.gate.in_progress", ack: true, change: 'any'}, function (obj) {  //    " ",      if (obj.state.val === 1) {      //    "",         const target = getState("javascript.0.gate_0.target");      if (target.val === 0) {          // ""          setState("javascript.0.gate_0.current", 2, true);      } else {          // ""          setState("javascript.0.gate_0.current", 3, true);      }  }  //    " ",      if (obj.state.val === 0) {      //    "",         const target = getState("javascript.0.gate_0.target");      if (target.val === 0) {          // ""          setState("javascript.0.gate_0.current", 0, true);      } else {          // ""          setState("javascript.0.gate_0.current", 1, true);      }  } }); 

运行脚本后,您可以配置车库门服务的特征:


GarageDoorOpener服务



摄影机


要将相机添加到HomeKit,将使用“经典”方法。 通过ffmpeg模块从摄像机广播图像是有组织的。 通过它,输入流将被编码,加密并提供给Homekit。


首先,您需要在ioBroker所在的服务器上安装ffmpeg。


对于每个平台,它以不同的方式安装,您可以从源代码进行组装,或者搜索现成的组件,例如,在这里: https://www.johnvansickle.com/ffmpeg/必须具有libx264编码器。 您可以使用以下命令在安装ffmpeg之后检查编码器:


 ffmpeg -codecs | grep 264 

结果应包含以下形式的一行:


 DEV.LS h264                 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (decoders: h264 h264_v4l2m2m h264_vdpau ) (encoders: libx264 libx264rgb h264_v4l2m2m ) 

对于Raspberry Pi 3,您可以使用现成的程序集 ,该程序集的编解码器支持GPU硬件编码(h264_omx,消耗更少的资源)。 这样说:


 wget https://github.com/legotheboss/YouTube-files/raw/master/ffmpeg_3.1.4-1_armhf.deb sudo dpkg -i ffmpeg_3.1.4-1_armhf.deb 

这两个编解码器都存在于此程序集中:libx264和h264_omx


接下来,您需要获取需要广播的摄像机流的地址(此步骤超出了本文的范围)。 例如,您可以使用现成的公共流


现在将摄像机添加到Yahka,指示流的地址,并在必要时更改编解码器,图像大小和帧速率的参数。


重要:参数组合对于在Homekit中正确显示摄像机非常重要,并且取决于摄像机和流。 它还会影响系统性能,因为 ffmpeg的运行过程消耗大量资源。


添加相机


流设置


摄像机是作为单独的设备添加到网关外部的,它们的添加方式必须与网关相同


相机缩略图


从相机播放


红利


作为奖励,我将讨论摄像机广播的特殊用法。


使用相同的ffmpeg,而不是相机,您可以尝试广播图像,任何图片。 这些图片也可以与视频流组合。 您可以在图片上显示文本,图形和其他信息。


文字叠加转换


结果,您可以获得一个有趣的仪表板。 而且,如果您定期更新图片,则会获得动态数据。


例如,我以图片(磁盘上的文件)的形式绘制了一些指标的变化图。 该图每分钟更新一次,并覆盖文件中的图像。


(函数createImage1,createImage2,图形的形成和在图片上的文字拼合超出了本文的范围,但我会给出提示)

我将告诉您如何获取图片形式的图形。


IoBroker具有构建图形的标准方法-Flot驱动程序。 该驱动程序与Web驱动程序配对,并在浏览器中显示结果。 但是,为了在服务器上(在脚本中)将创建的图形作为图像获取,需要额外的PhantomJS驱动程序,该驱动程序需要页面的“屏幕截图”(我们将在该屏幕上绘制Flot图)。


但是,我将讨论在脚本中在服务器上构建图形的另一种方法。


有一个Chart.js库http://www.chartjs.org/ ,可让您在浏览器中绘制漂亮的图形(例如http://www.chartjs.org/samples/latest/ )。


对于绘图,它使用浏览器的“画布”(画布,画布)。 因此,要在服务器上使用此库进行绘制,您需要使用“画布”和DOM对象的“服务器”版本。 这就是chartjs-node软件包的功能( https://github.com/vmpowerio/chartjs-node )。


该程序包的主要依赖项是canvas程序包( https://github.com/Automattic/node-canvas ),该程序包应全局安装(或安装在iobroker文件夹中)。 重要的是为放置https://github.com/Automattic/node-canvas#compiling的平台安装所有依赖项。


之后,您可以在javascript驱动程序设置中添加chart.js,chartjs-node模块。 它们应该正确安装,没有错误。 否则,请处理并解决错误。


然后,您可以编写一个脚本。


下面是一个脚本示例,如下 它包括使用历史记录驱动程序并使用特定的状态名称。


注意! 对于初学者来说,该脚本具有复杂的结构-Promise。 这是一种方便的方法,而不是使用回调编写函数,而是制作步骤链。 因此,例如,这样做很方便,可以从状态历史中获取数据。


 'use strict'; const ChartjsNode = require('chartjs-node'); /** *  sendTo  Promise,      */ function sendToPromise(adapter, cmd, params) { return new Promise((resolve, reject) => { sendTo(adapter, cmd, params, (result) => { resolve(result); }); }); } //    const chartColors = { black: 'rgb(0, 0, 0)', red: 'rgb(255, 99, 132)', orange: 'rgb(255, 159, 64)', yellow: 'rgb(255, 205, 86)', green: 'rgb(75, 192, 192)', blue: 'rgb(54, 162, 235)', purple: 'rgb(153, 102, 255)', grey: 'rgb(201, 203, 207)' }; /** *        * : * @param config -     * @param filename -     * : * @param Promise -    */ function doDraw(config, filename) { //     640x480  var chartNode = new ChartjsNode(640, 480); return chartNode.drawChart(config) .then(() => { //     return chartNode.writeImageToFile('image/png', filename); }); } /** *     ChartJS. * : * @param Promise -    */ function prepareDraw0(){ // ,    var ; //  Promise     return new Promise((resolve, reject)=>{resolve()}) //       ,      .then(()=>{ //  ,   ,      = [ {"val":3,"ack":1,"ts":1539063874301}, {"val":5,"ack":1,"ts":1539063884299}, {"val":5.3,"ack":1,"ts":1539063894299}, {"val":3.39,"ack":1,"ts":1539063904301}, {"val":5.6,"ack":1,"ts":1539063914300}, {"val":-1.3,"ack":1,"ts":1539063924300}, {"val":-6.3,"ack":1,"ts":1539063934302}, {"val":1.23,"ack":1,"ts":1539063944301}, ]; }) //   -    .then(()=>{ const chartJsOptions = { //   -  type: 'line', data: { //    datasets: [ { //   label: '', //  backgroundColor: chartColors.black, borderColor: chartColors.black, //   pointRadius: 3, //    borderWidth: 3, //     ''        data: .map((item) => { return {y: item.val, t: new Date(item.ts)} }), //   -  fill: false, } ] }, options: { //   legend: { labels: { //   fontSize: 20, }, }, //   scales: { //  X xAxes: [{ //  -   type: 'time', display: true, //   scaleLabel: { display: true, labelString: '' }, }], //  Y yAxes: [{ //  -  type: 'linear', display: true, //   scaleLabel: { display: true, labelString: '' }, }] } } }; return chartJsOptions; }); } /** *     ChartJS. *         , *     . * * : * @param hours -  ,     * : * @param Promise -    */ function prepareDraw1(hours){ //   ,      const end = new Date().getTime(), start = end - 3600000*(hours || 1); // 1 =   //  ,       //   var , 2, 1, 2, 2; //  Promise     return new Promise((resolve, reject)=>{resolve()}) //       'mqtt.0.ESP_Easy..Temperature' .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'mqtt.0.ESP_Easy..Temperature', options: { start: start, end: end, aggregate: 'onchange' } } ).then((result) => { //     ''  = result.result; }); }) //       'sonoff.0.chicken2.DS18B20_Temperature' .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.chicken2.DS18B20_Temperature', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ //     '2' 2 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.sonoff_chicken_vent.DS18B20_Temperature', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ 1 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.chicken2.POWER1', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ 2 = result.result; }); }) .then(() => { return sendToPromise('history.0', 'getHistory', { id: 'sonoff.0.chicken2.POWER2', options: { start: start, end: end, aggregate: 'onchange' } }).then((result)=>{ 2 = result.result; }); }) //   -    .then(()=>{ const chartJsOptions = { //   -  type: 'line', data: { //    datasets: [ { //           label: ' ('+[.length - 1].val+')', //  backgroundColor: chartColors.blue, borderColor: chartColors.blue, //  . 0 -   pointRadius: 0, //    borderWidth: 3, //     ''        data: .map((item) => { return {y: item.val, t: new Date(item.ts)} }), //   -  fill: false, //   Y yAxisID: 'y-axis-1', },{ label: ' 1 ('+1[1.length - 1].val+')', backgroundColor: chartColors.green, borderColor: chartColors.green, pointRadius: 0, borderWidth: 3, data: 1.map((item) => { return {y: item.val, t: new Date(item.ts)} }), fill: false, yAxisID: 'y-axis-1', },{ label: ' 2 ('+2[2.length - 1].val+')', backgroundColor: chartColors.red, borderColor: chartColors.red, pointRadius: 0, borderWidth: 3, data: 2.map((item) => { return {y: item.val, t: new Date(item.ts)} }), fill: false, yAxisID: 'y-axis-1', },{ label: ' 2  ('+2[2.length - 1].val+')', backgroundColor: chartColors.yellow, borderColor: chartColors.yellow, pointRadius: 0, borderWidth: 1, data: 2.map((item) => { return {y: (item.val) ? 1 : 0, t: new Date(item.ts)} }), fill: true, lineTension: 0, steppedLine: true, yAxisID: 'y-axis-2', },{ label: ' 2  ('+2[2.length - 1].val+')', backgroundColor: chartColors.grey, borderColor: chartColors.grey, pointRadius: 0, borderWidth: 1, data: 2.map((item) => { return {y: (item.val) ? -1 : 0, t: new Date(item.ts)} }), fill: true, lineTension: 0, steppedLine: true, yAxisID: 'y-axis-2', } ] }, options: { //   legend: { labels: { //   fontSize: 20, }, }, //   scales: { //  X xAxes: [{ //  -   type: 'time', display: true, //   scaleLabel: { display: true, labelString: '' }, //    () time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, }], //  Y yAxes: [{ //  -  type: 'linear', display: true, //   scaleLabel: { display: true, labelString: '' }, //   -  position: 'left', //   id: 'y-axis-1', },{ type: 'linear', display: true, scaleLabel: { display: true, labelString: '  ' }, ticks: { min: -4, max: 2 }, //   -  position: 'right', id: 'y-axis-2', }] } } }; return chartJsOptions; }); } function createImage(filename, callback){ // filename -  ,       //    prepareDraw1(2) //     .then((result) => { //        return doDraw(result, filename); }) .then(()=>{ if (callback) callback(); }) .catch((err)=>{ console.error(err); }); } 

广播图像而不是流


缩略图大约每分钟更新一次,因此我们将图像设置为每10秒更新一次:


 var fs = require('fs'); //  10    schedule("*/10 * * * * *", () => {  createImage1('/tmp/1_new.jpg', ()=> {      fs.renameSync('/tmp/1_new.jpg', '/tmp/1.jpg');  });  createImage2('/tmp/2_new.jpg', ()=> {      fs.renameSync('/tmp/2_new.jpg', '/tmp/2.jpg');  }); }); 

特殊之处在于,在广播图像的过程中,必须足够快地替换图片,以免ffmpeg崩溃:)因此,图片首先形成为一个文件,然后将文件重命名为用于翻译的文件。


现在,在摄像机设置中,指定生成文件的名称而不是流地址,并添加“图片已更新”的设置(参数“ -loop 1”)。 这是在摄像机的高级属性中配置的。 这些属性仅是运行ffmpeg的命令行选项。 因此,应在ffmpeg文档和示例中找到参数的组合。


这些属性分为两种:用于接收“预览”(摄像机的小图像)和用于广播。 因此,您可以指定不同的图像源文件,例如具有不同的详细信息。


ffmpeg启动选项


直播影像


结论


ioBroker . , . , , .


, Yahka , Material. , HomeKit.


Yahka, HomeKit — Ham, HomeBridge . .

Source: https://habr.com/ru/post/zh-CN433798/


All Articles