[
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/eslint-recommended\",\n    \"plugin:@typescript-eslint/recommended\" // uses the recommended rules from the @typescript-eslint/eslint-plugin\n  ],\n  \"parserOptions\": {\n    \"ecmaVersion\": 2018,\n    \"sourceType\": \"module\"\n  },\n  \"ignorePatterns\": [\n    \"dist\"\n  ],\n  \"rules\": {\n    \"quotes\": [\"warn\", \"single\"],\n    \"indent\": [\"warn\", 2, { \"SwitchCase\": 1 }],\n    \"semi\": [\"off\"],\n    \"comma-dangle\": [\"warn\", \"always-multiline\"],\n    \"dot-notation\": \"off\",\n    \"eqeqeq\": \"warn\",\n    \"curly\": [\"warn\", \"all\"],\n    \"brace-style\": [\"warn\"],\n    \"prefer-arrow-callback\": [\"warn\"],\n    \"max-len\": [\"warn\", 140],\n    \"no-console\": [\"warn\"], // use the provided Homebridge log method instead\n    \"no-non-null-assertion\": [\"off\"],\n    \"comma-spacing\": [\"error\"],\n    \"no-multi-spaces\": [\"warn\", { \"ignoreEOLComments\": true }],\n    \"no-trailing-spaces\": [\"warn\"],\n    \"lines-between-class-members\": [\"warn\", \"always\", {\"exceptAfterSingleLine\": true}],\n    \"@typescript-eslint/explicit-function-return-type\": \"off\",\n    \"@typescript-eslint/no-non-null-assertion\": \"off\",\n    \"@typescript-eslint/explicit-module-boundary-types\": \"off\",\n    \"@typescript-eslint/semi\": [\"warn\"],\n    \"@typescript-eslint/member-delimiter-style\": [\"warn\"]\n  }\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: ['0x5e']\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Create a report to help us improve\nlabels: ['bug']\nbody:\n- type: checkboxes\n  id: prerequisite\n  attributes:\n    label: Prerequisite\n    description: Have you read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section?\n    options:\n      - label: Yes, I've read the readme completely.\n        required: true\n- type: checkboxes\n  id: cache\n  attributes:\n    label: Cache\n    description: Have you tried clean homebridge accessory cache and restart the service?\n    options:\n      - label: Yes, I've cleaned accessory cache and the issue still exists.\n        required: true\n- type: input\n  id: version\n  attributes:\n    label: Version\n    description: The version of this plugin you are using.\n    placeholder: 1.7.0-beta.xx\n  validations:\n    required: true\n- type: textarea\n  id: devide-info\n  attributes:\n    label: Device Infomation JSON File\n    description: If it's related to a device, please paste `TuyaDeviceInfo.{uid}.json` content here.\n    render: json\n- type: dropdown\n  id: control-mode\n  attributes:\n    label: Device Control Mode\n    description: If it's related to a device, please select the device control mode.\n    options:\n      - Standard Instruction\n      - DP Instruction\n- type: textarea\n  id: logs\n  attributes:\n    label: Logs\n    description: Please paste homebridge logs with debug mode on.\n    render: shell\n- type: textarea\n  id: infos\n  attributes:\n    label: Other Infomations\n    description: Also tell us, what did you expect to happen.\n  validations:\n    required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Homebridge Discord Community\n    url: https://discord.gg/homebridge-432663330281226270\n    about: 'Ask your questions in the #tuya channel'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest an idea for this project\nlabels: ['enhancement']\nbody:\n  - type: textarea\n    id: devide-info\n    attributes:\n      label: Device Infomation JSON File\n      description: If it's related to a device, please paste `TuyaDeviceInfo.{uid}.json` content here.\n      render: json\n  - type: textarea\n    id: infos\n    attributes:\n      label: Detail Informations\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/login_issue.yml",
    "content": "name: Login issue\ndescription: Failed to login Tuya Cloud\nlabels: ['login issue']\nbody:\n  - type: checkboxes\n    id: prerequisite\n    attributes:\n      label: Prerequisite\n      description: Have you read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section?\n      options:\n        - label: 'Yes'\n          required: true\n  - type: checkboxes\n    id: accounts\n    attributes:\n      label: Accounts\n      description: Do you know Tuya IoT Platform and Tuya App are using different account?\n      options:\n        - label: 'Yes'\n          required: true\n  - type: input\n    id: country-code\n    attributes:\n      label: Country Code\n      description: The country code of your app account.\n      placeholder: ex. 1\n    validations:\n      required: true\n  - type: dropdown\n    id: region-code\n    attributes:\n      label: Region Code\n      description: The region code from app network diagnosis result.\n      options:\n        - AY (China)\n        - AZ (West US)\n        - EU (Central Europe)\n        - IN (India)\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      description: Please post homebridge logs. Logs with debug mode on will be better.\n      render: shell\n    validations:\n      required: true\n  - type: textarea\n    id: infos\n    attributes:\n      label: Other Infomations\n      description: Any information might relate to this issue.\n  "
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and Lint\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version: [18.x, 20.x, 22.x]\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Lint the project\n        run: npm run lint\n\n      - name: Build the project\n        run: npm run build\n        env:\n          CI: true\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore compiled code\ndist\n\n# ------------- Defaults ------------- #\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# Hide vscode stuff\n.vscode/launch.json\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.pnp.*"
  },
  {
    "path": ".npmignore",
    "content": "# Ignore source code\nsrc\n\n# ------------- Defaults ------------- #\n\n# gitHub actions\n.github\n\n# eslint\n.eslintrc\n\n# typescript\ntsconfig.json\n\n# vscode\n.vscode\n\n# nodemon\nnodemon.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.pnp.*\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\"\n  ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"files.eol\": \"\\n\",\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\"\n  },\n  \"editor.rulers\": [ 140 ],\n  \"eslint.enable\": true\n}"
  },
  {
    "path": "ADVANCED_OPTIONS.md",
    "content": "# Advanced Options\n\n**During the beta version, the options are unstable, may get changed during updates.**\n\nThe main function of `deviceOverrides` is to convert \"non-standard schema\" to \"standard schema\", making the device compatible with this plugin.\n\nBefore configuring, you may need to:\n- Have basic programming skills in JavaScript (Only used in `onGet`/`onSet` handlers).\n- Understand the concept of device schema (also known as Data Type): [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k)\n- Read the documentation of your device product in [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md).\n- Obtain device info json from `/path/to/persist/TuyaDeviceList.xxx.json` (the full path can be found from logs).\n- Locate any \"incorrect schema\" in your device info json, and convert it to the \"correct schema\".\n\n\n### Configuration\n\n`options.deviceOverrides` is an **optional** array of device overriding config objects, which is used for converting \"non-standard schema\" to \"standard schema\", making the device compatible with this plugin. The structure of each element in the array is described as follows:\n\n- `id` - **required**: Device ID, Product ID, Scene ID, or `global`.\n- `category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide the device, product, or scene. **⚠️Overriding this property may lead to unexpected behaviors and exceptions, so please remove the accessory cache after making changes.**\n- `unbridged` - **optional**: Unbridge accessories. Defaults to `false`.\n- `adaptiveLighting` - **optional**: Adaptive Lighting. Defaults to `false`. Not all light device support this feature, please use it on demand.\n- `schema` - **optional**: An array of schema overriding config objects, used for describing datapoint (DP). When your device has non-standard DP, you need to transform them manually with configuration. Each element in the schema array is described as follows:\n  - `code` - **required**: DP code.\n  - `newCode` - **optional**: New DP code.\n  - `type` - **optional**: New DP type. One of `Boolean`, `Integer`, `Enum`, `String`, `Json`, or `Raw`.\n  - `property` - **optional**: New DP property object. For `Integer` type, the object should contain `min`, `max`, `scale`, and `step`. For `Enum` type, the object should contain `range`. For more information, see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts).\n  - `onGet` - **optional**: A one-line JavaScript code to convert the old value to the new value. The function is called with two arguments: `device` and `value`.\n  - `onSet` - **optional**: A one-line JavaScript code to convert the new value to the old value. The function is called with two arguments: `device` and `value`. Returning `undefined` means to skip sending the command.\n  - `hidden` - **optional**: Hide the schema. Defaults to `false`.\n\n\n## Examples\n\n### Change category code\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"category\": \"xxx\"\n    }]\n  }\n}\n```\n\n### Hide device / scene\n\nJust the same way as changing category code.\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id_or_scene_id}\",\n      \"category\": \"hidden\"\n    }]\n  }\n}\n```\n\n### Hide DP\n\nAn example of hide camera's floodlight (`floodlight_switch`):\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"floodlight_switch\",\n        \"hidden\": true\n      }]\n    }]\n  }\n}\n```\n\n### Enable Adaptive Lighting\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"adaptiveLighting\": true\n    }]\n  }\n}\n```\n\n\n### Offline as off\n\nIf you want to display off status when device is offline:\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"{dp_code}\",\n        \"onGet\": \"(device.online && value)\"\n      }]\n    }]\n  }\n}\n```\n\n\n### Change DP code\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n          \"code\": \"{old_dp_code}\",\n          \"newCode\": \"{new_dp_code}\"\n      }]\n    }]\n  }\n}\n```\n\n\n### Convert from enum DP to boolean DP\n\nAn example of convert `open`/`close` into `true`/`false`:\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"{dp_code}\",\n        \"type\": \"Boolean\",\n        \"onGet\": \"(value === 'open') ? true : false;\",\n        \"onSet\": \"(value === true) ? 'open' : 'close';\"\n      }]\n    }]\n  }\n}\n```\n\n\n### Adjust integer DP ranges\n\nSome odd thermostat stores double of the real value to keep the decimal part (0.5°C).\n\nWe need override both range and value in order to make it working. (Only override value is not enough, range is required too.)\n\nHere's an example of the invalid schema:\n```js\n{\n  code: 'temp_set',\n  mode: 'rw',\n  type: 'Integer',\n  property: { unit: '℃', min: 10, max: 70, scale: 1, step: 5 }\n}\n```\n\nThe value `41` actually represents for `20.5°C`, the range `10~70` actually represents for `5.0°C~35.0°C`.\n\nTo fix this, first we need set scale to `1`, and convert `41` to `205` when getting, convert `205` to `41` when getting, which means `value x 5` when getting, and `value / 5` when setting.\n\nHere's the example config:\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"temp_set\",\n        \"onGet\": \"(value * 5);\",\n        \"onSet\": \"(value / 5);\",\n        \"property\": {\n          \"min\": 50,\n          \"max\": 350,\n          \"scale\": 1,\n          \"step\": 5\n        }\n      }]\n    }]\n  }\n}\n```\n\nAfter transform value using `onGet` and `onSet`, and new range in `property`, it should be working now.\n\n\n### Reverse curtain motor's on/off state\n\nMost curtain motor have \"reverse mode\" setting in the Tuya App, if you don't have this, you can reverse `percent_control`/`position` and `percent_state` in the plugin config:\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"percent_control\",\n        \"onGet\": \"(100 - value)\",\n        \"onSet\": \"(100 - value)\"\n      }, {\n        \"code\": \"percent_state\",\n        \"onGet\": \"(100 - value)\",\n        \"onSet\": \"(100 - value)\"\n      }]\n    }]\n  }\n}\n```\n\n\n### Skip send on/off command when touching brightness/speed slider\n\nSome products (dimmer, fan) having issue when sending brightness/speed command with on/off command together. Here's an example of skip on/off command.\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"switch_led\",\n        \"onSet\": \"(value === device.status.find(status => status.code === 'switch_led').value) ? undefined : value\"\n      }]\n    }]\n  }\n}\n```\n\n\n### Convert Fahrenheit to Celsius\n\nF = 1.8 * C + 32\n\nC = (F - 32) / 1.8\n\n```js\n{\n  \"options\": {\n    // ...\n    \"deviceOverrides\": [{\n      \"id\": \"{device_id}\",\n      \"schema\": [{\n        \"code\": \"temp_current\",\n        \"onGet\": \"Math.round((value - 32) / 1.8);\",\n        \"onSet\": \"Math.round(1.8 * value + 32);\"\n      }]\n    }]\n  }\n}\n```\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [1.7.0] - (unreleased)\n\n### Added\n- Add scene support. (#118)\n- Add Wireless Switch support (`wxkg`).\n- Add Solar Light support (`tyndj`).\n- Add Dehumidifier support (`cs`).\n- Add Scene Switch support (`wxkg`).\n- Add device overriding config support. \"Non-standard DP\" devices have possibility to be supported now. (#119)\n- Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution\n- Add Air Conditioner support (`kt`). (#160)\n- Add Air Conditioner Controller support (`ktkzq`). (#160)\n- Add Diffuser support (`xxj`). (#175)\n- Add Temperature Control Socket support (`wkcz`).\n- Add Environmental Detector support (`hjjcy`).\n- Add Water Valve Controller support (`sfkzq`).\n- Add IR Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). (#191)\n- Add IR AC Controller support (`hwktwkq`).\n- Add Fingerbot support (`szjqr`).\n- Add Smart Lock support (`ms`, `jtmspro`). (#120) Thanks @pfgimutao for the contribution\n- Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution\n- Add Vibration Sensor support (`zd`). (#262)\n- Add adaptive lighting support. (#272)\n- Add Wireless Doorbell support (`wxml`). (#277)\n- Add IR Remote Control support (`wsdykq`).\n- Add Layout to display schema in sections. (#283) Thanks @donavanbecker for the contribution\n- Add option to make accessory and unbridged accessory (#285) Thanks @donavanbecker for the contribution\n- Add inching button for switches.\n- Add support to 2ch windows covering. (#339) Thanks @CryptoIR for the contribution\n- Add retry when network error happened.\n- Add Pet Feeder support (`cwwsq`). (#483) Thanks @aselekoglu for the contribution\n\n\n### Fixed\n- Fix `RotationSpeed` missing one level. (#170)\n- Fix `bright_value` not sent for the `C/CW` lights who doesn't have `work_mode`. (#171)\n- Fix crash when camera sends an invalid status message.\n- Fix incorrect Door and Window Controller state. (#178)\n- Fix Thermostat cold mode not working (#242).\n- Order temp before get the min and max for IRAirConditionerAccessory. (#433) Thanks @tuliocll for the contribution\n- Fix energy usage not updated after homebridge restart. (#268)\n\n\n### Changed\n- Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131)\n- Adjust humidity range of dehumidifier and humidifier.\n- Print scene id in logs.\n- Update support for RGB Power Switch (`dj`).\n- Support showing device online status via `StatusActive`. (#172)\n- Update unit and range of `RotationSpeed`, need clean accessory cache to take effect. (#174, #273)\n- Support Diffuser RGB light. (#184)\n- Support Fan light temperature and color. (#184)\n- Support Humidifier light. (#184)\n- Expose energy usage for outlets/switches. (#190) Thanks @lstrojny for the contribution\n- Strict config validate for `deviceOverrides`. (#278)\n- Support AirPurifier air quality.\n- Throw `HapStatusError` when device is offline.\n\n\n## [1.6.0] - (2022.12.3)\n\nThis version has been completely rewritten in TypeScript, brings a lot of bug fix and new device support.\n\n### New Accessories\n- Add CO Detector support (`cobj`).\n- Add CO2 Detector support (`co2bj`).\n- Add Water Detector support (`sj`).\n- Add Temperature and Humidity Sensor support (`wsdcg`, `wnykq`). Thanks @bimusiek for the contribution\n- Add Light Sensor support (`ldcg`).\n- Add Motion Sensor support (`pir`).\n- Add PM2.5 Detector support (`pm25`).\n- Add Door and Window Controller support (`mc`).\n- Add Curtain Switch support (`clkg`). (#8)\n- Add Human Presence Sensor support (`hps`). (#17)\n- Add Thermostat support (`wk`). (#19) Thanks @burcadoruciprian for the contribution\n- Add Spotlight support (`sxd`). (#21)\n- Add Irrigator support (`ggq`). (#28)\n- Add Scene Light Socket support (`qjdcz`). (#33)\n- Add Ceiling Fan Light support (`fsd`). (#37)\n- Add Thermostat Valve support (`wkf`). (#50)\n- Add Motion Sensor Light support (`gyd`). (#65)\n- Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). (#82)\n- Add Humidifier support (`jsq`). (#89) Thanks @akaminsky-net for the contribution\n\n\n### Added\n- Add config validation during plugin initialization.\n- Add instruction message for handling API errors.\n- Add debounce in `BaseAccessory.sendCommands()` for better API request peformance.\n- Persist `TuyaDeviceList.{uid}.json` for debugging. (#41)\n- Add `homeWhitelist` option for whitelisting homes. (#84) Thanks @JulianLepinski for the contribution\n\n\n### Fixed\n- Fix 1004 signature error when url query has more than 2 elements.\n- Fix 1010 token expired error when refresh access_token.\n- Fix 1106 permission error when polling device info list.\n- Fix 1100, 2017 errors when login. (via config validation)\n- Fix Lightbulb `RGBW` and `RGBCW` work mode not switched properly (#12 #56 #59)\n- Fix Lightbulb color temperature not working. (#13)\n- Fix Thermostat temperature units handling. (#20)\n- Fix Thermostat mode handling. (#26)\n- Fix Curtain Switch with no position feature. (#27)\n- Fallback when receiving MQTT message with wrong order. (#35)\n- Fix wrong temperature on sensor. (#38)\n- Fix fan speed issue. (#46 #51)\n- Workaround for Thermostat with wrong schema property (#74)\n- Fix Contact Sensor not working (#75)\n- Fix iOS 16 default accessory name issue. (#85)\n\n\n### Changed\n- Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc.\n- Reimplement accessory logics. More friendly for accessory developers.\n- Update device info list polling logic. Less API errors.\n- Now `Manufactor`, `Serial Number` and `Model` will be correctly displayed in HomeKit.\n- All devices will be shown in HomeKit by default (Including unsupported device).\n- Updated unit test.\n- Updated documentations. Thanks @prabch for the contribution\n\n\n### Removed\n- Remove `debug` option. Silence logs for users. For debugging, please refer to [troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting).\n- Remove `lang` option.\n- Remove `username` and `password` options for `Custom` project. User will be created and authorized automatically. (#11)\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014-2021 Tuya Inc.\nPermission is hereby granted, free of charge, to any person obtaining a copy \nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is \nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in \nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR \nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL \nTHE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER \nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING \nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER \nDEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# @0x5e/homebridge-tuya-platform\n\n[![npm](https://badgen.net/npm/v/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform)\n[![npm](https://badgen.net/npm/dt/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform)\n[![mit-license](https://badgen.net/npm/license/@0x5e/homebridge-tuya-platform)](https://github.com/0x5e/homebridge-tuya-platform/blob/main/LICENSE)\n[![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)\n[![Build and Lint](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml/badge.svg)](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml)\n[![join-discord](https://badgen.net/badge/icon/discord?icon=discord&label=homebridge/tuya)](https://discord.gg/homebridge-432663330281226270)\n\n\nFork version of the official Tuya Homebridge plugin, with a focus on fixing bugs and adding new device support.\n\n\n\n⚠️**Update on 2024.1.14:** Thanks for the attention on this project. There's more and more \"problem device\", which has wrong definition by manufacture (reversed 0%-100% state, wrong range, wrong unit, ...). Support them one by one really cost a lot. I'm not going to support them in the future, please try solve them by yourself. PRs are still welcome, and bugs will be focused. Thanks again :)\n\n\n\n\n## Features\n\n- Optimized and improved code for better readability and maintainability.\n- Enhanced stability.\n- Reduced duplicate code.\n- Fewer API errors.\n- Lower development costs for new accessory categories.\n- Supports Tuya Scenes (Tap-to-Run).\n- Includes the ability to override device configurations, which enables support for \"non-standard\" DPs.\n- Supports over 60+ device categories, including most light, switch, sensor, camera, lock, IR remote control, etc.\n\n\n## Supported Tuya Devices\nSee [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md)\n\n\n## Changelogs\nSee [CHANGELOG.md](./CHANGELOG.md)\n\n\n## Installation\nBefore using this plugin, please make sure to uninstall `homebridge-tuya-platform` first as these two plugins cannot run simultaneously. However, the configuration files are compatible, so there's no need to delete them.\n\n#### For Homebridge Web UI Users\nGo to plugin page, search for `@0x5e/homebridge-tuya-platform` and install it.\n\n\n#### For Homebridge Command Line Users\n\nRun the following command in the terminal:\n```\nnpm install @0x5e/homebridge-tuya-platform\n```\n\n\n## Configuration\n\nThere are two types of projects: `Custom` and `Smart Home`.\nThe difference between them is:\n- The `Custom` project pulls devices from the project's assets.\n- The `Smart Home` project pulls devices from the user's home in the Tuya app.\n\nIf you are a personal user and are unsure which one to choose, please use the `Smart Home` project.\n\nBefore you can configure, you must go to the [Tuya IoT Platform](https://iot.tuya.com):\n- Create a cloud development project, and select the data center where your app account is located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb)\n- Go to the `Project Page` > `Devices Panel` > `Link Tuya App Account`, and link your app account.\n- Go to the `Project Page` > `Service API` > `Go to Authorize`, and subscribe to the following APIs (it is free for trial):\n    - Authorization Token Management\n    - Device Status Notification\n    - IoT Core\n    - IoT Video Live Stream (for cameras)\n    - Industry Project Client Service (for the `Custom` project)\n    - IR Control Hub Open Service (for IR devices)\n    - Smart Home Scene Linkage (for scenes)\n    - Smart Lock Open Service (for Lock devices)\n- **⚠️Remember to extend the API trial period every 6 months here [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1) (the first-time subscription only gives you 1 month).**\n\n#### For \"Custom\" Project\n\n- `platform` - **required** : Must be 'TuyaPlatform'.\n- `options.projectType` - **required** : Must be '1'\n- `options.endpoint` - **required** : The endpoint URL taken from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table.\n- `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud).\n- `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud).\n- `options.debug` - **optional**: Includes debugging output in the Homebridge log. (Default: `false`)\n- `options.debugLevel` - **optional**: An optional list of strings seperated with comma `,`. `api` represents for HTTP API log, `mqtt` represents for MQTT log, and device ID represents for device log. If blank, all logs are outputed.\n\n#### For \"Smart Home\" Project\n\n- `platform` - **required** : Must be 'TuyaPlatform'.\n- `options.projectType` - **required** : Must be '2'.\n- `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud).\n- `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud).\n- `options.countryCode` - **required** : The numeric country code of your developer account's region. You can find the country codes list [here](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb).\n- `options.username` - **required** : The mobile app (Tuya or SmartLife) account's username. Don't use the Tuya IoT Platform developer username.\n- `options.password` - **required** : The mobile app (Tuya or SmartLife) account's password. MD5 salted password is also available for increased security. Don't use the Tuya IoT Platform developer password.\n- `options.appSchema` - **required** : The app schema: 'tuyaSmart' for the Tuya Smart App, or 'smartlife' for the Smart Life App.\n- `options.endpoint` - **optional** : The endpoint URL can be inferred from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table based on the country code provided. Only manually set this value if you encounter login issues and need to specify the endpoint for your account location.\n- `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the Homebridge log.\n- `options.debug` - **optional**: Includes debugging output in the Homebridge log. (Default: `false`)\n- `options.debugLevel` - **optional**: An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed.\n\n\n#### Advanced options\nSee [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md)\n\n\n## Limitations\n- **⚠️Don't forget to extend the API trial period every 6 months. Maybe you can set up a reminder in calendar.**\n- Using the same app account for multiple Homebridge/HomeAssistant instances is not supported. Please use separate app accounts for each instance.\n- The plugin requires an internet connection to the Tuya Cloud and does not support the LAN protocol. See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90) for more information.\n\n## FAQ\n\n#### About Login issue\n\nFor most users, you can easily find your app account's data center through the [documentation](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) and login without any issues. However, for some users, they may encounter error codes such as 1106 or 2406. If you encounter such errors, it's possible that there are differences between your data center and the documentation.\n\nTo determine the data center, follow these steps:\n\n1. Open the app and navigate to \"Me > Settings > Network Diagnosis\".\n2. Start the diagnosis and select \"Upload Log > Copy the Log to Clipboard\".\n3. Paste the log anywhere and find the line beginning with \"Region code:\".\n4. Look for the following codes: \"AY\" for China, \"AZ\" for the West US, \"EU\" for Central Europe, and \"IN\" for India.\n\nThen manually specify endpoint in the plugin config.\n\n\n#### What is \"Standard DP\" and \"Non-standard DP\"?\n\n<!-- If your device is working properly, you don't need to know this. -->\n\n\"Standard DP\" refers to device properties or functionalities that are specified in the Tuya IoT Development Platform documentation at [Tuya IoT Development Platform Documentation > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq).\n\nFor example, a light bulb should have a standard DP code of `switch_led` for power on/off, and optional codes `bright_value`/`bright_value_v2` for brightness, `temp_value`/`temp_value_v2` for color temperature, and `work_mode` for changing the working mode. These codes can be found in the above documentation.\n\nIf your light bulb can be adjusted in the Tuya app but not with the plugin, it most likely has \"Non-standard DP.\"\n\n\n#### Can \"Non-standard DP\" be supportd by this plugin?\n\nYes. The device must be listed in the support list and the following steps must be completed before it will work:\n1. Change the device's control mode on the Tuya Platform:\n  - Go to \"[Tuya Platform Cloud Development](https://iot.tuya.com/cloud/) > Your Project > Devices > All Devices > View Devices by Product\".\n  - Find the product related to your device, click the \"pencil\" icon (Change Control Instruction Mode).\n  - <img width=\"500\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5144674/202967707-8b934e05-36d6-4b42-bb7b-87e5b24474c4.png\">\n  - In the \"Table of Instructions\", you can see the cloud mapping and determine which DP codes are missing and need to be manually mapped later.\n  - <img width=\"500\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5144674/202967528-4838f9a1-0547-4102-afbb-180dc9b198b1.png\">\n  - Select \"DP Instruction\" and save.\n2. Override the device schema, see [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md).\n\n\n#### Local support\nSee [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90).\n\nAlthough the plugin didn't implemented tuya local protocol now, it still remains possibility in the future.\n\n\n## Troubleshooting\n\nIf your device is not supported, please follow these steps to collect information.\n\n#### 1. Get Device Information\n\nAfter Homebridge has been successfully launched, the device information list will be saved in Homebridge's persist path. You can find the file path in the Homebridge log:\n```\n[2022/11/3 18:37:43] [TuyaPlatform] Device list saved at /path/to/TuyaDeviceList.{uid}.json\n```\n\n**⚠️Please make sure to remove sensitive information such as `ip`, `lon`, `lat`, `local_key`, and `uid` before submitting the file.**\n\n\n#### 2. Enable Debug Mode\n\nAdd debug option in the plugin config, then restart Homebridge.\n\n#### 3. Collect Logs\n\nWith debug mode enabled, you can now receive MQTT logs. Operate your device, either physically or through the Tuya App, to receive MQTT logs like this:\n\n```\n[2022/12/8 12:51:59] [TuyaPlatform] [TuyaOpenMQ] onMessage:\ntopic = cloud/token/in/xxx\nprotocol = 4\nmessage = {\n  \"dataId\": \"xxx\",\n  \"devId\": \"xxx\",\n  \"productKey\": \"xxx\",\n  \"status\": [\n    {\n      \"1\": \"double_click\",\n      \"code\": \"switch1_value\",\n      \"t\": \"1670475119766\",\n      \"value\": \"double_click\"\n    }\n  ]\n}\n```\n\nIf you are unable to receive any MQTT logs while controlling the device, it likely means that your device has \"Non-standard DP\".\n\nBy submitting the device information JSON and MQTT logs, you can help us support new device categories.\n\n\n## Contributing\n\nPlease see https://github.com/homebridge/homebridge-plugin-template#setup-development-environment for setup development environment.\n\nPRs and issues are welcome.\n\n# \nThank you for spend time using the project. If it helps you, don't hesitate to give it a star 🌟:-)\n"
  },
  {
    "path": "SUPPORTED_DEVICES.md",
    "content": "# Supported Tuya Devices\n\nFirst-class category name, sedond-class category name, category code can be found here:\nhttps://developer.tuya.com/docs/iot/standarddescription?id=K9i5ql6waswzq\n\nMost category code is pinyin abbreviation of Chinese name.\n\n## Lighting\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Light | 光源 | dj<br> dsd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorydj?id=Kaiuyzy3eheyy) |\n| Ceiling Light | 吸顶灯 | xdd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r) |\n| Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g) |\n| String Lights | 灯串 | dc | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dc?id=Kaof7taxmvadu) |\n| Strip Lights | 灯带 | dd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dd?id=Kaof804aibg2l) |\n| Motion Sensor Light | 感应灯 | gyd | Lightbulb<br> MotionSensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy) |\n| Ceiling Fan Light | 风扇灯 | fsd | Lightbulb<br> Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v) |\n| Solar Light | 太阳能灯 | tyndj | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98) |\n| Dimmer | 调光器 | tgq | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4) |\n| Remote Control | 遥控器 | ykq | | | [Documentation](https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov) |\n| Spotlight | 射灯 | sxd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/sxd?id=Kb7jayalltstu) |\n| White Noise Light | 白噪音灯 | bzyd | Lightbulb<br> Switch | ✅ | Documentation |\n\n\n## Electrical Products\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Switch | 开关 | kg<br> tdq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y) |\n| Socket | 插座 | cz | Outlet | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y) |\n| Power Strip | 排插 | pc | Outlet | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y) |\n| Scene Switch | 场景开关 | cjkg | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycjkg?id=Kaiuz0bcukqc5) |\n| Card Switch | 插卡取电开关 | ckqdkg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryckqdkg?id=Kaiuz0e3wjryy) |\n| Curtain Switch | 窗帘开关 | clkg | Window Covering | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39) |\n| Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee) |\n| Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o) |\n| Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfskg?id=Kbcs129cl1gr9) |\n| Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/wxkg?id=Kbeo9t3ryuqm5) |\n| Scene Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | Documentation |\n| Temperature Control Socket | 温控插座 | wkcz | Switch<br> Temperature Sensor<br> Humidity Sensor | ✅ | Documentation |\n\n\n## Large Home Appliances\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Heater | 热水器 | rs | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryrs?id=Kaiuz0nfferyx) |\n| Ventilation System | 新风机 | xfj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxfj?id=Kaiuz0pphkowg) |\n| Refrigerator | 冰箱 | bx | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorybx?id=Kaiuz0s58ia6h) |\n| Bathtub | 浴缸 | yg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyg?id=Kaiuz0uoisp47) |\n| Washing Machine | 洗衣机 | xy | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxy?id=Kaiuz0wxh08jf) |\n| Air Conditioner | 空调 | kt | Heater Cooler<br> Humidifier Dehumidifier<br> Fanv2<br> Temperature Sensor<br> Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n) |\n| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler<br> Humidifier Dehumidifier<br> Fanv2<br> Temperature Sensor<br> Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryktkzq?id=Kaiuz11eqy892) |\n| Boiler | 壁挂炉 | bgl | | | [Documentation](https://developer.tuya.com/en/docs/iot/boilerbgl?id=Kaiuz13shgrhp) |\n| Sauna | 华氏度摄氏度两用(30-90) | qtwk | Lightbulb<br>Thermostat | ✅ | Documentation |\n\n\n## Small Home Appliances\n\n| Name                       | Name (zh) | Code          | Homebridge Service | Supported | Links                                                                                    |\n|----------------------------| ---- |---------------| ---- | ---- |------------------------------------------------------------------------------------------|\n| Robot Vacuum               | 扫地机 | sd            | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysd?id=Kaiuz16b2s6yd)      |\n| Heater                     | 取暖器 | qn            | Heater Cooler | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm)      |\n| Air Purifier               | 空气净化器 | kj            | Air Purifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7)      |\n| Drying Rack                | 晾衣架 | lyj           | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorylyj?id=Kaiuz1cy926vh)     |\n| Diffuser                   | 香薰机 | xxj           | Air Purifier<br> Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl)     |\n| Extraction hood            | 香薰机 | yyj           | Air Purifier<br> Lightbulb | ✅ | Documentation        |\n| Curtain                    | 窗帘 | cl            | Window Covering | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df)      |\n| Door and Window Controller | 门窗控制器 | mc            | Window | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymc?id=Kaiuz1jyoassg)      |\n| Thermostat                 | 温控器 | wk            | Thermostat | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywk?id=Kaiuz1m1xqnt6)      |\n| Thermostat Valve           | 温控阀 | wkf           | Thermostat | ✅ | Documentation                                                                            |\n| Bathroom Heater            | 浴霸 | yb            | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyb?id=Kaiuz1oajgpib)      |\n| Irrigator                  | 灌溉器 | ggq<br> sfkzq | Valve | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k)     |\n| Humidifier                 | 加湿器 | jsq           | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b)     |\n| Dehumidifier               | 除湿机 | cs            | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha)      |\n| Fan                        | 风扇 | fs            | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c)      |\n| Water Purifier             | 净水器 | js            | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjs?id=Kaiuz204l58n9)      |\n| Electric Blanket           | 电热毯 | dr            | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p)      |\n| Pet Treat Feeder           | 宠物弹射喂食器 | cwtswsq       | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwtswsq?id=Kaiuz24lq3fq5) |\n| Pet Ball Thrower           | 宠物网球发射器 | cwwqfsq       | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwqfsq?id=Kaiuz26r7g1up) |\n| HVAC                       | 暖通器 | ntq           | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryntq?id=Kaiuz292sjqcz)     |\n| Pet Feeder                 | 宠物喂食器 | cwwsq         | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld)   |\n| Pet Fountain               | 宠物饮水机 | cwysj         | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwysj?id=Kaiuz2dfro0nd)   |\n| Sofa                       | 沙发 | sf            | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysf?id=Kaiuz2fp9uqtt)      |\n| Electric Fireplace         | 电壁炉 | dbl           | | | [Documentation](https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6) |\n| Smart Milk Kettle          | 智能调奶器 | tnq           | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorytnq?id=Kakf01agbfkfa)     |\n| Cat Toilet                 | 猫砂盆 | msp           | Switch, Lightbulb, OccupancySensor, FilterMaintenance | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymsp?id=Kakg2t7714ky7)     |\n| Towel Rack                 | 毛巾架 | mjj           | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymjj?id=Kakkmlm9k4cir)     |\n| Smart Indoor Garden        | 植物生长机 | sz            | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0)      |\n\n\n## Kitchen Appliances\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Smart Kettle | 电茶壶 | bh | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorybh?id=Kaiuz2kly679h) |\n| Bread Maker | 面包机 | mb | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymb?id=Kaiuz2mrs0b2m) |\n| Coffee Maker | 咖啡机 | kfj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f) |\n| Bottle Warmer | 暖奶器 | nnq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorynnq?id=Kaiuz2riz1s8d) |\n| Milk Dispenser | 冲奶机 | cn | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycn?id=Kaiuz2tosvw2a) |\n| Sous Vide Cooker | 慢煮机 | mzj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux) |\n| Rice Cabinet | 米柜 | mg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymg?id=Kaiuz2yb04ocu) |\n| Induction Cooker | 电磁炉 | dcl | | | [Documentation](https://developer.tuya.com/en/docs/iot/induction-cooker?id=Kaiuz30l7adxo) |\n| Air Fryer | 空气炸锅 | kqzg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorykqzg?id=Kakda4kug3k1j) |\n| Bento Box | 智能饭盒 | znfh | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryznfh?id=Kako8jffneds3) |\n\n\n## Security & Video Surveillance\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Alarm Host | 报警主机 | mal | Security System | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf) |\n| Smart Camera | 智能摄像机 | sp | Motion Sensor<br> Doorbell | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12) |\n| Wireless Doorbell | 无线门铃 | wxml | StatelessProgrammableSwitch | ✅ | Documentation |\n| Siren Alarm | 声光报警传感器 | sgbj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu) |\n| Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw) |\n| Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952) |\n| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor<br> Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34) |\n| Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymcs?id=Kaiuz3bnflmh2) |\n| Vibration Sensor | 震动传感器 | zd | Motion Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno) |\n| Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli) |\n| Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8) |\n| Pressure Sensor | 压力传感器 | ylcg<br> ylcgq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm) |\n| Emergency Button | 紧急按钮 | sos | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy) |\n| PM2.5 Detector | PM2.5传感器 | pm25<br> pm2.5<br> pm25cgq | Air Quality Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu) |\n| CO Detector | CO报警传感器 | cobj<br> cocgq | Carbon Monoxide Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v) |\n| CO2 Detector | CO2报警传感器 | co2bj<br> co2cgq | Carbon Dioxide Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy) |\n| Multi-functional Sensor | 多功能传感器 | dgnbj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3) |\n| Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm) |\n| Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80) |\n| Human Presence Sensor | 人体存在传感器 | hps | Occupancy Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs) |\n| Smart Lock | 智能门锁 | ms<br> jtmspro | LockMechanism | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ms?id=Kb0o2s20fn9sy) |\n| Environmental Detector | 环境检测仪 | hjjcy | Air Quality Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/hjjcy?id=Kbeoad8y1nnlv) |\n\n\n## Exercise & Health\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Massage Chair | 按摩椅 | amy | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryamy?id=Kaiuz4bmwxufp) |\n| Physiotherapy Products| 理疗产品 | liliao | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryliliao?id=Kakobe16fjw3l) |\n| Smart Jump Rope | 跳绳 | ts | | | [Documentation](https://developer.tuya.com/en/docs/iot/ts?id=Kat27rqhu47br) |\n| Body Fat Scale | 体脂秤 | tzc1 | | | [Documentation](https://developer.tuya.com/en/docs/iot/tzc1?id=Kat27zmbbs56t) |\n| Smart Watch/Fitness Tracker | 手表/手环 | sb | | | [Documentation](https://developer.tuya.com/en/docs/iot/sb?id=Kat28k7efsbi9) |\n| Smart Pill Box | 智能药盒 | znyh | | | [Documentation](https://developer.tuya.com/en/docs/iot/znyh?id=Kb2yxpjfcojdt) |\n\n\n## Gateway Control\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Multifunctional Gateway | 多功能网关 | wg | | | [Documentation](https://developer.tuya.com/en/docs/iot/wg2?id=Kau22nplrptfe) |\n\n\n## Energy\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Smart Electricity Meter | 智能电表 | zndb | | | [Documentation](https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7) |\n| Smart Water Meter | 智能水表 | znsb | | | [Documentation](https://developer.tuya.com/en/docs/iot/smart-water-meter?id=Kaiuz4jf0jy9f) |\n| Circuit Breaker | 断路器 | dlq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dlq?id=Kb0kidk9enyh8) |\n\n\n## Digital Entertainment\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| TV | 电视 | ds | | | [Documentation](https://developer.tuya.com/en/docs/iot/ds?id=Kat8px3b6tb9o) |\n| Projector | 投影仪 | tyy | | | [Documentation](https://developer.tuya.com/en/docs/iot/tyy?id=Kat8qpj75z0vv) |\n\n\n## Outdoor Travel\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Tracker | 定位器 | tracker | | | [Documentation](https://developer.tuya.com/en/docs/iot/tracker?id=Kajk21wwy2mhi) |\n\n\n## IR Remote Control\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Universal Remote Control | 万能遥控器 | wnykq<br> hwktwkq<br> wsdykq | Temperature Sensor<br> Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) |\n| TV | 电视 | infrared_tv | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| STB | 机顶盒 | infrared_stb | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| TV Box | 电视盒子 | infrared_box | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Air Conditioner | 空调 | infrared_ac | Heater Cooler<br> Humidifier Dehumidifier<br> Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-air-conditioner-apis?id=Kb3oe9ehg02fn) |\n| Fan | 电风扇 | infrared_fan | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Light | 灯 | infrared_light | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Amplifier | 音响 | infrared_amplifier | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Projector | 投影仪 | infrared_projector | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| DVD | DVD | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Camera | 相机 | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Water Heater | 热水器 | infrared_waterheater | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| Air Purifier | 净化器 | infrared_airpurifier | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) |\n| DIY | - | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-learning-apis?id=Kb3oeap4nqqm3) |\n\n\n## Others\n\n| Name | Name (zh) | Code | Homebridge Service | Supported | Links |\n| ---- | ---- | ---- | ---- | ---- | ---- |\n| Fingerbot | 手指机器人 | szjqr | Switch | ✅ | Documentation |\n\n\nFor the undocumented product category, you can try override it to the most similar one. See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md).\n"
  },
  {
    "path": "config.schema.json",
    "content": "{\n    \"pluginAlias\": \"TuyaPlatform\",\n    \"pluginType\": \"platform\",\n    \"singular\": true,\n    \"headerDisplay\": \"\",\n    \"footerDisplay\": \"\",\n    \"customUi\": false,\n    \"schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\n                \"type\": \"string\",\n                \"title\": \"Name\",\n                \"required\": true,\n                \"default\": \"Tuya\"\n            },\n            \"options\": {\n                \"title\": \"Project Info\",\n                \"type\": \"object\",\n                \"required\": true,\n                \"properties\": {\n                    \"projectType\": {\n                        \"title\": \"Project Type (Development Method)\",\n                        \"type\": \"string\",\n                        \"default\": \"2\",\n                        \"oneOf\": [\n                            {\n                                \"title\": \"Custom\",\n                                \"enum\": [\n                                    \"1\"\n                                ]\n                            },\n                            {\n                                \"title\": \"Smart Home\",\n                                \"enum\": [\n                                    \"2\"\n                                ]\n                            }\n                        ],\n                        \"required\": true\n                    },\n                    \"endpoint\": {\n                        \"title\": \"Endpoint URL\",\n                        \"type\": \"string\",\n                        \"format\": \"url\"\n                    },\n                    \"accessId\": {\n                        \"title\": \"Access ID\",\n                        \"type\": \"string\",\n                        \"required\": true\n                    },\n                    \"accessKey\": {\n                        \"title\": \"Access Secret\",\n                        \"type\": \"string\",\n                        \"required\": true\n                    },\n                    \"countryCode\": {\n                        \"title\": \"Country Code\",\n                        \"type\": \"integer\",\n                        \"minimum\": 1,\n                        \"condition\": {\n                            \"functionBody\": \"return model.options.projectType === '2';\"\n                        }\n                    },\n                    \"username\": {\n                        \"title\": \"Username\",\n                        \"type\": \"string\",\n                        \"condition\": {\n                            \"functionBody\": \"return model.options.projectType === '2';\"\n                        }\n                    },\n                    \"password\": {\n                        \"title\": \"Password\",\n                        \"type\": \"string\",\n                        \"condition\": {\n                            \"functionBody\": \"return model.options.projectType === '2';\"\n                        }\n                    },\n                    \"appSchema\": {\n                        \"title\": \"App\",\n                        \"type\": \"string\",\n                        \"default\": \"tuyaSmart\",\n                        \"oneOf\": [\n                            {\n                                \"title\": \"Tuya Smart\",\n                                \"enum\": [\n                                    \"tuyaSmart\"\n                                ]\n                            },\n                            {\n                                \"title\": \"Smart Life\",\n                                \"enum\": [\n                                    \"smartlife\"\n                                ]\n                            }\n                        ],\n                        \"condition\": {\n                            \"functionBody\": \"return model.options.projectType === '2';\"\n                        }\n                    },\n                    \"homeWhitelist\": {\n                        \"title\": \"Whitelisted Home IDs\",\n                        \"description\": \"An optional list of Home IDs to match. If blank, all homes are matched.\",\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"title\": \"Home ID\",\n                            \"type\": \"integer\"\n                        },\n                        \"condition\": {\n                            \"functionBody\": \"return model.options.projectType === '2';\"\n                        }\n                    },\n                    \"deviceOverrides\": {\n                        \"title\": \"Device Overriding Configs\",\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                                \"id\": {\n                                    \"title\": \"ID\",\n                                    \"description\": \"Device ID or Product ID or `global`\",\n                                    \"type\": \"string\",\n                                    \"required\": true\n                                },\n                                \"category\": {\n                                    \"title\": \"Category\",\n                                    \"description\": \"Category Code or `hidden`\",\n                                    \"type\": \"string\",\n                                    \"condition\": {\n                                        \"functionBody\": \"return (model.options && model.options.deviceOverrides);\"\n                                    }\n                                },\n                                \"unbridged\": {\n                                    \"title\": \"Unbridge\",\n                                    \"description\": \"Would you like to make this device be an external device?\",\n                                    \"type\": \"boolean\",\n                                    \"condition\": {\n                                        \"functionBody\": \"return (model.options && model.options.deviceOverrides);\"\n                                    }\n                                },\n                                \"schema\": {\n                                    \"title\": \"Schema Overriding Configs\",\n                                    \"type\": \"array\",\n                                    \"items\": {\n                                        \"type\": \"object\",\n                                        \"properties\": {\n                                            \"code\": {\n                                                \"title\": \"DP Code\",\n                                                \"type\": \"string\",\n                                                \"required\": true,\n                                                \"condition\": {\n                                                    \"functionBody\": \"return (model.options && model.options.deviceOverrides);\"\n                                                }\n                                            },\n                                            \"newCode\": {\n                                                \"title\": \"New DP Code\",\n                                                \"type\": \"string\",\n                                                \"condition\": {\n                                                    \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].code && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                }\n                                            },\n                                            \"type\": {\n                                                \"title\": \"New DP Type\",\n                                                \"type\": \"string\",\n                                                \"default\": \"\",\n                                                \"oneOf\": [\n                                                    {\n                                                        \"title\": \"Boolean\",\n                                                        \"enum\": [\n                                                            \"Boolean\"\n                                                        ]\n                                                    },\n                                                    {\n                                                        \"title\": \"Integer\",\n                                                        \"enum\": [\n                                                            \"Integer\"\n                                                        ]\n                                                    },\n                                                    {\n                                                        \"title\": \"Enum\",\n                                                        \"enum\": [\n                                                            \"Enum\"\n                                                        ]\n                                                    },\n                                                    {\n                                                        \"title\": \"String\",\n                                                        \"enum\": [\n                                                            \"String\"\n                                                        ]\n                                                    },\n                                                    {\n                                                        \"title\": \"Json\",\n                                                        \"enum\": [\n                                                            \"Json\"\n                                                        ]\n                                                    },\n                                                    {\n                                                        \"title\": \"Raw\",\n                                                        \"enum\": [\n                                                            \"Raw\"\n                                                        ]\n                                                    }\n                                                ],\n                                                \"condition\": {\n                                                    \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].code && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                }\n                                            },\n                                            \"property\": {\n                                                \"title\": \"New DP Property\",\n                                                \"type\": \"object\",\n                                                \"properties\": {\n                                                    \"min\": {\n                                                        \"title\": \"min\",\n                                                        \"type\": \"integer\",\n                                                        \"condition\": {\n                                                            \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                        }\n                                                    },\n                                                    \"max\": {\n                                                        \"title\": \"max\",\n                                                        \"type\": \"integer\",\n                                                        \"condition\": {\n                                                            \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                        }\n                                                    },\n                                                    \"scale\": {\n                                                        \"title\": \"scale\",\n                                                        \"type\": \"integer\",\n                                                        \"condition\": {\n                                                            \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                        }\n                                                    },\n                                                    \"step\": {\n                                                        \"title\": \"step\",\n                                                        \"type\": \"integer\",\n                                                        \"condition\": {\n                                                            \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                        }\n                                                    },\n                                                    \"range\": {\n                                                        \"title\": \"range\",\n                                                        \"type\": \"array\",\n                                                        \"items\": {\n                                                            \"title\": \"value\",\n                                                            \"type\": \"string\"\n                                                        },\n                                                        \"condition\": {\n                                                            \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Enum' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                        }\n                                                    }\n                                                },\n                                                \"condition\": {\n                                                    \"functionBody\": \"return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].code && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);\"\n                                                }\n                                            },\n                                            \"hidden\": {\n                                                \"title\": \"Hidden\",\n                                                \"type\": \"boolean\",\n                                                \"condition\": {\n                                                    \"functionBody\": \"return (model.options && model.options.deviceOverrides);\"\n                                                }\n                                            }\n                                        }\n                                    },\n                                    \"condition\": {\n                                        \"functionBody\": \"return (model.options && model.options.deviceOverrides);\"\n                                    }\n                                }\n                            }\n                        }\n                    },\n                    \"debug\": {\n                        \"title\": \"Enable Debug Logging\",\n                        \"type\": \"boolean\",\n                        \"default\": false\n                    },\n                    \"debugLevel\": {\n                        \"title\": \"Debug Level\",\n                        \"description\": \"An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed.\",\n                        \"type\": \"string\",\n                        \"condition\": {\n                            \"functionBody\": \"return (model.options && model.options.debug);\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"layout\": [\n        {\n            \"type\": \"fieldset\",\n            \"title\": \"Tuya Account Info\",\n            \"expandable\": true,\n            \"expanded\": false,\n            \"items\": [\n                \"options.projectType\",\n                \"options.endpoint\",\n                \"options.accessId\",\n                \"options.accessKey\",\n                \"options.countryCode\",\n                \"options.username\",\n                \"options.password\",\n                \"options.appSchema\"\n            ]\n        },\n        {\n            \"type\": \"fieldset\",\n            \"title\": \"Tuya Home Settings\",\n            \"expandable\": true,\n            \"expanded\": false,\n            \"notitle\": false,\n            \"items\": [\n                {\n                    \"key\": \"options.homeWhitelist\",\n                    \"add\": \"Add Another Home ID\",\n                    \"title\": \"{{ 'New Whitelisted Home' }}\",\n                    \"type\": \"tabarray\",\n                    \"notitle\": true,\n                    \"items\": [\n                        {\n                            \"type\": \"div\",\n                            \"displayFlex\": true,\n                            \"flex-direction\": \"row\",\n                            \"notitle\": true,\n                            \"title\": \"{{ value }}\",\n                            \"items\": [\n                                {\n                                    \"key\": \"options.homeWhitelist[]\",\n                                    \"placeholder\": \"Home ID\"\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"fieldset\",\n            \"title\": \"Tuya Device Settings\",\n            \"expandable\": true,\n            \"expanded\": true,\n            \"notitle\": false,\n            \"items\": [\n                {\n                    \"key\": \"options.deviceOverrides\",\n                    \"add\": \"Add Another Device Override\",\n                    \"title\": \"{{ 'New Device Override' }}\",\n                    \"type\": \"tabarray\",\n                    \"notitle\": true,\n                    \"items\": [\n                        {\n                            \"type\": \"div\",\n                            \"displayFlex\": false,\n                            \"flex-direction\": \"row\",\n                            \"notitle\": true,\n                            \"title\": \"{{ value.id }}\",\n                            \"items\": [\n                                {\n                                    \"key\": \"options.deviceOverrides[].id\"\n                                },\n                                {\n                                    \"key\": \"options.deviceOverrides[].category\"\n                                },\n                                {\n                                    \"key\": \"options.deviceOverrides[].unbridged\"\n                                },\n                                {\n                                    \"key\": \"options.deviceOverrides[].schema\",\n                                    \"add\": \"Add New Schema\",\n                                    \"title\": \"{{ 'New Schema' }}\",\n                                    \"type\": \"tabarray\",\n                                    \"notitle\": true,\n                                    \"items\": [\n                                        {\n                                            \"type\": \"div\",\n                                            \"displayFlex\": true,\n                                            \"title\": \"{{ value.code }}\",\n                                            \"flex-direction\": \"column\",\n                                            \"notitle\": false,\n                                            \"items\": [\n                                                {\n                                                    \"key\": \"options.deviceOverrides[].schema[].code\"\n                                                },\n                                                {\n                                                    \"key\": \"options.deviceOverrides[].schema[].newCode\"\n                                                },\n                                                {\n                                                    \"key\": \"options.deviceOverrides[].schema[].hidden\"\n                                                },\n                                                {\n                                                    \"key\": \"options.deviceOverrides[].schema[].type\"\n                                                },\n                                                {\n                                                    \"key\": \"options.deviceOverrides[].schema[].property\",\n                                                    \"notitle\": false,\n                                                    \"items\": [\n                                                        \"options.deviceOverrides[].schema[].property.min\",\n                                                        \"options.deviceOverrides[].schema[].property.max\",\n                                                        \"options.deviceOverrides[].schema[].property.scale\",\n                                                        \"options.deviceOverrides[].schema[].property.step\",\n                                                        {\n                                                            \"key\": \"options.deviceOverrides[].schema[].property.range\",\n                                                            \"add\": \"Add Range\",\n                                                            \"title\": \"{{ 'New Range' }}\",\n                                                            \"type\": \"tabarray\",\n                                                            \"notitle\": true,\n                                                            \"items\": [\n                                                                {\n                                                                    \"type\": \"div\",\n                                                                    \"displayFlex\": true,\n                                                                    \"flex-direction\": \"row\",\n                                                                    \"notitle\": true,\n                                                                    \"title\": \"{{ value }}\",\n                                                                    \"items\": [\n                                                                        {\n                                                                            \"key\": \"options.deviceOverrides[].schema[].property.range[]\",\n                                                                            \"placeholder\": \"Range\"\n                                                                        }\n                                                                    ]\n                                                                }\n                                                            ]\n                                                        }\n                                                    ]\n                                                }\n                                            ]\n                                        }\n                                    ]\n                                }\n                            ]\n                        }\n                    ]\n                }\n            ]\n        },\n        {\n            \"type\": \"fieldset\",\n            \"title\": \"Advance Settings\",\n            \"expandable\": true,\n            \"expanded\": false,\n            \"notitle\": false,\n            \"items\": [\n                \"options.debug\",\n                \"options.debugLevel\"\n            ]\n        }\n    ]\n}\n"
  },
  {
    "path": "jest.config.js",
    "content": "/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n};\n"
  },
  {
    "path": "nodemon.json",
    "content": "{\n  \"watch\": [\n    \"src\"\n  ],\n  \"ext\": \"ts\",\n  \"ignore\": [],\n  \"exec\": \"tsc && homebridge -I -D\",\n  \"signal\": \"SIGTERM\",\n  \"env\": {\n    \"NODE_OPTIONS\": \"--trace-warnings\"\n  }\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@0x5e/homebridge-tuya-platform\",\n  \"displayName\": \"Tuya\",\n  \"version\": \"1.7.0-beta.58\",\n  \"description\": \"Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/0x5e/homebridge-tuya-platform\"\n  },\n  \"homepage\": \"https://github.com/0x5e/homebridge-tuya-platform#readme\",\n  \"bugs\": {\n    \"url\": \"https://github.com/0x5e/homebridge-tuya-platform/issues\"\n  },\n  \"funding\": [\n    {\n      \"type\": \"paypal\",\n      \"url\": \"https://paypal.me/0x5e\"\n    }\n  ],\n  \"engines\": {\n    \"node\": \">=14.18.1\",\n    \"homebridge\": \">=1.3.5\"\n  },\n  \"main\": \"dist/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src/**/*.ts --max-warnings=0\",\n    \"test\": \"jest\",\n    \"watch\": \"npm run build && npm link && nodemon\",\n    \"launch\": \"tsc && homebridge -I -D\",\n    \"build\": \"rimraf ./dist && tsc\",\n    \"prepublishOnly\": \"npm run lint && npm run build\"\n  },\n  \"keywords\": [\n    \"homebridge-plugin\",\n    \"homekit\",\n    \"tuya\"\n  ],\n  \"dependencies\": {\n    \"@homebridge/camera-utils\": \"^2.2.0\",\n    \"async-await-retry\": \"^2.0.1\",\n    \"color-convert\": \"^2.0.1\",\n    \"crypto-js\": \"^4.1.1\",\n    \"debounce\": \"^1.2.1\",\n    \"jsonschema\": \"^1.4.1\",\n    \"kelvin-to-rgb\": \"^1.0.2\",\n    \"lodash.isequal\": \"^4.5.0\",\n    \"mqtt\": \"^4.2.6\",\n    \"uuid\": \"^9.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/color-convert\": \"^2.0.0\",\n    \"@types/crypto-js\": \"^4.1.1\",\n    \"@types/debounce\": \"^1.2.1\",\n    \"@types/jest\": \"^29.1.2\",\n    \"@types/lodash.isequal\": \"^4.5.6\",\n    \"@types/node\": \"^18.11.9\",\n    \"@types/uuid\": \"^8.3.4\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.0.0\",\n    \"@typescript-eslint/parser\": \"^5.0.0\",\n    \"eslint\": \"^8.0.1\",\n    \"homebridge\": \"^1.3.5\",\n    \"jest\": \"^29.1.2\",\n    \"nodemon\": \"^2.0.13\",\n    \"rimraf\": \"^3.0.2\",\n    \"ts-jest\": \"^29.0.3\",\n    \"ts-node\": \"^10.3.0\",\n    \"typescript\": \"^4.8.4\"\n  }\n}\n"
  },
  {
    "path": "src/accessory/AccessoryFactory.ts",
    "content": "import { PlatformAccessory } from 'homebridge';\nimport TuyaDevice from '../device/TuyaDevice';\nimport { TuyaPlatform } from '../platform';\n\nimport BaseAccessory from './BaseAccessory';\nimport LightAccessory from './LightAccessory';\nimport DimmerAccessory from './DimmerAccessory';\nimport OutletAccessory from './OutletAccessory';\nimport SwitchAccessory from './SwitchAccessory';\nimport WirelessSwitchAccessory from './WirelessSwitchAccessory';\nimport SceneSwitchAccessory from './SceneSwitchAccessory';\nimport FanAccessory from './FanAccessory';\nimport GarageDoorAccessory from './GarageDoorAccessory';\nimport WindowAccessory from './WindowAccessory';\nimport WindowCoveringAccessory from './WindowCoveringAccessory';\nimport LockAccessory from './LockAccessory';\nimport ThermostatAccessory from './ThermostatAccessory';\nimport HeaterAccessory from './HeaterAccessory';\nimport ValveAccessory from './ValveAccessory';\nimport ContactSensorAccessory from './ContactSensorAccessory';\nimport LeakSensorAccessory from './LeakSensorAccessory';\nimport CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory';\nimport CarbonDioxideSensorAccessory from './CarbonDioxideSensorAccessory';\nimport SmokeSensorAccessory from './SmokeSensorAccessory';\nimport TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAccessory';\nimport LightSensorAccessory from './LightSensorAccessory';\nimport MotionSensorAccessory from './MotionSensorAccessory';\nimport AirQualitySensorAccessory from './AirQualitySensorAccessory';\nimport HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory';\nimport HumidifierAccessory from './HumidifierAccessory';\nimport DehumidifierAccessory from './DehumidifierAccessory';\nimport DiffuserAccessory from './DiffuserAccessory';\nimport AirPurifierAccessory from './AirPurifierAccessory';\nimport ExtractionHoodAccessory from './ExtractionHoodAccessory';\nimport CameraAccessory from './CameraAccessory';\nimport SceneAccessory from './SceneAccessory';\nimport AirConditionerAccessory from './AirConditionerAccessory';\nimport IRControlHubAccessory from './IRControlHubAccessory';\nimport IRGenericAccessory from './IRGenericAccessory';\nimport IRAirConditionerAccessory from './IRAirConditionerAccessory';\nimport SecuritySystemAccessory from './SecuritySystemAccessory';\nimport VibrationSensorAccessory from './VibrationSensorAccessory';\nimport WeatherStationAccessory from './WeatherStationAccessory';\nimport DoorbellAccessory from './DoorbellAccessory';\nimport PetFeederAccessory from './PetFeederAccessory';\nimport CatToiletAccessory from './CatToiletAccessory';\nimport WhiteNoiseLightAccessory from './WhiteNoiseLightAccessory';\nimport SaunaAccessory from './SaunaAccessory';\n\n\nexport default class AccessoryFactory {\n  static createAccessory(\n    platform: TuyaPlatform,\n    accessory: PlatformAccessory,\n    device: TuyaDevice,\n  ): BaseAccessory {\n\n    let handler : BaseAccessory | undefined;\n    switch (device.category) {\n\n      // Lighting\n      case 'dj':\n      case 'dsd':\n      case 'xdd':\n      case 'fwd':\n      case 'dc':\n      case 'dd':\n      case 'gyd':\n      case 'tyndj':\n      case 'sxd':\n        handler = new LightAccessory(platform, accessory);\n        break;\n      case 'tgq':\n      case 'tgkg':\n        handler = new DimmerAccessory(platform, accessory);\n        break;\n\n      // Electrical Products\n      case 'dlq':\n      case 'kg':\n      case 'tdq':\n      case 'qjdcz':\n      case 'szjqr':\n        handler = new SwitchAccessory(platform, accessory);\n        break;\n      case 'cz':\n      case 'pc':\n      case 'wkcz':\n        handler = new OutletAccessory(platform, accessory);\n        break;\n      case 'wxkg':\n        handler = new WirelessSwitchAccessory(platform, accessory);\n        break;\n      case 'cjkg':\n        handler = new SceneSwitchAccessory(platform, accessory);\n        break;\n      case 'bzyd':\n        handler = new WhiteNoiseLightAccessory(platform, accessory);\n        break;\n\n      // Large Home Appliances\n      case 'kt':\n      case 'ktkzq':\n        handler = new AirConditionerAccessory(platform, accessory);\n        break;\n      case 'qtwk':\n        handler = new SaunaAccessory(platform, accessory);\n        break;\n\n      // Small Home Appliances\n      case 'qn':\n        handler = new HeaterAccessory(platform, accessory);\n        break;\n      case 'kj':\n        handler = new AirPurifierAccessory(platform, accessory);\n        break;\n      case 'xxj':\n        handler = new DiffuserAccessory(platform, accessory);\n        break;\n      case 'ckmkzq':\n        handler = new GarageDoorAccessory(platform, accessory);\n        break;\n      case 'cl':\n      case 'clkg':\n        handler = new WindowCoveringAccessory(platform, accessory);\n        break;\n      case 'cwwsq':\n        handler = new PetFeederAccessory(platform, accessory);\n        break;\n      case 'msp':\n        handler = new CatToiletAccessory(platform, accessory);\n        break;\n      case 'mc':\n        handler = new WindowAccessory(platform, accessory);\n        break;\n      case 'wk':\n      case 'wkf':\n        handler = new ThermostatAccessory(platform, accessory);\n        break;\n      case 'ggq':\n      case 'sfkzq':\n        handler = new ValveAccessory(platform, accessory);\n        break;\n      case 'jsq':\n        handler = new HumidifierAccessory(platform, accessory);\n        break;\n      case 'cs':\n        handler = new DehumidifierAccessory(platform, accessory);\n        break;\n      case 'fs':\n      case 'fsd':\n      case 'fskg':\n        handler = new FanAccessory(platform, accessory);\n        break;\n      case 'yyj':\n        handler = new ExtractionHoodAccessory(platform, accessory);\n        break;\n\n      // Security & Video Surveillance\n      case 'sp':\n        handler = new CameraAccessory(platform, accessory);\n        break;\n      case 'ywbj':\n        handler = new SmokeSensorAccessory(platform, accessory);\n        break;\n      case 'mcs':\n        handler = new ContactSensorAccessory(platform, accessory);\n        break;\n      case 'zd':\n        handler = new VibrationSensorAccessory(platform, accessory);\n        break;\n      case 'rqbj':\n      case 'jwbj':\n      case 'sj':\n        handler = new LeakSensorAccessory(platform, accessory);\n        break;\n      case 'cobj':\n      case 'cocgq':\n        handler = new CarbonMonoxideSensorAccessory(platform, accessory);\n        break;\n      case 'co2bj':\n      case 'co2cgq':\n        handler = new CarbonDioxideSensorAccessory(platform, accessory);\n        break;\n      case 'wsdcg':\n        handler = new TemperatureHumiditySensorAccessory(platform, accessory);\n        break;\n      case 'ldcg':\n        handler = new LightSensorAccessory(platform, accessory);\n        break;\n      case 'pir':\n        handler = new MotionSensorAccessory(platform, accessory);\n        break;\n      case 'pm25':\n      case 'pm2.5':\n      case 'pm25cgq':\n      case 'hjjcy':\n        handler = new AirQualitySensorAccessory(platform, accessory);\n        break;\n      case 'hps':\n        handler = new HumanPresenceSensorAccessory(platform, accessory);\n        break;\n      case 'ms':\n      case 'jtmspro':\n        handler = new LockAccessory(platform, accessory);\n        break;\n      case 'mal':\n        handler = new SecuritySystemAccessory(platform, accessory);\n        break;\n      case 'wxml':\n        handler = new DoorbellAccessory(platform, accessory);\n        break;\n      case 'qxj':\n        handler = new WeatherStationAccessory(platform, accessory);\n        break;\n\n      // Other\n      case 'scene':\n        handler = new SceneAccessory(platform, accessory);\n        break;\n    }\n\n    // IR Control Hub\n    if (device.isIRControlHub()) {\n      handler = new IRControlHubAccessory(platform, accessory);\n    }\n\n    // IR Remote Control\n    if (device.isIRRemoteControl()) {\n      switch (device.remote_keys?.category_id) {\n        case 5: // AC\n          handler = new IRAirConditionerAccessory(platform, accessory);\n          break;\n        default:\n          handler = new IRGenericAccessory(platform, accessory);\n          break;\n      }\n    }\n\n    if (handler && !handler.checkRequirements()) {\n      handler = undefined;\n    }\n\n    if (!handler) {\n      platform.log.warn(`Unsupported device: ${device.name}.`);\n      handler = new BaseAccessory(platform, accessory);\n    }\n\n    handler.configureServices();\n    handler.configureStatusActive();\n    handler.updateAllValues();\n    handler.intialized = true;\n\n    return handler;\n  }\n}\n"
  },
  {
    "path": "src/accessory/AirConditionerAccessory.ts",
    "content": "import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureLockPhysicalControls } from './characteristic/LockPhysicalControls';\nimport { configureRelativeHumidityDehumidifierThreshold } from './characteristic/RelativeHumidityDehumidifierThreshold';\nimport { configureRotationSpeedLevel } from './characteristic/RotationSpeed';\n// import { configureSwingMode } from './characteristic/SwingMode';\nimport { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits';\n\nconst SCHEMA_CODE = {\n  // AirConditioner\n  ACTIVE: ['switch'],\n  MODE: ['mode'],\n  WORK_STATE: ['work_status', 'mode'],\n  CURRENT_TEMP: ['temp_current'],\n  TARGET_TEMP: ['temp_set'],\n  SPEED_LEVEL: ['fan_speed_enum', 'windspeed'],\n  LOCK: ['lock', 'child_lock'],\n  TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'],\n  SWING: ['switch_horizontal', 'switch_vertical'],\n  // Dehumidifier\n  CURRENT_HUMIDITY: ['humidity_current'],\n  TARGET_HUMIDITY: ['humidity_set'],\n};\n\nconst AC_MODES = ['auto', 'cold', 'hot'];\nconst DEHUMIDIFIER_MODE = 'wet';\nconst FAN_MODE = 'wind';\n\nexport default class AirConditionerAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE, SCHEMA_CODE.MODE, SCHEMA_CODE.WORK_STATE, SCHEMA_CODE.CURRENT_TEMP];\n  }\n\n  configureServices() {\n    this.configureAirConditioner();\n    this.configureDehumidifier();\n    this.configureFan();\n\n    // Add extra sensors for home automation use.\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n  }\n\n  configureAirConditioner() {\n    const activeSchema = this.getSchema(...SCHEMA_CODE.ACTIVE)!;\n    const modeSchema = this.getSchema(...SCHEMA_CODE.MODE)!;\n    const modeProperty = modeSchema.property as TuyaDeviceSchemaEnumProperty;\n\n    const service = this.mainService();\n\n    // Required Characteristics\n    const { INACTIVE, ACTIVE } = this.Characteristic.Active;\n    service.getCharacteristic(this.Characteristic.Active)\n      .onGet(() => {\n        const activeStatus = this.getStatus(activeSchema.code)!;\n        const modeStatus = this.getStatus(modeSchema.code)!;\n        return (activeStatus.value === true && AC_MODES.includes(modeStatus.value as string)) ? ACTIVE : INACTIVE;\n      })\n      .onSet(async value => {\n        const commands: TuyaDeviceStatus[] = [{\n          code: activeSchema.code,\n          value: (value === ACTIVE) ? true : false,\n        }];\n\n        const modeStatus = this.getStatus(modeSchema.code)!;\n        if (!AC_MODES.includes(modeStatus.value as string)) {\n          for (const mode of AC_MODES) {\n            if (modeProperty.range.includes(mode)) {\n              commands.push({ code: modeStatus.code, value: mode });\n              break;\n            }\n          }\n        }\n\n        await this.sendCommands(commands, true);\n      });\n\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureCurrentTemperature(this, service, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n\n    // Optional Characteristics\n    configureLockPhysicalControls(this, service, this.getSchema(...SCHEMA_CODE.LOCK));\n    configureRotationSpeedLevel(this, service, this.getSchema(...SCHEMA_CODE.SPEED_LEVEL), ['auto']);\n    // configureSwingMode(this, service, this.getSchema(...SCHEMA_CODE.SWING));\n    this.configureCoolingThreshouldTemp();\n    this.configureHeatingThreshouldTemp();\n    configureTempDisplayUnits(this, service, this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT));\n  }\n\n  configureDehumidifier() {\n    const activeSchema = this.getSchema(...SCHEMA_CODE.ACTIVE)!;\n    const modeSchema = this.getSchema(...SCHEMA_CODE.MODE)!;\n    const property = modeSchema.property as TuyaDeviceSchemaEnumProperty;\n    if (!property.range.includes(DEHUMIDIFIER_MODE)) {\n      return;\n    }\n\n    const service = this.dehumidifierService();\n\n    // Required Characteristics\n    const { INACTIVE, ACTIVE } = this.Characteristic.Active;\n    service.getCharacteristic(this.Characteristic.Active)\n      .onGet(() => {\n        const activeStatus = this.getStatus(activeSchema.code)!;\n        const modeStatus = this.getStatus(modeSchema.code)!;\n        return (activeStatus.value === true && modeStatus.value === DEHUMIDIFIER_MODE) ? ACTIVE : INACTIVE;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: activeSchema.code,\n          value: (value === ACTIVE) ? true : false,\n        }, {\n          code: modeSchema.code,\n          value: DEHUMIDIFIER_MODE,\n        }], true);\n      });\n\n    const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;\n    service.setCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState, DEHUMIDIFYING);\n\n    const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState;\n    service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState)\n      .updateValue(DEHUMIDIFIER)\n      .setProps({ validValues: [DEHUMIDIFIER] });\n\n    if (this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)) {\n      configureCurrentRelativeHumidity(this, service, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n    } else {\n      service.setCharacteristic(this.Characteristic.CurrentRelativeHumidity, 0);\n    }\n\n    // Optional Characteristics\n    configureLockPhysicalControls(this, service, this.getSchema(...SCHEMA_CODE.LOCK));\n    configureRotationSpeedLevel(this, service, this.getSchema(...SCHEMA_CODE.SPEED_LEVEL), ['auto']);\n    configureRelativeHumidityDehumidifierThreshold(this, service, this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY));\n    // configureSwingMode(this, service, this.getSchema(...SCHEMA_CODE.SWING));\n  }\n\n  configureFan() {\n    const activeSchema = this.getSchema(...SCHEMA_CODE.ACTIVE)!;\n    const modeSchema = this.getSchema(...SCHEMA_CODE.MODE)!;\n    const property = modeSchema.property as TuyaDeviceSchemaEnumProperty;\n    if (!property.range.includes(FAN_MODE)) {\n      return;\n    }\n\n    const service = this.fanService();\n\n    // Required Characteristics\n    const { INACTIVE, ACTIVE } = this.Characteristic.Active;\n    service.getCharacteristic(this.Characteristic.Active)\n      .onGet(() => {\n        const activeStatus = this.getStatus(activeSchema.code)!;\n        const modeStatus = this.getStatus(modeSchema.code)!;\n        return (activeStatus.value === true && modeStatus.value === FAN_MODE) ? ACTIVE : INACTIVE;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: activeSchema.code,\n          value: (value === ACTIVE) ? true : false,\n        }, {\n          code: modeSchema.code,\n          value: FAN_MODE,\n        }], true);\n      });\n\n    // Optional Characteristics\n    configureLockPhysicalControls(this, service, this.getSchema(...SCHEMA_CODE.LOCK));\n    configureRotationSpeedLevel(this, service, this.getSchema(...SCHEMA_CODE.SPEED_LEVEL), ['auto']);\n    // configureSwingMode(this, service, this.getSchema(...SCHEMA_CODE.SWING));\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.HeaterCooler)\n      || this.accessory.addService(this.Service.HeaterCooler);\n  }\n\n  dehumidifierService() {\n    return this.accessory.getService(this.Service.HumidifierDehumidifier)\n      || this.accessory.addService(this.Service.HumidifierDehumidifier, this.accessory.displayName + ' Dehumidifier');\n  }\n\n  fanService() {\n    return this.accessory.getService(this.Service.Fanv2)\n      || this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan');\n  }\n\n  configureCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.WORK_STATE);\n    if (!schema) {\n      return;\n    }\n\n    const { INACTIVE, HEATING, COOLING } = this.Characteristic.CurrentHeaterCoolerState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentHeaterCoolerState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        if (status.value === 'heating' || status.value === 'hot') {\n          return HEATING;\n        } else if (status.value === 'cooling' || status.value === 'cold') {\n          return COOLING;\n        } else {\n          return INACTIVE;\n        }\n      });\n  }\n\n  configureTargetState() {\n    const schema = this.getSchema(...SCHEMA_CODE.MODE);\n    if (!schema) {\n      return;\n    }\n\n    const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;\n\n    const validValues: number[] = [];\n    const property = schema.property as TuyaDeviceSchemaEnumProperty;\n    if (property.range.includes('auto')) {\n      validValues.push(AUTO);\n    }\n    if (property.range.includes('hot')) {\n      validValues.push(HEAT);\n    }\n    if (property.range.includes('cold')) {\n      validValues.push(COOL);\n    }\n\n    if (validValues.length === 0) {\n      this.log.warn('Invalid mode range for TargetHeaterCoolerState:', property.range);\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        if (status.value === 'hot') {\n          return HEAT;\n        } else if (status.value === 'cold') {\n          return COOL;\n        }\n\n        return validValues.includes(AUTO) ? AUTO : validValues[0];\n      })\n      .onSet(async value => {\n\n        let mode: string;\n        if (value === HEAT) {\n          mode = 'hot';\n        } else if (value === COOL) {\n          mode = 'cold';\n        } else {\n          mode = 'auto';\n        }\n\n        await this.sendCommands([{ code: schema.code, value: mode }], true);\n      })\n      .setProps({ validValues });\n  }\n\n  configureCoolingThreshouldTemp() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);\n    if (!schema) {\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property.scale);\n    const props = {\n      minValue: property.min / multiple,\n      maxValue: property.max / multiple,\n      minStep: Math.max(0.1, property.step / multiple),\n    };\n    this.log.debug('Set props for CoolingThresholdTemperature:', props);\n\n    this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature)\n      .onGet(() => {\n        const modeSchema = this.getSchema(...SCHEMA_CODE.MODE);\n        if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') {\n          return props.minValue;\n        }\n\n        const status = this.getStatus(schema.code)!;\n        const temp = status.value as number / multiple;\n        return limit(temp, props.minValue, props.maxValue);\n      })\n      .onSet(async value => {\n        const modeSchema = this.getSchema(...SCHEMA_CODE.MODE);\n        if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') {\n          this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature)\n            .updateValue(props.minValue);\n          return;\n        }\n\n        await this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true);\n      })\n      .setProps(props);\n  }\n\n  configureHeatingThreshouldTemp() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);\n    if (!schema) {\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property.scale);\n    const props = {\n      minValue: property.min / multiple,\n      maxValue: property.max / multiple,\n      minStep: Math.max(0.1, property.step / multiple),\n    };\n    this.log.debug('Set props for HeatingThresholdTemperature:', props);\n\n    this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature)\n      .onGet(() => {\n        const modeSchema = this.getSchema(...SCHEMA_CODE.MODE);\n        if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') {\n          return props.maxValue;\n        }\n\n        const status = this.getStatus(schema.code)!;\n        const temp = status.value as number / multiple;\n        return limit(temp, props.minValue, props.maxValue);\n      })\n      .onSet(async value => {\n        const modeSchema = this.getSchema(...SCHEMA_CODE.MODE);\n        if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') {\n          this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature)\n            .updateValue(props.maxValue);\n          return;\n        }\n\n        await this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true);\n      })\n      .setProps(props);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/AirPurifierAccessory.ts",
    "content": "import { TuyaDeviceSchemaType } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureAirQuality } from './characteristic/AirQuality';\nimport { configureLockPhysicalControls } from './characteristic/LockPhysicalControls';\nimport { configureRotationSpeed, configureRotationSpeedLevel } from './characteristic/RotationSpeed';\n\nconst SCHEMA_CODE = {\n  ACTIVE: ['switch'],\n  MODE: ['mode'],\n  LOCK: ['lock'],\n  SPEED: ['speed'],\n  SPEED_LEVEL: ['fan_speed_enum', 'speed'],\n  AIR_QUALITY: ['air_quality', 'pm25'],\n  PM2_5: ['pm25'],\n  VOC: ['tvoc'],\n};\n\nexport default class AirPurifierAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE];\n  }\n\n  configureServices() {\n    configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK));\n    if (this.getFanSpeedSchema()) {\n      configureRotationSpeed(this, this.mainService(), this.getFanSpeedSchema());\n    } else if (this.getFanSpeedLevelSchema()) {\n      configureRotationSpeedLevel(this, this.mainService(), this.getFanSpeedLevelSchema());\n    }\n\n    // Other\n    configureAirQuality(\n      this,\n      undefined,\n      this.getSchema(...SCHEMA_CODE.AIR_QUALITY),\n      this.getSchema(...SCHEMA_CODE.PM2_5),\n      undefined,\n      this.getSchema(...SCHEMA_CODE.VOC),\n    );\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.AirPurifier)\n      || this.accessory.addService(this.Service.AirPurifier);\n  }\n\n  getFanSpeedSchema() {\n    const schema = this.getSchema(...SCHEMA_CODE.SPEED);\n    if (schema && schema.type === TuyaDeviceSchemaType.Integer) {\n      return schema;\n    }\n    return undefined;\n  }\n\n  getFanSpeedLevelSchema() {\n    const schema = this.getSchema(...SCHEMA_CODE.SPEED_LEVEL);\n    if (schema && schema.type === TuyaDeviceSchemaType.Enum) {\n      return schema;\n    }\n    return undefined;\n  }\n\n\n  configureCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.ACTIVE);\n    if (!schema) {\n      return;\n    }\n\n    const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value as boolean ? PURIFYING_AIR : INACTIVE;\n      });\n  }\n\n  configureTargetState() {\n    const schema = this.getSchema(...SCHEMA_CODE.MODE);\n    if (!schema) {\n      return;\n    }\n\n    const { MANUAL, AUTO } = this.Characteristic.TargetAirPurifierState;\n    this.mainService().getCharacteristic(this.Characteristic.TargetAirPurifierState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value === 'auto') ? AUTO : MANUAL;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: schema.code,\n          value: (value === AUTO) ? 'auto' : 'manual',\n        }], true);\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/AirQualitySensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureAirQuality } from './characteristic/AirQuality';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\n\nconst SCHEMA_CODE = {\n  AIR_QUALITY: ['pm25_value'],\n  PM2_5: ['pm25_value'],\n  PM10: ['pm10_value', 'pm10'],\n  VOC: ['voc_value'],\n  CURRENT_TEMP: ['va_temperature', 'temp_indoor', 'temp_current'],\n  CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],\n};\n\nexport default class AirQualitySensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.AIR_QUALITY];\n  }\n\n  configureServices() {\n    configureAirQuality(\n      this,\n      undefined,\n      this.getSchema(...SCHEMA_CODE.AIR_QUALITY),\n      this.getSchema(...SCHEMA_CODE.PM2_5),\n      this.getSchema(...SCHEMA_CODE.PM10),\n      this.getSchema(...SCHEMA_CODE.VOC),\n    );\n\n    // Other\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/BaseAccessory.ts",
    "content": "/* eslint-disable @typescript-eslint/no-empty-function */\n/* eslint-disable @typescript-eslint/no-unused-vars */\nimport { PlatformAccessory, Service, Characteristic, Nullable, CharacteristicValue } from 'homebridge';\nimport { debounce } from 'debounce';\nimport isEqual from 'lodash.isequal';\n\nimport { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaMode, TuyaDeviceStatus } from '../device/TuyaDevice';\nimport { TuyaPlatform } from '../platform';\nimport { limit } from '../util/util';\nimport { PrefixLogger } from '../util/Logger';\n\nconst MANUFACTURER = 'Tuya Inc.';\n\nconst SCHEMA_CODE = {\n  BATTERY_STATE: ['battery_state'],\n  BATTERY_PERCENT: ['battery_percentage', 'residual_electricity', 'wireless_electricity', 'va_battery', 'battery'],\n  BATTERY_CHARGING: ['charge_state'],\n};\n\n\n/**\n * Homebridge Accessory Categories Documentation:\n *   https://developers.homebridge.io/#/categories\n * Tuya Standard Instruction Set Documentation:\n *   https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq\n */\nclass BaseAccessory {\n  public readonly Service: typeof Service = this.platform.api.hap.Service;\n  public readonly Characteristic: typeof Characteristic = this.platform.api.hap.Characteristic;\n\n  public deviceManager = this.platform.deviceManager!;\n  public device = this.deviceManager.getDevice(this.accessory.context.deviceID)!;\n  public log = new PrefixLogger(\n    this.platform.log,\n    this.device.name.length > 0 ? this.device.name : this.device.id,\n    this.platform.options.debug && ((this.platform.options.debugLevel ?? '').length > 0\n      ? this.platform.options.debugLevel?.includes(this.device.id)\n      : true),\n  );\n\n  public intialized = false;\n\n  public adaptiveLightingController?;\n\n  constructor(\n    public readonly platform: TuyaPlatform,\n    public readonly accessory: PlatformAccessory,\n  ) {\n    this.addAccessoryInfoService();\n    this.addBatteryService();\n  }\n\n  addAccessoryInfoService() {\n    const service = this.accessory.getService(this.Service.AccessoryInformation)\n      || this.accessory.addService(this.Service.AccessoryInformation);\n\n    service\n      .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER)\n      .setCharacteristic(this.Characteristic.Model, this.device.product_id)\n      .setCharacteristic(this.Characteristic.Name, this.device.name)\n      .setCharacteristic(this.Characteristic.ConfiguredName, this.device.name)\n      .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid)\n    ;\n  }\n\n  addBatteryService() {\n    const percentSchema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENT);\n    if (!percentSchema) {\n      return;\n    }\n\n    const { BATTERY_LEVEL_NORMAL, BATTERY_LEVEL_LOW } = this.Characteristic.StatusLowBattery;\n    const service = this.accessory.getService(this.Service.Battery)\n      || this.accessory.addService(this.Service.Battery);\n\n    const stateSchema = this.getSchema(...SCHEMA_CODE.BATTERY_STATE);\n    if (stateSchema || percentSchema) {\n      service.getCharacteristic(this.Characteristic.StatusLowBattery)\n        .onGet(() => {\n          if (stateSchema) {\n            const status = this.getStatus(stateSchema.code)!;\n            return (status!.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL;\n          }\n\n          // fallback\n          const status = this.getStatus(percentSchema.code)!;\n          return (status!.value as number <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL;\n        });\n    }\n\n    const property = percentSchema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property ? property.scale : 0);\n    service.getCharacteristic(this.Characteristic.BatteryLevel)\n      .onGet(() => {\n        const status = this.getStatus(percentSchema.code)!;\n        return limit(status.value as number / multiple, 0, 100);\n      });\n\n    const chargingSchema = this.getSchema(...SCHEMA_CODE.BATTERY_CHARGING);\n    if (chargingSchema) {\n      const { NOT_CHARGING, CHARGING } = this.Characteristic.ChargingState;\n      service.getCharacteristic(this.Characteristic.ChargingState)\n        .onGet(() => {\n          const status = this.getStatus(chargingSchema.code)!;\n          return (status.value as boolean) ? CHARGING : NOT_CHARGING;\n        });\n    }\n  }\n\n  configureStatusActive() {\n    for (const service of this.accessory.services) {\n      if (!service.testCharacteristic(this.Characteristic.StatusActive)) { // silence warning\n        service.addOptionalCharacteristic(this.Characteristic.StatusActive);\n      }\n      service.getCharacteristic(this.Characteristic.StatusActive)\n        .onGet(() => this.device.online);\n    }\n  }\n\n  async updateAllValues() {\n    for (const service of this.accessory.services) {\n      for (const characteristic of service.characteristics) {\n        if (characteristic.UUID === this.Characteristic.ProgrammableSwitchEvent.UUID) {\n          continue;\n        }\n\n        let newValue: Nullable<CharacteristicValue> | Error = characteristic.value;\n        const getHandler = characteristic['getHandler'];\n        if (getHandler) {\n          try {\n            newValue = await getHandler();\n          } catch (error) {\n            // TODO: why `characteristic.updateValue(HapStatusError)` not working?\n            // newValue = error as Error;\n            continue;\n          }\n        }\n\n        if (characteristic.value !== newValue && !(newValue instanceof Error)) {\n          this.log.debug(\n            '[%s/%s/%s] Update value: %o => %o',\n            service.constructor.name,\n            service.subtype,\n            characteristic.constructor.name,\n            characteristic.value,\n            newValue,\n          );\n        }\n        characteristic.updateValue(newValue);\n      }\n    }\n  }\n\n  checkOnlineStatus() {\n    if (!this.device.online) {\n      const { HapStatusError, HAPStatus } = this.platform.api.hap;\n      throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n    }\n  }\n\n  getSchema(...codes: string[]) {\n    for (const code of codes) {\n      const schema = this.device.schema.find(schema => schema.code === code);\n      if (!schema) {\n        continue;\n      }\n\n      // Readable schema must have a status\n      if ([TuyaDeviceSchemaMode.READ_WRITE, TuyaDeviceSchemaMode.READ_ONLY].includes(schema.mode)\n        && !this.getStatus(schema.code)) {\n        continue;\n      }\n\n      return schema;\n    }\n    return undefined;\n  }\n\n  getStatus(code: string) {\n    return this.device.status.find(status => status.code === code);\n  }\n\n  private sendQueue = new Map<string, TuyaDeviceStatus>();\n  private debounceSendCommands = debounce(async () => {\n    const commands = [...this.sendQueue.values()];\n    if (commands.length === 0) {\n      return;\n    }\n    await this.deviceManager.sendCommands(this.device.id, commands);\n    this.sendQueue.clear();\n  }, 100);\n\n  async sendCommands(commands: TuyaDeviceStatus[], debounce = false) {\n    if (commands.length === 0) {\n      return;\n    }\n\n    commands = commands.filter((status) => status.code && status.value !== undefined);\n\n    if (this.device.online === false) {\n      this.log.warn('Device is offline, skip send command.');\n      this.updateAllValues();\n      const { HapStatusError, HAPStatus } = this.platform.api.hap;\n      throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n      return;\n    }\n\n    // Update cache immediately\n    for (const newStatus of commands) {\n      const oldStatus = this.device.status.find(_status => _status.code === newStatus.code);\n      if (oldStatus) {\n        oldStatus.value = newStatus.value;\n      }\n    }\n\n    if (debounce === false) {\n      return await this.deviceManager.sendCommands(this.device.id, commands);\n    }\n\n    for (const newStatus of commands) {\n      // Update send queue\n      this.sendQueue.set(newStatus.code, newStatus);\n    }\n\n    this.debounceSendCommands();\n  }\n\n  checkRequirements() {\n    let result = true;\n    for (const codes of this.requiredSchema()) {\n      const schema = this.getSchema(...codes);\n      if (schema) {\n        continue;\n      }\n      this.log.warn('Product Category: %s', this.device.category);\n      this.log.warn('Missing one of the required schema: %s', codes);\n      this.log.warn('Please switch device control mode to \"DP Insctrution\", and set `deviceOverrides` manually.');\n      this.log.warn('Detail information: https://github.com/0x5e/homebridge-tuya-platform#faq');\n      result = false;\n    }\n\n    if (!result) {\n      this.log.warn('Existing schema: %o', this.device.schema);\n    }\n\n    return result;\n  }\n\n  requiredSchema(): string[][] {\n    return [];\n  }\n\n  configureServices() {\n    //\n  }\n\n  async onDeviceInfoUpdate(info) {\n    this.updateAllValues();\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n    this.updateAllValues();\n  }\n\n}\n\n// Overriding getSchema, getStatus, sendCommands\nexport default class OverridedBaseAccessory extends BaseAccessory {\n\n  private eval = (script: string, device, value) => eval(script);\n\n  private getOverridedSchema(code: string) {\n    const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code);\n    if (!schemaConfig) {\n      return undefined;\n    }\n\n    const oldSchema = this.device.schema.find(schema => schema.code === schemaConfig.code);\n    if (!oldSchema) {\n      return undefined;\n    }\n\n    const schema = {\n      code,\n      mode: oldSchema.mode,\n      type: schemaConfig.type || oldSchema.type,\n      property: schemaConfig.property || oldSchema.property,\n      _hidden: schemaConfig.hidden,\n    } as TuyaDeviceSchema;\n\n    if (!isEqual(oldSchema, schema)) {\n      this.log.debug('Override schema %o => %o', oldSchema, schema);\n    }\n\n    return schema;\n  }\n\n  getSchema(...codes: string[]) {\n    for (const code of codes) {\n      const schema = this.getOverridedSchema(code) || super.getSchema(code);\n      if (!schema) {\n        continue;\n      }\n      if (schema['_hidden']) {\n        return undefined;\n      }\n      return schema;\n    }\n    return undefined;\n  }\n\n\n  private getOverridedStatus(code: string) {\n    const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code);\n    if (!schemaConfig) {\n      return undefined;\n    }\n\n    const oldStatus = super.getStatus(schemaConfig.code);\n    if (!oldStatus) {\n      return undefined;\n    }\n\n    const status = { code: schemaConfig.newCode || schemaConfig.code, value: oldStatus.value } as TuyaDeviceStatus;\n    if (schemaConfig.onGet) {\n      status.value = this.eval(schemaConfig.onGet, this.device, oldStatus.value);\n    }\n\n    if (!isEqual(oldStatus, status)) {\n      this.log.debug('Override status %o => %o', oldStatus, status);\n    }\n\n    return status;\n  }\n\n  getStatus(code: string) {\n    return this.getOverridedStatus(code) || super.getStatus(code);\n  }\n\n\n  async sendCommands(commands: TuyaDeviceStatus[], debounce?: boolean) {\n\n    // convert to original commands\n    for (const command of commands) {\n      const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, command.code);\n      if (!schemaConfig) {\n        continue;\n      }\n\n      const oldCommand = { code: schemaConfig.code, value: command.value } as TuyaDeviceStatus;\n      if (schemaConfig.onSet) {\n        oldCommand.value = this.eval(schemaConfig.onSet, this.device, command.value);\n      }\n\n      if (!isEqual(oldCommand, command)) {\n        this.log.debug('Override command %o => %o', command, oldCommand);\n        command.code = oldCommand.code;\n        command.value = oldCommand.value;\n      }\n    }\n\n    await super.sendCommands(commands, debounce);\n  }\n}\n"
  },
  {
    "path": "src/accessory/CameraAccessory.ts",
    "content": "import { TuyaDeviceStatus } from '../device/TuyaDevice';\nimport { TuyaStreamingDelegate } from '../util/TuyaStreamDelegate';\nimport BaseAccessory from './BaseAccessory';\nimport { configureLight } from './characteristic/Light';\nimport { configureOn } from './characteristic/On';\nimport { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent';\n\nconst SCHEMA_CODE = {\n  MOTION_ON: ['motion_switch'],\n  MOTION_DETECT: ['movement_detect_pic'],\n  // Indicates that this is possibly a doorbell\n  DOORBELL: ['doorbell_ring_exist'],\n  // Notifies when a doorbell ring occurs.\n  DOORBELL_RING: ['doorbell_pic'],\n  // Notifies when a doorbell ring occurs.\n  ALARM_MESSAGE: ['alarm_message'],\n  LIGHT_ON: ['floodlight_switch'],\n  LIGHT_BRIGHT: ['floodlight_lightness'],\n};\n\nexport default class CameraAccessory extends BaseAccessory {\n\n  private stream: TuyaStreamingDelegate | undefined;\n\n  requiredSchema() {\n    return [];\n  }\n\n  configureServices() {\n    this.configureDoorbell();\n    this.configureCamera();\n    this.configureMotion();\n    this.configureFloodLight();\n  }\n\n  configureMotion() {\n    const onSchema = this.getSchema(...SCHEMA_CODE.MOTION_ON);\n    if (onSchema) {\n      const onService = this.accessory.getService(onSchema.code)\n        || this.accessory.addService(this.Service.Switch, onSchema.code, onSchema.code);\n\n      configureOn(this, onService, onSchema);\n    }\n\n    this.getMotionService().setCharacteristic(this.Characteristic.MotionDetected, false);\n  }\n\n  configureDoorbell() {\n    // Check to see if it is indeed a doorbell.\n    if (!this.getSchema(...SCHEMA_CODE.DOORBELL)) {\n      return;\n    }\n\n    const schema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING, ...SCHEMA_CODE.ALARM_MESSAGE);\n    if (!schema) {\n      return;\n    }\n\n    configureProgrammableSwitchEvent(this, this.getDoorbellService(), schema);\n  }\n\n  configureCamera() {\n    if (this.stream !== undefined) {\n      return;\n    }\n\n    if (this.device.isVirtualDevice()) {\n      return;\n    }\n\n    this.stream = new TuyaStreamingDelegate(this);\n    this.accessory.configureController(this.stream.controller);\n  }\n\n  configureFloodLight() {\n    if (!this.getSchema(...SCHEMA_CODE.LIGHT_ON)) {\n      return;\n    }\n\n    configureLight(\n      this,\n      this.getLightService(),\n      this.getSchema(...SCHEMA_CODE.LIGHT_ON),\n      this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT),\n      undefined,\n      undefined,\n      undefined,\n    );\n  }\n\n  getLightService() {\n    return this.accessory.getService(this.Service.Lightbulb)\n      || this.accessory.addService(this.Service.Lightbulb, this.accessory.displayName + ' Floodlight');\n  }\n\n  getDoorbellService() {\n    return this.accessory.getService(this.Service.Doorbell)\n      || this.accessory.addService(this.Service.Doorbell);\n  }\n\n  getMotionService() {\n    return this.accessory.getService(this.Service.MotionSensor)\n      || this.accessory.addService(this.Service.MotionSensor, this.accessory.displayName + ' Motion Detect');\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n    super.onDeviceStatusUpdate(status);\n\n    const doorbellRingSchema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING);\n    const alarmMessageSchema = this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE);\n    if (this.getSchema(...SCHEMA_CODE.DOORBELL) && (doorbellRingSchema || alarmMessageSchema)) {\n      const doorbellRingStatus = doorbellRingSchema && status.find(_status => _status.code === doorbellRingSchema.code);\n      const alarmMessageStatus = alarmMessageSchema && status.find(_status => _status.code === alarmMessageSchema.code);\n      if (doorbellRingStatus && (doorbellRingStatus.value as string).length > 1) { // Compared with '1' in order to filter value '$'\n        onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellRingStatus);\n      } else if (alarmMessageStatus && (alarmMessageStatus.value as string).length > 1) {\n        onProgrammableSwitchEvent(this, this.getDoorbellService(), alarmMessageStatus);\n      }\n    }\n\n    const motionSchema = this.getSchema(...SCHEMA_CODE.MOTION_DETECT);\n    if (motionSchema) {\n      const motionStatus = status.find(_status => _status.code === motionSchema.code);\n      motionStatus && this.onMotionDetected(motionStatus);\n    }\n  }\n\n  private timer?: NodeJS.Timeout;\n  onMotionDetected(status: TuyaDeviceStatus) {\n    if (!this.intialized) {\n      return;\n    }\n\n    const data = Buffer.from(status.value as string, 'base64').toString('binary');\n    if (data.length === 0) {\n      return;\n    }\n\n    this.log.info('Motion event:', data);\n    const characteristic = this.getMotionService().getCharacteristic(this.Characteristic.MotionDetected);\n    characteristic.updateValue(true);\n\n    this.timer && clearTimeout(this.timer);\n    this.timer = setTimeout(() => characteristic.updateValue(false), 30 * 1000);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/CarbonDioxideSensorAccessory.ts",
    "content": "import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  CO2_STATUS: ['co2_state'],\n  CO2_LEVEL: ['co2_value'],\n};\n\n\nexport default class CarbonDioxideSensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.CO2_STATUS];\n  }\n\n  configureServices() {\n    this.configureCarbonDioxideDetected();\n    this.configureCarbonDioxideLevel();\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.CarbonDioxideSensor)\n      || this.accessory.addService(this.Service.CarbonDioxideSensor);\n  }\n\n  configureCarbonDioxideDetected() {\n    const schema = this.getSchema(...SCHEMA_CODE.CO2_STATUS);\n    if (!schema) {\n      return;\n    }\n\n    const { CO2_LEVELS_ABNORMAL, CO2_LEVELS_NORMAL } = this.Characteristic.CarbonDioxideDetected;\n    this.mainService().getCharacteristic(this.Characteristic.CarbonDioxideDetected)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value === 'alarm') ? CO2_LEVELS_ABNORMAL : CO2_LEVELS_NORMAL;\n      });\n  }\n\n  configureCarbonDioxideLevel() {\n    const schema = this.getSchema(...SCHEMA_CODE.CO2_LEVEL);\n    if (!schema) {\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property ? property.scale : 0);\n    this.mainService().getCharacteristic(this.Characteristic.CarbonDioxideLevel)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const value = limit(status.value as number / multiple, 0, 100000);\n        return value;\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/CarbonMonoxideSensorAccessory.ts",
    "content": "import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  CO_STATUS: ['co_status', 'co_state'],\n  CO_LEVEL: ['co_value'],\n};\n\nexport default class CarbonMonoxideSensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.CO_STATUS];\n  }\n\n  configureServices() {\n    this.configureCarbonMonoxideDetected();\n    this.configureCarbonMonoxideLevel();\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.CarbonMonoxideSensor)\n      || this.accessory.addService(this.Service.CarbonMonoxideSensor);\n  }\n\n  configureCarbonMonoxideDetected() {\n    const schema = this.getSchema(...SCHEMA_CODE.CO_STATUS);\n    if (!schema) {\n      return;\n    }\n\n    const { CO_LEVELS_ABNORMAL, CO_LEVELS_NORMAL } = this.Characteristic.CarbonMonoxideDetected;\n    this.mainService().getCharacteristic(this.Characteristic.CarbonMonoxideDetected)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value === 'alarm' || status.value === '1') ? CO_LEVELS_ABNORMAL : CO_LEVELS_NORMAL;\n      });\n  }\n\n  configureCarbonMonoxideLevel() {\n    const schema = this.getSchema(...SCHEMA_CODE.CO_LEVEL);\n    if (!schema) {\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property ? property.scale : 0);\n    this.mainService().getCharacteristic(this.Characteristic.CarbonMonoxideLevel)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const value = limit(status.value as number / multiple, 0, 100);\n        return value;\n      });\n  }\n}\n"
  },
  {
    "path": "src/accessory/CatToiletAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureName } from './characteristic/Name';\nimport { configureOn } from './characteristic/On';\n\nconst SCHEMA_CODE = {\n  SWITCH: ['switch'],\n  AUTO_CLEAN: ['auto_clean'],\n  MANUAL_CLEAN: ['manual_clean'],\n  DEODORIZATION: ['deodorization'],\n  UV: ['uv'],\n  LIGHT: ['light'],\n  STATUS: ['status'],\n  CAT_WEIGHT: ['cat_weight'],\n  EXCRETION_TIMES: ['excretion_times_day'],\n  EXCRETION_TIME: ['excretion_time_day'],\n  NOTIFICATION: ['notification'],\n  FAULT: ['fault'],\n};\n\nexport default class CatToiletAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.SWITCH];\n  }\n\n  configureServices() {\n    // Main power switch\n    configureOn(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWITCH));\n    configureName(this, this.mainService(), this.device.name);\n\n    // Additional switches\n    this.configureSwitch(SCHEMA_CODE.AUTO_CLEAN, 'Auto Clean');\n    this.configureSwitch(SCHEMA_CODE.MANUAL_CLEAN, 'Manual Clean');\n    this.configureSwitch(SCHEMA_CODE.DEODORIZATION, 'Deodorization');\n    this.configureSwitch(SCHEMA_CODE.UV, 'UV Sterilization');\n\n    // Mood light as Lightbulb\n    this.configureLight();\n\n    // Occupancy sensor for active status\n    this.configureOccupancySensor();\n\n    // Filter maintenance for garbage box full\n    this.configureFilterMaintenance();\n\n    // Fault handling\n    this.configureFault();\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.Switch)\n      || this.accessory.addService(this.Service.Switch, this.device.name, 'switch');\n  }\n\n  configureSwitch(schemaCodes: string[], name: string) {\n    const schema = this.getSchema(...schemaCodes);\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.Service.Switch, name, schema.code);\n\n    configureName(this, service, name);\n    configureOn(this, service, schema);\n  }\n\n  configureLight() {\n    const schema = this.getSchema(...SCHEMA_CODE.LIGHT);\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.Service.Lightbulb, 'Mood Light', schema.code);\n\n    configureName(this, service, 'Mood Light');\n    service.getCharacteristic(this.Characteristic.On)\n      .onGet(() => {\n        this.checkOnlineStatus();\n        const status = this.getStatus(schema.code)!;\n        return status.value as boolean;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: schema.code,\n          value: value as boolean,\n        }], true);\n      });\n  }\n\n  configureOccupancySensor() {\n    const schema = this.getSchema(...SCHEMA_CODE.STATUS);\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(this.Service.OccupancySensor)\n      || this.accessory.addService(this.Service.OccupancySensor, 'Status', 'status');\n\n    configureName(this, service, 'Status');\n\n    const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = this.Characteristic.OccupancyDetected;\n    service.getCharacteristic(this.Characteristic.OccupancyDetected)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const activeStates = ['cleaning', 'uv', 'deodorization'];\n        return activeStates.includes(status.value as string)\n          ? OCCUPANCY_DETECTED\n          : OCCUPANCY_NOT_DETECTED;\n      });\n  }\n\n  configureFilterMaintenance() {\n    const schema = this.getSchema(...SCHEMA_CODE.NOTIFICATION);\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(this.Service.FilterMaintenance)\n      || this.accessory.addService(this.Service.FilterMaintenance, 'Waste Box', 'notification');\n\n    configureName(this, service, 'Waste Box');\n\n    const { CHANGE_FILTER, FILTER_OK } = this.Characteristic.FilterChangeIndication;\n    service.getCharacteristic(this.Characteristic.FilterChangeIndication)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        // Bit 0 = garbage_box_full\n        const value = status.value as number;\n        return (value & 1) ? CHANGE_FILTER : FILTER_OK;\n      });\n  }\n\n  configureFault() {\n    const schema = this.getSchema(...SCHEMA_CODE.FAULT);\n    if (!schema) {\n      return;\n    }\n\n    // Add fault status to main service\n    this.mainService().getCharacteristic(this.Characteristic.StatusFault)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const { GENERAL_FAULT, NO_FAULT } = this.Characteristic.StatusFault;\n        return (status.value as number) > 0 ? GENERAL_FAULT : NO_FAULT;\n      });\n  }\n}\n"
  },
  {
    "path": "src/accessory/ContactSensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  CONTACT_STATE: ['doorcontact_state', 'switch'],\n};\n\nexport default class ContaceSensor extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.CONTACT_STATE];\n  }\n\n  configureServices() {\n    const schema = this.getSchema(...SCHEMA_CODE.CONTACT_STATE);\n    if (!schema) {\n      return;\n    }\n    const service = this.accessory.getService(this.Service.ContactSensor)\n    || this.accessory.addService(this.Service.ContactSensor);\n\n    const { CONTACT_NOT_DETECTED, CONTACT_DETECTED } = this.Characteristic.ContactSensorState;\n    service.getCharacteristic(this.Characteristic.ContactSensorState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value ? CONTACT_NOT_DETECTED : CONTACT_DETECTED;\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/DehumidifierAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\nimport { configureRotationSpeedLevel } from './characteristic/RotationSpeed';\nimport { configureSwingMode } from './characteristic/SwingMode';\nimport { configureLockPhysicalControls } from './characteristic/LockPhysicalControls';\nimport { configureRelativeHumidityDehumidifierThreshold } from './characteristic/RelativeHumidityDehumidifierThreshold';\n\nconst SCHEMA_CODE = {\n  ACTIVE: ['switch'],\n  CURRENT_HUMIDITY: ['humidity_indoor'],\n  TARGET_HUMIDITY: ['dehumidify_set_value'],\n  CURRENT_TEMP: ['temp_indoor'],\n  SPEED_LEVEL: ['fan_speed_enum'],\n  SWING: ['swing'],\n  LOCK: ['child_lock'],\n};\n\nexport default class DehumidifierAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE, SCHEMA_CODE.CURRENT_HUMIDITY];\n  }\n\n  configureServices() {\n    // Required Characteristics\n    configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureCurrentRelativeHumidity(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n\n    // Optional Characteristics\n    configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK));\n    configureRelativeHumidityDehumidifierThreshold(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY));\n    configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPEED_LEVEL));\n    configureSwingMode(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWING));\n\n    // Other\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.HumidifierDehumidifier)\n      || this.accessory.addService(this.Service.HumidifierDehumidifier);\n  }\n\n\n  configureCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.ACTIVE);\n    if (!schema) {\n      this.log.warn('CurrentHumidifierDehumidifierState not supported.');\n      return;\n    }\n\n    const { INACTIVE, DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;\n\n    this.mainService().getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code);\n        return (status?.value as boolean) ? DEHUMIDIFYING : INACTIVE;\n      });\n  }\n\n  configureTargetState() {\n    const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState;\n    const validValues = [DEHUMIDIFIER];\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState)\n      .onGet(() => {\n        return DEHUMIDIFIER;\n      }).setProps({ validValues });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/DiffuserAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureLight } from './characteristic/Light';\nimport { configureOn } from './characteristic/On';\nimport { configureRotationSpeedLevel } from './characteristic/RotationSpeed';\n\nconst SCHEMA_CODE = {\n  ON: ['switch'],\n  SPRAY_ON: ['switch_spray'],\n  SPRAY_MODE: ['mode'],\n  SPRAY_LEVEL: ['level'],\n  LIGHT_ON: ['switch_led'],\n  LIGHT_MODE: ['work_mode'],\n  LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'],\n  LIGHT_COLOR: ['colour_data', 'colour_data_hsv'],\n  SOUND_ON: ['switch_sound'],\n};\n\nexport default class DiffuserAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.SPRAY_ON];\n  }\n\n  configureServices() {\n    // Main Switch\n    configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.ON));\n\n    this.configureDiffuser();\n\n    configureLight(\n      this,\n      undefined,\n      this.getSchema(...SCHEMA_CODE.LIGHT_ON),\n      this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT),\n      undefined,\n      this.getSchema(...SCHEMA_CODE.LIGHT_COLOR),\n      this.getSchema(...SCHEMA_CODE.LIGHT_MODE),\n    );\n\n    configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.SOUND_ON)); // Sound Switch\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.AirPurifier)\n      || this.accessory.addService(this.Service.AirPurifier);\n  }\n\n  configureDiffuser() {\n    const sprayOnSchema = this.getSchema(...SCHEMA_CODE.SPRAY_ON)!;\n\n    // Required Characteristics\n    configureActive(this, this.mainService(), sprayOnSchema);\n\n    const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState)\n      .onGet(() => {\n        const status = this.getStatus(sprayOnSchema.code)!;\n        return (status.value as boolean) ? PURIFYING_AIR : INACTIVE;\n      });\n\n    // const { MANUAL } = this.Characteristic.TargetAirPurifierState;\n    // this.mainService().getCharacteristic(this.Characteristic.TargetAirPurifierState)\n    //   .setProps({ validValues: [MANUAL] });\n\n\n    // Optional Characteristics\n    configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPRAY_LEVEL));\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/DimmerAccessory.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice';\nimport { remap, limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\nimport { configureName } from './characteristic/Name';\nimport { configureOn } from './characteristic/On';\n\nconst SCHEMA_CODE = {\n  ON: ['switch', 'switch_led', 'switch_1', 'switch_led_1'],\n  BRIGHTNESS: ['bright_value', 'bright_value_1'],\n};\n\nexport default class DimmerAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS];\n  }\n\n  configureServices() {\n\n    const oldService = this.accessory.getService(this.Service.Lightbulb);\n    if (oldService && oldService?.subtype === undefined) {\n      this.platform.log.warn('Remove old service:', oldService.UUID);\n      this.accessory.removeService(oldService);\n    }\n\n    const schema = this.device.schema.filter((schema) => schema.code.startsWith('bright_value'));\n    for (const _schema of schema) {\n      const suffix = _schema.code.replace('bright_value', '');\n      const name = (schema.length === 1) ? this.device.name : _schema.code;\n\n      const service = this.accessory.getService(_schema.code)\n        || this.accessory.addService(this.Service.Lightbulb, name, _schema.code);\n\n      configureName(this, service, name);\n      configureOn(this, service, this.getSchema('switch' + suffix, 'switch_led' + suffix));\n      this.configureBrightness(service, suffix);\n    }\n  }\n\n\n  configureBrightness(service: Service, suffix: string) {\n    const schema = this.getSchema('bright_value' + suffix);\n    if (!schema) {\n      return;\n    }\n\n    const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const range = max; // not max - min\n    const props = {\n      minValue: 0,\n      maxValue: 100,\n      minStep: 1,\n    };\n\n    const minStatus = this.getStatus('brightness_min' + suffix);\n    const maxStatus = this.getStatus('brightness_max' + suffix);\n    if (minStatus && maxStatus && maxStatus.value > minStatus.value) {\n      const minValue = Math.ceil(remap(minStatus.value as number, 0, range, 0, 100));\n      const maxValue = Math.floor(remap(maxStatus.value as number, 0, range, 0, 100));\n      props.minValue = Math.max(props.minValue, minValue);\n      props.maxValue = Math.min(props.maxValue, maxValue);\n    }\n    this.log.debug('Set props for Brightness:', props);\n\n    service.getCharacteristic(this.Characteristic.Brightness)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        let value = status.value as number;\n        value = remap(value, 0, range, 0, 100);\n        value = Math.round(value);\n        value = limit(value, props.minValue, props.maxValue);\n        return value;\n      })\n      .onSet(async value => {\n        this.log.debug(`Characteristic.Brightness set to: ${value}`);\n        let brightValue = value as number;\n        brightValue = remap(brightValue, 0, 100, 0, range);\n        brightValue = Math.round(brightValue);\n        brightValue = limit(brightValue, min, max);\n        await this.sendCommands([{ code: schema.code, value: brightValue }], true);\n      })\n      .setProps(props);\n\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n\n    // brightness range updated\n    if (status.length !== this.device.status.length) {\n      for (const _status of status) {\n        if (!_status.code.startsWith('brightness_min')\n          && !_status.code.startsWith('brightness_max')) {\n          continue;\n        }\n\n        this.platform.log.warn('Brightness range updated, please restart homebridge to take effect.');\n        // TODO updating props\n        // this.platform.log.debug('Brightness range updated, resetting props...');\n        // this.configure();\n        break;\n      }\n    }\n\n    super.onDeviceStatusUpdate(status);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/DoorbellAccessory.ts",
    "content": "import { TuyaDeviceStatus } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent';\n\nconst SCHEMA_CODE = {\n  // ALARM_MESSAGE: ['alarm_message'],\n  // ALARM_SWITCH: ['alarm_propel_switch'],\n  // VOLUME: ['doorbell_volume_value'],\n  DOORBELL_CALL: ['doorbell_call'],\n};\n\nexport default class DoorbellAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.DOORBELL_CALL];\n  }\n\n  configureServices() {\n    this.log.warn('HomeKit Doorbell service does not work without camera anymore.');\n    this.log.warn('Downgrade to StatelessProgrammableSwitch. \"Mute\" and \"Volume\" not available.');\n    configureProgrammableSwitchEvent(this, this.getDoorbellService(), this.getSchema(...SCHEMA_CODE.DOORBELL_CALL));\n    // this.configureMute();\n    // this.configureVolume();\n  }\n\n  /*\n  configureMute() {\n    const schema = this.getSchema(...SCHEMA_CODE.ALARM_SWITCH);\n    if (!schema) {\n      return;\n    }\n\n    this.getDoorbellService().getCharacteristic(this.Characteristic.Mute)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const value = !(status.value as boolean);\n        return value;\n      })\n      .onSet(async value => {\n        const mute = !(value as boolean);\n        await this.sendCommands([{ code: schema.code, value: mute }], true);\n      });\n  }\n\n  configureVolume() {\n    const schema = this.getSchema(...SCHEMA_CODE.VOLUME);\n    if (!schema) {\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property.scale);\n    const props = {\n      minValue: property.min / multiple,\n      maxValue: property.max / multiple,\n      minStep: Math.max(1, property.step / multiple),\n    };\n    this.getDoorbellService().getCharacteristic(this.Characteristic.Volume)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const value = status.value as number / multiple;\n        return value;\n      })\n      .onSet(async value => {\n        const volume = (value as number) * multiple;\n        await this.sendCommands([{ code: schema.code, value: volume }], true);\n      })\n      .setProps(props);\n  }\n\n  getDoorbellService() {\n    return this.accessory.getService(this.Service.Doorbell)\n      || this.accessory.addService(this.Service.Doorbell);\n  }\n  */\n\n  getDoorbellService() {\n    return this.accessory.getService(this.Service.StatelessProgrammableSwitch)\n      || this.accessory.addService(this.Service.StatelessProgrammableSwitch);\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n    super.onDeviceStatusUpdate(status);\n\n    const doorbellCallSchema = this.getSchema(...SCHEMA_CODE.DOORBELL_CALL);\n    if (doorbellCallSchema) {\n      const doorbellCallStatus = status.find(_status => _status.code === doorbellCallSchema.code);\n      doorbellCallStatus && onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellCallStatus);\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/ExtractionHoodAccessory.ts",
    "content": "import { TuyaDeviceSchemaType } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureLockPhysicalControls } from './characteristic/LockPhysicalControls';\nimport { configureRotationSpeed, configureRotationSpeedLevel } from './characteristic/RotationSpeed';\nimport {configureLight} from './characteristic/Light';\nimport {configureOn} from './characteristic/On';\n\nconst SCHEMA_CODE = {\n  ACTIVE: ['switch'],\n  MODE: ['mode'],\n  LOCK: ['lock'],\n  SPEED: ['speed'],\n  SPEED_LEVEL: ['fan_speed_enum', 'speed'],\n  AIR_QUALITY: ['air_quality', 'pm25'],\n  PM2_5: ['pm25'],\n  VOC: ['tvoc'],\n  LIGHT_ON: ['light', 'switch_led'],\n  LIGHT_MODE: ['work_mode'],\n  LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'],\n  LIGHT_TEMP: ['temp_value', 'temp_value_v2'],\n  LIGHT_COLOR: ['colour_data'],\n};\n\nexport default class ExtractionHoodAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE];\n  }\n\n  configureServices() {\n    configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK));\n    if (this.getFanSpeedSchema()) {\n      configureRotationSpeed(this, this.mainService(), this.getFanSpeedSchema());\n    } else if (this.getFanSpeedLevelSchema()) {\n      configureRotationSpeedLevel(this, this.mainService(), this.getFanSpeedLevelSchema());\n    }\n\n    // Light\n    if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) {\n      if (this.lightServiceType() === this.Service.Lightbulb) {\n        configureLight(\n          this,\n          this.lightService(),\n          this.getSchema(...SCHEMA_CODE.LIGHT_ON),\n          this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT),\n          this.getSchema(...SCHEMA_CODE.LIGHT_TEMP),\n          this.getSchema(...SCHEMA_CODE.LIGHT_COLOR),\n          this.getSchema(...SCHEMA_CODE.LIGHT_MODE),\n        );\n      } else if (this.lightServiceType() === this.Service.Switch) {\n        configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.LIGHT_ON));\n        const unusedService = this.accessory.getService(this.Service.Lightbulb);\n        unusedService && this.accessory.removeService(unusedService);\n      }\n    }\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.AirPurifier)\n      || this.accessory.addService(this.Service.AirPurifier);\n  }\n\n  getFanSpeedSchema() {\n    const schema = this.getSchema(...SCHEMA_CODE.SPEED);\n    if (schema && schema.type === TuyaDeviceSchemaType.Integer) {\n      return schema;\n    }\n    return undefined;\n  }\n\n  getFanSpeedLevelSchema() {\n    const schema = this.getSchema(...SCHEMA_CODE.SPEED_LEVEL);\n    if (schema && schema.type === TuyaDeviceSchemaType.Enum) {\n      return schema;\n    }\n    return undefined;\n  }\n\n\n  configureCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.ACTIVE);\n    if (!schema) {\n      return;\n    }\n\n    const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value as boolean ? PURIFYING_AIR : INACTIVE;\n      });\n  }\n\n  configureTargetState() {\n    const schema = this.getSchema(...SCHEMA_CODE.MODE);\n    if (!schema) {\n      return;\n    }\n\n    const { MANUAL, AUTO } = this.Characteristic.TargetAirPurifierState;\n    this.mainService().getCharacteristic(this.Characteristic.TargetAirPurifierState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value === 'auto') ? AUTO : MANUAL;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: schema.code,\n          value: (value === AUTO) ? 'auto' : 'manual',\n        }], true);\n      });\n  }\n\n  lightServiceType() {\n    if (this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT)\n        || this.getSchema(...SCHEMA_CODE.LIGHT_TEMP)\n        || this.getSchema(...SCHEMA_CODE.LIGHT_COLOR)\n        || this.getSchema(...SCHEMA_CODE.LIGHT_MODE)) {\n      return this.Service.Lightbulb;\n    }\n    return this.Service.Switch;\n  }\n\n  lightService() {\n    return this.accessory.getService(this.Service.Lightbulb)\n        || this.accessory.addService(this.Service.Lightbulb);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/FanAccessory.ts",
    "content": "import { TuyaDeviceSchemaType } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureLight } from './characteristic/Light';\nimport { configureLockPhysicalControls } from './characteristic/LockPhysicalControls';\nimport { configureOn } from './characteristic/On';\nimport { configureRotationSpeed, configureRotationSpeedLevel, configureRotationSpeedOn } from './characteristic/RotationSpeed';\nimport { configureSwingMode } from './characteristic/SwingMode';\n\nconst SCHEMA_CODE = {\n  FAN_ON: ['switch_fan', 'fan_switch', 'switch'],\n  FAN_DIRECTION: ['fan_direction'],\n  FAN_SPEED: ['fan_speed', 'fan_speed_percent'],\n  FAN_SPEED_LEVEL: ['fan_speed_enum', 'fan_speed'],\n  FAN_LOCK: ['child_lock'],\n  FAN_SWING: ['switch_horizontal', 'switch_vertical'],\n  LIGHT_ON: ['light', 'switch_led'],\n  LIGHT_MODE: ['work_mode'],\n  LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'],\n  LIGHT_TEMP: ['temp_value', 'temp_value_v2'],\n  LIGHT_COLOR: ['colour_data'],\n};\n\nexport default class FanAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.FAN_ON];\n  }\n\n  configureServices() {\n\n    if (this.fanServiceType() === this.Service.Fan) {\n      const unusedService = this.accessory.getService(this.Service.Fanv2);\n      unusedService && this.accessory.removeService(unusedService);\n\n      configureOn(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON));\n    } else if (this.fanServiceType() === this.Service.Fanv2) {\n      const unusedService = this.accessory.getService(this.Service.Fan);\n      unusedService && this.accessory.removeService(unusedService);\n\n      configureActive(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON));\n      configureLockPhysicalControls(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_LOCK));\n      configureSwingMode(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_SWING));\n    }\n\n    // Common Characteristics\n    if (this.getFanSpeedSchema()) {\n      configureRotationSpeed(this, this.fanService(), this.getFanSpeedSchema());\n    } else if (this.getFanSpeedLevelSchema()) {\n      configureRotationSpeedLevel(this, this.fanService(), this.getFanSpeedLevelSchema());\n    } else {\n      configureRotationSpeedOn(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON));\n    }\n\n    this.configureRotationDirection();\n\n    // Dual-light: two independent light channels (warm + white)\n    const warmOn = this.getSchema('light');\n    const warmBright = this.getSchema('bright_value');\n    const coldOn = this.getSchema('switch_led');\n    const coldBright = this.getSchema('bright_value_1');\n\n    if (warmOn && warmBright && coldOn && coldBright) {\n      const warmService = this.accessory.getService('Warm Light')\n        || this.accessory.addService(this.Service.Lightbulb, 'Warm Light', 'warm_light');\n      configureLight(this, warmService, warmOn, warmBright);\n\n      const whiteService = this.accessory.getService('White Light')\n        || this.accessory.addService(this.Service.Lightbulb, 'White Light', 'white_light');\n      configureLight(this, whiteService, coldOn, coldBright);\n    } else if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) {\n      if (this.lightServiceType() === this.Service.Lightbulb) {\n        configureLight(\n          this,\n          this.lightService(),\n          this.getSchema(...SCHEMA_CODE.LIGHT_ON),\n          this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT),\n          this.getSchema(...SCHEMA_CODE.LIGHT_TEMP),\n          this.getSchema(...SCHEMA_CODE.LIGHT_COLOR),\n          this.getSchema(...SCHEMA_CODE.LIGHT_MODE),\n        );\n      } else if (this.lightServiceType() === this.Service.Switch) {\n        configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.LIGHT_ON));\n        const unusedService = this.accessory.getService(this.Service.Lightbulb);\n        unusedService && this.accessory.removeService(unusedService);\n      }\n    }\n  }\n\n  fanServiceType() {\n    if (this.getSchema(...SCHEMA_CODE.FAN_LOCK)\n      || this.getSchema(...SCHEMA_CODE.FAN_SWING)) {\n      return this.Service.Fanv2;\n    }\n    return this.Service.Fan;\n  }\n\n  fanService() {\n    const serviceType = this.fanServiceType();\n    return this.accessory.getService(serviceType)\n      || this.accessory.addService(serviceType);\n  }\n\n  lightServiceType() {\n    if (this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT)\n      || this.getSchema(...SCHEMA_CODE.LIGHT_TEMP)\n      || this.getSchema(...SCHEMA_CODE.LIGHT_COLOR)\n      || this.getSchema(...SCHEMA_CODE.LIGHT_MODE)) {\n      return this.Service.Lightbulb;\n    }\n    return this.Service.Switch;\n  }\n\n  lightService() {\n    return this.accessory.getService(this.Service.Lightbulb)\n    || this.accessory.addService(this.Service.Lightbulb);\n  }\n\n\n  getFanSpeedSchema() {\n    const schema = this.getSchema(...SCHEMA_CODE.FAN_SPEED);\n    if (schema && schema.type === TuyaDeviceSchemaType.Integer) {\n      return schema;\n    }\n    return undefined;\n  }\n\n  getFanSpeedLevelSchema() {\n    const schema = this.getSchema(...SCHEMA_CODE.FAN_SPEED_LEVEL);\n    if (schema && schema.type === TuyaDeviceSchemaType.Enum) {\n      return schema;\n    }\n    return undefined;\n  }\n\n  configureRotationDirection() {\n    const schema = this.getSchema(...SCHEMA_CODE.FAN_DIRECTION);\n    if (!schema) {\n      return;\n    }\n\n    const { CLOCKWISE, COUNTER_CLOCKWISE } = this.Characteristic.RotationDirection;\n    this.fanService().getCharacteristic(this.Characteristic.RotationDirection)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value !== 'reverse') ? CLOCKWISE : COUNTER_CLOCKWISE;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{ code: schema.code, value: (value === CLOCKWISE) ? 'forward' : 'reverse' }]);\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/GarageDoorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  CURRENT_DOOR_STATE: ['doorcontact_state'],\n  TARGET_DOOR_STATE: ['switch_1'],\n};\n\nexport default class GarageDoorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.TARGET_DOOR_STATE];\n  }\n\n  configureServices() {\n\n    this.configureCurrentDoorState();\n    this.configureTargetDoorState();\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.GarageDoorOpener)\n      || this.accessory.addService(this.Service.GarageDoorOpener);\n  }\n\n  configureCurrentDoorState() {\n    const { OPEN, CLOSED, OPENING, CLOSING, STOPPED } = this.Characteristic.CurrentDoorState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentDoorState)\n      .onGet(() => {\n        const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_DOOR_STATE);\n        const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_DOOR_STATE);\n        if (!currentSchema || !targetSchema) {\n          return STOPPED;\n        }\n\n        const currentStatus = this.getStatus(currentSchema.code)!;\n        const targetStatus = this.getStatus(targetSchema.code)!;\n        if (currentStatus.value === true && targetStatus.value === true) {\n          return OPEN;\n        } else if (currentStatus.value === false && targetStatus.value === false) {\n          return CLOSED;\n        } else if (currentStatus.value === false && targetStatus.value === true) {\n          return OPENING;\n        } else if (currentStatus.value === true && targetStatus.value === false) {\n          return CLOSING;\n        }\n\n        return STOPPED;\n      });\n  }\n\n  configureTargetDoorState() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_DOOR_STATE);\n    if (!schema) {\n      return;\n    }\n\n    const { OPEN, CLOSED } = this.Characteristic.TargetDoorState;\n    this.mainService().getCharacteristic(this.Characteristic.TargetDoorState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value as boolean ? OPEN : CLOSED;\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: schema.code,\n          value: (value === OPEN) ? true : false,\n        }]);\n      });\n  }\n}\n"
  },
  {
    "path": "src/accessory/HeaterAccessory.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureLockPhysicalControls } from './characteristic/LockPhysicalControls';\nimport { configureSwingMode } from './characteristic/SwingMode';\nimport { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits';\n\nconst SCHEMA_CODE = {\n  ACTIVE: ['switch'],\n  WORK_STATE: ['work_state'],\n  CURRENT_TEMP: ['temp_current'],\n  TARGET_TEMP: ['temp_set'],\n  LOCK: ['lock'],\n  SWING: ['shake'],\n  TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'],\n};\n\nexport default class HeaterAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE];\n  }\n\n  configureServices() {\n    configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK));\n    configureSwingMode(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWING));\n    this.configureHeatingThreshouldTemp();\n    configureTempDisplayUnits(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT));\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.HeaterCooler)\n      || this.accessory.addService(this.Service.HeaterCooler);\n  }\n\n  configureCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.WORK_STATE);\n    const { INACTIVE, IDLE, HEATING } = this.Characteristic.CurrentHeaterCoolerState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentHeaterCoolerState)\n      .onGet(() => {\n        if (!schema) {\n          return IDLE;\n        }\n        const status = this.getStatus(schema.code)!;\n        if (status.value === 'heating') {\n          return HEATING;\n        } else if (status.value === 'warming') {\n          return IDLE;\n        }\n\n        return INACTIVE;\n      });\n  }\n\n  configureTargetState() {\n    const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;\n    const validValues = [ AUTO ];\n    this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)\n      .onGet(() => {\n        return AUTO;\n      })\n      .onSet(async value => {\n        // TODO\n      })\n      .setProps({ validValues });\n  }\n\n  configureHeatingThreshouldTemp() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);\n    if (!schema) {\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = property ? Math.pow(10, property.scale) : 1;\n    const props = {\n      minValue: property.min / multiple,\n      maxValue: property.max / multiple,\n      minStep: Math.max(0.1, property.step / multiple),\n    };\n    this.log.debug('Set props for HeatingThresholdTemperature:', props);\n\n    this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const temp = status.value as number / multiple;\n        return limit(temp, props.minValue, props.maxValue);\n      })\n      .onSet(async value => {\n        await this.sendCommands([{ code: schema.code, value: (value as number) * multiple}]);\n      })\n      .setProps(props);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/HumanPresenceSensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureOccupancyDetected } from './characteristic/OccupancyDetected';\n\nconst SCHEMA_CODE = {\n  PRESENCE: ['presence_state'],\n};\n\nexport default class HumanPresenceSensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.PRESENCE];\n  }\n\n  configureServices() {\n    configureOccupancyDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PRESENCE));\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/HumidifierAccessory.ts",
    "content": "import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice';\nimport { limit, remap } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\nimport { configureLight } from './characteristic/Light';\n\nconst SCHEMA_CODE = {\n  ACTIVE: ['switch'],\n  CURRENT_HUMIDITY: ['humidity_current'],\n  TARGET_HUMIDITY: ['humidity_set'],\n  CURRENT_TEMP: ['temp_current'],\n  LIGHT_ON: ['switch_led'],\n  LIGHT_MODE: ['work_mode'],\n  LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'],\n  LIGHT_COLOR: ['colour_data', 'colour_data_hsv'],\n};\n\nexport default class HumidifierAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE];\n  }\n\n  configureServices() {\n    // Required Characteristics\n    configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureCurrentRelativeHumidity(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n\n    // Optional Characteristics\n    this.configureRelativeHumidityHumidifierThreshold();\n    this.configureRotationSpeed();\n\n    // Other\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureLight(\n      this,\n      undefined,\n      this.getSchema(...SCHEMA_CODE.LIGHT_ON),\n      this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT),\n      undefined,\n      this.getSchema(...SCHEMA_CODE.LIGHT_COLOR),\n      this.getSchema(...SCHEMA_CODE.LIGHT_MODE),\n    );\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.HumidifierDehumidifier)\n      || this.accessory.addService(this.Service.HumidifierDehumidifier);\n  }\n\n\n  configureTargetState() {\n    const { HUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState;\n    const validValues = [HUMIDIFIER];\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState)\n      .onGet(() => {\n        return HUMIDIFIER;\n      }).setProps({ validValues });\n  }\n\n  configureCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.ACTIVE);\n    if (!schema) {\n      this.log.warn('CurrentHumidifierDehumidifierState not supported.');\n      return;\n    }\n\n    const { INACTIVE, HUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;\n\n    this.mainService().getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code);\n        return (status?.value as boolean) ? HUMIDIFYING : INACTIVE;\n      });\n  }\n\n  configureRelativeHumidityHumidifierThreshold() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY);\n    if (!schema) {\n      this.log.warn('Humidity setting is not supported.');\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property ? property.scale : 0);\n    const props = {\n      minValue: 0,\n      maxValue: 100,\n      minStep: Math.max(1, property.step / multiple),\n    };\n    this.log.debug('Set props for RelativeHumidityHumidifierThreshold:', props);\n\n    this.mainService().getCharacteristic(this.Characteristic.RelativeHumidityHumidifierThreshold)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return limit(status.value as number / multiple, 0, 100);\n      })\n      .onSet(async value => {\n        const humidity_set = limit(value as number * multiple, property.min, property.max);\n        await this.sendCommands([{ code: schema.code, value: humidity_set }]);\n        // also set spray mode to humidity\n        await this.setSprayModeToHumidity();\n      }).setProps(props);\n  }\n\n\n  configureRotationSpeed() {\n    const schema = this.getSchema('mode');\n    if (!schema) {\n      this.log.warn('Mode setting is not supported.');\n      return;\n    }\n\n    const unusedService = this.accessory.getService(this.Service.Fan);\n    unusedService && this.accessory.removeService(unusedService);\n\n    this.mainService().getCharacteristic(this.Characteristic.RotationSpeed)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        let v = 3;\n        switch (status.value as string) {\n          case 'small':\n            v = 1;\n            break;\n          case 'middle':\n            v = 2;\n            break;\n        }\n        return remap(v, 0, 3, 0, 100);\n      }).onSet(async value => {\n        value = Math.round(remap(value as number, 0, 100, 0, 3));\n        let mode = 'small';\n        switch (value) {\n          case 2:\n            mode = 'middle';\n            break;\n          case 3:\n            mode = 'large';\n            break;\n        }\n        await this.sendCommands([{ code: schema.code, value: mode }]);\n      });\n  }\n\n  async setSprayModeToHumidity() {\n    const schema = this.getSchema('spray_mode');\n    if (!schema) {\n      this.log.debug('Spray mode not supported.');\n      return;\n    }\n    await this.sendCommands([{ code: schema.code, value: 'humidity' }]);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/IRAirConditionerAccessory.ts",
    "content": "import debounce from 'debounce';\nimport BaseAccessory from './BaseAccessory';\n\nconst POWER_OFF = 0;\nconst POWER_ON = 1;\n\nconst AC_MODE_COOL = 0;\nconst AC_MODE_HEAT = 1;\nconst AC_MODE_AUTO = 2;\nconst AC_MODE_FAN = 3;\nconst AC_MODE_DEHUMIDIFIER = 4;\n\nconst FAN_SPEED_AUTO = 0;\nconst FAN_SPEED_LOW = 1;\n// const FAN_SPEED_MEDIUM = 2;\nconst FAN_SPEED_HIGH = 3;\n\nexport default class IRAirConditionerAccessory extends BaseAccessory {\n\n  configureServices() {\n    this.configureAirConditioner();\n    this.configureDehumidifier();\n    this.configureFan();\n  }\n\n  configureAirConditioner() {\n\n    const service = this.mainService();\n    const { INACTIVE, ACTIVE } = this.Characteristic.Active;\n\n    // Required Characteristics\n    service.getCharacteristic(this.Characteristic.Active)\n      .onGet(() => {\n        return ([AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode()) && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE;\n      })\n      .onSet(async value => {\n        if (value === ACTIVE) {\n          // Turn off Dehumidifier & Fan\n          this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);\n          this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);\n          this.fanService().getCharacteristic(this.Characteristic.Active).value = INACTIVE;\n        }\n\n        if (value === ACTIVE && ![AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode())) {\n          this.setMode(AC_MODE_AUTO);\n        }\n        this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF);\n      });\n\n    const { IDLE } = this.Characteristic.CurrentHeaterCoolerState;\n    service.setCharacteristic(this.Characteristic.CurrentHeaterCoolerState, IDLE);\n\n    this.configureTargetState();\n    this.configureCurrentTemperature();\n\n    // Optional Characteristics\n    this.configureRotationSpeed(service);\n\n    const key_range = this.device.remote_keys?.key_range || [];\n    if (key_range.find(item => item.mode === AC_MODE_HEAT)) {\n      const [minValue, maxValue] = this.getTempRange(AC_MODE_HEAT)!;\n      service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature)\n        .onGet(() => {\n          if (this.getMode() === AC_MODE_AUTO) {\n            return minValue;\n          }\n          return this.getTemp();\n        })\n        .onSet(async value => {\n          if (this.getMode() === AC_MODE_AUTO) {\n            return;\n          }\n          this.setTemp(value);\n        })\n        .setProps({ minValue, maxValue, minStep: 1 });\n    }\n    if (key_range.find(item => item.mode === AC_MODE_COOL)) {\n      const [minValue, maxValue] = this.getTempRange(AC_MODE_COOL)!;\n      service.getCharacteristic(this.Characteristic.CoolingThresholdTemperature)\n        .onGet(this.getTemp.bind(this))\n        .onSet(this.setTemp.bind(this))\n        .setProps({ minValue, maxValue, minStep: 1 });\n    }\n  }\n\n  configureDehumidifier() {\n    if (!this.supportDehumidifier()) {\n      return;\n    }\n\n    const service = this.dehumidifierService();\n    const { INACTIVE, ACTIVE } = this.Characteristic.Active;\n\n    // Required Characteristics\n    service.getCharacteristic(this.Characteristic.Active)\n      .onGet(() => {\n        return (this.getMode() === AC_MODE_DEHUMIDIFIER && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE;\n      })\n      .onSet(async value => {\n        if (value === ACTIVE) {\n          // Turn off AC & Fan\n          this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);\n          this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);\n        }\n\n        this.setMode(AC_MODE_DEHUMIDIFIER);\n        this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF);\n      });\n\n    const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState;\n    service.setCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState, DEHUMIDIFYING);\n\n    const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState;\n    service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState)\n      .updateValue(DEHUMIDIFIER)\n      .setProps({ validValues: [DEHUMIDIFIER] });\n\n    service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)\n      .onGet(() => {\n        const handler = this.getParentAccessory().accessory\n          .getService(this.Service.HumiditySensor)\n          ?.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)['getHandler'];\n        const humidity = handler ? handler() : 0;\n        return humidity;\n      });\n\n    // Optional Characteristics\n    this.configureRotationSpeed(service);\n  }\n\n  configureFan() {\n    if (!this.supportFan()) {\n      return;\n    }\n\n    const service = this.fanService();\n    const { INACTIVE, ACTIVE } = this.Characteristic.Active;\n\n    // Required Characteristics\n    service.getCharacteristic(this.Characteristic.Active)\n      .onGet(() => {\n        return (this.getMode() === AC_MODE_FAN && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE;\n      })\n      .onSet(async value => {\n        if (value === ACTIVE) {\n          // Turn off AC & Dehumidifier\n          this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);\n          this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE);\n        }\n\n        this.setMode(AC_MODE_FAN);\n        this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF);\n      });\n\n    // Optional Characteristics\n    this.configureTargetFanState(service);\n    this.configureRotationSpeed(service);\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.HeaterCooler)\n      || this.accessory.addService(this.Service.HeaterCooler);\n  }\n\n  dehumidifierService() {\n    return this.accessory.getService(this.Service.HumidifierDehumidifier)\n      || this.accessory.addService(this.Service.HumidifierDehumidifier, this.accessory.displayName + ' Dehumidifier');\n  }\n\n  fanService() {\n    return this.accessory.getService(this.Service.Fanv2)\n      || this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan');\n  }\n\n  getPower() {\n    const value = this.getStatus('power')?.value || '0';\n    return (value === true || parseInt(value.toString()) === 1) ? POWER_ON : POWER_OFF;\n  }\n\n  setPower(value) {\n    this.getStatus('power')!.value = value;\n    this.debounceSendACCommands();\n  }\n\n  getMode() {\n    const value = this.getStatus('mode')?.value || '0';\n    return parseInt(value.toString());\n  }\n\n  setMode(value) {\n    this.getStatus('mode')!.value = value;\n    this.debounceSendACCommands();\n  }\n\n  getWind() {\n    const value = this.getStatus('wind')?.value || '0';\n    return parseInt(value.toString());\n  }\n\n  setWind(value) {\n    this.getStatus('wind')!.value = value;\n    this.debounceSendACCommands();\n  }\n\n  getTemp() {\n    const value = this.getStatus('temp')?.value || '0';\n    return parseInt(value.toString());\n  }\n\n  setTemp(value) {\n    this.getStatus('temp')!.value = value;\n    this.debounceSendACCommands();\n  }\n\n  getKeyRangeItem(mode: number) {\n    const key_range = this.device.remote_keys?.key_range || [];\n    return key_range.find(item => item.mode === mode);\n  }\n\n  supportDehumidifier() {\n    return this.getKeyRangeItem(AC_MODE_DEHUMIDIFIER) !== undefined;\n  }\n\n  supportFan() {\n    return this.getKeyRangeItem(AC_MODE_FAN) !== undefined;\n  }\n\n  getTempRange(mode: number) {\n    const keyRangeItem = this.getKeyRangeItem(mode);\n    if (!keyRangeItem || !keyRangeItem.temp_list || keyRangeItem.temp_list.length === 0) {\n      return undefined;\n    }\n\n    const tempList = keyRangeItem.temp_list.map((temp) => temp.temp);\n\n    const min = Math.min(...tempList);\n    const max = Math.max(...tempList);\n    return [min, max];\n  }\n\n  getParentAccessory() {\n    return this.platform.accessoryHandlers.find(accessory => accessory.device.id === this.device.parent_id)!;\n  }\n\n  configureTargetState() {\n    const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState;\n\n    const validValues: number[] = [];\n    const key_range = this.device.remote_keys?.key_range || [];\n    if (key_range.find(item => item.mode === AC_MODE_AUTO)) {\n      validValues.push(AUTO);\n    }\n    if (key_range.find(item => item.mode === AC_MODE_HEAT)) {\n      validValues.push(HEAT);\n    }\n    if (key_range.find(item => item.mode === AC_MODE_COOL)) {\n      validValues.push(COOL);\n    }\n\n    if (validValues.length === 0) {\n      this.log.warn('Invalid mode range for TargetHeaterCoolerState:', key_range);\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState)\n      .onGet(() => ({\n        [AC_MODE_COOL.toString()]: COOL,\n        [AC_MODE_HEAT.toString()]: HEAT,\n        [AC_MODE_AUTO.toString()]: AUTO,\n      }[this.getMode().toString()] || AUTO))\n      .onSet(async value => {\n        this.setMode({\n          [COOL.toString()]: AC_MODE_COOL,\n          [HEAT.toString()]: AC_MODE_HEAT,\n          [AUTO.toString()]: AC_MODE_AUTO,\n        }[value.toString()]);\n      })\n      .setProps({ validValues });\n  }\n\n  configureCurrentTemperature() {\n    this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature)\n      .onGet(() => {\n        const handler = this.getParentAccessory().accessory\n          .getService(this.Service.TemperatureSensor)\n          ?.getCharacteristic(this.Characteristic.CurrentTemperature)['getHandler'];\n        const temp = handler ? handler() : this.getTemp();\n        return temp;\n      });\n  }\n\n  configureTargetFanState(service) {\n    const { MANUAL, AUTO } = this.Characteristic.TargetFanState;\n    service.getCharacteristic(this.Characteristic.TargetFanState)\n      .onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? AUTO : MANUAL)\n      .onSet(async value => {\n        this.setWind((value === AUTO) ? FAN_SPEED_AUTO : FAN_SPEED_LOW);\n      });\n  }\n\n  configureRotationSpeed(service) {\n    service.getCharacteristic(this.Characteristic.RotationSpeed)\n      .onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? FAN_SPEED_HIGH : this.getWind())\n      .onSet(async value => {\n        // if (this.getWind() === FAN_SPEED_AUTO) {\n        //   return;\n        // }\n        if (value !== 0) {\n          this.setWind(value);\n        }\n      })\n      .setProps({ minValue: 0, maxValue: 3, minStep: 1, unit: 'speed' });\n  }\n\n  debounceSendACCommands = debounce(this.sendACCommands, 100);\n\n  async sendACCommands() {\n    const { parent_id, id } = this.device;\n    await this.deviceManager.sendInfraredACCommands(parent_id!, id, this.getPower(), this.getMode(), this.getTemp(), this.getWind());\n  }\n}\n"
  },
  {
    "path": "src/accessory/IRControlHubAccessory.ts",
    "content": "import { TuyaDeviceStatus } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\n\nconst SCHEMA_CODE = {\n  CURRENT_TEMP: ['va_temperature'],\n  CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],\n};\n\nexport default class IRControlHubAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [];\n  }\n\n  configureServices() {\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n  }\n\n  getSubAccessories() {\n    return this.platform.accessoryHandlers.filter(accessory => accessory.device.parent_id === this.device.id);\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n    super.onDeviceStatusUpdate(status);\n\n    // Trigger sub device update temperature & humidity from parent device.\n    for (const subAccessory of this.getSubAccessories()) {\n      await subAccessory.updateAllValues();\n    }\n  }\n}\n"
  },
  {
    "path": "src/accessory/IRGenericAccessory.ts",
    "content": "import { TuyaIRRemoteKeyListItem } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureName } from './characteristic/Name';\n\nexport default class IRGenericAccessory extends BaseAccessory {\n\n  configureServices() {\n    let key_list = this.device.remote_keys?.key_list || [];\n\n    // Max 99 services allowed (one for AccessoryInformation)\n    if (key_list.length > 99) {\n      this.log.warn(`Skipping ${key_list.length - 99} keys for ${this.device.name}, ` +\n        'as we reached the limit of HomeKit (100 services per accessory)');\n    }\n    key_list = key_list.slice(0, 99);\n\n    for (const key of key_list) {\n      this.configureSwitch(key);\n    }\n  }\n\n  configureSwitch(key: TuyaIRRemoteKeyListItem) {\n    const service = this.accessory.getService(key.key)\n      || this.accessory.addService(this.Service.Switch, key.key, key.key);\n\n    configureName(this, service, key.key_name);\n\n    service.getCharacteristic(this.Characteristic.On)\n      .onGet(() => false)\n      .onSet(async value => {\n        if (value === false) {\n          return;\n        }\n\n        this.sendInfraredCommands(key);\n        setTimeout(() => {\n          service.getCharacteristic(this.Characteristic.On).updateValue(false);\n        }, 150);\n\n      });\n  }\n\n  async sendInfraredCommands(key: TuyaIRRemoteKeyListItem) {\n    const { parent_id, id } = this.device;\n    const { category_id, remote_index } = this.device.remote_keys!;\n    if (key.learning_code) {\n      await this.deviceManager.sendInfraredDIYCommands(parent_id!, id, key.learning_code);\n    } else {\n      await this.deviceManager.sendInfraredCommands(parent_id!, id, category_id, remote_index, key.key, key.key_id);\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/LeakSensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  LEAK: ['gas_sensor_status', 'gas_sensor_state', 'ch4_sensor_state', 'watersensor_state'],\n};\n\nexport default class LeakSensor extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.LEAK];\n  }\n\n  configureServices() {\n    const { LEAK_NOT_DETECTED, LEAK_DETECTED } = this.Characteristic.LeakDetected;\n    const service = this.accessory.getService(this.Service.LeakSensor)\n      || this.accessory.addService(this.Service.LeakSensor);\n\n    service.getCharacteristic(this.Characteristic.LeakDetected)\n      .onGet(() => {\n        const gas = this.getStatus('gas_sensor_status')\n          || this.getStatus('gas_sensor_state');\n        const ch4 = this.getStatus('ch4_sensor_state');\n        const water = this.getStatus('watersensor_state');\n\n        if ((gas && (gas.value === 'alarm' || gas.value === '1'))\n          || (ch4 && ch4.value === 'alarm')\n          || (water && water.value === 'alarm')) {\n          return LEAK_DETECTED;\n        } else {\n          return LEAK_NOT_DETECTED;\n        }\n      });\n\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/LightAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureOn } from './characteristic/On';\nimport { configureMotionDetected } from './characteristic/MotionDetected';\nimport { configureLight } from './characteristic/Light';\n\nconst SCHEMA_CODE = {\n  ON: ['switch_led'],\n  BRIGHTNESS: ['bright_value', 'bright_value_v2'],\n  COLOR_TEMP: ['temp_value', 'temp_value_v2'],\n  COLOR: ['colour_data', 'colour_data_v2'],\n  WORK_MODE: ['work_mode'],\n  PIR: ['pir_state'],\n  PIR_ON: ['switch_pir'],\n  POWER_SWITCH: ['switch'],\n};\n\nexport default class LightAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ON];\n  }\n\n  configureServices() {\n\n    const service = this.accessory.getService(this.Service.Lightbulb)\n      || this.accessory.addService(this.Service.Lightbulb);\n\n    configureLight(\n      this,\n      service,\n      this.getSchema(...SCHEMA_CODE.ON),\n      this.getSchema(...SCHEMA_CODE.BRIGHTNESS),\n      this.getSchema(...SCHEMA_CODE.COLOR_TEMP),\n      this.getSchema(...SCHEMA_CODE.COLOR),\n      this.getSchema(...SCHEMA_CODE.WORK_MODE),\n    );\n\n    // PIR\n    configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.PIR_ON));\n    configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR));\n\n    // RGB Power Switch\n    configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.POWER_SWITCH));\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/LightSensorAccessory.ts",
    "content": "import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  BRIGHT_LEVEL: ['bright_value'],\n};\n\nexport default class LightSensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.BRIGHT_LEVEL];\n  }\n\n  configureServices() {\n    const schema = this.getSchema(...SCHEMA_CODE.BRIGHT_LEVEL);\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(this.Service.LightSensor)\n      || this.accessory.addService(this.Service.LightSensor);\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    const multiple = Math.pow(10, property ? property.scale : 0);\n    service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return limit(status.value as number / multiple, 0.0001, 100000);\n      });\n\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/LockAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  LOCK_CURRENT_STATE: ['open_close', 'closed_opened', 'lock_motor_state'],\n  LOCK_TARGET_STATE: ['lock_motor_state'],\n};\n\nexport default class LockAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.LOCK_CURRENT_STATE];\n  }\n\n  configureServices() {\n    this.configureLockCurrentState();\n    this.configureLockTargetState();\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.LockMechanism)\n      || this.accessory.addService(this.Service.LockMechanism);\n  }\n\n  configureLockCurrentState() {\n    const schema = this.getSchema(...SCHEMA_CODE.LOCK_CURRENT_STATE);\n    if (!schema) {\n      return;\n    }\n\n    const { UNSECURED, SECURED } = this.Characteristic.LockCurrentState;\n    this.mainService().getCharacteristic(this.Characteristic.LockCurrentState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value as boolean) ? UNSECURED : SECURED;\n      });\n  }\n\n  configureLockTargetState() {\n    const schema = this.getSchema(...SCHEMA_CODE.LOCK_TARGET_STATE);\n    if (!schema) {\n      return;\n    }\n\n    const { UNSECURED, SECURED } = this.Characteristic.LockTargetState;\n    this.mainService().getCharacteristic(this.Characteristic.LockTargetState)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return (status.value as boolean) ? UNSECURED : SECURED;\n      })\n      .onSet(async value => {\n        const res = await this.deviceManager.getLockTemporaryKey(this.device.id);\n        if (!res.success) {\n          return;\n        }\n        await this.deviceManager.sendLockCommands(this.device.id, res.result.ticket_id, (value === UNSECURED));\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/MotionSensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureMotionDetected } from './characteristic/MotionDetected';\n\nconst SCHEMA_CODE = {\n  PIR: ['pir'],\n};\n\nexport default class MotionSensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.PIR];\n  }\n\n  configureServices() {\n    configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR));\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/OutletAccessory.ts",
    "content": "import SwitchAccessory from './SwitchAccessory';\n\nexport default class OutletAccessory extends SwitchAccessory {\n  mainService() {\n    return this.Service.Outlet;\n  }\n}\n"
  },
  {
    "path": "src/accessory/PetFeederAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { CharacteristicValue } from 'homebridge';\n\nconst SCHEMA_CODE = {\n  ACTIVE: ['switch'],\n  LIGHT: ['light'],\n  QUICK_FEED: ['quick_feed'],\n  SLOW_FEED: ['slow_feed'],\n  MANUAL_FEED: ['manual_feed'],\n  MEAL_PLAN: ['meal_plan'],\n  BATTERY_PERCENTAGE: ['battery_percentage'],\n  FEED_REPORT: ['feed_report'],\n  FEED_STATE: ['feed_state'],\n};\n\nexport default class PetFeederAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ACTIVE];\n  }\n\n  configureServices() {\n    configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));\n    this.configureLight();\n    this.configureQuickFeed();\n    this.configureSlowFeed();\n    this.configureManualFeed();\n    this.configureMealPlan();\n    this.configureBatteryPercentage();\n    this.configureFeedReport();\n    this.configureFeedState();\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.Switch)\n      || this.accessory.addService(this.Service.Switch);\n  }\n\n  configureLight() {\n    const schema = this.getSchema(...SCHEMA_CODE.LIGHT);\n    if (!schema) {\n      this.log.warn('Light is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.On)\n      .onSet(async (value: CharacteristicValue) => {\n        await this.sendCommands([{ code: schema.code, value: value as boolean }]);\n      });\n  }\n\n  configureQuickFeed() {\n    const schema = this.getSchema(...SCHEMA_CODE.QUICK_FEED);\n    if (!schema) {\n      this.log.warn('Quick feed is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.On)\n      .onSet(async (value: CharacteristicValue) => {\n        if (value as boolean) {\n          await this.sendCommands([{ code: schema.code, value: true }]);\n        }\n      });\n  }\n\n  configureSlowFeed() {\n    const schema = this.getSchema(...SCHEMA_CODE.SLOW_FEED);\n    if (!schema) {\n      this.log.warn('Slow feed is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.On)\n      .onSet(async (value: CharacteristicValue) => {\n        if (value as boolean) {\n          await this.sendCommands([{ code: schema.code, value: true }]);\n        }\n      });\n  }\n\n  configureManualFeed() {\n    const schema = this.getSchema(...SCHEMA_CODE.MANUAL_FEED);\n    if (!schema) {\n      this.log.warn('Manual feed is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.On)\n      .onSet(async (value: CharacteristicValue) => {\n        if (value as boolean) {\n          await this.sendCommands([{ code: schema.code, value: 1 }]);\n        }\n      });\n  }\n\n  configureMealPlan() {\n    const schema = this.getSchema(...SCHEMA_CODE.MEAL_PLAN);\n    if (!schema) {\n      this.log.warn('Meal plan is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.On)\n      .onSet(async (value: CharacteristicValue) => {\n        if (value as boolean) {\n          await this.sendCommands([{ code: schema.code, value: value as boolean }]);\n        }\n      });\n  }\n\n  configureBatteryPercentage() {\n    const schema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENTAGE);\n    if (!schema) {\n      this.log.warn('Battery percentage is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.BatteryLevel)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value as number;\n      });\n  }\n\n  configureFeedReport() {\n    const schema = this.getSchema(...SCHEMA_CODE.FEED_REPORT);\n    if (!schema) {\n      this.log.warn('Feed report is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.StatusActive)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value as number;\n      });\n  }\n\n  configureFeedState() {\n    const schema = this.getSchema(...SCHEMA_CODE.FEED_STATE);\n    if (!schema) {\n      this.log.warn('Feed state is not supported.');\n      return;\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.StatusActive)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value === 'feeding';\n      });\n  }\n}\n"
  },
  {
    "path": "src/accessory/SaunaAccessory.ts",
    "content": "import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits';\nimport { configureLight } from './characteristic/Light';\n\n\nconst SCHEMA_CODE = {\n  ON: ['powerswitch'],\n  CURRENT_TEMP: ['currtemp', 'settemp'],\n  TARGET_TEMP: ['settemp'],\n  TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'],\n  LIGHT: ['lightswitch'],\n  LED: ['ledswitch'],\n  // TIMER: ['settime'], // Not currently supppored by homekit\n};\n\nexport default class SaunaAccessory extends BaseAccessory {\n\n\n  requiredSchema() {\n    return [SCHEMA_CODE.CURRENT_TEMP, SCHEMA_CODE.TARGET_TEMP];\n  }\n\n  configureServices() {\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    this.configureTargetTemp();\n    configureTempDisplayUnits(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT));\n    this.configureLight();\n\n\n  }\n\n  mainService() {\n    return this.accessory.getService(this.Service.Thermostat)\n      || this.accessory.addService(this.Service.Thermostat);\n  }\n\n  configureCurrentState() {\n\n    const { OFF, HEAT } = this.Characteristic.CurrentHeatingCoolingState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState)\n      .onGet(() => {\n        const on = this.getStatus('powerswitch');\n        if (on && on.value === false) {\n          return OFF;\n        } else {\n          return HEAT;\n        }\n      });\n  }\n\n\n\n  configureTargetState() {\n    const { OFF, HEAT } = this.Characteristic.TargetHeatingCoolingState;\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetHeatingCoolingState)\n      .onGet(() => {\n        const on = this.getStatus('powerswitch');\n        if (on && on.value === false) {\n          return OFF;\n        } else {\n          return HEAT;\n        }\n      })\n\n      .onSet(async value => {\n        const commands: TuyaDeviceStatus[] = [];\n\n        if (value === OFF) {\n          commands.push({\n            code: 'powerswitch',\n            value: false,\n          });\n        } else if (value === HEAT) {\n          commands.push({\n            code: 'powerswitch',\n            value: true,\n          });\n        }\n\n        if (commands.length !== 0) {\n          await this.sendCommands(commands);\n        }\n      })\n      .setProps({ validValues: [OFF, HEAT] });\n\n  }\n\n\n  configureTargetTemp() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);\n    if (!schema) {\n      this.log.warn('TargetTemperature not supported.');\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    let multiple = Math.pow(10, property.scale);\n    let props = {\n      minValue: Math.max(30, property.min / multiple),\n      maxValue: Math.min(90, property.max / multiple),\n      minStep: Math.max(0.1, property.step / multiple),\n    };\n    if (props.maxValue <= props.minValue) {\n      this.log.warn('Invalid schema: %o, props will be reset to the default value.', schema);\n      multiple = 1;\n      props = { minValue: 30, maxValue: 90, minStep: 1 };\n    }\n    this.log.debug('Set props for TargetTemperature:', props);\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetTemperature)\n\n      .onGet(() => {\n        const status = this.getStatus(schema.code);\n        if (!status || typeof status.value !== 'number') {\n          this.log.debug('No valid settemp available, returning default.');\n          return props.minValue; // or any fallback value like 45\n        }\n        const temp = (status.value as number) / multiple;\n        return limit(temp, props.minValue, props.maxValue);\n      })\n\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: schema.code,\n          value: value as number * multiple,\n        }]);\n      })\n      .setProps(props);\n  }\n\n  configureLight() {\n\n    const lightswitchSchema = this.getSchema('lightswitch');\n    const ledswitchSchema = this.getSchema('ledswitch');\n\n    const light1Service = this.accessory.getService('Sauna Main Light') ||\n        this.accessory.addService(this.Service.Lightbulb, 'Sauna Main Light', 'lightswitch');\n\n    const light2Service = this.accessory.getService('Sauna LED Light') ||\n        this.accessory.addService(this.Service.Lightbulb, 'Sauna LED Light', 'ledswitch');\n\n    if (lightswitchSchema) {\n      configureLight(this, light1Service, lightswitchSchema);\n    }\n\n    if (ledswitchSchema) {\n      configureLight(this, light2Service, ledswitchSchema);\n    }\n  }\n\n\n}\n"
  },
  {
    "path": "src/accessory/SceneAccessory.ts",
    "content": "import { PlatformAccessory } from 'homebridge';\nimport TuyaHomeDeviceManager from '../device/TuyaHomeDeviceManager';\nimport { TuyaPlatform } from '../platform';\nimport BaseAccessory from './BaseAccessory';\n\nexport default class SceneAccessory extends BaseAccessory {\n\n  constructor(platform: TuyaPlatform, accessory: PlatformAccessory) {\n    super(platform, accessory);\n\n    const service = this.accessory.getService(this.Service.Switch)\n      || this.accessory.addService(this.Service.Switch);\n\n    service.getCharacteristic(this.Characteristic.On)\n      .onGet(() => false)\n      .onSet(async value => {\n        if (value === false) {\n          return;\n        }\n        const deviceManager = this.platform.deviceManager as TuyaHomeDeviceManager;\n        const res = await deviceManager.executeScene(this.device.owner_id, this.device.id);\n        setTimeout(() => {\n          service.getCharacteristic(this.Characteristic.On).updateValue(false);\n        }, 150);\n        if (res.success === false) {\n          this.log.warn('ExecuteScene failed. homeId = %s, code = %s, msg = %s', this.device.owner_id, res.code, res.msg);\n          const { HapStatusError, HAPStatus } = this.platform.api.hap;\n          throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n        }\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/SceneSwitchAccessory.ts",
    "content": "import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureName } from './characteristic/Name';\n\nexport default class SceneSwitchAccessory extends BaseAccessory {\n\n  configureServices() {\n    const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean);\n    for (const _schema of schema) {\n      const name = (schema.length === 1) ? this.device.name : _schema.code;\n      this.configureSwitch(_schema, name);\n    }\n  }\n\n  configureSwitch(schema: TuyaDeviceSchema, name: string) {\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.Service.Switch, name, schema.code);\n\n    configureName(this, service, name);\n\n    const suffix = schema.code.replace('switch', '');\n    const modeSchema = this.getSchema('mode' + suffix);\n    service.getCharacteristic(this.Characteristic.On)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value as boolean;\n      })\n      .onSet(async value => {\n        if (modeSchema) {\n          const mode = this.getStatus(modeSchema.code)!;\n          if ((mode.value as string).startsWith('scene')) {\n            await this.sendCommands([{ code: schema.code, value: false }]);\n            return;\n          }\n        }\n\n        await this.sendCommands([{ code: schema.code, value: value as boolean }]);\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/SecuritySystemAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureSecuritySystemCurrentState, configureSecuritySystemTargetState } from './characteristic/SecuritySystemState';\nimport { configureName } from './characteristic/Name';\n\nconst SCHEMA_CODE = {\n  MASTER_MODE: ['master_mode'],\n  SOS_STATE: ['sos_state'],\n};\n\nexport default class SecuritySystemAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.MASTER_MODE, SCHEMA_CODE.SOS_STATE];\n  }\n\n  isNightArm = false;\n\n  configureServices() {\n    const service = this.accessory.getService(this.Service.SecuritySystem)\n      || this.accessory.addService(this.Service.SecuritySystem);\n\n    configureName(this, service, this.device.name);\n\n    configureSecuritySystemCurrentState(this, service, this.getSchema(...SCHEMA_CODE.MASTER_MODE),\n      this.getSchema(...SCHEMA_CODE.SOS_STATE));\n    configureSecuritySystemTargetState(this, service, this.getSchema(...SCHEMA_CODE.MASTER_MODE),\n      this.getSchema(...SCHEMA_CODE.SOS_STATE));\n  }\n}\n"
  },
  {
    "path": "src/accessory/SmokeSensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  SENSOR_STATUS: ['smoke_sensor_status', 'smoke_sensor_state'],\n};\n\nexport default class SmokeSensor extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.SENSOR_STATUS];\n  }\n\n  configureServices() {\n    const schema = this.getSchema(...SCHEMA_CODE.SENSOR_STATUS);\n    if (!schema) {\n      return;\n    }\n\n    const { SMOKE_NOT_DETECTED, SMOKE_DETECTED } = this.Characteristic.SmokeDetected;\n    const service = this.accessory.getService(this.Service.SmokeSensor)\n      || this.accessory.addService(this.Service.SmokeSensor);\n\n    service.getCharacteristic(this.Characteristic.SmokeDetected)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        if ((status.value === 'alarm' || status.value === '1')) {\n          return SMOKE_DETECTED;\n        } else {\n          return SMOKE_NOT_DETECTED;\n        }\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/SwitchAccessory.ts",
    "content": "import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureName } from './characteristic/Name';\nimport { configureOn } from './characteristic/On';\nimport { configureEnergyUsage } from './characteristic/EnergyUsage';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\n\nconst SCHEMA_CODE = {\n  ON: ['switch', 'switch_1'], // switch_2, switch_3, switch_4, ..., switch_usb1, switch_usb2, switch_usb3, ..., switch_backlight\n  CURRENT: ['cur_current'],\n  POWER: ['cur_power'],\n  VOLTAGE: ['cur_voltage'],\n  TOTAL_POWER: ['add_ele'],\n  CURRENT_TEMP: ['va_temperature', 'temp_current'],\n  CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],\n  INCHING: ['switch_inching'],\n};\n\nexport default class SwitchAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ON];\n  }\n\n  configureServices() {\n\n    const oldService = this.accessory.getService(this.mainService());\n    if (oldService && oldService?.subtype === undefined) {\n      this.platform.log.warn('Remove old service:', oldService.UUID);\n      this.accessory.removeService(oldService);\n    }\n\n    const schemata = this.device.schema.filter(\n      (schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean,\n    );\n\n    schemata.forEach((schema) => {\n      const name = (schemata.length === 1) ? this.device.name : schema.code;\n      this.configureSwitch(schema, name);\n    });\n\n\n    // Other\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n    this.configureInching();\n  }\n\n\n  mainService() {\n    return this.Service.Switch;\n  }\n\n  configureSwitch(schema: TuyaDeviceSchema, name: string) {\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.mainService(), name, schema.code);\n\n    configureName(this, service, name);\n    configureOn(this, service, schema);\n\n    if (schema.code === this.getSchema(...SCHEMA_CODE.ON)?.code) {\n      configureEnergyUsage(\n        this.platform.api,\n        this,\n        service,\n        this.getSchema(...SCHEMA_CODE.CURRENT),\n        this.getSchema(...SCHEMA_CODE.POWER),\n        this.getSchema(...SCHEMA_CODE.VOLTAGE),\n        this.getSchema(...SCHEMA_CODE.TOTAL_POWER),\n      );\n    }\n  }\n\n  configureInching() {\n    const schema = this.getSchema(...SCHEMA_CODE.INCHING);\n    if (!schema || schema.type !== TuyaDeviceSchemaType.String) {\n      return;\n    }\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.Service.Switch, schema.code, schema.code);\n\n    configureName(this, service, schema.code);\n    service.getCharacteristic(this.Characteristic.On)\n      .onGet(() => {\n        this.checkOnlineStatus();\n        const status = this.getStatus(schema.code)!;\n        const buffer = Buffer.from(status.value as string, 'base64');\n        return (buffer.length === 3) && (buffer[0] === 1);\n      })\n      .onSet(async value => {\n        const status = this.getStatus(schema.code)!;\n        let buffer = Buffer.from(status.value as string, 'base64');\n        if (buffer.length !== 3) {\n          buffer = Buffer.alloc(3);\n        }\n        buffer[0] = (value as boolean) ? 1 : 0;\n        await this.sendCommands([{\n          code: schema.code,\n          value: buffer.toString('base64'),\n        }], true);\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/TemperatureHumiditySensorAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\n\nconst SCHEMA_CODE = {\n  SENSOR_STATUS: ['va_temperature', 'va_humidity', 'humidity_value'],\n  CURRENT_TEMP: ['va_temperature'],\n  CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'],\n};\n\nexport default class TemperatureHumiditySensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.SENSOR_STATUS];\n  }\n\n  configureServices(): void {\n    configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY));\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/ThermostatAccessory.ts",
    "content": "import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\nimport { configureCurrentTemperature } from './characteristic/CurrentTemperature';\nimport { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits';\n\nconst SCHEMA_CODE = {\n  ON: ['switch'],\n  CURRENT_MODE: ['work_state', 'mode'],\n  TARGET_MODE: ['mode'],\n  CURRENT_TEMP: ['temp_current', 'temp_set'],\n  TARGET_TEMP: ['temp_set'],\n  TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'],\n};\n\nexport default class ThermostatAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.CURRENT_TEMP, SCHEMA_CODE.TARGET_TEMP];\n  }\n\n  configureServices() {\n    this.configureCurrentState();\n    this.configureTargetState();\n    configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP));\n    this.configureTargetTemp();\n    configureTempDisplayUnits(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT));\n  }\n\n\n  mainService() {\n    return this.accessory.getService(this.Service.Thermostat)\n      || this.accessory.addService(this.Service.Thermostat);\n  }\n\n  configureCurrentState() {\n\n    const { OFF, HEAT, COOL } = this.Characteristic.CurrentHeatingCoolingState;\n    this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState)\n      .onGet(() => {\n        const on = this.getStatus('switch');\n        if (on && on.value === false) {\n          return OFF;\n        }\n\n        const schema = this.getSchema(...SCHEMA_CODE.CURRENT_MODE);\n        if (!schema) {\n          // If don't support mode, compare current and target temp.\n          const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP);\n          const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);\n          if (!currentSchema || !targetSchema) {\n            return OFF;\n          }\n          const current = this.getStatus(currentSchema.code)!;\n          const target = this.getStatus(targetSchema.code)!;\n          if (target.value > current.value) {\n            return HEAT;\n          } else if (target.value < current.value) {\n            return COOL;\n          } else {\n            return OFF;\n          }\n        }\n\n        const status = this.getStatus(schema.code)!;\n        if (status.value === 'hot' || status.value === 'opened' || status.value === 'heating') {\n          return HEAT;\n        } else if (\n          status.value === 'cold' ||\n          status.value === 'eco' ||\n          status.value === 'idle' ||\n          status.value === 'window_opened'\n        ) {\n          return COOL;\n        }\n        // Don't know how to display unsupported work mode.\n        return OFF;\n      });\n\n  }\n\n  configureTargetState() {\n    const { OFF, HEAT, COOL, AUTO } = this.Characteristic.TargetHeatingCoolingState;\n    const validValues = [AUTO];\n\n    // Thermostat valve may not support 'Power Off'\n    if (this.getStatus('switch')) {\n      validValues.push(OFF);\n    }\n\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_MODE);\n    const property = schema?.property as TuyaDeviceSchemaEnumProperty;\n    if (property) {\n      if (property.range.includes('hot')) {\n        validValues.push(HEAT);\n      }\n      if (property.range.includes('cold') || property.range.includes('eco')) {\n        validValues.push(COOL);\n      }\n    }\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetHeatingCoolingState)\n      .onGet(() => {\n        const on = this.getStatus('switch');\n        if (on && on.value === false) {\n          return OFF;\n        }\n\n        if (!schema) {\n          // If don't support mode, display auto.\n          return AUTO;\n        }\n\n        const status = this.getStatus(schema.code)!;\n        if (status.value === 'hot') {\n          return HEAT;\n        } else if (status.value === 'cold' || status.value === 'eco') {\n          return COOL;\n        } else if (status.value === 'auto' || status.value === 'temp_auto') {\n          return AUTO;\n        }\n\n        // Don't know how to display unsupported mode.\n        return AUTO;\n      })\n      .onSet(async value => {\n        const commands: TuyaDeviceStatus[] = [];\n\n        // Thermostat valve may not support 'Power Off'\n        const on = this.getStatus('switch');\n        if (on) {\n          if (value === OFF) {\n            commands.push({\n              code: 'switch',\n              value: false,\n            });\n          } else if (on.value === false) {\n            commands.push({\n              code: 'switch',\n              value: true,\n            });\n          }\n        }\n\n        if (schema) {\n          if ((value === HEAT) && property.range.includes('hot')) {\n            commands.push({ code: schema.code, value: 'hot' });\n          } else if (value === COOL) {\n            if (property.range.includes('eco')) {\n              commands.push({ code: schema.code, value: 'eco' });\n            } else if (property.range.includes('cold')) {\n              commands.push({ code: schema.code, value: 'cold' });\n            }\n          } else if ((value === AUTO) && property.range.includes('auto')) {\n            commands.push({ code: schema.code, value: 'auto' });\n          }\n        }\n\n        if (commands.length !== 0) {\n          await this.sendCommands(commands);\n        }\n      })\n      .setProps({ validValues });\n\n  }\n\n  configureTargetTemp() {\n    const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP);\n    if (!schema) {\n      this.log.warn('TargetTemperature not supported.');\n      return;\n    }\n\n    const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n    let multiple = Math.pow(10, property.scale);\n    let props = {\n      minValue: Math.max(10, property.min / multiple),\n      maxValue: Math.min(38, property.max / multiple),\n      minStep: Math.max(0.1, property.step / multiple),\n    };\n    if (props.maxValue <= props.minValue) {\n      this.log.warn('Invalid schema: %o, props will be reset to the default value.', schema);\n      multiple = 1;\n      props = { minValue: 10, maxValue: 38, minStep: 1 };\n    }\n    this.log.debug('Set props for TargetTemperature:', props);\n\n    this.mainService().getCharacteristic(this.Characteristic.TargetTemperature)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        const temp = status.value as number / multiple;\n        return limit(temp, props.minValue, props.maxValue);\n      })\n      .onSet(async value => {\n        await this.sendCommands([{\n          code: schema.code,\n          value: value as number * multiple,\n        }]);\n      })\n      .setProps(props);\n\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/ValveAccessory.ts",
    "content": "import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureActive } from './characteristic/Active';\nimport { configureName } from './characteristic/Name';\n\nconst SCHEMA_CODE = {\n  ON: ['switch', 'switch_1'],\n};\n\nexport default class ValveAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ON];\n  }\n\n  configureServices(): void {\n    const oldService = this.accessory.getService(this.Service.Valve);\n    if (oldService && oldService?.subtype === undefined) {\n      this.platform.log.warn('Remove old service:', oldService.UUID);\n      this.accessory.removeService(oldService);\n    }\n\n    const schema = SCHEMA_CODE.ON.map(code => this.getSchema(code))\n      .filter((s: TuyaDeviceSchema | undefined): s is TuyaDeviceSchema => !!s && s.type === TuyaDeviceSchemaType.Boolean);\n\n    for (const _schema of schema) {\n      const name = (schema.length === 1) ? this.device.name : _schema.code;\n      this.configureValve(_schema, name);\n    }\n  }\n\n\n  configureValve(schema: TuyaDeviceSchema, name: string) {\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.Service.Valve, name, schema.code);\n\n    configureName(this, service, name);\n\n    service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION);\n\n    const { NOT_IN_USE, IN_USE } = this.Characteristic.InUse;\n    service.getCharacteristic(this.Characteristic.InUse)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return status.value ? IN_USE : NOT_IN_USE;\n      });\n\n    configureActive(this, service, schema);\n\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/VibrationSensorAccessory.ts",
    "content": "import { TuyaDeviceStatus } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = {\n  STATE: ['shock_state'],\n};\n\nexport default class VibrationSensorAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.STATE];\n  }\n\n  configureServices() {\n    this.getMotionService().setCharacteristic(this.Characteristic.MotionDetected, false);\n  }\n\n  getMotionService() {\n    return this.accessory.getService(this.Service.MotionSensor)\n      || this.accessory.addService(this.Service.MotionSensor);\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n    super.onDeviceStatusUpdate(status);\n\n    const motionSchema = this.getSchema(...SCHEMA_CODE.STATE)!;\n    const motionStatus = status.find(_status => _status.code === motionSchema.code);\n    motionStatus && this.onMotionDetected(motionStatus);\n  }\n\n  private timer?: NodeJS.Timeout;\n  onMotionDetected(status: TuyaDeviceStatus) {\n    if (!this.intialized) {\n      return;\n    }\n\n    if (status.value !== 'vibration' && status.value !== 'drop') {\n      return;\n    }\n\n    this.log.info('Motion event:', status.value);\n    const characteristic = this.getMotionService().getCharacteristic(this.Characteristic.MotionDetected);\n    characteristic.updateValue(true);\n\n    this.timer && clearTimeout(this.timer);\n    this.timer = setTimeout(() => characteristic.updateValue(false), 3 * 1000);\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/WeatherStationAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\n\nexport default class TemperatureHumiditySensorAccessory extends BaseAccessory {\n  configureServices(): void {\n    const { temperatureSchemas, humiditySchemas } = this.getDynamicSchemaCodes();\n\n    temperatureSchemas.forEach((schema, index) => {\n      const serviceName = `Temperature Sensor ${index + 1}`;\n      const serviceSubtype = `temperature_sensor_${index + 1}`;\n      const service =\n        this.accessory.getServiceById(this.Service.TemperatureSensor, serviceSubtype) ||\n        this.accessory.addService(this.Service.TemperatureSensor, serviceName, serviceSubtype);\n\n      service\n        .getCharacteristic(this.Characteristic.CurrentTemperature)\n        .onGet(() => {\n          const status = this.getStatus(schema.code);\n          if (status) {\n            const property = this.getSchema(schema.code)?.property as { scale: number };\n            const multiple = Math.pow(10, property.scale || 0);\n            return Math.min(Math.max((status.value as number) / multiple, -100), 100);\n          }\n          return 0; // Default value if no status is found\n        });\n    });\n\n    humiditySchemas.forEach((schema, index) => {\n      const serviceName = `Humidity Sensor ${index + 1}`;\n      const serviceSubtype = `humidity_sensor_${index + 1}`;\n      const service =\n        this.accessory.getServiceById(this.Service.HumiditySensor, serviceSubtype) ||\n        this.accessory.addService(this.Service.HumiditySensor, serviceName, serviceSubtype);\n\n      service\n        .getCharacteristic(this.Characteristic.CurrentRelativeHumidity)\n        .onGet(() => {\n          const status = this.getStatus(schema.code);\n          if (status) {\n            return status.value as number;\n          }\n          return 0; // Default value if no status is found\n        });\n    });\n  }\n\n  private getDynamicSchemaCodes() {\n    const temperatureSchemas: { code: string }[] = [];\n    const humiditySchemas: { code: string }[] = [];\n\n    this.device.schema.forEach((schema) => {\n      if (schema.code.includes('ToutCh')) {\n        temperatureSchemas.push(schema);\n      } else if (schema.code.includes('HoutCh')) {\n        humiditySchemas.push(schema);\n      }\n    });\n\n    return { temperatureSchemas, humiditySchemas };\n  }\n}\n"
  },
  {
    "path": "src/accessory/WhiteNoiseLightAccessory.ts",
    "content": "import BaseAccessory from './BaseAccessory';\nimport { configureOn } from './characteristic/On';\nimport { configureLight } from './characteristic/Light';\n\nconst SCHEMA_CODE = {\n  LIGHT_ON: ['switch_led'],\n  LIGHT_COLOR: ['colour_data'],\n  MUSIC_ON: ['switch_music'],\n};\n\nexport default class WhiteNoiseLightAccessory extends BaseAccessory {\n  requiredSchema() {\n    return [SCHEMA_CODE.LIGHT_ON, SCHEMA_CODE.MUSIC_ON];\n  }\n\n  configureServices() {\n    // Light\n    if (this.lightServiceType() === this.Service.Lightbulb) {\n      configureLight(\n        this,\n        this.lightService(),\n        this.getSchema(...SCHEMA_CODE.LIGHT_ON),\n        undefined,\n        undefined,\n        this.lightColorSchema(),\n        undefined,\n      );\n    } else if (this.lightServiceType() === this.Service.Switch) {\n      configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.LIGHT_ON));\n      const unusedService = this.accessory.getService(this.Service.Lightbulb);\n      unusedService && this.accessory.removeService(unusedService);\n    }\n\n    // White Noise\n    configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.MUSIC_ON));\n  }\n\n  lightColorSchema() {\n    const colorSchema = this.getSchema(...SCHEMA_CODE.LIGHT_COLOR);\n    if (!colorSchema) {\n      return;\n    }\n\n    const { h, s, v } = (colorSchema.property || {}) as never;\n    if (!h || !s || !v) {\n      // Set sensible defaults for missing properties\n      colorSchema.property = {\n        h: { min: 0, scale: 0, unit: '', max: 360, step: 1 },\n        s: { min: 0, scale: 0, unit: '', max: 1000, step: 1 },\n        v: { min: 0, scale: 0, unit: '', max: 1000, step: 1 },\n      };\n    }\n\n    return colorSchema;\n  }\n\n  lightServiceType() {\n    if (this.lightColorSchema()) {\n      return this.Service.Lightbulb;\n    }\n    return this.Service.Switch;\n  }\n\n  lightService() {\n    return (\n      this.accessory.getService(this.Service.Lightbulb) ||\n      this.accessory.addService(this.Service.Lightbulb)\n    );\n  }\n}\n"
  },
  {
    "path": "src/accessory/WindowAccessory.ts",
    "content": "import WindowCoveringAccessory from './WindowCoveringAccessory';\n\nexport default class WindowAccessory extends WindowCoveringAccessory {\n  mainService() {\n    return this.accessory.getService(this.Service.Window)\n      || this.accessory.addService(this.Service.Window);\n  }\n}\n"
  },
  {
    "path": "src/accessory/WindowCoveringAccessory.ts",
    "content": "import { TuyaDeviceSchemaEnumProperty } from '../device/TuyaDevice';\nimport { limit } from '../util/util';\nimport BaseAccessory from './BaseAccessory';\n\nconst SCHEMA_CODE = [\n  {\n    NAME : 'control',\n    CURRENT_POSITION: ['percent_state'],\n    TARGET_POSITION_CONTROL: ['control', 'mach_operate'],\n    TARGET_POSITION_PERCENT: ['percent_control', 'position'],\n  },\n  {\n    NAME : 'control_2',\n    CURRENT_POSITION: ['percent_state'],\n    TARGET_POSITION_CONTROL: ['control_2', 'mach_operate'],\n    TARGET_POSITION_PERCENT: ['percent_control_2', 'position'],\n  },\n];\n\nexport default class WindowCoveringAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE[0].TARGET_POSITION_CONTROL];//, SCHEMA_CODE[1].TARGET_POSITION_CONTROL];\n  }\n\n  configureServices() {\n\n    let amount = 1;\n    const schema = this.getSchema('control_2');\n    if (schema) {\n      amount = 2;\n    }\n    this.log.warn('Curtain amount:', amount);\n    for (let i = 0; i < amount; i++) {\n\n      this.configureCurrentPosition(i);\n      this.configurePositionState(i);\n      if (this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT)) {\n        this.configureTargetPositionPercent(i);\n      } else {\n        this.configureTargetPositionControl(i);\n      }\n    }\n  }\n\n  configureCurrentPosition(i : number) {\n    const currentSchema = this.getSchema(...SCHEMA_CODE[i].CURRENT_POSITION);\n    const targetSchema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT);\n    const targetControlSchema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_CONTROL)!;\n\n    const service = this.accessory.getService(SCHEMA_CODE[i].NAME) ||\n       this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME);\n\n    service.getCharacteristic(this.Characteristic.CurrentPosition)\n      .onGet(() => {\n        if (currentSchema) {\n          const status = this.getStatus(currentSchema.code)!;\n          return limit(status.value as number, 0, 100);\n        } else if (targetSchema) {\n          const status = this.getStatus(targetSchema.code)!;\n          return limit(status.value as number, 0, 100);\n        }\n\n        const status = this.getStatus(targetControlSchema.code)!;\n        if (status.value === 'close' || status.value === 'FZ') {\n          return 0;\n        } else if (status.value === 'stop' || status.value === 'STOP') {\n          return 50;\n        } else if (status.value === 'open' || status.value === 'ZZ') {\n          return 100;\n        }\n\n        this.log.warn('Unknown CurrentPosition:', status.value);\n        return 50;\n      });\n  }\n\n  configurePositionState(i : number) {\n    const currentSchema = this.getSchema(...SCHEMA_CODE[i].CURRENT_POSITION);\n    const targetSchema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT);\n\n    const { DECREASING, INCREASING, STOPPED } = this.Characteristic.PositionState;\n\n    const service = this.accessory.getService(SCHEMA_CODE[i].NAME) ||\n       this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME);\n\n    service.getCharacteristic(this.Characteristic.PositionState)\n      .onGet(() => {\n        if (!currentSchema || !targetSchema) {\n          return STOPPED;\n        }\n\n        const currentStatus = this.getStatus(currentSchema.code)!;\n        const targetStatus = this.getStatus(targetSchema.code)!;\n        if (targetStatus.value === 100 && currentStatus.value !== 100) {\n          return INCREASING;\n        } else if (targetStatus.value === 0 && currentStatus.value !== 0) {\n          return DECREASING;\n        } else {\n          return STOPPED;\n        }\n      });\n  }\n\n  configureTargetPositionPercent(i : number) {\n    const schema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT);\n    if (!schema) {\n      return;\n    }\n\n    const service = this.accessory.getService(SCHEMA_CODE[i].NAME) ||\n       this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME);\n\n    service.getCharacteristic(this.Characteristic.TargetPosition)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        return limit(status.value as number, 0, 100);\n      })\n      .onSet(async value => {\n        await this.sendCommands([{ code: schema.code, value: value as number }], true);\n      });\n  }\n\n  configureTargetPositionControl(i : number) {\n    const schema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_CONTROL);\n    if (!schema) {\n      return;\n    }\n\n    const isOldSchema = !(schema.property as TuyaDeviceSchemaEnumProperty).range.includes('open');\n\n    const service = this.accessory.getService(SCHEMA_CODE[i].NAME) ||\n       this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME);\n\n    service.getCharacteristic(this.Characteristic.TargetPosition)\n      .onGet(() => {\n        const status = this.getStatus(schema.code)!;\n        if (status.value === 'close' || status.value === 'FZ') {\n          return 0;\n        } else if (status.value === 'stop' || status.value === 'STOP') {\n          return 50;\n        } else if (status.value === 'open' || status.value === 'ZZ') {\n          return 100;\n        }\n\n        this.log.warn('Unknown TargetPosition:', status.value);\n        return 50;\n      })\n      .onSet(async value => {\n        let control: string;\n        if (value === 0) {\n          control = isOldSchema ? 'FZ' : 'close';\n        } else if (value === 100) {\n          control = isOldSchema ? 'ZZ' : 'open';\n        } else {\n          control = isOldSchema ? 'STOP' :'stop';\n        }\n        await this.sendCommands([{ code: schema.code, value: control }], true);\n      })\n      .setProps({\n        minStep: 50,\n      });\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/WirelessSwitchAccessory.ts",
    "content": "import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice';\nimport BaseAccessory from './BaseAccessory';\nimport { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent';\n\nconst SCHEMA_CODE = {\n  ON: ['switch_mode1', 'switch1_value'],\n};\n\nexport default class SwitchAccessory extends BaseAccessory {\n\n  requiredSchema() {\n    return [SCHEMA_CODE.ON];\n  }\n\n  configureServices() {\n    const schema = this.device.schema.filter(schema => schema.code.match(/switch_mode(\\d+)/) || schema.code.match(/switch(\\d+)_value/));\n    for (const _schema of schema) {\n      const name = (schema.length === 1) ? this.device.name : _schema.code;\n      this.configureSwitch(_schema, name);\n    }\n  }\n\n  configureSwitch(schema: TuyaDeviceSchema, name: string) {\n\n    const service = this.accessory.getService(schema.code)\n      || this.accessory.addService(this.Service.StatelessProgrammableSwitch, name, schema.code);\n\n    const group = schema.code.match(/switch_mode(\\d+)/) || schema.code.match(/switch(\\d+)_value/);\n    const index = group![1];\n    service.setCharacteristic(this.Characteristic.ServiceLabelIndex, index);\n\n    configureProgrammableSwitchEvent(this, service, schema);\n  }\n\n  async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) {\n    super.onDeviceStatusUpdate(status);\n\n    for (const _status of status) {\n      const service = this.accessory.getService(_status.code);\n      if (!service) {\n        continue;\n      }\n\n      onProgrammableSwitchEvent(this, service, _status);\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/characteristic/Active.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureActive(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  const { ACTIVE, INACTIVE } = accessory.Characteristic.Active;\n  service.getCharacteristic(accessory.Characteristic.Active)\n    .onGet(() => {\n      accessory.checkOnlineStatus();\n      const status = accessory.getStatus(schema.code)!;\n      return status.value as boolean ? ACTIVE : INACTIVE;\n    })\n    .onSet(async value => {\n      await accessory.sendCommands([{\n        code: schema.code,\n        value: (value === ACTIVE) ? true : false,\n      }], true);\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/AirQuality.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\nimport { limit } from '../../util/util';\n\nexport function configureAirQuality(\n  accessory: BaseAccessory,\n  service?: Service,\n  airQualitySchema?: TuyaDeviceSchema,\n  pm2_5Schema?: TuyaDeviceSchema,\n  pm10Schema?: TuyaDeviceSchema,\n  vocSchema?: TuyaDeviceSchema,\n) {\n  if (!airQualitySchema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(accessory.Service.AirQualitySensor)\n      || accessory.accessory.addService(accessory.Service.AirQualitySensor);\n  }\n\n  const property = airQualitySchema.property as TuyaDeviceSchemaIntegerProperty;\n  const multiple = Math.pow(10, property ? property.scale : 0);\n  const { UNKNOWN, EXCELLENT, GOOD, FAIR, INFERIOR, POOR } = accessory.Characteristic.AirQuality;\n  service.getCharacteristic(accessory.Characteristic.AirQuality)\n    .onGet(() => {\n      const status = accessory.getStatus(airQualitySchema.code)!;\n      if (airQualitySchema.type === TuyaDeviceSchemaType.Integer) {\n        const value = limit(status.value as number / multiple, 0, 1000);\n        if (value <= 10) {\n          return EXCELLENT;\n        } else if (value <= 50) {\n          return GOOD;\n        } else if (value <= 100) {\n          return FAIR;\n        } else if (value <= 200) {\n          return INFERIOR;\n        } else {\n          return POOR;\n        }\n      } else if (airQualitySchema.type === TuyaDeviceSchemaType.Enum) {\n        if (status.value === 'great') {\n          return EXCELLENT;\n        } else if (status.value === 'good') {\n          return GOOD;\n        } else if (status.value === 'mild') {\n          return FAIR;\n        } else if (status.value === 'medium') {\n          return INFERIOR;\n        } else if (status.value === 'severe') {\n          return POOR;\n        }\n      }\n\n      return UNKNOWN;\n    });\n\n  pm2_5Schema && configureDensity(accessory, service, accessory.Characteristic.PM2_5Density, pm2_5Schema);\n  pm10Schema && configureDensity(accessory, service, accessory.Characteristic.PM10Density, pm10Schema);\n  vocSchema && configureDensity(accessory, service, accessory.Characteristic.VOCDensity, vocSchema);\n}\n\nfunction configureDensity(\n  accessory: BaseAccessory,\n  service: Service,\n  characteristic,\n  schema?: TuyaDeviceSchema,\n) {\n  if (!schema) {\n    return;\n  }\n\n  const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n  const multiple = Math.pow(10, property ? property.scale : 0);\n  service.getCharacteristic(characteristic)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      const value = limit(status.value as number / multiple, 0, 1000);\n      return value;\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/CurrentRelativeHumidity.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice';\nimport { limit } from '../../util/util';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureCurrentRelativeHumidity(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(accessory.Service.HumiditySensor)\n      || accessory.accessory.addService(accessory.Service.HumiditySensor);\n  }\n\n  const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n  const multiple = Math.pow(10, property ? property.scale : 0);\n  service.getCharacteristic(accessory.Characteristic.CurrentRelativeHumidity)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return limit(status.value as number / multiple, 0, 100);\n    });\n\n}\n"
  },
  {
    "path": "src/accessory/characteristic/CurrentTemperature.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice';\nimport { limit } from '../../util/util';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureCurrentTemperature(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(accessory.Service.TemperatureSensor)\n      || accessory.accessory.addService(accessory.Service.TemperatureSensor);\n  }\n\n  const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n  const multiple = Math.pow(10, property ? property.scale : 0);\n  const props = {\n    minValue: Math.max(-270, property.min / multiple),\n    maxValue: Math.min(100, property.max / multiple),\n    minStep: Math.max(0.1, property.step / multiple),\n  };\n  accessory.log.debug('Set props for CurrentTemperature:', props);\n\n  service.getCharacteristic(accessory.Characteristic.CurrentTemperature)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return limit(status.value as number / multiple, props.minValue, props.maxValue);\n    })\n    .setProps(props);\n\n}\n"
  },
  {
    "path": "src/accessory/characteristic/EnergyUsage.ts",
    "content": "import BaseAccessory from '../BaseAccessory';\nimport { API, Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice';\nimport OverridedBaseAccessory from '../BaseAccessory';\n\nexport function configureEnergyUsage(\n  api: API,\n  accessory: OverridedBaseAccessory,\n  service: Service,\n  currentSchema?: TuyaDeviceSchema,\n  powerSchema?: TuyaDeviceSchema,\n  voltageSchema?: TuyaDeviceSchema,\n  totalSchema?: TuyaDeviceSchema,\n) {\n\n  if (currentSchema) {\n    const amperes = createAmperesCharacteristic(api);\n    if (!service.testCharacteristic(amperes)) {\n      service.addCharacteristic(amperes);\n    }\n    service.getCharacteristic(amperes).onGet(\n      createStatusGetter(accessory, currentSchema, isUnit(currentSchema, 'mA') ? 1000 : 0),\n    );\n  }\n\n  if (powerSchema) {\n    const watts = createWattsCharacteristic(api);\n    if (!service.testCharacteristic(watts)) {\n      service.addCharacteristic(watts);\n    }\n    service.getCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema));\n  }\n\n  if (voltageSchema) {\n    const volts = createVoltsCharacteristic(api);\n    if (!service.testCharacteristic(volts)) {\n      service.addCharacteristic(volts);\n    }\n    service.getCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema));\n  }\n\n  if (totalSchema) {\n    const kwh = createKilowattHourCharacteristic(api);\n    if (!service.testCharacteristic(kwh)) {\n      service.addCharacteristic(kwh);\n    }\n    service.getCharacteristic(kwh).onGet(createStatusGetter(accessory, totalSchema));\n  }\n}\n\nfunction isUnit(schema: TuyaDeviceSchema, ...units: string[]): boolean {\n  return units.includes((schema.property as TuyaDeviceSchemaIntegerProperty).unit);\n}\n\nfunction createStatusGetter(accessory: BaseAccessory, schema: TuyaDeviceSchema, divisor = 1): () => number {\n  const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n  divisor *= Math.pow(10, property.scale);\n  return () => {\n    const status = accessory.getStatus(schema.code)!;\n\n    return (status.value as number) / divisor;\n  };\n}\n\nfunction createAmperesCharacteristic(api: API) {\n  return class Amperes extends api.hap.Characteristic {\n    static readonly UUID = 'E863F126-079E-48FF-8F27-9C2605A29F52';\n\n    constructor() {\n      super('Amperes', Amperes.UUID, {\n        format: api.hap.Formats.FLOAT,\n        perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n        unit: 'A',\n      });\n    }\n  };\n}\n\nfunction createWattsCharacteristic(api: API) {\n  return class Watts extends api.hap.Characteristic {\n    static readonly UUID = 'E863F10D-079E-48FF-8F27-9C2605A29F52';\n\n    constructor() {\n      super('Consumption', Watts.UUID, {\n        format: api.hap.Formats.FLOAT,\n        perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n        unit: 'W',\n      });\n    }\n  };\n}\n\nfunction createVoltsCharacteristic(api: API) {\n  return class Volts extends api.hap.Characteristic {\n    static readonly UUID = 'E863F10A-079E-48FF-8F27-9C2605A29F52';\n\n    constructor() {\n      super('Volts', Volts.UUID, {\n        format: api.hap.Formats.FLOAT,\n        perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n        unit: 'V',\n      });\n    }\n  };\n}\n\nfunction createKilowattHourCharacteristic(api: API) {\n  return class KilowattHour extends api.hap.Characteristic {\n    static readonly UUID = 'E863F10C-079E-48FF-8F27-9C2605A29F52';\n\n    constructor() {\n      super('Total Consumption', KilowattHour.UUID, {\n        format: api.hap.Formats.FLOAT,\n        perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n        unit: 'kWh',\n      });\n    }\n  };\n}\n"
  },
  {
    "path": "src/accessory/characteristic/Light.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../../device/TuyaDevice';\nimport { kelvinToHSV, kelvinToMired, miredToKelvin } from '../../util/color';\nimport { limit, remap } from '../../util/util';\nimport BaseAccessory from '../BaseAccessory';\nimport { configureOn } from './On';\n\nconst DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500;\n\nenum LightType {\n  Unknown = 'Unknown',\n  Normal = 'Normal', // Normal Accessory, similar to SwitchAccessory, OutletAccessory.\n  C = 'C', // Accessory with brightness.\n  CW = 'CW', // Accessory with brightness and color temperature (Cold and Warm).\n  RGB = 'RGB', // Accessory with color (RGB <--> HSB).\n  RGBC = 'RGBC', // Accessory with color and brightness.\n  RGBCW = 'RGBCW', // Accessory with color, brightness and color temperature (two work mode).\n}\n\ntype TuyaDeviceSchemaColorProperty = {\n  h: TuyaDeviceSchemaIntegerProperty;\n  s: TuyaDeviceSchemaIntegerProperty;\n  v: TuyaDeviceSchemaIntegerProperty;\n};\n\nfunction getLightType(\n  accessory: BaseAccessory,\n  on?: TuyaDeviceSchema,\n  bright?: TuyaDeviceSchema,\n  temp?: TuyaDeviceSchema,\n  color?: TuyaDeviceSchema,\n  mode?: TuyaDeviceSchema,\n) {\n  const modeRange = mode && (mode.property as TuyaDeviceSchemaEnumProperty).range;\n  const { h, s, v } = (color?.property || {}) as never;\n\n  let lightType: LightType;\n  if (on && bright && temp && h && s && v && modeRange && modeRange.includes('colour') && modeRange.includes('white')) {\n    lightType = LightType.RGBCW;\n  } else if (on && bright && !temp && h && s && v && modeRange && modeRange.includes('colour') && modeRange.includes('white')) {\n    lightType = LightType.RGBC;\n  } else if (on && !temp && h && s && v) {\n    lightType = LightType.RGB;\n  } else if (on && bright && temp) {\n    lightType = LightType.CW;\n  } else if (on && bright && !temp) {\n    lightType = LightType.C;\n  } else if (on && !bright && !temp) {\n    lightType = LightType.Normal;\n  } else {\n    lightType = LightType.Unknown;\n  }\n\n  return lightType;\n}\n\nfunction getColorValue(accessory: BaseAccessory, schema: TuyaDeviceSchema) {\n  const status = accessory.getStatus(schema!.code);\n  if (!status || !status.value || status.value === '' || status.value === '{}') {\n    return { h: 0, s: 0, v: 0 };\n  }\n\n  const { h, s, v } = JSON.parse(status.value as string);\n  return {\n    h: h as number,\n    s: s as number,\n    v: v as number,\n  };\n}\n\nfunction inWhiteMode(\n  accessory: BaseAccessory,\n  lightType: LightType,\n  modeSchema?: TuyaDeviceSchema,\n) {\n  if (lightType === LightType.C || lightType === LightType.CW) {\n    return true;\n  } else if (lightType === LightType.RGB) {\n    return false;\n  }\n\n  if (!modeSchema) {\n    return false;\n  }\n  const status = accessory.getStatus(modeSchema.code)!;\n  return (status.value === 'white');\n}\n\nfunction inColorMode(\n  accessory: BaseAccessory,\n  lightType: LightType,\n  modeSchema?: TuyaDeviceSchema,\n) {\n  if (lightType === LightType.RGB) {\n    return true;\n  } else if (lightType === LightType.C || lightType === LightType.CW) {\n    return false;\n  }\n\n  if (!modeSchema) {\n    return false;\n  }\n  const status = accessory.getStatus(modeSchema.code)!;\n  return (status.value === 'colour');\n}\n\nfunction configureLightOn(\n  accessory: BaseAccessory,\n  service: Service,\n  onSchema: TuyaDeviceSchema,\n  brightSchema?: TuyaDeviceSchema,\n) {\n  service.getCharacteristic(accessory.Characteristic.On)\n    .onGet(() => {\n      accessory.checkOnlineStatus();\n      const status = accessory.getStatus(onSchema.code)!;\n      return status.value as boolean;\n    })\n    .onSet(async value => {\n      const commands: TuyaDeviceStatus[] = [{ code: onSchema.code, value: value as boolean }];\n      // Bundle cached brightness with ON to prevent the device from turning on\n      // at stale brightness when commands arrive in separate debounce batches\n      // (e.g. HomeKit automations controlling multiple services simultaneously).\n      if (value && brightSchema) {\n        const brightStatus = accessory.getStatus(brightSchema.code);\n        if (brightStatus) {\n          commands.push({ code: brightSchema.code, value: brightStatus.value });\n        }\n      }\n      await accessory.sendCommands(commands, true);\n    });\n}\n\nfunction configureBrightness(\n  accessory: BaseAccessory,\n  service: Service,\n  lightType: LightType,\n  brightSchema?: TuyaDeviceSchema,\n  colorSchema?: TuyaDeviceSchema,\n  modeSchema?: TuyaDeviceSchema,\n) {\n\n  service.getCharacteristic(accessory.Characteristic.Brightness)\n    .onGet(() => {\n      if (inColorMode(accessory, lightType, modeSchema) && colorSchema) {\n        // Color mode, get brightness from `color_data.v`\n        const { max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v;\n        const colorValue = getColorValue(accessory, colorSchema);\n        const value = Math.round(100 * colorValue.v / max);\n        return limit(value, 0, 100);\n      } else if (inWhiteMode(accessory, lightType, modeSchema) && brightSchema) {\n        // White mode, get brightness from `brightness_value`\n        const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty;\n        const status = accessory.getStatus(brightSchema.code)!;\n        const value = Math.round(100 * (status.value as number) / max);\n        return limit(value, 0, 100);\n      } else {\n        // Unsupported mode\n        return 100;\n      }\n    })\n    .onSet(async value => {\n      accessory.log.debug(`Characteristic.Brightness set to: ${value}`);\n      if (inColorMode(accessory, lightType, modeSchema) && colorSchema) {\n        // Color mode, set brightness to `color_data.v`\n        const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v;\n        const colorValue = getColorValue(accessory, colorSchema);\n        colorValue.v = Math.round(value as number * max / 100);\n        colorValue.v = limit(colorValue.v, min, max);\n        await accessory.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true);\n      } else if (inWhiteMode(accessory, lightType, modeSchema) && brightSchema) {\n        // White mode, set brightness to `brightness_value`\n        const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty;\n        let brightValue = Math.round(value as number * max / 100);\n        brightValue = limit(brightValue, min, max);\n        await accessory.sendCommands([{ code: brightSchema.code, value: brightValue }], true);\n      } else {\n        // Unsupported mode\n        accessory.log.warn('Neither color mode nor white mode.');\n      }\n    });\n\n}\n\nfunction configureColourTemperature(\n  accessory: BaseAccessory,\n  service: Service,\n  lightType: LightType,\n  tempSchema: TuyaDeviceSchema,\n  modeSchema?: TuyaDeviceSchema,\n) {\n  const props = { minValue: 140, maxValue: 500, minStep: 1 };\n\n  if (lightType === LightType.RGBC) {\n    props.minValue = props.maxValue = Math.round(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN));\n  }\n  accessory.log.debug('Set props for ColorTemperature:', props);\n\n  service.getCharacteristic(accessory.Characteristic.ColorTemperature)\n    .onGet(() => {\n      if (lightType === LightType.RGBC) {\n        return props.minValue;\n      }\n\n      // const schema = accessory.getSchema(...SCHEMA_CODE.COLOR_TEMP)!;\n      const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty;\n      const status = accessory.getStatus(tempSchema.code)!;\n      const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue));\n      const mired = Math.round(kelvinToMired(kelvin));\n      return limit(mired, props.minValue, props.maxValue);\n    })\n    .onSet(async value => {\n      accessory.log.debug(`Characteristic.ColorTemperature set to: ${value}`);\n\n      const commands: TuyaDeviceStatus[] = [];\n      if (modeSchema) {\n        commands.push({ code: modeSchema.code, value: 'white' });\n      }\n\n      if (lightType !== LightType.RGBC) {\n        const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty;\n        const kelvin = miredToKelvin(value as number);\n        const temp = Math.round(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max));\n        commands.push({ code: tempSchema.code, value: temp });\n      }\n\n      await accessory.sendCommands(commands, true);\n    })\n    .setProps(props);\n\n}\n\nfunction configureHue(\n  accessory: BaseAccessory,\n  service: Service,\n  lightType: LightType,\n  colorSchema: TuyaDeviceSchema,\n  modeSchema?: TuyaDeviceSchema,\n) {\n  const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h;\n  service.getCharacteristic(accessory.Characteristic.Hue)\n    .onGet(() => {\n      if (inWhiteMode(accessory, lightType, modeSchema)) {\n        return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h;\n      }\n\n      const hue = Math.round(360 * getColorValue(accessory, colorSchema).h / max);\n      return limit(hue, 0, 360);\n    })\n    .onSet(async value => {\n      accessory.log.debug(`Characteristic.Hue set to: ${value}`);\n      const colorValue = getColorValue(accessory, colorSchema);\n      colorValue.h = Math.round(value as number * max / 360);\n      colorValue.h = limit(colorValue.h, min, max);\n      const commands: TuyaDeviceStatus[] = [{\n        code: colorSchema.code,\n        value: JSON.stringify(colorValue),\n      }];\n\n      if (modeSchema) {\n        commands.push({ code: modeSchema.code, value: 'colour' });\n      }\n\n      await accessory.sendCommands(commands, true);\n    });\n}\n\nfunction configureSaturation(\n  accessory: BaseAccessory,\n  service: Service,\n  lightType: LightType,\n  colorSchema: TuyaDeviceSchema,\n  modeSchema?: TuyaDeviceSchema,\n) {\n  const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s;\n  service.getCharacteristic(accessory.Characteristic.Saturation)\n    .onGet(() => {\n      if (inWhiteMode(accessory, lightType, modeSchema)) {\n        return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s;\n      }\n\n      const saturation = Math.round(100 * getColorValue(accessory, colorSchema).s / max);\n      return limit(saturation, 0, 100);\n    })\n    .onSet(async value => {\n      accessory.log.debug(`Characteristic.Saturation set to: ${value}`);\n      const colorValue = getColorValue(accessory, colorSchema);\n      colorValue.s = Math.round(value as number * max / 100);\n      colorValue.s = limit(colorValue.s, min, max);\n      const commands: TuyaDeviceStatus[] = [{\n        code: colorSchema.code,\n        value: JSON.stringify(colorValue),\n      }];\n\n      if (modeSchema) {\n        commands.push({ code: modeSchema.code, value: 'colour' });\n      }\n\n      await accessory.sendCommands(commands, true);\n    });\n}\n\nexport function configureLight(\n  accessory: BaseAccessory,\n  service?: Service,\n  onSchema?: TuyaDeviceSchema,\n  brightSchema?: TuyaDeviceSchema,\n  tempSchema?: TuyaDeviceSchema,\n  colorSchema?: TuyaDeviceSchema,\n  modeSchema?: TuyaDeviceSchema,\n) {\n  if (!onSchema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(accessory.Service.Lightbulb)\n      || accessory.accessory.addService(accessory.Service.Lightbulb, accessory.accessory.displayName + ' Light');\n  }\n\n  const lightType = getLightType(accessory, onSchema, brightSchema, tempSchema, colorSchema, modeSchema);\n  accessory.log.info('Light type:', lightType);\n\n  switch (lightType) {\n    case LightType.Normal:\n      configureOn(accessory, service, onSchema);\n      break;\n    case LightType.C:\n      configureLightOn(accessory, service!, onSchema, brightSchema);\n      configureBrightness(accessory, service!, lightType, brightSchema, colorSchema, modeSchema);\n      break;\n    case LightType.CW:\n      configureLightOn(accessory, service!, onSchema, brightSchema);\n      configureBrightness(accessory, service!, lightType, brightSchema, colorSchema, modeSchema);\n      configureColourTemperature(accessory, service!, lightType, tempSchema!, modeSchema);\n      break;\n    case LightType.RGB:\n      configureLightOn(accessory, service!, onSchema, brightSchema);\n      configureBrightness(accessory, service!, lightType, brightSchema, colorSchema, modeSchema);\n      configureHue(accessory, service!, lightType, colorSchema!, modeSchema);\n      configureSaturation(accessory, service!, lightType, colorSchema!, modeSchema);\n      break;\n    case LightType.RGBC:\n    case LightType.RGBCW:\n      configureLightOn(accessory, service!, onSchema, brightSchema);\n      configureBrightness(accessory, service!, lightType, brightSchema, colorSchema, modeSchema);\n      configureColourTemperature(accessory, service!, lightType, tempSchema!, modeSchema);\n      configureHue(accessory, service!, lightType, colorSchema!, modeSchema);\n      configureSaturation(accessory, service!, lightType, colorSchema!, modeSchema);\n      break;\n  }\n\n  configureAdaptiveLighting(accessory, service, brightSchema, tempSchema);\n\n}\n\nfunction configureAdaptiveLighting(\n  accessory: BaseAccessory,\n  service: Service,\n  brightSchema?: TuyaDeviceSchema,\n  tempSchema?: TuyaDeviceSchema,\n) {\n  const config = accessory.platform.getDeviceConfig(accessory.device);\n  if (!config || config.adaptiveLighting !== true) {\n    accessory.log.info('Adaptive Lighting disabled.');\n    return;\n  }\n  accessory.log.info('Adaptive Lighting enabled.');\n\n  if (!brightSchema || !tempSchema) {\n    accessory.log.warn('Adaptive Lighting not supported. Missing brightness or color temperature schema.');\n    return;\n  }\n\n  const { AdaptiveLightingController } = accessory.platform.api.hap;\n  const controller = new AdaptiveLightingController(service);\n  accessory.accessory.configureController(controller);\n  accessory.adaptiveLightingController = controller;\n}\n"
  },
  {
    "path": "src/accessory/characteristic/LockPhysicalControls.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureLockPhysicalControls(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = accessory.Characteristic.LockPhysicalControls;\n  service.getCharacteristic(accessory.Characteristic.LockPhysicalControls)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED;\n    })\n    .onSet(async value => {\n      await accessory.sendCommands([{\n        code: schema.code,\n        value: (value === CONTROL_LOCK_ENABLED) ? true : false,\n      }], true);\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/MotionDetected.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureMotionDetected(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(accessory.Service.MotionSensor)\n      || accessory.accessory.addService(accessory.Service.MotionSensor);\n  }\n\n  service.getCharacteristic(accessory.Characteristic.MotionDetected)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      if (schema.type === TuyaDeviceSchemaType.Enum) { // pir\n        return (status.value === 'pir');\n      }\n      return false;\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/Name.ts",
    "content": "import { Service } from 'homebridge';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureName(accessory: BaseAccessory, service: Service, name: string) {\n\n  service.setCharacteristic(accessory.Characteristic.Name, name);\n  if (!service.testCharacteristic(accessory.Characteristic.ConfiguredName)) {\n    service.addOptionalCharacteristic(accessory.Characteristic.ConfiguredName); // silence warning\n    service.setCharacteristic(accessory.Characteristic.ConfiguredName, name); // only add once\n  }\n\n}\n"
  },
  {
    "path": "src/accessory/characteristic/OccupancyDetected.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureOccupancyDetected(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(accessory.Service.OccupancySensor)\n      || accessory.accessory.addService(accessory.Service.OccupancySensor);\n  }\n\n  const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = accessory.Characteristic.OccupancyDetected;\n  service.getCharacteristic(accessory.Characteristic.OccupancyDetected)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return (status.value === 'presence') ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED;\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/On.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureOn(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  if (!service) {\n    service = accessory.accessory.getService(schema.code)\n      || accessory.accessory.addService(accessory.Service.Switch, schema.code, schema.code);\n  }\n\n  service.getCharacteristic(accessory.Characteristic.On)\n    .onGet(() => {\n      accessory.checkOnlineStatus();\n      const status = accessory.getStatus(schema.code)!;\n      return status.value as boolean;\n    })\n    .onSet(async value => {\n      await accessory.sendCommands([{\n        code: schema.code,\n        value: value as boolean,\n      }], true);\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/ProgrammableSwitchEvent.ts",
    "content": "import { CharacteristicProps, PartialAllowingNull, Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaType, TuyaDeviceStatus } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nconst SINGLE_PRESS = 0;\nconst DOUBLE_PRESS = 1;\nconst LONG_PRESS = 2;\n\nexport function configureProgrammableSwitchEvent(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  let props: PartialAllowingNull<CharacteristicProps>;\n  if (schema.type === TuyaDeviceSchemaType.Enum) {\n    const { range } = schema.property as TuyaDeviceSchemaEnumProperty;\n    props = GetStatelessSwitchProps(\n      range.includes('click') || range.includes('single_click') || range.includes('1'),\n      range.includes('double_click'),\n      range.includes('press') || range.includes('long_press'),\n    );\n  } else {\n    props = GetStatelessSwitchProps(true, false, false);\n  }\n\n  service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent)\n    .setProps(props);\n}\n\nexport function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Service, status: TuyaDeviceStatus) {\n  if (!accessory.intialized) {\n    return;\n  }\n\n  let value: number | undefined;\n\n  const schema = accessory.getSchema(status.code)!;\n  if (schema.type === TuyaDeviceSchemaType.Raw || schema.type === TuyaDeviceSchemaType.String) { // doorbell_pic or alarm_message\n    const url = Buffer.from(status.value as string, 'base64').toString('binary');\n    if (url.length === 0) {\n      return;\n    }\n    accessory.log.info('Alarm message:', url);\n    value = SINGLE_PRESS;\n  } else if (schema.type === TuyaDeviceSchemaType.Enum) {\n    if (status.value === 'click' || status.value === 'single_click' || status.value === '1') {\n      value = SINGLE_PRESS;\n    } else if (status.value === 'double_click') {\n      value = DOUBLE_PRESS;\n    } else if (status.value === 'press' || status.value === 'long_press') {\n      value = LONG_PRESS;\n    }\n  } else if (schema.type === TuyaDeviceSchemaType.Integer) {\n    if (status.value as number > 0) {\n      value = SINGLE_PRESS;\n    }\n  }\n\n  if (value === undefined) {\n    accessory.log.warn('Unknown ProgrammableSwitchEvent status:', status);\n    return;\n  }\n\n  accessory.log.debug('ProgrammableSwitchEvent updateValue: %o %o', status.code, value);\n  service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent)\n    .updateValue(value);\n\n}\n\n// Modified version of\n// https://github.com/benzman81/homebridge-http-webhooks/blob/master/src/homekit/accessories/HttpWebHookStatelessSwitchAccessory.js\nfunction GetStatelessSwitchProps(single_press: boolean, double_press: boolean, long_press: boolean) {\n  const props: PartialAllowingNull<CharacteristicProps> = {};\n\n  if (single_press) {\n    props.minValue = SINGLE_PRESS;\n  } else if (double_press) {\n    props.minValue = DOUBLE_PRESS;\n  } else if (long_press) {\n    props.minValue = LONG_PRESS;\n  }\n\n  if (single_press) {\n    props.maxValue = SINGLE_PRESS;\n  }\n\n  if (double_press) {\n    props.maxValue = DOUBLE_PRESS;\n  }\n\n  if (long_press) {\n    props.maxValue = LONG_PRESS;\n  }\n\n  if (single_press && !double_press && long_press) {\n    props.validValues = [SINGLE_PRESS, LONG_PRESS];\n  }\n\n  return props;\n}\n"
  },
  {
    "path": "src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice';\nimport { limit } from '../../util/util';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureRelativeHumidityDehumidifierThreshold(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n  const multiple = Math.pow(10, property.scale);\n  const props = {\n    minValue: 0,\n    maxValue: 100,\n    minStep: Math.max(1, property.step / multiple),\n  };\n  accessory.log.debug('Set props for RelativeHumidityDehumidifierThreshold:', props);\n\n  service.getCharacteristic(accessory.Characteristic.RelativeHumidityDehumidifierThreshold)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return limit(status.value as number / multiple, 0, 100);\n    })\n    .onSet(async value => {\n      const dehumidity_set = limit(value as number * multiple, property.min, property.max);\n      await accessory.sendCommands([{ code: schema.code, value: dehumidity_set }]);\n    })\n    .setProps(props);\n}\n"
  },
  {
    "path": "src/accessory/characteristic/RotationSpeed.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice';\nimport { limit } from '../../util/util';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureRotationSpeed(\n  accessory: BaseAccessory,\n  service: Service,\n  schema?: TuyaDeviceSchema,\n) {\n\n  if (!schema) {\n    return;\n  }\n\n  const property = schema.property as TuyaDeviceSchemaIntegerProperty;\n  const multiple = Math.pow(10, property.scale);\n  const props = {\n    minValue: property.min / multiple,\n    maxValue: property.max / multiple,\n    minStep: Math.max(1, property.step / multiple),\n  };\n  service.getCharacteristic(accessory.Characteristic.RotationSpeed)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      const value = status.value as number / multiple;\n      return limit(value, props.minValue, props.maxValue);\n    })\n    .onSet(async value => {\n      const speed = (value as number) * multiple;\n      await accessory.sendCommands([{ code: schema.code, value: speed }], true);\n    })\n    .setProps(props);\n\n}\n\nexport function configureRotationSpeedLevel(\n  accessory: BaseAccessory,\n  service: Service,\n  schema?: TuyaDeviceSchema,\n  ignoreValues?: string[],\n) {\n\n  if (!schema) {\n    return;\n  }\n\n  const property = schema.property as TuyaDeviceSchemaEnumProperty;\n  const range: string[] = [];\n  for (const value of property.range) {\n    if (ignoreValues?.includes(value)) {\n      continue;\n    }\n    range.push(value);\n  }\n\n  const props = { minValue: 0, maxValue: range.length, minStep: 1, unit: 'speed' };\n  accessory.log.debug('Set props for RotationSpeed:', props);\n\n  const onGetHandler = () => {\n    const status = accessory.getStatus(schema.code)!;\n    const index = range.indexOf(status.value as string);\n    return limit(index + 1, props.minValue, props.maxValue);\n  };\n\n  service.getCharacteristic(accessory.Characteristic.RotationSpeed)\n    .onGet(onGetHandler)\n    .onSet(async value => {\n      accessory.log.debug('Set RotationSpeed to:', value);\n      const index = Math.round(value as number - 1);\n      if (index < 0 || index >= range.length) {\n        accessory.log.debug('Out of range, return.');\n        return;\n      }\n      const speedLevel = range[index].toString();\n      accessory.log.debug('Set RotationSpeedLevel to:', speedLevel);\n      await accessory.sendCommands([{ code: schema.code, value: speedLevel }], true);\n    })\n    .updateValue(onGetHandler()) // ensure the value is correct before set props\n    .setProps(props);\n}\n\nexport function configureRotationSpeedOn(\n  accessory: BaseAccessory,\n  service: Service,\n  schema?: TuyaDeviceSchema,\n) {\n\n  if (!schema) {\n    return;\n  }\n\n  const props = { minValue: 0, maxValue: 100, minStep: 100 };\n  accessory.log.debug('Set props for RotationSpeed:', props);\n\n  service.getCharacteristic(accessory.Characteristic.RotationSpeed)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return (status.value as boolean) ? 100 : 0;\n    })\n    .setProps(props);\n}\n"
  },
  {
    "path": "src/accessory/characteristic/SecuritySystemState.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\nimport SecuritySystemAccessory from '../SecuritySystemAccessory';\n\nconst TUYA_CODES = {\n  MASTER_MODE: {\n    ARMED: 'arm',\n    DISARMED: 'disarmed',\n    HOME: 'home',\n  },\n};\n\nfunction getTuyaHomebridgeMap(accessory: BaseAccessory) {\n  const tuyaHomebridgeMap = new Map();\n\n  tuyaHomebridgeMap.set(TUYA_CODES.MASTER_MODE.ARMED, accessory.Characteristic.SecuritySystemCurrentState.AWAY_ARM);\n  tuyaHomebridgeMap.set(TUYA_CODES.MASTER_MODE.DISARMED, accessory.Characteristic.SecuritySystemCurrentState.DISARMED);\n  tuyaHomebridgeMap.set(TUYA_CODES.MASTER_MODE.HOME, accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM);\n  tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.AWAY_ARM, TUYA_CODES.MASTER_MODE.ARMED);\n  tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.DISARMED, TUYA_CODES.MASTER_MODE.DISARMED);\n  tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM, TUYA_CODES.MASTER_MODE.HOME);\n  tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.NIGHT_ARM, TUYA_CODES.MASTER_MODE.HOME);\n\n  return tuyaHomebridgeMap;\n}\n\nexport function configureSecuritySystemCurrentState(accessory: SecuritySystemAccessory, service: Service,\n  masterModeSchema?: TuyaDeviceSchema, sosStateSchema?: TuyaDeviceSchema) {\n  if (!masterModeSchema || !sosStateSchema) {\n    return;\n  }\n\n  const tuyaHomebridgeMap = getTuyaHomebridgeMap(accessory);\n\n  service.getCharacteristic(accessory.Characteristic.SecuritySystemCurrentState)\n    .onGet(() => {\n      const alarmTriggered = accessory.getStatus(sosStateSchema.code)!.value;\n\n      if (alarmTriggered) {\n        return accessory.Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;\n      } else {\n        const currentState = accessory.getStatus(masterModeSchema.code)!.value;\n        if (currentState === TUYA_CODES.MASTER_MODE.HOME) {\n          return accessory.isNightArm ? accessory.Characteristic.SecuritySystemCurrentState.NIGHT_ARM :\n            accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM;\n        }\n\n        return tuyaHomebridgeMap.get(currentState);\n      }\n    });\n}\n\nexport function configureSecuritySystemTargetState(accessory: SecuritySystemAccessory, service: Service,\n  masterModeSchema?: TuyaDeviceSchema, sosStateSchema?: TuyaDeviceSchema) {\n  if (!masterModeSchema || !sosStateSchema) {\n    return;\n  }\n\n  const tuyaHomebridgeMap = getTuyaHomebridgeMap(accessory);\n\n  service.getCharacteristic(accessory.Characteristic.SecuritySystemTargetState)\n    .onGet(() => {\n      const currentState = accessory.getStatus(masterModeSchema.code)!.value;\n      if (currentState === TUYA_CODES.MASTER_MODE.HOME) {\n        return accessory.isNightArm ? accessory.Characteristic.SecuritySystemCurrentState.NIGHT_ARM :\n          accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM;\n      }\n\n      return tuyaHomebridgeMap.get(currentState);\n    })\n    .onSet(async value => {\n\n      const sosState = accessory.getStatus(sosStateSchema.code)?.value;\n\n      // If we received a request to disarm the alarm, we make sure sos_state is set to false\n      if (sosState && value === accessory.Characteristic.SecuritySystemTargetState.DISARM) {\n        await accessory.sendCommands([{\n          code: sosStateSchema.code,\n          value: false,\n        }], true);\n      }\n\n      accessory.isNightArm = value === accessory.Characteristic.SecuritySystemTargetState.NIGHT_ARM;\n\n      await accessory.sendCommands([{\n        code: masterModeSchema.code,\n        value: tuyaHomebridgeMap.get(value),\n      }], true);\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/SwingMode.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureSwingMode(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  const { SWING_DISABLED, SWING_ENABLED } = accessory.Characteristic.SwingMode;\n  service.getCharacteristic(accessory.Characteristic.SwingMode)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return (status.value as boolean) ? SWING_ENABLED : SWING_DISABLED;\n    })\n    .onSet(async (value) => {\n      await accessory.sendCommands([{\n        code: schema.code,\n        value: (value === SWING_ENABLED) ? true : false,\n      }], true);\n    });\n}\n"
  },
  {
    "path": "src/accessory/characteristic/TemperatureDisplayUnits.ts",
    "content": "import { Service } from 'homebridge';\nimport { TuyaDeviceSchema } from '../../device/TuyaDevice';\nimport BaseAccessory from '../BaseAccessory';\n\nexport function configureTempDisplayUnits(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) {\n  if (!schema) {\n    return;\n  }\n\n  const { CELSIUS, FAHRENHEIT } = accessory.Characteristic.TemperatureDisplayUnits;\n  service.getCharacteristic(accessory.Characteristic.TemperatureDisplayUnits)\n    .onGet(() => {\n      const status = accessory.getStatus(schema.code)!;\n      return ((status.value as string).toLowerCase() === 'c') ? CELSIUS : FAHRENHEIT;\n    })\n    .onSet(async value => {\n      const status = accessory.getStatus(schema.code)!;\n      const isLowerCase = (status.value as string).toLowerCase() === status.value;\n\n      let unit = (value === CELSIUS) ? 'c' : 'f';\n      unit = isLowerCase ? unit.toLowerCase() : unit.toUpperCase();\n      await accessory.sendCommands([{\n        code: schema.code,\n        value: unit,\n      }]);\n    });\n}\n"
  },
  {
    "path": "src/config.ts",
    "content": "import { PlatformConfig } from 'homebridge';\nimport { TuyaDeviceSchemaProperty, TuyaDeviceSchemaType } from './device/TuyaDevice';\n\nexport interface TuyaPlatformDeviceSchemaConfig {\n  code: string;\n  newCode?: string;\n  type?: TuyaDeviceSchemaType;\n  property?: TuyaDeviceSchemaProperty;\n  onGet?: string;\n  onSet?: string;\n  hidden?: boolean;\n}\n\nexport interface TuyaPlatformDeviceConfig {\n  id: string;\n  category?: string;\n  schema?: Array<TuyaPlatformDeviceSchemaConfig>;\n  unbridged?: boolean;\n  adaptiveLighting?: boolean;\n}\n\nexport interface TuyaPlatformCustomConfigOptions {\n  projectType: '1';\n  endpoint: string;\n  accessId: string;\n  accessKey: string;\n  username: string;\n  password: string;\n  deviceOverrides?: Array<TuyaPlatformDeviceConfig>;\n  debug?: boolean;\n  debugLevel?: string;\n}\n\nexport interface TuyaPlatformHomeConfigOptions {\n  projectType: '2';\n  endpoint?: string;\n  accessId: string;\n  accessKey: string;\n  countryCode: number;\n  username: string;\n  password: string;\n  appSchema: string;\n  homeWhitelist?: Array<number>;\n  deviceOverrides?: Array<TuyaPlatformDeviceConfig>;\n  debug?: boolean;\n  debugLevel?: string;\n}\n\nexport type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions;\n\nexport interface TuyaPlatformConfig extends PlatformConfig {\n  options: TuyaPlatformConfigOptions;\n}\n\nexport const customOptionsSchema = {\n  properties: {\n    endpoint: { type: 'string', format: 'url', required: true },\n    accessId: { type: 'string', required: true },\n    accessKey: { type: 'string', required: true },\n    deviceOverrides: { 'type': 'array' },\n    debug: { type: 'boolean' },\n    debugLevel: { 'type': 'string' },\n  },\n};\n\nexport const homeOptionsSchema = {\n  properties: {\n    accessId: { type: 'string', required: true },\n    accessKey: { type: 'string', required: true },\n    endpoint: { type: 'string', format: 'url' },\n    countryCode: { 'type': 'integer', 'minimum': 1, required: true },\n    username: { type: 'string', required: true },\n    password: { type: 'string', required: true },\n    appSchema: { 'type': 'string', required: true },\n    homeWhitelist: { 'type': 'array' },\n    deviceOverrides: { 'type': 'array' },\n    debug: { type: 'boolean' },\n    debugLevel: { 'type': 'string' },\n  },\n};\n"
  },
  {
    "path": "src/core/TuyaOpenAPI.ts",
    "content": "/* eslint-disable max-len */\n/* eslint-disable @typescript-eslint/no-empty-function */\n/* eslint-disable @typescript-eslint/no-unused-vars */\nimport https from 'https';\nimport Crypto from 'crypto';\nimport { v4 as uuidv4 } from 'uuid';\nimport retry from 'async-await-retry';\n\n// eslint-disable-next-line\n// @ts-ignore\nimport { version } from '../../package.json';\n\nimport Logger, { PrefixLogger } from '../util/Logger';\n\nenum Endpoints {\n  AMERICA = 'https://openapi.tuyaus.com',\n  AMERICA_EAST = 'https://openapi-ueaz.tuyaus.com',\n  CHINA = 'https://openapi.tuyacn.com',\n  EUROPE = 'https://openapi.tuyaeu.com',\n  EUROPE_WEST = 'https://openapi-weaz.tuyaeu.com',\n  INDIA = 'https://openapi.tuyain.com',\n}\n\nconst DEFAULT_ENDPOINTS = {\n  [Endpoints.AMERICA.toString()]: [1, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 66, 81, 82, 84, 95, 239, 245, 246, 500, 502, 591, 593, 594, 595, 597, 598, 670, 672, 674, 675, 677, 678, 682, 683, 686, 690, 852, 853, 886, 970, 1721, 1787, 1809, 1829, 1849, 4779, 5999, 35818],\n  [Endpoints.CHINA.toString()]: [86],\n  [Endpoints.EUROPE.toString()]: [7, 20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 61, 65, 90, 92, 93, 94, 212, 213, 216, 218, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 242, 243, 244, 248, 250, 251, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 291, 297, 298, 299, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 385, 386, 387, 389, 420, 421, 423, 501, 503, 504, 505, 506, 507, 508, 509, 590, 592, 596, 673, 676, 679, 680, 681, 685, 687, 688, 689, 691, 692, 855, 856, 880, 960, 961, 962, 964, 965, 966, 967, 968, 971, 972, 973, 974, 975, 976, 977, 992, 993, 994, 995, 996, 998, 1242, 1246, 1264, 1268, 1284, 1340, 1345, 1441, 1473, 1649, 1664, 1670, 1671, 1684, 1758, 1767, 1784, 1868, 1869, 1876],\n  [Endpoints.INDIA.toString()]: [91],\n};\n\nexport const LOGIN_ERROR_MESSAGES = {\n  1004: 'Please make sure your endpoint, accessId, accessKey is right.',\n  1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.',\n  1114: 'Please make sure your endpoint, accessId, accessKey is right.',\n  2401: 'Username or password is wrong.',\n  2406: 'Please make sure you selected the right data center where your app account located, and the app account is linked with cloud project.',\n};\n\nconst API_NOT_SUBSCRIBED_ERROR = `\nAPI not subscribed. Please go to \"Tuya IoT Platform -> Cloud -> Development -> Project -> Service API\",\nand Authorize the following APIs before using:\n- Authorization Token Management\n- Device Status Notification\n- IoT Core\n- Industry Project Client Service (for \"Custom\" project)\n`;\n\nconst API_ERROR_MESSAGES = {\n  1010: 'Token expired. Tuya Cloud don\\'t support running multiple HomeBridge/HomeAssistant instance with same tuya account.',\n  28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.',\n  28841101: API_NOT_SUBSCRIBED_ERROR,\n  28841105: API_NOT_SUBSCRIBED_ERROR,\n};\n\ntype TuyaOpenAPIResponseSuccess = {\n  success: true;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  result: any;\n  t: number;\n  tid: string;\n};\n\ntype TuyaOpenAPIResponseError = {\n  success: false;\n  result: unknown;\n  code: number;\n  msg: string;\n  t: number;\n  tid: string;\n};\n\nexport type TuyaOpenAPIResponse = TuyaOpenAPIResponseSuccess | TuyaOpenAPIResponseError;\n\nexport default class TuyaOpenAPI {\n\n  static readonly Endpoints = Endpoints;\n\n  public assetIDArr: Array<string> = [];\n  public deviceArr: Array<object> = [];\n\n  public tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 };\n\n  constructor(\n    public endpoint: Endpoints | string,\n    public accessId: string,\n    public accessKey: string,\n    public log: Logger = console,\n    public lang = 'en',\n    public debug = false,\n  ) {\n    this.log = new PrefixLogger(log, TuyaOpenAPI.name, debug);\n  }\n\n  static getDefaultEndpoint(countryCode: number) {\n    for (const endpoint of Object.keys(DEFAULT_ENDPOINTS)) {\n      const countryCodeList = DEFAULT_ENDPOINTS[endpoint];\n      if (countryCodeList.includes(countryCode)) {\n        return <Endpoints>endpoint;\n      }\n    }\n    return Endpoints.AMERICA;\n  }\n\n  isLogin() {\n    return this.tokenInfo.access_token.length > 0;\n  }\n\n  isTokenExpired() {\n    return (this.tokenInfo.expire - 60 * 1000 <= new Date().getTime());\n  }\n\n  isTokenManagementAPI(path: string) {\n    if (path.startsWith('/v1.0/token')) {\n      return true;\n    }\n    return false;\n  }\n\n  async _refreshAccessTokenIfNeed(path: string) {\n    if (!this.isLogin()) {\n      return;\n    }\n\n    if (!this.isTokenExpired()) {\n      return;\n    }\n\n    if (this.isTokenManagementAPI(path)) {\n      return;\n    }\n\n    this.log.debug('Refreshing access_token');\n    const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`);\n    if (res.success === false) {\n      this.log.error('Refresh access_token failed. code = %s, msg = %s', res.code, res.msg);\n      return;\n    }\n\n    const { access_token, refresh_token, uid, expire_time } = res.result;\n    this.tokenInfo = {\n      access_token: access_token,\n      refresh_token: refresh_token,\n      uid: uid,\n      expire: expire_time * 1000 + new Date().getTime(),\n    };\n\n  }\n\n  /**\n   * In 'Custom' project, get a token directly. (Login with admin)\n   * Have permission on asset management, user management.\n   * But lost some permission on device management.\n   * @returns\n   */\n  async getToken() {\n    const res = await this.get('/v1.0/token', { grant_type: 1 });\n    if (res.success) {\n      const { access_token, refresh_token, uid, expire_time } = res.result;\n      this.tokenInfo = {\n        access_token: access_token,\n        refresh_token: refresh_token,\n        uid: uid,\n        expire: expire_time * 1000 + new Date().getTime(),\n      };\n    }\n    return res;\n  }\n\n  /**\n   * In 'Smart Home' project, login with App's user.\n   * @param countryCode 2-digit Country Code\n   * @param username Username\n   * @param password Password\n   * @param appSchema App Schema: 'tuyaSmart', 'smartlife'\n   * @returns\n   */\n  async homeLogin(countryCode: number, username: string, password: string, appSchema: string) {\n\n    if (this._isSaltedPassword(password)) {\n      this.log.info('Login with md5 salted password.');\n    } else {\n      password = Crypto.createHash('md5').update(password).digest('hex');\n    }\n\n    this.log.info('Login to: %s', this.endpoint);\n\n    this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 };\n    const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', {\n      country_code: countryCode,\n      username: username,\n      password: password,\n      schema: appSchema,\n    });\n\n    if (res.success) {\n      const { access_token, refresh_token, uid, expire_time, platform_url } = res.result;\n      this.endpoint = platform_url || this.endpoint;\n      this.tokenInfo = {\n        access_token: access_token,\n        refresh_token: refresh_token,\n        uid: uid,\n        expire: expire_time * 1000 + new Date().getTime(),\n      };\n    }\n\n    return res;\n  }\n\n  /**\n   * In 'Custom' project, Search user by username.\n   * @param username Username\n   * @returns\n   */\n  async customGetUserInfo(username: string) {\n    const res = await this.get(`/v1.2/iot-02/users/${username}`);\n    return res;\n  }\n\n  /**\n   * In 'Custom' project, create a user.\n   * @param username Username\n   * @param password Password\n   * @param country_code Country Code (Useless)\n   * @returns\n   */\n  async customCreateUser(username: string, password: string, country_code = 1) {\n    const res = await this.post('/v1.0/iot-02/users', {\n      username,\n      password: Crypto.createHash('sha256').update(password).digest('hex'),\n      country_code,\n    });\n    return res;\n  }\n\n  /**\n   * In 'Custom' project, login with user.\n   * @param username Username\n   * @param password Password\n   * @returns\n   */\n  async customLogin(username: string, password: string) {\n    this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 };\n    const res = await this.post('/v1.0/iot-03/users/login', {\n      username: username,\n      password: Crypto.createHash('sha256').update(password).digest('hex'),\n    });\n\n    if (res.success) {\n      const { access_token, refresh_token, uid, expire } = res.result;\n      this.tokenInfo = {\n        access_token: access_token,\n        refresh_token: refresh_token,\n        uid: uid,\n        expire: expire * 1000 + new Date().getTime(),\n      };\n    }\n\n    return res;\n  }\n\n  async request(method: string, path: string, params?, body?) {\n    await this._refreshAccessTokenIfNeed(path);\n\n    const now = new Date().getTime();\n    const nonce = uuidv4();\n    const accessToken = this.tokenInfo.access_token || '';\n    const stringToSign = this._getStringToSign(method, path, params, body);\n    const headers = {\n      't': `${now}`,\n      'client_id': this.accessId,\n      'nonce': nonce,\n      'Signature-Headers': 'client_id',\n      'sign': this._getSign(this.accessId, this.accessKey, this.isTokenManagementAPI(path) ? '' : this.tokenInfo.access_token, now, nonce, stringToSign),\n      'sign_method': 'HMAC-SHA256',\n      'access_token': accessToken,\n      'lang': this.lang,\n      'dev_lang': 'javascript',\n      'dev_channel': 'homebridge',\n      'devVersion': version,\n    };\n    this.log.debug('Request:\\nmethod = %s\\nendpoint = %s\\npath = %s\\nquery = %s\\nheaders = %s\\nbody = %s',\n      method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2));\n\n    if (params) {\n      path += '?' + new URLSearchParams(params).toString();\n    }\n\n    const res: TuyaOpenAPIResponse = await retry(async () => new Promise((resolve, reject) => {\n\n      const req = https.request({\n        host: new URL(this.endpoint).host,\n        method,\n        headers,\n        path,\n      }, res => {\n        if (res.statusCode !== 200) {\n          this.log.warn('Status: %d %s', res.statusCode, res.statusMessage);\n          return;\n        }\n        res.setEncoding('utf8');\n        let rawData = '';\n        res.on('data', (chunk) => {\n          rawData += chunk;\n        });\n        res.on('end', () => {\n          resolve(JSON.parse(rawData));\n        });\n      });\n\n      if (body) {\n        req.write(JSON.stringify(body));\n      }\n\n      req.on('error', e => {\n        this.log.error('Network error: %s. Retrying...', e.message);\n        reject(e);\n      });\n      req.end();\n    }), undefined, {retriesMax: 10, interval: 100, exponential: true, factor: 2, jitter: 100});\n\n    this.log.debug('Response:\\npath = %s\\ndata = %s', path, JSON.stringify(res, null, 2));\n    if (res && res.success !== true && API_ERROR_MESSAGES[res.code]) {\n      this.log.error(API_ERROR_MESSAGES[res.code]);\n    }\n\n    return res;\n  }\n\n  async get(path: string, params?) {\n    return this.request('get', path, params, null);\n  }\n\n  async post(path: string, params?) {\n    return this.request('post', path, null, params);\n  }\n\n  async delete(path: string, params?) {\n    return this.request('delete', path, params, null);\n  }\n\n  _getSign(accessId: string, accessKey: string, accessToken = '', timestamp = 0, nonce: string, stringToSign: string) {\n    const message = [accessId, accessToken, timestamp, nonce, stringToSign].join('');\n    const sign = Crypto.createHmac('SHA256', accessKey).update(message).digest('hex').toUpperCase();\n    return sign;\n  }\n\n  _getStringToSign(method: string, path: string, params, body) {\n    const httpMethod = method.toUpperCase();\n    const bodyStream = body ? JSON.stringify(body) : '';\n    const contentSHA256 = Crypto.createHash('sha256').update(bodyStream).digest('hex');\n    const headers = `client_id:${this.accessId}\\n`;\n    const url = this._getSignUrl(path, params);\n    const result = [httpMethod, contentSHA256, headers, url].join('\\n');\n    return result;\n  }\n\n  _getSignUrl(path: string, params) {\n    if (!params) {\n      return path;\n    }\n\n    const sortedKeys = Object.keys(params).sort();\n    const kv: string[] = [];\n    for (const key of sortedKeys) {\n      if (params[key] !== null && params[key] !== undefined) {\n        kv.push(`${key}=${params[key]}`);\n      }\n    }\n    const url = `${path}?${kv.join('&')}`;\n\n    return url;\n  }\n\n  _isSaltedPassword(password: string) {\n    return Buffer.from(password, 'hex').length === 16;\n  }\n\n}\n"
  },
  {
    "path": "src/core/TuyaOpenMQ.ts",
    "content": "import mqtt from 'mqtt';\nimport { v4 as uuid_v4 } from 'uuid';\nimport Crypto from 'crypto';\nimport CryptoJS from 'crypto-js';\n\nimport TuyaOpenAPI from './TuyaOpenAPI';\nimport Logger, { PrefixLogger } from '../util/Logger';\n\nconst GCM_TAG_LENGTH = 16;\n\ninterface TuyaMQTTConfigSourceTopic {\n  device: string;\n}\n\ninterface TuyaMQTTConfig {\n  url: string;\n  client_id: string;\n  username: string;\n  password: string;\n  expire_time: number;\n  source_topic: TuyaMQTTConfigSourceTopic;\n  sink_topic: object;\n}\n\ntype TuyaMQTTCallback = (topic: string, protocol: number, data) => void;\n\nexport default class TuyaOpenMQ {\n\n  public client?: mqtt.MqttClient;\n  public config?: TuyaMQTTConfig;\n  public version = '1.0';\n  public messageListeners = new Set<TuyaMQTTCallback>();\n  public linkId = uuid_v4();\n\n  public timer?: NodeJS.Timer;\n\n  constructor(\n    public api: TuyaOpenAPI,\n    public log: Logger = console,\n    public debug = false,\n  ) {\n    this.log = new PrefixLogger(log, TuyaOpenMQ.name, debug);\n  }\n\n  start() {\n    this._connect();\n  }\n\n  stop() {\n    if (this.timer) {\n      clearTimeout(this.timer);\n    }\n    if (this.client) {\n      this.client.removeAllListeners();\n      this.client.end();\n    }\n  }\n\n  async _connect() {\n    this.stop();\n\n    const res = await this._getMQConfig('mqtt');\n    if (res.success === false) {\n      this.log.warn('Get MQTT config failed. code = %s, msg = %s', res.code, res.msg);\n      return;\n    }\n\n    const { url, client_id, username, password, expire_time, source_topic } = res.result;\n    this.log.debug('Connecting to:', url);\n    const client = mqtt.connect(url, {\n      clientId: client_id,\n      username: username,\n      password: password,\n    });\n\n    client.on('connect', this._onConnect.bind(this));\n    client.on('error', this._onError.bind(this));\n    client.on('end', this._onEnd.bind(this));\n    client.on('message', this._onMessage.bind(this));\n    client.subscribe(source_topic.device);\n\n    this.client = client;\n    this.config = res.result;\n\n    // reconnect every 2 hours required\n    this.timer = setTimeout(this._connect.bind(this), (expire_time - 60) * 1000);\n\n  }\n\n  async _getMQConfig(linkType: string) {\n    const res = await this.api.post('/v1.0/iot-03/open-hub/access-config', {\n      'uid': this.api.tokenInfo.uid,\n      'link_id': this.linkId,\n      'link_type': linkType,\n      'topics': 'device',\n      'msg_encrypted_version': this.version,\n    });\n    return res;\n  }\n\n  _onConnect() {\n    this.log.debug('Connected');\n  }\n\n  _onError(error: Error) {\n    this.log.error('Error:', error);\n  }\n\n  _onEnd() {\n    this.log.debug('End');\n  }\n\n  async _onMessage(topic: string, payload: Buffer) {\n    const { protocol, data, t } = JSON.parse(payload.toString());\n    const messageData = this._decodeMQMessage(data, this.config!.password, t);\n    if (!messageData) {\n      this.log.warn('Message decode failed:', payload.toString());\n      return;\n    }\n    const message = JSON.parse(messageData);\n    this.log.debug('onMessage:\\ntopic = %s\\nprotocol = %s\\nmessage = %s\\nt = %s', topic, protocol, JSON.stringify(message, null, 2), t);\n\n    this._fixWrongOrderMessage(protocol, message, t);\n\n    for (const listener of this.messageListeners) {\n      listener(topic, protocol, message);\n    }\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  private consumedQueue: any[] = [];\n  _fixWrongOrderMessage(protocol: number, message, t: number) {\n    if (protocol !== 4) {\n      return;\n    }\n\n    const currentPayload = { protocol, message, t };\n\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const lastPayload : any = this.consumedQueue[this.consumedQueue.length - 1];\n    if (lastPayload && currentPayload.t < lastPayload.t) {\n      this.log.debug('Message received with wrong order.');\n      this.log.debug('LastMessage: dataId = %s, t = %s', lastPayload.message.dataId, lastPayload.t);\n      this.log.debug('CurrentMessage: dataId = %s, t = %s', message.dataId, t);\n      this.log.debug('This may cause outdated device status update.');\n\n      // Use newer status to override current status.\n      for (const _status of message.status) {\n        for (const payload of this.consumedQueue.reverse()) {\n          if (message.devId !== payload.message.devId) {\n            continue;\n          }\n\n          const latestStatus = payload.message.status.find(item => item.code === _status.code);\n          if (latestStatus) {\n            if (latestStatus.value !== _status.value) {\n              this.log.debug('Override status %o => %o', latestStatus, _status);\n              _status.value = latestStatus.value;\n              _status.t = latestStatus.t;\n            }\n            break;\n          }\n        }\n      }\n\n      return;\n    }\n\n    this.consumedQueue.push(currentPayload);\n\n    while (this.consumedQueue.length > 0) {\n      let t = this.consumedQueue[0].t as number;\n      if (t > Math.pow(10, 12)) { // timestamp format always changing, seconds or milliseconds is not certain :(\n        t = t / 1000;\n      }\n\n      // Remove message older than 30 seconds\n      if (Date.now() / 1000 > t + 30) {\n        this.consumedQueue.shift();\n      } else {\n        break;\n      }\n    }\n  }\n\n  _decodeMQMessage_1_0(b64msg: string, password: string) {\n    password = password.substring(8, 24);\n    const msg = CryptoJS.AES.decrypt(b64msg, CryptoJS.enc.Utf8.parse(password), {\n      mode: CryptoJS.mode.ECB,\n      padding: CryptoJS.pad.Pkcs7,\n    }).toString(CryptoJS.enc.Utf8);\n    return msg;\n  }\n\n  _decodeMQMessage_2_0(data: string, password: string, t: number) {\n    // Base64 decoding generates Buffers\n    const tmpbuffer = Buffer.from(data, 'base64');\n    const key = password.substring(8, 24).toString();\n    //get iv_length & iv_buffer\n    const iv_length = tmpbuffer.readUIntBE(0, 4);\n    const iv_buffer = tmpbuffer.slice(4, iv_length + 4);\n    //Removes the IV bits of the head and 16 bits of the tail tags\n    const data_buffer = tmpbuffer.slice(iv_length + 4, tmpbuffer.length - GCM_TAG_LENGTH);\n    const cipher = Crypto.createDecipheriv('aes-128-gcm', key, iv_buffer);\n    //setAuthTag buffer\n    cipher.setAuthTag(tmpbuffer.slice(tmpbuffer.length - GCM_TAG_LENGTH, tmpbuffer.length));\n    //setAAD buffer\n    const buf = Buffer.allocUnsafe(6);\n    buf.writeUIntBE(t, 0, 6);\n    cipher.setAAD(buf);\n\n    const msg = cipher.update(data_buffer);\n    return msg.toString('utf8');\n  }\n\n  _decodeMQMessage(data: string, password: string, t: number) {\n    if (this.version === '2.0') {\n      return this._decodeMQMessage_2_0(data, password, t);\n    } else {\n      return this._decodeMQMessage_1_0(data, password);\n    }\n  }\n\n  addMessageListener(listener: TuyaMQTTCallback) {\n    this.messageListeners.add(listener);\n  }\n\n  removeMessageListener(listener: TuyaMQTTCallback) {\n    this.messageListeners.delete(listener);\n  }\n\n}\n"
  },
  {
    "path": "src/device/TuyaCustomDeviceManager.ts",
    "content": "import TuyaOpenAPI from '../core/TuyaOpenAPI';\nimport TuyaDevice from './TuyaDevice';\nimport TuyaDeviceManager from './TuyaDeviceManager';\n\nexport default class TuyaCustomDeviceManager extends TuyaDeviceManager {\n\n  constructor(\n    public api: TuyaOpenAPI,\n    public debug = false,\n  ) {\n    super(api, debug);\n    this.mq.version = '2.0';\n  }\n\n  async getAssetList(parent_asset_id = -1) {\n    // const res = await this.api.get('/v1.0/iot-03/users/assets', {\n    const res = await this.api.get(`/v1.0/iot-02/assets/${parent_asset_id}/sub-assets`, {\n      'page_no': 0,\n      'page_size': 100,\n    });\n    return res;\n  }\n\n  async authorizeAssetList(uid: string, asset_ids: string[] = [], authorized_children = false) {\n    const res = await this.api.post(`/v1.0/iot-03/users/${uid}/actions/batch-assets-authorized`, {\n      asset_ids: asset_ids.join(','),\n      authorized_children,\n    });\n    return res;\n  }\n\n  async getAssetDeviceIDList(assetID: string) {\n    let deviceIDs: string[] = [];\n    const params = {\n      page_size: 50,\n    };\n    // eslint-disable-next-line no-constant-condition\n    while (true) {\n      const res = await this.api.get(`/v1.0/iot-02/assets/${assetID}/devices`, params);\n      deviceIDs = deviceIDs.concat((res.result.list as []).map(item => item['device_id']));\n      params['last_row_key'] = res.result.last_row_key;\n      if (!res.result.has_next) {\n        break;\n      }\n    }\n\n    return deviceIDs;\n  }\n\n  async updateDevices(assetIDList: string[]) {\n\n    let deviceIDs: string[] = [];\n    for (const assetID of assetIDList) {\n      deviceIDs = deviceIDs.concat(await this.getAssetDeviceIDList(assetID));\n    }\n    if (deviceIDs.length === 0) {\n      return [];\n    }\n\n    const res = await this.getDeviceListInfo(deviceIDs);\n    const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj));\n\n    for (const device of devices) {\n      device.schema = await this.getDeviceSchema(device.id);\n    }\n\n    // this.log.debug('Devices updated.\\n', JSON.stringify(devices, null, 2));\n    this.devices = devices;\n    return devices;\n  }\n\n}\n"
  },
  {
    "path": "src/device/TuyaDevice.ts",
    "content": "\nexport enum TuyaDeviceSchemaMode {\n  UNKNOWN = '',\n  READ_WRITE = 'rw',\n  READ_ONLY = 'ro',\n  WRITE_ONLY = 'wo',\n}\n\nexport enum TuyaDeviceSchemaType {\n  Boolean = 'Boolean',\n  Integer = 'Integer',\n  Enum = 'Enum',\n  String = 'String',\n  Json = 'Json',\n  Raw = 'Raw',\n}\n\nexport type TuyaDeviceSchemaIntegerProperty = {\n  min: number;\n  max: number;\n  scale: number;\n  step: number;\n  unit: string;\n};\n\nexport type TuyaDeviceSchemaEnumProperty = {\n  range: string[];\n};\n\nexport type TuyaDeviceSchemaStringProperty = string;\n\nexport type TuyaDeviceSchemaJSONProperty = object;\n\nexport type TuyaDeviceSchemaProperty = TuyaDeviceSchemaIntegerProperty\n  | TuyaDeviceSchemaEnumProperty\n  | TuyaDeviceSchemaStringProperty\n  | TuyaDeviceSchemaJSONProperty;\n\nexport type TuyaDeviceSchema = {\n  code: string;\n  // name: string;\n  mode: TuyaDeviceSchemaMode;\n  type: TuyaDeviceSchemaType;\n  property: TuyaDeviceSchemaProperty;\n};\n\nexport type TuyaDeviceStatus = {\n  code: string;\n  value: string | number | boolean;\n};\n\nexport type TuyaIRRemoteKeyListItem = {\n  key: string;\n  key_id: number;\n  key_name: string;\n  standard_key: boolean;\n  learning_code?: string; // IR DIY device learning code.\n};\n\nexport type TuyaIRRemoteTempListItem = {\n  temp: number;\n  temp_name: string;\n  fan_list: TuyaIRRemoteFanListItem[];\n};\n\nexport type TuyaIRRemoteKeyRangeItem = {\n  mode: number;\n  mode_name: string;\n  temp_list: TuyaIRRemoteTempListItem[];\n};\n\nexport type TuyaIRRemoteFanListItem = {\n  fan: number;\n  fan_name: string;\n};\n\nexport type TuyaIRRemoteKeys = {\n  category_id: number;\n  brand_id: number;\n  remote_index: number;\n  single_air: boolean;\n  duplicate_power: boolean;\n  key_list: TuyaIRRemoteKeyListItem[];\n  key_range: TuyaIRRemoteKeyRangeItem[];\n};\n\nexport default class TuyaDevice {\n\n  // device\n  id!: string;\n  uuid!: string;\n  name!: string;\n  online!: boolean;\n  owner_id!: string; // homeID or assetID\n\n  // product\n  product_id!: string;\n  product_name!: string;\n  icon!: string;\n  category!: string;\n  unbridged?: boolean;\n  schema!: TuyaDeviceSchema[];\n\n  // status\n  status!: TuyaDeviceStatus[];\n\n  // location\n  ip!: string;\n  lat!: string;\n  lon!: string;\n  time_zone!: string;\n\n  // time\n  create_time!: number;\n  active_time!: number;\n  update_time!: number;\n\n  // ...\n  sub!: boolean;\n  parent_id?: string;\n  remote_keys?: TuyaIRRemoteKeys;\n\n  constructor(obj: Partial<TuyaDevice>) {\n    Object.assign(this, obj);\n    this.status.sort((a, b) => a.code > b.code ? 1 : -1);\n  }\n\n  isVirtualDevice() {\n    return this.id.startsWith('vdevo');\n  }\n\n  isIRControlHub() {\n    return ['wnykq', 'hwktwkq', 'wsdykq']\n      .includes(this.category);\n  }\n\n  isIRRemoteControl() {\n    return this.remote_keys !== undefined;\n  }\n\n}\n"
  },
  {
    "path": "src/device/TuyaDeviceManager.ts",
    "content": "import EventEmitter from 'events';\nimport TuyaOpenAPI from '../core/TuyaOpenAPI';\nimport TuyaOpenMQ from '../core/TuyaOpenMQ';\nimport Logger, { PrefixLogger } from '../util/Logger';\nimport TuyaDevice, {\n  TuyaDeviceSchema,\n  TuyaDeviceSchemaMode,\n  TuyaDeviceSchemaProperty,\n  TuyaDeviceStatus,\n} from './TuyaDevice';\n\nenum Events {\n  DEVICE_ADD = 'DEVICE_ADD',\n  DEVICE_INFO_UPDATE = 'DEVICE_INFO_UPDATE',\n  DEVICE_STATUS_UPDATE = 'DEVICE_STATUS_UPDATE',\n  DEVICE_DELETE = 'DEVICE_DELETE',\n}\n\nenum TuyaMQTTProtocol {\n  DEVICE_STATUS_UPDATE = 4,\n  DEVICE_INFO_UPDATE = 20,\n}\n\nexport default class TuyaDeviceManager extends EventEmitter {\n\n  static readonly Events = Events;\n\n  public mq: TuyaOpenMQ;\n  public ownerIDs: string[] = [];\n  public devices: TuyaDevice[] = [];\n  public log: Logger;\n\n  constructor(\n    public api: TuyaOpenAPI,\n    public debug = false,\n  ) {\n    super();\n\n    const log = (this.api.log as PrefixLogger).log;\n    this.log = new PrefixLogger(log, TuyaDeviceManager.name, debug);\n\n    this.mq = new TuyaOpenMQ(api, log);\n    this.mq.addMessageListener(this.onMQTTMessage.bind(this));\n  }\n\n  getDevice(deviceID: string) {\n    return Array.from(this.devices).find(device => device.id === deviceID);\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-unused-vars\n  async updateDevices(ownerIDs: []): Promise<TuyaDevice[]> {\n    return [];\n  }\n\n  async updateDevice(deviceID: string) {\n\n    const res = await this.getDeviceInfo(deviceID);\n    if (!res.success) {\n      return null;\n    }\n\n    const device = new TuyaDevice(res.result);\n    device.schema = await this.getDeviceSchema(deviceID);\n\n    const oldDevice = this.getDevice(deviceID);\n    if (oldDevice) {\n      this.devices.splice(this.devices.indexOf(oldDevice), 1);\n    }\n\n    this.devices.push(device);\n\n    return device;\n  }\n\n  async getDeviceInfo(deviceID: string) {\n    const res = await this.api.get(`/v1.0/devices/${deviceID}`);\n    return res;\n  }\n\n  async getDeviceListInfo(deviceIDs: string[] = []) {\n    const res = await this.api.get('/v1.0/devices', { 'device_ids': deviceIDs.join(',') });\n    return res;\n  }\n\n  async getDeviceSchema(deviceID: string) {\n    // const res = await this.api.get(`/v1.2/iot-03/devices/${deviceID}/specification`);\n    const res = await this.api.get(`/v1.0/devices/${deviceID}/specifications`);\n    if (res.success === false) {\n      this.log.warn('Get device specification failed. devId = %s, code = %s, msg = %s', deviceID, res.code, res.msg);\n      return [];\n    }\n\n    // Combine functions and status together, as it used to be.\n    const schemas = new Map<string, TuyaDeviceSchema>();\n    for (const { code, type, values } of [...res.result.status, ...res.result.functions]) {\n      if (schemas[code]) {\n        continue;\n      }\n\n      const read = (res.result.status).find(schema => schema.code === code) !== undefined;\n      const write = (res.result.functions).find(schema => schema.code === code) !== undefined;\n      let mode = TuyaDeviceSchemaMode.UNKNOWN;\n      if (read && write) {\n        mode = TuyaDeviceSchemaMode.READ_WRITE;\n      } else if (read && !write) {\n        mode = TuyaDeviceSchemaMode.READ_ONLY;\n      } else if (!read && write) {\n        mode = TuyaDeviceSchemaMode.WRITE_ONLY;\n      }\n      let property: TuyaDeviceSchemaProperty;\n      try {\n        property = JSON.parse(values);\n        schemas[code] = { code, mode, type, property };\n      } catch (error) {\n        // ignore infrared remote's invalid schema because it's not used.\n      }\n    }\n\n    return Object.values(schemas).sort((a, b) => a.code > b.code ? 1 : -1) as TuyaDeviceSchema[];\n  }\n\n  async getInfraredRemotes(infraredID: string) {\n    const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes`);\n    return res;\n  }\n\n  async getInfraredKeys(infraredID: string, remoteID: string) {\n    const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/keys`);\n    return res;\n  }\n\n  async getInfraredACStatus(infraredID: string, remoteID: string) {\n    const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/ac/status`);\n    return res;\n  }\n\n  async getInfraredDIYKeys(infraredID: string, remoteID: string) {\n    const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/learning-codes`);\n    return res;\n  }\n\n  async updateInfraredRemotes(allDevices: TuyaDevice[]) {\n\n    const irDevices = allDevices.filter(device => device.isIRControlHub());\n    for (const irDevice of irDevices) {\n      const res = await this.getInfraredRemotes(irDevice.id);\n      if (!res.success) {\n        this.log.warn('Get infrared remotes failed. deviceId = %d, code = %s, msg = %s', irDevice.id, res.code, res.msg);\n        continue;\n      }\n\n      for (const { category_id, remote_id } of res.result) {\n        const subDevice = allDevices.find(device => device.id === remote_id);\n        if (!subDevice) {\n          continue;\n        }\n        subDevice.parent_id = irDevice.id;\n        subDevice.schema = [];\n        const res = await this.getInfraredKeys(irDevice.id, subDevice.id);\n        if (!res.success) {\n          this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg);\n          continue;\n        }\n        subDevice.remote_keys = res.result;\n\n        if (subDevice.category === 'infrared_ac') { // AC Device\n          const res = await this.getInfraredACStatus(irDevice.id, subDevice.id);\n          if (!res.success) {\n            this.log.warn('Get infrared ac status failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg);\n            continue;\n          }\n          subDevice.status = Object.entries(res.result).map(([key, value]) => ({code: key, value} as TuyaDeviceStatus));\n        } else if (category_id === 999) { // DIY Device\n          const res = await this.getInfraredDIYKeys(irDevice.id, subDevice.id);\n          if (!res.success) {\n            this.log.warn('Get infrared diy keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg);\n            continue;\n          }\n          const key_list = subDevice.remote_keys?.key_list || [];\n          for (const key of key_list) {\n            const item = (res.result as []).find(item => item['id'] === key.key_id && item['key'] === key.key);\n            if (!item) {\n              continue;\n            }\n            this.log.debug('learning_code:', item['code']);\n            key.learning_code = item['code'];\n          }\n        }\n      }\n    }\n  }\n\n  async sendInfraredCommands(infraredID: string, remoteID: string, category_id: number, remote_index: number, key: string, key_id: number) {\n    const res = await this.api.post(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/raw/command`, {\n      category_id, remote_index, key, key_id,\n    });\n    return res;\n  }\n\n  async sendInfraredACCommands(infraredID: string, remoteID: string, power: number, mode: number, temp: number, wind: number) {\n    const commands = (power === 1) ? { power, mode, temp, wind } : { power };\n    const res = await this.api.post(`/v2.0/infrareds/${infraredID}/air-conditioners/${remoteID}/scenes/command`, commands);\n    if (!res.success) {\n      this.log.info('Send AC command failed. code = %d, msg = %s', res.code, res.msg);\n    }\n    return res;\n  }\n\n  async sendInfraredDIYCommands(infraredID: string, remoteID: string, code: string) {\n    const res = await this.api.post(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/learning-codes`, { code });\n    return res;\n  }\n\n\n  async getLockTemporaryKey(deviceID: string) {\n    // const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/door-lock/password-ticket`);\n    const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/password-ticket`);\n    if (res.success === false) {\n      this.log.warn('Get Temporary Pass failed. devID = %s, code = %s, msg = %s', deviceID, res.code, res.msg);\n    }\n    return res;\n  }\n\n  async sendLockCommands(deviceID: string, ticketID: string, open: boolean) {\n    const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/password-free/door-operate`, {\n      device_id: deviceID,\n      ticket_id: ticketID,\n      open,\n    });\n    return res;\n  }\n\n\n  async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) {\n    const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands });\n    return res.result;\n  }\n\n\n  async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) {\n    switch(protocol) {\n      case TuyaMQTTProtocol.DEVICE_STATUS_UPDATE: {\n        const { devId, status } = message;\n        const device = this.getDevice(devId);\n        if (!device) {\n          return;\n        }\n\n        for (const item of device.status) {\n          const _item = status.find(_item => _item.code === item.code);\n          if (!_item) {\n            continue;\n          }\n          item.value = _item.value;\n        }\n\n        this.emit(Events.DEVICE_STATUS_UPDATE, device, status);\n        break;\n      }\n      case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: {\n        const { bizCode, bizData, devId } = message;\n        if (bizCode === 'bindUser') {\n          const { ownerId } = bizData;\n          if (!this.ownerIDs.includes(ownerId)) {\n            this.log.warn('Update devId = %s not included in your ownerIDs. Skip.', devId);\n            return;\n          }\n\n          // TODO failed if request to quickly\n          await new Promise(resolve => setTimeout(resolve, 10000));\n\n          const device = await this.updateDevice(devId);\n          if (!device) {\n            return;\n          }\n          this.mq.start(); // Force reconnect, unless new device status update won't get received\n          this.emit(Events.DEVICE_ADD, device);\n        } else if (bizCode === 'nameUpdate') {\n          const { name } = bizData;\n          const device = this.getDevice(devId);\n          if (!device) {\n            return;\n          }\n          device.name = name;\n          this.emit(Events.DEVICE_INFO_UPDATE, device, bizData);\n        } else if (bizCode === 'online' || bizCode === 'offline') {\n          const device = this.getDevice(devId);\n          if (!device) {\n            return;\n          }\n          device.online = (bizCode === 'online') ? true : false;\n          this.emit(Events.DEVICE_INFO_UPDATE, device, bizData);\n        } else if (bizCode === 'delete') {\n          const { ownerId } = bizData;\n          if (!this.ownerIDs.includes(ownerId)) {\n            this.log.warn('Remove devId = %s not included in your ownerIDs. Skip.', devId);\n            return;\n          }\n\n          const device = this.getDevice(devId);\n          if (!device) {\n            return;\n          }\n          this.devices.splice(this.devices.indexOf(device), 1);\n          this.emit(Events.DEVICE_DELETE, devId);\n        } else if (bizCode === 'event_notify') {\n          // doorbell event\n        } else if (bizCode === 'p2pSignal') {\n          // p2p signal\n        } else {\n          this.log.warn('Unhandled mqtt message: bizCode = %s, bizData = %o', bizCode, bizData);\n        }\n        break;\n      }\n      default:\n        this.log.warn('Unhandled mqtt message: protocol = %s, message = %o', protocol, message);\n        break;\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/device/TuyaHomeDeviceManager.ts",
    "content": "import TuyaDevice from './TuyaDevice';\nimport TuyaDeviceManager from './TuyaDeviceManager';\n\nexport default class TuyaHomeDeviceManager extends TuyaDeviceManager {\n\n  async getHomeList() {\n    const res = await this.api.get(`/v1.0/users/${this.api.tokenInfo.uid}/homes`);\n    return res;\n  }\n\n  async getHomeDeviceList(homeID: number) {\n    const res = await this.api.get(`/v1.0/homes/${homeID}/devices`);\n    return res;\n  }\n\n  async updateDevices(homeIDList: number[]) {\n\n    let devices: TuyaDevice[] = [];\n    for (const homeID of homeIDList) {\n      const res = await this.getHomeDeviceList(homeID);\n      devices = devices.concat((res.result as []).map(obj => new TuyaDevice(obj)));\n    }\n    if (devices.length === 0) {\n      return [];\n    }\n\n    for (const device of devices) {\n      device.schema = await this.getDeviceSchema(device.id);\n    }\n\n    // this.log.debug('Devices updated.\\n', JSON.stringify(devices, null, 2));\n    this.devices = devices;\n    return devices;\n  }\n\n  async getSceneList(homeID: number) {\n    const res = await this.api.get(`/v1.1/homes/${homeID}/scenes`);\n    if (res.success === false) {\n      this.log.warn('Get scene list failed. homeId = %d, code = %s, msg = %s', homeID, res.code, res.msg);\n      return [];\n    }\n\n    const scenes: TuyaDevice[] = [];\n    for (const { scene_id, name, enabled, status } of res.result) {\n      if (enabled !== true || status !== '1') {\n        continue;\n      }\n\n      scenes.push(new TuyaDevice({\n        id: scene_id,\n        uuid: scene_id,\n        name,\n        owner_id: homeID.toString(),\n        product_id: 'scene',\n        category: 'scene',\n        schema: [],\n        status: [],\n        online: true,\n      }));\n    }\n    return scenes;\n  }\n\n  async executeScene(homeID: string | number, sceneID: string) {\n    const res = await this.api.post(`/v1.0/homes/${homeID}/scenes/${sceneID}/trigger`);\n    return res;\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { API } from 'homebridge';\n\nimport { PLATFORM_NAME } from './settings';\nimport { TuyaPlatform } from './platform';\n\n/**\n * This method registers the platform with Homebridge\n */\nexport = (api: API) => {\n  api.registerPlatform(PLATFORM_NAME, TuyaPlatform);\n};\n"
  },
  {
    "path": "src/platform.ts",
    "content": "import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge';\nimport { Validator } from 'jsonschema';\nimport path from 'path';\nimport fs from 'fs';\n\nimport TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice';\nimport TuyaDeviceManager from './device/TuyaDeviceManager';\nimport TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager';\nimport TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager';\n\nimport { PLATFORM_NAME, PLUGIN_NAME } from './settings';\nimport { TuyaPlatformConfigOptions, customOptionsSchema, homeOptionsSchema } from './config';\nimport AccessoryFactory from './accessory/AccessoryFactory';\nimport BaseAccessory from './accessory/BaseAccessory';\nimport TuyaOpenAPI, { LOGIN_ERROR_MESSAGES } from './core/TuyaOpenAPI';\n\n\n/**\n * HomebridgePlatform\n * This class is the main constructor for your plugin, this is where you should\n * parse the user config and discover/register accessories with Homebridge.\n */\nexport class TuyaPlatform implements DynamicPlatformPlugin {\n  public readonly Service: typeof Service = this.api.hap.Service;\n  public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;\n\n  public options = this.config.options as TuyaPlatformConfigOptions;\n\n  // this is used to track restored cached accessories\n  public cachedAccessories: PlatformAccessory[] = [];\n\n  public deviceManager?: TuyaDeviceManager;\n  public accessoryHandlers: BaseAccessory[] = [];\n\n  validate() {\n    let result;\n    if (!this.options) {\n      this.log.error('Not configured, exit.');\n      return false;\n    } else if (this.options.projectType === '1') {\n      result = new Validator().validate(this.options, customOptionsSchema);\n    } else if (this.options.projectType === '2') {\n      result = new Validator().validate(this.options, homeOptionsSchema);\n    } else {\n      this.log.error(`Unsupported projectType: ${this.options['projectType']}, exit.`);\n      return false;\n    }\n    result.errors.forEach(error => this.log.error(error.stack));\n    if (result.errors.length > 0) {\n      return false;\n    }\n\n    if (!this.validateDeviceOverrides() || !this.validateSchema()) {\n      return false;\n    }\n\n    return true;\n  }\n\n  validateDeviceOverrides() {\n    if (!this.options.deviceOverrides) {\n      return true;\n    }\n\n    const idMap = new Map();\n    for (const item of this.options.deviceOverrides) {\n      if (idMap.has(item.id)) {\n        idMap.get(item.id)?.push(item);\n      } else {\n        idMap.set(item.id, [item]);\n      }\n    }\n    for (const items of idMap.values()) {\n      if (items.length > 1) {\n        this.log.error('\"deviceOverrides\" conflict, \"id\" must be unique: %o.', items);\n        return false;\n      }\n    }\n    return true;\n  }\n\n  validateSchema() {\n    if (!this.options.deviceOverrides) {\n      return true;\n    }\n\n    for (const deviceOverride of this.options.deviceOverrides) {\n      if (!deviceOverride.schema) {\n        continue;\n      }\n      const idMap = new Map();\n      for (const item of deviceOverride.schema) {\n        if (idMap.has(item.code)) {\n          idMap.get(item.code)?.push(item);\n        } else {\n          idMap.set(item.code, [item]);\n        }\n      }\n      for (const items of idMap.values()) {\n        if (items.length > 1) {\n          this.log.error('\"schema\" conflict, \"code\" must be unique: %o.', items);\n          return false;\n        }\n      }\n    }\n    return true;\n  }\n\n  constructor(\n    public readonly log: Logger,\n    public readonly config: PlatformConfig,\n    public readonly api: API,\n  ) {\n\n    if (!this.validate()) {\n      return;\n    }\n\n    this.log.debug('Finished initializing platform');\n\n    // When this event is fired it means Homebridge has restored all cached accessories from disk.\n    // Dynamic Platform plugins should only register new accessories after this event was fired,\n    // in order to ensure they weren't added to homebridge already. This event can also be used\n    // to start discovery of new accessories.\n    this.api.on('didFinishLaunching', async () => {\n      this.log.debug('Executed didFinishLaunching callback');\n      // run the method to discover / register your devices as accessories\n      await this.initDevices();\n    });\n  }\n\n  /**\n   * This function is invoked when homebridge restores cached accessories from disk at startup.\n   * It should be used to setup event handlers for characteristics and update respective values.\n   */\n  configureAccessory(accessory: PlatformAccessory) {\n    this.log.info('Loading accessory from cache:', accessory.displayName);\n\n    // add the restored accessory to the accessories cache so we can track if it has already been registered\n    this.cachedAccessories.push(accessory);\n  }\n\n  /**\n   * This is an example method showing how to register discovered accessories.\n   * Accessories must only be registered once, previously created accessories\n   * must not be registered again to prevent \"duplicate UUID\" errors.\n   */\n  async initDevices() {\n\n    let devices: TuyaDevice[] | undefined;\n    if (this.options.projectType === '1') {\n      devices = await this.initCustomProject();\n    } else if (this.options.projectType === '2') {\n      devices = await this.initHomeProject();\n    } else {\n      this.log.warn(`Unsupported projectType: ${this.config.options.projectType}.`);\n    }\n\n    if (!devices || !this.deviceManager) {\n      return;\n    }\n\n    // override device category\n    for (const device of devices) {\n      const deviceConfig = this.getDeviceConfig(device);\n      if (!deviceConfig || !deviceConfig.category) {\n        continue;\n      }\n      this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category);\n      device.category = deviceConfig.category;\n    }\n    // override device bridged\n    for (const device of devices) {\n      const deviceConfig = this.getDeviceConfig(device);\n      if (!deviceConfig || !deviceConfig.unbridged) {\n        continue;\n      }\n\n      this.log.warn('Unbridge %o category %o', device.name, device.category );\n      device.unbridged = deviceConfig.unbridged;\n    }\n\n    await this.deviceManager.updateInfraredRemotes(devices);\n\n    this.log.info(`Got ${devices.length} device(s) and scene(s).`);\n    const file = path.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager.api.tokenInfo.uid}.json`);\n    this.log.info('Device list saved at %s', file);\n    if (!fs.existsSync(this.api.user.persistPath())) {\n      await fs.promises.mkdir(this.api.user.persistPath());\n    }\n    await fs.promises.writeFile(file, JSON.stringify(devices, null, 2));\n\n    // add accessories\n    for (const device of devices) {\n      this.addAccessory(device);\n    }\n\n    // remove unused accessories\n    for (const cachedAccessory of this.cachedAccessories) {\n      this.log.warn('Removing unused accessory from cache:', cachedAccessory.displayName);\n      this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedAccessory]);\n    }\n    this.cachedAccessories = [];\n\n    this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_ADD, this.addAccessory.bind(this));\n    this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this));\n    this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this));\n    this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_DELETE, this.removeAccessory.bind(this));\n\n  }\n\n  getDeviceConfig(device: TuyaDevice) {\n    if (!this.options.deviceOverrides) {\n      return undefined;\n    }\n\n    const deviceConfig = this.options.deviceOverrides.find(config => config.id === device.id || config.id === device.uuid);\n    const productConfig = this.options.deviceOverrides.find(config => config.id === device.product_id);\n    const globalConfig = this.options.deviceOverrides.find(config => config.id === 'global');\n\n    return deviceConfig || productConfig || globalConfig;\n  }\n\n  getDeviceSchemaConfig(device: TuyaDevice, code: string) {\n    const deviceConfig = this.getDeviceConfig(device);\n    if (!deviceConfig || !deviceConfig.schema) {\n      return undefined;\n    }\n\n    // migrate old config\n    deviceConfig.schema.forEach(item => {\n      if (item['oldCode']) {\n        item.newCode = item.code;\n        item.code = item['oldCode'];\n        item['oldCode'] = undefined;\n      }\n    });\n\n    const schemaConfig = deviceConfig.schema.find(item => item.newCode ? item.newCode === code : item.code === code);\n    if (!schemaConfig) {\n      return undefined;\n    }\n\n    return schemaConfig;\n  }\n\n  async initCustomProject() {\n    if (this.options.projectType !== '1') {\n      return undefined;\n    }\n\n    const DEFAULT_USER = 'homebridge';\n    const DEFAULT_PASS = 'homebridge';\n\n    let res;\n    const { endpoint, accessId, accessKey, debug, debugLevel } = this.options;\n    const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true);\n    const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log, 'en', debugMode);\n    const deviceManager = new TuyaCustomDeviceManager(api, debugMode);\n\n    this.log.info('Get token.');\n    res = await api.getToken();\n    if (res.success === false) {\n      this.log.error(`Get token failed. code=${res.code}, msg=${res.msg}`);\n      return undefined;\n    }\n\n\n    this.log.info(`Search default user \"${DEFAULT_USER}\"`);\n    res = await api.customGetUserInfo(DEFAULT_USER);\n    if (res.success === false) {\n      this.log.error(`Search user failed. code=${res.code}, msg=${res.msg}`);\n      return undefined;\n    }\n\n\n    if (!res.result.user_name) {\n      this.log.info(`Default user \"${DEFAULT_USER}\" not exist.`);\n      this.log.info(`Creating default user \"${DEFAULT_USER}\".`);\n      res = await api.customCreateUser(DEFAULT_USER, DEFAULT_PASS);\n      if (res.success === false) {\n        this.log.error(`Create default user failed. code=${res.code}, msg=${res.msg}`);\n        return undefined;\n      }\n    } else {\n      this.log.info(`Default user \"${DEFAULT_USER}\" exists.`);\n    }\n    const uid = res.result.user_id;\n\n\n    this.log.info('Fetching asset list.');\n    res = await deviceManager.getAssetList();\n    if (res.success === false) {\n      this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`);\n      return undefined;\n    }\n\n    const assetIDList: string[] = [];\n    for (const { asset_id, asset_name } of res.result.list) {\n      this.log.info(`Got asset_id=${asset_id}, asset_name=${asset_name}`);\n      assetIDList.push(asset_id);\n    }\n\n    if (assetIDList.length === 0) {\n      this.log.warn('Asset list is empty. exit.');\n      return undefined;\n    }\n\n\n    this.log.info('Authorize asset list.');\n    res = await deviceManager.authorizeAssetList(uid, assetIDList, true);\n    if (res.success === false) {\n      this.log.error(`Authorize asset list failed. code=${res.code}, msg=${res.msg}`);\n      return undefined;\n    }\n\n\n    this.log.info(`Log in with user \"${DEFAULT_USER}\".`);\n    res = await api.customLogin(DEFAULT_USER, DEFAULT_USER);\n    if (res.success === false) {\n      this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`);\n      if (LOGIN_ERROR_MESSAGES[res.code]) {\n        this.log.error(LOGIN_ERROR_MESSAGES[res.code]);\n      }\n      return undefined;\n    }\n\n    this.log.info('Start MQTT connection.');\n    deviceManager.mq.start();\n\n    this.log.info('Fetching device list.');\n    deviceManager.ownerIDs = assetIDList;\n    const devices = await deviceManager.updateDevices(assetIDList);\n\n    this.deviceManager = deviceManager;\n    return devices;\n  }\n\n  async initHomeProject() {\n    if (this.options.projectType !== '2') {\n      return undefined;\n    }\n\n    let res;\n    const { accessId, accessKey, countryCode, username, password, appSchema, endpoint, debug, debugLevel } = this.options;\n    const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true);\n    const api = new TuyaOpenAPI(\n      (endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI.getDefaultEndpoint(countryCode),\n      accessId,\n      accessKey,\n      this.log,\n      'en',\n      debugMode);\n    const deviceManager = new TuyaHomeDeviceManager(api, debugMode);\n\n    this.log.info('Log in to Tuya Cloud.');\n    res = await api.homeLogin(countryCode, username, password, appSchema);\n    if (res.success === false) {\n      this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`);\n      if (LOGIN_ERROR_MESSAGES[res.code]) {\n        this.log.error(LOGIN_ERROR_MESSAGES[res.code]);\n      }\n      return undefined;\n    }\n\n    this.log.info('Start MQTT connection.');\n    deviceManager.mq.start();\n\n    this.log.info('Fetching home list.');\n    res = await deviceManager.getHomeList();\n    if (res.success === false) {\n      this.log.error(`Fetching home list failed. code=${res.code}, msg=${res.msg}`);\n      return undefined;\n    }\n\n    const homeIDList: number[] = [];\n    for (const { home_id, name } of res.result) {\n      this.log.info(`Got home_id=${home_id}, name=${name}`);\n      if (this.options.homeWhitelist) {\n        if (this.options.homeWhitelist.includes(home_id)) {\n          this.log.info(`Found home_id=${home_id} in whitelist; including devices from this home.`);\n          homeIDList.push(home_id);\n        } else {\n          this.log.info(`Did not find home_id=${home_id} in whitelist; excluding devices from this home.`);\n        }\n      } else {\n        homeIDList.push(home_id);\n      }\n    }\n\n    if (homeIDList.length === 0) {\n      this.log.warn('Home list is empty.');\n    }\n\n    this.log.info('Fetching device list.');\n    deviceManager.ownerIDs = homeIDList.map(homeID =>homeID.toString());\n    const devices = await deviceManager.updateDevices(homeIDList);\n\n    this.log.info('Fetching scene list.');\n    for (const homeID of homeIDList) {\n      const scenes = await deviceManager.getSceneList(homeID);\n      for (const scene of scenes) {\n        this.log.info(`Got scene_id=${scene.id}, name=${scene.name}`);\n      }\n      devices.push(...scenes);\n    }\n\n    this.deviceManager = deviceManager;\n    return devices;\n  }\n\n  addAccessory(device: TuyaDevice) {\n    if (device.category === 'hidden') {\n      this.log.info('Hide Accessory:', device.name);\n      return;\n    }\n\n    const uuid = this.api.hap.uuid.generate(device.id);\n    const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid);\n    if (existingAccessory && !device.unbridged) {\n      this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName);\n\n      // Update context\n      if (!existingAccessory.context || !existingAccessory.context.deviceID) {\n        this.log.info('Update accessory context:', existingAccessory.displayName);\n        existingAccessory.context.deviceID = device.id;\n        this.api.updatePlatformAccessories([existingAccessory]);\n      }\n\n      // create the accessory handler for the restored accessory\n      const handler = AccessoryFactory.createAccessory(this, existingAccessory, device);\n      this.accessoryHandlers.push(handler);\n\n      const index = this.cachedAccessories.indexOf(existingAccessory);\n      if (index >= 0) {\n        this.cachedAccessories.splice(index, 1);\n      }\n\n    } else {\n      // the accessory does not yet exist, so we need to create it\n      this.log.info('Adding new accessory:', device.name);\n\n      // create a new accessory\n      const accessory = new this.api.platformAccessory(device.name, uuid);\n      accessory.context.deviceID = device.id;\n\n      // create the accessory handler for the newly create accessory\n      const handler = AccessoryFactory.createAccessory(this, accessory, device);\n      this.accessoryHandlers.push(handler);\n\n      // link the accessory to your platform\n      if (device.unbridged) {\n        this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);\n      } else {\n        this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);\n      }\n    }\n  }\n\n  updateAccessoryInfo(device: TuyaDevice, info) {\n    const handler = this.getAccessoryHandler(device.id);\n    if (!handler) {\n      return;\n    }\n\n    // this.log.debug('onDeviceInfoUpdate devId = %s, status = %o}', device.id, info);\n    handler.onDeviceInfoUpdate(info);\n  }\n\n  updateAccessoryStatus(device: TuyaDevice, status: TuyaDeviceStatus[]) {\n    const handler = this.getAccessoryHandler(device.id);\n    if (!handler) {\n      return;\n    }\n\n    // this.log.debug('onDeviceStatusUpdate devId = %s, status = %o}', device.id, status);\n    handler.onDeviceStatusUpdate(status);\n  }\n\n  removeAccessory(deviceID: string) {\n    const handler = this.getAccessoryHandler(deviceID);\n    if (!handler) {\n      return;\n    }\n\n    const index = this.accessoryHandlers.indexOf(handler);\n    if (index >= 0) {\n      this.accessoryHandlers.splice(index, 1);\n    }\n\n    this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [handler.accessory]);\n    this.log.info('Removing existing accessory from cache:', handler.accessory.displayName);\n  }\n\n  getAccessoryHandler(deviceID: string) {\n    return this.accessoryHandlers.find(handler => handler.device.id === deviceID);\n  }\n\n}\n"
  },
  {
    "path": "src/settings.ts",
    "content": "// eslint-disable-next-line\n// @ts-ignore\nimport { pluginAlias as platformName } from '../config.schema.json';\n\n// eslint-disable-next-line\n// @ts-ignore\nimport { name as pluginName } from '../package.json';\n\n/**\n * This is the name of the platform that users will use to register the plugin in the Homebridge config.json\n */\nexport const PLATFORM_NAME = platformName;\n\n/**\n * This must match the name of your plugin as defined the package.json\n */\nexport const PLUGIN_NAME = pluginName;\n"
  },
  {
    "path": "src/util/FfmpegStreamingProcess.ts",
    "content": "import {\n  ChildProcessWithoutNullStreams,\n  spawn,\n} from 'child_process';\nimport {\n  StreamRequestCallback,\n  StreamSessionIdentifier,\n} from 'homebridge';\nimport os from 'os';\nimport readline from 'readline';\nimport { Writable } from 'stream';\nimport { PrefixLogger } from './Logger';\n\nexport interface StreamingDelegate {\n    stopStream(sessionId: StreamSessionIdentifier): void;\n    forceStopStream(sessionId: StreamSessionIdentifier): void;\n}\n\ntype FfmpegProgress = {\n    frame: number;\n    fps: number;\n    stream_q: number;\n    bitrate: number;\n    total_size: number;\n    out_time_us: number;\n    out_time: string;\n    dup_frames: number;\n    drop_frames: number;\n    speed: number;\n    progress: string;\n};\n\nexport class FfmpegStreamingProcess {\n  private readonly process: ChildProcessWithoutNullStreams;\n  private killTimeout?: NodeJS.Timeout;\n  readonly stdin: Writable;\n\n  constructor(\n    sessionId: string,\n    videoProcessor: string,\n    ffmpegArgs: string[],\n    log: PrefixLogger,\n    delegate: StreamingDelegate,\n    callback?: StreamRequestCallback,\n  ) {\n\n    log.debug(`Stream command: ${videoProcessor} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`);\n\n    let started = false;\n    const startTime = Date.now();\n\n    this.process = spawn(videoProcessor, ffmpegArgs, { env: process.env });\n\n    this.stdin = this.process.stdin;\n\n    this.process.stdout.on('data', (data) => {\n      const progress = this.parseProgress(data);\n      if (progress) {\n        if (!started && progress.frame > 0) {\n          started = true;\n          const runtime = (Date.now() - startTime) / 1000;\n          const message = 'Getting the first frames took ' + runtime + ' seconds.';\n          if (runtime < 5) {\n            log.debug(message);\n          } else if (runtime < 22) {\n            log.warn(message);\n          } else {\n            log.error(message);\n          }\n        }\n      }\n    });\n    const stderr = readline.createInterface({\n      input: this.process.stderr,\n      terminal: false,\n    });\n    stderr.on('line', (line: string) => {\n      if (callback) {\n        callback();\n        callback = undefined;\n      }\n      if (line.match(/\\[(panic|fatal|error)\\]/)) {\n        log.error(line);\n      }\n    });\n    this.process.on('error', (error: Error) => {\n      log.error('FFmpeg process creation failed: ' + error.message);\n      if (callback) {\n        callback(new Error('FFmpeg process creation failed'));\n      }\n      delegate.stopStream(sessionId);\n    });\n    this.process.on('exit', (code: number, signal: NodeJS.Signals) => {\n      if (this.killTimeout) {\n        clearTimeout(this.killTimeout);\n      }\n\n      const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;\n\n      if (this.killTimeout && code === 0) {\n        log.debug(message + ' (Expected)');\n      } else if (code === null || code === 255) {\n        if (this.process.killed) {\n          log.debug(message + ' (Forced)');\n        } else {\n          log.error(message + ' (Unexpected)');\n        }\n      } else {\n        log.error(message + ' (Error)');\n        delegate.stopStream(sessionId);\n        if (!started && callback) {\n          callback(new Error(message));\n        } else {\n          delegate.forceStopStream(sessionId);\n        }\n      }\n    });\n  }\n\n  parseProgress(data: Uint8Array): FfmpegProgress | undefined {\n    const input = data.toString();\n\n    if (input.indexOf('frame=') === 0) {\n      try {\n        const progress = new Map<string, string>();\n        input.split(/\\r?\\n/).forEach((line) => {\n          const split = line.split('=', 2);\n          progress.set(split[0], split[1]);\n        });\n\n        return {\n          frame: parseInt(progress.get('frame')!),\n          fps: parseFloat(progress.get('fps')!),\n          stream_q: parseFloat(progress.get('stream_0_0_q')!),\n          bitrate: parseFloat(progress.get('bitrate')!),\n          total_size: parseInt(progress.get('total_size')!),\n          out_time_us: parseInt(progress.get('out_time_us')!),\n          out_time: progress.get('out_time')!.trim(),\n          dup_frames: parseInt(progress.get('dup_frames')!),\n          drop_frames: parseInt(progress.get('drop_frames')!),\n          speed: parseFloat(progress.get('speed')!),\n          progress: progress.get('progress')!.trim(),\n        };\n      } catch {\n        return undefined;\n      }\n    } else {\n      return undefined;\n    }\n  }\n\n  getStdin() {\n    return this.process.stdin;\n  }\n\n  public stop(): void {\n    this.process.stdin.write('q' + os.EOL);\n    this.killTimeout = setTimeout(() => {\n      this.process.kill('SIGKILL');\n    }, 2 * 1000);\n  }\n}\n"
  },
  {
    "path": "src/util/Logger.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport default interface Logger {\n  info(message?: any, ...args: any[]): void;\n  warn(message?: any, ...args: any[]): void;\n  debug(message?: any, ...args: any[]): void;\n  error(message?: any, ...args: any[]): void;\n}\n\nexport class PrefixLogger {\n  constructor(\n    public log: Logger,\n    public prefix: string,\n    public debugMode = false,\n  ) {\n    this.debugMode = this.debugMode || process.argv.includes('-D') || process.argv.includes('--debug');\n  }\n\n  debug(message?: any, ...args: any[]) {\n    if (this.debugMode) {\n      this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);\n    } else {\n      this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);\n    }\n  }\n\n  info(message?: any, ...args: any[]) {\n    this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);\n  }\n\n  warn(message?: any, ...args: any[]) {\n    this.log.warn((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);\n  }\n\n  error(message?: any, ...args: any[]) {\n    this.log.error((this.prefix ? `[${this.prefix}] ` : '') + message, ...args);\n  }\n\n}\n"
  },
  {
    "path": "src/util/TuyaRecordingDelegate.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\nimport {\n  CameraRecordingConfiguration,\n  CameraRecordingDelegate,\n  HDSProtocolSpecificErrorReason,\n  RecordingPacket,\n} from 'homebridge';\n\nexport class TuyaRecordingDelegate implements CameraRecordingDelegate {\n  updateRecordingActive(active: boolean): void {\n    throw new Error('Method not implemented.');\n  }\n\n  updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void {\n    throw new Error('Method not implemented.');\n  }\n\n  handleRecordingStreamRequest(streamId: number): AsyncGenerator<RecordingPacket> {\n    throw new Error('Method not implemented.');\n  }\n\n  acknowledgeStream?(streamId: number): void {\n    throw new Error('Method not implemented.');\n  }\n\n  closeRecordingStream(streamId: number, reason: HDSProtocolSpecificErrorReason | undefined): void {\n    throw new Error('Method not implemented.');\n  }\n}\n"
  },
  {
    "path": "src/util/TuyaStreamDelegate.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unused-vars */\n/* eslint-disable max-len */\n\nimport {\n  AudioStreamingCodecType,\n  AudioStreamingSamplerate,\n  CameraController,\n  CameraControllerOptions,\n  CameraRecordingOptions,\n  CameraStreamingDelegate,\n  CameraStreamingOptions,\n  EventTriggerOption,\n  HAP,\n  H264Level,\n  H264Profile,\n  MediaContainerType,\n  PrepareStreamCallback,\n  PrepareStreamRequest,\n  Resolution,\n  SnapshotRequest,\n  SnapshotRequestCallback,\n  SRTPCryptoSuites,\n  StreamingRequest,\n  StreamRequestCallback,\n  PrepareStreamResponse,\n  StartStreamRequest,\n} from 'homebridge';\n\nimport {\n  defaultFfmpegPath,\n  reservePorts,\n} from '@homebridge/camera-utils';\n\nimport CameraAccessory from '../accessory/CameraAccessory';\n\nimport {\n  TuyaRecordingDelegate,\n} from './TuyaRecordingDelegate';\nimport { spawn } from 'child_process';\nimport { createSocket, Socket } from 'dgram';\nimport { FfmpegStreamingProcess, StreamingDelegate as FfmpegStreamingDelegate } from './FfmpegStreamingProcess';\n\ninterface SessionInfo {\n    address: string; // address of the HAP controller\n    addressVersion: 'ipv4' | 'ipv6';\n\n    videoPort: number;\n    videoIncomingPort: number;\n    videoCryptoSuite: SRTPCryptoSuites; // should be saved if multiple suites are supported\n    videoSRTP: Buffer; // key and salt concatenated\n    videoSSRC: number; // rtp synchronisation source\n\n    audioPort: number;\n    audioIncomingPort: number;\n    audioCryptoSuite: SRTPCryptoSuites;\n    audioSRTP: Buffer;\n    audioSSRC: number;\n}\n\ntype ActiveSession = {\n    mainProcess?: FfmpegStreamingProcess;\n    returnProcess?: FfmpegStreamingProcess;\n    timeout?: NodeJS.Timeout;\n    socket?: Socket;\n};\n\n/*\ninterface SampleRateEntry {\n    type: AudioRecordingCodecType;\n    bitrateMode: number;\n    samplerate: AudioRecordingSamplerate[];\n    audioChannels: number;\n}\n*/\n\nexport class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStreamingDelegate {\n  public readonly controller: CameraController;\n\n  private pendingSessions: { [index: string]: SessionInfo } = {};\n  private ongoingSessions: { [index: string]: ActiveSession } = {};\n\n  private readonly camera: CameraAccessory;\n  private readonly hap: HAP;\n  constructor(camera: CameraAccessory) {\n    this.camera = camera;\n    this.hap = camera.platform.api.hap;\n\n    // this.recordingDelegate = new TuyaRecordingDelegate();\n\n    const resolutions: Resolution[] = [\n      [320, 180, 30],\n      [320, 240, 15],\n      [320, 240, 30],\n      [480, 270, 30],\n      [480, 360, 30],\n      [640, 360, 30],\n      [640, 480, 30],\n      [1280, 720, 30],\n      [1280, 960, 30],\n      [1920, 1080, 30],\n      [1600, 1200, 30],\n    ];\n\n    const streamingOptions: CameraStreamingOptions = {\n      supportedCryptoSuites: [SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80],\n      video: {\n        codec: {\n          profiles: [H264Profile.BASELINE, H264Profile.MAIN, H264Profile.HIGH],\n          levels: [H264Level.LEVEL3_1, H264Level.LEVEL3_2, H264Level.LEVEL4_0],\n        },\n        resolutions: resolutions,\n      },\n      audio: {\n        twoWayAudio: false,\n        codecs: [\n          {\n            type: AudioStreamingCodecType.AAC_ELD,\n            samplerate: AudioStreamingSamplerate.KHZ_16,\n          },\n        ],\n      },\n    };\n\n    const recordingOptions: CameraRecordingOptions = {\n      overrideEventTriggerOptions: [\n        EventTriggerOption.MOTION,\n        EventTriggerOption.DOORBELL,\n      ],\n      prebufferLength: 4 * 1000, // prebufferLength always remains 4s ?\n      mediaContainerConfiguration: [\n        {\n          type: MediaContainerType.FRAGMENTED_MP4,\n          fragmentLength: 4000,\n        },\n      ],\n      video: {\n        parameters: {\n          profiles: [\n            H264Profile.BASELINE,\n            H264Profile.MAIN,\n            H264Profile.HIGH,\n          ],\n          levels: [\n            H264Level.LEVEL3_1,\n            H264Level.LEVEL3_2,\n            H264Level.LEVEL4_0,\n          ],\n        },\n        resolutions: resolutions,\n        type: this.hap.VideoCodecType.H264,\n      },\n      audio: {\n        codecs: [\n          {\n            samplerate: this.hap.AudioRecordingSamplerate.KHZ_32,\n            type: this.hap.AudioRecordingCodecType.AAC_LC,\n          },\n        ],\n      },\n    };\n\n    const options: CameraControllerOptions = {\n      delegate: this,\n      streamingOptions: streamingOptions,\n      // recording: {\n      // options: recordingOptions,\n      // delegate: this.recordingDelegate\n      // }\n    };\n\n    this.controller = new this.hap.CameraController(options);\n  }\n\n  stopStream(sessionId: string): void {\n    const session = this.ongoingSessions[sessionId];\n\n    if (session) {\n      if (session.timeout) {\n        clearTimeout(session.timeout);\n      }\n\n      try {\n        session.socket?.close();\n      } catch (error) {\n        this.camera.log.error(`Error occurred closing socket: ${error}`);\n      }\n\n      try {\n        session.mainProcess?.stop();\n      } catch (error) {\n        this.camera.log.error(`Error occurred terminating main FFmpeg process: ${error}`);\n      }\n\n      try {\n        session.returnProcess?.stop();\n      } catch (error) {\n        this.camera.log.error(`Error occurred terminating two-way FFmpeg process: ${error}`);\n      }\n\n      delete this.ongoingSessions[sessionId];\n\n      this.camera.log.info('Stopped video stream.');\n    }\n  }\n\n  forceStopStream(sessionId: string) {\n    this.controller.forceStopStreamingSession(sessionId);\n  }\n\n  async handleSnapshotRequest(\n    request: SnapshotRequest,\n    callback: SnapshotRequestCallback,\n  ) {\n    try {\n      this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`);\n\n      const snapshot = await this.fetchSnapshot();\n\n      this.camera.log.debug('Sending snapshot');\n\n      callback(undefined, snapshot);\n    } catch (error) {\n      callback(error as Error);\n    }\n  }\n\n  async prepareStream(\n    request: PrepareStreamRequest,\n    callback: PrepareStreamCallback,\n  ) {\n    const videoIncomingPort = await reservePorts({\n      count: 1,\n    });\n    const videoSSRC = this.hap.CameraController.generateSynchronisationSource();\n\n    const audioIncomingPort = await reservePorts({\n      count: 1,\n    });\n    const audioSSRC = this.hap.CameraController.generateSynchronisationSource();\n\n    const sessionInfo: SessionInfo = {\n      address: request.targetAddress,\n      addressVersion: request.addressVersion,\n\n      audioCryptoSuite: request.audio.srtpCryptoSuite,\n      audioPort: request.audio.port,\n      audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]),\n      audioSSRC: audioSSRC,\n      audioIncomingPort: audioIncomingPort[0],\n\n      videoCryptoSuite: request.video.srtpCryptoSuite,\n      videoPort: request.video.port,\n      videoSRTP: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]),\n      videoSSRC: videoSSRC,\n      videoIncomingPort: videoIncomingPort[0],\n    };\n\n    const response: PrepareStreamResponse = {\n      video: {\n        port: sessionInfo.videoIncomingPort,\n        ssrc: videoSSRC,\n        srtp_key: request.video.srtp_key,\n        srtp_salt: request.video.srtp_salt,\n      },\n      audio: {\n        port: sessionInfo.audioIncomingPort,\n        ssrc: audioSSRC,\n        srtp_key: request.audio.srtp_key,\n        srtp_salt: request.audio.srtp_salt,\n      },\n    };\n\n    this.pendingSessions[request.sessionID] = sessionInfo;\n    callback(undefined, response);\n  }\n\n  async handleStreamRequest(\n    request: StreamingRequest,\n    callback: StreamRequestCallback,\n  ) {\n    switch (request.type) {\n      case this.hap.StreamRequestTypes.START: {\n        this.camera.log.debug(`Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`);\n\n        await this.startStream(request, callback);\n        break;\n      }\n\n      case this.hap.StreamRequestTypes.RECONFIGURE: {\n        this.camera.log.debug(`Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`);\n\n        callback();\n        break;\n      }\n\n      case this.hap.StreamRequestTypes.STOP: {\n        this.camera.log.debug('Stop stream requested');\n\n        this.stopStream(request.sessionID);\n        callback();\n        break;\n      }\n    }\n  }\n\n  private async retrieveDeviceRTSP(): Promise<string> {\n    const data = await this.camera.deviceManager.api.post(\n      `/v1.0/devices/${this.camera.device.id}/stream/actions/allocate`,\n      {\n        type: 'rtsp',\n      },\n    );\n\n    return data.result.url;\n  }\n\n  private async startStream(request: StartStreamRequest, callback: StreamRequestCallback) {\n    const sessionInfo = this.pendingSessions[request.sessionID];\n\n    if (!sessionInfo) {\n      this.camera.log.error('Error finding session information.');\n      callback(new Error('Error finding session information'));\n    }\n\n    const vcodec = 'libx264';\n    const mtu = 1316; // request.video.mtu is not used\n\n    const fps = request.video.fps;\n    const videoBitrate = request.video.max_bit_rate;\n\n    const rtspUrl = await this.retrieveDeviceRTSP();\n\n    const ffmpegArgs: string[] = [\n      '-hide_banner',\n      '-loglevel', 'verbose',\n      '-i', rtspUrl,\n      '-an', '-sn', '-dn',\n      '-r', fps.toString(),\n      '-codec:v', vcodec,\n      '-pix_fmt', 'yuv420p',\n      '-color_range', 'mpeg',\n      '-f', 'rawvideo',\n    ];\n\n    const encoderOptions = '-preset ultrafast -tune zerolatency';\n\n    if (encoderOptions) {\n      ffmpegArgs.push(...encoderOptions.split(/\\s+/));\n    }\n\n    if (videoBitrate > 0) {\n      ffmpegArgs.push('-b:v', `${videoBitrate}k`);\n    }\n\n    // Video Stream\n\n    ffmpegArgs.push(\n      '-payload_type', `${request.video.pt}`,\n      '-ssrc', `${sessionInfo.videoSSRC}`,\n      '-f', 'rtp',\n      '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',\n      '-srtp_out_params', sessionInfo.videoSRTP.toString('base64'),\n      `srtp://${sessionInfo.address}:${sessionInfo.videoPort}?rtcpport=${sessionInfo.videoPort}&pkt_size=${mtu}`,\n    );\n\n    // Setting up audio\n\n    if (\n      request.audio.codec === AudioStreamingCodecType.OPUS ||\n            request.audio.codec === AudioStreamingCodecType.AAC_ELD\n    ) {\n      ffmpegArgs.push('-vn', '-sn', '-dn');\n\n      if (request.audio.codec === AudioStreamingCodecType.OPUS) {\n        ffmpegArgs.push('-acodec', 'libopus', '-application', 'lowdelay');\n      } else {\n        ffmpegArgs.push('-acodec', 'libfdk_aac', '-profile:a', 'aac_eld');\n      }\n\n      ffmpegArgs.push(\n        '-flags', '+global_header',\n        '-f', 'null',\n        '-ar', `${request.audio.sample_rate}k`,\n        '-b:a', `${request.audio.max_bit_rate}k`,\n        '-ac', `${request.audio.channel}`,\n        '-payload_type', `${request.audio.pt}`,\n        '-ssrc', `${sessionInfo.audioSSRC}`,\n        '-f', 'rtp',\n        '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',\n        '-srtp_out_params', sessionInfo.audioSRTP.toString('base64'),\n        `srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`,\n      );\n    } else {\n      this.camera.log.error(`Unsupported audio codec requested: ${request.audio.codec}`);\n    }\n\n    ffmpegArgs.push('-progress', 'pipe:1');\n\n    const activeSession: ActiveSession = {};\n\n    activeSession.socket = createSocket(sessionInfo.addressVersion === 'ipv6' ? 'udp6' : 'udp4');\n\n    activeSession.socket.on('error', (err: Error) => {\n      this.camera.log.error('Socket error: ' + err.message);\n      this.stopStream(request.sessionID);\n    });\n\n    activeSession.socket.on('message', () => {\n      if (activeSession.timeout) {\n        clearTimeout(activeSession.timeout);\n      }\n      activeSession.timeout = setTimeout(() => {\n        this.camera.log.info('Device appears to be inactive. Stopping stream.');\n        this.controller.forceStopStreamingSession(request.sessionID);\n        this.stopStream(request.sessionID);\n      }, request.video.rtcp_interval * 5 * 1000);\n    });\n\n    activeSession.socket.bind(sessionInfo.videoIncomingPort);\n\n    activeSession.mainProcess = new FfmpegStreamingProcess(\n      request.sessionID,\n      defaultFfmpegPath,\n      ffmpegArgs,\n      this.camera.log,\n      this,\n      callback,\n    );\n\n    this.ongoingSessions[request.sessionID] = activeSession;\n    delete this.pendingSessions[request.sessionID];\n  }\n\n  private async fetchSnapshot(): Promise<Buffer> {\n    if (!this.camera.device.online) {\n      this.camera.log.debug('Device is currently offline.');\n      throw new Error('Device is currently offline.');\n    }\n\n    // TODO: Check if there is a stream already running to fetch snapshot.\n\n    const rtspUrl = await this.retrieveDeviceRTSP();\n\n    const ffmpegArgs = [\n      '-i', rtspUrl,\n      '-frames:v', '1',\n      '-hide_banner',\n      '-loglevel',\n      'error',\n      '-f',\n      'image2',\n      '-',\n    ];\n\n    return new Promise((resolve, reject) => {\n\n      this.camera.log.debug(`Running Snapshot command: ${defaultFfmpegPath} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`);\n\n      const ffmpeg = spawn(\n        defaultFfmpegPath,\n        ffmpegArgs.map(x => x.toString()),\n        { env: process.env },\n      );\n\n      let errors: string[] = [];\n\n      let snapshotBuffer = Buffer.alloc(0);\n\n      ffmpeg.stdout.on('data', (data) => {\n        snapshotBuffer = Buffer.concat([snapshotBuffer, data]);\n      });\n\n      ffmpeg.on('error', (error) => {\n        this.camera.log.error(`FFmpeg process creation failed: ${error.message} - Showing \"offline\" image instead.`);\n        reject('Failed to fetch snapshot.');\n      });\n\n      ffmpeg.stderr.on('data', (data) => {\n        errors = errors.slice(-5);\n        errors.push(data.toString().replace(/(\\r\\n|\\n|\\r)/gm, ' '));\n      });\n\n      ffmpeg.on('close', () => {\n        if (snapshotBuffer.length > 0) {\n          resolve(snapshotBuffer);\n        } else {\n          this.camera.log.error('Failed to fetch snapshot. Showing \"offline\" image instead.');\n\n          if (errors.length > 0) {\n            this.camera.log.error(errors.join(' - '));\n          }\n\n          reject('Unable to fetch snapshot.');\n        }\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "src/util/color.ts",
    "content": "import convert from 'color-convert';\nimport kelvinToRgb from 'kelvin-to-rgb';\n\nexport function kelvinToHSV(kevin: number) {\n  const [r, g, b] = kelvinToRgb(kevin);\n  const [h, s, v] = convert.rgb.hsv(r, g, b);\n  return { h, s, v };\n}\n\n// https://en.wikipedia.org/wiki/Mired\nexport function kelvinToMired(kelvin: number) {\n  return 1e6 / kelvin;\n}\n\nexport function miredToKelvin(mired: number) {\n  return 1e6 / mired;\n}\n"
  },
  {
    "path": "src/util/util.ts",
    "content": "export function remap(\n  value: number,\n  srcStart: number,\n  srcEnd: number,\n  dstStart: number,\n  dstEnd: number,\n) {\n  const percent = (value - srcStart) / (srcEnd - srcStart);\n  const result = percent * (dstEnd - dstStart) + dstStart;\n  return result;\n}\n\nexport function limit(\n  value: number,\n  start: number,\n  end: number,\n) {\n  let result = value;\n  result = Math.min(end, result);\n  result = Math.max(start, result);\n  return result;\n}\n"
  },
  {
    "path": "test/FanAccessory.test.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { describe, expect, test, jest, beforeEach } from '@jest/globals';\nimport { API, PlatformAccessory } from 'homebridge';\nimport FanAccessory from '../src/accessory/FanAccessory';\nimport TuyaDevice, { TuyaDeviceSchemaMode, TuyaDeviceSchemaType } from '../src/device/TuyaDevice';\nimport { TuyaPlatform } from '../src/platform';\n\nconst mockConfigureLight = jest.fn();\nconst mockConfigureOn = jest.fn();\njest.mock('../src/accessory/characteristic/Light', () => ({\n  configureLight: (...args: any[]) => mockConfigureLight(...args),\n}));\njest.mock('../src/accessory/characteristic/On', () => ({\n  configureOn: (...args: any[]) => mockConfigureOn(...args),\n}));\njest.mock('../src/accessory/characteristic/Active');\njest.mock('../src/accessory/characteristic/RotationSpeed');\njest.mock('../src/accessory/characteristic/SwingMode');\njest.mock('../src/accessory/characteristic/LockPhysicalControls');\n\ndescribe('FanAccessory', () => {\n  let mockPlatform: any;\n  let mockAccessory: any;\n  let mockAPI: any;\n  let mockDeviceManager: any;\n\n  beforeEach(() => {\n    mockConfigureLight.mockClear();\n    mockConfigureOn.mockClear();\n\n    mockAPI = {\n      hap: {\n        Service: {\n          Fan: jest.fn(),\n          Fanv2: jest.fn(),\n          Lightbulb: jest.fn(),\n          Switch: jest.fn(),\n          AccessoryInformation: jest.fn(),\n        },\n        Characteristic: {\n          On: jest.fn(),\n          Active: jest.fn(),\n          RotationSpeed: jest.fn(),\n          RotationDirection: jest.fn(),\n          Brightness: jest.fn(),\n        },\n        uuid: {\n          generate: jest.fn(() => 'mock-uuid'),\n        },\n      },\n      user: {\n        persistPath: jest.fn(() => '/mock/path'),\n      },\n    } as unknown as API;\n\n    mockDeviceManager = {\n      getDevice: jest.fn(),\n      sendCommands: jest.fn(),\n    };\n\n    mockPlatform = {\n      api: mockAPI,\n      Service: mockAPI.hap.Service,\n      Characteristic: mockAPI.hap.Characteristic,\n      log: {\n        info: jest.fn(),\n        warn: jest.fn(),\n        error: jest.fn(),\n        debug: jest.fn(),\n      },\n      options: {\n        debug: false,\n        debugLevel: '',\n      },\n      deviceManager: mockDeviceManager,\n      getDeviceConfig: jest.fn(() => undefined),\n      getDeviceSchemaConfig: jest.fn(() => undefined),\n    } as unknown as TuyaPlatform;\n\n    mockAccessory = {\n      UUID: 'mock-uuid',\n      displayName: 'Test Fan',\n      context: {\n        deviceID: 'test-device-id',\n      },\n      services: [],\n      getService: jest.fn((name: string) => {\n        return mockAccessory.services.find((s: any) => s.displayName === name || s.UUID === name);\n      }),\n      addService: jest.fn((serviceType: any, name?: string, subtype?: string) => {\n        const service = {\n          UUID: subtype || name || 'mock-service',\n          displayName: name || 'Mock Service',\n          subtype: subtype,\n          getCharacteristic: jest.fn(() => ({\n            onGet: jest.fn().mockReturnThis(),\n            onSet: jest.fn().mockReturnThis(),\n            setProps: jest.fn().mockReturnThis(),\n            updateValue: jest.fn().mockReturnThis(),\n          })),\n          setCharacteristic: jest.fn().mockReturnThis(),\n        };\n        mockAccessory.services.push(service);\n        return service;\n      }),\n      removeService: jest.fn(),\n    } as unknown as PlatformAccessory;\n  });\n\n  function createMockDevice(codes: string[]): TuyaDevice {\n    const schema = codes.map(code => ({\n      code,\n      mode: TuyaDeviceSchemaMode.READ_WRITE,\n      type: code.includes('bright') ? TuyaDeviceSchemaType.Integer : TuyaDeviceSchemaType.Boolean,\n      property: code.includes('bright')\n        ? { min: 10, max: 1000, scale: 0, step: 1 }\n        : {},\n    }));\n\n    const status = codes.map(code => ({\n      code,\n      value: code.includes('bright') ? 500 : true,\n    }));\n\n    return new TuyaDevice({\n      id: 'test-device-id',\n      uuid: 'test-uuid',\n      name: 'Test Fan',\n      online: true,\n      owner_id: 'owner-1',\n      product_id: 'fs',\n      product_name: 'Smart Fan',\n      category: 'fs',\n      schema,\n      status,\n    });\n  }\n\n  function setupAndConfigure(codes: string[]) {\n    const device = createMockDevice(codes);\n    mockDeviceManager.getDevice.mockReturnValue(device);\n    const fanAccessory = new FanAccessory(mockPlatform, mockAccessory);\n    fanAccessory.configureServices();\n    return fanAccessory;\n  }\n\n  function getDualLightServices() {\n    return mockAccessory.addService.mock.calls.filter((call: any[]) =>\n      call[1] === 'Warm Light' || call[1] === 'White Light',\n    );\n  }\n\n  function expectNoDualLight() {\n    expect(getDualLightServices().length).toBe(0);\n  }\n\n  function expectLightCall(index: number, onCode: string, brightCode?: string) {\n    const args = mockConfigureLight.mock.calls[index];\n    expect(args[2]).toEqual(expect.objectContaining({ code: onCode }));\n    if (brightCode) {\n      expect(args[3]).toEqual(expect.objectContaining({ code: brightCode }));\n    } else {\n      expect(args[3]).toBeUndefined();\n    }\n  }\n\n  // ─── Fan service type ─────────────────────────────────────────────\n\n  describe('fan service type', () => {\n    test('should use Fanv2 when child_lock present', () => {\n      const fanAccessory = setupAndConfigure(['switch', 'fan_speed', 'child_lock']);\n      expect(fanAccessory.fanServiceType()).toBe(mockAPI.hap.Service.Fanv2);\n    });\n\n    test('should use Fanv2 when swing present', () => {\n      const fanAccessory = setupAndConfigure(['switch', 'fan_speed', 'switch_horizontal']);\n      expect(fanAccessory.fanServiceType()).toBe(mockAPI.hap.Service.Fanv2);\n    });\n\n    test('should use Fan when no lock or swing present', () => {\n      const fanAccessory = setupAndConfigure(['switch', 'fan_speed']);\n      expect(fanAccessory.fanServiceType()).toBe(mockAPI.hap.Service.Fan);\n    });\n  });\n\n  // ─── A. Dual-light path ───────────────────────────────────────────\n  // Requires ALL 4: light + bright_value + switch_led + bright_value_1\n\n  describe('dual-light (all 4 DPs present)', () => {\n    test('light + bright_value + switch_led + bright_value_1', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value', 'switch_led', 'bright_value_1',\n      ]);\n\n      const lightServices = getDualLightServices();\n      expect(lightServices.length).toBe(2);\n      expect(lightServices[0][1]).toBe('Warm Light');\n      expect(lightServices[0][2]).toBe('warm_light');\n      expect(lightServices[1][1]).toBe('White Light');\n      expect(lightServices[1][2]).toBe('white_light');\n\n      expect(mockConfigureLight).toHaveBeenCalledTimes(2);\n      expectLightCall(0, 'light', 'bright_value');\n      expectLightCall(1, 'switch_led', 'bright_value_1');\n    });\n\n    test('dual-light with extra bright_value_v2 (ignored)', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value', 'bright_value_v2', 'switch_led', 'bright_value_1',\n      ]);\n\n      expect(getDualLightServices().length).toBe(2);\n      expect(mockConfigureLight).toHaveBeenCalledTimes(2);\n      expectLightCall(0, 'light', 'bright_value');\n      expectLightCall(1, 'switch_led', 'bright_value_1');\n    });\n\n    test('dual-light with extra temp_value (not passed to dual-light configureLight)', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value', 'switch_led', 'bright_value_1', 'temp_value',\n      ]);\n\n      expect(getDualLightServices().length).toBe(2);\n      expect(mockConfigureLight).toHaveBeenCalledTimes(2);\n      // Dual-light calls only pass on+bright, not temp/color/mode\n      expect(mockConfigureLight.mock.calls[0].length).toBe(4);\n      expect(mockConfigureLight.mock.calls[1].length).toBe(4);\n    });\n  });\n\n  // ─── B. Single-light Lightbulb path ───────────────────────────────\n  // Has LIGHT_ON match + at least one of: bright_value/v2, temp_value/v2, colour_data, work_mode\n\n  describe('single-light as Lightbulb', () => {\n    test('light + bright_value', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light', 'bright_value']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value');\n    });\n\n    test('light + bright_value_v2', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light', 'bright_value_v2']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value_v2');\n    });\n\n    test('switch_led + bright_value', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'switch_led', 'bright_value']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'switch_led', 'bright_value');\n    });\n\n    test('switch_led + bright_value_v2', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'switch_led', 'bright_value_v2']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'switch_led', 'bright_value_v2');\n    });\n\n    test('light + bright_value + both bright versions (prefers bright_value)', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light', 'bright_value', 'bright_value_v2']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value');\n    });\n\n    test('light + temp_value only (no brightness)', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light', 'temp_value']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      const args = mockConfigureLight.mock.calls[0];\n      expect(args[2]).toEqual(expect.objectContaining({ code: 'light' }));\n      expect(args[3]).toBeUndefined(); // no brightness DP\n      expect(args[4]).toEqual(expect.objectContaining({ code: 'temp_value' }));\n    });\n\n    test('switch_led + temp_value_v2 only', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'switch_led', 'temp_value_v2']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      const args = mockConfigureLight.mock.calls[0];\n      expect(args[2]).toEqual(expect.objectContaining({ code: 'switch_led' }));\n      expect(args[4]).toEqual(expect.objectContaining({ code: 'temp_value_v2' }));\n    });\n\n    test('light + bright_value + temp_value + colour_data + work_mode (full feature set)', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value', 'temp_value', 'colour_data', 'work_mode',\n      ]);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      const args = mockConfigureLight.mock.calls[0];\n      expect(args[2]).toEqual(expect.objectContaining({ code: 'light' }));\n      expect(args[3]).toEqual(expect.objectContaining({ code: 'bright_value' }));\n      expect(args[4]).toEqual(expect.objectContaining({ code: 'temp_value' }));\n      expect(args[5]).toEqual(expect.objectContaining({ code: 'colour_data' }));\n      expect(args[6]).toEqual(expect.objectContaining({ code: 'work_mode' }));\n    });\n\n    test('both on/off DPs + switch_led fallback: prefers light (LIGHT_ON order)', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light', 'switch_led', 'bright_value']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value');\n    });\n  });\n\n  // ─── C. Single-light Switch path ──────────────────────────────────\n  // Has LIGHT_ON match but NO brightness/temp/color/mode DPs\n\n  describe('single-light as Switch (on/off only)', () => {\n    test('light only', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n      expect(mockConfigureOn).toHaveBeenCalledWith(\n        expect.anything(),\n        undefined,\n        expect.objectContaining({ code: 'light' }),\n      );\n    });\n\n    test('switch_led only', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'switch_led']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n      expect(mockConfigureOn).toHaveBeenCalledWith(\n        expect.anything(),\n        undefined,\n        expect.objectContaining({ code: 'switch_led' }),\n      );\n    });\n\n    test('light + switch_led, no brightness (prefers light)', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'light', 'switch_led']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n      expect(mockConfigureOn).toHaveBeenCalledWith(\n        expect.anything(),\n        undefined,\n        expect.objectContaining({ code: 'light' }),\n      );\n    });\n  });\n\n  // ─── D. No light ──────────────────────────────────────────────────\n\n  describe('no light at all', () => {\n    test('fan-only device', () => {\n      setupAndConfigure(['switch', 'fan_speed']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n    });\n\n    test('brightness DPs without any on/off DP are ignored', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'bright_value']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n    });\n\n    test('bright_value_1 alone is ignored', () => {\n      setupAndConfigure(['switch', 'fan_speed', 'bright_value_1']);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n    });\n  });\n\n  // ─── E. Edge cases: dual-light guard must NOT match ────────────────\n  // These are the critical backward-compatibility scenarios.\n  // Each has 3 of the 4 required DPs but not the 4th,\n  // so must fall through to the single-light path.\n\n  describe('partial dual-light DPs (must NOT trigger dual-light)', () => {\n    test('missing bright_value_1: light + bright_value + switch_led', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value', 'switch_led',\n      ]);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value');\n    });\n\n    test('missing switch_led: light + bright_value + bright_value_1', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value', 'bright_value_1',\n      ]);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value');\n    });\n\n    test('missing bright_value: light + switch_led + bright_value_1', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'switch_led', 'bright_value_1',\n      ]);\n\n      expectNoDualLight();\n      // lightServiceType: no bright_value/v2, no temp, no color, no mode → Switch\n      expect(mockConfigureLight).not.toHaveBeenCalled();\n      expect(mockConfigureOn).toHaveBeenCalledWith(\n        expect.anything(),\n        undefined,\n        expect.objectContaining({ code: 'light' }),\n      );\n    });\n\n    test('missing light: switch_led + bright_value + bright_value_1', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'switch_led', 'bright_value', 'bright_value_1',\n      ]);\n\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'switch_led', 'bright_value');\n    });\n\n    test('has bright_value_v2 instead of bright_value: light + bright_value_v2 + switch_led + bright_value_1', () => {\n      setupAndConfigure([\n        'switch', 'fan_speed',\n        'light', 'bright_value_v2', 'switch_led', 'bright_value_1',\n      ]);\n\n      // bright_value is MISSING so dual-light guard fails\n      expectNoDualLight();\n      expect(mockConfigureLight).toHaveBeenCalledTimes(1);\n      expectLightCall(0, 'light', 'bright_value_v2');\n    });\n  });\n});\n"
  },
  {
    "path": "test/Light.test.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { describe, expect, test, jest, beforeEach } from '@jest/globals';\nimport { configureLight } from '../src/accessory/characteristic/Light';\n\ndescribe('configureLight - ON handler bundles brightness', () => {\n  let mockAccessory: any;\n  let mockService: any;\n  let handlers: Record<string, { onGet?: Function; onSet?: Function }>;\n\n  beforeEach(() => {\n    handlers = {};\n\n    const makeCharChain = (name: string) => ({\n      onGet: jest.fn(function (this: any, handler: Function) {\n        handlers[name] = handlers[name] || {};\n        handlers[name].onGet = handler;\n        return this;\n      }),\n      onSet: jest.fn(function (this: any, handler: Function) {\n        handlers[name] = handlers[name] || {};\n        handlers[name].onSet = handler;\n        return this;\n      }),\n      setProps: jest.fn().mockReturnThis(),\n      updateValue: jest.fn().mockReturnThis(),\n    });\n\n    const onChar = makeCharChain('On');\n    const brightChar = makeCharChain('Brightness');\n\n    mockService = {\n      getCharacteristic: jest.fn((charType: any) => {\n        if (charType === 'MockOn') return onChar;\n        if (charType === 'MockBrightness') return brightChar;\n        return makeCharChain('other');\n      }),\n    };\n\n    mockAccessory = {\n      Characteristic: { On: 'MockOn', Brightness: 'MockBrightness' },\n      Service: { Lightbulb: 'MockLightbulb' },\n      accessory: {\n        displayName: 'Test Light',\n        getService: jest.fn(() => null),\n        addService: jest.fn(() => mockService),\n      },\n      log: { info: jest.fn(), debug: jest.fn(), warn: jest.fn() },\n      platform: { getDeviceConfig: jest.fn(() => undefined) },\n      checkOnlineStatus: jest.fn(),\n      getStatus: jest.fn((code: string) => {\n        if (code === 'light') return { code: 'light', value: true };\n        if (code === 'bright_value') return { code: 'bright_value', value: 420 };\n        if (code === 'switch_led') return { code: 'switch_led', value: false };\n        if (code === 'bright_value_1') return { code: 'bright_value_1', value: 100 };\n        return undefined;\n      }),\n      sendCommands: jest.fn(),\n    };\n  });\n\n  function makeSchema(code: string, type = 'Boolean', property: any = {}) {\n    return { code, mode: 'rw', type, property };\n  }\n\n  function brightSchema(code = 'bright_value') {\n    return makeSchema(code, 'Integer', { min: 10, max: 1000, scale: 0, step: 1 });\n  }\n\n  test('LightType.C: ON sends both on + cached brightness', async () => {\n    configureLight(\n      mockAccessory,\n      mockService,\n      makeSchema('light') as any,\n      brightSchema() as any,\n    );\n\n    expect(handlers['On']?.onSet).toBeDefined();\n    await handlers['On'].onSet!(true);\n\n    expect(mockAccessory.sendCommands).toHaveBeenCalledWith(\n      [\n        { code: 'light', value: true },\n        { code: 'bright_value', value: 420 },\n      ],\n      true,\n    );\n  });\n\n  test('LightType.C: OFF sends only the on command (no brightness)', async () => {\n    configureLight(\n      mockAccessory,\n      mockService,\n      makeSchema('light') as any,\n      brightSchema() as any,\n    );\n\n    await handlers['On'].onSet!(false);\n\n    expect(mockAccessory.sendCommands).toHaveBeenCalledWith(\n      [{ code: 'light', value: false }],\n      true,\n    );\n  });\n\n  test('dual-light warm channel: ON bundles warm brightness', async () => {\n    configureLight(\n      mockAccessory,\n      mockService,\n      makeSchema('light') as any,\n      brightSchema('bright_value') as any,\n    );\n\n    await handlers['On'].onSet!(true);\n\n    expect(mockAccessory.sendCommands).toHaveBeenCalledWith(\n      [\n        { code: 'light', value: true },\n        { code: 'bright_value', value: 420 },\n      ],\n      true,\n    );\n  });\n\n  test('dual-light white channel: ON bundles white brightness', async () => {\n    configureLight(\n      mockAccessory,\n      mockService,\n      makeSchema('switch_led') as any,\n      brightSchema('bright_value_1') as any,\n    );\n\n    await handlers['On'].onSet!(true);\n\n    expect(mockAccessory.sendCommands).toHaveBeenCalledWith(\n      [\n        { code: 'switch_led', value: true },\n        { code: 'bright_value_1', value: 100 },\n      ],\n      true,\n    );\n  });\n\n  test('brightness-only set does not include on command', async () => {\n    configureLight(\n      mockAccessory,\n      mockService,\n      makeSchema('light') as any,\n      brightSchema() as any,\n    );\n\n    await handlers['Brightness'].onSet!(42);\n\n    expect(mockAccessory.sendCommands).toHaveBeenCalledWith(\n      [{ code: 'bright_value', value: 420 }],\n      true,\n    );\n  });\n\n  test('ON without brightness schema does not crash', async () => {\n    configureLight(\n      mockAccessory,\n      mockService,\n      makeSchema('light') as any,\n    );\n\n    expect(handlers['On']?.onSet).toBeDefined();\n    await handlers['On'].onSet!(true);\n\n    expect(mockAccessory.sendCommands).toHaveBeenCalledWith(\n      [{ code: 'light', value: true }],\n      true,\n    );\n  });\n});\n"
  },
  {
    "path": "test/custom.test.ts",
    "content": "/* eslint-disable no-console */\nimport { describe, expect, test } from '@jest/globals';\n\nimport TuyaOpenAPI from '../src/core/TuyaOpenAPI';\nimport TuyaDevice from '../src/device/TuyaDevice';\n\nimport TuyaCustomDeviceManager from '../src/device/TuyaCustomDeviceManager';\n\nimport { config, expectDevice, expectSuccessResponse } from './util';\n\nconst { options } = config;\nif (options.projectType === '1') {\n  const api = new TuyaOpenAPI(options.endpoint, options.accessId, options.accessKey);\n  const deviceManager = new TuyaCustomDeviceManager(api);\n\n  describe('TuyaOpenAPI', () => {\n    test('getToken()', async () => {\n      const res = await api.getToken();\n      expectSuccessResponse(res);\n    });\n\n    test('customGetUserInfo()', async () => {\n      const res = await api.customGetUserInfo('homebridge');\n      expectSuccessResponse(res);\n    });\n\n    test('customCreateUser()', async () => {\n      const res = await api.customCreateUser('homebridge', 'homebridge');\n      if (res.success === false && res.code === 14520015) {\n        // already exist\n      } else {\n        expectSuccessResponse(res);\n      }\n    });\n\n    test('customLogin()', async () => {\n      const res = await api.customLogin('homebridge', 'homebridge');\n      expectSuccessResponse(res);\n      expect(api.isLogin()).toBeTruthy();\n    });\n\n    test('_refreshAccessTokenIfNeed()', async () => {\n      api.tokenInfo.expire = 0;\n      await api._refreshAccessTokenIfNeed('');\n    });\n  });\n\n  describe('TuyaCustomDeviceManager', () => {\n\n    const assetIDList: string[] = [];\n    test('getAssetList()', async () => {\n      const res = await deviceManager.getAssetList();\n      expectSuccessResponse(res);\n      const assets = res.result.list || res.result.assets;\n      for (const { asset_id } of assets) {\n        assetIDList.push(asset_id);\n      }\n    });\n\n    test('updateDevices()', async () => {\n      const devices = await deviceManager.updateDevices(assetIDList);\n      expect(devices).not.toBeNull();\n      for (const device of devices) {\n        expectDevice(device);\n      }\n    }, 30 * 1000);\n\n    test('updateDevice()', async () => {\n      let device: TuyaDevice | null = Array.from(deviceManager.devices)[0];\n      expectDevice(device);\n      device = await deviceManager.updateDevice(device.id);\n      expectDevice(device!);\n    });\n  });\n\n  describe('TuyaOpenMQ', () => {\n\n    test('start()', async () => {\n      await new Promise((resolve, reject) => {\n        deviceManager.mq._onConnect = () => {\n          console.log('TuyaOpenMQ connected');\n          resolve(null);\n        };\n        deviceManager.mq._onError = (err) => {\n          console.log('TuyaOpenMQ error:', err);\n          reject(err);\n        };\n        deviceManager.mq.start();\n      });\n    });\n\n    test('stop()', async () => {\n      deviceManager.mq.stop();\n    });\n\n  });\n} else {\n  test('', async () => {\n    //\n  });\n}\n"
  },
  {
    "path": "test/home.test.ts",
    "content": "/* eslint-disable no-console */\nimport { describe, expect, test } from '@jest/globals';\n\nimport TuyaOpenAPI from '../src/core/TuyaOpenAPI';\nimport TuyaDevice from '../src/device/TuyaDevice';\n\nimport TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager';\n\nimport { config, expectDevice, expectSuccessResponse } from './util';\n\nconst { options } = config;\nif (options.projectType === '2') {\n  const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey);\n  const deviceManager = new TuyaHomeDeviceManager(api);\n\n  describe('TuyaOpenAPI', () => {\n    test('homeLogin()', async () => {\n      await api.homeLogin(options.countryCode, options.username, options.password, options.appSchema);\n      expect(api.isLogin()).toBeTruthy();\n    });\n\n    test('_refreshAccessTokenIfNeed()', async () => {\n      api.tokenInfo.expire = 0;\n      await api._refreshAccessTokenIfNeed('');\n    });\n  });\n\n  describe('TuyaHomeDeviceManager', () => {\n\n    const homeIDList: number[] = [];\n    test('getHomeList()', async () => {\n      const res = await deviceManager.getHomeList();\n      expectSuccessResponse(res);\n      for (const { home_id } of res.result) {\n        homeIDList.push(home_id);\n      }\n    });\n\n    test('updateDevices()', async () => {\n      const devices = await deviceManager.updateDevices(homeIDList);\n      expect(devices).not.toBeNull();\n      for (const device of devices) {\n        expectDevice(device);\n      }\n    }, 30 * 1000);\n\n    test('updateDevice()', async () => {\n      let device: TuyaDevice | null = Array.from(deviceManager.devices)[0];\n      expectDevice(device);\n      device = await deviceManager.updateDevice(device.id);\n      expectDevice(device!);\n    });\n\n  });\n\n  describe('TuyaOpenMQ', () => {\n\n    test('start()', async () => {\n      await new Promise((resolve, reject) => {\n        deviceManager.mq._onConnect = () => {\n          console.log('TuyaOpenMQ connected');\n          resolve(null);\n        };\n        deviceManager.mq._onError = (err) => {\n          console.log('TuyaOpenMQ error:', err);\n          reject(err);\n        };\n        deviceManager.mq.start();\n      });\n    });\n\n    test('stop()', async () => {\n      deviceManager.mq.stop();\n    });\n\n  });\n} else {\n  test('', async () => {\n    //\n  });\n}\n"
  },
  {
    "path": "test/util.ts",
    "content": "import fs from 'fs';\nimport { PLATFORM_NAME } from '../src/settings';\nimport { TuyaPlatformConfig } from '../src/config';\nimport TuyaDevice, { TuyaDeviceSchema } from '../src/device/TuyaDevice';\nimport { TuyaOpenAPIResponse } from '../src/core/TuyaOpenAPI';\n\nconst file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`);\nconst { platforms } = JSON.parse(file.toString());\n\nexport const config: TuyaPlatformConfig = platforms.find(platform => platform.platform === PLATFORM_NAME);\n\nexport function expectDevice(device: TuyaDevice) {\n  // console.debug(JSON.stringify(device));\n\n  expect(device).not.toBeUndefined();\n\n  expect(device.id.length).toBeGreaterThan(0);\n  expect(device.uuid.length).toBeGreaterThan(0);\n  expect(device.online).toBeDefined();\n\n  expect(device.product_id.length).toBeGreaterThan(0);\n  expect(device.category.length).toBeGreaterThan(0);\n  for (const schema of device.schema) {\n    expectDeviceSchema(schema);\n  }\n\n  expect(device.status).toBeDefined();\n}\n\nexport function expectDeviceSchema(schema: TuyaDeviceSchema) {\n  expect(schema.code.length).toBeGreaterThan(0);\n  expect(schema.mode.length).toBeGreaterThan(0);\n  expect(schema.type.length).toBeGreaterThan(0);\n  expect(schema.property).toBeDefined();\n}\n\nexport function expectSuccessResponse(res: TuyaOpenAPIResponse) {\n  expect(res.success).toBeTruthy();\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2018\", // ~node10\n    \"module\": \"commonjs\",\n    \"lib\": [\n      \"es2015\",\n      \"es2016\",\n      \"es2017\",\n      \"es2018\"\n    ],\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"noImplicitAny\": false,\n    \"resolveJsonModule\": true,\n    \"allowJs\": true\n  },\n  \"include\": [\n    \"src/\"\n  ],\n  \"exclude\": [\n    \"**/*.spec.ts\"\n  ]\n}\n"
  }
]