├── sensor ├── hum.txt └── temp.txt ├── .gitignore ├── .markdownlint.json ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── stale.yml │ ├── prerelease.js │ ├── publish-beta.yml │ ├── nodejs.yml │ └── codeql-analysis.yml ├── tsconfig.json ├── src ├── configTypes.ts ├── deviceTypes.ts └── index.ts ├── LICENSE ├── .eslintrc.js ├── package.json ├── config.schema.json └── README.md /sensor/hum.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sensor/temp.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __pycache__ 3 | dist 4 | .github 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line_length": false, 4 | "no-duplicate-heading": false 5 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: NikDevx 2 | ko_fi: NikDevx 3 | liberapay: NikDevx 4 | custom: ["https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JF8XFPLT3MDEJ"] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "10:00" 14 | open-pull-requests-limit: 10 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "allowJs": true, 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "removeComments": true 13 | }, 14 | "include": [ 15 | "./src/**/*", 16 | "./sensor/**/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/configTypes.ts: -------------------------------------------------------------------------------- 1 | export type PhilipsAirPlatformConfig = { 2 | name: string; 3 | timeout_seconds: number; 4 | devices: Array 5 | }; 6 | 7 | export type DeviceConfig = { 8 | name: string; 9 | ip: string; 10 | protocol: string; 11 | sleep_speed: boolean; 12 | light_control: boolean; 13 | allergic_func: boolean; 14 | temperature_sensor: boolean; 15 | polling: number; 16 | humidity_sensor: boolean; 17 | humidifier: boolean; 18 | logger: boolean; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Stale 2 | 3 | on: 4 | issues: 5 | types: [reopened] 6 | schedule: 7 | - cron: "*/60 * * * *" 8 | 9 | jobs: 10 | stale: 11 | 12 | runs-on: ubuntu-latest 13 | env: 14 | ACTIONS_STEP_DEBUG: true 15 | steps: 16 | - uses: actions/stale@v3.0.18 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 20 | stale-issue-label: 'stale' 21 | days-before-stale: 28 22 | days-before-close: 7 23 | exempt-issue-labels: 'long running,help wanted,tested config' 24 | remove-stale-when-updated: true 25 | -------------------------------------------------------------------------------- /src/deviceTypes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | export type PurifierStatus = { 4 | om: string; 5 | pwr: string; 6 | cl: boolean; 7 | aqil: number; 8 | uil: string; 9 | dt: number; 10 | dtrs: number; 11 | mode: string; 12 | pm25: number; 13 | iaql: number; 14 | aqit: number; 15 | wl: number; 16 | rhset: number; 17 | rh: number; 18 | func: string; 19 | temp: number; 20 | ddp: string; 21 | err: number; 22 | }; 23 | 24 | export type PurifierFilters = { 25 | fltt1: string; 26 | fltt2: string; 27 | fltsts0: number; 28 | fltsts1: number; 29 | fltsts2: number; 30 | wicksts: number; 31 | }; 32 | 33 | export type PurifierFirmware = { 34 | name: string; 35 | version: string; 36 | upgrade: string; 37 | state: string; 38 | progress: number; 39 | statusmsg: string; 40 | mandatory: boolean; 41 | modelid: string; 42 | }; 43 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.js: -------------------------------------------------------------------------------- 1 | #!/bin/env node 2 | 3 | const fs = require('fs'); 4 | const semver = require('semver'); 5 | const child_process = require('child_process'); 6 | 7 | function getTagVersionFromNpm(tag) { 8 | try { 9 | return child_process.execSync(`npm info ${package.name} version --tag="${tag}"`).toString('utf8').trim(); 10 | } catch (e) { 11 | return null; 12 | } 13 | } 14 | 15 | // load package.json 16 | const package = JSON.parse(fs.readFileSync('package.json', 'utf8')); 17 | 18 | // work out the correct tag 19 | const currentLatest = getTagVersionFromNpm('latest') || '0.0.0'; 20 | const currentBeta = getTagVersionFromNpm('beta') || '0.0.0'; 21 | const latestNpmTag = semver.gt(currentBeta, currentLatest, { includePrerelease: true }) ? currentBeta : currentLatest; 22 | const publishTag = semver.gt(package.version, latestNpmTag, { includePrerelease: true }) ? package.version : latestNpmTag; 23 | 24 | // save the package.json 25 | package.version = publishTag; 26 | fs.writeFileSync('package.json', JSON.stringify(package, null, 4)); 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-beta.yml: -------------------------------------------------------------------------------- 1 | name: Publish Beta 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | ref: 7 | description: 'Branch' 8 | required: true 9 | default: 'master' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2.3.4 16 | with: 17 | ref: ${{github.event.inputs.ref}} 18 | - uses: actions/setup-node@v2.1.5 19 | with: 20 | node-version: 10 21 | - name: npm install and build 22 | run: | 23 | npm ci 24 | npm run build --if-present 25 | env: 26 | CI: true 27 | 28 | publish-npm: 29 | if: github.repository == 'Sunoo/homebridge-philips-air' 30 | 31 | needs: build 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2.3.4 37 | with: 38 | ref: ${{github.event.inputs.ref}} 39 | - uses: actions/setup-node@v2.1.5 40 | with: 41 | node-version: 10 42 | registry-url: https://registry.npmjs.org/ 43 | - run: npm ci 44 | - run: node .github/workflows/prerelease.js 45 | - run: npm --no-git-tag-version version prerelease --preid=beta 46 | - run: npm publish --tag=beta 47 | env: 48 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, David Maher | Nik_Dev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: NodeJS 2 | 3 | on: 4 | push: 5 | branches: '**' 6 | pull_request: 7 | release: # Run when release is created 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | 13 | strategy: 14 | matrix: 15 | node-version: [10.x, 12.x, 13.x, 14.x] 16 | os: [ubuntu-latest] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v2.3.4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2.1.5 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - name: npm install and build 27 | run: | 28 | npm ci 29 | npm run build --if-present 30 | env: 31 | CI: true 32 | 33 | publish-npm: 34 | # publish only if we are on our own repo, event was 'release' (a tag was created) and the tag starts with "v" (aka version tag) 35 | if: github.repository == 'Sunoo/homebridge-philips-air' && github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') 36 | 37 | needs: build # only run if build succeeds 38 | 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v2.3.4 43 | - uses: actions/setup-node@v2.1.5 44 | with: 45 | node-version: 10 # use the minimum required version 46 | registry-url: https://registry.npmjs.org/ 47 | - run: npm ci 48 | - run: npm publish 49 | env: 50 | NODE_AUTH_TOKEN: ${{ secrets.npm_token }} 51 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended' 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, 10 | sourceType: 'module', 11 | project: './tsconfig.json' 12 | }, 13 | rules: { 14 | '@typescript-eslint/array-type': ['error', {default: 'generic'}], 15 | '@typescript-eslint/brace-style': 'error', 16 | '@typescript-eslint/comma-spacing': 'error', 17 | '@typescript-eslint/explicit-function-return-type': 'error', 18 | '@typescript-eslint/func-call-spacing': 'error', 19 | '@typescript-eslint/indent': ['error', 2], 20 | '@typescript-eslint/lines-between-class-members': ['error', {"exceptAfterSingleLine": true}], 21 | '@typescript-eslint/no-base-to-string': 'error', 22 | '@typescript-eslint/no-explicit-any': 'error', 23 | '@typescript-eslint/no-extra-parens': 'error', 24 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': ['error', {"allowComparingNullableBooleansToTrue": false, "allowComparingNullableBooleansToFalse": false}], 25 | '@typescript-eslint/no-var-requires': 'error', 26 | '@typescript-eslint/prefer-optional-chain': 'error', 27 | '@typescript-eslint/prefer-readonly': 'error', 28 | '@typescript-eslint/quotes': ['error', 'single', {"allowTemplateLiterals": false}], 29 | '@typescript-eslint/semi': ['error'], 30 | '@typescript-eslint/space-before-function-paren': ['error', 'never'], 31 | '@typescript-eslint/type-annotation-spacing': 'error', 32 | 'comma-dangle': 'error', 33 | 'no-confusing-arrow': 'error', 34 | 'no-lonely-if': 'error', 35 | 'no-trailing-spaces': 'error', 36 | 'no-unneeded-ternary': 'error', 37 | 'one-var': ['error', 'never'] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "Homebridge Philips Air", 3 | "name": "homebridge-philips-air", 4 | "version": "3.5.2", 5 | "description": "Homebridge Plugin for Philips Air Purifiers", 6 | "main": "dist/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/NikDevx/homebridge-philips-air.git" 10 | }, 11 | "keywords": [ 12 | "homebridge-plugin", 13 | "philips", 14 | "air purifier" 15 | ], 16 | "author": { 17 | "name": "Nik_Dev", 18 | "email": "andrievskiy_nikita@yahoo.com" 19 | }, 20 | "license": "BSD-2-Clause", 21 | "funding": [ 22 | { 23 | "type": "kofi", 24 | "url": "https://ko-fi.com/NikDevx" 25 | }, 26 | { 27 | "type": "paypal", 28 | "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JF8XFPLT3MDEJ" 29 | }, 30 | { 31 | "type": "github", 32 | "url": "https://github.com/NikDevx" 33 | }, 34 | { 35 | "type": "liberapay", 36 | "url": "https://liberapay.com/NikDevx" 37 | } 38 | ], 39 | "bugs": { 40 | "url": "https://github.com/NikDevx/homebridge-philips-air/issues" 41 | }, 42 | "homepage": "https://github.com/NikDevx/homebridge-philips-air#readme", 43 | "scripts": { 44 | "clean": "rimraf ./dist ./coverage", 45 | "build": "rimraf ./dist ./coverage && tsc", 46 | "format": "markdownlint --fix *.md", 47 | "lint": "eslint src/*.ts --fix", 48 | "lint-check": "eslint src/*.ts", 49 | "prepare": "npm run build", 50 | "prepublishOnly": "npm run lint-check", 51 | "postpublish": "npm run clean", 52 | "watch": "npm run clean && tsc --watch" 53 | }, 54 | "devDependencies": { 55 | "@types/chmodr": "^1.0.0", 56 | "@types/node": "^14.14.36", 57 | "@typescript-eslint/eslint-plugin": "^4.22.0", 58 | "@typescript-eslint/parser": "^4.22.0", 59 | "eslint": "^7.5.0", 60 | "homebridge": "^1.3.4", 61 | "markdownlint-cli": "^0.27.1", 62 | "node-sessionstorage": "^1.0.0", 63 | "rimraf": "^3.0.2", 64 | "typescript": "^4.0.2" 65 | }, 66 | "engines": { 67 | "node": ">=10", 68 | "homebridge": ">=1.0.0" 69 | }, 70 | "files": [ 71 | "config.schema.json", 72 | "dist/**/*", 73 | "LICENSE", 74 | "package.json", 75 | "README.md", 76 | "sensor" 77 | ], 78 | "dependencies": { 79 | "dns-packet": "^5.2.3", 80 | "node-sessionstorage": "^1.0.0", 81 | "philips-air": "^0.6.0", 82 | "time-stamp": "^2.2.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '36 11 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "philipsAir", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "headerDisplay": "Homebridge Plugin for Philips Air Purifiers.", 6 | "footerDisplay": "Raise [Issues](https://github.com/NikDevx/homebridge-philips-air/issues) or submit [Pull Requests](https://github.com/NikDevx/homebridge-philips-air/pulls) on [Project Page](https://github.com/NikDevx/homebridge-philips-air).", 7 | "schema": { 8 | "type": "object", 9 | "properties": { 10 | "name": { 11 | "title": "Name", 12 | "type": "string", 13 | "required": true, 14 | "default": "Philips Air", 15 | "description": "A unique name for the accessory. It will be used as the accessory name in HomeKit." 16 | }, 17 | "timeout_seconds": { 18 | "title": "Timeout Seconds", 19 | "type": "integer", 20 | "placeholder": 5, 21 | "description": "Number of seconds to wait for a response from the purifier." 22 | }, 23 | "devices": { 24 | "title": "Devices", 25 | "type": "array", 26 | "required": true, 27 | "minLength": 1, 28 | "items": { 29 | "title": "Air Purifier", 30 | "type": "object", 31 | "properties": { 32 | "name": { 33 | "title": "Name", 34 | "type": "string", 35 | "required": true, 36 | "placeholder": "Living Room Purifier", 37 | "description": "Name of your device." 38 | }, 39 | "ip": { 40 | "title": "IP Address", 41 | "type": "string", 42 | "required": true, 43 | "format": "ipv4", 44 | "placeholder": "10.0.1.16", 45 | "description": "IP address of your device." 46 | }, 47 | "protocol": { 48 | "title": "Protocol", 49 | "type": "string", 50 | "required": true, 51 | "default": "http", 52 | "oneOf": [ 53 | { 54 | "title": "HTTP", 55 | "enum": [ 56 | "http" 57 | ] 58 | }, 59 | { 60 | "title": "Plain CoAP", 61 | "enum": [ 62 | "plain_coap" 63 | ] 64 | }, 65 | { 66 | "title": "CoAP", 67 | "enum": [ 68 | "coap" 69 | ] 70 | } 71 | ], 72 | "description": "Protocol used by your device." 73 | }, 74 | "sleep_speed": { 75 | "title": "Sleep Speed", 76 | "type": "boolean", 77 | "description": "Does this device support 'sleep' speed?" 78 | }, 79 | "light_control": { 80 | "title": "Light Control", 81 | "type": "boolean", 82 | "description": "Expose device lights as lightbulbs." 83 | }, 84 | "allergic_func": { 85 | "title": "Allergic mode function", 86 | "type": "boolean", 87 | "description": "Does this device support 'allergic' function?" 88 | }, 89 | "temperature_sensor": { 90 | "title": "Temperature sensor", 91 | "type": "boolean", 92 | "description": "Expose device temperature as temperature sensor." 93 | }, 94 | "humidity_sensor": { 95 | "title": "Humidity sensor", 96 | "type": "boolean", 97 | "description": "Expose device humidity as humidity sensor." 98 | }, 99 | "humidifier": { 100 | "title": "Humidifier", 101 | "type": "boolean", 102 | "description": "Adding humidified support." 103 | }, 104 | "logger": { 105 | "title": "Logger", 106 | "type": "boolean", 107 | "description": "Getting data from humidity and temp sensors and save value into txt file." 108 | }, 109 | "polling": { 110 | "title": "Polling interval in seconds", 111 | "type": "integer", 112 | "description": "Adding a refresh time for the all sensors and logger in seconds.." 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-philips-air 2 | 3 | [![npm](https://img.shields.io/npm/v/homebridge-philips-air) ![npm](https://img.shields.io/npm/dt/homebridge-philips-air)](https://www.npmjs.com/package/homebridge-philips-air) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 4 | 5 | Homeridge Plugin for Philips Air Purifiers 6 | 7 | ## 🔴 Foreword 🔴 8 | 9 | **This plugin is now using [py-air-control](https://github.com/rgerganov/py-air-control) directly to enable support for newer Philips connected air purifier models.** 10 | 11 | 12 | 13 | ## 🟡 Installation 🟡 14 | 15 | 1. Install Homebridge using the [official instructions](https://github.com/homebridge/homebridge/wiki). 16 | 2. Install this plugin using `sudo npm install -g homebridge-philips-air --unsafe-perm`. 17 | 3. Run command in your console `sudo chmod -R 777 /usr/lib/node_modules/homebridge-philips-air/sensor`. 18 | 4. Update your configuration file. See configuration sample below. 19 | 20 | 21 | **If you're using HTTP protocol:** 22 | 23 | 1. Install pip and git using `sudo apt install python3-pip git`. 24 | 2. Install py-air-control using `sudo pip3 install py-air-control`. 25 | 26 | **If you're using CoAP protocol:** 27 | 28 | 1. Install pip and git using `sudo apt install python3-pip git`. 29 | 2. Install py-air-control using `sudo pip3 install py-air-control`. 30 | 3. Update CoAPthon3 using `sudo pip3 install -U git+https://github.com/Tanganelli/CoAPthon3@89d5173`. 31 | 32 | 33 | For new firmware version 34 | 35 | (Who get error `Unexpected error:'NoneType' object has no attribute "payload"`) 36 | 37 | 1. Found coap_client.py path using `sudo find / -name *coap_client.py`. 38 | 2. Open file coap_client.py in `line 91` change `timeout to 60`, 39 | `line 145` same and `line 174` add `timeout=60` after `encrypted_payload` 40 | 3. Change in plugin settings Timeout Seconds to 30 or 60. 41 | 42 | **If you're using Plain CoAP protocol:** 43 | 44 | 1. Install pip and git using `sudo apt install python3-pip git`. 45 | 2. Install py-air-control using `sudo pip3 install py-air-control`. 46 | 3. Update CoAPthon3 using `sudo pip3 install -U git+https://github.com/Tanganelli/CoAPthon3@89d5173`. 47 | 4. Allow non-root to send pings using `echo "net.ipv4.ping_group_range=0 1000" | sudo tee -a /etc/sysctl.conf`. 48 | 5. Update running sysctl configuration using `sudo sysctl -p`. 49 | 50 | ### 🟢 Configuration 🟢 51 | 52 | Edit your `config.json` accordingly. Configuration sample: 53 | 54 | ```json 55 | "platforms": [{ 56 | "platform": "philipsAir", 57 | "devices": [{ 58 | "name": "Living Room Purifier", 59 | "ip": "10.0.1.16", 60 | "protocol": "http" 61 | }] 62 | }] 63 | ``` 64 | 65 | | Fields | Description | Required | 66 | |--------------------|------------------------------------------------------------------------------|----------| 67 | | platform | Must always be `philipsAir`. | Yes | 68 | | name | For logging purposes. | No | 69 | | timeout_seconds | Number of seconds to wait for a response from the purifier. (Default: 5) | No | 70 | | devices | Array of Philips air purifiers (multiple supported). | Yes | 71 | |- name | Name of your device. | No | 72 | |- ip | IP address of your device. | Yes | 73 | |- protocol | Protocol used by your device: http (default), plain\_coap, coap | No | 74 | |- sleep\_speed | Does this device support 'sleep' speed? | No | 75 | |- light\_control | Expose device lights as lightbulbs. | No | 76 | |- allergic\_func | Does this device support 'allergic' function? | No | 77 | |- temperature\_sensor | Expose device temperature as temperature sensor. | No | 78 | |- humidity\_sensor | Expose device humidity as humidity sensor. | No | 79 | |- polling | Adding a refresh time for the all sensors in seconds. | No | 80 | |- humidifier | Adding humidified support. | No | 81 | |- logger | Getting data from humidity and temp sensors and save value into txt file. | No | 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | API, 3 | APIEvent, 4 | CharacteristicSetCallback, 5 | CharacteristicValue, 6 | DynamicPlatformPlugin, 7 | HAP, 8 | Logging, 9 | PlatformAccessory, 10 | PlatformAccessoryEvent, 11 | PlatformConfig 12 | } from 'homebridge'; 13 | import {AirClient, HttpClient, CoapClient, PlainCoapClient, HttpClientLegacy} from 'philips-air'; 14 | import {promisify} from 'util'; 15 | import {exec} from 'child_process'; 16 | import * as fs from 'fs'; 17 | import timestamp from 'time-stamp'; 18 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 19 | // @ts-ignore 20 | import localStorage from 'node-sessionstorage'; 21 | import {PhilipsAirPlatformConfig, DeviceConfig} from './configTypes'; 22 | import {PurifierStatus, PurifierFilters, PurifierFirmware} from './deviceTypes'; 23 | 24 | let hap: HAP; 25 | let Accessory: typeof PlatformAccessory; 26 | 27 | const PLUGIN_NAME = 'homebridge-philips-air'; 28 | const PLATFORM_NAME = 'philipsAir'; 29 | const pathToModule = require.resolve(PLUGIN_NAME); 30 | const pathTopyaircontrol = pathToModule.replace('dist/index.js', 'node_modules/philips-air/pyaircontrol.py'); 31 | const pathToSensorFiles = pathToModule.replace('dist/index.js', 'sensor/'); 32 | 33 | enum CommandType { 34 | Polling = 0, 35 | GetFirmware, 36 | GetFilters, 37 | GetStatus, 38 | SetData 39 | } 40 | 41 | type Command = { 42 | purifier: Purifier, 43 | type: CommandType, 44 | callback?: (error?: Error | null | undefined) => void, 45 | data?: any // eslint-disable-line @typescript-eslint/no-explicit-any 46 | }; 47 | 48 | type Purifier = { 49 | accessory: PlatformAccessory, 50 | client: AirClient, 51 | config: DeviceConfig, 52 | timeout?: NodeJS.Timeout, 53 | lastfirmware?: number, 54 | lastfilters?: number, 55 | laststatus?: number, 56 | aqil?: number, 57 | uil?: string, 58 | rh?: number, 59 | rhset?: number, 60 | func?: string 61 | }; 62 | 63 | class PhilipsAirPlatform implements DynamicPlatformPlugin { 64 | private readonly log: Logging; 65 | private readonly api: API; 66 | private readonly config: PhilipsAirPlatformConfig; 67 | private readonly timeout: number; 68 | private readonly cachedAccessories: Array = []; 69 | private readonly purifiers: Map = new Map(); 70 | private readonly commandQueue: Array = []; 71 | private queueRunning = false; 72 | 73 | enqueuePromise = promisify(this.enqueueCommand); 74 | 75 | constructor(log: Logging, config: PlatformConfig, api: API) { 76 | this.log = log; 77 | this.config = config as unknown as PhilipsAirPlatformConfig; 78 | this.api = api; 79 | 80 | this.timeout = (this.config.timeout_seconds || 5) * 1000; 81 | 82 | api.on(APIEvent.DID_FINISH_LAUNCHING, this.didFinishLaunching.bind(this)); 83 | } 84 | 85 | configureAccessory(accessory: PlatformAccessory): void { 86 | this.cachedAccessories.push(accessory); 87 | } 88 | 89 | didFinishLaunching(): void { 90 | const ips: Array = []; 91 | this.config.devices.forEach((device: DeviceConfig) => { 92 | this.addAccessory(device); 93 | const uuid = hap.uuid.generate(device.ip); 94 | ips.push(uuid); 95 | }); 96 | 97 | const badAccessories: Array = []; 98 | this.cachedAccessories.forEach(cachedAcc => { 99 | if (!ips.includes(cachedAcc.UUID)) { 100 | badAccessories.push(cachedAcc); 101 | } 102 | }); 103 | this.removeAccessories(badAccessories); 104 | 105 | this.purifiers.forEach((purifier) => { 106 | this.enqueueCommand(CommandType.Polling, purifier); 107 | this.enqueueCommand(CommandType.GetFirmware, purifier); 108 | this.enqueueCommand(CommandType.GetStatus, purifier); 109 | this.enqueueCommand(CommandType.GetFilters, purifier); 110 | }); 111 | } 112 | 113 | async storeKey(purifier: Purifier): Promise { 114 | if (purifier.client && purifier.client instanceof HttpClient) { 115 | purifier.accessory.context.key = (purifier.client as HttpClient).key; 116 | } 117 | } 118 | 119 | async setData(purifier: Purifier, values: any, // eslint-disable-line @typescript-eslint/no-explicit-any 120 | callback?: (error?: Error | null | undefined) => void): Promise { 121 | try { 122 | await purifier.client?.setValues(values); 123 | await this.storeKey(purifier); 124 | if (callback) { 125 | callback(); 126 | } 127 | } catch (err) { 128 | if (callback) { 129 | callback(err); 130 | } 131 | } 132 | } 133 | 134 | async updatePolling(purifier: Purifier): Promise { 135 | try { 136 | // Polling interval 137 | let polling = purifier.config.polling || 60; 138 | if (polling < 60) { 139 | polling = 60; 140 | } 141 | setInterval(function() { 142 | exec('python3 ' + pathTopyaircontrol + ' --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --status', (error, stdout, stderr) => { 143 | if (error || stderr) { 144 | console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[36m[Philips Air] \x1b[31m[' + purifier.config.name + '] Unable to get data for polling: Error: spawnSync python3 ETIMEDOUT.\x1b[0m'); 145 | console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[33mIf your have "Error: spawnSync python3 ETIMEDOUT" your need unplug the accessory from outlet for 10 seconds and plug again.\x1b[0m'); 146 | } 147 | 148 | if (error || stderr || error && stderr) { 149 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 150 | // @ts-ignore 151 | stdout = {om: localStorage.getItem('om'), pwr: localStorage.getItem('pwr'), cl: false, aqil: localStorage.getItem('aqil'), uil: localStorage.getItem('uil'), dt: 0, dtrs: 0, mode: localStorage.getItem('mode'), func: localStorage.getItem('func'), rhset: localStorage.getItem('rhset'), 'rh': localStorage.getItem('rh'), 'temp': localStorage.getItem('temp'), pm25: localStorage.getItem('pm25'), iaql: localStorage.getItem('iaql'), aqit: 4, ddp: '1', rddp: localStorage.getItem('rddp'), err: 0, wl: localStorage.getItem('wl'), fltt1: localStorage.getItem('fltt1'), fltt2: localStorage.getItem('fltt2'), fltsts0: localStorage.getItem('fltsts0'), fltsts1: localStorage.getItem('fltsts1'), fltsts2: localStorage.getItem('fltsts2'), wicksts: localStorage.getItem('wicksts')}; 152 | stdout = JSON.stringify(stdout); 153 | } 154 | const obj = JSON.parse(stdout); 155 | if (!error || !stderr || !error && !stderr) { 156 | localStorage.setItem('pwr', obj.pwr); 157 | localStorage.setItem('om', obj.om); 158 | localStorage.setItem('aqil', obj.aqil); 159 | localStorage.setItem('uil', obj.uil); 160 | localStorage.setItem('mode', obj.mode); 161 | localStorage.setItem('func', obj.func); 162 | localStorage.setItem('rhset', obj.rhset); 163 | localStorage.setItem('iaql', obj.iaql); 164 | localStorage.setItem('pm25', obj.pm25); 165 | localStorage.setItem('rh', obj.rh); 166 | localStorage.setItem('temp', obj.temp); 167 | localStorage.setItem('rddp', obj.rddp); 168 | localStorage.setItem('wl', obj.wl); 169 | localStorage.setItem('fltt1', obj.fltt1); 170 | localStorage.setItem('fltt2', obj.fltt2); 171 | localStorage.setItem('fltsts0', obj.fltsts0); 172 | localStorage.setItem('fltsts1', obj.fltsts1); 173 | localStorage.setItem('fltsts2', obj.fltsts2); 174 | localStorage.setItem('wicksts', obj.wicksts); 175 | } else { 176 | localStorage.setItem('pwr', 1); 177 | localStorage.setItem('om', '0'); 178 | localStorage.setItem('aqil', '0'); 179 | localStorage.setItem('uil', 0); 180 | localStorage.setItem('mode', 'A'); 181 | localStorage.setItem('func', 'PH'); 182 | localStorage.setItem('rhset', 50); 183 | localStorage.setItem('iaql', 1); 184 | localStorage.setItem('pm25', 1); 185 | localStorage.setItem('rh', 45); 186 | localStorage.setItem('temp', 25); 187 | localStorage.setItem('rddp', 1); 188 | localStorage.setItem('wl', 100); 189 | localStorage.setItem('fltt1', 'A3'); 190 | localStorage.setItem('fltt2', 'C7'); 191 | localStorage.setItem('fltsts0', 287); 192 | localStorage.setItem('fltsts1', 2553); 193 | localStorage.setItem('fltsts2', 2553); 194 | localStorage.setItem('wicksts', 4005); 195 | } 196 | 197 | const purifierService = purifier.accessory.getService(hap.Service.AirPurifier); 198 | if (purifierService) { 199 | const state = parseInt(obj.pwr) * 2; 200 | 201 | purifierService 202 | .updateCharacteristic(hap.Characteristic.Active, obj.pwr) 203 | .updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state) 204 | .updateCharacteristic(hap.Characteristic.LockPhysicalControls, obj.cl); 205 | } 206 | const qualityService = purifier.accessory.getService(hap.Service.AirQualitySensor); 207 | if (qualityService) { 208 | const iaql = Math.ceil(obj.iaql / 3); 209 | qualityService 210 | .updateCharacteristic(hap.Characteristic.AirQuality, iaql) 211 | .updateCharacteristic(hap.Characteristic.PM2_5Density, obj.pm25); 212 | } 213 | if (purifier.config.temperature_sensor) { 214 | const temperature_sensor = purifier.accessory.getService('Temperature'); 215 | if (temperature_sensor) { 216 | temperature_sensor.updateCharacteristic(hap.Characteristic.CurrentTemperature, obj.temp); 217 | } 218 | } 219 | if (purifier.config.humidity_sensor) { 220 | const humidity_sensor = purifier.accessory.getService('Humidity'); 221 | if (humidity_sensor) { 222 | humidity_sensor.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, obj.rh); 223 | } 224 | } 225 | if (purifier.config.light_control) { 226 | const lightsService = purifier.accessory.getService('Lights'); 227 | if (obj.pwr == '1') { 228 | if (lightsService) { 229 | lightsService 230 | .updateCharacteristic(hap.Characteristic.On, obj.aqil > 0) 231 | .updateCharacteristic(hap.Characteristic.Brightness, obj.aqil); 232 | } 233 | } 234 | } 235 | if (purifier.config.humidifier) { 236 | let water_level = 100; 237 | let speed_humidity = 0; 238 | if (obj.func == 'PH' && obj.wl == 0) { 239 | water_level = 0; 240 | } 241 | if (obj.pwr == '1') { 242 | if (obj.func == 'PH' && water_level == 100) { 243 | if (obj.rhset == 40) { 244 | speed_humidity = 25; 245 | } else if (obj.rhset == 50) { 246 | speed_humidity = 50; 247 | } else if (obj.rhset == 60) { 248 | speed_humidity = 75; 249 | } else if (obj.rhset == 70) { 250 | speed_humidity = 100; 251 | } 252 | } 253 | } 254 | const Humidifier = purifier.accessory.getService('Humidifier'); 255 | if (Humidifier) { 256 | Humidifier 257 | .updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, obj.rh) 258 | .updateCharacteristic(hap.Characteristic.WaterLevel, water_level) 259 | .updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1) 260 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); 261 | if (water_level == 0) { 262 | if (obj.func != 'P') { 263 | exec('airctrl --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --func P', (error, stdout, stderr) => { 264 | if (error || stderr) { 265 | console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[36m[Philips Air] \x1b[31m[' + purifier.config.name + '] Unable to get data for polling: Error: spawnSync python3 ETIMEDOUT.\x1b[0m'); 266 | console.log(timestamp('[DD.MM.YYYY, HH:mm:ss] ') + '\x1b[33mIf your have "Error: spawnSync python3 ETIMEDOUT" your need unplug the accessory from outlet for 10 seconds and plug again.\x1b[0m'); 267 | } 268 | }); 269 | } 270 | Humidifier 271 | .updateCharacteristic(hap.Characteristic.Active, 0) 272 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0) 273 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0); 274 | } 275 | } 276 | } 277 | if (purifier.config.logger) { 278 | if (purifier.config.temperature_sensor) { 279 | if (!error || !stderr || !error && !stderr) { 280 | const logger_temp = fs.createWriteStream(pathToSensorFiles + 'temp.txt', { 281 | flags: 'w' 282 | }); 283 | 284 | logger_temp.write(obj.temp.toString()); 285 | logger_temp.end(); 286 | } 287 | } 288 | if (purifier.config.humidity_sensor) { 289 | if (!error || !stderr || !error && !stderr) { 290 | const logger_hum = fs.createWriteStream(pathToSensorFiles + 'hum.txt', { 291 | flags: 'w' 292 | }); 293 | 294 | logger_hum.write(obj.rh.toString()); 295 | logger_hum.end(); 296 | } 297 | } 298 | } 299 | }); 300 | }, polling * 1000); 301 | } catch (err) { 302 | this.log.error('[' + purifier.config.name + '] Unable to load polling info'); 303 | } 304 | } 305 | 306 | async updateFirmware(purifier: Purifier): Promise { 307 | try { 308 | purifier.lastfirmware = Date.now(); 309 | const firmware: PurifierFirmware = await purifier.client?.getFirmware(); 310 | await this.storeKey(purifier); 311 | const accInfo = purifier.accessory.getService(hap.Service.AccessoryInformation); 312 | if (accInfo) { 313 | const name = firmware.modelid; 314 | 315 | accInfo 316 | .updateCharacteristic(hap.Characteristic.Manufacturer, 'Philips') 317 | .updateCharacteristic(hap.Characteristic.SerialNumber, purifier.config.ip) 318 | .updateCharacteristic(hap.Characteristic.Model, name) 319 | .updateCharacteristic(hap.Characteristic.FirmwareRevision, firmware.version); 320 | } 321 | } catch (err) { 322 | this.log.error('[' + purifier.config.name + '] Unable to load firmware info: ' + err); 323 | } 324 | } 325 | 326 | async updateFilters(purifier: Purifier): Promise { 327 | try { 328 | const filters: PurifierFilters = await purifier.client?.getFilters(); 329 | purifier.lastfilters = Date.now(); 330 | await this.storeKey(purifier); 331 | const preFilter = purifier.accessory.getService('Pre-filter'); 332 | if (preFilter) { 333 | const fltsts0change = filters.fltsts0 == 0; 334 | const fltsts0life = filters.fltsts0 / 360 * 100; 335 | 336 | preFilter 337 | .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts0change) 338 | .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts0life); 339 | } 340 | 341 | const carbonFilter = purifier.accessory.getService('Active carbon filter'); 342 | if (carbonFilter) { 343 | const fltsts2change = filters.fltsts2 == 0; 344 | const fltsts2life = filters.fltsts2 / 4800 * 100; 345 | 346 | carbonFilter 347 | .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts2change) 348 | .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts2life); 349 | } 350 | 351 | const hepaFilter = purifier.accessory.getService('HEPA filter'); 352 | if (hepaFilter) { 353 | const fltsts1change = filters.fltsts1 == 0; 354 | const fltsts1life = filters.fltsts1 / 4800 * 100; 355 | 356 | hepaFilter 357 | .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltsts1change) 358 | .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltsts1life); 359 | } 360 | if (purifier.config.humidifier) { 361 | const wickFilter = purifier.accessory.getService('Wick filter'); 362 | if (wickFilter) { 363 | const fltwickchange = filters.wicksts == 0; 364 | const fltwicklife = Math.round(filters.wicksts / 4800 * 100); 365 | wickFilter 366 | .updateCharacteristic(hap.Characteristic.FilterChangeIndication, fltwickchange) 367 | .updateCharacteristic(hap.Characteristic.FilterLifeLevel, fltwicklife); 368 | } 369 | } 370 | } catch (err) { 371 | this.log.error('[' + purifier.config.name + '] Unable to load filter info: ' + err); 372 | } 373 | } 374 | 375 | async updateStatus(purifier: Purifier): Promise { 376 | try { 377 | const status: PurifierStatus = await purifier.client?.getStatus(); 378 | purifier.laststatus = Date.now(); 379 | await this.storeKey(purifier); 380 | const purifierService = purifier.accessory.getService(hap.Service.AirPurifier); 381 | if (purifierService) { 382 | const state = parseInt(status.pwr) * 2; 383 | 384 | purifierService 385 | .updateCharacteristic(hap.Characteristic.Active, status.pwr) 386 | .updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state) 387 | .updateCharacteristic(hap.Characteristic.LockPhysicalControls, status.cl); 388 | } 389 | const qualityService = purifier.accessory.getService(hap.Service.AirQualitySensor); 390 | if (qualityService) { 391 | const iaql = Math.ceil(status.iaql / 3); 392 | qualityService 393 | .updateCharacteristic(hap.Characteristic.AirQuality, iaql) 394 | .updateCharacteristic(hap.Characteristic.PM2_5Density, status.pm25); 395 | } 396 | if (purifier.config.temperature_sensor) { 397 | const temperature_sensor = purifier.accessory.getService('Temperature'); 398 | if (temperature_sensor) { 399 | temperature_sensor.updateCharacteristic(hap.Characteristic.CurrentTemperature, status.temp); 400 | } 401 | } 402 | if (purifier.config.humidity_sensor) { 403 | const humidity_sensor = purifier.accessory.getService('Humidity'); 404 | if (humidity_sensor) { 405 | humidity_sensor.updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, status.rh); 406 | } 407 | } 408 | 409 | if (purifier.config.light_control) { 410 | const lightsService = purifier.accessory.getService('Lights'); 411 | if (status.pwr == '1') { 412 | if (lightsService) { 413 | lightsService 414 | .updateCharacteristic(hap.Characteristic.On, status.aqil > 0) 415 | .updateCharacteristic(hap.Characteristic.Brightness, status.aqil); 416 | } 417 | } 418 | } 419 | if (purifier.config.humidifier) { 420 | const Humidifier = purifier.accessory.getService('Humidifier'); 421 | if (Humidifier) { 422 | let speed_humidity = 0; 423 | let state_ph = 0; 424 | let water_level = 100; 425 | if (status.func == 'PH' && status.wl == 0) { 426 | water_level = 0; 427 | } 428 | if (status.pwr == '1') { 429 | if (status.func == 'PH' && water_level == 100) { 430 | state_ph = 1; 431 | if (status.rhset == 40) { 432 | speed_humidity = 25; 433 | } else if (status.rhset == 50) { 434 | speed_humidity = 50; 435 | } else if (status.rhset == 60) { 436 | speed_humidity = 75; 437 | } else if (status.rhset == 70) { 438 | speed_humidity = 100; 439 | } 440 | } 441 | } 442 | Humidifier 443 | .updateCharacteristic(hap.Characteristic.CurrentRelativeHumidity, status.rh) 444 | .updateCharacteristic(hap.Characteristic.WaterLevel, water_level) 445 | .updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); 446 | if (state_ph && status.rhset >= 40) { 447 | Humidifier 448 | .updateCharacteristic(hap.Characteristic.Active, state_ph) 449 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2) 450 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); 451 | } 452 | if (water_level == 0) { 453 | if (status.func != 'P') { 454 | exec('airctrl --ipaddr ' + purifier.config.ip + ' --protocol ' + purifier.config.protocol + ' --func P', (err, stdout, stderr) => { 455 | if (err) { 456 | return; 457 | } 458 | if (stderr) { 459 | console.error('Unable to switch off purifier ' + stderr + '. If your have "sync timeout" error your need unplug the accessory from the outlet for 10 seconds.'); 460 | } 461 | }); 462 | } 463 | } 464 | } 465 | } 466 | } catch (err) { 467 | this.log.error('[' + purifier.config.name + '] Unable to load status info: ' + err); 468 | } 469 | } 470 | 471 | async setPower(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 472 | const purifier = this.purifiers.get(accessory.displayName); 473 | if (purifier) { 474 | const values = { 475 | pwr: (state as boolean).toString() 476 | }; 477 | try { 478 | const status: PurifierStatus = await purifier.client?.getStatus(); 479 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 480 | // @ts-ignore 481 | purifier.laststatus = Date.now(); 482 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 483 | // @ts-ignore 484 | await this.storeKey(purifier); 485 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 486 | // @ts-ignore 487 | let water_level = 100; 488 | if (status.func == 'PH' && status.wl == 0) { 489 | water_level = 0; 490 | } 491 | const purifierService = accessory.getService(hap.Service.AirPurifier); 492 | if (purifierService) { 493 | purifierService.updateCharacteristic(hap.Characteristic.CurrentAirPurifierState, state as number * 2); 494 | } 495 | if (purifier.config.humidifier) { 496 | if (water_level == 0) { 497 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 498 | // @ts-ignore 499 | values['func'] = 'P'; 500 | } 501 | } 502 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 503 | // @ts-ignore 504 | await this.enqueuePromise(CommandType.SetData, purifier, values); 505 | if (purifier.config.light_control) { 506 | const lightsService = accessory.getService('Lights'); 507 | if (lightsService) { 508 | if (state) { 509 | lightsService 510 | .updateCharacteristic(hap.Characteristic.On, status.aqil > 0) 511 | .updateCharacteristic(hap.Characteristic.Brightness, status.aqil); 512 | } else { 513 | lightsService 514 | .updateCharacteristic(hap.Characteristic.On, 0) 515 | .updateCharacteristic(hap.Characteristic.Brightness, 0); 516 | } 517 | } 518 | } 519 | if (purifier.config.humidifier) { 520 | const Humidifier = accessory.getService('Humidifier'); 521 | let state_ph = 0; 522 | if (status.func == 'PH' && water_level == 100) { 523 | state_ph = 1; 524 | } 525 | if (Humidifier) { 526 | Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); 527 | if (state) { 528 | Humidifier 529 | .updateCharacteristic(hap.Characteristic.Active, state_ph) 530 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2); 531 | } else { 532 | Humidifier 533 | .updateCharacteristic(hap.Characteristic.Active, 0) 534 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0) 535 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0); 536 | } 537 | } 538 | } 539 | } catch (err) { 540 | this.log.error('[' + purifier.config.name + '] Error setting power: ' + err); 541 | } 542 | } 543 | } 544 | 545 | async setBrightness(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 546 | const purifier = this.purifiers.get(accessory.displayName); 547 | 548 | if (purifier) { 549 | const values = { 550 | aqil: state, 551 | uil: state ? '1' : '0' 552 | }; 553 | 554 | try { 555 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 556 | // @ts-ignore 557 | await this.enqueuePromise(CommandType.SetData, purifier, values); 558 | } catch (err) { 559 | this.log.error('[' + purifier.config.name + '] Error setting brightness: ' + err); 560 | } 561 | } 562 | } 563 | 564 | async setMode(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 565 | const purifier = this.purifiers.get(accessory.displayName); 566 | if (purifier) { 567 | const values = { 568 | mode: state ? 'P' : 'M' 569 | }; 570 | if (purifier.config.allergic_func) { 571 | values.mode = state ? 'P' : 'A'; 572 | } else { 573 | values.mode = state ? 'P' : 'M'; 574 | } 575 | try { 576 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 577 | // @ts-ignore 578 | await this.enqueuePromise(CommandType.SetData, purifier, values); 579 | 580 | if (state != 0) { 581 | const purifierService = accessory.getService(hap.Service.AirPurifier); 582 | if (purifierService) { 583 | purifierService 584 | .updateCharacteristic(hap.Characteristic.RotationSpeed, 0) 585 | .updateCharacteristic(hap.Characteristic.TargetAirPurifierState, state); 586 | } 587 | } 588 | } catch (err) { 589 | this.log.error('[' + purifier.config.name + '] Error setting mode: ' + err); 590 | } 591 | } 592 | } 593 | 594 | async setLock(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 595 | const purifier = this.purifiers.get(accessory.displayName); 596 | 597 | if (purifier) { 598 | const values = { 599 | cl: state == 1 600 | }; 601 | 602 | try { 603 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 604 | // @ts-ignore 605 | await this.enqueuePromise(CommandType.SetData, purifier, values); 606 | } catch (err) { 607 | this.log.error('[' + purifier.config.name + '] Error setting lock: ' + err); 608 | } 609 | } 610 | } 611 | 612 | async setHumidity(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 613 | const purifier = this.purifiers.get(accessory.displayName); 614 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 615 | // @ts-ignore 616 | const status: PurifierStatus = await purifier.client?.getStatus(); 617 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 618 | // @ts-ignore 619 | purifier.laststatus = Date.now(); 620 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 621 | // @ts-ignore 622 | await this.storeKey(purifier); 623 | const Humidifier = accessory.getService('Humidifier'); 624 | if (purifier) { 625 | const values = { 626 | func: state ? 'PH' : 'P' 627 | }; 628 | let water_level = 100; 629 | if (status.func == 'PH' && status.wl == 0) { 630 | water_level = 0; 631 | } 632 | let speed_humidity = 0; 633 | let state_ph = 0; 634 | if (status.func == 'PH' && water_level == 100) { 635 | state_ph = 1; 636 | if (status.rhset == 40) { 637 | speed_humidity = 25; 638 | } else if (status.rhset == 50) { 639 | speed_humidity = 50; 640 | } else if (status.rhset == 60) { 641 | speed_humidity = 75; 642 | } else if (status.rhset == 70) { 643 | speed_humidity = 100; 644 | } 645 | } 646 | try { 647 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 648 | // @ts-ignore 649 | await this.enqueuePromise(CommandType.SetData, purifier, values); 650 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 651 | // @ts-ignore 652 | Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); 653 | if (state) { 654 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 655 | // @ts-ignore 656 | Humidifier 657 | .updateCharacteristic(hap.Characteristic.Active, 1) 658 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, state_ph * 2) 659 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); 660 | } else { 661 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 662 | // @ts-ignore 663 | Humidifier 664 | .updateCharacteristic(hap.Characteristic.Active, 0) 665 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 0) 666 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, 0); 667 | } 668 | } catch (err) { 669 | this.log.error('[' + purifier.config.name + '] Error setting func: ' + err); 670 | } 671 | } 672 | } 673 | 674 | async setHumidityTarget(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 675 | const purifier = this.purifiers.get(accessory.displayName); 676 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 677 | // @ts-ignore 678 | const status: PurifierStatus = await purifier.client?.getStatus(); 679 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 680 | // @ts-ignore 681 | purifier.laststatus = Date.now(); 682 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 683 | // @ts-ignore 684 | await this.storeKey(purifier); 685 | const Humidifier = accessory.getService('Humidifier'); 686 | if (purifier) { 687 | const speed = state; 688 | const values = { 689 | func: state ? 'PH' : 'P', 690 | rhset: 40 691 | }; 692 | let speed_humidity = 0; 693 | if (speed > 0 && speed <= 25) { 694 | values.rhset = 40; 695 | speed_humidity = 25; 696 | } else if (speed > 25 && speed <= 50) { 697 | values.rhset = 50; 698 | speed_humidity = 50; 699 | } else if (speed > 50 && speed <= 75) { 700 | values.rhset = 60; 701 | speed_humidity = 75; 702 | } else if (speed > 75 && speed <= 100) { 703 | values.rhset = 70; 704 | speed_humidity = 100; 705 | } 706 | try { 707 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 708 | // @ts-ignore 709 | await this.enqueuePromise(CommandType.SetData, purifier, values); 710 | if (Humidifier) { 711 | let water_level = 100; 712 | if (status.func == 'PH' && status.wl == 0) { 713 | water_level = 0; 714 | } 715 | Humidifier.updateCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState, 1); 716 | if (speed_humidity > 0) { 717 | Humidifier 718 | .updateCharacteristic(hap.Characteristic.Active, 1) 719 | .updateCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState, 2) 720 | .updateCharacteristic(hap.Characteristic.WaterLevel, water_level) 721 | .updateCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold, speed_humidity); 722 | } else { 723 | Humidifier 724 | .updateCharacteristic(hap.Characteristic.Active, 0); 725 | } 726 | } 727 | } catch (err) { 728 | this.log.error('[' + purifier.config.name + '] Error setting humidifier: ' + err); 729 | } 730 | } 731 | } 732 | 733 | async setFan(accessory: PlatformAccessory, state: CharacteristicValue): Promise { 734 | const purifier = this.purifiers.get(accessory.displayName); 735 | 736 | if (purifier) { 737 | let divisor = 25; 738 | let offset = 0; 739 | if (purifier.config.sleep_speed) { 740 | divisor = 20; 741 | offset = 1; 742 | } 743 | const speed = Math.ceil(state as number / divisor); 744 | if (speed > 0) { 745 | const values = { 746 | mode: 'M', 747 | om: '' 748 | }; 749 | if (offset == 1 && speed == 1) { 750 | values.om = 's'; 751 | } else if (speed < 4 + offset) { 752 | values.om = (speed - offset).toString(); 753 | } else { 754 | values.om = 't'; 755 | } 756 | 757 | try { 758 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 759 | // @ts-ignore 760 | await this.enqueuePromise(CommandType.SetData, purifier, values); 761 | 762 | const service = accessory.getService(hap.Service.AirPurifier); 763 | if (service) { 764 | service.updateCharacteristic(hap.Characteristic.TargetAirPurifierState, 0); 765 | } 766 | 767 | if (purifier.timeout) { 768 | clearTimeout(purifier.timeout); 769 | } 770 | purifier.timeout = setTimeout(() => { 771 | if (service) { 772 | service.updateCharacteristic(hap.Characteristic.RotationSpeed, speed * divisor); 773 | } 774 | purifier.timeout = undefined; 775 | }, 1000); 776 | } catch (err) { 777 | this.log.error('[' + purifier.config.name + '] Error setting fan: ' + err); 778 | } 779 | } 780 | } 781 | } 782 | 783 | addAccessory(config: DeviceConfig): void { 784 | this.log('[' + config.name + '] Initializing accessory...'); 785 | 786 | const uuid = hap.uuid.generate(config.ip); 787 | let accessory = this.cachedAccessories.find(cachedAcc => { 788 | return cachedAcc.UUID == uuid; 789 | }); 790 | 791 | if (!accessory) { 792 | accessory = new Accessory(config.name, uuid); 793 | 794 | accessory.addService(hap.Service.AirPurifier, config.name); 795 | accessory.addService(hap.Service.AirQualitySensor, 'Air quality', 'Air quality'); 796 | 797 | if (config.light_control) { 798 | accessory.addService(hap.Service.Lightbulb, 'Lights', 'Lights') 799 | .addCharacteristic(hap.Characteristic.Brightness); 800 | } 801 | 802 | accessory.addService(hap.Service.FilterMaintenance, 'Pre-filter', 'Pre-filter'); 803 | accessory.addService(hap.Service.FilterMaintenance, 'Active carbon filter', 'Active carbon filter'); 804 | accessory.addService(hap.Service.FilterMaintenance, 'HEPA filter', 'HEPA filter'); 805 | if (config.temperature_sensor) { 806 | accessory.addService(hap.Service.TemperatureSensor, 'Temperature', 'Temperature'); 807 | } 808 | if (config.humidity_sensor) { 809 | accessory.addService(hap.Service.HumiditySensor, 'Humidity', 'Humidity'); 810 | } 811 | if (config.humidifier) { 812 | accessory.addService(hap.Service.HumidifierDehumidifier, 'Humidifier', 'Humidifier'); 813 | accessory.addService(hap.Service.FilterMaintenance, 'Wick filter', 'Wick filter'); 814 | } 815 | 816 | this.api.registerPlatformAccessories('homebridge-philips-air', 'philipsAir', [accessory]); 817 | } else { 818 | let lightsService = accessory.getService('Lights'); 819 | 820 | if (config.light_control) { 821 | if (lightsService == undefined) { 822 | lightsService = accessory.addService(hap.Service.Lightbulb, 'Lights', 'Lights'); 823 | lightsService.addCharacteristic(hap.Characteristic.Brightness); 824 | } 825 | } else if (lightsService != undefined) { 826 | accessory.removeService(lightsService); 827 | } 828 | const temperature_sensor = accessory.getService('Temperature'); 829 | if (config.temperature_sensor) { 830 | if (temperature_sensor == undefined) { 831 | accessory.addService(hap.Service.TemperatureSensor, 'Temperature', 'Temperature'); 832 | } 833 | } else if (temperature_sensor != undefined) { 834 | accessory.removeService(temperature_sensor); 835 | } 836 | const humidity_sensor = accessory.getService('Humidity'); 837 | if (config.humidity_sensor) { 838 | if (humidity_sensor == undefined) { 839 | accessory.addService(hap.Service.HumiditySensor, 'Humidity', 'Humidity'); 840 | } 841 | } else if (humidity_sensor != undefined) { 842 | accessory.removeService(humidity_sensor); 843 | } 844 | const Humidifier = accessory.getService('Humidifier'); 845 | if (config.humidifier) { 846 | if (Humidifier == undefined) { 847 | accessory.addService(hap.Service.HumidifierDehumidifier, 'Humidifier', 'Humidifier'); 848 | } 849 | } else if (Humidifier != undefined) { 850 | accessory.removeService(Humidifier); 851 | } 852 | } 853 | 854 | this.setService(accessory, config); 855 | 856 | let client: AirClient; 857 | switch (config.protocol) { 858 | case 'coap': 859 | client = new CoapClient(config.ip, this.timeout); 860 | break; 861 | case 'plain_coap': 862 | client = new PlainCoapClient(config.ip, this.timeout); 863 | break; 864 | case 'http_legacy': 865 | client = new HttpClientLegacy(config.ip, this.timeout); 866 | break; 867 | case 'http': 868 | default: 869 | if (accessory.context.key) { 870 | client = new HttpClient(config.ip, this.timeout, accessory.context.key); 871 | } else { 872 | client = new HttpClient(config.ip, this.timeout); 873 | } 874 | } 875 | 876 | this.purifiers.set(accessory.displayName, { 877 | accessory: accessory, 878 | client: client, 879 | config: config 880 | }); 881 | } 882 | 883 | removeAccessories(accessories: Array): void { 884 | accessories.forEach(accessory => { 885 | this.log('[' + accessory.displayName + '] Removed from Homebridge.'); 886 | this.api.unregisterPlatformAccessories('homebridge-philips-air', 'philipsAir', [accessory]); 887 | }); 888 | } 889 | 890 | setService(accessory: PlatformAccessory, config: DeviceConfig): void { 891 | accessory.on(PlatformAccessoryEvent.IDENTIFY, () => { 892 | this.log('[' + accessory.displayName + '] Identify requested.'); 893 | }); 894 | 895 | const purifierService = accessory.getService(hap.Service.AirPurifier); 896 | let min_step_purifier_speed = 25; 897 | if (config.sleep_speed) { 898 | min_step_purifier_speed = 20; 899 | } 900 | if (purifierService) { 901 | purifierService 902 | .getCharacteristic(hap.Characteristic.Active) 903 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 904 | try { 905 | await this.setPower(accessory, state); 906 | callback(); 907 | } catch (err) { 908 | callback(err); 909 | } 910 | }); 911 | 912 | purifierService 913 | .getCharacteristic(hap.Characteristic.TargetAirPurifierState) 914 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 915 | try { 916 | await this.setMode(accessory, state); 917 | callback(); 918 | } catch (err) { 919 | callback(err); 920 | } 921 | }); 922 | 923 | purifierService 924 | .getCharacteristic(hap.Characteristic.LockPhysicalControls) 925 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 926 | try { 927 | await this.setLock(accessory, state); 928 | callback(); 929 | } catch (err) { 930 | callback(err); 931 | } 932 | }); 933 | 934 | purifierService 935 | .getCharacteristic(hap.Characteristic.RotationSpeed) 936 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 937 | try { 938 | await this.setFan(accessory, state); 939 | callback(); 940 | } catch (err) { 941 | callback(err); 942 | } 943 | }).setProps({ 944 | minValue: 0, 945 | maxValue: 100, 946 | minStep: min_step_purifier_speed 947 | }); 948 | } 949 | 950 | if (config.light_control) { 951 | const lightService = accessory.getService('Lights'); 952 | if (lightService) { 953 | lightService 954 | .getCharacteristic(hap.Characteristic.Brightness) 955 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 956 | try { 957 | await this.setBrightness(accessory, state); 958 | callback(); 959 | } catch (err) { 960 | callback(err); 961 | } 962 | }).setProps({ 963 | minValue: 0, 964 | maxValue: 100, 965 | minStep: 25 966 | }); 967 | } 968 | } 969 | 970 | if (config.humidifier) { 971 | const Humidifier = accessory.getService('Humidifier'); 972 | if (Humidifier) { 973 | Humidifier 974 | .getCharacteristic(hap.Characteristic.Active) 975 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 976 | try { 977 | await this.setHumidity(accessory, state); 978 | callback(); 979 | } catch (err) { 980 | callback(err); 981 | } 982 | }); 983 | Humidifier 984 | .getCharacteristic(hap.Characteristic.CurrentHumidifierDehumidifierState) 985 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 986 | try { 987 | await this.setHumidityTarget(accessory, state); 988 | await this.setHumidity(accessory, state); 989 | callback(); 990 | } catch (err) { 991 | callback(err); 992 | } 993 | }).setProps({ 994 | validValues: [ 995 | hap.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE, 996 | hap.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING 997 | ] 998 | }); 999 | Humidifier 1000 | .getCharacteristic(hap.Characteristic.TargetHumidifierDehumidifierState) 1001 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 1002 | try { 1003 | await this.setHumidityTarget(accessory, state); 1004 | await this.setHumidity(accessory, state); 1005 | callback(); 1006 | } catch (err) { 1007 | callback(err); 1008 | } 1009 | }).setProps({ 1010 | validValues: [ 1011 | hap.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER 1012 | ] 1013 | }); 1014 | Humidifier 1015 | .getCharacteristic(hap.Characteristic.RelativeHumidityHumidifierThreshold) 1016 | .on('set', async(state: CharacteristicValue, callback: CharacteristicSetCallback) => { 1017 | try { 1018 | await this.setHumidityTarget(accessory, state); 1019 | callback(); 1020 | } catch (err) { 1021 | callback(err); 1022 | } 1023 | }).setProps({ 1024 | minValue: 0, 1025 | maxValue: 100, 1026 | minStep: 25 1027 | }); 1028 | } 1029 | } 1030 | } 1031 | 1032 | enqueueAccessory(commandType: CommandType, accessory: PlatformAccessory): void { 1033 | const purifier = this.purifiers.get(accessory.displayName); 1034 | 1035 | if (purifier) { 1036 | this.enqueueCommand(commandType, purifier); 1037 | } 1038 | } 1039 | 1040 | enqueueCommand(commandType: CommandType, purifier: Purifier, data?: any, // eslint-disable-line @typescript-eslint/no-explicit-any 1041 | callback?: (error?: Error | null | undefined) => void): void { 1042 | if (commandType != CommandType.SetData) { 1043 | const exists = this.commandQueue.find((command) => { 1044 | return command.purifier.config.ip == purifier.config.ip && command.type == commandType; 1045 | }); 1046 | if (exists) { 1047 | return; // Don't enqueue commands we already have in the queue 1048 | } 1049 | } 1050 | this.commandQueue.push({ 1051 | purifier: purifier, 1052 | type: commandType, 1053 | callback: callback, 1054 | data: data 1055 | }); 1056 | if (!this.queueRunning) { 1057 | this.queueRunning = true; 1058 | this.nextCommand(); 1059 | } 1060 | } 1061 | 1062 | nextCommand(): void { 1063 | const todoItem = this.commandQueue.shift(); 1064 | if (!todoItem) { 1065 | return; 1066 | } 1067 | 1068 | let command; 1069 | switch (todoItem.type) { 1070 | case CommandType.Polling: 1071 | command = this.updatePolling(todoItem.purifier); 1072 | break; 1073 | case CommandType.GetFirmware: 1074 | command = this.updateFirmware(todoItem.purifier); 1075 | break; 1076 | case CommandType.GetFilters: 1077 | command = this.updateFilters(todoItem.purifier); 1078 | break; 1079 | case CommandType.GetStatus: 1080 | command = this.updateStatus(todoItem.purifier); 1081 | break; 1082 | case CommandType.SetData: 1083 | command = this.setData(todoItem.purifier, todoItem.data, todoItem.callback); 1084 | } 1085 | 1086 | command.then(() => { 1087 | if (this.commandQueue.length > 0) { 1088 | this.nextCommand(); 1089 | } else { 1090 | this.queueRunning = false; 1091 | } 1092 | }); 1093 | } 1094 | } 1095 | 1096 | export = (api: API): void => { 1097 | hap = api.hap; 1098 | Accessory = api.platformAccessory; 1099 | 1100 | api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, PhilipsAirPlatform); 1101 | }; 1102 | --------------------------------------------------------------------------------