├── .npmignore ├── .gitignore ├── package.json ├── lib ├── parse │ ├── Characteristic.js │ ├── Accessory.js │ ├── Homebridge.js │ ├── Homebridges.js │ ├── Service.js │ └── messages.js ├── aliceLocal.js └── aliceActions.js ├── config.schema.json ├── plugin.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | gh-md-toc 2 | docs 3 | publish.sh 4 | README.md.orig.* 5 | README.md.toc.* 6 | tools/* 7 | test_commands/* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | 3 | *.zip 4 | node_modules 5 | package-lock.json 6 | password* 7 | README.md.toc.* 8 | README.md.orig.* 9 | Troubleshooting.md.toc.* 10 | Troubleshooting.md.orig.* 11 | test_commands 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-g-on-alice", 3 | "version": "0.2.0", 4 | "description": "Yandex Alice Voice Assistant plugin for Homebridge. Developed by G-On", 5 | "main": "plugin.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/G-On-dev/homebridge-g-on-alice.git" 12 | }, 13 | "keywords": [ 14 | "alice", 15 | "yandex", 16 | "homebridge-plugin", 17 | "voice control", 18 | "smart home" 19 | ], 20 | "engines": { 21 | "node": ">=10.17.0", 22 | "homebridge": "^1.6.0" 23 | }, 24 | "devDependencies": {}, 25 | "dependencies": { 26 | "@oznu/hap-client": "^1.7.2", 27 | "bottleneck": ">=2.18.0", 28 | "debug": ">2.6.9", 29 | "hap-node-client": "^0.2.4", 30 | "mqtt": ">=2.18.8", 31 | "mqtt-pattern": "^1.2.0" 32 | }, 33 | "author": "G-On-dev", 34 | "license": "Apache-2.0", 35 | "bugs": { 36 | "url": "https://github.com/G-On-dev/homebridge-g-on-alice/issues" 37 | }, 38 | "homepage": "https://github.com/G-On-dev/homebridge-g-on-alice#readme" 39 | } 40 | -------------------------------------------------------------------------------- /lib/parse/Characteristic.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Characteristic'); 2 | var messages = require('./messages.js'); 3 | 4 | module.exports = { 5 | Characteristic: Characteristic 6 | }; 7 | 8 | /* 9 | * Homebridges -> Homebridge -> Accessory -> Service -> Characteristic 10 | */ 11 | 12 | function Characteristic(devices, context) { 13 | //console.log("Characteristic", devices, context); 14 | this.aid = context.aid; 15 | this.iid = devices.iid; 16 | this.type = parseInt(devices.type,16).toString(16).toUpperCase(); 17 | this.serviceType = context.type; 18 | this.value = devices.value; 19 | if (devices.minValue) this.minValue = devices.minValue; 20 | if (devices.maxValue) this.maxValue = devices.maxValue; 21 | 22 | this.description = devices.description; 23 | 24 | if (devices["valid-values"]) { 25 | // allowes thermostat modes or temperature units 26 | this.validValues = devices["valid-values"]; 27 | } else { 28 | if (devices.validValues) { 29 | // allowes thermostat modes or temperature units 30 | this.validValues = devices.validValues; 31 | } 32 | } 33 | this.capabilities = messages.lookupCapabilities(this); 34 | this.properties = messages.lookupProperties(this); 35 | } 36 | -------------------------------------------------------------------------------- /lib/parse/Accessory.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Accessory'); 2 | var Service = require('./Service.js').Service; 3 | 4 | module.exports = { 5 | Accessory: Accessory 6 | }; 7 | 8 | /* 9 | * Homebridges -> Homebridge -> Accessory -> Service -> Characteristic 10 | */ 11 | function Accessory(devices, context) { 12 | //console.log("Accessory", devices); 13 | this.aid = devices.aid; 14 | this.host = context.host; 15 | this.port = context.port; 16 | this.hb_name = context.hb_name; 17 | this.services = {}; 18 | devices.services.forEach(function(element) { 19 | switch (parseInt(element.type,16).toString(16).toUpperCase()) { 20 | case "3E": // Accessory Information 21 | this.info = information(element.characteristics); 22 | this.name = this.info.Name; 23 | break; 24 | default: 25 | if (!this.info) { 26 | this.name = "Unknown"; 27 | this.info = {}; 28 | this.info.Manufacturer = "Unknown"; 29 | this.info.Name = "Unknown"; 30 | } 31 | var service = new Service(element, this); 32 | this.services[service.iid] = service; 33 | } 34 | }.bind(this)); 35 | } 36 | 37 | Accessory.prototype.getDeviceCapabilities = function() { 38 | var list = []; 39 | var context = { 40 | aid: this.aid, 41 | name: this.info.Name, 42 | manufacturer: this.info.Manufacturer, 43 | model: this.info.Model 44 | } 45 | 46 | for (var index in this.services) { 47 | var service = this.services[index]; 48 | var service_data = service.getDeviceCapabilities(context); 49 | if (service_data) { 50 | list = list.concat(service_data); 51 | } 52 | } 53 | 54 | return (list); 55 | }; 56 | 57 | function information(characteristics) { 58 | var result = {}; 59 | characteristics.forEach(function(characteristic) { 60 | if (characteristic.description) { 61 | var key = characteristic.description.replace(/ /g, '').replace(/\./g, '_'); 62 | result[key] = characteristic.value; 63 | } 64 | }); 65 | return result; 66 | } 67 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "G-On Alice", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "headerDisplay": "An account to link your Yandex Alice to Homebridge needs to created on https://homebridge.g-on.io/. This account will be used when you enable the skill in the Yandex App, and in the configuration below.", 6 | "footerDisplay": "See https://github.com/G-On-dev/homebridge-g-on-alice for more information and instructions.\n\nHomebridge Yandex Alice Skill: %link to Yandex Alice Skill, once it's public% ", 7 | "schema": { 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "title": "Name", 12 | "type": "string", 13 | "required": true, 14 | "default": "G-On Alice", 15 | "description": "Plugin name as displayed in the Homebridge log" 16 | }, 17 | "username": { 18 | "title": "Username", 19 | "type": "string", 20 | "required": true, 21 | "description": "Username for https://homebridge.g-on.io/" 22 | }, 23 | "password": { 24 | "title": "Password", 25 | "type": "string", 26 | "required": true, 27 | "description": "Password for https://homebridge.g-on.io/" 28 | }, 29 | "pin": { 30 | "title": "Homebridge Pin", 31 | "type": "string", 32 | "required": true, 33 | "placeholder": "031-45-154", 34 | "description": "This needs to match the Homebridge pin set in your config.json file" 35 | }, 36 | "debug": { 37 | "title": " Enable Debug Mode", 38 | "type": "boolean" 39 | }, 40 | "notifies": { 41 | "title": " Notifies the Yandex smart home platform about the changed state of the devices.", 42 | "type": "boolean" 43 | } 44 | } 45 | }, 46 | "layout": [ 47 | "name", 48 | { 49 | "type": "flex", 50 | "flex-flow": "row wrap", 51 | "items": [ 52 | "username", 53 | { 54 | "key": "password", 55 | "type": "password" 56 | } 57 | ] 58 | }, 59 | "notifies", 60 | { 61 | "type": "fieldset", 62 | "title": "Optional Settings", 63 | "expandable": true, 64 | "expanded": false, 65 | "items": [ 66 | "debug" 67 | ] 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /lib/parse/Homebridge.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Homebridge'); 2 | var Accessory = require('./Accessory.js').Accessory; 3 | 4 | module.exports = { 5 | Homebridge: Homebridge 6 | }; 7 | 8 | /* 9 | * Homebridges -> Homebridge -> Accessory -> Service -> Characteristic 10 | */ 11 | 12 | function Homebridge(devices, context) { 13 | //console.log("Homebridge", devices); 14 | this.accessories = []; 15 | this.host = devices.ipAddress; 16 | this.port = devices.instance.port; 17 | this.deviceID = devices.instance.deviceID; 18 | this.hb_name = this.findHomebridgeName(devices); 19 | devices.accessories.accessories.forEach(function(element) { 20 | var accessory = new Accessory(element, this); 21 | if (this.accessories[accessory.name]) { 22 | debug("Duplicate", accessory.name); 23 | } else { 24 | // debug("Adding", accessory.name) 25 | this.accessories[accessory.name] = accessory; 26 | } 27 | }.bind(this)); 28 | } 29 | 30 | Homebridge.prototype.findHomebridgeName = function(devices) { 31 | var found_hb = false; 32 | var hb_name = null; 33 | for (accessory of devices.accessories.accessories) { 34 | for (service of accessory.services) { 35 | if (parseInt(service.type,16).toString(16).toUpperCase() == "3E") { 36 | // Accessory Information 37 | for (characteristic of service.characteristics) { 38 | if (characteristic.description) { 39 | var key = characteristic.description.replace(/ /g, '').replace(/\./g, '_'); 40 | if (key == "Model") { 41 | if (characteristic.value == "homebridge") { 42 | found_hb = true; 43 | } 44 | } 45 | if (key == "Name") { 46 | hb_name = characteristic.value; 47 | } 48 | } 49 | } 50 | } 51 | 52 | if (found_hb) { 53 | break; 54 | } 55 | } 56 | 57 | if (found_hb) { 58 | break; 59 | } 60 | } 61 | 62 | if (hb_name) { 63 | return hb_name; 64 | } else { 65 | return "unknown_hb"; 66 | } 67 | } 68 | 69 | Homebridge.prototype.getDevicesAndCapabilities = function() { 70 | var list = []; 71 | 72 | // Alice devices made up of multiple homekit accessories in a single homebridge instance 73 | for (var index in this.accessories) { 74 | var accessory = this.accessories[index]; 75 | var accessory_data = accessory.getDeviceCapabilities(); 76 | 77 | if (accessory_data) { 78 | list = list.concat(accessory_data); 79 | } 80 | } 81 | return (list); 82 | }; 83 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var AliceLocal = require('./lib/aliceLocal.js').aliceLocal; 4 | var aliceActions = require('./lib/aliceActions.js'); 5 | var EventEmitter = require('events').EventEmitter; 6 | // var debug = require('debug')('alicePlugin'); 7 | 8 | const packageConfig = require('./package.json'); 9 | 10 | var options = {}; 11 | 12 | module.exports = function(homebridge) { 13 | homebridge.registerPlatform("homebridge-g-on-alice", "G-On Alice", aliceHome); 14 | }; 15 | 16 | function aliceHome(log, config, api) { 17 | this.log = log; 18 | this.eventBus = new EventEmitter(); 19 | this.config = config; 20 | this.pin = config['pin'] || "031-45-154"; 21 | this.username = config['username'] || false; 22 | this.password = config['password'] || false; 23 | this.notifies = config['notifies'] || false; 24 | 25 | // Enable config based DEBUG logging enable 26 | this.debug = config['debug'] || false; 27 | if (this.debug) { 28 | let debugEnable = require('debug'); 29 | let namespaces = debugEnable.disable(); 30 | 31 | // this.log("DEBUG-1", namespaces); 32 | if (namespaces) { 33 | namespaces = namespaces + ',g-on-alice*'; 34 | } else { 35 | namespaces = 'g-on-alice*'; 36 | } 37 | // this.log("DEBUG-2", namespaces); 38 | debugEnable.enable(namespaces); 39 | } 40 | 41 | if (!this.username || !this.password) { 42 | this.log.error("Missing username and password"); 43 | } 44 | 45 | if (api) { 46 | this.api = api; 47 | this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this)); 48 | } 49 | 50 | this.log.info( 51 | '%s v%s, node %s, homebridge v%s', 52 | packageConfig.name, packageConfig.version, process.version, api.serverVersion 53 | ); 54 | } 55 | 56 | aliceHome.prototype = { 57 | accessories: function(callback) { 58 | this.log("accessories"); 59 | callback(); 60 | } 61 | }; 62 | 63 | aliceHome.prototype.didFinishLaunching = function() { 64 | var host = 'homebridge.g-on.io'; 65 | // var host = 'localhost'; 66 | 67 | options = { 68 | eventBus: this.eventBus, 69 | username: this.username, 70 | password: this.password, 71 | clientId: this.username, 72 | debug: this.debug, 73 | notifies: this.notifies, 74 | log: this.log, 75 | pin: this.pin, 76 | servers: [{ 77 | protocol: 'mqtt', 78 | host: host, 79 | port: 1883 80 | }] 81 | }; 82 | 83 | // Initialize HAP Connections 84 | aliceActions.hapDiscovery(options); 85 | 86 | var alice = new AliceLocal(options); 87 | 88 | // Alice mesages 89 | 90 | this.eventBus.on('hapEvent', aliceActions.aliceEvent.bind(this)); 91 | this.eventBus.on('discovery', aliceActions.aliceDiscovery.bind(this)); 92 | this.eventBus.on('action', aliceActions.aliceAction.bind(this)); 93 | this.eventBus.on('query', aliceActions.aliceQuery.bind(this)); 94 | }; 95 | 96 | aliceHome.prototype.configureAccessory = function(accessory) { 97 | this.log("configureAccessory"); 98 | // callback(); 99 | }; 100 | -------------------------------------------------------------------------------- /lib/aliceLocal.js: -------------------------------------------------------------------------------- 1 | // Local event based client for alice 2 | // 3 | // Generates events for each G-On Alice Skill message 4 | // 5 | 6 | "use strict"; 7 | 8 | var mqtt = require('mqtt'); 9 | var MQTTPattern = require('mqtt-pattern'); 10 | var debug = require('debug')('aliceLocal'); 11 | const packageConfig = require('../package.json'); 12 | var Bottleneck = require("bottleneck"); 13 | 14 | var connection = {}; 15 | var count = 0; 16 | var username; 17 | var limiter; 18 | 19 | module.exports = { 20 | aliceLocal: aliceLocal, 21 | aliceEvent: aliceEvent 22 | }; 23 | 24 | function aliceLocal(options) { 25 | debug("Connecting to Homebridge Smart Home Skill"); 26 | // Throttle event's to match Amazon's Rate API 27 | // Limit events to one every 30 seconds, and keep at most 5 minutes worth 28 | limiter = new Bottleneck({ 29 | maxConcurrent: 1, 30 | highWater: 10, 31 | minTime: 2000, 32 | strategy: Bottleneck.strategy.BLOCK 33 | }); 34 | 35 | limiter.on("dropped", function(dropped) { 36 | options.log("WARNING: Dropped event message, message rate too high."); 37 | }); 38 | 39 | username = options.username; 40 | connection.client = mqtt.connect(options); 41 | // connection.client.setMaxListeners(0); 42 | connection.client.on('connect', function() { 43 | debug('connect', "command/" + options.username + "/#"); 44 | connection.client.removeAllListeners('message'); // This hangs up everyone on the channel 45 | connection.client.subscribe("command/" + options.username + "/discovery"); 46 | connection.client.subscribe("command/" + options.username + "/action"); 47 | connection.client.subscribe("command/" + options.username + "/query"); 48 | connection.client.publish("presence/" + options.username + "/1", JSON.stringify({ 49 | Connected: options.username, 50 | version: packageConfig.version 51 | })); 52 | connection.client.on('message', function(topic, message) { 53 | var msg = {}; 54 | 55 | try { 56 | msg = JSON.parse(message.toString()); 57 | } catch(e) { 58 | debug("JSON message is empty or not valid"); 59 | msg = {}; 60 | } 61 | 62 | var topic_params = MQTTPattern.exec("command/+login/+request", topic); 63 | 64 | if (options.eventBus.listenerCount(topic_params.request) > 0) { 65 | options.eventBus.emit(topic_params.request, msg, function(err, response) { 66 | // TODO: if no message, return error Response 67 | if (response == null) { 68 | if (err) { 69 | debug('Error', err.message); 70 | } else { 71 | debug('Error no response'); 72 | } 73 | } else { 74 | if (err) { 75 | debug('Error, but still emitting response', err.message); 76 | connection.client.publish("response/" + options.username + "/" + topic_params.request, JSON.stringify(response)); 77 | } else { 78 | debug('Emitting', topic_params.request); 79 | connection.client.publish("response/" + options.username + "/" + topic_params.request, JSON.stringify(response)); 80 | } 81 | } 82 | }); 83 | } else { 84 | debug('No listener for', topic_params.request); 85 | } 86 | }); 87 | }); 88 | 89 | connection.client.on('offline', function() { 90 | debug('offline'); 91 | }); 92 | 93 | connection.client.on('reconnect', function() { 94 | count++; 95 | debug('reconnect'); 96 | if (count % 5 === 0) options.log("ERROR: No connection to homebridge.g-on.io. Retrying... please review the README and the Homebridge configuration."); 97 | }); 98 | 99 | connection.client.on('error', function(err) { 100 | debug('error', err); 101 | }); 102 | } 103 | 104 | function aliceEvent(message) { 105 | // connection.client.publish("response/" + username + "/callback", JSON.stringify(message)); 106 | } -------------------------------------------------------------------------------- /lib/parse/Homebridges.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Homebridges'); 2 | var Homebridge = require('./Homebridge.js').Homebridge; 3 | 4 | var os = require('os'); 5 | var ifaces = os.networkInterfaces(); 6 | 7 | module.exports = { 8 | Homebridges: Homebridges 9 | }; 10 | 11 | /* 12 | * Homebridges -> Homebridge -> Accessory -> Service -> Characteristic 13 | */ 14 | 15 | function Homebridges(devices, context) { 16 | //console.log("Homebridges", devices); 17 | this.homebridges = []; 18 | 19 | devices.forEach(function(element) { 20 | var homebridge = new Homebridge(element, context); 21 | this.homebridges.push(homebridge); 22 | }.bind(this)); 23 | } 24 | 25 | Homebridges.prototype.getDevicesAndCapabilities = function(message) { 26 | var list = []; 27 | for (var index in this.homebridges) { 28 | var homebridge = this.homebridges[index]; 29 | list = list.concat(homebridge.getDevicesAndCapabilities()); 30 | } 31 | 32 | var response = { 33 | "payload": { 34 | "devices": list 35 | } 36 | }; 37 | 38 | if (message && message.hasOwnProperty('request_id')) { 39 | response.request_id = message.request_id; 40 | } 41 | return (response); 42 | }; 43 | 44 | 45 | Homebridges.prototype.checkThatDeviceExists = function(device_id) { 46 | // device_id = homebridge-name_accessory-id_service-id 47 | 48 | var homebridge_data = null; 49 | for (var homebridge_index in this.homebridges) { 50 | var homebridge_data = this.homebridges[homebridge_index]; 51 | for (var accessory_index in homebridge_data.accessories) { 52 | var accessory_data = homebridge_data.accessories[accessory_index]; 53 | for (var service_index in accessory_data.services) { 54 | var service_data = accessory_data.services[service_index]; 55 | if (accessory_data.services[service_index].id == device_id) { 56 | // we found the device 57 | return { 58 | homebridge: homebridge_data, 59 | accessory: accessory_data, 60 | service: service_data 61 | }; 62 | } 63 | } 64 | } 65 | } 66 | 67 | // no such HomeBridge Device Id found 68 | return { 69 | error_code: "DEVICE_NOT_FOUND", 70 | error_message: "Homebridge Device with such id is not found" 71 | }; 72 | } 73 | 74 | Homebridges.prototype.getDevicesHBName = function(devices) { 75 | return devices.map(device_data => this.checkThatDeviceExists(device_data.id).homebridge.hb_name) 76 | } 77 | 78 | Homebridges.prototype.checkThatDeviceExistsNoti = function(event_data) { 79 | var homebridge_data = null; 80 | for (var homebridge_index in this.homebridges) { 81 | var homebridge_data = this.homebridges[homebridge_index]; 82 | if (homebridge_data.deviceID == event_data.deviceID) { 83 | for (var accessory_index in homebridge_data.accessories) { 84 | var accessory_data = homebridge_data.accessories[accessory_index]; 85 | for (var service_index in accessory_data.services) { 86 | var service_data = accessory_data.services[service_index]; 87 | for (var characteristic_index in service_data.characteristics) { 88 | var characteristic_data = service_data.characteristics[characteristic_index]; 89 | if (characteristic_data.aid == event_data.aid && characteristic_data.iid == event_data.iid) { 90 | characteristic_data.value = event_data.value; 91 | return { 92 | homebridge: homebridge_data, 93 | accessory: accessory_data, 94 | service: service_data, 95 | characteristic: characteristic_data, 96 | }; 97 | 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | // no such HomeBridge Device Id found 105 | return { 106 | error_code: "DEVICE_NOT_FOUND", 107 | error_message: "Homebridge Device with such id is not found" 108 | }; 109 | } 110 | 111 | Homebridges.prototype.onNotifies = function() { 112 | var list = []; 113 | this.homebridges.forEach(function(homebridge) { 114 | var data = { 115 | "deviceID": homebridge.deviceID, 116 | "characteristics": [] 117 | } 118 | for (var index in homebridge.accessories) { 119 | var accessory = homebridge.accessories[index]; 120 | for (var index in accessory.services) { 121 | var service = accessory.services[index]; 122 | for (var index in service.characteristics) { 123 | var characteristic = service.characteristics[index]; 124 | if (characteristic.capabilities.length || characteristic.properties.length) { 125 | data.characteristics.push({ 126 | "aid": characteristic.aid, 127 | "iid": characteristic.iid, 128 | "ev": true 129 | }) 130 | } 131 | } 132 | } 133 | } 134 | if (data.characteristics.length > 0) { 135 | list.push(data); 136 | } 137 | }); 138 | return list; 139 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **homebridge-g-on-alice** 2 | 3 | Enable Yandex Alice access and control your homebridge controlled devices and accessories. Supports Alice on Smartphone, PC, Yandex Station and other Yandex devices. 4 | 5 | # Features 6 | 7 | * Supports one HomeBridge, running on the same host as this plugin 8 | * Supports the following HomeKit accessory types: Lightbulb, Outlet, Switch, Fan, Thermostat and others. 9 | * This plugin does not have any devices or accessories that are visible from Homekit, and does not need to be added on the Home app. 10 | 11 | # Supported devices 12 | 13 | * Support for Light Bulbs (on/off, brightness, color change and color temperature for white light) 14 | * Support for Switches and outlets (on/off) 15 | * Support for Thermostats (target heating cooling ctate, target and current temperature, target and current humidity), 16 | * Support for Heater/Cooler (on/off, target heating cooling ctate, current temperature, rotation speed) 17 | * Support for Humidifier Dehumidifier (on/off, current humidity, relative humidity dehumidifier or humidifier threshold (only one is available), rotation speed) 18 | * Support for Air Purifier (on/off, rotation speed) 19 | * Support for Door, Window, Window Covering (open/close, target position) 20 | * Support for Garage Door Opener, Lock Mechanism (open/close) 21 | * Support for Buttons (single, double, long press) 22 | * Support for Sensors: Temperature, Humidity, Light, Carbon Dioxide (level and status low battery) 23 | * Support for Air Quality Sensor (PM10 Density, PM2.5 Density, VOC Density, status low battery) 24 | * Support for Binary Sensors: Contact, Leak, Motion, Occupancy, Smoke (level and status low battery) 25 | * Support for Fan (on/off, rotation speed) 26 | * Support for Valve, Faucet (on/off) 27 | * Support for Televisions, Television Speakers, Speakers (on/off, play/pause, mute, volume) 28 | * Support up to 100 accessories 29 | 30 | Alice device names are the same as the homebridge device names. 31 | 32 | This only supports accessories connected via a homebridge plugin, any 'Homekit' accessories are not supported. 33 | 34 | ## HomeKit/Homebridge Devices supported 35 | 36 | ### Native Support 37 | 38 | * Lightbulbs, outlets and switches 39 | * Dimmable lightbulbs, outlets and switches 40 | * Color light bulbs and white light bulbs with color temperature adjustment 41 | * Thermostat 42 | * Heater/Cooler 43 | * Humidifiers 44 | * Air Purifiers 45 | * Doors, windows, curtains 46 | * Buttons 47 | * Televisions, Speakers 48 | 49 | ### Unsupported device types 50 | 51 | * Camera's 52 | * Eve devices 53 | * Security Systems 54 | * Some audio and playback systems 55 | 56 | # Alice Voice Commands 57 | 58 | ## Light bulbs / Switches / Dimmer Switches 59 | 60 | * Алиса, включи *device* 61 | * Алиса, выключи *device* 62 | 63 | * Алиса, установи яркость *device* на минимум 64 | * Алиса, установи яркость *device* на 50% 65 | * Алиса, прибавь яркость *device* 66 | 67 | ## Thermostat's and Heater / Cooler's 68 | 69 | * Алиса, установи температуру *device* на 20 градусов. 70 | * Алиса, переведи *device* в режим охлаждения/нагрева. 71 | 72 | # Installation of homebridge-g-on-alice 73 | 74 | * If you are looking for a basic setup to get this plugin up and running check out this guide (https://homebridge.g-on.io/setup). 75 | 76 | ## Install Plugin 77 | 78 | 2. The setup of homebridge-g-on-alice is similar to other plugins, except it doesn't have any devices in the Home app. 79 | 80 | ``` 81 | sudo npm install -g git+https://github.com/G-On-dev/homebridge-g-on-alice.git 82 | ``` 83 | 84 | ## Create homebridge-g-on-alice account 85 | 86 | 3. An account to link your Yandex Alice to HomeBridge needs to created on this website https://homebridge.g-on.io/. This account will be used when you enable the home skill in the Yandex App on your mobile, and in the configuration of the plugin in homebridge. 87 | 88 | 89 | ## HomeBridge-g-on-alice plugin configuration 90 | 91 | 4. Add the plugin to your config.json. The login and password in the config.json, are the credentials you created earlier for the https://homebridge.g-on.io/ website. This only needs to be completed for one instance of homebridge in your environment, it will discover the accessories connected to your other homebridges automatically. 92 | 93 | ``` 94 | "platforms": [ 95 | { 96 | "platform": "G-On Alice", 97 | "name": "G-On Alice", 98 | "username": "....", 99 | "password": "...." 100 | } 101 | ], 102 | ``` 103 | 104 | ### Required parameters 105 | 106 | * username - Login created for the skill linking website https://homebridge.g-on.io/ 107 | * password - Login created for the skill linking website https://homebridge.g-on.io/ 108 | 109 | ### Optional parameters 110 | 111 | * pin - If you had changed your homebridge pin from the default of "pin": "031-45-154" ie 112 | * Notifies the smart home platform about the changed state of the devices 113 | 114 | ``` 115 | "platforms": [ 116 | { 117 | "platform": "G-On Alice", 118 | "name": "G-On Alice", 119 | "username": "....", 120 | "password": "....", 121 | "pin": "031-45-155" 122 | } 123 | ], 124 | ``` 125 | 126 | * debug - This enables debug logging mode, can be used instead of the command line option ( DEBUG=* homebridge ) 127 | 128 | ``` 129 | "platforms": [ 130 | { 131 | "platform": "G-On Alice", 132 | "name": "G-On Alice", 133 | "username": "....", 134 | "password": "....", 135 | "debug": true 136 | } 137 | ], 138 | ``` 139 | 140 | 141 | ## Initial Testing and confirming configuration 142 | 143 | 5. Start homebridge in DEBUG mode, to ensure configuration of homebridge-g-on-alice is correct. This will need to be executed with your implementations configuration options and as the same user as you are running homebridge. If you are homebridge with an autostart script ie systemd, you will need to stop the autostart temporarily. 144 | 145 | ie 146 | ``` 147 | DEBUG=g-on-alice* homebridge -I 148 | ``` 149 | 150 | 6. Please ensure that homebridge starts without errors. 151 | 152 | 153 | ## Enable Homebridge smarthome skill and link accounts 154 | 155 | 7. In your Yandex app on your phone, please go to "Devices", press "Add device" and search for the "G-On Homebridge" skill, and enable the skill. You will need to Enable and link the skill to the account you created earlier on https://homebridge.g-on.io/ 156 | 157 | ## Discover Devices 158 | 159 | 8. At this point you are ready to have Yandex Alice discover devices. Do it using the phone. 160 | You should see some information about the discovery in the log files. 161 | 162 | In the event you have errors, or no devices returned please review your config. 163 | 164 | 9. Installation is now complete, good luck and enjoy. 165 | 166 | 167 | # Credits 168 | 169 | This particular implementation is forked from original `homebridge-alexa` plugin. 170 | https://github.com/NorthernMan54/homebridge-alexa 171 | 172 | This implementation of Alice plugin would have been impossible without them. 173 | 174 | * NorthernMan54 - for the actual implementation of the `homebridge-alexa` plugin 175 | -------------------------------------------------------------------------------- /lib/parse/Service.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('Service'); 2 | var Characteristic = require('./Characteristic.js').Characteristic; 3 | var messages = require('./messages.js'); 4 | 5 | module.exports = { 6 | Service: Service 7 | }; 8 | 9 | /* 10 | * Homebridges -> Homebridge -> Accessory -> Service -> Characteristic 11 | */ 12 | 13 | function Service(devices, context) { 14 | //console.log("Service", devices); 15 | this.iid = devices.iid; 16 | this.type = parseInt(devices.type,16).toString(16).toUpperCase(); 17 | 18 | this.aid = context.aid; 19 | this.host = context.host; 20 | this.port = context.port; 21 | this.hb_name = context.hb_name; 22 | this.info = context.info; 23 | this.characteristics = []; 24 | this.id = this.hb_name + "_" + this.aid + "_" + this.iid; 25 | devices.characteristics.forEach(function(element) { 26 | var service = new Characteristic(element, this); 27 | if (parseInt(element.type,16).toString(16).toUpperCase() === '23' && element.description === "Name") { 28 | this.name = element.value; 29 | } else { 30 | if (this.characteristics[service.description]) { 31 | // debug("Duplicate", this.name, service.description); 32 | } else { 33 | // debug("Adding", this.name, service.iid, service.description); 34 | this.characteristics[service.description] = service; 35 | } 36 | } 37 | }.bind(this)); 38 | } 39 | 40 | Service.prototype.getDeviceCapabilities = function(context) { 41 | var capabilities = []; 42 | var properties = []; 43 | 44 | if (this.name) { 45 | context.name = this.name; 46 | } 47 | 48 | for (var index in this.characteristics) { 49 | var characteristic = this.characteristics[index]; 50 | if (characteristic.type !== '23') { 51 | var _capabilities = characteristic.capabilities; 52 | if (_capabilities.length) { 53 | var f_1 = _capabilities.findIndex(cap => cap.type == "devices.capabilities.color_setting"); 54 | var f_2 = capabilities.findIndex(cap => cap.type == "devices.capabilities.color_setting"); 55 | if (f_1 !== -1 && f_2 !== -1) { 56 | Object.assign(capabilities[f_2].parameters, _capabilities[f_1].parameters); 57 | } else 58 | capabilities = capabilities.concat(_capabilities); 59 | } 60 | var _properties = characteristic.properties; 61 | if (_properties.length) { 62 | properties = properties.concat(_properties); 63 | } 64 | } 65 | } 66 | 67 | if (capabilities.length > 0 || properties.length > 0) { 68 | var data = { 69 | id: this.id, 70 | name: context.name, 71 | description: this.hb_name + " " + context.name, 72 | type: messages.lookupDeviceType(this.type), 73 | device_info: { 74 | manufacturer: context.manufacturer, 75 | model: context.model 76 | } 77 | }; 78 | if (capabilities.length > 0) data.capabilities = capabilities; 79 | if (properties.length > 0) data.properties = properties; 80 | 81 | return data; 82 | } 83 | }; 84 | Service.prototype.getDeviceState = function(homebridge, deviceId) { 85 | return new Promise((resolve, reject) => { 86 | var device_state = { 87 | id: this.id 88 | }; 89 | var res = []; 90 | 91 | for (var index in this.characteristics) { 92 | var characteristic_data = this.characteristics[index]; 93 | if (characteristic_data.type !== '23' && characteristic_data.capabilities) { 94 | var pr = new Promise((resolve, reject) => { 95 | var chd = characteristic_data; 96 | homebridge.HAPstatusByDeviceID(deviceId, "?id=" + characteristic_data.aid + "." + characteristic_data.iid, function(err, response) { 97 | if (err) reject() 98 | else { 99 | var characteristic_data = chd; 100 | var capts = []; 101 | for (var i = 0; i < characteristic_data.capabilities.length; i++) { 102 | var capability_data = characteristic_data.capabilities[i]; 103 | characteristic_data = Object.assign(characteristic_data, response.characteristics[0]); 104 | var converted_capability_state = messages.convertHomeBridgeValueToAliceValue(capability_data, characteristic_data); 105 | if (converted_capability_state.error_code) 106 | reject() 107 | else 108 | capts.push(converted_capability_state); 109 | } 110 | for (var i = 0; i < characteristic_data.properties.length; i++) { 111 | var propertie_data = characteristic_data.properties[i]; 112 | characteristic_data = Object.assign(characteristic_data, response.characteristics[0]); 113 | var converted_capability_state = messages.convertHomeBridgeValueToAliceValue(propertie_data, characteristic_data); 114 | if (converted_capability_state.error_code) { 115 | reject() 116 | } else { 117 | capts.push(converted_capability_state); 118 | } 119 | } 120 | resolve(capts); 121 | } 122 | }); 123 | }); 124 | res.push(pr); 125 | } 126 | } 127 | 128 | Promise.all(res).then((data) => { 129 | var dataF = data.flat(); 130 | var dataHSV = dataF.map((element, index) => element.type === "devices.capabilities.color_setting" && element.state.instance && element.state.instance == "hsv" ? index : -1).filter(element => element !== -1); 131 | if (dataHSV.length > 1) { 132 | for (var i = 1; i < dataHSV.length; i++) { 133 | Object.assign(dataF[dataHSV[0]].state.value, dataF[dataHSV[i]].state.value); 134 | dataF.splice(dataHSV[i], 1); 135 | } 136 | } else if (dataHSV.length == 1) { 137 | if (!dataF[dataHSV[0]].state.value.hasOwnProperty("h")) dataF[dataHSV[0]].state.value.h = 0; 138 | if (!dataF[dataHSV[0]].state.value.hasOwnProperty("s")) dataF[dataHSV[0]].state.value.s = 100; 139 | if (!dataF[dataHSV[0]].state.value.hasOwnProperty("v")) dataF[dataHSV[0]].state.value.v = 100; 140 | } 141 | for (var i = 0; i < dataF.length; i++) { 142 | var d = dataF[i]; 143 | if (d.type === "devices.properties.float" || d.type === "devices.properties.event") { 144 | if (!device_state.hasOwnProperty("properties")) device_state.properties = []; 145 | device_state.properties.push(d); 146 | } else { 147 | if (!device_state.hasOwnProperty("capabilities")) device_state.capabilities = []; 148 | device_state.capabilities.push(d); 149 | } 150 | 151 | } 152 | resolve(device_state); 153 | }).catch(error => { 154 | resolve({ 155 | id: this.id, 156 | error_code: "INTERNAL_ERROR", 157 | error_message: "Requested capability is not found for requested Homebridge Device" 158 | }); 159 | }); 160 | }) 161 | } 162 | Service.prototype.getCharacteristicIidAndValueFromCapability = function(request_capability_data) { 163 | var data = []; 164 | for (var index in this.characteristics) { 165 | var characteristic_data = this.characteristics[index]; 166 | if (characteristic_data.type !== '23' && characteristic_data.capabilities) { 167 | for (var i = 0; i < characteristic_data.capabilities.length; i++) { 168 | var service_capability = characteristic_data.capabilities[i]; 169 | if (service_capability.type == request_capability_data.type) { 170 | if (request_capability_data.state && request_capability_data.state.instance && service_capability.parameters && service_capability.parameters.instance && service_capability.parameters.instance !== request_capability_data.state.instance) 171 | continue; 172 | if (service_capability.type == "devices.capabilities.color_setting") { 173 | if ((characteristic_data.description == 'Hue' || characteristic_data.description == 'Saturation') && request_capability_data.state.instance != 'hsv') continue; 174 | if (characteristic_data.description == 'Color Temperature' && request_capability_data.state.instance != 'temperature_k') continue; 175 | } 176 | // we found request capability 177 | var converted_value = messages.convertAliceValueToHomeBridgeValue(characteristic_data, request_capability_data); 178 | if (converted_value.error_code) { 179 | data.push(converted_value); 180 | } else { 181 | data.push({ 182 | aid: characteristic_data.aid, 183 | iid: characteristic_data.iid, 184 | value: converted_value.value, 185 | }); 186 | } 187 | } 188 | } 189 | } 190 | } 191 | if (data.length) return data; 192 | 193 | // no such capability found 194 | return { 195 | error_code: "INVALID_ACTION", 196 | error_message: "Requested capability is not found for requested Homebridge Device" 197 | }; 198 | } 199 | -------------------------------------------------------------------------------- /lib/aliceActions.js: -------------------------------------------------------------------------------- 1 | var HAPNodeJSClient = require('hap-node-client').HAPNodeJSClient; 2 | var Homebridges = require('./parse/Homebridges.js').Homebridges; 3 | var debug = require('debug')('aliceActions'); 4 | var aliceLocal = require('./aliceLocal.js'); 5 | var messages = require('./parse/messages.js'); 6 | 7 | var homebridge; 8 | var notiData = []; 9 | var notiDataTMR; 10 | var notiOption; 11 | 12 | module.exports = { 13 | aliceDiscovery: aliceDiscovery, 14 | aliceEvent: aliceEvent, 15 | aliceAction: aliceAction, 16 | aliceQuery: aliceQuery, 17 | registerNotifies: registerNotifies, 18 | hapDiscovery: hapDiscovery 19 | }; 20 | 21 | function hapDiscovery(options) { 22 | homebridge = new HAPNodeJSClient(options); 23 | notiOption = options.notifies; 24 | 25 | homebridge.on('Ready', function() { 26 | aliceDiscovery.call(options, null, function() { 27 | // debug("options", options); 28 | }); 29 | }); 30 | homebridge.on('hapEvent', function(event) { 31 | options.eventBus.emit('hapEvent', event); 32 | }); 33 | } 34 | 35 | function aliceDiscovery(message, callback) { 36 | // debug('aliceDiscovery', this); 37 | homebridge.HAPaccessories(function(endPoints) { 38 | debug("aliceDiscovery"); 39 | var response; 40 | var hbDevices = new Homebridges(endPoints, this); 41 | response = hbDevices.getDevicesAndCapabilities(message); 42 | debug("response", response); 43 | 44 | // debug("RESPONSE", JSON.stringify(response)); 45 | if (response && response.payload.devices.length < 1) { 46 | this.log("ERROR: HAP Discovery failed, please review config"); 47 | } else { 48 | this.log("aliceDiscovery - returned %s devices", response.payload.devices.length); 49 | } 50 | if (notiOption) { 51 | var dataNotifies = hbDevices.onNotifies(); 52 | for (var i = 0; i < dataNotifies.length; i++) { 53 | registerNotifies(dataNotifies[i].deviceID, '{"characteristics":' + JSON.stringify(dataNotifies[i].characteristics) + '}') 54 | } 55 | for (var i = 0; i < response.payload.devices.length; i++) { 56 | var device_data = response.payload.devices[i]; 57 | if (device_data.hasOwnProperty("capabilities")) 58 | for (var j = 0; j < device_data.capabilities.length; j++) device_data.capabilities[j].reportable = true; 59 | if (device_data.hasOwnProperty("properties")) 60 | for (var j = 0; j < device_data.properties.length; j++) device_data.properties[j].reportable = true; 61 | } 62 | } 63 | // debug("Discovery Response", JSON.stringify(response, null, 4)); 64 | callback(null, response); 65 | }.bind(this)); 66 | } 67 | 68 | function aliceAction(message, callback) { 69 | var response = { 70 | payload: { 71 | devices : [] 72 | } 73 | }; 74 | 75 | if (message && message.hasOwnProperty('request_id')) { 76 | response.request_id = message.request_id; 77 | } 78 | 79 | var command_body = { 80 | "characteristics": [] 81 | }; 82 | 83 | var command_params = {}; 84 | 85 | try { 86 | if (message.payload.devices.length == 0) { 87 | throw new Error('device array is empty'); 88 | } 89 | homebridge.HAPaccessories(function(endPoints) { 90 | var hbDevices = new Homebridges(endPoints, this); 91 | 92 | for(var i = 0; i < message.payload.devices.length; i++) { 93 | var device_data = message.payload.devices[i]; 94 | var dev_status = hbDevices.checkThatDeviceExists(device_data.id); 95 | 96 | if (dev_status.error_code) { 97 | this.log(dev_status.error_message, device_data); 98 | response.payload.devices.push({ 99 | id: device_data.id, 100 | error_code: dev_status.error_code, 101 | error_message: dev_status.error_message 102 | }); 103 | continue; 104 | } 105 | 106 | var response_device_body = { 107 | id: device_data.id, 108 | capabilities: [] 109 | }; 110 | 111 | if (!command_params.host || !command_params.port) { 112 | command_params.host = dev_status.homebridge.host; 113 | command_params.port = dev_status.homebridge.port; 114 | } 115 | 116 | if (device_data.capabilities.length == 0) { 117 | throw new Error('device id ' + + ' capabilities array is empty'); 118 | } 119 | 120 | for(var j = 0; j < device_data.capabilities.length; j++) { 121 | var capability_data = device_data.capabilities[j]; 122 | var cap_status_arr = dev_status.service.getCharacteristicIidAndValueFromCapability(capability_data); 123 | 124 | for (var k = 0; k < cap_status_arr.length; k++) { 125 | cap_status = cap_status_arr[k]; 126 | if (cap_status.error_code) { 127 | // something went wrong 128 | this.log(cap_status.error_message, capability_data.type); 129 | response_device_body.capabilities.push({ 130 | type: capability_data.type, 131 | state: { 132 | instance: capability_data.state.instance, 133 | action_result: { 134 | status: "ERROR", 135 | error_code: cap_status.error_code, 136 | error_message: cap_status.error_message 137 | } 138 | } 139 | }); 140 | continue; 141 | } 142 | 143 | command_body.characteristics.push({ 144 | "aid": cap_status.aid, 145 | "iid": cap_status.iid, 146 | "value": cap_status.value 147 | }); 148 | } 149 | 150 | response_device_body.capabilities.push({ 151 | type: capability_data.type, 152 | state: { 153 | instance: capability_data.state.instance, 154 | action_result: { 155 | status: "DONE" 156 | } 157 | } 158 | }); 159 | } 160 | 161 | response.payload.devices.push(response_device_body); 162 | } 163 | 164 | if(command_body.characteristics.length < 1) { 165 | // no valid device commands were found 166 | callback(null,response); 167 | return; 168 | } 169 | 170 | var devicesHBName = hbDevices.getDevicesHBName(message.payload.devices); 171 | var currentEndpoint = endPoints.find(function (endpoint) { 172 | // Compare endpoint homebridge name and current hb_name 173 | return devicesHBName.includes(endpoint.instance.name) 174 | }) 175 | 176 | if (!currentEndpoint) { 177 | this.log("Cannot find the current endpoint") 178 | callback(null,response); 179 | return; 180 | } 181 | 182 | homebridge.HAPcontrolByDeviceID(currentEndpoint.instance.deviceID, JSON.stringify(command_body), function(err, status) { 183 | this.log("Action", currentEndpoint.instance.deviceID, JSON.stringify(command_body), status, err); 184 | if (err) { 185 | for(var i = 0; i < response.payload.devices.length; i++) { 186 | for(var j = 0; j < response.payload.devices[i].capabilities.length; j++) { 187 | var capability_response = response.payload.devices[i].capabilities[j]; 188 | if (capability_response.state.action_result.status == "DONE") { 189 | capability_response.state.action_result = { 190 | status: "ERROR", 191 | error_code: "INTERNAL_ERROR", 192 | error_message: err.message 193 | }; 194 | } 195 | } 196 | } 197 | } 198 | 199 | callback(null,response); 200 | }.bind(this)); 201 | }.bind(this)); 202 | } catch(e) { 203 | // probably JSON does not have those fields. 204 | this.log("error with action JSON data", e.message); 205 | callback(e, { 206 | "payload": { 207 | "error_code": 400, // bad request data 208 | "error_message": e.message 209 | } 210 | }); 211 | } 212 | } 213 | 214 | 215 | function aliceQuery(message, callback) { 216 | var response = { 217 | payload: { 218 | devices : [] 219 | } 220 | }; 221 | 222 | if (message && message.hasOwnProperty('request_id')) { 223 | response.request_id = message.request_id; 224 | } 225 | try { 226 | if (message.devices.length == 0) { 227 | throw new Error('device array is empty'); 228 | } 229 | homebridge.HAPaccessories(function(endPoints) { 230 | var hbDevices = new Homebridges(endPoints, this); 231 | var devicesHBName = hbDevices.getDevicesHBName(message.devices); 232 | var currentEndpoint = endPoints.find(function(endpoint) { 233 | // Compare endpoint homebridge name and current hb_name 234 | return devicesHBName.includes(endpoint.instance.name) 235 | }) 236 | 237 | for (var i = 0; i < message.devices.length; i++) { 238 | var device_data = message.devices[i]; 239 | var dev_status = hbDevices.checkThatDeviceExists(device_data.id); 240 | 241 | if (dev_status.error_code) { 242 | debug(dev_status.error_message, device_data); 243 | response.payload.devices.push({ 244 | id: device_data.id, 245 | error_code: dev_status.error_code, 246 | error_message: dev_status.error_message 247 | }); 248 | continue; 249 | } 250 | 251 | response.payload.devices.push(dev_status.service.getDeviceState(homebridge, currentEndpoint.instance.deviceID)); 252 | } 253 | 254 | Promise.all(response.payload.devices).then((result) => { 255 | response.payload.devices = result; 256 | callback(null, response); 257 | }); 258 | }.bind(this)); 259 | } catch(e) { 260 | // probably JSON does not have those fields. 261 | debug("error with action JSON data", e.message); 262 | callback(e, { 263 | "payload": { 264 | "error_code": 400, // bad request data 265 | "error_message": e.message 266 | } 267 | }); 268 | } 269 | } 270 | 271 | function aliceEvent(message, callback) { 272 | homebridge.HAPaccessories(function(endPoints) { 273 | var hbDevices = new Homebridges(endPoints, this); 274 | var response = { 275 | payload: {} 276 | }; 277 | 278 | for (var i = 0; i < message.length; i++) { 279 | var device_data = {}; 280 | if (!message[i].hasOwnProperty("value")) continue; 281 | var dev_status = hbDevices.checkThatDeviceExistsNoti(message[i]); 282 | if (dev_status.error_code) { 283 | continue; 284 | } 285 | 286 | device_data.id = dev_status.service.id; 287 | for (var j = 0; j < dev_status.characteristic.capabilities.length; j++) { 288 | var capability_data = dev_status.characteristic.capabilities[j]; 289 | var converted_capability_state = messages.convertHomeBridgeValueToAliceValue(capability_data, dev_status.characteristic); 290 | if (converted_capability_state.error_code) continue; 291 | if (!device_data.hasOwnProperty("capabilities")) device_data.capabilities = []; 292 | device_data.capabilities.push(converted_capability_state); 293 | } 294 | 295 | for (var j = 0; j < dev_status.characteristic.properties.length; j++) { 296 | var propertie_data = dev_status.characteristic.properties[j]; 297 | var converted_capability_state = messages.convertHomeBridgeValueToAliceValue(propertie_data, dev_status.characteristic); 298 | if (converted_capability_state.error_code) continue; 299 | if (!device_data.hasOwnProperty("properties")) device_data.properties = []; 300 | device_data.properties.push(converted_capability_state); 301 | } 302 | 303 | if (notiData.length) { 304 | var notiDataIndex = notiData.findIndex(data => data.id == device_data.id); 305 | 306 | if (notiDataIndex !== -1) { 307 | if (device_data.hasOwnProperty("capabilities")) { 308 | for (var j = 0; j < device_data.capabilities.length; j++) { 309 | var capability_data = device_data.capabilities[j]; 310 | if (notiData[notiDataIndex].hasOwnProperty("capabilities") && notiData[notiDataIndex].capabilities.length) { 311 | var capability_index = notiData[notiDataIndex].capabilities.findIndex(data => data.type === capability_data.type && data.state.instance == capability_data.state.instance); 312 | if (capability_index !== -1) notiData[notiDataIndex].capabilities.splice(capability_index, 1); 313 | } else notiData[notiDataIndex].capabilities = []; 314 | notiData[notiDataIndex].capabilities.push(capability_data); 315 | } 316 | } 317 | 318 | if (device_data.hasOwnProperty("properties")) { 319 | for (var j = 0; j < device_data.properties.length; j++) { 320 | var propertie_data = device_data.properties[j]; 321 | if (notiData[notiDataIndex].hasOwnProperty("properties") && notiData[notiDataIndex].properties.length) { 322 | var propertie_index = notiData[notiDataIndex].properties.findIndex(data => data.type === propertie_data.type && data.state.instance == propertie_data.state.instance); 323 | if (propertie_index !== -1) notiData[notiDataIndex].properties.splice(propertie_index, 1); 324 | } else notiData[notiDataIndex].properties = []; 325 | notiData[notiDataIndex].properties.push(propertie_data); 326 | } 327 | } 328 | } else notiData.push(device_data); 329 | } else notiData.push(device_data); 330 | } 331 | 332 | if (!notiDataTMR && notiData.length) { 333 | response.payload.devices = notiData; 334 | aliceLocal.aliceEvent(response); 335 | notiData = []; 336 | notiDataTMR = setTimeout(function() { 337 | if (notiData.length) { 338 | response.payload.devices = notiData; 339 | aliceLocal.aliceEvent(response); 340 | notiData = []; 341 | } 342 | notiDataTMR = false; 343 | }, 2 * 1000); 344 | } 345 | }.bind(this)); 346 | } 347 | 348 | function registerNotifies(deviceID, data) { 349 | homebridge.HAPeventByDeviceID(deviceID, data, function(err, status) {}); 350 | } -------------------------------------------------------------------------------- /lib/parse/messages.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('messages'); 2 | 3 | module.exports = { 4 | lookupCapabilities: lookupCapabilities, 5 | lookupProperties: lookupProperties, 6 | lookupDeviceType: lookupDeviceType, 7 | convertAliceValueToHomeBridgeValue: convertAliceValueToHomeBridgeValue, 8 | convertHomeBridgeValueToAliceValue: convertHomeBridgeValueToAliceValue 9 | }; 10 | 11 | function lookupCapabilities(characteristic_data) { 12 | var response = []; 13 | 14 | switch (characteristic_data.description) { 15 | case "Active": // Air Purifier, Fanv2, Faucet, Heater Cooler, Humidifier Dehumidifier, Valve, Television, Television Speaker, Speaker 16 | if (characteristic_data.serviceType != "41" && characteristic_data.serviceType != "45" && characteristic_data.serviceType != "81" && characteristic_data.serviceType != "8B" && characteristic_data.serviceType != "8C" && characteristic_data.serviceType != "4A") { 17 | response.push({ 18 | "type": "devices.capabilities.on_off", 19 | "retrievable": true 20 | }); 21 | } 22 | break; 23 | case "On": // Fan, Lightbulb, Outlet, Switch 24 | case "Target Door State": // Garage Door Opener 25 | case "Lock Target State": // Lock Mechanism 26 | response.push({ 27 | "type": "devices.capabilities.on_off", 28 | "retrievable": true 29 | }); 30 | break; 31 | case "Rotation Speed": // Air Purifier, Fan, Fanv2, Heater Cooler, Humidifier Dehumidifier, 32 | response.push({ 33 | "type": "devices.capabilities.mode", 34 | "retrievable": true, 35 | "parameters": { 36 | "instance": "fan_speed", 37 | "modes": [{ 38 | "value": "high" 39 | }, { 40 | "value": "medium" 41 | }, { 42 | "value": "low" 43 | }] 44 | } 45 | }); 46 | break; 47 | case "Target Position": // Door, Window, Window Covering 48 | response.push({ 49 | "type": "devices.capabilities.on_off", 50 | "retrievable": true 51 | }, { 52 | "type": "devices.capabilities.range", 53 | "retrievable": true, 54 | "parameters": { 55 | "instance": "open", 56 | "unit": "unit.percent", 57 | "range": { 58 | "min": 0, 59 | "max": 100, 60 | "precision": 10 61 | } 62 | } 63 | }); 64 | break; 65 | case "Target Heater-Cooler State": // Heater Cooler 66 | response.push({ 67 | "type": "devices.capabilities.mode", 68 | "retrievable": true, 69 | "parameters": { 70 | "instance": "thermostat", 71 | "modes": [{ 72 | "value": "heat" 73 | }, { 74 | "value": "cool" 75 | }, { 76 | "value": "auto" 77 | }] 78 | } 79 | }); 80 | break; 81 | case "Relative Humidity Dehumidifier Threshold": // Humidifier Dehumidifier 82 | case "Relative Humidity Humidifier Threshold": // Humidifier Dehumidifier 83 | case "Target Relative Humidity": // Thermostat 84 | response.push({ 85 | "type": "devices.capabilities.range", 86 | "retrievable": true, 87 | "parameters": { 88 | "instance": "humidity", 89 | "unit": "unit.percent", 90 | "range": { 91 | "min": 0, 92 | "max": 100, 93 | "precision": 1 94 | } 95 | } 96 | }); 97 | break; 98 | case "Brightness": // Lightbulb 99 | response.push({ 100 | "type": "devices.capabilities.range", 101 | "retrievable": true, 102 | "parameters": { 103 | "instance": "brightness", 104 | "unit": "unit.percent", 105 | "range": { 106 | "min": 0, 107 | "max": 100, 108 | "precision": 1 109 | } 110 | } 111 | }); 112 | break; 113 | case "Color Temperature": // Lightbulb 114 | response.push({ 115 | "type": "devices.capabilities.color_setting", 116 | "retrievable": true, 117 | "parameters": { 118 | "temperature_k": { 119 | "max": characteristic_data.maxValue ? characteristic_data.maxValue * 10 : 6500, 120 | "min": characteristic_data.minValue ? characteristic_data.minValue * 10 : 2700 121 | } 122 | } 123 | }); 124 | break; 125 | case "Hue": // Lightbulb 126 | case "Saturation": // Lightbulb 127 | response.push({ 128 | "type": "devices.capabilities.color_setting", 129 | "retrievable": true, 130 | "parameters": { 131 | "color_model": "hsv" 132 | } 133 | }); 134 | break; 135 | case "Target Heating Cooling State": // Thermostat 136 | if (!characteristic_data.validValues) { 137 | // allowed modes are not specified, enable all 138 | response.push({ 139 | "type": "devices.capabilities.on_off", 140 | "retrievable": true 141 | }); 142 | response.push({ 143 | "type": "devices.capabilities.mode", 144 | "retrievable": true, 145 | "parameters": { 146 | "instance": "thermostat", 147 | "modes": [{ 148 | "value": "heat" 149 | }, { 150 | "value": "cool" 151 | }, { 152 | "value": "auto" 153 | }], 154 | "ordered": false 155 | } 156 | }); 157 | } else { 158 | var modes = []; 159 | for (var i = 0; i < characteristic_data.validValues.length; i++) { 160 | switch (characteristic_data.validValues[i]) { 161 | case 0: 162 | //off 163 | response.push({ 164 | "type": "devices.capabilities.on_off", 165 | "retrievable": true 166 | }); 167 | break; 168 | case 1: 169 | modes.push({ 170 | "value": "heat" 171 | }); 172 | break; 173 | case 2: 174 | modes.push({ 175 | "value": "cool" 176 | }); 177 | break; 178 | case 3: 179 | modes.push({ 180 | "value": "auto" 181 | }); 182 | break; 183 | } 184 | } 185 | if (modes.length) { 186 | response.push({ 187 | "type": "devices.capabilities.mode", 188 | "retrievable": true, 189 | "parameters": { 190 | "instance": "thermostat", 191 | "modes": modes, 192 | "ordered": false 193 | } 194 | }); 195 | } 196 | } 197 | break; 198 | case "Target Temperature": // Thermostat 199 | response.push({ 200 | "type": "devices.capabilities.range", 201 | "retrievable": true, 202 | "parameters": { 203 | "instance": "temperature", 204 | "unit": "unit.temperature.celsius", 205 | "range": { 206 | "min": characteristic_data.hasOwnProperty('minValue') ? characteristic_data.minValue : 10, 207 | "max": characteristic_data.hasOwnProperty('maxValue') ? characteristic_data.maxValue : 38, 208 | "precision": characteristic_data.hasOwnProperty('minStep') ? characteristic_data.minStep : 1, 209 | } 210 | } 211 | }); 212 | break; 213 | case "Target Media State": // Television, Target Media State 214 | response.push({ 215 | "type": "devices.capabilities.toggle", 216 | "retrievable": true, 217 | "parameters": { 218 | "instance": "pause" 219 | } 220 | }); 221 | break; 222 | case "Mute": // Smart Speaker, Speaker, Television Speaker 223 | response.push({ 224 | "type": "devices.capabilities.toggle", 225 | "retrievable": true, 226 | "parameters": { 227 | "instance": "mute" 228 | } 229 | }); 230 | break; 231 | case "Volume": // Smart Speaker, Speaker, Television Speaker 232 | response.push({ 233 | "type": "devices.capabilities.range", 234 | "retrievable": true, 235 | "parameters": { 236 | "instance": "volume", 237 | "unit": "unit.percent", 238 | "range": { 239 | "min": characteristic_data.hasOwnProperty('minValue') ? characteristic_data.minValue : 0, 240 | "max": characteristic_data.hasOwnProperty('maxValue') ? characteristic_data.maxValue : 100, 241 | "precision": characteristic_data.hasOwnProperty('minStep') ? characteristic_data.minStep : 1, 242 | } 243 | } 244 | }); 245 | break; 246 | default: 247 | // unsupported characteristic_data.description 248 | break; 249 | } 250 | return response; 251 | } 252 | 253 | function lookupProperties(characteristic_data) { 254 | var response = []; 255 | switch (characteristic_data.description) { 256 | // float 257 | case "Current Temperature": // Heater Cooler, Temperature Sensor, Thermostat 258 | response.push({ 259 | "type": "devices.properties.float", 260 | "retrievable": true, 261 | "parameters": { 262 | "instance": "temperature", 263 | "unit": "unit.temperature.celsius" 264 | } 265 | }); 266 | break; 267 | case "Current Relative Humidity": // Humidifier Dehumidifier, Humidity Sensor, Thermostat 268 | response.push({ 269 | "type": "devices.properties.float", 270 | "retrievable": true, 271 | "parameters": { 272 | "instance": "humidity", 273 | "unit": "unit.percent" 274 | } 275 | }); 276 | break; 277 | case "PM10 Density": // Air Quality Sensor 278 | response.push({ 279 | "type": "devices.properties.float", 280 | "retrievable": true, 281 | "parameters": { 282 | "instance": "pm10_density", 283 | "unit": "unit.density.mcg_m3" 284 | } 285 | }); 286 | break; 287 | case "PM2.5 Density": // Air Quality Sensor 288 | response.push({ 289 | "type": "devices.properties.float", 290 | "retrievable": true, 291 | "parameters": { 292 | "instance": "pm2.5_density", 293 | "unit": "unit.density.mcg_m3" 294 | } 295 | }); 296 | break; 297 | case "VOC Density": // Air Quality Sensor 298 | response.push({ 299 | "type": "devices.properties.float", 300 | "retrievable": true, 301 | "parameters": { 302 | "instance": "tvoc", 303 | "unit": "unit.density.mcg_m3" 304 | } 305 | }); 306 | break; 307 | case "Carbon Dioxide Level": // Carbon Dioxide Sensor 308 | response.push({ 309 | "type": "devices.properties.float", 310 | "retrievable": true, 311 | "parameters": { 312 | "instance": "co2_level", 313 | "unit": "unit.ppm" 314 | } 315 | }); 316 | break; 317 | case "Current Ambient Light Level": // Light Sensor 318 | response.push({ 319 | "type": "devices.properties.float", 320 | "retrievable": true, 321 | "parameters": { 322 | "instance": "illumination", 323 | "unit": "unit.illumination.lux" 324 | } 325 | }); 326 | break; 327 | // binary 328 | case "Contact Sensor State": // Contact Sensor 329 | response.push({ 330 | "type": "devices.properties.event", 331 | "retrievable": true, 332 | "parameters": { 333 | "instance": "open", 334 | "events": [{ 335 | "value": "closed" 336 | }, { 337 | "value": "opened" 338 | }] 339 | } 340 | }); 341 | break; 342 | case "Leak Detected": // Leak Sensor 343 | response.push({ 344 | "type": "devices.properties.event", 345 | "retrievable": true, 346 | "parameters": { 347 | "instance": "water_leak", 348 | "events": [{ 349 | "value": "dry" 350 | }, { 351 | "value": "leak" 352 | }] 353 | } 354 | }); 355 | break; 356 | case "Motion Detected": // Motion Sensor 357 | case "Occupancy Detected": // Occupancy Sensor 358 | response.push({ 359 | "type": "devices.properties.event", 360 | "retrievable": true, 361 | "parameters": { 362 | "instance": "motion", 363 | "events": [{ 364 | "value": "not_detected" 365 | }, { 366 | "value": "detected" 367 | }] 368 | } 369 | }); 370 | break; 371 | case "Smoke Detected": // Smoke Sensor 372 | response.push({ 373 | "type": "devices.properties.event", 374 | "retrievable": true, 375 | "parameters": { 376 | "instance": "smoke", 377 | "events": [{ 378 | "value": "not_detected" 379 | }, { 380 | "value": "detected" 381 | }] 382 | } 383 | }); 384 | break; 385 | case "Programmable Switch Event": // Stateless Programmable Switch 386 | response.push({ 387 | "type": "devices.properties.event", 388 | "retrievable": true, 389 | "parameters": { 390 | "instance": "button", 391 | "events": [{ 392 | "value": "click" 393 | }, { 394 | "value": "double_click" 395 | }, { 396 | "value": "long_press" 397 | }] 398 | } 399 | }); 400 | break; 401 | case "Status Low Battery": 402 | response.push({ 403 | "type": "devices.properties.event", 404 | "retrievable": true, 405 | "parameters": { 406 | "instance": "battery_level", 407 | "events": [{ 408 | "value": "normal" 409 | }, { 410 | "value": "low" 411 | }] 412 | } 413 | }); 414 | break; 415 | default: 416 | // unsupported characteristic_data.description 417 | break; 418 | } 419 | return response; 420 | } 421 | 422 | function lookupDeviceType(service) { 423 | var category; 424 | switch (service.substr(0, 8)) { 425 | case "BB": // Air Purifier 426 | category = "devices.types.purifier"; 427 | break; 428 | case "BC": // Heater Cooler 429 | case "4A": // Thermostat 430 | category = "devices.types.thermostat"; 431 | break; 432 | case "BD": // Humidifier Dehumidifier 433 | category = "devices.types.humidifier"; 434 | break; 435 | case "43": // Lightbulb 436 | category = "devices.types.light"; 437 | break; 438 | case "47": // Outlet 439 | category = "devices.types.socket"; 440 | break; 441 | case "49": // Switch 442 | category = "devices.types.switch"; 443 | break; 444 | case "41": // Garage Door Opener 445 | case "45": // Lock Mechanism 446 | case "81": // Door 447 | case "8B": // Window 448 | category = "devices.types.openable"; 449 | break; 450 | case "8C": // Window Covering 451 | category = "devices.types.openable.curtain"; 452 | break; 453 | case "8A": // Temperature Sensor 454 | case "82": // Humidity Sensor 455 | case "8D": // Air Quality Sensor 456 | case "97": // Carbon Dioxide Sensor 457 | category = "devices.types.sensor.climate"; 458 | break; 459 | case "84": // Light Sensor 460 | category = "devices.types.sensor.illumination"; 461 | break; 462 | case "80": // Contact Sensor 463 | category = "devices.types.sensor.open"; 464 | break; 465 | case "83": // Leak Sensor 466 | category = "devices.types.sensor.water_leak"; 467 | break; 468 | case "85": // Motion Sensor 469 | case "86": // Occupancy Sensor 470 | category = "devices.types.sensor.motion"; 471 | break; 472 | case "87": // Smoke Sensor 473 | category = "devices.types.sensor.smoke"; 474 | break; 475 | case "89": // Stateless Programmable Switch 476 | category = "devices.types.sensor.button"; 477 | break; 478 | case "40": // Fan 479 | case "B7": // Fanv2 480 | case "D7": // Faucet 481 | case "D0": // Valve 482 | category = "devices.types.other"; 483 | break; 484 | case "28": // Smart Speaker 485 | case "13": // Television Speaker, Speaker 486 | category = "devices.types.media_device"; 487 | break; 488 | case "D8": // Television 489 | category = "devices.types.media_device.tv"; 490 | break; 491 | default: 492 | // No mapping exists 493 | // debug("No display category for %s using other", service.substr(0, 8)); 494 | category = "devices.types.other"; 495 | break; 496 | } 497 | return category; 498 | } 499 | 500 | function convertAliceValueToHomeBridgeValue(characteristic_data, request_capability_data) { 501 | switch (request_capability_data.type) { 502 | case "devices.capabilities.on_off": 503 | if (request_capability_data.state.instance == 'on') { 504 | if (characteristic_data.description == 'Target Door State' || characteristic_data.description == 'Lock Target State') { 505 | if (request_capability_data.state.value) { 506 | return { 507 | value: 0 508 | }; 509 | } else { 510 | return { 511 | value: 1 512 | }; 513 | } 514 | } 515 | if (request_capability_data.state.value) { 516 | return { 517 | value: characteristic_data.description == 'Target Position' ? 100 : 1 518 | }; 519 | } else { 520 | return { 521 | value: 0 522 | }; 523 | } 524 | } 525 | break; 526 | 527 | case "devices.capabilities.range": 528 | switch (request_capability_data.state.instance) { 529 | case 'brightness': 530 | case 'humidity': 531 | case 'volume': 532 | case 'open': 533 | if ((request_capability_data.state.value >= 0) || (request_capability_data.state.value <= 100)) { 534 | return { 535 | value: request_capability_data.state.value 536 | }; 537 | } 538 | break; 539 | 540 | case 'temperature': 541 | if ((request_capability_data.state.value >= 10) || (request_capability_data.state.value <= 38)) { 542 | return { 543 | value: request_capability_data.state.value 544 | }; 545 | } 546 | break; 547 | 548 | default: 549 | break; 550 | } 551 | break; 552 | 553 | case "devices.capabilities.mode": 554 | if (request_capability_data.state.instance == 'thermostat') { 555 | switch (request_capability_data.state.value) { 556 | case "auto": 557 | return { 558 | value: 3 559 | }; 560 | case "cool": 561 | return { 562 | value: 2 563 | }; 564 | case "heat": 565 | return { 566 | value: 1 567 | }; 568 | default: 569 | break; 570 | } 571 | } else if (request_capability_data.state.instance == 'fan_speed') { 572 | switch (request_capability_data.state.value) { 573 | case "high": 574 | return { 575 | value: 100 576 | }; 577 | case "medium": 578 | return { 579 | value: 60 580 | }; 581 | case "low": 582 | return { 583 | value: 30 584 | }; 585 | default: 586 | break; 587 | } 588 | } 589 | break; 590 | 591 | case "devices.capabilities.color_setting": 592 | if (request_capability_data.state.instance == 'hsv') { 593 | if (characteristic_data.description == "Hue") { 594 | return { 595 | value: request_capability_data.state.value.h 596 | }; 597 | } else if (characteristic_data.description == "Saturation") { 598 | return { 599 | value: request_capability_data.state.value.s 600 | }; 601 | } 602 | } else if (request_capability_data.state.instance == 'temperature_k') { 603 | return { 604 | value: request_capability_data.state.value / 10 605 | }; 606 | } 607 | break; 608 | 609 | case "devices.capabilities.toggle": 610 | if (request_capability_data.state.value) { 611 | return { 612 | value: 1 613 | }; 614 | } else { 615 | return { 616 | value: 0 617 | }; 618 | } 619 | break; 620 | 621 | default: 622 | break; 623 | } 624 | 625 | return { 626 | error_code: "INVALID_VALUE", 627 | error_message: "Requested value is not valid for requested capability" 628 | }; 629 | } 630 | 631 | function convertHomeBridgeValueToAliceValue(capability_data, characteristic_data) { 632 | var converted_capability_data = { 633 | type: capability_data.type, 634 | state: {} 635 | } 636 | switch (capability_data.type) { 637 | case "devices.capabilities.on_off": 638 | converted_capability_data.state.instance = "on"; 639 | if (characteristic_data.description == 'Target Door State' || characteristic_data.description == 'Lock Target State') { 640 | if (characteristic_data.value) { 641 | converted_capability_data.state.value = false; 642 | } else { 643 | converted_capability_data.state.value = true; 644 | } 645 | return converted_capability_data; 646 | } 647 | if (characteristic_data.value) { 648 | converted_capability_data.state.value = true; 649 | } else { 650 | converted_capability_data.state.value = false; 651 | } 652 | return converted_capability_data; 653 | case "devices.capabilities.toggle": 654 | converted_capability_data.state.instance = capability_data.parameters.instance; 655 | if (characteristic_data.value) { 656 | converted_capability_data.state.value = true; 657 | } else { 658 | converted_capability_data.state.value = false; 659 | } 660 | return converted_capability_data; 661 | case "devices.capabilities.range": 662 | if (capability_data.parameters && capability_data.parameters.instance) { 663 | converted_capability_data.state.instance = capability_data.parameters.instance; 664 | converted_capability_data.state.value = characteristic_data.value; 665 | return converted_capability_data; 666 | } 667 | break; 668 | 669 | case "devices.capabilities.mode": 670 | if (capability_data.parameters && capability_data.parameters.instance) { 671 | converted_capability_data.state.instance = capability_data.parameters.instance; 672 | if (capability_data.parameters.instance == 'thermostat') { 673 | switch (characteristic_data.value) { 674 | case 3: 675 | converted_capability_data.state.value = "auto"; 676 | return converted_capability_data; 677 | 678 | case 2: 679 | converted_capability_data.state.value = "cool"; 680 | return converted_capability_data; 681 | case 1: 682 | converted_capability_data.state.value = "heat"; 683 | return converted_capability_data; 684 | case 0: 685 | converted_capability_data.state.value = capability_data.parameters.modes[0].value; 686 | return converted_capability_data; 687 | default: 688 | break; 689 | } 690 | } else if (capability_data.parameters.instance == 'fan_speed') { 691 | switch (true) { 692 | case (characteristic_data.value > 66): 693 | converted_capability_data.state.value = "high"; 694 | return converted_capability_data; 695 | case (characteristic_data.value < 33): 696 | converted_capability_data.state.value = "low"; 697 | return converted_capability_data; 698 | default: 699 | converted_capability_data.state.value = "medium"; 700 | return converted_capability_data; 701 | break; 702 | } 703 | } 704 | } 705 | break; 706 | case "devices.capabilities.color_setting": 707 | if (capability_data.parameters && capability_data.parameters.color_model) { 708 | converted_capability_data.state.instance = capability_data.parameters.color_model; 709 | converted_capability_data.state.value = {}; 710 | converted_capability_data.state.value.v = 100; 711 | if (characteristic_data.description == "Hue") 712 | converted_capability_data.state.value.h = characteristic_data.value; 713 | else if (characteristic_data.description == "Saturation") 714 | converted_capability_data.state.value.s = characteristic_data.value; 715 | return converted_capability_data; 716 | } else if (capability_data.parameters && capability_data.parameters.temperature_k) { 717 | converted_capability_data.state.instance = "temperature_k"; 718 | converted_capability_data.state.value = characteristic_data.value * 10; 719 | return converted_capability_data; 720 | } 721 | break; 722 | case "devices.properties.float": 723 | if (capability_data.parameters && capability_data.parameters.instance) { 724 | converted_capability_data.state.instance = capability_data.parameters.instance; 725 | converted_capability_data.state.value = characteristic_data.value; 726 | return converted_capability_data; 727 | } 728 | break; 729 | case "devices.properties.event": 730 | if (capability_data.parameters && capability_data.parameters.instance) { 731 | converted_capability_data.state.instance = capability_data.parameters.instance; 732 | if (capability_data.parameters.instance == "open") 733 | converted_capability_data.state.value = characteristic_data.value ? "opened" : "closed"; 734 | else if (capability_data.parameters.instance == "water_leak") 735 | converted_capability_data.state.value = characteristic_data.value ? "leak" : "dry"; 736 | else if (capability_data.parameters.instance == "motion" || capability_data.parameters.instance == "smoke") 737 | converted_capability_data.state.value = characteristic_data.value ? "detected" : "not_detected"; 738 | else if (capability_data.parameters.instance == "battery_level") 739 | converted_capability_data.state.value = characteristic_data.value ? "low" : "normal"; 740 | else if (capability_data.parameters.instance == "button") { 741 | switch (characteristic_data.value) { 742 | case 2: 743 | converted_capability_data.state.value = "long_press"; 744 | return converted_capability_data; 745 | case 1: 746 | converted_capability_data.state.value = "long_press"; 747 | return converted_capability_data; 748 | case 0: 749 | converted_capability_data.state.value = "click"; 750 | return converted_capability_data; 751 | default: 752 | break; 753 | } 754 | } 755 | return converted_capability_data; 756 | } 757 | break; 758 | default: 759 | break; 760 | } 761 | 762 | return { 763 | error_code: "INTERNAL_ERROR", 764 | error_message: "Couldn't convert device capability value for Alice" 765 | }; 766 | } 767 | --------------------------------------------------------------------------------