├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ ├── feedback.md │ └── new-issue.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.schema.json ├── eslint.config.js ├── lib ├── connection │ ├── http.js │ └── upnp.js ├── device │ ├── coffee.js │ ├── crockpot.js │ ├── dimmer.js │ ├── heater.js │ ├── humidifier.js │ ├── index.js │ ├── insight.js │ ├── lightswitch.js │ ├── link-bulb.js │ ├── link-hub.js │ ├── maker-garage.js │ ├── maker-switch.js │ ├── motion.js │ ├── outlet.js │ ├── purifier.js │ └── simulation │ │ ├── purifier-insight.js │ │ ├── purifier.js │ │ ├── switch-insight.js │ │ └── switch.js ├── fakegato │ ├── LICENSE │ ├── fakegato-history.js │ ├── fakegato-storage.js │ ├── fakegato-timer.js │ └── uuid.js ├── homebridge-ui │ ├── public │ │ └── index.html │ └── server.js ├── index.js ├── platform.js └── utils │ ├── colour.js │ ├── constants.js │ ├── eve-chars.js │ ├── functions.js │ └── lang-en.js ├── package-lock.json └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: bwp91 2 | patreon: bwp91 3 | ko_fi: bwp91 4 | custom: ['https://www.paypal.me/BenPotter'] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### What issue do you have? Please be as thorough and explicit as possible. 8 | 9 | ### Details of your setup. 10 | * Do you use Homebridge (with config-ui?) or HOOBS? 11 | 12 | * Which version of Homebridge/HOOBS do you have? 13 | 14 | * Which version of this plugin (homebridge-wemo) do you have? Has the issue started since upgrading from a previous version? 15 | 16 | * Which Wemo devices do you have that are causing issues? Please include product models if applicable. 17 | 18 | ### Please paste any relevant logs below. 19 | 27 | 28 | ``` 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🌍 Homebridge Discord Server 4 | url: https://discord.gg/bHjKNkN 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🆕 Device/Feature Request 3 | about: Submit new ideas for the plugin or request support for a new device. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | * **Please explain your feature request in a one or two sentences.** 11 | 12 | * **Is your feature request related to a problem? Please describe.** 13 | 14 | * **Any particular Wemo devices that this relates to?** 15 | 16 | * **Anything else?** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ℹ️ Information/Feedback Request 3 | about: Ask general information about this plugin or give valuable feedback. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚨 Problem/Bug Alert 3 | about: The plugin isn't working as expected or it's showing an error. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | ### What issue do you have? Please be as thorough and explicit as possible. 16 | 17 | ### Details of your setup. 18 | * Do you use (1) Homebridge UI-X (2) Homebridge CLI or (3) HOOBS? 19 | 20 | * Which version of Homebridge/HOOBS do you have? 21 | 22 | * Which platform do you run Homebridge/HOOBS on (e.g. Raspberry Pi/Windows/HOOBS Box)? Please also mention your version of Node.js/NPM if known. 23 | 24 | * Which version of this plugin (homebridge-wemo) do you have? Has the issue started since upgrading from a previous version? 25 | 26 | * Which Wemo devices do you have that are causing issues? Please include product models if applicable. 27 | 28 | ### Please paste any relevant logs below. 29 | 36 | 37 | ``` 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: npm # See documentation for possible values 9 | directory: / # Location of package manifests 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | # the Node.js versions to build on 12 | node-version: [18.x, 20.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [latest] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [latest] 9 | schedule: 10 | - cron: '0 1 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v3 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | # - run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nova 3 | .npm 4 | node_modules 5 | .idea 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .nova 3 | .npm 4 | .idea 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 - 2025 Ben Potter 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Homebridge Verified 3 |

