UNPKG

@homebridge-plugins/homebridge-ewelink

Version:

Homebridge plugin to integrate eWeLink devices into HomeKit.

509 lines (472 loc) 20.2 kB
import { Buffer } from 'node:buffer' export default class { constructor(platform, devicesInHB) { // Set up variables from the platform this.config = platform.config this.debug = platform.config.debug this.devicesInHB = devicesInHB this.hapChar = platform.api.hap.Characteristic this.hapServ = platform.api.hap.Service this.hapUUIDGen = platform.api.hap.uuid.generate this.log = platform.log } showHome() { // Return the home page for the API return `<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous"> <title>homebridge-ewelink API Docs</title> </head> <body> <div class="container-fluid my-3"> <div class="row content"> <div class="col-lg-2"></div> <div class="col-lg-8"> <p class="text-center"> <img src="https://user-images.githubusercontent.com/43026681/101325266-63126600-3863-11eb-9382-4a2924f0e540.png" alt="homebridge-ewelink logo" style="width: 60%;"> <h4 class="text-center">homebridge-ewelink</h3> </p> <hr class="mb-0"> <h5 class="mt-4 mb-2">Intro</h5> <ul class="small"> <li>The internal API allows you to query and control your homebridge-ewelink accessories using HTTP requests</li> </ul> <h5 class="mt-4 mb-2">Documentation</h5> <ul class="small"> <li>All requests are of type <code>HTTP GET</code></li> <li>All requests must include an HTTP authentication header in the form: <ul> <li><code>Authorization: "Basic %%%"</code></li> <li>Where <code>%%%</code> is a base64-encoded string of your eWeLink credentials in the form <code>username:password</code></li> </ul> </li> <li>All requests will return a <code>HTTP 200 OK</code> success code with a JSON response</li> <li>Success or failure of any request can be determined by the <code>success</code> response property which will be <code>true</code> or <code>false</code></li> <li>Error messages will be returned in the <code>error</code> parameter as a <code>string</code></li> <li>Replace <code>{hbDeviceId}</code> with the Homebridge ID of the device, e.g. <code>10000abcdeSWX</code></li> <li>A note about offline devices: <ul> <li>Querying any characteristic will return the HomeKit cache value</li> <li>Updating a characteristic which matches the HomeKit cache value will return a <code>success:true</code> response</li> <li>Updating a characteristic which does not match the HomeKit cache value will return a <code>success:false</code> with an <code>error:"HAP Status Error: -70402"</code> response</li> </ul> </li> </ul> <div class="alert alert-secondary small text-center" role="alert" style="display: none;" id="baseUrlBanner"></div> <h5 class="mt-4 mb-2">Accessory Query Commands</h5> <div class="table-responsive"> <table class="table table-sm table-hover"> <thead> <tr> <th scope="col" class="small" style="width: 50%;">Function</th> <th scope="col" class="small" style="width: 50%;">Path</th> </tr> </thead> <tbody> <tr> <td class="small"> <strong>Obtain a device list.</strong><br> <code>response</code> property is an <code>array</code>, for example:<br> <code><pre>{ "success": true, "response":[ { "name": "Bedroom Switch", "hbdeviceid": "1000aa1a1aSW1", "status": { "wan": true, "lan": true, "ip": "192.168.1.20" } } ] }</pre> </code> </td> <td class="align-middle small"><code>/get/devicelist</code></td> </tr> <tr> <td class="small"> <strong>Obtain a device's current state.</strong><br> <code>response</code> property is an <code>object</code>, for example:<br> <code><pre>{ "success": true, "response": { "status": { "wan": true, "lan": true, "ip": "192.168.1.20" }, "services": ["switch"], "switch": { "state": "off" } } }</pre> </code> </td> <td class="align-middle small"><code>/get/{hbDeviceId}</code></td> </tr> </tbody> </table> </div> <ul class="small"> <li><code>services</code> is an array of services that the device has</li> <li>Possible services include <code>switch</code>, <code>outlet</code>, <code>light</code>, <code>fan</code>, <code>temperature</code> and <code>humidity</code> <li>Each service will have a corresponding property of characteristic(s) <ul> <li>In the above example the service is <code>switch</code> and the only characteristic is <code>state</code></li> </ul> </li> <li>Use the service and characteristic when using the below commands to control an accessory</li> </ul> <h5 class="mt-4 mb-2">Accessory Control Commands</h5> <div class="table-responsive"> <table class="table table-sm table-hover"> <thead> <tr> <th scope="col" class="small" style="width: 50%;">Function</th> <th scope="col" class="small" style="width: 50%;">Path</th> </tr> </thead> <tbody> <tr> <td class="small">Template</td> <td class="align-middle small"><code>/set/{hbDeviceId}/{service}/{characteristic}/{value}</code></td> </tr> <tr> <td class="small"> Service: <code>switch</code>.<br> Characteristic: <code>state</code>.<br> Value: must be <code>on</code>, <code>off</code> or <code>toggle</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/switch/state/on</code></td> </tr> <tr> <td class="small"> Service: <code>outlet</code>.<br> Characteristic: <code>state</code>.<br> Value: must be <code>on</code>, <code>off</code> or <code>toggle</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/outlet/state/off</code></td> </tr> <tr> <td class="small"> Service: <code>light</code>.<br> Characteristic: <code>state</code>.<br> Value: must be <code>on</code>, <code>off</code> or <code>toggle</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/light/state/on</code></td> </tr> <tr> <td class="small"> Service: <code>light</code>.<br> Characteristic: <code>brightness</code>.<br> Value: must be between <code>0</code> and <code>100</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/light/brightness/54</code></td> </tr> <tr> <td class="small"> Service: <code>light</code>.<br> Characteristic: <code>hue</code>.<br> Value: must be between <code>0</code> and <code>360</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/light/hue/157</code></td> </tr> <tr> <td class="small"> Service: <code>light</code>.<br> Characteristic: <code>colourtemperature</code>.<br> Value: must be between <code>140</code> and <code>500</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/light/colourtemperature/300</code></td> </tr> <tr> <td class="small"> Service: <code>fan</code>.<br> Characteristic: <code>speed</code>.<br> Value: must be <code>low</code>, <code>medium</code> or <code>high</code>. </td> <td class="align-middle small"><code>/set/{hbDeviceId}/fan/speed/medium</code></td> </tr> </tbody> </table> </div> </div> <div class="col-lg-2"></div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.min.js" integrity="sha384-lpyLfhYuitXl2zRZ5Bn2fqnhNAKOAaM/0Kr9laMspuaMiZfGmfwRNFh8HlMy49eQ" crossorigin="anonymous"></script> <script> const base = window.location.href.slice(0, -1) const banner = document.getElementById('baseUrlBanner') banner.innerHTML = 'All paths are relative to <strong><code>' + base + '</code></strong>' banner.style.display = 'block' </script> </body> </html>` } async action(req) { // Log the request if appropriate if (this.debug) { this.log('API request [%s].', req.url) } // Authenticate the request if ( !req.headers || !req.headers.authorization || !req.headers.authorization.includes('Basic ') ) { throw new Error('Invalid authentication') } const encodedCreds = req.headers.authorization.split(' ')[1] const buff = Buffer.from(encodedCreds, 'base64') const decodedCreds = buff .toString('utf8') .replace(/\r\n|\n|\r/g, '') .trim() const [user, pass] = decodedCreds.split(':') if (user !== this.config.username || pass !== this.config.password) { throw new Error('Invalid authentication') } // Obtain the parts of the request url const pathParts = req.url.split('/') const action = pathParts[1] const device = pathParts[2] const servToUpdate = pathParts[3] const charToUpdate = pathParts[4] const newValue = pathParts[5] // Check an action was specified if (!action) { throw new Error('No action specified') } // Check the action is either get or set if (!['get', 'set'].includes(action)) { throw new Error('Action must be \'get\' or \'set\'') } // Check a device was specified if (!device) { throw new Error('No accessory specified') } // Special case for the device list if (device === 'devicelist') { const deviceList = [] this.devicesInHB.forEach((accessory) => { if (accessory.context.hidden) { return } deviceList.push({ name: accessory.displayName, hbdeviceid: accessory.context.hbDeviceId, status: { wan: !!accessory.context.reachableWAN, lan: !!accessory.context.reachableLAN, ip: accessory.context.ip || false, }, }) }) return deviceList } // Try and find the device in Homebridge const uuid = this.hapUUIDGen(device) if (!this.devicesInHB.has(uuid)) { throw new Error('Accessory not found in Homebridge') } // Obtain the corresponding accessory const accessory = this.devicesInHB.get(uuid) // Check the accessory isn't hidden from Homebridge if (accessory.context.hidden) { throw new Error('Accessory not found in Homebridge') } // Check the device is controllable if (!accessory.control) { throw new Error('Accessory has not been initialised yet') } // If the action is 'get' then return the properties if (action === 'get') { if (!accessory.control.currentState()) { throw new Error('Accessory does not yet support querying') } return { status: { wan: !!accessory.context.reachableWAN, lan: !!accessory.context.reachableLAN, ip: accessory.context.ip || false, }, ...(await accessory.control.currentState()), } } // From now on the action must be 'set' // Check a servToUpdate to update was specified if (!servToUpdate) { throw new Error('No service specified') } let service switch (servToUpdate) { case 'fan': service = this.hapServ.Fan break case 'light': service = this.hapServ.Lightbulb break case 'outlet': service = this.hapServ.Outlet break case 'switch': service = this.hapServ.Switch break default: throw new Error('Invalid service specified') } if (!accessory.getService(service)) { throw new Error(`Accessory does not have service:${servToUpdate}`) } // Check an charToUpdate for a servToUpdate was specified if (!charToUpdate) { throw new Error(`No characteristic specified for service:${servToUpdate}`) } const accServ = accessory.getService(service) // These variables depend on the charToUpdate that was supplied let charName switch (charToUpdate) { case 'adaptivelighting': case 'colourtemperature': charName = accServ.testCharacteristic(this.hapChar.ColorTemperature) ? this.hapChar.ColorTemperature : false break case 'brightness': charName = accServ.testCharacteristic(this.hapChar.Brightness) ? this.hapChar.Brightness : false break case 'hue': charName = accServ.testCharacteristic(this.hapChar.Hue) ? this.hapChar.Hue : false break case 'speed': charName = accServ.testCharacteristic(this.hapChar.RotationSpeed) ? this.hapChar.RotationSpeed : false break case 'state': charName = accServ.testCharacteristic(this.hapChar.On) ? this.hapChar.On : false break default: throw new Error(`Invalid characteristic specified for service:${servToUpdate}`) } // Check that the accessory has the corresponding characteristic for the charToUpdate if (!charName) { throw new Error( `Accessory service:${servToUpdate} does not support characteristic:${charToUpdate}`, ) } // Check a new status was supplied if the action is set if (!newValue) { throw new Error(`No value specified for characteristic:${charToUpdate}`) } let newHKStatus switch (charToUpdate) { case 'brightness': { // The new status for brightness must be an integer between 0 and 100 newHKStatus = Number.parseInt(newValue, 10) if (Number.isNaN(newHKStatus) || newHKStatus < 0 || newHKStatus > 100) { throw new Error('Value must be integer 0-100 for characteristic:brightness') } // Check the accessory has a correct set handler for on/off if (!accessory.control.internalBrightnessUpdate) { throw new Error('Function to control accessory not found') } // Call the set handler to send the request to eWeLink await accessory.control.internalBrightnessUpdate(newHKStatus) break } case 'colourtemperature': { // The new status for colour temperature must be an integer between 140 and 500 newHKStatus = Number.parseInt(newValue, 10) if (Number.isNaN(newHKStatus) || newHKStatus < 140 || newHKStatus > 500) { throw new Error('Value must be integer 140-500 for characteristic:colourtemperature') } // Check the accessory has a correct set handler for on/off if (!accessory.control.internalCTUpdate) { throw new Error(`Accessory does not support controlling characteristic:${charToUpdate}`) } // Call the set handler to send the request to eWeLink if (accessory?.alController?.isAdaptiveLightingActive()) { accessory.alController.disableAdaptiveLighting() } await accessory.control.internalCTUpdate(newHKStatus) break } case 'hue': { // The new status for hue must be an integer between 0 and 360 newHKStatus = Number.parseInt(newValue, 10) if (Number.isNaN(newHKStatus) || newHKStatus < 0 || newHKStatus > 360) { throw new Error('Value must be integer 0-360 for characteristic:hue') } // Check the accessory has a correct set handler for on/off if (!accessory.control.internalColourUpdate) { throw new Error('Function to control accessory not found') } // Call the set handler to send the request to eWeLink if (accessory?.alController?.isAdaptiveLightingActive()) { accessory.alController.disableAdaptiveLighting() } await accessory.control.internalColourUpdate(newHKStatus) break } case 'speed': // The new status for speed must be low, medium or high switch (newValue) { case 'low': newHKStatus = 33 break case 'medium': newHKStatus = 66 break case 'high': newHKStatus = 99 break default: throw new Error('Value must be \'low\', \'medium\' or \'high\' for characteristic:speed') } // Check the accessory has a correct set handler for speed if (!accessory.control.internalSpeedUpdate) { throw new Error('Function to control accessory not found') } // Call the set handler to send the request to eWeLink await accessory.control.internalSpeedUpdate(newHKStatus) break case 'state': // The new status for state must be on, off or toggle switch (newValue) { case 'on': newHKStatus = true break case 'off': newHKStatus = false break case 'toggle': newHKStatus = !accServ.getCharacteristic(this.hapChar.On).value break default: throw new Error('Value must be \'on\', \'off\' or \'toggle\' for characteristic:state') } // Check the accessory has a correct set handler for on/off if (!accessory.control.internalStateUpdate) { throw new Error('Function to control accessory not found') } // Call the set handler to send the request to eWeLink await accessory.control.internalStateUpdate(newHKStatus) break default: throw new Error(`Invalid value for characteristic:${charToUpdate}`) } // The eWeLink request was successful so update the characteristic in HomeKit accServ.updateCharacteristic(charName, newHKStatus) return true } }