Initial commit
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
/* jshint -W097 */
|
||||
/* jshint strict: false */
|
||||
/* jslint node: true */
|
||||
/* jslint esversion: 6 */
|
||||
'use strict';
|
||||
|
||||
const inherits = require('util').inherits;
|
||||
|
||||
let User;
|
||||
let hap;
|
||||
//let version;
|
||||
let Server;
|
||||
//let Plugin;
|
||||
//let log;
|
||||
let Characteristic;
|
||||
let once;
|
||||
|
||||
let server;
|
||||
const charMap = {};
|
||||
|
||||
let logger;
|
||||
let updateState;
|
||||
let updateDev;
|
||||
let updateChannel;
|
||||
let setState;
|
||||
let mapper;
|
||||
let ignoreInfoAccessoryServices;
|
||||
|
||||
function customStringify(v, func, intent) {
|
||||
const cache = new Map();
|
||||
return JSON.stringify(v, function (key, value) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.get(value)) {
|
||||
// Circular reference found, discard key
|
||||
return;
|
||||
}
|
||||
// Store value in our map
|
||||
cache.set(value, true);
|
||||
}
|
||||
return value;
|
||||
}, intent);
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
logger = config.logger;
|
||||
updateState = config.updateState;
|
||||
updateDev = config.updateDev;
|
||||
updateChannel = config.updateChannel;
|
||||
setState = config.setState;
|
||||
ignoreInfoAccessoryServices = config.ignoreInfoAccessoryServices;
|
||||
|
||||
mapper = require('./mapper')(config);
|
||||
|
||||
User = require(config.homebridgeBasePath + 'lib/user').User;
|
||||
User.setStoragePath(config.homebridgeConfigPath);
|
||||
hap = require(config.homebridgeBasePath + 'node_modules/hap-nodejs');
|
||||
//version = require(config.homebridgeBasePath + 'lib/version');
|
||||
Server = require(config.homebridgeBasePath + 'lib/server').Server;
|
||||
//Plugin = require(config.homebridgeBasePath + 'lib/plugin').Plugin;
|
||||
//log = require(config.homebridgeBasePath + 'lib/logger')._system;
|
||||
once = require(config.homebridgeBasePath + 'node_modules/hap-nodejs/lib/util/once').once;
|
||||
Characteristic = hap.Characteristic;
|
||||
|
||||
inherits(MyBridge, hap.Bridge);
|
||||
|
||||
override(MyBridge, function publish(info, allowInsecureRequest) {
|
||||
logger.debug('yunkong2.ham Bridge publish ' + customStringify(info));
|
||||
// Вызов метода родительского класса
|
||||
// Calling the method of the parent class
|
||||
publish.inherited.call(this, info, allowInsecureRequest);
|
||||
});
|
||||
|
||||
/*
|
||||
// Переопределение метода в дочернем классе
|
||||
// Overriding a method in a child class
|
||||
override(MyBridge, function addService(service) {
|
||||
// Собственный функционал
|
||||
// Own Functionality
|
||||
logger.info('yunkong2.ham Bridge addService '+JSON.stringify(service)); //OK, undefined
|
||||
// Вызов метода родительского класса
|
||||
// Calling the method of the parent class
|
||||
return addService.inherited.call(this, service);
|
||||
});
|
||||
*/
|
||||
function iterateCharArray(chars, accessory, service, dev_idname, sr_id, sr_idname) {
|
||||
for (const channelIndex in chars) {
|
||||
if (!chars.hasOwnProperty(channelIndex)) continue;
|
||||
|
||||
const char = chars[channelIndex];
|
||||
const ch_id = char.UUID;
|
||||
const ch_name = char.displayName;
|
||||
const ch_val = char.value;
|
||||
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, ch_name);
|
||||
const id = dev_idname + '.' + sr_idname + '.' + ch_idname;
|
||||
|
||||
const common = mapper.mapCharacteristicProperties(char);
|
||||
logger.debug('Mapped Common for ' + id + ': ' + JSON.stringify(common));
|
||||
if (common.write) {
|
||||
charMap[id] = char; // TODO only if write allowed!
|
||||
logger.silly('Add object to charmap with id ' + id + '/' + customStringify(char));
|
||||
}
|
||||
|
||||
char.on('change', data => {
|
||||
logger.debug('Char change event: ' + data.oldValue + ' --> ' + data.newValue);
|
||||
handleCharValue(accessory, service, char, data.newValue);
|
||||
});
|
||||
|
||||
updateState(dev_idname, sr_idname, ch_idname, ch_name, ch_val, common, ch_id, () => {
|
||||
char.getValue((err, value) => {
|
||||
if (err) {
|
||||
logger.warn('Error while getting current value: ' + err);
|
||||
return;
|
||||
}
|
||||
handleCharValue(accessory, service, char, value);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override(MyBridge, function addBridgedAccessory(accessory, deferUpdate) {
|
||||
// Вызов метода родительского класса
|
||||
// Calling the method of the parent class
|
||||
accessory = addBridgedAccessory.inherited.call(this, accessory, deferUpdate);
|
||||
logger.debug('yunkong2.ham Bridge addBridgedAccessory ' + customStringify(accessory)); //OK
|
||||
// Новое устройство
|
||||
// New device
|
||||
const dev_id = accessory.UUID;
|
||||
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
|
||||
const dev_name = accessory.displayName;
|
||||
const dev_cat = accessory.category;
|
||||
|
||||
updateDev(dev_idname, dev_name, dev_cat, dev_id);
|
||||
|
||||
for (const index in accessory.services) {
|
||||
if (!accessory.services.hasOwnProperty(index)) continue;
|
||||
|
||||
const service = accessory.services[index];
|
||||
const sr_id = service.UUID;
|
||||
const sr_idname = mapper.mapServiceType(sr_id, service.displayName);
|
||||
const sr_name = service.displayName;
|
||||
|
||||
if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.silly('Add service class=' + customStringify(service));
|
||||
updateChannel(dev_idname, sr_idname, sr_name, sr_id);
|
||||
|
||||
iterateCharArray(service.characteristics, accessory, service, dev_idname, sr_id, sr_idname);
|
||||
if (service.optionalCharacteristics) {
|
||||
iterateCharArray(service.optionalCharacteristics, accessory, service, dev_idname, sr_id, sr_idname);
|
||||
}
|
||||
|
||||
}
|
||||
return accessory;
|
||||
});
|
||||
|
||||
Server.prototype._createBridge = function() {
|
||||
logger.debug('yunkong2.ham Bridge create'); //OK
|
||||
// pull out our custom Bridge settings from config.json, if any
|
||||
const bridgeConfig = this._config.bridge || {};
|
||||
|
||||
// Create our Bridge which will host all loaded Accessories
|
||||
return new MyBridge(bridgeConfig.name || 'Homebridge', hap.uuid.generate('HomeBridge'));
|
||||
};
|
||||
|
||||
// Updated to compare value differetly. Needed till officially updated
|
||||
Characteristic.prototype.setValue = function(newValue, callback, context, connectionID) {
|
||||
|
||||
if ( newValue instanceof Error ) {
|
||||
this.status = newValue
|
||||
} else {
|
||||
this.status = null;
|
||||
}
|
||||
|
||||
newValue = this.validateValue(newValue); //validateValue returns a value that has be cooerced into a valid value.
|
||||
|
||||
var oldValue = this.value;
|
||||
|
||||
if (this.listeners('set').length > 0) {
|
||||
|
||||
// allow a listener to handle the setting of this value, and wait for completion
|
||||
this.emit('set', newValue, once(function(err) {
|
||||
this.status = err;
|
||||
if (err) {
|
||||
// pass the error along to our callback
|
||||
if (callback) callback(err);
|
||||
}
|
||||
else {
|
||||
if (newValue === undefined || newValue === null)
|
||||
newValue = this.getDefaultValue();
|
||||
// setting the value was a success; so we can cache it now
|
||||
this.value = newValue;
|
||||
if (callback) callback();
|
||||
|
||||
if (this.eventOnlyCharacteristic === true || oldValue !== newValue)
|
||||
this.emit('change', { oldValue:oldValue, newValue:newValue, context:context });
|
||||
}
|
||||
|
||||
}.bind(this)), context, connectionID);
|
||||
|
||||
}
|
||||
else {
|
||||
if (newValue === undefined || newValue === null)
|
||||
newValue = this.getDefaultValue();
|
||||
// no one is listening to the 'set' event, so just assign the value blindly
|
||||
this.value = newValue;
|
||||
if (callback) callback();
|
||||
|
||||
if (this.eventOnlyCharacteristic === true || oldValue !== newValue)
|
||||
this.emit('change', { oldValue:oldValue, newValue:newValue, context:context });
|
||||
}
|
||||
|
||||
return this; // for chaining
|
||||
}
|
||||
|
||||
function MyBridge(displayName, serialNumber) {
|
||||
logger.debug('yunkong2.ham Bridge constructor');
|
||||
MyBridge.super_.call(this, displayName, serialNumber);
|
||||
}
|
||||
}
|
||||
|
||||
function registerExistingAccessory(UUID, name) {
|
||||
mapper.mapAccessoryUUID(UUID, name);
|
||||
}
|
||||
|
||||
function start() {
|
||||
const insecureAccess = false;
|
||||
logger.info('Using Homebridge Config Path: ' + User.persistPath());
|
||||
// Initialize HAP-NodeJS with a custom persist directory
|
||||
hap.init(User.persistPath());
|
||||
|
||||
server = new Server(insecureAccess);
|
||||
|
||||
server.run();
|
||||
}
|
||||
|
||||
function end() {
|
||||
if (server) {
|
||||
server._teardown();
|
||||
// Save cached accessories to persist storage.
|
||||
server._updateCachedAccessories();
|
||||
}
|
||||
}
|
||||
|
||||
function setValueForCharId(id, value) {
|
||||
if (charMap[id]) {
|
||||
logger.debug('set value of char for ' + id);
|
||||
charMap[id].setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCharValue(accessory, serv, char, newValue){
|
||||
logger.debug('handleCharValue = ' + newValue);
|
||||
logger.silly('characteristic = ' + customStringify(char));
|
||||
logger.silly('accessory =' + customStringify(accessory));
|
||||
|
||||
const sr_id = serv.UUID;
|
||||
const sr_idname = mapper.mapServiceType(sr_id, serv.displayName);
|
||||
const ch_id = char.UUID;
|
||||
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, char.displayName);
|
||||
const dev_id = accessory.UUID;
|
||||
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
|
||||
const value = newValue;
|
||||
|
||||
setState(dev_idname, sr_idname, ch_idname, value);
|
||||
}
|
||||
|
||||
// Средство для переопределения функций
|
||||
// Tools for overriding functions
|
||||
function override(child, fn) {
|
||||
child.prototype[fn.name] = fn;
|
||||
fn.inherited = child.super_.prototype[fn.name];
|
||||
}
|
||||
|
||||
exports.init = init;
|
||||
exports.end = end;
|
||||
exports.setValueForCharId = setValueForCharId;
|
||||
exports.start = start;
|
||||
exports.registerExistingAccessory = registerExistingAccessory;
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/* jshint -W097 */
|
||||
/* jshint strict: false */
|
||||
/* jslint node: true */
|
||||
/* jslint esversion: 6 */
|
||||
'use strict';
|
||||
|
||||
module.exports = function (config) {
|
||||
const module = {};
|
||||
|
||||
let accTypes;
|
||||
if (config.useGlobalHomebridge) {
|
||||
accTypes = require(config.homebridgeBasePath + 'node_modules/hap-nodejs/accessories/types');
|
||||
}
|
||||
else {
|
||||
accTypes = require('homebridge-plugin-wrapper').HapTypes;
|
||||
}
|
||||
|
||||
const forbiddenCharacters = /[\]\[*,;'"`<>\\\s?]/g;
|
||||
|
||||
const accessoryNameMap = {};
|
||||
module.mapAccessoryUUID = function (accessoryUUID, displayName) {
|
||||
if (accessoryNameMap[accessoryUUID]) {
|
||||
return accessoryNameMap[accessoryUUID];
|
||||
}
|
||||
if (displayName) {
|
||||
displayName = displayName.replace(forbiddenCharacters, '-');
|
||||
}
|
||||
else {
|
||||
displayName = accessoryUUID;
|
||||
}
|
||||
|
||||
let found = false;
|
||||
for (const uuid in accessoryNameMap) {
|
||||
if (accessoryNameMap.hasOwnProperty(uuid) && accessoryNameMap[uuid] === displayName) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
displayName = displayName + '-' + accessoryUUID;
|
||||
}
|
||||
|
||||
accessoryNameMap[accessoryUUID] = displayName;
|
||||
return displayName;
|
||||
};
|
||||
|
||||
|
||||
const serviceTypeMap = {};
|
||||
serviceTypeMap[accTypes.LIGHTBULB_STYPE] = 'Lightbulb';
|
||||
serviceTypeMap[accTypes.SWITCH_STYPE] = 'Switch';
|
||||
serviceTypeMap[accTypes.THERMOSTAT_STYPE] = 'Thermostat';
|
||||
serviceTypeMap[accTypes.GARAGE_DOOR_OPENER_STYPE] = 'Garage-Door-Opener';
|
||||
serviceTypeMap[accTypes.ACCESSORY_INFORMATION_STYPE] = 'Accessory-Information';
|
||||
serviceTypeMap[accTypes.FAN_STYPE] = 'Fan';
|
||||
serviceTypeMap[accTypes.OUTLET_STYPE] = 'Outlet';
|
||||
serviceTypeMap[accTypes.LOCK_MECHANISM_STYPE] = 'Lock-Mechanism';
|
||||
serviceTypeMap[accTypes.LOCK_MANAGEMENT_STYPE] = 'Lock-Management';
|
||||
serviceTypeMap[accTypes.ALARM_STYPE] = 'Alarm';
|
||||
serviceTypeMap[accTypes.WINDOW_COVERING_STYPE] = 'Window-Covering';
|
||||
serviceTypeMap[accTypes.OCCUPANCY_SENSOR_STYPE] = 'Occupancy-Sensor';
|
||||
serviceTypeMap[accTypes.CONTACT_SENSOR_STYPE] = 'Contact-Sensor';
|
||||
serviceTypeMap[accTypes.MOTION_SENSOR_STYPE] = 'Motion-Sensor';
|
||||
serviceTypeMap[accTypes.HUMIDITY_SENSOR_STYPE] = 'Humidity-Sensor';
|
||||
serviceTypeMap[accTypes.TEMPERATURE_SENSOR_STYPE] = 'Temperature-Sensor';
|
||||
|
||||
module.mapServiceType = function (serviceUUID, displayName) {
|
||||
if (displayName) {
|
||||
displayName = displayName.replace(forbiddenCharacters, '-');
|
||||
return displayName;
|
||||
}
|
||||
if (serviceTypeMap[serviceUUID]) {
|
||||
return serviceTypeMap[serviceUUID];
|
||||
}
|
||||
|
||||
const pos = serviceUUID.indexOf('-0000-1000-8000-0026BB765291');
|
||||
if (pos !== -1) {
|
||||
return serviceUUID.substring(0, pos);
|
||||
}
|
||||
|
||||
return serviceUUID;
|
||||
};
|
||||
|
||||
|
||||
const characteristicTypeMap = {};
|
||||
characteristicTypeMap[accTypes.ALARM_CURRENT_STATE_CTYPE] = {name: 'Current-Alarm-State', roleDetail: '.alarm'};
|
||||
characteristicTypeMap[accTypes.ALARM_TARGET_STATE_CTYPE] = {name: 'Target-Alarm-State', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.ADMIN_ONLY_ACCESS_CTYPE] = {name: 'Admin-Only-Access', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.AUDIO_FEEDBACK_CTYPE] = {name: 'Audio-Feedback', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.BRIGHTNESS_CTYPE] = {name: 'Brightness', roleDetail: '.brightness'};
|
||||
characteristicTypeMap[accTypes.BATTERY_LEVEL_CTYPE] = {name: 'Battery-Level', roleDetail: '.battery'};
|
||||
characteristicTypeMap[accTypes.COOLING_THRESHOLD_CTYPE] = {name: 'Cooling-Threshold', roleDetail: '.temperature'};
|
||||
characteristicTypeMap[accTypes.CONTACT_SENSOR_STATE_CTYPE] = {name: 'Contact-Sensor-State', roleDetail: '.window'}; //??
|
||||
characteristicTypeMap[accTypes.CURRENT_DOOR_STATE_CTYPE] = {name: 'Current-Door-State', roleDetail: '.door'};
|
||||
characteristicTypeMap[accTypes.CURRENT_LOCK_MECHANISM_STATE_CTYPE] = {name: 'Current-Lock-Mechanism-State', roleDetail: '.lock'};
|
||||
characteristicTypeMap[accTypes.CURRENT_RELATIVE_HUMIDITY_CTYPE] = {name: 'Current-Relative-Humidity', roleDetail: '.humidity'};
|
||||
characteristicTypeMap[accTypes.CURRENT_TEMPERATURE_CTYPE] = {name: 'Current-Temperature', roleDetail: '.temperature'};
|
||||
characteristicTypeMap[accTypes.HEATING_THRESHOLD_CTYPE] = {name: 'Heating-Threshold', roleDetail: '.temperature'};
|
||||
characteristicTypeMap[accTypes.HUE_CTYPE] = {name: 'Hue', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.IDENTIFY_CTYPE] = {name: 'Identify', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.LOCK_MANAGEMENT_AUTO_SECURE_TIMEOUT_CTYPE] = {name: 'Auto-Secure-Timeout', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE] = {name: 'Control-Point', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.LOCK_MECHANISM_LAST_KNOWN_ACTION_CTYPE] = {name: 'Last-Known-Action', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.LOGS_CTYPE] = {name: 'Logs', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.MANUFACTURER_CTYPE] = {name: 'Manufacturer', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.MODEL_CTYPE] = {name: 'Model', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.MOTION_DETECTED_CTYPE] = {name: 'Motion-Detected', roleDetail: '.motion'};
|
||||
characteristicTypeMap[accTypes.NAME_CTYPE] = {name: 'Name', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.OBSTRUCTION_DETECTED_CTYPE] = {name: 'Obstruction-Detected', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.OUTLET_IN_USE_CTYPE] = {name: 'Outlet-In-Use', roleDetail: 'indicator.working'};
|
||||
characteristicTypeMap[accTypes.OCCUPANCY_DETECTED_CTYPE] = {name: 'Occupancy-Detected', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.POWER_STATE_CTYPE] = {name: 'Power-State', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.PROGRAMMABLE_SWITCH_SWITCH_EVENT_CTYPE] = {name: 'Switch-Event-Programmable-Switch', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.PROGRAMMABLE_SWITCH_OUTPUT_STATE_CTYPE] = {name: 'Output-State-Programmable-Switch', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.ROTATION_DIRECTION_CTYPE] = {name: 'Rotation-Direction', roleDetail: '.direction'};
|
||||
characteristicTypeMap[accTypes.ROTATION_SPEED_CTYPE] = {name: 'Rotation-Speed', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.SATURATION_CTYPE] = {name: 'Saturation', roleDetail: '.color.saturation'};
|
||||
characteristicTypeMap[accTypes.SERIAL_NUMBER_CTYPE] = {name: 'Serial-Number', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.STATUS_LOW_BATTERY_CTYPE] = {name: 'Low-Battery', roleDetail: '.lowbat'};
|
||||
characteristicTypeMap[accTypes.STATUS_FAULT_CTYPE] = {name: 'Fault', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.TARGET_DOORSTATE_CTYPE] = {name: 'Target-Doorstate', roleDetail: '.door'};
|
||||
characteristicTypeMap[accTypes.TARGET_LOCK_MECHANISM_STATE_CTYPE] = {name: 'Target-Lock-Mechanism-State', roleDetail: '.lock'};
|
||||
characteristicTypeMap[accTypes.TARGET_RELATIVE_HUMIDITY_CTYPE] = {name: 'Target-Relative-Humidity', roleDetail: '.humidity'};
|
||||
characteristicTypeMap[accTypes.TARGET_TEMPERATURE_CTYPE] = {name: 'Target-Temperature', roleDetail: '.temperature'};
|
||||
characteristicTypeMap[accTypes.TEMPERATURE_UNITS_CTYPE] = {name: 'Temperature-Units', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.VERSION_CTYPE] = {name: 'Version', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.WINDOW_COVERING_TARGET_POSITION_CTYPE] = {name: 'Target-Position-Window-Covering', roleDetail: '.blind'};
|
||||
characteristicTypeMap[accTypes.WINDOW_COVERING_CURRENT_POSITION_CTYPE] = {name: 'Current-Position-Window-Covering', roleDetail: '.blind'};
|
||||
characteristicTypeMap[accTypes.WINDOW_COVERING_OPERATION_STATE_CTYPE] = {name: 'Operation-State-Window-Covering', roleDetail: '.working'};
|
||||
characteristicTypeMap[accTypes.CURRENTHEATINGCOOLING_CTYPE] = {name: 'Current-Heating-Cooling', roleDetail: ''};
|
||||
characteristicTypeMap[accTypes.TARGETHEATINGCOOLING_CTYPE] = {name: 'Target-Heating-Cooling', roleDetail: ''};
|
||||
|
||||
module.mapCharacteristicType = function (serviceUUID, charUUID, displayName) {
|
||||
if (displayName) {
|
||||
displayName = displayName.replace(forbiddenCharacters, '-');
|
||||
return displayName;
|
||||
}
|
||||
if (characteristicTypeMap[charUUID]) return characteristicTypeMap[charUUID].name;
|
||||
|
||||
const pos = charUUID.indexOf("-0000-1000-8000-0026BB765291");
|
||||
if (pos !== -1) return charUUID.substring(0, pos);
|
||||
|
||||
return charUUID;
|
||||
};
|
||||
|
||||
|
||||
const characteristicFormats = {
|
||||
'bool': 'boolean',
|
||||
'int': 'number',
|
||||
'float': 'number',
|
||||
'string': 'string',
|
||||
'uint8': 'number',
|
||||
'uint16': 'number',
|
||||
'uint32': 'number',
|
||||
'uint64': 'number',
|
||||
'data': 'string',
|
||||
'tlv8': 'string',
|
||||
'array': 'string', //Not in HAP Spec
|
||||
'dict': 'string' //Not in HAP Spec
|
||||
};
|
||||
|
||||
// Known HomeKit unit types
|
||||
const characteristicUnits = {
|
||||
// HomeKit only defines Celsius, for Fahrenheit, it requires iOS app to do the conversion.
|
||||
'celsius': '°C',
|
||||
'percentage': '%',
|
||||
'arcdegrees': '°',
|
||||
'lux': 'lx',
|
||||
'seconds': 's'
|
||||
};
|
||||
|
||||
module.mapCharacteristicProperties = function (char) {
|
||||
const common = {};
|
||||
if (characteristicFormats[char.props.format]) {
|
||||
common.type = characteristicFormats[char.props.format];
|
||||
}
|
||||
else common.type = 'string';
|
||||
|
||||
if (characteristicUnits[char.props.unit]) {
|
||||
common.unit = characteristicUnits[char.props.unit];
|
||||
}
|
||||
else if (char.props.unit) {
|
||||
common.unit = char.props.unit;
|
||||
}
|
||||
|
||||
if (char.props.minValue !== null && char.props.minValue !== undefined) {
|
||||
common.min = char.props.minValue;
|
||||
}
|
||||
if (char.props.maxValue !== null && char.props.maxValue !== undefined) {
|
||||
common.max = char.props.maxValue;
|
||||
}
|
||||
if (common.min === undefined || common.max === undefined) {
|
||||
switch(char.props.format) {
|
||||
case 'int':
|
||||
if (common.min === undefined) common.min = -2147483648;
|
||||
if (common.max === undefined) common.max = 2147483647;
|
||||
break;
|
||||
case 'uint8':
|
||||
if (common.min === undefined) common.min = 0;
|
||||
if (common.max === undefined) common.max = 255;
|
||||
break;
|
||||
case 'uint16':
|
||||
if (common.min === undefined) common.min = 0;
|
||||
if (common.max === undefined) common.max = 65535;
|
||||
break;
|
||||
case 'uint32':
|
||||
if (common.min === undefined) common.min = 0;
|
||||
if (common.max === undefined) common.max = 4294967295;
|
||||
break;
|
||||
case 'uint64':
|
||||
if (common.min === undefined) common.min = 0;
|
||||
if (common.max === undefined) common.max = 18446744073709551615;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (char.validValues && char.validValues.length > 0 && common.type !== 'boolean') {
|
||||
common.states = {};
|
||||
for (let i = 0; i < char.validValues; i++) {
|
||||
common.states[char.validValues[i]] = char.validValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
common.read = (char.props.perms.indexOf('pr') !== -1);
|
||||
common.write = (char.props.perms.indexOf('pw') !== -1);
|
||||
if (!common.read && !common.write) {
|
||||
common.read = true;
|
||||
}
|
||||
|
||||
let roleDetail = null;
|
||||
if (characteristicTypeMap[char.UUID]) {
|
||||
roleDetail = characteristicTypeMap[char.UUID].roleDetail;
|
||||
}
|
||||
|
||||
// Try to set roles
|
||||
let role = '';
|
||||
if (common.type === 'boolean') {
|
||||
if (common.read && !common.write) { // Boolean, read-only --> Sensor OR Indicator!
|
||||
role = 'sensor';
|
||||
}
|
||||
else if (common.write && !common.read) { // Boolean, write-only --> Button
|
||||
role = 'button';
|
||||
}
|
||||
else if (common.read && common.write) { // Boolean, read-write --> Switch
|
||||
role = 'switch';
|
||||
}
|
||||
}
|
||||
else if (common.type === 'number') {
|
||||
if (common.read && !common.write) { // Number, read-only --> Value
|
||||
role = 'value';
|
||||
}
|
||||
else if (common.write && !common.read) { // Boolean, write-only --> ?? Level?
|
||||
role = 'level';
|
||||
}
|
||||
else if (common.read && common.write) { // Number, read-write --> Level
|
||||
role = 'level';
|
||||
}
|
||||
}
|
||||
else if (common.type === 'string') {
|
||||
role = 'text';
|
||||
}
|
||||
if (roleDetail && roleDetail !== '' && roleDetail.indexOf('.') === 0 && role !== '') {
|
||||
role += roleDetail;
|
||||
}
|
||||
else if (roleDetail && roleDetail !== '' && roleDetail.indexOf('.') === -1) {
|
||||
role = roleDetail;
|
||||
}
|
||||
if (role !== '') common.role = role;
|
||||
|
||||
if (!common.role) common.role = 'state';
|
||||
|
||||
return common;
|
||||
};
|
||||
|
||||
|
||||
return module;
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let controllerDir;
|
||||
let appName;
|
||||
|
||||
/**
|
||||
* returns application name
|
||||
*
|
||||
* The name of the application can be different and this function finds it out.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function getAppName() {
|
||||
const parts = __dirname.replace(/\\/g, '/').split('/');
|
||||
return parts[parts.length - 2].split('.')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* looks for js-controller home folder
|
||||
*
|
||||
* @param {boolean} isInstall
|
||||
* @returns {string}
|
||||
*/
|
||||
function getControllerDir(isInstall) {
|
||||
// Find the js-controller location
|
||||
const possibilities = [
|
||||
'yunkong2.js-controller',
|
||||
'yunkong2.js-controller',
|
||||
];
|
||||
/** @type {string} */
|
||||
let controllerPath;
|
||||
for (const pkg of possibilities) {
|
||||
try {
|
||||
const possiblePath = require.resolve(pkg);
|
||||
if (fs.existsSync(possiblePath)) {
|
||||
controllerPath = possiblePath;
|
||||
break;
|
||||
}
|
||||
} catch (e) { /* not found */ }
|
||||
}
|
||||
if (!controllerPath) {
|
||||
if (!isInstall) {
|
||||
console.log('Cannot find js-controller');
|
||||
process.exit(10);
|
||||
} else {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
// we found the controller
|
||||
return path.dirname(controllerPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* reads controller base settings
|
||||
*
|
||||
* @alias getConfig
|
||||
* @returns {object}
|
||||
*/
|
||||
function getConfig() {
|
||||
let configPath;
|
||||
if (fs.existsSync(
|
||||
configPath = path.join(controllerDir, 'conf', appName + '.json')
|
||||
)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} else if (fs.existsSync(
|
||||
configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json')
|
||||
)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} else {
|
||||
throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json');
|
||||
}
|
||||
}
|
||||
appName = getAppName();
|
||||
controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1);
|
||||
const adapter = require(path.join(controllerDir, 'lib/adapter.js'));
|
||||
|
||||
exports.controllerDir = controllerDir;
|
||||
exports.getConfig = getConfig;
|
||||
exports.Adapter = adapter;
|
||||
exports.appName = appName;
|
||||
@@ -0,0 +1,148 @@
|
||||
/* jshint strict: false */
|
||||
/* jslint node: true */
|
||||
/* jslint esversion: 6 */
|
||||
'use strict';
|
||||
|
||||
const HomebridgeWrapper = require('homebridge-plugin-wrapper').Wrapper;
|
||||
|
||||
let homebridgeWrapper;
|
||||
const charMap = {};
|
||||
|
||||
let logger;
|
||||
let updateState;
|
||||
let updateDev;
|
||||
let updateChannel;
|
||||
let setState;
|
||||
let mapper;
|
||||
let ignoreInfoAccessoryServices;
|
||||
|
||||
function customStringify(v, func, intent) {
|
||||
const cache = new Map();
|
||||
return JSON.stringify(v, function (key, value) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.get(value)) {
|
||||
// Circular reference found, discard key
|
||||
return;
|
||||
}
|
||||
// Store value in our map
|
||||
cache.set(value, true);
|
||||
}
|
||||
return value;
|
||||
}, intent);
|
||||
}
|
||||
|
||||
function init(config) {
|
||||
logger = config.logger;
|
||||
updateState = config.updateState;
|
||||
updateDev = config.updateDev;
|
||||
updateChannel = config.updateChannel;
|
||||
setState = config.setState;
|
||||
ignoreInfoAccessoryServices = config.ignoreInfoAccessoryServices;
|
||||
|
||||
mapper = require('./mapper')(config);
|
||||
homebridgeWrapper = new HomebridgeWrapper(config);
|
||||
|
||||
homebridgeWrapper.on('characteristic-value-change', data => {
|
||||
logger.debug('Char change event: ' + data.oldValue + ' --> ' + data.newValue);
|
||||
handleCharValue(data.accessory, data.service, data.characteristic, data.newValue);
|
||||
});
|
||||
|
||||
homebridgeWrapper.on('addAccessory', accessory => {
|
||||
function iterateCharArray(chars, dev_idname, sr_id, sr_idname) {
|
||||
for (const chindex in chars) {
|
||||
if (!chars.hasOwnProperty(chindex)) continue;
|
||||
const char = chars[chindex];
|
||||
const ch_id = char.UUID;
|
||||
const ch_name = char.displayName;
|
||||
const ch_val = char.value;
|
||||
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, ch_name);
|
||||
const id = dev_idname + '.' + sr_idname + '.' + ch_idname;
|
||||
|
||||
const common = mapper.mapCharacteristicProperties(char);
|
||||
logger.debug('Mapped Common for ' + id + ': ' + JSON.stringify(common));
|
||||
if (common.write) {
|
||||
charMap[id] = char; // TODO only if write allowed!
|
||||
logger.silly('Add object to charmap with id ' + id + '/' + customStringify(char));
|
||||
}
|
||||
|
||||
updateState(dev_idname, sr_idname, ch_idname, ch_name, ch_val, common, ch_id);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('yunkong2.ham Bridge addBridgedAccessory ' + customStringify(accessory)); //OK
|
||||
// Новое устройство
|
||||
// New device
|
||||
const dev_id = accessory.UUID;
|
||||
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
|
||||
const dev_name = accessory.displayName;
|
||||
const dev_cat = accessory.category;
|
||||
|
||||
updateDev(dev_idname, dev_name, dev_cat, dev_id);
|
||||
for (const index in accessory.services) {
|
||||
if (!accessory.services.hasOwnProperty(index)) continue;
|
||||
|
||||
const service = accessory.services[index];
|
||||
const sr_id = service.UUID;
|
||||
const sr_idname = mapper.mapServiceType(sr_id, service.displayName);
|
||||
const sr_name = service.displayName;
|
||||
|
||||
if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.silly('Add service class=' + customStringify(service));
|
||||
updateChannel(dev_idname, sr_idname, sr_name, sr_id);
|
||||
|
||||
iterateCharArray(service.characteristics, dev_idname, sr_id, sr_idname);
|
||||
if (service.optionalCharacteristics) {
|
||||
iterateCharArray(service.optionalCharacteristics, dev_idname, sr_id, sr_idname);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function registerExistingAccessory(UUID, name) {
|
||||
mapper.mapAccessoryUUID(UUID, name);
|
||||
}
|
||||
|
||||
function start() {
|
||||
homebridgeWrapper.init();
|
||||
}
|
||||
|
||||
function end() {
|
||||
homebridgeWrapper.finish();
|
||||
}
|
||||
|
||||
function setValueForCharId(id, value) {
|
||||
if (charMap[id]) {
|
||||
logger.debug('set value ' + value + ' of char for ' + id);
|
||||
charMap[id].setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCharValue(accessory, serv, char, newValue){
|
||||
const sr_id = serv.UUID;
|
||||
const sr_idname = mapper.mapServiceType(sr_id, serv.displayName);
|
||||
const ch_id = char.UUID;
|
||||
const ch_idname = mapper.mapCharacteristicType(sr_id, ch_id, char.displayName);
|
||||
const dev_id = accessory.UUID;
|
||||
const dev_idname = mapper.mapAccessoryUUID(dev_id, accessory.displayName);
|
||||
const value = newValue;
|
||||
|
||||
logger.debug('handleCharValue = ' + newValue);
|
||||
logger.silly('characteristic = ' + customStringify(char));
|
||||
logger.silly('accessory =' + customStringify(accessory));
|
||||
|
||||
if (ignoreInfoAccessoryServices && sr_idname === 'Accessory-Information') {
|
||||
return;
|
||||
}
|
||||
setState(dev_idname, sr_idname, ch_idname, value);
|
||||
}
|
||||
|
||||
exports.init = init;
|
||||
exports.end = end;
|
||||
exports.setValueForCharId = setValueForCharId;
|
||||
exports.start = start;
|
||||
exports.registerExistingAccessory = registerExistingAccessory;
|
||||
Reference in New Issue
Block a user