4 | 5 | 6 | # homebridge-wemo 7 | 8 | Homebridge plugin to integrate Wemo devices into HomeKit 9 | 10 | [![npm](https://img.shields.io/npm/v/homebridge-wemo/latest?label=latest)](https://www.npmjs.com/package/homebridge-wemo) 11 | [![npm](https://img.shields.io/npm/v/homebridge-wemo/beta?label=beta)](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Beta-Version) 12 | 13 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 14 | [![hoobs-certified](https://badgen.net/badge/HOOBS/certified/yellow?label=hoobs)](https://plugins.hoobs.org/plugin/homebridge-wemo) 15 | 16 | [![npm](https://img.shields.io/npm/dt/homebridge-wemo)](https://www.npmjs.com/package/homebridge-wemo) 17 | [![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=hb-discord)](https://discord.com/channels/432663330281226270/742733745743855627) 18 | 19 | 20 | 21 | ### Plugin Information 22 | 23 | - This plugin allows you to view and control your Wemo devices within HomeKit. The plugin: 24 | - does not require your Wemo credentials as uses local network discovery (SSDP) and local control 25 | - will attempt to control your devices via a local HTTP request 26 | - will attempt to establish a UPnP connection to your devices to listen for external changes (if disabled, HTTP polling is used) 27 | 28 | ### Prerequisites 29 | 30 | - To use this plugin, you will need to already have: 31 | - [Node](https://nodejs.org): latest version of `v18`, `v20` or `v22` - any other major version is not supported. 32 | - [Homebridge](https://homebridge.io): `v1.6` - refer to link for more information and installation instructions. 33 | - For the UPnP connection, make sure your Homebridge instance has an allocated IP from the same IP network or VLAN as your Wemo devices. Otherwise, you should disable the UPnP connection to avoid connection errors. 34 | 35 | ### Setup 36 | 37 | - [Installation](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Installation) 38 | - [Configuration](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Configuration) 39 | - [Beta Version](https://github.com/homebridge/homebridge/wiki/How-to-Install-Alternate-Plugin-Versions) 40 | - [Node Version](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Node-Version) 41 | 42 | ### Features 43 | 44 | - [Supported Devices](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Supported-Devices) 45 | 46 | ### Help/About 47 | 48 | - [Common Errors](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Common-Errors) 49 | - [Support Request](https://github.com/homebridge-plugins/homebridge-wemo/issues/new/choose) 50 | - [Changelog](https://github.com/homebridge-plugins/homebridge-wemo/blob/latest/CHANGELOG.md) 51 | - [About Me](https://github.com/sponsors/bwp91) 52 | 53 | ### Credits 54 | 55 | - To the creator of this plugin: [@rudders](https://github.com/rudders), and to [@devbobo](https://github.com/devbobo) for his contributions. 56 | - To the creator of [wemo-client](https://github.com/timonreinhard/wemo-client) (which is now contained within this plugin): [@timonreinhard](https://github.com/timonreinhard). 57 | - To [Ben Hardill](http://www.hardill.me.uk/wordpress/tag/wemo/) for his research on Wemo devices. 58 | - To all users who have helped/tested to enable functionality for new devices. 59 | - To the creators/contributors of [Fakegato](https://github.com/simont77/fakegato-history): [@simont77](https://github.com/simont77) and [@NorthernMan54](https://github.com/NorthernMan54). 60 | - To the creator of the awesome plugin header logo: [Keryan Belahcene](https://www.instagram.com/keryan.me). 61 | - To the creators/contributors of [Homebridge](https://homebridge.io) who make this plugin possible. 62 | 63 | ### Disclaimer 64 | 65 | - I am in no way affiliated with Belkin/Wemo and this plugin is a personal project that I maintain in my free time. 66 | - Use this plugin entirely at your own risk - please see licence for more information. 67 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { antfu } from '@antfu/eslint-config' 2 | 3 | /** @type {typeof antfu} */ 4 | export default antfu( 5 | { 6 | ignores: [], 7 | jsx: false, 8 | rules: { 9 | 'curly': ['error', 'multi-line'], 10 | 'new-cap': 'off', 11 | 'import/extensions': ['error', 'ignorePackages'], 12 | 'import/order': 0, 13 | 'jsdoc/check-alignment': 'warn', 14 | 'jsdoc/check-line-alignment': 'warn', 15 | 'jsdoc/require-returns-check': 0, 16 | 'jsdoc/require-returns-description': 0, 17 | 'no-undef': 'error', 18 | 'perfectionist/sort-exports': 'error', 19 | 'perfectionist/sort-imports': [ 20 | 'error', 21 | { 22 | groups: [ 23 | 'type', 24 | 'internal-type', 25 | 'builtin', 26 | 'external', 27 | 'internal', 28 | ['parent-type', 'sibling-type', 'index-type'], 29 | ['parent', 'sibling', 'index'], 30 | 'object', 31 | 'unknown', 32 | ], 33 | order: 'asc', 34 | type: 'natural', 35 | newlinesBetween: 'always', 36 | }, 37 | ], 38 | 'perfectionist/sort-named-exports': 'error', 39 | 'perfectionist/sort-named-imports': 'error', 40 | 'quotes': ['error', 'single'], 41 | 'sort-imports': 0, 42 | 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 43 | 'style/quote-props': ['error', 'consistent-as-needed'], 44 | 'test/no-only-tests': 'error', 45 | 'unicorn/no-useless-spread': 'error', 46 | 'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }], 47 | }, 48 | typescript: false, 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /lib/connection/http.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import PQueue from 'p-queue' 3 | import { parseStringPromise } from 'xml2js' 4 | import xmlbuilder from 'xmlbuilder' 5 | 6 | import { decodeXML, hasProperty, parseError } from '../utils/functions.js' 7 | import platformLang from '../utils/lang-en.js' 8 | 9 | export default class { 10 | constructor(platform) { 11 | // Set up global vars from the platform 12 | this.platform = platform 13 | this.queue = new PQueue({ 14 | concurrency: 1, 15 | interval: 250, 16 | intervalCap: 1, 17 | timeout: 9000, 18 | throwOnTimeout: true, 19 | }) 20 | } 21 | 22 | async sendDeviceUpdate(accessory, serviceType, action, body) { 23 | try { 24 | return await this.queue.add(async () => { 25 | // Check the device has this service (it should have) 26 | if ( 27 | !accessory.context.serviceList[serviceType] 28 | || !accessory.context.serviceList[serviceType].controlURL 29 | ) { 30 | throw new Error(platformLang.noService) 31 | } 32 | 33 | // Generate the XML to send to the device 34 | const xml = xmlbuilder 35 | .create('s:Envelope', { 36 | version: '1.0', 37 | encoding: 'utf-8', 38 | allowEmpty: true, 39 | }) 40 | .att('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/') 41 | .att('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/') 42 | .ele('s:Body') 43 | .ele(`u:${action}`) 44 | .att('xmlns:u', serviceType) 45 | 46 | // Send the request to the device 47 | const hostPort = `http://${accessory.context.ipAddress}:${accessory.context.port}` 48 | const res = await axios({ 49 | url: hostPort + accessory.context.serviceList[serviceType].controlURL, 50 | method: 'post', 51 | headers: { 52 | 'SOAPACTION': `"${serviceType}#${action}"`, 53 | 'Content-Type': 'text/xml; charset="utf-8"', 54 | }, 55 | data: (body ? xml.ele(body) : xml).end(), 56 | timeout: 10000, 57 | }) 58 | 59 | // Parse the response from the device 60 | const xmlRes = res.data 61 | const response = await parseStringPromise(xmlRes, { 62 | explicitArray: false, 63 | }) 64 | 65 | if (!accessory.context.httpOnline) { 66 | this.platform.updateHTTPStatus(accessory, true) 67 | } 68 | 69 | // Return the parsed response 70 | return response['s:Envelope']['s:Body'][`u:${action}Response`] 71 | }) 72 | } catch (err) { 73 | const eText = parseError(err) 74 | if (['at Object.', 'EHOSTUNREACH'].some(el => eText.includes(el))) { 75 | // Device disconnected from network 76 | if (accessory.context.httpOnline) { 77 | this.platform.updateHTTPStatus(accessory, false) 78 | } 79 | throw new Error( 80 | eText.includes('EHOSTUNREACH') ? platformLang.timeoutUnreach : platformLang.timeout, 81 | ) 82 | } 83 | throw err 84 | } 85 | } 86 | 87 | async receiveDeviceUpdate(accessory, body) { 88 | try { 89 | accessory.logDebug(`${platformLang.incKnown}:\n${body.trim()}`) 90 | 91 | // Convert the XML to JSON 92 | const json = await parseStringPromise(body, { explicitArray: false }) 93 | 94 | // Loop through the JSON for the necessary information 95 | 96 | for (const prop in json['e:propertyset']['e:property']) { 97 | if (hasProperty(json['e:propertyset']['e:property'], prop)) { 98 | const data = json['e:propertyset']['e:property'][prop] 99 | switch (prop) { 100 | case 'BinaryState': 101 | try { 102 | accessory.control.receiveDeviceUpdate({ 103 | name: 'BinaryState', 104 | value: Number.parseInt(data.substring(0, 1), 10), 105 | }) 106 | } catch (err) { 107 | accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`) 108 | } 109 | break 110 | case 'Brightness': 111 | try { 112 | accessory.control.receiveDeviceUpdate({ 113 | name: 'Brightness', 114 | value: Number.parseInt(data, 10), 115 | }) 116 | } catch (err) { 117 | accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`) 118 | } 119 | break 120 | case 'InsightParams': { 121 | try { 122 | const params = data.split('|') 123 | accessory.control.receiveDeviceUpdate({ 124 | name: 'InsightParams', 125 | value: { 126 | state: Number.parseInt(params[0], 10), 127 | power: Number.parseInt(params[7], 10), 128 | todayWm: Number.parseFloat(params[8]), 129 | todayOnSeconds: Number.parseFloat(params[3]), 130 | }, 131 | }) 132 | } catch (err) { 133 | accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`) 134 | } 135 | break 136 | } 137 | case 'attributeList': 138 | try { 139 | const decoded = decodeXML(data) 140 | const xml = `${decoded}` 141 | 142 | const result = await parseStringPromise(xml, { explicitArray: true }) 143 | result.attributeList.attribute.forEach((attribute) => { 144 | accessory.control.receiveDeviceUpdate({ 145 | name: attribute.name[0], 146 | value: Number.parseInt(attribute.value[0], 10), 147 | }) 148 | }) 149 | } catch (err) { 150 | accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`) 151 | } 152 | break 153 | case 'StatusChange': 154 | try { 155 | const xml = await parseStringPromise(data, { explicitArray: false }) 156 | accessory.control.receiveDeviceUpdate(xml.StateEvent.DeviceID._, { 157 | name: xml.StateEvent.CapabilityId, 158 | value: xml.StateEvent.Value, 159 | }) 160 | } catch (err) { 161 | accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`) 162 | } 163 | break 164 | default: 165 | return 166 | } 167 | } 168 | } 169 | } catch (err) { 170 | // Catch any errors during this process 171 | accessory.logWarn(`${platformLang.incFail} ${parseError(err)}`) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/connection/upnp.js: -------------------------------------------------------------------------------- 1 | import { request } from 'node:http' 2 | 3 | import platformConsts from '../utils/constants.js' 4 | import { parseError } from '../utils/functions.js' 5 | import platformLang from '../utils/lang-en.js' 6 | 7 | export default class { 8 | constructor(platform, accessory) { 9 | // Set up global vars from the platform 10 | this.platform = platform 11 | this.upnpInterval = platform.config.upnpInterval 12 | this.upnpIntervalMilli = this.upnpInterval * 1000 13 | 14 | // Set up other variables we need 15 | this.accessory = accessory 16 | this.services = accessory.context.serviceList 17 | this.subs = {} 18 | } 19 | 20 | startSubscriptions() { 21 | // Subscribe to each of the services that the device supports, that the plugin uses 22 | Object.keys(this.services) 23 | .filter(el => platformConsts.servicesToSubscribe.includes(el)) 24 | .forEach((service) => { 25 | // Subscript to the service 26 | this.subs[service] = {} 27 | this.subscribe(service) 28 | }) 29 | } 30 | 31 | stopSubscriptions() { 32 | Object.entries(this.subs).forEach((entry) => { 33 | const [serviceType, sub] = entry 34 | if (sub.timeout) { 35 | clearTimeout(sub.timeout) 36 | } 37 | if (sub.status) { 38 | this.unsubscribe(serviceType) 39 | } 40 | }) 41 | this.accessory.logDebugWarn(platformLang.stoppedSubs) 42 | } 43 | 44 | subscribe(serviceType) { 45 | try { 46 | // Check to see an already sent request is still pending 47 | if (this.subs[serviceType].status === 'PENDING') { 48 | this.accessory.logDebug(`[${serviceType}] ${platformLang.subPending}`) 49 | return 50 | } 51 | 52 | // Set up the options for the subscription request 53 | const timeout = this.upnpInterval + 10 54 | const options = { 55 | host: this.accessory.context.ipAddress, 56 | port: this.accessory.context.port, 57 | path: this.services[serviceType].eventSubURL, 58 | method: 'SUBSCRIBE', 59 | headers: { TIMEOUT: `Second-${timeout}` }, 60 | } 61 | 62 | // The remaining options depend on whether the subscription already exists 63 | if (this.subs[serviceType].status) { 64 | // Subscription already exists so renew 65 | options.headers.SID = this.subs[serviceType].status 66 | } else { 67 | // Subscription doesn't exist yet to set up for new subscription 68 | this.subs[serviceType].status = 'PENDING' 69 | this.accessory.logDebug(`[${serviceType}] ${platformLang.subInit}`) 70 | options.headers.CALLBACK = `` 71 | options.headers.NT = 'upnp:event' 72 | } 73 | 74 | // Execute the subscription request 75 | const req = request(options, (res) => { 76 | if (res.statusCode === 200) { 77 | // Subscription request successful 78 | this.subs[serviceType].status = res.headers.sid 79 | 80 | // Renew subscription after 150 seconds 81 | this.subs[serviceType].timeout = setTimeout( 82 | () => this.subscribe(serviceType), 83 | this.upnpIntervalMilli, 84 | ) 85 | } else { 86 | // Subscription request failure 87 | this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.subError} [${res.statusCode}]`) 88 | this.subs[serviceType].status = null 89 | 90 | // Try to recover from a failed subscription after 10 seconds 91 | this.subs[serviceType].timeout = setTimeout(() => this.subscribe(serviceType), 10000) 92 | } 93 | }) 94 | 95 | // Listen for errors on the subscription 96 | req.removeAllListeners('error') 97 | req.on('error', (err) => { 98 | if (!err) { 99 | return 100 | } 101 | 102 | // Stop the subscriptions 103 | this.stopSubscriptions() 104 | 105 | // Use the platform function to disable the upnp client for this accessory 106 | this.platform.disableUPNP(this.accessory, err) 107 | }) 108 | req.end() 109 | } catch (err) { 110 | // Catch any errors during the process 111 | this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.subscribeError} ${parseError(err)}`) 112 | } 113 | } 114 | 115 | unsubscribe(serviceType) { 116 | try { 117 | // Check to see an already sent request is still pending 118 | if (!this.subs[serviceType] || this.subs[serviceType].status === 'PENDING') { 119 | return 120 | } 121 | 122 | // Set up the options for the subscription request 123 | const options = { 124 | host: this.accessory.context.ipAddress, 125 | port: this.accessory.context.port, 126 | path: this.services[serviceType].eventSubURL, 127 | method: 'UNSUBSCRIBE', 128 | headers: { SID: this.subs[serviceType].status }, 129 | } 130 | 131 | // Execute the subscription request 132 | const req = request(options, (res) => { 133 | if (res.statusCode === 200) { 134 | // Unsubscribed 135 | this.accessory.logDebug(`[${serviceType}] unsubscribe successful`) 136 | } else { 137 | // Subscription request failure 138 | this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.unsubFail} [${res.statusCode}]`) 139 | } 140 | }) 141 | 142 | // Listen for errors on the subscription 143 | req.removeAllListeners('error') 144 | req.on('error', (err) => { 145 | if (!err) { 146 | return 147 | } 148 | this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.unsubFail} [${err.message}]`) 149 | }) 150 | req.end() 151 | } catch (err) { 152 | this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.unsubFail} [${parseError(err)}]`) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/device/coffee.js: -------------------------------------------------------------------------------- 1 | import { Builder, parseStringPromise } from 'xml2js' 2 | 3 | import { decodeXML, parseError } from '../utils/functions.js' 4 | import platformLang from '../utils/lang-en.js' 5 | 6 | export default class { 7 | constructor(platform, accessory) { 8 | // Set up variables from the platform 9 | this.hapChar = platform.api.hap.Characteristic 10 | this.hapErr = platform.api.hap.HapStatusError 11 | this.hapServ = platform.api.hap.Service 12 | this.platform = platform 13 | 14 | // Set up variables from the accessory 15 | this.accessory = accessory 16 | 17 | // Add the switch service if it doesn't already exist 18 | this.service = this.accessory.getService(this.hapServ.Switch) 19 | || this.accessory.addService(this.hapServ.Switch) 20 | 21 | // Add the set handler to the outlet on/off characteristic 22 | this.service 23 | .getCharacteristic(this.hapChar.On) 24 | .removeOnSet() 25 | .onSet(async value => this.internalModeUpdate(value)) 26 | 27 | // Output the customised options to the log 28 | const opts = JSON.stringify({ 29 | }) 30 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 31 | 32 | // Request a device update immediately 33 | this.requestDeviceUpdate() 34 | 35 | // Start a polling interval if the user has disabled upnp 36 | if (this.accessory.context.connection === 'http') { 37 | this.pollingInterval = setInterval( 38 | () => this.requestDeviceUpdate(), 39 | platform.config.pollingInterval * 1000, 40 | ) 41 | } 42 | } 43 | 44 | receiveDeviceUpdate(attribute) { 45 | // Log the receiving update if debug is enabled 46 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 47 | 48 | // Check which attribute we are getting 49 | switch (attribute.name) { 50 | case 'Mode': 51 | this.externalModeUpdate(attribute.value) 52 | break 53 | default: 54 | } 55 | } 56 | 57 | async sendDeviceUpdate(attributes) { 58 | // Log the sending update if debug is enabled 59 | this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`) 60 | 61 | // Generate the XML to send 62 | const builder = new Builder({ 63 | rootName: 'attribute', 64 | headless: true, 65 | renderOpts: { pretty: false }, 66 | }) 67 | const xmlAttributes = Object.keys(attributes) 68 | .map(attributeKey => builder.buildObject({ 69 | name: attributeKey, 70 | value: attributes[attributeKey], 71 | })) 72 | .join('') 73 | 74 | // Send the update 75 | await this.platform.httpClient.sendDeviceUpdate( 76 | this.accessory, 77 | 'urn:Belkin:service:deviceevent:1', 78 | 'SetAttributes', 79 | { 80 | attributeList: { '#text': xmlAttributes }, 81 | }, 82 | ) 83 | } 84 | 85 | async requestDeviceUpdate() { 86 | try { 87 | // Request the update 88 | const data = await this.platform.httpClient.sendDeviceUpdate( 89 | this.accessory, 90 | 'urn:Belkin:service:deviceevent:1', 91 | 'GetAttributes', 92 | ) 93 | 94 | // Parse the response 95 | const decoded = decodeXML(data.attributeList) 96 | const xml = `${decoded}` 97 | const result = await parseStringPromise(xml, { explicitArray: false }) 98 | Object.keys(result.attributeList.attribute).forEach((key) => { 99 | // Only send the required attributes to the receiveDeviceUpdate function 100 | switch (result.attributeList.attribute[key].name) { 101 | case 'Mode': 102 | this.receiveDeviceUpdate({ 103 | name: result.attributeList.attribute[key].name, 104 | value: Number.parseInt(result.attributeList.attribute[key].value, 10), 105 | }) 106 | break 107 | default: 108 | } 109 | }) 110 | } catch (err) { 111 | const eText = parseError(err, [ 112 | platformLang.timeout, 113 | platformLang.timeoutUnreach, 114 | platformLang.noService, 115 | ]) 116 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 117 | } 118 | } 119 | 120 | async internalModeUpdate(value) { 121 | try { 122 | // Coffee maker cannot be turned off remotely 123 | if (!value) { 124 | throw new Error('coffee maker cannot be turned off remotely') 125 | } 126 | 127 | // Send the update to turn ON 128 | await this.sendDeviceUpdate({ Mode: 4 }) 129 | 130 | // Update the cache value and log the change if appropriate 131 | this.cacheState = true 132 | this.accessory.log(`${platformLang.curState} [on]`) 133 | } catch (err) { 134 | // Catch any errors 135 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 136 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 137 | 138 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 139 | setTimeout(() => { 140 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 141 | }, 2000) 142 | throw new this.hapErr(-70402) 143 | } 144 | } 145 | 146 | externalModeUpdate(value) { 147 | try { 148 | // Value of 4 means brewing (ON) otherwise (OFF) 149 | value = value === 4 150 | 151 | // Check to see if the cache value is different 152 | if (value === this.cacheState) { 153 | return 154 | } 155 | 156 | // Update the HomeKit characteristics 157 | this.service.updateCharacteristic(this.hapChar.On, value) 158 | 159 | // Update the cache value and log the change if appropriate 160 | this.cacheState = value 161 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 162 | } catch (err) { 163 | // Catch any errors 164 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/device/crockpot.js: -------------------------------------------------------------------------------- 1 | import { 2 | generateRandomString, 3 | hasProperty, 4 | parseError, 5 | sleep, 6 | } from '../utils/functions.js' 7 | import platformLang from '../utils/lang-en.js' 8 | 9 | export default class { 10 | constructor(platform, accessory) { 11 | // Set up variables from the platform 12 | this.hapChar = platform.api.hap.Characteristic 13 | this.hapErr = platform.api.hap.HapStatusError 14 | this.hapServ = platform.api.hap.Service 15 | this.platform = platform 16 | 17 | // Set up variables from the accessory 18 | this.accessory = accessory 19 | 20 | // Add the heater service if it doesn't already exist 21 | this.service = this.accessory.getService(this.hapServ.HeaterCooler) 22 | || this.accessory.addService(this.hapServ.HeaterCooler) 23 | 24 | // Add the set handler to the heater active characteristic 25 | this.service 26 | .getCharacteristic(this.hapChar.Active) 27 | .removeOnSet() 28 | .onSet(async value => this.internalStateUpdate(value)) 29 | 30 | // Add options to the heater target state characteristic 31 | this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({ 32 | minValue: 0, 33 | maxValue: 0, 34 | validValues: [0], 35 | }) 36 | 37 | // Add the set handler and a range to the heater target temperature characteristic 38 | this.service 39 | .getCharacteristic(this.hapChar.HeatingThresholdTemperature) 40 | .setProps({ 41 | minValue: 0, 42 | maxValue: 24, 43 | minStep: 0.5, 44 | }) 45 | .onSet(async (value) => { 46 | await this.internalCookingTimeUpdate(value) 47 | }) 48 | 49 | // Add the set handler to the heater rotation speed characteristic 50 | this.service 51 | .getCharacteristic(this.hapChar.RotationSpeed) 52 | .setProps({ minStep: 33 }) 53 | .onSet(async (value) => { 54 | await this.internalModeUpdate(value) 55 | }) 56 | 57 | // Add a range to the heater current temperature characteristic 58 | this.service.getCharacteristic(this.hapChar.CurrentTemperature).setProps({ 59 | minValue: 0, 60 | maxValue: 24, 61 | minStep: 0.5, 62 | }) 63 | 64 | // Some conversion objects 65 | this.modeLabels = { 66 | 0: platformLang.labelOff, 67 | 50: platformLang.labelWarm, 68 | 51: platformLang.labelLow, 69 | 52: platformLang.labelHigh, 70 | } 71 | 72 | // Output the customised options to the log 73 | const opts = JSON.stringify({ 74 | }) 75 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 76 | 77 | // Request a device update immediately 78 | this.requestDeviceUpdate() 79 | 80 | // Start a polling interval if the user has disabled upnp 81 | if (this.accessory.context.connection === 'http') { 82 | this.pollingInterval = setInterval( 83 | () => this.requestDeviceUpdate(), 84 | platform.config.pollingInterval * 1000, 85 | ) 86 | } 87 | } 88 | 89 | async requestDeviceUpdate() { 90 | try { 91 | // Request the update 92 | const data = await this.platform.httpClient.sendDeviceUpdate( 93 | this.accessory, 94 | 'urn:Belkin:service:basicevent:1', 95 | 'GetCrockpotState', 96 | ) 97 | 98 | // Check for existence since data.mode can be 0 99 | if (hasProperty(data, 'mode')) { 100 | // Log the receiving update if debug is enabled 101 | this.accessory.logDebug(`${platformLang.recUpd} [mode: ${data.mode}]`) 102 | 103 | // Send the data to the receiver function 104 | this.externalModeUpdate(Number.parseInt(data.mode, 10)) 105 | } 106 | 107 | // data.time can be 0 so check for existence 108 | if (hasProperty(data, 'time')) { 109 | // Log the receiving update if debug is enabled 110 | this.accessory.logDebug(`${platformLang.recUpd} [time: ${data.time}]`) 111 | 112 | // Send the data to the receiver function 113 | this.externalTimeLeftUpdate(Number.parseInt(data.time, 10)) 114 | } 115 | } catch (err) { 116 | const eText = parseError(err, [ 117 | platformLang.timeout, 118 | platformLang.timeoutUnreach, 119 | platformLang.noService, 120 | ]) 121 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 122 | } 123 | } 124 | 125 | receiveDeviceUpdate(attribute) { 126 | // Log the receiving update if debug is enabled 127 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 128 | 129 | // Send a HomeKit needed true/false argument 130 | // attribute.value is 0 if and only if the outlet is off 131 | // this.externalStateUpdate(attribute.value !== 0) 132 | } 133 | 134 | async sendDeviceUpdate(mode, time) { 135 | // Log the sending update if debug is enabled 136 | this.accessory.logDebug(`${platformLang.senUpd} {"mode": ${mode}, "time": ${time}`) 137 | 138 | // Send the update 139 | await this.platform.httpClient.sendDeviceUpdate( 140 | this.accessory, 141 | 'urn:Belkin:service:basicevent:1', 142 | 'SetCrockpotState', 143 | { 144 | mode: { '#text': mode }, 145 | time: { '#text': time }, 146 | }, 147 | ) 148 | } 149 | 150 | async internalStateUpdate(value) { 151 | const prevState = this.service.getCharacteristic(this.hapChar.Active).value 152 | try { 153 | // Don't continue if the new value is the same as before 154 | if (value === prevState) { 155 | return 156 | } 157 | 158 | // A slight pause seems to make Home app more responsive for characteristic updates later 159 | await sleep(500) 160 | 161 | // Note value === 0 is OFF, value === 1 is ON 162 | if (value === 0) { 163 | // Turn everything off 164 | this.service.setCharacteristic(this.hapChar.RotationSpeed, 0) 165 | this.service.updateCharacteristic(this.hapChar.CurrentTemperature, 0) 166 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, 0) 167 | this.accessory.context.cacheTime = 0 168 | } else { 169 | // Set rotation speed to the lowest ON value 170 | this.service.setCharacteristic(this.hapChar.RotationSpeed, 33) 171 | } 172 | } catch (err) { 173 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 174 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 175 | 176 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 177 | setTimeout(() => { 178 | this.service.updateCharacteristic(this.hapChar.Active, prevState) 179 | }, 2000) 180 | throw new this.hapErr(-70402) 181 | } 182 | } 183 | 184 | async internalModeUpdate(value) { 185 | const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value 186 | try { 187 | // Avoid multiple updates in quick succession 188 | const updateKeyMode = generateRandomString(5) 189 | this.updateKeyMode = updateKeyMode 190 | await sleep(500) 191 | if (updateKeyMode !== this.updateKeyMode) { 192 | return 193 | } 194 | 195 | // Generate newValue for the needed mode and newSpeed in 33% multiples 196 | let newValue = 0 197 | let newSpeed = 0 198 | if (value > 25 && value <= 50) { 199 | newValue = 50 200 | newSpeed = 33 201 | } else if (value > 50 && value <= 75) { 202 | newValue = 51 203 | newSpeed = 66 204 | } else if (value > 75) { 205 | newValue = 52 206 | newSpeed = 99 207 | } 208 | 209 | // Don't continue if the speed is the same as before 210 | if (prevSpeed === newSpeed) { 211 | return 212 | } 213 | 214 | // A slight pause seems to make Home app more responsive for characteristic updates later 215 | await sleep(500) 216 | if ([0, 33].includes(newSpeed)) { 217 | // Reset the cooking times to 0 if turned off or set to warm 218 | this.service.updateCharacteristic(this.hapChar.CurrentTemperature, 0) 219 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, 0) 220 | this.accessory.context.cacheTime = 0 221 | 222 | // Log the change if appropriate 223 | this.accessory.log(`${platformLang.curTimer} [0:00]`) 224 | } 225 | 226 | // Send the update 227 | await this.sendDeviceUpdate(newValue, this.accessory.context.cacheTime) 228 | 229 | // Update the cache and log if appropriate 230 | this.cacheMode = newValue 231 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`) 232 | } catch (err) { 233 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 234 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 235 | 236 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 237 | setTimeout(() => { 238 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed) 239 | }, 2000) 240 | throw new this.hapErr(-70402) 241 | } 242 | } 243 | 244 | async internalCookingTimeUpdate(value) { 245 | const prevTemp = this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value 246 | try { 247 | // Avoid multiple updates in quick succession 248 | const updateKeyTemp = generateRandomString(5) 249 | this.updateKeyTemp = updateKeyTemp 250 | await sleep(500) 251 | if (updateKeyTemp !== this.updateKeyTemp) { 252 | return 253 | } 254 | 255 | // The value is cooking hours, I don't think device can be set to cook for 24 hours as max 256 | if (value === 24) { 257 | value = 23.5 258 | } 259 | 260 | // Don't continue if the value is the same as before 261 | if (value === prevTemp) { 262 | return 263 | } 264 | 265 | // Find the needed mode based on the new value 266 | const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value 267 | let modeChange = this.cacheMode 268 | // If cooking time is changed to above zero and mode is OFF or WARM, then set to LOW 269 | if (value !== 0 && [0, 33].includes(prevSpeed)) { 270 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, 66) 271 | modeChange = 51 272 | this.cacheMode = 51 273 | 274 | // Log the mode change if appropriate 275 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[51]}]`) 276 | } 277 | 278 | // Send the update 279 | const minutes = value * 60 280 | await this.sendDeviceUpdate(modeChange, minutes) 281 | 282 | // Log the change of cooking minutes if appropriate 283 | const modMinutes = minutes % 60 284 | this.accessory.log(`${platformLang.curTimer} [${Math.floor(value)}:${modMinutes >= 10 ? modMinutes : `0${modMinutes}`}]`) 285 | } catch (err) { 286 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 287 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 288 | 289 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 290 | setTimeout(() => { 291 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, prevTemp) 292 | }, 2000) 293 | throw new this.hapErr(-70402) 294 | } 295 | } 296 | 297 | externalModeUpdate(value) { 298 | try { 299 | // Don't continue if the given mode is the same as before 300 | if (value === this.cacheMode) { 301 | return 302 | } 303 | 304 | // Find the needed rotation speed based on the given mode 305 | let rotSpeed = 0 306 | switch (value) { 307 | case 0: 308 | break 309 | case 50: { 310 | rotSpeed = 33 311 | break 312 | } 313 | case 51: { 314 | rotSpeed = 66 315 | break 316 | } 317 | case 52: { 318 | rotSpeed = 99 319 | break 320 | } 321 | default: 322 | throw new Error(`Unknown value passed [${value} ${typeof value}]`) 323 | } 324 | // Update the HomeKit characteristics 325 | this.service.updateCharacteristic(this.hapChar.Active, value !== 0 ? 1 : 0) 326 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed) 327 | 328 | // Update the cache and log if appropriate 329 | this.cacheMode = value 330 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`) 331 | 332 | // If turned off then set the cooking time characteristics to 0 333 | if (value === 0) { 334 | this.service.updateCharacteristic(this.hapChar.CurrentTemperature, 0) 335 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, 0) 336 | } 337 | } catch (err) { 338 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 339 | } 340 | } 341 | 342 | externalTimeLeftUpdate(value) { 343 | try { 344 | // Don't continue if the rounded cooking time is the same as before 345 | if (value === this.accessory.context.cacheTime) { 346 | return 347 | } 348 | 349 | // The value is passed in minutes (cooking time remaining) 350 | let hkValue = 0 351 | if (value > 0) { 352 | /* 353 | (1) convert to half-hour units (e.g. 159 -> 5.3) 354 | (2) round to nearest 0.5 hour unit (e.g. 5.3 -> 5) 355 | (3) if 0 then raise to 0.5 (as technically still cooking even if 1 minute) 356 | */ 357 | hkValue = Math.max(Math.round(value / 30) / 2, 0.5) 358 | } 359 | 360 | const rotSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value 361 | 362 | // Change to LOW mode if cooking but cache is OFF 363 | if (hkValue > 0 && rotSpeed === 0) { 364 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, 33) 365 | this.cacheMode = 50 366 | } 367 | 368 | // Update the cooking time HomeKit characteristics 369 | this.service.updateCharacteristic(this.hapChar.CurrentTemperature, hkValue) 370 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, hkValue) 371 | 372 | // Update the cache and log if appropriate 373 | this.accessory.context.cacheTime = value 374 | const modMinutes = value % 60 375 | this.accessory.log(`${platformLang.curTimer} [${Math.floor(value / 60)}:${modMinutes >= 10 ? modMinutes : `0${modMinutes}`}]`) 376 | } catch (err) { 377 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /lib/device/dimmer.js: -------------------------------------------------------------------------------- 1 | import platformConsts from '../utils/constants.js' 2 | import { 3 | generateRandomString, 4 | hasProperty, 5 | parseError, 6 | sleep, 7 | } from '../utils/functions.js' 8 | import platformLang from '../utils/lang-en.js' 9 | 10 | export default class { 11 | constructor(platform, accessory) { 12 | // Set up variables from the platform 13 | this.hapChar = platform.api.hap.Characteristic 14 | this.hapErr = platform.api.hap.HapStatusError 15 | this.hapServ = platform.api.hap.Service 16 | this.platform = platform 17 | 18 | // Set up variables from the accessory 19 | this.accessory = accessory 20 | 21 | // Set up custom variables for this device type 22 | const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {} 23 | this.brightStep = deviceConf.brightnessStep 24 | ? Math.min(deviceConf.brightnessStep, 100) 25 | : platformConsts.defaultValues.brightnessStep 26 | 27 | // Add the lightbulb service if it doesn't already exist 28 | this.service = accessory.getService(this.hapServ.Lightbulb) || accessory.addService(this.hapServ.Lightbulb) 29 | 30 | // Add the set handler to the lightbulb on/off characteristic 31 | this.service 32 | .getCharacteristic(this.hapChar.On) 33 | .removeOnSet() 34 | .onSet(async value => this.internalStateUpdate(value)) 35 | 36 | // Add the set handler to the lightbulb brightness characteristic 37 | this.service 38 | .getCharacteristic(this.hapChar.Brightness) 39 | .setProps({ minStep: this.brightStep }) 40 | .onSet(async (value) => { 41 | await this.internalBrightnessUpdate(value) 42 | }) 43 | 44 | // Output the customised options to the log 45 | const opts = JSON.stringify({ 46 | brightnessStep: this.brightStep, 47 | }) 48 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 49 | 50 | // Request a device update immediately 51 | this.requestDeviceUpdate() 52 | 53 | // Start a polling interval if the user has disabled upnp 54 | if (this.accessory.context.connection === 'http') { 55 | this.pollingInterval = setInterval( 56 | () => this.requestDeviceUpdate(), 57 | platform.config.pollingInterval * 1000, 58 | ) 59 | } 60 | } 61 | 62 | receiveDeviceUpdate(attribute) { 63 | // Log the receiving update if debug is enabled 64 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 65 | 66 | // Check which attribute we are getting 67 | switch (attribute.name) { 68 | case 'BinaryState': { 69 | // Send a HomeKit needed true/false argument 70 | // attribute.value is 0 if and only if the device is off 71 | const hkValue = attribute.value !== 0 72 | this.externalSwitchUpdate(hkValue) 73 | break 74 | } 75 | case 'Brightness': 76 | // Send a HomeKit needed INT argument 77 | this.externalBrightnessUpdate(attribute.value) 78 | break 79 | default: 80 | } 81 | } 82 | 83 | async sendDeviceUpdate(value) { 84 | // Log the sending update if debug is enabled 85 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 86 | 87 | // Send the update 88 | await this.platform.httpClient.sendDeviceUpdate( 89 | this.accessory, 90 | 'urn:Belkin:service:basicevent:1', 91 | 'SetBinaryState', 92 | value, 93 | ) 94 | } 95 | 96 | async requestDeviceUpdate() { 97 | try { 98 | // Request the update 99 | const data = await this.platform.httpClient.sendDeviceUpdate( 100 | this.accessory, 101 | 'urn:Belkin:service:basicevent:1', 102 | 'GetBinaryState', 103 | ) 104 | 105 | // Check for existence since BinaryState can be int 0 106 | if (hasProperty(data, 'BinaryState')) { 107 | this.receiveDeviceUpdate({ 108 | name: 'BinaryState', 109 | value: Number.parseInt(data.BinaryState, 10), 110 | }) 111 | } 112 | 113 | // Check for existence since brightness can be int 0 114 | if (hasProperty(data, 'brightness')) { 115 | this.receiveDeviceUpdate({ 116 | name: 'Brightness', 117 | value: Number.parseInt(data.brightness, 10), 118 | }) 119 | } 120 | } catch (err) { 121 | const eText = parseError(err, [ 122 | platformLang.timeout, 123 | platformLang.timeoutUnreach, 124 | platformLang.noService, 125 | ]) 126 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 127 | } 128 | } 129 | 130 | async getCurrentBrightness() { 131 | // A quick function to get the current brightness of the device 132 | const data = await this.platform.httpClient.sendDeviceUpdate( 133 | this.accessory, 134 | 'urn:Belkin:service:basicevent:1', 135 | 'GetBinaryState', 136 | ) 137 | return Number.parseInt(data.brightness, 10) 138 | } 139 | 140 | async internalStateUpdate(value) { 141 | try { 142 | // Wait a longer time than the brightness so in scenes brightness is sent first 143 | await sleep(500) 144 | 145 | // Send the update 146 | await this.sendDeviceUpdate({ 147 | BinaryState: value ? 1 : 0, 148 | }) 149 | 150 | // Update the cache and log if appropriate 151 | this.cacheState = value 152 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 153 | 154 | // Don't continue if turning the device off 155 | if (!value) { 156 | return 157 | } 158 | 159 | // Wrap the extra brightness request in another try, so it doesn't affect the on/off change 160 | try { 161 | // When turning the device on we want to update the HomeKit brightness 162 | const updatedBrightness = await this.getCurrentBrightness() 163 | 164 | // Don't continue if the brightness is the same 165 | if (updatedBrightness === this.cacheBright) { 166 | return 167 | } 168 | 169 | // Update the brightness characteristic 170 | this.service.updateCharacteristic(this.hapChar.Brightness, updatedBrightness) 171 | 172 | // Update the cache and log if appropriate 173 | this.cacheBright = updatedBrightness 174 | this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`) 175 | } catch (err) { 176 | this.accessory.logWarn(`${platformLang.brightnessFail} ${parseError(err)}`) 177 | } 178 | } catch (err) { 179 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 180 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 181 | 182 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 183 | setTimeout(() => { 184 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 185 | }, 2000) 186 | throw new this.hapErr(-70402) 187 | } 188 | } 189 | 190 | async internalBrightnessUpdate(value) { 191 | try { 192 | // Avoid multiple updates in quick succession 193 | const updateKey = generateRandomString(5) 194 | this.updateKey = updateKey 195 | await sleep(300) 196 | if (updateKey !== this.updateKey) { 197 | return 198 | } 199 | 200 | // Send the update 201 | await this.sendDeviceUpdate({ 202 | BinaryState: value === 0 ? 0 : 1, 203 | brightness: value, 204 | }) 205 | 206 | // Update the cache and log if appropriate 207 | this.cacheBright = value 208 | this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`) 209 | } catch (err) { 210 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 211 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 212 | 213 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 214 | setTimeout(() => { 215 | this.service.updateCharacteristic(this.hapChar.Brightness, this.cacheBright) 216 | }, 2000) 217 | throw new this.hapErr(-70402) 218 | } 219 | } 220 | 221 | async externalSwitchUpdate(value) { 222 | try { 223 | // Don't continue if the value is the same as the cache 224 | if (value === this.cacheState) { 225 | return 226 | } 227 | 228 | // Update the ON/OFF characteristic 229 | this.service.updateCharacteristic(this.hapChar.On, value) 230 | 231 | // Update the cache and log if appropriate 232 | this.cacheState = value 233 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 234 | 235 | // Don't continue if the new state is OFF 236 | if (!value) { 237 | return 238 | } 239 | 240 | // Wrap the extra brightness request in another try, so it doesn't affect the on/off change 241 | try { 242 | // If the new state is ON then we want to update the HomeKit brightness 243 | const updatedBrightness = await this.getCurrentBrightness() 244 | 245 | // Don't continue if the brightness is the same 246 | if (updatedBrightness === this.cacheBright) { 247 | return 248 | } 249 | 250 | // Update the HomeKit brightness characteristic 251 | this.service.updateCharacteristic(this.hapChar.Brightness, updatedBrightness) 252 | 253 | // Update the cache and log if appropriate 254 | this.cacheBright = updatedBrightness 255 | this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`) 256 | } catch (err) { 257 | this.accessory.logWarn(`${platformLang.brightnessFail} ${parseError(err)}`) 258 | } 259 | } catch (err) { 260 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 261 | } 262 | } 263 | 264 | externalBrightnessUpdate(value) { 265 | try { 266 | // Don't continue if the brightness is the same as the cache value 267 | if (value === this.cacheBright) { 268 | return 269 | } 270 | // Update the HomeKit brightness characteristic 271 | this.service.updateCharacteristic(this.hapChar.Brightness, value) 272 | 273 | // Update the cache and log if appropriate 274 | this.cacheBright = value 275 | this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`) 276 | } catch (err) { 277 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /lib/device/heater.js: -------------------------------------------------------------------------------- 1 | import { Builder, parseStringPromise } from 'xml2js' 2 | 3 | import { 4 | decodeXML, 5 | generateRandomString, 6 | parseError, 7 | sleep, 8 | } from '../utils/functions.js' 9 | import platformLang from '../utils/lang-en.js' 10 | 11 | export default class { 12 | constructor(platform, accessory) { 13 | // Set up variables from the platform 14 | this.hapChar = platform.api.hap.Characteristic 15 | this.hapErr = platform.api.hap.HapStatusError 16 | this.hapServ = platform.api.hap.Service 17 | this.platform = platform 18 | 19 | // Set up variables from the accessory 20 | this.accessory = accessory 21 | 22 | // Add the heater service if it doesn't already exist 23 | this.service = this.accessory.getService(this.hapServ.HeaterCooler) 24 | || this.accessory.addService(this.hapServ.HeaterCooler) 25 | 26 | // Add the set handler to the heater active characteristic 27 | this.service 28 | .getCharacteristic(this.hapChar.Active) 29 | .removeOnSet() 30 | .onSet(async value => this.internalStateUpdate(value)) 31 | 32 | // Add options to the heater target state characteristic 33 | this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({ 34 | minValue: 0, 35 | maxValue: 0, 36 | validValues: [0], 37 | }) 38 | 39 | // Add the set handler and a range to the heater target temperature characteristic 40 | this.service 41 | .getCharacteristic(this.hapChar.HeatingThresholdTemperature) 42 | .setProps({ 43 | minStep: 1, 44 | minValue: 16, 45 | maxValue: 29, 46 | }) 47 | .onSet(async (value) => { 48 | await this.internalTargetTempUpdate(value) 49 | }) 50 | 51 | // Add the set handler to the heater rotation speed characteristic 52 | this.service 53 | .getCharacteristic(this.hapChar.RotationSpeed) 54 | .setProps({ minStep: 33 }) 55 | .onSet(async (value) => { 56 | await this.internalModeUpdate(value) 57 | }) 58 | 59 | // Add a last mode cache value if not already set 60 | const cacheMode = this.accessory.context.cacheLastOnMode 61 | if (!cacheMode || [0, 1].includes(cacheMode)) { 62 | this.accessory.context.cacheLastOnMode = 4 63 | } 64 | 65 | // Add a last temperature cache value if not already set 66 | if (!this.accessory.context.cacheLastOnTemp) { 67 | this.accessory.context.cacheLastOnTemp = 16 68 | } 69 | 70 | // Some conversion objects 71 | this.modeLabels = { 72 | 0: platformLang.labelOff, 73 | 1: platformLang.labelFP, 74 | 2: platformLang.labelHigh, 75 | 3: platformLang.labelLow, 76 | 4: platformLang.labelEco, 77 | } 78 | this.cToF = { 79 | 16: 61, 80 | 17: 63, 81 | 18: 64, 82 | 19: 66, 83 | 20: 68, 84 | 21: 70, 85 | 22: 72, 86 | 23: 73, 87 | 24: 75, 88 | 25: 77, 89 | 26: 79, 90 | 27: 81, 91 | 28: 83, 92 | 29: 84, 93 | } 94 | 95 | // Output the customised options to the log 96 | const opts = JSON.stringify({ 97 | }) 98 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 99 | 100 | // Request a device update immediately 101 | this.requestDeviceUpdate() 102 | 103 | // Start a polling interval if the user has disabled upnp 104 | if (this.accessory.context.connection === 'http') { 105 | this.pollingInterval = setInterval( 106 | () => this.requestDeviceUpdate(), 107 | platform.config.pollingInterval * 1000, 108 | ) 109 | } 110 | } 111 | 112 | receiveDeviceUpdate(attribute) { 113 | // Log the receiving update if debug is enabled 114 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 115 | 116 | // Check which attribute we are getting 117 | switch (attribute.name) { 118 | case 'Mode': 119 | this.externalModeUpdate(attribute.value) 120 | break 121 | case 'Temperature': 122 | this.externalCurrentTempUpdate(attribute.value) 123 | break 124 | case 'SetTemperature': 125 | this.externalTargetTempUpdate(attribute.value) 126 | break 127 | default: 128 | } 129 | } 130 | 131 | async requestDeviceUpdate() { 132 | try { 133 | // Request the update 134 | const data = await this.platform.httpClient.sendDeviceUpdate( 135 | this.accessory, 136 | 'urn:Belkin:service:deviceevent:1', 137 | 'GetAttributes', 138 | ) 139 | 140 | // Parse the response 141 | const decoded = decodeXML(data.attributeList) 142 | const xml = `${decoded}` 143 | const result = await parseStringPromise(xml, { explicitArray: false }) 144 | Object.keys(result.attributeList.attribute).forEach((key) => { 145 | // Only send the required attributes to the receiveDeviceUpdate function 146 | switch (result.attributeList.attribute[key].name) { 147 | case 'Mode': 148 | case 'Temperature': 149 | case 'SetTemperature': 150 | this.receiveDeviceUpdate({ 151 | name: result.attributeList.attribute[key].name, 152 | value: Number.parseInt(result.attributeList.attribute[key].value, 10), 153 | }) 154 | break 155 | default: 156 | } 157 | }) 158 | } catch (err) { 159 | const eText = parseError(err, [ 160 | platformLang.timeout, 161 | platformLang.timeoutUnreach, 162 | platformLang.noService, 163 | ]) 164 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 165 | } 166 | } 167 | 168 | async sendDeviceUpdate(attributes) { 169 | // Log the sending update if debug is enabled 170 | this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`) 171 | 172 | // Generate the XML to send 173 | const builder = new Builder({ 174 | rootName: 'attribute', 175 | headless: true, 176 | renderOpts: { pretty: false }, 177 | }) 178 | const xmlAttributes = Object.keys(attributes) 179 | .map(attributeKey => builder.buildObject({ 180 | name: attributeKey, 181 | value: attributes[attributeKey], 182 | })) 183 | .join('') 184 | 185 | // Send the update 186 | await this.platform.httpClient.sendDeviceUpdate( 187 | this.accessory, 188 | 'urn:Belkin:service:deviceevent:1', 189 | 'SetAttributes', 190 | { 191 | attributeList: { '#text': xmlAttributes }, 192 | }, 193 | ) 194 | } 195 | 196 | async internalStateUpdate(value) { 197 | const prevState = this.service.getCharacteristic(this.hapChar.Active).value 198 | try { 199 | // Don't continue if the state is the same as before 200 | if (value === prevState) { 201 | return 202 | } 203 | 204 | // We also want to update the mode (by rotation speed) 205 | let newRotSpeed = 0 206 | if (value !== 0) { 207 | // If turning on then we want to show the last used mode (by rotation speed) 208 | switch (this.accessory.context.cacheLastOnMode) { 209 | case 2: 210 | newRotSpeed = 99 211 | break 212 | case 3: 213 | newRotSpeed = 66 214 | break 215 | default: 216 | newRotSpeed = 33 217 | } 218 | } 219 | 220 | // Update the rotation speed, use setCharacteristic so the set handler is run to send updates 221 | this.service.setCharacteristic(this.hapChar.RotationSpeed, newRotSpeed) 222 | } catch (err) { 223 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 224 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 225 | 226 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 227 | setTimeout(() => { 228 | this.service.updateCharacteristic(this.hapChar.Active, prevState) 229 | }, 2000) 230 | throw new this.hapErr(-70402) 231 | } 232 | } 233 | 234 | async internalModeUpdate(value) { 235 | const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value 236 | try { 237 | // Avoid multiple updates in quick succession 238 | const updateKeyMode = generateRandomString(5) 239 | this.updateKeyMode = updateKeyMode 240 | await sleep(500) 241 | if (updateKeyMode !== this.updateKeyMode) { 242 | return 243 | } 244 | 245 | // Generate newValue for the needed mode and newSpeed in 33% multiples 246 | let newValue = 1 247 | let newSpeed = 0 248 | if (value > 25 && value <= 50) { 249 | newValue = 4 250 | newSpeed = 33 251 | } else if (value > 50 && value <= 75) { 252 | newValue = 3 253 | newSpeed = 66 254 | } else if (value > 75) { 255 | newValue = 2 256 | newSpeed = 99 257 | } 258 | 259 | // Don't continue if the speed is the same as before 260 | if (newSpeed === prevSpeed) { 261 | return 262 | } 263 | 264 | // Send the update 265 | await this.sendDeviceUpdate({ 266 | Mode: newValue, 267 | SetTemperature: this.cToF[Number.parseInt(this.accessory.context.cacheLastOnTemp, 10)], 268 | }) 269 | 270 | // Update the cache last used mode if not turning off 271 | if (newValue !== 1) { 272 | this.accessory.context.cacheLastOnMode = newValue 273 | } 274 | 275 | // Log the new mode if appropriate 276 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`) 277 | } catch (err) { 278 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 279 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 280 | 281 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 282 | setTimeout(() => { 283 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed) 284 | }, 2000) 285 | throw new this.hapErr(-70402) 286 | } 287 | } 288 | 289 | async internalTargetTempUpdate(value) { 290 | const prevTemp = this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value 291 | try { 292 | // Avoid multiple updates in quick succession 293 | const updateKeyTemp = generateRandomString(5) 294 | this.updateKeyTemp = updateKeyTemp 295 | await sleep(500) 296 | if (updateKeyTemp !== this.updateKeyTemp) { 297 | return 298 | } 299 | 300 | // We want an integer target temp value and to not continue if this is the same as before 301 | value = Number.parseInt(value, 10) 302 | if (value === prevTemp) { 303 | return 304 | } 305 | 306 | // Send the update 307 | await this.sendDeviceUpdate({ SetTemperature: this.cToF[value] }) 308 | 309 | // Update the cache and log if appropriate 310 | this.accessory.context.cacheLastOnTemp = value 311 | this.accessory.log(`${platformLang.tarTemp} [${value}°C]`) 312 | } catch (err) { 313 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 314 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 315 | 316 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 317 | setTimeout(() => { 318 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, prevTemp) 319 | }, 2000) 320 | throw new this.hapErr(-70402) 321 | } 322 | } 323 | 324 | externalModeUpdate(value) { 325 | try { 326 | // We want to find a rotation speed based on the given mode 327 | let rotSpeed = 0 328 | switch (value) { 329 | case 2: { 330 | rotSpeed = 99 331 | break 332 | } 333 | case 3: { 334 | rotSpeed = 66 335 | break 336 | } 337 | case 4: { 338 | rotSpeed = 33 339 | break 340 | } 341 | default: 342 | return 343 | } 344 | 345 | // Update the HomeKit characteristics 346 | this.service.updateCharacteristic(this.hapChar.Active, value !== 1 ? 1 : 0) 347 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed) 348 | 349 | // Update the last used mode if the device is not off 350 | if (value !== 1) { 351 | this.accessory.context.cacheLastOnMode = value 352 | } 353 | 354 | // Log the change of mode if appropriate 355 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`) 356 | } catch (err) { 357 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 358 | } 359 | } 360 | 361 | externalTargetTempUpdate(value) { 362 | try { 363 | // Don't continue if receiving frost-protect temperature (°C or °F) 364 | if (value === 4 || value === 40) { 365 | return 366 | } 367 | 368 | // A value greater than 50 normally means °F, so convert to °C 369 | if (value > 50) { 370 | value = Math.round(((value - 32) * 5) / 9) 371 | } 372 | 373 | // Make sure the value is in the [16, 29] range 374 | value = Math.max(Math.min(value, 29), 16) 375 | 376 | // Check if the new target temperature is different from the current target temperature 377 | if ( 378 | this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value !== value 379 | ) { 380 | // Update the target temperature HomeKit characteristic 381 | this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, value) 382 | 383 | // Log the change if appropriate 384 | this.accessory.log(`${platformLang.tarTemp} [${value}°C]`) 385 | } 386 | 387 | // Update the last-ON-target-temp cache 388 | this.accessory.context.cacheLastOnTemp = value 389 | } catch (err) { 390 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 391 | } 392 | } 393 | 394 | externalCurrentTempUpdate(value) { 395 | try { 396 | // A value greater than 50 normally means °F, so convert to °C 397 | if (value > 50) { 398 | value = Math.round(((value - 32) * 5) / 9) 399 | } 400 | 401 | // Don't continue if new current temperature is the same as before 402 | if (this.cacheTemp === value) { 403 | return 404 | } 405 | 406 | // Update the current temperature HomeKit characteristic 407 | this.service.updateCharacteristic(this.hapChar.CurrentTemperature, value) 408 | 409 | // Update the cache and log the change if appropriate 410 | this.cacheTemp = value 411 | this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`) 412 | } catch (err) { 413 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /lib/device/humidifier.js: -------------------------------------------------------------------------------- 1 | import { Builder, parseStringPromise } from 'xml2js' 2 | 3 | import { 4 | decodeXML, 5 | generateRandomString, 6 | parseError, 7 | sleep, 8 | } from '../utils/functions.js' 9 | import platformLang from '../utils/lang-en.js' 10 | 11 | export default class { 12 | constructor(platform, accessory) { 13 | // Set up variables from the platform 14 | this.hapChar = platform.api.hap.Characteristic 15 | this.hapErr = platform.api.hap.HapStatusError 16 | this.hapServ = platform.api.hap.Service 17 | this.platform = platform 18 | 19 | // Set up variables from the accessory 20 | this.accessory = accessory 21 | 22 | // Add the humidifier service if it doesn't already exist 23 | this.service = this.accessory.getService(this.hapServ.HumidifierDehumidifier) 24 | || this.accessory.addService(this.hapServ.HumidifierDehumidifier) 25 | 26 | // Add the set handler to the humidifier active characteristic 27 | this.service 28 | .getCharacteristic(this.hapChar.Active) 29 | .removeOnSet() 30 | .onSet(async value => this.internalStateUpdate(value)) 31 | 32 | // Add options to the humidifier target state characteristic 33 | this.service 34 | .getCharacteristic(this.hapChar.TargetHumidifierDehumidifierState) 35 | .updateValue(1) 36 | .setProps({ 37 | minValue: 1, 38 | maxValue: 1, 39 | validValues: [1], 40 | }) 41 | 42 | // Add the set handler to the humidifier target relative humidity characteristic 43 | this.service 44 | .getCharacteristic(this.hapChar.RelativeHumidityHumidifierThreshold) 45 | .onSet(async (value) => { 46 | await this.internalTargetHumidityUpdate(value) 47 | }) 48 | 49 | // Add the set handler to the humidifier target state characteristic 50 | this.service 51 | .getCharacteristic(this.hapChar.RotationSpeed) 52 | .setProps({ minStep: 20 }) 53 | .onSet(async (value) => { 54 | await this.internalModeUpdate(value) 55 | }) 56 | 57 | // Add a last mode cache value if not already set 58 | const cacheMode = this.accessory.context.cacheLastOnMode 59 | if (!cacheMode || cacheMode === 0) { 60 | this.accessory.context.cacheLastOnMode = 1 61 | } 62 | 63 | // Some conversion objects 64 | this.modeLabels = { 65 | 0: platformLang.labelOff, 66 | 1: platformLang.labelMin, 67 | 2: platformLang.labelLow, 68 | 3: platformLang.labelMed, 69 | 4: platformLang.labelHigh, 70 | 5: platformLang.labelMax, 71 | } 72 | this.hToWemoFormat = { 73 | 45: 0, 74 | 50: 1, 75 | 55: 2, 76 | 60: 3, 77 | 100: 4, 78 | } 79 | this.wemoFormatToH = { 80 | 0: 45, 81 | 1: 50, 82 | 2: 55, 83 | 3: 60, 84 | 4: 100, 85 | } 86 | 87 | // Output the customised options to the log 88 | const opts = JSON.stringify({ 89 | }) 90 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 91 | 92 | // Request a device update immediately 93 | this.requestDeviceUpdate() 94 | 95 | // Start a polling interval if the user has disabled upnp 96 | if (this.accessory.context.connection === 'http') { 97 | this.pollingInterval = setInterval( 98 | () => this.requestDeviceUpdate(), 99 | platform.config.pollingInterval * 1000, 100 | ) 101 | } 102 | } 103 | 104 | receiveDeviceUpdate(attribute) { 105 | // Log the receiving update if debug is enabled 106 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 107 | 108 | // Check which attribute we are getting 109 | switch (attribute.name) { 110 | case 'FanMode': 111 | this.externalModeUpdate(attribute.value) 112 | break 113 | case 'CurrentHumidity': 114 | this.externalCurrentHumidityUpdate(attribute.value) 115 | break 116 | case 'DesiredHumidity': 117 | this.externalTargetHumidityUpdate(attribute.value) 118 | break 119 | default: 120 | } 121 | } 122 | 123 | async requestDeviceUpdate() { 124 | try { 125 | // Request the update 126 | const data = await this.platform.httpClient.sendDeviceUpdate( 127 | this.accessory, 128 | 'urn:Belkin:service:deviceevent:1', 129 | 'GetAttributes', 130 | ) 131 | 132 | // Parse the response 133 | const decoded = decodeXML(data.attributeList) 134 | const xml = `${decoded}` 135 | const result = await parseStringPromise(xml, { explicitArray: false }) 136 | Object.keys(result.attributeList.attribute).forEach((key) => { 137 | // Only send the required attributes to the receiveDeviceUpdate function 138 | switch (result.attributeList.attribute[key].name) { 139 | case 'FanMode': 140 | case 'CurrentHumidity': 141 | case 'DesiredHumidity': 142 | this.receiveDeviceUpdate({ 143 | name: result.attributeList.attribute[key].name, 144 | value: Number.parseInt(result.attributeList.attribute[key].value, 10), 145 | }) 146 | break 147 | default: 148 | } 149 | }) 150 | } catch (err) { 151 | const eText = parseError(err, [ 152 | platformLang.timeout, 153 | platformLang.timeoutUnreach, 154 | platformLang.noService, 155 | ]) 156 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 157 | } 158 | } 159 | 160 | async sendDeviceUpdate(attributes) { 161 | // Log the sending update if debug is enabled 162 | this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`) 163 | 164 | // Generate the XML to send 165 | const builder = new Builder({ 166 | rootName: 'attribute', 167 | headless: true, 168 | renderOpts: { pretty: false }, 169 | }) 170 | const xmlAttributes = Object.keys(attributes) 171 | .map(attributeKey => builder.buildObject({ 172 | name: attributeKey, 173 | value: attributes[attributeKey], 174 | })) 175 | .join('') 176 | 177 | // Send the update 178 | await this.platform.httpClient.sendDeviceUpdate( 179 | this.accessory, 180 | 'urn:Belkin:service:deviceevent:1', 181 | 'SetAttributes', 182 | { 183 | attributeList: { '#text': xmlAttributes }, 184 | }, 185 | ) 186 | } 187 | 188 | async internalStateUpdate(value) { 189 | const prevState = this.service.getCharacteristic(this.hapChar.Active).value 190 | try { 191 | // Don't continue if the state is the same as before 192 | if (value === prevState) { 193 | return 194 | } 195 | 196 | // We also want to update the mode by rotation speed when turning on/off 197 | // Use the set handler to run the RotationSpeed set handler, to send updates to device 198 | this.service.setCharacteristic( 199 | this.hapChar.RotationSpeed, 200 | value === 0 ? 0 : this.accessory.context.cacheLastOnMode * 20, 201 | ) 202 | } catch (err) { 203 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 204 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 205 | 206 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 207 | setTimeout(() => { 208 | this.service.updateCharacteristic(this.hapChar.Active, prevState) 209 | }, 2000) 210 | throw new this.hapErr(-70402) 211 | } 212 | } 213 | 214 | async internalModeUpdate(value) { 215 | const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value 216 | try { 217 | // Avoid multiple updates in quick succession 218 | const updateKeyMode = generateRandomString(5) 219 | this.updateKeyMode = updateKeyMode 220 | await sleep(500) 221 | if (updateKeyMode !== this.updateKeyMode) { 222 | return 223 | } 224 | 225 | // Find the new needed mode from the given rotation speed 226 | let newValue = 0 227 | if (value > 10 && value <= 30) { 228 | newValue = 1 229 | } else if (value > 30 && value <= 50) { 230 | newValue = 2 231 | } else if (value > 50 && value <= 70) { 232 | newValue = 3 233 | } else if (value > 70 && value <= 90) { 234 | newValue = 4 235 | } else if (value > 90) { 236 | newValue = 5 237 | } 238 | 239 | // Don't continue if the rotation speed is the same as before 240 | if (value === prevSpeed) { 241 | return 242 | } 243 | 244 | // Send the update 245 | await this.sendDeviceUpdate({ 246 | FanMode: newValue.toString(), 247 | }) 248 | 249 | // Update the last used mode cache if rotation speed is not 0 250 | if (newValue !== 0) { 251 | this.accessory.context.cacheLastOnMode = newValue 252 | } 253 | 254 | // Log the update if appropriate 255 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`) 256 | } catch (err) { 257 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 258 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 259 | 260 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 261 | setTimeout(() => { 262 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed) 263 | }, 2000) 264 | throw new this.hapErr(-70402) 265 | } 266 | } 267 | 268 | async internalTargetHumidityUpdate(value) { 269 | const prevHumi = this.service.getCharacteristic( 270 | this.hapChar.RelativeHumidityHumidifierThreshold, 271 | ).value 272 | try { 273 | // Avoid multiple updates in quick succession 274 | const updateKeyHumi = generateRandomString(5) 275 | this.updateKeyHumi = updateKeyHumi 276 | await sleep(500) 277 | if (updateKeyHumi !== this.updateKeyHumi) { 278 | return 279 | } 280 | 281 | // Find the new target humidity mode from the target humidity given 282 | let newValue = 45 283 | if (value >= 47 && value < 52) { 284 | newValue = 50 285 | } else if (value >= 52 && value < 57) { 286 | newValue = 55 287 | } else if (value >= 57 && value < 80) { 288 | newValue = 60 289 | } else if (value >= 80) { 290 | newValue = 100 291 | } 292 | 293 | // Don't continue if the new mode is the same as before 294 | if (newValue === prevHumi) { 295 | return 296 | } 297 | 298 | // Send the update 299 | await this.sendDeviceUpdate({ 300 | DesiredHumidity: this.hToWemoFormat[newValue], 301 | }) 302 | 303 | // Log the change if appropriate 304 | this.accessory.log(`${platformLang.tarHumi} [${newValue}%]`) 305 | } catch (err) { 306 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 307 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 308 | 309 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 310 | setTimeout(() => { 311 | this.service.updateCharacteristic( 312 | this.hapChar.RelativeHumidityHumidifierThreshold, 313 | prevHumi, 314 | ) 315 | }, 2000) 316 | throw new this.hapErr(-70402) 317 | } 318 | } 319 | 320 | externalModeUpdate(value) { 321 | try { 322 | // Find the needed rotation speed from the given mode 323 | const rotSpeed = value * 20 324 | 325 | // Update the HomeKit characteristics 326 | this.service.updateCharacteristic(this.hapChar.Active, value !== 0 ? 1 : 0) 327 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed) 328 | 329 | // Update the last used mode if not off 330 | if (value !== 0) { 331 | this.accessory.context.cacheLastOnMode = value 332 | } 333 | 334 | // Log the change if appropriate 335 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`) 336 | } catch (err) { 337 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 338 | } 339 | } 340 | 341 | externalTargetHumidityUpdate(value) { 342 | try { 343 | // Find the HomeKit value version from the given target humidity mode 344 | value = this.wemoFormatToH[value] 345 | 346 | // Don't continue if the new target is the same as the current target 347 | const t = this.service.getCharacteristic(this.hapChar.RelativeHumidityHumidifierThreshold) 348 | .value 349 | if (t === value) { 350 | return 351 | } 352 | 353 | // Update the target humidity HomeKit characteristics 354 | this.service.updateCharacteristic(this.hapChar.RelativeHumidityHumidifierThreshold, value) 355 | 356 | // Log the change if appropriate 357 | this.accessory.log(`${platformLang.tarHumi} [${value}%]`) 358 | } catch (err) { 359 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 360 | } 361 | } 362 | 363 | externalCurrentHumidityUpdate(value) { 364 | try { 365 | // Don't continue if the new current humidity is the same as before 366 | if (this.service.getCharacteristic(this.hapChar.CurrentRelativeHumidity).value === value) { 367 | return 368 | } 369 | 370 | // Update the current relative humidity HomeKit characteristic 371 | this.service.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, value) 372 | 373 | // Log the change if appropriate 374 | this.accessory.log(`${platformLang.curHumi} [${value}%]`) 375 | } catch (err) { 376 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /lib/device/index.js: -------------------------------------------------------------------------------- 1 | import deviceCoffee from './coffee.js' 2 | import deviceCrockpot from './crockpot.js' 3 | import deviceDimmer from './dimmer.js' 4 | import deviceHeater from './heater.js' 5 | import deviceHumidifier from './humidifier.js' 6 | import deviceInsight from './insight.js' 7 | import deviceLightSwitch from './lightswitch.js' 8 | import deviceLinkBulb from './link-bulb.js' 9 | import deviceLinkHub from './link-hub.js' 10 | import deviceMakerGarage from './maker-garage.js' 11 | import deviceMakerSwitch from './maker-switch.js' 12 | import deviceMotion from './motion.js' 13 | import deviceOutlet from './outlet.js' 14 | import devicePurifier from './purifier.js' 15 | import deviceSimPurifierInsight from './simulation/purifier-insight.js' 16 | import deviceSimPurifier from './simulation/purifier.js' 17 | import deviceSimSwitchInsight from './simulation/switch-insight.js' 18 | import deviceSimSwitch from './simulation/switch.js' 19 | 20 | export default { 21 | deviceCoffee, 22 | deviceCrockpot, 23 | deviceDimmer, 24 | deviceHeater, 25 | deviceHumidifier, 26 | deviceInsight, 27 | deviceLightSwitch, 28 | deviceLinkBulb, 29 | deviceLinkHub, 30 | deviceMakerGarage, 31 | deviceMakerSwitch, 32 | deviceMotion, 33 | deviceOutlet, 34 | devicePurifier, 35 | deviceSimPurifierInsight, 36 | deviceSimPurifier, 37 | deviceSimSwitchInsight, 38 | deviceSimSwitch, 39 | } 40 | -------------------------------------------------------------------------------- /lib/device/insight.js: -------------------------------------------------------------------------------- 1 | import platformConsts from '../utils/constants.js' 2 | import { hasProperty, parseError } from '../utils/functions.js' 3 | import platformLang from '../utils/lang-en.js' 4 | 5 | export default class { 6 | constructor(platform, accessory) { 7 | // Set up variables from the platform 8 | this.eveChar = platform.eveChar 9 | this.hapChar = platform.api.hap.Characteristic 10 | this.hapErr = platform.api.hap.HapStatusError 11 | this.hapServ = platform.api.hap.Service 12 | this.platform = platform 13 | 14 | // Set up variables from the accessory 15 | this.accessory = accessory 16 | 17 | // Set up custom variables for this device type 18 | const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {} 19 | this.showTodayTC = deviceConf.showTodayTC 20 | this.outletInUseTrue = deviceConf.outletInUseTrue 21 | this.wattDiff = deviceConf.wattDiff || platformConsts.defaultValues.wattDiff 22 | this.timeDiff = deviceConf.timeDiff || platformConsts.defaultValues.timeDiff 23 | if (this.timeDiff === 1) { 24 | this.timeDiff = false 25 | } 26 | this.skipTimeDiff = false 27 | 28 | if (!hasProperty(this.accessory.context, 'cacheLastWM')) { 29 | this.accessory.context.cacheLastWM = 0 30 | } 31 | if (!hasProperty(this.accessory.context, 'cacheLastTC')) { 32 | this.accessory.context.cacheLastTC = 0 33 | } 34 | if (!hasProperty(this.accessory.context, 'cacheTotalTC')) { 35 | this.accessory.context.cacheTotalTC = 0 36 | } 37 | 38 | // If the accessory has an air purifier service then remove it 39 | if (this.accessory.getService(this.hapServ.AirPurifier)) { 40 | this.accessory.removeService(this.accessory.getService(this.hapServ.AirPurifier)) 41 | } 42 | 43 | // If the accessory has a switch service then remove it 44 | if (this.accessory.getService(this.hapServ.Switch)) { 45 | this.accessory.removeService(this.accessory.getService(this.hapServ.Switch)) 46 | } 47 | 48 | // Add the outlet service if it doesn't already exist 49 | this.service = this.accessory.getService(this.hapServ.Outlet) 50 | if (!this.service) { 51 | this.service = this.accessory.addService(this.hapServ.Outlet) 52 | this.service.addCharacteristic(this.eveChar.CurrentConsumption) 53 | this.service.addCharacteristic(this.eveChar.TotalConsumption) 54 | this.service.addCharacteristic(this.eveChar.ResetTotal) 55 | } 56 | 57 | // Add the set handler to the outlet on/off characteristic 58 | this.service 59 | .getCharacteristic(this.hapChar.On) 60 | .removeOnSet() 61 | .onSet(async value => this.internalStateUpdate(value)) 62 | 63 | // Add the set handler to the switch reset (eve) characteristic 64 | this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => { 65 | this.accessory.context.cacheLastWM = 0 66 | this.accessory.context.cacheLastTC = 0 67 | this.accessory.context.cacheTotalTC = 0 68 | this.service.updateCharacteristic(this.eveChar.TotalConsumption, 0) 69 | }) 70 | 71 | if (this.outletInUseTrue) { 72 | // Keep this as true, and throughout this file, to keep the outlet in use as true 73 | this.service.updateCharacteristic(this.hapChar.OutletInUse, true) 74 | } 75 | 76 | // Pass the accessory to fakegato to set up the Eve info service 77 | this.accessory.historyService = new platform.eveService('energy', this.accessory, { 78 | log: () => {}, 79 | }) 80 | 81 | // Output the customised options to the log 82 | const opts = JSON.stringify({ 83 | showAs: 'outlet', 84 | showTodayTC: this.showTodayTC, 85 | timeDiff: this.timeDiff, 86 | wattDiff: this.wattDiff, 87 | }) 88 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 89 | 90 | // Request a device update immediately 91 | this.requestDeviceUpdate() 92 | 93 | // Start a polling interval if the user has disabled upnp 94 | if (this.accessory.context.connection === 'http') { 95 | this.pollingInterval = setInterval( 96 | () => this.requestDeviceUpdate(), 97 | platform.config.pollingInterval * 1000, 98 | ) 99 | } 100 | } 101 | 102 | receiveDeviceUpdate(attribute) { 103 | // Log the receiving update if debug is enabled 104 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 105 | 106 | // Let's see which attribute has been provided 107 | switch (attribute.name) { 108 | case 'BinaryState': { 109 | // BinaryState is reported as 0=off, 1=on, 8=standby 110 | // Send a HomeKit needed true/false argument (0=false, 1,8=true) 111 | this.externalStateUpdate(attribute.value !== 0) 112 | break 113 | } 114 | case 'InsightParams': 115 | // Send the insight data straight to the function 116 | this.externalInsightUpdate( 117 | attribute.value.state, 118 | attribute.value.power, 119 | attribute.value.todayWm, 120 | attribute.value.todayOnSeconds, 121 | ) 122 | break 123 | default: 124 | } 125 | } 126 | 127 | async sendDeviceUpdate(value) { 128 | // Log the sending update if debug is enabled 129 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 130 | 131 | // Send the update 132 | await this.platform.httpClient.sendDeviceUpdate( 133 | this.accessory, 134 | 'urn:Belkin:service:basicevent:1', 135 | 'SetBinaryState', 136 | value, 137 | ) 138 | } 139 | 140 | async requestDeviceUpdate() { 141 | try { 142 | // Request the update 143 | const data = await this.platform.httpClient.sendDeviceUpdate( 144 | this.accessory, 145 | 'urn:Belkin:service:basicevent:1', 146 | 'GetBinaryState', 147 | ) 148 | 149 | // Check for existence since BinaryState can be int 0 150 | if (hasProperty(data, 'BinaryState')) { 151 | this.receiveDeviceUpdate({ 152 | name: 'BinaryState', 153 | value: Number.parseInt(data.BinaryState, 10), 154 | }) 155 | } 156 | } catch (err) { 157 | const eText = parseError(err, [ 158 | platformLang.timeout, 159 | platformLang.timeoutUnreach, 160 | platformLang.noService, 161 | ]) 162 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 163 | } 164 | } 165 | 166 | async internalStateUpdate(value) { 167 | try { 168 | // Send the update 169 | await this.sendDeviceUpdate({ 170 | BinaryState: value ? 1 : 0, 171 | }) 172 | 173 | // Update the cache value 174 | this.cacheState = value 175 | 176 | // Log the change if appropriate 177 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 178 | 179 | // If turning the switch off then update the outlet-in-use and current consumption 180 | if (!value) { 181 | // Update the HomeKit characteristics 182 | if (!this.outletInUseTrue) { 183 | this.service.updateCharacteristic(this.hapChar.OutletInUse, false) 184 | } 185 | this.service.updateCharacteristic(this.eveChar.CurrentConsumption, 0) 186 | 187 | // Add an Eve entry for no power 188 | this.accessory.historyService.addEntry({ power: 0 }) 189 | 190 | // Log the change if appropriate 191 | this.accessory.log(`${platformLang.curOIU} [no]`) 192 | this.accessory.log(`${platformLang.curCons} [0W]`) 193 | } 194 | } catch (err) { 195 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 196 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 197 | 198 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 199 | setTimeout(() => { 200 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 201 | }, 2000) 202 | throw new this.hapErr(-70402) 203 | } 204 | } 205 | 206 | externalInsightUpdate(value, power, todayWm, todayOnSeconds) { 207 | // Update whether the switch is ON (value=1) or OFF (value=0) 208 | this.externalStateUpdate(value !== 0) 209 | 210 | // Update whether the outlet-in-use is YES (value=1) or NO (value=0,8) 211 | this.externalInUseUpdate(value === 1) 212 | 213 | // Update the total consumption 214 | this.externalTotalConsumptionUpdate(todayWm, todayOnSeconds) 215 | 216 | // Update the current consumption 217 | this.externalConsumptionUpdate(power) 218 | } 219 | 220 | externalStateUpdate(value) { 221 | try { 222 | // Check to see if the cache value is different 223 | if (value === this.cacheState) { 224 | return 225 | } 226 | 227 | // Update the HomeKit characteristics 228 | this.service.updateCharacteristic(this.hapChar.On, value) 229 | 230 | // Update the cache value 231 | this.cacheState = value 232 | 233 | // Log the change if appropriate 234 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 235 | 236 | // If the device has turned off then update the outlet-in-use and consumption 237 | if (!value) { 238 | this.externalInUseUpdate(false) 239 | this.externalConsumptionUpdate(0) 240 | } 241 | } catch (err) { 242 | // Catch any errors 243 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 244 | } 245 | } 246 | 247 | externalInUseUpdate(value) { 248 | try { 249 | // Check to see if the cache value is different 250 | if (value === this.cacheInUse) { 251 | return 252 | } 253 | 254 | // Update the HomeKit characteristic 255 | if (!this.outletInUseTrue) { 256 | this.service.updateCharacteristic(this.hapChar.OutletInUse, value) 257 | } 258 | 259 | // Update the cache value 260 | this.cacheInUse = value 261 | 262 | // Log the change if appropriate 263 | this.accessory.log(`${platformLang.curOIU} [${value ? 'yes' : 'no'}]`) 264 | } catch (err) { 265 | // Catch any errors 266 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 267 | } 268 | } 269 | 270 | externalConsumptionUpdate(power) { 271 | try { 272 | // Divide by 1000 to get the power value in W 273 | const powerInWatts = Math.round(power / 1000) 274 | 275 | // Check to see if the cache value is different 276 | if (powerInWatts === this.cachePowerInWatts) { 277 | return 278 | } 279 | 280 | // Update the power in watts cache 281 | this.cachePowerInWatts = powerInWatts 282 | 283 | // Update the HomeKit characteristic 284 | this.service.updateCharacteristic(this.eveChar.CurrentConsumption, this.cachePowerInWatts) 285 | 286 | // Add the Eve wattage entry 287 | this.accessory.historyService.addEntry({ power: this.cachePowerInWatts }) 288 | 289 | // Calculate a difference from the last reading 290 | const diff = Math.abs(powerInWatts - this.cachePowerInWatts) 291 | 292 | // Don't continue with logging if the user has set a timeout between entries or a min difference between entries 293 | if (!this.skipTimeDiff && diff >= this.wattDiff) { 294 | // Log the change if appropriate 295 | this.accessory.log(`${platformLang.curCons} [${this.cachePowerInWatts}W]`) 296 | 297 | // Set the time difference timeout if needed 298 | if (this.timeDiff) { 299 | this.skipTimeDiff = true 300 | setTimeout(() => { 301 | this.skipTimeDiff = false 302 | }, this.timeDiff * 1000) 303 | } 304 | } 305 | } catch (err) { 306 | // Catch any errors 307 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 308 | } 309 | } 310 | 311 | externalTotalConsumptionUpdate(todayWm, todayOnSeconds) { 312 | try { 313 | if (todayWm === this.cacheLastWM) { 314 | return 315 | } 316 | 317 | // Update the cache last value 318 | this.cacheLastWM = todayWm 319 | this.accessory.context.cacheLastWM = todayWm 320 | 321 | // Convert to Wh (hours) from raw data of Wm (minutes) 322 | const todayWh = Math.round(todayWm / 60000) 323 | 324 | // Convert to kWh 325 | const todaykWh = todayWh / 1000 326 | 327 | // Convert to hours, minutes and seconds (HH:MM:SS) 328 | const todayOnHours = new Date(todayOnSeconds * 1000).toISOString().substr(11, 8) 329 | 330 | // Calculate the difference (ie extra usage from the last reading) 331 | const difference = Math.max(todaykWh - this.accessory.context.cacheLastTC, 0) 332 | 333 | // Update the caches 334 | this.accessory.context.cacheTotalTC += difference 335 | this.accessory.context.cacheLastTC = todaykWh 336 | 337 | // Update the total consumption characteristic 338 | this.service.updateCharacteristic( 339 | this.eveChar.TotalConsumption, 340 | this.showTodayTC ? todaykWh : this.accessory.context.cacheTotalTC, 341 | ) 342 | 343 | if (!this.skipTimeDiff) { 344 | this.accessory.log( 345 | `${platformLang.insOnTime} [${todayOnHours}] ${platformLang.insCons} [${todaykWh.toFixed(3)} kWh] ${platformLang.insTC} [${this.accessory.context.cacheTotalTC.toFixed(3)} kWh]`, 346 | ) 347 | } 348 | } catch (err) { 349 | // Catch any errors 350 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /lib/device/lightswitch.js: -------------------------------------------------------------------------------- 1 | import { hasProperty, parseError } from '../utils/functions.js' 2 | import platformLang from '../utils/lang-en.js' 3 | 4 | export default class { 5 | constructor(platform, accessory) { 6 | // Set up variables from the platform 7 | this.hapChar = platform.api.hap.Characteristic 8 | this.hapErr = platform.api.hap.HapStatusError 9 | this.hapServ = platform.api.hap.Service 10 | this.platform = platform 11 | 12 | // Set up variables from the accessory 13 | this.accessory = accessory 14 | 15 | // If the accessory has an outlet service then remove it 16 | if (this.accessory.getService(this.hapServ.Outlet)) { 17 | this.accessory.removeService(this.accessory.getService(this.hapServ.Outlet)) 18 | } 19 | 20 | // If the accessory has an air purifier service then remove it 21 | if (this.accessory.getService(this.hapServ.AirPurifier)) { 22 | this.accessory.removeService(this.accessory.getService(this.hapServ.AirPurifier)) 23 | } 24 | 25 | // Add the switch service if it doesn't already exist 26 | this.service = this.accessory.getService(this.hapServ.Switch) 27 | || this.accessory.addService(this.hapServ.Switch) 28 | 29 | // Add the set handler to the switch on/off characteristic 30 | this.service 31 | .getCharacteristic(this.hapChar.On) 32 | .removeOnSet() 33 | .onSet(async value => this.internalStateUpdate(value)) 34 | 35 | // Pass the accessory to fakegato to set up the eve info service 36 | this.accessory.historyService = new platform.eveService('switch', this.accessory, { 37 | log: () => {}, 38 | }) 39 | 40 | // Output the customised options to the log 41 | const opts = JSON.stringify({ 42 | showAs: 'switch', 43 | }) 44 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 45 | 46 | // Request a device update immediately 47 | this.requestDeviceUpdate() 48 | 49 | // Start a polling interval if the user has disabled upnp 50 | if (this.accessory.context.connection === 'http') { 51 | this.pollingInterval = setInterval( 52 | () => this.requestDeviceUpdate(), 53 | platform.config.pollingInterval * 1000, 54 | ) 55 | } 56 | } 57 | 58 | receiveDeviceUpdate(attribute) { 59 | // Log the receiving update if debug is enabled 60 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 61 | 62 | // Send a HomeKit needed true/false argument 63 | // attribute.value is 0 if and only if the switch is off 64 | this.externalStateUpdate(attribute.value !== 0) 65 | } 66 | 67 | async sendDeviceUpdate(value) { 68 | // Log the sending update if debug is enabled 69 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 70 | 71 | // Send the update 72 | await this.platform.httpClient.sendDeviceUpdate( 73 | this.accessory, 74 | 'urn:Belkin:service:basicevent:1', 75 | 'SetBinaryState', 76 | value, 77 | ) 78 | } 79 | 80 | async requestDeviceUpdate() { 81 | try { 82 | // Request the update 83 | const data = await this.platform.httpClient.sendDeviceUpdate( 84 | this.accessory, 85 | 'urn:Belkin:service:basicevent:1', 86 | 'GetBinaryState', 87 | ) 88 | 89 | // Check for existence since BinaryState can be int 0 90 | if (hasProperty(data, 'BinaryState')) { 91 | // Send the data to the receiver function 92 | this.receiveDeviceUpdate({ 93 | name: 'BinaryState', 94 | value: Number.parseInt(data.BinaryState, 10), 95 | }) 96 | } 97 | } catch (err) { 98 | const eText = parseError(err, [ 99 | platformLang.timeout, 100 | platformLang.timeoutUnreach, 101 | platformLang.noService, 102 | ]) 103 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 104 | } 105 | } 106 | 107 | async internalStateUpdate(value) { 108 | try { 109 | // Send the update 110 | await this.sendDeviceUpdate({ 111 | BinaryState: value ? 1 : 0, 112 | }) 113 | 114 | // Update the cache value 115 | this.cacheState = value 116 | this.accessory.historyService.addEntry({ status: value ? 1 : 0 }) 117 | 118 | // Log the change if appropriate 119 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 120 | } catch (err) { 121 | // Catch any errors 122 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 123 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 124 | 125 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 126 | setTimeout(() => { 127 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 128 | }, 2000) 129 | throw new this.hapErr(-70402) 130 | } 131 | } 132 | 133 | externalStateUpdate(value) { 134 | try { 135 | // Check to see if the cache value is different 136 | if (value === this.cacheState) { 137 | return 138 | } 139 | 140 | // Update the HomeKit characteristic 141 | this.service.updateCharacteristic(this.hapChar.On, value) 142 | 143 | // Update the cache value 144 | this.cacheState = value 145 | 146 | // Log the change if appropriate 147 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 148 | this.accessory.historyService.addEntry({ status: value ? 1 : 0 }) 149 | } catch (err) { 150 | // Catch any errors 151 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/device/link-hub.js: -------------------------------------------------------------------------------- 1 | import { create as xmlCreate } from 'xmlbuilder' 2 | 3 | export default class { 4 | constructor(platform, accessory, devicesInHB) { 5 | // Set up variables from the platform 6 | this.devicesInHB = devicesInHB 7 | this.hapChar = platform.api.hap.Characteristic 8 | this.hapErr = platform.api.hap.HapStatusError 9 | this.hapServ = platform.api.hap.Service 10 | this.platform = platform 11 | 12 | // Set up variables from the accessory 13 | this.accessory = accessory 14 | } 15 | 16 | receiveDeviceUpdate(deviceId, attribute) { 17 | // Find the accessory to which this relates 18 | this.devicesInHB.forEach((accessory) => { 19 | if ( 20 | accessory.context.serialNumber === deviceId 21 | && accessory.control?.receiveDeviceUpdate 22 | ) { 23 | accessory.control.receiveDeviceUpdate(attribute) 24 | } 25 | }) 26 | } 27 | 28 | async sendDeviceUpdate(deviceId, capability, value) { 29 | // Generate the XML to send 30 | const deviceStatusList = xmlCreate('DeviceStatus', { 31 | version: '1.0', 32 | encoding: 'utf-8', 33 | }) 34 | .ele({ 35 | IsGroupAction: deviceId.length === 10 ? 'YES' : 'NO', 36 | DeviceID: deviceId, 37 | CapabilityID: capability, 38 | CapabilityValue: value, 39 | }) 40 | .end() 41 | 42 | // Send the update 43 | return await this.platform.httpClient.sendDeviceUpdate( 44 | this.accessory, 45 | 'urn:Belkin:service:bridge:1', 46 | 'SetDeviceStatus', 47 | { 48 | DeviceStatusList: { '#text': deviceStatusList }, 49 | }, 50 | ) 51 | } 52 | 53 | async requestDeviceUpdate(deviceId) { 54 | return await this.platform.httpClient.sendDeviceUpdate( 55 | this.accessory, 56 | 'urn:Belkin:service:bridge:1', 57 | 'GetDeviceStatus', 58 | { 59 | DeviceIDs: deviceId, 60 | }, 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/device/maker-switch.js: -------------------------------------------------------------------------------- 1 | import { parseStringPromise } from 'xml2js' 2 | 3 | import { decodeXML, parseError } from '../utils/functions.js' 4 | import platformLang from '../utils/lang-en.js' 5 | 6 | export default class { 7 | constructor(platform, accessory) { 8 | // Set up variables from the platform 9 | this.hapChar = platform.api.hap.Characteristic 10 | this.hapErr = platform.api.hap.HapStatusError 11 | this.hapServ = platform.api.hap.Service 12 | this.platform = platform 13 | 14 | // Set up variables from the accessory 15 | this.accessory = accessory 16 | 17 | // Set up custom variables for this device type 18 | const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {} 19 | this.reversePolarity = deviceConf.reversePolarity 20 | 21 | // If the accessory has a garage door service then remove it 22 | if (this.accessory.getService(this.hapServ.GarageDoorOpener)) { 23 | this.accessory.removeService(this.accessory.getService(this.hapServ.GarageDoorOpener)) 24 | } 25 | 26 | // Add the switch service if it doesn't already exist 27 | this.service = this.accessory.getService(this.hapServ.Switch) 28 | || this.accessory.addService(this.hapServ.Switch) 29 | 30 | // This is used to remove any no response status on startup 31 | this.service.updateCharacteristic( 32 | this.hapChar.On, 33 | this.service.getCharacteristic(this.hapChar.On).value || false, 34 | ) 35 | 36 | // Add the set handler to the switch on/off characteristic 37 | this.service 38 | .getCharacteristic(this.hapChar.On) 39 | .removeOnSet() 40 | .onSet(async value => this.internalStateUpdate(value)) 41 | 42 | // Output the customised options to the log 43 | const opts = JSON.stringify({ 44 | }) 45 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 46 | 47 | // Request a device update immediately 48 | this.requestDeviceUpdate() 49 | 50 | // Start a polling interval if the user has disabled upnp 51 | if (this.accessory.context.connection === 'http') { 52 | this.pollingInterval = setInterval( 53 | () => this.requestDeviceUpdate(), 54 | platform.config.pollingInterval * 1000, 55 | ) 56 | } 57 | } 58 | 59 | receiveDeviceUpdate(attribute) { 60 | // Log the receiving update if debug is enabled 61 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 62 | 63 | // Check which attribute we are getting 64 | switch (attribute.name) { 65 | case 'Switch': { 66 | const hkValue = attribute.value === 1 67 | this.externalStateUpdate(hkValue) 68 | break 69 | } 70 | case 'Sensor': 71 | this.externalSensorUpdate(attribute.value) 72 | break 73 | default: 74 | } 75 | } 76 | 77 | async sendDeviceUpdate(value) { 78 | // Log the sending update if debug is enabled 79 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 80 | 81 | // Send the update 82 | await this.platform.httpClient.sendDeviceUpdate( 83 | this.accessory, 84 | 'urn:Belkin:service:basicevent:1', 85 | 'SetBinaryState', 86 | value, 87 | ) 88 | } 89 | 90 | async requestDeviceUpdate() { 91 | try { 92 | // Request the update 93 | const data = await this.platform.httpClient.sendDeviceUpdate( 94 | this.accessory, 95 | 'urn:Belkin:service:deviceevent:1', 96 | 'GetAttributes', 97 | ) 98 | 99 | // Parse the response 100 | const decoded = decodeXML(data.attributeList) 101 | const xml = `${decoded}` 102 | const result = await parseStringPromise(xml, { explicitArray: false }) 103 | const attributes = {} 104 | Object.keys(result.attributeList.attribute).forEach((key) => { 105 | const attribute = result.attributeList.attribute[key] 106 | attributes[attribute.name] = Number.parseInt(attribute.value, 10) 107 | }) 108 | 109 | // Only send the required attributes to the receiveDeviceUpdate function 110 | if (attributes.Switch) { 111 | this.externalStateUpdate(attributes.Switch === 1) 112 | } 113 | 114 | // Check to see if the accessory has a contact sensor 115 | const contactSensor = this.accessory.getService(this.hapServ.ContactSensor) 116 | if (attributes.SensorPresent === 1) { 117 | // Add a contact sensor service if the physical device has one 118 | if (!contactSensor) { 119 | this.accessory.addService(this.hapServ.ContactSensor) 120 | } 121 | if (attributes.Sensor) { 122 | this.externalSensorUpdate(attributes.Sensor) 123 | } 124 | } else if (contactSensor) { 125 | // Remove the contact sensor service if the physical device doesn't have one 126 | this.accessory.removeService(contactSensor) 127 | } 128 | } catch (err) { 129 | const eText = parseError(err, [ 130 | platformLang.timeout, 131 | platformLang.timeoutUnreach, 132 | platformLang.noService, 133 | ]) 134 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 135 | } 136 | } 137 | 138 | async internalStateUpdate(value) { 139 | try { 140 | // Send the update 141 | await this.sendDeviceUpdate({ 142 | BinaryState: value ? 1 : 0, 143 | }) 144 | 145 | // Update the cache and log if appropriate 146 | this.cacheState = value 147 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 148 | } catch (err) { 149 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 150 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 151 | 152 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 153 | setTimeout(() => { 154 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 155 | }, 2000) 156 | throw new this.hapErr(-70402) 157 | } 158 | } 159 | 160 | externalStateUpdate(value) { 161 | try { 162 | // Don't continue if the value is the same as before 163 | if (value === this.cacheState) { 164 | return 165 | } 166 | 167 | // Update the HomeKit characteristic 168 | this.service.updateCharacteristic(this.hapChar.On, value) 169 | 170 | // Update the cache and log if appropriate 171 | this.cacheState = value 172 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 173 | } catch (err) { 174 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 175 | } 176 | } 177 | 178 | externalSensorUpdate(value) { 179 | try { 180 | // Reverse the polarity if enabled by user 181 | if (this.reversePolarity) { 182 | value = 1 - value 183 | } 184 | 185 | // Don't continue if the sensor value is the same as before 186 | if (value === this.cacheContact) { 187 | return 188 | } 189 | 190 | // Update the HomeKit characteristic 191 | this.accessory 192 | .getService(this.hapServ.ContactSensor) 193 | .updateCharacteristic(this.hapChar.ContactSensorState, value) 194 | 195 | // Update the cache and log the change if appropriate 196 | this.cacheContact = value 197 | this.accessory.log(`${platformLang.curCont} [${value === 1 ? platformLang.detectedNo : platformLang.detectedYes}]`) 198 | } catch (err) { 199 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /lib/device/motion.js: -------------------------------------------------------------------------------- 1 | import platformConsts from '../utils/constants.js' 2 | import { hasProperty, parseError } from '../utils/functions.js' 3 | import platformLang from '../utils/lang-en.js' 4 | 5 | export default class { 6 | constructor(platform, accessory) { 7 | // Set up variables from the platform 8 | this.eveChar = platform.eveChar 9 | this.hapChar = platform.api.hap.Characteristic 10 | this.hapServ = platform.api.hap.Service 11 | this.platform = platform 12 | 13 | // Set up variables from the accessory 14 | this.accessory = accessory 15 | 16 | // Set up custom variables for this device type 17 | const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {} 18 | this.noMotionTimer = deviceConf.noMotionTimer || platformConsts.defaultValues.noMotionTimer 19 | 20 | // Add the motion sensor service if it doesn't already exist 21 | this.service = this.accessory.getService(this.hapServ.MotionSensor) 22 | || this.accessory.addService(this.hapServ.MotionSensor) 23 | 24 | // Pass the accessory to fakegato to set up the Eve info service 25 | this.accessory.historyService = new platform.eveService('motion', this.accessory, { 26 | log: () => {}, 27 | }) 28 | 29 | // Output the customised options to the log 30 | const opts = JSON.stringify({ 31 | noMotionTimer: this.noMotionTimer, 32 | }) 33 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 34 | 35 | // Request a device update immediately 36 | this.requestDeviceUpdate() 37 | 38 | // Start a polling interval if the user has disabled upnp 39 | if (this.accessory.context.connection === 'http') { 40 | this.pollingInterval = setInterval( 41 | () => this.requestDeviceUpdate(), 42 | platform.config.pollingInterval * 1000, 43 | ) 44 | } 45 | } 46 | 47 | receiveDeviceUpdate(attribute) { 48 | // Log the receiving update if debug is enabled 49 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 50 | 51 | // Send a HomeKit needed true/false argument 52 | // attribute.value is 1 if and only if motion is detected 53 | this.externalUpdate(attribute.value === 1) 54 | } 55 | 56 | async requestDeviceUpdate() { 57 | try { 58 | // Request the update 59 | const data = await this.platform.httpClient.sendDeviceUpdate( 60 | this.accessory, 61 | 'urn:Belkin:service:basicevent:1', 62 | 'GetBinaryState', 63 | ) 64 | 65 | // Check for existence since BinaryState can be int 0 66 | if (hasProperty(data, 'BinaryState')) { 67 | // Send the data to the receiver function 68 | this.receiveDeviceUpdate({ 69 | name: 'BinaryState', 70 | value: Number.parseInt(data.BinaryState, 10), 71 | }) 72 | } 73 | } catch (err) { 74 | const eText = parseError(err, [ 75 | platformLang.timeout, 76 | platformLang.timeoutUnreach, 77 | platformLang.noService, 78 | ]) 79 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 80 | } 81 | } 82 | 83 | externalUpdate(value) { 84 | try { 85 | // Obtain the previous state of the motion sensor 86 | const prevState = this.service.getCharacteristic(this.hapChar.MotionDetected).value 87 | 88 | // Don't continue in the following cases: 89 | // (1) the previous state is the same as before and the motion timer isn't running 90 | // (2) the new value is 'no motion detected' but the motion timer is still running 91 | if ((value === prevState && !this.motionTimer) || (!value && this.motionTimer)) { 92 | return 93 | } 94 | 95 | // Next logic depends on two cases 96 | if (value || this.noMotionTimer === 0) { 97 | // CASE: new motion detected or the user motion timer is set to 0 seconds 98 | // If a motion timer is already present then stop it 99 | if (this.motionTimer) { 100 | this.accessory.log(platformLang.timerStopped) 101 | clearTimeout(this.motionTimer) 102 | this.motionTimer = false 103 | } 104 | 105 | // Update the HomeKit characteristics 106 | this.service.updateCharacteristic(this.hapChar.MotionDetected, value) 107 | 108 | // Add the entry to Eve 109 | this.accessory.historyService.addEntry({ status: value ? 1 : 0 }) 110 | 111 | // If motion detected then update the LastActivation Eve characteristic 112 | if (value) { 113 | this.service.updateCharacteristic( 114 | this.eveChar.LastActivation, 115 | Math.round(new Date().valueOf() / 1000) - this.accessory.historyService.getInitialTime(), 116 | ) 117 | } 118 | 119 | // Log the change if appropriate 120 | this.accessory.log(`${platformLang.motionSensor} [${value ? platformLang.motionYes : platformLang.motionNo}]`) 121 | } else { 122 | // CASE: motion not detected and the user motion timer is more than 0 seconds 123 | this.accessory.log(`${platformLang.timerStarted} [${this.noMotionTimer}s]`) 124 | 125 | // Clear any existing timers 126 | clearTimeout(this.motionTimer) 127 | 128 | // Create a new 'no motion timer' 129 | this.motionTimer = setTimeout(() => { 130 | // Update the HomeKit characteristic to false 131 | this.service.updateCharacteristic(this.hapChar.MotionDetected, false) 132 | 133 | // Add a no motion detected value to Eve 134 | this.accessory.historyService.addEntry({ status: 0 }) 135 | 136 | // Log the change if appropriate 137 | this.accessory.log(`${platformLang.motionSensor} [${platformLang.motionNo}] [${platformLang.timerComplete}]`) 138 | 139 | // Set the motion timer in use to false 140 | this.motionTimer = false 141 | }, this.noMotionTimer * 1000) 142 | } 143 | } catch (err) { 144 | // Catch any errors 145 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/device/outlet.js: -------------------------------------------------------------------------------- 1 | import { hasProperty, parseError } from '../utils/functions.js' 2 | import platformLang from '../utils/lang-en.js' 3 | 4 | export default class { 5 | constructor(platform, accessory) { 6 | // Set up variables from the platform 7 | this.hapChar = platform.api.hap.Characteristic 8 | this.hapErr = platform.api.hap.HapStatusError 9 | this.hapServ = platform.api.hap.Service 10 | this.platform = platform 11 | 12 | // Set up variables from the accessory 13 | this.accessory = accessory 14 | 15 | // If the accessory has a switch service then remove it 16 | if (this.accessory.getService(this.hapServ.Switch)) { 17 | this.accessory.removeService(this.accessory.getService(this.hapServ.Switch)) 18 | } 19 | 20 | // If the accessory has an air purifier service then remove it 21 | if (this.accessory.getService(this.hapServ.AirPurifier)) { 22 | this.accessory.removeService(this.accessory.getService(this.hapServ.AirPurifier)) 23 | } 24 | 25 | // Add the outlet service if it doesn't already exist 26 | this.service = this.accessory.getService(this.hapServ.Outlet) 27 | || this.accessory.addService(this.hapServ.Outlet) 28 | 29 | // Add the set handler to the outlet on/off characteristic 30 | this.service 31 | .getCharacteristic(this.hapChar.On) 32 | .removeOnSet() 33 | .onSet(async value => this.internalStateUpdate(value)) 34 | 35 | // Remove the outlet-in-use characteristic as it only matches the state in this case 36 | if (this.service.testCharacteristic(this.hapChar.OutletInUse)) { 37 | this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.OutletInUse)) 38 | } 39 | 40 | // Output the customised options to the log 41 | const opts = JSON.stringify({ 42 | showAs: 'outlet', 43 | }) 44 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 45 | 46 | // Request a device update immediately 47 | this.requestDeviceUpdate() 48 | 49 | // Start a polling interval if the user has disabled upnp 50 | if (this.accessory.context.connection === 'http') { 51 | this.pollingInterval = setInterval( 52 | () => this.requestDeviceUpdate(), 53 | platform.config.pollingInterval * 1000, 54 | ) 55 | } 56 | } 57 | 58 | receiveDeviceUpdate(attribute) { 59 | // Log the receiving update if debug is enabled 60 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 61 | 62 | // Send a HomeKit needed true/false argument 63 | // attribute.value is 0 if and only if the outlet is off 64 | this.externalStateUpdate(attribute.value !== 0) 65 | } 66 | 67 | async sendDeviceUpdate(value) { 68 | // Log the sending update if debug is enabled 69 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 70 | 71 | // Send the update 72 | await this.platform.httpClient.sendDeviceUpdate( 73 | this.accessory, 74 | 'urn:Belkin:service:basicevent:1', 75 | 'SetBinaryState', 76 | value, 77 | ) 78 | } 79 | 80 | async requestDeviceUpdate() { 81 | try { 82 | // Request the update 83 | const data = await this.platform.httpClient.sendDeviceUpdate( 84 | this.accessory, 85 | 'urn:Belkin:service:basicevent:1', 86 | 'GetBinaryState', 87 | ) 88 | 89 | // Check for existence since BinaryState can be int 0 90 | if (hasProperty(data, 'BinaryState')) { 91 | // Send the data to the receiver function 92 | this.receiveDeviceUpdate({ 93 | name: 'BinaryState', 94 | value: Number.parseInt(data.BinaryState, 10), 95 | }) 96 | } 97 | } catch (err) { 98 | const eText = parseError(err, [ 99 | platformLang.timeout, 100 | platformLang.timeoutUnreach, 101 | platformLang.noService, 102 | ]) 103 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 104 | } 105 | } 106 | 107 | async internalStateUpdate(value) { 108 | try { 109 | // Send the update 110 | await this.sendDeviceUpdate({ 111 | BinaryState: value ? 1 : 0, 112 | }) 113 | 114 | // Update the cache value and log the change if appropriate 115 | this.cacheState = value 116 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 117 | } catch (err) { 118 | // Catch any errors 119 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 120 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 121 | 122 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 123 | setTimeout(() => { 124 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 125 | }, 2000) 126 | throw new this.hapErr(-70402) 127 | } 128 | } 129 | 130 | externalStateUpdate(value) { 131 | try { 132 | // Check to see if the cache value is different 133 | if (value === this.cacheState) { 134 | return 135 | } 136 | 137 | // Update the HomeKit characteristics 138 | this.service.updateCharacteristic(this.hapChar.On, value) 139 | 140 | // Update the cache value and log the change if appropriate 141 | this.cacheState = value 142 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 143 | } catch (err) { 144 | // Catch any errors 145 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/device/purifier.js: -------------------------------------------------------------------------------- 1 | import { Builder, parseStringPromise } from 'xml2js' 2 | 3 | import { 4 | decodeXML, 5 | generateRandomString, 6 | parseError, 7 | sleep, 8 | } from '../utils/functions.js' 9 | import platformLang from '../utils/lang-en.js' 10 | 11 | export default class { 12 | constructor(platform, accessory) { 13 | // Set up variables from the platform 14 | this.hapChar = platform.api.hap.Characteristic 15 | this.hapErr = platform.api.hap.HapStatusError 16 | this.hapServ = platform.api.hap.Service 17 | this.platform = platform 18 | 19 | // Set up variables from the accessory 20 | this.accessory = accessory 21 | 22 | // Add the purifier service if it doesn't already exist 23 | this.service = this.accessory.getService(this.hapServ.AirPurifier) 24 | || this.accessory.addService(this.hapServ.AirPurifier) 25 | 26 | // Add the air quality service if it doesn't already exist 27 | this.airService = this.accessory.getService(this.hapServ.AirQualitySensor) 28 | || this.accessory.addService(this.hapServ.AirQualitySensor, 'Air Quality', 'airquality') 29 | 30 | // Add the (ionizer) switch service if it doesn't already exist 31 | this.ioService = this.accessory.getService(this.hapServ.Switch) 32 | || this.accessory.addService(this.hapServ.Switch, 'Ionizer', 'ionizer') 33 | 34 | // Add the set handler to the purifier active characteristic 35 | this.service 36 | .getCharacteristic(this.hapChar.Active) 37 | .removeOnSet() 38 | .onSet(async value => this.internalStateUpdate(value)) 39 | 40 | // Add options to the purifier target state characteristic 41 | this.service 42 | .getCharacteristic(this.hapChar.TargetAirPurifierState) 43 | .updateValue(1) 44 | .setProps({ 45 | minValue: 1, 46 | maxValue: 1, 47 | validValues: [1], 48 | }) 49 | 50 | // Add the set handler to the purifier rotation speed (for mode) characteristic 51 | this.service 52 | .getCharacteristic(this.hapChar.RotationSpeed) 53 | .setProps({ minStep: 25 }) 54 | .onSet(async (value) => { 55 | await this.internalModeUpdate(value) 56 | }) 57 | 58 | // Add the FilterChangeIndication characteristic to the air purifier if it isn't already 59 | if (!this.service.testCharacteristic(this.hapChar.FilterChangeIndication)) { 60 | this.service.addCharacteristic(this.hapChar.FilterChangeIndication) 61 | } 62 | this.cacheFilterX = this.service.getCharacteristic(this.hapChar.FilterChangeIndication).value 63 | 64 | // Add the FilterLifeLevel characteristic to the air purifier if it isn't already 65 | if (!this.service.testCharacteristic(this.hapChar.FilterLifeLevel)) { 66 | this.service.addCharacteristic(this.hapChar.FilterLifeLevel) 67 | } 68 | this.cacheFilter = this.service.getCharacteristic(this.hapChar.FilterLifeLevel).value 69 | 70 | // Add the set handler to the switch (for ionizer) characteristic 71 | this.ioService.getCharacteristic(this.hapChar.On).onSet(async (value) => { 72 | await this.internalIonizerUpdate(value) 73 | }) 74 | 75 | // Add a last mode cache value if not already set 76 | if (![1, 2, 3, 4].includes(this.accessory.context.cacheLastOnMode)) { 77 | this.accessory.context.cacheLastOnMode = 1 78 | } 79 | 80 | // Add an ionizer on/off cache value if not already set 81 | if (![0, 1].includes(this.accessory.context.cacheIonizerOn)) { 82 | this.accessory.context.cacheIonizerOn = 0 83 | } 84 | 85 | // Some conversion objects 86 | this.aqW2HK = { 87 | 0: 5, // poor -> poor 88 | 1: 3, // moderate -> fair 89 | 2: 1, // good -> excellent 90 | } 91 | this.aqLabels = { 92 | 5: platformLang.labelPoor, 93 | 3: platformLang.labelFair, 94 | 1: platformLang.labelExc, 95 | } 96 | this.modeLabels = { 97 | 0: platformLang.labelOff, 98 | 1: platformLang.labelLow, 99 | 2: platformLang.labelMed, 100 | 3: platformLang.labelHigh, 101 | 4: platformLang.labelAuto, 102 | } 103 | 104 | // Output the customised options to the log 105 | const opts = JSON.stringify({ 106 | }) 107 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 108 | 109 | // Request a device update immediately 110 | this.requestDeviceUpdate() 111 | 112 | // Start a polling interval if the user has disabled upnp 113 | if (this.accessory.context.connection === 'http') { 114 | this.pollingInterval = setInterval( 115 | () => this.requestDeviceUpdate(), 116 | platform.config.pollingInterval * 1000, 117 | ) 118 | } 119 | } 120 | 121 | receiveDeviceUpdate(attribute) { 122 | // Log the receiving update if debug is enabled 123 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 124 | 125 | // Check which attribute we are getting 126 | switch (attribute.name) { 127 | case 'AirQuality': 128 | this.externalAirQualityUpdate(attribute.value) 129 | break 130 | case 'ExpiredFilterTime': 131 | this.externalFilterChangeUpdate(attribute.value !== 0 ? 1 : 0) 132 | break 133 | case 'FilterLife': 134 | this.externalFilterLifeUpdate(Math.round((attribute.value / 60480) * 100)) 135 | break 136 | case 'Ionizer': 137 | this.externalIonizerUpdate(attribute.value) 138 | break 139 | case 'Mode': 140 | this.externalModeUpdate(attribute.value) 141 | break 142 | default: 143 | } 144 | } 145 | 146 | async requestDeviceUpdate() { 147 | try { 148 | // Request the update 149 | const data = await this.platform.httpClient.sendDeviceUpdate( 150 | this.accessory, 151 | 'urn:Belkin:service:deviceevent:1', 152 | 'GetAttributes', 153 | ) 154 | 155 | // Parse the response 156 | const decoded = decodeXML(data.attributeList) 157 | const xml = `${decoded}` 158 | const result = await parseStringPromise(xml, { explicitArray: false }) 159 | Object.keys(result.attributeList.attribute).forEach((key) => { 160 | // Only send the required attributes to the receiveDeviceUpdate function 161 | switch (result.attributeList.attribute[key].name) { 162 | case 'AirQuality': 163 | case 'ExpiredFilterTime': 164 | case 'FilterLife': 165 | case 'Ionizer': 166 | case 'Mode': 167 | this.receiveDeviceUpdate({ 168 | name: result.attributeList.attribute[key].name, 169 | value: Number.parseInt(result.attributeList.attribute[key].value, 10), 170 | }) 171 | break 172 | default: 173 | } 174 | }) 175 | } catch (err) { 176 | const eText = parseError(err, [ 177 | platformLang.timeout, 178 | platformLang.timeoutUnreach, 179 | platformLang.noService, 180 | ]) 181 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 182 | } 183 | } 184 | 185 | async sendDeviceUpdate(attributes) { 186 | // Log the sending update if debug is enabled 187 | this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`) 188 | 189 | // Generate the XML to send 190 | const builder = new Builder({ 191 | rootName: 'attribute', 192 | headless: true, 193 | renderOpts: { pretty: false }, 194 | }) 195 | const xmlAttributes = Object.keys(attributes) 196 | .map(attributeKey => builder.buildObject({ 197 | name: attributeKey, 198 | value: attributes[attributeKey], 199 | })) 200 | .join('') 201 | 202 | // Send the update 203 | await this.platform.httpClient.sendDeviceUpdate( 204 | this.accessory, 205 | 'urn:Belkin:service:deviceevent:1', 206 | 'SetAttributes', 207 | { 208 | attributeList: { '#text': xmlAttributes }, 209 | }, 210 | ) 211 | } 212 | 213 | async internalStateUpdate(value) { 214 | const prevState = this.service.getCharacteristic(this.hapChar.Active).value 215 | try { 216 | // Don't continue if the state is the same as before 217 | if (value === prevState) { 218 | return 219 | } 220 | 221 | // We also want to update the mode (by rotation speed) 222 | let newSpeed = 0 223 | if (value !== 0) { 224 | // If turning on then we want to show the last used mode (by rotation speed) 225 | switch (this.accessory.context.cacheLastOnMode) { 226 | case 2: 227 | newSpeed = 50 228 | break 229 | case 3: 230 | newSpeed = 75 231 | break 232 | case 4: 233 | newSpeed = 100 234 | break 235 | default: 236 | newSpeed = 25 237 | } 238 | } 239 | 240 | // Update the rotation speed, use setCharacteristic so the set handler is run to send updates 241 | this.service.setCharacteristic(this.hapChar.RotationSpeed, newSpeed) 242 | 243 | // Update the characteristic if we are now ON ie purifying air 244 | this.service.updateCharacteristic( 245 | this.hapChar.CurrentAirPurifierState, 246 | newSpeed === 0 ? 0 : 2, 247 | ) 248 | 249 | // Update the ionizer characteristic if the purifier is on and the ionizer was on before 250 | this.ioService.updateCharacteristic( 251 | this.hapChar.On, 252 | value === 1 && this.accessory.context.cacheIonizerOn === 1, 253 | ) 254 | } catch (err) { 255 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 256 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 257 | 258 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 259 | setTimeout(() => { 260 | this.service.updateCharacteristic(this.hapChar.Active, prevState) 261 | }, 2000) 262 | throw new this.hapErr(-70402) 263 | } 264 | } 265 | 266 | async internalModeUpdate(value) { 267 | const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value 268 | try { 269 | // Avoid multiple updates in quick succession 270 | const updateKey = generateRandomString(5) 271 | this.updateKey = updateKey 272 | await sleep(500) 273 | if (updateKey !== this.updateKey) { 274 | return 275 | } 276 | 277 | // Don't continue if the speed is the same as before 278 | if (value === prevSpeed) { 279 | return 280 | } 281 | 282 | // Generate newValue for the needed mode depending on the new rotation speed value 283 | let newValue = 0 284 | if (value > 10 && value <= 35) { 285 | newValue = 1 286 | } else if (value > 35 && value <= 60) { 287 | newValue = 2 288 | } else if (value > 60 && value <= 85) { 289 | newValue = 3 290 | } else if (value > 85) { 291 | newValue = 4 292 | } 293 | 294 | // Send the update 295 | await this.sendDeviceUpdate({ 296 | Mode: newValue.toString(), 297 | }) 298 | 299 | // Update the cache last used mode if not turning off 300 | if (newValue !== 0) { 301 | this.accessory.context.cacheLastOnMode = newValue 302 | } 303 | 304 | // Log the new mode if appropriate 305 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`) 306 | } catch (err) { 307 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 308 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 309 | 310 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 311 | setTimeout(() => { 312 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed) 313 | }, 2000) 314 | throw new this.hapErr(-70402) 315 | } 316 | } 317 | 318 | async internalIonizerUpdate(value) { 319 | const prevState = this.ioService.getCharacteristic(this.hapChar.On).value 320 | try { 321 | // If turning on, but the purifier device is off, then turn the ionizer back off 322 | if (value && this.service.getCharacteristic(this.hapChar.Active).value === 0) { 323 | await sleep(1000) 324 | this.ioService.updateCharacteristic(this.hapChar.On, false) 325 | return 326 | } 327 | 328 | // Send the update 329 | await this.sendDeviceUpdate({ 330 | Ionizer: value ? 1 : 0, 331 | }) 332 | 333 | // Update the cache state of the ionizer 334 | this.accessory.context.cacheIonizerOn = value ? 1 : 0 335 | 336 | // Log the update if appropriate 337 | this.accessory.log(`${platformLang.curIon} [${value ? 'on' : 'off'}}]`) 338 | } catch (err) { 339 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 340 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 341 | 342 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 343 | setTimeout(() => { 344 | this.ioService.updateCharacteristic(this.hapChar.On, prevState) 345 | }, 2000) 346 | throw new this.hapErr(-70402) 347 | } 348 | } 349 | 350 | externalModeUpdate(value) { 351 | try { 352 | // We want to find a rotation speed based on the given mode 353 | let rotSpeed = 0 354 | switch (value) { 355 | case 1: { 356 | rotSpeed = 25 357 | break 358 | } 359 | case 2: { 360 | rotSpeed = 50 361 | break 362 | } 363 | case 3: { 364 | rotSpeed = 75 365 | break 366 | } 367 | case 4: { 368 | rotSpeed = 100 369 | break 370 | } 371 | default: 372 | return 373 | } 374 | 375 | // Update the HomeKit characteristics 376 | this.service.updateCharacteristic(this.hapChar.Active, value !== 0 ? 1 : 0) 377 | this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed) 378 | 379 | // Turn the ionizer on or off based on whether the purifier is on or off 380 | if (value === 0) { 381 | this.ioService.updateCharacteristic(this.hapChar.On, false) 382 | } else { 383 | this.ioService.updateCharacteristic( 384 | this.hapChar.On, 385 | this.accessory.context.cacheIonizerOn === 1, 386 | ) 387 | this.accessory.context.cacheLastOnMode = value 388 | } 389 | 390 | // Log the change if appropriate 391 | this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`) 392 | } catch (err) { 393 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 394 | } 395 | } 396 | 397 | externalAirQualityUpdate(value) { 398 | try { 399 | const newValue = this.aqW2HK[value] 400 | // Don't continue if the value is the same as before 401 | if (this.airService.getCharacteristic(this.hapChar.AirQuality).value === newValue) { 402 | return 403 | } 404 | 405 | // Update the HomeKit characteristics 406 | this.airService.updateCharacteristic(this.hapChar.AirQuality, newValue) 407 | 408 | // Log the change if appropriate 409 | this.accessory.log(`${platformLang.curAir} [${this.aqLabels[newValue]}]`) 410 | } catch (err) { 411 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 412 | } 413 | } 414 | 415 | externalIonizerUpdate(value) { 416 | try { 417 | // Don't continue if the value is the same as before 418 | const state = this.ioService.getCharacteristic(this.hapChar.On).value ? 1 : 0 419 | if (state === value) { 420 | return 421 | } 422 | 423 | // Update the HomeKit characteristics 424 | this.ioService.updateCharacteristic(this.hapChar.On, value === 1) 425 | 426 | // Update the cache value and log the change if appropriate 427 | this.accessory.context.cacheIonizerOn = value 428 | this.accessory.log(`${platformLang.curIon} [${value === 1 ? 'on' : 'off'}]`) 429 | } catch (err) { 430 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 431 | } 432 | } 433 | 434 | externalFilterChangeUpdate(value) { 435 | try { 436 | // Don't continue if the value is the same as before 437 | if (value === this.cacheFilterX) { 438 | return 439 | } 440 | 441 | // Update the HomeKit characteristics 442 | this.service.updateCharacteristic(this.hapChar.FilterChangeIndication, value) 443 | 444 | // Update the cache value and log the change if appropriate 445 | this.cacheFilterX = value 446 | } catch (err) { 447 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 448 | } 449 | } 450 | 451 | externalFilterLifeUpdate(value) { 452 | try { 453 | // Don't continue if the value is the same as before 454 | if (value === this.cacheFilter) { 455 | return 456 | } 457 | 458 | // Update the HomeKit characteristics 459 | this.service.updateCharacteristic(this.hapChar.FilterLifeLevel, value) 460 | 461 | // Update the cache value and log the change if appropriate 462 | this.cacheFilter = value 463 | this.accessory.log(`${platformLang.curFilter} [${value}%]`) 464 | } catch (err) { 465 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 466 | } 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /lib/device/simulation/purifier-insight.js: -------------------------------------------------------------------------------- 1 | import platformConsts from '../../utils/constants.js' 2 | import { hasProperty, parseError } from '../../utils/functions.js' 3 | import platformLang from '../../utils/lang-en.js' 4 | 5 | export default class { 6 | constructor(platform, accessory) { 7 | // Set up variables from the platform 8 | this.eveChar = platform.eveChar 9 | this.hapChar = platform.api.hap.Characteristic 10 | this.hapErr = platform.api.hap.HapStatusError 11 | this.hapServ = platform.api.hap.Service 12 | this.platform = platform 13 | 14 | // Set up variables from the accessory 15 | this.accessory = accessory 16 | 17 | // Set up custom variables for this device type 18 | const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {} 19 | this.showTodayTC = deviceConf.showTodayTC 20 | this.wattDiff = deviceConf.wattDiff || platformConsts.defaultValues.wattDiff 21 | this.timeDiff = deviceConf.timeDiff || platformConsts.defaultValues.timeDiff 22 | if (this.timeDiff === 1) { 23 | this.timeDiff = false 24 | } 25 | this.skipTimeDiff = false 26 | 27 | if (!hasProperty(this.accessory.context, 'cacheLastWM')) { 28 | this.accessory.context.cacheLastWM = 0 29 | } 30 | if (!hasProperty(this.accessory.context, 'cacheLastTC')) { 31 | this.accessory.context.cacheLastTC = 0 32 | } 33 | if (!hasProperty(this.accessory.context, 'cacheTotalTC')) { 34 | this.accessory.context.cacheTotalTC = 0 35 | } 36 | 37 | // If the accessory has an outlet service then remove it 38 | if (this.accessory.getService(this.hapServ.Outlet)) { 39 | this.accessory.removeService(this.accessory.getService(this.hapServ.Outlet)) 40 | } 41 | 42 | // If the accessory has a switch service then remove it 43 | if (this.accessory.getService(this.hapServ.Switch)) { 44 | this.accessory.removeService(this.accessory.getService(this.hapServ.Switch)) 45 | } 46 | 47 | // Add the purifier service if it doesn't already exist 48 | this.service = this.accessory.getService(this.hapServ.AirPurifier) 49 | || this.accessory.addService(this.hapServ.AirPurifier) 50 | 51 | // Add the Eve power characteristics 52 | if (!this.service.testCharacteristic(this.eveChar.CurrentConsumption)) { 53 | this.service.addCharacteristic(this.eveChar.CurrentConsumption) 54 | } 55 | if (!this.service.testCharacteristic(this.eveChar.TotalConsumption)) { 56 | this.service.addCharacteristic(this.eveChar.TotalConsumption) 57 | } 58 | if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) { 59 | this.service.addCharacteristic(this.eveChar.ResetTotal) 60 | } 61 | 62 | // Add the set handler to the purifier active characteristic 63 | this.service 64 | .getCharacteristic(this.hapChar.Active) 65 | .removeOnSet() 66 | .onSet(async value => this.internalStateUpdate(value)) 67 | 68 | // Add options to the purifier target state characteristic 69 | this.service.getCharacteristic(this.hapChar.TargetAirPurifierState).setProps({ 70 | minValue: 1, 71 | maxValue: 1, 72 | validValues: [1], 73 | }) 74 | this.service.updateCharacteristic(this.hapChar.TargetAirPurifierState, 1) 75 | 76 | // Add the set handler to the switch reset (eve) characteristic 77 | this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => { 78 | this.accessory.context.cacheLastWM = 0 79 | this.accessory.context.cacheLastTC = 0 80 | this.accessory.context.cacheTotalTC = 0 81 | this.service.updateCharacteristic(this.eveChar.TotalConsumption, 0) 82 | }) 83 | 84 | // Pass the accessory to fakegato to set up the Eve info service 85 | this.accessory.historyService = new platform.eveService('energy', this.accessory, { 86 | log: () => {}, 87 | }) 88 | 89 | // Output the customised options to the log 90 | const opts = JSON.stringify({ 91 | showAs: 'purifier', 92 | showTodayTC: this.showTodayTC, 93 | timeDiff: this.timeDiff, 94 | wattDiff: this.wattDiff, 95 | }) 96 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 97 | 98 | // Request a device update immediately 99 | this.requestDeviceUpdate() 100 | 101 | // Start a polling interval if the user has disabled upnp 102 | if (this.accessory.context.connection === 'http') { 103 | this.pollingInterval = setInterval( 104 | () => this.requestDeviceUpdate(), 105 | platform.config.pollingInterval * 1000, 106 | ) 107 | } 108 | } 109 | 110 | receiveDeviceUpdate(attribute) { 111 | // Log the receiving update if debug is enabled 112 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 113 | 114 | // Let's see which attribute has been provided 115 | switch (attribute.name) { 116 | case 'BinaryState': { 117 | // BinaryState is reported as 0=off, 1=on, 8=standby 118 | // Send a HomeKit needed 1/0 argument (0=0, 1,8=1) 119 | this.externalStateUpdate(attribute.value === 0 ? 0 : 1) 120 | break 121 | } 122 | case 'InsightParams': 123 | // Send the insight data straight to the function 124 | this.externalInsightUpdate( 125 | attribute.value.state, 126 | attribute.value.power, 127 | attribute.value.todayWm, 128 | attribute.value.todayOnSeconds, 129 | ) 130 | break 131 | default: 132 | } 133 | } 134 | 135 | async sendDeviceUpdate(value) { 136 | // Log the sending update if debug is enabled 137 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 138 | 139 | // Send the update 140 | await this.platform.httpClient.sendDeviceUpdate( 141 | this.accessory, 142 | 'urn:Belkin:service:basicevent:1', 143 | 'SetBinaryState', 144 | value, 145 | ) 146 | } 147 | 148 | async requestDeviceUpdate() { 149 | try { 150 | // Request the update 151 | const data = await this.platform.httpClient.sendDeviceUpdate( 152 | this.accessory, 153 | 'urn:Belkin:service:basicevent:1', 154 | 'GetBinaryState', 155 | ) 156 | 157 | // Check for existence since BinaryState can be int 0 158 | if (hasProperty(data, 'BinaryState')) { 159 | this.receiveDeviceUpdate({ 160 | name: 'BinaryState', 161 | value: Number.parseInt(data.BinaryState, 10), 162 | }) 163 | } 164 | } catch (err) { 165 | const eText = parseError(err, [ 166 | platformLang.timeout, 167 | platformLang.timeoutUnreach, 168 | platformLang.noService, 169 | ]) 170 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 171 | } 172 | } 173 | 174 | async internalStateUpdate(value) { 175 | try { 176 | // Send the update 177 | await this.sendDeviceUpdate({ 178 | BinaryState: value ? 1 : 0, 179 | }) 180 | 181 | // Update the cache value 182 | this.cacheState = value 183 | 184 | // Log the change if appropriate 185 | this.accessory.log(`${platformLang.curState} [${value ? platformLang.purifyYes : platformLang.purifyNo}]`) 186 | 187 | // If turning the switch off then update the purifying state and current consumption 188 | if (!value) { 189 | // Update the HomeKit characteristics 190 | this.service.updateCharacteristic(this.eveChar.CurrentConsumption, 0) 191 | this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, 0) 192 | 193 | // Add an Eve entry for no power 194 | this.accessory.historyService.addEntry({ power: 0 }) 195 | 196 | // Log the change if appropriate 197 | this.accessory.log(`${platformLang.curCons} [0W]`) 198 | } else { 199 | // Set the current state to purifying 200 | this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, 2) 201 | } 202 | } catch (err) { 203 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 204 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 205 | 206 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 207 | setTimeout(() => { 208 | this.service.updateCharacteristic(this.hapChar.Active, this.cacheState) 209 | }, 2000) 210 | throw new this.hapErr(-70402) 211 | } 212 | } 213 | 214 | externalInsightUpdate(value, power, todayWm, todayOnSeconds) { 215 | // Update whether the switch is ON (value=1) or OFF (value=0) 216 | this.externalStateUpdate(value === 0 ? 0 : 1) 217 | 218 | // Update whether the outlet-in-use is YES (value=1) or NO (value=0,8) 219 | this.externalInUseUpdate(value === 1) 220 | 221 | // Update the total consumption 222 | this.externalTotalConsumptionUpdate(todayWm, todayOnSeconds) 223 | 224 | // Update the current consumption 225 | this.externalConsumptionUpdate(power) 226 | } 227 | 228 | externalStateUpdate(value) { 229 | try { 230 | // Check to see if the cache value is different 231 | if (value === this.cacheState) { 232 | return 233 | } 234 | 235 | // Update the HomeKit characteristics 236 | this.service.updateCharacteristic(this.hapChar.Active, value) 237 | 238 | // Update the cache value 239 | this.cacheState = value 240 | 241 | // Log the change if appropriate 242 | this.accessory.log(`${platformLang.curState} [${value ? platformLang.purifyYes : platformLang.purifyNo}]`) 243 | 244 | // If the device has turned off then update the current consumption 245 | if (!value) { 246 | this.externalConsumptionUpdate(0) 247 | } 248 | } catch (err) { 249 | // Catch any errors 250 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 251 | } 252 | } 253 | 254 | externalInUseUpdate(value) { 255 | try { 256 | // Check to see if the cache value is different 257 | if (value === this.cacheInUse) { 258 | return 259 | } 260 | 261 | // Update the HomeKit characteristic 262 | this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, value ? 2 : 1) 263 | 264 | // Update the cache value 265 | this.cacheInUse = value 266 | 267 | // Log the change if appropriate 268 | this.accessory.log(`${platformLang.curState} [${value ? platformLang.purifyYes : platformLang.purifyNo}]`) 269 | } catch (err) { 270 | // Catch any errors 271 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 272 | } 273 | } 274 | 275 | externalConsumptionUpdate(power) { 276 | try { 277 | // Divide by 1000 to get the power value in W 278 | const powerInWatts = Math.round(power / 1000) 279 | 280 | // Check to see if the cache value is different 281 | if (powerInWatts === this.cachePowerInWatts) { 282 | return 283 | } 284 | 285 | // Update the power in watts cache 286 | this.cachePowerInWatts = powerInWatts 287 | 288 | // Update the HomeKit characteristic 289 | this.service.updateCharacteristic(this.eveChar.CurrentConsumption, this.cachePowerInWatts) 290 | 291 | // Add the Eve wattage entry 292 | this.accessory.historyService.addEntry({ power: this.cachePowerInWatts }) 293 | 294 | // Calculate a difference from the last reading 295 | const diff = Math.abs(powerInWatts - this.cachePowerInWatts) 296 | 297 | // Don't continue with logging if the user has set a timeout between entries or a min difference between entries 298 | if (!this.skipTimeDiff && diff >= this.wattDiff) { 299 | // Log the change if appropriate 300 | this.accessory.log(`${platformLang.curCons} [${this.cachePowerInWatts}W]`) 301 | 302 | // Set the time difference timeout if needed 303 | if (this.timeDiff) { 304 | this.skipTimeDiff = true 305 | setTimeout(() => { 306 | this.skipTimeDiff = false 307 | }, this.timeDiff * 1000) 308 | } 309 | } 310 | } catch (err) { 311 | // Catch any errors 312 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 313 | } 314 | } 315 | 316 | externalTotalConsumptionUpdate(todayWm, todayOnSeconds) { 317 | try { 318 | if (todayWm === this.accessory.context.cacheLastWM) { 319 | return 320 | } 321 | 322 | // Update the cache last value 323 | this.accessory.context.cacheLastWM = todayWm 324 | 325 | // Convert to Wh (hours) from raw data of Wm (minutes) 326 | const todayWh = Math.round(todayWm / 60000) 327 | 328 | // Convert to kWh 329 | const todaykWh = todayWh / 1000 330 | 331 | // Convert to hours, minutes and seconds (HH:MM:SS) 332 | const todayOnHours = new Date(todayOnSeconds * 1000).toISOString().substr(11, 8) 333 | 334 | // Calculate the difference (ie extra usage from the last reading) 335 | const difference = Math.max(todaykWh - this.accessory.context.cacheLastTC, 0) 336 | 337 | // Update the caches 338 | this.accessory.context.cacheTotalTC += difference 339 | this.accessory.context.cacheLastTC = todaykWh 340 | 341 | // Update the total consumption characteristic 342 | this.service.updateCharacteristic( 343 | this.eveChar.TotalConsumption, 344 | this.showTodayTC ? todaykWh : this.accessory.context.cacheTotalTC, 345 | ) 346 | 347 | if (!this.skipTimeDiff) { 348 | this.accessory.log( 349 | `${platformLang.insOnTime} [${todayOnHours}] ${platformLang.insCons} [${todaykWh.toFixed(3)} kWh] ${platformLang.insTC} [${this.accessory.context.cacheTotalTC.toFixed(3)} kWh]`, 350 | ) 351 | } 352 | } catch (err) { 353 | // Catch any errors 354 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /lib/device/simulation/purifier.js: -------------------------------------------------------------------------------- 1 | import { hasProperty, parseError } from '../../utils/functions.js' 2 | import platformLang from '../../utils/lang-en.js' 3 | 4 | export default class { 5 | constructor(platform, accessory) { 6 | // Set up variables from the platform 7 | this.hapChar = platform.api.hap.Characteristic 8 | this.hapErr = platform.api.hap.HapStatusError 9 | this.hapServ = platform.api.hap.Service 10 | this.platform = platform 11 | 12 | // Set up variables from the accessory 13 | this.accessory = accessory 14 | 15 | // If the accessory has an outlet service then remove it 16 | if (this.accessory.getService(this.hapServ.Outlet)) { 17 | this.accessory.removeService(this.accessory.getService(this.hapServ.Outlet)) 18 | } 19 | 20 | // If the accessory has a switch service then remove it 21 | if (this.accessory.getService(this.hapServ.Switch)) { 22 | this.accessory.removeService(this.accessory.getService(this.hapServ.Switch)) 23 | } 24 | 25 | // Add the air purifier service if it doesn't already exist 26 | this.service = this.accessory.getService(this.hapServ.AirPurifier) 27 | || this.accessory.addService(this.hapServ.AirPurifier) 28 | 29 | // Add the set handler to the purifier active characteristic 30 | this.service 31 | .getCharacteristic(this.hapChar.Active) 32 | .removeOnSet() 33 | .onSet(async value => this.internalStateUpdate(value)) 34 | 35 | // Add options to the purifier target state characteristic 36 | this.service.getCharacteristic(this.hapChar.TargetAirPurifierState).setProps({ 37 | minValue: 1, 38 | maxValue: 1, 39 | validValues: [1], 40 | }) 41 | this.service.updateCharacteristic(this.hapChar.TargetAirPurifierState, 1) 42 | 43 | // Output the customised options to the log 44 | const opts = JSON.stringify({ 45 | showAs: 'purifier', 46 | }) 47 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 48 | 49 | // Request a device update immediately 50 | this.requestDeviceUpdate() 51 | 52 | // Start a polling interval if the user has disabled upnp 53 | if (this.accessory.context.connection === 'http') { 54 | this.pollingInterval = setInterval( 55 | () => this.requestDeviceUpdate(), 56 | platform.config.pollingInterval * 1000, 57 | ) 58 | } 59 | } 60 | 61 | receiveDeviceUpdate(attribute) { 62 | // Log the receiving update if debug is enabled 63 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 64 | 65 | // Send a HomeKit needed 1/0 argument 66 | // attribute.value is 0 if and only if the switch is off 67 | this.externalStateUpdate(attribute.value) 68 | } 69 | 70 | async sendDeviceUpdate(value) { 71 | // Log the sending update if debug is enabled 72 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 73 | 74 | // Send the update 75 | await this.platform.httpClient.sendDeviceUpdate( 76 | this.accessory, 77 | 'urn:Belkin:service:basicevent:1', 78 | 'SetBinaryState', 79 | value, 80 | ) 81 | } 82 | 83 | async requestDeviceUpdate() { 84 | try { 85 | // Request the update 86 | const data = await this.platform.httpClient.sendDeviceUpdate( 87 | this.accessory, 88 | 'urn:Belkin:service:basicevent:1', 89 | 'GetBinaryState', 90 | ) 91 | 92 | // Check for existence since BinaryState can be int 0 93 | if (hasProperty(data, 'BinaryState')) { 94 | // Send the data to the receiver function 95 | this.receiveDeviceUpdate({ 96 | name: 'BinaryState', 97 | value: Number.parseInt(data.BinaryState, 10), 98 | }) 99 | } 100 | } catch (err) { 101 | const eText = parseError(err, [ 102 | platformLang.timeout, 103 | platformLang.timeoutUnreach, 104 | platformLang.noService, 105 | ]) 106 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 107 | } 108 | } 109 | 110 | async internalStateUpdate(value) { 111 | try { 112 | // Send the update 113 | await this.sendDeviceUpdate({ 114 | BinaryState: value, 115 | }) 116 | 117 | // Update the HomeKit characteristic 118 | this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, value === 1 ? 2 : 0) 119 | 120 | // Update the cache value 121 | this.cacheState = value 122 | 123 | // Log the change if appropriate 124 | this.accessory.log(`${platformLang.curState} [${value === 1 ? platformLang.purifyYes : platformLang.purifyNo}]`) 125 | } catch (err) { 126 | // Catch any errors 127 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 128 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 129 | 130 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 131 | setTimeout(() => { 132 | this.service.updateCharacteristic(this.hapChar.Active, this.cacheState) 133 | }, 2000) 134 | throw new this.hapErr(-70402) 135 | } 136 | } 137 | 138 | externalStateUpdate(value) { 139 | try { 140 | // Check to see if the cache value is different 141 | if (value === this.cacheState) { 142 | return 143 | } 144 | 145 | // Update the HomeKit characteristic 146 | this.service.updateCharacteristic(this.hapChar.Active, value) 147 | this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, value === 1 ? 2 : 0) 148 | 149 | // Update the cache value 150 | this.cacheState = value 151 | 152 | // Log the change if appropriate 153 | this.accessory.log(`${platformLang.curState} [${value === 1 ? platformLang.purifyYes : platformLang.purifyNo}]`) 154 | } catch (err) { 155 | // Catch any errors 156 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/device/simulation/switch-insight.js: -------------------------------------------------------------------------------- 1 | import platformConsts from '../../utils/constants.js' 2 | import { hasProperty, parseError } from '../../utils/functions.js' 3 | import platformLang from '../../utils/lang-en.js' 4 | 5 | export default class { 6 | constructor(platform, accessory) { 7 | // Set up variables from the platform 8 | this.eveChar = platform.eveChar 9 | this.hapChar = platform.api.hap.Characteristic 10 | this.hapErr = platform.api.hap.HapStatusError 11 | this.hapServ = platform.api.hap.Service 12 | this.platform = platform 13 | 14 | // Set up variables from the accessory 15 | this.accessory = accessory 16 | 17 | // Set up custom variables for this device type 18 | const deviceConf = platform.deviceConf[accessory.context.serialNumber] || {} 19 | this.showTodayTC = deviceConf.showTodayTC 20 | this.wattDiff = deviceConf.wattDiff || platformConsts.defaultValues.wattDiff 21 | this.timeDiff = deviceConf.timeDiff || platformConsts.defaultValues.timeDiff 22 | if (this.timeDiff === 1) { 23 | this.timeDiff = false 24 | } 25 | this.skipTimeDiff = false 26 | 27 | if (!hasProperty(this.accessory.context, 'cacheLastWM')) { 28 | this.accessory.context.cacheLastWM = 0 29 | } 30 | if (!hasProperty(this.accessory.context, 'cacheLastTC')) { 31 | this.accessory.context.cacheLastTC = 0 32 | } 33 | if (!hasProperty(this.accessory.context, 'cacheTotalTC')) { 34 | this.accessory.context.cacheTotalTC = 0 35 | } 36 | 37 | // If the accessory has an air purifier service then remove it 38 | if (this.accessory.getService(this.hapServ.AirPurifier)) { 39 | this.accessory.removeService(this.accessory.getService(this.hapServ.AirPurifier)) 40 | } 41 | 42 | // If the accessory has an outlet service then remove it 43 | if (this.accessory.getService(this.hapServ.Outlet)) { 44 | this.accessory.removeService(this.accessory.getService(this.hapServ.Outlet)) 45 | } 46 | 47 | // Add the switch service if it doesn't already exist 48 | this.service = this.accessory.getService(this.hapServ.Switch) 49 | if (!this.service) { 50 | this.service = this.accessory.addService(this.hapServ.Switch) 51 | this.service.addCharacteristic(this.eveChar.CurrentConsumption) 52 | this.service.addCharacteristic(this.eveChar.TotalConsumption) 53 | this.service.addCharacteristic(this.eveChar.ResetTotal) 54 | } 55 | 56 | // Add the set handler to the switch on/off characteristic 57 | this.service 58 | .getCharacteristic(this.hapChar.On) 59 | .removeOnSet() 60 | .onSet(async value => this.internalStateUpdate(value)) 61 | 62 | // Add the set handler to the switch reset (eve) characteristic 63 | this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => { 64 | this.accessory.context.cacheLastWM = 0 65 | this.accessory.context.cacheLastTC = 0 66 | this.accessory.context.cacheTotalTC = 0 67 | this.service.updateCharacteristic(this.eveChar.TotalConsumption, 0) 68 | }) 69 | 70 | // Pass the accessory to fakegato to set up the Eve info service 71 | this.accessory.historyService = new platform.eveService('switch', this.accessory, { 72 | log: () => {}, 73 | }) 74 | 75 | // Output the customised options to the log 76 | const opts = JSON.stringify({ 77 | showAs: 'switch', 78 | showTodayTC: this.showTodayTC, 79 | timeDiff: this.timeDiff, 80 | wattDiff: this.wattDiff, 81 | }) 82 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 83 | 84 | // Request a device update immediately 85 | this.requestDeviceUpdate() 86 | 87 | // Start a polling interval if the user has disabled upnp 88 | if (this.accessory.context.connection === 'http') { 89 | this.pollingInterval = setInterval( 90 | () => this.requestDeviceUpdate(), 91 | platform.config.pollingInterval * 1000, 92 | ) 93 | } 94 | } 95 | 96 | receiveDeviceUpdate(attribute) { 97 | // Log the receiving update if debug is enabled 98 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 99 | 100 | // Let's see which attribute has been provided 101 | switch (attribute.name) { 102 | case 'BinaryState': { 103 | // BinaryState is reported as 0=off, 1=on, 8=standby 104 | // Send a HomeKit needed true/false argument (0=false, 1,8=true) 105 | this.externalStateUpdate(attribute.value !== 0) 106 | break 107 | } 108 | case 'InsightParams': 109 | // Send the insight data straight to the function 110 | this.externalInsightUpdate( 111 | attribute.value.state, 112 | attribute.value.power, 113 | attribute.value.todayWm, 114 | attribute.value.todayOnSeconds, 115 | ) 116 | break 117 | default: 118 | } 119 | } 120 | 121 | async sendDeviceUpdate(value) { 122 | // Log the sending update if debug is enabled 123 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 124 | 125 | // Send the update 126 | await this.platform.httpClient.sendDeviceUpdate( 127 | this.accessory, 128 | 'urn:Belkin:service:basicevent:1', 129 | 'SetBinaryState', 130 | value, 131 | ) 132 | } 133 | 134 | async requestDeviceUpdate() { 135 | try { 136 | // Request the update 137 | const data = await this.platform.httpClient.sendDeviceUpdate( 138 | this.accessory, 139 | 'urn:Belkin:service:basicevent:1', 140 | 'GetBinaryState', 141 | ) 142 | 143 | // Check for existence since BinaryState can be int 0 144 | if (hasProperty(data, 'BinaryState')) { 145 | this.receiveDeviceUpdate({ 146 | name: 'BinaryState', 147 | value: Number.parseInt(data.BinaryState, 10), 148 | }) 149 | } 150 | } catch (err) { 151 | const eText = parseError(err, [ 152 | platformLang.timeout, 153 | platformLang.timeoutUnreach, 154 | platformLang.noService, 155 | ]) 156 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 157 | } 158 | } 159 | 160 | async internalStateUpdate(value) { 161 | try { 162 | // Send the update 163 | await this.sendDeviceUpdate({ 164 | BinaryState: value ? 1 : 0, 165 | }) 166 | 167 | // Update the cache value 168 | this.cacheState = value 169 | 170 | // Log the change if appropriate 171 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 172 | 173 | // If turning the switch off then update the current consumption 174 | if (!value) { 175 | // Update the HomeKit characteristics 176 | this.service.updateCharacteristic(this.eveChar.CurrentConsumption, 0) 177 | 178 | // Add an Eve entry for no power 179 | this.accessory.historyService.addEntry({ power: 0 }) 180 | 181 | // Log the change if appropriate 182 | this.accessory.log(`${platformLang.curCons} [0W]`) 183 | } 184 | } catch (err) { 185 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 186 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 187 | 188 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 189 | setTimeout(() => { 190 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 191 | }, 2000) 192 | throw new this.hapErr(-70402) 193 | } 194 | } 195 | 196 | externalInsightUpdate(value, power, todayWm, todayOnSeconds) { 197 | // Update whether the switch is ON (value=1) or OFF (value=0) 198 | this.externalStateUpdate(value !== 0) 199 | 200 | // Update the total consumption 201 | this.externalTotalConsumptionUpdate(todayWm, todayOnSeconds) 202 | 203 | // Update the current consumption 204 | this.externalConsumptionUpdate(power) 205 | } 206 | 207 | externalStateUpdate(value) { 208 | try { 209 | // Check to see if the cache value is different 210 | if (value === this.cacheState) { 211 | return 212 | } 213 | 214 | // Update the HomeKit characteristics 215 | this.service.updateCharacteristic(this.hapChar.On, value) 216 | 217 | // Update the cache value 218 | this.cacheState = value 219 | 220 | // Log the change if appropriate 221 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 222 | 223 | // If the device has turned off then update the consumption 224 | if (!value) { 225 | this.externalConsumptionUpdate(0) 226 | } 227 | } catch (err) { 228 | // Catch any errors 229 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 230 | } 231 | } 232 | 233 | externalConsumptionUpdate(power) { 234 | try { 235 | // Divide by 1000 to get the power value in W 236 | const powerInWatts = Math.round(power / 1000) 237 | 238 | // Check to see if the cache value is different 239 | if (powerInWatts === this.cachePowerInWatts) { 240 | return 241 | } 242 | 243 | // Update the power in watts cache 244 | this.cachePowerInWatts = powerInWatts 245 | 246 | // Update the HomeKit characteristic 247 | this.service.updateCharacteristic(this.eveChar.CurrentConsumption, this.cachePowerInWatts) 248 | 249 | // Add the Eve wattage entry 250 | this.accessory.historyService.addEntry({ power: this.cachePowerInWatts }) 251 | 252 | // Calculate a difference from the last reading 253 | const diff = Math.abs(powerInWatts - this.cachePowerInWatts) 254 | 255 | // Don't continue with logging if the user has set a timeout between entries or a min difference between entries 256 | if (!this.skipTimeDiff && diff >= this.wattDiff) { 257 | // Log the change if appropriate 258 | this.accessory.log(`${platformLang.curCons} [${this.cachePowerInWatts}W]`) 259 | 260 | // Set the time difference timeout if needed 261 | if (this.timeDiff) { 262 | this.skipTimeDiff = true 263 | setTimeout(() => { 264 | this.skipTimeDiff = false 265 | }, this.timeDiff * 1000) 266 | } 267 | } 268 | } catch (err) { 269 | // Catch any errors 270 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 271 | } 272 | } 273 | 274 | externalTotalConsumptionUpdate(todayWm, todayOnSeconds) { 275 | try { 276 | if (todayWm === this.accessory.context.cacheLastWM) { 277 | return 278 | } 279 | 280 | // Update the cache last value 281 | this.accessory.context.cacheLastWM = todayWm 282 | 283 | // Convert to Wh (hours) from raw data of Wm (minutes) 284 | const todayWh = Math.round(todayWm / 60000) 285 | 286 | // Convert to kWh 287 | const todaykWh = todayWh / 1000 288 | 289 | // Convert to hours, minutes and seconds (HH:MM:SS) 290 | const todayOnHours = new Date(todayOnSeconds * 1000).toISOString().substr(11, 8) 291 | 292 | // Calculate the difference (ie extra usage from the last reading) 293 | const difference = Math.max(todaykWh - this.accessory.context.cacheLastTC, 0) 294 | 295 | // Update the caches 296 | this.accessory.context.cacheTotalTC += difference 297 | this.accessory.context.cacheLastTC = todaykWh 298 | 299 | // Update the total consumption characteristic 300 | this.service.updateCharacteristic( 301 | this.eveChar.TotalConsumption, 302 | this.showTodayTC ? todaykWh : this.accessory.context.cacheTotalTC, 303 | ) 304 | 305 | if (!this.skipTimeDiff) { 306 | this.accessory.log( 307 | `${platformLang.insOnTime} [${todayOnHours}] ${platformLang.insCons} [${todaykWh.toFixed(3)} kWh] ${platformLang.insTC} [${this.accessory.context.cacheTotalTC.toFixed(3)} kWh]`, 308 | ) 309 | } 310 | } catch (err) { 311 | // Catch any errors 312 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /lib/device/simulation/switch.js: -------------------------------------------------------------------------------- 1 | import { hasProperty, parseError } from '../../utils/functions.js' 2 | import platformLang from '../../utils/lang-en.js' 3 | 4 | export default class { 5 | constructor(platform, accessory) { 6 | // Set up variables from the platform 7 | this.hapChar = platform.api.hap.Characteristic 8 | this.hapErr = platform.api.hap.HapStatusError 9 | this.hapServ = platform.api.hap.Service 10 | this.platform = platform 11 | 12 | // Set up variables from the accessory 13 | this.accessory = accessory 14 | 15 | // If the accessory has an outlet service then remove it 16 | if (this.accessory.getService(this.hapServ.Outlet)) { 17 | this.accessory.removeService(this.accessory.getService(this.hapServ.Outlet)) 18 | } 19 | 20 | // If the accessory has an air purifier service then remove it 21 | if (this.accessory.getService(this.hapServ.AirPurifier)) { 22 | this.accessory.removeService(this.accessory.getService(this.hapServ.AirPurifier)) 23 | } 24 | 25 | // Add the switch service if it doesn't already exist 26 | this.service = this.accessory.getService(this.hapServ.Switch) 27 | || this.accessory.addService(this.hapServ.Switch) 28 | 29 | // Add the set handler to the switch on/off characteristic 30 | this.service 31 | .getCharacteristic(this.hapChar.On) 32 | .removeOnSet() 33 | .onSet(async value => this.internalStateUpdate(value)) 34 | 35 | // Pass the accessory to fakegato to set up the eve info service 36 | this.accessory.historyService = new platform.eveService('switch', this.accessory, { 37 | log: () => {}, 38 | }) 39 | 40 | // Output the customised options to the log 41 | const opts = JSON.stringify({ 42 | showAs: 'switch', 43 | }) 44 | platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) 45 | 46 | // Request a device update immediately 47 | this.requestDeviceUpdate() 48 | 49 | // Start a polling interval if the user has disabled upnp 50 | if (this.accessory.context.connection === 'http') { 51 | this.pollingInterval = setInterval( 52 | () => this.requestDeviceUpdate(), 53 | platform.config.pollingInterval * 1000, 54 | ) 55 | } 56 | } 57 | 58 | receiveDeviceUpdate(attribute) { 59 | // Log the receiving update if debug is enabled 60 | this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`) 61 | 62 | // Send a HomeKit needed true/false argument 63 | // attribute.value is 0 if and only if the switch is off 64 | this.externalStateUpdate(attribute.value !== 0) 65 | } 66 | 67 | async sendDeviceUpdate(value) { 68 | // Log the sending update if debug is enabled 69 | this.accessory.logDebug(`${platformLang.senUpd} ${JSON.stringify(value)}`) 70 | 71 | // Send the update 72 | await this.platform.httpClient.sendDeviceUpdate( 73 | this.accessory, 74 | 'urn:Belkin:service:basicevent:1', 75 | 'SetBinaryState', 76 | value, 77 | ) 78 | } 79 | 80 | async requestDeviceUpdate() { 81 | try { 82 | // Request the update 83 | const data = await this.platform.httpClient.sendDeviceUpdate( 84 | this.accessory, 85 | 'urn:Belkin:service:basicevent:1', 86 | 'GetBinaryState', 87 | ) 88 | 89 | // Check for existence since BinaryState can be int 0 90 | if (hasProperty(data, 'BinaryState')) { 91 | // Send the data to the receiver function 92 | this.receiveDeviceUpdate({ 93 | name: 'BinaryState', 94 | value: Number.parseInt(data.BinaryState, 10), 95 | }) 96 | } 97 | } catch (err) { 98 | const eText = parseError(err, [ 99 | platformLang.timeout, 100 | platformLang.timeoutUnreach, 101 | platformLang.noService, 102 | ]) 103 | this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`) 104 | } 105 | } 106 | 107 | async internalStateUpdate(value) { 108 | try { 109 | // Send the update 110 | await this.sendDeviceUpdate({ 111 | BinaryState: value ? 1 : 0, 112 | }) 113 | 114 | // Update the cache value 115 | this.cacheState = value 116 | this.accessory.historyService.addEntry({ status: value ? 1 : 0 }) 117 | 118 | // Log the change if appropriate 119 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 120 | } catch (err) { 121 | // Catch any errors 122 | const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach]) 123 | this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`) 124 | 125 | // Throw a 'no response' error and set a timeout to revert this after 2 seconds 126 | setTimeout(() => { 127 | this.service.updateCharacteristic(this.hapChar.On, this.cacheState) 128 | }, 2000) 129 | throw new this.hapErr(-70402) 130 | } 131 | } 132 | 133 | externalStateUpdate(value) { 134 | try { 135 | // Check to see if the cache value is different 136 | if (value === this.cacheState) { 137 | return 138 | } 139 | 140 | // Update the HomeKit characteristic 141 | this.service.updateCharacteristic(this.hapChar.On, value) 142 | 143 | // Update the cache value 144 | this.cacheState = value 145 | 146 | // Log the change if appropriate 147 | this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`) 148 | this.accessory.historyService.addEntry({ status: value ? 1 : 0 }) 149 | } catch (err) { 150 | // Catch any errors 151 | this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`) 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/fakegato/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 simont77 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 | -------------------------------------------------------------------------------- /lib/fakegato/fakegato-storage.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import os from 'node:os' 3 | import path from 'node:path' 4 | 5 | const hostname = os.hostname().split('.')[0] 6 | 7 | export default class { 8 | constructor(params) { 9 | if (!params) { 10 | params = {} 11 | } 12 | this.writers = [] 13 | this.log = params.log || {} 14 | if (!this.log) { 15 | this.log = () => {} 16 | } 17 | this.addingWriter = false 18 | } 19 | 20 | addWriter(service, params) { 21 | if (!this.addingWriter) { 22 | this.addingWriter = true 23 | if (!params) { 24 | params = {} 25 | } 26 | this.log('[%s] FGS addWriter().', service.accessoryName) 27 | const newWriter = { 28 | service, 29 | callback: params.callback, 30 | fileName: `${hostname}_${service.accessoryName}_persist.json`, 31 | } 32 | const onReady = typeof params.onReady === 'function' ? params.onReady : () => {} 33 | newWriter.storageHandler = fs 34 | newWriter.path = params.path || path.join(os.homedir(), '.homebridge') 35 | this.writers.push(newWriter) 36 | this.addingWriter = false 37 | onReady() 38 | } else { 39 | setTimeout(() => this.addWriter(service, params), 100) 40 | } 41 | } 42 | 43 | getWriter(service) { 44 | return this.writers.find(ele => ele.service === service) 45 | } 46 | 47 | _getWriterIndex(service) { 48 | return this.writers.findIndex(ele => ele.service === service) 49 | } 50 | 51 | getWriters() { 52 | return this.writers 53 | } 54 | 55 | delWriter(service) { 56 | const index = this._getWriterIndex(service) 57 | this.writers.splice(index, 1) 58 | } 59 | 60 | write(params) { 61 | if (!this.writing) { 62 | this.writing = true 63 | const writer = this.getWriter(params.service) 64 | const callBack = typeof params.callback === 'function' 65 | ? params.callback 66 | : typeof writer.callback === 'function' 67 | ? writer.callback 68 | : () => {} 69 | const fileLoc = path.join(writer.path, writer.fileName) 70 | this.log( 71 | '[%s] FGS write file [%s] [%s].', 72 | params.service.accessoryName, 73 | fileLoc, 74 | params.data.substr(1, 80), 75 | ) 76 | writer.storageHandler.writeFile(fileLoc, params.data, 'utf8', (...args) => { 77 | this.writing = false 78 | callBack(args) 79 | }) 80 | } else { 81 | setTimeout(() => this.write(params), 100) 82 | } 83 | } 84 | 85 | read(params) { 86 | const writer = this.getWriter(params.service) 87 | const callBack = typeof params.callback === 'function' 88 | ? params.callback 89 | : typeof writer.callback === 'function' 90 | ? writer.callback 91 | : () => {} 92 | const fileLoc = path.join(writer.path, writer.fileName) 93 | this.log('[%s] FGS read file [%s].', params.service.accessoryName, fileLoc) 94 | writer.storageHandler.readFile(fileLoc, 'utf8', callBack) 95 | } 96 | 97 | remove(params) { 98 | const writer = this.getWriter(params.service) 99 | const callBack = typeof params.callback === 'function' 100 | ? params.callback 101 | : typeof writer.callback === 'function' 102 | ? writer.callback 103 | : () => {} 104 | const fileLoc = path.join(writer.path, writer.fileName) 105 | this.log('[%s] FGS delete file [%s].', params.service.accessoryName, fileLoc) 106 | writer.storageHandler.unlink(fileLoc, callBack) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/fakegato/fakegato-timer.js: -------------------------------------------------------------------------------- 1 | export default class { 2 | constructor(params) { 3 | if (!params) { 4 | params = {} 5 | } 6 | this.subscribedServices = [] 7 | this.minutes = params.minutes || 10 8 | this.intervalID = null 9 | this.running = false 10 | this.log = params.log || {} 11 | if (!this.log) { 12 | this.log = () => {} 13 | } 14 | } 15 | 16 | subscribe(service, callback) { 17 | this.log('[%s] FGT new subscription.', service.accessoryName) 18 | const newService = { 19 | service, 20 | callback, 21 | backLog: [], 22 | previousBackLog: [], 23 | previousAvrg: {}, 24 | } 25 | this.subscribedServices.push(newService) 26 | } 27 | 28 | getSubscriber(service) { 29 | return this.subscribedServices.find(el => el.service === service) 30 | } 31 | 32 | _getSubscriberIndex(service) { 33 | return this.subscribedServices.findIndex(el => el.service === service) 34 | } 35 | 36 | getSubscribers() { 37 | return this.subscribedServices 38 | } 39 | 40 | unsubscribe(service) { 41 | const index = this._getSubscriberIndex(service) 42 | this.subscribedServices.splice(index, 1) 43 | if (this.subscribedServices.length === 0 && this.running) { 44 | this.stop() 45 | } 46 | } 47 | 48 | start() { 49 | this.log('Starting global FGT [%s minutes].', this.minutes) 50 | if (this.running) { 51 | this.stop() 52 | } 53 | this.running = true 54 | this.intervalID = setInterval(this.executeCallbacks.bind(this), this.minutes * 60 * 1000) 55 | } 56 | 57 | stop() { 58 | this.log('Stopping global FGT.') 59 | clearInterval(this.intervalID) 60 | this.running = false 61 | this.intervalID = null 62 | } 63 | 64 | executeCallbacks() { 65 | this.log('FGT executeCallbacks().') 66 | if (this.subscribedServices.length !== 0) { 67 | for (const s in this.subscribedServices) { 68 | if (Object.prototype.hasOwnProperty.call(this.subscribedServices, s)) { 69 | const service = this.subscribedServices[s] 70 | if (typeof service.callback === 'function') { 71 | service.previousAvrg = service.callback({ 72 | backLog: service.backLog, 73 | previousAvrg: service.previousAvrg, 74 | timer: this, 75 | immediate: false, 76 | }) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | executeImmediateCallback(service) { 84 | this.log('[%s] FGT executeImmediateCallback().', service.accessoryName) 85 | if (typeof service.callback === 'function' && service.backLog.length) { 86 | service.callback({ 87 | backLog: service.backLog, 88 | timer: this, 89 | immediate: true, 90 | }) 91 | } 92 | } 93 | 94 | addData(params) { 95 | const data = params.entry 96 | const { service } = params 97 | const immediateCallback = params.immediateCallback || false 98 | this.log( 99 | '[%s] FGT addData() [%s] immediate [%s].', 100 | service.accessoryName, 101 | data, 102 | immediateCallback, 103 | ) 104 | if (immediateCallback) { 105 | this.getSubscriber(service).backLog[0] = data 106 | } else { 107 | this.getSubscriber(service).backLog.push(data) 108 | } 109 | if (immediateCallback) { 110 | this.executeImmediateCallback(this.getSubscriber(service)) 111 | } 112 | if (!this.running) { 113 | this.start() 114 | } 115 | } 116 | 117 | emptyData(service) { 118 | this.log('[%s] FGT emptyData().', service.accessoryName) 119 | const source = this.getSubscriber(service) 120 | if (source.backLog.length) { 121 | source.previousBackLog = source.backLog 122 | } 123 | source.backLog = [] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/fakegato/uuid.js: -------------------------------------------------------------------------------- 1 | // https://github.com/homebridge/HAP-NodeJS/blob/master/src/lib/util/uuid.ts 2 | 3 | function isValid(UUID) { 4 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i 5 | return uuidRegex.test(UUID) 6 | } 7 | 8 | function toLongFormUUID(uuid, base = '-0000-1000-8000-0026BB765291') { 9 | const shortRegex = /^[0-9a-f]{1,8}$/i 10 | if (isValid(uuid)) { 11 | return uuid.toUpperCase() 12 | } 13 | if (!shortRegex.test(uuid)) { 14 | throw new TypeError('uuid was not a valid UUID or short form UUID') 15 | } 16 | if (!isValid(`00000000${base}`)) { 17 | throw new TypeError('base was not a valid base UUID') 18 | } 19 | return ((`00000000${uuid}`).substr(-8) + base).toUpperCase() 20 | } 21 | 22 | function toShortFormUUID(uuid, base = '-0000-1000-8000-0026BB765291') { 23 | uuid = toLongFormUUID(uuid, base) 24 | return uuid.substr(0, 8) 25 | } 26 | 27 | export { toLongFormUUID, toShortFormUUID } 28 | -------------------------------------------------------------------------------- /lib/homebridge-ui/public/index.html: -------------------------------------------------------------------------------- 1 |

