homebridge-rtl
Version:
RTL_433 based sensor for Homebridge
440 lines (390 loc) • 17.7 kB
JavaScript
'use strict';
var debug = require('debug')('homebridge-rtl_433');
var Logger = require("mcuiot-logger").logger;
const moment = require('moment');
var os = require("os");
var _ = require('lodash');
var hostname = os.hostname();
var homebridgeLib = require('homebridge-lib');
let Service, Characteristic;
var CustomCharacteristics;
var FakeGatoHistoryService;
var myAccessories = [];
module.exports = (homebridge) => {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
CustomCharacteristics = new homebridgeLib.EveHomeKitTypes(homebridge).Characteristics;
FakeGatoHistoryService = require('fakegato-history')(homebridge);
homebridge.registerPlatform('homebridge-rtl', 'rtl_433', rtl433Plugin);
};
function rtl433Plugin(log, config, api) {
this.log = log;
this.refresh = config['refresh'] || 60; // Update every minute
this.options = config.options || {};
this.storage = config['storage'] || "fs";
//8/28/2022 JDR added this for Windows.
this.rtl433Path = config['rtl433Path'] || "/usr/local/bin/";
this.rtl433Bin = config['rtl433Bin'] || "rtl_433";
this.killCommand = config['killCommand'] || ("pkill " + this.rtl433Bin);// for linux/mac windows: taskkill /im "rtl_433_64bit_static.exe"
this.spreadsheetId = config['spreadsheetId'];
this.devices = config['devices'];
if (this.spreadsheetId) {
this.log_event_counter = 59;
this.logger = new Logger(this.spreadsheetId);
}
}
rtl433Plugin.prototype = {
accessories: function (callback) {
for (var i in this.devices) {
this.log("Adding device", i, this.devices[i].name);
myAccessories.push(new Rtl433Accessory(this.devices[i], this.log, i));
}
callback(myAccessories);
// console.log("Pre-This", this);
rtl433Server.call(this);
}
};
function rtl433Server() {
// console.log("This", this);
var childProcess = require('child_process');
var readline = require('readline');
var previousMessage;
this.log("Spawning rtl_433 " + this.killCommand + '; ' + this.rtl433Path + this.rtl433Bin);
// if you start rtl_433 outside homebride to get log: rtl_433 -v -F json -C si -M protocol > /tmp/rtl433.json
//var proc = childProcess.spawn('/usr/bin/truncate -s 0 /tmp/rtl433.json;/usr/bin/tail', ['-F','/tmp/rtl433.json'], {
////8/28/2022 JDR added this for Windows. fixed command line arguments. They weren't working for Windows, but should still work for others.
var proc = childProcess.spawn(this.killCommand + '; ' + this.rtl433Path + this.rtl433Bin, ['-d 0', '-F json', '-C si'], {
shell: true
});
readline.createInterface({
input: proc.stdout,
terminal: false
}).on('line', function (message) {
this.log("Message", message.toString());
debug("Message", message.toString());
if (message.toString().startsWith('{')) {
try {
var data = JSON.parse(message.toString());
var devices = [];
this.message = message;
if (data.id) {
devices = getDevices.call(this, data.id);
} else if (data.channel) {
devices = getDevices.call(this, data.channel);
} else {
this.log.error("FYI: RTL Message missing device or channel identifier.");
this.log("Message", this.message.toString());
}
if (!duplicateMessage(previousMessage, data)) {
if (devices.length > 0) {
previousMessage = Object.assign({}, data);
for (var i in devices) {
var device = devices[i]
device.updateStatus(data);
}
}
}
// {"time" : "2020-03-14 11:34:22", "model" : "Philips outdoor temperature sensor", "channel" : 1, "temperature_C" : 1.500, "battery" : "LOW"}
// {"time" : "2018-06-02 08:27:20", "model" : "Acurite 986 Sensor", "id" : 3929, "channel" : "2F", "temperature_F" : -11, "temperature_C" : -23.889, "battery" : "OK", "status" : 0}
} catch (err) {
this.log.error("JSON Parse Error", message.toString(), err);
}
}
}.bind(this));
proc.on('close', function (code) {
this.log.error('child close code (spawn)', code);
setTimeout(rtl433Server.bind(this), 10 * 1000);
}.bind(this));
proc.on('disconnect', function (code) {
this.log.error('child disconnect code (spawn)', code);
}.bind(this));
proc.on('error', function (code) {
this.log.error('child error code (spawn)', code);
}.bind(this));
proc.on('exit', function (code) {
this.log.error('child exit code (spawn)', code);
}.bind(this));
}
function Rtl433Accessory(device, log, unit) {
this.id = device.id;
this.type = device.type;
this.log = log;
this.name = device.name;
this.alarm = device['alarm']
this.deviceTimeout = device['timeout'] || 120; // Mark as unavailable after 2 hours
this.humidity = device['humidity'] || false; // Add humidity data to temerature sensor
}
Rtl433Accessory.prototype = {
updateStatus: function (data) {
try {
this.log("Updating", this.name);
this.lastUpdated = Date.now();
clearTimeout(this.timeout);
this.timeout = setTimeout(deviceTimeout.bind(this), this.deviceTimeout * 60 * 1000);
var entry, batteryOk;
switch (this.type) {
case "temperature":
var humidity;
entry = {
time: moment().unix(),
temp: roundInt(data.temperature_C)
}
if (this.humidity && data.humidity) {
humidity = roundInt(data.humidity)
entry.humidity = humidity
}
this.loggingService.addEntry(entry);
if (this.spreadsheetId) {
this.log_event_counter = this.log_event_counter + 1;
if (this.log_event_counter > 59) {
this.logger.storeBME(this.name, 0, roundInt(data.temperature_C), humidity);
this.log_event_counter = 0;
}
}
this.sensorService
.setCharacteristic(Characteristic.CurrentTemperature, roundInt(data.temperature_C));
if (humidity !== undefined) {
this.sensorService
.setCharacteristic(Characteristic.CurrentRelativeHumidity, humidity)
}
if (data.battery !== undefined || data.battery_ok != undefined) {
batteryOk = data.battery === "OK" || data.battery_ok === 1
this.sensorService
.setCharacteristic(Characteristic.StatusLowBattery, batteryOk ? Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL : Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW);
}
if (this.alarm !== undefined) {
if (roundInt(data.temperature_C) > this.alarm) {
this.alarmService
.setCharacteristic(Characteristic.ContactSensorState, Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);
debug(this.name + " Temperature Alarm" + roundInt(data.temperature_C) + " > " + this.alarm);
} else {
this.alarmService
.setCharacteristic(Characteristic.ContactSensorState, Characteristic.ContactSensorState.CONTACT_DETECTED);
}
}
break;
case "humidity":
entry = {
time: moment().unix(),
humidity: roundInt(data.humidity)
}
this.loggingService.addEntry(entry);
if (this.spreadsheetId) {
this.log_event_counter = this.log_event_counter + 1;
if (this.log_event_counter > 59) {
this.logger.storeBME(this.name, 0, (undefined), roundInt(data.humidity));
this.log_event_counter = 0;
}
}
this.sensorService
.setCharacteristic(Characteristic.CurrentRelativeHumidity, roundInt(data.humidity))
if (data.battery !== undefined || data.battery_ok != undefined) {
batteryOk = data.battery === "OK" || data.battery_ok === 1
this.sensorService
.setCharacteristic(Characteristic.StatusLowBattery, batteryOk ? Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL : Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW);
}
if (this.alarm !== undefined) {
if (roundInt(data.humidity) > this.alarm) {
this.alarmService
.setCharacteristic(Characteristic.ContactSensorState, Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);
debug(this.name + " Humidity Alarm" + roundInt(data.humidity) + " > " + this.alarm);
} else {
this.alarmService
.setCharacteristic(Characteristic.ContactSensorState, Characteristic.ContactSensorState.CONTACT_DETECTED);
}
}
break;
case "motion":
// {"time" : "2018-09-30 19:20:26", "model" : "Skylink HA-434TL motion sensor", "motion" : "true", "id" : "1e3e8", "raw" : "be3e8"}
// {"time" : "2022-03-17 21:58:46.319049", "protocol" : 68, "model" : "Kerui-Security", "id" : 840811, "cmd" : 10, "motion" : 1, "state" : "motion" }
// debug("update motion this--->",this);
var value = (data.motion === "true" || data.motion === 1 ? true : false);
if (this.sensorService.getCharacteristic(Characteristic.MotionDetected).value !== value) {
this.sensorService.getCharacteristic(CustomCharacteristics.LastActivation)
.updateValue(moment().unix() - this.loggingService.getInitialTime());
}
this.sensorService.getCharacteristic(Characteristic.MotionDetected)
.updateValue(value);
this.loggingService.addEntry({
time: moment().unix(),
status: value
});
if (value) {
clearTimeout(this.motionTimeout);
this.motionTimeout = setTimeout(function () {
var value = false;
if (this.sensorService.getCharacteristic(Characteristic.MotionDetected).value !== value) {
this.sensorService.getCharacteristic(CustomCharacteristics.LastActivation)
.updateValue(moment().unix() - this.loggingService.getInitialTime());
}
this.sensorService.getCharacteristic(Characteristic.MotionDetected)
.updateValue(value);
this.loggingService.addEntry({
time: moment().unix(),
status: value
});
}.bind(this), 2 * 60 * 1000);
}
break;
case "contact":
// {"time" : "2022-03-17 23:04:08.430623", "protocol" : 68, "model" : "Kerui-Security", "id" : 297536, "cmd" : 14, "opened" : 1, "state" : "open" }
// {"time" : "2022-03-17 23:04:19.447761", "protocol" : 68, "model" : "Kerui-Security", "id" : 297536, "cmd" : 7, "opened" : 0, "state" : "close" }
// debug("update door this--->",this);
// console.log("update door this--->",this);
this.sensorService
.setCharacteristic(Characteristic.ContactSensorState, (data.opened === 1 ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED));
//this.timeout = setTimeout(deviceTimeout.bind(this), this.deviceTimeout * 60 * 1000);
clearTimeout(this.timeout);
if (data.battery !== undefined || data.battery_ok != undefined) {
this.sensorService
.setCharacteristic(Characteristic.StatusLowBattery, (data.battery === "OK" || data.battery_ok === 1 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL : Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW));
}
break;
default:
this.log.error("No events defined for sensor type %s", this.type);
}
} catch (err) {
this.log.error("Error", err);
}
},
getServices: function () {
this.log("getServices", this.name);
// Information Service
var informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Manufacturer, "homebridge-rtl_433")
.setCharacteristic(Characteristic.SerialNumber, hostname + "-" + this.name)
.setCharacteristic(Characteristic.FirmwareRevision, require('../package.json').version);
// Thermostat Service
switch (this.type) {
case "temperature":
this.sensorService = new Service.TemperatureSensor(this.name);
this.sensorService
.getCharacteristic(Characteristic.CurrentTemperature)
.setProps({
minValue: -100,
maxValue: 100
});
this.timeoutCharacteristic = Characteristic.CurrentTemperature;
this.timeout = setTimeout(deviceTimeout.bind(this), this.deviceTimeout * 60 * 1000); // 5 minutes
this.sensorService.log = this.log;
this.loggingService = new FakeGatoHistoryService("weather", this.sensorService, {
storage: this.storage,
minutes: this.refresh * 10 / 60,
disableRepeatLastData: true
});
if (this.alarm !== undefined) {
this.alarmService = new Service.ContactSensor(this.name + " Alarm");
informationService
.setCharacteristic(Characteristic.Model, "Temperature Sensor with Alarm @ " + this.alarm);
} else {
informationService
.setCharacteristic(Characteristic.Model, "Temperature Sensor");
}
break;
case "humidity":
this.sensorService = new Service.HumiditySensor(this.name);
this.sensorService
.getCharacteristic(Characteristic.CurrentRelativeHumidity)
.setProps({
minValue: 0,
maxValue: 100
});
this.timeoutCharacteristic = Characteristic.CurrentRelativeHumidity;
this.timeout = setTimeout(deviceTimeout.bind(this), this.deviceTimeout * 60 * 1000); // 5 minutes
this.sensorService.log = this.log;
this.loggingService = new FakeGatoHistoryService("weather", this.sensorService, {
storage: this.storage,
minutes: this.refresh * 10 / 60,
disableRepeatLastData: true
});
if (this.alarm !== undefined) {
this.alarmService = new Service.ContactSensor(this.name + " Alarm");
informationService
.setCharacteristic(Characteristic.Model, "Humidity Sensor with Alarm @ " + this.alarm);
} else {
informationService
.setCharacteristic(Characteristic.Model, "Humidity Sensor");
}
break;
case "motion":
this.sensorService = new Service.MotionSensor(this.name);
this.sensorService.addCharacteristic(CustomCharacteristics.LastActivation);
this.timeoutCharacteristic = Characteristic.MotionDetected;
this.timeout = setTimeout(deviceTimeout.bind(this), this.deviceTimeout * 60 * 1000); // 5 minutes
this.sensorService.log = this.log;
this.loggingService = new FakeGatoHistoryService("motion", this.sensorService, {
storage: this.storage,
minutes: this.refresh * 10 / 60
});
informationService
.setCharacteristic(Characteristic.Model, "Motion Sensor");
break;
case "contact":
// debug("get door this--->",this);
// console.log("get door this--->",this);
this.sensorService = new Service.ContactSensor(this.name);
this.sensorService.addCharacteristic(CustomCharacteristics.LastActivation);
//this.timeoutCharacteristic = Characteristic.ContactSensorState;
//this.timeout = setTimeout(deviceTimeout.bind(this), this.deviceTimeout * 60 * 1000); // 5 minutes
this.sensorService.log = this.log;
informationService
.setCharacteristic(Characteristic.Model, "Contact Sensor");
break;
default:
this.log.error("No events defined for sensor type %s", this.type);
}
if (this.alarm !== undefined) {
return [informationService, this.sensorService, this.alarmService, this.loggingService];
} else {
return [informationService, this.sensorService, this.loggingService];
}
}
};
function deviceTimeout() {
this.log("Timeout", this.name);
if (this.sensorService !== undefined && this.timeoutCharacteristic !== undefined)
this.sensorService
.getCharacteristic(this.timeoutCharacteristic).updateValue(new Error("No response"));
}
function roundInt(string) {
return Math.round(parseFloat(string) * 10) / 10;
}
function getDevices(unit) {
var devices = [];
for (var i in myAccessories) {
// == is correct test, Acurite uses a numeric value
if (myAccessories[i].id == unit) {
devices.push(myAccessories[i]);
}
}
if (devices.length === 0) {
this.log.error("FYI: Message from unknown device ID", unit);
this.log("Message", this.message.toString());
}
return devices;
}
function seconds(dateTime) {
// "2018-10-01 20:52:33"
var hms = dateTime.split(" ");
// debug("TIME", hms[1]);
var tt = hms[1].split(":");
var sec = tt[0] * 3600 + tt[1] * 60 + tt[2] * 1;
return (sec);
}
function duplicateMessage(last, current) {
if (last) {
// debug("Last %s, Current %s", JSON.stringify(last), JSON.stringify(current));
if ((seconds(current.time) - seconds(last.time)) < 2) {
var tCurrent = Object.assign({}, current);
var tLast = Object.assign({}, last);
delete tCurrent.time;
delete tLast.time;
// debug("LAST %s, CURRENT %s, isEqual %s", JSON.stringify(tLast), JSON.stringify(tCurrent),_.isEqual(tLast, tCurrent));
if (_.isEqual(tLast, tCurrent)) {
return true;
}
}
}
return false;
}