├── .vscode └── launch.json ├── package.json ├── LICENSE ├── .gitignore ├── test └── eventsource_test.js ├── README.md ├── lib ├── switch.js ├── base.js └── fan.js ├── utils └── esphome_webapi.js └── index.js /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-u", 14 | "tdd", 15 | "--timeout", 16 | "999999", 17 | "--colors", 18 | "${workspaceFolder}/test" 19 | ], 20 | "internalConsoleOptions": "openOnSessionStart" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-esphome", 3 | "version": "0.1.0", 4 | "description": "Homebridge plugin to control ESP8266/ESP32 devices running ESPHome firmware", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/basdelfos/homebridge-esphome.git" 12 | }, 13 | "keywords": [ 14 | "homebridge-plugin", 15 | "iot", 16 | "esphome", 17 | "homebridge" 18 | ], 19 | "author": "Bas Delfos (b.delfos@gmail.com)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/basdelfos/homebridge-esphome/issues" 23 | }, 24 | "homepage": "https://github.com/basdelfos/homebridge-esphome#readme", 25 | "engines": { 26 | "homebridge": ">=0.2.5" 27 | }, 28 | "dependencies": { 29 | "eventsource": "^1.0.7", 30 | "moment": "^2.24.0", 31 | "request": "^2.88.0" 32 | }, 33 | "devDependencies": { 34 | "mocha": "^6.2.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bas Delfos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /test/eventsource_test.js: -------------------------------------------------------------------------------- 1 | var EventSource = require('eventsource'); 2 | 3 | var assert = require('assert'); 4 | var describe = require('mocha').describe; 5 | var it = require('mocha').it; 6 | var before = require('mocha').before; 7 | var after = require('mocha').after; 8 | 9 | this.eventSource; 10 | 11 | describe('EventSource subscription tests', () => { 12 | 13 | before(() => { 14 | this.eventSource = new EventSource('http://192.168.178.35:5180/events'); 15 | }) 16 | 17 | describe('Receive real-time updates for sensor state', () => { 18 | it('should retreive playload(s) with state', (done) => { 19 | 20 | var receivedPayloads = 0; 21 | const expectedNumPayloads = 4; // 2 = only the immidiate response, 4 = wait until receiving the first update 22 | 23 | this.eventSource.addEventListener('state', function (e) { 24 | if (e.data) receivedPayloads++; 25 | if (receivedPayloads == expectedNumPayloads) { 26 | assert.ok('Received all payloads') 27 | done(); 28 | } 29 | }); 30 | }).timeout(90 * 1000); // Timeout after 90 seconds 31 | }); 32 | 33 | after(() => { 34 | // Clean up stuff 35 | this.eventSource.close(); 36 | }); 37 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | !! THIS REPOSITORY IS NOT ACTIVELY MAINTAINED ANYMORE, PLEASE FEEL FREE TO CREATE FORK !! 2 | 3 | # homebridge-esphome 4 | 5 | Homebridge plugin to control ESP8266/ESP32 devices running ESPHome firmware (https://esphome.io) 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm i homebridge-esphome -g 11 | ``` 12 | 13 | ## Basic config.json 14 | 15 | ```javascript 16 | { 17 | "platform": "ESPHomePlatform", 18 | "name": "ESPHomePlatform", 19 | "devices": [ 20 | { 21 | "name": "ESPHome Switch 1", 22 | "host": "hostname", 23 | "type": "switch" 24 | "id": "sonoff_basic_relay" 25 | }, 26 | { 27 | "name": "ESPHome Fan 1", 28 | "host": "hostname", 29 | "type": "fan", 30 | "id": "ventilation" 31 | } 32 | ] 33 | } 34 | ``` 35 | 36 | Each `device` object passed to the `devices` array has these properties: 37 | 38 | - `name` - The name that should appear in HomeKit. 39 | - `host` - The ip-adres or hostname of the device. 40 | - `type` - The type of device. Can be `switch` or `fan`. 41 | - `id` - The id refers to the id of the component - this ID is created by taking the name of the component, stripping out all non-alphanumeric characters, making everything lowercase and replacing all spaces by underscores. 42 | 43 | ### Prerequisites 44 | 45 | The ESPHome firmware on the device has to be compiled with support for the **REST API** as this plugin uses HTTP calls and EventSource Events. 46 | 47 | Info: [ESPHome Web Server API](https://esphome.io/web-api/index.html) 48 | 49 | Compile the ESPHome firmware with the following component enabled: 50 | 51 | ```yaml 52 | # Enable REST api in ESPHome firmware 53 | web_server: 54 | port: 5180 # optional 55 | ``` 56 | 57 | `port:` is optional, defaults to port 80. 58 | 59 | ### Notes 60 | 61 | Type `Fan` in HomeKit is controlled in percentage, whereas in ESPHome `fan` is controlled in steps `off`, `low`, `medium` and `high`. Therefor the plugin converts speed steps in the following percentages. 62 | - `off` = device is showing off in HomeKit. 63 | - `low` = 25 percentage. 64 | - `medium` = 50 percentage. 65 | - `high` = 100 percentage. 66 | 67 | ## TODO 68 | 69 | - Implement more device types. 70 | - More real-life testing. 71 | -------------------------------------------------------------------------------- /lib/switch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const BaseAccessory = require('./base'); 3 | const request = require('request'); 4 | 5 | let Accessory; 6 | let Service; 7 | let Characteristic; 8 | 9 | class SwitchAccessory extends BaseAccessory { 10 | constructor(platform, homebridgeAccessory, deviceConfig) { 11 | 12 | ({ Accessory, Characteristic, Service } = platform.api.hap); 13 | 14 | super(platform, 15 | homebridgeAccessory, 16 | deviceConfig, 17 | Accessory.Categories.SWITCH); 18 | 19 | this.getUrl = `/switch/${this.deviceConfig.id}`; 20 | this.setTurnOnUrl = `/switch/${this.deviceConfig.id}/turn_on`; 21 | this.setTurnOffUrl = `/switch/${this.deviceConfig.id}/turn_off`; 22 | this.setToggleUrl = `/switch/${this.deviceConfig.id}/toggle`; 23 | 24 | // EventSource callbacks 25 | this.espHomeWebApi.stateEvent((obj) => { 26 | this.log.debug('[ES][%s] State event:', this.homebridgeAccessory.displayName, obj); 27 | if (obj.id === `switch-${this.deviceConfig.id}`) { 28 | this.service.getCharacteristic(Characteristic.On).updateValue(obj.value); 29 | this.log.debug('Characteristic.On changed to', obj.value); 30 | } 31 | }); 32 | 33 | // Characteristics 34 | this.service.getCharacteristic(Characteristic.On) 35 | .on('get', (callback) => { 36 | 37 | this.log.debug('[GET][%s] Characteristic.On', this.homebridgeAccessory.displayName); 38 | 39 | // Send GET request 40 | this.espHomeWebApi.sendRequestJson(this.getUrl, '', 'GET', 41 | (response, obj) => { 42 | callback(null, obj.value); 43 | }, 44 | (error) => { 45 | this.log.error('[ERROR][GET][%s] %s', this.homebridgeAccessory.displayName, error); 46 | callback(error); 47 | }); 48 | }) 49 | .on('set', (state, callback) => { 50 | 51 | this.log.debug('[SET][%s] Characteristic.On: %s', this.homebridgeAccessory.displayName, state); 52 | 53 | const url = state ? this.setTurnOnUrl : this.setTurnOffUrl; 54 | 55 | // Send SET request 56 | this.espHomeWebApi.sendRequest(url, '', 'POST', 57 | (response, obj) => { 58 | callback(); 59 | }, 60 | (error) => { 61 | this.log.error('[ERROR][SET][%s] %s', this.homebridgeAccessory.displayName, error); 62 | callback(error); 63 | }); 64 | }); 65 | 66 | } 67 | } 68 | 69 | module.exports = SwitchAccessory; -------------------------------------------------------------------------------- /lib/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EspHomeWebApi = require('../utils/esphome_webapi'); 3 | 4 | let PlatformAccessory; 5 | let Accessory; 6 | let Service; 7 | let Characteristic; 8 | let UUIDGen; 9 | 10 | class BaseAccessory { 11 | constructor(platform, homebridgeAccessory, deviceConfig, categoryType) { 12 | this.platform = platform; 13 | this.log = platform.log; 14 | this.homebridgeAccessory = homebridgeAccessory; 15 | this.deviceConfig = deviceConfig; 16 | 17 | // Create ESPHome Web Api client 18 | this.espHomeWebApi = new EspHomeWebApi( 19 | this.log, 20 | this.deviceConfig.host, 21 | this.deviceConfig.port 22 | ); 23 | 24 | PlatformAccessory = platform.api.platformAccessory; 25 | ({ Accessory, Service, Characteristic, uuid: UUIDGen } = platform.api.hap); 26 | 27 | if (this.homebridgeAccessory) { 28 | if (!this.homebridgeAccessory.context.host) { 29 | this.homebridgeAccessory.context.host = this.deviceConfig.host; 30 | } 31 | 32 | this.log.info( 33 | 'Existing Accessory found [%s] [%s] [%s]', 34 | homebridgeAccessory.displayName, 35 | homebridgeAccessory.context.host, 36 | homebridgeAccessory.UUID 37 | ); 38 | this.homebridgeAccessory.displayName = this.deviceConfig.name; 39 | } 40 | else { 41 | this.log.info('Creating new Accessory %s', this.deviceConfig.name); 42 | this.homebridgeAccessory = new PlatformAccessory( 43 | this.deviceConfig.name, 44 | UUIDGen.generate(this.deviceConfig.name), 45 | categoryType 46 | ); 47 | this.homebridgeAccessory.context.host = this.deviceConfig.host; 48 | platform.registerPlatformAccessory(this.homebridgeAccessory); 49 | } 50 | 51 | let serviceType; 52 | switch (categoryType) { 53 | case Accessory.Categories.SWITCH: 54 | serviceType = Service.Switch; 55 | break; 56 | case Accessory.Categories.FAN: 57 | serviceType = Service.Fan; 58 | break; 59 | default: 60 | serviceType = Service.AccessoryInformation; 61 | } 62 | 63 | this.service = this.homebridgeAccessory.getService(serviceType); 64 | if (this.service) { 65 | this.service.setCharacteristic(Characteristic.Name, this.deviceConfig.name); 66 | } 67 | else { 68 | this.log.debug('Creating new Service %s', this.deviceConfig.name); 69 | this.service = this.homebridgeAccessory.addService(serviceType, this.deviceConfig.name); 70 | } 71 | 72 | this.homebridgeAccessory.on('identify', (paired, callback) => { 73 | this.log.debug('[IDENTIFY][%s]', this.homebridgeAccessory.displayName); 74 | callback(); 75 | }); 76 | } 77 | } 78 | 79 | module.exports = BaseAccessory; 80 | -------------------------------------------------------------------------------- /utils/esphome_webapi.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const EventSource = require('eventsource'); 3 | 4 | class EspHomeWebApi { 5 | constructor(log, host, port) { 6 | this.log = log; 7 | this.host = host; 8 | this.port = port; 9 | this.baseUrl = `http://${this.host}:${this.port}`; 10 | this.eventSource = new EventSource(this.baseUrl + '/events'); 11 | } 12 | 13 | stateEvent(callback) { 14 | this.log.debug('Starting State EventSource listener on', this.eventSource.url); 15 | this.eventSource.addEventListener('state', (e) => { 16 | try { 17 | const obj = JSON.parse(e.data); 18 | callback(obj); 19 | } 20 | catch (error) { 21 | callback(new Error('Could not parse json. ', error)); 22 | } 23 | }); 24 | } 25 | 26 | logEvent(callback) { 27 | this.log.debug('Starting Log EventSource listener on', this.eventSource.url); 28 | this.eventSource.addEventListener('log', (e) => { 29 | try { 30 | const obj = JSON.parse(e.data); 31 | callback(obj); 32 | } 33 | catch (error) { 34 | callback(new Error('Could not parse json. ', error)); 35 | } 36 | }); 37 | } 38 | 39 | pingEvent(callback) { 40 | this.log.debug('Starting Ping EventSource listener on', this.eventSource.url); 41 | this.eventSource.addEventListener('ping', (e) => { 42 | try { 43 | const obj = JSON.parse(e.data); 44 | callback(obj); 45 | } 46 | catch (error) { 47 | callback(new Error('Could not parse json. ', error)); 48 | } 49 | }); 50 | } 51 | 52 | async sendRequest(url, body, method, callbackSuccess, callbackError) { 53 | url = this.baseUrl + url; 54 | this.log.debug('Send %s request [%s]: %s', method, url, body); 55 | request({ 56 | url: url, 57 | body: body, 58 | method: method, 59 | timeout: 5 * 1000, // Device has to respond within 5 seconds 60 | rejectUnauthorized: false 61 | }, 62 | (error, response, body) => { 63 | this.log.debug('Received response: %s', body || '(empty)'); 64 | if (error) { 65 | if (error.code === 'ENOTFOUND') { 66 | callbackError(new Error('Could not connect to host')); 67 | } 68 | else { 69 | callbackError(error); 70 | } 71 | } 72 | else { 73 | callbackSuccess(response, body) 74 | } 75 | }); 76 | } 77 | 78 | sendRequestJson(url, body, method, callbackSuccess, callbackError) { 79 | this.sendRequest(url, body, method, 80 | (response, body) => { 81 | try { 82 | const obj = JSON.parse(body); 83 | callbackSuccess(response, obj); 84 | } 85 | catch (error) { 86 | callbackError(new Error('Could not parse json. ', error)); 87 | } 88 | }, 89 | (error) => { 90 | callbackError(error); 91 | } 92 | ); 93 | } 94 | } 95 | 96 | module.exports = EspHomeWebApi; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const FanAccessory = require('./lib/fan'); 2 | const SwitchAccessory = require('./lib/switch'); 3 | 4 | var Accessory, Service, Characteristic, UUIDGen; 5 | 6 | module.exports = function (homebridge) { 7 | 8 | // Accessory must be created from PlatformAccessory Constructor 9 | Accessory = homebridge.platformAccessory; 10 | 11 | // Service and Characteristic are from hap-nodejs 12 | Service = homebridge.hap.Service; 13 | Characteristic = homebridge.hap.Characteristic; 14 | UUIDGen = homebridge.hap.uuid; 15 | 16 | // For platform plugin to be considered as dynamic platform plugin, 17 | homebridge.registerPlatform("homebridge-esphome", "ESPHomePlatform", ESPHomePlatform, true); 18 | } 19 | 20 | // Platform constructor 21 | class ESPHomePlatform { 22 | constructor(log, config, api) { 23 | var platform = this; 24 | this.log = log; 25 | this.config = config || {}; 26 | 27 | // Keep track of all registered accessories by this platform 28 | this.accessories = new Map(); 29 | 30 | if (api) { 31 | // Save the API object as plugin needs to register new accessory via this object 32 | this.api = api; 33 | 34 | // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. 35 | // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event. 36 | // Or start discover new accessories. 37 | this.api.on('didFinishLaunching', function () { 38 | platform.log("Initializing ESPHomePlatform devices"); 39 | 40 | for (const device of this.config.devices) { 41 | if (!device.name) { 42 | this.log.error('Device has no name configured.') 43 | } 44 | else if (!device.host) { 45 | this.log.info('Device [%s] has no hostname configured.', device.name); 46 | } 47 | else if (!device.type) { 48 | this.log.error('Device [%s] has no type configured.', device.name); 49 | } 50 | else if (!device.id) { 51 | this.log.error('Device [%s] has no id configured.', device.name); 52 | } 53 | else { 54 | device.port = device.port || 80; 55 | this.initializeAccessory(device); 56 | } 57 | } 58 | }.bind(this)); 59 | } 60 | } 61 | 62 | // Function invoked when homebridge tries to restore cached accessory 63 | configureAccessory(accessory) { 64 | this.log.info( 65 | 'Configuring cached accessory: [%s] %s %s', 66 | accessory.displayName, 67 | accessory.context.host, 68 | accessory.UUID 69 | ); 70 | this.accessories.set(accessory.UUID, accessory); 71 | } 72 | 73 | registerPlatformAccessory(platformAccessory) { 74 | this.log.debug('Register PlatformAccessory: (%s)', platformAccessory.displayName); 75 | this.api.registerPlatformAccessories('homebridge-esphome', 'ESPHomePlatform', [platformAccessory]); 76 | } 77 | 78 | initializeAccessory(device, knownId) { 79 | // Get UUID 80 | const uuid = knownId || this.api.hap.uuid.generate(device.name); 81 | const homebridgeAccessory = this.accessories.get(uuid); 82 | 83 | this.log.info('Adding: %s (%s)', device.name || 'unnamed', device.type); 84 | 85 | // Construct new accessory 86 | let deviceAccessory; 87 | switch (device.type) { 88 | case 'switch': 89 | deviceAccessory = new SwitchAccessory(this, homebridgeAccessory, device); 90 | break; 91 | case 'fan': 92 | deviceAccessory = new FanAccessory(this, homebridgeAccessory, device); 93 | break; 94 | default: 95 | this.log.error('Device %s: configured type \'%s\' does not exist.'); 96 | break; 97 | } 98 | 99 | // Add to registered accessories 100 | this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); 101 | } 102 | } 103 | 104 | module.exports = function (homebridge) { 105 | homebridge.registerPlatform('homebridge-esphome', 'ESPHomePlatform', ESPHomePlatform, true); 106 | }; -------------------------------------------------------------------------------- /lib/fan.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const BaseAccessory = require('./base'); 3 | 4 | let Accessory; 5 | let Service; 6 | let Characteristic; 7 | 8 | class FanAccessory extends BaseAccessory { 9 | constructor(platform, homebridgeAccessory, deviceConfig) { 10 | 11 | ({ Accessory, Characteristic, Service } = platform.api.hap); 12 | 13 | super(platform, 14 | homebridgeAccessory, 15 | deviceConfig, 16 | Accessory.Categories.FAN); 17 | 18 | this.getUrl = `/fan/${this.deviceConfig.id}`; 19 | this.setTurnOnUrl = `/fan/${this.deviceConfig.id}/turn_on`; 20 | this.setTurnOffUrl = `/fan/${this.deviceConfig.id}/turn_off`; 21 | this.setToggleUrl = `/fan/${this.deviceConfig.id}/toggle`; 22 | 23 | // EventSource callbacks 24 | this.espHomeWebApi.stateEvent((obj) => { 25 | this.log.debug('[ES][%s] State event:', this.homebridgeAccessory.displayName, obj); 26 | if (obj.id === `fan-${this.deviceConfig.id}`) { 27 | this.service.getCharacteristic(Characteristic.On).updateValue(obj.value); 28 | this.log.debug('Characteristic.On changed to', obj.value); 29 | this.service.getCharacteristic(Characteristic.RotationSpeed).updateValue(this._convertSpeedToPercentage(obj.speed)); 30 | this.log.debug('Characteristic.RotationSpeed changed to', obj.speed, this._convertSpeedToPercentage(obj.speed)); 31 | } 32 | }); 33 | 34 | // Characteristic events 35 | this.service.getCharacteristic(Characteristic.On) 36 | .on('get', (callback) => { 37 | 38 | this.log.debug('[GET][%s] Characteristic.On', this.homebridgeAccessory.displayName); 39 | 40 | // Send GET request 41 | this.espHomeWebApi.sendRequestJson(this.getUrl, '', 'GET', 42 | (response, obj) => { 43 | this.log.debug('[GET] Response', obj) 44 | callback(null, obj.state === 'ON'); 45 | }, 46 | (error) => { 47 | this.log.error('[ERROR][GET][%s] %s', this.homebridgeAccessory.displayName, error); 48 | callback(error); 49 | }); 50 | }) 51 | .on('set', (state, callback) => { 52 | 53 | this.log.debug('[SET][%s] Characteristic.On: %s', this.homebridgeAccessory.displayName, state); 54 | 55 | const url = state ? this.setTurnOnUrl : this.setTurnOffUrl; 56 | 57 | // Send SET request 58 | this.espHomeWebApi.sendRequest(url, '', 'POST', 59 | (response, body) => { 60 | callback(); 61 | }, 62 | (error) => { 63 | this.log.error('[ERROR][SET][%s] %s', this.homebridgeAccessory.displayName, error); 64 | callback(error); 65 | }); 66 | }); 67 | 68 | if (!this.service.getCharacteristic(Characteristic.RotationSpeed)) { 69 | this.service.addCharacteristic(Characteristic.RotationSpeed); 70 | } 71 | this.service.getCharacteristic(Characteristic.RotationSpeed) 72 | .on("get", (callback) => { 73 | 74 | this.log.debug('[GET][%s] Characteristic.RotationSpeed', this.homebridgeAccessory.displayName); 75 | 76 | // Send GET request 77 | this.espHomeWebApi.sendRequestJson(this.getUrl, '', 'GET', 78 | (response, obj) => { 79 | 80 | callback(null, this._convertSpeedToPercentage(obj.speed)); 81 | }, 82 | (error) => { 83 | this.log.error('[ERROR][GET][%s] %s', this.homebridgeAccessory.displayName, error); 84 | callback(error); 85 | }); 86 | }) 87 | .on("set", (percentage, callback) => { 88 | 89 | this.log.debug('[SET][%s] Characteristic.RotationSpeed: %s', this.homebridgeAccessory.displayName, percentage); 90 | 91 | const speed = this._convertPercentageToSpeed(percentage); 92 | let url; 93 | if (speed == 0) { 94 | url = this.setTurnOffUrl; 95 | } 96 | else { 97 | url = this.setTurnOnUrl + '?speed=' + speed; 98 | } 99 | 100 | // Send SET request 101 | this.espHomeWebApi.sendRequest(url, '', 'POST', 102 | (response, body) => { 103 | callback(); 104 | }, 105 | (error) => { 106 | this.log.error('[ERROR][SET][%s] %s', this.homebridgeAccessory.displayName, error); 107 | callback(error); 108 | }); 109 | }); 110 | } 111 | 112 | _convertSpeedToPercentage(speed) { 113 | let percentage; 114 | switch (speed) { 115 | case "high": 116 | return 100; 117 | break; 118 | case "medium": 119 | return 50; 120 | break; 121 | case "low": 122 | return 25; 123 | break; 124 | case "off": 125 | default: 126 | return 0; 127 | } 128 | } 129 | 130 | _convertPercentageToSpeed(percentage) { 131 | if (percentage == 0) { 132 | return 'off'; 133 | } 134 | else { 135 | if (percentage < 25) { 136 | return 'low'; 137 | } 138 | else { 139 | if (percentage < 50) { 140 | return 'medium'; 141 | } 142 | else { 143 | if (percentage >= 75) { 144 | return 'high'; 145 | } 146 | } 147 | } 148 | return new Error('Could not convert percentage to fan speed'); 149 | } 150 | } 151 | } 152 | 153 | module.exports = FanAccessory; --------------------------------------------------------------------------------