2 | homebridge-wemo logo 7 |

8 | 13 | 24 | 78 | 173 | 316 | -------------------------------------------------------------------------------- /lib/homebridge-ui/server.js: -------------------------------------------------------------------------------- 1 | import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils' 2 | 3 | class PluginUiServer extends HomebridgePluginUiServer { 4 | constructor() { 5 | super() 6 | this.ready() 7 | } 8 | } 9 | 10 | (() => new PluginUiServer())() 11 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | import wemoPlatform from './platform.js' 4 | 5 | const require = createRequire(import.meta.url) 6 | const plugin = require('../package.json') 7 | 8 | export default hb => hb.registerPlatform(plugin.alias, wemoPlatform) 9 | -------------------------------------------------------------------------------- /lib/utils/colour.js: -------------------------------------------------------------------------------- 1 | function hs2rgb(h, s) { 2 | /* 3 | Credit: 4 | https://github.com/WickyNilliams/pure-color 5 | */ 6 | h = Number.parseInt(h, 10) / 60 7 | s = Number.parseInt(s, 10) / 100 8 | const f = h - Math.floor(h) 9 | const p = 255 * (1 - s) 10 | const q = 255 * (1 - s * f) 11 | const t = 255 * (1 - s * (1 - f)) 12 | let rgb 13 | switch (Math.floor(h) % 6) { 14 | case 0: 15 | rgb = [255, t, p] 16 | break 17 | case 1: 18 | rgb = [q, 255, p] 19 | break 20 | case 2: 21 | rgb = [p, 255, t] 22 | break 23 | case 3: 24 | rgb = [p, q, 255] 25 | break 26 | case 4: 27 | rgb = [t, p, 255] 28 | break 29 | case 5: 30 | rgb = [255, p, q] 31 | break 32 | default: 33 | return [] 34 | } 35 | if (rgb[0] === 255 && rgb[1] <= 25 && rgb[2] <= 25) { 36 | rgb[1] = 0 37 | rgb[2] = 0 38 | } 39 | return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])] 40 | } 41 | 42 | function rgb2hs(r, g, b) { 43 | /* 44 | Credit: 45 | https://github.com/WickyNilliams/pure-color 46 | */ 47 | r = Number.parseInt(r, 10) 48 | g = Number.parseInt(g, 10) 49 | b = Number.parseInt(b, 10) 50 | const min = Math.min(r, g, b) 51 | const max = Math.max(r, g, b) 52 | const delta = max - min 53 | let h 54 | let s 55 | if (max === 0) { 56 | s = 0 57 | } else { 58 | s = (delta / max) * 100 59 | } 60 | if (max === min) { 61 | h = 0 62 | } else if (r === max) { 63 | h = (g - b) / delta 64 | } else if (g === max) { 65 | h = 2 + (b - r) / delta 66 | } else if (b === max) { 67 | h = 4 + (r - g) / delta 68 | } 69 | h = Math.min(h * 60, 360) 70 | 71 | if (h < 0) { 72 | h += 360 73 | } 74 | return [Math.round(h), Math.round(s)] 75 | } 76 | 77 | function rgb2xy(r, g, b) { 78 | const redC = r / 255 79 | const greenC = g / 255 80 | const blueC = b / 255 81 | const redN = redC > 0.04045 ? ((redC + 0.055) / (1.0 + 0.055)) ** 2.4 : redC / 12.92 82 | const greenN = greenC > 0.04045 ? ((greenC + 0.055) / (1.0 + 0.055)) ** 2.4 : greenC / 12.92 83 | const blueN = blueC > 0.04045 ? ((blueC + 0.055) / (1.0 + 0.055)) ** 2.4 : blueC / 12.92 84 | const X = redN * 0.664511 + greenN * 0.154324 + blueN * 0.162028 85 | const Y = redN * 0.283881 + greenN * 0.668433 + blueN * 0.047685 86 | const Z = redN * 0.000088 + greenN * 0.07231 + blueN * 0.986039 87 | const x = X / (X + Y + Z) 88 | const y = Y / (X + Y + Z) 89 | return [x, y] 90 | } 91 | 92 | function xy2rgb(x, y) { 93 | const z = 1 - x - y 94 | const X = x / y 95 | const Z = z / y 96 | let red = X * 1.656492 - 1 * 0.354851 - Z * 0.255038 97 | let green = -X * 0.707196 + 1 * 1.655397 + Z * 0.036152 98 | let blue = X * 0.051713 - 1 * 0.121364 + Z * 1.01153 99 | if (red > blue && red > green && red > 1) { 100 | green /= red 101 | blue /= red 102 | red = 1 103 | } else if (green > blue && green > red && green > 1) { 104 | red /= green 105 | blue /= green 106 | green = 1 107 | } else if (blue > red && blue > green && blue > 1.0) { 108 | red /= blue 109 | green /= blue 110 | blue = 1.0 111 | } 112 | red = red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * red ** (1.0 / 2.4) - 0.055 113 | green = green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * green ** (1.0 / 2.4) - 0.055 114 | blue = blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * blue ** (1.0 / 2.4) - 0.055 115 | red = Math.abs(Math.round(red * 255)) 116 | green = Math.abs(Math.round(green * 255)) 117 | blue = Math.abs(Math.round(blue * 255)) 118 | if (Number.isNaN(red)) { 119 | red = 0 120 | } 121 | if (Number.isNaN(green)) { 122 | green = 0 123 | } 124 | if (Number.isNaN(blue)) { 125 | blue = 0 126 | } 127 | return [red, green, blue] 128 | } 129 | 130 | export { 131 | hs2rgb, 132 | rgb2hs, 133 | rgb2xy, 134 | xy2rgb, 135 | } 136 | -------------------------------------------------------------------------------- /lib/utils/constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | defaultConfig: { 3 | name: 'Wemo', 4 | mode: 'auto', 5 | hideConnectionErrors: false, 6 | disablePlugin: false, 7 | discoveryInterval: 30, 8 | pollingInterval: 30, 9 | upnpInterval: 300, 10 | disableUPNP: false, 11 | disableDeviceLogging: false, 12 | removeByName: '', 13 | wemoClient: { 14 | callback_url: '', 15 | listen_interface: '', 16 | port: 0, 17 | discover_opts: { 18 | interfaces: '', 19 | explicitSocketBind: true, 20 | }, 21 | }, 22 | makerTypes: [], 23 | wemoInsights: [], 24 | wemoLights: [], 25 | wemoLinks: [], 26 | wemoMotions: [], 27 | wemoOthers: [], 28 | wemoOutlets: [], 29 | platform: 'Wemo', 30 | }, 31 | 32 | defaultValues: { 33 | adaptiveLightingShift: 0, 34 | brightnessStep: 1, 35 | discoveryInterval: 30, 36 | makerTimer: 20, 37 | noMotionTimer: 60, 38 | pollingInterval: 30, 39 | port: 0, 40 | showAs: 'default', 41 | timeDiff: 1, 42 | transitionTime: 0, 43 | upnpInterval: 300, 44 | wattDiff: 1, 45 | }, 46 | 47 | minValues: { 48 | adaptiveLightingShift: -1, 49 | discoveryInterval: 15, 50 | brightnessStep: 1, 51 | makerTimer: 1, 52 | noMotionTimer: 0, 53 | pollingInterval: 15, 54 | port: 0, 55 | timeDiff: 1, 56 | transitionTime: 0, 57 | upnpInterval: 60, 58 | wattDiff: 1, 59 | }, 60 | 61 | allowed: { 62 | mode: ['auto', 'semi', 'manual'], 63 | makerTypes: [ 64 | 'label', 65 | 'serialNumber', 66 | 'ignoreDevice', 67 | 'makerType', 68 | 'makerTimer', 69 | 'reversePolarity', 70 | 'manualIP', 71 | 'listenerType', 72 | ], 73 | wemoInsights: [ 74 | 'label', 75 | 'serialNumber', 76 | 'ignoreDevice', 77 | 'showTodayTC', 78 | 'wattDiff', 79 | 'timeDiff', 80 | 'showAs', 81 | 'outletInUseTrue', 82 | 'manualIP', 83 | 'listenerType', 84 | ], 85 | wemoLights: [ 86 | 'label', 87 | 'serialNumber', 88 | 'ignoreDevice', 89 | 'enableColourControl', 90 | 'adaptiveLightingShift', 91 | 'brightnessStep', 92 | 'transitionTime', 93 | 'manualIP', 94 | 'listenerType', 95 | ], 96 | wemoLinks: ['label', 'serialNumber', 'ignoreDevice', 'manualIP', 'listenerType'], 97 | wemoMotions: [ 98 | 'label', 99 | 'serialNumber', 100 | 'ignoreDevice', 101 | 'noMotionTimer', 102 | 'manualIP', 103 | ], 104 | wemoOthers: [ 105 | 'label', 106 | 'serialNumber', 107 | 'ignoreDevice', 108 | 'manualIP', 109 | 'listenerType', 110 | ], 111 | wemoOutlets: [ 112 | 'label', 113 | 'serialNumber', 114 | 'ignoreDevice', 115 | 'showAs', 116 | 'manualIP', 117 | 'listenerType', 118 | ], 119 | listenerType: ['default', 'http'], 120 | showAs: ['default', 'switch', 'purifier'], 121 | }, 122 | 123 | portsToScan: [49153, 49152, 49154, 49155, 49151, 49156, 49157, 49158, 49159], 124 | servicesToSubscribe: [ 125 | 'urn:Belkin:service:basicevent:1', 126 | 'urn:Belkin:service:insight:1', 127 | 'urn:Belkin:service:bridge:1', 128 | ], 129 | } 130 | -------------------------------------------------------------------------------- /lib/utils/eve-chars.js: -------------------------------------------------------------------------------- 1 | import { inherits } from 'node:util' 2 | 3 | export default class { 4 | constructor(api) { 5 | this.hapServ = api.hap.Service 6 | this.hapChar = api.hap.Characteristic 7 | this.uuids = { 8 | currentConsumption: 'E863F10D-079E-48FF-8F27-9C2605A29F52', 9 | totalConsumption: 'E863F10C-079E-48FF-8F27-9C2605A29F52', 10 | voltage: 'E863F10A-079E-48FF-8F27-9C2605A29F52', 11 | electricCurrent: 'E863F126-079E-48FF-8F27-9C2605A29F52', 12 | resetTotal: 'E863F112-079E-48FF-8F27-9C2605A29F52', 13 | lastActivation: 'E863F11A-079E-48FF-8F27-9C2605A29F52', 14 | openDuration: 'E863F118-079E-48FF-8F27-9C2605A29F52', 15 | closedDuration: 'E863F119-079E-48FF-8F27-9C2605A29F52', 16 | timesOpened: 'E863F129-079E-48FF-8F27-9C2605A29F52', 17 | } 18 | const self = this 19 | this.CurrentConsumption = function CurrentConsumption() { 20 | self.hapChar.call(this, 'Current Consumption', self.uuids.currentConsumption) 21 | this.setProps({ 22 | format: api.hap.Formats.UINT16, 23 | unit: 'W', 24 | maxValue: 100000, 25 | minValue: 0, 26 | minStep: 1, 27 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY], 28 | }) 29 | this.value = this.getDefaultValue() 30 | } 31 | this.TotalConsumption = function TotalConsumption() { 32 | self.hapChar.call(this, 'Total Consumption', self.uuids.totalConsumption) 33 | this.setProps({ 34 | format: api.hap.Formats.FLOAT, 35 | unit: 'kWh', 36 | maxValue: 100000000000, 37 | minValue: 0, 38 | minStep: 0.01, 39 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY], 40 | }) 41 | this.value = this.getDefaultValue() 42 | } 43 | this.Voltage = function Voltage() { 44 | self.hapChar.call(this, 'Voltage', self.uuids.voltage) 45 | this.setProps({ 46 | format: api.hap.Formats.FLOAT, 47 | unit: 'V', 48 | maxValue: 100000000000, 49 | minValue: 0, 50 | minStep: 1, 51 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY], 52 | }) 53 | this.value = this.getDefaultValue() 54 | } 55 | this.ElectricCurrent = function ElectricCurrent() { 56 | self.hapChar.call(this, 'Electric Current', self.uuids.electricCurrent) 57 | this.setProps({ 58 | format: api.hap.Formats.FLOAT, 59 | unit: 'A', 60 | maxValue: 100000000000, 61 | minValue: 0, 62 | minStep: 0.1, 63 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY], 64 | }) 65 | this.value = this.getDefaultValue() 66 | } 67 | this.ResetTotal = function ResetTotal() { 68 | self.hapChar.call(this, 'Reset Total', self.uuids.resetTotal) 69 | this.setProps({ 70 | format: api.hap.Formats.UINT32, 71 | unit: api.hap.Units.seconds, 72 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_WRITE], 73 | }) 74 | this.value = this.getDefaultValue() 75 | } 76 | this.LastActivation = function LastActivation() { 77 | self.hapChar.call(this, 'Last Activation', self.uuids.lastActivation) 78 | this.setProps({ 79 | format: api.hap.Formats.UINT32, 80 | unit: api.hap.Units.SECONDS, 81 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY], 82 | }) 83 | this.value = this.getDefaultValue() 84 | } 85 | this.OpenDuration = function OpenDuration() { 86 | self.hapChar.call(this, 'Open Duration', self.uuids.openDuration) 87 | this.setProps({ 88 | format: api.hap.Formats.UINT32, 89 | unit: api.hap.Units.SECONDS, 90 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_WRITE], 91 | }) 92 | this.value = this.getDefaultValue() 93 | } 94 | this.ClosedDuration = function ClosedDuration() { 95 | self.hapChar.call(this, 'Closed Duration', self.uuids.closedDuration) 96 | this.setProps({ 97 | format: api.hap.Formats.UINT32, 98 | unit: api.hap.Units.SECONDS, 99 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_WRITE], 100 | }) 101 | this.value = this.getDefaultValue() 102 | } 103 | this.TimesOpened = function TimesOpened() { 104 | self.hapChar.call(this, 'Times Opened', self.uuids.timesOpened) 105 | this.setProps({ 106 | format: api.hap.Formats.UINT32, 107 | perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.NOTIFY], 108 | }) 109 | this.value = this.getDefaultValue() 110 | } 111 | inherits(this.CurrentConsumption, this.hapChar) 112 | inherits(this.TotalConsumption, this.hapChar) 113 | inherits(this.Voltage, this.hapChar) 114 | inherits(this.ElectricCurrent, this.hapChar) 115 | inherits(this.LastActivation, this.hapChar) 116 | inherits(this.ResetTotal, this.hapChar) 117 | inherits(this.OpenDuration, this.hapChar) 118 | inherits(this.ClosedDuration, this.hapChar) 119 | inherits(this.TimesOpened, this.hapChar) 120 | this.CurrentConsumption.UUID = this.uuids.currentConsumption 121 | this.TotalConsumption.UUID = this.uuids.totalConsumption 122 | this.Voltage.UUID = this.uuids.voltage 123 | this.ElectricCurrent.UUID = this.uuids.electricCurrent 124 | this.LastActivation.UUID = this.uuids.lastActivation 125 | this.ResetTotal.UUID = this.uuids.resetTotal 126 | this.OpenDuration.UUID = this.uuids.openDuration 127 | this.ClosedDuration.UUID = this.uuids.closedDuration 128 | this.TimesOpened.UUID = this.uuids.timesOpened 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /lib/utils/functions.js: -------------------------------------------------------------------------------- 1 | function decodeXML(input) { 2 | return input 3 | .replace(/</g, '<') 4 | .replace(/>/g, '>') 5 | .replace(/"/g, '"') 6 | .replace(/'/g, '\'') 7 | .replace(/&/g, '&') 8 | } 9 | 10 | function generateRandomString(length) { 11 | const chars = 'abcdefghijklmnopqrstuvwxyz0123456789' 12 | let nonce = '' 13 | while (nonce.length < length) { 14 | nonce += chars.charAt(Math.floor(Math.random() * chars.length)) 15 | } 16 | return nonce 17 | } 18 | 19 | const hasProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop) 20 | 21 | function parseError(err, hideStack = []) { 22 | let toReturn = err.message 23 | if (err?.stack?.length > 0 && !hideStack.includes(err.message)) { 24 | const stack = err.stack.split('\n') 25 | if (stack[1]) { 26 | toReturn += stack[1].replace(' ', '') 27 | } 28 | } 29 | return toReturn 30 | } 31 | 32 | function parseSerialNumber(input) { 33 | return input 34 | .toString() 35 | .replace(/[\s'"]+/g, '') 36 | .toUpperCase() 37 | } 38 | 39 | function sleep(ms) { 40 | return new Promise((resolve) => { 41 | setTimeout(resolve, ms) 42 | }) 43 | } 44 | 45 | export { 46 | decodeXML, 47 | generateRandomString, 48 | hasProperty, 49 | parseError, 50 | parseSerialNumber, 51 | sleep, 52 | } 53 | -------------------------------------------------------------------------------- /lib/utils/lang-en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | accNotFound: 'accessory not found', 3 | accNotReady: 'has not been discovered yet so commands will fail', 4 | alDisabled: 'adaptive lighting disabled due to significant colour change', 5 | awaiting: 'The following devices have still not been initially found', 6 | beta: 'You are using a beta version of the plugin - you will experience more logging than normal', 7 | brand: 'Belkin Wemo', 8 | brightnessFail: 'could not obtain updated brightness as', 9 | cantCtl: 'sending update failed as', 10 | cantUpd: 'receiving update failed as', 11 | cfgDef: 'is not a valid number so using default of', 12 | cfgDup: 'will be ignored since another entry with this ID already exists', 13 | cfgIgn: 'is not configured correctly so ignoring', 14 | cfgIgnItem: 'has an invalid entry which will be ignored', 15 | cfgItem: 'Config entry', 16 | cfgLow: 'is set too low so increasing to', 17 | cfgRmv: 'is unused and can be removed', 18 | cfgQts: 'should not have quotes around its entry', 19 | complete: '✓ Setup complete', 20 | connError: 'connection error', 21 | curAir: 'current air quality', 22 | curBright: 'current brightness', 23 | curCCT: 'current cct', 24 | curColour: 'current colour', 25 | curCons: 'current consumption', 26 | curCont: 'current contact', 27 | curFilter: 'current filter level', 28 | curHumi: 'current humidity', 29 | curIon: 'current ionizer', 30 | curOIU: 'current outlet-in-use', 31 | curMode: 'current mode', 32 | curState: 'current state', 33 | curTemp: 'current temperature', 34 | curTimer: 'current timer', 35 | detectedNo: 'not detected', 36 | detectedYes: 'detected', 37 | devAdd: 'has been added to Homebridge', 38 | devInitOpts: 'initialising with options', 39 | devNotAdd: 'could not be added to Homebridge as', 40 | devNotConf: 'could not be configured as', 41 | devNotInit: 'could not be initialised as', 42 | devNotRemove: 'could not be removed from Homebridge as', 43 | devOffline: 'appears to be offline', 44 | devRemove: 'has been removed from Homebridge', 45 | disabling: 'Disabling plugin', 46 | hbVersionFail: 'Your version of Homebridge is too low - please update to v1.6', 47 | identify: 'identify button pressed', 48 | incFail: 'failed to process incoming message as', 49 | incKnown: 'incoming notification', 50 | incUnknown: 'incoming notification from unknown accessory', 51 | initSer: 'initialised with s/n', 52 | initMac: 'and ip/port', 53 | initialised: 'Plugin initialised. Setting up accessories...', 54 | initialising: 'Initialising plugin', 55 | insCons: 'consumption', 56 | insOnTime: 'today ontime', 57 | insTC: 'total consumption', 58 | httpFail: 'http error (will be re-attempted)', 59 | httpGood: 'http has been established', 60 | labelAuto: 'auto', 61 | labelClosed: 'closed', 62 | labelClosing: 'closing', 63 | labelEco: 'eco', 64 | labelExc: 'excellent', 65 | labelFair: 'fair', 66 | labelFP: 'frost-protect', 67 | labelHigh: 'high', 68 | labelLow: 'low', 69 | labelMax: 'max', 70 | labelMed: 'med', 71 | labelMin: 'min', 72 | labelPoor: 'poor', 73 | labelOff: 'off', 74 | labelOpen: 'open', 75 | labelOpening: 'opening', 76 | labelStopped: 'stopped', 77 | labelWarm: 'warm', 78 | listenerClosed: 'Listener server gracefully closed', 79 | listenerError: 'Listener server error', 80 | listenerPort: 'Listener server port', 81 | makerClosed: 'is already closed so ignoring command', 82 | makerClosing: 'is already closing so ignoring command', 83 | makerNeedMMode: 'must be set to momentary mode to work as a garage door', 84 | makerOpen: 'is already open so ignoring command', 85 | makerOpening: 'is already opening so ignoring command', 86 | makerTrigExt: 'triggered externally', 87 | modelLED: 'LED Bulb (Via Link)', 88 | motionNo: 'clear', 89 | motionSensor: 'motion sensor', 90 | motionYes: 'motion detected', 91 | noInterface: 'Unable to find interface', 92 | noPort: 'could not find correct port for device', 93 | noService: 'device does not have this service', 94 | noServices: 'device does not have any upnp services', 95 | noSockets: 'NodeSSDP error [no sockets], if this continues to happen try restarting Homebridge', 96 | notConfigured: 'Plugin has not been configured', 97 | proEr: 'could not be processed as', 98 | purifyNo: 'not purifying', 99 | purifyYes: 'purifying', 100 | rduErr: 'http polling failed as', 101 | recUpd: 'receiving update', 102 | repError: 'reported error', 103 | senUpd: 'sending update', 104 | ssdpFail: 'SSDP search failed as', 105 | ssdpStopped: 'SSDP client gracefully stopped', 106 | stoppedSubs: 'existing upnp subscriptions have been stopped', 107 | subError: 'subscription error, retrying in 10 seconds', 108 | subInit: 'initial subscription for service', 109 | subPending: 'subscription still pending', 110 | subscribeError: 'could not subscribe as', 111 | tarHumi: 'target humidity', 112 | tarState: 'target state', 113 | tarTemp: 'target temperature', 114 | timeout: 'a connection timeout occurred', 115 | timeoutUnreach: 'a connection timeout occurred (EHOSTUNREACH)', 116 | timerComplete: 'timer complete', 117 | timerStarted: 'timer started', 118 | timerStopped: 'timer stopped', 119 | unsubFail: 'unsubscribe failed', 120 | unsupported: 'is unsupported but feel free to create a GitHub issue', 121 | upnpFail: 'upnp error (will be re-attempted)', 122 | upnpGood: 'upnp has been established', 123 | viaAL: 'via adaptive lighting', 124 | welcome: 'This plugin has been made with ♥ by bwp91, please consider a ☆ on GitHub if you are finding it useful!', 125 | } 126 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@homebridge-plugins/homebridge-wemo", 3 | "alias": "Wemo", 4 | "type": "module", 5 | "version": "7.0.1-beta.0", 6 | "description": "Homebridge plugin to integrate Wemo devices into HomeKit.", 7 | "author": { 8 | "name": "bwp91", 9 | "email": "bwp91@icloud.com" 10 | }, 11 | "license": "MIT", 12 | "funding": [ 13 | { 14 | "type": "github", 15 | "url": "https://github.com/sponsors/bwp91" 16 | }, 17 | { 18 | "type": "kofi", 19 | "url": "https://ko-fi.com/bwp91" 20 | }, 21 | { 22 | "type": "patreon", 23 | "url": "https://www.patreon.com/bwp91" 24 | }, 25 | { 26 | "type": "paypal", 27 | "url": "https://www.paypal.me/BenPotter" 28 | } 29 | ], 30 | "homepage": "https://github.com/homebridge-plugins/homebridge-wemo", 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/homebridge-plugins/homebridge-wemo.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/homebridge-plugins/homebridge-wemo/issues" 37 | }, 38 | "keywords": [ 39 | "homebridge", 40 | "homebridge-plugin", 41 | "hoobs", 42 | "hoobs-plugin", 43 | "homekit", 44 | "siri", 45 | "wemo", 46 | "belkin" 47 | ], 48 | "main": "lib/index.js", 49 | "engines": { 50 | "homebridge": "^1.6.0 || ^2.0.0-beta.0", 51 | "node": "^18.20.8 || ^20.19.0 || ^22.14.0" 52 | }, 53 | "scripts": { 54 | "lint": "eslint . --fix", 55 | "rebuild": "rm -rf package-lock.json && rm -rf node_modules && npm install" 56 | }, 57 | "dependencies": { 58 | "@homebridge/plugin-ui-utils": "^2.0.2", 59 | "axios": "^1.8.4", 60 | "ip": "^2.0.1", 61 | "node-ssdp": "^4.0.1", 62 | "p-queue": "^8.1.0", 63 | "xml2js": "^0.6.2", 64 | "xmlbuilder": "^15.1.1" 65 | }, 66 | "devDependencies": { 67 | "@antfu/eslint-config": "^4.12.0", 68 | "eslint": "^9.24.0" 69 | } 70 | } 71 | --------------------------------------------------------------------------------