Files
homebridge/platforms/FHEM.js
2015-09-12 20:07:18 +02:00

658 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;