mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 21:02:38 +00:00
658 lines
19 KiB
JavaScript
658 lines
19 KiB
JavaScript
// FHEM Platform Shim for HomeBridge
|
||
//
|
||
// Remember to add platform to config.json. Example:
|
||
// "platforms": [
|
||
// {
|
||
// "platform": "FHEM",
|
||
// "name": "FHEM",
|
||
// "server": "127.0.0.1",
|
||
// "port": "8083",
|
||
// "filter": "room=xyz"
|
||
// }
|
||
// ],
|
||
//
|
||
// When you attempt to add a device, it will ask for a "PIN code".
|
||
// The default code for all HomeBridge accessories is 031-45-154.
|
||
//
|
||
var types = require("HAP-NodeJS/accessories/types.js");
|
||
var request = require("request");
|
||
var util = require('util');
|
||
|
||
function FHEMPlatform(log, config) {
|
||
this.log = log;
|
||
this.server = config["server"];
|
||
this.port = config["port"];
|
||
this.filter = config["filter"];
|
||
}
|
||
|
||
function
|
||
FHEM_sortByKey(array, key) {
|
||
return array.sort( function(a, b) {
|
||
var x = a[key]; var y = b[key];
|
||
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||
});
|
||
}
|
||
|
||
function
|
||
FHEM_rgb2hex(r,g,b) {
|
||
if( g == undefined )
|
||
return Number(0x1000000 + r[0]*0x10000 + r[1]*0x100 + r[2]).toString(16).substring(1);
|
||
|
||
return Number(0x1000000 + r*0x10000 + g*0x100 + b).toString(16).substring(1);
|
||
}
|
||
|
||
function
|
||
FHEM_hsv2rgb(h,s,v) {
|
||
var r = 0.0;
|
||
var g = 0.0;
|
||
var b = 0.0;
|
||
|
||
if( s == 0 ) {
|
||
r = v;
|
||
g = v;
|
||
b = v;
|
||
|
||
} else {
|
||
var i = Math.floor( h * 6.0 );
|
||
var f = ( h * 6.0 ) - i;
|
||
var p = v * ( 1.0 - s );
|
||
var q = v * ( 1.0 - s * f );
|
||
var t = v * ( 1.0 - s * ( 1.0 - f ) );
|
||
i = i % 6;
|
||
|
||
if( i == 0 ) {
|
||
r = v;
|
||
g = t;
|
||
b = p;
|
||
} else if( i == 1 ) {
|
||
r = q;
|
||
g = v;
|
||
b = p;
|
||
} else if( i == 2 ) {
|
||
r = p;
|
||
g = v;
|
||
b = t;
|
||
} else if( i == 3 ) {
|
||
r = p;
|
||
g = q;
|
||
b = v;
|
||
} else if( i == 4 ) {
|
||
r = t;
|
||
g = p;
|
||
b = v;
|
||
} else if( i == 5 ) {
|
||
r = v;
|
||
g = p;
|
||
b = q;
|
||
}
|
||
}
|
||
|
||
return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) );
|
||
}
|
||
|
||
function
|
||
FHEM_rgb2h(r,g,b){
|
||
if( r == undefined )
|
||
return;
|
||
|
||
if( g == undefined ) {
|
||
var str = r;
|
||
r = parseInt( str.substr(0,2), 16 );
|
||
g = parseInt( str.substr(2,2), 16 );
|
||
b = parseInt( str.substr(4,2), 16 );
|
||
}
|
||
|
||
var M = Math.max( r, g, b );
|
||
var m = Math.min( r, g, b );
|
||
var c = M - m;
|
||
|
||
var h, s, v;
|
||
if( c == 0 ) {
|
||
h = 0;
|
||
} else if( M == r ) {
|
||
h = ( 60 * ( ( g - b ) / c ) % 360 ) / 360;
|
||
} else if( M == g ) {
|
||
h = ( 60 * ( ( b - r ) / c ) + 120 ) / 360;
|
||
} else if( M == b ) {
|
||
h = ( 60 * ( ( r - g ) / c ) + 240 ) / 360;
|
||
}
|
||
|
||
return h;
|
||
|
||
if( M == 0 ) {
|
||
s = 0;
|
||
} else {
|
||
s = c / M;
|
||
}
|
||
|
||
v = M;
|
||
|
||
return h;
|
||
}
|
||
|
||
|
||
FHEMPlatform.prototype = {
|
||
accessories: function(callback) {
|
||
this.log("Fetching FHEM switchable devices...");
|
||
|
||
var that = this;
|
||
var foundAccessories = [];
|
||
|
||
var cmd = 'jsonlist';
|
||
if( this.filter )
|
||
cmd += " " + this.filter;
|
||
var url = encodeURI("http://" + this.server + ":" + this.port + "/fhem?cmd=" + cmd + "&XHR=1");
|
||
this.log( 'fetching: ' + url );
|
||
|
||
request.get( { url: url, json: true },
|
||
function(err, response, json) {
|
||
if( !err && response.statusCode == 200 ) {
|
||
that.log( 'got: ' + json['totalResultsReturned'] + ' results' );
|
||
//that.log("got json: " + util.inspect(json) );
|
||
if( json['totalResultsReturned'] ) {
|
||
var sArray=FHEM_sortByKey(json['Results'],"Name");
|
||
sArray.map(function(s) {
|
||
if( s.Attributes.disable == 1 ) {
|
||
that.log( s.Internals.NAME + ' is disabled');
|
||
|
||
} else if( s.PossibleSets.match(/\bon\b/)
|
||
&& s.PossibleSets.match(/\boff\b/) ) {
|
||
accessory = new FHEMAccessory(that.log, that.server, that.port, s);
|
||
foundAccessories.push(accessory);
|
||
|
||
} else if( s.PossibleSets.match(/\bvolume\b/) ) {
|
||
that.log( s.Internals.NAME + ' has volume');
|
||
accessory = new FHEMAccessory(that.log, that.server, that.port, s);
|
||
foundAccessories.push(accessory);
|
||
|
||
} else if( s.Readings.temperature ) {
|
||
accessory = new FHEMAccessory(that.log, that.server, that.port, s);
|
||
foundAccessories.push(accessory);
|
||
|
||
} else if( s.Readings.humidity ) {
|
||
accessory = new FHEMAccessory(that.log, that.server, that.port, s);
|
||
foundAccessories.push(accessory);
|
||
|
||
} else {
|
||
that.log( s.Internals.NAME + ' is not switchable');
|
||
|
||
}
|
||
});
|
||
}
|
||
callback(foundAccessories);
|
||
|
||
} else {
|
||
that.log("There was a problem connecting to FHEM.");
|
||
|
||
}
|
||
|
||
});
|
||
}
|
||
}
|
||
|
||
function
|
||
FHEMAccessory(log, server, port, s) {
|
||
//log( 'sets: ' + s.PossibleSets );
|
||
//log("got json: " + util.inspect(s) );
|
||
//log("got json: " + util.inspect(s.Internals) );
|
||
|
||
var match;
|
||
if( match = s.PossibleSets.match(/\bpct\b/) ) {
|
||
s.hasPct = true;
|
||
s.pctMax = 100;
|
||
}
|
||
if( match = s.PossibleSets.match(/\bhue[^\b\s]*(,(\d*)?)+\b/) ) {
|
||
s.hasHue = true;
|
||
s.hueMax = 360;
|
||
if( match[2] != undefined )
|
||
s.hueMax = match[2];
|
||
}
|
||
if( match = s.PossibleSets.match(/\bsat[^\b\s]*(,(\d*)?)+\b/) ) {
|
||
s.hasSat = true;
|
||
s.satMax = 100;
|
||
if( match[2] != undefined )
|
||
s.satMax = match[2];
|
||
}
|
||
if( s.PossibleSets.match(/\brgb\b/) ) {
|
||
s.hasRGB = true;
|
||
}
|
||
|
||
if( s.Readings.temperature )
|
||
s.hasTemperature = true;
|
||
if( s.Readings.humidity )
|
||
s.hasHumidity = true;
|
||
|
||
if( s.hasHue )
|
||
log( s.Internals.NAME + ' has hue [0-' + s.hueMax +']');
|
||
else if( s.hasRGB )
|
||
log( s.Internals.NAME + ' has RGB');
|
||
else if( s.hasPct )
|
||
log( s.Internals.NAME + ' is dimable [' + s.pctMax +']');
|
||
else if( s.hasTemperature )
|
||
log( s.Internals.NAME + ' has temperature' );
|
||
else
|
||
log( s.Internals.NAME + ' is switchable');
|
||
|
||
if( s.hasHumidity )
|
||
log( s.Internals.NAME + ' has humidity' );
|
||
|
||
// device info
|
||
this.name = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME;
|
||
this.name = s.Internals.NAME;
|
||
this.device = s.Internals.NAME;
|
||
this.type = s.Internals.TYPE;
|
||
this.model = s.Attributes.model ? s.Attributes.model : s.Internals.model;
|
||
this.PossibleSets = s.PossibleSets;
|
||
|
||
if( this.type == 'CUL_HM' )
|
||
this.serial = s.Internals.DEF;
|
||
else if( this.type == 'HUEDevice' )
|
||
this.serial = s.Internals.uniqueid;
|
||
else if( this.type == 'SONOSPLAYER' )
|
||
this.serial = s.Internals.UDN;
|
||
|
||
this.hasPct = s.hasPct;
|
||
this.pctMax = s.pctMax;
|
||
this.hasHue = s.hasHue;
|
||
this.hueMax = s.hueMax;
|
||
this.hasSat = s.hasSat;
|
||
this.satMax = s.satMax;
|
||
this.hasRGB = s.hasRGB;
|
||
|
||
this.hasTemperature = s.hasTemperature;
|
||
this.hasHumidity = s.hasHumidity;
|
||
|
||
log( util.inspect(s.Readings) );
|
||
|
||
this.log = log;
|
||
this.server = server;
|
||
this.port = port;
|
||
}
|
||
|
||
FHEMAccessory.prototype = {
|
||
command: function(c,value) {
|
||
this.log(this.name + " sending command " + c + " with value " + value);
|
||
if( c == 'on' ) {
|
||
if( this.PossibleSets.match(/\bplay\b/i) )
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " play&XHR=1";
|
||
else if( this.PossibleSets.match(/\bon\b/) )
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " on&XHR=1";
|
||
else
|
||
this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value);
|
||
|
||
} else if( c == 'off' ) {
|
||
if( this.PossibleSets.match(/\bpause\b/i) )
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " pause&XHR=1";
|
||
else if( this.PossibleSets.match(/\boff\b/) )
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " off&XHR=1";
|
||
else
|
||
this.log(this.device + " Unhandled command! cmd=" + c + ", value=" + value);
|
||
|
||
} else if( c == 'pct' ) {
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " pct " + value + "&XHR=1";
|
||
|
||
} else if( c == 'hue' ) {
|
||
if( !this.hasHue ) {
|
||
value = FHEM_hsv2rgb( value/360.0, this.sat?this.sat/100.0:1.0, this.pct?this.pct/100.0:1.0 );
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " rgb " + value + "&XHR=1";
|
||
|
||
} else {
|
||
value = Math.round(value * this.hueMax / 360);
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " hue " + value + "&XHR=1";
|
||
}
|
||
|
||
} else if( c == 'sat' ) {
|
||
value = value / 100 * this.satMax;
|
||
url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " sat " + value + "&XHR=1";
|
||
|
||
} else if( value != undefined ) {
|
||
this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value);
|
||
|
||
}
|
||
|
||
var that = this;
|
||
request.put( { url: encodeURI(url) },
|
||
function(err, response) {
|
||
if( err ) {
|
||
that.log("There was a problem sending command " + c + " to" + that.name);
|
||
that.log(url);
|
||
} else {
|
||
that.log(that.name + " sent command " + c);
|
||
that.log(url);
|
||
}
|
||
} );
|
||
},
|
||
|
||
query: function(reading, callback) {
|
||
this.log("query: " + reading);
|
||
|
||
var rgb_to_hue = false;
|
||
if( reading == 'hue' && !this.hasHue && this.hasRGB ) {
|
||
reading = 'rgb';
|
||
rgb_to_hue = true;
|
||
|
||
} else if( reading == 'state'
|
||
&& this.type == 'SONOSPLAYER' ) {
|
||
reading = 'transportState';
|
||
|
||
}
|
||
|
||
if( reading == 'rgb'
|
||
&& this.type == 'SWAP_0000002200000003' ) {
|
||
reading = '0B-RGBlevel';
|
||
|
||
}
|
||
|
||
var cmd = '{ReadingsVal("'+this.device+'","'+reading+'","")}';
|
||
var url = encodeURI("http://" + this.server + ":" + this.port + "/fhem?cmd=" + cmd + "&XHR=1");
|
||
this.log( ' querying: ' + url );
|
||
|
||
var that = this;
|
||
request.get( { url: url },
|
||
function(err, response, result) {
|
||
if( !err && response.statusCode == 200 ) {
|
||
result = result.replace(/[\r\n]/g, "");
|
||
that.log(" result: " + result);
|
||
|
||
if( rgb_to_hue ) {
|
||
result = FHEM_rgb2h(result) * 360;
|
||
that.hue = result;
|
||
} else if( reading == 'hue' ) {
|
||
result = Math.round(result * 360 / that.hueMax);
|
||
that.hue = result;
|
||
} else if( reading == 'sat' ) {
|
||
result = Math.round(result * 100 / that.satMax);
|
||
that.sat = result;
|
||
} else if( reading == 'pct' ) {
|
||
that.pct = result;
|
||
} else if( reading == 'transportState' ) {
|
||
if( result == 'PLAYING' )
|
||
result = 1;
|
||
else
|
||
result = 0;
|
||
that.state = result;
|
||
} else if( reading == 'state' ) {
|
||
if( result == 'off' )
|
||
result = 0;
|
||
else if( result == 'on' )
|
||
result = 1;
|
||
else if( result == '000000' )
|
||
result = 0;
|
||
else
|
||
result = 1;
|
||
|
||
that.state = result;
|
||
}
|
||
that.log(" mapped: " + result);
|
||
|
||
callback(result);
|
||
|
||
} else {
|
||
that.log("There was a problem connecting to FHEM.");
|
||
|
||
}
|
||
} );
|
||
},
|
||
|
||
informationCharacteristics: function() {
|
||
return [
|
||
{
|
||
cType: types.NAME_CTYPE,
|
||
onUpdate: null,
|
||
perms: ["pr"],
|
||
format: "string",
|
||
initialValue: this.name,
|
||
supportEvents: false,
|
||
supportBonjour: false,
|
||
manfDescription: "Name of the accessory",
|
||
designedMaxLength: 255
|
||
},{
|
||
cType: types.MANUFACTURER_CTYPE,
|
||
onUpdate: null,
|
||
perms: ["pr"],
|
||
format: "string",
|
||
initialValue: "FHEM:"+this.type,
|
||
supportEvents: false,
|
||
supportBonjour: false,
|
||
manfDescription: "Manufacturer",
|
||
designedMaxLength: 255
|
||
},{
|
||
cType: types.MODEL_CTYPE,
|
||
onUpdate: null,
|
||
perms: ["pr"],
|
||
format: "string",
|
||
initialValue: this.model ? this.model : '<unknown>',
|
||
supportEvents: false,
|
||
supportBonjour: false,
|
||
manfDescription: "Model",
|
||
designedMaxLength: 255
|
||
},{
|
||
cType: types.SERIAL_NUMBER_CTYPE,
|
||
onUpdate: null,
|
||
perms: ["pr"],
|
||
format: "string",
|
||
initialValue: this.serial ? this.serial : "A1S2NASF88EW",
|
||
supportEvents: false,
|
||
supportBonjour: false,
|
||
manfDescription: "SN",
|
||
designedMaxLength: 255
|
||
},{
|
||
cType: types.IDENTIFY_CTYPE,
|
||
onUpdate: null,
|
||
perms: ["pw"],
|
||
format: "bool",
|
||
initialValue: false,
|
||
supportEvents: false,
|
||
supportBonjour: false,
|
||
manfDescription: "Identify Accessory",
|
||
designedMaxLength: 1
|
||
}
|
||
]
|
||
},
|
||
|
||
controlCharacteristics: function(that) {
|
||
cTypes = [{
|
||
cType: types.NAME_CTYPE,
|
||
onUpdate: null,
|
||
perms: ["pr"],
|
||
format: "string",
|
||
initialValue: this.name,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Name of service",
|
||
designedMaxLength: 255
|
||
}]
|
||
|
||
if( this.name != undefined
|
||
&& !this.hasTemperature ) {
|
||
cTypes.push({
|
||
cType: types.POWER_STATE_CTYPE,
|
||
onRegister: function(assignedCharacteristic) {
|
||
//that.log("onRegister: " + util.inspect(assignedCharacteristic) );
|
||
},
|
||
onUpdate: function(value) {
|
||
if( value == 0 ) {
|
||
that.command("off")
|
||
} else {
|
||
that.command("on")
|
||
}
|
||
},
|
||
onRead: function(callback) {
|
||
that.query('state', function(powerState){
|
||
callback(powerState);
|
||
});
|
||
},
|
||
perms: ["pw","pr","ev"],
|
||
format: "bool",
|
||
initialValue: 0,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Change the power state",
|
||
designedMaxLength: 1
|
||
})
|
||
}
|
||
|
||
if( this.hasPct == true ) {
|
||
cTypes.push({
|
||
cType: types.BRIGHTNESS_CTYPE,
|
||
onUpdate: function(value) { that.command('pct', value); },
|
||
onRead: function(callback) {
|
||
that.query('pct', function(pct){
|
||
callback(pct);
|
||
});
|
||
},
|
||
perms: ["pw","pr","ev"],
|
||
format: "int",
|
||
initialValue: 0,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Adjust Brightness of the Light",
|
||
designedMinValue: 0,
|
||
designedMaxValue: this.pctMax,
|
||
designedMinStep: 1,
|
||
unit: "%"
|
||
})
|
||
}
|
||
|
||
if( this.hasHue == true || this.hasRGB == true ) {
|
||
cTypes.push({
|
||
cType: types.HUE_CTYPE,
|
||
onUpdate: function(value) { that.command('hue', value); },
|
||
onRead: function(callback) {
|
||
that.query('hue', function(hue){
|
||
callback(hue);
|
||
});
|
||
},
|
||
perms: ["pw","pr","ev"],
|
||
format: "int",
|
||
initialValue: 0,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Adjust the Hue of the Light",
|
||
designedMinValue: 0,
|
||
designedMaxValue: 360,
|
||
designedMinStep: 1,
|
||
unit: "arcdegrees"
|
||
})
|
||
}
|
||
|
||
if( this.hasSat == true ) {
|
||
cTypes.push({
|
||
cType: types.SATURATION_CTYPE,
|
||
onUpdate: function(value) { that.command('sat', value); },
|
||
onRead: function(callback) {
|
||
that.query('sat', function(sat){
|
||
callback(sat);
|
||
});
|
||
},
|
||
perms: ["pw","pr","ev"],
|
||
format: "int",
|
||
initialValue: 100,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Adjust the Saturation of the Light",
|
||
designedMinValue: 0,
|
||
designedMaxValue: 100,
|
||
designedMinStep: 1,
|
||
unit: "%"
|
||
})
|
||
}
|
||
|
||
if( match = this.PossibleSets.match(/\bvolume\b/) ) {
|
||
cTypes.push({
|
||
cType: types.OUTPIUTVOLUME_CTYPE,
|
||
onUpdate: function(value) { that.command('volume', value); },
|
||
onRead: function(callback) {
|
||
that.query('volume', function(vol){
|
||
callback(vol);
|
||
});
|
||
},
|
||
perms: ["pw","pr","ev"],
|
||
format: "int",
|
||
initialValue: 10,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Adjust the Volume of the device",
|
||
designedMinValue: 0,
|
||
designedMaxValue: 100,
|
||
designedMinStep: 1,
|
||
unit: "%"
|
||
})
|
||
}
|
||
|
||
if( this.hasTemperature ) {
|
||
cTypes.push({
|
||
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||
//onUpdate: function(value) { console.log("Change:",value); execute("Thermostat", "Current Temperature", value); },
|
||
onRead: function(callback) {
|
||
that.query('temperature', function(temperature){
|
||
callback(parseFloat(temperature));
|
||
});
|
||
},
|
||
perms: ["pr","ev"],
|
||
format: "float",
|
||
initialValue: 20,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Current Temperature",
|
||
unit: "celsius"
|
||
})
|
||
}
|
||
|
||
if( this.hasHumidity ) {
|
||
cTypes.push({
|
||
cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE,
|
||
//onUpdate: function(value) { console.log("Change:",value); execute("Thermostat", "Current Temperature", value); },
|
||
onRead: function(callback) {
|
||
that.query('humidity', function(humidity){
|
||
callback(parseInt(humidity));
|
||
});
|
||
},
|
||
perms: ["pr","ev"],
|
||
format: "int",
|
||
initialValue: 50,
|
||
designedMinValue: 0,
|
||
designedMaxValue: 100,
|
||
supportEvents: true,
|
||
supportBonjour: false,
|
||
manfDescription: "Current Humidity",
|
||
unit: "%"
|
||
})
|
||
|
||
}
|
||
|
||
return cTypes;
|
||
},
|
||
|
||
sType: function() {
|
||
if( match = this.PossibleSets.match(/\bvolume\b/) ) {
|
||
return types.SPEAKER_STYPE
|
||
} else if( this.hasTemperature ) {
|
||
return types.TEMPERATURE_SENSOR_STYPE
|
||
} else if( this.hasHumidity ) {
|
||
return types.HUMIDITY_SENSOR_STYPE
|
||
} else if( this.hasPct || this.hasHue || this.hasRGB ) {
|
||
return types.LIGHTBULB_STYPE
|
||
} else {
|
||
return types.SWITCH_STYPE
|
||
}
|
||
},
|
||
|
||
getServices: function() {
|
||
var that = this;
|
||
var services = [{
|
||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||
characteristics: this.informationCharacteristics(),
|
||
},
|
||
{
|
||
sType: this.sType(),
|
||
characteristics: this.controlCharacteristics(that)
|
||
}];
|
||
this.log("Loaded services for " + this.name)
|
||
return services;
|
||
}
|
||
};
|
||
|
||
//module.exports.accessory = FHEMAccessory;
|
||
module.exports.platform = FHEMPlatform;
|