[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Bug Report / 报告问题\ndescription: Create a report to help us improve. / 报告问题以帮助我们改进\nbody:\n  - type: textarea\n    attributes:\n      label: Describe the Bug / 描述问题\n      description: |\n        > A clear and concise description of what the bug is. Please include the device model information (Like xiaomi.gateway.hub1 which can be found in Device info page).\n        > 清晰且简明地描述问题。请注明设备 model 信息（例如 xiaomi.gateway.hub1，可在设备详情页查询）。\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: How to Reproduce / 复现步骤\n      description: |\n        > If applicable, add screenshots to help explain your problem. You can attach images by clicking this area to highlight it and then dragging files in. Steps to reproduce the behavior:\n        > 如有需要，可添加截图以帮助解释问题。点击此区域以高亮显示并拖动截图文件以上传。请详细描述复现步骤：\n      placeholder: |\n        1. Go to ...\n        2. Click on ...\n        3. Scroll down to ...\n        4. See error\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Expected Behavior / 预期结果\n      description: |\n        > A clear and concise description of what you expected to happen.\n        > 描述预期结果。\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Reproduce Time / 问题复现的时间点\n      description: |\n        > Year-month-day, 24-hour time.\n        > 年-月-日，24小时制。\n      placeholder: \"2025-01-01 17:00:00\"\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Home Assistant Logs / 系统日志\n      description: |\n        > Please [set the log level](https://github.com/XiaoMi/ha_xiaomi_home/blob/main/CONTRIBUTING.md#reporting-bugs) to `debug` and try to reproduce the problem.\n        > [Settings > System > Logs > DOWNLOAD FULL LOG](https://my.home-assistant.io/redirect/logs) > Filter `xiaomi_home`\n        > If you are concerned about privacy, you can send the log to ha_xiaomi_home@xiaomi.com . The mail body should include the link to this issue.\n        > 请将[日志级别设置](https://github.com/XiaoMi/ha_xiaomi_home/blob/main/doc/CONTRIBUTING_zh.md#%E6%88%91%E5%8F%AF%E4%BB%A5%E5%A6%82%E4%BD%95%E8%B4%A1%E7%8C%AE)为 `debug` 并尝试复现问题。\n        > [设置 > 系统 > 日志 > 下载完整日志](https://my.home-assistant.io/redirect/logs) > 筛选 `xiaomi_home`\n        > 如果您担心隐私问题，可将日志发送至 ha_xiaomi_home@xiaomi.com ，邮件正文附上此问题的链接。\n\n  - type: input\n    attributes:\n      label: Log Timezone / 日志时区\n      description: |\n        > The [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) of the timestamp in the log.\n        > 日志所用时间戳的[时区](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)。\n      placeholder: \"Asia/Shanghai\"\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Home Assistant Core Version / Home Assistant Core 版本\n      description: |\n        > [Settings > About](https://my.home-assistant.io/redirect/info)\n        > [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info)\n      placeholder: \"2024.11.0\"\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Home Assistant Operation System Version / Home Assistant Operation System 版本\n      description: |\n        > [Settings > About](https://my.home-assistant.io/redirect/info)\n        > [设置 > 关于 Home Assistant](https://my.home-assistant.io/redirect/info)\n      placeholder: \"13.0\"\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Xiaomi Home Integration Version / 米家集成版本\n      description: |\n        > [Settings > Devices & services > Configured > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)\n        > [设置 > 设备与服务 > 已配置 > `Xiaomi Home`](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)\n      placeholder: \"v0.1.0\"\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Additional Context / 其他说明\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Feature Suggestion / 功能建议\n    url: https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas\n    about: Share ideas for enhancements or new features. / 建议改进或增加新功能\n\n  - name: Support and Help / 支持与帮助\n    url: https://github.com/XiaoMi/ha_xiaomi_home/discussions/categories/q-a\n    about: Please ask and answer questions here. / 请在这里提问和答疑\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  release:\n    types: [published]\n\njobs:\n  release-zip:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: ZIP Component Dir\n        run: |\n          cd ${{ github.workspace }}/custom_components/xiaomi_home\n          zip -r xiaomi_home.zip ./\n\n      - name: Upload zip to release\n        uses: svenstaro/upload-release-action@v2\n        with:\n          repo_token: ${{ secrets.GITHUB_TOKEN }}\n          file: ${{ github.workspace }}/custom_components/xiaomi_home/xiaomi_home.zip\n          asset_name: xiaomi_home.zip\n          tag: ${{ github.ref }}\n          overwrite: true\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n  workflow_dispatch:\n\njobs:\n  check-rule-format:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography slugify\n\n      - name: Check rule format with pytest\n        run: |\n          pytest -v -s -m github ./test/check_rule_format.py\n\n      - name: Unit test with pytest\n        run: |\n          pytest -v -s -m github ./test/\n"
  },
  {
    "path": ".github/workflows/validate.yaml",
    "content": "name: Validate\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\n  workflow_dispatch:\n\njobs:\n  validate-hassfest:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Hassfest validation\n        uses: home-assistant/actions/hassfest@master\n\n  validate-hacs:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: HACS validation\n        uses: hacs/action@main\n        with:\n          category: integration\n\n  validate-lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          pip install pylint\n\n      - name: Static analyse the code with pylint\n        run: |\n          pylint $(git ls-files '*.py')\n\n  validate-setup:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Install the integration\n        run: |\n          export config_path=./test_config\n          mkdir $config_path\n          ./install.sh $config_path\n          echo \"default_config:\" >> $config_path/configuration.yaml\n          echo \"logger:\" >> $config_path/configuration.yaml\n          echo \"  default: info\" >> $config_path/configuration.yaml\n          echo \"  logs:\" >> $config_path/configuration.yaml\n          echo \"    custom_components.xiaomi_home: debug\" >> $config_path/configuration.yaml\n\n      - name: Setup Home Assistant\n        id: homeassistant\n        uses: ludeeus/setup-homeassistant@main\n        with:\n          config-dir: ./test_config\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__\n.pytest_cache\n.vscode\n.idea\nrequirements.txt\n"
  },
  {
    "path": ".pylintrc",
    "content": "# This Pylint rcfile contains a best-effort configuration to uphold the\n# best-practices and style described in the Google Python style guide:\n#   https://google.github.io/styleguide/pyguide.html\n#\n# Its canonical open-source location is:\n#   https://google.github.io/styleguide/pylintrc\n\n[MAIN]\n\n# Files or directories to be skipped. They should be base names, not paths.\nignore=third_party\n\n# Files or directories matching the regex patterns are skipped. The regex\n# matches against base names, not paths.\nignore-patterns=\n\n# Pickle collected data for later comparisons.\npersistent=no\n\n# List of plugins (as comma separated values of python modules names) to load,\n# usually to register additional checkers.\nload-plugins=\n\n# Use multiple processes to speed up Pylint.\njobs=4\n\n# Allow loading of arbitrary C extensions. Extensions are imported into the\n# active Python interpreter and may run arbitrary code.\nunsafe-load-any-extension=no\n\n\n[MESSAGES CONTROL]\n\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED\nconfidence=\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time (only on the command line, not in the configuration file where\n# it should appear only once). See also the \"--disable\" option for examples.\n#enable=\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once).You can also use \"--disable=all\" to\n# disable everything first and then reenable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use\"--disable=all --enable=classes\n# --disable=W\"\ndisable=R,\n        abstract-method,\n        apply-builtin,\n        arguments-differ,\n        attribute-defined-outside-init,\n        backtick,\n        bad-option-value,\n        basestring-builtin,\n        buffer-builtin,\n        c-extension-no-member,\n        consider-using-enumerate,\n        cmp-builtin,\n        cmp-method,\n        coerce-builtin,\n        coerce-method,\n        delslice-method,\n        div-method,\n        eq-without-hash,\n        execfile-builtin,\n        file-builtin,\n        filter-builtin-not-iterating,\n        fixme,\n        getslice-method,\n        global-statement,\n        hex-method,\n        idiv-method,\n        implicit-str-concat,\n        import-error,\n        import-self,\n        import-star-module-level,\n        input-builtin,\n        intern-builtin,\n        invalid-str-codec,\n        locally-disabled,\n        long-builtin,\n        long-suffix,\n        map-builtin-not-iterating,\n        misplaced-comparison-constant,\n        missing-function-docstring,\n        metaclass-assignment,\n        next-method-called,\n        next-method-defined,\n        no-absolute-import,\n        no-init,  # added\n        no-member,\n        no-name-in-module,\n        no-self-use,\n        nonzero-method,\n        oct-method,\n        old-division,\n        old-ne-operator,\n        old-octal-literal,\n        old-raise-syntax,\n        parameter-unpacking,\n        print-statement,\n        raising-string,\n        range-builtin-not-iterating,\n        raw_input-builtin,\n        rdiv-method,\n        reduce-builtin,\n        relative-import,\n        reload-builtin,\n        round-builtin,\n        setslice-method,\n        signature-differs,\n        standarderror-builtin,\n        suppressed-message,\n        sys-max-int,\n        trailing-newlines,\n        unichr-builtin,\n        unicode-builtin,\n        unnecessary-pass,\n        unpacking-in-except,\n        useless-else-on-loop,\n        useless-suppression,\n        using-cmp-argument,\n        wrong-import-order,\n        xrange-builtin,\n        zip-builtin-not-iterating,\n\n\n[REPORTS]\n\n# Set the output format. Available formats are text, parseable, colorized, msvs\n# (visual studio) and html. You can also give a reporter class, eg\n# mypackage.mymodule.MyReporterClass.\noutput-format=text\n\n# Tells whether to display a full report or only the messages\nreports=no\n\n# Python expression which should return a note less than 10 (10 is the highest\n# note). You have access to the variables errors warning, statement which\n# respectively contain the number of errors / warnings messages and the total\n# number of statements analyzed. This is used by the global evaluation report\n# (RP0004).\nevaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)\n\n# Template used to display messages. This is a python new-style format string\n# used to format the message information. See doc for all details\n#msg-template=\n\n\n[BASIC]\n\n# Good variable names which should always be accepted, separated by a comma\ngood-names=main,_\n\n# Bad variable names which should always be refused, separated by a comma\nbad-names=\n\n# Colon-delimited sets of names that determine each other's naming style when\n# the name regexes allow several styles.\nname-group=\n\n# Include a hint for the correct naming format with invalid-name\ninclude-naming-hint=no\n\n# List of decorators that produce properties, such as abc.abstractproperty. Add\n# to this list to register other decorators that produce valid properties.\nproperty-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl\n\n# Regular expression matching correct function names\nfunction-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$\n\n# Regular expression matching correct variable names\nvariable-rgx=^[a-z][a-z0-9_]*$\n\n# Regular expression matching correct constant names\nconst-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$\n\n# Regular expression matching correct attribute names\nattr-rgx=^_{0,2}[a-z][a-z0-9_]*$\n\n# Regular expression matching correct argument names\nargument-rgx=^[a-z][a-z0-9_]*$\n\n# Regular expression matching correct class attribute names\nclass-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$\n\n# Regular expression matching correct inline iteration names\ninlinevar-rgx=^[a-z][a-z0-9_]*$\n\n# Regular expression matching correct class names\nclass-rgx=^_?[A-Z][a-zA-Z0-9]*$\n\n# Regular expression matching correct module names\nmodule-rgx=^(_?[a-z][a-z0-9_]*|__init__)$\n\n# Regular expression matching correct method names\nmethod-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$\n\n# Regular expression which should only match function or class names that do\n# not require a docstring.\nno-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$\n\n# Minimum line length for functions/classes that require docstrings, shorter\n# ones are exempt.\ndocstring-min-length=12\n\n\n[TYPECHECK]\n\n# List of decorators that produce context managers, such as\n# contextlib.contextmanager. Add to this list to register other decorators that\n# produce valid context managers.\ncontextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis. It\n# supports qualified module names, as well as Unix pattern matching.\nignored-modules=\n\n# List of class names for which member attributes should not be checked (useful\n# for classes with dynamically set attributes). This supports the use of\n# qualified names.\nignored-classes=optparse.Values,thread._local,_thread._local\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E1101 when accessed. Python regular\n# expressions are accepted.\ngenerated-members=\n\n\n[FORMAT]\n\n# Maximum number of characters on a single line.\nmax-line-length=80\n\n# TODO(https://github.com/pylint-dev/pylint/issues/3352): Direct pylint to exempt\n# lines made too long by directives to pytype.\n\n# Regexp for a line that is allowed to be longer than the limit.\nignore-long-lines=(?x)(\n  ^\\s*(\\#\\ )?<?https?://\\S+>?$|\n  ^\\s*(from\\s+\\S+\\s+)?import\\s+.+$)\n\n# Allow the body of an if to be on the same line as the test if there is no\n# else.\nsingle-line-if-stmt=yes\n\n# Maximum number of lines in a module\nmax-module-lines=99999\n\n# String used as indentation unit.  The internal Google style guide mandates 2\n# spaces.  Google's externaly-published style guide says 4, consistent with\n# PEP 8.  Here, we use 4 spaces.\nindent-string='    '\n\n# Number of spaces of indent required inside a hanging  or continued line.\nindent-after-paren=4\n\n# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.\nexpected-line-ending-format=\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=TODO\n\n\n[STRING]\n\n# This flag controls whether inconsistent-quotes generates a warning when the\n# character used as a quote delimiter is used inconsistently within a module.\ncheck-quote-consistency=yes\n\n\n[VARIABLES]\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# A regular expression matching the name of dummy variables (i.e. expectedly\n# not used).\ndummy-variables-rgx=^\\*{0,2}(_$|unused_|dummy_)\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid to define new builtins when possible.\nadditional-builtins=\n\n# List of strings which can identify a callback function by name. A callback\n# name must start or end with one of those strings.\ncallbacks=cb_,_cb\n\n# List of qualified module names which can have objects that can redefine\n# builtins.\nredefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools\n\n\n[LOGGING]\n\n# Logging modules to check that the string format arguments are in logging\n# function parameter format\nlogging-modules=logging,absl.logging,tensorflow.io.logging\n\n\n[SIMILARITIES]\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=4\n\n# Ignore comments when computing similarities.\nignore-comments=yes\n\n# Ignore docstrings when computing similarities.\nignore-docstrings=yes\n\n# Ignore imports when computing similarities.\nignore-imports=no\n\n\n[SPELLING]\n\n# Spelling dictionary name. Available dictionaries: none. To make it working\n# install python-enchant package.\nspelling-dict=\n\n# List of comma separated words that should not be checked.\nspelling-ignore-words=\n\n# A path to a file that contains private dictionary; one word per line.\nspelling-private-dict-file=\n\n# Tells whether to store unknown words to indicated private dictionary in\n# --spelling-private-dict-file option instead of raising a message.\nspelling-store-unknown-words=no\n\n\n[IMPORTS]\n\n# Deprecated modules which should not be used, separated by a comma\ndeprecated-modules=regsub,\n                   TERMIOS,\n                   Bastion,\n                   rexec,\n                   sets\n\n# Create a graph of every (i.e. internal and external) dependencies in the\n# given file (report RP0402 must not be disabled)\nimport-graph=\n\n# Create a graph of external dependencies in the given file (report RP0402 must\n# not be disabled)\next-import-graph=\n\n# Create a graph of internal dependencies in the given file (report RP0402 must\n# not be disabled)\nint-import-graph=\n\n# Force import order to recognize a module as part of the standard\n# compatibility libraries.\nknown-standard-library=\n\n# Force import order to recognize a module as part of a third party library.\nknown-third-party=enchant, absl\n\n# Analyse import fallback blocks. This can be used to support both Python 2 and\n# 3 compatible code, which means that the block might have code that exists\n# only in one or another interpreter, leading to false positives when analysed.\nanalyse-fallback-blocks=no\n\n\n[CLASSES]\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,\n                      __new__,\n                      setUp\n\n# List of member names, which should be excluded from the protected access\n# warning.\nexclude-protected=_asdict,\n                  _fields,\n                  _replace,\n                  _source,\n                  _make\n\n# List of valid names for the first argument in a class method.\nvalid-classmethod-first-arg=cls,\n                            class_\n\n# List of valid names for the first argument in a metaclass class method.\nvalid-metaclass-classmethod-first-arg=mcs\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n## v0.4.7\n### Added\n- Add turkish language in multi_lang.json. [#1593](https://github.com/XiaoMi/ha_xiaomi_home/pull/1593)\n### Changed\n- Remove unused info getting from central hub gateway. [#1574](https://github.com/XiaoMi/ha_xiaomi_home/pull/1574)\n- Remove xiaomi.router.rd03 from `UNSUPPORTED_MODELS` and add era.airp.cwb03, k0918.toothbrush.t700 into it. [#1567](https://github.com/XiaoMi/ha_xiaomi_home/pull/1567)\n### Fixed\n- Update the BLE mesh device online state from the central hub gateway. Partially fix the BLE mesh device online state. [#1579](https://github.com/XiaoMi/ha_xiaomi_home/pull/1579)\n- Add unit for xiaomi.toothbrush.p001 brush-head-left-level property. [#1588](https://github.com/XiaoMi/ha_xiaomi_home/pull/1588)\n- Fix the playing-state property's access field of xiaomi.wifispeaker.lx04, xiaomi.wifispeaker.lx06, xiaomi.wifispeaker.x08c and xiaomi.wifispeaker.l04m. [#1567](https://github.com/XiaoMi/ha_xiaomi_home/pull/1567)\n- Fix the MIoT-Spec-V2 of xiaomi.airc.h09h00 humidity-range unit. [#1567](https://github.com/XiaoMi/ha_xiaomi_home/pull/1567)\n\n## v0.4.6\n### Added\n- Add tv-box device as the media player entity. [#1562](https://github.com/XiaoMi/ha_xiaomi_home/pull/1562)\n- Set play-control service's play-loop-mode property as the sound mode. [#1562](https://github.com/XiaoMi/ha_xiaomi_home/pull/1562)\n### Changed\n- Use constant value to indicate the cloud MQTT broker host domain. [#1530](https://github.com/XiaoMi/ha_xiaomi_home/pull/1530)\n- Use constant value to indicate the timer delay of refreshing devices. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555)\n- Set the playing-state property as the required property in the play-control service of the speaker device. [#1552](https://github.com/XiaoMi/ha_xiaomi_home/pull/1552)\n- Set the playing-state property as the required property in the optional play-control service of the television. [#1562](https://github.com/XiaoMi/ha_xiaomi_home/pull/1562)\n### Fixed\n- Catch paho-mqtt subscribe error properly. [#1551](https://github.com/XiaoMi/ha_xiaomi_home/pull/1551)\n- After the network resumes, keep retrying to fetch the device list until it succeeds. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555)\n- Catch the http post error properly. [#1555](https://github.com/XiaoMi/ha_xiaomi_home/pull/1555)\n- Fix the format and the access field of daikin.aircondition.k2 and daikin.airfresh.k33 string value properties. [#1561](https://github.com/XiaoMi/ha_xiaomi_home/pull/1561)\n\n## v0.4.5\n### Changed\n- Ignore mdns REMOVED package. [#1296](https://github.com/XiaoMi/ha_xiaomi_home/pull/1296)\n- Format value type first, then evaluate by expression, and set precision at last. [#1516](https://github.com/XiaoMi/ha_xiaomi_home/pull/1516)\n### Fixed\n- Fix xiaomi.derh.lite temperature precision. [#1505](https://github.com/XiaoMi/ha_xiaomi_home/pull/1505)\n- Fix xiaomi.waterpuri.s1200g filter property unit, lxzn.valve.02 electricity property unit, xiaomi.aircondition.c24 power consumption device class, and cuco.plug.cp7pd power consumption and power value precision. [#1517](https://github.com/XiaoMi/ha_xiaomi_home/pull/1517)\n\n## v0.4.4\n### Added\n- Add Turkish language support. [#1468](https://github.com/XiaoMi/ha_xiaomi_home/pull/1468)\n### Fixed\n- Stop MQTT internal loop immediately when main loop is closed. [#1465](https://github.com/XiaoMi/ha_xiaomi_home/pull/1465)\n- Fix the float value precision. [#1485](https://github.com/XiaoMi/ha_xiaomi_home/pull/1485)\n- Fix the climate entity swing mode setting. [#1486](https://github.com/XiaoMi/ha_xiaomi_home/pull/1486)\n- Fix the MIoT-Spec-V2 of cykj.hood.jyj22 urn version 4, lumi.motion.bmgl01 siid=2 piid=2 value-list, ykcn.valve.cbcs power property value unit, ainice.sensor_occupy.3b people-number property and qdhkl.airc.a42 hvac mode. [#1496](https://github.com/XiaoMi/ha_xiaomi_home/pull/1496)\n\n## v0.4.3\n### Changed\n- Remove `VacuumEntityFeature.BATTERY` from the vacuum entity. [#1433](https://github.com/XiaoMi/ha_xiaomi_home/pull/1433)\n- Subscribe the proxy gateway child device up messages even though the device is offline. [#1393](https://github.com/XiaoMi/ha_xiaomi_home/pull/1393)\n### Fixed\n- Fix the integer value step. [#1388](https://github.com/XiaoMi/ha_xiaomi_home/pull/1388)\n- Fix the contact-state property value format. [#1387](https://github.com/XiaoMi/ha_xiaomi_home/pull/1387)\n- Fix the MIoT-Spec-V2 of xiaomi.airc.rr0r00 swing mode and hyd.airer.lyjpro current-position. [#1394](https://github.com/XiaoMi/ha_xiaomi_home/pull/1394)\n- Fix roidmi.vacuum.v60 siid=2 aiid=3 out field format.\n- Ignore unsupported properties of xiaomi.wifispeaker.l15a and 759413.aircondition.iez.\n- Add an alongside button entity for xiaomi.wifispeaker.l05b play action.\n- Add zhimi.fan.za1 fan mode description in zh_Hans.\n- Fix the error reported by pylint-4.0.0. [#1455](https://github.com/XiaoMi/ha_xiaomi_home/pull/1455)\n\n## v0.4.2\n### Changed\n- Set the battery service's start-charge action as the fallback action to support RETURN_HOME feature of the vacuum entity. [#1344](https://github.com/XiaoMi/ha_xiaomi_home/pull/1344)\n### Fixed\n- Correct the property value format after expression calculation. [#1366](https://github.com/XiaoMi/ha_xiaomi_home/pull/1366)\n- Fix the MIoT-Spec-V2 of xiaomi.fan.p70 and xiaomi.fan.p76 fan level, xiaomi.airc.rr0r00 and xiaomi.airc.h43h00 humidity-range, and zhimi.humidifier.ca4 water level. [#1367](https://github.com/XiaoMi/ha_xiaomi_home/pull/1367)\n- Ignore the unsupported model hmpace.motion.v6nfc.\n- Delete all unsupported MIoT-Spec-V2 instances of narwa.vacuum.001 and narwa.vacuum.ax11. [#1355](https://github.com/XiaoMi/ha_xiaomi_home/pull/1355)\n\n## v0.4.1\n### Changed\n- The setting option \"Cover closed position\" in CONFIGURE is changed to \"Cover dead zone width\". [#1301](https://github.com/XiaoMi/ha_xiaomi_home/pull/1301)\n- Add an alongside switch entity for 090615.aircondition.ktf and juhl.aircondition.hvac. [#1303](https://github.com/XiaoMi/ha_xiaomi_home/pull/1303)\n### Fixed\n- Fix the vacuum status so that the vacuum activity will not always be idle. [#1299](https://github.com/XiaoMi/ha_xiaomi_home/pull/1299)\n- Set the device on when the switch status is False or None. [#1303](https://github.com/XiaoMi/ha_xiaomi_home/pull/1303)\n- Hide sensitive info in printing logs. [#1328](https://github.com/XiaoMi/ha_xiaomi_home/pull/1328)\n- Fix the MIoT-Spec-V2 of cuco.plug.cp2d electric current, xiaomi.fan.p45 fan level, sanmei.valve.s1 power consumption, current and voltage, xiaomi.aircondition.c17, xiaomi.aircondition.m16 and xiaomi.airc.h40h00 humidity-range unit. [#1329](https://github.com/XiaoMi/ha_xiaomi_home/pull/1329)\n\n## v0.4.0\n### Added\n- Add the watch as the device tracker entity. [#1189](https://github.com/XiaoMi/ha_xiaomi_home/pull/1189)\n- Add the wifi speaker and the television as the media player entity. [#706](https://github.com/XiaoMi/ha_xiaomi_home/pull/706)\n- Add an option in CONFIGURE to set the cover closed position. [#1242](https://github.com/XiaoMi/ha_xiaomi_home/pull/1242)\n- Add notifications to show the status of the local connection to the central hub gateway. [#1280](https://github.com/XiaoMi/ha_xiaomi_home/pull/1280)\n- Import the device from the third party cloud. [#1258](https://github.com/XiaoMi/ha_xiaomi_home/pull/1258)\n### Changed\n- Add an alongside switch entity for viomi.waterheater.m1. [#1255](https://github.com/XiaoMi/ha_xiaomi_home/pull/1255)\n- Do not subscribe BLE device online/offline state message. [#1264](https://github.com/XiaoMi/ha_xiaomi_home/pull/1264)\n### Fixed\n- Keep the first element of the discovered ip address list as the recently added address when getting mdns result. [#1250](https://github.com/XiaoMi/ha_xiaomi_home/pull/1250)\n- Subscribe local topics every time when connected to the central hub gateway. [#1266](https://github.com/XiaoMi/ha_xiaomi_home/pull/1266)\n- Record the \"closing\" and \"closed\" status that occur frequently in the motor-controller, the window-opener and the curtain service. [#1262](https://github.com/XiaoMi/ha_xiaomi_home/pull/1262)\n- Fix xiaomi.aircondition.c24 total power consumption unit, adp.motor.adswb4 motor switch, cgllc.airm.cgd1st environment temperature, and shhf.light.sflt11 fan switch status. [#1256](https://github.com/XiaoMi/ha_xiaomi_home/pull/1256)\n\n## v0.3.4\n### Added\n- Exclude the unsupported device models. [#1205](https://github.com/XiaoMi/ha_xiaomi_home/pull/1205)\n### Changed\n- Subscribe the BLE device upstream messages even though the device is offline. [#1207](https://github.com/XiaoMi/ha_xiaomi_home/pull/1207)\n- Record \"opening\", \"closing\" and \"closed\" status of the airer service that occur frequently and do not record \"stop\" status for the cover entity. [#1235](https://github.com/XiaoMi/ha_xiaomi_home/pull/1235)\n- Modify README about spec_filter.yaml and the event attributes. [#1237](https://github.com/XiaoMi/ha_xiaomi_home/pull/1237)\n### Fixed\n- Fix the reconnect delay time to be reset when the client is connected to the broker. [#1200](https://github.com/XiaoMi/ha_xiaomi_home/pull/1200)\n- Fix the HA warning in the logs related to vacuum state setting. [#694](https://github.com/XiaoMi/ha_xiaomi_home/pull/694)\n- Fix the operation mode when the device does not have a mode property. [#1199](https://github.com/XiaoMi/ha_xiaomi_home/pull/1199)\n- Fix 090615.aircondition.ktf environment temperature. [#1210](https://github.com/XiaoMi/ha_xiaomi_home/pull/1210)\n- Fix a missing variable in translation it.json. [#1215](https://github.com/XiaoMi/ha_xiaomi_home/pull/1215)\n- Fix yutai.plug.fsov8m power consumption and ignore bjkcz.curtain.kczble curtain status. [#1236](https://github.com/XiaoMi/ha_xiaomi_home/pull/1236)\n\n## v0.3.3\n### Changed\n- Change the log level of error \"mips unsub internal error, 4, None\". [#1135](https://github.com/XiaoMi/ha_xiaomi_home/pull/1135)\n- Add necessary logs for distinguishing the set_properties command source. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)\n### Fixed\n- Fix tofan.airrtc.wk01 thermostat and air conditioner service. [#1160](https://github.com/XiaoMi/ha_xiaomi_home/pull/1160)\n- Fix mrbond.airer.m1t closing status. [#1134](https://github.com/XiaoMi/ha_xiaomi_home/pull/1134)\n- Fix the MIoT-Spec-V2 of xiaomi.fan.p69 fan service, ainice.sensor_occupy.3b people number, cykj.hood.jyj22 ventilation switch status, xiaomi.fan.p43 fan level, zhimi.airp.ua1a pm10 density, 090615.switch.x1tpm switch status, dmaker.fan.p33 fan-level. [#1132](https://github.com/XiaoMi/ha_xiaomi_home/pull/1132)\n- Fix cubee.airrtc.th123e and cubee.airrtc.th123w MIoT-Spec-V2 instance descriptions in Russian.\n- Fix ijai.vacuum.v1 suction-state value-list descriptions in Chinese.\n- Fix the misuse of Chinese brackets in multi_lang.json.\n- The unit of the humidity-range property of xiaomi.aircondition.mt0, xiaomi.aircondition.c35, xiaomi.aircondition.c24 and xiaomi.aircondition.c20 is \"none\". [#1187](https://github.com/XiaoMi/ha_xiaomi_home/pull/1187)\n\n## v0.3.2\n> Xiaomi Home has been added to the Home Assistant Community Store (HACS) as a default since May 8, 2025.\n### Added\n- Modify MIoT-Spec-V2 property format by spec_modify.yaml. [#1111](https://github.com/XiaoMi/ha_xiaomi_home/pull/1111)\n### Changed\n- Update the instructions of Xiaomi Home integration installation from HACS. [#102](https://github.com/XiaoMi/ha_xiaomi_home/pull/102) [#1088](https://github.com/XiaoMi/ha_xiaomi_home/pull/1088)\n- Add an alongside switch entity for zimi.waterheater.h03 and xiaomi.waterheater.yms2. [#1115](https://github.com/XiaoMi/ha_xiaomi_home/pull/1115)\n### Fixed\n- Fix Chinese encoding in LAN Control. [#1114](https://github.com/XiaoMi/ha_xiaomi_home/pull/1114)\n- Fix the MIoT-Spec-V2 of lxzn.switch.jcbcsm power consumption, voltage and current, shhf.light.sfla10 fan direction, zhimi.fan.za4 fan-level, zhimi.fan.sa1 fan-level. [#1110](https://github.com/XiaoMi/ha_xiaomi_home/pull/1110)\n- Revise the Chinese descriptions of loock.lock.t2pv1 door state value-list. [#1110](https://github.com/XiaoMi/ha_xiaomi_home/pull/1110)\n\n## v0.3.1\n### Changed\n- Setting the fan speed level when the fan is off will turning the fan on first. [#1031](https://github.com/XiaoMi/ha_xiaomi_home/pull/1031)\n### Fixed\n- Fix update device list error when there is no shared devices. [#1024](https://github.com/XiaoMi/ha_xiaomi_home/pull/1024)\n- Fix the humidifier get_prop_value error when the property is None. [#1035](https://github.com/XiaoMi/ha_xiaomi_home/pull/1035)\n- Fix the MIoT-Spec-V2 of zhimi.fan.v3 fan-level, cuco.plug.cp1md voltage and current, zimi.plug.zncz01 electric-power, giot.plug.v8icm power-consumption unit, yunmi.kettle.r3 tds unit, and dmaker.fan.p5 fan-level. [#1037](https://github.com/XiaoMi/ha_xiaomi_home/pull/1037)\n\n## v0.3.0\n注意：v0.3.0 变更了部分实体 unique_id 的生成规则，如果勾选 xiaomi_home > 配置 > 更新实体转换规则，会导致部分实体已配置的自动化失效。如果想要避免重新配置大量自动化，可使用这个[补丁](https://github.com/XiaoMi/ha_xiaomi_home/pull/972)。\n\nCAUTION: v0.3.0 changes the unique_id of some entities. If you check the option `xiaomi_home > CONFIGURE > Update entity conversion rules`, it may cause the automation settings for these entities to fail. To avoid having to reconfigure a large number of automation settings, you can use this [patch](https://github.com/XiaoMi/ha_xiaomi_home/pull/972).\n### Added\n- Import the devices in the shared homes and the separated shared devices. [#1021](https://github.com/XiaoMi/ha_xiaomi_home/pull/1021)\n- Support _attr_hvac_action of the climate entity. [#956](https://github.com/XiaoMi/ha_xiaomi_home/pull/956)\n- Add custom defined MIoT-Spec-V2 instance via spec_add.json. [#953](https://github.com/XiaoMi/ha_xiaomi_home/pull/953)\n### Fixed\n- Ignore 'Event loop is closed' when unsub a closed event loop. [#991](https://github.com/XiaoMi/ha_xiaomi_home/pull/991)\n- Fix contact-state for linp.magnet.m1 and loock.safe.v1. [#977](https://github.com/XiaoMi/ha_xiaomi_home/pull/977)\n- Fix the mode initialization error of aupu.bhf_light.s368m. [#955](https://github.com/XiaoMi/ha_xiaomi_home/pull/955)\n- Fix the MIoT-Spec-V2 of lumi.gateway.mcn001, qmi.plug.psv3, lumi.motion.acn001, izq.sensor_occupy.24, linp.sensor_occupy.hb01 and yunmi.waterpuri.s20. [#949](https://github.com/XiaoMi/ha_xiaomi_home/pull/949)\n\n## v0.2.4\n### Added\n- Convert the submersion-state, the contact-state and the occupancy-status property to the binary_sensor entity. [#905](https://github.com/XiaoMi/ha_xiaomi_home/pull/905)\n### Changed\n- suittc.airrtc.wk168 mode descriptions are set to strings of numbers from 1 to 16. [#921](https://github.com/XiaoMi/ha_xiaomi_home/pull/921)\n- Do not set _attr_suggested_display_precision when the spec.expr is set in spec_modify.yaml [#929](https://github.com/XiaoMi/ha_xiaomi_home/pull/929)\n- Set \"unknown event msg\" log to info level.\n### Fixed\n- hhcc.plantmonitor.v1 soil moisture and soil ec icon and unit. [#927](https://github.com/XiaoMi/ha_xiaomi_home/pull/27)\n- cuco.plug.cp2 voltage and power value ratio.\n- cgllc.airmonitor.s1 unit ppb.\n- roswan.waterpuri.lte01 tds unit.\n- lumi.relay.c2acn01 power consumption unit\n- xiaomi.bhf_light.s1 fan level of ventilation.\n\n## v0.2.3\n### Changed\n- Specify the service name and the property name during the climate entity's on/off feature initialization. [#899](https://github.com/XiaoMi/ha_xiaomi_home/pull/899)\n- Remove the useless total-battery property from `SPEC_PROP_TRANS_MAP`.\n### Fixed\n- Fix the hvac mode setting error when changing the preset mode of the ptc-bath-heater.\n- Fix the ambiguous descriptions of yeelink.bhf_light.v10 ptc-bath-heater mode value-list.\n- Fix the power consumption value of chuangmi.plug.212a01. [#910](https://github.com/XiaoMi/ha_xiaomi_home/pull/910)\n\n## v0.2.2\nThis version has modified the conversion rules of the climate entity, which will have effect on the devices with the ptc-bath-heater, the air-conditioner and the air-fresh service. After updating, you need to restart Home Assistant and check `xiaomi_home > CONFIGURE >\nUpdate entity conversion rules > NEXT` to reload the integration.\n\n这个版本修改了浴霸、空调、新风机的实体转换规则，更新之后需要重启 Home Assistant，并且勾选 `xiaomi_home > 配置 > 更新实体转换规则 > 下一步` 重新加载集成。\n### Added\n- Add conversion rules for the air-conditioner service and the air-fresh service. [#879](https://github.com/XiaoMi/ha_xiaomi_home/pull/879)\n### Changed\n- Convert the mode of the ptc bath heater to the preset mode of the climate entity. [#874](https://github.com/XiaoMi/ha_xiaomi_home/pull/874)\n- Use Home Assistant default icon when device_class is set. [#855](https://github.com/XiaoMi/ha_xiaomi_home/pull/855)\n### Fixed\n- Fix xiaomi.aircondition.m9 humidity-range unit. [#878](https://github.com/XiaoMi/ha_xiaomi_home/pull/878)\n- Fix MIoT-Spec-V2 conflicts of xiaomi.fan.p5 and mike.bhf_light.2. [#866](https://github.com/XiaoMi/ha_xiaomi_home/pull/866)\n\n## v0.2.1\n### Added\n- Add the preset mode for the thermostat. [#833](https://github.com/XiaoMi/ha_xiaomi_home/pull/833)\n### Changed\n- Change paho-mqtt version to adapt Home Assistant 2025.03. [#839](https://github.com/XiaoMi/ha_xiaomi_home/pull/839)\n- Revert to use multi_lang.json. [#834](https://github.com/XiaoMi/ha_xiaomi_home/pull/834)\n### Fixed\n- Fix the opening and the closing status of linp.wopener.wd1lb. [#826](https://github.com/XiaoMi/ha_xiaomi_home/pull/826)\n- Fix the format type of the wind-reverse property. [#810](https://github.com/XiaoMi/ha_xiaomi_home/pull/810)\n- Fix the fan-level property without value-list but with value-range. [#808](https://github.com/XiaoMi/ha_xiaomi_home/pull/808)\n\n## v0.2.0\nThis version has modified some default units of sensors. After updating, it may cause Home Assistant to pop up some compatibility warnings. You can re-add the integration to resolve it.\n\n这个版本修改了一些传感器默认单位，更新后会导致 Home Assistant 弹出一些兼容性提示，您可以重新添加集成解决。\n\n### Added\n- Add prop trans rule for surge-power. [#595](https://github.com/XiaoMi/ha_xiaomi_home/pull/595)\n- Support modify spec and value conversion. [#663](https://github.com/XiaoMi/ha_xiaomi_home/pull/663)\n- Support the electric blanket. [#781](https://github.com/XiaoMi/ha_xiaomi_home/pull/781)\n- Add device with motor-control service as cover entity. [#688](https://github.com/XiaoMi/ha_xiaomi_home/pull/688)\n### Changed\n- Update README file. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681) [#747](https://github.com/XiaoMi/ha_xiaomi_home/pull/747)\n- Update CONTRIBUTING.md. [#681](https://github.com/XiaoMi/ha_xiaomi_home/pull/681)\n- Refactor climate.py. [#614](https://github.com/XiaoMi/ha_xiaomi_home/pull/614)\n### Fixed\n- Fix variable name or comment errors & fix test_lan error. [#678](https://github.com/XiaoMi/ha_xiaomi_home/pull/678) [#690](https://github.com/XiaoMi/ha_xiaomi_home/pull/690)\n- Fix water heater error & some type error. [#684](https://github.com/XiaoMi/ha_xiaomi_home/pull/684)\n- Fix fan level with value-list & fan reverse [#689](https://github.com/XiaoMi/ha_xiaomi_home/pull/689)\n- Fix sensor display precision [#708](https://github.com/XiaoMi/ha_xiaomi_home/pull/708)\n- Fix event:motion-detected without arguments [#712](https://github.com/XiaoMi/ha_xiaomi_home/pull/712)\n\n## v0.1.5b2\n### Added\n- Support binary sensors to be displayed as text sensor entities and binary sensor entities. [#592](https://github.com/XiaoMi/ha_xiaomi_home/pull/592)\n- Add miot cloud test case. [#620](https://github.com/XiaoMi/ha_xiaomi_home/pull/620)\n- Add test case for user cert. [#638](https://github.com/XiaoMi/ha_xiaomi_home/pull/638)\n- Add mips test case & Change mips reconnect logic. [#641](https://github.com/XiaoMi/ha_xiaomi_home/pull/641)\n- Support remove device. [#622](https://github.com/XiaoMi/ha_xiaomi_home/pull/622)\n- Support italian translation. [#183](https://github.com/XiaoMi/ha_xiaomi_home/pull/183)\n### Changed\n- Refactor miot spec. [#592](https://github.com/XiaoMi/ha_xiaomi_home/pull/592)\n- Refactor miot mips & fix type errors. [#365](https://github.com/XiaoMi/ha_xiaomi_home/pull/365)\n- Using logging for test case log print. [#636](https://github.com/XiaoMi/ha_xiaomi_home/pull/636)\n- Add power properties trans. [#571](https://github.com/XiaoMi/ha_xiaomi_home/pull/571)\n- Move web page to html. [#627](https://github.com/XiaoMi/ha_xiaomi_home/pull/627)\n### Fixed\n- Fix miot cloud and mdns error. [#637](https://github.com/XiaoMi/ha_xiaomi_home/pull/637)\n- Fix type error\n\n## v0.1.5b1\nThis version will cause some Xiaomi routers that do not support access (#564) to become unavailable. You can update the device list in the configuration or delete it manually.\n### Added\n- Fan entity support direction ctrl [#556](https://github.com/XiaoMi/ha_xiaomi_home/pull/556)\n### Changed\n- Filter miwifi.* devices and xiaomi.router.rd03 [#564](https://github.com/XiaoMi/ha_xiaomi_home/pull/564)\n### Fixed\n- Fix multi ha instance login [#560](https://github.com/XiaoMi/ha_xiaomi_home/pull/560)\n- Fix fan speed [#464](https://github.com/XiaoMi/ha_xiaomi_home/pull/464)\n- The number of profile models updated from 660 to 823. [#583](https://github.com/XiaoMi/ha_xiaomi_home/pull/583)\n\n## v0.1.5b0\n### Added\n- Add missing parameter state_class  [#101](https://github.com/XiaoMi/ha_xiaomi_home/pull/101)\n### Changed\n- Make git update guide more accurate [#561](https://github.com/XiaoMi/ha_xiaomi_home/pull/561)\n### Fixed\n- Limit *light.mode count (value-range) [#535](https://github.com/XiaoMi/ha_xiaomi_home/pull/535)\n- Update miot cloud raise error msg [#551](https://github.com/XiaoMi/ha_xiaomi_home/pull/551)\n- Fix table header misplacement [#554](https://github.com/XiaoMi/ha_xiaomi_home/pull/554)\n\n## v0.1.4\n### Added\n- Refactor miot network, add network detection logic, improve devices filter logic. [458](https://github.com/XiaoMi/ha_xiaomi_home/pull/458) [#191](https://github.com/XiaoMi/ha_xiaomi_home/pull/191)\n### Changed\n- Remove tev dependency for lan control & fixs. [#333](https://github.com/XiaoMi/ha_xiaomi_home/pull/333)\n- Use yaml to parse action params. [#447](https://github.com/XiaoMi/ha_xiaomi_home/pull/447)\n- Update issue template. [#445](https://github.com/XiaoMi/ha_xiaomi_home/pull/445)\n- Remove duplicate dependency(aiohttp) [#390](https://github.com/XiaoMi/ha_xiaomi_home/pull/390)\n### Fixed\n\n## v0.1.4b1\n### Added\n- Support devices filter, and device changed notify logical refinement. [#332](https://github.com/XiaoMi/ha_xiaomi_home/pull/332)\n### Changed\n- Readme amend HACS installation. [#404](https://github.com/XiaoMi/ha_xiaomi_home/pull/404)\n### Fixed\n- Fix unit_convert AttributeError, Change to catch all Exception. [#396](https://github.com/XiaoMi/ha_xiaomi_home/pull/396)\n- Ignore undefined piid and keep processing following arguments. [#377](https://github.com/XiaoMi/ha_xiaomi_home/pull/377)\n- Fix some type error, wrong use of any and Any. [#338](https://github.com/XiaoMi/ha_xiaomi_home/pull/338)\n- Fix lumi.switch.acn040 identify service translation of zh-Hans [#412](https://github.com/XiaoMi/ha_xiaomi_home/pull/412)\n\n## v0.1.4b0\n### Added\n### Changed\n### Fixed\n- Fix miot cloud token refresh logic. [#307](https://github.com/XiaoMi/ha_xiaomi_home/pull/307)\n- Fix lan ctrl filter logic. [#303](https://github.com/XiaoMi/ha_xiaomi_home/pull/303)\n\n## v0.1.3\n### Added\n### Changed\n- Remove default bug label. [#276](https://github.com/XiaoMi/ha_xiaomi_home/pull/276)\n- Improve multi-language translation actions. [#256](https://github.com/XiaoMi/ha_xiaomi_home/pull/256)\n- Use aiohttp instead of waiting for blocking calls. [#227](https://github.com/XiaoMi/ha_xiaomi_home/pull/227)\n- Language supports dt. [#237](https://github.com/XiaoMi/ha_xiaomi_home/pull/237)\n### Fixed\n- Fix local control error. [#271](https://github.com/XiaoMi/ha_xiaomi_home/pull/271)\n- Fix README_zh and miot_storage. [#270](https://github.com/XiaoMi/ha_xiaomi_home/pull/270)\n\n## v0.1.2\n### Added\n- Support Xiaomi Heater devices. https://github.com/XiaoMi/ha_xiaomi_home/issues/124 https://github.com/XiaoMi/ha_xiaomi_home/issues/117\n- Language supports pt, pt-BR.\n### Changed\n- Adjust the minimum version of HASS core to 2024.4.4 and above versions.\n### Fixed\n\n## v0.1.1\n### Added\n### Changed\n### Fixed\n- Fix humidifier trans rule. https://github.com/XiaoMi/ha_xiaomi_home/issues/59\n- Fix get homeinfo error.  https://github.com/XiaoMi/ha_xiaomi_home/issues/22\n- Fix air-conditioner switch on. https://github.com/XiaoMi/ha_xiaomi_home/issues/37 https://github.com/XiaoMi/ha_xiaomi_home/issues/16\n- Fix invalid cover status. https://github.com/XiaoMi/ha_xiaomi_home/issues/11  https://github.com/XiaoMi/ha_xiaomi_home/issues/85\n- Water heater entity add STATE_OFF. https://github.com/XiaoMi/ha_xiaomi_home/issues/105 https://github.com/XiaoMi/ha_xiaomi_home/issues/17\n\n## v0.1.0\n### Added\n- First version\n### Changed\n### Fixed\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Project Overview\n\nXiaomi Home Integration is an official Home Assistant integration for controlling Xiaomi IoT smart devices. It connects to devices via Xiaomi Cloud (MQTT) or locally through Xiaomi Central Hub Gateway. The integration converts MIoT-Spec-V2 device specifications into Home Assistant entities.\n\n## Development Commands\n\n### Installation & Setup\n```bash\n# Install to Home Assistant config directory\n./install.sh /path/to/config\n\n# Install test dependencies\npip install pytest pytest-asyncio pytest-dependency zeroconf paho.mqtt psutil cryptography slugify\n```\n\n### Testing\n```bash\n# Run all tests\npytest -v -s -m github ./test/\n\n# Run specific test files\npytest -v -s ./test/test_spec.py\npytest -v -s ./test/test_cloud.py\npytest -v -s ./test/test_lan.py\n\n# Check rule format\npytest -v -s -m github ./test/check_rule_format.py\n```\n\n### Code Quality\n```bash\n# Run pylint (follows Google Python Style Guide)\npylint $(git ls-files '*.py')\n\n# Lint specific files\npylint custom_components/xiaomi_home/*.py\n```\n\n### Validation\n```bash\n# HACS validation (run by GitHub Actions)\n# Uses: hacs/action@main\n\n# Hassfest validation (run by GitHub Actions)\n# Uses: home-assistant/actions/hassfest@master\n```\n\n## Architecture Overview\n\n### Core Components (miot/)\n\nThe integration is built around the `miot/` core package:\n\n**miot_client.py**: Top-level client instance representing a logged-in Xiaomi user. Each user login creates one MIoTClient. Manages authentication, device list, and message routing.\n\n**miot_cloud.py**: OAuth 2.0 authentication and HTTP API calls to Xiaomi Cloud. Handles token refresh, user info, device control commands, and spec downloads.\n\n**miot_mips.py**: Message bus (MQTT) for subscribing to device property changes and events. Implements both cloud (MipsCloudClient) and local (MipsLocalClient) message handling.\n\n**miot_device.py**: Device entity class. Each MIoT device creates multiple MIoTDevice instances (one per Home Assistant entity). Handles property updates, action execution, and event processing.\n\n**miot_spec.py**: MIoT-Spec-V2 parser. Parses device specifications (URN-based type system) from cloud or local cache. Each spec defines services, properties, events, and actions.\n\n**miot_lan.py**: Local LAN control for IP devices in same network. Discovery and control without cloud (optional).\n\n**miot_mdns.py**: mDNS discovery for Xiaomi Central Hub Gateway services.\n\n**miot_storage.py**: File storage for certificates, device specs, translations, and cached data.\n\n**miot_network.py**: Network status monitoring and IP address detection.\n\n**miot_i18n.py**: Multi-language support (13 languages). Manages translations for entity names.\n\n### Entity Conversion (specs/specv2entity.py)\n\nMIoT-Spec-V2 instances are converted to Home Assistant entities using three mapping dictionaries:\n\n- **SPEC_DEVICE_TRANS_MAP**: Whole-device patterns (e.g., vacuum, humidifier, climate)\n- **SPEC_SERVICE_TRANS_MAP**: Service-level patterns (e.g., battery, air-purifier)\n- **SPEC_PROP_TRANS_MAP**: Property-level patterns (e.g., temperature, humidity)\n\nConversion priority: Device > Service > Property > General rules\n\n### Spec Customization Files (miot/specs/)\n\n**spec_filter.yaml**: Filters out MIoT-Spec-V2 instances that should NOT be converted to entities. Uses device URN keys and supports wildcard matching for service/property/event/action IIDs.\n\n**spec_modify.yaml**: Modifies spec instances before conversion (e.g., changing value ranges, access modes).\n\n**multi_lang.json**: Local translation overrides with higher priority than cloud translations. Keyed by device URN (without version).\n\n**spec_add.json**: Additional spec definitions for devices not in cloud database.\n\n**bool_trans.yaml**: Boolean value translation mappings.\n\nAfter editing spec files, you MUST update conversion rules via Integration CONFIGURE page in Home Assistant.\n\n### Platform Files (custom_components/xiaomi_home/)\n\nStandard Home Assistant platform files (sensor.py, switch.py, climate.py, etc.) implement entity registration and state management. Each platform imports from miot_device.py and creates entity subclasses.\n\n**config_flow.py**: Configuration flow for OAuth login and device selection.\n\n**__init__.py**: Integration setup, entry management, and data structure initialization.\n\n## MIoT-Spec-V2 Concepts\n\n**URN Format**: `urn:<namespace>:<type>:<name>:<value>[:<vendor-product>:<version>]`\n- namespace: miot-spec-v2 (Xiaomi), bluetooth-spec (SIG), or vendor-specific\n- type: device, service, property, event, action\n- name: human-readable identifier (used for mapping)\n\n**IIDs (Instance IDs)**: Decimal identifiers\n- siid: Service Instance ID\n- piid: Property Instance ID\n- eiid: Event Instance ID\n- aiid: Action Instance ID\n\n**Instance Code Format**:\n```\nservice:<siid>                              # service\nservice:<siid>:property:<piid>              # property\nservice:<siid>:property:<piid>:valuelist:<index>  # value list item\nservice:<siid>:event:<eiid>                 # event\nservice:<siid>:action:<aiid>                # action\n```\n\n## Naming Conventions\n\nFrom CONTRIBUTING.md:\n\n- **Xiaomi**: Always \"Xiaomi\" in text. Variables: \"xiaomi\" or \"mi\"\n- **Xiaomi Home**: Always \"Xiaomi Home\" in text. Variables: \"mihome\" or \"MiHome\"\n- **Xiaomi IoT**: Always \"MIoT\" in text. Variables: \"miot\" or \"MIoT\"\n- **Home Assistant**: Always \"Home Assistant\" in text. Variables: \"hass\" or \"hass_xxx\"\n\nMixed Chinese/English: Add space between Chinese and English or use Chinese quotation marks.\n\n## Commit Message Format\n\n```\n<type>: <subject>\n\n<body>\n\n<footer>\n```\n\nTypes: feat, fix, docs, style, refactor, perf, test, chore, revert\n\nSubject: Imperative, present tense. Not capitalized. No period.\n\nBody: Detailed description (mandatory except for docs type).\n\n## Code Style\n\nFollow [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). Use the provided `.pylintrc` for linting.\n\n## Debugging\n\nEnable debug logging in Home Assistant configuration.yaml:\n```yaml\nlogger:\n  default: critical\n  logs:\n    custom_components.xiaomi_home: debug\n```\n\n## Control Modes\n\n- **Cloud Control**: MQTT message subscription + HTTP command API\n- **Local Control**: Via Xiaomi Central Hub Gateway (firmware 3.3.0_0023+) or LAN control (IP devices only, may be unstable)\n\nCentral gateway local control takes priority over LAN control when both are available.\n\n## Multi-Region Support\n\nRegions: China (cn), Europe (eu), India (in), Russia (ru), Singapore (sg), USA (us)\n\nUser data is isolated per region. Integration supports multiple regions in same Home Assistant instance.\n\n## Important Files Location\n\n- Integration source: `custom_components/xiaomi_home/`\n- Spec mappings: `custom_components/xiaomi_home/miot/specs/specv2entity.py`\n- Spec filters: `custom_components/xiaomi_home/miot/specs/spec_filter.yaml`\n- Translations: `custom_components/xiaomi_home/translations/` and `custom_components/xiaomi_home/miot/i18n/`\n- Tests: `test/`\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guidelines\n\n[English](./CONTRIBUTING.md) | [简体中文](./doc/CONTRIBUTING_zh.md)\n\nThank you for considering contributing to our project! We appreciate your efforts to make our project better.\n\nBefore you start contributing, please take a moment to review the following guidelines.\n\n## How Can I Contribute?\n\n### Reporting Bugs\n\nIf you encounter a bug in the project, please [open an issue](https://github.com/XiaoMi/ha_xiaomi_home/issues/new/) on GitHub and provide the detailed information about the bug, including the steps to reproduce the bug, the logs of debug level and the time when it occurs.\n\nThe [method](https://www.home-assistant.io/integrations/logger/#log-filters) to set the integration's log level:\n\n```\n# Set the log level in configuration.yaml\n\nlogger:\n  default: critical\n  logs:\n    custom_components.xiaomi_home: debug\n```\n\n### Suggesting Enhancements\n\nIf you have ideas for enhancements or new features, you are welcomed to [start a discussion on ideas](https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas) on GitHub to discuss your ideas.\n\n### Contributing Code\n\n1. Fork the repository and create your branch from `main`.\n2. Ensure that your code adheres to the project coding standard.\n3. Make sure that your commit messages are descriptive and meaningful.\n4. Pull requests should be accompanied by a clear description of the problem and the solution.\n5. Update the documents if necessary.\n6. Run tests if they are available and ensure they pass.\n\n## Pull Request Guidelines\n\nBefore submitting a pull request, please make sure that the following requirements are met:\n\n- Your pull request addresses a single issue or feature.\n- You have tested your changes locally.\n- Your code follows the project's [code style](#code-style). Run [`pylint`](https://github.com/google/pyink) over your code using this [pylintrc](../.pylintrc).\n- All existing tests pass, and you have added new tests if applicable.\n- Any dependent changes are documented.\n\n## Code Style\n\nWe follow [Google Style](https://google.github.io/styleguide/pyguide.html) for code style and formatting. Please make sure to adhere to this guideline in your contributions.\n\n## Commit Message Format\n\n```\n<type>: <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\ntype: commit type is one of the following\n\n- feat: A new feature.\n- fix: A bug fix.\n- docs: Documentation only changes.\n- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.).\n- refactor: A code change that neither fixes a bug nor adds a feature.\n- perf: A code change that improves performance.\n- test: Adding missing tests or correcting existing tests.\n- chore: Changes to the build process or auxiliary tools and libraries.\n- revert: Reverting a previous commit.\n\nsubject: A short summary in imperative, present tense. Not capitalized. No period at the end.\n\nbody: A detailed description of the commit and the motivation for the change. The body is mandatory for all commits except for those of type \"docs\".\n\nfooter: Optional. The footer is the place to reference GitHub issues and PRs that this commit closes or is related to.\n\n## Naming Conventions\n\n### Xiaomi Naming Convention\n\n- When describing Xiaomi, always use \"Xiaomi\" in full. Variable names can use \"xiaomi\" or \"mi\".\n- When describing Xiaomi Home, always use \"Xiaomi Home\". Variable names can use \"mihome\" or \"MiHome\".\n- When describing Xiaomi IoT, always use \"MIoT\". Variable names can use \"miot\" or \"MIoT\".\n\n### Third-Party Platform Naming Convention\n\n- When describing Home Assistant, always use \"Home Assistant\". Variables can use \"hass\" or \"hass_xxx\".\n\n### Other Naming Conventions\n\n- When using mixed Chinese and English sentences in the document, there must be a space between Chinese and English or the English words must be quoted by Chinese quotation marks. (It is best to write code comments this way too.)\n\n## Licensing\n\nWhen contributing to this project, you agree that your contributions will be licensed under the project's [LICENSE](../LICENSE.md).\n\n\nWhen you submit your first pull request, GitHub Action will prompt you to sign the Contributor License Agreement (CLA). Only after you sign the CLA, your pull request will be merged.\n\n## How to Get Help\n\nIf you need help or have questions, feel free to ask in [discussions](https://github.com/XiaoMi/ha_xiaomi_home/discussions/) on GitHub.\n\nYou can also contact ha_xiaomi_home@xiaomi.com\n"
  },
  {
    "path": "LICENSE.md",
    "content": "# 许可证\n\n版权声明 (C) 2024 小米公司。\n\n在本许可证下提供的 Home Assistant 米家集成（Xiaomi Home Integration）和相关米家云服务 API 接口，包括源代码和目标代码（统称为“授权作品”）的所有权及知识产权归小米所有。小米在此授予您一项个人的、有限的、非排他的、不可转让的、不可转授权的、免费的权利，仅限于您为非商业性目的使用 Home Assistant 而复制、使用、修改、分发授权作品。为避免疑义，本许可证未授权您将授权作品用于任何其他用途，包括但不限于开发应用程序（APP）、Web 服务以及其他形式的软件等。\n\n您在重新分发授权作品时，无论修改与否，无论以源码形式或目标代码形式，您均需保留本授权作品中的版权标识、免责声明及本许可证的副本。\n\n授权作品是按“现状”分发的，小米不对授权作品承担任何明示或暗示的保证或担保，包括但不限于对授权作品没有错误或疏漏、持续性、可靠性、适用于某一特定用途或不侵权等的保证、声明或承诺。在任何情况下，对于因使用授权作品或无法使用授权作品而引起的任何直接、间接、特殊、偶然或后果性损害或损失，您需自行承担全部责任。\n\n本许可证中未明确授予的所有权利均予保留，除本许可证明确授予您的权利外，小米未以任何形式授权您使用小米及小米关联公司的商标、著作权或其他任何形式的知识产权，例如在未获得小米另行书面许可的情况下，您不得使用“小米”、“米家”等与小米相关的字样或其他会使得公众联想到小米的字样对您使用授权作品的软件或搭载授权作品的硬件做任何形式的宣传或推广。\n\n在下述情况下，小米有权立即终止对您依据本许可证获得的授权：\n1. 您对小米或其关联公司的专利或其他知识产权提起专利无效、诉讼或其他主张；或，\n2. 您生产、制造（含委托制造）、销售（含委托销售）模仿或复制小米产品（包含小米关联公司的产品）的山寨产品。\n\n---\n\n# License\n\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant Integration and related Xiaomi cloud service API interface provided under this license, including source code and object code (collectively, \"Licensed Work\"), are owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi hereby grants you a personal, limited, non-exclusive, non-transferable, non-sublicensable, and royalty-free license to reproduce, use, modify, and distribute the Licensed Work only for your use of Home Assistant for non-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize you to use the Licensed Work for any other purpose, including but not limited to use Licensed Work to develop applications (APP), Web services, and other forms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without modifications, whether in source or object form, provided that you must give any other recipients of the Licensed Work a copy of this License and retain all copyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR OMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible for any direct, indirect, special, incidental, or consequential damages or losses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License. Except for the rights expressly granted by Xiaomi under this License, Xiaomi does not authorize you in any form to use the trademarks, copyrights, or other forms of intellectual property rights of Xiaomi and its affiliates, including, without limitation, without obtaining other written permission from Xiaomi, you shall not use \"Xiaomi\", \"Xiaomi home\", \"Mijia\" and other words related to Xiaomi or words that may make the public associate with Xiaomi in any form to publicize or promote the software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this License in the event:\n1. You assert patent invalidation, litigation, or other claims against patents or other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock off Xiaomi or its affiliates' products.\n"
  },
  {
    "path": "LegalNotice.md",
    "content": "# 法律声明\n\n版权声明 (C) 2024 小米。\nHome Assistant 米家集成（Xiaomi Home Integration）所使用的米家云服务 API 接口（以下简称小米云接口）的所有权及其知识产权为小米所有。您仅限于在[米家集成许可证](./LICENSE.md)规定的范围内使用，任何超出前述许可证规定范围外的行为，包括但不限于在非 Home Assistant 平台上使用小米云接口、以及基于商业目的在 Home Assistant 平台上使用小米云接口等行为均应被视为侵权行为，小米有权对您使用的小米云接口采取包括但不限于停止使用、删除、屏蔽、断开连接等措施，同时保留向您追究相关法律责任的权利。\n小米拥有本声明的最终解释权。\n\n---\n\n# Legal Notice\n\nCopyright (C) 2024 Xiaomi Corporation.\nAll rights, title, interest and intellectual property rights of the Xiaomi Cloud Service API interface (hereinafter referred to as Xiaomi Cloud Interface) provided to use the Home Assistant Xiaomi Home Integration shall be solely owned by Xiaomi. You are only permitted to use the Xiaomi Cloud Interface within the scope specified in the [Xiaomi Home Integration License](./LICENSE.md). Any behavior beyond the scope of the aforesaid license, including but not limited to using the Xiaomi Cloud Interface on non-Home Assistant platforms and using the Xiaomi Cloud Interface on the Home Assistant platform for any commercial purposes, shall be deemed as infringement. Xiaomi has the right to take measures, including but not limited to stopping usage, deleting, blocking and disconnecting the Xiaomi Cloud Interface used by You, and also reserves the right to pursue relevant legal responsibilities against You.\nXiaomi reserves the right of the final interpretation of this notice.\n"
  },
  {
    "path": "README.md",
    "content": "# Xiaomi Home Integration for Home Assistant\n\n[English](./README.md) | [简体中文](./doc/README_zh.md)\n\nXiaomi Home Integration is an integrated component of Home Assistant supported by Xiaomi official. It allows you to use Xiaomi IoT smart devices in Home Assistant.\n\n## Installation\n\n> Home Assistant version requirement:\n>\n> - Core $\\geq$ 2024.4.4\n> - Operating System $\\geq$ 13.0\n\n### Method 1: Git clone from GitHub\n\n```bash\ncd config\ngit clone https://github.com/XiaoMi/ha_xiaomi_home.git\ncd ha_xiaomi_home\n./install.sh /config\n```\n\nWe recommend this installation method, for it is convenient to switch to a tag when updating `xiaomi_home` to a certain version.\n\nFor example, update to version v1.0.0\n\n```bash\ncd config/ha_xiaomi_home\ngit fetch\ngit checkout v1.0.0\n./install.sh /config\n```\n\n### Method 2: [HACS](https://hacs.xyz/)\n\nOne-click installation from HACS:\n\n[![Open your Home Assistant instance and open the Xiaomi Home integration inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)\n\nOr, HACS > In the search box, type **Xiaomi Home** > Click **Xiaomi Home**, getting into the detail page > DOWNLOAD\n\n### Method 3: Manually installation via [Samba](https://github.com/home-assistant/addons/tree/master/samba) / [FTPS](https://github.com/hassio-addons/addon-ftp)\n\nDownload and copy `custom_components/xiaomi_home` folder to `config/custom_components` folder in your Home Assistant.\n\n## Configuration\n\n### Login\n\n[Settings > Devices & services > ADD INTEGRATION](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > Search `Xiaomi Home` > NEXT > Click here to login > Sign in with Xiaomi account\n\n[![Open your Home Assistant instance and start setting up a new Xiaomi Home integration instance.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)\n\n### Add MIoT Devices\n\nAfter logging in successfully, a dialog box named \"Select Home and Devices\" pops up. You can select the home containing the device that you want to import in Home Assistant.\n\n### Multiple User Login\n\nAfter a Xiaomi account login and its user configuration are completed, you can continue to add other Xiaomi accounts in the configured Xiaomi Home Integration page.\n\nMethod: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > ADD HUB > NEXT > Click here to login > Sign in with Xiaomi account\n\n[![Open your Home Assistant instance and show Xiaomi Home integration.](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)\n\n### Update Configurations\n\nYou can change the configurations in the \"Configuration Options\" dialog box, in which you can update your user nickname and the list of the devices importing from Xiaomi Home APP, etc.\n\nMethod: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Select the option to update\n\n### Debug Mode for Action\n\nYou can manually send Action command message with parameters to the device when the debug mode for action is activated. The user interface for sending the Action command with parameters is shown as a Text entity.\n\nMethod: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Debug mode for action\n\n## Security\n\nXiaomi Home Integration and the affiliated cloud interface is provided by Xiaomi officially. You need to use your Xiaomi account to login to get your device list. Xiaomi Home Integration implements OAuth 2.0 login process, which does not keep your account password in the Home Assistant application. However, due to the limitation of the Home Assistant platform, the user information (including device information, certificates, tokens, etc.) of your Xiaomi account will be saved in the Home Assistant configuration file in clear text after successful login. You need to ensure that your Home Assistant configuration file is properly stored. The exposure of your configuration file may result in others logging in with your identity.\n\n> If you suspect that your OAuth 2.0 token has been leaked, you can revoke the login authorization of your Xiaomi account by the following steps: Xiaomi Home APP -> Profile -> Click your username and get into Xiaomi Account management page -> Basic info: Apps -> Xiaomi Home (Home Assistant Integration) -> Remove\n\n## FAQ\n\n- Does Xiaomi Home Integration support all Xiaomi smart devices?\n\n  Xiaomi Home Integration currently supports most categories of the smart device. Only a few categories are not supported. They are Bluetooth device, infrared device and virtual device.\n\n- Does Xiaomi Home Integration support multiple Xiaomi accounts?\n\n  Yes, it supports multiple Xiaomi accounts. Furthermore, Xiaomi Home Integration allows that devices belonging to different accounts can be added to a same area.\n\n- Does Xiaomi Home Integration support local mode?\n\n  Local mode is implemented by [Xiaomi Central Hub Gateway](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search) (firmware version 3.3.0_0023 and above) or Xiaomi smart devices with [built-in central hub gateway](https://github.com/XiaoMi/ha_xiaomi_home/wiki/Central-hub-gateway-device-models) (software version 0.8.9 and above) inside. If you do not have a Xiaomi central hub gateway or other devices having central hub gateway function, all control commands are sent through Xiaomi Cloud. The firmware for Xiaomi central hub gateway including the built-in central hub gateway supporting Home Assistant local mode feature has been released.\n\n  Xiaomi central hub gateway is only available in mainland China. In other regions, it is not available.\n\n  Xiaomi Home Integration can also implement partial local mode by enabling Xiaomi LAN control function. Xiaomi LAN control function can only control IP devices (devices connected to the router via WiFi or ethernet cable) in the same local area network as Home Assistant. It cannot control BLE Mesh, ZigBee, etc. devices. This function may cause some abnormalities. We recommend NOT using this function. Xiaomi LAN control function is enabled by [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update LAN control configuration.\n\n  Xiaomi LAN control function is not restricted by region. It is available in all regions. However, if there is a central gateway in the local area network where Home Assistant is located, even Xiaomi LAN control function is enabled in the integration, it will not take effect.\n\n- In which regions is Xiaomi Home Integration available?\n\n  Xiaomi Home Integration can be used in the mainland of China, Europe, India, Russia, Singapore, and USA. As user data in Xiaomi Cloud of different regions is isolated, you need to choose your region when importing MIoT devices in the configuration process. Xiaomi Home Integration allows you to import devices of different regions to a same area.\n\n## Principle of Messaging\n\n### Control through the Cloud\n\n<div align=center>\n<img src=\"./doc/images/cloud_control.jpg\" width=300>\n\nImage 1: Cloud control architecture\n\n </div>\n\nXiaomi Home Integration subscribes to the interested device messages on the MQTT Broker in MIoT Cloud. When a device property changes or a device event occurs, the device sends an upstream message to MIoT Cloud, and the MQTT Broker pushes the subscribed device message to Xiaomi Home Integration. Because Xiaomi Home Integration does not need to poll to obtain the current device property value in the cloud, it can immediately receive the notification message when the properties change or the events occur. Thanks to the message subscription mechanism, Xiaomi Home Integration only queries the properties of all devices from the cloud once when the integration configuration is completed, which puts little access pressure on the cloud.\n\nXiaomi Home Integration sends command messages to the devices via the HTTP interface of MIoT Cloud to control devices. The device reacts and responds after receiving the downstream message sent forward by MIoT Cloud.\n\n### Control locally\n\n<div align=center>\n<img src=\"./doc/images/local_control.jpg\" width=300>\n\nImage 2: Local control architecture\n\n</div>\n\nXiaomi central hub gateway contains a standard MQTT Broker, which implements a complete subscribe-publish mechanism. Xiaomi Home Integration subscribes to the interested device messages through Xiaomi central hub gateway. When a device property changes or a device event occurs, the device sends an upstream message to Xiaomi central hub gateway, and the MQTT Broker pushes the subscribed device message to Xiaomi Home Integration.\n\nWhen Xiaomi Home Integration needs to control a device, it publishes a device command message to the MQTT Broker, which is then forwarded to the device by Xiaomi central hub gateway. The device reacts and responds after receiving the downstream message from the gateway.\n\n## Mapping Relationship between MIoT-Spec-V2 and Home Assistant Entity\n\n[MIoT-Spec-V2](https://iot.mi.com/v2/new/doc/introduction/knowledge/spec) is the abbreviation for MIoT Specification Version 2, which is an IoT protocol formulated by Xiaomi IoT platform to give a standard functional description of IoT devices. It includes function definition (referred to as data model by other IoT platforms), interaction model, message format, and encoding.\n\nIn MIoT-Spec-V2 protocol, a product is defined as a device. A device contains several services. A service may have some properties, events and actions. Xiaomi Home Integration creates Home Assistant entities according to MIoT-Spec-V2. The conversion relationship is as follows.\n\n### General Conversion\n\n- Property\n\n| access       | format                | value-list   | value-range | Entity in Home Assistant |\n| ------------ | --------------------- | ------------ | ----------- | ------------------------ |\n| writable     | string                | -            | -           | Text                     |\n| writable     | bool                  | -            | -           | Switch                   |\n| writable     | not string & not bool | existent     | -           | Select                   |\n| writable     | not string & not bool | non-existent | existent    | Number                   |\n| not writable | -                     | -            | -           | Sensor                   |\n\n- Event\n\nMIoT-Spec-V2 event is transformed to Event entity in Home Assistant. The event's parameters are also passed to entity's `_trigger_event`.\n\nMIoT-Spec-V2 event's arguments field is the list of parameters of the event. The list elements represent the piid of the property in the same service. For example, the [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057) of the Xiaomi Wireless Double-key Switch contains the siid=2 Switch Sensor service. The eiid=1014 Long Press event of the service is triggered when a button is long pressed. The debug level log will print `Press and hold, attributes: {'Button Type': 1}`. This is an example log that the button type is 1, which means the right button is long pressed.\n\n- Action\n\n| in        | Entity in Home Assistant |\n| --------- | ------------------------ |\n| empty     | Button                   |\n| not empty | Notify                   |\n\nIf the debug mode for action is activated, the Text entity will be created when the \"in\" field in the action spec is not empty.\n\nThe \"Attribute\" item in the entity details page displays the format of the input parameter which is an ordered list, enclosed in square brackets []. The string elements in the list are enclosed in double quotation marks \"\".\n\nFor example, the \"Attributes\" item in the details page of the Notify entity converted by the \"Intelligent Speaker Execute Text Directive\" action of xiaomi.wifispeaker.s12 siid=5, aiid=5 instance shows the action params as `[Text Content(str), Silent Execution(bool)]`. A properly formatted input is `[\"Hello\", true]`.\n\n### Specific Conversion\n\nMIoT-Spec-V2 uses URN for defining types. The format is `urn:<namespace>:<type>:<name>:<value>[:<vendor-product>:<version>]`, in which `name` is a human-readable word or phrase describing the instance of device, service, property, event and action. Xiaomi Home Integration first determines whether to convert the MIoT-Spec-V2 instance into a specific Home Assistant entity based on the instance's name. For the instance that does not meet the specific conversion rules, general conversion rules are used for conversion.\n\n`namespace` is the namespace of MIoT-Spec-V2 instance. When its value is miot-spec-v2, it means that the specification is defined by Xiaomi. When its value is bluetooth-spec, it means that the specification is defined by Bluetooth Special Interest Group (SIG). When its value is not miot-spec-v2 or bluetooth-spec, it means that the specification is defined by other vendors. If MIoT-Spec-V2 `namespace` is not miot-spec-v2, a star mark `*` is added in front of the entity's name .\n\n- Device\n\nThe conversion follows `SPEC_DEVICE_TRANS_MAP`.\n\n```\n{\n    '<device instance name>':{\n        'required':{\n            '<service instance name>':{\n                'required':{\n                    'properties': {\n                        '<property instance name>': set<property access: str>\n                    },\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                },\n                'optional':{\n                    'properties': set<property instance name: str>,\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                }\n            }\n        },\n        'optional':{\n            '<service instance name>':{\n                'required':{\n                    'properties': {\n                        '<property instance name>': set<property access: str>\n                    },\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                },\n                'optional':{\n                    'properties': set<property instance name: str>,\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                }\n            }\n        },\n        'entity': str\n    }\n}\n```\n\nThe \"required\" field under \"device instance name\" indicates the required services of the device. The \"optional\" field under \"device instance name\" indicates the optional services of the device. The \"entity\" field indicates the Home Assistant entity to be created. The \"required\" and the \"optional\" field under \"service instance name\" are required and optional properties, events and actions of the service respectively. The value of \"property instance name\" under \"required\" \"properties\" field is the access mode of the property. The condition for a successful match is that the value of \"property instance name\" is a subset of the access mode of the corresponding MIoT-Spec-V2 property instance.\n\nHome Assistant entity will not be created if MIoT-Spec-V2 device instance does not contain all required services, properties, events or actions.\n\n- Service\n\nThe conversion follows `SPEC_SERVICE_TRANS_MAP`.\n\n```\n{\n    '<service instance name>':{\n        'required':{\n            'properties': {\n                '<property instance name>': set<property access: str>\n            },\n            'events': set<event instance name: str>,\n            'actions': set<action instance name: str>\n        },\n        'optional':{\n            'properties': set<property instance name: str>,\n            'events': set<event instance name: str>,\n            'actions': set<action instance name: str>\n        },\n        'entity': str\n    }\n}\n```\n\nThe \"required\" field under \"service instance name\" indicates the required properties, events and actions of the service. The \"optional\" field indicates the optional properties, events and actions of the service. The \"entity\" field indicates the Home Assistant entity to be created. The value of \"property instance name\" under \"required\" \"properties\" field is the access mode of the property. The condition for a successful match is that the value of \"property instance name\" is a subset of the access mode of the corresponding MIoT-Spec-V2 property instance.\n\nHome Assistant entity will not be created if MIoT-Spec-V2 service instance does not contain all required properties, events or actions.\n\n- Property\n\nThe conversion follows `SPEC_PROP_TRANS_MAP`.\n\n```\n{\n    'entities':{\n        '<entity name>':{\n            'format': set<str>,\n            'access': set<str>\n        }\n    },\n    'properties': {\n        '<property instance name>':{\n            'device_class': str,\n            'entity': str\n        }\n    }\n}\n```\n\nThe \"format\" field under \"entity name\" represents the data format of the property, and matching with one value indicates a successful match. The \"access\" field under \"entity name\" represents the access mode of the property, and matching with all values is considered a successful match.\n\nThe \"entity\" field under \"property instance name\", of which value is one of entity name under \"entities\" field, indicates the Home Assistant entity to be created. The \"device_class\" field under \"property instance name\" indicates the Home Assistant entity's `_attr_device_class`.\n\n- Event\n\nThe conversion follows `SPEC_EVENT_TRANS_MAP`.\n\n```\n{\n    '<event instance name>': str\n}\n```\n\nThe value of the event instance name indicates `_attr_device_class` of the Home Assistant entity to be created.\n\n### MIoT-Spec-V2 Filter\n\n`spec_filter.yaml` is used to filter out the MIoT-Spec-V2 instance that will not be converted to Home Assistant entity.\n\nThe format of `spec_filter.yaml` is as follows.\n\n```yaml\n<MIoT-Spec-V2 device instance urn without the version field>:\n    services: list<service_iid: str>\n    properties: list<service_iid.property_iid: str>\n    events: list<service_iid.event_iid: str>\n    actions: list<service_iid.action_iid: str>\n```\n\nThe key of `spec_filter.yaml` dictionary is the urn excluding the \"version\" field of the MIoT-Spec-V2 device instance. The firmware of different versions of the same product may be associated with the MIoT-Spec-V2 device instances of different versions. It is required that the MIoT-Spec-V2 instance of a higher version must contain all MIoT-Spec-V2 instances of the lower versions when a vendor defines the MIoT-Spec-V2 of its product on MIoT platform. Thus, the key of `spec_filter.yaml` does not need to specify the version number of MIoT-Spec-V2 device instance.\n\nThe value of \"services\", \"properties\", \"events\" or \"actions\" fields under \"device instance\" is the instance id (iid) of the service, property, event or action that will be ignored in the conversion process. Wildcard matching is supported.\n\nExample:\n\n```yaml\nurn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:\n    services:\n    - '*'   # Filter out all services. It is equivalent to completely ignoring the device with such MIoT-Spec-V2.\nurn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:\n    services:\n    - '3'   # Filter out the siid=3 service.\n    properties:\n    - '4.*' # Filter out all properties in the siid=4 service.\n    events:\n    - '4.1' # Filter out the eiid=1 event in the siid=4 service.\n    actions:\n    - '4.1' # Filter out the aiid=1 action in the siid=4 service.\n```\n\nDevice information service (urn:miot-spec-v2:service:device-information:00007801) of all devices will never be converted to Home Assistant entity.\n\n## Multiple Language Support\n\nThere are 13 languages available for selection in the config flow language option of Xiaomi Home, including Simplified Chinese, Traditional Chinese, English, Spanish, Russian, French, German, Japanese, Italian, Dutch, Portuguese, Brazilian Portuguese, and Turkish. The config flow page in Simplified Chinese and English has been manually reviewed by the developer. Other languages are translated by machine translation or community contributions. If you want to modify the words and sentences in the config flow page, you need to modify the json file of the certain language in `custom_components/xiaomi_home/translations/` and `custom_components/xiaomi_home/miot/i18n/` directory.\n\nWhen displaying Home Assistant entity name, Xiaomi Home downloads the multiple language file configured by the device vendor from MIoT Cloud, which contains translations for MIoT-Spec-V2 instances of the device. `multi_lang.json` is a locally maintained multiple language dictionary, which has a higher priority than the multiple language file obtained from the cloud and can be used to supplement or modify the multiple language translation of devices.\n\nThe format of `multi_lang.json` is as follows.\n\n```\n{\n    \"<MIoT-Spec-V2 device instance>\": {\n        \"<language code>\": {\n            \"<instance code>\": <translation: str>\n        }\n    }\n}\n```\n\nThe key of `multi_lang.json` dictionary is the urn excluding the \"version\" field of the MIoT-Spec-V2 device instance.\n\nThe language code is zh-Hans, zh-Hant, en, es, ru, fr, de, ja, it, nl, pt, pt-BR, or tr, corresponding to the 13 selectable languages mentioned above.\n\nThe instance code is the code of the MIoT-Spec-V2 instance, which is in the format of:\n\n```\nservice:<siid>                  # service\nservice:<siid>:property:<piid>  # property\nservice:<siid>:property:<piid>:valuelist:<index> # The index of a value in the value-list of a property\nservice:<siid>:event:<eiid>     # event\nservice:<siid>:action:<aiid>    # action\n```\n\nsiid, piid, eiid, aiid and value are all decimal three-digit integers.\n\nExample:\n\n```\n{\n    \"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1\": {\n        \"zh-Hant\": {\n            \"service:002\": \"養生壺\",\n            \"service:002:property:001\": \"工作狀態\",\n            \"service:002:property:001:valuelist:000\": \"待機中\",\n            \"service:002:action:002\": \"停止烹飪\",\n            \"service:005:event:001\": \"烹飪完成\"\n        }\n    }\n}\n```\n\n> If you edit any files in the `custom_components/xiaomi_home/miot/specs` directory (`spec_filter.yaml`, `spec_modify.yaml`, `multi_lang.json`, etc.) in your Home Assistant, you need to update the entity conversion rule in the integration's CONFIGURE page to take effect. Method: [Settings > Devices & services > Configured > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > CONFIGURE > Update entity conversion rules\n\n## Documents\n\n- [License](./LICENSE.md)\n- Contribution Guidelines: [English](./CONTRIBUTING.md) | [简体中文](./doc/CONTRIBUTING_zh.md)\n- [ChangeLog](./CHANGELOG.md)\n- Development Documents: https://developers.home-assistant.io/docs/creating_component_index\n- [FAQ](https://github.com/XiaoMi/ha_xiaomi_home/wiki)\n\n## Directory Structure\n\n- miot: core code.\n- miot/miot_client: Adding a login user in the integration needs adding a miot_client instance.\n- miot/miot_cloud: Contains functions related to the cloud service, including OAuth login process, HTTP interface functions (to get the user information, to send the device control command, etc.)\n- miot/miot_device: Device entity, including device information, processing logic of property, event and action.\n- miot/miot_mips: Message bus for subscribing and publishing method.\n- miot/miot_spec: Parse MIoT-Spec-V2.\n- miot/miot_lan: Device LAN control, including device discovery, device control, etc.\n- miot/miot_mdns: Central hub gateway service LAN discovery.\n- miot/miot_network: Obtain network status and network information.\n- miot/miot_storage: File storage for the integration.\n- miot/test: Test scripts.\n- config_flow: Config flow.\n"
  },
  {
    "path": "custom_components/xiaomi_home/__init__.py",
    "content": "# -*- coding: utf-8 -*-\r\n\"\"\"\r\nCopyright (C) 2024 Xiaomi Corporation.\r\n\r\nThe ownership and intellectual property rights of Xiaomi Home Assistant\r\nIntegration and related Xiaomi cloud service API interface provided under this\r\nlicense, including source code and object code (collectively, \"Licensed Work\"),\r\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\r\nhereby grants you a personal, limited, non-exclusive, non-transferable,\r\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\r\ndistribute the Licensed Work only for your use of Home Assistant for\r\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\r\nyou to use the Licensed Work for any other purpose, including but not limited\r\nto use Licensed Work to develop applications (APP), Web services, and other\r\nforms of software.\r\n\r\nYou may reproduce and distribute copies of the Licensed Work, with or without\r\nmodifications, whether in source or object form, provided that you must give\r\nany other recipients of the Licensed Work a copy of this License and retain all\r\ncopyright and disclaimers.\r\n\r\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\r\nCONDITIONS OF ANY KIND, either express or implied, including, without\r\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\r\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\r\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\r\nfor any direct, indirect, special, incidental, or consequential damages or\r\nlosses arising from the use or inability to use the Licensed Work.\r\n\r\nXiaomi reserves all rights not expressly granted to you in this License.\r\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\r\ndoes not authorize you in any form to use the trademarks, copyrights, or other\r\nforms of intellectual property rights of Xiaomi and its affiliates, including,\r\nwithout limitation, without obtaining other written permission from Xiaomi, you\r\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\r\nmay make the public associate with Xiaomi in any form to publicize or promote\r\nthe software or hardware devices that use the Licensed Work.\r\n\r\nXiaomi has the right to immediately terminate all your authorization under this\r\nLicense in the event:\r\n1. You assert patent invalidation, litigation, or other claims against patents\r\nor other intellectual property rights of Xiaomi or its affiliates; or,\r\n2. You make, have made, manufacture, sell, or offer to sell products that knock\r\noff Xiaomi or its affiliates' products.\r\n\r\nThe Xiaomi Home integration Init File.\r\n\"\"\"\r\nfrom __future__ import annotations\r\nimport logging\r\nfrom typing import Optional\r\n\r\nfrom homeassistant.config_entries import ConfigEntry\r\nfrom homeassistant.core import HomeAssistant\r\nfrom homeassistant.components import persistent_notification\r\nfrom homeassistant.helpers import device_registry, entity_registry\r\n\r\nfrom .miot.common import slugify_did\r\nfrom .miot.miot_storage import (\r\n    DeviceManufacturer, MIoTStorage, MIoTCert)\r\nfrom .miot.miot_spec import (\r\n    MIoTSpecInstance, MIoTSpecParser, MIoTSpecService)\r\nfrom .miot.const import (\r\n    DEFAULT_INTEGRATION_LANGUAGE, DOMAIN, SUPPORTED_PLATFORMS)\r\nfrom .miot.miot_error import MIoTOauthError\r\nfrom .miot.miot_device import MIoTDevice\r\nfrom .miot.miot_client import MIoTClient, get_miot_instance_async\r\n\r\n_LOGGER = logging.getLogger(__name__)\r\n\r\n\r\nasync def async_setup(hass: HomeAssistant, hass_config: dict) -> bool:\r\n    # pylint: disable=unused-argument\r\n    hass.data.setdefault(DOMAIN, {})\r\n    # {[entry_id:str]: MIoTClient}, miot client instance\r\n    hass.data[DOMAIN].setdefault('miot_clients', {})\r\n    # {[entry_id:str]: list[MIoTDevice]}\r\n    hass.data[DOMAIN].setdefault('devices', {})\r\n    # {[entry_id:str]: entities}\r\n    hass.data[DOMAIN].setdefault('entities', {})\r\n    for platform in SUPPORTED_PLATFORMS:\r\n        hass.data[DOMAIN]['entities'][platform] = []\r\n    return True\r\n\r\n\r\nasync def async_setup_entry(\r\n    hass: HomeAssistant, config_entry: ConfigEntry\r\n) -> bool:\r\n    \"\"\"Set up an entry.\"\"\"\r\n    def ha_persistent_notify(\r\n        notify_id: str, title: Optional[str] = None,\r\n        message: Optional[str] = None\r\n    ) -> None:\r\n        \"\"\"Send messages in Notifications dialog box.\"\"\"\r\n        if title:\r\n            persistent_notification.async_create(\r\n                hass=hass,  message=message or '',\r\n                title=title, notification_id=notify_id)\r\n        else:\r\n            persistent_notification.async_dismiss(\r\n                hass=hass, notification_id=notify_id)\r\n\r\n    entry_id = config_entry.entry_id\r\n    entry_data = dict(config_entry.data)\r\n\r\n    ha_persistent_notify(\r\n        notify_id=f'{entry_id}.oauth_error', title=None, message=None)\r\n\r\n    try:\r\n        miot_client: MIoTClient = await get_miot_instance_async(\r\n            hass=hass, entry_id=entry_id,\r\n            entry_data=entry_data,\r\n            persistent_notify=ha_persistent_notify)\r\n        # Spec parser\r\n        spec_parser = MIoTSpecParser(\r\n            lang=entry_data.get(\r\n                'integration_language', DEFAULT_INTEGRATION_LANGUAGE),\r\n            storage=miot_client.miot_storage,\r\n            loop=miot_client.main_loop\r\n        )\r\n        await spec_parser.init_async()\r\n        # Manufacturer\r\n        manufacturer: DeviceManufacturer = DeviceManufacturer(\r\n            storage=miot_client.miot_storage,\r\n            loop=miot_client.main_loop)\r\n        await manufacturer.init_async()\r\n        miot_devices: list[MIoTDevice] = []\r\n        er = entity_registry.async_get(hass=hass)\r\n        for did, info in miot_client.device_list.items():\r\n            spec_instance = await spec_parser.parse(urn=info['urn'])\r\n            if not isinstance(spec_instance, MIoTSpecInstance):\r\n                _LOGGER.error('spec content is None, %s, %s', did, info)\r\n                continue\r\n            device: MIoTDevice = MIoTDevice(\r\n                miot_client=miot_client,\r\n                device_info={\r\n                    **info, 'manufacturer': manufacturer.get_name(\r\n                        info.get('manufacturer', ''))},\r\n                spec_instance=spec_instance)\r\n            miot_devices.append(device)\r\n            device.spec_transform()\r\n            # Remove filter entities and non-standard entities\r\n            for platform in SUPPORTED_PLATFORMS:\r\n                # ONLY support filter spec service translate entity\r\n                if platform in device.entity_list:\r\n                    filter_entities = list(filter(\r\n                        lambda entity: (\r\n                            isinstance(entity.spec, MIoTSpecService)\r\n                            and (\r\n                                entity.spec.need_filter\r\n                                or (\r\n                                    miot_client.hide_non_standard_entities\r\n                                    and entity.spec.proprietary))\r\n                        ),\r\n                        device.entity_list[platform]))\r\n                    for entity in filter_entities:\r\n                        device.entity_list[platform].remove(entity)\r\n                        entity_id = device.gen_service_entity_id(\r\n                            ha_domain=platform,\r\n                            siid=entity.spec.iid,\r\n                            description=entity.spec.description)\r\n                        if er.async_get(entity_id_or_uuid=entity_id):\r\n                            er.async_remove(entity_id=entity_id)\r\n                if platform in device.prop_list:\r\n                    filter_props = list(filter(\r\n                        lambda prop: (\r\n                            prop.need_filter or (\r\n                                miot_client.hide_non_standard_entities\r\n                                and prop.proprietary)),\r\n                        device.prop_list[platform]))\r\n                    for prop in filter_props:\r\n                        device.prop_list[platform].remove(prop)\r\n                        entity_id = device.gen_prop_entity_id(\r\n                            ha_domain=platform, spec_name=prop.name,\r\n                            siid=prop.service.iid, piid=prop.iid)\r\n                        if er.async_get(entity_id_or_uuid=entity_id):\r\n                            er.async_remove(entity_id=entity_id)\r\n                if platform in device.event_list:\r\n                    filter_events = list(filter(\r\n                        lambda event: (\r\n                            event.need_filter or (\r\n                                miot_client.hide_non_standard_entities\r\n                                and event.proprietary)),\r\n                        device.event_list[platform]))\r\n                    for event in filter_events:\r\n                        device.event_list[platform].remove(event)\r\n                        entity_id = device.gen_event_entity_id(\r\n                            ha_domain=platform, spec_name=event.name,\r\n                            siid=event.service.iid, eiid=event.iid)\r\n                        if er.async_get(entity_id_or_uuid=entity_id):\r\n                            er.async_remove(entity_id=entity_id)\r\n                if platform in device.action_list:\r\n                    filter_actions = list(filter(\r\n                        lambda action: (\r\n                            action.need_filter or (\r\n                                miot_client.hide_non_standard_entities\r\n                                and action.proprietary)),\r\n                        device.action_list[platform]))\r\n                    for action in filter_actions:\r\n                        device.action_list[platform].remove(action)\r\n                        entity_id = device.gen_action_entity_id(\r\n                            ha_domain=platform, spec_name=action.name,\r\n                            siid=action.service.iid, aiid=action.iid)\r\n                        if er.async_get(entity_id_or_uuid=entity_id):\r\n                            er.async_remove(entity_id=entity_id)\r\n                        # Remove non-standard action debug entity\r\n                        if platform == 'notify':\r\n                            entity_id = device.gen_action_entity_id(\r\n                                ha_domain='text', spec_name=action.name,\r\n                                siid=action.service.iid, aiid=action.iid)\r\n                            if er.async_get(entity_id_or_uuid=entity_id):\r\n                                er.async_remove(entity_id=entity_id)\r\n            # Action debug\r\n            if not miot_client.action_debug:\r\n                # Remove text entity for debug action\r\n                for action in device.action_list.get('notify', []):\r\n                    entity_id = device.gen_action_entity_id(\r\n                        ha_domain='text', spec_name=action.name,\r\n                        siid=action.service.iid, aiid=action.iid)\r\n                    if er.async_get(entity_id_or_uuid=entity_id):\r\n                        er.async_remove(entity_id=entity_id)\r\n            # Binary sensor display\r\n            if not miot_client.display_binary_bool:\r\n                for prop in device.prop_list.get('binary_sensor', []):\r\n                    entity_id = device.gen_prop_entity_id(\r\n                        ha_domain='binary_sensor', spec_name=prop.name,\r\n                        siid=prop.service.iid, piid=prop.iid)\r\n                    if er.async_get(entity_id_or_uuid=entity_id):\r\n                        er.async_remove(entity_id=entity_id)\r\n            if not miot_client.display_binary_text:\r\n                for prop in device.prop_list.get('binary_sensor', []):\r\n                    entity_id = device.gen_prop_entity_id(\r\n                        ha_domain='sensor', spec_name=prop.name,\r\n                        siid=prop.service.iid, piid=prop.iid)\r\n                    if er.async_get(entity_id_or_uuid=entity_id):\r\n                        er.async_remove(entity_id=entity_id)\r\n\r\n        hass.data[DOMAIN]['devices'][config_entry.entry_id] = miot_devices\r\n        await hass.config_entries.async_forward_entry_setups(\r\n            config_entry, SUPPORTED_PLATFORMS)\r\n\r\n        # Remove the deleted devices\r\n        devices_remove = (await miot_client.miot_storage.load_user_config_async(\r\n            uid=config_entry.data['uid'],\r\n            cloud_server=config_entry.data['cloud_server'],\r\n            keys=['devices_remove'])).get('devices_remove', [])\r\n        if isinstance(devices_remove, list) and devices_remove:\r\n            dr = device_registry.async_get(hass)\r\n            for did in devices_remove:\r\n                device_entry = dr.async_get_device(\r\n                    identifiers={(\r\n                        DOMAIN,\r\n                        slugify_did(\r\n                            cloud_server=config_entry.data['cloud_server'],\r\n                            did=did))},\r\n                    connections=None)\r\n                if not device_entry:\r\n                    _LOGGER.error('remove device not found, %s', did)\r\n                    continue\r\n                dr.async_remove_device(device_id=device_entry.id)\r\n                _LOGGER.info(\r\n                    'delete device entry, %s, %s', did, device_entry.id)\r\n            await miot_client.miot_storage.update_user_config_async(\r\n                uid=config_entry.data['uid'],\r\n                cloud_server=config_entry.data['cloud_server'],\r\n                config={'devices_remove': []})\r\n\r\n        await spec_parser.deinit_async()\r\n        await manufacturer.deinit_async()\r\n\r\n    except MIoTOauthError as oauth_error:\r\n        ha_persistent_notify(\r\n            notify_id=f'{entry_id}.oauth_error',\r\n            title='Xiaomi Home Oauth Error',\r\n            message=f'Please re-add.\\r\\nerror: {oauth_error}'\r\n        )\r\n    except Exception as err:\r\n        raise err\r\n\r\n    return True\r\n\r\n\r\nasync def async_unload_entry(\r\n    hass: HomeAssistant, config_entry: ConfigEntry\r\n) -> bool:\r\n    \"\"\"Unload the entry.\"\"\"\r\n    entry_id = config_entry.entry_id\r\n    # Unload the platform\r\n    unload_ok = await hass.config_entries.async_unload_platforms(\r\n        config_entry, SUPPORTED_PLATFORMS)\r\n    if unload_ok:\r\n        hass.data[DOMAIN]['entities'].pop(entry_id, None)\r\n        hass.data[DOMAIN]['devices'].pop(entry_id, None)\r\n    # Remove integration data\r\n    miot_client: MIoTClient = hass.data[DOMAIN]['miot_clients'].pop(\r\n        entry_id, None)\r\n    if miot_client:\r\n        await miot_client.deinit_async()\r\n    del miot_client\r\n    return True\r\n\r\n\r\nasync def async_remove_entry(\r\n    hass: HomeAssistant, config_entry: ConfigEntry\r\n) -> bool:\r\n    \"\"\"Remove the entry.\"\"\"\r\n    entry_data = dict(config_entry.data)\r\n    uid: str = entry_data['uid']\r\n    cloud_server: str = entry_data['cloud_server']\r\n    miot_storage: MIoTStorage = hass.data[DOMAIN]['miot_storage']\r\n    miot_cert: MIoTCert = MIoTCert(\r\n        storage=miot_storage, uid=uid, cloud_server=cloud_server)\r\n\r\n    # Clean device list\r\n    await miot_storage.remove_async(\r\n        domain='miot_devices', name=f'{uid}_{cloud_server}', type_=dict)\r\n    # Clean user configuration\r\n    await miot_storage.update_user_config_async(\r\n        uid=uid, cloud_server=cloud_server, config=None)\r\n    # Clean cert file\r\n    await miot_cert.remove_user_cert_async()\r\n    await miot_cert.remove_user_key_async()\r\n    return True\r\n\r\n\r\nasync def async_remove_config_entry_device(\r\n    hass: HomeAssistant,\r\n    config_entry: ConfigEntry,\r\n    device_entry: device_registry.DeviceEntry\r\n) -> bool:\r\n    \"\"\"Remove the device.\"\"\"\r\n    miot_client: MIoTClient = await get_miot_instance_async(\r\n        hass=hass, entry_id=config_entry.entry_id)\r\n\r\n    if len(device_entry.identifiers) != 1:\r\n        _LOGGER.error(\r\n            'remove device failed, invalid identifiers, %s, %s',\r\n            device_entry.id, device_entry.identifiers)\r\n        return False\r\n    identifiers = list(device_entry.identifiers)[0]\r\n    if identifiers[0] != DOMAIN:\r\n        _LOGGER.error(\r\n            'remove device failed, invalid domain, %s, %s',\r\n            device_entry.id, device_entry.identifiers)\r\n        return False\r\n\r\n    # Remove device\r\n    await miot_client.remove_device2_async(did_tag=identifiers[1])\r\n    device_registry.async_get(hass).async_remove_device(device_entry.id)\r\n    _LOGGER.info(\r\n        'remove device, %s, %s', identifiers[1], device_entry.id)\r\n    return True\r\n"
  },
  {
    "path": "custom_components/xiaomi_home/binary_sensor.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nBinary sensor entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.binary_sensor import BinarySensorEntity\n\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.miot_device import MIoTDevice, MIoTPropertyEntity\nfrom .miot.const import DOMAIN\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        if miot_device.miot_client.display_binary_bool:\n            for prop in miot_device.prop_list.get('binary_sensor', []):\n                new_entities.append(\n                    BinarySensor(miot_device=miot_device, spec=prop))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass BinarySensor(MIoTPropertyEntity, BinarySensorEntity):\n    \"\"\"Binary sensor entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        \"\"\"Initialize the BinarySensor.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        # Set device_class\n        self._attr_device_class = spec.device_class\n\n    @property\n    def is_on(self) -> bool:\n        \"\"\"On/Off state. True if the binary sensor is on, False otherwise.\"\"\"\n        if self.spec.name == 'contact-state':\n            return bool(self._value) is False\n        elif self.spec.name == 'occupancy-status':\n            return bool(self._value)\n        return self._value is True\n"
  },
  {
    "path": "custom_components/xiaomi_home/button.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nButton entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.button import ButtonEntity\n\nfrom .miot.miot_device import MIoTActionEntity, MIoTDevice\nfrom .miot.miot_spec import MIoTSpecAction\nfrom .miot.const import DOMAIN\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for action in miot_device.action_list.get('button', []):\n            new_entities.append(Button(miot_device=miot_device, spec=action))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Button(MIoTActionEntity, ButtonEntity):\n    \"\"\"Button entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:\n        \"\"\"Initialize the Button.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        # Use default device class\n\n    async def async_press(self) -> None:\n        \"\"\"Press the button.\"\"\"\n        return await self.action_async()\n"
  },
  {
    "path": "custom_components/xiaomi_home/climate.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nClimate entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any, Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.const import UnitOfTemperature\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.climate import (\n    FAN_ON, FAN_OFF, SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL,\n    ATTR_TEMPERATURE, HVACMode, HVACAction, ClimateEntity, ClimateEntityFeature)\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData\nfrom .miot.miot_spec import MIoTSpecProperty\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,\n                            async_add_entities: AddEntitiesCallback) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('air-conditioner', []):\n            new_entities.append(\n                AirConditioner(miot_device=miot_device, entity_data=data))\n        for data in miot_device.entity_list.get('heater', []):\n            new_entities.append(\n                Heater(miot_device=miot_device, entity_data=data))\n        for data in miot_device.entity_list.get('bath-heater', []):\n            new_entities.append(\n                PtcBathHeater(miot_device=miot_device, entity_data=data))\n        for data in miot_device.entity_list.get('thermostat', []):\n            new_entities.append(\n                Thermostat(miot_device=miot_device, entity_data=data))\n        for data in miot_device.entity_list.get('electric-blanket', []):\n            new_entities.append(\n                ElectricBlanket(miot_device=miot_device, entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass FeatureOnOff(MIoTServiceEntity, ClimateEntity):\n    \"\"\"TURN_ON and TURN_OFF feature of the climate entity.\"\"\"\n    _prop_on: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_on = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n    def _init_on_off(self, service_name: str, prop_name: str) -> None:\n        \"\"\"Initialize the on_off feature.\"\"\"\n        for prop in self.entity_data.props:\n            if prop.name == prop_name and prop.service.name == service_name:\n                if prop.format_ != bool:\n                    _LOGGER.error('wrong format %s %s, %s', service_name,\n                                  prop_name, self.entity_id)\n                    continue\n                self._attr_supported_features |= ClimateEntityFeature.TURN_ON\n                self._attr_supported_features |= ClimateEntityFeature.TURN_OFF\n                self._prop_on = prop\n                break\n\n    async def async_turn_on(self) -> None:\n        \"\"\"Turn on.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=True)\n\n    async def async_turn_off(self) -> None:\n        \"\"\"Turn off.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=False)\n\n\nclass FeatureTargetTemperature(MIoTServiceEntity, ClimateEntity):\n    \"\"\"TARGET_TEMPERATURE feature of the climate entity.\"\"\"\n    _prop_target_temp: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_target_temp = None\n        self._attr_temperature_unit = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'target-temperature':\n                if not prop.value_range:\n                    _LOGGER.error(\n                        'invalid target-temperature value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._attr_min_temp = prop.value_range.min_\n                self._attr_max_temp = prop.value_range.max_\n                self._attr_target_temperature_step = prop.value_range.step\n                self._attr_temperature_unit = prop.external_unit\n                self._attr_supported_features |= (\n                    ClimateEntityFeature.TARGET_TEMPERATURE)\n                self._prop_target_temp = prop\n                break\n        # temperature_unit is required by the climate entity\n        if not self._attr_temperature_unit:\n            self._attr_temperature_unit = UnitOfTemperature.CELSIUS\n\n    async def async_set_temperature(self, **kwargs):\n        \"\"\"Set the target temperature.\"\"\"\n        if ATTR_TEMPERATURE in kwargs:\n            temp = kwargs[ATTR_TEMPERATURE]\n            if temp > self._attr_max_temp:\n                temp = self._attr_max_temp\n            elif temp < self._attr_min_temp:\n                temp = self._attr_min_temp\n\n            await self.set_property_async(prop=self._prop_target_temp,\n                                          value=temp)\n\n    @property\n    def target_temperature(self) -> Optional[float]:\n        \"\"\"The current target temperature.\"\"\"\n        return (self.get_prop_value(\n            prop=self._prop_target_temp) if self._prop_target_temp else None)\n\n\nclass FeaturePresetMode(MIoTServiceEntity, ClimateEntity):\n    \"\"\"PRESET_MODE feature of the climate entity.\"\"\"\n    _prop_mode: Optional[MIoTSpecProperty]\n    _mode_map: Optional[dict[int, str]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_mode = None\n        self._mode_map = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n    def _init_preset_modes(self, service_name: str, prop_name: str) -> None:\n        \"\"\"Initialize the preset modes.\"\"\"\n        for prop in self.entity_data.props:\n            if prop.name == prop_name and prop.service.name == service_name:\n                if not prop.value_list:\n                    _LOGGER.error('invalid %s %s value_list, %s', service_name,\n                                  prop_name, self.entity_id)\n                    continue\n                self._mode_map = prop.value_list.to_map()\n                self._attr_preset_modes = prop.value_list.descriptions\n                self._attr_supported_features |= (\n                    ClimateEntityFeature.PRESET_MODE)\n                self._prop_mode = prop\n                break\n\n    async def async_set_preset_mode(self, preset_mode: str) -> None:\n        \"\"\"Set the preset mode.\"\"\"\n        await self.set_property_async(self._prop_mode,\n                                      value=self.get_map_key(\n                                          map_=self._mode_map,\n                                          value=preset_mode))\n\n    @property\n    def preset_mode(self) -> Optional[str]:\n        \"\"\"The current preset mode.\"\"\"\n        return (self.get_map_value(\n            map_=self._mode_map, key=self.get_prop_value(\n                prop=self._prop_mode)) if self._prop_mode else None)\n\n\nclass FeatureFanMode(MIoTServiceEntity, ClimateEntity):\n    \"\"\"FAN_MODE feature of the climate entity.\"\"\"\n    _prop_fan_on: Optional[MIoTSpecProperty]\n    _prop_fan_level: Optional[MIoTSpecProperty]\n    _fan_mode_map: Optional[dict[int, str]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_fan_on = None\n        self._prop_fan_level = None\n        self._fan_mode_map = None\n        self._attr_fan_modes = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if (prop.name == 'fan-level' and\n                (prop.service.name == 'fan-control' or\n                 prop.service.name == 'thermostat')):\n                if not prop.value_list:\n                    _LOGGER.error('invalid fan-level value_list, %s',\n                                  self.entity_id)\n                    continue\n                self._fan_mode_map = prop.value_list.to_map()\n                self._attr_fan_modes = prop.value_list.descriptions\n                self._attr_supported_features |= ClimateEntityFeature.FAN_MODE\n                self._prop_fan_level = prop\n            elif prop.name == 'on' and prop.service.name == 'fan-control':\n                self._prop_fan_on = prop\n                self._attr_supported_features |= ClimateEntityFeature.FAN_MODE\n\n        if self._prop_fan_on:\n            if self._attr_fan_modes is None:\n                self._attr_fan_modes = [FAN_ON, FAN_OFF]\n            else:\n                self._attr_fan_modes.append(FAN_OFF)\n\n    async def async_set_fan_mode(self, fan_mode):\n        \"\"\"Set the target fan mode.\"\"\"\n        if fan_mode == FAN_OFF:\n            await self.set_property_async(prop=self._prop_fan_on, value=False)\n            return\n        if fan_mode == FAN_ON:\n            await self.set_property_async(prop=self._prop_fan_on, value=True)\n            return\n        mode_value = self.get_map_key(map_=self._fan_mode_map, value=fan_mode)\n        if mode_value is None or not await self.set_property_async(\n                prop=self._prop_fan_level, value=mode_value):\n            raise RuntimeError(f'set climate prop.fan_mode failed, {fan_mode}, '\n                               f'{self.entity_id}')\n\n    @property\n    def fan_mode(self) -> Optional[str]:\n        \"\"\"The current fan mode.\"\"\"\n        if self._prop_fan_level is None and self._prop_fan_on is None:\n            return None\n        if self._prop_fan_level is None and self._prop_fan_on:\n            return (FAN_ON if self.get_prop_value(\n                prop=self._prop_fan_on) else FAN_OFF)\n        return self.get_map_value(\n            map_=self._fan_mode_map,\n            key=self.get_prop_value(prop=self._prop_fan_level))\n\n\nclass FeatureSwingMode(MIoTServiceEntity, ClimateEntity):\n    \"\"\"SWING_MODE feature of the climate entity.\"\"\"\n    _prop_horizontal_swing: Optional[MIoTSpecProperty]\n    _prop_vertical_swing: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_horizontal_swing = None\n        self._prop_vertical_swing = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        swing_modes = []\n        for prop in entity_data.props:\n            if prop.name == 'horizontal-swing':\n                swing_modes.append(SWING_HORIZONTAL)\n                self._prop_horizontal_swing = prop\n            elif prop.name == 'vertical-swing':\n                swing_modes.append(SWING_VERTICAL)\n                self._prop_vertical_swing = prop\n        # swing modes\n        if SWING_HORIZONTAL in swing_modes and SWING_VERTICAL in swing_modes:\n            swing_modes.append(SWING_BOTH)\n        if swing_modes:\n            swing_modes.insert(0, SWING_OFF)\n            self._attr_supported_features |= ClimateEntityFeature.SWING_MODE\n            self._attr_swing_modes = swing_modes\n\n    async def async_set_swing_mode(self, swing_mode):\n        \"\"\"Set the target swing operation.\"\"\"\n        if swing_mode == SWING_BOTH:\n            await self.set_property_async(prop=self._prop_horizontal_swing,\n                                          value=True)\n            await self.set_property_async(prop=self._prop_vertical_swing,\n                                          value=True)\n        elif swing_mode == SWING_HORIZONTAL:\n            if self._prop_vertical_swing:\n                await self.set_property_async(prop=self._prop_vertical_swing,\n                                              value=False)\n            await self.set_property_async(prop=self._prop_horizontal_swing,\n                                          value=True)\n        elif swing_mode == SWING_VERTICAL:\n            if self._prop_horizontal_swing:\n                await self.set_property_async(prop=self._prop_horizontal_swing,\n                                              value=False)\n            await self.set_property_async(prop=self._prop_vertical_swing,\n                                          value=True)\n        elif swing_mode == SWING_OFF:\n            if self._prop_horizontal_swing:\n                await self.set_property_async(prop=self._prop_horizontal_swing,\n                                              value=False)\n            if self._prop_vertical_swing:\n                await self.set_property_async(prop=self._prop_vertical_swing,\n                                              value=False)\n        else:\n            raise RuntimeError(\n                f'unknown swing_mode, {swing_mode}, {self.entity_id}')\n\n    @property\n    def swing_mode(self) -> Optional[str]:\n        \"\"\"The current swing mode of the fan.\"\"\"\n        if (self._prop_horizontal_swing is None and\n                self._prop_vertical_swing is None):\n            return None\n        horizontal: bool = (self.get_prop_value(\n            prop=self._prop_horizontal_swing)\n                            if self._prop_horizontal_swing else False)\n        vertical: bool = (self.get_prop_value(prop=self._prop_vertical_swing)\n                          if self._prop_vertical_swing else False)\n        if horizontal and vertical:\n            return SWING_BOTH\n        elif horizontal:\n            return SWING_HORIZONTAL\n        elif vertical:\n            return SWING_VERTICAL\n        else:\n            return SWING_OFF\n\n\nclass FeatureTemperature(MIoTServiceEntity, ClimateEntity):\n    \"\"\"Temperature of the climate entity.\"\"\"\n    _prop_env_temperature: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_env_temperature = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'temperature':\n                self._prop_env_temperature = prop\n                break\n\n    @property\n    def current_temperature(self) -> Optional[float]:\n        \"\"\"The current environment temperature.\"\"\"\n        return (self.get_prop_value(prop=self._prop_env_temperature)\n                if self._prop_env_temperature else None)\n\n\nclass FeatureHumidity(MIoTServiceEntity, ClimateEntity):\n    \"\"\"Humidity of the climate entity.\"\"\"\n    _prop_env_humidity: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_env_humidity = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'relative-humidity':\n                self._prop_env_humidity = prop\n                break\n\n    @property\n    def current_humidity(self) -> Optional[float]:\n        \"\"\"The current environment humidity.\"\"\"\n        return (self.get_prop_value(\n            prop=self._prop_env_humidity) if self._prop_env_humidity else None)\n\n\nclass FeatureTargetHumidity(MIoTServiceEntity, ClimateEntity):\n    \"\"\"TARGET_HUMIDITY feature of the climate entity.\"\"\"\n    _prop_target_humidity: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_target_humidity = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'target-humidity':\n                if not prop.value_range:\n                    _LOGGER.error(\n                        'invalid target-humidity value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._attr_min_humidity = prop.value_range.min_\n                self._attr_max_humidity = prop.value_range.max_\n                self._attr_supported_features |= (\n                    ClimateEntityFeature.TARGET_HUMIDITY)\n                self._prop_target_humidity = prop\n                break\n\n    async def async_set_humidity(self, humidity):\n        \"\"\"Set the target humidity.\"\"\"\n        if humidity > self._attr_max_humidity:\n            humidity = self._attr_max_humidity\n        elif humidity < self._attr_min_humidity:\n            humidity = self._attr_min_humidity\n        await self.set_property_async(prop=self._prop_target_humidity,\n                                      value=humidity)\n\n    @property\n    def target_humidity(self) -> Optional[int]:\n        \"\"\"The current target humidity.\"\"\"\n        return (self.get_prop_value(prop=self._prop_target_humidity)\n                if self._prop_target_humidity else None)\n\n\nclass Heater(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,\n             FeatureHumidity, FeaturePresetMode):\n    \"\"\"Heater\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the heater.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n        self._attr_icon = 'mdi:radiator'\n        # hvac modes\n        self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]\n        # on/off\n        self._init_on_off('heater', 'on')\n        # preset modes\n        self._init_preset_modes('heater', 'heat-level')\n\n    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:\n        \"\"\"Set the target hvac mode.\"\"\"\n        await self.set_property_async(\n            prop=self._prop_on,\n            value=False if hvac_mode == HVACMode.OFF else True)\n\n    @property\n    def hvac_mode(self) -> Optional[HVACMode]:\n        \"\"\"The current hvac mode.\"\"\"\n        return (HVACMode.HEAT if self.get_prop_value(\n            prop=self._prop_on) else HVACMode.OFF)\n\n    @property\n    def hvac_action(self) -> Optional[HVACAction]:\n        \"\"\"The current hvac action.\"\"\"\n        if self.hvac_mode == HVACMode.HEAT:\n            return HVACAction.HEATING\n        return HVACAction.OFF\n\n\nclass AirConditioner(FeatureOnOff, FeatureTargetTemperature,\n                     FeatureTargetHumidity, FeatureTemperature, FeatureHumidity,\n                     FeatureFanMode, FeatureSwingMode):\n    \"\"\"Air conditioner\"\"\"\n    _prop_mode: Optional[MIoTSpecProperty]\n    _hvac_mode_map: Optional[dict[int, HVACMode]]\n    _prop_ac_state: Optional[MIoTSpecProperty]\n    _value_ac_state: Optional[dict[str, int]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the air conditioner.\"\"\"\n        self._prop_mode = None\n        self._hvac_mode_map = None\n        self._prop_ac_state = None\n        self._value_ac_state = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._attr_icon = 'mdi:air-conditioner'\n        # on/off\n        self._init_on_off('air-conditioner', 'on')\n        # hvac modes\n        self._attr_hvac_modes = None\n        for prop in entity_data.props:\n            if prop.name == 'mode' and prop.service.name == 'air-conditioner':\n                if not prop.value_list:\n                    _LOGGER.error('invalid mode value_list, %s', self.entity_id)\n                    continue\n                self._hvac_mode_map = {}\n                for item in prop.value_list.items:\n                    if item.name in {'off', 'idle'}:\n                        self._hvac_mode_map[item.value] = HVACMode.OFF\n                    elif item.name in {'auto'}:\n                        self._hvac_mode_map[item.value] = HVACMode.AUTO\n                    elif item.name in {'cool'}:\n                        self._hvac_mode_map[item.value] = HVACMode.COOL\n                    elif item.name in {'heat'}:\n                        self._hvac_mode_map[item.value] = HVACMode.HEAT\n                    elif item.name in {'dry'}:\n                        self._hvac_mode_map[item.value] = HVACMode.DRY\n                    elif item.name in {'fan'}:\n                        self._hvac_mode_map[item.value] = HVACMode.FAN_ONLY\n                    elif item.name in {'heat_cool'}:\n                        self._hvac_mode_map[item.value] = HVACMode.HEAT_COOL\n                self._attr_hvac_modes = list(self._hvac_mode_map.values())\n                self._prop_mode = prop\n            elif prop.name == 'ac-state':\n                self._prop_ac_state = prop\n                self._value_ac_state = {}\n                self.sub_prop_changed(prop=prop,\n                                      handler=self.__ac_state_changed)\n\n        if self._attr_hvac_modes is None:\n            self._attr_hvac_modes = [HVACMode.OFF]\n        elif HVACMode.OFF not in self._attr_hvac_modes:\n            self._attr_hvac_modes.append(HVACMode.OFF)\n\n    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:\n        \"\"\"Set the target hvac mode.\"\"\"\n        # set the device off\n        if hvac_mode == HVACMode.OFF:\n            if not await self.set_property_async(prop=self._prop_on,\n                                                 value=False):\n                raise RuntimeError(f'set climate prop.on failed, {hvac_mode}, '\n                                   f'{self.entity_id}')\n            return\n        # set the device on\n        if self.get_prop_value(prop=self._prop_on) is not True:\n            await self.set_property_async(prop=self._prop_on,\n                                          value=True,\n                                          write_ha_state=False)\n        # set mode\n        if self._prop_mode is None:\n            return\n        mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode)\n        if mode_value is None or not await self.set_property_async(\n                prop=self._prop_mode, value=mode_value):\n            raise RuntimeError(\n                f'set climate prop.mode failed, {hvac_mode}, {self.entity_id}')\n\n    @property\n    def hvac_mode(self) -> Optional[HVACMode]:\n        \"\"\"The current hvac mode.\"\"\"\n        if self.get_prop_value(prop=self._prop_on) is False:\n            return HVACMode.OFF\n        return (self.get_map_value(map_=self._hvac_mode_map,\n                                   key=self.get_prop_value(\n                                       prop=self._prop_mode))\n                if self._prop_mode else None)\n\n    @property\n    def hvac_action(self) -> Optional[HVACAction]:\n        \"\"\"The current hvac action.\"\"\"\n        if self.hvac_mode is None:\n            return None\n        if self.hvac_mode == HVACMode.OFF:\n            return HVACAction.OFF\n        if self.hvac_mode == HVACMode.FAN_ONLY:\n            return HVACAction.FAN\n        if self.hvac_mode == HVACMode.COOL:\n            return HVACAction.COOLING\n        if self.hvac_mode == HVACMode.HEAT:\n            return HVACAction.HEATING\n        if self.hvac_mode == HVACMode.DRY:\n            return HVACAction.DRYING\n        return HVACAction.IDLE\n\n    def __ac_state_changed(self, prop: MIoTSpecProperty, value: Any) -> None:\n        del prop\n        if not isinstance(value, str):\n            _LOGGER.error('ac_status value format error, %s', value)\n            return\n        v_ac_state = {}\n        v_split = value.split('_')\n        for item in v_split:\n            if len(item) < 2:\n                _LOGGER.error('ac_status value error, %s', item)\n                continue\n            try:\n                v_ac_state[item[0]] = int(item[1:])\n            except ValueError:\n                _LOGGER.error('ac_status value error, %s', item)\n        # P: status. 0: on, 1: off\n        if 'P' in v_ac_state and self._prop_on:\n            self.set_prop_value(prop=self._prop_on, value=v_ac_state['P'] == 0)\n        # M: model. 0: cool, 1: heat, 2: auto, 3: fan, 4: dry\n        if 'M' in v_ac_state and self._prop_mode:\n            mode: Optional[HVACMode] = {\n                0: HVACMode.COOL,\n                1: HVACMode.HEAT,\n                2: HVACMode.AUTO,\n                3: HVACMode.FAN_ONLY,\n                4: HVACMode.DRY,\n            }.get(v_ac_state['M'], None)\n            if mode:\n                self.set_prop_value(prop=self._prop_mode,\n                                    value=self.get_map_key(\n                                        map_=self._hvac_mode_map, value=mode))\n        # T: target temperature\n        if 'T' in v_ac_state and self._prop_target_temp:\n            self.set_prop_value(prop=self._prop_target_temp,\n                                value=v_ac_state['T'])\n        # S: fan level. 0: auto, 1: low, 2: media, 3: high\n        if 'S' in v_ac_state and self._prop_fan_level:\n            self.set_prop_value(prop=self._prop_fan_level,\n                                value=v_ac_state['S'])\n        # D: swing mode. 0: on, 1: off\n        if ('D' in v_ac_state and self._attr_swing_modes and\n                len(self._attr_swing_modes) == 2):\n            if (SWING_HORIZONTAL in self._attr_swing_modes and\n                    self._prop_horizontal_swing):\n                self.set_prop_value(prop=self._prop_horizontal_swing,\n                                    value=v_ac_state['D'] == 0)\n            elif (SWING_VERTICAL in self._attr_swing_modes and\n                  self._prop_vertical_swing):\n                self.set_prop_value(prop=self._prop_vertical_swing,\n                                    value=v_ac_state['D'] == 0)\n\n        self._value_ac_state.update(v_ac_state)\n        _LOGGER.debug('ac_state update, %s', self._value_ac_state)\n\n\nclass PtcBathHeater(FeatureTargetTemperature, FeatureTemperature,\n                    FeatureFanMode, FeatureSwingMode, FeaturePresetMode):\n    \"\"\"Ptc bath heater\"\"\"\n    _prop_mode: Optional[MIoTSpecProperty]\n    _hvac_mode_map: Optional[dict[int, HVACMode]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the ptc bath heater.\"\"\"\n        self._prop_mode = None\n        self._hvac_mode_map = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._attr_icon = 'mdi:hvac'\n        # hvac modes\n        for prop in entity_data.props:\n            if prop.name == 'mode' and prop.service.name == 'ptc-bath-heater':\n                if not prop.value_list:\n                    _LOGGER.error('invalid mode value_list, %s', self.entity_id)\n                    continue\n                self._hvac_mode_map = {}\n                for item in prop.value_list.items:\n                    if item.name in {'off', 'idle'}:\n                        self._hvac_mode_map[item.value] = HVACMode.OFF\n                        break\n                if self._hvac_mode_map:\n                    self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]\n                else:\n                    _LOGGER.error('no idle mode, %s', self.entity_id)\n        # preset modes\n        self._init_preset_modes('ptc-bath-heater', 'mode')\n\n    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:\n        \"\"\"Set the target hvac mode.\"\"\"\n        if self._prop_mode is None or hvac_mode != HVACMode.OFF:\n            return\n        mode_value = self.get_map_key(map_=self._hvac_mode_map, value=hvac_mode)\n        if mode_value is None or not await self.set_property_async(\n                prop=self._prop_mode, value=mode_value):\n            raise RuntimeError(\n                f'set ptc-bath-heater {hvac_mode} failed, {self.entity_id}')\n\n    @property\n    def hvac_mode(self) -> Optional[HVACMode]:\n        \"\"\"The current hvac mode.\"\"\"\n        if self._prop_mode is None:\n            return None\n        current_mode = self.get_prop_value(prop=self._prop_mode)\n        if current_mode is None:\n            return None\n        mode_value = self.get_map_value(map_=self._hvac_mode_map,\n                                        key=current_mode)\n        return HVACMode.OFF if mode_value == HVACMode.OFF else HVACMode.AUTO\n\n\nclass Thermostat(FeatureOnOff, FeatureTargetTemperature, FeatureTemperature,\n                 FeatureHumidity, FeatureFanMode, FeaturePresetMode):\n    \"\"\"Thermostat\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the thermostat.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n        self._attr_icon = 'mdi:thermostat'\n        # hvac modes\n        self._attr_hvac_modes = [HVACMode.AUTO, HVACMode.OFF]\n        # on/off\n        self._init_on_off('thermostat', 'on')\n        # preset modes\n        self._init_preset_modes('thermostat', 'mode')\n\n    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:\n        \"\"\"Set the target hvac mode.\"\"\"\n        await self.set_property_async(\n            prop=self._prop_on,\n            value=False if hvac_mode == HVACMode.OFF else True)\n\n    @property\n    def hvac_mode(self) -> Optional[HVACMode]:\n        \"\"\"The current hvac mode.\"\"\"\n        return (HVACMode.AUTO if self.get_prop_value(\n            prop=self._prop_on) else HVACMode.OFF)\n\n\nclass ElectricBlanket(FeatureOnOff, FeatureTargetTemperature,\n                      FeatureTemperature, FeaturePresetMode):\n    \"\"\"Electric blanket\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the electric blanket.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n        self._attr_icon = 'mdi:rug'\n        # hvac modes\n        self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]\n        # on/off\n        self._init_on_off('electric-blanket', 'on')\n        # preset modes\n        self._init_preset_modes('electric-blanket', 'mode')\n\n    async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:\n        \"\"\"Set the target hvac mode.\"\"\"\n        await self.set_property_async(\n            prop=self._prop_on,\n            value=False if hvac_mode == HVACMode.OFF else True)\n\n    @property\n    def hvac_mode(self) -> Optional[HVACMode]:\n        \"\"\"The current hvac mode.\"\"\"\n        return (HVACMode.HEAT if self.get_prop_value(\n            prop=self._prop_on) else HVACMode.OFF)\n\n    @property\n    def hvac_action(self) -> Optional[HVACAction]:\n        \"\"\"The current hvac action.\"\"\"\n        if self.hvac_mode == HVACMode.OFF:\n            return HVACAction.OFF\n        return HVACAction.HEATING\n"
  },
  {
    "path": "custom_components/xiaomi_home/config_flow.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nConfig flow for Xiaomi Home.\n\"\"\"\nimport asyncio\nimport hashlib\nimport ipaddress\nimport json\nimport secrets\nimport traceback\nfrom typing import Optional, Set, Tuple\nfrom urllib.parse import urlparse\nfrom aiohttp import web\nfrom aiohttp.hdrs import METH_GET\nimport voluptuous as vol\nimport logging\n\nfrom homeassistant import config_entries\nfrom homeassistant.components import zeroconf\nfrom homeassistant.components.zeroconf import HaAsyncZeroconf\nfrom homeassistant.components.webhook import (\n    async_register as webhook_async_register,\n    async_unregister as webhook_async_unregister,\n    async_generate_path as webhook_async_generate_path\n)\nfrom homeassistant.core import callback\nfrom homeassistant.data_entry_flow import AbortFlow\nfrom homeassistant.helpers.instance_id import async_get\nimport homeassistant.helpers.config_validation as cv\n\nfrom .miot.const import (\n    DEFAULT_CLOUD_SERVER,\n    DEFAULT_CTRL_MODE,\n    DEFAULT_INTEGRATION_LANGUAGE,\n    DEFAULT_COVER_DEAD_ZONE_WIDTH,\n    MIN_COVER_DEAD_ZONE_WIDTH,\n    MAX_COVER_DEAD_ZONE_WIDTH,\n    DEFAULT_NICK_NAME,\n    DEFAULT_OAUTH2_API_HOST,\n    DEFAULT_CLOUD_BROKER_HOST,\n    DOMAIN,\n    OAUTH2_AUTH_URL,\n    OAUTH2_CLIENT_ID,\n    CLOUD_SERVERS,\n    OAUTH_REDIRECT_URL,\n    INTEGRATION_LANGUAGES,\n    SUPPORT_CENTRAL_GATEWAY_CTRL,\n    NETWORK_REFRESH_INTERVAL,\n    MIHOME_CERT_EXPIRE_MARGIN\n)\nfrom .miot.miot_cloud import MIoTHttpClient, MIoTOauthClient\nfrom .miot.miot_storage import MIoTStorage, MIoTCert\nfrom .miot.miot_mdns import MipsService\nfrom .miot.web_pages import oauth_redirect_page\nfrom .miot.miot_error import (\n    MIoTConfigError, MIoTError, MIoTErrorCode, MIoTOauthError)\nfrom .miot.miot_i18n import MIoTI18n\nfrom .miot.miot_network import MIoTNetwork\nfrom .miot.miot_client import MIoTClient, get_miot_instance_async\nfrom .miot.miot_spec import MIoTSpecParser\nfrom .miot.miot_lan import MIoTLan\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass XiaomiMihomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):\n    \"\"\"Xiaomi Home config flow.\"\"\"\n    # pylint: disable=unused-argument, inconsistent-quotes\n    VERSION = 1\n    MINOR_VERSION = 1\n    DEFAULT_AREA_NAME_RULE = 'room'\n    _main_loop: asyncio.AbstractEventLoop\n    _miot_network: MIoTNetwork\n    _mips_service: MipsService\n    _miot_storage: MIoTStorage\n    _miot_i18n: MIoTI18n\n    _miot_oauth: Optional[MIoTOauthClient]\n    _miot_http: Optional[MIoTHttpClient]\n\n    _storage_path: str\n    _virtual_did: str\n    _uid: str\n    _uuid: str\n    _ctrl_mode: str\n    _area_name_rule: str\n    _action_debug: bool\n    _hide_non_standard_entities: bool\n    _display_binary_mode: list[str]\n    _display_devices_changed_notify: list[str]\n\n    _cloud_server: str\n    _integration_language: str\n    _cover_dz_width: int\n    _auth_info: dict\n    _nick_name: str\n    _home_selected: dict\n    _devices_filter: dict\n    _device_list_sorted: dict\n    _oauth_redirect_url_full: str\n\n    # Config cache\n    _cc_home_info: dict\n    _cc_home_list_show: dict\n    _cc_network_detect_addr: str\n    _cc_oauth_auth_url: str\n    _cc_user_cert_done: bool\n    _cc_task_oauth: Optional[asyncio.Task[None]]\n    _cc_config_rc: Optional[str]\n    _cc_fut_oauth_code: Optional[asyncio.Future]\n    _opt_check_network_deps: bool\n\n    def __init__(self) -> None:\n        self._main_loop = asyncio.get_running_loop()\n        self._cloud_server = DEFAULT_CLOUD_SERVER\n        self._integration_language = DEFAULT_INTEGRATION_LANGUAGE\n        self._cover_dz_width = DEFAULT_COVER_DEAD_ZONE_WIDTH\n        self._storage_path = ''\n        self._virtual_did = ''\n        self._uid = ''\n        self._uuid = ''   # MQTT client id\n        self._ctrl_mode = DEFAULT_CTRL_MODE\n        self._area_name_rule = self.DEFAULT_AREA_NAME_RULE\n        self._action_debug = False\n        self._hide_non_standard_entities = False\n        self._display_binary_mode = ['bool']\n        self._display_devices_changed_notify = ['add', 'del', 'offline']\n        self._auth_info = {}\n        self._nick_name = DEFAULT_NICK_NAME\n        self._home_selected = {}\n        self._devices_filter = {}\n        self._device_list_sorted = {}\n        self._oauth_redirect_url_full = ''\n        self._miot_oauth = None\n        self._miot_http = None\n\n        self._cc_home_info = {}\n        self._cc_home_list_show = {}\n        self._cc_network_detect_addr = ''\n        self._cc_oauth_auth_url = ''\n        self._cc_user_cert_done = False\n        self._cc_task_oauth = None\n        self._cc_config_rc = None\n        self._cc_fut_oauth_code = None\n        self._opt_check_network_deps = False\n\n    async def async_step_user(\n        self, user_input: Optional[dict] = None\n    ):\n        self.hass.data.setdefault(DOMAIN, {})\n        if not self._virtual_did:\n            self._virtual_did = str(secrets.randbits(64))\n            self.hass.data[DOMAIN].setdefault(self._virtual_did, {})\n        if not self._storage_path:\n            self._storage_path = self.hass.config.path('.storage', DOMAIN)\n        # MIoT storage\n        self._miot_storage = self.hass.data[DOMAIN].get('miot_storage', None)\n        if not self._miot_storage:\n            self._miot_storage = MIoTStorage(\n                root_path=self._storage_path, loop=self._main_loop)\n            self.hass.data[DOMAIN]['miot_storage'] = self._miot_storage\n            _LOGGER.info(\n                'async_step_user, create miot storage, %s', self._storage_path)\n        # MIoT network\n        network_detect_addr = (await self._miot_storage.load_user_config_async(\n            uid='global_config', cloud_server='all',\n            keys=['network_detect_addr'])).get('network_detect_addr', {})\n        self._cc_network_detect_addr = ','.join(\n            network_detect_addr.get('ip', [])\n            + network_detect_addr.get('url', []))\n        self._miot_network = self.hass.data[DOMAIN].get('miot_network', None)\n        if not self._miot_network:\n            self._miot_network = MIoTNetwork(\n                ip_addr_list=network_detect_addr.get('ip', []),\n                url_addr_list=network_detect_addr.get('url', []),\n                refresh_interval=NETWORK_REFRESH_INTERVAL,\n                loop=self._main_loop)\n            self.hass.data[DOMAIN]['miot_network'] = self._miot_network\n            await self._miot_network.init_async()\n            _LOGGER.info('async_step_user, create miot network')\n        # MIPS service\n        self._mips_service = self.hass.data[DOMAIN].get('mips_service', None)\n        if not self._mips_service:\n            aiozc: HaAsyncZeroconf = await zeroconf.async_get_async_instance(\n                self.hass)\n            self._mips_service = MipsService(aiozc=aiozc, loop=self._main_loop)\n            self.hass.data[DOMAIN]['mips_service'] = self._mips_service\n            await self._mips_service.init_async()\n            _LOGGER.info('async_step_user, create mips service')\n\n        return await self.async_step_eula(user_input)\n\n    async def async_step_eula(\n        self, user_input: Optional[dict] = None\n    ):\n        if user_input:\n            if user_input.get('eula', None) is True:\n                return await self.async_step_auth_config()\n            return await self.__show_eula_form('eula_not_agree')\n        return await self.__show_eula_form('')\n\n    async def __show_eula_form(self, reason: str):\n        return self.async_show_form(\n            step_id='eula',\n            data_schema=vol.Schema({\n                vol.Required('eula', default=False): bool,  # type: ignore\n            }),\n            last_step=False,\n            errors={'base': reason},\n        )\n\n    async def async_step_auth_config(\n        self, user_input: Optional[dict] = None\n    ):\n        if user_input:\n            self._cloud_server = user_input.get(\n                'cloud_server', self._cloud_server)\n            # Gen instance uuid\n            ha_uuid = await async_get(self.hass)\n            if not ha_uuid:\n                raise AbortFlow(reason='ha_uuid_get_failed')\n            self._uuid = hashlib.sha256(\n                f'{ha_uuid}.{self._virtual_did}.{self._cloud_server}'.encode(\n                    'utf-8')).hexdigest()[:32]\n            self._integration_language = user_input.get(\n                'integration_language', DEFAULT_INTEGRATION_LANGUAGE)\n            self._miot_i18n = MIoTI18n(\n                lang=self._integration_language, loop=self._main_loop)\n            await self._miot_i18n.init_async()\n            webhook_path = webhook_async_generate_path(\n                webhook_id=self._virtual_did)\n            self._oauth_redirect_url_full = (\n                f'{user_input.get(\"oauth_redirect_url\")}{webhook_path}')\n\n            if user_input.get('network_detect_config', False):\n                return await self.async_step_network_detect_config()\n            return await self.async_step_oauth(user_input)\n        return await self.__show_auth_config_form('')\n\n    async def __show_auth_config_form(self, reason: str):\n        # Generate default language from HomeAssistant config (not user config)\n        default_language: str = self.hass.config.language\n        if default_language not in INTEGRATION_LANGUAGES:\n            if default_language.split('-', 1)[0] not in INTEGRATION_LANGUAGES:\n                default_language = DEFAULT_INTEGRATION_LANGUAGE\n            else:\n                default_language = default_language.split('-', 1)[0]\n        return self.async_show_form(\n            step_id='auth_config',\n            data_schema=vol.Schema({\n                vol.Required(\n                    'cloud_server',\n                    default=self._cloud_server  # type: ignore\n                ):  vol.In(CLOUD_SERVERS),\n                vol.Required(\n                    'integration_language',\n                    default=default_language  # type: ignore\n                ):   vol.In(INTEGRATION_LANGUAGES),\n                vol.Required(\n                    'oauth_redirect_url',\n                    default=OAUTH_REDIRECT_URL  # type: ignore\n                ): vol.In([OAUTH_REDIRECT_URL]),\n                vol.Required(\n                    'network_detect_config',\n                    default=False  # type: ignore\n                ): bool,\n            }),\n            errors={'base': reason},\n            last_step=False,\n        )\n\n    async def async_step_network_detect_config(\n        self, user_input: Optional[dict] = None\n    ):\n        if not user_input:\n            return await self.__show_network_detect_config_form(reason='')\n        self._cc_network_detect_addr = user_input.get(\n            'network_detect_addr', self._cc_network_detect_addr)\n\n        ip_list, url_list, invalid_list = _handle_network_detect_addr(\n            addr_str=self._cc_network_detect_addr)\n        if invalid_list:\n            return await self.__show_network_detect_config_form(\n                reason='invalid_network_addr')\n        if ip_list or url_list:\n            if ip_list and not await self._miot_network.ping_multi_async(\n                    ip_list=ip_list):\n                return await self.__show_network_detect_config_form(\n                    reason='invalid_ip_addr')\n            if url_list and not await self._miot_network.http_multi_async(\n                    url_list=url_list):\n                return await self.__show_network_detect_config_form(\n                    reason='invalid_http_addr')\n        else:\n            if not await self._miot_network.get_network_status_async():\n                return await self.__show_network_detect_config_form(\n                    reason='invalid_default_addr')\n        network_detect_addr: dict = {'ip': ip_list, 'url': url_list}\n        # Save\n        if await self._miot_storage.update_user_config_async(\n            uid='global_config', cloud_server='all', config={\n                'network_detect_addr': network_detect_addr}):\n            _LOGGER.info(\n                'update network_detect_addr, %s', network_detect_addr)\n        await self._miot_network.update_addr_list_async(\n            ip_addr_list=ip_list, url_addr_list=url_list)\n        # Check network deps\n        self._opt_check_network_deps = user_input.get(\n            'check_network_deps', self._opt_check_network_deps)\n        if self._opt_check_network_deps:\n            # OAuth2\n            if not await self._miot_network.http_multi_async(\n                    url_list=[OAUTH2_AUTH_URL]):\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_oauth2_host')\n            # HTTP API\n            http_host = (\n                DEFAULT_OAUTH2_API_HOST\n                if self._cloud_server == DEFAULT_CLOUD_SERVER\n                else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')\n            if not await self._miot_network.http_multi_async(\n                    url_list=[\n                        f'https://{http_host}/app/v2/ha/oauth/get_token']):\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_http_host')\n            # SPEC API\n            if not await self._miot_network.http_multi_async(\n                    url_list=[\n                        'https://miot-spec.org/miot-spec-v2/template/list/'\n                        'device']):\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_spec_host')\n            # MQTT Broker\n            # pylint: disable=import-outside-toplevel\n            try:\n                from paho.mqtt import client\n                mqtt_client = client.Client(\n                    client_id=f'ha.{self._uid}',\n                    protocol=client.MQTTv5)  # type: ignore\n                if mqtt_client.connect(\n                    host=f'{self._cloud_server}-{DEFAULT_CLOUD_BROKER_HOST}',\n                    port=8883) != 0:\n                    raise RuntimeError('mqtt connect error')\n                mqtt_client.disconnect()\n            except Exception as err:  # pylint: disable=broad-exception-caught\n                _LOGGER.error('try connect mqtt broker error, %s', err)\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_mqtt_broker')\n\n        return await self.async_step_oauth()\n\n    async def __show_network_detect_config_form(self, reason: str):\n        if not self._cc_network_detect_addr:\n            addr_list: dict = (await self._miot_storage.load_user_config_async(\n                'global_config', 'all', ['network_detect_addr'])).get(\n                    'network_detect_addr', {})\n            self._cc_network_detect_addr = ','.join(\n                addr_list.get('ip', [])+addr_list.get('url', []))\n        return self.async_show_form(\n            step_id='network_detect_config',\n            data_schema=vol.Schema({\n                vol.Optional(\n                    'network_detect_addr',\n                    default=self._cc_network_detect_addr  # type: ignore\n                ): str,\n                vol.Optional(\n                    'check_network_deps',\n                    default=self._opt_check_network_deps  # type: ignore\n                ): bool,\n            }),\n            errors={'base': reason},\n            description_placeholders={\n                'broker_host':\n                    f'{self._cloud_server}-{DEFAULT_CLOUD_BROKER_HOST}:8883',\n                'http_host': (\n                    DEFAULT_OAUTH2_API_HOST\n                    if self._cloud_server == DEFAULT_CLOUD_SERVER\n                    else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')},\n            last_step=False\n        )\n\n    async def async_step_oauth(\n        self, user_input: Optional[dict] = None\n    ):\n        # 1: Init miot_oauth, generate auth url\n        try:\n            if not self._miot_oauth:\n                _LOGGER.info(\n                    'async_step_oauth, redirect_url: %s',\n                    self._oauth_redirect_url_full)\n                miot_oauth = MIoTOauthClient(\n                    client_id=OAUTH2_CLIENT_ID,\n                    redirect_url=self._oauth_redirect_url_full,\n                    cloud_server=self._cloud_server,\n                    uuid=self._uuid,\n                    loop=self._main_loop)\n                self._cc_oauth_auth_url = miot_oauth.gen_auth_url(\n                    redirect_url=self._oauth_redirect_url_full)\n                self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = (\n                    miot_oauth.state)\n                self.hass.data[DOMAIN][self._virtual_did]['i18n'] = (\n                    self._miot_i18n)\n                _LOGGER.info(\n                    'async_step_oauth, oauth_url: %s', self._cc_oauth_auth_url)\n                webhook_async_unregister(\n                    self.hass, webhook_id=self._virtual_did)\n                webhook_async_register(\n                    self.hass,\n                    domain=DOMAIN,\n                    name='oauth redirect url webhook',\n                    webhook_id=self._virtual_did,\n                    handler=_handle_oauth_webhook,\n                    allowed_methods=(METH_GET,),\n                )\n                self._cc_fut_oauth_code = self.hass.data[DOMAIN][\n                    self._virtual_did].get('fut_oauth_code', None)\n                if not self._cc_fut_oauth_code:\n                    self._cc_fut_oauth_code = self._main_loop.create_future()\n                    self.hass.data[DOMAIN][self._virtual_did][\n                        'fut_oauth_code'] = self._cc_fut_oauth_code\n                _LOGGER.info(\n                    'async_step_oauth, webhook.async_register: %s',\n                    self._virtual_did)\n                self._miot_oauth = miot_oauth\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error(\n                'async_step_oauth, %s, %s', err, traceback.format_exc())\n            return self.async_show_progress_done(next_step_id='oauth_error')\n\n        # 2: show OAuth2 loading page\n        if self._cc_task_oauth is None:\n            self._cc_task_oauth = self.hass.async_create_task(\n                self.__check_oauth_async())\n        if self._cc_task_oauth.done():\n            if (error := self._cc_task_oauth.exception()):\n                _LOGGER.error('task_oauth exception, %s', error)\n                self._cc_config_rc = str(error)\n                return self.async_show_progress_done(next_step_id='oauth_error')\n            if self._miot_oauth:\n                await self._miot_oauth.deinit_async()\n                self._miot_oauth = None\n            return self.async_show_progress_done(next_step_id='homes_select')\n        # pylint: disable=unexpected-keyword-arg\n        return self.async_show_progress(\n            step_id='oauth',\n            progress_action='oauth',\n            description_placeholders={\n                'link_left':\n                    f'<a href=\"{self._cc_oauth_auth_url}\" target=\"_blank\">',\n                'link_right': '</a>'\n            },\n            progress_task=self._cc_task_oauth,  # type: ignore\n        )\n\n    async def __check_oauth_async(self) -> None:\n        # TASK 1: Get oauth code\n        if not self._cc_fut_oauth_code:\n            raise MIoTConfigError('oauth_code_fut_error')\n        oauth_code: Optional[str] = await self._cc_fut_oauth_code\n        if not oauth_code:\n            raise MIoTConfigError('oauth_code_error')\n        # TASK 2: Get access_token and user_info from miot_oauth\n        if not self._auth_info:\n            try:\n                if not self._miot_oauth:\n                    raise MIoTConfigError('oauth_client_error')\n                auth_info = await self._miot_oauth.get_access_token_async(\n                    code=oauth_code)\n                if not self._miot_http:\n                    self._miot_http = MIoTHttpClient(\n                        cloud_server=self._cloud_server,\n                        client_id=OAUTH2_CLIENT_ID,\n                        access_token=auth_info['access_token'])\n                else:\n                    self._miot_http.update_http_header(\n                        cloud_server=self._cloud_server,\n                        client_id=OAUTH2_CLIENT_ID,\n                        access_token=auth_info['access_token'])\n                self._auth_info = auth_info\n                try:\n                    self._nick_name = (\n                        await self._miot_http.get_user_info_async() or {}\n                    ).get('miliaoNick', self._nick_name)\n                except (MIoTOauthError, json.JSONDecodeError):\n                    self._nick_name = DEFAULT_NICK_NAME\n                    _LOGGER.error('get nick name failed')\n            except Exception as err:\n                _LOGGER.error(\n                    'get_access_token, %s, %s', err, traceback.format_exc())\n                raise MIoTConfigError('get_token_error') from err\n\n        # TASK 3: Get home info\n        try:\n            if not self._miot_http:\n                raise MIoTConfigError('http_client_error')\n            self._cc_home_info = (\n                await self._miot_http.get_devices_async())\n            _LOGGER.info('get_homeinfos response: %s', self._cc_home_info)\n            self._uid = self._cc_home_info['uid']\n            if self._uid == self._nick_name:\n                self._nick_name = DEFAULT_NICK_NAME\n            # Save auth_info\n            if not (await self._miot_storage.update_user_config_async(\n                    uid=self._uid, cloud_server=self._cloud_server, config={\n                        'auth_info': self._auth_info\n                    })):\n                raise MIoTError('miot_storage.update_user_config_async error')\n        except Exception as err:\n            _LOGGER.error(\n                'get_homeinfos error, %s, %s', err, traceback.format_exc())\n            raise MIoTConfigError('get_homeinfo_error') from err\n\n        # TASK 4: Abort if unique_id configured\n        # Each MiHome account can only configure one instance\n        await self.async_set_unique_id(f'{self._cloud_server}{self._uid}')\n        self._abort_if_unique_id_configured()\n\n        # TASK 5: Query mdns info\n        mips_list = None\n        if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL:\n            try:\n                mips_list = self._mips_service.get_services()\n            except Exception as err:\n                _LOGGER.error(\n                    'async_update_services error, %s, %s',\n                    err, traceback.format_exc())\n                raise MIoTConfigError('mdns_discovery_error') from err\n\n        # TASK 6: Generate devices filter\n        home_list = {}\n        tip_devices = self._miot_i18n.translate(key='config.other.devices')\n        # home list\n        for device_source in ['home_list','share_home_list',\n                              'separated_shared_list']:\n            if device_source not in self._cc_home_info['homes']:\n                continue\n            for home_id, home_info in self._cc_home_info[\n                    'homes'][device_source].items():\n                # i18n\n                tip_central = ''\n                group_id = home_info.get('group_id', None)\n                dev_list = {\n                    device['did']: device\n                    for device in list(self._cc_home_info['devices'].values())\n                    if device.get('home_id', None) == home_id}\n                if (\n                    mips_list\n                    and group_id in mips_list\n                    and mips_list[group_id].get('did', None) in dev_list\n                ):\n                    # i18n\n                    tip_central = self._miot_i18n.translate(\n                        key='config.other.found_central_gateway')\n                    home_info['central_did'] = mips_list[group_id].get(\n                        'did', None)\n                home_list[home_id] = (\n                    f'{home_info[\"home_name\"]} '\n                    f'[ {len(dev_list)} {tip_devices} {tip_central} ]')\n\n        self._cc_home_list_show = dict(sorted(home_list.items()))\n\n        # TASK 7: Get user's MiHome certificate\n        if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL:\n            miot_cert = MIoTCert(\n                storage=self._miot_storage,\n                uid=self._uid, cloud_server=self._cloud_server)\n            if not self._cc_user_cert_done:\n                try:\n                    if await miot_cert.user_cert_remaining_time_async(\n                            did=self._virtual_did) < MIHOME_CERT_EXPIRE_MARGIN:\n                        user_key = await miot_cert.load_user_key_async()\n                        if user_key is None:\n                            user_key = miot_cert.gen_user_key()\n                            if not await miot_cert.update_user_key_async(\n                                    key=user_key):\n                                raise MIoTError('update_user_key_async failed')\n                        csr_str = miot_cert.gen_user_csr(\n                            user_key=user_key, did=self._virtual_did)\n                        crt_str = await self._miot_http.get_central_cert_async(\n                            csr_str)\n                        if not crt_str:\n                            raise MIoTError('get_central_cert_async failed')\n                        if not await miot_cert.update_user_cert_async(\n                                cert=crt_str):\n                            raise MIoTError('update_user_cert_async failed')\n                        self._cc_user_cert_done = True\n                        _LOGGER.info(\n                            'get mihome cert success, %s, %s',\n                            self._uid, self._virtual_did)\n                except Exception as err:\n                    _LOGGER.error(\n                        'get user cert error, %s, %s',\n                        err, traceback.format_exc())\n                    raise MIoTConfigError('get_cert_error') from err\n\n        # Auth success, unregister oauth webhook\n        webhook_async_unregister(self.hass, webhook_id=self._virtual_did)\n        if self._miot_http:\n            await self._miot_http.deinit_async()\n            self._miot_http = None\n        _LOGGER.info(\n            '__check_oauth_async, webhook.async_unregister: %s',\n            self._virtual_did)\n\n    # Show setup error message\n    async def async_step_oauth_error(self, user_input=None):\n        if self._cc_config_rc is None:\n            return await self.async_step_oauth()\n        if self._cc_config_rc.startswith('Flow aborted: '):\n            raise AbortFlow(\n                reason=self._cc_config_rc.replace('Flow aborted: ', ''))\n        error_reason = self._cc_config_rc\n        self._cc_config_rc = None\n        return self.async_show_form(\n            step_id='oauth_error',\n            data_schema=vol.Schema({}),\n            last_step=False,\n            errors={'base': error_reason},\n        )\n\n    async def async_step_homes_select(\n        self, user_input: Optional[dict] = None\n    ):\n        _LOGGER.debug('async_step_homes_select')\n        try:\n            if not user_input:\n                return await self.__show_homes_select_form('')\n\n            home_selected: list = user_input.get('home_infos', [])\n            if not home_selected:\n                return await self.__show_homes_select_form(\n                    'no_family_selected')\n            for device_source in ['home_list','share_home_list',\n                                  'separated_shared_list']:\n                if device_source not in self._cc_home_info['homes']:\n                    continue\n                for home_id, home_info in self._cc_home_info[\n                        'homes'][device_source].items():\n                    if home_id in home_selected:\n                        self._home_selected[home_id] = home_info\n            self._area_name_rule = user_input.get(\n                'area_name_rule', self._area_name_rule)\n            # Storage device list\n            devices_list: dict[str, dict] = {\n                did: dev_info\n                for did, dev_info in self._cc_home_info['devices'].items()\n                if dev_info['home_id'] in home_selected}\n            if not devices_list:\n                return await self.__show_homes_select_form('no_devices')\n            self._device_list_sorted = dict(sorted(\n                devices_list.items(), key=lambda item:\n                    item[1].get('home_id', '')+item[1].get('room_id', '')))\n\n            if not await self._miot_storage.save_async(\n                    domain='miot_devices',\n                    name=f'{self._uid}_{self._cloud_server}',\n                    data=self._device_list_sorted):\n                _LOGGER.error(\n                    'save devices async failed, %s, %s',\n                    self._uid, self._cloud_server)\n                return await self.__show_homes_select_form(\n                    'devices_storage_failed')\n            if user_input.get('advanced_options', False):\n                return await self.async_step_advanced_options()\n            return await self.config_flow_done()\n        except Exception as err:\n            _LOGGER.error(\n                'async_step_homes_select, %s, %s',\n                err, traceback.format_exc())\n            raise AbortFlow(\n                reason='config_flow_error',\n                description_placeholders={\n                    'error': f'config_flow error, {err}'}\n            ) from err\n\n    async def __show_homes_select_form(self, reason: str):\n        return self.async_show_form(\n            step_id='homes_select',\n            data_schema=vol.Schema({\n                vol.Required('home_infos'): cv.multi_select(\n                    self._cc_home_list_show),\n                vol.Required(\n                    'area_name_rule',\n                    default=self._area_name_rule  # type: ignore\n                ): vol.In(self._miot_i18n.translate(\n                    key='config.room_name_rule')),\n                vol.Required(\n                    'advanced_options', default=False  # type: ignore\n                ): bool,\n            }),\n            errors={'base': reason},\n            description_placeholders={\n                'nick_name': self._nick_name,\n            },\n            last_step=False,\n        )\n\n    async def async_step_advanced_options(\n        self, user_input: Optional[dict] = None\n    ):\n        if user_input:\n            self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)\n            self._action_debug = user_input.get(\n                'action_debug', self._action_debug)\n            self._hide_non_standard_entities = user_input.get(\n                'hide_non_standard_entities', self._hide_non_standard_entities)\n            self._display_binary_mode = user_input.get(\n                'display_binary_mode', self._display_binary_mode)\n            self._display_devices_changed_notify = user_input.get(\n                'display_devices_changed_notify',\n                self._display_devices_changed_notify)\n            # Device filter\n            if user_input.get('devices_filter', False):\n                return await self.async_step_devices_filter()\n            return await self.config_flow_done()\n        return self.async_show_form(\n            step_id='advanced_options',\n            data_schema=vol.Schema({\n                vol.Required(\n                    'devices_filter', default=False): bool,  # type: ignore\n                vol.Required(\n                    'ctrl_mode', default=self._ctrl_mode  # type: ignore\n                ): vol.In(self._miot_i18n.translate(key='config.control_mode')),\n                vol.Required(\n                    'action_debug', default=self._action_debug  # type: ignore\n                ): bool,\n                vol.Required(\n                    'hide_non_standard_entities',\n                    default=self._hide_non_standard_entities  # type: ignore\n                ): bool,\n                vol.Required(\n                    'display_binary_mode',\n                    default=self._display_binary_mode  # type: ignore\n                ): cv.multi_select(\n                    self._miot_i18n.translate(\n                        key='config.binary_mode')),  # type: ignore\n                vol.Required(\n                    'display_devices_changed_notify',\n                    default=self._display_devices_changed_notify  # type: ignore\n                ): cv.multi_select(\n                    self._miot_i18n.translate(\n                        key='config.device_state')),  # type: ignore\n            }),\n            last_step=False,\n        )\n\n    async def async_step_devices_filter(\n        self, user_input: Optional[dict] = None\n    ):\n        if user_input:\n            # Room filter\n            include_items: dict = {}\n            exclude_items: dict = {}\n            room_list_in: list = user_input.get('room_list', [])\n            if room_list_in:\n                if user_input.get(\n                        'room_filter_mode', 'exclude') == 'include':\n                    include_items['room_id'] = room_list_in\n                else:\n                    exclude_items['room_id'] = room_list_in\n            # Connect Type filter\n            type_list_in: list = user_input.get('type_list', [])\n            if type_list_in:\n                if user_input.get(\n                        'type_filter_mode', 'exclude') == 'include':\n                    include_items['connect_type'] = type_list_in\n                else:\n                    exclude_items['connect_type'] = type_list_in\n            # Model filter\n            model_list_in: list = user_input.get('model_list', [])\n            if model_list_in:\n                if user_input.get(\n                        'model_filter_mode', 'exclude') == 'include':\n                    include_items['model'] = model_list_in\n                else:\n                    exclude_items['model'] = model_list_in\n            # Device filter\n            device_list_in: list = user_input.get('device_list', [])\n            if device_list_in:\n                if user_input.get(\n                        'devices_filter_mode', 'exclude') == 'include':\n                    include_items['did'] = device_list_in\n                else:\n                    exclude_items['did'] = device_list_in\n            device_filter_list = _handle_devices_filter(\n                devices=self._device_list_sorted,\n                logic_or=(user_input.get('statistics_logic', 'or') == 'or'),\n                item_in=include_items, item_ex=exclude_items)\n            if not device_filter_list:\n                return await self.__show_devices_filter_form(\n                    reason='no_filter_devices')\n            self._device_list_sorted = dict(sorted(\n                device_filter_list.items(), key=lambda item:\n                    item[1].get('home_id', '')+item[1].get('room_id', '')))\n            # Save devices\n            if not await self._miot_storage.save_async(\n                    domain='miot_devices',\n                    name=f'{self._uid}_{self._cloud_server}',\n                    data=self._device_list_sorted):\n                _LOGGER.error(\n                    'save devices async failed, %s, %s',\n                    self._uid, self._cloud_server)\n                raise AbortFlow(\n                    reason='storage_error', description_placeholders={\n                        'error': 'save user devices error'})\n            self._devices_filter = {\n                'room_list': {\n                    'items': room_list_in,\n                    'mode': user_input.get('room_filter_mode', 'exclude')},\n                'type_list': {\n                    'items': type_list_in,\n                    'mode': user_input.get('type_filter_mode', 'exclude')},\n                'model_list': {\n                    'items': model_list_in,\n                    'mode': user_input.get('model_filter_mode', 'exclude')},\n                'device_list': {\n                    'items': device_list_in,\n                    'mode': user_input.get('devices_filter_mode', 'exclude')},\n                'statistics_logic': user_input.get('statistics_logic', 'or'),\n            }\n            return await self.config_flow_done()\n        return await self.__show_devices_filter_form(reason='')\n\n    async def __show_devices_filter_form(self, reason: str):\n        tip_devices: str = self._miot_i18n.translate(\n            key='config.other.devices')  # type: ignore\n        tip_without_room: str = self._miot_i18n.translate(\n            key='config.other.without_room')  # type: ignore\n        trans_statistics_logic: dict = self._miot_i18n.translate(\n            key='config.statistics_logic')  # type: ignore\n        trans_filter_mode: dict = self._miot_i18n.translate(\n            key='config.filter_mode')  # type: ignore\n        trans_connect_type: dict = self._miot_i18n.translate(\n            key='config.connect_type')  # type: ignore\n\n        room_device_count: dict = {}\n        model_device_count: dict = {}\n        connect_type_count: dict = {}\n        device_list: dict = {}\n        for did, info in self._device_list_sorted.items():\n            device_list[did] = (\n                f'[ {info[\"home_name\"]} {info[\"room_name\"]} ] ' +\n                f'{info[\"name\"]}, {did}')\n            room_device_count.setdefault(info['room_id'], 0)\n            room_device_count[info['room_id']] += 1\n            model_device_count.setdefault(info['model'], 0)\n            model_device_count[info['model']] += 1\n            connect_type_count.setdefault(str(info['connect_type']), 0)\n            connect_type_count[str(info['connect_type'])] += 1\n        model_list: dict = {}\n        for model, count in model_device_count.items():\n            model_list[model] = f'{model} [ {count} {tip_devices} ]'\n        type_list: dict = {\n            k: f'{trans_connect_type.get(k, f\"Connect Type ({k})\")} '\n            f'[ {v} {tip_devices} ]'\n            for k, v in connect_type_count.items()}\n        room_list: dict = {}\n        for home_id, home_info in self._home_selected.items():\n            for room_id, room_name in home_info['room_info'].items():\n                if room_id not in room_device_count:\n                    continue\n                room_list[room_id] = (\n                    f'{home_info[\"home_name\"]} {room_name}'\n                    f' [ {room_device_count[room_id]}{tip_devices} ]')\n            if home_id in room_device_count:\n                room_list[home_id] = (\n                    f'{home_info[\"home_name\"]} {tip_without_room}'\n                    f' [ {room_device_count[home_id]}{tip_devices} ]')\n        return self.async_show_form(\n            step_id='devices_filter',\n            data_schema=vol.Schema({\n                vol.Required(\n                    'room_filter_mode', default='exclude'  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('room_list'): cv.multi_select(room_list),\n                vol.Required(\n                    'type_filter_mode', default='exclude'  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('type_list'): cv.multi_select(type_list),\n                vol.Required(\n                    'model_filter_mode', default='exclude'  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('model_list'): cv.multi_select(dict(sorted(\n                    model_list.items(), key=lambda item: item[0]))),\n                vol.Required(\n                    'devices_filter_mode', default='exclude'  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('device_list'): cv.multi_select(dict(sorted(\n                    device_list.items(), key=lambda device: device[1]))),\n                vol.Required(\n                    'statistics_logic', default='or'  # type: ignore\n                ): vol.In(trans_statistics_logic),\n            }),\n            errors={'base': reason},\n            last_step=False\n        )\n\n    async def config_flow_done(self):\n        return self.async_create_entry(\n            title=(\n                f'{self._nick_name}: {self._uid} '\n                f'[{CLOUD_SERVERS[self._cloud_server]}]'),\n            data={\n                'virtual_did': self._virtual_did,\n                'uuid': self._uuid,\n                'integration_language': self._integration_language,\n                'storage_path': self._storage_path,\n                'uid': self._uid,\n                'nick_name': self._nick_name,\n                'cloud_server': self._cloud_server,\n                'oauth_redirect_url': self._oauth_redirect_url_full,\n                'ctrl_mode': self._ctrl_mode,\n                'home_selected': self._home_selected,\n                'devices_filter': self._devices_filter,\n                'area_name_rule': self._area_name_rule,\n                'action_debug': self._action_debug,\n                'hide_non_standard_entities':\n                    self._hide_non_standard_entities,\n                'cover_dead_zone_width': self._cover_dz_width,\n                'display_binary_mode': self._display_binary_mode,\n                'display_devices_changed_notify':\n                    self._display_devices_changed_notify\n            })\n\n    @ staticmethod\n    @ callback\n    def async_get_options_flow(\n            config_entry: config_entries.ConfigEntry,\n    ) -> config_entries.OptionsFlow:\n        return OptionsFlowHandler(config_entry)\n\n\nclass OptionsFlowHandler(config_entries.OptionsFlow):\n    \"\"\"Xiaomi MiHome options flow.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    _config_entry: config_entries.ConfigEntry\n    _main_loop: asyncio.AbstractEventLoop\n    _miot_client: MIoTClient\n\n    _miot_network: MIoTNetwork\n    _miot_storage: MIoTStorage\n    _mips_service: MipsService\n    _miot_oauth: MIoTOauthClient\n    _miot_http: MIoTHttpClient\n    _miot_i18n: MIoTI18n\n    _miot_lan: MIoTLan\n\n    _entry_data: dict\n    _virtual_did: str\n    _uid: str\n    _storage_path: str\n    _cloud_server: str\n\n    _integration_language: str\n    _ctrl_mode: str\n    _nick_name: str\n    _home_selected_list: list\n    _devices_filter: dict\n    _action_debug: bool\n    _hide_non_standard_entities: bool\n    _display_binary_mode: list[str]\n    _display_devs_notify: list[str]\n    _cover_dz_width: int\n\n    _oauth_redirect_url_full: str\n    _auth_info: dict\n    _home_selected: dict\n    _device_list_sorted: dict\n    _devices_add: list[str]\n    _devices_remove: list[str]\n\n    # Config options\n    _lang_new: str\n    _nick_name_new: Optional[str]\n    _action_debug_new: bool\n    _hide_non_standard_entities_new: bool\n    _display_binary_mode_new: list[str]\n    _update_user_info: bool\n    _update_devices: bool\n    _update_trans_rules: bool\n    _opt_lan_ctrl_cfg: bool\n    _opt_network_detect_cfg: bool\n    _opt_check_network_deps: bool\n    _cover_width_new: int\n\n    _trans_rules_count: int\n    _trans_rules_count_success: int\n\n    _need_reload: bool\n\n    # Config cache\n    _cc_home_info: dict\n    _cc_home_list_show: dict\n    _cc_oauth_auth_url: Optional[str]\n    _cc_task_oauth: Optional[asyncio.Task[None]]\n    _cc_config_rc: Optional[str]\n    _cc_fut_oauth_code: Optional[asyncio.Future]\n    _cc_devices_local: dict\n    _cc_network_detect_addr: str\n\n    def __init__(self, config_entry: config_entries.ConfigEntry):\n        self._config_entry = config_entry\n        self._main_loop = asyncio.get_event_loop()\n\n        self._entry_data = dict(config_entry.data)\n        self._virtual_did = self._entry_data['virtual_did']\n        self._uid = self._entry_data['uid']\n        self._storage_path = self._entry_data['storage_path']\n        self._cloud_server = self._entry_data['cloud_server']\n        self._ctrl_mode = self._entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE)\n        self._integration_language = self._entry_data.get(\n            'integration_language', DEFAULT_INTEGRATION_LANGUAGE)\n        self._cover_dz_width = self._entry_data.get(\n            'cover_dead_zone_width', DEFAULT_COVER_DEAD_ZONE_WIDTH)\n        self._nick_name = self._entry_data.get('nick_name', DEFAULT_NICK_NAME)\n        self._action_debug = self._entry_data.get('action_debug', False)\n        self._hide_non_standard_entities = self._entry_data.get(\n            'hide_non_standard_entities', False)\n        self._display_binary_mode = self._entry_data.get(\n            'display_binary_mode', ['text'])\n        self._display_devs_notify = self._entry_data.get(\n            'display_devices_changed_notify', ['add', 'del', 'offline'])\n        self._home_selected_list = list(\n            self._entry_data['home_selected'].keys())\n        self._devices_filter = self._entry_data.get('devices_filter', {})\n\n        self._oauth_redirect_url_full = ''\n        self._auth_info = {}\n        self._home_selected = {}\n        self._device_list_sorted = {}\n\n        self._devices_add = []\n        self._devices_remove = []\n\n        self._lang_new = self._integration_language\n        self._nick_name_new = None\n        self._action_debug_new = False\n        self._hide_non_standard_entities_new = False\n        self._display_binary_mode_new = []\n        self._cover_width_new = self._cover_dz_width\n        self._update_user_info = False\n        self._update_devices = False\n        self._update_trans_rules = False\n        self._opt_lan_ctrl_cfg = False\n        self._opt_network_detect_cfg = False\n        self._opt_check_network_deps = False\n        self._trans_rules_count = 0\n        self._trans_rules_count_success = 0\n\n        self._need_reload = False\n\n        self._cc_home_info = {}\n        self._cc_home_list_show = {}\n        self._cc_oauth_auth_url = None\n        self._cc_task_oauth = None\n        self._cc_config_rc = None\n        self._cc_fut_oauth_code = None\n        self._cc_devices_local = {}\n        self._cc_network_detect_addr = ''\n\n        _LOGGER.info(\n            'options init, %s, %s, %s, %s', config_entry.entry_id,\n            config_entry.unique_id, config_entry.data, config_entry.options)\n\n    async def async_step_init(self, user_input=None):\n        self.hass.data.setdefault(DOMAIN, {})\n        self.hass.data[DOMAIN].setdefault(self._virtual_did, {})\n        try:\n            # MIoT client\n            self._miot_client = await get_miot_instance_async(\n                hass=self.hass, entry_id=self._config_entry.entry_id)\n            if not self._miot_client:\n                raise MIoTConfigError('invalid miot client')\n            # MIoT network\n            self._miot_network = self._miot_client.miot_network\n            if not self._miot_network:\n                raise MIoTConfigError('invalid miot network')\n            # MIoT storage\n            self._miot_storage = self._miot_client.miot_storage\n            if not self._miot_storage:\n                raise MIoTConfigError('invalid miot storage')\n            # Mips service\n            self._mips_service = self._miot_client.mips_service\n            if not self._mips_service:\n                raise MIoTConfigError('invalid mips service')\n            # MIoT oauth\n            self._miot_oauth = self._miot_client.miot_oauth\n            if not self._miot_oauth:\n                raise MIoTConfigError('invalid miot oauth')\n            # MIoT http\n            self._miot_http = self._miot_client.miot_http\n            if not self._miot_http:\n                raise MIoTConfigError('invalid miot http')\n            self._miot_i18n = self._miot_client.miot_i18n\n            if not self._miot_i18n:\n                raise MIoTConfigError('invalid miot i18n')\n            self._miot_lan = self._miot_client.miot_lan\n            if not self._miot_lan:\n                raise MIoTConfigError('invalid miot lan')\n            # Check token\n            if not await self._miot_client.refresh_oauth_info_async():\n                # Check network\n                if not await self._miot_network.get_network_status_async():\n                    raise AbortFlow(\n                        reason='network_connect_error',\n                        description_placeholders={})\n                self._need_reload = True\n                return await self.async_step_auth_config()\n            return await self.async_step_config_options()\n        except MIoTConfigError as err:\n            raise AbortFlow(\n                reason='options_flow_error',\n                description_placeholders={'error': str(err)}\n            ) from err\n        except AbortFlow as err:\n            raise err\n        except Exception as err:\n            _LOGGER.error(\n                'async_step_init error, %s, %s',\n                err, traceback.format_exc())\n            raise AbortFlow(\n                reason='re_add',\n                description_placeholders={'error': str(err)},\n            ) from err\n\n    async def async_step_auth_config(self, user_input=None):\n        if user_input:\n            webhook_path = webhook_async_generate_path(\n                webhook_id=self._virtual_did)\n            self._oauth_redirect_url_full = (\n                f'{user_input.get(\"oauth_redirect_url\")}{webhook_path}')\n            return await self.async_step_oauth(user_input)\n        return self.async_show_form(\n            step_id='auth_config',\n            data_schema=vol.Schema({\n                vol.Required(\n                    'oauth_redirect_url',\n                    default=OAUTH_REDIRECT_URL  # type: ignore\n                ): vol.In([OAUTH_REDIRECT_URL]),\n            }),\n            description_placeholders={\n                'cloud_server': CLOUD_SERVERS[self._cloud_server],\n            },\n            last_step=False,\n        )\n\n    async def async_step_oauth(self, user_input=None):\n        try:\n            if self._cc_task_oauth is None:\n                self._cc_oauth_auth_url = self._miot_oauth.gen_auth_url(\n                    redirect_url=self._oauth_redirect_url_full)\n                self.hass.data[DOMAIN][self._virtual_did]['oauth_state'] = (\n                    self._miot_oauth.state)\n                self.hass.data[DOMAIN][self._virtual_did]['i18n'] = (\n                    self._miot_i18n)\n                _LOGGER.info(\n                    'async_step_oauth, oauth_url: %s', self._cc_oauth_auth_url)\n                webhook_async_unregister(\n                    self.hass, webhook_id=self._virtual_did)\n                webhook_async_register(\n                    self.hass,\n                    domain=DOMAIN,\n                    name='oauth redirect url webhook',\n                    webhook_id=self._virtual_did,\n                    handler=_handle_oauth_webhook,\n                    allowed_methods=(METH_GET,),\n                )\n                self._cc_fut_oauth_code = self.hass.data[DOMAIN][\n                    self._virtual_did].get('fut_oauth_code', None)\n                if self._cc_fut_oauth_code is None:\n                    self._cc_fut_oauth_code = self._main_loop.create_future()\n                    self.hass.data[DOMAIN][self._virtual_did][\n                        'fut_oauth_code'] = self._cc_fut_oauth_code\n                self._cc_task_oauth = self.hass.async_create_task(\n                    self.__check_oauth_async())\n                _LOGGER.info(\n                    'async_step_oauth, webhook.async_register: %s',\n                    self._virtual_did)\n\n            if self._cc_task_oauth.done():\n                if (error := self._cc_task_oauth.exception()):\n                    _LOGGER.error('task_oauth exception, %s', error)\n                    self._cc_config_rc = str(error)\n                    self._cc_task_oauth = None\n                    return self.async_show_progress_done(\n                        next_step_id='oauth_error')\n                return self.async_show_progress_done(\n                    next_step_id='config_options')\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error(\n                'async_step_oauth error, %s, %s',\n                err, traceback.format_exc())\n            self._cc_config_rc = str(err)\n            return self.async_show_progress_done(next_step_id='oauth_error')\n        # pylint: disable=unexpected-keyword-arg\n        return self.async_show_progress(\n            step_id='oauth',\n            progress_action='oauth',\n            description_placeholders={\n                'link_left':\n                    f'<a href=\"{self._cc_oauth_auth_url}\" target=\"_blank\">',\n                'link_right': '</a>'\n            },\n            progress_task=self._cc_task_oauth,  # type: ignore\n        )\n\n    async def __check_oauth_async(self) -> None:\n        # Get oauth code\n        if not self._cc_fut_oauth_code:\n            raise MIoTConfigError('oauth_code_fut_error')\n        oauth_code: str = await self._cc_fut_oauth_code\n        if not oauth_code:\n            raise MIoTConfigError('oauth_code_error')\n        _LOGGER.debug('options flow __check_oauth_async, %s', oauth_code)\n        # Get access_token and user_info from miot_oauth\n        if not self._auth_info:\n            auth_info: dict = {}\n            try:\n                auth_info = await self._miot_oauth.get_access_token_async(\n                    code=oauth_code)\n            except Exception as err:\n                _LOGGER.error(\n                    'get_access_token, %s, %s', err, traceback.format_exc())\n                raise MIoTConfigError('get_token_error') from err\n            # Check uid\n            m_http: MIoTHttpClient = MIoTHttpClient(\n                cloud_server=self._cloud_server,\n                client_id=OAUTH2_CLIENT_ID,\n                access_token=auth_info['access_token'],\n                loop=self._main_loop)\n            if await m_http.get_uid_async() != self._uid:\n                raise AbortFlow('inconsistent_account')\n            del m_http\n            self._miot_http.update_http_header(\n                access_token=auth_info['access_token'])\n            if not await self._miot_storage.update_user_config_async(\n                    uid=self._uid,\n                    cloud_server=self._cloud_server,\n                    config={'auth_info': auth_info}):\n                raise AbortFlow('storage_error')\n            self._auth_info = auth_info\n\n        # Auth success, unregister oauth webhook\n        webhook_async_unregister(self.hass, webhook_id=self._virtual_did)\n        _LOGGER.info(\n            '__check_oauth_async, webhook.async_unregister: %s',\n            self._virtual_did)\n\n    # Show setup error message\n    async def async_step_oauth_error(self, user_input=None):\n        if self._cc_config_rc is None:\n            return await self.async_step_oauth()\n        if self._cc_config_rc.startswith('Flow aborted: '):\n            raise AbortFlow(\n                reason=self._cc_config_rc.replace('Flow aborted: ', ''))\n        error_reason = self._cc_config_rc\n        self._cc_config_rc = None\n        return self.async_show_form(\n            step_id='oauth_error',\n            data_schema=vol.Schema({}),\n            last_step=False,\n            errors={'base': error_reason},\n        )\n\n    async def async_step_config_options(self, user_input=None):\n        if not user_input:\n            return self.async_show_form(\n                step_id='config_options',\n                data_schema=vol.Schema({\n                    # Integration configure\n                    vol.Required(\n                        'integration_language',\n                        default=self._integration_language  # type: ignore\n                    ): vol.In(INTEGRATION_LANGUAGES),\n                    vol.Required(\n                        'update_user_info',\n                        default=self._update_user_info  # type: ignore\n                    ): bool,\n                    vol.Required(\n                        'network_detect_config',\n                        default=self._opt_network_detect_cfg  # type: ignore\n                    ): bool,\n                    # Device info configure\n                    vol.Required(\n                        'update_devices',\n                        default=self._update_devices  # type: ignore\n                    ): bool,\n                    vol.Required(\n                        'display_devices_changed_notify',\n                        default=self._display_devs_notify  # type: ignore\n                    ): cv.multi_select(\n                        self._miot_i18n.translate(\n                            'config.device_state')),  # type: ignore\n                    vol.Required(\n                        'update_lan_ctrl_config',\n                        default=self._opt_lan_ctrl_cfg  # type: ignore\n                    ): bool,\n                    # Entity info configure\n                    vol.Required(\n                        'action_debug',\n                        default=self._action_debug  # type: ignore\n                    ): bool,\n                    vol.Required(\n                        'hide_non_standard_entities',\n                        default=self._hide_non_standard_entities  # type: ignore\n                    ): bool,\n                    vol.Required(\n                        'display_binary_mode',\n                        default=self._display_binary_mode  # type: ignore\n                    ): cv.multi_select(\n                        self._miot_i18n.translate(\n                            'config.binary_mode')),  # type: ignore\n                    vol.Optional(\n                        'cover_dead_zone_width',\n                        default=self._cover_dz_width  # type: ignore\n                    ): vol.All(vol.Coerce(int), vol.Range(\n                        min=MIN_COVER_DEAD_ZONE_WIDTH,\n                        max=MAX_COVER_DEAD_ZONE_WIDTH)),\n                    vol.Required(\n                        'update_trans_rules',\n                        default=self._update_trans_rules  # type: ignore\n                    ): bool,\n                }),\n                errors={},\n                description_placeholders={\n                    'nick_name': self._nick_name,\n                    'uid': self._uid,\n                    'cloud_server': CLOUD_SERVERS[self._cloud_server],\n                    'instance_id': f'ha.{self._entry_data[\"uuid\"]}'\n                },\n                last_step=False,\n            )\n        # Check network\n        if not await self._miot_network.get_network_status_async():\n            raise AbortFlow(\n                reason='network_connect_error', description_placeholders={})\n        self._lang_new = user_input.get(\n            'integration_language', self._integration_language)\n        self._update_user_info = user_input.get(\n            'update_user_info', self._update_user_info)\n        self._update_devices = user_input.get(\n            'update_devices', self._update_devices)\n        self._action_debug_new = user_input.get(\n            'action_debug', self._action_debug)\n        self._hide_non_standard_entities_new = user_input.get(\n            'hide_non_standard_entities', self._hide_non_standard_entities)\n        self._display_binary_mode_new = user_input.get(\n            'display_binary_mode', self._display_binary_mode)\n        self._display_devs_notify = user_input.get(\n            'display_devices_changed_notify', self._display_devs_notify)\n        self._update_trans_rules = user_input.get(\n            'update_trans_rules', self._update_trans_rules)\n        self._opt_lan_ctrl_cfg = user_input.get(\n            'update_lan_ctrl_config', self._opt_lan_ctrl_cfg)\n        self._opt_network_detect_cfg = user_input.get(\n            'network_detect_config', self._opt_network_detect_cfg)\n        self._cover_width_new = user_input.get(\n            'cover_dead_zone_width', self._cover_dz_width)\n\n        return await self.async_step_update_user_info()\n\n    async def async_step_update_user_info(self, user_input=None):\n        if not self._update_user_info:\n            return await self.async_step_homes_select()\n        if not user_input:\n            nick_name_new = (\n                await self._miot_http.get_user_info_async() or {}).get(\n                    'miliaoNick', DEFAULT_NICK_NAME)\n            return self.async_show_form(\n                step_id='update_user_info',\n                data_schema=vol.Schema({\n                    vol.Required('nick_name', default=nick_name_new): str\n                }),\n                description_placeholders={\n                    'nick_name': self._nick_name\n                },\n                last_step=False\n            )\n\n        self._nick_name_new = user_input.get('nick_name')\n        return await self.async_step_homes_select()\n\n    async def async_step_homes_select(\n        self, user_input: Optional[dict] = None\n    ):\n        if not self._update_devices:\n            return await self.async_step_update_trans_rules()\n        if not user_input:\n            # Query mdns info\n            try:\n                mips_list = self._mips_service.get_services()\n            except Exception as err:\n                _LOGGER.error(\n                    'async_update_services error, %s, %s',\n                    err, traceback.format_exc())\n                raise MIoTConfigError('mdns_discovery_error') from err\n\n            # Get home info\n            try:\n                self._cc_home_info = (\n                    await self._miot_http.get_devices_async())\n            except Exception as err:\n                _LOGGER.error(\n                    'get_homeinfos error, %s, %s', err, traceback.format_exc())\n                raise MIoTConfigError('get_homeinfo_error') from err\n            # Generate devices filter\n            home_list = {}\n            tip_devices = self._miot_i18n.translate(key='config.other.devices')\n            # home list\n            for device_source in ['home_list','share_home_list',\n                                  'separated_shared_list']:\n                if device_source not in self._cc_home_info['homes']:\n                    continue\n                for home_id, home_info in self._cc_home_info[\n                        'homes'][device_source].items():\n                    # i18n\n                    tip_central = ''\n                    group_id = home_info.get('group_id', None)\n                    did_list = {\n                        device['did']: device for device in list(\n                            self._cc_home_info['devices'].values())\n                        if device.get('home_id', None) == home_id}\n                    if (\n                        group_id in mips_list\n                        and mips_list[group_id].get('did', None) in did_list\n                    ):\n                        # i18n\n                        tip_central = self._miot_i18n.translate(\n                            key='config.other.found_central_gateway')\n                        home_info['central_did'] = mips_list[group_id].get(\n                            'did', None)\n                    home_list[home_id] = (\n                        f'{home_info[\"home_name\"]} '\n                        f'[ {len(did_list)} {tip_devices} {tip_central} ]')\n            # Remove deleted item\n            self._home_selected_list = [\n                home_id for home_id in self._home_selected_list\n                if home_id in home_list]\n            self._cc_home_list_show = dict(sorted(home_list.items()))\n            # Get local devices\n            self._cc_devices_local = (\n                await self._miot_storage.load_async(\n                    domain='miot_devices',\n                    name=f'{self._uid}_{self._cloud_server}',\n                    type_=dict)) or {}  # type: ignore\n\n            return await self.__show_homes_select_form('')\n\n        self._home_selected_list = user_input.get('home_infos', [])\n        if not self._home_selected_list:\n            return await self.__show_homes_select_form('no_family_selected')\n        self._ctrl_mode = user_input.get('ctrl_mode', self._ctrl_mode)\n        self._home_selected = {}\n        for device_source in ['home_list','share_home_list',\n                              'separated_shared_list']:\n            if device_source not in self._cc_home_info['homes']:\n                continue\n            for home_id, home_info in self._cc_home_info[\n                    'homes'][device_source].items():\n                if home_id in self._home_selected_list:\n                    self._home_selected[home_id] = home_info\n        # Get device list\n        device_list: dict = {\n            did: dev_info\n            for did, dev_info in self._cc_home_info['devices'].items()\n            if dev_info['home_id'] in self._home_selected_list}\n        if not device_list:\n            return await self.__show_homes_select_form('no_devices')\n        self._device_list_sorted = dict(sorted(\n            device_list.items(), key=lambda item:\n                item[1].get('home_id', '')+item[1].get('room_id', '')))\n\n        if user_input.get('devices_filter', False):\n            return await self.async_step_devices_filter()\n        return await self.update_devices_done_async()\n\n    async def __show_homes_select_form(self, reason: str):\n        devices_local_count: str = str(len(self._cc_devices_local))\n        return self.async_show_form(\n            step_id='homes_select',\n            data_schema=vol.Schema({\n                vol.Required(\n                    'home_infos',\n                    default=self._home_selected_list  # type: ignore\n                ): cv.multi_select(self._cc_home_list_show),\n                vol.Required(\n                    'devices_filter', default=False  # type: ignore\n                ): bool,\n                vol.Required(\n                    'ctrl_mode', default=self._ctrl_mode  # type: ignore\n                ): vol.In(self._miot_i18n.translate(key='config.control_mode')),\n            }),\n            errors={'base': reason},\n            description_placeholders={\n                'local_count': devices_local_count\n            },\n            last_step=False\n        )\n\n    async def async_step_devices_filter(\n        self, user_input: Optional[dict] = None\n    ):\n        if user_input:\n            # Room filter\n            include_items: dict = {}\n            exclude_items: dict = {}\n            room_list_in: list = user_input.get('room_list', [])\n            room_filter_mode: str = user_input.get(\n                'room_filter_mode', 'exclude')\n            if room_list_in:\n                if room_filter_mode == 'include':\n                    include_items['room_id'] = room_list_in\n                else:\n                    exclude_items['room_id'] = room_list_in\n            # Connect Type filter\n            type_list_in: list = user_input.get('type_list', [])\n            type_filter_mode: str = user_input.get(\n                'type_filter_mode', 'exclude')\n            if type_list_in:\n                if type_filter_mode == 'include':\n                    include_items['connect_type'] = type_list_in\n                else:\n                    exclude_items['connect_type'] = type_list_in\n            # Model filter\n            model_list_in: list = user_input.get('model_list', [])\n            model_filter_mode: str = user_input.get(\n                'model_filter_mode', 'exclude')\n            if model_list_in:\n                if model_filter_mode == 'include':\n                    include_items['model'] = model_list_in\n                else:\n                    exclude_items['model'] = model_list_in\n            # Device filter\n            device_list_in: list = user_input.get('device_list', [])\n            device_filter_mode: str = user_input.get(\n                'devices_filter_mode', 'exclude')\n            if device_list_in:\n                if device_filter_mode == 'include':\n                    include_items['did'] = device_list_in\n                else:\n                    exclude_items['did'] = device_list_in\n            statistics_logic: str = user_input.get('statistics_logic', 'or')\n            device_filter_list = _handle_devices_filter(\n                devices=self._device_list_sorted,\n                logic_or=(statistics_logic == 'or'),\n                item_in=include_items, item_ex=exclude_items)\n            if not device_filter_list:\n                return await self.__show_devices_filter_form(\n                    reason='no_filter_devices')\n            self._device_list_sorted = dict(sorted(\n                device_filter_list.items(), key=lambda item:\n                    item[1].get('home_id', '')+item[1].get('room_id', '')))\n            self._devices_filter = {\n                'room_list': {\n                    'items': room_list_in, 'mode': room_filter_mode},\n                'type_list': {\n                    'items': type_list_in, 'mode': type_filter_mode},\n                'model_list': {\n                    'items': model_list_in, 'mode': model_filter_mode},\n                'device_list': {\n                    'items': device_list_in, 'mode': device_filter_mode},\n                'statistics_logic': statistics_logic}\n            return await self.update_devices_done_async()\n        return await self.__show_devices_filter_form(reason='')\n\n    async def __show_devices_filter_form(self, reason: str):\n        tip_devices: str = self._miot_i18n.translate(\n            key='config.other.devices')  # type: ignore\n        tip_without_room: str = self._miot_i18n.translate(\n            key='config.other.without_room')  # type: ignore\n        trans_statistics_logic: dict = self._miot_i18n.translate(\n            key='config.statistics_logic')  # type: ignore\n        trans_filter_mode: dict = self._miot_i18n.translate(\n            key='config.filter_mode')  # type: ignore\n        trans_connect_type: dict = self._miot_i18n.translate(\n            key='config.connect_type')  # type: ignore\n\n        room_device_count: dict = {}\n        model_device_count: dict = {}\n        connect_type_count: dict = {}\n        device_list: dict = {}\n        for did, info in self._device_list_sorted.items():\n            device_list[did] = (\n                f'[ {info[\"home_name\"]} {info[\"room_name\"]} ] ' +\n                f'{info[\"name\"]}, {did}')\n            room_device_count.setdefault(info['room_id'], 0)\n            room_device_count[info['room_id']] += 1\n            model_device_count.setdefault(info['model'], 0)\n            model_device_count[info['model']] += 1\n            connect_type_count.setdefault(str(info['connect_type']), 0)\n            connect_type_count[str(info['connect_type'])] += 1\n        model_list: dict = {}\n        for model, count in model_device_count.items():\n            model_list[model] = f'{model} [ {count} {tip_devices} ]'\n        type_list: dict = {\n            k: f'{trans_connect_type.get(k, f\"Connect Type ({k})\")} '\n            f'[ {v} {tip_devices} ]'\n            for k, v in connect_type_count.items()}\n        room_list: dict = {}\n        for home_id, home_info in self._home_selected.items():\n            for room_id, room_name in home_info['room_info'].items():\n                if room_id not in room_device_count:\n                    continue\n                room_list[room_id] = (\n                    f'{home_info[\"home_name\"]} {room_name}'\n                    f' [ {room_device_count[room_id]}{tip_devices} ]')\n            if home_id in room_device_count:\n                room_list[home_id] = (\n                    f'{home_info[\"home_name\"]} {tip_without_room}'\n                    f' [ {room_device_count[home_id]}{tip_devices} ]')\n        return self.async_show_form(\n            step_id='devices_filter',\n            data_schema=vol.Schema({\n                vol.Required(\n                    'room_filter_mode', default=self._devices_filter.get(\n                        'room_list', {}).get('mode', 'exclude')  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('room_list', default=[\n                    room_id for room_id in self._devices_filter.get(\n                        'room_list', {}).get('items', [])\n                    if room_id in room_list]  # type: ignore\n                ): cv.multi_select(room_list),\n                vol.Required(\n                    'type_filter_mode', default=self._devices_filter.get(\n                        'type_list', {}).get('mode', 'exclude')  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('type_list', default=[\n                    type_ for type_ in self._devices_filter.get(\n                        'type_list', {}).get('items', [])\n                    if type_ in type_list]  # type: ignore\n                ): cv.multi_select(type_list),\n                vol.Required(\n                    'model_filter_mode',\n                    default=self._devices_filter.get('model_list', {}).get(\n                        'mode', 'exclude')  # type: ignore\n                ): vol.In(trans_filter_mode),\n                vol.Optional('model_list', default=[\n                    model for model in self._devices_filter.get(\n                        'model_list', {}).get('items', [])\n                    if model in model_list]  # type: ignore\n                ): cv.multi_select(dict(sorted(\n                    model_list.items(), key=lambda item: item[0]))),\n                vol.Required(\n                    'devices_filter_mode', default=self._devices_filter.get(\n                        'device_list', {}).get(\n                            'mode', 'exclude')  # type: ignore\n                ):  vol.In(trans_filter_mode),\n                vol.Optional('device_list', default=[\n                    did for did in self._devices_filter.get(\n                        'device_list', {}).get('items', [])\n                    if did in device_list]  # type: ignore\n                ): cv.multi_select(dict(sorted(\n                    device_list.items(), key=lambda device: device[1]))),\n                vol.Required(\n                    'statistics_logic', default=self._devices_filter.get(\n                        'statistics_logic', 'or')\n                ): vol.In(trans_statistics_logic),\n            }),\n            errors={'base': reason},\n            last_step=False\n        )\n\n    async def update_devices_done_async(self):\n        # Statistics devices changed\n        self._devices_add = []\n        self._devices_remove = []\n\n        self._devices_add = [\n            did for did in list(self._device_list_sorted.keys())\n            if did not in self._cc_devices_local]\n        self._devices_remove = [\n            did for did in self._cc_devices_local.keys()\n            if did not in self._device_list_sorted]\n        _LOGGER.debug(\n            'devices update, add->%s, remove->%s',\n            self._devices_add, self._devices_remove)\n        return await self.async_step_update_trans_rules()\n\n    async def async_step_update_trans_rules(self, user_input=None):\n        if not self._update_trans_rules:\n            return await self.async_step_update_lan_ctrl_config()\n        urn_list: list[str] = list({\n            info['urn']\n            for info in list(self._miot_client.device_list.values())\n            if 'urn' in info})\n        self._trans_rules_count = len(urn_list)\n        if not user_input:\n            return self.async_show_form(\n                step_id='update_trans_rules',\n                data_schema=vol.Schema({\n                    vol.Required(\n                        'confirm', default=False  # type: ignore\n                    ): bool\n                }),\n                description_placeholders={\n                    'urn_count': str(self._trans_rules_count),\n                },\n                last_step=False\n            )\n        if user_input.get('confirm', False):\n            # Update trans rules\n            if urn_list:\n                spec_parser: MIoTSpecParser = MIoTSpecParser(\n                    lang=self._lang_new, storage=self._miot_storage)\n                await spec_parser.init_async()\n                self._trans_rules_count_success = (\n                    await spec_parser.refresh_async(urn_list=urn_list))\n                await spec_parser.deinit_async()\n        else:\n            # SKIP update trans rules\n            self._update_trans_rules = False\n\n        return await self.async_step_update_lan_ctrl_config()\n\n    async def async_step_update_lan_ctrl_config(self, user_input=None):\n        if not self._opt_lan_ctrl_cfg:\n            return await self.async_step_network_detect_config()\n        if not user_input:\n            notice_net_dup: str = ''\n            lan_ctrl_config = await self._miot_storage.load_user_config_async(\n                'global_config', 'all', ['net_interfaces', 'enable_subscribe'])\n            selected_if = lan_ctrl_config.get('net_interfaces', [])\n            enable_subscribe = lan_ctrl_config.get('enable_subscribe', False)\n            net_unavailable = self._miot_i18n.translate(\n                key='config.lan_ctrl_config.net_unavailable')\n            net_if = {\n                if_name: f'{if_name}: {net_unavailable}'\n                for if_name in selected_if}\n            net_info = await self._miot_network.get_network_info_async()\n            net_segs = set()\n            for if_name, info in net_info.items():\n                net_if[if_name] = (\n                    f'{if_name} ({info.ip}/{info.netmask})')\n                net_segs.add(info.net_seg)\n            if len(net_segs) != len(net_info):\n                notice_net_dup: str = self._miot_i18n.translate(\n                    key='config.lan_ctrl_config.notice_net_dup')  # type: ignore\n            return self.async_show_form(\n                step_id='update_lan_ctrl_config',\n                data_schema=vol.Schema({\n                    vol.Required(\n                        'net_interfaces', default=selected_if\n                    ): cv.multi_select(net_if),\n                    vol.Required(\n                        'enable_subscribe', default=enable_subscribe): bool\n                }),\n                description_placeholders={\n                    'notice_net_dup': notice_net_dup,\n                },\n                last_step=False\n            )\n\n        selected_if_new: list = user_input.get('net_interfaces', [])\n        enable_subscribe_new: bool = user_input.get('enable_subscribe', False)\n        lan_ctrl_config = await self._miot_storage.load_user_config_async(\n            'global_config', 'all', ['net_interfaces', 'enable_subscribe'])\n        selected_if = lan_ctrl_config.get('net_interfaces', [])\n        enable_subscribe = lan_ctrl_config.get('enable_subscribe', False)\n        if (\n            set(selected_if_new) != set(selected_if)\n            or enable_subscribe_new != enable_subscribe\n        ):\n            if not await self._miot_storage.update_user_config_async(\n                    'global_config', 'all', {\n                        'net_interfaces': selected_if_new,\n                        'enable_subscribe': enable_subscribe_new}\n            ):\n                raise AbortFlow(\n                    reason='storage_error',\n                    description_placeholders={\n                        'error': 'Update net config error'})\n            await self._miot_lan.update_net_ifs_async(net_ifs=selected_if_new)\n            await self._miot_lan.update_subscribe_option(\n                enable_subscribe=enable_subscribe_new)\n\n        return await self.async_step_network_detect_config()\n\n    async def async_step_network_detect_config(\n        self, user_input: Optional[dict] = None\n    ):\n        if not self._opt_network_detect_cfg:\n            return await self.async_step_config_confirm()\n        if not user_input:\n            return await self.__show_network_detect_config_form(reason='')\n        self._cc_network_detect_addr = user_input.get(\n            'network_detect_addr', self._cc_network_detect_addr)\n\n        ip_list, url_list, invalid_list = _handle_network_detect_addr(\n            addr_str=self._cc_network_detect_addr)\n        if invalid_list:\n            return await self.__show_network_detect_config_form(\n                reason='invalid_network_addr')\n        if ip_list or url_list:\n            if ip_list and not await self._miot_network.ping_multi_async(\n                    ip_list=ip_list):\n                return await self.__show_network_detect_config_form(\n                    reason='invalid_ip_addr')\n            if url_list and not await self._miot_network.http_multi_async(\n                    url_list=url_list):\n                return await self.__show_network_detect_config_form(\n                    reason='invalid_http_addr')\n        else:\n            if not await self._miot_network.get_network_status_async():\n                return await self.__show_network_detect_config_form(\n                    reason='invalid_default_addr')\n        network_detect_addr: dict = {'ip': ip_list, 'url': url_list}\n        # Save\n        if await self._miot_storage.update_user_config_async(\n            uid='global_config', cloud_server='all', config={\n                'network_detect_addr': network_detect_addr}):\n            _LOGGER.info(\n                'update network_detect_addr, %s', network_detect_addr)\n        await self._miot_network.update_addr_list_async(\n            ip_addr_list=ip_list, url_addr_list=url_list)\n        # Check network deps\n        self._opt_check_network_deps = user_input.get(\n            'check_network_deps', False)\n        if self._opt_check_network_deps:\n            # OAuth2\n            if not await self._miot_network.http_multi_async(\n                    url_list=[OAUTH2_AUTH_URL]):\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_oauth2_host')\n            # HTTP API\n            http_host = (\n                DEFAULT_OAUTH2_API_HOST\n                if self._cloud_server == DEFAULT_CLOUD_SERVER\n                else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')\n            if not await self._miot_network.http_multi_async(\n                    url_list=[\n                        f'https://{http_host}/app/v2/ha/oauth/get_token']):\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_http_host')\n            # SPEC API\n            if not await self._miot_network.http_multi_async(\n                    url_list=[\n                        'https://miot-spec.org/miot-spec-v2/template/list/'\n                        'device']):\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_spec_host')\n            # MQTT Broker\n            # pylint: disable=import-outside-toplevel\n            try:\n                from paho.mqtt import client\n                mqtt_client = client.Client(\n                    client_id=f'ha.{self._uid}',\n                    protocol=client.MQTTv5)  # type: ignore\n                if mqtt_client.connect(\n                    host=f'{self._cloud_server}-{DEFAULT_CLOUD_BROKER_HOST}',\n                    port=8883) != 0:\n                    raise RuntimeError('mqtt connect error')\n                mqtt_client.disconnect()\n            except Exception as err:  # pylint: disable=broad-exception-caught\n                _LOGGER.error('try connect mqtt broker error, %s', err)\n                return await self.__show_network_detect_config_form(\n                    reason='unreachable_mqtt_broker')\n\n        return await self.async_step_config_confirm()\n\n    async def __show_network_detect_config_form(self, reason: str):\n        if not self._cc_network_detect_addr:\n            addr_list: dict = (await self._miot_storage.load_user_config_async(\n                'global_config', 'all', ['network_detect_addr'])).get(\n                    'network_detect_addr', {})\n            self._cc_network_detect_addr = ','.join(\n                addr_list.get('ip', [])+addr_list.get('url', []))\n        return self.async_show_form(\n            step_id='network_detect_config',\n            data_schema=vol.Schema({\n                vol.Optional(\n                    'network_detect_addr',\n                    default=self._cc_network_detect_addr  # type: ignore\n                ): str,\n                vol.Optional(\n                    'check_network_deps',\n                    default=self._opt_check_network_deps  # type: ignore\n                ): bool,\n            }),\n            errors={'base': reason},\n            description_placeholders={\n                'broker_host':\n                    f'{self._cloud_server}-{DEFAULT_CLOUD_BROKER_HOST}:8883',\n                'http_host': (\n                    DEFAULT_OAUTH2_API_HOST\n                    if self._cloud_server == DEFAULT_CLOUD_SERVER\n                    else f'{self._cloud_server}.{DEFAULT_OAUTH2_API_HOST}')},\n            last_step=False\n        )\n\n    async def async_step_config_confirm(self, user_input=None):\n        if not user_input or not user_input.get('confirm', False):\n            enable_text = self._miot_i18n.translate(\n                key='config.option_status.enable')\n            disable_text = self._miot_i18n.translate(\n                key='config.option_status.disable')\n            trans_devs_display: dict = self._miot_i18n.translate(\n                key='config.device_state')  # type: ignore\n            return self.async_show_form(\n                step_id='config_confirm',\n                data_schema=vol.Schema({\n                    vol.Required(\n                        'confirm', default=False): bool  # type: ignore\n                }),\n                description_placeholders={\n                    'nick_name': self._nick_name,\n                    'lang_new': INTEGRATION_LANGUAGES[self._lang_new],\n                    'nick_name_new': self._nick_name_new,\n                    'cover_width_new': self._cover_width_new,\n                    'devices_add': len(self._devices_add),\n                    'devices_remove': len(self._devices_remove),\n                    'trans_rules_count': self._trans_rules_count,\n                    'trans_rules_count_success':\n                        self._trans_rules_count_success,\n                    'action_debug': (\n                        enable_text if self._action_debug_new\n                        else disable_text),\n                    'hide_non_standard_entities': (\n                        enable_text if self._hide_non_standard_entities_new\n                        else disable_text),\n                    'display_devices_changed_notify': (' '.join(\n                        trans_devs_display[key]\n                        for key in self._display_devs_notify\n                        if key in trans_devs_display)\n                        if self._display_devs_notify\n                        else self._miot_i18n.translate(\n                            key='config.other.no_display'))\n                },  # type: ignore\n                errors={'base': 'not_confirm'} if user_input else {},\n                last_step=True\n            )\n\n        if self._lang_new != self._integration_language:\n            self._entry_data['integration_language'] = self._lang_new\n            self._need_reload = True\n        if self._cover_width_new != self._cover_dz_width:\n            self._entry_data['cover_dead_zone_width'] = self._cover_width_new\n            self._need_reload = True\n        if self._update_user_info:\n            self._entry_data['nick_name'] = self._nick_name_new\n        if self._update_devices:\n            self._entry_data['ctrl_mode'] = self._ctrl_mode\n            self._entry_data['home_selected'] = self._home_selected\n            self._entry_data['devices_filter'] = self._devices_filter\n            if not await self._miot_storage.save_async(\n                    domain='miot_devices',\n                    name=f'{self._uid}_{self._cloud_server}',\n                    data=self._device_list_sorted):\n                _LOGGER.error(\n                    'save devices async failed, %s, %s',\n                    self._uid, self._cloud_server)\n                raise AbortFlow(\n                    reason='storage_error', description_placeholders={\n                        'error': 'save user devices error'})\n            self._need_reload = True\n        if self._update_trans_rules:\n            self._need_reload = True\n        if self._action_debug_new != self._action_debug:\n            self._entry_data['action_debug'] = self._action_debug_new\n            self._need_reload = True\n        if (\n            self._hide_non_standard_entities_new !=\n            self._hide_non_standard_entities\n        ):\n            self._entry_data['hide_non_standard_entities'] = (\n                self._hide_non_standard_entities_new)\n            self._need_reload = True\n        if set(self._display_binary_mode) != set(self._display_binary_mode_new):\n            self._entry_data['display_binary_mode'] = (\n                self._display_binary_mode_new)\n            self._need_reload = True\n        # Update display_devices_changed_notify\n        self._entry_data['display_devices_changed_notify'] = (\n            self._display_devs_notify)\n        self._miot_client.display_devices_changed_notify = (\n            self._display_devs_notify)\n        if (\n                self._devices_remove\n                and not await self._miot_storage.update_user_config_async(\n                    uid=self._uid,\n                    cloud_server=self._cloud_server,\n                    config={'devices_remove': self._devices_remove})\n        ):\n            raise AbortFlow(\n                reason='storage_error',\n                description_placeholders={'error': 'Update user config error'})\n        entry_title = (\n            f'{self._nick_name_new or self._nick_name}: '\n            f'{self._uid} [{CLOUD_SERVERS[self._cloud_server]}]')\n        # Update entry config\n        self.hass.config_entries.async_update_entry(\n            self._config_entry, title=entry_title, data=self._entry_data)\n        # Reload later\n        if self._need_reload:\n            self._main_loop.call_later(\n                0, lambda: self._main_loop.create_task(\n                    self.hass.config_entries.async_reload(\n                        entry_id=self._config_entry.entry_id)))\n        return self.async_create_entry(title='', data={})\n\n\nasync def _handle_oauth_webhook(hass, webhook_id, request):\n    \"\"\"Webhook to handle oauth2 callback.\"\"\"\n    # pylint: disable=inconsistent-quotes\n    i18n: MIoTI18n = hass.data[DOMAIN][webhook_id].get('i18n', None)\n    try:\n        data = dict(request.query)\n        if data.get('code', None) is None or data.get('state', None) is None:\n            raise MIoTConfigError(\n                'invalid oauth code or state',\n                MIoTErrorCode.CODE_CONFIG_INVALID_INPUT)\n\n        if data['state'] != hass.data[DOMAIN][webhook_id]['oauth_state']:\n            raise MIoTConfigError(\n                f'inconsistent state, '\n                f'{hass.data[DOMAIN][webhook_id][\"oauth_state\"]}!='\n                f'{data[\"state\"]}', MIoTErrorCode.CODE_CONFIG_INVALID_STATE)\n\n        fut_oauth_code: asyncio.Future = hass.data[DOMAIN][webhook_id].pop(\n            'fut_oauth_code', None)\n        fut_oauth_code.set_result(data['code'])\n        _LOGGER.info('webhook code: %s', data['code'])\n\n        success_trans: dict = {}\n        if i18n:\n            success_trans = i18n.translate(\n                'oauth2.success') or {}  # type: ignore\n        # Delete\n        del hass.data[DOMAIN][webhook_id]['oauth_state']\n        del hass.data[DOMAIN][webhook_id]['i18n']\n        return web.Response(\n            body=await oauth_redirect_page(\n                title=success_trans.get('title', 'Success'),\n                content=success_trans.get(\n                    'content', (\n                        'Please close this page and return to the account '\n                        'authentication page to click NEXT')),\n                button=success_trans.get('button', 'Close Page'),\n                success=True,\n            ), content_type='text/html')\n\n    except Exception as err:  # pylint: disable=broad-exception-caught\n        fail_trans: dict = {}\n        err_msg: str = str(err)\n        if i18n:\n            if isinstance(err, MIoTConfigError):\n                err_msg = i18n.translate(\n                    f'oauth2.error_msg.{err.code.value}'\n                ) or err.message  # type: ignore\n            fail_trans = i18n.translate('oauth2.fail') or {}  # type: ignore\n        return web.Response(\n            body=await oauth_redirect_page(\n                title=fail_trans.get('title', 'Authentication Failed'),\n                content=str(fail_trans.get('content', (\n                    '{error_msg}, Please close this page and return to the '\n                    'account authentication page to click the authentication '\n                    'link again.'))).replace('{error_msg}', err_msg),\n                button=fail_trans.get('button', 'Close Page'),\n                success=False),\n            content_type='text/html')\n\n\ndef _handle_devices_filter(\n    devices: dict, logic_or: bool, item_in: dict, item_ex: dict\n) -> dict:\n    \"\"\"Private method to filter devices.\"\"\"\n    include_set: Set = set([])\n    if not item_in:\n        include_set = set(devices.keys())\n    else:\n        filter_item: list[set] = []\n        for key, value in item_in.items():\n            filter_item.append(set([\n                did for did, info in devices.items()\n                if str(info[key]) in value]))\n        include_set = (\n            set.union(*filter_item)\n            if logic_or else set.intersection(*filter_item))\n    if not include_set:\n        return {}\n    if item_ex:\n        filter_item: list[set] = []\n        for key, value in item_ex.items():\n            filter_item.append(set([\n                did for did, info in devices.items()\n                if str(info[key]) in value]))\n        exclude_set: Set = (\n            set.union(*filter_item)\n            if logic_or else set.intersection(*filter_item))\n        if exclude_set:\n            include_set = include_set-exclude_set\n    if not include_set:\n        return {}\n    return {\n        did: info for did, info in devices.items() if did in include_set}\n\n\ndef _handle_network_detect_addr(\n    addr_str: str\n) -> Tuple[list[str], list[str], list[str]]:\n    ip_list: list[str] = []\n    url_list: list[str] = []\n    invalid_list: list[str] = []\n    if addr_str:\n        for addr in addr_str.split(','):\n            addr = addr.strip()\n            if not addr:\n                continue\n            # pylint: disable=broad-exception-caught\n            try:\n                ipaddress.ip_address(addr)\n                ip_list.append(addr)\n                continue\n            except Exception:\n                pass\n            try:\n                result = urlparse(addr)\n                if (\n                    result.netloc\n                    and result.scheme.startswith('http')\n                ):\n                    url_list.append(addr)\n                    continue\n            except Exception:\n                pass\n            invalid_list.append(addr)\n    return ip_list, url_list, invalid_list\n"
  },
  {
    "path": "custom_components/xiaomi_home/cover.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nCover entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Any, Optional\nimport re\nimport logging\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.cover import (ATTR_POSITION, CoverEntity,\n                                            CoverEntityFeature,\n                                            CoverDeviceClass)\n\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity\nfrom .miot.const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,\n                            async_add_entities: AddEntitiesCallback) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('cover', []):\n            if data.spec.name == 'curtain':\n                data.spec.device_class = CoverDeviceClass.CURTAIN\n            elif data.spec.name == 'window-opener':\n                data.spec.device_class = CoverDeviceClass.WINDOW\n            elif data.spec.name == 'motor-controller':\n                data.spec.device_class = CoverDeviceClass.SHUTTER\n            elif data.spec.name == 'airer':\n                data.spec.device_class = CoverDeviceClass.BLIND\n            new_entities.append(Cover(miot_device=miot_device,\n                                      entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Cover(MIoTServiceEntity, CoverEntity):\n    \"\"\"Cover entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n    _cover_dead_zone_width: int\n    _prop_motor_control: Optional[MIoTSpecProperty]\n    _prop_motor_value_open: Optional[int]\n    _prop_motor_value_close: Optional[int]\n    _prop_motor_value_pause: Optional[int]\n    _prop_status: Optional[MIoTSpecProperty]\n    _prop_status_opening: Optional[list[int]]\n    _prop_status_closing: Optional[list[int]]\n    _prop_status_closed: Optional[list[int]]\n    _prop_current_position: Optional[MIoTSpecProperty]\n    _prop_target_position: Optional[MIoTSpecProperty]\n    _prop_position_value_min: Optional[int]\n    _prop_position_value_max: Optional[int]\n    _prop_position_value_range: Optional[int]\n    _prop_pos_closing: bool\n    _prop_pos_opening: bool\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the Cover.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._attr_device_class = entity_data.spec.device_class\n        self._attr_supported_color_modes = set()\n        self._attr_supported_features = CoverEntityFeature(0)\n\n        self._cover_dead_zone_width = (\n            miot_device.miot_client.cover_dead_zone_width)\n\n        self._prop_motor_control = None\n        self._prop_motor_value_open = None\n        self._prop_motor_value_close = None\n        self._prop_motor_value_pause = None\n        self._prop_status = None\n        self._prop_status_opening = []\n        self._prop_status_closing = []\n        self._prop_status_closed = []\n        self._prop_current_position = None\n        self._prop_target_position = None\n        self._prop_position_value_min = None\n        self._prop_position_value_max = None\n        self._prop_position_value_range = None\n        self._prop_pos_closing = False\n        self._prop_pos_opening = False\n\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'motor-control':\n                if not prop.value_list:\n                    _LOGGER.error('motor-control value_list is None, %s',\n                                  self.entity_id)\n                    continue\n                for item in prop.value_list.items:\n                    if item.name in {'open', 'up'}:\n                        self._attr_supported_features |= (\n                            CoverEntityFeature.OPEN)\n                        self._prop_motor_value_open = item.value\n                    elif item.name in {'close', 'down'}:\n                        self._attr_supported_features |= (\n                            CoverEntityFeature.CLOSE)\n                        self._prop_motor_value_close = item.value\n                    elif item.name in {'pause', 'stop'}:\n                        self._attr_supported_features |= (\n                            CoverEntityFeature.STOP)\n                        self._prop_motor_value_pause = item.value\n                self._prop_motor_control = prop\n            elif prop.name == 'status':\n                if not prop.value_list:\n                    _LOGGER.error('status value_list is None, %s',\n                                  self.entity_id)\n                    continue\n                for item in prop.value_list.items:\n                    item_str: str = item.name\n                    item_name: str = re.sub(r'[^a-z]', '', item_str)\n                    if item_name in {\n                            'opening', 'open', 'up', 'uping', 'rise', 'rising'\n                    }:\n                        self._prop_status_opening.append(item.value)\n                    elif item_name in {\n                            'closing', 'close', 'down', 'dowm', 'falling',\n                            'fallin', 'dropping', 'downing', 'lower'\n                    }:\n                        self._prop_status_closing.append(item.value)\n                    elif item_name in {\n                            'closed', 'closeover', 'stopatlowest',\n                            'stoplowerlimit', 'lowerlimitstop', 'floor',\n                            'lowerlimit'\n                    }:\n                        self._prop_status_closed.append(item.value)\n                self._prop_status = prop\n            elif prop.name == 'current-position':\n                if not prop.value_range:\n                    _LOGGER.error(\n                        'invalid current-position value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._prop_position_value_min = prop.value_range.min_\n                self._prop_position_value_max = prop.value_range.max_\n                self._prop_position_value_range = (prop.value_range.max_ -\n                                                   prop.value_range.min_)\n                self._prop_current_position = prop\n            elif prop.name == 'target-position':\n                if not prop.value_range:\n                    _LOGGER.error(\n                        'invalid target-position value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._prop_position_value_min = prop.value_range.min_\n                self._prop_position_value_max = prop.value_range.max_\n                self._prop_position_value_range = (prop.value_range.max_ -\n                                                   prop.value_range.min_)\n                self._attr_supported_features |= CoverEntityFeature.SET_POSITION\n                self._prop_target_position = prop\n        # For the device that has the current position property but no status\n        # property, the current position property will be used to determine the\n        # opening and the closing status.\n        if (self._prop_status is None) and (self._prop_current_position\n                                            is not None):\n            self.sub_prop_changed(self._prop_current_position,\n                                  self._position_changed_handler)\n\n    def _position_changed_handler(self, prop: MIoTSpecProperty,\n                                  ctx: Any) -> None:\n        self._prop_pos_closing = False\n        self._prop_pos_opening = False\n        self.async_write_ha_state()\n\n    async def async_open_cover(self, **kwargs) -> None:\n        \"\"\"Open the cover.\"\"\"\n        current = None if (self._prop_current_position\n                           is None) else self.get_prop_value(\n                               prop=self._prop_current_position)\n        if (current is not None) and (current < self._prop_position_value_max):\n            self._prop_pos_opening = True\n            self._prop_pos_closing = False\n        await self.set_property_async(self._prop_motor_control,\n                                      self._prop_motor_value_open)\n\n    async def async_close_cover(self, **kwargs) -> None:\n        \"\"\"Close the cover.\"\"\"\n        current = None if (self._prop_current_position\n                           is None) else self.get_prop_value(\n                               prop=self._prop_current_position)\n        if (current is not None) and (current > self._prop_position_value_min):\n            self._prop_pos_opening = False\n            self._prop_pos_closing = True\n        await self.set_property_async(self._prop_motor_control,\n                                      self._prop_motor_value_close)\n\n    async def async_stop_cover(self, **kwargs) -> None:\n        \"\"\"Stop the cover.\"\"\"\n        self._prop_pos_opening = False\n        self._prop_pos_closing = False\n        await self.set_property_async(self._prop_motor_control,\n                                      self._prop_motor_value_pause)\n\n    async def async_set_cover_position(self, **kwargs) -> None:\n        \"\"\"Set the position of the cover.\"\"\"\n        pos = kwargs.get(ATTR_POSITION, None)\n        if pos is None:\n            return None\n        current = self.current_cover_position\n        if current is not None:\n            self._prop_pos_opening = pos > current\n            self._prop_pos_closing = pos < current\n        pos = round(pos * self._prop_position_value_range / 100)\n        await self.set_property_async(prop=self._prop_target_position,\n                                      value=pos)\n\n    @property\n    def current_cover_position(self) -> Optional[int]:\n        \"\"\"Return the current position.\n\n        0: the cover is closed, 100: the cover is fully opened, None: unknown.\n        \"\"\"\n        if self._prop_current_position is None:\n            # Assume that the current position is the same as the target\n            # position when the current position is not defined in the device's\n            # MIoT-Spec-V2.\n            if self._prop_target_position is None:\n                return None\n            self._prop_pos_opening = False\n            self._prop_pos_closing = False\n            return self.get_prop_value(prop=self._prop_target_position)\n        pos = self.get_prop_value(prop=self._prop_current_position)\n        if pos is None:\n            return None\n        pos = round(pos*100/self._prop_position_value_range)\n        if pos <= self._cover_dead_zone_width:\n            pos = 0\n        elif pos >= (100 - self._cover_dead_zone_width):\n            pos = 100\n        return pos\n\n    @property\n    def is_opening(self) -> Optional[bool]:\n        \"\"\"Return if the cover is opening.\"\"\"\n        if self._prop_status and self._prop_status_opening:\n            return (self.get_prop_value(prop=self._prop_status)\n                    in self._prop_status_opening)\n        # The status has higher priority when determining whether the cover\n        # is opening.\n        return self._prop_pos_opening\n\n    @property\n    def is_closing(self) -> Optional[bool]:\n        \"\"\"Return if the cover is closing.\"\"\"\n        if self._prop_status and self._prop_status_closing:\n            return (self.get_prop_value(prop=self._prop_status)\n                    in self._prop_status_closing)\n        # The status has higher priority when determining whether the cover\n        # is closing.\n        return self._prop_pos_closing\n\n    @property\n    def is_closed(self) -> Optional[bool]:\n        \"\"\"Return if the cover is closed.\"\"\"\n        if self.current_cover_position is not None:\n            return self.current_cover_position == 0\n        # The current position is prior to the status when determining\n        # whether the cover is closed.\n        if self._prop_status and self._prop_status_closed:\n            return (self.get_prop_value(prop=self._prop_status)\n                    in self._prop_status_closed)\n        return None\n"
  },
  {
    "path": "custom_components/xiaomi_home/device_tracker.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nDevice tracker entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.device_tracker import TrackerEntity\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData\nfrom .miot.miot_spec import MIoTSpecProperty\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('device_tracker', []):\n            new_entities.append(\n                DeviceTracker(miot_device=miot_device, entity_data=data))\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass DeviceTracker(MIoTServiceEntity, TrackerEntity):\n    \"\"\"Tracker entities for Xiaomi Home.\"\"\"\n    _prop_battery_level: Optional[MIoTSpecProperty]\n    _prop_latitude: Optional[MIoTSpecProperty]\n    _prop_longitude: Optional[MIoTSpecProperty]\n    _prop_area_id: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._prop_battery_level = None\n        self._prop_latitude = None\n        self._prop_longitude = None\n        self._prop_area_id = None\n\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'battery-level':\n                self._prop_battery_level = prop\n            elif prop.name == 'latitude':\n                self._prop_latitude = prop\n            elif prop.name == 'longitude':\n                self._prop_longitude = prop\n            elif prop.name == 'area-id':\n                self._prop_area_id = prop\n\n    @property\n    def battery_level(self) -> Optional[int]:\n        \"\"\"The battery level of the device.\"\"\"\n        return None if (self._prop_battery_level\n                        is None) else self.get_prop_value(\n                            prop=self._prop_battery_level)\n\n    @property\n    def latitude(self) -> Optional[float]:\n        \"\"\"The latitude coordinate of the device.\"\"\"\n        return None if self._prop_latitude is None else self.get_prop_value(\n            prop=self._prop_latitude)\n\n    @property\n    def longitude(self) -> Optional[float]:\n        \"\"\"The longitude coordinate of the device.\"\"\"\n        return None if self._prop_longitude is None else self.get_prop_value(\n            prop=self._prop_longitude)\n\n    @property\n    def location_name(self) -> Optional[str]:\n        \"\"\"The location name of the device.\"\"\"\n        return None if self._prop_area_id is None else self.get_prop_value(\n            prop=self._prop_area_id)\n"
  },
  {
    "path": "custom_components/xiaomi_home/event.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nEvent entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.event import EventEntity\n\nfrom .miot.miot_spec import MIoTSpecEvent\nfrom .miot.miot_device import MIoTDevice, MIoTEventEntity\nfrom .miot.const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for event in miot_device.event_list.get('event', []):\n            new_entities.append(Event(miot_device=miot_device, spec=event))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Event(MIoTEventEntity, EventEntity):\n    \"\"\"Event entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None:\n        \"\"\"Initialize the Event.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        # Set device_class\n        self._attr_device_class = spec.device_class\n\n    def on_event_occurred(\n        self, name: str, arguments: dict[str, Any] | None = None\n    ) -> None:\n        \"\"\"An event is occurred.\"\"\"\n        _LOGGER.debug('%s, attributes: %s', name, str(arguments))\n        self._trigger_event(event_type=name, event_attributes=arguments)\n"
  },
  {
    "path": "custom_components/xiaomi_home/fan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nFan entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Any, Optional\nimport logging\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.fan import (\n    FanEntity,\n    FanEntityFeature,\n    DIRECTION_FORWARD,\n    DIRECTION_REVERSE\n)\nfrom homeassistant.util.percentage import (\n    percentage_to_ranged_value,\n    ranged_value_to_percentage,\n    ordered_list_item_to_percentage,\n    percentage_to_ordered_list_item\n)\n\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n        hass: HomeAssistant,\n        config_entry: ConfigEntry,\n        async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('fan', []):\n            new_entities.append(Fan(miot_device=miot_device, entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Fan(MIoTServiceEntity, FanEntity):\n    \"\"\"Fan entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n    _prop_on: MIoTSpecProperty\n    _prop_fan_level: Optional[MIoTSpecProperty]\n    _prop_mode: Optional[MIoTSpecProperty]\n    _prop_horizontal_swing: Optional[MIoTSpecProperty]\n    _prop_wind_reverse: Optional[MIoTSpecProperty]\n    _prop_wind_reverse_forward: Any\n    _prop_wind_reverse_reverse: Any\n\n    _speed_min: int\n    _speed_max: int\n    _speed_step: int\n    _speed_names: Optional[list]\n    _speed_name_map: Optional[dict[int, str]]\n    _mode_map: Optional[dict[Any, Any]]\n\n    def __init__(\n        self, miot_device: MIoTDevice, entity_data: MIoTEntityData\n    ) -> None:\n        \"\"\"Initialize the Fan.\"\"\"\n        super().__init__(miot_device=miot_device,  entity_data=entity_data)\n        self._attr_preset_modes = []\n        self._attr_current_direction = None\n        self._attr_supported_features = FanEntityFeature(0)\n\n        # _prop_on is required\n        self._prop_fan_level = None\n        self._prop_mode = None\n        self._prop_horizontal_swing = None\n        self._prop_wind_reverse = None\n        self._prop_wind_reverse_forward = None\n        self._prop_wind_reverse_reverse = None\n        self._speed_min = 65535\n        self._speed_max = 0\n        self._speed_step = 1\n        self._speed_names = []\n        self._speed_name_map = {}\n\n        self._mode_map = None\n\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'on':\n                self._attr_supported_features |= FanEntityFeature.TURN_ON\n                self._attr_supported_features |= FanEntityFeature.TURN_OFF\n                self._prop_on = prop\n            elif prop.name == 'fan-level':\n                if prop.value_range:\n                    # Fan level with value-range\n                    self._speed_min = prop.value_range.min_\n                    self._speed_max = prop.value_range.max_\n                    self._speed_step = prop.value_range.step\n                    self._attr_speed_count = int((\n                        self._speed_max - self._speed_min)/self._speed_step)+1\n                    self._attr_supported_features |= FanEntityFeature.SET_SPEED\n                    self._prop_fan_level = prop\n                elif (\n                    self._prop_fan_level is None\n                    and prop.value_list\n                ):\n                    # Fan level with value-list\n                    # Fan level with value-range is prior to fan level with\n                    # value-list when a fan has both fan level properties.\n                    self._speed_name_map = prop.value_list.to_map()\n                    self._speed_names = list(self._speed_name_map.values())\n                    self._attr_speed_count = len(self._speed_names)\n                    self._attr_supported_features |= FanEntityFeature.SET_SPEED\n                    self._prop_fan_level = prop\n            elif prop.name == 'mode':\n                if not prop.value_list:\n                    _LOGGER.error(\n                        'mode value_list is None, %s', self.entity_id)\n                    continue\n                self._mode_map = prop.value_list.to_map()\n                self._attr_preset_modes = list(self._mode_map.values())\n                self._attr_supported_features |= FanEntityFeature.PRESET_MODE\n                self._prop_mode = prop\n            elif prop.name == 'horizontal-swing':\n                self._attr_supported_features |= FanEntityFeature.OSCILLATE\n                self._prop_horizontal_swing = prop\n            elif prop.name == 'wind-reverse':\n                if prop.format_ == bool:\n                    self._prop_wind_reverse_forward = False\n                    self._prop_wind_reverse_reverse = True\n                elif prop.value_list:\n                    for item in prop.value_list.items:\n                        if item.name in {'foreward', 'forward'}:\n                            self._prop_wind_reverse_forward = item.value\n                        elif item.name in {'reversal', 'reverse'}:\n                            self._prop_wind_reverse_reverse = item.value\n                if (\n                    self._prop_wind_reverse_forward is None\n                    or self._prop_wind_reverse_reverse is None\n                ):\n                    # NOTICE: Value may be 0 or False\n                    _LOGGER.error(\n                        'invalid wind-reverse, %s', self.entity_id)\n                    continue\n                self._attr_supported_features |= FanEntityFeature.DIRECTION\n                self._prop_wind_reverse = prop\n\n    async def async_turn_on(\n        self, percentage: Optional[int] = None,\n        preset_mode: Optional[str] = None, **kwargs: Any\n    ) -> None:\n        \"\"\"Turn the fan on.\n\n        Shall set the percentage or the preset_mode attr to complying\n        if applicable.\n        \"\"\"\n        # on\n        await self.set_property_async(prop=self._prop_on, value=True)\n        # percentage\n        if percentage:\n            if self._speed_names:\n                await self.set_property_async(\n                    prop=self._prop_fan_level,\n                    value=self.get_map_key(\n                        map_=self._speed_name_map,\n                        value=percentage_to_ordered_list_item(\n                            self._speed_names, percentage)))\n            else:\n                await self.set_property_async(\n                    prop=self._prop_fan_level,\n                    value=int(percentage_to_ranged_value(\n                        low_high_range=(self._speed_min, self._speed_max),\n                        percentage=percentage)))\n        # preset_mode\n        if preset_mode:\n            await self.set_property_async(\n                prop=self._prop_mode,\n                value=self.get_map_key(\n                    map_=self._mode_map, value=preset_mode))\n\n    async def async_turn_off(self, **kwargs: Any) -> None:\n        \"\"\"Turn the fan off.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=False)\n\n    async def async_toggle(self, **kwargs: Any) -> None:\n        \"\"\"Toggle the fan.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=not self.is_on)\n\n    async def async_set_percentage(self, percentage: int) -> None:\n        \"\"\"Set the percentage of the fan speed.\"\"\"\n        if percentage > 0:\n            if not self.is_on:\n                # If the fan is off, turn it on.\n                await self.set_property_async(prop=self._prop_on, value=True)\n            if self._speed_names:\n                await self.set_property_async(\n                    prop=self._prop_fan_level,\n                    value=self.get_map_key(\n                        map_=self._speed_name_map,\n                        value=percentage_to_ordered_list_item(\n                            self._speed_names, percentage)))\n            else:\n                await self.set_property_async(\n                    prop=self._prop_fan_level,\n                    value=int(percentage_to_ranged_value(\n                        low_high_range=(self._speed_min, self._speed_max),\n                        percentage=percentage)))\n        else:\n            await self.set_property_async(prop=self._prop_on, value=False)\n\n    async def async_set_preset_mode(self, preset_mode: str) -> None:\n        \"\"\"Set the preset mode.\"\"\"\n        await self.set_property_async(\n            prop=self._prop_mode,\n            value=self.get_map_key(\n                map_=self._mode_map, value=preset_mode))\n\n    async def async_set_direction(self, direction: str) -> None:\n        \"\"\"Set the direction of the fan.\"\"\"\n        if not self._prop_wind_reverse:\n            return\n        await self.set_property_async(\n            prop=self._prop_wind_reverse,\n            value=(\n                self._prop_wind_reverse_reverse\n                if direction == DIRECTION_REVERSE\n                else self._prop_wind_reverse_forward))\n\n    async def async_oscillate(self, oscillating: bool) -> None:\n        \"\"\"Oscillate the fan.\"\"\"\n        await self.set_property_async(\n            prop=self._prop_horizontal_swing, value=oscillating)\n\n    @property\n    def is_on(self) -> Optional[bool]:\n        \"\"\"Return if the fan is on. \"\"\"\n        return self.get_prop_value(\n            prop=self._prop_on) if self._prop_on else None\n\n    @property\n    def preset_mode(self) -> Optional[str]:\n        \"\"\"Return the current preset mode,\n        e.g., auto, smart, eco, favorite.\"\"\"\n        return (\n            self.get_map_value(\n                map_=self._mode_map,\n                key=self.get_prop_value(prop=self._prop_mode))\n            if self._prop_mode else None)\n\n    @property\n    def current_direction(self) -> Optional[str]:\n        \"\"\"Return the current direction of the fan.\"\"\"\n        if not self._prop_wind_reverse:\n            return None\n        return DIRECTION_REVERSE if self.get_prop_value(\n            prop=self._prop_wind_reverse\n        ) == self._prop_wind_reverse_reverse else DIRECTION_FORWARD\n\n    @property\n    def percentage(self) -> Optional[int]:\n        \"\"\"Return the current percentage of the fan speed.\"\"\"\n        fan_level = self.get_prop_value(prop=self._prop_fan_level)\n        if fan_level is None:\n            return None\n        if self._speed_names and self._speed_name_map:\n            return ordered_list_item_to_percentage(\n                self._speed_names, self._speed_name_map[fan_level])\n        else:\n            return ranged_value_to_percentage(\n                low_high_range=(self._speed_min, self._speed_max),\n                value=fan_level)\n\n    @property\n    def oscillating(self) -> Optional[bool]:\n        \"\"\"Return if the fan is oscillating.\"\"\"\n        return (\n            self.get_prop_value(\n                prop=self._prop_horizontal_swing)\n            if self._prop_horizontal_swing else None)\n"
  },
  {
    "path": "custom_components/xiaomi_home/humidifier.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nHumidifier entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any, Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.humidifier import (HumidifierEntity,\n                                                 HumidifierDeviceClass,\n                                                 HumidifierEntityFeature,\n                                                 HumidifierAction)\n\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity\nfrom .miot.const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('humidifier', []):\n            data.device_class = HumidifierDeviceClass.HUMIDIFIER\n            new_entities.append(\n                Humidifier(miot_device=miot_device, entity_data=data))\n        for data in miot_device.entity_list.get('dehumidifier', []):\n            data.device_class = HumidifierDeviceClass.DEHUMIDIFIER\n            new_entities.append(\n                Humidifier(miot_device=miot_device, entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Humidifier(MIoTServiceEntity, HumidifierEntity):\n    \"\"\"Humidifier entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n    _prop_on: Optional[MIoTSpecProperty]\n    _prop_mode: Optional[MIoTSpecProperty]\n    _prop_target_humidity: Optional[MIoTSpecProperty]\n    _prop_humidity: Optional[MIoTSpecProperty]\n\n    _mode_map: dict[Any, Any]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the Humidifier.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._attr_device_class = entity_data.device_class\n        self._attr_supported_features = HumidifierEntityFeature(0)\n        self._prop_on = None\n        self._prop_mode = None\n        self._prop_target_humidity = None\n        self._prop_humidity = None\n        self._mode_map = None\n\n        # properties\n        for prop in entity_data.props:\n            # on\n            if prop.name == 'on':\n                self._prop_on = prop\n            # target-humidity\n            elif prop.name == 'target-humidity':\n                if not prop.value_range:\n                    _LOGGER.error(\n                        'invalid target-humidity value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._attr_min_humidity = prop.value_range.min_\n                self._attr_max_humidity = prop.value_range.max_\n                self._prop_target_humidity = prop\n            # mode\n            elif prop.name == 'mode':\n                if not prop.value_list:\n                    _LOGGER.error('mode value_list is None, %s', self.entity_id)\n                    continue\n                self._mode_map = prop.value_list.to_map()\n                self._attr_available_modes = list(self._mode_map.values())\n                self._attr_supported_features |= HumidifierEntityFeature.MODES\n                self._prop_mode = prop\n            # relative-humidity\n            elif prop.name == 'relative-humidity':\n                self._prop_humidity = prop\n\n    async def async_turn_on(self, **kwargs):\n        \"\"\"Turn the humidifier on.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=True)\n\n    async def async_turn_off(self, **kwargs):\n        \"\"\"Turn the humidifier off.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=False)\n\n    async def async_set_humidity(self, humidity: int) -> None:\n        \"\"\"Set new target humidity.\"\"\"\n        if self._prop_target_humidity is None:\n            return\n        await self.set_property_async(prop=self._prop_target_humidity,\n                                      value=humidity)\n\n    async def async_set_mode(self, mode: str) -> None:\n        \"\"\"Set new target preset mode.\"\"\"\n        await self.set_property_async(prop=self._prop_mode,\n                                      value=self.get_map_key(\n                                          map_=self._mode_map, value=mode))\n\n    @property\n    def is_on(self) -> Optional[bool]:\n        \"\"\"Return if the humidifier is on.\"\"\"\n        return self.get_prop_value(prop=self._prop_on)\n\n    @property\n    def action(self) -> Optional[HumidifierAction]:\n        \"\"\"Return the current status of the device.\"\"\"\n        if not self.is_on:\n            return HumidifierAction.OFF\n        if self._attr_device_class == HumidifierDeviceClass.HUMIDIFIER:\n            return HumidifierAction.HUMIDIFYING\n        return HumidifierAction.DRYING\n\n    @property\n    def current_humidity(self) -> Optional[int]:\n        \"\"\"Return the current humidity.\"\"\"\n        return (self.get_prop_value(\n            prop=self._prop_humidity) if self._prop_humidity else None)\n\n    @property\n    def target_humidity(self) -> Optional[int]:\n        \"\"\"Return the target humidity.\"\"\"\n        return (self.get_prop_value(prop=self._prop_target_humidity)\n                if self._prop_target_humidity else None)\n\n    @property\n    def mode(self) -> Optional[str]:\n        \"\"\"Return the current preset mode.\"\"\"\n        return self.get_map_value(map_=self._mode_map,\n                                  key=self.get_prop_value(prop=self._prop_mode))\n"
  },
  {
    "path": "custom_components/xiaomi_home/light.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nLight entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any, Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.light import (\n    ATTR_BRIGHTNESS,\n    ATTR_COLOR_TEMP_KELVIN,\n    ATTR_RGB_COLOR,\n    ATTR_EFFECT,\n    LightEntity,\n    LightEntityFeature,\n    ColorMode\n)\nfrom homeassistant.util.color import (\n    value_to_brightness,\n    brightness_to_value\n)\n\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.miot_device import MIoTDevice, MIoTEntityData,  MIoTServiceEntity\nfrom .miot.const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n        hass: HomeAssistant,\n        config_entry: ConfigEntry,\n        async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('light', []):\n            new_entities.append(\n                Light(miot_device=miot_device, entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Light(MIoTServiceEntity, LightEntity):\n    \"\"\"Light entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n    _VALUE_RANGE_MODE_COUNT_MAX = 30\n    _prop_on: Optional[MIoTSpecProperty]\n    _prop_brightness: Optional[MIoTSpecProperty]\n    _prop_color_temp: Optional[MIoTSpecProperty]\n    _prop_color: Optional[MIoTSpecProperty]\n    _prop_mode: Optional[MIoTSpecProperty]\n\n    _brightness_scale: Optional[tuple[int, int]]\n    _mode_map: Optional[dict[Any, Any]]\n\n    def __init__(\n        self, miot_device: MIoTDevice,  entity_data: MIoTEntityData\n    ) -> None:\n        \"\"\"Initialize the Light.\"\"\"\n        super().__init__(miot_device=miot_device,  entity_data=entity_data)\n        self._attr_color_mode = None\n        self._attr_supported_color_modes = set()\n        self._attr_supported_features = LightEntityFeature(0)\n        if miot_device.did.startswith('group.'):\n            self._attr_icon = 'mdi:lightbulb-group'\n\n        self._prop_on = None\n        self._prop_brightness = None\n        self._prop_color_temp = None\n        self._prop_color = None\n        self._prop_mode = None\n        self._brightness_scale = None\n        self._mode_map = None\n\n        # properties\n        for prop in entity_data.props:\n            # on\n            if prop.name == 'on':\n                self._prop_on = prop\n            # brightness\n            if prop.name == 'brightness':\n                if prop.value_range:\n                    self._brightness_scale = (\n                        prop.value_range.min_, prop.value_range.max_)\n                    self._prop_brightness = prop\n                elif (\n                    self._mode_map is None\n                    and prop.value_list\n                ):\n                    # For value-list brightness\n                    self._mode_map = prop.value_list.to_map()\n                    self._attr_effect_list = list(self._mode_map.values())\n                    self._attr_supported_features |= LightEntityFeature.EFFECT\n                    self._prop_mode = prop\n                else:\n                    _LOGGER.info(\n                        'invalid brightness format, %s', self.entity_id)\n                    continue\n            # color-temperature\n            if prop.name == 'color-temperature':\n                if not prop.value_range:\n                    _LOGGER.info(\n                        'invalid color-temperature value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._attr_min_color_temp_kelvin = prop.value_range.min_\n                self._attr_max_color_temp_kelvin = prop.value_range.max_\n                self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP)\n                self._attr_color_mode = ColorMode.COLOR_TEMP\n                self._prop_color_temp = prop\n            # color\n            if prop.name == 'color':\n                self._attr_supported_color_modes.add(ColorMode.RGB)\n                self._attr_color_mode = ColorMode.RGB\n                self._prop_color = prop\n            # mode\n            if prop.name == 'mode':\n                mode_list = None\n                if prop.value_list:\n                    mode_list = prop.value_list.to_map()\n                elif prop.value_range:\n                    mode_list = {}\n                    if (\n                        int((\n                            prop.value_range.max_\n                            - prop.value_range.min_\n                        ) / prop.value_range.step)\n                        > self._VALUE_RANGE_MODE_COUNT_MAX\n                    ):\n                        _LOGGER.error(\n                            'too many mode values, %s, %s, %s',\n                            self.entity_id, prop.name, prop.value_range)\n                    else:\n                        for value in range(\n                                prop.value_range.min_,\n                                prop.value_range.max_,\n                                prop.value_range.step):\n                            mode_list[value] = f'mode {value}'\n                if mode_list:\n                    self._mode_map = mode_list\n                    self._attr_effect_list = list(self._mode_map.values())\n                    self._attr_supported_features |= LightEntityFeature.EFFECT\n                    self._prop_mode = prop\n                else:\n                    _LOGGER.info('invalid mode format, %s', self.entity_id)\n                    continue\n\n        if not self._attr_supported_color_modes:\n            if self._prop_brightness:\n                self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)\n                self._attr_color_mode = ColorMode.BRIGHTNESS\n            elif self._prop_on:\n                self._attr_supported_color_modes.add(ColorMode.ONOFF)\n                self._attr_color_mode = ColorMode.ONOFF\n\n    @property\n    def is_on(self) -> Optional[bool]:\n        \"\"\"Return if the light is on.\"\"\"\n        value_on = self.get_prop_value(prop=self._prop_on)\n        # Dirty logic for lumi.gateway.mgl03 indicator light\n        if isinstance(value_on, int):\n            value_on = value_on == 1\n        return value_on\n\n    @property\n    def brightness(self) -> Optional[int]:\n        \"\"\"Return the brightness.\"\"\"\n        brightness_value = self.get_prop_value(prop=self._prop_brightness)\n        if brightness_value is None:\n            return None\n        return value_to_brightness(self._brightness_scale, brightness_value)\n\n    @property\n    def color_temp_kelvin(self) -> Optional[int]:\n        \"\"\"Return the color temperature.\"\"\"\n        return self.get_prop_value(prop=self._prop_color_temp)\n\n    @property\n    def rgb_color(self) -> Optional[tuple[int, int, int]]:\n        \"\"\"Return the rgb color value.\"\"\"\n        rgb = self.get_prop_value(prop=self._prop_color)\n        if rgb is None:\n            return None\n        r = (rgb >> 16) & 0xFF\n        g = (rgb >> 8) & 0xFF\n        b = rgb & 0xFF\n        return r, g, b\n\n    @property\n    def effect(self) -> Optional[str]:\n        \"\"\"Return the current mode.\"\"\"\n        return self.get_map_value(\n            map_=self._mode_map,\n            key=self.get_prop_value(prop=self._prop_mode))\n\n    async def async_turn_on(self, **kwargs) -> None:\n        \"\"\"Turn the light on.\n\n        Shall set attributes in kwargs if applicable.\n        \"\"\"\n        # on\n        # Dirty logic for lumi.gateway.mgl03 indicator light\n        if self._prop_on:\n            value_on = True if self._prop_on.format_ == bool else 1\n            await self.set_property_async(\n                prop=self._prop_on, value=value_on)\n        # brightness\n        if ATTR_BRIGHTNESS in kwargs:\n            brightness = brightness_to_value(\n                self._brightness_scale, kwargs[ATTR_BRIGHTNESS])\n            await self.set_property_async(\n                prop=self._prop_brightness, value=brightness,\n                write_ha_state=False)\n        # color-temperature\n        if ATTR_COLOR_TEMP_KELVIN in kwargs:\n            await self.set_property_async(\n                prop=self._prop_color_temp,\n                value=kwargs[ATTR_COLOR_TEMP_KELVIN],\n                write_ha_state=False)\n            self._attr_color_mode = ColorMode.COLOR_TEMP\n        # rgb color\n        if ATTR_RGB_COLOR in kwargs:\n            r = kwargs[ATTR_RGB_COLOR][0]\n            g = kwargs[ATTR_RGB_COLOR][1]\n            b = kwargs[ATTR_RGB_COLOR][2]\n            rgb = (r << 16) | (g << 8) | b\n            await self.set_property_async(\n                prop=self._prop_color, value=rgb,\n                write_ha_state=False)\n            self._attr_color_mode = ColorMode.RGB\n        # mode\n        if ATTR_EFFECT in kwargs:\n            await self.set_property_async(\n                prop=self._prop_mode,\n                value=self.get_map_key(\n                    map_=self._mode_map, value=kwargs[ATTR_EFFECT]),\n                write_ha_state=False)\n        self.async_write_ha_state()\n\n    async def async_turn_off(self, **kwargs) -> None:\n        \"\"\"Turn the light off.\"\"\"\n        if not self._prop_on:\n            return\n        # Dirty logic for lumi.gateway.mgl03 indicator light\n        value_on = False if self._prop_on.format_ == bool else 0\n        await self.set_property_async(prop=self._prop_on, value=value_on)\n"
  },
  {
    "path": "custom_components/xiaomi_home/manifest.json",
    "content": "{\n    \"domain\": \"xiaomi_home\",\n    \"name\": \"Xiaomi Home\",\n    \"codeowners\": [\n        \"@XiaoMi\"\n    ],\n    \"config_flow\": true,\n    \"dependencies\": [\n        \"http\",\n        \"persistent_notification\",\n        \"ffmpeg\",\n        \"zeroconf\"\n    ],\n    \"documentation\": \"https://github.com/XiaoMi/ha_xiaomi_home/blob/main/README.md\",\n    \"integration_type\": \"hub\",\n    \"iot_class\": \"cloud_polling\",\n    \"issue_tracker\": \"https://github.com/XiaoMi/ha_xiaomi_home/issues\",\n    \"loggers\": [\n        \"Xiaomi Home\"\n    ],\n    \"requirements\": [\n        \"construct>=2.10.56\",\n        \"paho-mqtt\",\n        \"numpy\",\n        \"cryptography\",\n        \"psutil\"\n    ],\n    \"version\": \"v0.4.7\",\n    \"zeroconf\": [\n        \"_miot-central._tcp.local.\"\n    ]\n}\n"
  },
  {
    "path": "custom_components/xiaomi_home/media_player.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMedia player entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.media_player import (MediaPlayerEntity,\n                                                   MediaPlayerEntityFeature,\n                                                   MediaPlayerDeviceClass,\n                                                   MediaPlayerState, MediaType)\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData\nfrom .miot.miot_spec import MIoTSpecProperty, MIoTSpecAction\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,\n                            async_add_entities: AddEntitiesCallback) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('wifi-speaker', []):\n            new_entities.append(\n                WifiSpeaker(miot_device=miot_device, entity_data=data))\n        for data in miot_device.entity_list.get('television', []):\n            new_entities.append(\n                Television(miot_device=miot_device, entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass FeatureVolumeMute(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"VOLUME_MUTE feature of the media player entity.\"\"\"\n    _prop_mute: Optional[MIoTSpecProperty]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_mute = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'mute':\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.VOLUME_MUTE)\n                self._prop_mute = prop\n\n    @property\n    def is_volume_muted(self) -> Optional[bool]:\n        \"\"\"True if volume is currently muted.\"\"\"\n        return self.get_prop_value(\n            prop=self._prop_mute) if self._prop_mute else None\n\n    async def async_mute_volume(self, mute: bool) -> None:\n        \"\"\"Mute the volume.\"\"\"\n        await self.set_property_async(prop=self._prop_mute, value=mute)\n\n\nclass FeatureVolumeSet(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"VOLUME_SET feature of the media player entity.\"\"\"\n    _prop_volume: Optional[MIoTSpecProperty]\n    _volume_value_min: Optional[float]\n    _volume_value_max: Optional[float]\n    _volume_value_range: Optional[float]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_volume = None\n        self._volume_value_min = None\n        self._volume_value_max = None\n        self._volume_value_range = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'volume':\n                if not prop.value_range:\n                    _LOGGER.error('invalid volume value_range format, %s',\n                                  self.entity_id)\n                    continue\n                self._volume_value_min = prop.value_range.min_\n                self._volume_value_max = prop.value_range.max_\n                self._volume_value_range = (prop.value_range.max_ -\n                                            prop.value_range.min_)\n                self._attr_volume_step = (prop.value_range.step /\n                                          self._volume_value_range)\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.VOLUME_SET |\n                    MediaPlayerEntityFeature.VOLUME_STEP)\n                self._prop_volume = prop\n\n    async def async_set_volume_level(self, volume: float) -> None:\n        \"\"\"Set volume level.\"\"\"\n        value = volume * self._volume_value_range + self._volume_value_min\n        if value > self._volume_value_max:\n            value = self._volume_value_max\n        elif value < self._volume_value_min:\n            value = self._volume_value_min\n        await self.set_property_async(prop=self._prop_volume, value=value)\n\n    @property\n    def volume_level(self) -> Optional[float]:\n        \"\"\"The current volume level, range [0, 1].\"\"\"\n        value = self.get_prop_value(\n            prop=self._prop_volume) if self._prop_volume else None\n        if value is None:\n            return None\n        return (value - self._volume_value_min) / self._volume_value_range\n\n\nclass FeaturePlay(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"PLAY feature of the media player entity.\"\"\"\n    _action_play: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_play = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'play':\n                self._attr_supported_features |= (MediaPlayerEntityFeature.PLAY)\n                self._action_play = act\n\n    async def async_media_play(self) -> None:\n        \"\"\"Send play command.\"\"\"\n        await self.action_async(action=self._action_play)\n\n\nclass FeaturePause(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"PAUSE feature of the media player entity.\"\"\"\n    _action_pause: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_pause = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'pause':\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.PAUSE)\n                self._action_pause = act\n\n    async def async_media_pause(self) -> None:\n        \"\"\"Send pause command.\"\"\"\n        await self.action_async(action=self._action_pause)\n\n\nclass FeatureStop(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"STOP feature of the media player entity.\"\"\"\n    _action_stop: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_stop = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'stop':\n                self._attr_supported_features |= (MediaPlayerEntityFeature.STOP)\n                self._action_stop = act\n\n    async def async_media_stop(self) -> None:\n        \"\"\"Send stop command.\"\"\"\n        await self.action_async(action=self._action_stop)\n\n\nclass FeatureNextTrack(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"NEXT_TRACK feature of the media player entity.\"\"\"\n    _action_next: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_next = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'next':\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.NEXT_TRACK)\n                self._action_next = act\n\n    async def async_media_next_track(self) -> None:\n        \"\"\"Send next track command.\"\"\"\n        await self.action_async(action=self._action_next)\n\n\nclass FeaturePreviousTrack(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"PREVIOUS_TRACK feature of the media player entity.\"\"\"\n    _action_previous: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_previous = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'previous':\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.PREVIOUS_TRACK)\n                self._action_previous = act\n\n    async def async_media_previous_track(self) -> None:\n        \"\"\"Send previous track command.\"\"\"\n        await self.action_async(action=self._action_previous)\n\n\nclass FeatureSoundMode(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"SELECT_SOUND_MODE feature of the media player entity.\"\"\"\n    _prop_play_loop_mode: Optional[MIoTSpecProperty]\n    _sound_mode_map: Optional[dict[int, str]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_play_loop_mode = None\n        self._sound_mode_map = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'play-loop-mode':\n                if not prop.value_list:\n                    _LOGGER.error('invalid play-loop-mode value_list, %s',\n                                  self.entity_id)\n                    continue\n                self._sound_mode_map = prop.value_list.to_map()\n                self._attr_sound_mode_list = list(self._sound_mode_map.values())\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.SELECT_SOUND_MODE)\n                self._prop_play_loop_mode = prop\n\n    async def async_select_sound_mode(self, sound_mode: str):\n        \"\"\"Switch the sound mode of the entity.\"\"\"\n        await self.set_property_async(prop=self._prop_play_loop_mode,\n                                      value=self.get_map_key(\n                                          map_=self._sound_mode_map,\n                                          value=sound_mode))\n\n    @property\n    def sound_mode(self) -> Optional[str]:\n        \"\"\"The current sound mode.\"\"\"\n        return (self.get_map_value(map_=self._sound_mode_map,\n                                   key=self.get_prop_value(\n                                       prop=self._prop_play_loop_mode))\n                if self._prop_play_loop_mode else None)\n\n\nclass FeatureTurnOn(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"TURN_ON feature of the media player entity.\"\"\"\n    _action_turn_on: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_turn_on = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'turn-on':\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.TURN_ON)\n                self._action_turn_on = act\n\n    async def async_turn_on(self) -> None:\n        \"\"\"Turn the media player on.\"\"\"\n        await self.action_async(action=self._action_turn_on)\n\n\nclass FeatureTurnOff(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"TURN_OFF feature of the media player entity.\"\"\"\n    _action_turn_off: Optional[MIoTSpecAction]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._action_turn_off = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # actions\n        for act in entity_data.actions:\n            if act.name == 'turn-off':\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.TURN_OFF)\n                self._action_turn_off = act\n\n    async def async_turn_off(self) -> None:\n        \"\"\"Turn the media player off.\"\"\"\n        await self.action_async(action=self._action_turn_off)\n\n\nclass FeatureSource(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"SELECT_SOURCE feature of the media player entity.\"\"\"\n    _prop_input_control: Optional[MIoTSpecProperty]\n    _input_source_map: Optional[dict[int, str]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_input_control = None\n        self._input_source_map = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'input-control':\n                if not prop.value_list:\n                    _LOGGER.error('invalid input-control value_list, %s',\n                                  self.entity_id)\n                    continue\n                self._input_source_map = prop.value_list.to_map()\n                self._attr_source_list = list(self._input_source_map.values())\n                self._attr_supported_features |= (\n                    MediaPlayerEntityFeature.SELECT_SOURCE)\n                self._prop_input_control = prop\n\n    async def async_select_source(self, source: str) -> None:\n        \"\"\"Select input source.\"\"\"\n        await self.set_property_async(prop=self._prop_input_control,\n                                      value=self.get_map_key(\n                                          map_=self._input_source_map,\n                                          value=source))\n\n    @property\n    def source(self) -> Optional[str]:\n        \"\"\"The current input source.\"\"\"\n        return (self.get_map_value(map_=self._input_source_map,\n                                   key=self.get_prop_value(\n                                       prop=self._prop_input_control))\n                if self._prop_input_control else None)\n\n\nclass FeatureState(MIoTServiceEntity, MediaPlayerEntity):\n    \"\"\"States feature of the media player entity.\"\"\"\n    _prop_playing_state: Optional[MIoTSpecProperty]\n    _playing_state_map: Optional[dict[int, str]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the feature class.\"\"\"\n        self._prop_playing_state = None\n        self._playing_state_map = None\n\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'playing-state':\n                if not prop.value_list:\n                    _LOGGER.error('invalid mode value_list, %s', self.entity_id)\n                    continue\n                self._playing_state_map = {}\n                for item in prop.value_list.items:\n                    if item.name in {'off'}:\n                        self._playing_state_map[\n                            item.value] = MediaPlayerState.OFF\n                    elif item.name in {'idle', 'stop', 'stopped'}:\n                        self._playing_state_map[\n                            item.value] = MediaPlayerState.IDLE\n                    elif item.name in {'playing'}:\n                        self._playing_state_map[\n                            item.value] = MediaPlayerState.PLAYING\n                    elif item.name in {'pause', 'paused'}:\n                        self._playing_state_map[\n                            item.value] = MediaPlayerState.PAUSED\n                self._prop_playing_state = prop\n\n    @property\n    def state(self) -> Optional[MediaPlayerState]:\n        \"\"\"The current state.\"\"\"\n        current_state = self.get_prop_value(\n            prop=self._prop_playing_state) if self._prop_playing_state else None\n        return (MediaPlayerState.ON if\n                (current_state is None) else self.get_map_value(\n                    map_=self._playing_state_map, key=current_state))\n\n\nclass WifiSpeaker(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay,\n                  FeaturePause, FeatureStop, FeatureNextTrack,\n                  FeaturePreviousTrack, FeatureSoundMode, FeatureState):\n    \"\"\"WiFi speaker, aka XiaoAI sound speaker.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the device.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n        self._attr_device_class = MediaPlayerDeviceClass.SPEAKER\n        self._attr_media_content_type = MediaType.MUSIC\n\n\nclass Television(FeatureVolumeSet, FeatureVolumeMute, FeaturePlay, FeaturePause,\n                 FeatureStop, FeatureNextTrack, FeaturePreviousTrack,\n                 FeatureSoundMode, FeatureState, FeatureSource, FeatureTurnOn,\n                 FeatureTurnOff):\n    \"\"\"Television\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the device.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n\n        self._attr_device_class = MediaPlayerDeviceClass.TV\n        self._attr_media_content_type = MediaType.VIDEO\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/common.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nCommon utilities.\n\"\"\"\nimport asyncio\nimport json\nfrom os import path\nimport random\nfrom typing import Any, Optional\nimport hashlib\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\nfrom paho.mqtt.matcher import MQTTMatcher\nimport yaml\nfrom slugify import slugify\n\nMIOT_ROOT_PATH: str = path.dirname(path.abspath(__file__))\n\n\ndef gen_absolute_path(relative_path: str) -> str:\n    \"\"\"Generate an absolute path.\"\"\"\n    return path.join(MIOT_ROOT_PATH, relative_path)\n\n\ndef calc_group_id(uid: str, home_id: str) -> str:\n    \"\"\"Calculate the group ID based on a user ID and a home ID.\"\"\"\n    return hashlib.sha1(\n        f'{uid}central_service{home_id}'.encode('utf-8')).hexdigest()[:16]\n\n\ndef load_json_file(json_file: str) -> dict:\n    \"\"\"Load a JSON file.\"\"\"\n    with open(json_file, 'r', encoding='utf-8') as f:\n        return json.load(f)\n\n\ndef load_yaml_file(yaml_file: str) -> dict:\n    \"\"\"Load a YAML file.\"\"\"\n    with open(yaml_file, 'r', encoding='utf-8') as f:\n        return yaml.load(f, Loader=yaml.FullLoader)\n\n\ndef randomize_int(value: int, ratio: float) -> int:\n    \"\"\"Randomize an integer value.\"\"\"\n    return int(value * (1 - ratio + random.random()*2*ratio))\n\n\ndef randomize_float(value: float, ratio: float) -> float:\n    \"\"\"Randomize a float value.\"\"\"\n    return value * (1 - ratio + random.random()*2*ratio)\n\n\ndef slugify_name(name: str, separator: str = '_') -> str:\n    \"\"\"Slugify a name.\"\"\"\n    return slugify(name, separator=separator)\n\n\ndef slugify_did(cloud_server: str, did: str) -> str:\n    \"\"\"Slugify a device id.\"\"\"\n    return slugify(f'{cloud_server}_{did}', separator='_')\n\n\nclass MIoTMatcher(MQTTMatcher):\n    \"\"\"MIoT Pub/Sub topic matcher.\"\"\"\n\n    def iter_all_nodes(self) -> Any:\n        \"\"\"Return an iterator on all nodes with their paths and contents.\"\"\"\n        def rec(node, path_):\n            # pylint: disable=protected-access\n            if node._content:\n                yield ('/'.join(path_), node._content)\n            for part, child in node._children.items():\n                yield from rec(child, path_ + [part])\n        return rec(self._root, [])\n\n    def get(self, topic: str) -> Optional[Any]:\n        try:\n            return self[topic]\n        except KeyError:\n            return None\n\n\nclass MIoTHttp:\n    \"\"\"MIoT Common HTTP API.\"\"\"\n    @staticmethod\n    def get(\n        url: str, params: Optional[dict] = None, headers: Optional[dict] = None\n    ) -> Optional[str]:\n        full_url = url\n        if params:\n            encoded_params = urlencode(params)\n            full_url = f'{url}?{encoded_params}'\n        request = Request(full_url, method='GET', headers=headers or {})\n        content: Optional[bytes] = None\n        with urlopen(request) as response:\n            content = response.read()\n        return str(content, 'utf-8') if content else None\n\n    @staticmethod\n    def get_json(\n        url: str, params: Optional[dict] = None, headers: Optional[dict] = None\n    ) -> Optional[dict]:\n        response = MIoTHttp.get(url, params, headers)\n        return json.loads(response) if response else None\n\n    @staticmethod\n    def post(\n        url: str, data: Optional[dict] = None, headers: Optional[dict] = None\n    ) -> Optional[str]:\n        pass\n\n    @staticmethod\n    def post_json(\n        url: str, data: Optional[dict] = None, headers: Optional[dict] = None\n    ) -> Optional[dict]:\n        response = MIoTHttp.post(url, data, headers)\n        return json.loads(response) if response else None\n\n    @staticmethod\n    async def get_async(\n        url: str, params: Optional[dict] = None, headers: Optional[dict] = None,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> Optional[str]:\n        # TODO: Use aiohttp\n        ev_loop = loop or asyncio.get_running_loop()\n        return await ev_loop.run_in_executor(\n            None, MIoTHttp.get, url, params, headers)\n\n    @staticmethod\n    async def get_json_async(\n        url: str, params: Optional[dict] = None, headers: Optional[dict] = None,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> Optional[dict]:\n        ev_loop = loop or asyncio.get_running_loop()\n        return await ev_loop.run_in_executor(\n            None, MIoTHttp.get_json, url, params, headers)\n\n    @ staticmethod\n    async def post_async(\n        url: str, data: Optional[dict] = None, headers: Optional[dict] = None,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> Optional[str]:\n        ev_loop = loop or asyncio.get_running_loop()\n        return await ev_loop.run_in_executor(\n            None, MIoTHttp.post, url, data, headers)\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/const.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nConstants.\n\"\"\"\nDOMAIN: str = 'xiaomi_home'\nDEFAULT_NAME: str = 'Xiaomi Home'\n\nDEFAULT_NICK_NAME: str = 'Xiaomi'\n\nMIHOME_HTTP_API_TIMEOUT: int = 30\nMIHOME_MQTT_KEEPALIVE: int = 60\n# seconds, 3 days\nMIHOME_CERT_EXPIRE_MARGIN: int = 3600*24*3\n\nNETWORK_REFRESH_INTERVAL: int = 30\n\nOAUTH2_CLIENT_ID: str = '2882303761520251711'\nOAUTH2_AUTH_URL: str = 'https://account.xiaomi.com/oauth2/authorize'\nDEFAULT_OAUTH2_API_HOST: str = 'ha.api.io.mi.com'\nDEFAULT_CLOUD_BROKER_HOST: str = 'ha.mqtt.io.mi.com'\n\n# seconds, 14 days\nSPEC_STD_LIB_EFFECTIVE_TIME = 3600*24*14\n# seconds, 14 days\nMANUFACTURER_EFFECTIVE_TIME = 3600*24*14\n\nSUPPORTED_PLATFORMS: list = [\n    'binary_sensor',\n    'button',\n    'climate',\n    'cover',\n    'device_tracker',\n    'event',\n    'fan',\n    'humidifier',\n    'light',\n    'media_player',\n    'notify',\n    'number',\n    'select',\n    'sensor',\n    'switch',\n    'text',\n    'vacuum',\n    'water_heater',\n]\n\nUNSUPPORTED_MODELS: list = [\n    'chuangmi.ir.v2',\n    'era.airp.cwb03',\n    'hmpace.motion.v6nfc',\n    'k0918.toothbrush.t700'\n]\n\nDEFAULT_CLOUD_SERVER: str = 'cn'\nCLOUD_SERVERS: dict = {\n    'cn': '中国大陆',\n    'de': 'Europe',\n    'i2': 'India',\n    'ru': 'Russia',\n    'sg': 'Singapore',\n    'us': 'United States'\n}\n\nSUPPORT_CENTRAL_GATEWAY_CTRL: list = ['cn']\n\nDEFAULT_INTEGRATION_LANGUAGE: str = 'en'\nINTEGRATION_LANGUAGES = {\n    'de': 'Deutsch',\n    'en': 'English',\n    'es': 'Español',\n    'fr': 'Français',\n    'it': 'Italiano',\n    'ja': '日本語',\n    'nl': 'Nederlands',\n    'pt': 'Português',\n    'pt-BR': 'Português (Brasil)',\n    'ru': 'Русский',\n    'tr': 'Türkçe',\n    'zh-Hans': '简体中文',\n    'zh-Hant': '繁體中文'\n}\n\nDEFAULT_COVER_DEAD_ZONE_WIDTH: int = 0\nMIN_COVER_DEAD_ZONE_WIDTH: int = 0\nMAX_COVER_DEAD_ZONE_WIDTH: int = 5\n\nDEFAULT_CTRL_MODE: str = 'auto'\n\n# Registered in Xiaomi OAuth 2.0 Service\n# DO NOT CHANGE UNLESS YOU HAVE AN ADMINISTRATOR PERMISSION\nOAUTH_REDIRECT_URL: str = 'http://homeassistant.local:8123'\n\nMIHOME_CA_CERT_STR: str = '-----BEGIN CERTIFICATE-----\\n' \\\n    'MIIBazCCAQ+gAwIBAgIEA/UKYDAMBggqhkjOPQQDAgUAMCIxEzARBgNVBAoTCk1p\\n' \\\n    'amlhIFJvb3QxCzAJBgNVBAYTAkNOMCAXDTE2MTEyMzAxMzk0NVoYDzIwNjYxMTEx\\n' \\\n    'MDEzOTQ1WjAiMRMwEQYDVQQKEwpNaWppYSBSb290MQswCQYDVQQGEwJDTjBZMBMG\\n' \\\n    'ByqGSM49AgEGCCqGSM49AwEHA0IABL71iwLa4//4VBqgRI+6xE23xpovqPCxtv96\\n' \\\n    '2VHbZij61/Ag6jmi7oZ/3Xg/3C+whglcwoUEE6KALGJ9vccV9PmjLzAtMAwGA1Ud\\n' \\\n    'EwQFMAMBAf8wHQYDVR0OBBYEFJa3onw5sblmM6n40QmyAGDI5sURMAwGCCqGSM49\\n' \\\n    'BAMCBQADSAAwRQIgchciK9h6tZmfrP8Ka6KziQ4Lv3hKfrHtAZXMHPda4IYCIQCG\\n' \\\n    'az93ggFcbrG9u2wixjx1HKW4DUA5NXZG0wWQTpJTbQ==\\n' \\\n    '-----END CERTIFICATE-----\\n' \\\n    '-----BEGIN CERTIFICATE-----\\n' \\\n    'MIIBjzCCATWgAwIBAgIBATAKBggqhkjOPQQDAjAiMRMwEQYDVQQKEwpNaWppYSBS\\n' \\\n    'b290MQswCQYDVQQGEwJDTjAgFw0yMjA2MDkxNDE0MThaGA8yMDcyMDUyNzE0MTQx\\n' \\\n    'OFowLDELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFE1JT1QgQ0VOVFJBTCBHQVRFV0FZ\\n' \\\n    'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdYrzbnp/0x/cZLZnuEDXTFf8mhj4\\n' \\\n    'CVpZPwgj9e9Ve5r3K7zvu8Jjj7JF1JjQYvEC6yhp1SzBgglnK4L8xQzdiqNQME4w\\n' \\\n    'HQYDVR0OBBYEFCf9+YBU7pXDs6K6CAQPRhlGJ+cuMB8GA1UdIwQYMBaAFJa3onw5\\n' \\\n    'sblmM6n40QmyAGDI5sURMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIh\\n' \\\n    'AKUv+c8v98vypkGMTzMwckGjjVqTef8xodsy6PhcSCq+AiA/n9mDs62hAo5zXyJy\\n' \\\n    'Bs1s7mqXPf1XgieoxIvs1MqyiA==\\n' \\\n    '-----END CERTIFICATE-----\\n'\n\nMIHOME_CA_CERT_SHA256: str = \\\n    '8b7bf306be3632e08b0ead308249e5f2b2520dc921ad143872d5fcc7c68d6759'\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/de.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"Geräte\",\n            \"found_central_gateway\": \", lokales zentrales Gateway gefunden\",\n            \"without_room\": \"Kein Raum zugewiesen\",\n            \"no_display\": \"nicht anzeigen\"\n        },\n        \"control_mode\": {\n            \"auto\": \"automatisch\",\n            \"cloud\": \"Cloud\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"ODER-Logik\",\n            \"and\": \"UND-Logik\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"ausschließen\",\n            \"include\": \"einschließen\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi-Gerät\",\n            \"2\": \"Cloud-Gerät\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"virtuelles Gerät\",\n            \"6\": \"BLE\",\n            \"7\": \"lokaler AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"andere\",\n            \"10\": \"Funktions-Plug-in\",\n            \"11\": \"Zellnetz\",\n            \"12\": \"Kabel\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"Drittanbieter-Cloud-Zugriff\",\n            \"15\": \"Infrarot-Fernbedienungsgerät\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"virtuelle Gerätegruppe\",\n            \"18\": \"Gateway-Untergerät\",\n            \"19\": \"Sicherheitsstufe Gateway-Untergerät\",\n            \"22\": \"PLC\",\n            \"23\": \"nur Kabel\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+Zellnetz\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"nicht synchronisieren\",\n            \"home_room\": \"Hausname und Raumname (Xiaomi Home Schlafzimmer)\",\n            \"room\": \"Raumname (Schlafzimmer)\",\n            \"home\": \"Hausname (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"aktivieren\",\n            \"disable\": \"deaktivieren\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Textsensor-Entität\",\n            \"bool\": \"Binärsensor-Entität\"\n        },\n        \"device_state\": {\n            \"add\": \"hinzufügen\",\n            \"del\": \"nicht verfügbar\",\n            \"offline\": \"offline\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Hinweis]** Es wurden mehrere Netzwerkkarten erkannt, die möglicherweise mit demselben Netzwerk verbunden sind. Bitte achten Sie auf die Auswahl.\",\n            \"net_unavailable\": \"Schnittstelle nicht verfügbar\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Authentifizierung erfolgreich\",\n            \"content\": \"Bitte schließen Sie diese Seite und kehren Sie zur Kontoauthentifizierungsseite zurück, um auf „Weiter“ zu klicken.\",\n            \"button\": \"Schließen\"\n        },\n        \"fail\": {\n            \"title\": \"Authentifizierung fehlgeschlagen\",\n            \"content\": \"{error_msg}, bitte schließen Sie diese Seite und kehren Sie zur Kontoauthentifizierungsseite zurück, um den Authentifizierungslink erneut zu klicken.\",\n            \"button\": \"Schließen\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Ungültige Antwortparameter ('code' oder 'state' Feld ist leer)\",\n            \"-10101\": \"Übergebenes 'state' Feld stimmt nicht überein\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Ungültige Authentifizierungsinformationen, Cloud-Verbindung nicht verfügbar, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen', um die Authentifizierung erneut durchzuführen\",\n            \"invalid_device_cache\": \"Ungültige Gerätecache-Informationen, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen->Geräteliste aktualisieren', um den lokalen Gerätecache zu aktualisieren\",\n            \"invalid_cert_info\": \"Ungültiges Benutzerzertifikat, lokale zentrale Verbindung nicht verfügbar, bitte betreten Sie die Xiaomi Home-Integrationsseite und klicken Sie auf 'Optionen', um die Authentifizierung erneut durchzuführen\",\n            \"device_cloud_error\": \"Fehler beim Abrufen von Geräteinformationen aus der Cloud, bitte überprüfen Sie die lokale Netzwerkverbindung\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home-Integrationsfehler\",\n            \"xiaomi_home_error\": \"Fehler **{nick_name}({uid}, {cloud_server})** festgestellt, bitte betreten Sie die Optionen-Seite, um die Konfiguration erneut durchzuführen.\\n\\n**Fehlermeldung**: \\n{message}\",\n            \"device_list_changed_title\": \"Xiaomi Home-Geräteliste geändert\",\n            \"device_list_changed\": \"Änderung der Geräteinformationen **{nick_name}({uid}, {cloud_server})** festgestellt, bitte betreten Sie die Integrations-Optionen-Seite, klicken Sie auf 'Optionen->Geräteliste aktualisieren', um den lokalen Gerätecache zu aktualisieren.\\n\\nAktueller Netzwerkstatus: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} neue Geräte:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} Geräte nicht verfügbar:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} Geräte offline:** \\n{message}\",\n            \"network_status_online\": \"Online\",\n            \"network_status_offline\": \"Offline\",\n            \"central_state_changed_title\": \"Verbindungsstatus des Zentral-Gateways\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** Lokale Verbindungsstrecke des Zentral-Gateways: {conn_status}\",\n            \"central_state_connected\": \"verbunden\",\n            \"central_state_disconnected\": \"getrennt\",\n            \"device_exec_error\": \"Fehler bei der Ausführung\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Unbekannter Fehler\",\n            \"-10001\": \"Dienst nicht verfügbar\",\n            \"-10002\": \"Ungültiger Parameter\",\n            \"-10003\": \"Unzureichende Ressourcen\",\n            \"-10004\": \"Interner Fehler\",\n            \"-10005\": \"Unzureichende Berechtigungen\",\n            \"-10006\": \"Ausführungszeitüberschreitung\",\n            \"-10007\": \"Gerät offline oder nicht vorhanden\",\n            \"-10020\": \"Nicht autorisiert (OAuth2)\",\n            \"-10030\": \"Ungültiges Token (HTTP)\",\n            \"-10040\": \"Ungültiges Nachrichtenformat\",\n            \"-10050\": \"Ungültiges Zertifikat\",\n            \"-704000000\": \"Unbekannter Fehler\",\n            \"-704010000\": \"Nicht autorisiert (Gerät wurde möglicherweise gelöscht)\",\n            \"-704014006\": \"Gerätebeschreibung nicht gefunden\",\n            \"-704030013\": \"Eigenschaft nicht lesbar\",\n            \"-704030023\": \"Eigenschaft nicht beschreibbar\",\n            \"-704030033\": \"Eigenschaft nicht abonnierbar\",\n            \"-704040002\": \"Dienst existiert nicht\",\n            \"-704040003\": \"Eigenschaft existiert nicht\",\n            \"-704040004\": \"Ereignis existiert nicht\",\n            \"-704040005\": \"Aktion existiert nicht\",\n            \"-704040999\": \"Funktion nicht online\",\n            \"-704042001\": \"Gerät existiert nicht\",\n            \"-704042011\": \"Gerät offline\",\n            \"-704053036\": \"Gerätebetrieb zeitüberschreitung\",\n            \"-704053100\": \"Gerät kann diese Operation im aktuellen Zustand nicht ausführen\",\n            \"-704083036\": \"Gerätebetrieb zeitüberschreitung\",\n            \"-704090001\": \"Gerät existiert nicht\",\n            \"-704220008\": \"Ungültige ID\",\n            \"-704220025\": \"Aktionsparameteranzahl stimmt nicht überein\",\n            \"-704220035\": \"Aktionsparameterfehler\",\n            \"-704220043\": \"Eigenschaftswertfehler\",\n            \"-704222034\": \"Aktionsrückgabewertfehler\",\n            \"-705004000\": \"Unbekannter Fehler\",\n            \"-705004501\": \"Unbekannter Fehler\",\n            \"-705201013\": \"Eigenschaft nicht lesbar\",\n            \"-705201015\": \"Aktionsausführungsfehler\",\n            \"-705201023\": \"Eigenschaft nicht beschreibbar\",\n            \"-705201033\": \"Eigenschaft nicht abonnierbar\",\n            \"-706012000\": \"Unbekannter Fehler\",\n            \"-706012013\": \"Eigenschaft nicht lesbar\",\n            \"-706012015\": \"Aktionsausführungsfehler\",\n            \"-706012023\": \"Eigenschaft nicht beschreibbar\",\n            \"-706012033\": \"Eigenschaft nicht abonnierbar\",\n            \"-706012043\": \"Eigenschaftswertfehler\",\n            \"-706014006\": \"Gerätebeschreibung nicht gefunden\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/en.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"Devices\",\n            \"found_central_gateway\": \", Found Local Central Hub Gateway\",\n            \"without_room\": \"No room assigned\",\n            \"no_display\": \"Do not display\"\n        },\n        \"control_mode\": {\n            \"auto\": \"Auto\",\n            \"cloud\": \"Cloud\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"OR logic\",\n            \"and\": \"AND logic\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"Exclude\",\n            \"include\": \"Include\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi device\",\n            \"2\": \"Cloud device\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"Virtual device\",\n            \"6\": \"BLE\",\n            \"7\": \"Local AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"Other\",\n            \"10\": \"Function plug-in\",\n            \"11\": \"Cellular network\",\n            \"12\": \"Cable\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"Third-party cloud access\",\n            \"15\": \"Infrared remote control device\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"Virtual device group\",\n            \"18\": \"Gateway sub-device\",\n            \"19\": \"Security level gateway sub-device\",\n            \"22\": \"PLC\",\n            \"23\": \"Cable only\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+Cellular network\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"Do not synchronize\",\n            \"home_room\": \"Home Name and Room Name (Xiaomi Home Bedroom)\",\n            \"room\": \"Room Name (Bedroom)\",\n            \"home\": \"Home Name (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"Enable\",\n            \"disable\": \"Disable\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Text Sensor Entity\",\n            \"bool\": \"Binary Sensor Entity\"\n        },\n        \"device_state\": {\n            \"add\": \"Add\",\n            \"del\": \"Unavailable\",\n            \"offline\": \"Offline\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Notice]** Multiple network cards detected that may be connected to the same network. Please pay attention to the selection.\",\n            \"net_unavailable\": \"Interface unavailable\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Authentication Successful\",\n            \"content\": \"Please close this page and return to the account authentication page to click 'Next'.\",\n            \"button\": \"Close\"\n        },\n        \"fail\": {\n            \"title\": \"Authentication Failed\",\n            \"content\": \"{error_msg}, please close this page and return to the account authentication page to click the authentication link again.\",\n            \"button\": \"Close\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Invalid response parameters ('code' or 'state' field is empty)\",\n            \"-10101\": \"Passed-in 'state' field mismatch\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Authentication information is invalid, cloud link will be unavailable, please enter the Xiaomi Home integration page, click 'Options' to re-authenticate\",\n            \"invalid_device_cache\": \"Cache device information is abnormal, please enter the Xiaomi Home integration page, click 'Options->Update device list', update the local cache\",\n            \"invalid_cert_info\": \"Invalid user certificate, local central link will be unavailable, please enter the Xiaomi Home integration page, click 'Options' to re-authenticate\",\n            \"device_cloud_error\": \"An exception occurred when obtaining device information from the cloud, please check the local network connection\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home Integration Error\",\n            \"xiaomi_home_error\": \"Detected **{nick_name}({uid}, {cloud_server})** error, please enter the options page to reconfigure.\\n\\n**Error message**: \\n{message}\",\n            \"device_list_changed_title\": \"Xiaomi Home device list changes\",\n            \"device_list_changed\": \"Detected **{nick_name}({uid}, {cloud_server})** device information has changed, please enter the integration options page, click `Options->Update device list`, update local device information.\\n\\nCurrent network status: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} new devices:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} devices unavailable:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} devices offline:** \\n{message}\",\n            \"network_status_online\": \"Online\",\n            \"network_status_offline\": \"Offline\",\n            \"central_state_changed_title\": \"Central Hub Gateway Connection Status\",\n            \"central_state_changed\":\"**{nick_name}({uid}, {cloud_server})** local connection to Xiaomi central hub gateway: {conn_status}\",\n            \"central_state_connected\": \"Connected\",\n            \"central_state_disconnected\": \"Disconnected\",\n            \"device_exec_error\": \"Execution error\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Unknown error\",\n            \"-10001\": \"Service unavailable\",\n            \"-10002\": \"Invalid parameter\",\n            \"-10003\": \"Insufficient resources\",\n            \"-10004\": \"Internal error\",\n            \"-10005\": \"Insufficient permissions\",\n            \"-10006\": \"Execution timeout\",\n            \"-10007\": \"Device offline or does not exist\",\n            \"-10020\": \"Unauthorized (OAuth2)\",\n            \"-10030\": \"Invalid token (HTTP)\",\n            \"-10040\": \"Invalid message format\",\n            \"-10050\": \"Invalid certificate\",\n            \"-704000000\": \"Unknown error\",\n            \"-704010000\": \"Unauthorized (device may have been deleted)\",\n            \"-704014006\": \"Device description not found\",\n            \"-704030013\": \"Property not readable\",\n            \"-704030023\": \"Property not writable\",\n            \"-704030033\": \"Property not subscribable\",\n            \"-704040002\": \"Service does not exist\",\n            \"-704040003\": \"Property does not exist\",\n            \"-704040004\": \"Event does not exist\",\n            \"-704040005\": \"Action does not exist\",\n            \"-704040999\": \"Feature not online\",\n            \"-704042001\": \"Device does not exist\",\n            \"-704042011\": \"Device offline\",\n            \"-704053036\": \"Device operation timeout\",\n            \"-704053100\": \"Device cannot perform this operation in the current state\",\n            \"-704083036\": \"Device operation timeout\",\n            \"-704090001\": \"Device does not exist\",\n            \"-704220008\": \"Invalid ID\",\n            \"-704220025\": \"Action parameter count mismatch\",\n            \"-704220035\": \"Action parameter error\",\n            \"-704220043\": \"Property value error\",\n            \"-704222034\": \"Action return value error\",\n            \"-705004000\": \"Unknown error\",\n            \"-705004501\": \"Unknown error\",\n            \"-705201013\": \"Property not readable\",\n            \"-705201015\": \"Action execution error\",\n            \"-705201023\": \"Property not writable\",\n            \"-705201033\": \"Property not subscribable\",\n            \"-706012000\": \"Unknown error\",\n            \"-706012013\": \"Property not readable\",\n            \"-706012015\": \"Action execution error\",\n            \"-706012023\": \"Property not writable\",\n            \"-706012033\": \"Property not subscribable\",\n            \"-706012043\": \"Property value error\",\n            \"-706014006\": \"Device description not found\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/es.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"dispositivos\",\n            \"found_central_gateway\": \", se encontró la puerta de enlace central local\",\n            \"without_room\": \"Sin habitación asignada\",\n            \"no_display\": \"no mostrar\"\n        },\n        \"control_mode\": {\n            \"auto\": \"automático\",\n            \"cloud\": \"nube\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"lógica OR\",\n            \"and\": \"lógica AND\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"excluir\",\n            \"include\": \"incluir\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"dispositivo yunyi\",\n            \"2\": \"dispositivo de nube\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"dispositivo virtual\",\n            \"6\": \"BLE\",\n            \"7\": \"AP local\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"otro\",\n            \"10\": \"complemento de función\",\n            \"11\": \"red celular\",\n            \"12\": \"cable\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"acceso a la nube de terceros\",\n            \"15\": \"dispositivo de control remoto infrarrojo\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"grupo de dispositivos virtuales\",\n            \"18\": \"subdispositivo de puerta de enlace\",\n            \"19\": \"subdispositivo de puerta de enlace de nivel de seguridad\",\n            \"22\": \"PLC\",\n            \"23\": \"solo cable\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+red celular\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"no sincronizar\",\n            \"home_room\": \"nombre de la casa y nombre de la habitación (Xiaomi Home Dormitorio)\",\n            \"room\": \"nombre de la habitación (Dormitorio)\",\n            \"home\": \"nombre de la casa (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"habilitar\",\n            \"disable\": \"deshabilitar\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Entidad del sensor de texto\",\n            \"bool\": \"Entidad del sensor binario\"\n        },\n        \"device_state\": {\n            \"add\": \"agregar\",\n            \"del\": \"no disponible\",\n            \"offline\": \"fuera de línea\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Aviso]** Se detectaron varias tarjetas de red que pueden estar conectadas a la misma red. Por favor, preste atención a la selección.\",\n            \"net_unavailable\": \"Interfaz no disponible\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Autenticación exitosa\",\n            \"content\": \"Por favor, cierre esta página y regrese a la página de autenticación de la cuenta para hacer clic en 'Siguiente'.\",\n            \"button\": \"Cerrar\"\n        },\n        \"fail\": {\n            \"title\": \"Autenticación fallida\",\n            \"content\": \"{error_msg}, por favor, cierre esta página y regrese a la página de autenticación de la cuenta para hacer clic en el enlace de autenticación nuevamente.\",\n            \"button\": \"Cerrar\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Parámetros de respuesta inválidos ('code' o 'state' está vacío)\",\n            \"-10101\": \"El campo 'state' proporcionado no coincide\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"La información de autenticación es inválida, la conexión en la nube no estará disponible, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones' para volver a autenticar\",\n            \"invalid_device_cache\": \"La información de caché del dispositivo es anormal, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones -> Actualizar lista de dispositivos' para actualizar la información del dispositivo local\",\n            \"invalid_cert_info\": \"Certificado de usuario inválido, la conexión del centro local no estará disponible, por favor, vaya a la página de integración de Xiaomi Home, haga clic en 'Opciones' para volver a autenticar\",\n            \"device_cloud_error\": \"Error al obtener la información del dispositivo desde la nube, por favor, compruebe la conexión de red local\",\n            \"xiaomi_home_error_title\": \"Error de integración de Xiaomi Home\",\n            \"xiaomi_home_error\": \"Se detectó un error en **{nick_name}({uid}, {cloud_server})**, por favor, vaya a la página de opciones para reconfigurar.\\n\\n**Mensaje de error**: \\n{message}\",\n            \"device_list_changed_title\": \"Cambio en la lista de dispositivos de Xiaomi Home\",\n            \"device_list_changed\": \"Se detectó un cambio en la información del dispositivo **{nick_name}({uid}, {cloud_server})**, por favor, vaya a la página de integración, haga clic en 'Opciones -> Actualizar lista de dispositivos' para actualizar la información del dispositivo local.\\n\\nEstado actual de la red: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} nuevos dispositivos:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} dispositivos no disponibles:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} dispositivos sin conexión:** \\n{message}\",\n            \"network_status_online\": \"En línea\",\n            \"network_status_offline\": \"Desconectado\",\n            \"central_state_changed_title\": \"Estado de conexión de la puerta de enlace central\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** enlace de conexión local de la puerta de enlace central: {conn_status}\",\n            \"central_state_connected\": \"conectado\",\n            \"central_state_disconnected\": \"desconectado\",\n            \"device_exec_error\": \"Error de ejecución\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Error desconocido\",\n            \"-10001\": \"Servicio no disponible\",\n            \"-10002\": \"Parámetro inválido\",\n            \"-10003\": \"Recursos insuficientes\",\n            \"-10004\": \"Error interno\",\n            \"-10005\": \"Permisos insuficientes\",\n            \"-10006\": \"Tiempo de ejecución agotado\",\n            \"-10007\": \"Dispositivo fuera de línea o no existe\",\n            \"-10020\": \"No autorizado (OAuth2)\",\n            \"-10030\": \"Token inválido (HTTP)\",\n            \"-10040\": \"Formato de mensaje inválido\",\n            \"-10050\": \"Certificado inválido\",\n            \"-704000000\": \"Error desconocido\",\n            \"-704010000\": \"No autorizado (el dispositivo puede haber sido eliminado)\",\n            \"-704014006\": \"Descripción del dispositivo no encontrada\",\n            \"-704030013\": \"Propiedad no legible\",\n            \"-704030023\": \"Propiedad no escribible\",\n            \"-704030033\": \"Propiedad no suscribible\",\n            \"-704040002\": \"Servicio no existe\",\n            \"-704040003\": \"Propiedad no existe\",\n            \"-704040004\": \"Evento no existe\",\n            \"-704040005\": \"Acción no existe\",\n            \"-704040999\": \"Función no en línea\",\n            \"-704042001\": \"Dispositivo no existe\",\n            \"-704042011\": \"Dispositivo fuera de línea\",\n            \"-704053036\": \"Tiempo de operación del dispositivo agotado\",\n            \"-704053100\": \"El dispositivo no puede realizar esta operación en el estado actual\",\n            \"-704083036\": \"Tiempo de operación del dispositivo agotado\",\n            \"-704090001\": \"Dispositivo no existe\",\n            \"-704220008\": \"ID inválido\",\n            \"-704220025\": \"Número de parámetros de acción no coincide\",\n            \"-704220035\": \"Error de parámetro de acción\",\n            \"-704220043\": \"Error de valor de propiedad\",\n            \"-704222034\": \"Error de valor de retorno de acción\",\n            \"-705004000\": \"Error desconocido\",\n            \"-705004501\": \"Error desconocido\",\n            \"-705201013\": \"Propiedad no legible\",\n            \"-705201015\": \"Error de ejecución de acción\",\n            \"-705201023\": \"Propiedad no escribible\",\n            \"-705201033\": \"Propiedad no suscribible\",\n            \"-706012000\": \"Error desconocido\",\n            \"-706012013\": \"Propiedad no legible\",\n            \"-706012015\": \"Error de ejecución de acción\",\n            \"-706012023\": \"Propiedad no escribible\",\n            \"-706012033\": \"Propiedad no suscribible\",\n            \"-706012043\": \"Error de valor de propiedad\",\n            \"-706014006\": \"Descripción del dispositivo no encontrada\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/fr.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"appareils\",\n            \"found_central_gateway\": \", passerelle centrale locale trouvée\",\n            \"without_room\": \"Aucune pièce attribuée\",\n            \"no_display\": \"ne pas afficher\"\n        },\n        \"control_mode\": {\n            \"auto\": \"automatique\",\n            \"cloud\": \"cloud\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"ou logique\",\n            \"and\": \"et logique\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"exclure\",\n            \"include\": \"inclure\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"appareil yunyi\",\n            \"2\": \"appareil cloud\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"appareil virtuel\",\n            \"6\": \"BLE\",\n            \"7\": \"AP local\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"autre\",\n            \"10\": \"plug-in de fonction\",\n            \"11\": \"réseau cellulaire\",\n            \"12\": \"câble\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"accès cloud tiers\",\n            \"15\": \"appareil de télécommande infrarouge\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"groupe d'appareils virtuels\",\n            \"18\": \"sous-appareil de passerelle\",\n            \"19\": \"niveau de sécurité sous-appareil de passerelle\",\n            \"22\": \"PLC\",\n            \"23\": \"câble uniquement\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+réseau cellulaire\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"ne pas synchroniser\",\n            \"home_room\": \"nom de la maison et nom de la pièce (Xiaomi Home Chambre)\",\n            \"room\": \"nom de la pièce (Chambre)\",\n            \"home\": \"nom de la maison (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"activer\",\n            \"disable\": \"désactiver\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Entité du capteur de texte\",\n            \"bool\": \"Entité du capteur binaire\"\n        },\n        \"device_state\": {\n            \"add\": \"Ajouter\",\n            \"del\": \"Supprimer\",\n            \"offline\": \"hors ligne\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Remarque]** Plusieurs cartes réseau détectées qui peuvent être connectées au même réseau. Veuillez faire attention à la sélection.\",\n            \"net_unavailable\": \"Interface non disponible\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Authentification réussie\",\n            \"content\": \"Veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer sur 'Suivant'.\",\n            \"button\": \"Fermer\"\n        },\n        \"fail\": {\n            \"title\": \"Échec de l'authentification\",\n            \"content\": \"{error_msg}, veuillez fermer cette page et revenir à la page d'authentification du compte pour cliquer à nouveau sur le lien d'authentification.\",\n            \"button\": \"Fermer\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Paramètres de réponse invalides ('code' ou 'state' est vide)\",\n            \"-10101\": \"Le champ 'state' transmis ne correspond pas\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Informations d'authentification non valides, le lien cloud ne sera pas disponible, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur \\\"Options\\\" pour vous réauthentifier\",\n            \"invalid_device_cache\": \"Informations de cache de périphérique non valides, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur `Options-> Mettre à jour la liste des appareils`, pour mettre à jour les informations locales des appareils\",\n            \"invalid_cert_info\": \"Certificat utilisateur non valide, le lien central local ne sera pas disponible, veuillez accéder à la page d'intégration Xiaomi Home, cliquez sur \\\"Options\\\" pour vous réauthentifier\",\n            \"device_cloud_error\": \"Erreur lors de la récupération des informations de l'appareil à partir du cloud, veuillez vérifier la connexion réseau locale\",\n            \"xiaomi_home_error_title\": \"Erreur d'intégration Xiaomi Home\",\n            \"xiaomi_home_error\": \"Erreur détectée sur **{nick_name}({uid}, {cloud_server})**, veuillez accéder à la page d'options pour reconfigurer.\\n\\n**Message d'erreur**: \\n{message}\",\n            \"device_list_changed_title\": \"Changements dans la liste des appareils Xiaomi Home\",\n            \"device_list_changed\": \"Changements détectés sur **{nick_name}({uid}, {cloud_server})**, veuillez accéder à la page d'intégration, cliquez sur `Options-> Mettre à jour la liste des appareils`, pour mettre à jour les informations locales des appareils.\\n\\nÉtat actuel du réseau : {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} nouveaux appareils :** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} appareils non disponibles :** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} appareils hors ligne :** \\n{message}\",\n            \"network_status_online\": \"En ligne\",\n            \"network_status_offline\": \"Hors ligne\",\n            \"central_state_changed_title\": \"État de connexion de la passerelle centrale\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** liaison de connexion locale de la passerelle centrale : {conn_status}\",\n            \"central_state_connected\": \"connecté\",\n            \"central_state_disconnected\": \"déconnecté\",\n            \"device_exec_error\": \"Erreur d'exécution\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Erreur inconnue\",\n            \"-10001\": \"Service indisponible\",\n            \"-10002\": \"Paramètre invalide\",\n            \"-10003\": \"Ressources insuffisantes\",\n            \"-10004\": \"Erreur interne\",\n            \"-10005\": \"Permissions insuffisantes\",\n            \"-10006\": \"Délai d'exécution dépassé\",\n            \"-10007\": \"Appareil hors ligne ou n'existe pas\",\n            \"-10020\": \"Non autorisé (OAuth2)\",\n            \"-10030\": \"Jeton invalide (HTTP)\",\n            \"-10040\": \"Format de message invalide\",\n            \"-10050\": \"Certificat invalide\",\n            \"-704000000\": \"Erreur inconnue\",\n            \"-704010000\": \"Non autorisé (l'appareil peut avoir été supprimé)\",\n            \"-704014006\": \"Description de l'appareil introuvable\",\n            \"-704030013\": \"Propriété non lisible\",\n            \"-704030023\": \"Propriété non inscriptible\",\n            \"-704030033\": \"Propriété non abonnable\",\n            \"-704040002\": \"Service n'existe pas\",\n            \"-704040003\": \"Propriété n'existe pas\",\n            \"-704040004\": \"Événement n'existe pas\",\n            \"-704040005\": \"Action n'existe pas\",\n            \"-704040999\": \"Fonction non en ligne\",\n            \"-704042001\": \"Appareil n'existe pas\",\n            \"-704042011\": \"Appareil hors ligne\",\n            \"-704053036\": \"Délai d'opération de l'appareil dépassé\",\n            \"-704053100\": \"L'appareil ne peut pas effectuer cette opération dans l'état actuel\",\n            \"-704083036\": \"Délai d'opération de l'appareil dépassé\",\n            \"-704090001\": \"Appareil n'existe pas\",\n            \"-704220008\": \"ID invalide\",\n            \"-704220025\": \"Nombre de paramètres d'action ne correspond pas\",\n            \"-704220035\": \"Erreur de paramètre d'action\",\n            \"-704220043\": \"Erreur de valeur de propriété\",\n            \"-704222034\": \"Erreur de valeur de retour d'action\",\n            \"-705004000\": \"Erreur inconnue\",\n            \"-705004501\": \"Erreur inconnue\",\n            \"-705201013\": \"Propriété non lisible\",\n            \"-705201015\": \"Erreur d'exécution d'action\",\n            \"-705201023\": \"Propriété non inscriptible\",\n            \"-705201033\": \"Propriété non abonnable\",\n            \"-706012000\": \"Erreur inconnue\",\n            \"-706012013\": \"Propriété non lisible\",\n            \"-706012015\": \"Erreur d'exécution d'action\",\n            \"-706012023\": \"Propriété non inscriptible\",\n            \"-706012033\": \"Propriété non abonnable\",\n            \"-706012043\": \"Erreur de valeur de propriété\",\n            \"-706014006\": \"Description de l'appareil introuvable\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/it.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"Dispositivi\",\n            \"found_central_gateway\": \", Trovato Gateway Hub Centrale Locale\",\n            \"without_room\": \"Nessuna stanza assegnata\",\n            \"no_display\": \"Non visualizzare\"\n        },\n        \"control_mode\": {\n            \"auto\": \"Auto\",\n            \"cloud\": \"Cloud\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"Logica OR\",\n            \"and\": \"Logica AND\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"Escludere\",\n            \"include\": \"Includere\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"Dispositivo yunyi\",\n            \"2\": \"Dispositivo Cloud\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"Dispositivo Virtuale\",\n            \"6\": \"BLE\",\n            \"7\": \"AP Locale\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"Altro\",\n            \"10\": \"Plug-in Funzionale\",\n            \"11\": \"Rete Cellulare\",\n            \"12\": \"Cavo\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"Accesso cloud di terze parti\",\n            \"15\": \"Dispositivo di controllo remoto a infrarossi\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"Gruppo di Dispositivi Virtuali\",\n            \"18\": \"Sottodispositivo Gateway\",\n            \"19\": \"Sottodispositivo Gateway di livello di sicurezza\",\n            \"22\": \"PLC\",\n            \"23\": \"Solo Cavo\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+Rete Cellulare\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"Non sincronizzare\",\n            \"home_room\": \"Nome Casa e Nome Stanza (Camera da Letto Xiaomi Home)\",\n            \"room\": \"Nome Stanza (Camera da Letto)\",\n            \"home\": \"Nome Casa (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"Abilita\",\n            \"disable\": \"Disabilita\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Entità Sensore Testo\",\n            \"bool\": \"Entità Sensore Binario\"\n        },\n        \"device_state\": {\n            \"add\": \"Aggiungere\",\n            \"del\": \"Non disponibile\",\n            \"offline\": \"Offline\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Avviso]** Rilevate più schede di rete che potrebbero essere connesse alla stessa rete. Si prega di prestare attenzione alla selezione.\",\n            \"net_unavailable\": \"Interfaccia non disponibile\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Autenticazione Avvenuta\",\n            \"content\": \"Chiudere questa pagina e tornare alla pagina di autenticazione dell'account per fare clic su 'Avanti'.\",\n            \"button\": \"Chiudi\"\n        },\n        \"fail\": {\n            \"title\": \"Autenticazione Fallita\",\n            \"content\": \"{error_msg}, chiudere questa pagina e tornare alla pagina di autenticazione dell'account per fare clic nuovamente sul link di autenticazione.\",\n            \"button\": \"Chiudi\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Parametri di risposta non validi (i campi 'code' o 'state' sono vuoti)\",\n            \"-10101\": \"Campo 'state' passato non corrispondente\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Le informazioni di autenticazione non sono valide, il collegamento al cloud non sarà disponibile, si prega di accedere alla pagina di integrazione Xiaomi Home e cliccare 'Opzioni' per ri-autenticarsi\",\n            \"invalid_device_cache\": \"Le informazioni memorizzate nella cache del dispositivo sono anomale, si prega di accedere alla pagina di integrazione Xiaomi Home e cliccare 'Opzioni->Aggiorna elenco dispositivi' per aggiornare la cache locale\",\n            \"invalid_cert_info\": \"Certificato utente non valido, il collegamento centrale locale non sarà disponibile, si prega di accedere alla pagina di integrazione Xiaomi Home e cliccare 'Opzioni' per ri-autenticarsi\",\n            \"device_cloud_error\": \"Si è verificata un'eccezione durante l'ottenimento delle informazioni del dispositivo dal cloud, si prega di controllare la connessione alla rete locale\",\n            \"xiaomi_home_error_title\": \"Errore di Integrazione Xiaomi Home\",\n            \"xiaomi_home_error\": \"Rilevato errore per **{nick_name}({uid}, {cloud_server})**, si prega di accedere alla pagina delle opzioni per riconfigurare.\\n\\n**Messaggio di errore**: \\n{message}\",\n            \"device_list_changed_title\": \"Modifiche all'elenco dispositivi Xiaomi Home\",\n            \"device_list_changed\": \"Rilevato cambiamento nelle informazioni del dispositivo per **{nick_name}({uid}, {cloud_server})**, si prega di accedere alla pagina delle opzioni di integrazione, cliccare `Opzioni->Aggiorna elenco dispositivi` per aggiornare le informazioni locali dei dispositivi.\\n\\nStato corrente della rete: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} nuovi dispositivi:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} dispositivi non disponibili:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} dispositivi offline:** \\n{message}\",\n            \"network_status_online\": \"Online\",\n            \"network_status_offline\": \"Offline\",\n            \"central_state_changed_title\": \"Stato di connessione del gateway centrale\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** collegamento locale del gateway centrale: {conn_status}\",\n            \"central_state_connected\": \"connesso\",\n            \"central_state_disconnected\": \"disconnesso\",\n            \"device_exec_error\": \"Errore di esecuzione\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Errore sconosciuto\",\n            \"-10001\": \"Servizio non disponibile\",\n            \"-10002\": \"Parametro non valido\",\n            \"-10003\": \"Risorse insufficienti\",\n            \"-10004\": \"Errore interno\",\n            \"-10005\": \"Permessi insufficienti\",\n            \"-10006\": \"Timeout di esecuzione\",\n            \"-10007\": \"Dispositivo offline o inesistente\",\n            \"-10020\": \"Non autorizzato (OAuth2)\",\n            \"-10030\": \"Token non valido (HTTP)\",\n            \"-10040\": \"Formato messaggio non valido\",\n            \"-10050\": \"Certificato non valido\",\n            \"-704000000\": \"Errore sconosciuto\",\n            \"-704010000\": \"Non autorizzato (il dispositivo potrebbe essere stato eliminato)\",\n            \"-704014006\": \"Descrizione del dispositivo non trovata\",\n            \"-704030013\": \"Proprietà non leggibile\",\n            \"-704030023\": \"Proprietà non scrivibile\",\n            \"-704030033\": \"Proprietà non sottoscrivibile\",\n            \"-704040002\": \"Servizio inesistente\",\n            \"-704040003\": \"Proprietà inesistente\",\n            \"-704040004\": \"Evento inesistente\",\n            \"-704040005\": \"Azione inesistente\",\n            \"-704040999\": \"Funzione non online\",\n            \"-704042001\": \"Dispositivo inesistente\",\n            \"-704042011\": \"Dispositivo offline\",\n            \"-704053036\": \"Timeout operazione del dispositivo\",\n            \"-704053100\": \"Il dispositivo non può eseguire questa operazione nello stato attuale\",\n            \"-704083036\": \"Timeout operazione del dispositivo\",\n            \"-704090001\": \"Dispositivo inesistente\",\n            \"-704220008\": \"ID non valido\",\n            \"-704220025\": \"Conteggio parametri azione non corrispondente\",\n            \"-704220035\": \"Errore del parametro azione\",\n            \"-704220043\": \"Errore valore proprietà\",\n            \"-704222034\": \"Errore valore di ritorno dell'azione\",\n            \"-705004000\": \"Errore sconosciuto\",\n            \"-705004501\": \"Errore sconosciuto\",\n            \"-705201013\": \"Proprietà non leggibile\",\n            \"-705201015\": \"Errore di esecuzione azione\",\n            \"-705201023\": \"Proprietà non scrivibile\",\n            \"-705201033\": \"Proprietà non sottoscrivibile\",\n            \"-706012000\": \"Errore sconosciuto\",\n            \"-706012013\": \"Proprietà non leggibile\",\n            \"-706012015\": \"Errore di esecuzione azione\",\n            \"-706012023\": \"Proprietà non scrivibile\",\n            \"-706012033\": \"Proprietà non sottoscrivibile\",\n            \"-706012043\": \"Errore valore proprietà\",\n            \"-706014006\": \"Descrizione del dispositivo non trovata\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/ja.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"デバイス\",\n            \"found_central_gateway\": \"、ローカル中央ゲートウェイが見つかりました\",\n            \"without_room\": \"部屋が割り当てられていません\",\n            \"no_display\": \"表示しない\"\n        },\n        \"control_mode\": {\n            \"auto\": \"自動\",\n            \"cloud\": \"クラウド\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"ORロジック\",\n            \"and\": \"ANDロジック\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"除外\",\n            \"include\": \"含む\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyiデバイス\",\n            \"2\": \"クラウドデバイス\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"仮想デバイス\",\n            \"6\": \"BLE\",\n            \"7\": \"ローカルAP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"その他\",\n            \"10\": \"機能プラグイン\",\n            \"11\": \"セルラーネットワーク\",\n            \"12\": \"ケーブル\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"サードパーティクラウドアクセス\",\n            \"15\": \"赤外線リモコンデバイス\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"仮想デバイスグループ\",\n            \"18\": \"ゲートウェイサブデバイス\",\n            \"19\": \"セキュリティレベルゲートウェイサブデバイス\",\n            \"22\": \"PLC\",\n            \"23\": \"ケーブルのみ\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+セルラーネットワーク\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"同期しない\",\n            \"home_room\": \"家の名前と部屋の名前 （Xiaomi Home 寝室）\",\n            \"room\": \"部屋の名前（寝室）\",\n            \"home\": \"家の名前 （Xiaomi Home）\"\n        },\n        \"option_status\": {\n            \"enable\": \"有効\",\n            \"disable\": \"無効\"\n        },\n        \"binary_mode\": {\n            \"text\": \"テキストセンサーエンティティ\",\n            \"bool\": \"バイナリセンサーエンティティ\"\n        },\n        \"device_state\": {\n            \"add\": \"追加\",\n            \"del\": \"利用不可\",\n            \"offline\": \"オフライン\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[注意]** 複数のネットワークカードが同じネットワークに接続されている可能性があります。選択に注意してください。\",\n            \"net_unavailable\": \"インターフェースが利用できません\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"認証成功\",\n            \"content\": \"このページを閉じて、アカウント認証ページに戻り、「次へ」をクリックしてください。\",\n            \"button\": \"閉じる\"\n        },\n        \"fail\": {\n            \"title\": \"認証失敗\",\n            \"content\": \"{error_msg}、このページを閉じて、アカウント認証ページに戻り、再度認証リンクをクリックしてください。\",\n            \"button\": \"閉じる\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"無効な応答パラメータ（'code'または'state'フィールドが空です）\",\n            \"-10101\": \"渡された'state'フィールドが一致しません\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"認証情報が無効です。クラウドリンクは利用できません。Xiaomi Home統合ページに入り、[オプション]をクリックして再認証してください\",\n            \"invalid_device_cache\": \"キャッシュデバイス情報が異常です。Xiaomi Home統合ページに入り、[オプション->デバイスリストの更新]をクリックして、ローカルキャッシュを更新してください\",\n            \"invalid_cert_info\": \"無効なユーザー証明書です。ローカルセントラルリンクは利用できません。Xiaomi Home統合ページに入り、[オプション]をクリックして再認証してください\",\n            \"device_cloud_error\": \"クラウドからデバイス情報を取得する際に例外が発生しました。ローカルネットワーク接続を確認してください\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home統合エラー\",\n            \"xiaomi_home_error\": \"エラーが検出されました **{nick_name}({uid}, {cloud_server})** 、オプションページに入り再構成してください。\\n\\n**エラーメッセージ**: \\n{message}\",\n            \"device_list_changed_title\": \"Xiaomi Homeデバイスリストの変更\",\n            \"device_list_changed\": \"変更が検出されました **{nick_name}({uid}, {cloud_server})** デバイス情報が変更されました。統合オプションページに入り、`オプション->デバイスリストの更新`をクリックして、ローカルデバイス情報を更新してください。\\n\\n現在のネットワーク状態：{network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} 新しいデバイス:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} デバイスが利用できません:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} デバイスがオフライン:** \\n{message}\",\n            \"network_status_online\": \"オンライン\",\n            \"network_status_offline\": \"オフライン\",\n            \"central_state_changed_title\": \"中枢ゲートウェイ接続ステータス\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** 中枢ゲートウェイのローカル接続リンク: {conn_status}\",\n            \"central_state_connected\": \"接続済み\",\n            \"central_state_disconnected\": \"切断されました\",\n            \"device_exec_error\": \"実行エラー\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"不明なエラー\",\n            \"-10001\": \"サービス利用不可\",\n            \"-10002\": \"無効なパラメータ\",\n            \"-10003\": \"リソース不足\",\n            \"-10004\": \"内部エラー\",\n            \"-10005\": \"権限不足\",\n            \"-10006\": \"実行タイムアウト\",\n            \"-10007\": \"デバイスがオフラインまたは存在しない\",\n            \"-10020\": \"未認証（OAuth2）\",\n            \"-10030\": \"無効なトークン（HTTP）\",\n            \"-10040\": \"無効なメッセージ形式\",\n            \"-10050\": \"無効な証明書\",\n            \"-704000000\": \"不明なエラー\",\n            \"-704010000\": \"未認証（デバイスが削除された可能性があります）\",\n            \"-704014006\": \"デバイスの説明が見つかりません\",\n            \"-704030013\": \"プロパティが読み取れません\",\n            \"-704030023\": \"プロパティが書き込めません\",\n            \"-704030033\": \"プロパティが購読できません\",\n            \"-704040002\": \"サービスが存在しません\",\n            \"-704040003\": \"プロパティが存在しません\",\n            \"-704040004\": \"イベントが存在しません\",\n            \"-704040005\": \"アクションが存在しません\",\n            \"-704040999\": \"機能がオンラインではありません\",\n            \"-704042001\": \"デバイスが存在しません\",\n            \"-704042011\": \"デバイスがオフラインです\",\n            \"-704053036\": \"デバイス操作タイムアウト\",\n            \"-704053100\": \"デバイスが現在の状態でこの操作を実行できません\",\n            \"-704083036\": \"デバイス操作タイムアウト\",\n            \"-704090001\": \"デバイスが存在しません\",\n            \"-704220008\": \"無効なID\",\n            \"-704220025\": \"アクションパラメータの数が一致しません\",\n            \"-704220035\": \"アクションパラメータエラー\",\n            \"-704220043\": \"プロパティ値エラー\",\n            \"-704222034\": \"アクションの戻り値エラー\",\n            \"-705004000\": \"不明なエラー\",\n            \"-705004501\": \"不明なエラー\",\n            \"-705201013\": \"プロパティが読み取れません\",\n            \"-705201015\": \"アクション実行エラー\",\n            \"-705201023\": \"プロパティが書き込めません\",\n            \"-705201033\": \"プロパティが購読できません\",\n            \"-706012000\": \"不明なエラー\",\n            \"-706012013\": \"プロパティが読み取れません\",\n            \"-706012015\": \"アクション実行エラー\",\n            \"-706012023\": \"プロパティが書き込めません\",\n            \"-706012033\": \"プロパティが購読できません\",\n            \"-706012043\": \"プロパティ値エラー\",\n            \"-706014006\": \"デバイスの説明が見つかりません\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/nl.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"Apparaten\",\n            \"found_central_gateway\": \", Lokale centrale hub-gateway gevonden\",\n            \"without_room\": \"Niet toegewezen kamer\",\n            \"no_display\": \"Niet weergeven\"\n        },\n        \"control_mode\": {\n            \"auto\": \"Automatisch\",\n            \"cloud\": \"Cloud\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"OF-logica\",\n            \"and\": \"EN-logica\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"Uitsluiten\",\n            \"include\": \"Inclusief\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi-apparaat\",\n            \"2\": \"Cloudapparaat\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"Virtueel apparaat\",\n            \"6\": \"BLE\",\n            \"7\": \"Lokaal AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"Andere\",\n            \"10\": \"Functieplug-in\",\n            \"11\": \"Cellulair netwerk\",\n            \"12\": \"Kabel\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"Toegang van derden tot de cloud\",\n            \"15\": \"Infrarood afstandsbedieningsapparaat\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"Virtuele apparaatgroep\",\n            \"18\": \"Gateway sub-apparaat\",\n            \"19\": \"Beveiligingsniveau gateway sub-apparaat\",\n            \"22\": \"PLC\",\n            \"23\": \"Alleen kabel\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+Cellulair netwerk\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"Niet synchroniseren\",\n            \"home_room\": \"Huisnaam en Kamernaam (Xiaomi Home Slaapkamer)\",\n            \"room\": \"Kamernaam (Slaapkamer)\",\n            \"home\": \"Huisnaam (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"Inschakelen\",\n            \"disable\": \"Uitschakelen\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Tekstsensor-entiteit\",\n            \"bool\": \"Binairesensor-entiteit\"\n        },\n        \"device_state\": {\n            \"add\": \"Toevoegen\",\n            \"del\": \"Niet beschikbaar\",\n            \"offline\": \"Offline\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Let op]** Meerdere netwerkkaarten gedetecteerd die mogelijk zijn verbonden met hetzelfde netwerk. Let op bij de selectie.\",\n            \"net_unavailable\": \"Interface niet beschikbaar\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Authenticatie geslaagd\",\n            \"content\": \"Sluit deze pagina en ga terug naar de accountauthenticatiepagina om op 'Volgende' te klikken.\",\n            \"button\": \"Sluiten\"\n        },\n        \"fail\": {\n            \"title\": \"Authenticatie mislukt\",\n            \"content\": \"{error_msg}, sluit deze pagina en ga terug naar de accountauthenticatiepagina om opnieuw op de authenticatielink te klikken.\",\n            \"button\": \"Sluiten\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Ongeldige antwoordparameters ('code' of 'state' veld is leeg)\",\n            \"-10101\": \"Doorgegeven 'state' veld komt niet overeen\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Authenticatie-informatie is ongeldig, cloudverbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.\",\n            \"invalid_device_cache\": \"Cache van apparaatgegevens is abnormaal. Ga naar de Xiaomi Home-integratiepagina, klik op 'Opties->Apparaatlijst bijwerken' en werk de lokale cache bij.\",\n            \"invalid_cert_info\": \"Ongeldig gebruikerscertificaat, lokale centrale verbinding zal niet beschikbaar zijn. Ga naar de Xiaomi Home-integratiepagina en klik op 'Opties' om opnieuw te verifiëren.\",\n            \"device_cloud_error\": \"Er is een uitzondering opgetreden bij het ophalen van apparaatgegevens uit de cloud. Controleer de lokale netwerkverbinding.\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home-integratiefout\",\n            \"xiaomi_home_error\": \"Gedetecteerd **{nick_name}({uid}, {cloud_server})** fout, ga naar de optiespagina om opnieuw te configureren.\\n\\n**Foutmelding**: \\n{message}\",\n            \"device_list_changed_title\": \"Wijzigingen in Xiaomi Home-apparaatlijst\",\n            \"device_list_changed\": \"Gedetecteerd **{nick_name}({uid}, {cloud_server})** apparaatgegevens zijn gewijzigd. Ga naar de integratie-optiespagina, klik op `Opties->Apparaatlijst bijwerken` en werk lokale apparaatgegevens bij.\\n\\nHuidige netwerkstatus: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} nieuwe apparaten:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} apparaten niet beschikbaar:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} apparaten offline:** \\n{message}\",\n            \"network_status_online\": \"Online\",\n            \"network_status_offline\": \"Offline\",\n            \"central_state_changed_title\": \"Verbindingsstatus van centrale gateway\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** Lokale verbinding van centrale gateway: {conn_status}\",\n            \"central_state_connected\": \"verbonden\",\n            \"central_state_disconnected\": \"verbinding verbroken\",\n            \"device_exec_error\": \"Uitvoeringsfout\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Onbekende fout\",\n            \"-10001\": \"Service niet beschikbaar\",\n            \"-10002\": \"Ongeldige parameter\",\n            \"-10003\": \"Onvoldoende middelen\",\n            \"-10004\": \"Interne fout\",\n            \"-10005\": \"Onvoldoende machtigingen\",\n            \"-10006\": \"Uitvoeringstijd verstreken\",\n            \"-10007\": \"Apparaat offline of bestaat niet\",\n            \"-10020\": \"Niet geautoriseerd (OAuth2)\",\n            \"-10030\": \"Ongeldig token (HTTP)\",\n            \"-10040\": \"Ongeldig berichtformaat\",\n            \"-10050\": \"Ongeldig certificaat\",\n            \"-704000000\": \"Onbekende fout\",\n            \"-704010000\": \"Niet geautoriseerd (apparaat kan zijn verwijderd)\",\n            \"-704014006\": \"Apparaatbeschrijving niet gevonden\",\n            \"-704030013\": \"Eigenschap niet leesbaar\",\n            \"-704030023\": \"Eigenschap niet schrijfbaar\",\n            \"-704030033\": \"Eigenschap niet abonneerbaar\",\n            \"-704040002\": \"Service bestaat niet\",\n            \"-704040003\": \"Eigenschap bestaat niet\",\n            \"-704040004\": \"Gebeurtenis bestaat niet\",\n            \"-704040005\": \"Actie bestaat niet\",\n            \"-704040999\": \"Functie niet online\",\n            \"-704042001\": \"Apparaat bestaat niet\",\n            \"-704042011\": \"Apparaat offline\",\n            \"-704053036\": \"Apparaatbedieningstijd verstreken\",\n            \"-704053100\": \"Apparaat kan deze handeling niet uitvoeren in de huidige staat\",\n            \"-704083036\": \"Apparaatbedieningstijd verstreken\",\n            \"-704090001\": \"Apparaat bestaat niet\",\n            \"-704220008\": \"Ongeldige ID\",\n            \"-704220025\": \"Aantal actieparameters komt niet overeen\",\n            \"-704220035\": \"Fout in actieparameter\",\n            \"-704220043\": \"Fout in eigenschapswaarde\",\n            \"-704222034\": \"Fout in retourwaarde actie\",\n            \"-705004000\": \"Onbekende fout\",\n            \"-705004501\": \"Onbekende fout\",\n            \"-705201013\": \"Eigenschap niet leesbaar\",\n            \"-705201015\": \"Fout bij uitvoeren van actie\",\n            \"-705201023\": \"Eigenschap niet schrijfbaar\",\n            \"-705201033\": \"Eigenschap niet abonneerbaar\",\n            \"-706012000\": \"Onbekende fout\",\n            \"-706012013\": \"Eigenschap niet leesbaar\",\n            \"-706012015\": \"Fout bij uitvoeren van actie\",\n            \"-706012023\": \"Eigenschap niet schrijfbaar\",\n            \"-706012033\": \"Eigenschap niet abonneerbaar\",\n            \"-706012043\": \"Fout in eigenschapswaarde\",\n            \"-706014006\": \"Apparaatbeschrijving niet gevonden\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/pt-BR.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"dispositivos\",\n            \"found_central_gateway\": \"encontrado o gateway central local\",\n            \"without_room\": \"sem quarto atribuído\",\n            \"no_display\": \"não exibir\"\n        },\n        \"control_mode\": {\n            \"auto\": \"automático\",\n            \"cloud\": \"nuvem\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"lógica OU\",\n            \"and\": \"lógica E\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"excluir\",\n            \"include\": \"incluir\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"dispositivo yunyi\",\n            \"2\": \"dispositivo de nuvem\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"dispositivo virtual\",\n            \"6\": \"BLE\",\n            \"7\": \"AP local\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"outro\",\n            \"10\": \"plug-in de função\",\n            \"11\": \"rede celular\",\n            \"12\": \"cabo\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"acesso à nuvem de terceiros\",\n            \"15\": \"dispositivo de controle remoto infravermelho\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"grupo de dispositivos virtuais\",\n            \"18\": \"subdispositivo de gateway\",\n            \"19\": \"subdispositivo de gateway de nível de segurança\",\n            \"22\": \"PLC\",\n            \"23\": \"somente cabo\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+rede celular\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"não sincronizado\",\n            \"home_room\": \"Nome da casa e nome do quarto (Xiaomi Home Quarto)\",\n            \"room\": \"Nome do quarto (Quarto)\",\n            \"home\": \"Nome da casa (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"habilitado\",\n            \"disable\": \"desabilitado\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Entidade do sensor de texto\",\n            \"bool\": \"Entidade do sensor binário\"\n        },\n        \"device_state\": {\n            \"add\": \"adicionar\",\n            \"del\": \"indisponível\",\n            \"offline\": \"offline\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Aviso]** Detectado múltiplas interfaces de rede que podem estar conectando à mesma rede, por favor, selecione a correta.\",\n            \"net_unavailable\": \"Interface indisponível\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Autenticação bem-sucedida\",\n            \"content\": \"Por favor, feche esta página e volte para a página de autenticação da conta para clicar em 'Próximo'.\",\n            \"button\": \"Fechar\"\n        },\n        \"fail\": {\n            \"title\": \"Falha na autenticação\",\n            \"content\": \"{error_msg}, por favor, feche esta página e volte para a página de autenticação da conta para clicar no link de autenticação novamente.\",\n            \"button\": \"Fechar\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Parâmetros de resposta inválidos ('code' ou 'state' está vazio)\",\n            \"-10101\": \"O campo 'state' fornecido não corresponde\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Informações de autenticação inválidas, a conexão com a nuvem estará indisponível. Vá para a página de integração do Xiaomi Home e clique em 'Opções' para reautenticar.\",\n            \"invalid_device_cache\": \"Informações de dispositivo no cache inválidas. Vá para a página de integração do Xiaomi Home e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.\",\n            \"invalid_cert_info\": \"Certificado de usuário inválido. A conexão local do gateway central estará indisponível. Vá para a página de integração do Xiaomi Home e clique em 'Opções' para reautenticar.\",\n            \"device_cloud_error\": \"Erro ao obter informações do dispositivo da nuvem. Verifique a conexão da rede local.\",\n            \"xiaomi_home_error_title\": \"Erro de Integração do Xiaomi Home\",\n            \"xiaomi_home_error\": \"Erro detectado em **{nick_name}({uid}, {cloud_server})**. Vá para a página de opções para reconfigurar.\\n\\n**Erro**: \\n{message}\",\n            \"device_list_changed_title\": \"Mudança na lista de dispositivos do Xiaomi Home\",\n            \"device_list_changed\": \"Detectado que as informações do dispositivo **{nick_name}({uid}, {cloud_server})** mudaram. Vá para a página de integração e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.\\n\\nStatus atual da rede: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} dispositivos novos**: \\n{message}\",\n            \"device_list_del\": \"\\n**{count} dispositivos não disponíveis**: \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} dispositivos offline**: \\n{message}\",\n            \"network_status_online\": \"online\",\n            \"network_status_offline\": \"offline\",\n            \"central_state_changed_title\": \"Status de conexão do gateway central\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** conexão local do gateway central: {conn_status}\",\n            \"central_state_connected\": \"conectado\",\n            \"central_state_disconnected\": \"desconectado\",\n            \"device_exec_error\": \"Erro na execução\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Erro desconhecido\",\n            \"-10001\": \"Serviço indisponível\",\n            \"-10002\": \"Parâmetro inválido\",\n            \"-10003\": \"Recursos insuficientes\",\n            \"-10004\": \"Erro interno\",\n            \"-10005\": \"Permissões insuficientes\",\n            \"-10006\": \"Execução expirada\",\n            \"-10007\": \"Dispositivo offline ou inexistente\",\n            \"-10020\": \"OAuth2 não autorizado\",\n            \"-10030\": \"Token inválido (HTTP)\",\n            \"-10040\": \"Formato de mensagem inválido\",\n            \"-10050\": \"Certificado inválido\",\n            \"-704000000\": \"Erro desconhecido\",\n            \"-704010000\": \"Não autorizado (o dispositivo pode ter sido excluído)\",\n            \"-704014006\": \"Descrição do dispositivo não encontrada\",\n            \"-704030013\": \"Propriedade não pode ser lida\",\n            \"-704030023\": \"Propriedade não pode ser escrita\",\n            \"-704030033\": \"Propriedade não pode ser assinada\",\n            \"-704040002\": \"Serviço inexistente\",\n            \"-704040003\": \"Propriedade inexistente\",\n            \"-704040004\": \"Evento inexistente\",\n            \"-704040005\": \"Ação inexistente\",\n            \"-704040999\": \"Funcionalidade não lançada\",\n            \"-704042001\": \"Dispositivo inexistente\",\n            \"-704042011\": \"Dispositivo offline\",\n            \"-704053036\": \"Tempo de operação do dispositivo expirado\",\n            \"-704053100\": \"Dispositivo não pode executar esta operação no estado atual\",\n            \"-704083036\": \"Tempo de operação do dispositivo expirado\",\n            \"-704090001\": \"Dispositivo inexistente\",\n            \"-704220008\": \"ID inválido\",\n            \"-704220025\": \"Número de parâmetros de ação incompatível\",\n            \"-704220035\": \"Parâmetro de ação incorreto\",\n            \"-704220043\": \"Valor da propriedade incorreto\",\n            \"-704222034\": \"Valor de retorno de ação incorreto\",\n            \"-705004000\": \"Erro desconhecido\",\n            \"-705004501\": \"Erro desconhecido\",\n            \"-705201013\": \"Propriedade não pode ser lida\",\n            \"-705201015\": \"Erro na execução da ação\",\n            \"-705201023\": \"Propriedade não pode ser escrita\",\n            \"-705201033\": \"Propriedade não pode ser assinada\",\n            \"-706012000\": \"Erro desconhecido\",\n            \"-706012013\": \"Propriedade não pode ser lida\",\n            \"-706012015\": \"Erro na execução da ação\",\n            \"-706012023\": \"Propriedade não pode ser escrita\",\n            \"-706012033\": \"Propriedade não pode ser assinada\",\n            \"-706012043\": \"Valor da propriedade incorreto\",\n            \"-706014006\": \"Descrição do dispositivo não encontrada\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/pt.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"dispositivos\",\n            \"found_central_gateway\": \", encontrou a central de gateway local\",\n            \"without_room\": \"Sem quarto atribuído\",\n            \"no_display\": \"Não exibir\"\n        },\n        \"control_mode\": {\n            \"auto\": \"Automático\",\n            \"cloud\": \"Nuvem\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"Ou lógica\",\n            \"and\": \"E lógica\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"Excluir\",\n            \"include\": \"Incluir\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"dispositivo yunyi\",\n            \"2\": \"dispositivo de nuvem\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"dispositivo virtual\",\n            \"6\": \"BLE\",\n            \"7\": \"AP local\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"outro\",\n            \"10\": \"plug-in de função\",\n            \"11\": \"rede celular\",\n            \"12\": \"cabo\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"acesso à nuvem de terceiros\",\n            \"15\": \"dispositivo de controle remoto infravermelho\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"grupo de dispositivos virtuais\",\n            \"18\": \"subdispositivo de gateway\",\n            \"19\": \"subdispositivo de gateway de nível de segurança\",\n            \"22\": \"PLC\",\n            \"23\": \"somente cabo\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+rede celular\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"Não sincronizar\",\n            \"home_room\": \"Nome da casa e Nome do quarto (Xiaomi Home Quarto)\",\n            \"room\": \"Nome do quarto (Quarto)\",\n            \"home\": \"Nome da casa (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"Habilitar\",\n            \"disable\": \"Desabilitar\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Entidade do sensor de texto\",\n            \"bool\": \"Entidade do sensor binário\"\n        },\n        \"device_state\": {\n            \"add\": \"Adicionar\",\n            \"del\": \"Indisponível\",\n            \"offline\": \"Offline\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Aviso]** Detectado que várias interfaces podem estar conectadas à mesma rede, escolha com cuidado.\",\n            \"net_unavailable\": \"Interface indisponível\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Autenticação bem-sucedida\",\n            \"content\": \"Por favor, feche esta página e volte para a página de autenticação da conta para clicar em 'Seguinte'.\",\n            \"button\": \"Fechar\"\n        },\n        \"fail\": {\n            \"title\": \"Falha na autenticação\",\n            \"content\": \"{error_msg}, por favor, feche esta página e volte para a página de autenticação da conta para clicar no link de autenticação novamente.\",\n            \"button\": \"Fechar\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Parâmetros de resposta inválidos ('code' ou 'state' está vazio)\",\n            \"-10101\": \"O campo 'state' fornecido não corresponde\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Informações de autenticação inválidas, a conexão na nuvem ficará indisponível. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções' para autenticar novamente.\",\n            \"invalid_device_cache\": \"Erro no cache de informações do dispositivo. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.\",\n            \"invalid_cert_info\": \"Certificado de usuário inválido, a conexão com a central local ficará indisponível. Por favor, acesse a página de integração do Xiaomi Home e clique em 'Opções' para autenticar novamente.\",\n            \"device_cloud_error\": \"Erro ao obter informações do dispositivo na nuvem. Verifique a conexão de rede local.\",\n            \"xiaomi_home_error_title\": \"Erro de integração do Xiaomi Home\",\n            \"xiaomi_home_error\": \"Detectado erro em **{nick_name}({uid}, {cloud_server})**. Por favor, acesse a página de opções para reconfigurar.\\n\\n**Informação do erro**: \\n{message}\",\n            \"device_list_changed_title\": \"Mudança na lista de dispositivos do Xiaomi Home\",\n            \"device_list_changed\": \"Detectada alteração nas informações do dispositivo de **{nick_name}({uid}, {cloud_server})**. Por favor, acesse a página de opções de integração e clique em 'Opções -> Atualizar lista de dispositivos' para atualizar as informações locais.\\n\\nStatus atual da rede: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} novos dispositivos**: \\n{message}\",\n            \"device_list_del\": \"\\n**{count} dispositivos indisponíveis**: \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} dispositivos offline**: \\n{message}\",\n            \"network_status_online\": \"Online\",\n            \"network_status_offline\": \"Offline\",\n            \"central_state_changed_title\": \"Estado da ligação do gateway central\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** ligação local do gateway central: {conn_status}\",\n            \"central_state_connected\": \"ligado\",\n            \"central_state_disconnected\": \"desligado\",\n            \"device_exec_error\": \"Erro de execução\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Erro desconhecido\",\n            \"-10001\": \"Serviço indisponível\",\n            \"-10002\": \"Parâmetro inválido\",\n            \"-10003\": \"Recursos insuficientes\",\n            \"-10004\": \"Erro interno\",\n            \"-10005\": \"Permissão negada\",\n            \"-10006\": \"Tempo limite de execução\",\n            \"-10007\": \"Dispositivo offline ou inexistente\",\n            \"-10020\": \"Não autorizado (OAuth2)\",\n            \"-10030\": \"Token inválido (HTTP)\",\n            \"-10040\": \"Formato de mensagem inválido\",\n            \"-10050\": \"Certificado inválido\",\n            \"-704000000\": \"Erro desconhecido\",\n            \"-704010000\": \"Não autorizado (o dispositivo pode ter sido removido)\",\n            \"-704014006\": \"Descrição do dispositivo não encontrada\",\n            \"-704030013\": \"Propriedade não legível\",\n            \"-704030023\": \"Propriedade não gravável\",\n            \"-704030033\": \"Propriedade não subscritível\",\n            \"-704040002\": \"Serviço inexistente\",\n            \"-704040003\": \"Propriedade inexistente\",\n            \"-704040004\": \"Evento inexistente\",\n            \"-704040005\": \"Ação inexistente\",\n            \"-704040999\": \"Funcionalidade não disponível\",\n            \"-704042001\": \"Dispositivo inexistente\",\n            \"-704042011\": \"Dispositivo offline\",\n            \"-704053036\": \"Tempo limite de operação do dispositivo\",\n            \"-704053100\": \"O dispositivo não pode executar esta operação no estado atual\",\n            \"-704083036\": \"Tempo limite de operação do dispositivo\",\n            \"-704090001\": \"Dispositivo inexistente\",\n            \"-704220008\": \"ID inválido\",\n            \"-704220025\": \"Número de parâmetros da ação não corresponde\",\n            \"-704220035\": \"Erro nos parâmetros da ação\",\n            \"-704220043\": \"Valor de propriedade inválido\",\n            \"-704222034\": \"Erro no valor de retorno da ação\",\n            \"-705004000\": \"Erro desconhecido\",\n            \"-705004501\": \"Erro desconhecido\",\n            \"-705201013\": \"Propriedade não legível\",\n            \"-705201015\": \"Erro na execução da ação\",\n            \"-705201023\": \"Propriedade não gravável\",\n            \"-705201033\": \"Propriedade não subscritível\",\n            \"-706012000\": \"Erro desconhecido\",\n            \"-706012013\": \"Propriedade não legível\",\n            \"-706012015\": \"Erro na execução da ação\",\n            \"-706012023\": \"Propriedade não gravável\",\n            \"-706012033\": \"Propriedade não subscritível\",\n            \"-706012043\": \"Valor de propriedade inválido\",\n            \"-706014006\": \"Descrição do dispositivo não encontrada\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/ru.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"устройства\",\n            \"found_central_gateway\": \", найден локальный центральный шлюз\",\n            \"without_room\": \"без комнаты\",\n            \"no_display\": \"не отображать\"\n        },\n        \"control_mode\": {\n            \"auto\": \"автоматический\",\n            \"cloud\": \"облако\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"логика ИЛИ\",\n            \"and\": \"логика И\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"исключить\",\n            \"include\": \"включить\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi устройство\",\n            \"2\": \"Облачное устройство\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"Виртуальное устройство\",\n            \"6\": \"BLE\",\n            \"7\": \"Локальный AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"Другое\",\n            \"10\": \"Плагин функции\",\n            \"11\": \"Сотовая сеть\",\n            \"12\": \"Кабель\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"Доступ к стороннему облаку\",\n            \"15\": \"Устройство с ИК-пультом дистанционного управления\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"Группа виртуальных устройств\",\n            \"18\": \"Подустройство шлюза\",\n            \"19\": \"Подустройство шлюза уровня безопасности\",\n            \"22\": \"PLC\",\n            \"23\": \"Только кабель\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+Сотовая сеть\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"не синхронизировать\",\n            \"home_room\": \"название дома и название комнаты (Xiaomi Home Спальня)\",\n            \"room\": \"название комнаты (Спальня)\",\n            \"home\": \"название дома (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"Включить\",\n            \"disable\": \"Отключить\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Сущность текстового датчика\",\n            \"bool\": \"Сущность бинарного датчика\"\n        },\n        \"device_state\": {\n            \"add\": \"Добавить\",\n            \"del\": \"Недоступно\",\n            \"offline\": \"Не в сети\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Уведомление]** Обнаружено несколько сетевых карт, которые могут быть подключены к одной и той же сети. Пожалуйста, обратите внимание на выбор.\",\n            \"net_unavailable\": \"Интерфейс недоступен\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Аутентификация успешна\",\n            \"content\": \"Пожалуйста, закройте эту страницу и вернитесь на страницу аутентификации учетной записи, чтобы нажать 'Далее'.\",\n            \"button\": \"Закрыть\"\n        },\n        \"fail\": {\n            \"title\": \"Аутентификация не удалась\",\n            \"content\": \"{error_msg}, пожалуйста, закройте эту страницу и вернитесь на страницу аутентификации учетной записи, чтобы снова нажать на ссылку аутентификации.\",\n            \"button\": \"Закрыть\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Недействительные параметры ответа ('code' или 'state' поле пусто)\",\n            \"-10101\": \"Переданное поле 'state' не совпадает\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Информация об аутентификации недействительна, облако будет недоступно, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции' для повторной аутентификации\",\n            \"invalid_device_cache\": \"Кэш информации об устройстве ненормальный, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции->Обновить список устройств', обновите локальный кэш\",\n            \"invalid_cert_info\": \"Недействительный пользовательский сертификат, локальное центральное соединение будет недоступно, пожалуйста, войдите на страницу интеграции Xiaomi Home, нажмите 'Опции' для повторной аутентификации\",\n            \"device_cloud_error\": \"При получении информации об устройстве из облака произошло исключение, пожалуйста, проверьте локальное сетевое соединение\",\n            \"xiaomi_home_error_title\": \"Ошибка интеграции Xiaomi Home\",\n            \"xiaomi_home_error\": \"Обнаружена ошибка **{nick_name}({uid}, {cloud_server})**, пожалуйста, войдите на страницу опций для повторной настройки.\\n\\n**Сообщение об ошибке**: \\n{message}\",\n            \"device_list_changed_title\": \"Изменения в списке устройств Xiaomi Home\",\n            \"device_list_changed\": \"Обнаружены изменения в информации об устройствах **{nick_name}({uid}, {cloud_server})**, пожалуйста, войдите на страницу интеграции, нажмите `Опции->Обновить список устройств`, обновите локальную информацию об устройствах.\\n\\nТекущий статус сети: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} новых устройств:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} устройств недоступно:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} устройств недоступно:** \\n{message}\",\n            \"network_status_online\": \"В сети\",\n            \"network_status_offline\": \"Не в сети\",\n            \"central_state_changed_title\": \"Статус подключения центрального шлюза\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** локальное подключение центрального шлюза: {conn_status}\",\n            \"central_state_connected\": \"подключено\",\n            \"central_state_disconnected\": \"разъединено\",\n            \"device_exec_error\": \"Ошибка выполнения\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Неизвестная ошибка\",\n            \"-10001\": \"Сервис недоступен\",\n            \"-10002\": \"Недопустимый параметр\",\n            \"-10003\": \"Недостаточно ресурсов\",\n            \"-10004\": \"Внутренняя ошибка\",\n            \"-10005\": \"Недостаточно прав\",\n            \"-10006\": \"Тайм-аут выполнения\",\n            \"-10007\": \"Устройство не в сети или не существует\",\n            \"-10020\": \"Неавторизовано (OAuth2)\",\n            \"-10030\": \"Недействительный токен (HTTP)\",\n            \"-10040\": \"Недопустимый формат сообщения\",\n            \"-10050\": \"Недействительный сертификат\",\n            \"-704000000\": \"Неизвестная ошибка\",\n            \"-704010000\": \"Неавторизовано (устройство могло быть удалено)\",\n            \"-704014006\": \"Описание устройства не найдено\",\n            \"-704030013\": \"Свойство не читается\",\n            \"-704030023\": \"Свойство не записывается\",\n            \"-704030033\": \"Свойство не подписывается\",\n            \"-704040002\": \"Сервис не существует\",\n            \"-704040003\": \"Свойство не существует\",\n            \"-704040004\": \"Событие не существует\",\n            \"-704040005\": \"Действие не существует\",\n            \"-704040999\": \"Функция не в сети\",\n            \"-704042001\": \"Устройство не существует\",\n            \"-704042011\": \"Устройство не в сети\",\n            \"-704053036\": \"Тайм-аут операции устройства\",\n            \"-704053100\": \"Устройство не может выполнить эту операцию в текущем состоянии\",\n            \"-704083036\": \"Тайм-аут операции устройства\",\n            \"-704090001\": \"Устройство не существует\",\n            \"-704220008\": \"Недействительный ID\",\n            \"-704220025\": \"Несоответствие количества параметров действия\",\n            \"-704220035\": \"Ошибка параметра действия\",\n            \"-704220043\": \"Ошибка значения свойства\",\n            \"-704222034\": \"Ошибка возвращаемого значения действия\",\n            \"-705004000\": \"Неизвестная ошибка\",\n            \"-705004501\": \"Неизвестная ошибка\",\n            \"-705201013\": \"Свойство не читается\",\n            \"-705201015\": \"Ошибка выполнения действия\",\n            \"-705201023\": \"Свойство не записывается\",\n            \"-705201033\": \"Свойство не подписывается\",\n            \"-706012000\": \"Неизвестная ошибка\",\n            \"-706012013\": \"Свойство не читается\",\n            \"-706012015\": \"Ошибка выполнения действия\",\n            \"-706012023\": \"Свойство не записывается\",\n            \"-706012033\": \"Свойство не подписывается\",\n            \"-706012043\": \"Ошибка значения свойства\",\n            \"-706014006\": \"Описание устройства не найдено\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/tr.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"Cihazlar\",\n            \"found_central_gateway\": \", Yerel Merkezi Hub Ağ Geçidi Bulundu\",\n            \"without_room\": \"Oda atanmamış\",\n            \"no_display\": \"Gösterme\"\n        },\n        \"control_mode\": {\n            \"auto\": \"Otomatik\",\n            \"cloud\": \"Bulut\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"VEYA mantığı\",\n            \"and\": \"VE mantığı\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"Hariç tut\",\n            \"include\": \"Dahil et\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi cihazı\",\n            \"2\": \"Bulut cihazı\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"Sanal cihaz\",\n            \"6\": \"BLE\",\n            \"7\": \"Yerel AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"Diğer\",\n            \"10\": \"İşlev eklentisi\",\n            \"11\": \"Hücresel ağ\",\n            \"12\": \"Kablo\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"Üçüncü taraf bulut erişimi\",\n            \"15\": \"Kızılötesi uzaktan kumanda cihazı\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"Sanal cihaz grubu\",\n            \"18\": \"Ağ geçidi alt cihazı\",\n            \"19\": \"Güvenlik seviyesi ağ geçidi alt cihazı\",\n            \"22\": \"PLC\",\n            \"23\": \"Yalnızca kablo\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+Hücresel ağ\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"Senkronize etme\",\n            \"home_room\": \"Ev Adı ve Oda Adı (Xiaomi Home Yatak Odası)\",\n            \"room\": \"Oda Adı (Yatak Odası)\",\n            \"home\": \"Ev Adı (Xiaomi Home)\"\n        },\n        \"option_status\": {\n            \"enable\": \"Etkinleştir\",\n            \"disable\": \"Devre Dışı Bırak\"\n        },\n        \"binary_mode\": {\n            \"text\": \"Metin Sensör Varlığı\",\n            \"bool\": \"İkili Sensör Varlığı\"\n        },\n        \"device_state\": {\n            \"add\": \"Ekle\",\n            \"del\": \"Kullanılamıyor\",\n            \"offline\": \"Çevrimdışı\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[Bildirim]** Aynı ağa bağlı olabilecek birden fazla ağ kartı algılandı. Lütfen seçime dikkat edin.\",\n            \"net_unavailable\": \"Arayüz kullanılamıyor\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"Kimlik Doğrulama Başarılı\",\n            \"content\": \"Lütfen bu sayfayı kapatın ve 'İleri'ye tıklamak için hesap kimlik doğrulama sayfasına geri dönün.\",\n            \"button\": \"Kapat\"\n        },\n        \"fail\": {\n            \"title\": \"Kimlik Doğrulama Başarısız\",\n            \"content\": \"{error_msg}, lütfen bu sayfayı kapatın ve kimlik doğrulama bağlantısına tekrar tıklamak için hesap kimlik doğrulama sayfasına geri dönün.\",\n            \"button\": \"Kapat\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"Geçersiz yanıt parametreleri ('code' veya 'state' alanı boş)\",\n            \"-10101\": \"Geçirilen 'state' alanı uyumsuz\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"Kimlik doğrulama bilgileri geçersiz, bulut bağlantısı kullanılamayacak, lütfen Xiaomi Home entegrasyon sayfasına girin, yeniden kimlik doğrulaması yapmak için 'Seçenekler'e tıklayın\",\n            \"invalid_device_cache\": \"Önbellek cihaz bilgileri anormal, lütfen Xiaomi Home entegrasyon sayfasına girin, yerel önbelleği güncellemek için 'Seçenekler->Cihaz listesini güncelle'ye tıklayın\",\n            \"invalid_cert_info\": \"Geçersiz kullanıcı sertifikası, yerel merkezi bağlantı kullanılamayacak, lütfen Xiaomi Home entegrasyon sayfasına girin, yeniden kimlik doğrulaması yapmak için 'Seçenekler'e tıklayın\",\n            \"device_cloud_error\": \"Buluttan cihaz bilgileri alınırken bir istisna oluştu, lütfen yerel ağ bağlantısını kontrol edin\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home Entegrasyon Hatası\",\n            \"xiaomi_home_error\": \"**{nick_name}({uid}, {cloud_server})** hatası algılandı, lütfen yeniden yapılandırmak için seçenekler sayfasına girin.\\n\\n**Hata mesajı**: \\n{message}\",\n            \"device_list_changed_title\": \"Xiaomi Home cihaz listesi değişiklikleri\",\n            \"device_list_changed\": \"**{nick_name}({uid}, {cloud_server})** cihaz bilgilerinin değiştiği algılandı, lütfen entegrasyon seçenekleri sayfasına girin, yerel cihaz bilgilerini güncellemek için `Seçenekler->Cihaz listesini güncelle`ye tıklayın.\\n\\nGeçerli ağ durumu: {network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} yeni cihaz:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} cihaz kullanılamıyor:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} cihaz çevrimdışı:** \\n{message}\",\n            \"network_status_online\": \"Çevrimiçi\",\n            \"network_status_offline\": \"Çevrimdışı\",\n            \"central_state_changed_title\": \"Merkezi Hub Ağ Geçidi Bağlantı Durumu\",\n            \"central_state_changed\": \"**{nick_name}({uid}, {cloud_server})** Xiaomi merkezi hub ağ geçidine yerel bağlantı: {conn_status}\",\n            \"central_state_connected\": \"Bağlandı\",\n            \"central_state_disconnected\": \"Bağlantı kesildi\",\n            \"device_exec_error\": \"Yürütme hatası\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"Bilinmeyen hata\",\n            \"-10001\": \"Hizmet kullanılamıyor\",\n            \"-10002\": \"Geçersiz parametre\",\n            \"-10003\": \"Yetersiz kaynaklar\",\n            \"-10004\": \"İç hata\",\n            \"-10005\": \"Yetersiz izinler\",\n            \"-10006\": \"Yürütme zaman aşımı\",\n            \"-10007\": \"Cihaz çevrimdışı veya mevcut değil\",\n            \"-10020\": \"Yetkisiz (OAuth2)\",\n            \"-10030\": \"Geçersiz token (HTTP)\",\n            \"-10040\": \"Geçersiz mesaj formatı\",\n            \"-10050\": \"Geçersiz sertifika\",\n            \"-704000000\": \"Bilinmeyen hata\",\n            \"-704010000\": \"Yetkisiz (cihaz silinmiş olabilir)\",\n            \"-704014006\": \"Cihaz açıklaması bulunamadı\",\n            \"-704030013\": \"Özellik okunabilir değil\",\n            \"-704030023\": \"Özellik yazılabilir değil\",\n            \"-704030033\": \"Özellik abone edilebilir değil\",\n            \"-704040002\": \"Hizmet mevcut değil\",\n            \"-704040003\": \"Özellik mevcut değil\",\n            \"-704040004\": \"Olay mevcut değil\",\n            \"-704040005\": \"Eylem mevcut değil\",\n            \"-704040999\": \"Özellik çevrimiçi değil\",\n            \"-704042001\": \"Cihaz mevcut değil\",\n            \"-704042011\": \"Cihaz çevrimdışı\",\n            \"-704053036\": \"Cihaz işlemi zaman aşımı\",\n            \"-704053100\": \"Cihaz mevcut durumda bu işlemi gerçekleştiremiyor\",\n            \"-704083036\": \"Cihaz işlemi zaman aşımı\",\n            \"-704090001\": \"Cihaz mevcut değil\",\n            \"-704220008\": \"Geçersiz ID\",\n            \"-704220025\": \"Eylem parametre sayısı uyumsuz\",\n            \"-704220035\": \"Eylem parametre hatası\",\n            \"-704220043\": \"Özellik değer hatası\",\n            \"-704222034\": \"Eylem dönüş değer hatası\",\n            \"-705004000\": \"Bilinmeyen hata\",\n            \"-705004501\": \"Bilinmeyen hata\",\n            \"-705201013\": \"Özellik okunabilir değil\",\n            \"-705201015\": \"Eylem yürütme hatası\",\n            \"-705201023\": \"Özellik yazılabilir değil\",\n            \"-705201033\": \"Özellik abone edilebilir değil\",\n            \"-706012000\": \"Bilinmeyen hata\",\n            \"-706012013\": \"Özellik okunabilir değil\",\n            \"-706012015\": \"Eylem yürütme hatası\",\n            \"-706012023\": \"Özellik yazılabilir değil\",\n            \"-706012033\": \"Özellik abone edilebilir değil\",\n            \"-706012043\": \"Özellik değer hatası\",\n            \"-706014006\": \"Cihaz açıklaması bulunamadı\"\n        }\n    }\n}\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/zh-Hans.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"个设备\",\n            \"found_central_gateway\": \"，发现本地中枢网关\",\n            \"without_room\": \"未分配房间\",\n            \"no_display\": \"不显示\"\n        },\n        \"control_mode\": {\n            \"auto\": \"自动\",\n            \"cloud\": \"云端\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"不同步\",\n            \"home_room\": \"家庭名 和 房间名 （米家 卧室）\",\n            \"room\": \"房间名 （卧室）\",\n            \"home\": \"家庭名 （米家）\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"或逻辑\",\n            \"and\": \"与逻辑\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"排除\",\n            \"include\": \"包含\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi设备\",\n            \"2\": \"云接入设备\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"虚拟设备\",\n            \"6\": \"BLE\",\n            \"7\": \"本地AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"其他\",\n            \"10\": \"功能插件\",\n            \"11\": \"蜂窝网\",\n            \"12\": \"网线\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"第三方云接入\",\n            \"15\": \"红外遥控器子设备\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"虚拟设备组\",\n            \"18\": \"代理网关子设备\",\n            \"19\": \"安全级代理网关子设备\",\n            \"22\": \"PLC\",\n            \"23\": \"仅网线\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+蜂窝网\"\n        },\n        \"option_status\": {\n            \"enable\": \"启用\",\n            \"disable\": \"禁用\"\n        },\n        \"binary_mode\": {\n            \"text\": \"文本传感器实体\",\n            \"bool\": \"二进制传感器实体\"\n        },\n        \"device_state\": {\n            \"add\": \"新增\",\n            \"del\": \"不可用\",\n            \"offline\": \"离线\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[提示]** 检测到多个网卡可能连接同一个网络，请注意选择。\",\n            \"net_unavailable\": \"接口不可用\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"认证成功\",\n            \"content\": \"请关闭此页面，返回账号认证页面点击“下一步”\",\n            \"button\": \"关闭\"\n        },\n        \"fail\": {\n            \"title\": \"认证失败\",\n            \"content\": \"{error_msg}，请关闭此页面，返回账号认证页面重新点击认链接进行认证。\",\n            \"button\": \"关闭\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"无效的响应参数（“code”或者“state”字段为空）\",\n            \"-10101\": \"传入“state”字段不一致\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"认证信息失效，云端链路将不可用，请进入 Xiaomi Home 集成页面，点击“选项”重新认证\",\n            \"invalid_device_cache\": \"缓存设备信息异常，请进入 Xiaomi Home 集成页面，点击`选项->更新设备列表`，更新本地设备信息\",\n            \"invalid_cert_info\": \"无效的用户证书，本地中枢链路将不可用，请进入 Xiaomi Home 集成页面，点击“选项”重新认证\",\n            \"device_cloud_error\": \"从云端获取设备信息异常，请检查本地网络连接\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home 集成错误\",\n            \"xiaomi_home_error\": \"检测到 **{nick_name}({uid}, {cloud_server})** 出现错误，请进入选项页面重新配置。\\n\\n**错误信息**: \\n{message}\",\n            \"device_list_changed_title\": \"Xiaomi Home设备列表变化\",\n            \"device_list_changed\": \"检测到 **{nick_name}({uid}, {cloud_server})** 设备信息发生变化，请进入集成选项页面，点击`选项->更新设备列表`，更新本地设备信息。\\n\\n当前网络状态：{network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} 个新增设备**: \\n{message}\",\n            \"device_list_del\": \"\\n**{count} 个设备不可用**: \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} 个设备离线**: \\n{message}\",\n            \"network_status_online\": \"在线\",\n            \"network_status_offline\": \"离线\",\n            \"central_state_changed_title\": \"中枢网关连接状态\",\n            \"central_state_changed\":\"**{nick_name}({uid}, {cloud_server})** 中枢网关本地连接链路: {conn_status}\",\n            \"central_state_connected\": \"已连接\",\n            \"central_state_disconnected\": \"断连\",\n            \"device_exec_error\": \"执行错误\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"未知错误\",\n            \"-10001\": \"服务不可用\",\n            \"-10002\": \"参数无效\",\n            \"-10003\": \"资源不足\",\n            \"-10004\": \"内部错误\",\n            \"-10005\": \"权限不足\",\n            \"-10006\": \"执行超时\",\n            \"-10007\": \"设备离线或者不存在\",\n            \"-10020\": \"未授权OAuth2）\",\n            \"-10030\": \"无效的token（HTTP）\",\n            \"-10040\": \"无效的消息格式\",\n            \"-10050\": \"无效的证书\",\n            \"-704000000\": \"未知错误\",\n            \"-704010000\": \"未授权（设备可能被删除）\",\n            \"-704014006\": \"没找到设备描述\",\n            \"-704030013\": \"Property不可读\",\n            \"-704030023\": \"Property不可写\",\n            \"-704030033\": \"Property不可订阅\",\n            \"-704040002\": \"Service不存在\",\n            \"-704040003\": \"Property不存在\",\n            \"-704040004\": \"Event不存在\",\n            \"-704040005\": \"Action不存在\",\n            \"-704040999\": \"功能未上线\",\n            \"-704042001\": \"Device不存在\",\n            \"-704042011\": \"设备离线\",\n            \"-704053036\": \"设备操作超时\",\n            \"-704053100\": \"设备在当前状态下无法执行此操作\",\n            \"-704083036\": \"设备操作超时\",\n            \"-704090001\": \"Device不存在\",\n            \"-704220008\": \"无效的ID\",\n            \"-704220025\": \"Action参数个数不匹配\",\n            \"-704220035\": \"Action参数错误\",\n            \"-704220043\": \"Property值错误\",\n            \"-704222034\": \"Action返回值错误\",\n            \"-705004000\": \"未知错误\",\n            \"-705004501\": \"未知错误\",\n            \"-705201013\": \"Property不可读\",\n            \"-705201015\": \"Action执行错误\",\n            \"-705201023\": \"Property不可写\",\n            \"-705201033\": \"Property不可订阅\",\n            \"-706012000\": \"未知错误\",\n            \"-706012013\": \"Property不可读\",\n            \"-706012015\": \"Action执行错误\",\n            \"-706012023\": \"Property不可写\",\n            \"-706012033\": \"Property不可订阅\",\n            \"-706012043\": \"Property值错误\",\n            \"-706014006\": \"没找到设备描述\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/i18n/zh-Hant.json",
    "content": "{\n    \"config\": {\n        \"other\": {\n            \"devices\": \"個設備\",\n            \"found_central_gateway\": \"，發現本地中樞網關\",\n            \"without_room\": \"未分配房間\",\n            \"no_display\": \"不顯示\"\n        },\n        \"control_mode\": {\n            \"auto\": \"自動\",\n            \"cloud\": \"雲端\"\n        },\n        \"statistics_logic\": {\n            \"or\": \"或邏輯\",\n            \"and\": \"與邏輯\"\n        },\n        \"filter_mode\": {\n            \"exclude\": \"排除\",\n            \"include\": \"包含\"\n        },\n        \"connect_type\": {\n            \"0\": \"WiFi\",\n            \"1\": \"yunyi設備\",\n            \"2\": \"雲接入設備\",\n            \"3\": \"ZigBee\",\n            \"4\": \"webSocket\",\n            \"5\": \"虛擬設備\",\n            \"6\": \"BLE\",\n            \"7\": \"本地AP\",\n            \"8\": \"WiFi+BLE\",\n            \"9\": \"其他\",\n            \"10\": \"功能插件\",\n            \"11\": \"蜂窩網\",\n            \"12\": \"網線\",\n            \"13\": \"NB-IoT\",\n            \"14\": \"第三方雲接入\",\n            \"15\": \"紅外遙控器子設備\",\n            \"16\": \"BLE-Mesh\",\n            \"17\": \"虛擬設備組\",\n            \"18\": \"代理網關子設備\",\n            \"19\": \"安全級代理網關子設備\",\n            \"22\": \"PLC\",\n            \"23\": \"僅網線\",\n            \"24\": \"Matter\",\n            \"25\": \"WiFi+蜂窩網\"\n        },\n        \"room_name_rule\": {\n            \"none\": \"不同步\",\n            \"home_room\": \"家庭名 和 房間名 （米家 臥室）\",\n            \"room\": \"房間名 （臥室）\",\n            \"home\": \"家庭名 （米家）\"\n        },\n        \"option_status\": {\n            \"enable\": \"啟用\",\n            \"disable\": \"禁用\"\n        },\n        \"binary_mode\": {\n            \"text\": \"文本傳感器實體\",\n            \"bool\": \"二進制傳感器實體\"\n        },\n        \"device_state\": {\n            \"add\": \"新增\",\n            \"del\": \"不可用\",\n            \"offline\": \"離線\"\n        },\n        \"lan_ctrl_config\": {\n            \"notice_net_dup\": \"\\r\\n**[提示]** 檢測到多個網卡可能連接同一個網絡，請注意選擇。\",\n            \"net_unavailable\": \"接口不可用\"\n        }\n    },\n    \"oauth2\": {\n        \"success\": {\n            \"title\": \"認證成功\",\n            \"content\": \"請關閉此頁面，返回帳號認證頁面點擊“下一步”\",\n            \"button\": \"關閉\"\n        },\n        \"fail\": {\n            \"title\": \"認證失敗\",\n            \"content\": \"{error_msg}，請關閉此頁面，返回帳號認證頁面重新點擊認鏈接進行認證。\",\n            \"button\": \"關閉\"\n        },\n        \"error_msg\": {\n            \"-10100\": \"無效的響應參數（“code”或者“state”字段為空）\",\n            \"-10101\": \"傳入的“state”字段不一致\"\n        }\n    },\n    \"miot\": {\n        \"client\": {\n            \"invalid_oauth_info\": \"認證信息失效，雲端鏈路將不可用，請進入 Xiaomi Home 集成頁面，點擊“選項”重新認證\",\n            \"invalid_device_cache\": \"緩存設備信息異常，請進入 Xiaomi Home 集成頁面，點擊`選項->更新設備列表`，更新本地設備信息\",\n            \"invalid_cert_info\": \"無效的用戶證書，本地中樞鏈路將不可用，請進入 Xiaomi Home 集成頁面，點擊“選項”重新認證\",\n            \"device_cloud_error\": \"從雲端獲取設備信息異常，請檢查本地網絡連接\",\n            \"xiaomi_home_error_title\": \"Xiaomi Home 集成錯誤\",\n            \"xiaomi_home_error\": \"檢測到 **{nick_name}({uid}, {cloud_server})** 出現錯誤，請進入選項頁面重新配置。\\n\\n**錯誤信息**: \\n{message}\",\n            \"device_list_changed_title\": \"Xiaomi Home設備列表變化\",\n            \"device_list_changed\": \"檢測到 **{nick_name}({uid}, {cloud_server})** 設備信息發生變化，請進入集成選項頁面，點擊`選項->更新設備列表`，更新本地設備信息。\\n\\n當前網絡狀態：{network_status}\\n{message}\\n\",\n            \"device_list_add\": \"\\n**{count} 個新增設備:** \\n{message}\",\n            \"device_list_del\": \"\\n**{count} 個設備不可用:** \\n{message}\",\n            \"device_list_offline\": \"\\n**{count} 個設備離線:** \\n{message}\",\n            \"network_status_online\": \"在線\",\n            \"network_status_offline\": \"離線\",\n            \"central_state_changed_title\": \"中枢網關連接狀態\",\n            \"central_state_changed\":\"**{nick_name}({uid}, {cloud_server})** 中枢網關本地連接鏈路: {conn_status}\",\n            \"central_state_connected\": \"已連接\",\n            \"central_state_disconnected\": \"断連\",\n            \"device_exec_error\": \"執行錯誤\"\n        }\n    },\n    \"error\": {\n        \"common\": {\n            \"-10000\": \"未知錯誤\",\n            \"-10001\": \"服務不可用\",\n            \"-10002\": \"參數無效\",\n            \"-10003\": \"資源不足\",\n            \"-10004\": \"內部錯誤\",\n            \"-10005\": \"權限不足\",\n            \"-10006\": \"執行超時\",\n            \"-10007\": \"設備離線或者不存在\",\n            \"-10020\": \"未授權（OAuth2）\",\n            \"-10030\": \"無效的token（HTTP）\",\n            \"-10040\": \"無效的消息格式\",\n            \"-10050\": \"無效的證書\",\n            \"-704000000\": \"未知錯誤\",\n            \"-704010000\": \"未授權（設備可能被刪除）\",\n            \"-704014006\": \"沒找到設備描述\",\n            \"-704030013\": \"Property不可讀\",\n            \"-704030023\": \"Property不可寫\",\n            \"-704030033\": \"Property不可訂閱\",\n            \"-704040002\": \"Service不存在\",\n            \"-704040003\": \"Property不存在\",\n            \"-704040004\": \"Event不存在\",\n            \"-704040005\": \"Action不存在\",\n            \"-704040999\": \"功能未上線\",\n            \"-704042001\": \"Device不存在\",\n            \"-704042011\": \"設備離線\",\n            \"-704053036\": \"設備操作超時\",\n            \"-704053100\": \"設備在當前狀態下無法執行此操作\",\n            \"-704083036\": \"設備操作超時\",\n            \"-704090001\": \"Device不存在\",\n            \"-704220008\": \"無效的ID\",\n            \"-704220025\": \"Action參數個數不匹配\",\n            \"-704220035\": \"Action參數錯誤\",\n            \"-704220043\": \"Property值錯誤\",\n            \"-704222034\": \"Action返回值錯誤\",\n            \"-705004000\": \"未知錯誤\",\n            \"-705004501\": \"未知錯誤\",\n            \"-705201013\": \"Property不可讀\",\n            \"-705201015\": \"Action執行錯誤\",\n            \"-705201023\": \"Property不可寫\",\n            \"-705201033\": \"Property不可訂閱\",\n            \"-706012000\": \"未知錯誤\",\n            \"-706012013\": \"Property不可讀\",\n            \"-706012015\": \"Action執行錯誤\",\n            \"-706012023\": \"Property不可寫\",\n            \"-706012033\": \"Property不可訂閱\",\n            \"-706012043\": \"Property值錯誤\",\n            \"-706014006\": \"沒找到設備描述\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/lan/profile_models.yaml",
    "content": "090615.curtain.mt800w:\n  ts: 1604589513\n090615.curtain.p01:\n  ts: 1603355069\n090615.curtain.sidt82:\n  ts: 1604589553\n090615.switch.switch01:\n  ts: 1605166385\n090615.switch.switch02:\n  ts: 1604589655\n090615.switch.switch03:\n  ts: 1605161171\n090615.switch.xswitch01:\n  ts: 1603965395\n090615.switch.xswitch02:\n  ts: 1603967482\n090615.switch.xswitch03:\n  ts: 1603967572\n1245.airpurifier.dl01:\n  ts: 1607502661\n17216.magic_touch.d150:\n  ts: 1575097876\n17216.magic_touch.d152:\n  ts: 1575097876\n17216.massage.ec1266a:\n  ts: 1615881124\n397.light.hallight:\n  ts: 1605161883\n666.curtain.em75:\n  ts: 1605857796\naden.aircondition.a1:\n  ts: 1618982254\naden.aircondition.a2:\n  ts: 1605167390\naden.aircondition.a4:\n  ts: 1605167477\naice.motor.kzmu3:\n  ts: 1646394583\nair.fan.ca23ad9:\n  ts: 1635406920\nairdog.airpurifier.x5:\n  ts: 1623915265\nairdog.airpurifier.x7:\n  ts: 1614656371\nairdog.airpurifier.x7sm:\n  ts: 1614656402\nasp.treadmill.pbj:\n  ts: 1611217046\naux.aircondition.v1:\n  ts: 1626427548\nbdx.i_stove.a1xs:\n  ts: 1610614201\nbdx.i_stove.a1z:\n  ts: 1614667065\nbdx.i_stove.c1x:\n  ts: 1610614303\nbdx.i_stove.c2x:\n  ts: 1611823486\nbj352.airmonitor.m30:\n  ts: 1686644541\nbj352.waterpuri.s100cm:\n  ts: 1615795630\nbymiot.gateway.v1:\n  ts: 1575097876\nbymiot.gateway.v2:\n  ts: 1575097876\ncgllc.airmonitor.b1:\n  ts: 1676339912\ncgllc.airmonitor.s1:\n  ts: 1608189468\ncgllc.clock.cgc1:\n  ts: 1686644422\ncgllc.clock.dove:\n  ts: 1619607474\ncgllc.gateway.s1:\n  ts: 1575097876\ncgllc.magnet.hodor:\n  ts: 1724329476\ncgllc.motion.cgpr1:\n  ts: 1639479505\ncgllc.sensor_ht.cgm1:\n  ts: 1602667557\ncgllc.sensor_ht.dk2:\n  ts: 1620724988\ncgllc.sensor_ht.g1:\n  ts: 1607505446\ncgllc.sensor_ht.qpg1:\n  ts: 1602667461\nchuangmi.camera.ipc004b:\n  ts: 1531108800\nchuangmi.camera.ipc007b:\n  ts: 1531108800\nchuangmi.camera.ipc009:\n  ts: 1531108800\nchuangmi.camera.ipc010:\n  ts: 1531108800\nchuangmi.camera.ipc013:\n  ts: 1531108800\nchuangmi.camera.ipc013d:\n  ts: 1531108800\nchuangmi.camera.ipc016:\n  ts: 1531108800\nchuangmi.camera.ipc017:\n  ts: 1531108800\nchuangmi.camera.ipc019:\n  ts: 1531108800\nchuangmi.camera.ipc019b:\n  ts: 1531108800\nchuangmi.camera.ipc019e:\n  ts: 1531108800\nchuangmi.camera.ipc020:\n  ts: 1531108800\nchuangmi.camera.ipc021:\n  ts: 1531108800\nchuangmi.camera.v2:\n  ts: 1531108800\nchuangmi.camera.v3:\n  ts: 1531108800\nchuangmi.camera.v4:\n  ts: 1531108800\nchuangmi.camera.v5:\n  ts: 1531108800\nchuangmi.camera.v6:\n  ts: 1531108800\nchuangmi.camera.xiaobai:\n  ts: 1531108800\nchuangmi.cateye.i023a01:\n  ts: 1624535092\nchuangmi.cateye.ipc018:\n  ts: 1632735241\nchuangmi.cateye.ipc508:\n  ts: 1633677521\nchuangmi.door.hmi508:\n  ts: 1611733437\nchuangmi.door.hmi515:\n  ts: 1640334316\nchuangmi.gateway.ipc011:\n  ts: 1575097876\nchuangmi.ir.v2:\n  ts: 1575097876\nchuangmi.lock.hmi501:\n  ts: 1614742147\nchuangmi.lock.hmi501b01:\n  ts: 1614742108\nchuangmi.lock.hmi503a01:\n  ts: 1614742180\nchuangmi.lock.hmi505a01:\n  ts: 1614742900\nchuangmi.plug.hmi206:\n  ts: 1645409193\nchuangmi.plug.hmi208:\n  ts: 1677652804\nchuangmi.plug.m1:\n  ts: 1620814339\nchuangmi.plug.m3:\n  ts: 1637228027\nchuangmi.plug.v1:\n  ts: 1621925183\nchuangmi.plug.v3:\n  ts: 1644480255\nchuangmi.plug.vtl_v1:\n  ts: 1575097876\nchuangmi.radio.v1:\n  ts: 1531108800\nchuangmi.radio.v2:\n  ts: 1531108800\nchuangmi.remote.h102a03:\n  ts: 1575097876\nchuangmi.remote.h102c01:\n  ts: 1575097876\nchuangmi.remote.v2:\n  ts: 1575097876\nchunmi.cooker.eh1:\n  ts: 1607339278\nchunmi.cooker.eh402:\n  ts: 1638506382\nchunmi.cooker.k1pro1:\n  ts: 1607393855\nchunmi.cooker.normal2:\n  ts: 1607393898\nchunmi.cooker.normal3:\n  ts: 1620369915\nchunmi.cooker.normal4:\n  ts: 1607394370\nchunmi.cooker.normal5:\n  ts: 1607394381\nchunmi.cooker.normalcd1:\n  ts: 1607395198\nchunmi.cooker.normalcd2:\n  ts: 1607395220\nchunmi.cooker.press1:\n  ts: 1607395268\nchunmi.cooker.press2:\n  ts: 1607395280\nchunmi.ihcooker.chefnic:\n  ts: 1604591216\nchunmi.ihcooker.double:\n  ts: 1609924806\nchunmi.ihcooker.hk1:\n  ts: 1605167058\nchunmi.ihcooker.tkpro1:\n  ts: 1614756253\nchunmi.ihcooker.tkv1:\n  ts: 1615801005\nchunmi.ihcooker.v1:\n  ts: 1605167116\nchunmi.juicer.a1:\n  ts: 1624530635\nchunmi.microwave.n20l01:\n  ts: 1638505282\nchunmi.microwave.n23l01:\n  ts: 1638505524\nchunmi.oven.steam30lv1:\n  ts: 1605855556\nchunmi.pre_cooker.eh1:\n  ts: 1614570773\ncleargrass.sensor_ht.dk1:\n  ts: 1619344277\ndeerma.humidifier.jsq1:\n  ts: 1683863719\ndeerma.humidifier.mjjsq:\n  ts: 1684727440\ndicook.cooker.wfz4003:\n  ts: 1614678405\ndmaker.airfresh.a1:\n  ts: 1715677691\ndmaker.airfresh.t2017:\n  ts: 1686731233\ndmaker.fan.p5:\n  ts: 1655793784\ndoco.fcb.docov001:\n  ts: 1575097876\ndsm.lock.h3:\n  ts: 1615283790\ndsm.lock.q3:\n  ts: 1614741870\ndsm.lock.r5:\n  ts: 1614741913\ndun.cateye.nknk500:\n  ts: 1615358692\nduoqin.safe.pbfv01:\n  ts: 1614740167\nfawad.airrtc.fwd20011:\n  ts: 1610607149\nfbs.airmonitor.pth02:\n  ts: 1686644918\nfengmi.projector.fm05:\n  ts: 1575097876\nfengmi.projector.fm15:\n  ts: 1575097876\nfengmi.projector.fm154k:\n  ts: 1575097876\nfengmi.projector.l166:\n  ts: 1650352923\nfengmi.projector.l176:\n  ts: 1649936204\nfengmi.projector.l246:\n  ts: 1575097876\nfengmi.projector.m055:\n  ts: 1652839826\nfengmi.projector.m055d:\n  ts: 1654067980\nfengyu.intercom.beebird:\n  ts: 1575097876\nfengyu.intercom.sharkv1:\n  ts: 1575097876\nfotile.hood.emd1tmi:\n  ts: 1607483642\nguoshi.other.sem01:\n  ts: 1602662080\nhannto.printer.anise:\n  ts: 1618989537\nhannto.printer.honey:\n  ts: 1607504864\nhannto.printer.honey1s:\n  ts: 1614332725\nhfjh.fishbowl.v1:\n  ts: 1615278556\nhhcc.bleflowerpot.v2:\n  ts: 1575097876\nhhcc.plantmonitor.v1:\n  ts: 1664163526\nhith.foot_bath.q2:\n  ts: 1531108800\nhmpace.bracelet.v4:\n  ts: 1575097876\nhmpace.scales.mibfs:\n  ts: 1575097876\nhmpace.scales.miscale2:\n  ts: 1575097876\nhuohe.lock.m1:\n  ts: 1635410938\nhuoman.litter_box.co1:\n  ts: 1687165034\nhutlon.lock.v0001:\n  ts: 1634799698\nidelan.aircondition.g1:\n  ts: 1575097876\nidelan.aircondition.v1:\n  ts: 1614666973\nidelan.aircondition.v2:\n  ts: 1626427579\nihealth.bp.bpm1:\n  ts: 1608189506\nihealth.bpm.kd5907:\n  ts: 1614673307\nikea.light.led1536g5:\n  ts: 1605162819\nikea.light.led1537r6:\n  ts: 1605162872\nikea.light.led1545g12:\n  ts: 1605162937\nikea.light.led1546g12:\n  ts: 1575097876\nikea.light.led1623g12:\n  ts: 1605163009\nikea.light.led1649c5:\n  ts: 1605163064\nikea.light.led1650r5:\n  ts: 1575097876\nimibar.cooker.mbihr3:\n  ts: 1624620659\nimou99.camera.tp2:\n  ts: 1531108800\ninovel.projector.me2:\n  ts: 1575097876\niracc.aircondition.d19:\n  ts: 1609914362\nisa.camera.df3:\n  ts: 1531108800\nisa.camera.hl5:\n  ts: 1531108800\nisa.camera.hlc6:\n  ts: 1531108800\nisa.camera.isc5:\n  ts: 1531108800\nisa.camera.isc5c1:\n  ts: 1621238175\nisa.camera.qf3:\n  ts: 1575097876\nisa.cateye.hldb6:\n  ts: 1575097876\nisa.magnet.dw2hl:\n  ts: 1638274655\njieman.magic_touch.js78:\n  ts: 1575097876\njiqid.mistory.ipen1:\n  ts: 1575097876\njiqid.mistory.pro:\n  ts: 1531108800\njiqid.mistory.v1:\n  ts: 1531108800\njiqid.mistudy.v2:\n  ts: 1610612349\njiqid.robot.cube:\n  ts: 1575097876\njiwu.lock.jwp01:\n  ts: 1614752632\njyaiot.cm.ccj01:\n  ts: 1611824545\nk0918.toothbrush.kid01:\n  ts: 1575097876\nkejia.airer.th001:\n  ts: 1575097876\nksmb.treadmill.k12:\n  ts: 1575097876\nksmb.treadmill.v1:\n  ts: 1611211447\nksmb.treadmill.v2:\n  ts: 1610606684\nksmb.walkingpad.v1:\n  ts: 1621238199\nksmb.walkingpad.v3:\n  ts: 1621238214\nkxf321.mop.mo001:\n  ts: 1638343629\nlcrmcr.safe.20mini:\n  ts: 1617765416\nlcrmcr.safe.30mk:\n  ts: 1628215806\nlcrmcr.safe.an35sidz:\n  ts: 1614741042\nlcrmcr.safe.an35sizw:\n  ts: 1614741076\nlcrmcr.safe.d60ht:\n  ts: 1624351108\nlcrmcr.safe.ms30b:\n  ts: 1606986586\nlcrmcr.safe.ms30mp:\n  ts: 1604587256\nlcrmcr.safe.ms55kn:\n  ts: 1606980410\nlcrmcr.safe.ms80b:\n  ts: 1614740261\nlcrmcr.safe.sd003:\n  ts: 1637033698\nlcrmcr.safe.x142:\n  ts: 1634813125\nleshow.fan.ss310:\n  ts: 1686896880\nleshow.fan.ss320:\n  ts: 1686896668\nleshow.fan.ss4:\n  ts: 1606376586\nleshow.heater.bs1:\n  ts: 1608187309\nleshow.humidifier.is2:\n  ts: 1604589602\nlinp.doorbell.g03:\n  ts: 1609311251\nlinp.remote.k9b:\n  ts: 1621825941\nlinp.remote.k9b1:\n  ts: 1621825926\nlinp.remote.k9b11:\n  ts: 1621825903\nloock.cateye.v01:\n  ts: 1627291525\nloock.cateye.v02:\n  ts: 1661739898\nloock.lock.cc2s:\n  ts: 1614752275\nloock.lock.cc2xpro:\n  ts: 1614752365\nloock.lock.fcl112:\n  ts: 1658997537\nloock.lock.fcp50m:\n  ts: 1647246522\nloock.lock.fvl109:\n  ts: 1640252939\nloock.lock.fvl111:\n  ts: 1646134370\nloock.lock.ojjz1:\n  ts: 1614741370\nloock.lock.p50:\n  ts: 1644572168\nloock.lock.pfvl10:\n  ts: 1630040903\nloock.lock.s30:\n  ts: 1614740862\nloock.lock.s30v2:\n  ts: 1614829588\nloock.lock.s50c:\n  ts: 1649309703\nloock.lock.s50f:\n  ts: 1639967451\nloock.lock.t1:\n  ts: 1634542543\nloock.lock.t1pro:\n  ts: 1634543113\nloock.lock.t2v1:\n  ts: 1655983879\nloock.lock.v1:\n  ts: 1618889646\nloock.lock.v14:\n  ts: 1614741632\nloock.lock.v15:\n  ts: 1619508939\nloock.lock.v16:\n  ts: 1621235279\nloock.lock.v3:\n  ts: 1619341106\nloock.lock.v4:\n  ts: 1619340970\nloock.lock.v5:\n  ts: 1614752242\nloock.lock.v6:\n  ts: 1634796911\nloock.lock.v7:\n  ts: 1614741195\nloock.lock.v8:\n  ts: 1619413983\nloock.lock.v9:\n  ts: 1614741328\nloock.lock.xfvl10:\n  ts: 1632814256\nloock.safe.v1:\n  ts: 1619607755\nlumi.acpartner.mcn02:\n  ts: 1655791626\nlumi.acpartner.v1:\n  ts: 1531108800\nlumi.acpartner.v2:\n  ts: 1531108800\nlumi.acpartner.v3:\n  ts: 1531108800\nlumi.airer.acn01:\n  ts: 1611818317\nlumi.airrtc.tcpco2ecn01:\n  ts: 1531108800\nlumi.airrtc.tcpecn01:\n  ts: 1531108800\nlumi.airrtc.tcpecn02:\n  ts: 1531108800\nlumi.camera.aq1:\n  ts: 1531108800\nlumi.camera.gwagl01:\n  ts: 1531108800\nlumi.ctrl_86plug.aq1:\n  ts: 1627292331\nlumi.ctrl_86plug.v1:\n  ts: 1627291597\nlumi.ctrl_ln1.aq1:\n  ts: 1648791623\nlumi.ctrl_ln1.v1:\n  ts: 1653297884\nlumi.ctrl_ln2.aq1:\n  ts: 1653292857\nlumi.ctrl_ln2.v1:\n  ts: 1653299006\nlumi.ctrl_neutral1.v1:\n  ts: 1653294867\nlumi.ctrl_neutral2.v1:\n  ts: 1653294492\nlumi.curtain.aq2:\n  ts: 1605857829\nlumi.curtain.hagl04:\n  ts: 1615351634\nlumi.curtain.v1:\n  ts: 1608188918\nlumi.flood.bmcn01:\n  ts: 1614666824\nlumi.gateway.aqhm01:\n  ts: 1687160720\nlumi.gateway.aqhm02:\n  ts: 1687162682\nlumi.gateway.lmuk01:\n  ts: 1687164111\nlumi.gateway.mieu01:\n  ts: 1687163976\nlumi.gateway.mihk01:\n  ts: 1687163477\nlumi.gateway.mitw01:\n  ts: 1687160954\nlumi.gateway.v1:\n  ts: 1687162995\nlumi.gateway.v2:\n  ts: 1687163777\nlumi.gateway.v3:\n  ts: 1686895771\nlumi.light.aqcn02:\n  ts: 1620727535\nlumi.light.cwopcn01:\n  ts: 1605855768\nlumi.light.cwopcn02:\n  ts: 1605855809\nlumi.light.cwopcn03:\n  ts: 1605855836\nlumi.lock.acn02:\n  ts: 1623928631\nlumi.lock.acn03:\n  ts: 1614752574\nlumi.lock.aq1:\n  ts: 1612518044\nlumi.lock.bacn01:\n  ts: 1614741699\nlumi.lock.bmcn02:\n  ts: 1637292825\nlumi.lock.bmcn03:\n  ts: 1634546176\nlumi.lock.bmcn04:\n  ts: 1636451160\nlumi.lock.bmcn05:\n  ts: 1636454200\nlumi.lock.bzacn1:\n  ts: 1614741727\nlumi.lock.bzacn2:\n  ts: 1614741815\nlumi.lock.eicn02:\n  ts: 1639976382\nlumi.lock.mcn007:\n  ts: 1650446757\nlumi.lock.mcn01:\n  ts: 1679881881\nlumi.lock.v1:\n  ts: 1575097876\nlumi.lock.wbmcn1:\n  ts: 1619422072\nlumi.motion.bmgl01:\n  ts: 1639983139\nlumi.plug.v1:\n  ts: 1653299040\nlumi.relay.c2acn01:\n  ts: 1609310261\nlumi.remote.b186acn01:\n  ts: 1614154507\nlumi.remote.b186acn02:\n  ts: 1607395626\nlumi.remote.b1acn01:\n  ts: 1608794798\nlumi.remote.b286acn01:\n  ts: 1607397501\nlumi.remote.b286acn02:\n  ts: 1608794723\nlumi.remote.b286opcn01:\n  ts: 1614154602\nlumi.remote.b486opcn01:\n  ts: 1614154660\nlumi.remote.b686opcn01:\n  ts: 1614154713\nlumi.sensor_86sw1.v1:\n  ts: 1609311038\nlumi.sensor_86sw2.v1:\n  ts: 1608795035\nlumi.sensor_cube.aqgl01:\n  ts: 1575097876\nlumi.sensor_ht.v1:\n  ts: 1621239877\nlumi.sensor_magnet.aq2:\n  ts: 1641112867\nlumi.sensor_magnet.v1:\n  ts: 1606120416\nlumi.sensor_magnet.v2:\n  ts: 1641113779\nlumi.sensor_motion.aq2:\n  ts: 1676433994\nlumi.sensor_motion.v1:\n  ts: 1605093075\nlumi.sensor_motion.v2:\n  ts: 1672818550\nlumi.sensor_natgas.v1:\n  ts: 1635762582\nlumi.sensor_smoke.mcn02:\n  ts: 1634635296\nlumi.sensor_smoke.v1:\n  ts: 1634809938\nlumi.sensor_switch.aq2:\n  ts: 1615256430\nlumi.sensor_switch.aq3:\n  ts: 1607399487\nlumi.sensor_switch.v1:\n  ts: 1606874434\nlumi.sensor_switch.v2:\n  ts: 1609310683\nlumi.sensor_wleak.aq1:\n  ts: 1614669352\nlumi.switch.b1lacn02:\n  ts: 1653297814\nlumi.switch.b1nacn02:\n  ts: 1653297756\nlumi.switch.b2lacn02:\n  ts: 1653296711\nlumi.switch.b2nacn02:\n  ts: 1655201416\nlumi.switch.l3acn3:\n  ts: 1653296585\nlumi.switch.n3acn3:\n  ts: 1653294817\nlumi.vibration.aq1:\n  ts: 1614156721\nlumi.weather.v1:\n  ts: 1621239934\nmadv.alarm.winlock1:\n  ts: 1611215780\nmadv.cateye.dlowl:\n  ts: 1632714747\nmadv.cateye.dlowlplus:\n  ts: 1615876742\nmadv.cateye.dlowlse:\n  ts: 1607409634\nmadv.cateye.dlowlse2:\n  ts: 1615876830\nmadv.cateye.miowl:\n  ts: 1632714912\nmadv.cateye.miowlv2:\n  ts: 1614152626\nmadv.cateye.miowlv2l:\n  ts: 1615450328\nmiaomiaoce.clock.ht02:\n  ts: 1620728504\nmiaomiaoce.sensor_ht.h1:\n  ts: 1623929417\nmiaomiaoce.sensor_ht.t1:\n  ts: 1616057242\nmiaomiaoce.sensor_ht.t2:\n  ts: 1636603553\nmiaomiaoce.thermo.t01:\n  ts: 1575097876\nmidea.aircondition.v1:\n  ts: 1575097876\nmidea.aircondition.xa1:\n  ts: 1575097876\nmidea.aircondition.xa2:\n  ts: 1575097876\nmidr.rv_mirror.m2:\n  ts: 1575097876\nmidr.rv_mirror.m5:\n  ts: 1575097876\nmidr.rv_mirror.v1:\n  ts: 1575097876\nmiir.aircondition.ir01:\n  ts: 1531108800\nmiir.aircondition.ir02:\n  ts: 1531108800\nmiir.fan.ir01:\n  ts: 1531108800\nmiir.light.ir01:\n  ts: 1531108800\nmiir.projector.ir01:\n  ts: 1531108800\nmiir.stb.ir01:\n  ts: 1531108800\nmiir.tv.hir01:\n  ts: 1531108800\nmiir.tv.ir01:\n  ts: 1531108800\nmiir.tvbox.ir01:\n  ts: 1531108800\nmijia.camera.v1:\n  ts: 1531108800\nmijia.camera.v3:\n  ts: 1531108800\nminij.washer.v1:\n  ts: 1611818798\nminij.washer.v10:\n  ts: 1608792100\nminij.washer.v11:\n  ts: 1608792159\nminij.washer.v12:\n  ts: 1614656828\nminij.washer.v14:\n  ts: 1608792176\nminij.washer.v15:\n  ts: 1607410326\nminij.washer.v5:\n  ts: 1622792196\nminij.washer.v8:\n  ts: 1615777868\nminuo.tracker.lm001:\n  ts: 1575097876\nmiot.light.plato2:\n  ts: 1685518142\nmiot.light.plato3:\n  ts: 1675941846\nmiot.light.plato4:\n  ts: 1675941712\nmmgg.feeder.petfeeder:\n  ts: 1646394400\nmmgg.feeder.snack:\n  ts: 1607503182\nmoyu.washer.s1hm:\n  ts: 1624620888\nmrbond.airer.m0:\n  ts: 1575097876\nmrbond.airer.m1pro:\n  ts: 1646393746\nmrbond.airer.m1s:\n  ts: 1646393874\nmrbond.airer.m1super:\n  ts: 1575097876\nmsj.f_washer.m1:\n  ts: 1614914340\nmxiang.cateye.mdb10:\n  ts: 1616140362\nmxiang.cateye.xmcatt1:\n  ts: 1616140207\nnhy.airrtc.v1:\n  ts: 1575097876\nninebot.scooter.v1:\n  ts: 1602662395\nninebot.scooter.v6:\n  ts: 1575097876\nnuwa.robot.minikiwi:\n  ts: 1575097876\nnwt.derh.wdh318efw1:\n  ts: 1611822375\nonemore.wifispeaker.sm4:\n  ts: 1575097876\nopple.light.bydceiling:\n  ts: 1608187619\nopple.light.fanlight:\n  ts: 1608793315\nopple.remote.5pb111:\n  ts: 1627453753\nopple.remote.5pb112:\n  ts: 1627453840\nopple.remote.5pb113:\n  ts: 1636599905\norion.wifispeaker.cm1:\n  ts: 1575097876\nows.towel_w.mj1x0:\n  ts: 1610604939\nphilips.light.bceiling1:\n  ts: 1642048160\nphilips.light.bceiling2:\n  ts: 1642048073\nphilips.light.bulb:\n  ts: 1620814142\nphilips.light.candle:\n  ts: 1620814078\nphilips.light.candle2:\n  ts: 1620814121\nphilips.light.cbulb:\n  ts: 1639039185\nphilips.light.ceiling:\n  ts: 1646394746\nphilips.light.downlight:\n  ts: 1620814088\nphilips.light.lnblight1:\n  ts: 1605857128\nphilips.light.lnblight2:\n  ts: 1623902902\nphilips.light.lnlrlight:\n  ts: 1621845517\nphilips.light.lrceiling:\n  ts: 1642047751\nphilips.light.mono1:\n  ts: 1608790977\nphilips.light.moonlight:\n  ts: 1642582053\nphilips.light.nlight:\n  ts: 1614147878\nphilips.light.rwread:\n  ts: 1603352187\nphilips.light.sread1:\n  ts: 1620814093\nphilips.light.sread2:\n  ts: 1615259714\nphilips.light.zyceiling:\n  ts: 1637228869\nphilips.light.zysread:\n  ts: 1615970237\nphilips.light.zystrip:\n  ts: 1605166337\nphnix.waterheater.sf:\n  ts: 1616050758\npwzn.relay.apple:\n  ts: 1611217196\npwzn.relay.banana:\n  ts: 1646647255\nqicyc.bike.tdp02z:\n  ts: 1575097876\nqike.bhf_light.qk201801:\n  ts: 1608174909\nqmi.powerstrip.v1:\n  ts: 1621240280\nroborock.sweeper.s5v2:\n  ts: 1531108800\nroborock.vacuum.a01:\n  ts: 1685425888\nroborock.vacuum.a08:\n  ts: 1626231977\nroborock.vacuum.a09:\n  ts: 1611048152\nroborock.vacuum.a11:\n  ts: 1615360022\nroborock.vacuum.c1:\n  ts: 1531108800\nroborock.vacuum.e2:\n  ts: 1531108800\nroborock.vacuum.m1s:\n  ts: 1531108800\nroborock.vacuum.p5:\n  ts: 1611048094\nroborock.vacuum.s5:\n  ts: 1531108800\nroborock.vacuum.t4:\n  ts: 1615449261\nroborock.vacuum.t6:\n  ts: 1619423841\nrockrobo.vacuum.v1:\n  ts: 1531108800\nroidmi.carairpuri.pro:\n  ts: 1575097876\nroidmi.carairpuri.v1:\n  ts: 1575097876\nroidmi.cleaner.f8pro:\n  ts: 1575097876\nroidmi.cleaner.v1:\n  ts: 1575097876\nroidmi.cleaner.v2:\n  ts: 1638514177\nroidmi.cleaner.v382:\n  ts: 1575097876\nroidmi.vacuum.v1:\n  ts: 1575097876\nrokid.robot.me:\n  ts: 1575097876\nrokid.robot.mini:\n  ts: 1575097876\nrokid.robot.pebble:\n  ts: 1575097876\nrokid.robot.pebble2:\n  ts: 1575097876\nroome.bhf_light.yf6002:\n  ts: 1531108800\nrotai.magic_touch.sx300:\n  ts: 1602662578\nrotai.massage.rt5728:\n  ts: 1610607000\nrotai.massage.rt5850:\n  ts: 1611816888\nrotai.massage.rt5850s:\n  ts: 1616727205\nrotai.massage.rt5863:\n  ts: 1611827937\nrotai.massage.rt5870:\n  ts: 1632376570\nrunmi.suitcase.v1:\n  ts: 1575097876\nscishare.coffee.s1102:\n  ts: 1611824402\nshjszn.gateway.c1:\n  ts: 1575097876\nshjszn.lock.c1:\n  ts: 1575097876\nshjszn.lock.kx:\n  ts: 1575097876\nshuii.humidifier.jsq001:\n  ts: 1575097876\nshuii.humidifier.jsq002:\n  ts: 1606376290\nskyrc.feeder.dfeed:\n  ts: 1626082349\nskyrc.pet_waterer.fre1:\n  ts: 1608186812\nsmith.w_soften.cxs05ta1:\n  ts: 1575097876\nsmith.waterheater.cxea1:\n  ts: 1611826349\nsmith.waterheater.cxeb1:\n  ts: 1611826388\nsmith.waterpuri.jnt600:\n  ts: 1531108800\nsoocare.toothbrush.m1:\n  ts: 1575097876\nsoocare.toothbrush.m1s:\n  ts: 1610611310\nsoocare.toothbrush.mc1:\n  ts: 1575097876\nsoocare.toothbrush.t501:\n  ts: 1672192586\nsoocare.toothbrush.x3:\n  ts: 1575097876\nsxds.pillow.pillow02:\n  ts: 1611222235\nsyniot.curtain.syc1:\n  ts: 1608794071\ntinymu.toilet.ailid:\n  ts: 1615292165\ntinymu.toiletlid.v1:\n  ts: 1608791137\ntokit.cooker.press1:\n  ts: 1614842943\ntokit.cooker.tk20l01:\n  ts: 1646646903\ntokit.cooker.tk4001:\n  ts: 1639651673\ntokit.ihcooker.tkpro1:\n  ts: 1624533824\ntokit.ihcooker.tkv1:\n  ts: 1614667669\ntokit.oven.tk12l01:\n  ts: 1616729526\ntokit.oven.tk32pro1:\n  ts: 1617002408\ntokit.pre_cooker.tkih1:\n  ts: 1607410832\ntrios1.bleshoes.v02:\n  ts: 1602662599\ntxdd.wifispeaker.x1:\n  ts: 1575097876\nviomi.aircondition.v10:\n  ts: 1606375041\nviomi.aircondition.v21:\n  ts: 1610608182\nviomi.aircondition.v22:\n  ts: 1610608198\nviomi.aircondition.v23:\n  ts: 1610608214\nviomi.aircondition.v24:\n  ts: 1610608242\nviomi.aircondition.v25:\n  ts: 1610608276\nviomi.aircondition.v26:\n  ts: 1609924120\nviomi.aircondition.v6:\n  ts: 1608188576\nviomi.aircondition.v7:\n  ts: 1608794437\nviomi.aircondition.v8:\n  ts: 1606376969\nviomi.aircondition.v9:\n  ts: 1608794507\nviomi.bhf_light.v1:\n  ts: 1614308454\nviomi.cooker.v1:\n  ts: 1607410870\nviomi.cooker.v2:\n  ts: 1607410911\nviomi.curtain.v1:\n  ts: 1658732587\nviomi.dishwasher.m01:\n  ts: 1610605899\nviomi.dishwasher.m02:\n  ts: 1610605964\nviomi.dishwasher.v01:\n  ts: 1614307805\nviomi.dishwasher.v03:\n  ts: 1614308206\nviomi.dishwasher.v05:\n  ts: 1610606831\nviomi.fridge.m1:\n  ts: 1614667789\nviomi.fridge.p1:\n  ts: 1614655704\nviomi.fridge.u1:\n  ts: 1614667927\nviomi.fridge.u12:\n  ts: 1614666058\nviomi.fridge.u13:\n  ts: 1614667152\nviomi.fridge.u15:\n  ts: 1607505693\nviomi.fridge.u17:\n  ts: 1575097876\nviomi.fridge.u18:\n  ts: 1614655755\nviomi.fridge.u2:\n  ts: 1531108800\nviomi.fridge.u24:\n  ts: 1614667214\nviomi.fridge.u25:\n  ts: 1575097876\nviomi.fridge.u4:\n  ts: 1614667295\nviomi.fridge.u6:\n  ts: 1614667319\nviomi.fridge.u7:\n  ts: 1614667341\nviomi.fridge.u8:\n  ts: 1624589504\nviomi.fridge.v3:\n  ts: 1614667699\nviomi.fridge.w1:\n  ts: 1607505903\nviomi.fridge.w2:\n  ts: 1614655838\nviomi.fridge.x11:\n  ts: 1614655870\nviomi.fridge.x12:\n  ts: 1614656914\nviomi.fridge.x4:\n  ts: 1614223422\nviomi.fridge.x7:\n  ts: 1637305932\nviomi.health_pot.v1:\n  ts: 1607502489\nviomi.hood.a10:\n  ts: 1611823231\nviomi.hood.a9:\n  ts: 1611823172\nviomi.hood.c1:\n  ts: 1611220032\nviomi.hood.c2:\n  ts: 1611823145\nviomi.hood.h1:\n  ts: 1610612690\nviomi.hood.h3:\n  ts: 1611823101\nviomi.hood.h4:\n  ts: 1611823078\nviomi.hood.v1:\n  ts: 1684390912\nviomi.i_stove.v1:\n  ts: 1611825720\nviomi.i_stove.v3:\n  ts: 1606372593\nviomi.juicer.v1:\n  ts: 1619688553\nviomi.juicer.v2:\n  ts: 1607504511\nviomi.lock.lbt14a:\n  ts: 1617940506\nviomi.lock.lbt41e:\n  ts: 1633750723\nviomi.lock.lbt48a:\n  ts: 1648788518\nviomi.lock.lbt51a:\n  ts: 1648536993\nviomi.lock.link1:\n  ts: 1634546007\nviomi.lock.link2:\n  ts: 1614740735\nviomi.lock.link2p:\n  ts: 1626166712\nviomi.lock.link2v:\n  ts: 1626166615\nviomi.lock.link3:\n  ts: 1639967286\nviomi.lock.link4s:\n  ts: 1681980137\nviomi.oven.so1:\n  ts: 1621243328\nviomi.oven.so2:\n  ts: 1611816582\nviomi.steriliser.v1:\n  ts: 1607503139\nviomi.vacuum.v10:\n  ts: 1619171223\nviomi.vacuum.v3:\n  ts: 1619171356\nviomi.vacuum.v31:\n  ts: 1624348346\nviomi.vacuum.v6:\n  ts: 1620720046\nviomi.vacuum.v7:\n  ts: 1618969602\nviomi.vacuum.v9:\n  ts: 1626232015\nviomi.washer.s1:\n  ts: 1616644328\nviomi.washer.u1:\n  ts: 1648122511\nviomi.washer.u2:\n  ts: 1632886871\nviomi.washer.u3:\n  ts: 1616644429\nviomi.washer.u4:\n  ts: 1616644467\nviomi.washer.u5:\n  ts: 1616647921\nviomi.washer.v10:\n  ts: 1616647971\nviomi.washer.v11:\n  ts: 1616644507\nviomi.washer.v4:\n  ts: 1616644547\nviomi.washer.v5:\n  ts: 1616407442\nviomi.washer.v6:\n  ts: 1616644619\nviomi.washer.v7:\n  ts: 1616644253\nviomi.washer.v8:\n  ts: 1616644309\nviomi.waterheater.e1:\n  ts: 1615343927\nviomi.waterheater.e3:\n  ts: 1615344258\nviomi.waterheater.e4:\n  ts: 1615345547\nviomi.waterheater.e7:\n  ts: 1604589755\nviomi.waterheater.e8:\n  ts: 1606982993\nviomi.waterheater.u1:\n  ts: 1616648390\nviomi.waterheater.u10:\n  ts: 1606984271\nviomi.waterheater.u11:\n  ts: 1606984299\nviomi.waterheater.u12:\n  ts: 1606376892\nviomi.waterheater.u15:\n  ts: 1646394494\nviomi.waterheater.u16:\n  ts: 1637304955\nviomi.waterheater.u3:\n  ts: 1620811497\nviomi.waterheater.u4:\n  ts: 1615864332\nviomi.waterheater.u6:\n  ts: 1615442495\nviomi.waterheater.u7:\n  ts: 1605857988\nviomi.waterheater.u8:\n  ts: 1615864849\nxiaomi.aircondition.ma1:\n  ts: 1721628903\nxiaomi.aircondition.ma2:\n  ts: 1721181139\nxiaomi.aircondition.ma4:\n  ts: 1726230966\nxiaomi.aircondition.ma5:\n  ts: 1721629118\nxiaomi.aircondition.ma6:\n  ts: 1721629272\nxiaomi.aircondition.ma9:\n  ts: 1721629362\nxiaomi.plc.v1:\n  ts: 1575097876\nxiaomi.repeater.v1:\n  ts: 1575097876\nxiaomi.repeater.v2:\n  ts: 1575097876\nxiaomi.repeater.v3:\n  ts: 1575097876\nxiaomi.router.d01:\n  ts: 1575097876\nxiaomi.router.lv1:\n  ts: 1575097876\nxiaomi.router.lv3:\n  ts: 1575097876\nxiaomi.router.mv1:\n  ts: 1575097876\nxiaomi.router.r2100:\n  ts: 1575097876\nxiaomi.router.r3600:\n  ts: 1575097876\nxiaomi.router.r3a:\n  ts: 1575097876\nxiaomi.router.r3d:\n  ts: 1575097876\nxiaomi.router.r3g:\n  ts: 1575097876\nxiaomi.router.r3gv2:\n  ts: 1575097876\nxiaomi.router.r3gv2n:\n  ts: 1575097876\nxiaomi.router.r3p:\n  ts: 1575097876\nxiaomi.router.r4:\n  ts: 1575097876\nxiaomi.router.r4a:\n  ts: 1575097876\nxiaomi.router.r4ac:\n  ts: 1575097876\nxiaomi.router.r4c:\n  ts: 1575097876\nxiaomi.router.r4cm:\n  ts: 1575097876\nxiaomi.router.rm1800:\n  ts: 1575097876\nxiaomi.router.v1:\n  ts: 1575097876\nxiaomi.router.v2:\n  ts: 1575097876\nxiaomi.router.v3:\n  ts: 1575097876\nxiaomi.split_tv.b1:\n  ts: 1575097876\nxiaomi.split_tv.v1:\n  ts: 1575097876\nxiaomi.tv.b1:\n  ts: 1661248580\nxiaomi.tv.h1:\n  ts: 1575097876\nxiaomi.tv.i1:\n  ts: 1661248572\nxiaomi.tv.v1:\n  ts: 1670811870\nxiaomi.tvbox.b1:\n  ts: 1694503508\nxiaomi.tvbox.i1:\n  ts: 1694503515\nxiaomi.tvbox.v1:\n  ts: 1694503501\nxiaomi.watch.band1:\n  ts: 1575097876\nxiaomi.watch.band1A:\n  ts: 1575097876\nxiaomi.watch.band1S:\n  ts: 1575097876\nxiaomi.watch.band2:\n  ts: 1575097876\nxiaomi.wifispeaker.l04m:\n  ts: 1658817956\nxiaomi.wifispeaker.l06a:\n  ts: 1672731009\nxiaomi.wifispeaker.l09a:\n  ts: 1626336048\nxiaomi.wifispeaker.l7a:\n  ts: 1637306490\nxiaomi.wifispeaker.lx01:\n  ts: 1637306716\nxiaomi.wifispeaker.lx04:\n  ts: 1669695484\nxiaomi.wifispeaker.lx05:\n  ts: 1672299502\nxiaomi.wifispeaker.lx06:\n  ts: 1672299546\nxiaomi.wifispeaker.lx5a:\n  ts: 1672299577\nxiaomi.wifispeaker.s12:\n  ts: 1672299594\nxiaomi.wifispeaker.v1:\n  ts: 1575097876\nxiaomi.wifispeaker.v3:\n  ts: 1575097876\nxiaomi.wifispeaker.x08a:\n  ts: 1672818945\nxiaomi.wifispeaker.x08c:\n  ts: 1658819390\nxiaovv.camera.lamp:\n  ts: 1531108800\nxiaovv.camera.ptz:\n  ts: 1531108800\nxiaovv.camera.xva3:\n  ts: 1531108800\nxiaovv.camera.xvb4:\n  ts: 1531108800\nxiaovv.camera.xvd5:\n  ts: 1531108800\nxiaovv.camera.xvsnowman:\n  ts: 1531108800\nxiaoxun.robot.v1:\n  ts: 1575097876\nxiaoxun.tracker.v1:\n  ts: 1575097876\nxiaoxun.watch.sw306:\n  ts: 1575097876\nxiaoxun.watch.sw560:\n  ts: 1575097876\nxiaoxun.watch.sw705:\n  ts: 1575097876\nxiaoxun.watch.sw710a2:\n  ts: 1575097876\nxiaoxun.watch.sw760:\n  ts: 1575097876\nxiaoxun.watch.sw900:\n  ts: 1575097876\nxiaoxun.watch.sw960:\n  ts: 1575097876\nxiaoxun.watch.v1:\n  ts: 1575097876\nxiaoxun.watch.v10:\n  ts: 1575097876\nxiaoxun.watch.v11:\n  ts: 1575097876\nxiaoxun.watch.v2:\n  ts: 1575097876\nxiaoxun.watch.v3:\n  ts: 1575097876\nxiaoxun.watch.v4:\n  ts: 1575097876\nxiaoxun.watch.v5:\n  ts: 1575097876\nxiaoxun.watch.v7:\n  ts: 1575097876\nxiaoxun.watch.v8:\n  ts: 1575097876\nxiaoxun.watch.v9:\n  ts: 1575097876\nxjx.toilet.pro:\n  ts: 1615965466\nxjx.toilet.pure:\n  ts: 1615969114\nxjx.toilet.relax:\n  ts: 1615968257\nxjx.toilet.zero:\n  ts: 1618302169\nydhome.cateye.pr1:\n  ts: 1615360329\nydhome.lock.c1p:\n  ts: 1614740027\nydhome.lock.m2p:\n  ts: 1620728837\nydhome.lock.m2silver:\n  ts: 1614743037\nydzl.waterpuri.t1:\n  ts: 1609926143\nyeelink.bhf_light.v1:\n  ts: 1608790026\nyeelink.bhf_light.v2:\n  ts: 1608790085\nyeelink.bhf_light.v3:\n  ts: 1608790102\nyeelink.bhf_light.v5:\n  ts: 1601292562\nyeelink.gateway.v1:\n  ts: 1575097876\nyeelink.light.bslamp1:\n  ts: 1703120679\nyeelink.light.bslamp2:\n  ts: 1703120782\nyeelink.light.bslamp3:\n  ts: 1646394180\nyeelink.light.ceil27:\n  ts: 1646394323\nyeelink.light.ceiling1:\n  ts: 1626343431\nyeelink.light.ceiling10:\n  ts: 1646393927\nyeelink.light.ceiling11:\n  ts: 1626341443\nyeelink.light.ceiling12:\n  ts: 1646122053\nyeelink.light.ceiling13:\n  ts: 1626341533\nyeelink.light.ceiling14:\n  ts: 1626341941\nyeelink.light.ceiling15:\n  ts: 1626342080\nyeelink.light.ceiling16:\n  ts: 1626342123\nyeelink.light.ceiling17:\n  ts: 1626342230\nyeelink.light.ceiling18:\n  ts: 1626342296\nyeelink.light.ceiling19:\n  ts: 1626342431\nyeelink.light.ceiling2:\n  ts: 1637308139\nyeelink.light.ceiling20:\n  ts: 1626339402\nyeelink.light.ceiling21:\n  ts: 1703121290\nyeelink.light.ceiling22:\n  ts: 1703121209\nyeelink.light.ceiling23:\n  ts: 1703120912\nyeelink.light.ceiling24:\n  ts: 1626343504\nyeelink.light.ceiling3:\n  ts: 1626343459\nyeelink.light.ceiling4:\n  ts: 1646393658\nyeelink.light.ceiling5:\n  ts: 1626342461\nyeelink.light.ceiling6:\n  ts: 1626343314\nyeelink.light.ceiling7:\n  ts: 1626343090\nyeelink.light.ceiling8:\n  ts: 1626343130\nyeelink.light.ceiling9:\n  ts: 1626343286\nyeelink.light.color1:\n  ts: 1626343469\nyeelink.light.color2:\n  ts: 1626343174\nyeelink.light.color3:\n  ts: 1626166445\nyeelink.light.color4:\n  ts: 1626343343\nyeelink.light.color5:\n  ts: 1626227410\nyeelink.light.color6:\n  ts: 1637309445\nyeelink.light.color7:\n  ts: 1626227373\nyeelink.light.color8:\n  ts: 1626343365\nyeelink.light.ct2:\n  ts: 1626343390\nyeelink.light.lamp1:\n  ts: 1626342474\nyeelink.light.lamp10:\n  ts: 1646122763\nyeelink.light.lamp2:\n  ts: 1638589912\nyeelink.light.lamp3:\n  ts: 1626343245\nyeelink.light.lamp4:\n  ts: 1629022996\nyeelink.light.lamp5:\n  ts: 1626343399\nyeelink.light.lamp7:\n  ts: 1626418317\nyeelink.light.lamp9:\n  ts: 1626343409\nyeelink.light.mono1:\n  ts: 1626343418\nyeelink.light.mono4:\n  ts: 1626343373\nyeelink.light.mono5:\n  ts: 1626343352\nyeelink.light.nl1:\n  ts: 1639479487\nyeelink.light.panel1:\n  ts: 1626343379\nyeelink.light.panel3:\n  ts: 1626343334\nyeelink.light.strip1:\n  ts: 1626343486\nyeelink.light.strip2:\n  ts: 1732605062\nyeelink.light.strip4:\n  ts: 1626342354\nyeelink.light.strip6:\n  ts: 1726730787\nyeelink.mirror.bm1:\n  ts: 1607504924\nyeelink.remote.remote:\n  ts: 1608186464\nyeelink.ven_fan.vf1:\n  ts: 1621237766\nyeelink.wifispeaker.v1:\n  ts: 1611818727\nyilai.light.ceiling1:\n  ts: 1603354689\nyilai.light.ceiling2:\n  ts: 1623911828\nyilai.light.ceiling3:\n  ts: 1606986226\nyuemee.airmonitor.mhfd1:\n  ts: 1619608729\nyunlu.door.sd2101:\n  ts: 1656661489\nyunlu.door.sd2103:\n  ts: 1657762233\nyunlu.door.sd2104:\n  ts: 1658317727\nyunmi.kettle.r1:\n  ts: 1614648943\nyunmi.kettle.r2:\n  ts: 1606372087\nyunmi.kettle.r3:\n  ts: 1637309534\nyunmi.kettle.v1:\n  ts: 1575097876\nyunmi.kettle.v9:\n  ts: 1602662686\nyunmi.plmachine.mg2:\n  ts: 1611833658\nyunmi.waterpuri.c5:\n  ts: 1604588322\nyunmi.waterpuri.lx11:\n  ts: 1604588450\nyunmi.waterpuri.lx12:\n  ts: 1722239438\nyunmi.waterpuri.lx2:\n  ts: 1614155130\nyunmi.waterpuri.lx3:\n  ts: 1606371237\nyunmi.waterpuri.lx4:\n  ts: 1606978913\nyunmi.waterpuri.lx5:\n  ts: 1603963118\nyunmi.waterpuri.lx6:\n  ts: 1606371311\nyunmi.waterpuri.lx7:\n  ts: 1603963241\nyunmi.waterpuri.lx8:\n  ts: 1603963353\nyunmi.waterpuri.lx9:\n  ts: 1607505784\nyunmi.waterpuri.s3:\n  ts: 1614303228\nyunmi.waterpuri.s4:\n  ts: 1611221280\nyunmi.waterpuri.s5:\n  ts: 1607501816\nyunmi.waterpuri.x2:\n  ts: 1603963460\nyunmi.waterpuri.x7:\n  ts: 1603963565\nyunmi.waterpurifier.v2:\n  ts: 1632377061\nyunmi.waterpurifier.v3:\n  ts: 1611221428\nyunyi.camera.v1:\n  ts: 1575097876\nyyunyi.wopener.yypy24:\n  ts: 1616741966\nyyzhn.gateway.yn181126:\n  ts: 1610689325\nzdeer.ajh.a8:\n  ts: 1531108800\nzdeer.ajh.a9:\n  ts: 1531108800\nzdeer.ajh.ajb:\n  ts: 1608276454\nzdeer.ajh.zda10:\n  ts: 1531108800\nzdeer.ajh.zda9:\n  ts: 1531108800\nzdeer.ajh.zjy:\n  ts: 1531108800\nzhij.toothbrush.bv1:\n  ts: 1575097876\nzhimi.aircondition.ma1:\n  ts: 1615185265\nzhimi.aircondition.ma3:\n  ts: 1615185279\nzhimi.aircondition.ma4:\n  ts: 1626334057\nzhimi.aircondition.v1:\n  ts: 1610610931\nzhimi.aircondition.v2:\n  ts: 1575097876\nzhimi.aircondition.va1:\n  ts: 1609924720\nzhimi.aircondition.za1:\n  ts: 1626334343\nzhimi.aircondition.za2:\n  ts: 1626334315\nzhimi.airfresh.va2:\n  ts: 1690860366\nzhimi.airfresh.va4:\n  ts: 1690860327\nzhimi.airmonitor.v1:\n  ts: 1621243516\nzhimi.airpurifier.m1:\n  ts: 1636711231\nzhimi.airpurifier.m2:\n  ts: 1636711316\nzhimi.airpurifier.ma2:\n  ts: 1636962519\nzhimi.airpurifier.mb1:\n  ts: 1604588729\nzhimi.airpurifier.mc1:\n  ts: 1644487928\nzhimi.airpurifier.sa2:\n  ts: 1635820002\nzhimi.airpurifier.v1:\n  ts: 1635855633\nzhimi.airpurifier.v2:\n  ts: 1575097876\nzhimi.airpurifier.v3:\n  ts: 1676339933\nzhimi.airpurifier.v5:\n  ts: 1575097876\nzhimi.airpurifier.v6:\n  ts: 1636978652\nzhimi.airpurifier.v7:\n  ts: 1605856617\nzhimi.airpurifier.v8:\n  ts: 1626168185\nzhimi.fan.sa1:\n  ts: 1700644763\nzhimi.fan.v2:\n  ts: 1615975066\nzhimi.fan.v3:\n  ts: 1689041686\nzhimi.fan.za1:\n  ts: 1604590500\nzhimi.fan.za3:\n  ts: 1689040855\nzhimi.fan.za4:\n  ts: 1689040869\nzhimi.heater.ma1:\n  ts: 1645683725\nzhimi.heater.za1:\n  ts: 1645683820\nzhimi.humidifier.ca1:\n  ts: 1636099783\nzhimi.humidifier.v1:\n  ts: 1639639102\nzhimi.lock.da1:\n  ts: 1614741765\nzhimi.lock.da2:\n  ts: 1614741779\nzhimi.toilet.sa1:\n  ts: 1608186583\nzhimi.toilet.va1:\n  ts: 1607503081\nzimi.clock.myk01:\n  ts: 1531108800\nzimi.mosq.v1:\n  ts: 1620728957\nzimi.powerstrip.v2:\n  ts: 1620812714\nzimi.projector.v1:\n  ts: 1575097876\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT client instance.\n\"\"\"\nfrom copy import deepcopy\nfrom typing import Any, Callable, Optional, final\nimport asyncio\nimport json\nimport logging\nimport time\nimport traceback\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\n\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.components import zeroconf\n\n# pylint: disable=relative-beyond-top-level\nfrom .common import MIoTMatcher, slugify_did\nfrom .const import (\n    DEFAULT_CTRL_MODE, DEFAULT_INTEGRATION_LANGUAGE, DEFAULT_NICK_NAME, DOMAIN,\n    MIHOME_CERT_EXPIRE_MARGIN, NETWORK_REFRESH_INTERVAL,\n    OAUTH2_CLIENT_ID, SUPPORT_CENTRAL_GATEWAY_CTRL,\n    DEFAULT_COVER_DEAD_ZONE_WIDTH)\nfrom .miot_cloud import MIoTHttpClient, MIoTOauthClient\nfrom .miot_error import MIoTClientError, MIoTErrorCode\nfrom .miot_mips import (\n    MIoTDeviceState, MipsCloudClient, MipsDeviceState,\n    MipsLocalClient)\nfrom .miot_lan import MIoTLan\nfrom .miot_network import MIoTNetwork\nfrom .miot_storage import MIoTCert, MIoTStorage\nfrom .miot_mdns import MipsService, MipsServiceState\nfrom .miot_i18n import MIoTI18n\n\n_LOGGER = logging.getLogger(__name__)\n\n\nREFRESH_PROPS_DELAY = 0.2\nREFRESH_PROPS_RETRY_DELAY = 3\nREFRESH_CLOUD_DEVICES_DELAY = 6\nREFRESH_CLOUD_DEVICES_RETRY_DELAY = 60\nREFRESH_GATEWAY_DEVICES_DELAY = 3\n\n@dataclass\nclass MIoTClientSub:\n    \"\"\"MIoT client subscription.\"\"\"\n    topic: Optional[str]\n    handler: Callable[[dict, Any], None]\n    handler_ctx: Any = None\n\n    def __str__(self) -> str:\n        return f'{self.topic}, {id(self.handler)}, {id(self.handler_ctx)}'\n\n\nclass CtrlMode(Enum):\n    \"\"\"MIoT client control mode.\"\"\"\n    AUTO = 0\n    CLOUD = auto()\n\n    @staticmethod\n    def load(mode: str) -> 'CtrlMode':\n        if mode == 'auto':\n            return CtrlMode.AUTO\n        if mode == 'cloud':\n            return CtrlMode.CLOUD\n        raise MIoTClientError(f'unknown ctrl mode, {mode}')\n\n\nclass MIoTClient:\n    \"\"\"MIoT client instance.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=broad-exception-caught\n    # pylint: disable=inconsistent-quotes\n    _main_loop: asyncio.AbstractEventLoop\n\n    _uid: str\n    _entry_id: str\n    _entry_data: dict\n    _cloud_server: str\n    _ctrl_mode: CtrlMode\n    # MIoT network monitor\n    _network: MIoTNetwork\n    # MIoT storage client\n    _storage: MIoTStorage\n    # MIoT mips service\n    _mips_service: MipsService\n    # MIoT oauth client\n    _oauth: MIoTOauthClient\n    # MIoT http client\n    _http: MIoTHttpClient\n    # MIoT i18n client\n    _i18n: MIoTI18n\n    # MIoT cert client\n    _cert: MIoTCert\n    # User config, store in the .storage/xiaomi_home\n    _user_config: dict\n\n    # Multi local mips client, key=group_id\n    _mips_local: dict[str, MipsLocalClient]\n    # Cloud mips client\n    _mips_cloud: MipsCloudClient\n    # MIoT lan client\n    _miot_lan: MIoTLan\n\n    # Device list load from local storage, {did: <info>}\n    _device_list_cache: dict[str, dict]\n    # Device list obtained from cloud, {did: <info>}\n    _device_list_cloud: dict[str, dict]\n    # Device list obtained from gateway, {did: <info>}\n    _device_list_gateway: dict[str, dict]\n    # Device list scanned from LAN, {did: <info>}\n    _device_list_lan: dict[str, dict]\n    # Device list update timestamp\n    _device_list_update_ts: int\n\n    _sub_source_list: dict[str, Optional[str]]\n    _sub_tree: MIoTMatcher\n    _sub_device_state: dict[str, MipsDeviceState]\n\n    _mips_local_state_changed_timers: dict[str, asyncio.TimerHandle]\n    _refresh_token_timer: Optional[asyncio.TimerHandle]\n    _refresh_cert_timer: Optional[asyncio.TimerHandle]\n    _refresh_cloud_devices_timer: Optional[asyncio.TimerHandle]\n    # Refresh prop\n    _refresh_props_list: dict[str, dict]\n    _refresh_props_timer: Optional[asyncio.TimerHandle]\n    _refresh_props_retry_count: int\n\n    # Persistence notify handler, params: notify_id, title, message\n    _persistence_notify: Callable[[str, Optional[str], Optional[str]], None]\n    # Device list changed notify\n    _show_devices_changed_notify_timer: Optional[asyncio.TimerHandle]\n    # Display devices changed notify\n    _display_devs_notify: list[str]\n    _display_notify_content_hash: Optional[int]\n    # Display binary mode\n    _display_binary_text: bool\n    _display_binary_bool: bool\n\n    def __init__(\n            self,\n            entry_id: str,\n            entry_data: dict,\n            network: MIoTNetwork,\n            storage: MIoTStorage,\n            mips_service: MipsService,\n            miot_lan: MIoTLan,\n            loop: Optional[asyncio.AbstractEventLoop] = None) -> None:\n        # MUST run in a running event loop\n        self._main_loop = loop or asyncio.get_running_loop()\n        # Check params\n        if not isinstance(entry_data, dict):\n            raise MIoTClientError('invalid entry data')\n        if 'uid' not in entry_data or 'cloud_server' not in entry_data:\n            raise MIoTClientError('invalid entry data content')\n        if not isinstance(network, MIoTNetwork):\n            raise MIoTClientError('invalid miot network')\n        if not isinstance(storage, MIoTStorage):\n            raise MIoTClientError('invalid miot storage')\n        if not isinstance(mips_service, MipsService):\n            raise MIoTClientError('invalid mips service')\n        self._entry_id = entry_id\n        self._entry_data = entry_data\n        self._uid = entry_data['uid']\n        self._cloud_server = entry_data['cloud_server']\n        self._ctrl_mode = CtrlMode.load(\n            entry_data.get('ctrl_mode', DEFAULT_CTRL_MODE))\n        self._network = network\n        self._storage = storage\n        self._mips_service = mips_service\n        self._oauth = None\n        self._http = None\n        self._i18n = None\n        self._cert = None\n        self._user_config = None\n\n        self._mips_local = {}\n        self._mips_cloud = None\n        self._miot_lan = miot_lan\n\n        self._device_list_cache = {}\n        self._device_list_cloud = {}\n        self._device_list_gateway = {}\n        self._device_list_lan = {}\n        self._device_list_update_ts = 0\n        self._sub_source_list = {}\n        self._sub_tree = MIoTMatcher()\n        self._sub_device_state = {}\n\n        self._mips_local_state_changed_timers = {}\n        self._refresh_token_timer = None\n        self._refresh_cert_timer = None\n        self._refresh_cloud_devices_timer = None\n\n        # Refresh prop\n        self._refresh_props_list = {}\n        self._refresh_props_timer = None\n        self._refresh_props_retry_count = 0\n\n        self._persistence_notify = None\n        self._show_devices_changed_notify_timer = None\n\n        self._display_devs_notify = entry_data.get(\n            'display_devices_changed_notify', ['add', 'del', 'offline'])\n        self._display_notify_content_hash = None\n        self._display_binary_text = 'text' in entry_data.get(\n            'display_binary_mode', ['text'])\n        self._display_binary_bool = 'bool' in entry_data.get(\n            'display_binary_mode', ['text'])\n\n    async def init_async(self) -> None:\n        # Load user config and check\n        self._user_config = await self._storage.load_user_config_async(\n            uid=self._uid, cloud_server=self._cloud_server)\n        if not self._user_config:\n            # Integration need to be add again\n            raise MIoTClientError('load_user_config_async error')\n        # Hide sensitive info in printing\n        p_user_config: dict = deepcopy(self._user_config)\n        p_access_token: str = p_user_config['auth_info']['access_token']\n        p_refresh_token: str = p_user_config['auth_info']['refresh_token']\n        p_mac_key: str = p_user_config['auth_info']['mac_key']\n        p_user_config['auth_info'][\n            'access_token'] = f\"{p_access_token[:5]}***{p_access_token[-5:]}\"\n        p_user_config['auth_info'][\n            'refresh_token'] = f\"{p_refresh_token[:5]}***{p_refresh_token[-5:]}\"\n        p_user_config['auth_info'][\n            'mac_key'] = f\"{p_mac_key[:5]}***{p_mac_key[-5:]}\"\n        _LOGGER.debug('user config, %s', json.dumps(p_user_config))\n        # MIoT i18n client\n        self._i18n = MIoTI18n(\n            lang=self._entry_data.get(\n                'integration_language', DEFAULT_INTEGRATION_LANGUAGE),\n            loop=self._main_loop)\n        await self._i18n.init_async()\n        # Load cache device list\n        await self.__load_cache_device_async()\n        # MIoT oauth client instance\n        self._oauth = MIoTOauthClient(\n            client_id=OAUTH2_CLIENT_ID,\n            redirect_url=self._entry_data['oauth_redirect_url'],\n            cloud_server=self._cloud_server,\n            uuid=self._entry_data[\"uuid\"],\n            loop=self._main_loop)\n        # MIoT http client instance\n        self._http = MIoTHttpClient(\n            cloud_server=self._cloud_server,\n            client_id=OAUTH2_CLIENT_ID,\n            access_token=self._user_config['auth_info']['access_token'],\n            loop=self._main_loop)\n        # MIoT cert client\n        self._cert = MIoTCert(\n            storage=self._storage,\n            uid=self._uid,\n            cloud_server=self.cloud_server)\n        # MIoT cloud mips client\n        self._mips_cloud = MipsCloudClient(\n            uuid=self._entry_data['uuid'],\n            cloud_server=self._cloud_server,\n            app_id=OAUTH2_CLIENT_ID,\n            token=self._user_config['auth_info']['access_token'],\n            loop=self._main_loop)\n        self._mips_cloud.enable_logger(logger=_LOGGER)\n        self._mips_cloud.sub_mips_state(\n            key=f'{self._uid}-{self._cloud_server}',\n            handler=self.__on_mips_cloud_state_changed)\n        # Subscribe network status\n        self._network.sub_network_status(\n            key=f'{self._uid}-{self._cloud_server}',\n            handler=self.__on_network_status_changed)\n        await self.__on_network_status_changed(\n            status=self._network.network_status)\n        # Create multi mips local client instance according to the\n        # number of hub gateways\n        if self._ctrl_mode == CtrlMode.AUTO:\n            # Central hub gateway ctrl\n            if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL:\n                for home_id, info in self._entry_data['home_selected'].items():\n                    # Create local mips service changed listener\n                    self._mips_service.sub_service_change(\n                        key=f'{self._uid}-{self._cloud_server}',\n                        group_id=info['group_id'],\n                        handler=self.__on_mips_service_state_change)\n                    service_data = self._mips_service.get_services(\n                        group_id=info['group_id']).get(info['group_id'], None)\n                    if not service_data:\n                        _LOGGER.info(\n                            'central mips service not scanned, %s', home_id)\n                        continue\n                    _LOGGER.info(\n                        'central mips service scanned, %s, %s',\n                        home_id, service_data)\n                    mips = MipsLocalClient(\n                        did=self._entry_data['virtual_did'],\n                        group_id=info['group_id'],\n                        host=service_data['addresses'][0],\n                        ca_file=self._cert.ca_file,\n                        cert_file=self._cert.cert_file,\n                        key_file=self._cert.key_file,\n                        port=service_data['port'],\n                        home_name=info['home_name'],\n                        loop=self._main_loop)\n                    self._mips_local[info['group_id']] = mips\n                    mips.enable_logger(logger=_LOGGER)\n                    mips.on_dev_list_changed = self.__on_gw_device_list_changed\n                    mips.sub_mips_state(\n                        key=info['group_id'],\n                        handler=self.__on_mips_local_state_changed)\n                    mips.connect()\n            # Lan ctrl\n            await self._miot_lan.vote_for_lan_ctrl_async(\n                key=f'{self._uid}-{self._cloud_server}', vote=True)\n            self._miot_lan.sub_lan_state(\n                key=f'{self._uid}-{self._cloud_server}',\n                handler=self.__on_miot_lan_state_change)\n            if self._miot_lan.init_done:\n                await self.__on_miot_lan_state_change(True)\n        else:\n            self._miot_lan.unsub_lan_state(\n                key=f'{self._uid}-{self._cloud_server}')\n            if self._miot_lan.init_done:\n                self._miot_lan.unsub_device_state(\n                    key=f'{self._uid}-{self._cloud_server}')\n                self._miot_lan.delete_devices(\n                    devices=list(self._device_list_cache.keys()))\n            await self._miot_lan.vote_for_lan_ctrl_async(\n                key=f'{self._uid}-{self._cloud_server}', vote=False)\n\n        _LOGGER.info('init_async, %s, %s', self._uid, self._cloud_server)\n\n    async def deinit_async(self) -> None:\n        self._network.unsub_network_status(\n            key=f'{self._uid}-{self._cloud_server}')\n        # Cancel refresh props\n        if self._refresh_props_timer:\n            self._refresh_props_timer.cancel()\n            self._refresh_props_timer = None\n        self._refresh_props_list.clear()\n        self._refresh_props_retry_count = 0\n        # Cloud mips\n        self._mips_cloud.unsub_mips_state(\n            key=f'{self._uid}-{self._cloud_server}')\n        self._mips_cloud.deinit()\n        # Cancel refresh cloud devices\n        if self._refresh_cloud_devices_timer:\n            self._refresh_cloud_devices_timer.cancel()\n            self._refresh_cloud_devices_timer = None\n        if self._ctrl_mode == CtrlMode.AUTO:\n            # Central hub gateway mips\n            if self._cloud_server in SUPPORT_CENTRAL_GATEWAY_CTRL:\n                self._mips_service.unsub_service_change(\n                    key=f'{self._uid}-{self._cloud_server}')\n                for mips in self._mips_local.values():\n                    mips.on_dev_list_changed = None\n                    mips.unsub_mips_state(key=mips.group_id)\n                    mips.deinit()\n                if self._mips_local_state_changed_timers:\n                    for timer_item in (\n                            self._mips_local_state_changed_timers.values()):\n                        timer_item.cancel()\n                    self._mips_local_state_changed_timers.clear()\n            self._miot_lan.unsub_lan_state(\n                key=f'{self._uid}-{self._cloud_server}')\n            if self._miot_lan.init_done:\n                self._miot_lan.unsub_device_state(\n                    key=f'{self._uid}-{self._cloud_server}')\n                self._miot_lan.delete_devices(\n                    devices=list(self._device_list_cache.keys()))\n            await self._miot_lan.vote_for_lan_ctrl_async(\n                key=f'{self._uid}-{self._cloud_server}', vote=False)\n        # Cancel refresh auth info\n        if self._refresh_token_timer:\n            self._refresh_token_timer.cancel()\n            self._refresh_token_timer = None\n        if self._refresh_cert_timer:\n            self._refresh_cert_timer.cancel()\n            self._refresh_cert_timer = None\n        # Cancel device changed notify timer\n        if self._show_devices_changed_notify_timer:\n            self._show_devices_changed_notify_timer.cancel()\n            self._show_devices_changed_notify_timer = None\n        await self._oauth.deinit_async()\n        await self._http.deinit_async()\n        # Remove notify\n        self._persistence_notify(\n            self.__gen_notify_key('dev_list_changed'), None, None)\n        self.__show_client_error_notify(\n            message=None, notify_key='oauth_info')\n        self.__show_client_error_notify(\n            message=None, notify_key='user_cert')\n        self.__show_client_error_notify(\n            message=None, notify_key='device_cache')\n        self.__show_client_error_notify(\n            message=None, notify_key='device_cloud')\n\n        _LOGGER.info('deinit_async, %s', self._uid)\n\n    @property\n    def main_loop(self) -> asyncio.AbstractEventLoop:\n        return self._main_loop\n\n    @property\n    def miot_network(self) -> MIoTNetwork:\n        return self._network\n\n    @property\n    def miot_storage(self) -> MIoTStorage:\n        return self._storage\n\n    @property\n    def mips_service(self) -> MipsService:\n        return self._mips_service\n\n    @property\n    def miot_oauth(self) -> MIoTOauthClient:\n        return self._oauth\n\n    @property\n    def miot_http(self) -> MIoTHttpClient:\n        return self._http\n\n    @property\n    def miot_i18n(self) -> MIoTI18n:\n        return self._i18n\n\n    @property\n    def miot_lan(self) -> MIoTLan:\n        return self._miot_lan\n\n    @property\n    def user_config(self) -> dict:\n        return self._user_config\n\n    @property\n    def area_name_rule(self) -> Optional[str]:\n        return self._entry_data.get('area_name_rule', None)\n\n    @property\n    def cloud_server(self) -> str:\n        return self._cloud_server\n\n    @property\n    def action_debug(self) -> bool:\n        return self._entry_data.get('action_debug', False)\n\n    @property\n    def hide_non_standard_entities(self) -> bool:\n        return self._entry_data.get(\n            'hide_non_standard_entities', False)\n\n    @property\n    def display_devices_changed_notify(self) -> list[str]:\n        return self._display_devs_notify\n\n    @property\n    def display_binary_text(self) -> bool:\n        return self._display_binary_text\n\n    @property\n    def display_binary_bool(self) -> bool:\n        return self._display_binary_bool\n\n    @property\n    def cover_dead_zone_width(self) -> int:\n        return self._entry_data.get('cover_dead_zone_width',\n                                    DEFAULT_COVER_DEAD_ZONE_WIDTH)\n\n    @display_devices_changed_notify.setter\n    def display_devices_changed_notify(self, value: list[str]) -> None:\n        if set(value) == set(self._display_devs_notify):\n            return\n        self._display_devs_notify = value\n        if value:\n            self.__request_show_devices_changed_notify()\n        else:\n            self._persistence_notify(\n                self.__gen_notify_key('dev_list_changed'), None, None)\n\n    @property\n    def device_list(self) -> dict:\n        return self._device_list_cache\n\n    @property\n    def persistent_notify(self) -> Callable:\n        return self._persistence_notify\n\n    @persistent_notify.setter\n    def persistent_notify(self, func) -> None:\n        self._persistence_notify = func\n\n    @final\n    async def refresh_oauth_info_async(self) -> bool:\n        try:\n            # Load auth info\n            auth_info: Optional[dict] = None\n            user_config: dict = await self._storage.load_user_config_async(\n                uid=self._uid, cloud_server=self._cloud_server,\n                keys=['auth_info'])\n            if (\n                not user_config\n                or (auth_info := user_config.get('auth_info', None)) is None\n            ):\n                raise MIoTClientError('load_user_config_async error')\n            if (\n                'expires_ts' not in auth_info\n                or 'access_token' not in auth_info\n                or 'refresh_token' not in auth_info\n            ):\n                raise MIoTClientError('invalid auth info')\n            # Determine whether to update token\n            refresh_time = int(auth_info['expires_ts'] - time.time())\n            if refresh_time <= 60:\n                valid_auth_info = await self._oauth.refresh_access_token_async(\n                    refresh_token=auth_info['refresh_token'])\n                auth_info = valid_auth_info\n                # Update http token\n                self._http.update_http_header(\n                    access_token=valid_auth_info['access_token'])\n                # Update mips cloud token\n                self._mips_cloud.update_access_token(\n                    access_token=valid_auth_info['access_token'])\n                # Update storage\n                if not await self._storage.update_user_config_async(\n                        uid=self._uid, cloud_server=self._cloud_server,\n                        config={'auth_info': auth_info}):\n                    raise MIoTClientError('update_user_config_async error')\n                _LOGGER.info(\n                    'refresh oauth info, get new access_token, %s',\n                    auth_info)\n                refresh_time = int(auth_info['expires_ts'] - time.time())\n                if refresh_time <= 0:\n                    raise MIoTClientError('invalid expires time')\n            self.__show_client_error_notify(None, 'oauth_info')\n            self.__request_refresh_auth_info(refresh_time)\n\n            _LOGGER.debug(\n                'refresh oauth info (%s, %s) after %ds',\n                self._uid, self._cloud_server, refresh_time)\n            return True\n        except Exception as err:\n            self.__show_client_error_notify(\n                message=self._i18n.translate(\n                    'miot.client.invalid_oauth_info'),  # type: ignore\n                notify_key='oauth_info')\n            _LOGGER.error(\n                'refresh oauth info error (%s, %s), %s, %s',\n                self._uid, self._cloud_server, err, traceback.format_exc())\n        return False\n\n    async def refresh_user_cert_async(self) -> bool:\n        try:\n            if self._cloud_server not in SUPPORT_CENTRAL_GATEWAY_CTRL:\n                return True\n            if not await self._cert.verify_ca_cert_async():\n                raise MIoTClientError('ca cert is not ready')\n            refresh_time = (\n                await self._cert.user_cert_remaining_time_async() -\n                MIHOME_CERT_EXPIRE_MARGIN)\n            if refresh_time <= 60:\n                user_key = await self._cert.load_user_key_async()\n                if not user_key:\n                    user_key = self._cert.gen_user_key()\n                    if not await self._cert.update_user_key_async(key=user_key):\n                        raise MIoTClientError('update_user_key_async failed')\n                csr_str = self._cert.gen_user_csr(\n                    user_key=user_key, did=self._entry_data['virtual_did'])\n                crt_str = await self.miot_http.get_central_cert_async(csr_str)\n                if not await self._cert.update_user_cert_async(cert=crt_str):\n                    raise MIoTClientError('update user cert error')\n                _LOGGER.info('update_user_cert_async, %s', crt_str)\n                # Create cert update task\n                refresh_time = (\n                    await self._cert.user_cert_remaining_time_async() -\n                    MIHOME_CERT_EXPIRE_MARGIN)\n                if refresh_time <= 0:\n                    raise MIoTClientError('invalid refresh time')\n            self.__show_client_error_notify(None, 'user_cert')\n            self.__request_refresh_user_cert(refresh_time)\n\n            _LOGGER.debug(\n                'refresh user cert (%s, %s) after %ds',\n                self._uid, self._cloud_server, refresh_time)\n            return True\n        except MIoTClientError as error:\n            self.__show_client_error_notify(\n                message=self._i18n.translate(\n                    'miot.client.invalid_cert_info'),  # type: ignore\n                notify_key='user_cert')\n            _LOGGER.error(\n                'refresh user cert error, %s, %s',\n                error, traceback.format_exc())\n        return False\n\n    async def set_prop_async(\n        self, did: str, siid: int, piid: int, value: Any\n    ) -> bool:\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n        # Priority local control\n        if self._ctrl_mode == CtrlMode.AUTO:\n            # Gateway control\n            device_gw = self._device_list_gateway.get(did, None)\n            if (\n                device_gw and device_gw.get('online', False)\n                and device_gw.get('specv2_access', False)\n                and 'group_id' in device_gw\n            ):\n                mips = self._mips_local.get(device_gw['group_id'], None)\n                if mips is None:\n                    _LOGGER.error(\n                        'no gateway route, %s, try control through cloud',\n                        device_gw)\n                else:\n                    result = await mips.set_prop_async(\n                        did=did, siid=siid, piid=piid, value=value)\n                    _LOGGER.debug(\n                        'gateway set prop, %s.%d.%d, %s -> %s',\n                        did, siid, piid, value, result)\n                    rc = (result or {}).get(\n                        'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)\n                    if rc in [0, 1]:\n                        return True\n                    raise MIoTClientError(\n                        self.__get_exec_error_with_rc(rc=rc))\n            # Lan control\n            device_lan = self._device_list_lan.get(did, None)\n            if device_lan and device_lan.get('online', False):\n                result = await self._miot_lan.set_prop_async(\n                    did=did, siid=siid, piid=piid, value=value)\n                _LOGGER.debug(\n                    'lan set prop, %s.%d.%d, %s -> %s',\n                    did, siid, piid, value, result)\n                rc = (result or {}).get(\n                    'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)\n                if rc in [0, 1]:\n                    return True\n                raise MIoTClientError(\n                    self.__get_exec_error_with_rc(rc=rc))\n\n        # Cloud control\n        device_cloud = self._device_list_cloud.get(did, None)\n        if device_cloud and device_cloud.get('online', False):\n            result = await self._http.set_prop_async(\n                params=[\n                    {'did': did, 'siid': siid, 'piid': piid, 'value': value}\n                ])\n            _LOGGER.debug(\n                'cloud set prop, %s.%d.%d, %s -> %s',\n                did, siid, piid, value, result)\n            if result and len(result) == 1:\n                rc = result[0].get(\n                    'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)\n                if rc in [0, 1]:\n                    return True\n                if rc in [-704010000, -704042011]:\n                    # Device remove or offline\n                    _LOGGER.error('device may be removed or offline, %s', did)\n                    self._main_loop.create_task(\n                        await self.__refresh_cloud_device_with_dids_async(\n                            dids=[did]))\n                raise MIoTClientError(\n                    self.__get_exec_error_with_rc(rc=rc))\n\n        # Show error message\n        raise MIoTClientError(\n            f'{self._i18n.translate(\"miot.client.device_exec_error\")}, '\n            f'{self._i18n.translate(\"error.common.-10007\")}')\n\n    def request_refresh_prop(\n        self, did: str, siid: int, piid: int\n    ) -> None:\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n        key: str = f'{did}|{siid}|{piid}'\n        if key in self._refresh_props_list:\n            return\n        self._refresh_props_list[key] = {\n            'did': did, 'siid': siid, 'piid': piid}\n        if self._refresh_props_timer:\n            return\n        self._refresh_props_timer = self._main_loop.call_later(\n            REFRESH_PROPS_DELAY, lambda: self._main_loop.create_task(\n                self.__refresh_props_handler()))\n\n    async def get_prop_async(self, did: str, siid: int, piid: int) -> Any:\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n\n        # NOTICE: Since there are too many request attributes and obtaining\n        # them directly from the hub or device will cause device abnormalities,\n        # so obtaining the cache from the cloud is the priority here.\n        try:\n            if self._network.network_status:\n                result = await self._http.get_prop_async(\n                    did=did, siid=siid, piid=piid)\n                if result:\n                    return result\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            # Catch all exceptions\n            _LOGGER.error(\n                'client get prop from cloud error, %s, %s',\n                err, traceback.format_exc())\n        if self._ctrl_mode == CtrlMode.AUTO:\n            # Central hub gateway\n            device_gw = self._device_list_gateway.get(did, None)\n            if (\n                device_gw and device_gw.get('online', False)\n                and device_gw.get('specv2_access', False)\n                and 'group_id' in device_gw\n            ):\n                mips = self._mips_local.get(device_gw['group_id'], None)\n                if mips is None:\n                    _LOGGER.error('no gw route, %s', device_gw)\n                else:\n                    return await mips.get_prop_async(\n                        did=did, siid=siid, piid=piid)\n            # Lan\n            device_lan = self._device_list_lan.get(did, None)\n            if device_lan and device_lan.get('online', False):\n                return await self._miot_lan.get_prop_async(\n                    did=did, siid=siid, piid=piid)\n        # _LOGGER.error(\n        #     'client get prop failed, no-link, %s.%d.%d', did, siid, piid)\n        return None\n\n    async def action_async(\n        self, did: str, siid: int, aiid: int, in_list: list\n    ) -> list:\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n\n        device_gw = self._device_list_gateway.get(did, None)\n        # Priority local control\n        if self._ctrl_mode == CtrlMode.AUTO:\n            if (\n                device_gw and device_gw.get('online', False)\n                and device_gw.get('specv2_access', False)\n                and 'group_id' in device_gw\n            ):\n                mips = self._mips_local.get(\n                    device_gw['group_id'], None)\n                if mips is None:\n                    _LOGGER.error('no gw route, %s', device_gw)\n                else:\n                    result = await mips.action_async(\n                        did=did, siid=siid, aiid=aiid, in_list=in_list)\n                    rc = (result or {}).get(\n                        'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)\n                    if rc in [0, 1]:\n                        return result.get('out', [])\n                    raise MIoTClientError(\n                        self.__get_exec_error_with_rc(rc=rc))\n            # Lan control\n            device_lan = self._device_list_lan.get(did, None)\n            if device_lan and device_lan.get('online', False):\n                result = await self._miot_lan.action_async(\n                    did=did, siid=siid, aiid=aiid, in_list=in_list)\n                _LOGGER.debug(\n                    'lan action, %s, %s, %s -> %s', did, siid, aiid, result)\n                rc = (result or {}).get(\n                    'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)\n                if rc in [0, 1]:\n                    return result.get('out', [])\n                raise MIoTClientError(\n                    self.__get_exec_error_with_rc(rc=rc))\n        # Cloud control\n        device_cloud = self._device_list_cloud.get(did, None)\n        if device_cloud and device_cloud.get('online', False):\n            result: dict = await self._http.action_async(\n                did=did, siid=siid, aiid=aiid, in_list=in_list)\n            if result:\n                rc = result.get(\n                    'code', MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value)\n                if rc in [0, 1]:\n                    return result.get('out', [])\n                if rc in [-704010000, -704042011]:\n                    # Device remove or offline\n                    _LOGGER.error('device removed or offline, %s', did)\n                    self._main_loop.create_task(\n                        await self.__refresh_cloud_device_with_dids_async(\n                            dids=[did]))\n                raise MIoTClientError(\n                    self.__get_exec_error_with_rc(rc=rc))\n        # TODO: Show error message\n        _LOGGER.error(\n            'client action failed, %s.%d.%d', did, siid, aiid)\n        return []\n\n    def sub_prop(\n        self, did: str, handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None, piid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n\n        topic = (\n            f'{did}/p/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}/{piid}\"}')\n        self._sub_tree[topic] = MIoTClientSub(\n            topic=topic, handler=handler, handler_ctx=handler_ctx)\n        _LOGGER.debug('client sub prop, %s', topic)\n        return True\n\n    def unsub_prop(\n        self, did: str, siid: Optional[int] = None, piid: Optional[int] = None\n    ) -> bool:\n        topic = (\n            f'{did}/p/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}/{piid}\"}')\n        if self._sub_tree.get(topic=topic):\n            del self._sub_tree[topic]\n        _LOGGER.debug('client unsub prop, %s', topic)\n        return True\n\n    def sub_event(\n        self, did: str, handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None, eiid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n        topic = (\n            f'{did}/e/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}/{eiid}\"}')\n        self._sub_tree[topic] = MIoTClientSub(\n            topic=topic, handler=handler, handler_ctx=handler_ctx)\n        _LOGGER.debug('client sub event, %s', topic)\n        return True\n\n    def unsub_event(\n        self, did: str, siid: Optional[int] = None, eiid: Optional[int] = None\n    ) -> bool:\n        topic = (\n            f'{did}/e/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}/{eiid}\"}')\n        if self._sub_tree.get(topic=topic):\n            del self._sub_tree[topic]\n        _LOGGER.debug('client unsub event, %s', topic)\n        return True\n\n    def sub_device_state(\n        self, did: str, handler: Callable[[str, MIoTDeviceState, Any], None],\n        handler_ctx: Any = None\n    ) -> bool:\n        \"\"\"Call callback handler in main loop\"\"\"\n        if did not in self._device_list_cache:\n            raise MIoTClientError(f'did not exist, {did}')\n        self._sub_device_state[did] = MipsDeviceState(\n            did=did, handler=handler, handler_ctx=handler_ctx)\n        _LOGGER.debug('client sub device state, %s', did)\n        return True\n\n    def unsub_device_state(self, did: str) -> bool:\n        self._sub_device_state.pop(did, None)\n        _LOGGER.debug('client unsub device state, %s', did)\n        return True\n\n    async def remove_device_async(self, did: str) -> None:\n        if did not in self._device_list_cache:\n            return\n        sub_from = self._sub_source_list.pop(did, None)\n        # Unsub\n        if sub_from:\n            self.__unsub_from(sub_from, did)\n        # Storage\n        await self._storage.save_async(\n            domain='miot_devices',\n            name=f'{self._uid}_{self._cloud_server}',\n            data=self._device_list_cache)\n        # Update notify\n        self.__request_show_devices_changed_notify()\n\n    async def remove_device2_async(self, did_tag: str) -> None:\n        for did in self._device_list_cache:\n            d_tag = slugify_did(cloud_server=self._cloud_server, did=did)\n            if did_tag == d_tag:\n                await self.remove_device_async(did)\n                break\n\n    def __get_exec_error_with_rc(self, rc: int) -> str:\n        err_msg: str = self._i18n.translate(\n            key=f'error.common.{rc}')  # type: ignore\n        if not err_msg:\n            err_msg = f'{self._i18n.translate(key=\"error.common.-10000\")}, '\n            err_msg += f'code={rc}'\n        return (\n            f'{self._i18n.translate(key=\"miot.client.device_exec_error\")}, '\n            + err_msg)\n\n    @final\n    def __gen_notify_key(self, name: str) -> str:\n        return f'{DOMAIN}-{self._uid}-{self._cloud_server}-{name}'\n\n    @final\n    def __request_refresh_auth_info(self, delay_sec: int) -> None:\n        if self._refresh_token_timer:\n            self._refresh_token_timer.cancel()\n            self._refresh_token_timer = None\n        self._refresh_token_timer = self._main_loop.call_later(\n            delay_sec, lambda: self._main_loop.create_task(\n                self.refresh_oauth_info_async()))\n\n    @final\n    def __request_refresh_user_cert(self, delay_sec: int) -> None:\n        if self._refresh_cert_timer:\n            self._refresh_cert_timer.cancel()\n            self._refresh_cert_timer = None\n        self._refresh_cert_timer = self._main_loop.call_later(\n            delay_sec, lambda: self._main_loop.create_task(\n                self.refresh_user_cert_async()))\n\n    @final\n    def __unsub_from(self, sub_from: str, did: str) -> None:\n        mips: Any = None\n        if sub_from == 'cloud':\n            mips = self._mips_cloud\n        elif sub_from == 'lan':\n            mips = self._miot_lan\n        elif sub_from in self._mips_local:\n            mips = self._mips_local[sub_from]\n        if mips is not None:\n            try:\n                mips.unsub_prop(did=did)\n                mips.unsub_event(did=did)\n            except RuntimeError as e:\n                if 'Event loop is closed' in str(e):\n                    # Ignore unsub exception when loop is closed\n                    pass\n                else:\n                    raise\n\n    @final\n    def __sub_from(self, sub_from: str, did: str) -> None:\n        mips = None\n        if sub_from == 'cloud':\n            mips = self._mips_cloud\n        elif sub_from == 'lan':\n            mips = self._miot_lan\n        elif sub_from in self._mips_local:\n            mips = self._mips_local[sub_from]\n        if mips is not None:\n            mips.sub_prop(did=did, handler=self.__on_prop_msg)\n            mips.sub_event(did=did, handler=self.__on_event_msg)\n\n    @final\n    def __update_device_msg_sub(self, did: str) -> None:\n        if did not in self._device_list_cache:\n            return\n        from_old: Optional[str] = self._sub_source_list.get(did, None)\n        from_new: Optional[str] = None\n        if self._ctrl_mode == CtrlMode.AUTO:\n            if (\n                did in self._device_list_gateway\n                and self._device_list_gateway[did].get('online', False)\n                and self._device_list_gateway[did].get('push_available', False)\n            ):\n                from_new = self._device_list_gateway[did]['group_id']\n            elif (\n                did in self._device_list_lan\n                and self._device_list_lan[did].get('online', False)\n                and self._device_list_lan[did].get('push_available', False)\n            ):\n                from_new = 'lan'\n\n        if (\n            from_new is None\n            and did in self._device_list_cloud\n            and self._device_list_cloud[did].get('online', False)\n        ):\n            from_new = 'cloud'\n        if (from_new == from_old) and (from_new=='cloud' or from_new=='lan'):\n            # No need to update\n            return\n        # Unsub old\n        if from_old:\n            self.__unsub_from(from_old, did)\n        # Sub new\n        self.__sub_from(from_new, did)\n        self._sub_source_list[did] = from_new\n        _LOGGER.info(\n            'device sub changed, %s, from %s to %s', did, from_old, from_new)\n\n    @final\n    async def __on_network_status_changed(self, status: bool) -> None:\n        _LOGGER.info('network status changed, %s', status)\n        if status:\n            # Check auth_info\n            if await self.refresh_oauth_info_async():\n                # Connect to mips cloud\n                self._mips_cloud.connect()\n                # Update device list\n                self.__request_refresh_cloud_devices()\n            await self.refresh_user_cert_async()\n        else:\n            self.__request_show_devices_changed_notify(delay_sec=30)\n            # Cancel refresh cloud devices\n            if self._refresh_cloud_devices_timer:\n                self._refresh_cloud_devices_timer.cancel()\n                self._refresh_cloud_devices_timer = None\n            # Disconnect cloud mips\n            self._mips_cloud.disconnect()\n\n    @final\n    async def __on_mips_service_state_change(\n        self, group_id: str, state: MipsServiceState, data: dict\n    ) -> None:\n        _LOGGER.info(\n            'mips service state changed, %s, %s, %s', group_id, state, data)\n\n        mips = self._mips_local.get(group_id, None)\n        if mips:\n            # if state == MipsServiceState.REMOVED:\n            #     mips.disconnect()\n            #     self._mips_local.pop(group_id, None)\n            #     return\n            if ( # ADDED or UPDATED\n                mips.client_id == self._entry_data['virtual_did']\n                and mips.host == data['addresses'][0]\n                and mips.port == data['port']\n            ):\n                return\n            mips.disconnect()\n            self._mips_local.pop(group_id, None)\n        home_name: str = ''\n        for info in list(self._entry_data['home_selected'].values()):\n            if info.get('group_id', None) == group_id:\n                home_name = info.get('home_name', '')\n        mips = MipsLocalClient(\n            did=self._entry_data['virtual_did'],\n            group_id=group_id,\n            host=data['addresses'][0],\n            ca_file=self._cert.ca_file,\n            cert_file=self._cert.cert_file,\n            key_file=self._cert.key_file,\n            port=data['port'],\n            home_name=home_name,\n            loop=self._main_loop)\n        self._mips_local[group_id] = mips\n        mips.enable_logger(logger=_LOGGER)\n        mips.on_dev_list_changed = self.__on_gw_device_list_changed\n        mips.sub_mips_state(\n            key=group_id, handler=self.__on_mips_local_state_changed)\n        mips.connect()\n\n    @final\n    async def __on_mips_cloud_state_changed(\n        self, key: str, state: bool\n    ) -> None:\n        _LOGGER.info('cloud mips state changed, %s, %s', key, state)\n        if state:\n            # Connect\n            self.__request_refresh_cloud_devices(immediately=True)\n            # Sub cloud device state\n            for did in list(self._device_list_cache.keys()):\n                self._mips_cloud.sub_device_state(\n                    did=did, handler=self.__on_cloud_device_state_changed)\n        else:\n            # Disconnect\n            for did, info in self._device_list_cloud.items():\n                cloud_state_old: Optional[bool] = info.get('online', None)\n                if not cloud_state_old:\n                    # Cloud state is None or False, no need to update\n                    continue\n                info['online'] = False\n                if did not in self._device_list_cache:\n                    continue\n                self.__update_device_msg_sub(did=did)\n                state_old: Optional[bool] = self._device_list_cache[did].get(\n                    'online', None)\n                state_new: Optional[bool] = self.__check_device_state(\n                    False,\n                    self._device_list_gateway.get(\n                        did, {}).get('online', False),\n                    self._device_list_lan.get(did, {}).get('online', False))\n                if state_old == state_new:\n                    continue\n                self._device_list_cache[did]['online'] = state_new\n                sub = self._sub_device_state.get(did, None)\n                if sub and sub.handler:\n                    sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)\n            self.__request_show_devices_changed_notify()\n\n    @final\n    async def __on_mips_local_state_changed(\n        self, group_id: str, state: bool\n    ) -> None:\n        _LOGGER.info('local mips state changed, %s, %s', group_id, state)\n        mips = self._mips_local.get(group_id, None)\n        if not mips:\n            _LOGGER.info(\n                'local mips state changed, mips not exist, %s', group_id)\n            # The connection to the central hub gateway is definitely broken.\n            self.__show_central_state_changed_notify(False)\n            return\n        if state:\n            # Connected\n            self.__request_refresh_gw_devices_by_group_id(group_id=group_id)\n        else:\n            # Disconnect\n            for did, info in self._device_list_gateway.items():\n                if info.get('group_id', None) != group_id:\n                    # Not belong to this gateway\n                    continue\n                if not info.get('online', False):\n                    # Device offline, no need to update\n                    continue\n                # Update local device info\n                info['online'] = False\n                info['push_available'] = False\n                if did not in self._device_list_cache:\n                    # Device not exist\n                    continue\n                self.__update_device_msg_sub(did=did)\n                state_old: Optional[bool] = self._device_list_cache.get(\n                    did, {}).get('online', None)\n                state_new: Optional[bool] = self.__check_device_state(\n                    self._device_list_cloud.get(did, {}).get('online', None),\n                    False,\n                    self._device_list_lan.get(did, {}).get('online', False))\n                if state_old == state_new:\n                    continue\n                self._device_list_cache[did]['online'] = state_new\n                sub = self._sub_device_state.get(did, None)\n                if sub and sub.handler:\n                    sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)\n            self.__request_show_devices_changed_notify()\n        self.__show_central_state_changed_notify(state)\n\n    @final\n    async def __on_miot_lan_state_change(self, state: bool) -> None:\n        _LOGGER.info(\n            'miot lan state changed, %s, %s, %s',\n            self._uid, self._cloud_server,  state)\n        if state:\n            # Update device\n            self._miot_lan.sub_device_state(\n                key=f'{self._uid}-{self._cloud_server}',\n                handler=self.__on_lan_device_state_changed)\n            for did, info in (\n                    await self._miot_lan.get_dev_list_async()).items():\n                await self.__on_lan_device_state_changed(\n                    did=did, state=info, ctx=None)\n            _LOGGER.info('lan device list, %s', self._device_list_lan)\n            self._miot_lan.update_devices(devices={\n                did: {\n                    'token': info['token'],\n                    'model': info['model'],\n                    'connect_type': info['connect_type']}\n                for did, info in self._device_list_cache.items()\n                if 'token' in info and 'connect_type' in info\n                and info['connect_type'] in [0, 8, 12, 23]\n            })\n        else:\n            for did, info in self._device_list_lan.items():\n                if not info.get('online', False):\n                    continue\n                # Update local device info\n                info['online'] = False\n                info['push_available'] = False\n                self.__update_device_msg_sub(did=did)\n                state_old: Optional[bool] = self._device_list_cache.get(\n                    did, {}).get('online', None)\n                state_new: Optional[bool] = self.__check_device_state(\n                    self._device_list_cloud.get(did, {}).get('online', None),\n                    self._device_list_gateway.get(\n                        did, {}).get('online', False),\n                    False)\n                if state_old == state_new:\n                    continue\n                self._device_list_cache[did]['online'] = state_new\n                sub = self._sub_device_state.get(did, None)\n                if sub and sub.handler:\n                    sub.handler(did, MIoTDeviceState.OFFLINE, sub.handler_ctx)\n            self._device_list_lan = {}\n            self.__request_show_devices_changed_notify()\n\n    @final\n    def __on_cloud_device_state_changed(\n        self, did: str, state: MIoTDeviceState, ctx: Any\n    ) -> None:\n        _LOGGER.info('cloud device state changed, %s, %s', did, state)\n        cloud_device = self._device_list_cloud.get(did, None)\n        if not cloud_device:\n            return\n        cloud_state_new: bool = state == MIoTDeviceState.ONLINE\n        if cloud_device.get('online', False) == cloud_state_new:\n            return\n        cloud_device['online'] = cloud_state_new\n        if did not in self._device_list_cache:\n            return\n        self.__update_device_msg_sub(did=did)\n        state_old: Optional[bool] = self._device_list_cache[did].get(\n            'online', None)\n        state_new: Optional[bool] = self.__check_device_state(\n            cloud_state_new,\n            self._device_list_gateway.get(did, {}).get('online', False),\n            self._device_list_lan.get(did, {}).get('online', False))\n        if state_old == state_new:\n            return\n        self._device_list_cache[did]['online'] = state_new\n        sub = self._sub_device_state.get(did, None)\n        if sub and sub.handler:\n            sub.handler(\n                did, MIoTDeviceState.ONLINE if state_new\n                else MIoTDeviceState.OFFLINE, sub.handler_ctx)\n        self.__request_show_devices_changed_notify()\n\n    @final\n    async def __on_gw_device_list_changed(\n        self, mips: MipsLocalClient, did_list: list[str]\n    ) -> None:\n        _LOGGER.info(\n            'gateway devices list changed, %s, %s', mips.group_id, did_list)\n        payload: dict = {\n            'filter': {\n                'did': did_list\n            },\n            'info': [\n                'name', 'model', 'urn',\n                'online', 'specV2Access', 'pushAvailable'\n            ]\n        }\n        gw_list = await mips.get_dev_list_async(\n            payload=json.dumps(payload))\n        if gw_list is None:\n            _LOGGER.error('local mips get_dev_list_async failed, %s', did_list)\n            return\n        await self.__update_devices_from_gw_async(\n            gw_list=gw_list, group_id=mips.group_id, filter_dids=[\n                did for did in did_list\n                if self._device_list_gateway.get(did, {}).get(\n                    'group_id', None) == mips.group_id])\n        self.__request_show_devices_changed_notify()\n\n    @final\n    async def __on_lan_device_state_changed(\n        self, did: str, state: dict, ctx: Any\n    ) -> None:\n        _LOGGER.info('lan device state changed, %s, %s', did, state)\n        lan_state_new: bool = state.get('online', False)\n        lan_sub_new: bool = state.get('push_available', False)\n        self._device_list_lan.setdefault(did, {})\n        if (\n            lan_state_new == self._device_list_lan[did].get('online', False)\n            and lan_sub_new == self._device_list_lan[did].get(\n                'push_available', False)\n        ):\n            return\n        self._device_list_lan[did]['online'] = lan_state_new\n        self._device_list_lan[did]['push_available'] = lan_sub_new\n        if did not in self._device_list_cache:\n            return\n        self.__update_device_msg_sub(did=did)\n        if lan_state_new == self._device_list_cache[did].get('online', False):\n            return\n        state_old: Optional[bool] = self._device_list_cache[did].get(\n            'online', None)\n        state_new: Optional[bool] = self.__check_device_state(\n            self._device_list_cloud.get(did, {}).get('online', None),\n            self._device_list_gateway.get(did, {}).get('online', False),\n            lan_state_new)\n        if state_old == state_new:\n            return\n        self._device_list_cache[did]['online'] = state_new\n        sub = self._sub_device_state.get(did, None)\n        if sub and sub.handler:\n            sub.handler(\n                did, MIoTDeviceState.ONLINE if state_new\n                else MIoTDeviceState.OFFLINE, sub.handler_ctx)\n        self.__request_show_devices_changed_notify()\n\n    @final\n    def __on_prop_msg(self, params: dict, ctx: Any) -> None:\n        \"\"\"params MUST contain did, siid, piid, value\"\"\"\n        # BLE device has no online/offline msg\n        try:\n            subs: list[MIoTClientSub] = list(self._sub_tree.iter_match(\n                f'{params[\"did\"]}/p/{params[\"siid\"]}/{params[\"piid\"]}'))\n            for sub in subs:\n                sub.handler(params, sub.handler_ctx)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('on prop msg error, %s, %s', params, err)\n\n    @final\n    def __on_event_msg(self, params: dict, ctx: Any) -> None:\n        try:\n            subs: list[MIoTClientSub] = list(self._sub_tree.iter_match(\n                f'{params[\"did\"]}/e/{params[\"siid\"]}/{params[\"eiid\"]}'))\n            for sub in subs:\n                sub.handler(params, sub.handler_ctx)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('on event msg error, %s, %s', params, err)\n\n    @final\n    def __check_device_state(\n        self, cloud_state: Optional[bool], gw_state: bool, lan_state: bool\n    ) -> Optional[bool]:\n        if cloud_state is None and not gw_state and not lan_state:\n            # Device remove\n            return None\n        if cloud_state or gw_state or lan_state:\n            return True\n        return False\n\n    @final\n    async def __load_cache_device_async(self) -> None:\n        \"\"\"Load device list from cache.\"\"\"\n        cache_list: Optional[dict[str, dict]] = await self._storage.load_async(\n            domain='miot_devices', name=f'{self._uid}_{self._cloud_server}',\n            type_=dict)  # type: ignore\n        if not cache_list:\n            self.__show_client_error_notify(\n                message=self._i18n.translate(\n                    'miot.client.invalid_device_cache'),  # type: ignore\n                notify_key='device_cache')\n            raise MIoTClientError('load device list from cache error')\n        else:\n            self.__show_client_error_notify(\n                message=None, notify_key='device_cache')\n        # Set default online status = False\n        self._device_list_cache = {}\n        for did, info in cache_list.items():\n            if info.get('online', None):\n                self._device_list_cache[did] = {\n                    **info, 'online': False}\n            else:\n                self._device_list_cache[did] = info\n        self._device_list_cloud = deepcopy(self._device_list_cache)\n        self._device_list_gateway = {\n            did: {\n                'did': did,\n                'name': info.get('name', None),\n                'group_id': info.get('group_id', None),\n                'online': False,\n                'push_available': False}\n            for did, info in self._device_list_cache.items()}\n\n    @final\n    async def __update_devices_from_cloud_async(\n        self, cloud_list: dict[str, dict],\n        filter_dids: Optional[list[str]] = None\n    ) -> None:\n        \"\"\"Update cloud devices.\n        NOTICE: This function will operate the cloud_list\n        \"\"\"\n        # MIoT cloud may not publish the online state updating message\n        # for the BLE device. Assume that all BLE devices are online.\n        # MIoT cloud does not publish the online state updating message for the\n        # child device under the proxy gateway (eg, VRF air conditioner\n        # controller). Assume that all proxy gateway child devices are online.\n        for did, info in cloud_list.items():\n            if did.startswith('blt.') or did.startswith('proxy.'):\n                info['online'] = True\n        for did, info in self._device_list_cache.items():\n            if filter_dids and did not in filter_dids:\n                continue\n            state_old: Optional[bool] = info.get('online', None)\n            cloud_state_old: Optional[bool] = self._device_list_cloud.get(\n                did, {}).get('online', None)\n            cloud_state_new: Optional[bool] = None\n            device_new = cloud_list.pop(did, None)\n            if device_new:\n                cloud_state_new = device_new.get('online', None)\n                # Update cache device info\n                info.update(\n                    {**device_new, 'online': state_old})\n                # Update cloud device\n                self._device_list_cloud[did] = device_new\n            else:\n                # Device deleted\n                self._device_list_cloud[did]['online'] = None\n            if cloud_state_old == cloud_state_new:\n                # Cloud online status no change\n                continue\n            # Update sub from\n            self.__update_device_msg_sub(did=did)\n            state_new: Optional[bool] = self.__check_device_state(\n                cloud_state_new,\n                self._device_list_gateway.get(did, {}).get('online', False),\n                self._device_list_lan.get(did, {}).get('online', False))\n            if state_old == state_new:\n                # Online status no change\n                continue\n            info['online'] = state_new\n            # Call device state changed callback\n            sub = self._sub_device_state.get(did, None)\n            if sub and sub.handler:\n                sub.handler(\n                    did, MIoTDeviceState.ONLINE if state_new\n                    else MIoTDeviceState.OFFLINE, sub.handler_ctx)\n        # New devices\n        self._device_list_cloud.update(cloud_list)\n        # Update storage\n        if not await self._storage.save_async(\n            domain='miot_devices',\n            name=f'{self._uid}_{self._cloud_server}',\n            data=self._device_list_cache\n        ):\n            _LOGGER.error('save device list to cache failed')\n\n    @final\n    async def __refresh_cloud_devices_async(self) -> None:\n        _LOGGER.debug(\n            'refresh cloud devices, %s, %s', self._uid, self._cloud_server)\n        if self._refresh_cloud_devices_timer:\n            self._refresh_cloud_devices_timer.cancel()\n            self._refresh_cloud_devices_timer = None\n        try:\n            result = await self._http.get_devices_async(\n                home_ids=list(self._entry_data.get('home_selected', {}).keys()))\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('refresh cloud devices failed, %s', err)\n            self._refresh_cloud_devices_timer = self._main_loop.call_later(\n                REFRESH_CLOUD_DEVICES_RETRY_DELAY,\n                lambda: self._main_loop.create_task(\n                    self.__refresh_cloud_devices_async()))\n            return\n        if not result and 'devices' not in result:\n            self.__show_client_error_notify(\n                message=self._i18n.translate(\n                    'miot.client.device_cloud_error'),  # type: ignore\n                notify_key='device_cloud')\n            return\n        else:\n            self.__show_client_error_notify(\n                message=None, notify_key='device_cloud')\n        cloud_list: dict[str, dict] = result['devices']\n        await self.__update_devices_from_cloud_async(cloud_list=cloud_list)\n        # Update lan device\n        if (\n            self._ctrl_mode == CtrlMode.AUTO\n            and self._miot_lan.init_done\n        ):\n            self._miot_lan.update_devices(devices={\n                did: {\n                    'token': info['token'],\n                    'model': info['model'],\n                    'connect_type': info['connect_type']}\n                for did, info in self._device_list_cache.items()\n                if 'token' in info and 'connect_type' in info\n                and info['connect_type'] in [0, 8, 12, 23]\n            })\n\n        self.__request_show_devices_changed_notify()\n\n    @final\n    async def __refresh_cloud_device_with_dids_async(\n        self, dids: list[str]\n    ) -> None:\n        _LOGGER.debug('refresh cloud device with dids, %s', dids)\n        cloud_list = await self._http.get_devices_with_dids_async(dids=dids)\n        if cloud_list is None:\n            _LOGGER.error('cloud http get_dev_list_async failed, %s', dids)\n            return\n        await self.__update_devices_from_cloud_async(\n            cloud_list=cloud_list, filter_dids=dids)\n        self.__request_show_devices_changed_notify()\n\n    def __request_refresh_cloud_devices(self, immediately=False) -> None:\n        _LOGGER.debug(\n            'request refresh cloud devices, %s, %s',\n            self._uid, self._cloud_server)\n        delay_sec : int = 0 if immediately else REFRESH_CLOUD_DEVICES_DELAY\n        if self._refresh_cloud_devices_timer:\n            self._refresh_cloud_devices_timer.cancel()\n        self._refresh_cloud_devices_timer = self._main_loop.call_later(\n            delay_sec, lambda: self._main_loop.create_task(\n                self.__refresh_cloud_devices_async()))\n\n    @final\n    async def __update_devices_from_gw_async(\n        self, gw_list: dict[str, dict],\n        group_id: Optional[str] = None,\n        filter_dids: Optional[list[str]] = None\n    ) -> None:\n        \"\"\"Update cloud devices.\n        NOTICE: This function will operate the gw_list\"\"\"\n        _LOGGER.debug('update gw devices, %s, %s', group_id, filter_dids)\n        if not gw_list and not filter_dids:\n            return\n        for did, info in self._device_list_cache.items():\n            if did not in filter_dids:\n                continue\n            device_old = self._device_list_gateway.get(did, None)\n            gw_state_old = device_old.get(\n                'online', False) if device_old else False\n            gw_state_new: bool = False\n            device_new = gw_list.pop(did, None)\n            if device_new:\n                # Update gateway device info\n                self._device_list_gateway[did] = {\n                    **device_new, 'group_id': group_id}\n                gw_state_new = device_new.get('online', False)\n            else:\n                # Device offline\n                if device_old:\n                    device_old['online'] = False\n            # Update cache group_id\n            info['group_id'] = group_id\n            if (gw_state_old == gw_state_new) and (not gw_state_new):\n                continue\n            self.__update_device_msg_sub(did=did)\n            state_old: Optional[bool] = info.get('online', None)\n            state_new: Optional[bool] = self.__check_device_state(\n                self._device_list_cloud.get(did, {}).get('online', None),\n                gw_state_new,\n                self._device_list_lan.get(did, {}).get('online', False))\n            if state_old == state_new:\n                continue\n            info['online'] = state_new\n            sub = self._sub_device_state.get(did, None)\n            if sub and sub.handler:\n                sub.handler(\n                    did, MIoTDeviceState.ONLINE if state_new\n                    else MIoTDeviceState.OFFLINE, sub.handler_ctx)\n        # New devices or device home info changed\n        for did, info in gw_list.items():\n            self._device_list_gateway[did] = {**info, 'group_id': group_id}\n            if did not in self._device_list_cache:\n                continue\n            group_id_old: str = self._device_list_cache[did].get(\n                'group_id', None)\n            self._device_list_cache[did]['group_id'] = group_id\n            _LOGGER.info(\n                'move device %s from %s to %s', did, group_id_old, group_id)\n            self.__update_device_msg_sub(did=did)\n            state_old: Optional[bool] = self._device_list_cache[did].get(\n                'online', None)\n            state_new: Optional[bool] = self.__check_device_state(\n                self._device_list_cloud.get(did, {}).get('online', None),\n                info.get('online', False),\n                self._device_list_lan.get(did, {}).get('online', False))\n            if state_old == state_new:\n                continue\n            self._device_list_cache[did]['online'] = state_new\n            sub = self._sub_device_state.get(did, None)\n            if sub and sub.handler:\n                sub.handler(\n                    did, MIoTDeviceState.ONLINE if state_new\n                    else MIoTDeviceState.OFFLINE, sub.handler_ctx)\n\n    @final\n    async def __refresh_gw_devices_with_group_id_async(\n        self, group_id: str\n    ) -> None:\n        \"\"\"Refresh gateway devices by group_id\"\"\"\n        _LOGGER.debug(\n            'refresh gw devices with group_id, %s', group_id)\n        # Remove timer\n        self._mips_local_state_changed_timers.pop(group_id, None)\n        mips = self._mips_local.get(group_id, None)\n        if not mips:\n            _LOGGER.error('mips not exist, %s', group_id)\n            return\n        if not mips.mips_state:\n            _LOGGER.debug('local mips disconnect, skip refresh, %s', group_id)\n            return\n        payload: dict = {\n            'info': [\n                'name', 'model', 'urn',\n                'online', 'specV2Access', 'pushAvailable'\n            ]\n        }\n        gw_list: dict = await mips.get_dev_list_async(\n            payload=json.dumps(payload))\n        if gw_list is None:\n            _LOGGER.error(\n                'refresh gw devices with group_id failed, %s, %s',\n                self._uid, group_id)\n            # Retry until success\n            self.__request_refresh_gw_devices_by_group_id(\n                group_id=group_id)\n            return\n        await self.__update_devices_from_gw_async(\n            gw_list=gw_list, group_id=group_id, filter_dids=[\n                did for did, info in self._device_list_gateway.items()\n                if info.get('group_id', None) == group_id])\n        self.__request_show_devices_changed_notify()\n\n    @final\n    def __request_refresh_gw_devices_by_group_id(\n        self, group_id: str, immediately: bool = False\n    ) -> None:\n        \"\"\"Request refresh gateway devices by group_id\"\"\"\n        refresh_timer = self._mips_local_state_changed_timers.get(\n            group_id, None)\n        if immediately:\n            if refresh_timer:\n                self._mips_local_state_changed_timers.pop(group_id, None)\n                refresh_timer.cancel()\n            self._mips_local_state_changed_timers[group_id] = (\n                self._main_loop.call_later(\n                    0, lambda: self._main_loop.create_task(\n                        self.__refresh_gw_devices_with_group_id_async(\n                            group_id=group_id))))\n        if refresh_timer:\n            return\n        self._mips_local_state_changed_timers[group_id] = (\n            self._main_loop.call_later(\n                REFRESH_GATEWAY_DEVICES_DELAY,\n                lambda: self._main_loop.create_task(\n                    self.__refresh_gw_devices_with_group_id_async(\n                        group_id=group_id))))\n\n    @final\n    async def __refresh_props_from_cloud(self, patch_len: int = 150) -> bool:\n        if not self._network.network_status:\n            return False\n\n        request_list = None\n        if len(self._refresh_props_list) < patch_len:\n            request_list = self._refresh_props_list\n            self._refresh_props_list = {}\n        else:\n            request_list = {}\n            for _ in range(patch_len):\n                key, value = self._refresh_props_list.popitem()\n                request_list[key] = value\n        try:\n            results = await self._http.get_props_async(\n                params=list(request_list.values()))\n            if not results:\n                raise MIoTClientError('get_props_async failed')\n            for result in results:\n                if (\n                    'did' not in result\n                    or 'siid' not in result\n                    or 'piid' not in result\n                    or 'value' not in result\n                ):\n                    continue\n                request_list.pop(\n                    f'{result[\"did\"]}|{result[\"siid\"]}|{result[\"piid\"]}',\n                    None)\n                self.__on_prop_msg(params=result, ctx=None)\n            if request_list:\n                _LOGGER.info(\n                    'refresh props failed, cloud, %s',\n                    list(request_list.keys()))\n                request_list = None\n            return True\n        except Exception as err:  # pylint:disable=broad-exception-caught\n            _LOGGER.error(\n                'refresh props error, cloud, %s, %s',\n                err, traceback.format_exc())\n            # Add failed request back to the list\n            self._refresh_props_list.update(request_list)\n            return False\n\n    @final\n    async def __refresh_props_from_gw(self) -> bool:\n        if not self._mips_local or not self._device_list_gateway:\n            return False\n        request_list = {}\n        succeed_once = False\n        for key in list(self._refresh_props_list.keys()):\n            did = key.split('|')[0]\n            if did in request_list:\n                # NOTICE: A device only requests once a cycle, continuous\n                # acquisition of properties can cause device exceptions.\n                continue\n            params = self._refresh_props_list.pop(key)\n            device_gw = self._device_list_gateway.get(did, None)\n            if not device_gw:\n                # Device not exist\n                continue\n            mips_gw = self._mips_local.get(device_gw['group_id'], None)\n            if not mips_gw:\n                _LOGGER.error('mips gateway not exist, %s', key)\n                continue\n            request_list[did] = {\n                **params,\n                'fut': mips_gw.get_prop_async(\n                    did=did, siid=params['siid'], piid=params['piid'],\n                    timeout_ms=6000)}\n        results = await asyncio.gather(\n            *[v['fut'] for v in request_list.values()])\n        for (did, param), result in zip(request_list.items(), results):\n            if result is None:\n                # Don't use \"not result\", it will be skipped when result\n                # is 0, false\n                continue\n            self.__on_prop_msg(\n                params={\n                    'did': did,\n                    'siid': param['siid'],\n                    'piid': param['piid'],\n                    'value': result},\n                ctx=None)\n            succeed_once = True\n        if succeed_once:\n            return True\n        _LOGGER.info(\n            'refresh props failed, gw, %s', list(request_list.keys()))\n        # Add failed request back to the list\n        self._refresh_props_list.update(request_list)\n        return False\n\n    @final\n    async def __refresh_props_from_lan(self) -> bool:\n        if not self._miot_lan.init_done or len(self._mips_local) > 0:\n            return False\n        request_list = {}\n        succeed_once = False\n        for key in list(self._refresh_props_list.keys()):\n            did = key.split('|')[0]\n            if did in request_list:\n                # NOTICE: A device only requests once a cycle, continuous\n                # acquisition of properties can cause device exceptions.\n                continue\n            params = self._refresh_props_list.pop(key)\n            if did not in self._device_list_lan:\n                continue\n            request_list[did] = {\n                **params,\n                'fut': self._miot_lan.get_prop_async(\n                    did=did, siid=params['siid'], piid=params['piid'],\n                    timeout_ms=6000)}\n        results = await asyncio.gather(\n            *[v['fut'] for v in request_list.values()])\n        for (did, param), result in zip(request_list.items(), results):\n            if result is None:\n                # Don't use \"not result\", it will be skipped when result\n                # is 0, false\n                continue\n            self.__on_prop_msg(\n                params={\n                    'did': did,\n                    'siid': param['siid'],\n                    'piid': param['piid'],\n                    'value': result},\n                ctx=None)\n            succeed_once = True\n        if succeed_once:\n            return True\n        _LOGGER.info(\n            'refresh props failed, lan, %s', list(request_list.keys()))\n        # Add failed request back to the list\n        self._refresh_props_list.update(request_list)\n        return False\n\n    @final\n    async def __refresh_props_handler(self) -> None:\n        if not self._refresh_props_list:\n            return\n        # Cloud, Central hub gateway, Lan control\n        if (\n            await self.__refresh_props_from_cloud()\n            or await self.__refresh_props_from_gw()\n            or await self.__refresh_props_from_lan()\n        ):\n            self._refresh_props_retry_count = 0\n            if self._refresh_props_list:\n                self._refresh_props_timer = self._main_loop.call_later(\n                    REFRESH_PROPS_DELAY, lambda: self._main_loop.create_task(\n                        self.__refresh_props_handler()))\n            else:\n                self._refresh_props_timer = None\n            return\n\n        # Try three times, and if it fails three times, empty the list.\n        if self._refresh_props_retry_count >= 3:\n            self._refresh_props_list = {}\n            self._refresh_props_retry_count = 0\n            if self._refresh_props_timer:\n                self._refresh_props_timer.cancel()\n                self._refresh_props_timer = None\n            _LOGGER.info('refresh props failed, retry count exceed')\n            return\n        self._refresh_props_retry_count += 1\n        _LOGGER.info(\n            'refresh props failed, retry, %s', self._refresh_props_retry_count)\n        self._refresh_props_timer = self._main_loop.call_later(\n            REFRESH_PROPS_RETRY_DELAY, lambda: self._main_loop.create_task(\n                self.__refresh_props_handler()))\n\n    @final\n    def __show_client_error_notify(\n        self, message: Optional[str], notify_key: str = ''\n    ) -> None:\n        if message:\n\n            self._persistence_notify(\n                f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error',\n                self._i18n.translate(\n                    key='miot.client.xiaomi_home_error_title'),  # type: ignore\n                self._i18n.translate(\n                    key='miot.client.xiaomi_home_error',\n                    replace={\n                        'nick_name': self._entry_data.get(\n                            'nick_name', DEFAULT_NICK_NAME),\n                        'uid': self._uid,\n                        'cloud_server': self._cloud_server,\n                        'message': message}))  # type: ignore\n        else:\n            self._persistence_notify(\n                f'{DOMAIN}{self._uid}{self._cloud_server}{notify_key}error',\n                None, None)\n\n    @final\n    def __show_devices_changed_notify(self) -> None:\n        \"\"\"Show device list changed notify\"\"\"\n        self._show_devices_changed_notify_timer = None\n        if self._persistence_notify is None:\n            return\n\n        message_add: str = ''\n        count_add: int = 0\n        message_del: str = ''\n        count_del: int = 0\n        message_offline: str = ''\n        count_offline: int = 0\n\n        # New devices\n        if 'add' in self._display_devs_notify:\n            for did, info in {\n                    **self._device_list_gateway, **self._device_list_cloud\n            }.items():\n                if did in self._device_list_cache:\n                    continue\n                count_add += 1\n                message_add += (\n                    f'- {info.get(\"name\", \"unknown\")} ({did}, '\n                    f'{info.get(\"model\", \"unknown\")})\\n')\n        # Get unavailable and offline devices\n        home_name_del: Optional[str] = None\n        home_name_offline: Optional[str] = None\n        for did, info in self._device_list_cache.items():\n            online: Optional[bool] = info.get('online', None)\n            home_name_new = info.get('home_name', 'unknown')\n            if online:\n                # Skip online device\n                continue\n            if 'del' in self._display_devs_notify and online is None:\n                # Device not exist\n                if home_name_del != home_name_new:\n                    message_del += f'\\n[{home_name_new}]\\n'\n                    home_name_del = home_name_new\n                count_del += 1\n                message_del += (\n                    f'- {info.get(\"name\", \"unknown\")} ({did}, '\n                    f'{info.get(\"room_name\", \"unknown\")})\\n')\n                continue\n            if 'offline' in self._display_devs_notify:\n                # Device offline\n                if home_name_offline != home_name_new:\n                    message_offline += f'\\n[{home_name_new}]\\n'\n                    home_name_offline = home_name_new\n                count_offline += 1\n                message_offline += (\n                    f'- {info.get(\"name\", \"unknown\")} ({did}, '\n                    f'{info.get(\"room_name\", \"unknown\")})\\n')\n\n        message = ''\n        if 'add' in self._display_devs_notify and count_add:\n            message += self._i18n.translate(\n                key='miot.client.device_list_add',\n                replace={\n                    'count': count_add,\n                    'message': message_add})  # type: ignore\n        if 'del' in self._display_devs_notify and count_del:\n            message += self._i18n.translate(\n                key='miot.client.device_list_del',\n                replace={\n                    'count': count_del,\n                    'message': message_del})  # type: ignore\n        if 'offline' in self._display_devs_notify and count_offline:\n            message += self._i18n.translate(\n                key='miot.client.device_list_offline',\n                replace={\n                    'count': count_offline,\n                    'message': message_offline})  # type: ignore\n        if message != '':\n            msg_hash = hash(message)\n            if msg_hash == self._display_notify_content_hash:\n                # Notify content no change, return\n                _LOGGER.debug(\n                    'device list changed notify content no change, return')\n                return\n            network_status = self._i18n.translate(\n                key='miot.client.network_status_online'\n                if self._network.network_status\n                else 'miot.client.network_status_offline')\n            self._persistence_notify(\n                self.__gen_notify_key('dev_list_changed'),\n                self._i18n.translate(\n                    'miot.client.device_list_changed_title'),  # type: ignore\n                self._i18n.translate(\n                    key='miot.client.device_list_changed',\n                    replace={\n                        'nick_name': self._entry_data.get(\n                            'nick_name', DEFAULT_NICK_NAME),\n                        'uid': self._uid,\n                        'cloud_server': self._cloud_server,\n                        'network_status': network_status,\n                        'message': message}))  # type: ignore\n            self._display_notify_content_hash = msg_hash\n            _LOGGER.debug(\n                'show device list changed notify, add %s, del %s, offline %s',\n                count_add, count_del, count_offline)\n        else:\n            self._persistence_notify(\n                self.__gen_notify_key('dev_list_changed'), None, None)\n\n    @final\n    def __request_show_devices_changed_notify(\n        self, delay_sec: float = 6\n    ) -> None:\n        if not self._display_devs_notify:\n            return\n        if not self._mips_cloud and not self._mips_local and not self._miot_lan:\n            return\n        if self._show_devices_changed_notify_timer:\n            self._show_devices_changed_notify_timer.cancel()\n        self._show_devices_changed_notify_timer = self._main_loop.call_later(\n            delay_sec, self.__show_devices_changed_notify)\n\n    @final\n    def __show_central_state_changed_notify(self, connected: bool) -> None:\n        conn_status: str = (\n            self._i18n.translate('miot.client.central_state_connected')\n            if connected else\n            self._i18n.translate('miot.client.central_state_disconnected'))\n        self._persistence_notify(\n            self.__gen_notify_key('central_state_changed'),\n            self._i18n.translate('miot.client.central_state_changed_title'),\n            self._i18n.translate(key='miot.client.central_state_changed',\n                replace={\n                    'nick_name': self._entry_data.get(\n                                'nick_name', DEFAULT_NICK_NAME),\n                    'uid': self._uid,\n                    'cloud_server': self._cloud_server,\n                    'conn_status': conn_status\n                }))\n\n@staticmethod\nasync def get_miot_instance_async(\n    hass: HomeAssistant, entry_id: str, entry_data: Optional[dict] = None,\n    persistent_notify: Optional[Callable[[str, str, str], None]] = None\n) -> MIoTClient:\n    if entry_id is None:\n        raise MIoTClientError('invalid entry_id')\n    miot_client = hass.data[DOMAIN].get('miot_clients', {}).get(entry_id, None)\n    if miot_client:\n        _LOGGER.info('instance exist, %s', entry_id)\n        return miot_client\n    # Create new instance\n    if not entry_data:\n        raise MIoTClientError('entry data is None')\n    # Get running loop\n    loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()\n    if not loop:\n        raise MIoTClientError('loop is None')\n    # MIoT storage\n    storage: Optional[MIoTStorage] = hass.data[DOMAIN].get(\n        'miot_storage', None)\n    if not storage:\n        storage = MIoTStorage(\n            root_path=entry_data['storage_path'], loop=loop)\n        hass.data[DOMAIN]['miot_storage'] = storage\n        _LOGGER.info('create miot_storage instance')\n    global_config: dict = await storage.load_user_config_async(\n        uid='global_config', cloud_server='all',\n        keys=['network_detect_addr', 'net_interfaces', 'enable_subscribe'])\n    # MIoT network\n    network_detect_addr: dict = global_config.get('network_detect_addr', {})\n    network: Optional[MIoTNetwork] = hass.data[DOMAIN].get(\n        'miot_network', None)\n    if not network:\n        network = MIoTNetwork(\n            ip_addr_list=network_detect_addr.get('ip', []),\n            url_addr_list=network_detect_addr.get('url', []),\n            refresh_interval=NETWORK_REFRESH_INTERVAL,\n            loop=loop)\n        hass.data[DOMAIN]['miot_network'] = network\n        await network.init_async()\n        _LOGGER.info('create miot_network instance')\n    # MIoT service\n    mips_service: Optional[MipsService] = hass.data[DOMAIN].get(\n        'mips_service', None)\n    if not mips_service:\n        aiozc = await zeroconf.async_get_async_instance(hass)\n        mips_service = MipsService(aiozc=aiozc, loop=loop)\n        hass.data[DOMAIN]['mips_service'] = mips_service\n        await mips_service.init_async()\n        _LOGGER.info('create mips_service instance')\n    # MIoT lan\n    miot_lan: Optional[MIoTLan] = hass.data[DOMAIN].get('miot_lan', None)\n    if not miot_lan:\n        miot_lan = MIoTLan(\n            net_ifs=global_config.get('net_interfaces', []),\n            network=network,\n            mips_service=mips_service,\n            enable_subscribe=global_config.get('enable_subscribe', False),\n            loop=loop)\n        hass.data[DOMAIN]['miot_lan'] = miot_lan\n        _LOGGER.info('create miot_lan instance')\n    # MIoT client\n    miot_client = MIoTClient(\n        entry_id=entry_id,\n        entry_data=entry_data,\n        network=network,\n        storage=storage,\n        mips_service=mips_service,\n        miot_lan=miot_lan,\n        loop=loop\n    )\n    miot_client.persistent_notify = persistent_notify\n    hass.data[DOMAIN]['miot_clients'].setdefault(entry_id, miot_client)\n    _LOGGER.info('new miot_client instance, %s, %s', entry_id, entry_data)\n    await miot_client.init_async()\n    return miot_client\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_cloud.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT http client.\n\"\"\"\nimport asyncio\nimport base64\nimport hashlib\nimport json\nimport logging\nimport re\nimport time\nfrom typing import Any, Optional\nfrom urllib.parse import urlencode\nimport aiohttp\n\n# pylint: disable=relative-beyond-top-level\nfrom .common import calc_group_id\nfrom .const import (\n    UNSUPPORTED_MODELS,\n    DEFAULT_OAUTH2_API_HOST,\n    MIHOME_HTTP_API_TIMEOUT,\n    OAUTH2_AUTH_URL)\nfrom .miot_error import MIoTErrorCode, MIoTHttpError, MIoTOauthError\n\n_LOGGER = logging.getLogger(__name__)\n\nTOKEN_EXPIRES_TS_RATIO = 0.7\n\n\nclass MIoTOauthClient:\n    \"\"\"oauth agent url, default: product env.\"\"\"\n    _main_loop: asyncio.AbstractEventLoop\n    _session: aiohttp.ClientSession\n    _oauth_host: str\n    _client_id: int\n    _redirect_url: str\n    _device_id: str\n    _state: str\n\n    def __init__(\n            self, client_id: str, redirect_url: str, cloud_server: str,\n            uuid: str, loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._main_loop = loop or asyncio.get_running_loop()\n        if client_id is None or client_id.strip() == '':\n            raise MIoTOauthError('invalid client_id')\n        if not redirect_url:\n            raise MIoTOauthError('invalid redirect_url')\n        if not cloud_server:\n            raise MIoTOauthError('invalid cloud_server')\n        if not uuid:\n            raise MIoTOauthError('invalid uuid')\n\n        self._client_id = int(client_id)\n        self._redirect_url = redirect_url\n        if cloud_server == 'cn':\n            self._oauth_host = DEFAULT_OAUTH2_API_HOST\n        else:\n            self._oauth_host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'\n        self._device_id = f'ha.{uuid}'\n        self._state = hashlib.sha1(\n            f'd={self._device_id}'.encode('utf-8')).hexdigest()\n        self._session = aiohttp.ClientSession(loop=self._main_loop)\n\n    @property\n    def state(self) -> str:\n        return self._state\n\n    async def deinit_async(self) -> None:\n        if self._session and not self._session.closed:\n            await self._session.close()\n\n    def set_redirect_url(self, redirect_url: str) -> None:\n        if not isinstance(redirect_url, str) or redirect_url.strip() == '':\n            raise MIoTOauthError('invalid redirect_url')\n        self._redirect_url = redirect_url\n\n    def gen_auth_url(\n        self,\n        redirect_url: Optional[str] = None,\n        state: Optional[str] = None,\n        scope: Optional[list] = None,\n        skip_confirm: Optional[bool] = False,\n    ) -> str:\n        \"\"\"get auth url\n\n        Args:\n            redirect_url\n            state\n            scope (list, optional):\n                开放数据接口权限 ID，可以传递多个，用空格分隔，具体值可以参考开放\n                [数据接口权限列表](https://dev.mi.com/distribute/doc/details?pId=1518).\n                Defaults to None.\\n\n            skip_confirm (bool, optional):\n                默认值为true，授权有效期内的用户在已登录情况下，不显示授权页面，直接通过。\n                如果需要用户每次手动授权，设置为false. Defaults to True.\\n\n\n        Returns:\n            str: _description_\n        \"\"\"\n        params: dict = {\n            'redirect_uri': redirect_url or self._redirect_url,\n            'client_id': self._client_id,\n            'response_type': 'code',\n            'device_id': self._device_id,\n            'state': self._state\n        }\n        if state:\n            params['state'] = state\n        if scope:\n            params['scope'] = ' '.join(scope).strip()\n        params['skip_confirm'] = skip_confirm\n        encoded_params = urlencode(params)\n\n        return f'{OAUTH2_AUTH_URL}?{encoded_params}'\n\n    async def __get_token_async(self, data) -> dict:\n        http_res = await self._session.get(\n            url=f'https://{self._oauth_host}/app/v2/ha/oauth/get_token',\n            params={'data': json.dumps(data)},\n            headers={'content-type': 'application/x-www-form-urlencoded'},\n            timeout=MIHOME_HTTP_API_TIMEOUT\n        )\n        if http_res.status == 401:\n            raise MIoTOauthError(\n                'unauthorized(401)', MIoTErrorCode.CODE_OAUTH_UNAUTHORIZED)\n        if http_res.status != 200:\n            raise MIoTOauthError(\n                f'invalid http status code, {http_res.status}')\n\n        res_str = await http_res.text()\n        res_obj = json.loads(res_str)\n        if (\n            not res_obj\n            or res_obj.get('code', None) != 0\n            or 'result' not in res_obj\n            or not all(\n                key in res_obj['result']\n                for key in ['access_token', 'refresh_token', 'expires_in'])\n        ):\n            raise MIoTOauthError(f'invalid http response, {res_str}')\n\n        return {\n            **res_obj['result'],\n            'expires_ts': int(\n                time.time() +\n                (res_obj['result'].get('expires_in', 0)*TOKEN_EXPIRES_TS_RATIO))\n        }\n\n    async def get_access_token_async(self, code: str) -> dict:\n        \"\"\"get access token by authorization code\n\n        Args:\n            code (str): auth code\n\n        Returns:\n            str: _description_\n        \"\"\"\n        if not isinstance(code, str):\n            raise MIoTOauthError('invalid code')\n\n        return await self.__get_token_async(data={\n            'client_id': self._client_id,\n            'redirect_uri': self._redirect_url,\n            'code': code,\n            'device_id': self._device_id\n        })\n\n    async def refresh_access_token_async(self, refresh_token: str) -> dict:\n        \"\"\"get access token  by refresh token.\n\n        Args:\n            refresh_token (str): refresh_token\n\n        Returns:\n            str: _description_\n        \"\"\"\n        if not isinstance(refresh_token, str):\n            raise MIoTOauthError('invalid refresh_token')\n\n        return await self.__get_token_async(data={\n            'client_id': self._client_id,\n            'redirect_uri': self._redirect_url,\n            'refresh_token': refresh_token,\n        })\n\n\nclass MIoTHttpClient:\n    \"\"\"MIoT http client.\"\"\"\n    # pylint: disable=inconsistent-quotes\n    GET_PROP_AGGREGATE_INTERVAL: float = 0.2\n    GET_PROP_MAX_REQ_COUNT = 150\n    _main_loop: asyncio.AbstractEventLoop\n    _session: aiohttp.ClientSession\n    _host: str\n    _base_url: str\n    _client_id: str\n    _access_token: str\n\n    _get_prop_timer: Optional[asyncio.TimerHandle]\n    _get_prop_list: dict[str, dict]\n\n    def __init__(\n            self, cloud_server: str, client_id: str, access_token: str,\n            loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._main_loop = loop or asyncio.get_running_loop()\n        self._host = DEFAULT_OAUTH2_API_HOST\n        self._base_url = ''\n        self._client_id = ''\n        self._access_token = ''\n\n        self._get_prop_timer = None\n        self._get_prop_list = {}\n\n        if (\n            not isinstance(cloud_server, str)\n            or not isinstance(client_id, str)\n            or not isinstance(access_token, str)\n        ):\n            raise MIoTHttpError('invalid params')\n\n        self.update_http_header(\n            cloud_server=cloud_server, client_id=client_id,\n            access_token=access_token)\n\n        self._session = aiohttp.ClientSession(loop=self._main_loop)\n\n    async def deinit_async(self) -> None:\n        if self._get_prop_timer:\n            self._get_prop_timer.cancel()\n            self._get_prop_timer = None\n        for item in self._get_prop_list.values():\n            fut: Optional[asyncio.Future] = item.get('fut', None)\n            if fut:\n                fut.cancel()\n        self._get_prop_list.clear()\n        if self._session and not self._session.closed:\n            await self._session.close()\n\n    def update_http_header(\n        self, cloud_server: Optional[str] = None,\n        client_id: Optional[str] = None,\n        access_token: Optional[str] = None\n    ) -> None:\n        if isinstance(cloud_server, str):\n            if cloud_server != 'cn':\n                self._host = f'{cloud_server}.{DEFAULT_OAUTH2_API_HOST}'\n            self._base_url = f'https://{self._host}'\n        if isinstance(client_id, str):\n            self._client_id = client_id\n        if isinstance(access_token, str):\n            self._access_token = access_token\n\n    @property\n    def __api_request_headers(self) -> dict:\n        return {\n            'Host': self._host,\n            'X-Client-BizId': 'haapi',\n            'Content-Type': 'application/json',\n            'Authorization': f'Bearer{self._access_token}',\n            'X-Client-AppId': self._client_id,\n        }\n\n    # pylint: disable=unused-private-member\n    async def __mihome_api_get_async(\n        self, url_path: str, params: dict,\n        timeout: int = MIHOME_HTTP_API_TIMEOUT\n    ) -> dict:\n        http_res = await self._session.get(\n            url=f'{self._base_url}{url_path}',\n            params=params,\n            headers=self.__api_request_headers,\n            timeout=timeout)\n        if http_res.status == 401:\n            raise MIoTHttpError(\n                'mihome api get failed, unauthorized(401)',\n                MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN)\n        if http_res.status != 200:\n            raise MIoTHttpError(\n                f'mihome api get failed, {http_res.status}, '\n                f'{url_path}, {params}')\n        res_str = await http_res.text()\n        res_obj: dict = json.loads(res_str)\n        if res_obj.get('code', None) != 0:\n            raise MIoTHttpError(\n                f'invalid response code, {res_obj.get(\"code\",None)}, '\n                f'{res_obj.get(\"message\",\"\")}')\n        _LOGGER.debug(\n            'mihome api get, %s%s, %s -> %s',\n            self._base_url, url_path, params, res_obj)\n        return res_obj\n\n    async def __mihome_api_post_async(\n        self, url_path: str, data: dict,\n        timeout: int = MIHOME_HTTP_API_TIMEOUT\n    ) -> dict:\n        http_res = await self._session.post(\n            url=f'{self._base_url}{url_path}',\n            json=data,\n            headers=self.__api_request_headers,\n            timeout=timeout)\n        if http_res.status == 401:\n            raise MIoTHttpError(\n                'mihome api get failed, unauthorized(401)',\n                MIoTErrorCode.CODE_HTTP_INVALID_ACCESS_TOKEN)\n        if http_res.status != 200:\n            raise MIoTHttpError(\n                f'mihome api post failed, {http_res.status}, '\n                f'{url_path}, {data}')\n        res_str = await http_res.text()\n        res_obj: dict = json.loads(res_str)\n        if res_obj.get('code', None) != 0:\n            raise MIoTHttpError(\n                f'invalid response code, {res_obj.get(\"code\",None)}, '\n                f'{res_obj.get(\"message\",\"\")}')\n        _LOGGER.debug(\n            'mihome api post, %s%s, %s -> %s',\n            self._base_url, url_path, data, res_obj)\n        return res_obj\n\n    async def get_user_info_async(self) -> dict:\n        http_res = await self._session.get(\n            url='https://open.account.xiaomi.com/user/profile',\n            params={\n                'clientId': self._client_id, 'token': self._access_token},\n            headers={'content-type': 'application/x-www-form-urlencoded'},\n            timeout=MIHOME_HTTP_API_TIMEOUT\n        )\n\n        res_str = await http_res.text()\n        res_obj = json.loads(res_str)\n        if (\n            not res_obj\n            or res_obj.get('code', None) != 0\n            or 'data' not in res_obj\n            or 'miliaoNick' not in res_obj['data']\n        ):\n            raise MIoTOauthError(f'invalid http response, {http_res.text}')\n\n        return res_obj['data']\n\n    async def get_central_cert_async(self, csr: str) -> str:\n        if not isinstance(csr, str):\n            raise MIoTHttpError('invalid params')\n\n        res_obj: dict = await self.__mihome_api_post_async(\n            url_path='/app/v2/ha/oauth/get_central_crt',\n            data={\n                'csr': str(base64.b64encode(csr.encode('utf-8')), 'utf-8')\n            }\n        )\n        if 'result' not in res_obj:\n            raise MIoTHttpError('invalid response result')\n        cert: str = res_obj['result'].get('cert', None)\n        if not isinstance(cert, str):\n            raise MIoTHttpError('invalid cert')\n\n        return cert\n\n    async def __get_dev_room_page_async(\n        self, max_id: Optional[str] = None\n    ) -> dict:\n        res_obj = await self.__mihome_api_post_async(\n            url_path='/app/v2/homeroom/get_dev_room_page',\n            data={\n                'start_id': max_id,\n                'limit': 150,\n            },\n        )\n        if 'result' not in res_obj and 'info' not in res_obj['result']:\n            raise MIoTHttpError('invalid response result')\n        home_list: dict = {}\n        for home in res_obj['result']['info']:\n            if 'id' not in home:\n                _LOGGER.error(\n                    'get dev room page error, invalid home, %s', home)\n                continue\n            home_list[str(home['id'])] = {'dids': home.get(\n                'dids', None) or [], 'room_info': {}}\n            for room in home.get('roomlist', []):\n                if 'id' not in room:\n                    _LOGGER.error(\n                        'get dev room page error, invalid room, %s', room)\n                    continue\n                home_list[str(home['id'])]['room_info'][str(room['id'])] = {\n                    'dids': room.get('dids', None) or []}\n        if (\n            res_obj['result'].get('has_more', False)\n            and isinstance(res_obj['result'].get('max_id', None), str)\n        ):\n            next_list = await self.__get_dev_room_page_async(\n                max_id=res_obj['result']['max_id'])\n            for home_id, info in next_list.items():\n                home_list.setdefault(home_id, {'dids': [], 'room_info': {}})\n                home_list[home_id]['dids'].extend(info['dids'])\n                for room_id, info in info['room_info'].items():\n                    home_list[home_id]['room_info'].setdefault(\n                        room_id, {'dids': []})\n                    home_list[home_id]['room_info'][room_id]['dids'].extend(\n                        info['dids'])\n\n        return home_list\n\n    async def get_separated_shared_devices_async(self) -> dict[str, dict]:\n        separated_shared_devices: dict = {}\n        device_list: dict[str, dict] = await self.__get_device_list_page_async(\n            dids=[], start_did=None)\n        for did, value in device_list.items():\n            if value['owner'] is not None and ('userid' in value['owner']) and (\n                'nickname' in value['owner']\n            ):\n                separated_shared_devices.setdefault(did, value['owner'])\n        return separated_shared_devices\n\n    async def get_homeinfos_async(self) -> dict:\n        res_obj = await self.__mihome_api_post_async(\n            url_path='/app/v2/homeroom/gethome',\n            data={\n                'limit': 150,\n                'fetch_share': True,\n                'fetch_share_dev': True,\n                'plat_form': 0,\n                'app_ver': 9,\n            },\n        )\n        if 'result' not in res_obj:\n            raise MIoTHttpError('invalid response result')\n\n        uid: Optional[str] = None\n        home_infos: dict = {}\n        for device_source in ['homelist', 'share_home_list']:\n            home_infos.setdefault(device_source, {})\n            for home in res_obj['result'].get(device_source, []):\n                if (\n                    'id' not in home\n                    or 'name' not in home\n                    or 'roomlist' not in home\n                ):\n                    continue\n                if uid is None and device_source == 'homelist':\n                    uid = str(home['uid'])\n                home_infos[device_source][home['id']] = {\n                    'home_id': home['id'],\n                    'home_name': home['name'],\n                    'city_id': home.get('city_id', None),\n                    'longitude': home.get('longitude', None),\n                    'latitude': home.get('latitude', None),\n                    'address': home.get('address', None),\n                    'dids': home.get('dids', []),\n                    'room_info': {\n                        room['id']: {\n                            'room_id': room['id'],\n                            'room_name': room['name'],\n                            'dids': room.get('dids', [])\n                        }\n                        for room in home.get('roomlist', [])\n                        if 'id' in room\n                    },\n                    'group_id': calc_group_id(\n                        uid=home['uid'], home_id=home['id']),\n                    'uid': str(home['uid'])\n                }\n            home_infos['uid'] = uid\n        if (\n            res_obj['result'].get('has_more', False)\n            and isinstance(res_obj['result'].get('max_id', None), str)\n        ):\n            more_list = await self.__get_dev_room_page_async(\n                max_id=res_obj['result']['max_id'])\n            for device_source in ['homelist', 'share_home_list']:\n                for home_id, info in more_list.items():\n                    if home_id not in home_infos[device_source]:\n                        _LOGGER.info('unknown home, %s, %s', home_id, info)\n                        continue\n                    home_infos[device_source][home_id]['dids'].extend(\n                        info['dids'])\n                    for room_id, info in info['room_info'].items():\n                        home_infos[device_source][home_id][\n                            'room_info'].setdefault(\n                            room_id, {\n                                'room_id': room_id,\n                                'room_name': '',\n                                'dids': []})\n                        home_infos[device_source][home_id]['room_info'][\n                            room_id]['dids'].extend(info['dids'])\n\n        return {\n            'uid': uid,\n            'home_list': home_infos.get('homelist', {}),\n            'share_home_list': home_infos.get('share_home_list', [])\n        }\n\n    async def get_uid_async(self) -> str:\n        return (await self.get_homeinfos_async()).get('uid', None)\n\n    async def __get_device_list_page_async(\n        self, dids: list[str], start_did: Optional[str] = None\n    ) -> dict[str, dict]:\n        req_data: dict = {\n            'limit': 200,\n            'get_split_device': True,\n            'get_third_device': True,\n            'dids': dids\n        }\n        if start_did:\n            req_data['start_did'] = start_did\n        device_infos: dict = {}\n        res_obj = await self.__mihome_api_post_async(\n            url_path='/app/v2/home/device_list_page',\n            data=req_data\n        )\n        if 'result' not in res_obj:\n            raise MIoTHttpError('invalid response result')\n        res_obj = res_obj['result']\n\n        for device in res_obj.get('list', []) or []:\n            did = device.get('did', None)\n            name = device.get('name', None)\n            urn = device.get('spec_type', None)\n            model = device.get('model', None)\n            if did is None or name is None:\n                _LOGGER.info(\n                    'invalid device, cloud, %s', device)\n                continue\n            if urn is None or model is None:\n                _LOGGER.info(\n                    'missing the urn|model field, cloud, %s', device)\n                continue\n            if did.startswith('miwifi.'):\n                # The miwifi.* routers defined SPEC functions, but none of them\n                # were implemented.\n                _LOGGER.info('ignore miwifi.* device, cloud, %s', did)\n                continue\n            if model in UNSUPPORTED_MODELS:\n                _LOGGER.info('ignore unsupported model %s, cloud, %s',\n                             model, did)\n                continue\n            device_infos[did] = {\n                'did': did,\n                'uid': device.get('uid', None),\n                'name': name,\n                'urn': urn,\n                'model': model,\n                'connect_type': device.get('pid', -1),\n                'token': device.get('token', None),\n                'online': device.get('isOnline', False),\n                'icon': device.get('icon', None),\n                'parent_id': device.get('parent_id', None),\n                'manufacturer': model.split('.')[0],\n                # 2: xiao-ai, 1: general speaker\n                'voice_ctrl': device.get('voice_ctrl', 0),\n                'rssi': device.get('rssi', None),\n                'owner': device.get('owner', None),\n                'pid': device.get('pid', None),\n                'local_ip': device.get('local_ip', None),\n                'ssid': device.get('ssid', None),\n                'bssid': device.get('bssid', None),\n                'order_time': device.get('orderTime', 0),\n                'fw_version': device.get('extra', {}).get(\n                    'fw_version', None)\n            }\n\n        next_start_did = res_obj.get('next_start_did', None)\n        if res_obj.get('has_more', False) and next_start_did:\n            device_infos.update(await self.__get_device_list_page_async(\n                dids=dids, start_did=next_start_did))\n\n        return device_infos\n\n    async def get_devices_with_dids_async(\n        self, dids: list[str]\n    ) -> Optional[dict[str, dict]]:\n        results: list[dict[str, dict]] = await asyncio.gather(\n            *[self.__get_device_list_page_async(dids=dids[index:index+150])\n                for index in range(0, len(dids), 150)])\n        devices = {}\n        for result in results:\n            if result is None:\n                return None\n            devices.update(result)\n        return devices\n\n    async def get_devices_async(\n        self, home_ids: Optional[list[str]] = None\n    ) -> dict[str, dict]:\n        homeinfos = await self.get_homeinfos_async()\n        homes: dict[str, dict[str, Any]] = {}\n        devices: dict[str, dict] = {}\n        for device_type in ['home_list', 'share_home_list']:\n            homes.setdefault(device_type, {})\n            for home_id, home_info in (homeinfos.get(\n                    device_type, None) or {}).items():\n                if isinstance(home_ids, list) and home_id not in home_ids:\n                    continue\n                home_name: str = home_info['home_name']\n                group_id: str = home_info['group_id']\n                homes[device_type].setdefault(\n                    home_id, {\n                        'home_name': home_name,\n                        'uid': home_info['uid'],\n                        'group_id': group_id,\n                        'room_info': {}\n                    })\n                devices.update({did: {\n                    'home_id': home_id,\n                    'home_name': home_name,\n                    'room_id': home_id,\n                    'room_name': home_name,\n                    'group_id': group_id\n                } for did in home_info.get('dids', [])})\n                for room_id, room_info in home_info.get('room_info').items():\n                    room_name: str = room_info.get('room_name', '')\n                    homes[device_type][home_id]['room_info'][\n                        room_id] = room_name\n                    devices.update({\n                        did: {\n                            'home_id': home_id,\n                            'home_name': home_name,\n                            'room_id': room_id,\n                            'room_name': room_name,\n                            'group_id': group_id\n                        } for did in room_info.get('dids', [])})\n        separated_shared_devices: dict = (\n            await self.get_separated_shared_devices_async())\n        if separated_shared_devices:\n            homes.setdefault('separated_shared_list', {})\n            for did, owner in separated_shared_devices.items():\n                owner_id = str(owner['userid'])\n                homes['separated_shared_list'].setdefault(owner_id,{\n                    'home_name': owner['nickname'],\n                    'uid': owner_id,\n                    'group_id': 'NotSupport',\n                    'room_info': {'shared_device': 'shared_device'}\n                })\n                devices.update({did: {\n                    'home_id': owner_id,\n                    'home_name': owner['nickname'],\n                    'room_id': 'shared_device',\n                    'room_name': 'shared_device',\n                    'group_id': 'NotSupport'\n                }})\n        dids = sorted(list(devices.keys()))\n        results = await self.get_devices_with_dids_async(dids=dids)\n        if results is None:\n            raise MIoTHttpError('get devices failed')\n        for did in dids:\n            if did not in results:\n                devices.pop(did, None)\n                _LOGGER.info('get device info failed, %s', did)\n                continue\n            devices[did].update(results[did])\n            # Whether sub devices\n            match_str = re.search(r'\\.s\\d+$', did)\n            if not match_str:\n                continue\n            device = devices.pop(did, None)\n            parent_did = did.replace(match_str.group(), '')\n            if parent_did in devices:\n                devices[parent_did].setdefault('sub_devices', {})\n                devices[parent_did]['sub_devices'][match_str.group()[\n                    1:]] = device\n            else:\n                _LOGGER.error(\n                    'unknown sub devices, %s, %s', did, parent_did)\n        return {\n            'uid': homeinfos['uid'],\n            'homes': homes,\n            'devices': devices\n        }\n\n    async def get_props_async(self, params: list) -> list:\n        \"\"\"\n        params = [{\"did\": \"xxxx\", \"siid\": 2, \"piid\": 1},\n                    {\"did\": \"xxxxxx\", \"siid\": 2, \"piid\": 2}]\n        \"\"\"\n        res_obj = await self.__mihome_api_post_async(\n            url_path='/app/v2/miotspec/prop/get',\n            data={\n                'datasource': 1,\n                'params': params\n            },\n        )\n        if 'result' not in res_obj:\n            raise MIoTHttpError('invalid response result')\n        return res_obj['result']\n\n    async def __get_prop_async(self, did: str, siid: int, piid: int) -> Any:\n        results = await self.get_props_async(\n            params=[{'did': did, 'siid': siid, 'piid': piid}])\n        if not results:\n            return None\n        result = results[0]\n        if 'value' not in result:\n            return None\n        return result['value']\n\n    async def __get_prop_handler(self) -> bool:\n        props_req: set[str] = set()\n        props_buffer: list[dict] = []\n\n        for key, item in self._get_prop_list.items():\n            if item.get('tag', False):\n                continue\n            # NOTICE: max req prop\n            if len(props_req) >= self.GET_PROP_MAX_REQ_COUNT:\n                break\n            item['tag'] = True\n            props_buffer.append(item['param'])\n            props_req.add(key)\n\n        if not props_buffer:\n            _LOGGER.error('get prop error, empty request list')\n            return False\n        results = await self.get_props_async(props_buffer)\n\n        for result in results:\n            if not all(\n                    key in result for key in ['did', 'siid', 'piid', 'value']):\n                continue\n            key = f'{result[\"did\"]}.{result[\"siid\"]}.{result[\"piid\"]}'\n            prop_obj = self._get_prop_list.pop(key, None)\n            if prop_obj is None:\n                _LOGGER.info('get prop error, key not exists, %s', result)\n                continue\n            prop_obj['fut'].set_result(result['value'])\n            props_req.remove(key)\n\n        for key in props_req:\n            prop_obj = self._get_prop_list.pop(key, None)\n            if prop_obj is None:\n                continue\n            prop_obj['fut'].set_result(None)\n        if props_req:\n            _LOGGER.info(\n                'get prop from cloud failed, %s', props_req)\n\n        if self._get_prop_list:\n            self._get_prop_timer = self._main_loop.call_later(\n                self.GET_PROP_AGGREGATE_INTERVAL,\n                lambda: self._main_loop.create_task(\n                    self.__get_prop_handler()))\n        else:\n            self._get_prop_timer = None\n        return True\n\n    async def get_prop_async(\n        self, did: str, siid: int, piid: int, immediately: bool = False\n    ) -> Any:\n        if immediately:\n            return await self.__get_prop_async(did, siid, piid)\n        key: str = f'{did}.{siid}.{piid}'\n        prop_obj = self._get_prop_list.get(key, None)\n        if prop_obj:\n            return await prop_obj['fut']\n        fut = self._main_loop.create_future()\n        self._get_prop_list[key] = {\n            'param': {'did': did, 'siid': siid, 'piid': piid},\n            'fut': fut\n        }\n        if self._get_prop_timer is None:\n            self._get_prop_timer = self._main_loop.call_later(\n                self.GET_PROP_AGGREGATE_INTERVAL,\n                lambda: self._main_loop.create_task(\n                    self.__get_prop_handler()))\n\n        return await fut\n\n    async def set_prop_async(self, params: list) -> list:\n        \"\"\"\n        params = [{\"did\": \"xxxx\", \"siid\": 2, \"piid\": 1, \"value\": False}]\n        \"\"\"\n        res_obj = await self.__mihome_api_post_async(\n            url_path='/app/v2/miotspec/prop/set',\n            data={\n                'params': params\n            },\n            timeout=15\n        )\n        if 'result' not in res_obj:\n            raise MIoTHttpError('invalid response result')\n\n        return res_obj['result']\n\n    async def action_async(\n        self, did: str, siid: int, aiid: int, in_list: list[dict]\n    ) -> dict:\n        \"\"\"\n        params = {\"did\": \"xxxx\", \"siid\": 2, \"aiid\": 1, \"in\": []}\n        \"\"\"\n        # NOTICE: Non-standard action param\n        res_obj = await self.__mihome_api_post_async(\n            url_path='/app/v2/miotspec/action',\n            data={\n                'params': {\n                    'did': did,\n                    'siid': siid,\n                    'aiid': aiid,\n                    'in': [item['value'] for item in in_list]}\n            },\n            timeout=15\n        )\n        if 'result' not in res_obj:\n            raise MIoTHttpError('invalid response result')\n\n        return res_obj['result']\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_device.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT device instance.\n\"\"\"\nimport asyncio\nfrom abc import abstractmethod\nfrom typing import Any, Callable, Optional\nimport logging\n\nfrom homeassistant.helpers.entity import Entity\nfrom homeassistant.const import (\n    CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,\n    CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,\n    CONCENTRATION_PARTS_PER_BILLION,\n    CONCENTRATION_PARTS_PER_MILLION,\n    DEGREE,\n    LIGHT_LUX,\n    REVOLUTIONS_PER_MINUTE,\n    PERCENTAGE,\n    SIGNAL_STRENGTH_DECIBELS,\n    UnitOfBloodGlucoseConcentration,\n    UnitOfEnergy,\n    UnitOfElectricCurrent,\n    UnitOfElectricPotential,\n    UnitOfFrequency,\n    UnitOfInformation,\n    UnitOfLength,\n    UnitOfMass,\n    UnitOfSpeed,\n    UnitOfTime,\n    UnitOfTemperature,\n    UnitOfPressure,\n    UnitOfPower,\n    UnitOfVolume,\n    UnitOfVolumeFlowRate,\n    UnitOfDataRate\n)\nfrom homeassistant.helpers.entity import DeviceInfo\nfrom homeassistant.components.switch import SwitchDeviceClass\n\n\n# pylint: disable=relative-beyond-top-level\nfrom .specs.specv2entity import (\n    SPEC_ACTION_TRANS_MAP,\n    SPEC_DEVICE_TRANS_MAP,\n    SPEC_EVENT_TRANS_MAP,\n    SPEC_PROP_TRANS_MAP,\n    SPEC_SERVICE_TRANS_MAP\n)\nfrom .common import slugify_name, slugify_did\nfrom .const import DOMAIN\nfrom .miot_client import MIoTClient\nfrom .miot_error import MIoTClientError, MIoTDeviceError\nfrom .miot_mips import MIoTDeviceState\nfrom .miot_spec import (\n    MIoTSpecAction,\n    MIoTSpecEvent,\n    MIoTSpecInstance,\n    MIoTSpecProperty,\n    MIoTSpecService,\n    MIoTSpecValueList,\n    MIoTSpecValueRange\n)\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass MIoTEntityData:\n    \"\"\"MIoT Entity Data.\"\"\"\n    platform: str\n    device_class: Any\n    spec: MIoTSpecInstance | MIoTSpecService\n\n    props: set[MIoTSpecProperty]\n    events: set[MIoTSpecEvent]\n    actions: set[MIoTSpecAction]\n\n    def __init__(\n        self, platform: str, spec: MIoTSpecInstance | MIoTSpecService\n    ) -> None:\n        self.platform = platform\n        self.spec = spec\n        self.device_class = None\n        self.props = set()\n        self.events = set()\n        self.actions = set()\n\n\nclass MIoTDevice:\n    \"\"\"MIoT Device Instance.\"\"\"\n    # pylint: disable=unused-argument\n    miot_client: MIoTClient\n    spec_instance: MIoTSpecInstance\n\n    _online: bool\n\n    _did: str\n    _name: str\n    _model: str\n    _model_strs: list[str]\n    _manufacturer: str\n    _fw_version: str\n\n    _icon: str\n    _home_id: str\n    _home_name: str\n    _room_id: str\n    _room_name: str\n\n    _suggested_area: Optional[str]\n\n    _sub_id: int\n    _device_state_sub_list: dict[str, dict[\n        str, Callable[[str, MIoTDeviceState], None]]]\n    _value_sub_list: dict[str, dict[str, Callable[[dict, Any], None]]]\n\n    _entity_list: dict[str, list[MIoTEntityData]]\n    _prop_list: dict[str, list[MIoTSpecProperty]]\n    _event_list: dict[str, list[MIoTSpecEvent]]\n    _action_list: dict[str, list[MIoTSpecAction]]\n\n    def __init__(\n        self, miot_client: MIoTClient,\n        device_info: dict[str, Any],\n        spec_instance: MIoTSpecInstance\n    ) -> None:\n        self.miot_client = miot_client\n        self.spec_instance = spec_instance\n\n        self._online = device_info.get('online', False)\n        self._did = device_info['did']\n        self._name = device_info['name']\n        self._model = device_info['model']\n        self._model_strs = self._model.split('.')\n        self._manufacturer = device_info.get('manufacturer', None)\n        self._fw_version = device_info.get('fw_version', None)\n\n        self._icon = device_info.get('icon', None)\n        self._home_id = device_info.get('home_id', None)\n        self._home_name = device_info.get('home_name', None)\n        self._room_id = device_info.get('room_id', None)\n        self._room_name = device_info.get('room_name', None)\n        match self.miot_client.area_name_rule:\n            case 'home_room':\n                self._suggested_area = (\n                    f'{self._home_name} {self._room_name}'.strip())\n            case 'home':\n                self._suggested_area = self._home_name.strip()\n            case 'room':\n                self._suggested_area = self._room_name.strip()\n            case _:\n                self._suggested_area = None\n\n        self._sub_id = 0\n        self._device_state_sub_list = {}\n        self._value_sub_list = {}\n        self._entity_list = {}\n        self._prop_list = {}\n        self._event_list = {}\n        self._action_list = {}\n\n        # Sub devices name\n        sub_devices: dict[str, dict] = device_info.get('sub_devices', None)\n        if isinstance(sub_devices, dict) and sub_devices:\n            for service in spec_instance.services:\n                sub_info = sub_devices.get(f's{service.iid}', None)\n                if sub_info is None:\n                    continue\n                _LOGGER.debug(\n                    'miot device, update service sub info, %s, %s',\n                    self.did, sub_info)\n                service.description_trans = sub_info.get(\n                    'name', service.description_trans)\n\n        # Sub device state\n        self.miot_client.sub_device_state(\n            self._did, self.__on_device_state_changed)\n\n        _LOGGER.debug('miot device init %s', device_info)\n\n    @property\n    def online(self) -> bool:\n        return self._online\n\n    @property\n    def entity_list(self) -> dict[str, list[MIoTEntityData]]:\n        return self._entity_list\n\n    @property\n    def prop_list(self) -> dict[str, list[MIoTSpecProperty]]:\n        return self._prop_list\n\n    @property\n    def event_list(self) -> dict[str, list[MIoTSpecEvent]]:\n        return self._event_list\n\n    @property\n    def action_list(self) -> dict[str, list[MIoTSpecAction]]:\n        return self._action_list\n\n    async def action_async(self, siid: int, aiid: int, in_list: list) -> list:\n        return await self.miot_client.action_async(\n            did=self._did, siid=siid, aiid=aiid, in_list=in_list)\n\n    def sub_device_state(\n        self, key: str, handler: Callable[[str, MIoTDeviceState], None]\n    ) -> int:\n        sub_id = self.__gen_sub_id()\n        if key in self._device_state_sub_list:\n            self._device_state_sub_list[key][str(sub_id)] = handler\n        else:\n            self._device_state_sub_list[key] = {str(sub_id): handler}\n        return sub_id\n\n    def unsub_device_state(self, key: str, sub_id: int) -> None:\n        sub_list = self._device_state_sub_list.get(key, None)\n        if sub_list:\n            sub_list.pop(str(sub_id), None)\n        if not sub_list:\n            self._device_state_sub_list.pop(key, None)\n\n    def sub_property(\n        self, handler: Callable[[dict, Any], None], siid: int, piid: int\n    ) -> int:\n        key: str = f'p.{siid}.{piid}'\n\n        def _on_prop_changed(params: dict, ctx: Any) -> None:\n            for handler in self._value_sub_list[key].values():\n                handler(params, ctx)\n\n        sub_id = self.__gen_sub_id()\n        if key in self._value_sub_list:\n            self._value_sub_list[key][str(sub_id)] = handler\n        else:\n            self._value_sub_list[key] = {str(sub_id): handler}\n            self.miot_client.sub_prop(\n                did=self._did, handler=_on_prop_changed, siid=siid, piid=piid)\n        return sub_id\n\n    def unsub_property(self, siid: int, piid: int, sub_id: int) -> None:\n        key: str = f'p.{siid}.{piid}'\n\n        sub_list = self._value_sub_list.get(key, None)\n        if sub_list:\n            sub_list.pop(str(sub_id), None)\n        if not sub_list:\n            self.miot_client.unsub_prop(did=self._did, siid=siid, piid=piid)\n            self._value_sub_list.pop(key, None)\n\n    def sub_event(\n        self, handler: Callable[[dict, Any], None], siid: int, eiid: int\n    ) -> int:\n        key: str = f'e.{siid}.{eiid}'\n\n        def _on_event_occurred(params: dict, ctx: Any) -> None:\n            for handler in self._value_sub_list[key].values():\n                handler(params, ctx)\n\n        sub_id = self.__gen_sub_id()\n        if key in self._value_sub_list:\n            self._value_sub_list[key][str(sub_id)] = handler\n        else:\n            self._value_sub_list[key] = {str(sub_id): handler}\n            self.miot_client.sub_event(\n                did=self._did, handler=_on_event_occurred, siid=siid, eiid=eiid)\n        return sub_id\n\n    def unsub_event(self, siid: int, eiid: int, sub_id: int) -> None:\n        key: str = f'e.{siid}.{eiid}'\n\n        sub_list = self._value_sub_list.get(key, None)\n        if sub_list:\n            sub_list.pop(str(sub_id), None)\n        if not sub_list:\n            self.miot_client.unsub_event(did=self._did, siid=siid, eiid=eiid)\n            self._value_sub_list.pop(key, None)\n\n    @property\n    def device_info(self) -> DeviceInfo:\n        \"\"\"information about this entity/device.\"\"\"\n        return DeviceInfo(\n            identifiers={(DOMAIN, self.did_tag)},\n            name=self._name,\n            sw_version=self._fw_version,\n            model=self._model,\n            manufacturer=self._manufacturer,\n            suggested_area=self._suggested_area,\n            configuration_url=(\n                f'https://home.mi.com/webapp/content/baike/product/index.html?'\n                f'model={self._model}')\n        )\n\n    @property\n    def did(self) -> str:\n        \"\"\"Device Id.\"\"\"\n        return self._did\n\n    @property\n    def did_tag(self) -> str:\n        return slugify_did(\n            cloud_server=self.miot_client.cloud_server, did=self._did)\n\n    def gen_device_entity_id(self, ha_domain: str) -> str:\n        return (\n            f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'\n            f'{self._model_strs[-1][:20]}')\n\n    def gen_service_entity_id(self, ha_domain: str, siid: int,\n                              description: str) -> str:\n        return (\n            f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'\n            f'{self._model_strs[-1][:20]}_s_{siid}_{description}')\n\n    def gen_prop_entity_id(\n        self, ha_domain: str, spec_name: str, siid: int, piid: int\n    ) -> str:\n        return (\n            f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'\n            f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'\n            f'_p_{siid}_{piid}')\n\n    def gen_event_entity_id(\n        self, ha_domain: str, spec_name: str, siid: int, eiid: int\n    ) -> str:\n        return (\n            f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'\n            f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'\n            f'_e_{siid}_{eiid}')\n\n    def gen_action_entity_id(\n        self, ha_domain: str, spec_name: str, siid: int, aiid: int\n    ) -> str:\n        return (\n            f'{ha_domain}.{self._model_strs[0][:9]}_{self.did_tag}_'\n            f'{self._model_strs[-1][:20]}_{slugify_name(spec_name)}'\n            f'_a_{siid}_{aiid}')\n\n    @property\n    def name(self) -> str:\n        return self._name\n\n    @property\n    def model(self) -> str:\n        return self._model\n\n    @property\n    def icon(self) -> str:\n        return self._icon\n\n    def append_entity(self, entity_data: MIoTEntityData) -> None:\n        self._entity_list.setdefault(entity_data.platform, [])\n        self._entity_list[entity_data.platform].append(entity_data)\n\n    def append_prop(self, prop: MIoTSpecProperty) -> None:\n        if not prop.platform:\n            return\n        self._prop_list.setdefault(prop.platform, [])\n        self._prop_list[prop.platform].append(prop)\n\n    def append_event(self, event: MIoTSpecEvent) -> None:\n        if not event.platform:\n            return\n        self._event_list.setdefault(event.platform, [])\n        self._event_list[event.platform].append(event)\n\n    def append_action(self, action: MIoTSpecAction) -> None:\n        if not action.platform:\n            return\n        self._action_list.setdefault(action.platform, [])\n        self._action_list[action.platform].append(action)\n\n    def parse_miot_device_entity(\n        self, spec_instance: MIoTSpecInstance\n    ) -> Optional[MIoTEntityData]:\n        if spec_instance.name not in SPEC_DEVICE_TRANS_MAP:\n            return None\n        spec_name: str = spec_instance.name\n        if isinstance(SPEC_DEVICE_TRANS_MAP[spec_name], str):\n            spec_name = SPEC_DEVICE_TRANS_MAP[spec_name]\n        if 'required' not in SPEC_DEVICE_TRANS_MAP[spec_name]:\n            return None\n        # 1. The device shall have all required services.\n        required_services = SPEC_DEVICE_TRANS_MAP[spec_name]['required'].keys()\n        if not {\n            service.name for service in spec_instance.services\n        }.issuperset(required_services):\n            return None\n        optional_services = SPEC_DEVICE_TRANS_MAP[spec_name]['optional'].keys()\n\n        platform = SPEC_DEVICE_TRANS_MAP[spec_name]['entity']\n        entity_data = MIoTEntityData(platform=platform, spec=spec_instance)\n        for service in spec_instance.services:\n            if service.platform:\n                continue\n            required_properties: dict\n            optional_properties: dict\n            required_actions: set\n            optional_actions: set\n            # 2. The required service shall have all required properties\n            # and actions.\n            if service.name in required_services:\n                required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'required'].get(\n                        service.name, {}\n                ).get('required', {}).get('properties', {})\n                optional_properties = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'required'].get(\n                        service.name, {}\n                ).get('optional', {}).get('properties', set({}))\n                required_actions = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'required'].get(\n                        service.name, {}\n                ).get('required', {}).get('actions', set({}))\n                optional_actions = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'required'].get(\n                        service.name, {}\n                ).get('optional', {}).get('actions', set({}))\n                if not {\n                    prop.name for prop in service.properties if prop.access\n                }.issuperset(set(required_properties.keys())):\n                    return None\n                if not {\n                    action.name for action in service.actions\n                }.issuperset(required_actions):\n                    return None\n                # 3. The required property in required service shall have all\n                # required access mode.\n                for prop in service.properties:\n                    if prop.name in required_properties:\n                        if not set(prop.access).issuperset(\n                                required_properties[prop.name]):\n                            return None\n            # 4. The optional service shall have all required properties\n            # and actions.\n            elif service.name in optional_services:\n                required_properties = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'optional'].get(\n                        service.name, {}\n                ).get('required', {}).get('properties', {})\n                optional_properties = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'optional'].get(\n                        service.name, {}\n                ).get('optional', {}).get('properties', set({}))\n                required_actions = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'optional'].get(\n                        service.name, {}\n                ).get('required', {}).get('actions', set({}))\n                optional_actions = SPEC_DEVICE_TRANS_MAP[spec_name][\n                    'optional'].get(\n                    service.name, {}\n                ).get('optional', {}).get('actions', set({}))\n                if not {\n                    prop.name for prop in service.properties if prop.access\n                }.issuperset(set(required_properties.keys())):\n                    continue\n                if not {\n                    action.name for action in service.actions\n                }.issuperset(required_actions):\n                    continue\n                # 5. The required property in optional service shall have all\n                # required access mode.\n                for prop in service.properties:\n                    if prop.name in required_properties:\n                        if not set(prop.access).issuperset(\n                                required_properties[prop.name]):\n                            continue\n            else:\n                continue\n            # property\n            for prop in service.properties:\n                if prop.name in set.union(\n                        set(required_properties.keys()), optional_properties):\n                    if prop.unit:\n                        prop.external_unit = self.unit_convert(prop.unit)\n                    #     prop.icon = self.icon_convert(prop.unit)\n                    prop.platform = platform\n                    entity_data.props.add(prop)\n            # action\n            for action in service.actions:\n                if action.name in set.union(\n                        required_actions, optional_actions):\n                    action.platform = platform\n                    entity_data.actions.add(action)\n            # event\n            # No events is in SPEC_DEVICE_TRANS_MAP now.\n            service.platform = platform\n        return entity_data\n\n    def parse_miot_service_entity(\n        self, miot_service: MIoTSpecService\n    ) -> Optional[MIoTEntityData]:\n        if (\n            miot_service.platform\n            or miot_service.name not in SPEC_SERVICE_TRANS_MAP\n        ):\n            return None\n        service_name = miot_service.name\n        if isinstance(SPEC_SERVICE_TRANS_MAP[service_name], str):\n            service_name = SPEC_SERVICE_TRANS_MAP[service_name]\n        if 'required' not in SPEC_SERVICE_TRANS_MAP[service_name]:\n            return None\n        # Required properties, required access mode\n        required_properties: dict = SPEC_SERVICE_TRANS_MAP[service_name][\n            'required'].get('properties', {})\n        if not {\n            prop.name for prop in miot_service.properties if prop.access\n        }.issuperset(set(required_properties.keys())):\n            return None\n        for prop in miot_service.properties:\n            if prop.name in required_properties:\n                if not set(prop.access).issuperset(\n                        required_properties[prop.name]):\n                    return None\n        # Required actions\n        # Required events\n        platform = SPEC_SERVICE_TRANS_MAP[service_name]['entity']\n        entity_data = MIoTEntityData(platform=platform, spec=miot_service)\n        # Optional properties\n        optional_properties = SPEC_SERVICE_TRANS_MAP[service_name][\n            'optional'].get('properties', set({}))\n        for prop in miot_service.properties:\n            if prop.name in set.union(\n                    set(required_properties.keys()), optional_properties):\n                if prop.unit:\n                    prop.external_unit = self.unit_convert(prop.unit)\n                    # prop.icon = self.icon_convert(prop.unit)\n                prop.platform = platform\n                entity_data.props.add(prop)\n        # Optional actions\n        # Optional events\n        miot_service.platform = platform\n        # entity_category\n        if entity_category := SPEC_SERVICE_TRANS_MAP[service_name].get(\n            'entity_category', None):\n            miot_service.entity_category = entity_category\n        return entity_data\n\n    def parse_miot_property_entity(self, miot_prop: MIoTSpecProperty) -> bool:\n        if (\n            miot_prop.platform\n            or miot_prop.name not in SPEC_PROP_TRANS_MAP['properties']\n        ):\n            return False\n        prop_name = miot_prop.name\n        if isinstance(SPEC_PROP_TRANS_MAP['properties'][prop_name], str):\n            prop_name = SPEC_PROP_TRANS_MAP['properties'][prop_name]\n        platform = SPEC_PROP_TRANS_MAP['properties'][prop_name]['entity']\n        # Check\n        prop_access: set = set({})\n        if miot_prop.readable:\n            prop_access.add('read')\n        if miot_prop.writable:\n            prop_access.add('write')\n        if prop_access != (SPEC_PROP_TRANS_MAP[\n                'entities'][platform]['access']):\n            return False\n        if miot_prop.format_.__name__ not in SPEC_PROP_TRANS_MAP[\n                'entities'][platform]['format']:\n            return False\n        miot_prop.device_class = SPEC_PROP_TRANS_MAP['properties'][prop_name][\n            'device_class']\n        # Optional params\n        if 'state_class' in SPEC_PROP_TRANS_MAP['properties'][prop_name]:\n            miot_prop.state_class = SPEC_PROP_TRANS_MAP['properties'][\n                prop_name]['state_class']\n        if (\n            not miot_prop.external_unit\n            and 'unit_of_measurement' in SPEC_PROP_TRANS_MAP['properties'][\n                prop_name]\n        ):\n            # Priority: spec_modify.unit > unit_convert > specv2entity.unit\n            miot_prop.external_unit = SPEC_PROP_TRANS_MAP['properties'][\n                prop_name]['unit_of_measurement']\n        # Priority: default.icon when device_class is set > spec_modify.icon\n        #           > icon_convert\n        miot_prop.platform = platform\n        return True\n\n    def spec_transform(self) -> None:\n        \"\"\"Parse service, property, event, action from device spec.\"\"\"\n        # STEP 1: device conversion\n        device_entity = self.parse_miot_device_entity(\n            spec_instance=self.spec_instance)\n        if device_entity:\n            self.append_entity(entity_data=device_entity)\n        # STEP 2: service conversion\n        for service in self.spec_instance.services:\n            service_entity = self.parse_miot_service_entity(\n                miot_service=service)\n            if service_entity:\n                self.append_entity(entity_data=service_entity)\n            # STEP 3.1: property conversion\n            for prop in service.properties:\n                if prop.platform or not prop.access:\n                    continue\n                if prop.unit:\n                    prop.external_unit = self.unit_convert(prop.unit)\n                    if not prop.icon:\n                        prop.icon = self.icon_convert(prop.unit)\n                # Special conversion\n                self.parse_miot_property_entity(miot_prop=prop)\n                # General conversion\n                if not prop.platform:\n                    if prop.writable:\n                        if prop.format_ == str:\n                            prop.platform = 'text'\n                        elif prop.format_ == bool:\n                            prop.platform = 'switch'\n                            prop.device_class = SwitchDeviceClass.SWITCH\n                        elif prop.value_list:\n                            prop.platform = 'select'\n                        elif prop.value_range:\n                            prop.platform = 'number'\n                        else:\n                            # Irregular property will not be transformed.\n                            continue\n                    elif prop.readable or prop.notifiable:\n                        if prop.format_ == bool:\n                            prop.platform = 'binary_sensor'\n                        else:\n                            prop.platform = 'sensor'\n                self.append_prop(prop=prop)\n            # STEP 3.2: event conversion\n            for event in service.events:\n                if event.platform:\n                    continue\n                event.platform = 'event'\n                if event.name in SPEC_EVENT_TRANS_MAP:\n                    event.device_class = SPEC_EVENT_TRANS_MAP[event.name]\n                self.append_event(event=event)\n            # STEP 3.3: action conversion\n            for action in service.actions:\n                if action.platform:\n                    continue\n                if action.name in SPEC_ACTION_TRANS_MAP:\n                    continue\n                if action.in_:\n                    action.platform = 'notify'\n                else:\n                    action.platform = 'button'\n                self.append_action(action=action)\n\n    def unit_convert(self, spec_unit: str) -> Optional[str]:\n        \"\"\"Convert MIoT unit to Home Assistant unit.\n        2026/01/06: property unit statistics of the latest released\n        MIoT-Spec-V2 for all device models: unit, quantity.\n        {\n            \"no_unit\": 148499,\n            \"percentage\": 12074,\n            \"none\": 11857,\n            \"minutes\": 5707,\n            \"celsius\": 5767,\n            \"seconds\": 3062,\n            \"kelvin\": 2511,\n            \"hours\": 1380,\n            \"days\": 615,\n            \"rgb\": 752,         // color\n            \"L\": 379,\n            \"mg/m3\": 335,\n            \"ppm\": 182,\n            \"watt\": 246,\n            \"arcdegrees\": 130,\n            \"μg/m3\": 117,\n            \"kWh\": 149,\n            \"ms\": 108,\n            \"pascal\": 108,\n            \"lux\": 100,\n            \"V\": 59,\n            \"m\": 45,\n            \"A\": 36,\n            \"mL\": 30,\n            \"arcdegress\": 25,\n            \"mA\": 26,\n            \"bpm\": 21,          // realtime-heartrate\n            \"B/s\": 21,\n            \"weeks\": 18,\n            \"dB\": 17,\n            \"calorie\": 18,      // 1 cal = 4.184 J\n            \"metre\": 15,\n            \"hour\": 11,\n            \"cm\": 12,\n            \"gram\": 8,\n            \"km/h\": 8,\n            \"mV\": 9,\n            \"times\": 4,         // exercise-count\n            \"kCal\": 4,\n            \"mmHg\": 4,\n            \"pcs\": 3,\n            \"meter\": 3,\n            \"kW\": 2,\n            \"KByte/s\": 2,\n            \"毫摩尔每升\": 2,      // blood-sugar, cholesterol\n            \"m3/h\": 2,\n            \"ppb\": 2,\n            \"mv\": 2,\n            \"w\": 1,\n            \"bar\": 1,\n            \"megapascal\": 1,\n            \"kB\": 1,\n            \"mmol/L\": 1,        // urea\n            \"min/km\": 1,\n            \"kilopascal\": 1,\n            \"liter\": 1,\n            \"W\": 1\n        }\n        \"\"\"\n        unit_map = {\n            'percentage': PERCENTAGE,\n            'weeks': UnitOfTime.WEEKS,\n            'days': UnitOfTime.DAYS,\n            'hour': UnitOfTime.HOURS,\n            'hours': UnitOfTime.HOURS,\n            'minutes': UnitOfTime.MINUTES,\n            'seconds': UnitOfTime.SECONDS,\n            'ms': UnitOfTime.MILLISECONDS,\n            'μs': UnitOfTime.MICROSECONDS,\n            'celsius': UnitOfTemperature.CELSIUS,\n            'fahrenheit': UnitOfTemperature.FAHRENHEIT,\n            'kelvin': UnitOfTemperature.KELVIN,\n            'μg/m3': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,\n            'mg/m3': CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,\n            'ppm': CONCENTRATION_PARTS_PER_MILLION,\n            'ppb': CONCENTRATION_PARTS_PER_BILLION,\n            'lux': LIGHT_LUX,\n            'pascal': UnitOfPressure.PA,\n            'kilopascal': UnitOfPressure.KPA,\n            'mmHg': UnitOfPressure.MMHG,\n            'bar': UnitOfPressure.BAR,\n            'L': UnitOfVolume.LITERS,\n            'liter': UnitOfVolume.LITERS,\n            'mL': UnitOfVolume.MILLILITERS,\n            'Hz': UnitOfFrequency.HERTZ,\n            'calorie': UnitOfEnergy.CALORIE,\n            'kCal': UnitOfEnergy.KILO_CALORIE,\n            'km/h': UnitOfSpeed.KILOMETERS_PER_HOUR,\n            'm/s': UnitOfSpeed.METERS_PER_SECOND,\n            'watt': UnitOfPower.WATT,\n            'w': UnitOfPower.WATT,\n            'W': UnitOfPower.WATT,\n            'kW': UnitOfPower.KILO_WATT,\n            'Wh': UnitOfEnergy.WATT_HOUR,\n            'kWh': UnitOfEnergy.KILO_WATT_HOUR,\n            'A': UnitOfElectricCurrent.AMPERE,\n            'mA': UnitOfElectricCurrent.MILLIAMPERE,\n            'V': UnitOfElectricPotential.VOLT,\n            'mv': UnitOfElectricPotential.MILLIVOLT,\n            'mV': UnitOfElectricPotential.MILLIVOLT,\n            'cm': UnitOfLength.CENTIMETERS,\n            'm': UnitOfLength.METERS,\n            'meter': UnitOfLength.METERS,\n            'km': UnitOfLength.KILOMETERS,\n            'm3/h': UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,\n            '毫摩尔每升': UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER,\n            'mmol/L': UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER,\n            'rpm': REVOLUTIONS_PER_MINUTE,\n            'gram': UnitOfMass.GRAMS,\n            'kilogram': UnitOfMass.KILOGRAMS,\n            'dB': SIGNAL_STRENGTH_DECIBELS,\n            'arcdegrees': DEGREE,\n            'arcdegress': DEGREE,\n            'kB': UnitOfInformation.KILOBYTES,\n            'MB': UnitOfInformation.MEGABYTES,\n            'GB': UnitOfInformation.GIGABYTES,\n            'TB': UnitOfInformation.TERABYTES,\n            'B/s': UnitOfDataRate.BYTES_PER_SECOND,\n            'KB/s': UnitOfDataRate.KILOBYTES_PER_SECOND,\n            'KByte/s': UnitOfDataRate.KILOBYTES_PER_SECOND,\n            'MB/s': UnitOfDataRate.MEGABYTES_PER_SECOND,\n            'GB/s': UnitOfDataRate.GIGABYTES_PER_SECOND\n        }\n\n        # Handle UnitOfConductivity separately since\n        # it might not be available in all HA versions\n        try:\n            # pylint: disable=import-outside-toplevel\n            from homeassistant.const import UnitOfConductivity  # type: ignore\n            unit_map['μS/cm'] = UnitOfConductivity.MICROSIEMENS_PER_CM\n            unit_map['mWh'] = UnitOfEnergy.MILLIWATT_HOUR\n        except Exception:  # pylint: disable=broad-except\n            unit_map['μS/cm'] = 'μS/cm'\n            unit_map['mWh'] = 'mWh'\n\n        return unit_map.get(spec_unit, None)\n\n    def icon_convert(self, spec_unit: str) -> Optional[str]:\n        if spec_unit in {'percentage'}:\n            return 'mdi:percent'\n        if spec_unit in {\n            'weeks', 'days', 'hour', 'hours', 'minutes', 'seconds', 'ms', 'μs'\n        }:\n            return 'mdi:clock'\n        if spec_unit in {'celsius'}:\n            return 'mdi:temperature-celsius'\n        if spec_unit in {'fahrenheit'}:\n            return 'mdi:temperature-fahrenheit'\n        if spec_unit in {'kelvin'}:\n            return 'mdi:temperature-kelvin'\n        if spec_unit in {'μg/m3', 'mg/m3', 'ppm', 'ppb'}:\n            return 'mdi:blur'\n        if spec_unit in {'lux'}:\n            return 'mdi:brightness-6'\n        if spec_unit in {'pascal', 'kilopascal', 'megapascal', 'mmHg', 'bar'}:\n            return 'mdi:gauge'\n        if spec_unit in {'watt', 'w', 'W'}:\n            return 'mdi:flash-triangle'\n        if spec_unit in {'L', 'mL'}:\n            return 'mdi:gas-cylinder'\n        if spec_unit in {'km/h', 'm/s'}:\n            return 'mdi:speedometer'\n        if spec_unit in {'kWh'}:\n            return 'mdi:transmission-tower'\n        if spec_unit in {'A', 'mA'}:\n            return 'mdi:current-ac'\n        if spec_unit in {'V', 'mv', 'mV'}:\n            return 'mdi:current-dc'\n        if spec_unit in {'cm', 'm', 'meter', 'km'}:\n            return 'mdi:ruler'\n        if spec_unit in {'rgb'}:\n            return 'mdi:palette'\n        if spec_unit in {'m3/h', 'L/s'}:\n            return 'mdi:pipe-leak'\n        if spec_unit in {'μS/cm'}:\n            return 'mdi:resistor-nodes'\n        if spec_unit in {'gram', 'kilogram'}:\n            return 'mdi:weight'\n        if spec_unit in {'dB'}:\n            return 'mdi:signal-distance-variant'\n        if spec_unit in {'times'}:\n            return 'mdi:counter'\n        if spec_unit in {'mmol/L'}:\n            return 'mdi:dots-hexagon'\n        if spec_unit in {'kB', 'MB', 'GB'}:\n            return 'mdi:network-pos'\n        if spec_unit in {'arcdegress', 'arcdegrees'}:\n            return 'mdi:angle-obtuse'\n        if spec_unit in {'B/s', 'KB/s', 'MB/s', 'GB/s'}:\n            return 'mdi:network'\n        if spec_unit in {'calorie', 'kCal'}:\n            return 'mdi:food'\n        if spec_unit in {'rpm'}:\n            return 'mdi:fan-clock'\n        return None\n\n    def __gen_sub_id(self) -> int:\n        self._sub_id += 1\n        return self._sub_id\n\n    def __on_device_state_changed(\n        self, did: str, state: MIoTDeviceState, ctx: Any\n    ) -> None:\n        self._online = state == MIoTDeviceState.ONLINE\n        for key, sub_list in self._device_state_sub_list.items():\n            for handler in sub_list.values():\n                self.miot_client.main_loop.call_soon_threadsafe(\n                    handler, key, state)\n\n\nclass MIoTServiceEntity(Entity):\n    \"\"\"MIoT Service Entity.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    miot_device: MIoTDevice\n    entity_data: MIoTEntityData\n\n    _main_loop: asyncio.AbstractEventLoop\n    _prop_value_map: dict[MIoTSpecProperty, Any]\n    _state_sub_id: int\n    _value_sub_ids: dict[str, int]\n\n    _event_occurred_handler: Optional[\n        Callable[[MIoTSpecEvent, dict], None]]\n    _prop_changed_subs: dict[\n        MIoTSpecProperty, Callable[[MIoTSpecProperty, Any], None]]\n\n    _pending_write_ha_state_timer: Optional[asyncio.TimerHandle]\n\n    def __init__(\n        self, miot_device: MIoTDevice, entity_data: MIoTEntityData\n    ) -> None:\n        if (\n            miot_device is None\n            or entity_data is None\n            or entity_data.spec is None\n        ):\n            raise MIoTDeviceError('init error, invalid params')\n        self.miot_device = miot_device\n        self.entity_data = entity_data\n        self._main_loop = miot_device.miot_client.main_loop\n        self._prop_value_map = {}\n        self._state_sub_id = 0\n        self._value_sub_ids = {}\n        # Gen entity id\n        if isinstance(self.entity_data.spec, MIoTSpecInstance):\n            self.entity_id = miot_device.gen_device_entity_id(DOMAIN)\n            self._attr_name = f' {self.entity_data.spec.description_trans}'\n        elif isinstance(self.entity_data.spec, MIoTSpecService):\n            self.entity_id = miot_device.gen_service_entity_id(\n                DOMAIN, siid=self.entity_data.spec.iid,\n                description=self.entity_data.spec.description)\n            self._attr_name = (\n                f'{\"* \"if self.entity_data.spec.proprietary else \" \"}'\n                f'{self.entity_data.spec.description_trans}')\n            self._attr_entity_category = entity_data.spec.entity_category\n        # Set entity attr\n        self._attr_unique_id = self.entity_id\n        self._attr_should_poll = False\n        self._attr_has_entity_name = True\n        self._attr_available = miot_device.online\n\n        self._event_occurred_handler = None\n        self._prop_changed_subs = {}\n        self._pending_write_ha_state_timer = None\n        _LOGGER.info(\n            'new miot service entity, %s, %s, %s, %s',\n            self.miot_device.name, self._attr_name, self.entity_data.spec.name,\n            self.entity_id)\n\n    @property\n    def event_occurred_handler(\n        self\n    ) -> Optional[Callable[[MIoTSpecEvent, dict], None]]:\n        return self._event_occurred_handler\n\n    @event_occurred_handler.setter\n    def event_occurred_handler(self, func) -> None:\n        self._event_occurred_handler = func\n\n    def sub_prop_changed(\n        self, prop: MIoTSpecProperty,\n        handler: Callable[[MIoTSpecProperty, Any], None]\n    ) -> None:\n        if not prop or not handler:\n            _LOGGER.error(\n                'sub_prop_changed error, invalid prop/handler')\n            return\n        self._prop_changed_subs[prop] = handler\n\n    def unsub_prop_changed(self, prop: MIoTSpecProperty) -> None:\n        self._prop_changed_subs.pop(prop, None)\n\n    @property\n    def device_info(self) -> Optional[DeviceInfo]:\n        return self.miot_device.device_info\n\n    async def async_added_to_hass(self) -> None:\n        state_id = 's.0'\n        if isinstance(self.entity_data.spec, MIoTSpecService):\n            state_id = f's.{self.entity_data.spec.iid}'\n        self._state_sub_id = self.miot_device.sub_device_state(\n            key=state_id, handler=self.__on_device_state_changed)\n        # Sub prop\n        for prop in self.entity_data.props:\n            if not prop.notifiable and not prop.readable:\n                continue\n            key = f'p.{prop.service.iid}.{prop.iid}'\n            self._value_sub_ids[key] = self.miot_device.sub_property(\n                handler=self.__on_properties_changed,\n                siid=prop.service.iid, piid=prop.iid)\n        # Sub event\n        for event in self.entity_data.events:\n            key = f'e.{event.service.iid}.{event.iid}'\n            self._value_sub_ids[key] = self.miot_device.sub_event(\n                handler=self.__on_event_occurred,\n                siid=event.service.iid, eiid=event.iid)\n\n        # Refresh value\n        if self._attr_available:\n            self.__refresh_props_value()\n\n    async def async_will_remove_from_hass(self) -> None:\n        if self._pending_write_ha_state_timer:\n            self._pending_write_ha_state_timer.cancel()\n            self._pending_write_ha_state_timer = None\n        state_id = 's.0'\n        if isinstance(self.entity_data.spec, MIoTSpecService):\n            state_id = f's.{self.entity_data.spec.iid}'\n        self.miot_device.unsub_device_state(\n            key=state_id, sub_id=self._state_sub_id)\n        # Unsub prop\n        for prop in self.entity_data.props:\n            if not prop.notifiable and not prop.readable:\n                continue\n            sub_id = self._value_sub_ids.pop(\n                f'p.{prop.service.iid}.{prop.iid}', None)\n            if sub_id:\n                self.miot_device.unsub_property(\n                    siid=prop.service.iid, piid=prop.iid, sub_id=sub_id)\n        # Unsub event\n        for event in self.entity_data.events:\n            sub_id = self._value_sub_ids.pop(\n                f'e.{event.service.iid}.{event.iid}', None)\n            if sub_id:\n                self.miot_device.unsub_event(\n                    siid=event.service.iid, eiid=event.iid, sub_id=sub_id)\n\n    def get_map_value(\n        self, map_: Optional[dict[int, Any]], key: int\n    ) -> Any:\n        if map_ is None:\n            return None\n        return map_.get(key, None)\n\n    def get_map_key(\n        self, map_: Optional[dict[int, Any]], value: Any\n    ) -> Optional[int]:\n        if map_ is None:\n            return None\n        for key, value_ in map_.items():\n            if value_ == value:\n                return key\n        return None\n\n    def get_prop_value(self, prop: Optional[MIoTSpecProperty]) -> Any:\n        if not prop:\n            _LOGGER.error(\n                'get_prop_value error, property is None, %s, %s',\n                self._attr_name, self.entity_id)\n            return None\n        return self._prop_value_map.get(prop, None)\n\n    def set_prop_value(\n        self, prop: Optional[MIoTSpecProperty], value: Any\n    ) -> None:\n        if not prop:\n            _LOGGER.error(\n                'set_prop_value error, property is None, %s, %s',\n                self._attr_name, self.entity_id)\n            return\n        self._prop_value_map[prop] = value\n\n    async def set_property_async(\n        self, prop: Optional[MIoTSpecProperty], value: Any,\n        update_value: bool = True, write_ha_state: bool = True\n    ) -> bool:\n        if not prop:\n            raise RuntimeError(\n                f'set property failed, property is None, '\n                f'{self.entity_id}, {self.name}')\n        value = prop.value_format(value)\n        value = prop.value_precision(value)\n        if prop not in self.entity_data.props:\n            raise RuntimeError(\n                f'set property failed, unknown property, '\n                f'{self.entity_id}, {self.name}, {prop.name}')\n        if not prop.writable:\n            raise RuntimeError(\n                f'set property failed, not writable, '\n                f'{self.entity_id}, {self.name}, {prop.name}')\n        try:\n            await self.miot_device.miot_client.set_prop_async(\n                did=self.miot_device.did, siid=prop.service.iid,\n                piid=prop.iid, value=value)\n        except MIoTClientError as e:\n            raise RuntimeError(\n                f'{e}, {self.entity_id}, {self.name}, {prop.name}') from e\n        if update_value:\n            self._prop_value_map[prop] = value\n        if write_ha_state:\n            self.async_write_ha_state()\n        return True\n\n    async def get_property_async(self, prop: MIoTSpecProperty) -> Any:\n        if not prop:\n            _LOGGER.error(\n                'get property failed, property is None, %s, %s',\n                self.entity_id, self.name)\n            return None\n        if prop not in self.entity_data.props:\n            _LOGGER.error(\n                'get property failed, unknown property, %s, %s, %s',\n                self.entity_id, self.name, prop.name)\n            return None\n        if not prop.readable:\n            _LOGGER.error(\n                'get property failed, not readable, %s, %s, %s',\n                self.entity_id, self.name, prop.name)\n            return None\n        value: Any = prop.value_format(\n            await self.miot_device.miot_client.get_prop_async(\n                did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid))\n        value = prop.eval_expr(value)\n        result = prop.value_precision(value)\n        if result != self._prop_value_map[prop]:\n            self._prop_value_map[prop] = result\n            self.async_write_ha_state()\n        return result\n\n    async def action_async(\n        self, action: MIoTSpecAction, in_list: Optional[list] = None\n    ) -> bool:\n        if not action:\n            raise RuntimeError(\n                f'action failed, action is None, {self.entity_id}, {self.name}')\n        try:\n            await self.miot_device.miot_client.action_async(\n                did=self.miot_device.did, siid=action.service.iid,\n                aiid=action.iid, in_list=in_list or [])\n        except MIoTClientError as e:\n            raise RuntimeError(\n                f'{e}, {self.entity_id}, {self.name}, {action.name}') from e\n        return True\n\n    def __on_properties_changed(self, params: dict, ctx: Any) -> None:\n        _LOGGER.debug('properties changed, %s', params)\n        for prop in self.entity_data.props:\n            if (\n                prop.iid != params['piid']\n                or prop.service.iid != params['siid']\n            ):\n                continue\n            value: Any = prop.value_format(params['value'])\n            value = prop.eval_expr(value)\n            value = prop.value_precision(value)\n            self._prop_value_map[prop] = value\n            if prop in self._prop_changed_subs:\n                self._prop_changed_subs[prop](prop, value)\n            break\n        if not self._pending_write_ha_state_timer:\n            self.async_write_ha_state()\n\n    def __on_event_occurred(self, params: dict, ctx: Any) -> None:\n        _LOGGER.debug('event occurred, %s', params)\n        if self._event_occurred_handler is None:\n            return\n        for event in self.entity_data.events:\n            if (\n                event.iid != params['eiid']\n                or event.service.iid != params['siid']\n            ):\n                continue\n            trans_arg = {}\n            for item in params['arguments']:\n                for prop in event.argument:\n                    if prop.iid == item['piid']:\n                        trans_arg[prop.description_trans] = item['value']\n                        break\n            self._event_occurred_handler(event, trans_arg)\n            break\n\n    def __on_device_state_changed(\n        self, key: str, state: MIoTDeviceState\n    ) -> None:\n        state_new = state == MIoTDeviceState.ONLINE\n        if state_new == self._attr_available:\n            return\n        self._attr_available = state_new\n        if not self._attr_available:\n            self.async_write_ha_state()\n            return\n        self.__refresh_props_value()\n\n    def __refresh_props_value(self) -> None:\n        for prop in self.entity_data.props:\n            if not prop.readable:\n                continue\n            self.miot_device.miot_client.request_refresh_prop(\n                did=self.miot_device.did, siid=prop.service.iid, piid=prop.iid)\n        if self._pending_write_ha_state_timer:\n            self._pending_write_ha_state_timer.cancel()\n        self._pending_write_ha_state_timer = self._main_loop.call_later(\n            1, self.__write_ha_state_handler)\n\n    def __write_ha_state_handler(self) -> None:\n        self._pending_write_ha_state_timer = None\n        self.async_write_ha_state()\n\n\nclass MIoTPropertyEntity(Entity):\n    \"\"\"MIoT Property Entity.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    miot_device: MIoTDevice\n    spec: MIoTSpecProperty\n    service: MIoTSpecService\n\n    _main_loop: asyncio.AbstractEventLoop\n    _value_range: Optional[MIoTSpecValueRange]\n    # {Any: Any}\n    _value_list: Optional[MIoTSpecValueList]\n    _value: Any\n    _state_sub_id: int\n    _value_sub_id: int\n\n    _pending_write_ha_state_timer: Optional[asyncio.TimerHandle]\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        if miot_device is None or spec is None or spec.service is None:\n            raise MIoTDeviceError('init error, invalid params')\n        self.miot_device = miot_device\n        self.spec = spec\n        self.service = spec.service\n        self._main_loop = miot_device.miot_client.main_loop\n        self._value_range = spec.value_range\n        self._value_list = spec.value_list\n        self._value = None\n        self._state_sub_id = 0\n        self._value_sub_id = 0\n        self._pending_write_ha_state_timer = None\n        # Gen entity_id\n        self.entity_id = self.miot_device.gen_prop_entity_id(\n            ha_domain=DOMAIN, spec_name=spec.name,\n            siid=spec.service.iid, piid=spec.iid)\n        # Set entity attr\n        self._attr_unique_id = self.entity_id\n        self._attr_should_poll = False\n        self._attr_has_entity_name = True\n        self._attr_name = (\n            f'{\"* \"if self.spec.proprietary else \" \"}'\n            f'{self.service.description_trans} {spec.description_trans}')\n        self._attr_available = miot_device.online\n\n        _LOGGER.info(\n            'new miot property entity, %s, %s, %s, %s, %s',\n            self.miot_device.name, self._attr_name, spec.platform,\n            spec.device_class, self.entity_id)\n\n    @property\n    def device_info(self) -> Optional[DeviceInfo]:\n        return self.miot_device.device_info\n\n    async def async_added_to_hass(self) -> None:\n        # Sub device state changed\n        self._state_sub_id = self.miot_device.sub_device_state(\n            key=f'{ self.service.iid}.{self.spec.iid}',\n            handler=self.__on_device_state_changed)\n        # Sub value changed\n        self._value_sub_id = self.miot_device.sub_property(\n            handler=self.__on_value_changed,\n            siid=self.service.iid, piid=self.spec.iid)\n        # Refresh value\n        if self._attr_available:\n            self.__request_refresh_prop()\n\n    async def async_will_remove_from_hass(self) -> None:\n        if self._pending_write_ha_state_timer:\n            self._pending_write_ha_state_timer.cancel()\n            self._pending_write_ha_state_timer = None\n        self.miot_device.unsub_device_state(\n            key=f'{ self.service.iid}.{self.spec.iid}',\n            sub_id=self._state_sub_id)\n        self.miot_device.unsub_property(\n            siid=self.service.iid, piid=self.spec.iid,\n            sub_id=self._value_sub_id)\n\n    def get_vlist_description(self, value: Any) -> Optional[str]:\n        if not self._value_list:\n            return None\n        return self._value_list.get_description_by_value(value)\n\n    def get_vlist_value(self, description: str) -> Any:\n        if not self._value_list:\n            return None\n        return self._value_list.get_value_by_description(description)\n\n    async def set_property_async(self, value: Any) -> bool:\n        if not self.spec.writable:\n            raise RuntimeError(\n                f'set property failed, not writable, '\n                f'{self.entity_id}, {self.name}')\n        value = self.spec.value_format(value)\n        value = self.spec.value_precision(value)\n        try:\n            await self.miot_device.miot_client.set_prop_async(\n                did=self.miot_device.did, siid=self.spec.service.iid,\n                piid=self.spec.iid, value=value)\n        except MIoTClientError as e:\n            raise RuntimeError(\n                f'{e}, {self.entity_id}, {self.name}') from e\n        self._value = value\n        self.async_write_ha_state()\n        return True\n\n    async def get_property_async(self) -> Any:\n        if not self.spec.readable:\n            _LOGGER.error(\n                'get property failed, not readable, %s, %s',\n                self.entity_id, self.name)\n            return None\n        value: Any = self.spec.value_format(\n            await self.miot_device.miot_client.get_prop_async(\n                did=self.miot_device.did, siid=self.spec.service.iid,\n                piid=self.spec.iid))\n        value = self.spec.eval_expr(value)\n        result = self.spec.value_precision(value)\n        return result\n\n    def __on_value_changed(self, params: dict, ctx: Any) -> None:\n        _LOGGER.debug('property changed, %s', params)\n        value: Any = self.spec.value_format(params['value'])\n        value = self.spec.eval_expr(value)\n        self._value = self.spec.value_precision(value)\n        if not self._pending_write_ha_state_timer:\n            self.async_write_ha_state()\n\n    def __on_device_state_changed(\n        self, key: str, state: MIoTDeviceState\n    ) -> None:\n        self._attr_available = state == MIoTDeviceState.ONLINE\n        if not self._attr_available:\n            self.async_write_ha_state()\n            return\n        # Refresh value\n        self.__request_refresh_prop()\n\n    def __request_refresh_prop(self) -> None:\n        if self.spec.readable:\n            self.miot_device.miot_client.request_refresh_prop(\n                did=self.miot_device.did, siid=self.service.iid,\n                piid=self.spec.iid)\n        if self._pending_write_ha_state_timer:\n            self._pending_write_ha_state_timer.cancel()\n        self._pending_write_ha_state_timer = self._main_loop.call_later(\n            1, self.__write_ha_state_handler)\n\n    def __write_ha_state_handler(self) -> None:\n        self._pending_write_ha_state_timer = None\n        self.async_write_ha_state()\n\n\nclass MIoTEventEntity(Entity):\n    \"\"\"MIoT Event Entity.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    miot_device: MIoTDevice\n    spec: MIoTSpecEvent\n    service: MIoTSpecService\n\n    _main_loop: asyncio.AbstractEventLoop\n    _attr_event_types: list[str]\n    _arguments_map: dict[int, str]\n    _state_sub_id: int\n    _value_sub_id: int\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecEvent) -> None:\n        if miot_device is None or spec is None or spec.service is None:\n            raise MIoTDeviceError('init error, invalid params')\n        self.miot_device = miot_device\n        self.spec = spec\n        self.service = spec.service\n        self._main_loop = miot_device.miot_client.main_loop\n        # Gen entity_id\n        self.entity_id = self.miot_device.gen_event_entity_id(\n            ha_domain=DOMAIN, spec_name=spec.name,\n            siid=spec.service.iid,  eiid=spec.iid)\n        # Set entity attr\n        self._attr_unique_id = self.entity_id\n        self._attr_should_poll = False\n        self._attr_has_entity_name = True\n        self._attr_name = (\n            f'{\"* \"if self.spec.proprietary else \" \"}'\n            f'{self.service.description_trans} {spec.description_trans}')\n        self._attr_available = miot_device.online\n        self._attr_event_types = [spec.description_trans]\n\n        self._arguments_map = {}\n        for prop in spec.argument:\n            self._arguments_map[prop.iid] = prop.description_trans\n        self._state_sub_id = 0\n        self._value_sub_id = 0\n\n        _LOGGER.info(\n            'new miot event entity, %s, %s, %s, %s, %s',\n            self.miot_device.name, self._attr_name, spec.platform,\n            spec.device_class, self.entity_id)\n\n    @property\n    def device_info(self) -> Optional[DeviceInfo]:\n        return self.miot_device.device_info\n\n    async def async_added_to_hass(self) -> None:\n        # Sub device state changed\n        self._state_sub_id = self.miot_device.sub_device_state(\n            key=f'event.{ self.service.iid}.{self.spec.iid}',\n            handler=self.__on_device_state_changed)\n        # Sub value changed\n        self._value_sub_id = self.miot_device.sub_event(\n            handler=self.__on_event_occurred,\n            siid=self.service.iid, eiid=self.spec.iid)\n\n    async def async_will_remove_from_hass(self) -> None:\n        self.miot_device.unsub_device_state(\n            key=f'event.{ self.service.iid}.{self.spec.iid}',\n            sub_id=self._state_sub_id)\n        self.miot_device.unsub_event(\n            siid=self.service.iid, eiid=self.spec.iid,\n            sub_id=self._value_sub_id)\n\n    @abstractmethod\n    def on_event_occurred(\n        self, name: str, arguments: dict[str, Any] | None = None\n    ) -> None: ...\n\n    def __on_event_occurred(self, params: dict, ctx: Any) -> None:\n        _LOGGER.debug('event occurred, %s',  params)\n        trans_arg = {}\n        for item in params['arguments']:\n            try:\n                if 'value' not in item:\n                    continue\n                if 'piid' in item:\n                    trans_arg[self._arguments_map[item['piid']]] = item[\n                        'value']\n                elif (\n                    isinstance(item['value'], list)\n                    and len(item['value']) == len(self.spec.argument)\n                ):\n                    # Dirty fix for cloud multi-arguments\n                    trans_arg = {\n                        prop.description_trans: item['value'][index]\n                        for index, prop in enumerate(self.spec.argument)\n                    }\n                    break\n            except KeyError as error:\n                _LOGGER.debug(\n                    'on event msg, invalid args, %s, %s, %s',\n                    self.entity_id, params, error)\n        self.on_event_occurred(\n            name=self.spec.description_trans, arguments=trans_arg)\n        self.async_write_ha_state()\n\n    def __on_device_state_changed(\n        self, key: str, state: MIoTDeviceState\n    ) -> None:\n        state_new = state == MIoTDeviceState.ONLINE\n        if state_new == self._attr_available:\n            return\n        self._attr_available = state_new\n        self.async_write_ha_state()\n\n\nclass MIoTActionEntity(Entity):\n    \"\"\"MIoT Action Entity.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    miot_device: MIoTDevice\n    spec: MIoTSpecAction\n    service: MIoTSpecService\n\n    _main_loop: asyncio.AbstractEventLoop\n    _in_map: dict[int, MIoTSpecProperty]\n    _out_map: dict[int, MIoTSpecProperty]\n    _state_sub_id: int\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:\n        if miot_device is None or spec is None or spec.service is None:\n            raise MIoTDeviceError('init error, invalid params')\n        self.miot_device = miot_device\n        self.spec = spec\n        self.service = spec.service\n        self._main_loop = miot_device.miot_client.main_loop\n        self._state_sub_id = 0\n        # Gen entity_id\n        self.entity_id = self.miot_device.gen_action_entity_id(\n            ha_domain=DOMAIN, spec_name=spec.name,\n            siid=spec.service.iid, aiid=spec.iid)\n        # Set entity attr\n        self._attr_unique_id = self.entity_id\n        self._attr_should_poll = False\n        self._attr_has_entity_name = True\n        self._attr_name = (\n            f'{\"* \"if self.spec.proprietary else \" \"}'\n            f'{self.service.description_trans} {spec.description_trans}')\n        self._attr_available = miot_device.online\n\n        _LOGGER.debug(\n            'new miot action entity, %s, %s, %s, %s, %s',\n            self.miot_device.name, self._attr_name, spec.platform,\n            spec.device_class, self.entity_id)\n\n    @property\n    def device_info(self) -> Optional[DeviceInfo]:\n        return self.miot_device.device_info\n\n    async def async_added_to_hass(self) -> None:\n        self._state_sub_id = self.miot_device.sub_device_state(\n            key=f'a.{ self.service.iid}.{self.spec.iid}',\n            handler=self.__on_device_state_changed)\n\n    async def async_will_remove_from_hass(self) -> None:\n        self.miot_device.unsub_device_state(\n            key=f'a.{ self.service.iid}.{self.spec.iid}',\n            sub_id=self._state_sub_id)\n\n    async def action_async(\n        self, in_list: Optional[list] = None\n    ) -> Optional[list]:\n        try:\n            return await self.miot_device.miot_client.action_async(\n                did=self.miot_device.did,\n                siid=self.service.iid,\n                aiid=self.spec.iid,\n                in_list=in_list or [])\n        except MIoTClientError as e:\n            raise RuntimeError(f'{e}, {self.entity_id}, {self.name}') from e\n\n    def __on_device_state_changed(\n        self, key: str, state: MIoTDeviceState\n    ) -> None:\n        state_new = state == MIoTDeviceState.ONLINE\n        if state_new == self._attr_available:\n            return\n        self._attr_available = state_new\n        self.async_write_ha_state()\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_error.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT error code and exception.\n\"\"\"\nfrom enum import Enum\nfrom typing import Any\n\n\nclass MIoTErrorCode(Enum):\n    \"\"\"MIoT error code.\"\"\"\n    # Base error code\n    CODE_UNKNOWN = -10000\n    CODE_UNAVAILABLE = -10001\n    CODE_INVALID_PARAMS = -10002\n    CODE_RESOURCE_ERROR = -10003\n    CODE_INTERNAL_ERROR = -10004\n    CODE_UNAUTHORIZED_ACCESS = -10005\n    CODE_TIMEOUT = -10006\n    # OAuth error code\n    CODE_OAUTH_UNAUTHORIZED = -10020\n    # Http error code\n    CODE_HTTP_INVALID_ACCESS_TOKEN = -10030\n    # MIoT mips error code\n    CODE_MIPS_INVALID_RESULT = -10040\n    # MIoT cert error code\n    CODE_CERT_INVALID_CERT = -10050\n    # MIoT spec error code, -10060\n    # MIoT storage error code, -10070\n    # MIoT ev error code, -10080\n    # Mips service error code, -10090\n    # Config flow error code, -10100\n    CODE_CONFIG_INVALID_INPUT = -10100\n    CODE_CONFIG_INVALID_STATE = -10101\n    # Options flow error code , -10110\n    # MIoT lan error code, -10120\n    CODE_LAN_UNAVAILABLE = -10120\n\n\nclass MIoTError(Exception):\n    \"\"\"MIoT error.\"\"\"\n    code: MIoTErrorCode\n    message: Any\n\n    def __init__(\n        self,  message: Any, code: MIoTErrorCode = MIoTErrorCode.CODE_UNKNOWN\n    ) -> None:\n        self.message = message\n        self.code = code\n        super().__init__(self.message)\n\n    def to_str(self) -> str:\n        return f'{{\"code\":{self.code.value},\"message\":\"{self.message}\"}}'\n\n    def to_dict(self) -> dict:\n        return {\"code\": self.code.value, \"message\": self.message}\n\n\nclass MIoTOauthError(MIoTError):\n    ...\n\n\nclass MIoTHttpError(MIoTError):\n    ...\n\n\nclass MIoTMipsError(MIoTError):\n    ...\n\n\nclass MIoTDeviceError(MIoTError):\n    ...\n\n\nclass MIoTSpecError(MIoTError):\n    ...\n\n\nclass MIoTStorageError(MIoTError):\n    ...\n\n\nclass MIoTCertError(MIoTError):\n    ...\n\n\nclass MIoTClientError(MIoTError):\n    ...\n\n\nclass MIoTEvError(MIoTError):\n    ...\n\n\nclass MipsServiceError(MIoTError):\n    ...\n\n\nclass MIoTConfigError(MIoTError):\n    ...\n\n\nclass MIoTOptionsError(MIoTError):\n    ...\n\n\nclass MIoTLanError(MIoTError):\n    ...\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_i18n.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT internationalization translation.\n\"\"\"\nimport asyncio\nimport logging\nimport os\nfrom typing import Optional, Union\n\n# pylint: disable=relative-beyond-top-level\nfrom .common import load_json_file\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass MIoTI18n:\n    \"\"\"MIoT Internationalization Translation.\n    Translate by Copilot, which does not guarantee the accuracy of the \n    translation. If there is a problem with the translation, please submit \n    the ISSUE feedback. After the review, we will modify it as soon as possible.\n    \"\"\"\n    _main_loop: asyncio.AbstractEventLoop\n    _lang: str\n    _data: dict\n\n    def __init__(\n        self, lang: str, loop: Optional[asyncio.AbstractEventLoop]\n    ) -> None:\n        self._main_loop = loop or asyncio.get_event_loop()\n        self._lang = lang\n        self._data = {}\n\n    async def init_async(self) -> None:\n        if self._data:\n            return\n        data = None\n        self._data = {}\n        try:\n            data = await self._main_loop.run_in_executor(\n                None, load_json_file,\n                os.path.join(\n                    os.path.dirname(os.path.abspath(__file__)),\n                    f'i18n/{self._lang}.json'))\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('load file error, %s', err)\n            return\n        # Check if the file is a valid JSON file\n        if not isinstance(data, dict):\n            _LOGGER.error('valid file, %s', data)\n            return\n        self._data = data\n\n    async def deinit_async(self) -> None:\n        self._data = {}\n\n    def translate(\n        self, key: str, replace: Optional[dict[str, str]] = None\n    ) -> Union[str, dict, None]:\n        result = self._data\n        for item in key.split('.'):\n            if item not in result:\n                return None\n            result = result[item]\n        if isinstance(result, str) and replace:\n            for k, v in replace.items():\n                result = result.replace('{'+k+'}', str(v))\n        return result or None\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_lan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT lan device control, only support MIoT SPEC-v2 WiFi devices.\n\"\"\"\n\n\nimport json\nimport time\nimport asyncio\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\nimport logging\nimport random\nimport secrets\nimport socket\nimport struct\nimport threading\nfrom typing import Any, Callable, Coroutine, Optional, final\nfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\nfrom cryptography.hazmat.primitives import padding\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\n\n# pylint: disable=relative-beyond-top-level\nfrom .miot_error import MIoTError, MIoTLanError, MIoTErrorCode\nfrom .miot_network import InterfaceStatus, MIoTNetwork, NetworkInfo\nfrom .miot_mdns import MipsService, MipsServiceState\nfrom .common import (\n    randomize_float, load_yaml_file, gen_absolute_path, MIoTMatcher)\n\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@dataclass\nclass _MIoTLanGetDevListData:\n    handler: Callable[[dict, Any], None]\n    handler_ctx: Any\n    timeout_ms: int\n\n\n@dataclass\nclass _MIoTLanUnregisterBroadcastData:\n    key: str\n\n\n@dataclass\nclass _MIoTLanRegisterBroadcastData:\n    key: str\n    handler: Callable[[dict, Any], None]\n    handler_ctx: Any\n\n\n@dataclass\nclass _MIoTLanUnsubDeviceData:\n    key: str\n\n\n@dataclass\nclass _MIoTLanSubDeviceData:\n    key: str\n    handler: Callable[[str, dict, Any], Coroutine]\n    handler_ctx: Any\n\n\n@dataclass\nclass _MIoTLanNetworkUpdateData:\n    status: InterfaceStatus\n    if_name: str\n\n\n@dataclass\nclass _MIoTLanRequestData:\n    msg_id: int\n    handler: Optional[Callable[[dict, Any], None]]\n    handler_ctx: Any\n    timeout: Optional[asyncio.TimerHandle]\n\n\nclass _MIoTLanDeviceState(Enum):\n    FRESH = 0\n    PING1 = auto()\n    PING2 = auto()\n    PING3 = auto()\n    DEAD = auto()\n\n\nclass _MIoTLanDevice:\n    \"\"\"MIoT lan device.\"\"\"\n    # pylint: disable=unused-argument\n    OT_HEADER: int = 0x2131\n    OT_HEADER_LEN: int = 32\n    NETWORK_UNSTABLE_CNT_TH: int = 10\n    NETWORK_UNSTABLE_TIME_TH: float = 120\n    NETWORK_UNSTABLE_RESUME_TH: float = 300\n    FAST_PING_INTERVAL: float = 5\n    CONSTRUCT_STATE_PENDING: float = 15\n    KA_INTERVAL_MIN: float = 10\n    KA_INTERVAL_MAX: float = 50\n\n    did: str\n    token: bytes\n    cipher: Cipher\n    ip: Optional[str]\n\n    offset: int\n    subscribed: bool\n    sub_ts: int\n    supported_wildcard_sub: bool\n\n    _manager: 'MIoTLan'\n    _if_name: Optional[str]\n    _sub_locked: bool\n    _state: _MIoTLanDeviceState\n    _online: bool\n    _online_offline_history: list[dict[str, Any]]\n    _online_offline_timer: Optional[asyncio.TimerHandle]\n\n    _ka_timer: Optional[asyncio.TimerHandle]\n    _ka_internal: float\n\n# All functions SHOULD be called from the internal loop\n\n    def __init__(\n        self,\n        manager: 'MIoTLan',\n        did: str,\n        token: str,\n        ip: Optional[str] = None\n    ) -> None:\n        self._manager: MIoTLan = manager\n        self.did = did\n        self.token = bytes.fromhex(token)\n        aes_key: bytes = self.__md5(self.token)\n        aex_iv: bytes = self.__md5(aes_key + self.token)\n        self.cipher = Cipher(\n            algorithms.AES128(aes_key), modes.CBC(aex_iv), default_backend())\n        self.ip = ip\n        self.offset = 0\n        self.subscribed = False\n        self.sub_ts = 0\n        self.supported_wildcard_sub = False\n        self._if_name = None\n        self._sub_locked = False\n        self._state = _MIoTLanDeviceState.DEAD\n        self._online = False\n        self._online_offline_history = []\n        self._online_offline_timer = None\n\n        def ka_init_handler() -> None:\n            self._ka_internal = self.KA_INTERVAL_MIN\n            self.__update_keep_alive(state=_MIoTLanDeviceState.DEAD)\n        self._ka_timer = self._manager.internal_loop.call_later(\n            randomize_float(self.CONSTRUCT_STATE_PENDING, 0.5),\n            ka_init_handler,)\n        _LOGGER.debug('miot lan device add, %s', self.did)\n\n    def keep_alive(self, ip: str, if_name: str) -> None:\n        self.ip = ip\n        if self._if_name != if_name:\n            self._if_name = if_name\n            _LOGGER.info(\n                'device if_name change, %s, %s', self._if_name, self.did)\n        self.__update_keep_alive(state=_MIoTLanDeviceState.FRESH)\n\n    @property\n    def online(self) -> bool:\n        return self._online\n\n    @online.setter\n    def online(self, online: bool) -> None:\n        if self._online == online:\n            return\n        self._online = online\n        self._manager.broadcast_device_state(\n            did=self.did, state={\n                'online': self._online, 'push_available': self.subscribed})\n\n    @property\n    def if_name(self) -> Optional[str]:\n        return self._if_name\n\n    def gen_packet(\n        self, out_buffer: bytearray, clear_data: dict, did: str, offset: int\n    ) -> int:\n        clear_bytes = json.dumps(clear_data, ensure_ascii=False).encode('utf-8')\n        padder = padding.PKCS7(algorithms.AES128.block_size).padder()\n        padded_data = padder.update(clear_bytes) + padder.finalize()\n        if len(padded_data) + self.OT_HEADER_LEN > len(out_buffer):\n            raise ValueError('rpc too long')\n        encryptor = self.cipher.encryptor()\n        encrypted_data = encryptor.update(padded_data) + encryptor.finalize()\n        data_len: int = len(encrypted_data)+self.OT_HEADER_LEN\n        out_buffer[:32] = struct.pack(\n            '>HHQI16s', self.OT_HEADER, data_len, int(did), offset,\n            self.token)\n        out_buffer[32:data_len] = encrypted_data\n        msg_md5: bytes = self.__md5(out_buffer[0:data_len])\n        out_buffer[16:32] = msg_md5\n        return data_len\n\n    def decrypt_packet(self, encrypted_data: bytearray) -> dict:\n        data_len: int = struct.unpack('>H', encrypted_data[2:4])[0]\n        md5_orig: bytes = encrypted_data[16:32]\n        encrypted_data[16:32] = self.token\n        md5_calc: bytes = self.__md5(encrypted_data[0:data_len])\n        if md5_orig != md5_calc:\n            raise ValueError(f'invalid md5, {md5_orig}, {md5_calc}')\n        decryptor = self.cipher.decryptor()\n        decrypted_padded_data = decryptor.update(\n            encrypted_data[32:data_len]) + decryptor.finalize()\n        unpadder = padding.PKCS7(algorithms.AES128.block_size).unpadder()\n        decrypted_data = unpadder.update(\n            decrypted_padded_data) + unpadder.finalize()\n        # Some device will add a redundant \\0 at the end of JSON string\n        decrypted_data = decrypted_data.rstrip(b'\\x00')\n        return json.loads(decrypted_data)\n\n    def subscribe(self) -> None:\n        if self._sub_locked:\n            return\n        self._sub_locked = True\n        try:\n            sub_ts: int = int(time.time())\n            self._manager.send2device(\n                did=self.did,\n                msg={\n                    'method': 'miIO.sub',\n                    'params': {\n                        'version': '2.0',\n                        'did': self._manager.virtual_did,\n                        'update_ts': sub_ts,\n                        'sub_method': '.'\n                    }\n                },\n                handler=self.__subscribe_handler,\n                handler_ctx=sub_ts,\n                timeout_ms=5000)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('subscribe device error, %s', err)\n\n        self._sub_locked = False\n\n    def unsubscribe(self) -> None:\n        if not self.subscribed:\n            return\n        self._manager.send2device(\n            did=self.did,\n            msg={\n                'method': 'miIO.unsub',\n                'params': {\n                    'version': '2.0',\n                    'did': self._manager.virtual_did,\n                    'update_ts': self.sub_ts or 0,\n                    'sub_method': '.'\n                }\n            },\n            handler=self.__unsubscribe_handler,\n            timeout_ms=5000)\n        self.subscribed = False\n        self._manager.broadcast_device_state(\n            did=self.did, state={\n                'online': self._online, 'push_available': self.subscribed})\n\n    def on_delete(self) -> None:\n        if self._ka_timer:\n            self._ka_timer.cancel()\n            self._ka_timer = None\n        if self._online_offline_timer:\n            self._online_offline_timer.cancel()\n            self._online_offline_timer = None\n        _LOGGER.debug('miot lan device delete, %s', self.did)\n\n    def update_info(self, info: dict) -> None:\n        if (\n            'token' in info\n            and len(info['token']) == 32\n            and info['token'].upper() != self.token.hex().upper()\n        ):\n            # Update token\n            self.token = bytes.fromhex(info['token'])\n            aes_key: bytes = self.__md5(self.token)\n            aex_iv: bytes = self.__md5(aes_key + self.token)\n            self.cipher = Cipher(\n                algorithms.AES128(aes_key),\n                modes.CBC(aex_iv), default_backend())\n            _LOGGER.debug('update token, %s', self.did)\n\n    def __subscribe_handler(self, msg: dict, sub_ts: int) -> None:\n        if (\n            'result' not in msg\n            or 'code' not in msg['result']\n            or msg['result']['code'] != 0\n        ):\n            _LOGGER.error('subscribe device error, %s, %s', self.did, msg)\n            return\n        self.subscribed = True\n        self.sub_ts = sub_ts\n        self._manager.broadcast_device_state(\n            did=self.did, state={\n                'online': self._online, 'push_available': self.subscribed})\n        _LOGGER.info('subscribe success, %s, %s', self._if_name, self.did)\n\n    def __unsubscribe_handler(self, msg: dict, ctx: Any) -> None:\n        if (\n            'result' not in msg\n            or 'code' not in msg['result']\n            or msg['result']['code'] != 0\n        ):\n            _LOGGER.error('unsubscribe device error, %s, %s', self.did, msg)\n            return\n        _LOGGER.info('unsubscribe success, %s, %s', self._if_name, self.did)\n\n    def __update_keep_alive(self, state: _MIoTLanDeviceState) -> None:\n        last_state: _MIoTLanDeviceState = self._state\n        self._state = state\n        if self._state != _MIoTLanDeviceState.FRESH:\n            _LOGGER.debug('device status, %s, %s', self.did, self._state)\n        if self._ka_timer:\n            self._ka_timer.cancel()\n            self._ka_timer = None\n        match state:\n            case _MIoTLanDeviceState.FRESH:\n                if last_state == _MIoTLanDeviceState.DEAD:\n                    self._ka_internal = self.KA_INTERVAL_MIN\n                    self.__change_online(True)\n                self._ka_timer = self._manager.internal_loop.call_later(\n                    self.__get_next_ka_timeout(), self.__update_keep_alive,\n                    _MIoTLanDeviceState.PING1)\n            case (\n                    _MIoTLanDeviceState.PING1\n                    | _MIoTLanDeviceState.PING2\n                    | _MIoTLanDeviceState.PING3\n            ):\n                # Set the timer first to avoid Any early returns\n                self._ka_timer = self._manager.internal_loop.call_later(\n                    self.FAST_PING_INTERVAL, self.__update_keep_alive,\n                    _MIoTLanDeviceState(state.value+1))\n                # Fast ping\n                if self._if_name is None:\n                    _LOGGER.error(\n                        'if_name is Not set for device, %s', self.did)\n                    return\n                if self.ip is None:\n                    _LOGGER.error('ip is Not set for device, %s', self.did)\n                    return\n                self._manager.ping(if_name=self._if_name, target_ip=self.ip)\n            case _MIoTLanDeviceState.DEAD:\n                if last_state == _MIoTLanDeviceState.PING3:\n                    self._ka_internal = self.KA_INTERVAL_MIN\n                    self.__change_online(False)\n            case _:\n                _LOGGER.error('invalid state, %s', state)\n\n    def __get_next_ka_timeout(self) -> float:\n        self._ka_internal = min(self._ka_internal*2, self.KA_INTERVAL_MAX)\n        return randomize_float(self._ka_internal, 0.1)\n\n    def __change_online(self, online: bool) -> None:\n        _LOGGER.info('change online, %s, %s', self.did, online)\n        ts_now: int = int(time.time())\n        self._online_offline_history.append({'ts': ts_now, 'online': online})\n        if len(self._online_offline_history) > self.NETWORK_UNSTABLE_CNT_TH:\n            self._online_offline_history.pop(0)\n        if self._online_offline_timer:\n            self._online_offline_timer.cancel()\n            self._online_offline_timer = None\n        if not online:\n            self.online = False\n        else:\n            if (\n                len(self._online_offline_history) < self.NETWORK_UNSTABLE_CNT_TH\n                or (\n                    ts_now - self._online_offline_history[0]['ts'] >\n                    self.NETWORK_UNSTABLE_TIME_TH)\n            ):\n                self.online = True\n            else:\n                _LOGGER.info('unstable device detected, %s', self.did)\n                self._online_offline_timer = (\n                    self._manager.internal_loop.call_later(\n                        self.NETWORK_UNSTABLE_RESUME_TH,\n                        self.__online_resume_handler))\n\n    def __online_resume_handler(self) -> None:\n        _LOGGER.info('unstable resume threshold past, %s', self.did)\n        self.online = True\n\n    def __md5(self, data: bytes) -> bytes:\n        hasher = hashes.Hash(hashes.MD5(), default_backend())\n        hasher.update(data)\n        return hasher.finalize()\n\n\nclass MIoTLan:\n    \"\"\"MIoT lan device control.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    OT_HEADER: bytes = b'\\x21\\x31'\n    OT_PORT: int = 54321\n    OT_PROBE_LEN: int = 32\n    OT_MSG_LEN: int = 1400\n    OT_SUPPORT_WILDCARD_SUB: int = 0xFE\n\n    OT_PROBE_INTERVAL_MIN: float = 5\n    OT_PROBE_INTERVAL_MAX: float = 45\n\n    PROFILE_MODELS_FILE: str = 'lan/profile_models.yaml'\n\n    _main_loop: asyncio.AbstractEventLoop\n    _net_ifs: set[str]\n    _network: MIoTNetwork\n    _mips_service: MipsService\n    _enable_subscribe: bool\n    _lan_devices: dict[str, _MIoTLanDevice]\n    _virtual_did: str\n    _probe_msg: bytes\n    _write_buffer: bytearray\n    _read_buffer: bytearray\n\n    _internal_loop: asyncio.AbstractEventLoop\n    _thread: threading.Thread\n\n    _available_net_ifs: set[str]\n    _broadcast_socks: dict[str, socket.socket]\n    _local_port: Optional[int]\n    _scan_timer: Optional[asyncio.TimerHandle]\n    _last_scan_interval: Optional[float]\n    _msg_id_counter: int\n    _pending_requests: dict[int, _MIoTLanRequestData]\n    _device_msg_matcher: MIoTMatcher\n    _device_state_sub_map: dict[str, _MIoTLanSubDeviceData]\n    _reply_msg_buffer: dict[str, asyncio.TimerHandle]\n\n    _lan_state_sub_map: dict[str, Callable[[bool], Coroutine]]\n    _lan_ctrl_vote_map: dict[str, bool]\n\n    _profile_models: dict[str, dict]\n\n    _init_lock: asyncio.Lock\n    _init_done: bool\n\n# The following should be called from the main loop\n\n    def __init__(\n        self,\n        net_ifs: list[str],\n        network: MIoTNetwork,\n        mips_service: MipsService,\n        enable_subscribe: bool = False,\n        virtual_did: Optional[int] = None,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        if not network:\n            raise ValueError('network is required')\n        if not mips_service:\n            raise ValueError('mips_service is required')\n        self._main_loop = loop or asyncio.get_event_loop()\n        self._net_ifs = set(net_ifs)\n        self._network = network\n        self._network.sub_network_info(\n            key='miot_lan',\n            handler=self.__on_network_info_change_external_async)\n        self._mips_service = mips_service\n        self._mips_service.sub_service_change(\n            key='miot_lan', group_id='*',\n            handler=self.__on_mips_service_change)\n        self._enable_subscribe = enable_subscribe\n        self._virtual_did = (\n            str(virtual_did) if (virtual_did is not None)\n            else str(secrets.randbits(64)))\n        # Init socket probe message\n        probe_bytes = bytearray(self.OT_PROBE_LEN)\n        probe_bytes[:20] = (\n            b'!1\\x00\\x20\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFF\\xFFMDID')\n        probe_bytes[20:28] = struct.pack('>Q', int(self._virtual_did))\n        probe_bytes[28:32] = b'\\x00\\x00\\x00\\x00'\n        self._probe_msg = bytes(probe_bytes)\n        self._read_buffer = bytearray(self.OT_MSG_LEN)\n        self._write_buffer = bytearray(self.OT_MSG_LEN)\n\n        self._lan_devices = {}\n        self._available_net_ifs = set()\n        self._broadcast_socks = {}\n        self._local_port = None\n        self._scan_timer = None\n        self._last_scan_interval = None\n        self._msg_id_counter = int(random.random()*0x7FFFFFFF)\n        self._pending_requests = {}\n        self._device_msg_matcher = MIoTMatcher()\n        self._device_state_sub_map = {}\n        self._reply_msg_buffer = {}\n\n        self._lan_state_sub_map = {}\n        self._lan_ctrl_vote_map = {}\n\n        self._init_lock = asyncio.Lock()\n        self._init_done = False\n\n        if (\n            len(self._mips_service.get_services()) == 0\n            and len(self._net_ifs) > 0\n        ):\n            _LOGGER.info('no central hub gateway service, init miot lan')\n            self._main_loop.call_later(\n                0, lambda: self._main_loop.create_task(\n                    self.init_async()))\n\n    def __assert_service_ready(self) -> None:\n        if not self._init_done:\n            raise MIoTLanError(\n                'MIoT lan is not ready',\n                MIoTErrorCode.CODE_LAN_UNAVAILABLE)\n\n    @property\n    def virtual_did(self) -> str:\n        return self._virtual_did\n\n    @property\n    def internal_loop(self) -> asyncio.AbstractEventLoop:\n        return self._internal_loop\n\n    @property\n    def init_done(self) -> bool:\n        return self._init_done\n\n    async def init_async(self) -> None:\n        # Avoid race condition\n        async with self._init_lock:\n            if self._init_done:\n                _LOGGER.info('miot lan already init')\n                return\n            if len(self._net_ifs) == 0:\n                _LOGGER.info('no net_ifs')\n                return\n            if not any(self._lan_ctrl_vote_map.values()):\n                _LOGGER.info('no vote for lan ctrl')\n                return\n            if len(self._mips_service.get_services()) > 0:\n                _LOGGER.info('central hub gateway service exist')\n                return\n            for if_name in list(self._network.network_info.keys()):\n                self._available_net_ifs.add(if_name)\n            if len(self._available_net_ifs) == 0:\n                _LOGGER.info('no available net_ifs')\n                return\n            if self._net_ifs.isdisjoint(self._available_net_ifs):\n                _LOGGER.info('no valid net_ifs')\n                return\n            try:\n                self._profile_models = await self._main_loop.run_in_executor(\n                    None, load_yaml_file,\n                    gen_absolute_path(self.PROFILE_MODELS_FILE))\n            except Exception as err:  # pylint: disable=broad-exception-caught\n                _LOGGER.error('load profile models error, %s', err)\n                self._profile_models = {}\n            self._internal_loop = asyncio.new_event_loop()\n            # All tasks meant for the internal loop should happen in this thread\n            self._thread = threading.Thread(target=self.__internal_loop_thread)\n            self._thread.name = 'miot_lan'\n            self._thread.daemon = True\n            self._thread.start()\n            self._init_done = True\n            for handler in list(self._lan_state_sub_map.values()):\n                self._main_loop.create_task(handler(True))\n            _LOGGER.info(\n                'miot lan init, %s ,%s', self._net_ifs, self._available_net_ifs)\n\n    def __internal_loop_thread(self) -> None:\n        _LOGGER.info('miot lan thread start')\n        self.__init_socket()\n        self._scan_timer = self._internal_loop.call_later(\n            int(3*random.random()), self.__scan_devices)\n        self._internal_loop.run_forever()\n        _LOGGER.info('miot lan thread exit')\n\n    async def deinit_async(self) -> None:\n        if not self._init_done:\n            _LOGGER.info('miot lan not init')\n            return\n        self._init_done = False\n        self._internal_loop.call_soon_threadsafe(self.__deinit)\n        self._thread.join()\n        self._internal_loop.close()\n\n        self._profile_models = {}\n        self._lan_devices = {}\n        self._broadcast_socks = {}\n        self._local_port = None\n        self._scan_timer = None\n        self._last_scan_interval = None\n        self._msg_id_counter = int(random.random()*0x7FFFFFFF)\n        self._pending_requests = {}\n        self._device_msg_matcher = MIoTMatcher()\n        self._device_state_sub_map = {}\n        self._reply_msg_buffer = {}\n        for handler in list(self._lan_state_sub_map.values()):\n            self._main_loop.create_task(handler(False))\n        _LOGGER.info('miot lan deinit')\n\n    async def update_net_ifs_async(self, net_ifs: list[str]) -> None:\n        _LOGGER.info('update net_ifs, %s', net_ifs)\n        if not isinstance(net_ifs, list):\n            _LOGGER.error('invalid net_ifs, %s', net_ifs)\n            return\n        if len(net_ifs) == 0:\n            # Deinit lan\n            await self.deinit_async()\n            self._net_ifs = set(net_ifs)\n            return\n        available_net_ifs = set()\n        for if_name in list(self._network.network_info.keys()):\n            available_net_ifs.add(if_name)\n        if set(net_ifs).isdisjoint(available_net_ifs):\n            _LOGGER.error('no valid net_ifs, %s', net_ifs)\n            await self.deinit_async()\n            self._net_ifs = set(net_ifs)\n            self._available_net_ifs = available_net_ifs\n            return\n        if not self._init_done:\n            self._net_ifs = set(net_ifs)\n            await self.init_async()\n            return\n        self._internal_loop.call_soon_threadsafe(\n            self.__update_net_ifs,\n            net_ifs)\n\n    async def vote_for_lan_ctrl_async(self, key: str, vote: bool) -> None:\n        _LOGGER.info('vote for lan ctrl, %s, %s', key, vote)\n        self._lan_ctrl_vote_map[key] = vote\n        if not any(self._lan_ctrl_vote_map.values()):\n            await self.deinit_async()\n            return\n        await self.init_async()\n\n    async def update_subscribe_option(self, enable_subscribe: bool) -> None:\n        _LOGGER.info('update subscribe option, %s', enable_subscribe)\n        if not self._init_done:\n            self._enable_subscribe = enable_subscribe\n            return\n        self._internal_loop.call_soon_threadsafe(\n            self.__update_subscribe_option,\n            {'enable_subscribe': enable_subscribe})\n\n    def update_devices(self, devices: dict[str, dict]) -> bool:\n        _LOGGER.info('update devices, %s', devices)\n        if not self._init_done:\n            return False\n        self._internal_loop.call_soon_threadsafe(\n            self.__update_devices, devices)\n        return True\n\n    def delete_devices(self, devices: list[str]) -> bool:\n        _LOGGER.info('delete devices, %s', devices)\n        if not self._init_done:\n            return False\n        self._internal_loop.call_soon_threadsafe(\n            self.__delete_devices, devices)\n        return True\n\n    def sub_lan_state(\n        self, key: str, handler: Callable[[bool], Coroutine]\n    ) -> None:\n        self._lan_state_sub_map[key] = handler\n\n    def unsub_lan_state(self, key: str) -> None:\n        self._lan_state_sub_map.pop(key, None)\n\n    @final\n    def sub_device_state(\n        self, key: str, handler: Callable[[str, dict, Any], Coroutine],\n        handler_ctx: Any = None\n    ) -> bool:\n        if not self._init_done:\n            return False\n        self._internal_loop.call_soon_threadsafe(\n            self.__sub_device_state,\n            _MIoTLanSubDeviceData(\n                key=key, handler=handler, handler_ctx=handler_ctx))\n        return True\n\n    @final\n    def unsub_device_state(self, key: str) -> bool:\n        if not self._init_done:\n            return False\n        self._internal_loop.call_soon_threadsafe(\n            self.__unsub_device_state, _MIoTLanUnsubDeviceData(key=key))\n        return True\n\n    @final\n    def sub_prop(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        piid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        if not self._init_done:\n            return False\n        if not self._enable_subscribe:\n            return False\n        key = (\n            f'{did}/p/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}/{piid}\"}')\n        self._internal_loop.call_soon_threadsafe(\n            self.__sub_broadcast,\n            _MIoTLanRegisterBroadcastData(\n                key=key, handler=handler, handler_ctx=handler_ctx))\n        return True\n\n    @final\n    def unsub_prop(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        piid: Optional[int] = None\n    ) -> bool:\n        if not self._init_done:\n            return False\n        if not self._enable_subscribe:\n            return False\n        key = (\n            f'{did}/p/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}/{piid}\"}')\n        self._internal_loop.call_soon_threadsafe(\n            self.__unsub_broadcast,\n            _MIoTLanUnregisterBroadcastData(key=key))\n        return True\n\n    @final\n    def sub_event(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        if not self._init_done:\n            return False\n        if not self._enable_subscribe:\n            return False\n        key = (\n            f'{did}/e/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}/{eiid}\"}')\n        self._internal_loop.call_soon_threadsafe(\n            self.__sub_broadcast,\n            _MIoTLanRegisterBroadcastData(\n                key=key, handler=handler, handler_ctx=handler_ctx))\n        return True\n\n    @final\n    def unsub_event(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None\n    ) -> bool:\n        if not self._init_done:\n            return False\n        if not self._enable_subscribe:\n            return False\n        key = (\n            f'{did}/e/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}/{eiid}\"}')\n        self._internal_loop.call_soon_threadsafe(\n            self.__unsub_broadcast,\n            _MIoTLanUnregisterBroadcastData(key=key))\n        return True\n\n    @final\n    async def get_prop_async(\n        self, did: str, siid: int, piid: int, timeout_ms: int = 10000\n    ) -> Any:\n        self.__assert_service_ready()\n        result_obj = await self.__call_api_async(\n            did=did, msg={\n                'method': 'get_properties',\n                'params': [{'did': did, 'siid': siid, 'piid': piid}]\n            }, timeout_ms=timeout_ms)\n\n        if (\n            result_obj and 'result' in result_obj\n            and len(result_obj['result']) == 1\n            and 'did' in result_obj['result'][0]\n            and result_obj['result'][0]['did'] == did\n        ):\n            return result_obj['result'][0].get('value', None)\n        return None\n\n    @final\n    async def set_prop_async(\n        self, did: str, siid: int, piid: int, value: Any,\n        timeout_ms: int = 10000\n    ) -> dict:\n        self.__assert_service_ready()\n        result_obj = await self.__call_api_async(\n            did=did, msg={\n                'method': 'set_properties',\n                'params': [{\n                    'did': did, 'siid': siid, 'piid': piid, 'value': value}]\n            }, timeout_ms=timeout_ms)\n        if result_obj:\n            if (\n                'result' in result_obj\n                and len(result_obj['result']) == 1\n                and 'did' in result_obj['result'][0]\n                and result_obj['result'][0]['did'] == did\n                and 'code' in result_obj['result'][0]\n            ):\n                return result_obj['result'][0]\n            if 'code' in result_obj:\n                return result_obj\n        raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR)\n\n    @final\n    async def action_async(\n        self, did: str, siid: int, aiid: int, in_list: list,\n        timeout_ms: int = 10000\n    ) -> dict:\n        self.__assert_service_ready()\n        result_obj = await self.__call_api_async(\n            did=did, msg={\n                'method': 'action',\n                'params': {\n                    'did': did, 'siid': siid, 'aiid': aiid, 'in': in_list}\n            }, timeout_ms=timeout_ms)\n        if result_obj:\n            if 'result' in result_obj and 'code' in result_obj['result']:\n                return result_obj['result']\n            if 'code' in result_obj:\n                return result_obj\n        raise MIoTError('Invalid result', MIoTErrorCode.CODE_INTERNAL_ERROR)\n\n    @final\n    async def get_dev_list_async(\n        self, timeout_ms: int = 10000\n    ) -> dict[str, dict]:\n        if not self._init_done:\n            return {}\n\n        def get_device_list_handler(msg: dict, fut: asyncio.Future):\n            self._main_loop.call_soon_threadsafe(\n                fut.set_result, msg)\n\n        fut: asyncio.Future = self._main_loop.create_future()\n        self._internal_loop.call_soon_threadsafe(\n            self.__get_dev_list,\n            _MIoTLanGetDevListData(\n                handler=get_device_list_handler,\n                handler_ctx=fut,\n                timeout_ms=timeout_ms))\n        return await fut\n\n    async def __call_api_async(\n        self, did: str, msg: dict, timeout_ms: int = 10000\n    ) -> dict:\n        def call_api_handler(msg: dict, fut: asyncio.Future):\n            self._main_loop.call_soon_threadsafe(\n                fut.set_result, msg)\n\n        fut: asyncio.Future = self._main_loop.create_future()\n        self._internal_loop.call_soon_threadsafe(\n            self.__call_api, did, msg, call_api_handler, fut, timeout_ms)\n        return await fut\n\n    async def __on_network_info_change_external_async(\n        self,\n        status: InterfaceStatus,\n        info: NetworkInfo\n    ) -> None:\n        _LOGGER.info(\n            'on network info change, status: %s, info: %s', status, info)\n        available_net_ifs = set()\n        for if_name in list(self._network.network_info.keys()):\n            available_net_ifs.add(if_name)\n        if len(available_net_ifs) == 0:\n            await self.deinit_async()\n            self._available_net_ifs = available_net_ifs\n            return\n        if self._net_ifs.isdisjoint(available_net_ifs):\n            _LOGGER.info('no valid net_ifs')\n            await self.deinit_async()\n            self._available_net_ifs = available_net_ifs\n            return\n        if not self._init_done:\n            self._available_net_ifs = available_net_ifs\n            await self.init_async()\n            return\n        self._internal_loop.call_soon_threadsafe(\n            self.__on_network_info_change,\n            _MIoTLanNetworkUpdateData(status=status, if_name=info.name))\n\n    async def __on_mips_service_change(\n        self, group_id: str,  state: MipsServiceState, data: dict\n    ) -> None:\n        _LOGGER.info(\n            'on mips service change, %s, %s, %s',  group_id, state, data)\n        if len(self._mips_service.get_services()) > 0:\n            _LOGGER.info('find central service, deinit miot lan')\n            await self.deinit_async()\n        else:\n            _LOGGER.info('no central service, init miot lan')\n            await self.init_async()\n\n# The following methods SHOULD ONLY be called in the internal loop\n\n    def ping(self, if_name: Optional[str], target_ip: str) -> None:\n        if not target_ip:\n            return\n        self.__sendto(\n            if_name=if_name, data=self._probe_msg, address=target_ip,\n            port=self.OT_PORT)\n\n    def send2device(\n        self, did: str,\n        msg: dict,\n        handler: Optional[Callable[[dict, Any], None]] = None,\n        handler_ctx: Any = None,\n        timeout_ms: Optional[int] = None\n    ) -> None:\n        if timeout_ms and not handler:\n            raise ValueError('handler is required when timeout_ms is set')\n        device: Optional[_MIoTLanDevice] = self._lan_devices.get(did)\n        if not device:\n            raise ValueError('invalid device')\n        if not device.cipher:\n            raise ValueError('invalid device cipher')\n        if not device.if_name:\n            raise ValueError('invalid device if_name')\n        if not device.ip:\n            raise ValueError('invalid device ip')\n        in_msg = {'id': self.__gen_msg_id(), **msg}\n        msg_len = device.gen_packet(\n            out_buffer=self._write_buffer,\n            clear_data=in_msg,\n            did=did,\n            offset=int(time.time())-device.offset)\n\n        return self.__make_request(\n            msg_id=in_msg['id'],\n            msg=self._write_buffer[0: msg_len],\n            if_name=device.if_name,\n            ip=device.ip,\n            handler=handler,\n            handler_ctx=handler_ctx,\n            timeout_ms=timeout_ms)\n\n    def __make_request(\n        self,\n        msg_id: int,\n        msg: bytearray,\n        if_name: str,\n        ip: str,\n        handler: Optional[Callable[[dict, Any], None]],\n        handler_ctx: Any = None,\n        timeout_ms: Optional[int] = None\n    ) -> None:\n        def request_timeout_handler(req_data: _MIoTLanRequestData):\n            self._pending_requests.pop(req_data.msg_id, None)\n            if req_data and req_data.handler:\n                req_data.handler({\n                    'code': MIoTErrorCode.CODE_TIMEOUT.value,\n                    'error': 'timeout'},\n                    req_data.handler_ctx)\n\n        timer: Optional[asyncio.TimerHandle] = None\n        request_data = _MIoTLanRequestData(\n            msg_id=msg_id,\n            handler=handler,\n            handler_ctx=handler_ctx,\n            timeout=timer)\n        if timeout_ms:\n            timer = self._internal_loop.call_later(\n                timeout_ms/1000, request_timeout_handler, request_data)\n            request_data.timeout = timer\n        self._pending_requests[msg_id] = request_data\n        self.__sendto(if_name=if_name, data=msg, address=ip, port=self.OT_PORT)\n\n    def broadcast_device_state(self, did: str, state: dict) -> None:\n        for handler in self._device_state_sub_map.values():\n            self._main_loop.call_soon_threadsafe(\n                self._main_loop.create_task,\n                handler.handler(did, state, handler.handler_ctx))\n\n    def __gen_msg_id(self) -> int:\n        if not self._msg_id_counter:\n            self._msg_id_counter = int(random.random()*0x7FFFFFFF)\n        self._msg_id_counter += 1\n        if self._msg_id_counter > 0x80000000:\n            self._msg_id_counter = 1\n        return self._msg_id_counter\n\n    def __call_api(\n        self,\n        did: str,\n        msg: dict,\n        handler: Callable,\n        handler_ctx: Any,\n        timeout_ms: int = 10000\n    ) -> None:\n        try:\n            self.send2device(\n                did=did,\n                msg={'from': 'ha.xiaomi_home', **msg},\n                handler=handler,\n                handler_ctx=handler_ctx,\n                timeout_ms=timeout_ms)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('send2device error, %s', err)\n            handler({\n                'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,\n                'error': str(err)},\n                handler_ctx)\n\n    def __sub_device_state(self, data: _MIoTLanSubDeviceData) -> None:\n        self._device_state_sub_map[data.key] = data\n\n    def __unsub_device_state(self, data: _MIoTLanUnsubDeviceData) -> None:\n        self._device_state_sub_map.pop(data.key, None)\n\n    def __sub_broadcast(self, data: _MIoTLanRegisterBroadcastData) -> None:\n        self._device_msg_matcher[data.key] = data\n        _LOGGER.debug('lan register broadcast, %s', data.key)\n\n    def __unsub_broadcast(self, data: _MIoTLanUnregisterBroadcastData) -> None:\n        if self._device_msg_matcher.get(topic=data.key):\n            del self._device_msg_matcher[data.key]\n        _LOGGER.debug('lan unregister broadcast, %s', data.key)\n\n    def __get_dev_list(self, data: _MIoTLanGetDevListData) -> None:\n        dev_list = {\n            device.did: {\n                'online': device.online,\n                'push_available': device.subscribed\n            }\n            for device in self._lan_devices.values()\n            if device.online}\n        data.handler(\n            dev_list, data.handler_ctx)\n\n    def __update_devices(self, devices: dict[str, dict]) -> None:\n        for did, info in devices.items():\n            # did MUST be digit(UINT64)\n            if not did.isdigit():\n                _LOGGER.info('invalid did, %s', did)\n                continue\n            if (\n                    'model' not in info\n                    or info['model'] in self._profile_models):\n                # Do not support the local control of\n                # Profile device for the time being\n                _LOGGER.info(\n                    'model not support local ctrl, %s, %s',\n                    did, info.get('model'))\n                continue\n            if did not in self._lan_devices:\n                if 'token' not in info:\n                    _LOGGER.error(\n                        'token not found, %s, %s', did, info)\n                    continue\n                if len(info['token']) != 32:\n                    _LOGGER.error(\n                        'invalid device token, %s, %s', did, info)\n                    continue\n                self._lan_devices[did] = _MIoTLanDevice(\n                    manager=self, did=did, token=info['token'],\n                    ip=info.get('ip', None))\n            else:\n                self._lan_devices[did].update_info(info)\n\n    def __delete_devices(self, devices: list[str]) -> None:\n        for did in devices:\n            lan_device = self._lan_devices.pop(did, None)\n            if not lan_device:\n                continue\n            lan_device.on_delete()\n\n    def __on_network_info_change(self, data: _MIoTLanNetworkUpdateData) -> None:\n        if data.status == InterfaceStatus.ADD:\n            self._available_net_ifs.add(data.if_name)\n            if data.if_name in self._net_ifs:\n                self.__create_socket(if_name=data.if_name)\n        elif data.status == InterfaceStatus.REMOVE:\n            self._available_net_ifs.remove(data.if_name)\n            self.__destroy_socket(if_name=data.if_name)\n\n    def __update_net_ifs(self, net_ifs: list[str]) -> None:\n        if self._net_ifs != set(net_ifs):\n            self._net_ifs = set(net_ifs)\n            for if_name in self._net_ifs:\n                self.__create_socket(if_name=if_name)\n            for if_name in list(self._broadcast_socks.keys()):\n                if if_name not in self._net_ifs:\n                    self.__destroy_socket(if_name=if_name)\n\n    def __update_subscribe_option(self, options: dict) -> None:\n        if 'enable_subscribe' in options:\n            if options['enable_subscribe'] != self._enable_subscribe:\n                self._enable_subscribe = options['enable_subscribe']\n                if not self._enable_subscribe:\n                    # Unsubscribe all\n                    for device in self._lan_devices.values():\n                        device.unsubscribe()\n\n    def __deinit(self) -> None:\n        # Release all resources\n        if self._scan_timer:\n            self._scan_timer.cancel()\n            self._scan_timer = None\n        for device in self._lan_devices.values():\n            device.on_delete()\n        self._lan_devices.clear()\n        for req_data in self._pending_requests.values():\n            if req_data.timeout:\n                req_data.timeout.cancel()\n                req_data.timeout = None\n        self._pending_requests.clear()\n        for timer in self._reply_msg_buffer.values():\n            timer.cancel()\n        self._reply_msg_buffer.clear()\n        self._device_msg_matcher = MIoTMatcher()\n        self.__deinit_socket()\n        self._internal_loop.stop()\n\n    def __init_socket(self) -> None:\n        self.__deinit_socket()\n        for if_name in self._net_ifs:\n            if if_name not in self._available_net_ifs:\n                return\n            self.__create_socket(if_name=if_name)\n\n    def __create_socket(self, if_name: str) -> None:\n        if if_name in self._broadcast_socks:\n            _LOGGER.info('socket already created, %s', if_name)\n            return\n        # Create socket\n        try:\n            sock = socket.socket(\n                socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)\n            sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)\n            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n            # Set SO_BINDTODEVICE\n            sock.setsockopt(\n                socket.SOL_SOCKET, socket.SO_BINDTODEVICE, if_name.encode())\n            sock.bind(('', self._local_port or 0))\n            self._internal_loop.add_reader(\n                sock.fileno(), self.__socket_read_handler, (if_name, sock))\n            self._broadcast_socks[if_name] = sock\n            self._local_port = self._local_port or sock.getsockname()[1]\n            _LOGGER.info(\n                'created socket, %s, %s', if_name, self._local_port)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('create socket error, %s, %s', if_name, err)\n\n    def __deinit_socket(self) -> None:\n        for if_name in list(self._broadcast_socks.keys()):\n            self.__destroy_socket(if_name)\n        self._broadcast_socks.clear()\n\n    def __destroy_socket(self, if_name: str) -> None:\n        sock = self._broadcast_socks.pop(if_name, None)\n        if not sock:\n            return\n        self._internal_loop.remove_reader(sock.fileno())\n        sock.close()\n        _LOGGER.info('destroyed socket, %s', if_name)\n\n    def __socket_read_handler(self, ctx: tuple[str, socket.socket]) -> None:\n        try:\n            data_len, addr = ctx[1].recvfrom_into(\n                self._read_buffer, self.OT_MSG_LEN, socket.MSG_DONTWAIT)\n            if data_len < 0:\n                # Socket error\n                _LOGGER.error('socket read error, %s, %s', ctx[0], data_len)\n                return\n            if addr[1] != self.OT_PORT:\n                # Not ot msg\n                return\n            self.__raw_message_handler(\n                self._read_buffer[:data_len], data_len, addr[0], ctx[0])\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('socket read handler error, %s', err)\n\n    def __raw_message_handler(\n        self, data: bytearray, data_len: int, ip: str, if_name: str\n    ) -> None:\n        if data[:2] != self.OT_HEADER:\n            return\n        # Keep alive message\n        did: str = str(struct.unpack('>Q', data[4:12])[0])\n        device: Optional[_MIoTLanDevice] = self._lan_devices.get(did)\n        if not device:\n            return\n        timestamp: int = struct.unpack('>I', data[12:16])[0]\n        device.offset = int(time.time()) - timestamp\n        # Keep alive if this is a probe\n        if data_len == self.OT_PROBE_LEN or device.subscribed:\n            device.keep_alive(ip=ip, if_name=if_name)\n        # Manage device subscribe status\n        if (\n            self._enable_subscribe\n            and data_len == self.OT_PROBE_LEN\n            and data[16:20] == b'MSUB'\n            and data[24:27] == b'PUB'\n        ):\n            device.supported_wildcard_sub = (\n                int(data[28]) == self.OT_SUPPORT_WILDCARD_SUB)\n            sub_ts = struct.unpack('>I', data[20:24])[0]\n            sub_type = int(data[27])\n            if (\n                device.supported_wildcard_sub\n                and sub_type in [0, 1, 4]\n                and sub_ts != device.sub_ts\n            ):\n                device.subscribed = False\n                device.subscribe()\n        if data_len > self.OT_PROBE_LEN:\n            # handle device message\n            try:\n                decrypted_data = device.decrypt_packet(data)\n                self.__message_handler(did, decrypted_data)\n            except Exception as err:   # pylint: disable=broad-exception-caught\n                _LOGGER.error('decrypt packet error, %s, %s', did, err)\n                return\n\n    def __message_handler(self, did: str, msg: dict) -> None:\n        if 'id' not in msg:\n            _LOGGER.warning('invalid message, no id, %s, %s', did, msg)\n            return\n        # Reply\n        req: Optional[_MIoTLanRequestData] = (\n            self._pending_requests.pop(msg['id'], None))\n        if req:\n            if req.timeout:\n                req.timeout.cancel()\n                req.timeout = None\n            if req.handler is not None:\n                self._main_loop.call_soon_threadsafe(\n                    req.handler, msg, req.handler_ctx)\n            return\n        # Handle up link message\n        if 'method' not in msg or 'params' not in msg:\n            _LOGGER.debug(\n                'invalid message, no method or params, %s, %s', did, msg)\n            return\n        # Filter dup message\n        if self.__filter_dup_message(did, msg['id']):\n            self.send2device(\n                did=did, msg={'id': msg['id'], 'result': {'code': 0}})\n            return\n        _LOGGER.debug('lan message, %s, %s', did, msg)\n        if msg['method'] == 'properties_changed':\n            for param in msg['params']:\n                if 'siid' not in param and 'piid' not in param:\n                    _LOGGER.debug(\n                        'invalid message, no siid or piid, %s, %s', did, msg)\n                    continue\n                key = f'{did}/p/{param[\"siid\"]}/{param[\"piid\"]}'\n                subs: list[_MIoTLanRegisterBroadcastData] = list(\n                    self._device_msg_matcher.iter_match(key))\n                for sub in subs:\n                    self._main_loop.call_soon_threadsafe(\n                        sub.handler, param, sub.handler_ctx)\n        elif (\n                msg['method'] == 'event_occured'\n                and 'siid' in msg['params']\n                and 'eiid' in msg['params']\n        ):\n            key = f'{did}/e/{msg[\"params\"][\"siid\"]}/{msg[\"params\"][\"eiid\"]}'\n            subs: list[_MIoTLanRegisterBroadcastData] = list(\n                self._device_msg_matcher.iter_match(key))\n            for sub in subs:\n                self._main_loop.call_soon_threadsafe(\n                    sub.handler, msg['params'], sub.handler_ctx)\n        else:\n            _LOGGER.debug(\n                'invalid message, unknown method, %s, %s', did, msg)\n        # Reply\n        self.send2device(\n            did=did, msg={'id': msg['id'], 'result': {'code': 0}})\n\n    def __filter_dup_message(self, did: str, msg_id: int) -> bool:\n        filter_id = f'{did}.{msg_id}'\n        if filter_id in self._reply_msg_buffer:\n            return True\n        self._reply_msg_buffer[filter_id] = self._internal_loop.call_later(\n            5,\n            lambda filter_id: self._reply_msg_buffer.pop(filter_id, None),\n            filter_id)\n        return False\n\n    def __sendto(\n        self, if_name: Optional[str], data: bytes, address: str, port: int\n    ) -> None:\n        if if_name is None:\n            # Broadcast\n            for if_n, sock in self._broadcast_socks.items():\n                _LOGGER.debug('send broadcast, %s', if_n)\n                sock.sendto(data, socket.MSG_DONTWAIT, (address, port))\n        else:\n            # Unicast\n            sock = self._broadcast_socks.get(if_name, None)\n            if not sock:\n                _LOGGER.error('invalid socket, %s', if_name)\n                return\n            sock.sendto(data, socket.MSG_DONTWAIT, (address, port))\n\n    def __scan_devices(self) -> None:\n        if self._scan_timer:\n            self._scan_timer.cancel()\n            self._scan_timer = None\n        try:\n            # Scan devices\n            self.ping(if_name=None, target_ip='255.255.255.255')\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            # Ignore any exceptions to avoid blocking the loop\n            _LOGGER.error('ping device error, %s', err)\n            pass\n        scan_time = self.__get_next_scan_time()\n        self._scan_timer = self._internal_loop.call_later(\n            scan_time, self.__scan_devices)\n        _LOGGER.debug('next scan time: %ss', scan_time)\n\n    def __get_next_scan_time(self) -> float:\n        if not self._last_scan_interval:\n            self._last_scan_interval = self.OT_PROBE_INTERVAL_MIN\n        self._last_scan_interval = min(\n            self._last_scan_interval*2, self.OT_PROBE_INTERVAL_MAX)\n        return self._last_scan_interval\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_mdns.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT central hub gateway service discovery.\n\"\"\"\nimport asyncio\nimport base64\nimport binascii\nimport copy\nfrom enum import Enum\nfrom typing import Callable, Coroutine, Optional\nimport logging\n\nfrom zeroconf import (\n    DNSQuestionType,\n    IPVersion,\n    ServiceStateChange,\n    Zeroconf)\nfrom zeroconf.asyncio import (\n    AsyncServiceInfo,\n    AsyncZeroconf,\n    AsyncServiceBrowser)\n\n# pylint: disable=relative-beyond-top-level\nfrom .miot_error import MipsServiceError\n\n_LOGGER = logging.getLogger(__name__)\n\nMIPS_MDNS_TYPE = '_miot-central._tcp.local.'\nMIPS_MDNS_REQUEST_TIMEOUT_MS = 5000\nMIPS_MDNS_UPDATE_INTERVAL_S = 600\n\n\nclass MipsServiceState(Enum):\n    ADDED = 1\n    REMOVED = 2\n    UPDATED = 3\n\n\nclass MipsServiceData:\n    \"\"\"Mips service data.\"\"\"\n    profile: str\n    profile_bin: bytes\n\n    name: str\n    addresses: list[str]\n    port: int\n    type: str\n    server: str\n\n    did: str\n    group_id: str\n    role: int\n    suite_mqtt: bool\n\n    def __init__(self, service_info: AsyncServiceInfo) -> None:\n        if service_info is None:\n            raise MipsServiceError('invalid params')\n        properties: dict = service_info.decoded_properties\n        if not properties:\n            raise MipsServiceError('invalid service properties')\n        self.profile = properties.get('profile', None)\n        if self.profile is None:\n            raise MipsServiceError('invalid service profile')\n        self.profile_bin = base64.b64decode(self.profile)\n        self.name = service_info.name\n        self.addresses = service_info.parsed_addresses(\n            version=IPVersion.V4Only)\n        if not self.addresses:\n            raise MipsServiceError('invalid addresses')\n        if not service_info.port:\n            raise MipsServiceError('invalid port')\n        self.port = service_info.port\n        self.type = service_info.type\n        self.server = service_info.server or ''\n        # Parse profile\n        self.did = str(int.from_bytes(self.profile_bin[1:9], byteorder='big'))\n        self.group_id = binascii.hexlify(\n            self.profile_bin[9:17][::-1]).decode('utf-8')\n        self.role = int(self.profile_bin[20] >> 4)\n        self.suite_mqtt = ((self.profile_bin[22] >> 1) & 0x01) == 0x01\n\n    def valid_service(self) -> bool:\n        if self.role != 1:\n            return False\n        return self.suite_mqtt\n\n    def to_dict(self) -> dict:\n        return {\n            'name': self.name,\n            'addresses': self.addresses,\n            'port': self.port,\n            'type': self.type,\n            'server': self.server,\n            'did': self.did,\n            'group_id': self.group_id,\n            'role': self.role,\n            'suite_mqtt': self.suite_mqtt\n        }\n\n    def __str__(self) -> str:\n        return str(self.to_dict())\n\n\nclass MipsService:\n    \"\"\"MIPS service discovery.\"\"\"\n    _aiozc: AsyncZeroconf\n    _main_loop: asyncio.AbstractEventLoop\n    _aio_browser: AsyncServiceBrowser\n    _services: dict[str, dict]\n    # key = (key, group_id)\n    _sub_list: dict[tuple[str, str], Callable[[\n        str, MipsServiceState, dict], Coroutine]]\n\n    def __init__(\n        self, aiozc: AsyncZeroconf,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._aiozc = aiozc\n        self._main_loop = loop or asyncio.get_running_loop()\n\n        self._services = {}\n        self._sub_list = {}\n\n    async def init_async(self) -> None:\n        await self._aiozc.zeroconf.async_wait_for_start()\n\n        self._aio_browser = AsyncServiceBrowser(\n            zeroconf=self._aiozc.zeroconf,\n            type_=MIPS_MDNS_TYPE,\n            handlers=[self.__on_service_state_change],\n            question_type=DNSQuestionType.QM)\n\n    async def deinit_async(self) -> None:\n        await self._aio_browser.async_cancel()\n        self._services = {}\n        self._sub_list = {}\n\n    def get_services(self, group_id: Optional[str] = None) -> dict[str, dict]:\n        \"\"\"get mips services.\n\n        Args:\n            group_id (str, optional): _description_. Defaults to None.\n\n        Returns: {\n            [group_id:str]: {\n                \"name\": str,\n                \"addresses\": list[str],\n                \"port\": number,\n                \"type\": str,\n                \"server\": str,\n                \"version\": int,\n                \"did\": str,\n                \"group_id\": str,\n                \"role\": int,\n                \"suite_mqtt\": bool\n            }\n        }\n        \"\"\"\n        if group_id:\n            if group_id not in self._services:\n                return {}\n            return {group_id: copy.deepcopy(self._services[group_id])}\n        return copy.deepcopy(self._services)\n\n    def sub_service_change(\n            self, key: str, group_id: str,\n            handler: Callable[[str, MipsServiceState, dict], Coroutine]\n    ) -> None:\n        if key is None or group_id is None or handler is None:\n            raise MipsServiceError('invalid params')\n        self._sub_list[(key, group_id)] = handler\n\n    def unsub_service_change(self, key: str) -> None:\n        if key is None:\n            return\n        for keys in list(self._sub_list.keys()):\n            if key == keys[0]:\n                self._sub_list.pop(keys, None)\n\n    def __on_service_state_change(\n            self, zeroconf: Zeroconf, service_type: str, name: str,\n            state_change: ServiceStateChange\n    ) -> None:\n        _LOGGER.debug(\n            'mdns discovery changed, %s, %s, %s',\n            state_change, name, service_type)\n\n        if state_change is ServiceStateChange.Removed:\n            for item in list(self._services.values()):\n                if item['name'] != name:\n                    continue\n            # Ignore mdns REMOVED package. Let the connection close by itself.\n                # service_data = self._services.pop(item['group_id'], {})\n                # self.__call_service_change(\n                #     state=MipsServiceState.REMOVED, data=service_data)\n                return\n        self._main_loop.create_task(\n            self.__request_service_info_async(zeroconf, service_type, name))\n\n    async def __request_service_info_async(\n            self, zeroconf: Zeroconf, service_type: str, name: str\n    ) -> None:\n        info = AsyncServiceInfo(service_type, name)\n        await info.async_request(\n            zeroconf, MIPS_MDNS_REQUEST_TIMEOUT_MS,\n            question_type=DNSQuestionType.QU)\n\n        try:\n            service_data = MipsServiceData(info)\n            if not service_data.valid_service():\n                raise MipsServiceError(\n                    'no primary role, no support mqtt connection')\n            if service_data.group_id in self._services:\n                # Update mips service\n                buffer_data = self._services[service_data.group_id]\n                if (\n                    service_data.did != buffer_data['did']\n                    or service_data.addresses != buffer_data['addresses']\n                    or service_data.port != buffer_data['port']\n                ):\n                    self._services[service_data.group_id].update(\n                        service_data.to_dict())\n                    self.__call_service_change(\n                        state=MipsServiceState.UPDATED,\n                        data=service_data.to_dict())\n            else:\n                # Add mips service\n                self._services[service_data.group_id] = service_data.to_dict()\n                self.__call_service_change(\n                    state=MipsServiceState.ADDED,\n                    data=self._services[service_data.group_id])\n        except MipsServiceError as error:\n            _LOGGER.error('invalid mips service, %s, %s', error, info)\n\n    def __call_service_change(\n        self, state: MipsServiceState, data: dict\n    ) -> None:\n        _LOGGER.info('call service change, %s, %s', state, data)\n        for keys in list(self._sub_list.keys()):\n            if keys[1] in [data.get('group_id', None), '*']:\n                self._main_loop.create_task(\n                    self._sub_list[keys](data['group_id'], state, data))\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_mips.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT Pub/Sub client.\n\"\"\"\nimport asyncio\nimport json\nimport logging\nimport random\nimport re\nimport ssl\nimport struct\nimport threading\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\nfrom typing import Any, Callable, Optional, final, Coroutine\n\nfrom paho.mqtt.client import (\n    MQTT_ERR_SUCCESS,\n    MQTT_ERR_NO_CONN,\n    MQTT_ERR_UNKNOWN,\n    Client,\n    MQTTv5,\n    MQTTMessage)\n\n# pylint: disable=relative-beyond-top-level\nfrom .common import MIoTMatcher\nfrom .const import (\n    UNSUPPORTED_MODELS,\n    MIHOME_MQTT_KEEPALIVE,\n    DEFAULT_CLOUD_BROKER_HOST\n)\nfrom .miot_error import MIoTErrorCode, MIoTMipsError\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass _MipsMsgTypeOptions(Enum):\n    \"\"\"MIoT Pub/Sub message type.\"\"\"\n    ID = 0\n    RET_TOPIC = auto()\n    PAYLOAD = auto()\n    FROM = auto()\n    MAX = auto()\n\n\nclass _MipsMessage:\n    \"\"\"MIoT Pub/Sub message.\"\"\"\n    mid: int = 0\n    msg_from: Optional[str] = None\n    ret_topic: Optional[str] = None\n    payload: Optional[str] = None\n\n    @staticmethod\n    def unpack(data: bytes) -> '_MipsMessage':\n        mips_msg = _MipsMessage()\n        data_len = len(data)\n        data_start = 0\n        data_end = 0\n        while data_start < data_len:\n            data_end = data_start+5\n            unpack_len, unpack_type = struct.unpack(\n                '<IB', data[data_start:data_end])\n            unpack_data = data[data_end:data_end+unpack_len]\n            #  string end with \\x00\n            match unpack_type:\n                case _MipsMsgTypeOptions.ID.value:\n                    mips_msg.mid = int.from_bytes(\n                        unpack_data, byteorder='little')\n                case _MipsMsgTypeOptions.RET_TOPIC.value:\n                    mips_msg.ret_topic = str(\n                        unpack_data.strip(b'\\x00'), 'utf-8')\n                case _MipsMsgTypeOptions.PAYLOAD.value:\n                    mips_msg.payload = str(unpack_data.strip(b'\\x00'), 'utf-8')\n                case _MipsMsgTypeOptions.FROM.value:\n                    mips_msg.msg_from = str(\n                        unpack_data.strip(b'\\x00'), 'utf-8')\n                case _:\n                    pass\n            data_start = data_end+unpack_len\n        return mips_msg\n\n    @staticmethod\n    def pack(\n        mid: int,\n        payload: str,\n        msg_from: Optional[str] = None,\n        ret_topic: Optional[str] = None\n    ) -> bytes:\n        if mid is None or payload is None:\n            raise MIoTMipsError('invalid mid or payload')\n        pack_msg: bytes = b''\n        # mid\n        pack_msg += struct.pack('<IBI', 4, _MipsMsgTypeOptions.ID.value, mid)\n        # msg_from\n        if msg_from:\n            pack_len = len(msg_from)\n            pack_msg += struct.pack(\n                f'<IB{pack_len}sx', pack_len+1,\n                _MipsMsgTypeOptions.FROM.value, msg_from.encode('utf-8'))\n        # ret_topic\n        if ret_topic:\n            pack_len = len(ret_topic)\n            pack_msg += struct.pack(\n                f'<IB{pack_len}sx', pack_len+1,\n                _MipsMsgTypeOptions.RET_TOPIC.value, ret_topic.encode('utf-8'))\n        # payload\n        pack_len = len(payload)\n        pack_msg += struct.pack(\n            f'<IB{pack_len}sx', pack_len+1,\n            _MipsMsgTypeOptions.PAYLOAD.value, payload.encode('utf-8'))\n        return pack_msg\n\n    def __str__(self) -> str:\n        return f'{self.mid}, {self.msg_from}, {self.ret_topic}, {self.payload}'\n\n\n@dataclass\nclass _MipsRequest:\n    \"\"\"MIoT Pub/Sub request.\"\"\"\n    mid: int\n    on_reply: Callable[[str, Any], None]\n    on_reply_ctx: Any\n    timer: Optional[asyncio.TimerHandle]\n\n\n@dataclass\nclass _MipsBroadcast:\n    \"\"\"MIoT Pub/Sub broadcast.\"\"\"\n    topic: str\n    \"\"\"\n    param 1: msg topic\n    param 2: msg payload\n    param 3: handle_ctx\n    \"\"\"\n    handler: Callable[[str, str, Any], None]\n    handler_ctx: Any\n\n    def __str__(self) -> str:\n        return f'{self.topic}, {id(self.handler)}, {id(self.handler_ctx)}'\n\n\n@dataclass\nclass _MipsState:\n    \"\"\"MIoT Pub/Sub state.\"\"\"\n    key: str\n    \"\"\"\n    str: key\n    bool: mips connect state\n    \"\"\"\n    handler: Callable[[str, bool], Coroutine]\n\n\nclass MIoTDeviceState(Enum):\n    \"\"\"MIoT device state define.\"\"\"\n    DISABLE = 0\n    OFFLINE = auto()\n    ONLINE = auto()\n\n\n@dataclass\nclass MipsDeviceState:\n    \"\"\"MIoT Pub/Sub device state.\"\"\"\n    did: Optional[str] = None\n    \"\"\"handler\n    str: did\n    MIoTDeviceState: online/offline/disable\n    Any: ctx\n    \"\"\"\n    handler: Optional[Callable[[str, MIoTDeviceState, Any], None]] = None\n    handler_ctx: Any = None\n\n\nclass _MipsClient(ABC):\n    \"\"\"MIoT Pub/Sub client.\"\"\"\n    # pylint: disable=unused-argument\n    MQTT_INTERVAL_S = 1\n    MIPS_QOS: int = 2\n    UINT32_MAX: int = 0xFFFFFFFF\n    MIPS_RECONNECT_INTERVAL_MIN: float = 10\n    MIPS_RECONNECT_INTERVAL_MAX: float = 600\n    MIPS_SUB_PATCH: int = 300\n    MIPS_SUB_INTERVAL: float = 1\n    main_loop: asyncio.AbstractEventLoop\n    _logger: Optional[logging.Logger]\n    _client_id: str\n    _host: str\n    _port: int\n    _username: Optional[str]\n    _password: Optional[str]\n    _ca_file: Optional[str]\n    _cert_file: Optional[str]\n    _key_file: Optional[str]\n\n    _mqtt_logger: Optional[logging.Logger]\n    _mqtt: Optional[Client]\n    _mqtt_fd: int\n    _mqtt_timer: Optional[asyncio.TimerHandle]\n    _mqtt_state: bool\n\n    _event_connect: asyncio.Event\n    _event_disconnect: asyncio.Event\n    _internal_loop: asyncio.AbstractEventLoop\n    _mips_thread: Optional[threading.Thread]\n    _mips_reconnect_tag: bool\n    _mips_reconnect_interval: float\n    _mips_reconnect_timer: Optional[asyncio.TimerHandle]\n    _mips_state_sub_map: dict[str, _MipsState]\n    _mips_state_sub_map_lock: threading.Lock\n    _mips_sub_pending_map: dict[str, int]\n    _mips_sub_pending_timer: Optional[asyncio.TimerHandle]\n\n    def __init__(\n            self,\n            client_id: str,\n            host: str,\n            port: int,\n            username: Optional[str] = None,\n            password: Optional[str] = None,\n            ca_file: Optional[str] = None,\n            cert_file: Optional[str] = None,\n            key_file: Optional[str] = None,\n            loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        # MUST run with running loop\n        self.main_loop = loop or asyncio.get_running_loop()\n        self._logger = None\n        self._client_id = client_id\n        self._host = host\n        self._port = port\n        self._username = username\n        self._password = password\n        self._ca_file = ca_file\n        self._cert_file = cert_file\n        self._key_file = key_file\n\n        self._mqtt_logger = None\n        self._mqtt_fd = -1\n        self._mqtt_timer = None\n        self._mqtt_state = False\n        self._mqtt = None\n\n        # Mips init\n        self._event_connect = asyncio.Event()\n        self._event_disconnect = asyncio.Event()\n        self._mips_thread = None\n        self._mips_reconnect_tag = False\n        self._mips_reconnect_interval = 0\n        self._mips_reconnect_timer = None\n        self._mips_state_sub_map = {}\n        self._mips_state_sub_map_lock = threading.Lock()\n        self._mips_sub_pending_map = {}\n        self._mips_sub_pending_timer = None\n        # DO NOT start the thread yet. Do that on connect\n\n    @property\n    def client_id(self) -> str:\n        return self._client_id\n\n    @property\n    def host(self) -> str:\n        return self._host\n\n    @property\n    def port(self) -> int:\n        return self._port\n\n    @final\n    @property\n    def mips_state(self) -> bool:\n        \"\"\"mips connect state.\n\n        Returns:\n            bool: True: connected, False: disconnected\n        \"\"\"\n        if self._mqtt:\n            return self._mqtt.is_connected()\n        return False\n\n    def connect(self, thread_name: Optional[str] = None) -> None:\n        \"\"\"mips connect.\"\"\"\n        # Start mips thread\n        if self._mips_thread:\n            return\n        self._internal_loop = asyncio.new_event_loop()\n        self._mips_thread = threading.Thread(target=self.__mips_loop_thread)\n        self._mips_thread.daemon = True\n        self._mips_thread.name = (\n            self._client_id if thread_name is None else thread_name)\n        self._mips_thread.start()\n\n    async def connect_async(self) -> None:\n        \"\"\"mips connect async.\"\"\"\n        self.connect()\n        await self._event_connect.wait()\n\n    def disconnect(self) -> None:\n        \"\"\"mips disconnect.\"\"\"\n        if not self._mips_thread:\n            return\n        self._internal_loop.call_soon_threadsafe(self.__mips_disconnect)\n        self._mips_thread.join()\n        self._mips_thread = None\n        self._internal_loop.close()\n\n    async def disconnect_async(self) -> None:\n        \"\"\"mips disconnect async.\"\"\"\n        self.disconnect()\n        await self._event_disconnect.wait()\n\n    @final\n    def deinit(self) -> None:\n        self.disconnect()\n\n        self._logger = None\n        self._username = None\n        self._password = None\n        self._ca_file = None\n        self._cert_file = None\n        self._key_file = None\n        self._mqtt_logger = None\n        with self._mips_state_sub_map_lock:\n            self._mips_state_sub_map.clear()\n        self._mips_sub_pending_map.clear()\n        self._mips_sub_pending_timer = None\n\n    @final\n    async def deinit_async(self) -> None:\n        await self.disconnect_async()\n\n        self._logger = None\n        self._username = None\n        self._password = None\n        self._ca_file = None\n        self._cert_file = None\n        self._key_file = None\n        self._mqtt_logger = None\n        with self._mips_state_sub_map_lock:\n            self._mips_state_sub_map.clear()\n        self._mips_sub_pending_map.clear()\n        self._mips_sub_pending_timer = None\n\n    def update_mqtt_password(self, password: str) -> None:\n        self._password = password\n        if self._mqtt:\n            self._mqtt.username_pw_set(\n                username=self._username, password=self._password)\n\n    def log_debug(self, msg, *args, **kwargs) -> None:\n        if self._logger:\n            self._logger.debug(f'{self._client_id}, '+msg, *args, **kwargs)\n\n    def log_info(self, msg, *args, **kwargs) -> None:\n        if self._logger:\n            self._logger.info(f'{self._client_id}, '+msg, *args, **kwargs)\n\n    def log_error(self, msg, *args, **kwargs) -> None:\n        if self._logger:\n            self._logger.error(f'{self._client_id}, '+msg, *args, **kwargs)\n\n    def enable_logger(self, logger: Optional[logging.Logger] = None) -> None:\n        self._logger = logger\n\n    def enable_mqtt_logger(\n        self, logger: Optional[logging.Logger] = None\n    ) -> None:\n        self._mqtt_logger = logger\n        if self._mqtt:\n            if logger:\n                self._mqtt.enable_logger(logger=logger)\n            else:\n                self._mqtt.disable_logger()\n\n    @final\n    def sub_mips_state(\n        self, key: str, handler: Callable[[str, bool], Coroutine]\n    ) -> bool:\n        \"\"\"Subscribe mips state.\n        NOTICE: callback to main loop thread\n        This will be called before the client is connected.\n        So use mutex instead of IPC.\n        \"\"\"\n        if isinstance(key, str) is False or handler is None:\n            raise MIoTMipsError('invalid params')\n        state = _MipsState(key=key, handler=handler)\n        with self._mips_state_sub_map_lock:\n            self._mips_state_sub_map[key] = state\n        self.log_debug(f'mips register mips state, {key}')\n        return True\n\n    @final\n    def unsub_mips_state(self, key: str) -> bool:\n        \"\"\"Unsubscribe mips state.\"\"\"\n        if isinstance(key, str) is False:\n            raise MIoTMipsError('invalid params')\n        with self._mips_state_sub_map_lock:\n            del self._mips_state_sub_map[key]\n        self.log_debug(f'mips unregister mips state, {key}')\n        return True\n\n    @abstractmethod\n    def sub_prop(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        piid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool: ...\n\n    @abstractmethod\n    def unsub_prop(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        piid: Optional[int] = None\n    ) -> bool: ...\n\n    @abstractmethod\n    def sub_event(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool: ...\n\n    @abstractmethod\n    def unsub_event(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None\n    ) -> bool: ...\n\n    @abstractmethod\n    async def get_dev_list_async(\n        self,\n        payload: Optional[str] = None,\n        timeout_ms: int = 10000\n    ) -> dict[str, dict]: ...\n\n    @abstractmethod\n    async def get_prop_async(\n        self, did: str, siid: int, piid: int, timeout_ms: int = 10000\n    ) -> Any: ...\n\n    @abstractmethod\n    async def set_prop_async(\n        self, did: str, siid: int, piid: int, value: Any,\n        timeout_ms: int = 10000\n    ) -> dict: ...\n\n    @abstractmethod\n    async def action_async(\n        self, did: str, siid: int, aiid: int, in_list: list,\n        timeout_ms: int = 10000\n    ) -> dict: ...\n\n    @abstractmethod\n    def _on_mips_message(self, topic: str, payload: bytes) -> None: ...\n\n    @abstractmethod\n    def _on_mips_connect(self, rc: int, props: dict) -> None: ...\n\n    @abstractmethod\n    def _on_mips_disconnect(self, rc: int, props: dict) -> None: ...\n\n    @final\n    def _mips_sub_internal(self, topic: str) -> None:\n        \"\"\"mips subscribe.\n        NOTICE: Internal function, only mips threads are allowed to call\n        \"\"\"\n        self.__thread_check()\n        if not self._mqtt or not self._mqtt.is_connected():\n            self.log_error(f'mips sub when not connected, {topic}')\n            return\n\n        if topic not in self._mips_sub_pending_map:\n            self._mips_sub_pending_map[topic] = 0\n        if not self._mips_sub_pending_timer:\n            self._mips_sub_pending_timer = self._internal_loop.call_later(\n                0.01, self.__mips_sub_internal_pending_handler, topic)\n\n    @final\n    def _mips_unsub_internal(self, topic: str) -> None:\n        \"\"\"mips unsubscribe.\n        NOTICE: Internal function, only mips threads are allowed to call\n        \"\"\"\n        self.__thread_check()\n        if not self._mqtt or not self._mqtt.is_connected():\n            self.log_debug(f'mips unsub when not connected, {topic}')\n            return\n        try:\n            result, mid = self._mqtt.unsubscribe(topic=topic)\n            if (result == MQTT_ERR_SUCCESS) or (result == MQTT_ERR_NO_CONN):\n                self.log_debug(\n                    f'mips unsub internal success, {result}, {mid}, {topic}')\n                return\n            self.log_error(\n                f'mips unsub internal error, {result}, {mid}, {topic}')\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            # Catch all exception\n            self.log_error(f'mips unsub internal error, {topic}, {err}')\n\n    @final\n    def _mips_publish_internal(\n        self, topic: str, payload: str | bytes,\n        wait_for_publish: bool = False, timeout_ms: int = 10000\n    ) -> bool:\n        \"\"\"mips publish message.\n        NOTICE: Internal function, only mips threads are allowed to call\n\n        \"\"\"\n        self.__thread_check()\n        if not self._mqtt or not self._mqtt.is_connected():\n            return False\n        try:\n            handle = self._mqtt.publish(\n                topic=topic, payload=payload, qos=self.MIPS_QOS)\n            # self.log_debug(f'_mips_publish_internal, {topic}, {payload}')\n            if wait_for_publish is True:\n                handle.wait_for_publish(timeout_ms/1000.0)\n            return True\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            # Catch other exception\n            self.log_error(f'mips publish internal error, {err}')\n        return False\n\n    def __thread_check(self) -> None:\n        if threading.current_thread() is not self._mips_thread:\n            raise MIoTMipsError('illegal call')\n\n    def __mqtt_read_handler(self) -> None:\n        self.__mqtt_loop_handler()\n\n    def __mqtt_write_handler(self) -> None:\n        self._internal_loop.remove_writer(self._mqtt_fd)\n        self.__mqtt_loop_handler()\n\n    def __mqtt_timer_handler(self) -> None:\n        self.__mqtt_loop_handler()\n        if self._mqtt:\n            self._mqtt_timer = self._internal_loop.call_later(\n                self.MQTT_INTERVAL_S, self.__mqtt_timer_handler)\n\n    def __mqtt_loop_handler(self) -> None:\n        try:\n            # If the main loop is closed, stop the internal loop immediately\n            if self.main_loop.is_closed():\n                self.log_debug(\n                    'The main loop is closed, stop the internal loop.')\n                if not self._internal_loop.is_closed():\n                    self._internal_loop.stop()\n                return\n            if self._mqtt:\n                self._mqtt.loop_read()\n            if self._mqtt:\n                self._mqtt.loop_write()\n            if self._mqtt:\n                self._mqtt.loop_misc()\n            if self._mqtt and self._mqtt.want_write():\n                self._internal_loop.add_writer(\n                    self._mqtt_fd, self.__mqtt_write_handler)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            # Catch all exception\n            self.log_error(f'__mqtt_loop_handler, {err}')\n            raise err\n\n    def __mips_loop_thread(self) -> None:\n        self.log_info('mips_loop_thread start')\n        # mqtt init for API_VERSION2,\n        # callback_api_version=CallbackAPIVersion.VERSION2,\n        self._mqtt = Client(client_id=self._client_id, protocol=MQTTv5)\n        self._mqtt.enable_logger(logger=self._mqtt_logger)\n        # Set mqtt config\n        if self._username:\n            self._mqtt.username_pw_set(\n                username=self._username, password=self._password)\n        if (\n            self._ca_file\n            and self._cert_file\n            and self._key_file\n        ):\n            self._mqtt.tls_set(\n                tls_version=ssl.PROTOCOL_TLS_CLIENT,\n                ca_certs=self._ca_file,\n                certfile=self._cert_file,\n                keyfile=self._key_file)\n        else:\n            self._mqtt.tls_set(tls_version=ssl.PROTOCOL_TLS_CLIENT)\n        self._mqtt.tls_insecure_set(True)\n        self._mqtt.on_connect = self.__on_connect\n        self._mqtt.on_connect_fail = self.__on_connect_failed\n        self._mqtt.on_disconnect = self.__on_disconnect\n        self._mqtt.on_message = self.__on_message\n        # Connect to mips\n        self.__mips_start_connect_tries()\n        # Run event loop\n        self._internal_loop.run_forever()\n        self.log_info('mips_loop_thread exit!')\n\n    def __on_connect(self, client, user_data, flags, rc, props) -> None:\n        if not self._mqtt:\n            _LOGGER.error('__on_connect, but mqtt is None')\n            return\n        if not self._mqtt.is_connected():\n            _LOGGER.error('__on_connect, but mqtt is disconnected')\n            return\n        self.log_info(f'mips connect, {flags}, {rc}, {props}')\n        self.__reset_reconnect_time()\n        self._mqtt_state = True\n        self._internal_loop.call_soon(\n            self._on_mips_connect, rc, props)\n        with self._mips_state_sub_map_lock:\n            for item in self._mips_state_sub_map.values():\n                if item.handler is None:\n                    continue\n                self.main_loop.call_soon_threadsafe(\n                    self.main_loop.create_task,\n                    item.handler(item.key, True))\n        # Resolve future\n        self.main_loop.call_soon_threadsafe(\n            self._event_connect.set)\n        self.main_loop.call_soon_threadsafe(\n            self._event_disconnect.clear)\n\n    def __on_connect_failed(self, client: Client, user_data: Any) -> None:\n        self.log_error('mips connect failed')\n        # Try to reconnect\n        self.__mips_try_reconnect()\n\n    def __on_disconnect(self,  client, user_data, rc, props) -> None:\n        if self._mqtt_state:\n            (self.log_info if rc == 0 else self.log_error)(\n                f'mips disconnect, {rc}, {props}')\n            self._mqtt_state = False\n            if self._mqtt_timer:\n                self._mqtt_timer.cancel()\n                self._mqtt_timer = None\n            if self._mqtt_fd != -1:\n                self._internal_loop.remove_reader(self._mqtt_fd)\n                self._internal_loop.remove_writer(self._mqtt_fd)\n                self._mqtt_fd = -1\n            # Clear retry sub\n            if self._mips_sub_pending_timer:\n                self._mips_sub_pending_timer.cancel()\n                self._mips_sub_pending_timer = None\n            self._mips_sub_pending_map = {}\n            self._internal_loop.call_soon(\n                self._on_mips_disconnect, rc, props)\n            # Call state sub handler\n            with self._mips_state_sub_map_lock:\n                for item in self._mips_state_sub_map.values():\n                    if item.handler is None:\n                        continue\n                    self.main_loop.call_soon_threadsafe(\n                        self.main_loop.create_task,\n                        item.handler(item.key, False))\n\n        # Try to reconnect\n        self.__mips_try_reconnect()\n        # Set event\n        self.main_loop.call_soon_threadsafe(\n            self._event_disconnect.set)\n        self.main_loop.call_soon_threadsafe(\n            self._event_connect.clear)\n\n    def __on_message(\n        self,\n        client: Client,\n        user_data: Any,\n        msg: MQTTMessage\n    ) -> None:\n        self._on_mips_message(topic=msg.topic, payload=msg.payload)\n\n    def __mips_sub_internal_pending_handler(self, ctx: Any) -> None:\n        if not self._mqtt or not self._mqtt.is_connected():\n            _LOGGER.error(\n                'mips sub internal pending, but mqtt is None or disconnected')\n            return\n        subbed_count = 1\n        for topic in list(self._mips_sub_pending_map.keys()):\n            if subbed_count > self.MIPS_SUB_PATCH:\n                break\n            count = self._mips_sub_pending_map[topic]\n            if count > 3:\n                self._mips_sub_pending_map.pop(topic)\n                self.log_error(f'retry mips sub internal error, {topic}')\n                continue\n            subbed_count += 1\n            result = mid = None\n            try:\n                result, mid = self._mqtt.subscribe(topic, qos=self.MIPS_QOS)\n                if result == MQTT_ERR_SUCCESS:\n                    self._mips_sub_pending_map.pop(topic)\n                    self.log_debug(f'mips sub internal success, {topic}')\n                    continue\n            except Exception as err:  # pylint: disable=broad-exception-caught\n                # Catch all exception\n                self.log_error(f'mips sub internal error, {topic}. {err}')\n            self._mips_sub_pending_map[topic] = count+1\n            self.log_error(\n                f'retry mips sub internal, {count}, {topic}, {result}, {mid}')\n\n        if len(self._mips_sub_pending_map):\n            self._mips_sub_pending_timer = self._internal_loop.call_later(\n                self.MIPS_SUB_INTERVAL,\n                self.__mips_sub_internal_pending_handler, None)\n        else:\n            self._mips_sub_pending_timer = None\n\n    def __mips_connect(self) -> None:\n        if not self._mqtt:\n            _LOGGER.error('__mips_connect, but mqtt is None')\n            return\n        result = MQTT_ERR_UNKNOWN\n        if self._mips_reconnect_timer:\n            self._mips_reconnect_timer.cancel()\n            self._mips_reconnect_timer = None\n        try:\n            # Try clean mqtt fd before mqtt connect\n            if self._mqtt_timer:\n                self._mqtt_timer.cancel()\n                self._mqtt_timer = None\n            if self._mqtt_fd != -1:\n                self._internal_loop.remove_reader(self._mqtt_fd)\n                self._internal_loop.remove_writer(self._mqtt_fd)\n                self._mqtt_fd = -1\n            result = self._mqtt.connect(\n                host=self._host, port=self._port,\n                clean_start=True, keepalive=MIHOME_MQTT_KEEPALIVE)\n            self.log_info(f'__mips_connect success, {result}')\n        except (TimeoutError, OSError) as error:\n            self.log_error('__mips_connect, connect error, %s', error)\n\n        if result == MQTT_ERR_SUCCESS:\n            socket = self._mqtt.socket()\n            if socket is None:\n                self.log_error(\n                    '__mips_connect, connect success, but socket is None')\n                self.__mips_try_reconnect()\n                return\n            self._mqtt_fd = socket.fileno()\n            self.log_debug(f'__mips_connect, _mqtt_fd, {self._mqtt_fd}')\n            self._internal_loop.add_reader(\n                self._mqtt_fd, self.__mqtt_read_handler)\n            if self._mqtt.want_write():\n                self._internal_loop.add_writer(\n                    self._mqtt_fd, self.__mqtt_write_handler)\n            self._mqtt_timer = self._internal_loop.call_later(\n                self.MQTT_INTERVAL_S, self.__mqtt_timer_handler)\n        else:\n            self.log_error(f'__mips_connect error result, {result}')\n            self.__mips_try_reconnect()\n\n    def __mips_try_reconnect(self, immediately: bool = False) -> None:\n        if self._mips_reconnect_timer:\n            self._mips_reconnect_timer.cancel()\n            self._mips_reconnect_timer = None\n        if not self._mips_reconnect_tag:\n            return\n        interval: float = 0\n        if not immediately:\n            interval = self.__get_next_reconnect_time()\n            self.log_error(\n                'mips try reconnect after %ss', interval)\n        self._mips_reconnect_timer = self._internal_loop.call_later(\n            interval, self.__mips_connect)\n\n    def __mips_start_connect_tries(self) -> None:\n        self._mips_reconnect_tag = True\n        self.__mips_try_reconnect(immediately=True)\n\n    def __mips_disconnect(self) -> None:\n        self._mips_reconnect_tag = False\n        if self._mips_reconnect_timer:\n            self._mips_reconnect_timer.cancel()\n            self._mips_reconnect_timer = None\n        if self._mqtt_timer:\n            self._mqtt_timer.cancel()\n            self._mqtt_timer = None\n        if self._mqtt_fd != -1:\n            self._internal_loop.remove_reader(self._mqtt_fd)\n            self._internal_loop.remove_writer(self._mqtt_fd)\n            self._mqtt_fd = -1\n        # Clear retry sub\n        if self._mips_sub_pending_timer:\n            self._mips_sub_pending_timer.cancel()\n            self._mips_sub_pending_timer = None\n        self._mips_sub_pending_map = {}\n        if self._mqtt:\n            self._mqtt.disconnect()\n            self._mqtt = None\n        self._internal_loop.stop()\n\n    def __get_next_reconnect_time(self) -> float:\n        if self._mips_reconnect_interval < self.MIPS_RECONNECT_INTERVAL_MIN:\n            self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN\n        else:\n            self._mips_reconnect_interval = min(\n                self._mips_reconnect_interval*2,\n                self.MIPS_RECONNECT_INTERVAL_MAX)\n        return self._mips_reconnect_interval\n\n    def __reset_reconnect_time(self) -> None:\n        self._mips_reconnect_interval = 0\n\n\nclass MipsCloudClient(_MipsClient):\n    \"\"\"MIoT Pub/Sub Cloud Client.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    _msg_matcher: MIoTMatcher\n\n    def __init__(\n            self, uuid: str, cloud_server: str, app_id: str,\n            token: str, port: int = 8883,\n            loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._msg_matcher = MIoTMatcher()\n        super().__init__(\n            client_id=f'ha.{uuid}',\n            host=f'{cloud_server}-{DEFAULT_CLOUD_BROKER_HOST}',\n            port=port, username=app_id, password=token, loop=loop)\n\n    @final\n    def disconnect(self) -> None:\n        super().disconnect()\n        self._msg_matcher = MIoTMatcher()\n\n    def update_access_token(self, access_token: str) -> bool:\n        if not isinstance(access_token, str):\n            raise MIoTMipsError('invalid token')\n        self.update_mqtt_password(password=access_token)\n        return True\n\n    @final\n    def sub_prop(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        piid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        if not isinstance(did, str) or handler is None:\n            raise MIoTMipsError('invalid params')\n\n        topic: str = (\n            f'device/{did}/up/properties_changed/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}/{piid}\"}')\n\n        def on_prop_msg(topic: str, payload: str, ctx: Any) -> None:\n            try:\n                msg: dict = json.loads(payload)\n            except json.JSONDecodeError:\n                self.log_error(\n                    f'on_prop_msg, invalid msg, {topic}, {payload}')\n                return\n            if (\n                not isinstance(msg.get('params', None), dict)\n                or 'siid' not in msg['params']\n                or 'piid' not in msg['params']\n                or 'value' not in msg['params']\n            ):\n                self.log_error(\n                    f'on_prop_msg, invalid msg, {topic}, {payload}')\n                return\n            if handler:\n                self.log_debug('on properties_changed, %s', payload)\n                handler(msg['params'], ctx)\n        return self.__reg_broadcast_external(\n            topic=topic, handler=on_prop_msg, handler_ctx=handler_ctx)\n\n    @final\n    def unsub_prop(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        piid: Optional[int] = None\n    ) -> bool:\n        if not isinstance(did, str):\n            raise MIoTMipsError('invalid params')\n        topic: str = (\n            f'device/{did}/up/properties_changed/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}/{piid}\"}')\n        return self.__unreg_broadcast_external(topic=topic)\n\n    @final\n    def sub_event(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        if not isinstance(did, str) or handler is None:\n            raise MIoTMipsError('invalid params')\n        # Spelling error: event_occured\n        topic: str = (\n            f'device/{did}/up/event_occured/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}/{eiid}\"}')\n\n        def on_event_msg(topic: str, payload: str, ctx: Any) -> None:\n            try:\n                msg: dict = json.loads(payload)\n            except json.JSONDecodeError:\n                self.log_error(\n                    f'on_event_msg, invalid msg, {topic}, {payload}')\n                return\n            if (\n                not isinstance(msg.get('params', None), dict)\n                or 'siid' not in msg['params']\n                or 'eiid' not in msg['params']\n                or 'arguments' not in msg['params']\n            ):\n                self.log_error(\n                    f'on_event_msg, invalid msg, {topic}, {payload}')\n                return\n            if handler:\n                self.log_debug('on on_event_msg, %s', payload)\n                msg['params']['from'] = 'cloud'\n                handler(msg['params'], ctx)\n        return self.__reg_broadcast_external(\n            topic=topic, handler=on_event_msg, handler_ctx=handler_ctx)\n\n    @final\n    def unsub_event(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None\n    ) -> bool:\n        if not isinstance(did, str):\n            raise MIoTMipsError('invalid params')\n        # Spelling error: event_occured\n        topic: str = (\n            f'device/{did}/up/event_occured/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}/{eiid}\"}')\n        return self.__unreg_broadcast_external(topic=topic)\n\n    @final\n    def sub_device_state(\n        self, did: str, handler: Callable[[str, MIoTDeviceState, Any], None],\n        handler_ctx: Any = None\n    ) -> bool:\n        \"\"\"subscribe online state.\"\"\"\n        if not isinstance(did, str) or handler is None:\n            raise MIoTMipsError('invalid params')\n        topic: str = f'device/{did}/state/#'\n\n        def on_state_msg(topic: str, payload: str, ctx: Any) -> None:\n            msg: dict = json.loads(payload)\n            # {\"device_id\":\"xxxx\",\"device_name\":\"米家智能插座3   \",\"event\":\"online\",\n            # \"model\": \"cuco.plug.v3\",\"timestamp\":1709001070828,\"uid\":xxxx}\n            if msg is None or 'device_id' not in msg or 'event' not in msg:\n                self.log_error(f'on_state_msg, recv unknown msg, {payload}')\n                return\n            if msg['device_id'] != did:\n                self.log_error(\n                    f'on_state_msg, err msg, {did}!={msg[\"device_id\"]}')\n                return\n            if handler:\n                self.log_debug('cloud, device state changed, %s', payload)\n                handler(\n                    did, MIoTDeviceState.ONLINE if msg['event'] == 'online'\n                    else MIoTDeviceState.OFFLINE, ctx)\n\n        if did.startswith('blt.') or did.startswith('proxy.'):\n        # MIoT cloud may not publish BLE device or proxy gateway child device\n        # online/offline state message.\n        # Do not subscribe BLE device or proxy gateway child device\n        # online/offline state.\n            return True\n        return self.__reg_broadcast_external(\n            topic=topic, handler=on_state_msg, handler_ctx=handler_ctx)\n\n    @final\n    def unsub_device_state(self, did: str) -> bool:\n        if not isinstance(did, str):\n            raise MIoTMipsError('invalid params')\n        topic: str = f'device/{did}/state/#'\n        return self.__unreg_broadcast_external(topic=topic)\n\n    async def get_dev_list_async(\n        self, payload: Optional[str] = None, timeout_ms: int = 10000\n    ) -> dict[str, dict]:\n        raise NotImplementedError('please call in http client')\n\n    async def get_prop_async(\n        self, did: str, siid: int, piid: int,  timeout_ms: int = 10000\n    ) -> Any:\n        raise NotImplementedError('please call in http client')\n\n    async def set_prop_async(\n        self, did: str, siid: int, piid: int, value: Any,\n        timeout_ms: int = 10000\n    ) -> dict:\n        raise NotImplementedError('please call in http client')\n\n    async def action_async(\n        self, did: str, siid: int, aiid: int, in_list: list,\n        timeout_ms: int = 10000\n    ) -> dict:\n        raise NotImplementedError('please call in http client')\n\n    def __reg_broadcast_external(\n        self, topic: str, handler: Callable[[str, str, Any], None],\n        handler_ctx: Any = None\n    ) -> bool:\n        self._internal_loop.call_soon_threadsafe(\n            self.__reg_broadcast, topic, handler, handler_ctx)\n        return True\n\n    def __unreg_broadcast_external(self, topic: str) -> bool:\n        self._internal_loop.call_soon_threadsafe(\n            self.__unreg_broadcast, topic)\n        return True\n\n    def __reg_broadcast(\n        self, topic: str, handler: Callable[[str, str, Any], None],\n        handler_ctx: Any = None\n    ) -> None:\n        if not self._msg_matcher.get(topic=topic):\n            sub_bc: _MipsBroadcast = _MipsBroadcast(\n                topic=topic, handler=handler,\n                handler_ctx=handler_ctx)\n            self._msg_matcher[topic] = sub_bc\n            self._mips_sub_internal(topic=topic)\n        else:\n            self.log_debug(f'mips cloud re-reg broadcast, {topic}')\n\n    def __unreg_broadcast(self, topic: str) -> None:\n        if self._msg_matcher.get(topic=topic):\n            del self._msg_matcher[topic]\n            self._mips_unsub_internal(topic=topic)\n\n    def _on_mips_connect(self, rc: int, props: dict) -> None:\n        \"\"\"sub topic.\"\"\"\n        for topic, _ in list(\n                self._msg_matcher.iter_all_nodes()):\n            self._mips_sub_internal(topic=topic)\n\n    def _on_mips_disconnect(self, rc: int, props: dict) -> None:\n        \"\"\"unsub topic.\"\"\"\n        pass\n\n    def _on_mips_message(self, topic: str, payload: bytes) -> None:\n        \"\"\"\n        NOTICE thread safe, this function will be called at the **mips** thread\n        \"\"\"\n        # broadcast\n        bc_list: list[_MipsBroadcast] = list(\n            self._msg_matcher.iter_match(topic))\n        if not bc_list:\n            return\n        # The message from the cloud is not packed.\n        payload_str: str = payload.decode('utf-8')\n        # self.log_debug(f\"on broadcast, {topic}, {payload}\")\n        for item in bc_list or []:\n            if item.handler is None:\n                continue\n            # NOTICE: call threadsafe\n            self.main_loop.call_soon_threadsafe(\n                item.handler, topic, payload_str, item.handler_ctx)\n\n\nclass MipsLocalClient(_MipsClient):\n    \"\"\"MIoT Pub/Sub Local Client.\"\"\"\n    # pylint: disable=unused-argument\n    # pylint: disable=inconsistent-quotes\n    MIPS_RECONNECT_INTERVAL_MIN: float = 6\n    MIPS_RECONNECT_INTERVAL_MAX: float = 60\n    MIPS_SUB_PATCH: int = 1000\n    MIPS_SUB_INTERVAL: float = 0.1\n    _did: str\n    _group_id: str\n    _home_name: str\n    _mips_seed_id: int\n    _reply_topic: str\n    _dev_list_change_topic: str\n    _request_map: dict[str, _MipsRequest]\n    _msg_matcher: MIoTMatcher\n    _get_prop_queue: dict[str, list]\n    _get_prop_timer: Optional[asyncio.TimerHandle]\n    _on_dev_list_changed: Optional[Callable[[Any, list[str]], Coroutine]]\n\n    def __init__(\n        self, did: str, host: str, group_id: str,\n        ca_file: str, cert_file: str, key_file: str,\n        port: int = 8883, home_name: str = '',\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._did = did\n        self._group_id = group_id\n        self._home_name = home_name\n        self._mips_seed_id = random.randint(0, self.UINT32_MAX)\n        self._reply_topic = f'{did}/reply'\n        self._dev_list_change_topic = f'{did}/appMsg/devListChange'\n        self._request_map = {}\n        self._msg_matcher = MIoTMatcher()\n        self._get_prop_queue = {}\n        self._get_prop_timer = None\n        self._on_dev_list_changed = None\n\n        super().__init__(\n            client_id=did, host=host, port=port,\n            ca_file=ca_file, cert_file=cert_file, key_file=key_file, loop=loop)\n\n    @property\n    def group_id(self) -> str:\n        return self._group_id\n\n    def log_debug(self, msg, *args, **kwargs) -> None:\n        if self._logger:\n            self._logger.debug(f'{self._home_name}, '+msg, *args, **kwargs)\n\n    def log_info(self, msg, *args, **kwargs) -> None:\n        if self._logger:\n            self._logger.info(f'{self._home_name}, '+msg, *args, **kwargs)\n\n    def log_error(self, msg, *args, **kwargs) -> None:\n        if self._logger:\n            self._logger.error(f'{self._home_name}, '+msg, *args, **kwargs)\n\n    @final\n    def connect(self, thread_name: Optional[str] = None) -> None:\n        # MIPS local thread name use group_id\n        super().connect(self._group_id)\n\n    @final\n    def disconnect(self) -> None:\n        super().disconnect()\n        self._request_map = {}\n        self._msg_matcher = MIoTMatcher()\n\n    @final\n    def sub_prop(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        piid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        topic: str = (\n            f'appMsg/notify/iot/{did}/property/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}.{piid}\"}')\n\n        def on_prop_msg(topic: str, payload: str, ctx: Any):\n            msg: dict = json.loads(payload)\n            if (\n                msg is None\n                or 'did' not in msg\n                or 'siid' not in msg\n                or 'piid' not in msg\n                or 'value' not in msg\n            ):\n                self.log_info('unknown prop msg, %s', payload)\n                return\n            if handler:\n                self.log_debug('local, on properties_changed, %s', payload)\n                handler(msg, ctx)\n        return self.__reg_broadcast_external(\n            topic=topic, handler=on_prop_msg, handler_ctx=handler_ctx)\n\n    @final\n    def unsub_prop(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        piid: Optional[int] = None\n    ) -> bool:\n        topic: str = (\n            f'appMsg/notify/iot/{did}/property/'\n            f'{\"#\" if siid is None or piid is None else f\"{siid}.{piid}\"}')\n        return self.__unreg_broadcast_external(topic=topic)\n\n    @final\n    def sub_event(\n        self,\n        did: str,\n        handler: Callable[[dict, Any], None],\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None,\n        handler_ctx: Any = None\n    ) -> bool:\n        topic: str = (\n            f'appMsg/notify/iot/{did}/event/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}.{eiid}\"}')\n\n        def on_event_msg(topic: str, payload: str, ctx: Any):\n            msg: dict = json.loads(payload)\n            if (\n                msg is None\n                or 'did' not in msg\n                or 'siid' not in msg\n                or 'eiid' not in msg\n                # or 'arguments' not in msg\n            ):\n                self.log_info('unknown event msg, %s', payload)\n                return\n            if 'arguments' not in msg:\n                self.log_info('wrong event msg, %s', payload)\n                msg['arguments'] = []\n            if handler:\n                self.log_debug('local, on event_occurred, %s', payload)\n                handler(msg, ctx)\n        return self.__reg_broadcast_external(\n            topic=topic, handler=on_event_msg, handler_ctx=handler_ctx)\n\n    @final\n    def unsub_event(\n        self,\n        did: str,\n        siid: Optional[int] = None,\n        eiid: Optional[int] = None\n    ) -> bool:\n        topic: str = (\n            f'appMsg/notify/iot/{did}/event/'\n            f'{\"#\" if siid is None or eiid is None else f\"{siid}.{eiid}\"}')\n        return self.__unreg_broadcast_external(topic=topic)\n\n    @final\n    async def get_prop_safe_async(\n        self, did: str, siid: int, piid: int, timeout_ms: int = 10000\n    ) -> Any:\n        self._get_prop_queue.setdefault(did, [])\n        fut: asyncio.Future = self.main_loop.create_future()\n        self._get_prop_queue[did].append({\n            'param': json.dumps({\n                'did': did,\n                'siid': siid,\n                'piid': piid\n            }),\n            'fut': fut,\n            'timeout_ms': timeout_ms\n        })\n        if self._get_prop_timer is None:\n            self._get_prop_timer = self.main_loop.call_later(\n                0.1,\n                self.main_loop.create_task,\n                self.__get_prop_timer_handle())\n        return await fut\n\n    @final\n    async def get_prop_async(\n        self, did: str, siid: int, piid: int, timeout_ms: int = 10000\n    ) -> Any:\n        result_obj = await self.__request_async(\n            topic='proxy/get',\n            payload=json.dumps({\n                'did': did,\n                'siid': siid,\n                'piid': piid\n            }),\n            timeout_ms=timeout_ms)\n        if not isinstance(result_obj, dict) or 'value' not in result_obj:\n            return None\n        return result_obj['value']\n\n    @final\n    async def set_prop_async(\n        self, did: str, siid: int, piid: int, value: Any,\n        timeout_ms: int = 10000\n    ) -> dict:\n        payload_obj: dict = {\n            'did': did,\n            'rpc': {\n                'id': self.__gen_mips_id,\n                'method': 'set_properties',\n                'params': [{\n                    'did': did,\n                    'siid': siid,\n                    'piid': piid,\n                    'value': value\n                }]\n            }\n        }\n        result_obj = await self.__request_async(\n            topic='proxy/rpcReq',\n            payload=json.dumps(payload_obj),\n            timeout_ms=timeout_ms)\n        if result_obj:\n            if (\n                'result' in result_obj\n                and len(result_obj['result']) == 1\n                and 'did' in result_obj['result'][0]\n                and result_obj['result'][0]['did'] == did\n                and 'code' in result_obj['result'][0]\n            ):\n                return result_obj['result'][0]\n            if 'error' in result_obj:\n                return result_obj['error']\n        return {\n            'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,\n            'message': 'Invalid result'}\n\n    @final\n    async def action_async(\n        self, did: str, siid: int, aiid: int, in_list: list,\n        timeout_ms: int = 10000\n    ) -> dict:\n        payload_obj: dict = {\n            'did': did,\n            'rpc': {\n                'id': self.__gen_mips_id,\n                'method': 'action',\n                'params': {\n                    'did': did,\n                    'siid': siid,\n                    'aiid': aiid,\n                    'in': in_list\n                }\n            }\n        }\n        result_obj = await self.__request_async(\n            topic='proxy/rpcReq', payload=json.dumps(payload_obj),\n            timeout_ms=timeout_ms)\n        if result_obj:\n            if 'result' in result_obj and 'code' in result_obj['result']:\n                return result_obj['result']\n            if 'error' in result_obj:\n                return result_obj['error']\n        return {\n            'code': MIoTErrorCode.CODE_INTERNAL_ERROR.value,\n            'message': 'Invalid result'}\n\n    @final\n    async def get_dev_list_async(\n        self, payload: Optional[str] = None, timeout_ms: int = 10000\n    ) -> dict[str, dict]:\n        result_obj = await self.__request_async(\n            topic='proxy/getDevList', payload=payload or '{}',\n            timeout_ms=timeout_ms)\n        if not result_obj or 'devList' not in result_obj:\n            raise MIoTMipsError('invalid result')\n        device_list = {}\n        for did, info in result_obj['devList'].items():\n            name: str = info.get('name', None)\n            urn: str = info.get('urn', None)\n            model: str = info.get('model', None)\n            if name is None or urn is None or model is None:\n                self.log_error(f'invalid device info, {did}, {info}')\n                continue\n            if model in UNSUPPORTED_MODELS:\n                self.log_info(f'unsupported model, {model}, {did}')\n                continue\n            device_list[did] = {\n                'did': did,\n                'online': info.get('online', False),\n                'specv2_access': info.get('specV2Access', False),\n                'push_available': info.get('pushAvailable', False)\n            }\n        return device_list\n\n    @final\n    async def get_action_group_list_async(\n        self, timeout_ms: int = 10000\n    ) -> list[str]:\n        result_obj = await self.__request_async(\n            topic='proxy/getMijiaActionGroupList',\n            payload='{}',\n            timeout_ms=timeout_ms)\n        if not result_obj or 'result' not in result_obj:\n            raise MIoTMipsError('invalid result')\n        return result_obj['result']\n\n    @final\n    async def exec_action_group_list_async(\n        self, ag_id: str, timeout_ms: int = 10000\n    ) -> dict:\n        result_obj = await self.__request_async(\n            topic='proxy/execMijiaActionGroup',\n            payload=f'{{\"id\":\"{ag_id}\"}}',\n            timeout_ms=timeout_ms)\n        if result_obj:\n            if 'result' in result_obj:\n                return result_obj['result']\n            if 'error' in result_obj:\n                return result_obj['error']\n        return {\n            'code': MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value,\n            'message': 'invalid result'}\n\n    @final\n    @property\n    def on_dev_list_changed(\n        self\n    ) -> Optional[Callable[[Any, list[str]], Coroutine]]:\n        return self._on_dev_list_changed\n\n    @final\n    @on_dev_list_changed.setter\n    def on_dev_list_changed(\n        self, func: Optional[Callable[[Any, list[str]], Coroutine]]\n    ) -> None:\n        \"\"\"run in main loop.\"\"\"\n        self._on_dev_list_changed = func\n\n    def __request(\n            self, topic: str, payload: str,\n            on_reply: Callable[[str, Any], None],\n            on_reply_ctx: Any = None, timeout_ms: int = 10000\n    ) -> None:\n        req = _MipsRequest(\n            mid=self.__gen_mips_id,\n            on_reply=on_reply,\n            on_reply_ctx=on_reply_ctx,\n            timer=None)\n        pub_topic: str = f'master/{topic}'\n        result = self.__mips_publish(\n            topic=pub_topic, payload=payload, mid=req.mid,\n            ret_topic=self._reply_topic)\n        self.log_debug(\n            f'mips local call api, {result}, {req.mid}, {pub_topic}, '\n            f'{payload}')\n\n        def on_request_timeout(req: _MipsRequest):\n            self.log_error(\n                f'on mips request timeout, {req.mid}, {pub_topic}'\n                f', {payload}')\n            self._request_map.pop(str(req.mid), None)\n            req.on_reply(\n                '{\"error\":{\"code\":-10006, \"message\":\"timeout\"}}',\n                req.on_reply_ctx)\n        req.timer = self._internal_loop.call_later(\n            timeout_ms/1000, on_request_timeout, req)\n        self._request_map[str(req.mid)] = req\n\n    def __reg_broadcast(\n        self, topic: str, handler: Callable[[str, str, Any], None],\n        handler_ctx: Any\n    ) -> None:\n        sub_topic: str = f'{self._did}/{topic}'\n        if not self._msg_matcher.get(sub_topic):\n            sub_bc: _MipsBroadcast = _MipsBroadcast(\n                topic=sub_topic, handler=handler,\n                handler_ctx=handler_ctx)\n            self._msg_matcher[sub_topic] = sub_bc\n            self._mips_sub_internal(topic=f'master/{topic}')\n        else:\n            self.log_debug(f'mips re-reg broadcast, {sub_topic}')\n\n    def __unreg_broadcast(self, topic) -> None:\n        # Central hub gateway needs to add prefix\n        unsub_topic: str = f'{self._did}/{topic}'\n        if self._msg_matcher.get(unsub_topic):\n            del self._msg_matcher[unsub_topic]\n            self._mips_unsub_internal(\n                topic=re.sub(f'^{self._did}', 'master', unsub_topic))\n\n    @final\n    def _on_mips_connect(self, rc: int, props: dict) -> None:\n        self.log_debug('__on_mips_connect_handler')\n        # Sub did/#, include reply topic\n        self._mips_sub_internal(f'{self._did}/#')\n        # Sub device list change\n        self._mips_sub_internal('master/appMsg/devListChange')\n        # Do not need to subscribe api topics, for they are covered by did/#\n        # Sub api topic.\n        # Sub broadcast topic\n        for topic, _ in list(self._msg_matcher.iter_all_nodes()):\n            self._mips_sub_internal(\n                topic=re.sub(f'^{self._did}', 'master', topic))\n\n    @final\n    def _on_mips_disconnect(self, rc: int, props: dict) -> None:\n        pass\n\n    @final\n    def _on_mips_message(self, topic: str, payload: bytes) -> None:\n        mips_msg: _MipsMessage = _MipsMessage.unpack(payload)\n        # self.log_debug(\n        #     f\"mips local client, on_message, {topic} -> {mips_msg}\")\n        # Reply\n        if topic == self._reply_topic:\n            self.log_debug(f'on request reply, {mips_msg}')\n            req: Optional[_MipsRequest] = self._request_map.pop(\n                str(mips_msg.mid), None)\n            if req:\n                # Cancel timer\n                if req.timer:\n                    req.timer.cancel()\n                if req.on_reply:\n                    self.main_loop.call_soon_threadsafe(\n                        req.on_reply, mips_msg.payload or '{}',\n                        req.on_reply_ctx)\n            return\n        # Broadcast\n        bc_list: list[_MipsBroadcast] = list(self._msg_matcher.iter_match(\n            topic=topic))\n        if bc_list:\n            self.log_debug(f'on broadcast, {topic}, {mips_msg}')\n            for item in bc_list or []:\n                if item.handler is None:\n                    continue\n                self.main_loop.call_soon_threadsafe(\n                    item.handler, topic[topic.find('/')+1:],\n                    mips_msg.payload or '{}', item.handler_ctx)\n            return\n        # Device list change\n        if topic == self._dev_list_change_topic:\n            if mips_msg.payload is None:\n                self.log_error('devListChange msg is None')\n                return\n            payload_obj: dict = json.loads(mips_msg.payload)\n            dev_list = payload_obj.get('devList', None)\n            if not isinstance(dev_list, list) or not dev_list:\n                _LOGGER.error(\n                    'unknown devListChange msg, %s', mips_msg.payload)\n                return\n            if self._on_dev_list_changed:\n                self.main_loop.call_soon_threadsafe(\n                    self.main_loop.create_task,\n                    self._on_dev_list_changed(self, dev_list))\n            return\n\n        self.log_debug(\n            f'mips local client, recv unknown msg, {topic} -> {mips_msg}')\n\n    @property\n    def __gen_mips_id(self) -> int:\n        mips_id: int = self._mips_seed_id\n        self._mips_seed_id = int((self._mips_seed_id+1) % self.UINT32_MAX)\n        return mips_id\n\n    def __mips_publish(\n        self,\n        topic: str,\n        payload: str,\n        mid: Optional[int] = None,\n        ret_topic: Optional[str] = None,\n        wait_for_publish: bool = False,\n        timeout_ms: int = 10000\n    ) -> bool:\n        mips_msg: bytes = _MipsMessage.pack(\n            mid=mid or self.__gen_mips_id, payload=payload,\n            msg_from='local', ret_topic=ret_topic)\n        return self._mips_publish_internal(\n            topic=topic.strip(), payload=mips_msg,\n            wait_for_publish=wait_for_publish, timeout_ms=timeout_ms)\n\n    def __request_external(\n            self, topic: str, payload: str,\n            on_reply: Callable[[str, Any], None],\n            on_reply_ctx: Any = None, timeout_ms: int = 10000\n    ) -> bool:\n        if topic is None or payload is None or on_reply is None:\n            raise MIoTMipsError('invalid params')\n        self._internal_loop.call_soon_threadsafe(\n            self.__request, topic, payload, on_reply, on_reply_ctx, timeout_ms)\n        return True\n\n    def __reg_broadcast_external(\n        self, topic: str, handler: Callable[[str, str, Any], None],\n        handler_ctx: Any\n    ) -> bool:\n        self._internal_loop.call_soon_threadsafe(\n            self.__reg_broadcast,\n            topic, handler, handler_ctx)\n        return True\n\n    def __unreg_broadcast_external(self, topic) -> bool:\n        self._internal_loop.call_soon_threadsafe(\n            self.__unreg_broadcast, topic)\n        return True\n\n    @final\n    async def __request_async(\n        self, topic: str, payload: str, timeout_ms: int = 10000\n    ) -> dict:\n        fut_handler: asyncio.Future = self.main_loop.create_future()\n\n        def on_msg_reply(payload: str, ctx: Any):\n            fut: asyncio.Future = ctx\n            if fut:\n                self.main_loop.call_soon_threadsafe(fut.set_result, payload)\n        if not self.__request_external(\n                topic=topic,\n                payload=payload,\n                on_reply=on_msg_reply,\n                on_reply_ctx=fut_handler,\n                timeout_ms=timeout_ms):\n            # Request error\n            fut_handler.set_result('internal request error')\n\n        result = await fut_handler\n        try:\n            return json.loads(result)\n        except json.JSONDecodeError:\n            return {\n                'code': MIoTErrorCode.CODE_MIPS_INVALID_RESULT.value,\n                'message': f'Error: {result}'}\n\n    async def __get_prop_timer_handle(self) -> None:\n        for did in list(self._get_prop_queue.keys()):\n            item = self._get_prop_queue[did].pop()\n            _LOGGER.debug('get prop, %s, %s', did, item)\n            result_obj = await self.__request_async(\n                topic='proxy/get',\n                payload=item['param'],\n                timeout_ms=item['timeout_ms'])\n            if result_obj is None or 'value' not in result_obj:\n                item['fut'].set_result(None)\n            else:\n                item['fut'].set_result(result_obj['value'])\n\n            if not self._get_prop_queue[did]:\n                self._get_prop_queue.pop(did, None)\n\n        if self._get_prop_queue:\n            self._get_prop_timer = self.main_loop.call_later(\n                0.1, lambda: self.main_loop.create_task(\n                    self.__get_prop_timer_handle()))\n        else:\n            self._get_prop_timer = None\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_network.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT network utilities.\n\"\"\"\nimport asyncio\nimport logging\nimport platform\nimport socket\nfrom dataclasses import dataclass\nfrom enum import Enum, auto\nimport subprocess\nfrom typing import Callable, Coroutine, Optional\nimport aiohttp\nimport psutil\nimport ipaddress\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass InterfaceStatus(Enum):\n    \"\"\"Interface status.\"\"\"\n    ADD = 0\n    UPDATE = auto()\n    REMOVE = auto()\n\n\n@dataclass\nclass NetworkInfo:\n    \"\"\"Network information.\"\"\"\n    name: str\n    ip: str\n    netmask: str\n    net_seg: str\n\n\nclass MIoTNetwork:\n    \"\"\"MIoT network utilities.\"\"\"\n    _IP_ADDRESS_LIST: list[str] = [\n        '1.2.4.8',          # CNNIC sDNS\n        '8.8.8.8',          # Google Public DNS\n        '9.9.9.9'           # Quad9\n    ]\n    _URL_ADDRESS_LIST: list[str] = [\n        'https://www.bing.com',\n        'https://www.google.com',\n        'https://www.baidu.com'\n    ]\n    _REFRESH_INTERVAL = 30\n    _DETECT_TIMEOUT = 6\n\n    _main_loop: asyncio.AbstractEventLoop\n\n    _ip_addr_map: dict[str, float]\n    _http_addr_map: dict[str, float]\n    _http_session: aiohttp.ClientSession\n\n    _refresh_interval: int\n    _refresh_task: Optional[asyncio.Task]\n    _refresh_timer: Optional[asyncio.TimerHandle]\n\n    _network_status: bool\n    _network_info: dict[str, NetworkInfo]\n\n    _sub_list_network_status: dict[str, Callable[[bool], Coroutine]]\n    _sub_list_network_info: dict[str, Callable[[\n        InterfaceStatus, NetworkInfo], Coroutine]]\n    _done_event: asyncio.Event\n\n    def __init__(\n        self,\n        ip_addr_list: Optional[list[str]] = None,\n        url_addr_list: Optional[list[str]] = None,\n        refresh_interval: Optional[int] = None,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._main_loop = loop or asyncio.get_running_loop()\n        self._ip_addr_map = {\n            ip: self._DETECT_TIMEOUT for ip in\n            ip_addr_list or self._IP_ADDRESS_LIST}\n        self._http_addr_map = {\n            url: self._DETECT_TIMEOUT for url in\n            url_addr_list or self._URL_ADDRESS_LIST}\n        self._http_session = aiohttp.ClientSession()\n        self._refresh_interval = refresh_interval or self._REFRESH_INTERVAL\n\n        self._refresh_task = None\n        self._refresh_timer = None\n\n        self._network_status = False\n        self._network_info = {}\n\n        self._sub_list_network_status = {}\n        self._sub_list_network_info = {}\n\n        self._done_event = asyncio.Event()\n\n    async def init_async(self) -> bool:\n        self.__refresh_timer_handler()\n        # MUST get network info before starting\n        return await self._done_event.wait()\n\n    async def deinit_async(self) -> None:\n        if self._refresh_task:\n            self._refresh_task.cancel()\n            self._refresh_task = None\n        if self._refresh_timer:\n            self._refresh_timer.cancel()\n            self._refresh_timer = None\n        await self._http_session.close()\n\n        self._network_status = False\n        self._network_info.clear()\n        self._sub_list_network_status.clear()\n        self._sub_list_network_info.clear()\n        self._done_event.clear()\n\n    @property\n    def network_status(self) -> bool:\n        return self._network_status\n\n    @property\n    def network_info(self) -> dict[str, NetworkInfo]:\n        return self._network_info\n\n    async def update_addr_list_async(\n        self,\n        ip_addr_list: Optional[list[str]] = None,\n        url_addr_list: Optional[list[str]] = None,\n    ) -> None:\n        new_ip_map: dict = {}\n        for ip in ip_addr_list or self._IP_ADDRESS_LIST:\n            if ip in self._ip_addr_map:\n                new_ip_map[ip] = self._ip_addr_map[ip]\n            else:\n                new_ip_map[ip] = self._DETECT_TIMEOUT\n        self._ip_addr_map = new_ip_map\n        new_url_map: dict = {}\n        for url in url_addr_list or self._URL_ADDRESS_LIST:\n            if url in self._http_addr_map:\n                new_url_map[url] = self._http_addr_map[url]\n            else:\n                new_url_map[url] = self._DETECT_TIMEOUT\n        self._http_addr_map = new_url_map\n\n    def sub_network_status(\n        self, key: str, handler: Callable[[bool], Coroutine]\n    ) -> None:\n        self._sub_list_network_status[key] = handler\n\n    def unsub_network_status(self, key: str) -> None:\n        self._sub_list_network_status.pop(key, None)\n\n    def sub_network_info(\n        self, key: str,\n        handler: Callable[[InterfaceStatus, NetworkInfo], Coroutine]\n    ) -> None:\n        self._sub_list_network_info[key] = handler\n\n    def unsub_network_info(self, key: str) -> None:\n        self._sub_list_network_info.pop(key, None)\n\n    async def refresh_async(self) -> None:\n        self.__refresh_timer_handler()\n\n    async def get_network_status_async(self) -> bool:\n        try:\n            ip_addr: str = ''\n            ip_ts: float = self._DETECT_TIMEOUT\n            for ip, ts in self._ip_addr_map.items():\n                if ts < ip_ts:\n                    ip_addr = ip\n                    ip_ts = ts\n            if (\n                ip_ts < self._DETECT_TIMEOUT\n                and await self.ping_multi_async(ip_list=[ip_addr])\n            ):\n                return True\n            url_addr: str = ''\n            url_ts: float = self._DETECT_TIMEOUT\n            for http, ts in self._http_addr_map.items():\n                if ts < url_ts:\n                    url_addr = http\n                    url_ts = ts\n            if (\n                url_ts < self._DETECT_TIMEOUT\n                and await self.http_multi_async(url_list=[url_addr])\n            ):\n                return True\n            # Detect all addresses\n            results = await asyncio.gather(\n                *[self.ping_multi_async(), self.http_multi_async()])\n            return any(results)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('get network status error, %s', err)\n        return False\n\n    async def get_network_info_async(self) -> dict[str, NetworkInfo]:\n        return await self._main_loop.run_in_executor(\n            None, self.__get_network_info)\n\n    async def ping_multi_async(\n        self, ip_list: Optional[list[str]] = None\n    ) -> bool:\n        addr_list = ip_list or list(self._ip_addr_map.keys())\n        tasks = []\n        for addr in addr_list:\n            tasks.append(self.__ping_async(addr))\n        results = await asyncio.gather(*tasks)\n        for addr, ts in zip(addr_list, results):\n            if addr in self._ip_addr_map:\n                self._ip_addr_map[addr] = ts\n        return any([ts < self._DETECT_TIMEOUT for ts in results])\n\n    async def http_multi_async(\n        self, url_list: Optional[list[str]] = None\n    ) -> bool:\n        addr_list = url_list or list(self._http_addr_map.keys())\n        tasks = []\n        for addr in addr_list:\n            tasks.append(self.__http_async(url=addr))\n        results = await asyncio.gather(*tasks)\n        for addr, ts in zip(addr_list, results):\n            if addr in self._http_addr_map:\n                self._http_addr_map[addr] = ts\n        return any([ts < self._DETECT_TIMEOUT for ts in results])\n\n    def __calc_network_address(self, ip: str, netmask: str) -> str:\n        return str(ipaddress.IPv4Network(\n            f'{ip}/{netmask}', strict=False).network_address)\n\n    async def __ping_async(self, address: Optional[str] = None) -> float:\n        start_ts: float = self._main_loop.time()\n        try:\n            process = await asyncio.create_subprocess_exec(\n                *(\n                    [\n                        'ping', '-n', '1', '-w',\n                        str(self._DETECT_TIMEOUT*1000), address]\n                    if platform.system().lower() == 'windows' else\n                    [\n                        'ping', '-c', '1', '-w',\n                        str(self._DETECT_TIMEOUT), address]),\n                stdout=subprocess.DEVNULL,\n                stderr=subprocess.DEVNULL\n            )\n            await process.communicate()\n            if process.returncode == 0:\n                return self._main_loop.time() - start_ts\n            return self._DETECT_TIMEOUT\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.debug('ping error, %s',err)\n            return self._DETECT_TIMEOUT\n\n    async def __http_async(self, url: str) -> float:\n        start_ts: float = self._main_loop.time()\n        try:\n            async with self._http_session.get(\n                    url, timeout=self._DETECT_TIMEOUT):\n                return self._main_loop.time() - start_ts\n        except Exception:  # pylint: disable=broad-exception-caught\n            pass\n        return self._DETECT_TIMEOUT\n\n    def __get_network_info(self) -> dict[str, NetworkInfo]:\n        interfaces = psutil.net_if_addrs()\n        results: dict[str, NetworkInfo] = {}\n        for name, addresses in interfaces.items():\n            # Skip hassio and docker* interface\n            if name == 'hassio' or name.startswith('docker'):\n                continue\n            for address in addresses:\n                if (\n                    address.family != socket.AF_INET\n                    or not address.address\n                    or not address.netmask\n                ):\n                    continue\n                # skip lo interface\n                if address.address == '127.0.0.1':\n                    continue\n                results[name] = NetworkInfo(\n                    name=name,\n                    ip=address.address,\n                    netmask=address.netmask,\n                    net_seg=self.__calc_network_address(\n                        address.address, address.netmask))\n        return results\n\n    def __call_network_info_change(\n        self, status: InterfaceStatus, info: NetworkInfo\n    ) -> None:\n        for handler in self._sub_list_network_info.values():\n            self._main_loop.create_task(handler(status, info))\n\n    async def __update_status_and_info_async(self) -> None:\n        try:\n            status: bool = await self.get_network_status_async()\n            infos = await self.get_network_info_async()\n\n            if self._network_status != status:\n                for handler in self._sub_list_network_status.values():\n                    self._main_loop.create_task(handler(status))\n                self._network_status = status\n\n            for name in list(self._network_info.keys()):\n                info = infos.pop(name, None)\n                if info:\n                    # Update\n                    if (\n                        info.ip != self._network_info[name].ip\n                        or info.netmask != self._network_info[name].netmask\n                    ):\n                        self._network_info[name] = info\n                        self.__call_network_info_change(\n                            InterfaceStatus.UPDATE, info)\n                else:\n                    # Remove\n                    self.__call_network_info_change(\n                        InterfaceStatus.REMOVE,\n                        self._network_info.pop(name))\n            # Add\n            for name, info in infos.items():\n                self._network_info[name] = info\n                self.__call_network_info_change(InterfaceStatus.ADD, info)\n\n            if not self._done_event.is_set():\n                self._done_event.set()\n        except asyncio.CancelledError:\n            _LOGGER.error('update_status_and_info task was cancelled')\n\n    def __refresh_timer_handler(self) -> None:\n        if self._refresh_timer:\n            self._refresh_timer.cancel()\n            self._refresh_timer = None\n        if self._refresh_task is None or self._refresh_task.done():\n            self._refresh_task = self._main_loop.create_task(\n                self.__update_status_and_info_async())\n        self._refresh_timer = self._main_loop.call_later(\n            self._refresh_interval, self.__refresh_timer_handler)\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_spec.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT-Spec-V2 parser.\n\"\"\"\nimport asyncio\nimport os\nimport platform\nimport time\nfrom typing import Any, Optional, Type, Union\nimport logging\nfrom slugify import slugify\n\n# pylint: disable=relative-beyond-top-level\nfrom .const import DEFAULT_INTEGRATION_LANGUAGE, SPEC_STD_LIB_EFFECTIVE_TIME\nfrom .common import MIoTHttp, load_yaml_file, load_json_file\nfrom .miot_error import MIoTSpecError\nfrom .miot_storage import MIoTStorage\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass MIoTSpecValueRange:\n    \"\"\"MIoT SPEC value range class.\"\"\"\n    min_: int\n    max_: int\n    step: int | float\n\n    def __init__(self, value_range: Union[dict, list]) -> None:\n        if isinstance(value_range, dict):\n            self.load(value_range)\n        elif isinstance(value_range, list):\n            self.from_spec(value_range)\n        else:\n            raise MIoTSpecError('invalid value range format')\n\n    def load(self, value_range: dict) -> None:\n        if ('min' not in value_range or 'max' not in value_range or\n                'step' not in value_range):\n            raise MIoTSpecError('invalid value range')\n        self.min_ = value_range['min']\n        self.max_ = value_range['max']\n        self.step = value_range['step']\n\n    def from_spec(self, value_range: list) -> None:\n        if len(value_range) != 3:\n            raise MIoTSpecError('invalid value range')\n        self.min_ = value_range[0]\n        self.max_ = value_range[1]\n        self.step = value_range[2]\n\n    def dump(self) -> dict:\n        return {'min': self.min_, 'max': self.max_, 'step': self.step}\n\n    def __str__(self) -> str:\n        return f'[{self.min_}, {self.max_}, {self.step}'\n\n\nclass MIoTSpecValueListItem:\n    \"\"\"MIoT SPEC value list item class.\"\"\"\n    # NOTICE: bool type without name\n    name: str\n    # Value\n    value: Any\n    # Descriptions after multilingual conversion.\n    description: str\n\n    def __init__(self, item: dict) -> None:\n        self.load(item)\n\n    def load(self, item: dict) -> None:\n        if 'value' not in item or 'description' not in item:\n            raise MIoTSpecError('invalid value list item, %s')\n\n        self.name = item.get('name', None)\n        self.value = item['value']\n        self.description = item['description']\n\n    @staticmethod\n    def from_spec(item: dict) -> 'MIoTSpecValueListItem':\n        if ('name' not in item or 'value' not in item or\n                'description' not in item):\n            raise MIoTSpecError('invalid value list item, %s')\n        # Slugify name and convert to lower-case.\n        cache = {\n            'name': slugify(text=item['name'], separator='_').lower(),\n            'value': item['value'],\n            'description': item['description']\n        }\n        return MIoTSpecValueListItem(cache)\n\n    def dump(self) -> dict:\n        return {\n            'name': self.name,\n            'value': self.value,\n            'description': self.description\n        }\n\n    def __str__(self) -> str:\n        return f'{self.name}: {self.value} - {self.description}'\n\n\nclass MIoTSpecValueList:\n    \"\"\"MIoT SPEC value list class.\"\"\"\n    # pylint: disable=inconsistent-quotes\n    items: list[MIoTSpecValueListItem]\n\n    def __init__(self, value_list: list[dict]) -> None:\n        if not isinstance(value_list, list):\n            raise MIoTSpecError('invalid value list format')\n        self.items = []\n        self.load(value_list)\n\n    @property\n    def names(self) -> list[str]:\n        return [item.name for item in self.items]\n\n    @property\n    def values(self) -> list[Any]:\n        return [item.value for item in self.items]\n\n    @property\n    def descriptions(self) -> list[str]:\n        return [item.description for item in self.items]\n\n    @staticmethod\n    def from_spec(value_list: list[dict]) -> 'MIoTSpecValueList':\n        result = MIoTSpecValueList([])\n        dup_desc: dict[str, int] = {}\n        for item in value_list:\n            # Handle duplicate descriptions.\n            count = 0\n            if item['description'] in dup_desc:\n                count = dup_desc[item['description']]\n            count += 1\n            dup_desc[item['description']] = count\n            if count > 1:\n                item['name'] = f'{item[\"name\"]}_{count}'\n                item['description'] = f'{item[\"description\"]}_{count}'\n\n            result.items.append(MIoTSpecValueListItem.from_spec(item))\n        return result\n\n    def load(self, value_list: list[dict]) -> None:\n        for item in value_list:\n            self.items.append(MIoTSpecValueListItem(item))\n\n    def to_map(self) -> dict:\n        return {item.value: item.description for item in self.items}\n\n    def get_value_by_description(self, description: str) -> Any:\n        for item in self.items:\n            if item.description == description:\n                return item.value\n        return None\n\n    def get_description_by_value(self, value: Any) -> Optional[str]:\n        for item in self.items:\n            if item.value == value:\n                return item.description\n        return None\n\n    def dump(self) -> list:\n        return [item.dump() for item in self.items]\n\n\nclass _SpecStdLib:\n    \"\"\"MIoT-Spec-V2 standard library.\"\"\"\n    # pylint: disable=inconsistent-quotes\n    _lang: str\n    _devices: dict[str, dict[str, str]]\n    _services: dict[str, dict[str, str]]\n    _properties: dict[str, dict[str, str]]\n    _events: dict[str, dict[str, str]]\n    _actions: dict[str, dict[str, str]]\n    _values: dict[str, dict[str, str]]\n\n    def __init__(self, lang: str) -> None:\n        self._lang = lang\n        self._devices = {}\n        self._services = {}\n        self._properties = {}\n        self._events = {}\n        self._actions = {}\n        self._values = {}\n\n        self._spec_std_lib = None\n\n    def load(self, std_lib: dict[str, dict[str, dict[str, str]]]) -> None:\n        if (not isinstance(std_lib, dict) or 'devices' not in std_lib or\n                'services' not in std_lib or 'properties' not in std_lib or\n                'events' not in std_lib or 'actions' not in std_lib or\n                'values' not in std_lib):\n            return\n        self._devices = std_lib['devices']\n        self._services = std_lib['services']\n        self._properties = std_lib['properties']\n        self._events = std_lib['events']\n        self._actions = std_lib['actions']\n        self._values = std_lib['values']\n\n    def device_translate(self, key: str) -> Optional[str]:\n        if not self._devices or key not in self._devices:\n            return None\n        if self._lang not in self._devices[key]:\n            return self._devices[key].get(DEFAULT_INTEGRATION_LANGUAGE, None)\n        return self._devices[key][self._lang]\n\n    def service_translate(self, key: str) -> Optional[str]:\n        if not self._services or key not in self._services:\n            return None\n        if self._lang not in self._services[key]:\n            return self._services[key].get(DEFAULT_INTEGRATION_LANGUAGE, None)\n        return self._services[key][self._lang]\n\n    def property_translate(self, key: str) -> Optional[str]:\n        if not self._properties or key not in self._properties:\n            return None\n        if self._lang not in self._properties[key]:\n            return self._properties[key].get(DEFAULT_INTEGRATION_LANGUAGE, None)\n        return self._properties[key][self._lang]\n\n    def event_translate(self, key: str) -> Optional[str]:\n        if not self._events or key not in self._events:\n            return None\n        if self._lang not in self._events[key]:\n            return self._events[key].get(DEFAULT_INTEGRATION_LANGUAGE, None)\n        return self._events[key][self._lang]\n\n    def action_translate(self, key: str) -> Optional[str]:\n        if not self._actions or key not in self._actions:\n            return None\n        if self._lang not in self._actions[key]:\n            return self._actions[key].get(DEFAULT_INTEGRATION_LANGUAGE, None)\n        return self._actions[key][self._lang]\n\n    def value_translate(self, key: str) -> Optional[str]:\n        if not self._values or key not in self._values:\n            return None\n        if self._lang not in self._values[key]:\n            return self._values[key].get(DEFAULT_INTEGRATION_LANGUAGE, None)\n        return self._values[key][self._lang]\n\n    def dump(self) -> dict[str, dict[str, dict[str, str]]]:\n        return {\n            'devices': self._devices,\n            'services': self._services,\n            'properties': self._properties,\n            'events': self._events,\n            'actions': self._actions,\n            'values': self._values\n        }\n\n    async def refresh_async(self) -> bool:\n        std_lib_new = await self.__request_from_cloud_async()\n        if std_lib_new:\n            self.load(std_lib_new)\n            return True\n        return False\n\n    async def __request_from_cloud_async(self) -> Optional[dict]:\n        std_libs: Optional[dict] = None\n        for index in range(3):\n            try:\n                tasks: list = []\n                # Get std lib\n                for name in [\n                        'device', 'service', 'property', 'event', 'action'\n                ]:\n                    tasks.append(\n                        self.__get_template_list(\n                            'https://miot-spec.org/miot-spec-v2/template/list/'\n                            + name))\n                tasks.append(self.__get_property_value())\n                # Async request\n                results = await asyncio.gather(*tasks)\n                if None in results:\n                    raise MIoTSpecError('init failed, None in result')\n                std_libs = {\n                    'devices': results[0],\n                    'services': results[1],\n                    'properties': results[2],\n                    'events': results[3],\n                    'actions': results[4],\n                    'values': results[5],\n                }\n                # Get external std lib, Power by LM\n                tasks.clear()\n                for name in [\n                        'device', 'service', 'property', 'event', 'action',\n                        'property_value'\n                ]:\n                    tasks.append(\n                        MIoTHttp.get_json_async(\n                            'https://cdn.cnbj1.fds.api.mi-img.com/res-conf/'\n                            f'xiaomi-home/std_ex_{name}.json'))\n                results = await asyncio.gather(*tasks)\n                if results[0]:\n                    for key, value in results[0].items():\n                        if key in std_libs['devices']:\n                            std_libs['devices'][key].update(value)\n                        else:\n                            std_libs['devices'][key] = value\n                else:\n                    _LOGGER.error('get external std lib failed, devices')\n                if results[1]:\n                    for key, value in results[1].items():\n                        if key in std_libs['services']:\n                            std_libs['services'][key].update(value)\n                        else:\n                            std_libs['services'][key] = value\n                else:\n                    _LOGGER.error('get external std lib failed, services')\n                if results[2]:\n                    for key, value in results[2].items():\n                        if key in std_libs['properties']:\n                            std_libs['properties'][key].update(value)\n                        else:\n                            std_libs['properties'][key] = value\n                else:\n                    _LOGGER.error('get external std lib failed, properties')\n                if results[3]:\n                    for key, value in results[3].items():\n                        if key in std_libs['events']:\n                            std_libs['events'][key].update(value)\n                        else:\n                            std_libs['events'][key] = value\n                else:\n                    _LOGGER.error('get external std lib failed, events')\n                if results[4]:\n                    for key, value in results[4].items():\n                        if key in std_libs['actions']:\n                            std_libs['actions'][key].update(value)\n                        else:\n                            std_libs['actions'][key] = value\n                else:\n                    _LOGGER.error('get external std lib failed, actions')\n                if results[5]:\n                    for key, value in results[5].items():\n                        if key in std_libs['values']:\n                            std_libs['values'][key].update(value)\n                        else:\n                            std_libs['values'][key] = value\n                else:\n                    _LOGGER.error('get external std lib failed, values')\n                return std_libs\n            except Exception as err:  # pylint: disable=broad-exception-caught\n                _LOGGER.error('update spec std lib error, retry, %d, %s', index,\n                              err)\n        return None\n\n    async def __get_property_value(self) -> dict:\n        reply = await MIoTHttp.get_json_async(\n            url='https://miot-spec.org/miot-spec-v2'\n            '/normalization/list/property_value')\n        if reply is None or 'result' not in reply:\n            raise MIoTSpecError('get property value failed')\n        result = {}\n        for item in reply['result']:\n            if (not isinstance(item, dict) or 'normalization' not in item or\n                    'description' not in item or 'proName' not in item or\n                    'urn' not in item):\n                continue\n            result[\n                f'{item[\"urn\"]}|{item[\"proName\"]}|{item[\"normalization\"]}'] = {\n                    'zh-Hans': item['description'],\n                    'en': item['normalization']\n                }\n        return result\n\n    async def __get_template_list(self, url: str) -> dict:\n        reply = await MIoTHttp.get_json_async(url=url)\n        if reply is None or 'result' not in reply:\n            raise MIoTSpecError(f'get service failed, {url}')\n        result: dict = {}\n        for item in reply['result']:\n            if (not isinstance(item, dict) or 'type' not in item or\n                    'description' not in item):\n                continue\n            if 'zh_cn' in item['description']:\n                item['description']['zh-Hans'] = item['description'].pop(\n                    'zh_cn')\n            if 'zh_hk' in item['description']:\n                item['description']['zh-Hant'] = item['description'].pop(\n                    'zh_hk')\n                item['description'].pop('zh_tw', None)\n            elif 'zh_tw' in item['description']:\n                item['description']['zh-Hant'] = item['description'].pop(\n                    'zh_tw')\n            result[item['type']] = item['description']\n        return result\n\n\nclass _MIoTSpecBase:\n    \"\"\"MIoT SPEC base class.\"\"\"\n    iid: int\n    type_: str\n    description: str\n    description_trans: Optional[str]\n    proprietary: bool\n    need_filter: bool\n    name: str\n    icon: Optional[str]\n\n    # External params\n    platform: Optional[str]\n    device_class: Any\n    state_class: Any\n    external_unit: Any\n    entity_category: Optional[str]\n\n    spec_id: int\n\n    def __init__(self, spec: dict) -> None:\n        self.iid = spec['iid']\n        self.type_ = spec['type']\n        self.description = spec['description']\n\n        self.description_trans = spec.get('description_trans', None)\n        self.proprietary = spec.get('proprietary', False)\n        self.need_filter = spec.get('need_filter', False)\n        self.name = spec.get('name', 'xiaomi')\n        self.icon = spec.get('icon', None)\n\n        self.platform = None\n        self.device_class = None\n        self.state_class = None\n        self.external_unit = None\n        self.entity_category = None\n\n        self.spec_id = hash(f'{self.type_}.{self.iid}')\n\n    def __hash__(self) -> int:\n        return self.spec_id\n\n    def __eq__(self, value) -> bool:\n        return self.spec_id == value.spec_id\n\n\nclass MIoTSpecProperty(_MIoTSpecBase):\n    \"\"\"MIoT SPEC property class.\"\"\"\n    unit: Optional[str]\n    precision: int\n    expr: Optional[str]\n\n    _format_: Type\n    _value_range: Optional[MIoTSpecValueRange]\n    _value_list: Optional[MIoTSpecValueList]\n\n    _access: list\n    _writable: bool\n    _readable: bool\n    _notifiable: bool\n\n    service: 'MIoTSpecService'\n\n    def __init__(self,\n                 spec: dict,\n                 service: 'MIoTSpecService',\n                 format_: str,\n                 access: list,\n                 unit: Optional[str] = None,\n                 value_range: Optional[dict] = None,\n                 value_list: Optional[list[dict]] = None,\n                 precision: Optional[int] = None,\n                 expr: Optional[str] = None) -> None:\n        super().__init__(spec=spec)\n        self.service = service\n        self.format_ = format_\n        self.access = access\n        self.unit = unit\n        self.value_range = value_range\n        self.value_list = value_list\n        self.precision = precision if precision is not None else 1\n        self.expr = expr\n\n        self.spec_id = hash(f'p.{self.name}.{self.service.iid}.{self.iid}')\n\n    @property\n    def format_(self) -> Type:\n        return self._format_\n\n    @format_.setter\n    def format_(self, value: str) -> None:\n        self._format_ = {\n            'string': str,\n            'str': str,\n            'bool': bool,\n            'float': float\n        }.get(value, int)\n\n    @property\n    def access(self) -> list:\n        return self._access\n\n    @access.setter\n    def access(self, value: list) -> None:\n        self._access = value\n        if isinstance(value, list):\n            self._writable = 'write' in value\n            self._readable = 'read' in value\n            self._notifiable = 'notify' in value\n\n    @property\n    def writable(self) -> bool:\n        return self._writable\n\n    @property\n    def readable(self) -> bool:\n        return self._readable\n\n    @property\n    def notifiable(self):\n        return self._notifiable\n\n    @property\n    def value_range(self) -> Optional[MIoTSpecValueRange]:\n        return self._value_range\n\n    @value_range.setter\n    def value_range(self, value: Union[dict, list, None]) -> None:\n        \"\"\"Set value-range, precision.\"\"\"\n        if not value:\n            self._value_range = None\n            return\n        self._value_range = MIoTSpecValueRange(value_range=value)\n        if isinstance(value, list):\n            step_: str = format(value[2], '.10f').rstrip('0').rstrip('.')\n            self.precision = len(step_.split('.')[1]) if '.' in step_ else 0\n\n    @property\n    def value_list(self) -> Optional[MIoTSpecValueList]:\n        return self._value_list\n\n    @value_list.setter\n    def value_list(self, value: Union[list[dict], MIoTSpecValueList,\n                                      None]) -> None:\n        if not value:\n            self._value_list = None\n            return\n        if isinstance(value, list):\n            self._value_list = MIoTSpecValueList(value_list=value)\n        elif isinstance(value, MIoTSpecValueList):\n            self._value_list = value\n\n    def eval_expr(self, src_value: Any) -> Any:\n        if not self.expr:\n            return src_value\n        try:\n            # pylint: disable=eval-used\n            return eval(self.expr, {'src_value': src_value})\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('eval expression error, %s, %s, %s, %s', self.iid,\n                          src_value, self.expr, err)\n            return src_value\n\n    def value_format(self, value: Any) -> Any:\n        if value is None:\n            return None\n        if isinstance(value, str):\n            if self.format_ == int:\n                value = int(float(value))\n            elif self.format_ == float:\n                value = float(value)\n        if self.format_ == bool:\n            return bool(value in [True, 1, 'True', 'true', '1'])\n        return value\n\n    def value_precision(self, value: Any) -> Any:\n        if value is None:\n            return None\n        if self.format_ == float:\n            return round(value, self.precision)\n        if self.format_ == int:\n            if self.value_range is None:\n                return int(round(value))\n            return int(\n                round(value / self.value_range.step) * self.value_range.step)\n        return value\n\n    def dump(self) -> dict:\n        return {\n            'type': self.type_,\n            'name': self.name,\n            'iid': self.iid,\n            'description': self.description,\n            'description_trans': self.description_trans,\n            'proprietary': self.proprietary,\n            'need_filter': self.need_filter,\n            'format': self.format_.__name__,\n            'access': self._access,\n            'unit': self.unit,\n            'value_range':\n                (self._value_range.dump() if self._value_range else None),\n            'value_list': self._value_list.dump() if self._value_list else None,\n            'precision': self.precision,\n            'expr': self.expr,\n            'icon': self.icon\n        }\n\n\nclass MIoTSpecEvent(_MIoTSpecBase):\n    \"\"\"MIoT SPEC event class.\"\"\"\n    argument: list[MIoTSpecProperty]\n    service: 'MIoTSpecService'\n\n    def __init__(self,\n                 spec: dict,\n                 service: 'MIoTSpecService',\n                 argument: Optional[list[MIoTSpecProperty]] = None) -> None:\n        super().__init__(spec=spec)\n        self.argument = argument or []\n        self.service = service\n\n        self.spec_id = hash(f'e.{self.name}.{self.service.iid}.{self.iid}')\n\n    def dump(self) -> dict:\n        return {\n            'type': self.type_,\n            'name': self.name,\n            'iid': self.iid,\n            'description': self.description,\n            'description_trans': self.description_trans,\n            'proprietary': self.proprietary,\n            'argument': [prop.iid for prop in self.argument],\n            'need_filter': self.need_filter\n        }\n\n\nclass MIoTSpecAction(_MIoTSpecBase):\n    \"\"\"MIoT SPEC action class.\"\"\"\n    in_: list[MIoTSpecProperty]\n    out: list[MIoTSpecProperty]\n    service: 'MIoTSpecService'\n\n    def __init__(self,\n                 spec: dict,\n                 service: 'MIoTSpecService',\n                 in_: Optional[list[MIoTSpecProperty]] = None,\n                 out: Optional[list[MIoTSpecProperty]] = None) -> None:\n        super().__init__(spec=spec)\n        self.in_ = in_ or []\n        self.out = out or []\n        self.service = service\n\n        self.spec_id = hash(f'a.{self.name}.{self.service.iid}.{self.iid}')\n\n    def dump(self) -> dict:\n        return {\n            'type': self.type_,\n            'name': self.name,\n            'iid': self.iid,\n            'description': self.description,\n            'description_trans': self.description_trans,\n            'in': [prop.iid for prop in self.in_],\n            'out': [prop.iid for prop in self.out],\n            'proprietary': self.proprietary,\n            'need_filter': self.need_filter\n        }\n\n\nclass MIoTSpecService(_MIoTSpecBase):\n    \"\"\"MIoT SPEC service class.\"\"\"\n    properties: list[MIoTSpecProperty]\n    events: list[MIoTSpecEvent]\n    actions: list[MIoTSpecAction]\n\n    def __init__(self, spec: dict) -> None:\n        super().__init__(spec=spec)\n        self.properties = []\n        self.events = []\n        self.actions = []\n\n    def dump(self) -> dict:\n        return {\n            'type': self.type_,\n            'name': self.name,\n            'iid': self.iid,\n            'description': self.description,\n            'description_trans': self.description_trans,\n            'proprietary': self.proprietary,\n            'properties': [prop.dump() for prop in self.properties],\n            'events': [event.dump() for event in self.events],\n            'actions': [action.dump() for action in self.actions],\n            'need_filter': self.need_filter\n        }\n\n\nclass MIoTSpecInstance:\n    \"\"\"MIoT SPEC instance class.\"\"\"\n    urn: str\n    name: str\n    # urn_name: str\n    description: str\n    description_trans: str\n    services: list[MIoTSpecService]\n\n    # External params\n    platform: str\n    device_class: Any\n    icon: str\n\n    def __init__(self, urn: str, name: str, description: str,\n                 description_trans: str) -> None:\n        self.urn = urn\n        self.name = name\n        self.description = description\n        self.description_trans = description_trans\n        self.services = []\n\n    @staticmethod\n    def load(specs: dict) -> 'MIoTSpecInstance':\n        instance = MIoTSpecInstance(\n            urn=specs['urn'],\n            name=specs['name'],\n            description=specs['description'],\n            description_trans=specs['description_trans'])\n        for service in specs['services']:\n            spec_service = MIoTSpecService(spec=service)\n            for prop in service['properties']:\n                spec_prop = MIoTSpecProperty(spec=prop,\n                                             service=spec_service,\n                                             format_=prop['format'],\n                                             access=prop['access'],\n                                             unit=prop['unit'],\n                                             value_range=prop['value_range'],\n                                             value_list=prop['value_list'],\n                                             precision=prop.get(\n                                                 'precision', None),\n                                             expr=prop.get('expr', None))\n                spec_service.properties.append(spec_prop)\n            for event in service['events']:\n                spec_event = MIoTSpecEvent(spec=event, service=spec_service)\n                arg_list: list[MIoTSpecProperty] = []\n                for piid in event['argument']:\n                    for prop in spec_service.properties:\n                        if prop.iid == piid:\n                            arg_list.append(prop)\n                            break\n                spec_event.argument = arg_list\n                spec_service.events.append(spec_event)\n            for action in service['actions']:\n                spec_action = MIoTSpecAction(spec=action,\n                                             service=spec_service,\n                                             in_=action['in'])\n                in_list: list[MIoTSpecProperty] = []\n                for piid in action['in']:\n                    for prop in spec_service.properties:\n                        if prop.iid == piid:\n                            in_list.append(prop)\n                            break\n                spec_action.in_ = in_list\n                out_list: list[MIoTSpecProperty] = []\n                for piid in action['out']:\n                    for prop in spec_service.properties:\n                        if prop.iid == piid:\n                            out_list.append(prop)\n                            break\n                spec_action.out = out_list\n                spec_service.actions.append(spec_action)\n            instance.services.append(spec_service)\n        return instance\n\n    def dump(self) -> dict:\n        return {\n            'urn': self.urn,\n            'name': self.name,\n            'description': self.description,\n            'description_trans': self.description_trans,\n            'services': [service.dump() for service in self.services]\n        }\n\n\nclass _MIoTSpecMultiLang:\n    \"\"\"MIoT SPEC multi lang class.\"\"\"\n    # pylint: disable=broad-exception-caught\n    _DOMAIN: str = 'miot_specs_multi_lang'\n    _MULTI_LANG_FILE = 'specs/multi_lang.json'\n    _lang: str\n    _storage: MIoTStorage\n    _main_loop: asyncio.AbstractEventLoop\n\n    _custom_cache: dict[str, dict]\n    _current_data: Optional[dict[str, str]]\n\n    def __init__(self,\n                 lang: Optional[str],\n                 storage: MIoTStorage,\n                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:\n        self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE\n        self._storage = storage\n        self._main_loop = loop or asyncio.get_running_loop()\n\n        self._custom_cache = {}\n        self._current_data = None\n\n    async def set_spec_async(self, urn: str) -> None:\n        if urn in self._custom_cache:\n            self._current_data = self._custom_cache[urn]\n            return\n\n        trans_cache: dict[str, str] = {}\n        trans_cloud: dict = {}\n        trans_local: dict = {}\n        # Get multi lang from cloud\n        try:\n            trans_cloud = await self.__get_multi_lang_async(urn)\n            if self._lang == 'zh-Hans':\n                # Simplified Chinese\n                trans_cache = trans_cloud.get('zh_cn', {})\n            elif self._lang == 'zh-Hant':\n                # Traditional Chinese, zh_hk or zh_tw\n                trans_cache = trans_cloud.get('zh_hk', {})\n                if not trans_cache:\n                    trans_cache = trans_cloud.get('zh_tw', {})\n            else:\n                trans_cache = trans_cloud.get(self._lang, {})\n        except Exception as err:\n            trans_cloud = {}\n            _LOGGER.info('get multi lang from cloud failed, %s, %s', urn, err)\n        # Get multi lang from local\n        try:\n            trans_local = await self._storage.load_async(domain=self._DOMAIN,\n                                                         name=urn,\n                                                         type_=dict\n                                                        )  # type: ignore\n            if (isinstance(trans_local, dict) and self._lang in trans_local):\n                trans_cache.update(trans_local[self._lang])\n        except Exception as err:\n            trans_local = {}\n            _LOGGER.info('get multi lang from local failed, %s, %s', urn, err)\n        # Revert: load multi_lang.json\n        try:\n            trans_local_json = await self._main_loop.run_in_executor(\n                None, load_json_file,\n                os.path.join(os.path.dirname(os.path.abspath(__file__)),\n                             self._MULTI_LANG_FILE))\n            urn_strs: list[str] = urn.split(':')\n            urn_key: str = ':'.join(urn_strs[:6])\n            if (isinstance(trans_local_json, dict) and\n                    urn_key in trans_local_json and\n                    self._lang in trans_local_json[urn_key]):\n                trans_cache.update(trans_local_json[urn_key][self._lang])\n                trans_local = trans_local_json[urn_key]\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('multi lang, load json file error, %s', err)\n        # Revert end\n        # Default language\n        if not trans_cache:\n            if trans_cloud and DEFAULT_INTEGRATION_LANGUAGE in trans_cloud:\n                trans_cache = trans_cloud[DEFAULT_INTEGRATION_LANGUAGE]\n            if trans_local and DEFAULT_INTEGRATION_LANGUAGE in trans_local:\n                trans_cache.update(trans_local[DEFAULT_INTEGRATION_LANGUAGE])\n        trans_data: dict[str, str] = {}\n        for tag, value in trans_cache.items():\n            if value is None or value.strip() == '':\n                continue\n            # The dict key is like:\n            # 'service:002:property:001:valuelist:000' or\n            # 'service:002:property:001' or 'service:002'\n            strs: list = tag.split(':')\n            strs_len = len(strs)\n            if strs_len == 2:\n                trans_data[f's:{int(strs[1])}'] = value\n            elif strs_len == 4:\n                type_ = 'p' if strs[2] == 'property' else (\n                    'a' if strs[2] == 'action' else 'e')\n                trans_data[f'{type_}:{int(strs[1])}:{int(strs[3])}'] = value\n            elif strs_len == 6:\n                trans_data[\n                    f'v:{int(strs[1])}:{int(strs[3])}:{int(strs[5])}'] = value\n\n        self._custom_cache[urn] = trans_data\n        self._current_data = trans_data\n\n    def translate(self, key: str) -> Optional[str]:\n        if not self._current_data:\n            return None\n        return self._current_data.get(key, None)\n\n    async def __get_multi_lang_async(self, urn: str) -> dict:\n        res_trans = await MIoTHttp.get_json_async(\n            url='https://miot-spec.org/instance/v2/multiLanguage',\n            params={'urn': urn})\n        if (not isinstance(res_trans, dict) or 'data' not in res_trans or\n                not isinstance(res_trans['data'], dict)):\n            raise MIoTSpecError('invalid translation data')\n        return res_trans['data']\n\n\nclass _SpecBoolTranslation:\n    \"\"\"\n    Boolean value translation.\n    \"\"\"\n    _BOOL_TRANS_FILE = 'specs/bool_trans.yaml'\n    _main_loop: asyncio.AbstractEventLoop\n    _lang: str\n    _data: Optional[dict[str, list]]\n    _data_default: Optional[list[dict]]\n\n    def __init__(self,\n                 lang: str,\n                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:\n        self._main_loop = loop or asyncio.get_event_loop()\n        self._lang = lang\n        self._data = None\n        self._data_default = None\n\n    async def init_async(self) -> None:\n        if isinstance(self._data, dict):\n            return\n        data = None\n        self._data = {}\n        try:\n            data = await self._main_loop.run_in_executor(\n                None, load_yaml_file,\n                os.path.join(os.path.dirname(os.path.abspath(__file__)),\n                             self._BOOL_TRANS_FILE))\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('bool trans, load file error, %s', err)\n            return\n        # Check if the file is a valid file\n        if (not isinstance(data, dict) or 'data' not in data or\n                not isinstance(data['data'], dict) or 'translate' not in data or\n                not isinstance(data['translate'], dict)):\n            _LOGGER.error('bool trans, valid file')\n            return\n\n        if 'default' in data['translate']:\n            data_default = (data['translate']['default'].get(self._lang, None)\n                            or data['translate']['default'].get(\n                                DEFAULT_INTEGRATION_LANGUAGE, None))\n            if data_default:\n                self._data_default = [{\n                    'value': True,\n                    'description': data_default['true']\n                }, {\n                    'value': False,\n                    'description': data_default['false']\n                }]\n\n        for urn, key in data['data'].items():\n            if key not in data['translate']:\n                _LOGGER.error('bool trans, unknown key, %s, %s', urn, key)\n                continue\n            trans_data = (data['translate'][key].get(self._lang, None) or\n                          data['translate'][key].get(\n                              DEFAULT_INTEGRATION_LANGUAGE, None))\n            if trans_data:\n                self._data[urn] = [{\n                    'value': True,\n                    'description': trans_data['true']\n                }, {\n                    'value': False,\n                    'description': trans_data['false']\n                }]\n\n    async def deinit_async(self) -> None:\n        self._data = None\n        self._data_default = None\n\n    async def translate_async(self, urn: str) -> Optional[list[dict]]:\n        \"\"\"\n        MUST call init_async() before calling this method.\n        [\n            {'value': True, 'description': 'True'},\n            {'value': False, 'description': 'False'}\n        ]\n        \"\"\"\n        if not self._data or urn not in self._data:\n            return self._data_default\n        return self._data[urn]\n\n\nclass _SpecFilter:\n    \"\"\"\n    MIoT-Spec-V2 filter for entity conversion.\n    \"\"\"\n    _SPEC_FILTER_FILE = 'specs/spec_filter.yaml'\n    _main_loop: asyncio.AbstractEventLoop\n    _data: Optional[dict[str, dict[str, set]]]\n    _cache: Optional[dict]\n\n    def __init__(self, loop: Optional[asyncio.AbstractEventLoop]) -> None:\n        self._main_loop = loop or asyncio.get_event_loop()\n        self._data = None\n        self._cache = None\n\n    async def init_async(self) -> None:\n        if isinstance(self._data, dict):\n            return\n        filter_data = None\n        self._data = {}\n        try:\n            filter_data = await self._main_loop.run_in_executor(\n                None, load_yaml_file,\n                os.path.join(os.path.dirname(os.path.abspath(__file__)),\n                             self._SPEC_FILTER_FILE))\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('spec filter, load file error, %s', err)\n            return\n        if not isinstance(filter_data, dict):\n            _LOGGER.error('spec filter, invalid spec filter content')\n            return\n        for values in list(filter_data.values()):\n            if not isinstance(values, dict):\n                _LOGGER.error('spec filter, invalid spec filter data')\n                return\n            for value in values.values():\n                if not isinstance(value, list):\n                    _LOGGER.error('spec filter, invalid spec filter rules')\n                    return\n\n        self._data = filter_data\n\n    async def deinit_async(self) -> None:\n        self._cache = None\n        self._data = None\n\n    async def set_spec_spec(self, urn_key: str) -> None:\n        \"\"\"MUST call init_async() first.\"\"\"\n        if not self._data:\n            return\n        self._cache = self._data.get(urn_key, None)\n\n    def filter_service(self, siid: int) -> bool:\n        \"\"\"Filter service by siid.\n        MUST call init_async() and set_spec_spec() first.\"\"\"\n        if (self._cache and 'services' in self._cache and\n            (str(siid) in self._cache['services'] or\n             '*' in self._cache['services'])):\n            return True\n\n        return False\n\n    def filter_property(self, siid: int, piid: int) -> bool:\n        \"\"\"Filter property by piid.\n        MUST call init_async() and set_spec_spec() first.\"\"\"\n        if (self._cache and 'properties' in self._cache and\n            (f'{siid}.{piid}' in self._cache['properties'] or\n             f'{siid}.*' in self._cache['properties'])):\n            return True\n        return False\n\n    def filter_event(self, siid: int, eiid: int) -> bool:\n        \"\"\"Filter event by eiid.\n        MUST call init_async() and set_spec_spec() first.\"\"\"\n        if (self._cache and 'events' in self._cache and\n            (f'{siid}.{eiid}' in self._cache['events'] or\n             f'{siid}.*' in self._cache['events'])):\n            return True\n        return False\n\n    def filter_action(self, siid: int, aiid: int) -> bool:\n        \"\"\"\"Filter action by aiid.\n        MUST call init_async() and set_spec_spec() first.\"\"\"\n        if (self._cache and 'actions' in self._cache and\n            (f'{siid}.{aiid}' in self._cache['actions'] or\n             f'{siid}.*' in self._cache['actions'])):\n            return True\n        return False\n\n\nclass _SpecAdd:\n    \"\"\"MIoT-Spec-V2 add for entity conversion.\"\"\"\n    _SPEC_ADD_FILE = 'specs/spec_add.json'\n    _main_loop: asyncio.AbstractEventLoop\n    _data: Optional[dict]\n    _selected: Optional[dict]\n\n    def __init__(self,\n                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:\n        self._main_loop = loop or asyncio.get_running_loop()\n        self._data = None\n\n    async def init_async(self) -> None:\n        if isinstance(self._data, dict):\n            return\n        add_data = None\n        self._data = {}\n        self._selected = None\n        try:\n            add_data = await self._main_loop.run_in_executor(\n                None, load_json_file,\n                os.path.join(os.path.dirname(os.path.abspath(__file__)),\n                             self._SPEC_ADD_FILE))\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('spec add, load file error, %s', err)\n            return\n        if not isinstance(add_data, dict):\n            _LOGGER.error('spec add, invalid spec add content')\n            return\n        for key, value in add_data.items():\n            if not isinstance(key, str) or not isinstance(value, (list, str)):\n                _LOGGER.error('spec add, invalid spec modify data')\n                return\n\n        self._data = add_data\n\n    async def deinit_async(self) -> None:\n        self._data = None\n        self._selected = None\n\n    async def set_spec_async(self, urn: str) -> None:\n        if not self._data:\n            return\n        self._selected = self._data.get(urn, None)\n        if isinstance(self._selected, str):\n            return await self.set_spec_async(urn=self._selected)\n\n    def get_service_add(self) -> Optional[list[dict]]:\n        return self._selected\n\n\nclass _SpecModify:\n    \"\"\"MIoT-Spec-V2 modify for entity conversion.\"\"\"\n    _SPEC_MODIFY_FILE = 'specs/spec_modify.yaml'\n    _main_loop: asyncio.AbstractEventLoop\n    _data: Optional[dict]\n    _selected: Optional[dict]\n\n    def __init__(self,\n                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:\n        self._main_loop = loop or asyncio.get_running_loop()\n        self._data = None\n\n    async def init_async(self) -> None:\n        if isinstance(self._data, dict):\n            return\n        modify_data = None\n        self._data = {}\n        self._selected = None\n        try:\n            modify_data = await self._main_loop.run_in_executor(\n                None, load_yaml_file,\n                os.path.join(os.path.dirname(os.path.abspath(__file__)),\n                             self._SPEC_MODIFY_FILE))\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('spec modify, load file error, %s', err)\n            return\n        if not isinstance(modify_data, dict):\n            _LOGGER.error('spec modify, invalid spec modify content')\n            return\n        for key, value in modify_data.items():\n            if not isinstance(key, str) or not isinstance(value, (dict, str)):\n                _LOGGER.error('spec modify, invalid spec modify data')\n                return\n\n        self._data = modify_data\n\n    async def deinit_async(self) -> None:\n        self._data = None\n        self._selected = None\n\n    async def set_spec_async(self, urn: str) -> None:\n        if not self._data:\n            return\n        self._selected = self._data.get(urn, None)\n        if isinstance(self._selected, str):\n            return await self.set_spec_async(urn=self._selected)\n\n    def get_prop_name(self, siid: int, piid: int) -> Optional[str]:\n        return self.__get_prop_item(siid=siid, piid=piid, key='name')\n\n    def get_prop_unit(self, siid: int, piid: int) -> Optional[str]:\n        return self.__get_prop_item(siid=siid, piid=piid, key='unit')\n\n    def get_prop_format(self, siid: int, piid: int) -> Optional[str]:\n        return self.__get_prop_item(siid=siid, piid=piid, key='format')\n\n    def get_prop_expr(self, siid: int, piid: int) -> Optional[str]:\n        return self.__get_prop_item(siid=siid, piid=piid, key='expr')\n\n    def get_prop_icon(self, siid: int, piid: int) -> Optional[str]:\n        return self.__get_prop_item(siid=siid, piid=piid, key='icon')\n\n    def get_prop_access(self, siid: int, piid: int) -> Optional[list]:\n        access = self.__get_prop_item(siid=siid, piid=piid, key='access')\n        if not isinstance(access, list):\n            return None\n        return access\n\n    def get_prop_value_range(self, siid: int, piid: int) -> Optional[list]:\n        value_range = self.__get_prop_item(siid=siid,\n                                           piid=piid,\n                                           key='value-range')\n        if not isinstance(value_range, list):\n            return None\n        return value_range\n\n    def get_prop_value_list(self, siid: int, piid: int) -> Optional[list]:\n        value_list = self.__get_prop_item(siid=siid,\n                                          piid=piid,\n                                          key='value-list')\n        if not isinstance(value_list, list):\n            return None\n        return value_list\n\n    def __get_prop_item(self, siid: int, piid: int, key: str) -> Optional[str]:\n        if not self._selected:\n            return None\n        prop = self._selected.get(f'prop.{siid}.{piid}', None)\n        if not prop:\n            return None\n        return prop.get(key, None)\n\n\nclass MIoTSpecParser:\n    \"\"\"MIoT SPEC parser.\"\"\"\n    # pylint: disable=inconsistent-quotes\n    VERSION: int = 1\n    _DOMAIN: str = 'miot_specs'\n    _lang: str\n    _storage: MIoTStorage\n    _main_loop: asyncio.AbstractEventLoop\n\n    _std_lib: _SpecStdLib\n    _multi_lang: _MIoTSpecMultiLang\n    _bool_trans: _SpecBoolTranslation\n    _spec_filter: _SpecFilter\n    _spec_add: _SpecAdd\n    _spec_modify: _SpecModify\n\n    _init_done: bool\n\n    def __init__(self,\n                 lang: Optional[str],\n                 storage: MIoTStorage,\n                 loop: Optional[asyncio.AbstractEventLoop] = None) -> None:\n        self._lang = lang or DEFAULT_INTEGRATION_LANGUAGE\n        self._storage = storage\n        self._main_loop = loop or asyncio.get_running_loop()\n        self._std_lib = _SpecStdLib(lang=self._lang)\n        self._multi_lang = _MIoTSpecMultiLang(lang=self._lang,\n                                              storage=self._storage,\n                                              loop=self._main_loop)\n        self._bool_trans = _SpecBoolTranslation(lang=self._lang,\n                                                loop=self._main_loop)\n        self._spec_filter = _SpecFilter(loop=self._main_loop)\n        self._spec_add = _SpecAdd(loop=self._main_loop)\n        self._spec_modify = _SpecModify(loop=self._main_loop)\n\n        self._init_done = False\n\n    async def init_async(self) -> None:\n        if self._init_done is True:\n            return\n        await self._bool_trans.init_async()\n        await self._spec_filter.init_async()\n        await self._spec_add.init_async()\n        await self._spec_modify.init_async()\n        std_lib_cache = await self._storage.load_async(domain=self._DOMAIN,\n                                                       name='spec_std_lib',\n                                                       type_=dict)\n        if (isinstance(std_lib_cache, dict) and 'data' in std_lib_cache and\n                'ts' in std_lib_cache and\n                isinstance(std_lib_cache['ts'], int) and\n                int(time.time()) - std_lib_cache['ts']\n                < SPEC_STD_LIB_EFFECTIVE_TIME):\n            # Use the cache if the update time is less than 14 day\n            _LOGGER.debug('use local spec std cache, ts->%s',\n                          std_lib_cache['ts'])\n            self._std_lib.load(std_lib_cache['data'])\n            self._init_done = True\n            return\n        # Update spec std lib\n        if await self._std_lib.refresh_async():\n            if not await self._storage.save_async(\n                    domain=self._DOMAIN,\n                    name='spec_std_lib',\n                    data={\n                        'data': self._std_lib.dump(),\n                        'ts': int(time.time())\n                    }):\n                _LOGGER.error('save spec std lib failed')\n        else:\n            if isinstance(std_lib_cache, dict) and 'data' in std_lib_cache:\n                self._std_lib.load(std_lib_cache['data'])\n                _LOGGER.info('get spec std lib failed, use local cache')\n            else:\n                _LOGGER.error('load spec std lib failed')\n        self._init_done = True\n\n    async def deinit_async(self) -> None:\n        self._init_done = False\n        # self._std_lib.deinit()\n        await self._bool_trans.deinit_async()\n        await self._spec_filter.deinit_async()\n        await self._spec_add.deinit_async()\n        await self._spec_modify.deinit_async()\n\n    async def parse(\n        self,\n        urn: str,\n        skip_cache: bool = False,\n    ) -> Optional[MIoTSpecInstance]:\n        \"\"\"MUST await init first !!!\"\"\"\n        if not skip_cache:\n            cache_result = await self.__cache_get(urn=urn)\n            if isinstance(cache_result, dict):\n                _LOGGER.debug('get from cache, %s', urn)\n                return MIoTSpecInstance.load(specs=cache_result)\n        # Retry three times\n        for index in range(3):\n            try:\n                return await self.__parse(urn=urn)\n            except Exception as err:  # pylint: disable=broad-exception-caught\n                _LOGGER.error('parse error, retry, %d, %s, %s', index, urn, err)\n        return None\n\n    async def refresh_async(self, urn_list: list[str]) -> int:\n        \"\"\"MUST await init first !!!\"\"\"\n        if not urn_list:\n            return False\n        if await self._std_lib.refresh_async():\n            if not await self._storage.save_async(\n                    domain=self._DOMAIN,\n                    name='spec_std_lib',\n                    data={\n                        'data': self._std_lib.dump(),\n                        'ts': int(time.time())\n                    }):\n                _LOGGER.error('save spec std lib failed')\n        else:\n            raise MIoTSpecError('get spec std lib failed')\n        success_count = 0\n        for index in range(0, len(urn_list), 5):\n            batch = urn_list[index:index + 5]\n            task_list = [\n                self._main_loop.create_task(self.parse(urn=urn,\n                                                       skip_cache=True))\n                for urn in batch\n            ]\n            results = await asyncio.gather(*task_list)\n            success_count += sum(1 for result in results if result is not None)\n        return success_count\n\n    async def __cache_get(self, urn: str) -> Optional[dict]:\n        if platform.system() == 'Windows':\n            urn = urn.replace(':', '_')\n        return await self._storage.load_async(domain=self._DOMAIN,\n                                              name=f'{urn}_{self._lang}',\n                                              type_=dict)  # type: ignore\n\n    async def __cache_set(self, urn: str, data: dict) -> bool:\n        if platform.system() == 'Windows':\n            urn = urn.replace(':', '_')\n        return await self._storage.save_async(domain=self._DOMAIN,\n                                              name=f'{urn}_{self._lang}',\n                                              data=data)\n\n    async def __get_instance(self, urn: str) -> Optional[dict]:\n        return await MIoTHttp.get_json_async(\n            url='https://miot-spec.org/miot-spec-v2/instance',\n            params={'type': urn})\n\n    async def __parse(self, urn: str) -> MIoTSpecInstance:\n        _LOGGER.debug('parse urn, %s', urn)\n        # Load spec instance\n        instance = await self.__get_instance(urn=urn)\n        if (not isinstance(instance, dict) or 'type' not in instance or\n                'description' not in instance or 'services' not in instance):\n            raise MIoTSpecError(f'invalid urn instance, {urn}')\n        urn_strs: list[str] = urn.split(':')\n        urn_key: str = ':'.join(urn_strs[:6])\n        # Set translation cache\n        await self._multi_lang.set_spec_async(urn=urn)\n        # Set spec filter\n        await self._spec_filter.set_spec_spec(urn_key=urn_key)\n        # Set spec add\n        await self._spec_add.set_spec_async(urn=urn)\n        # Set spec modify\n        await self._spec_modify.set_spec_async(urn=urn)\n        # Parse device type\n        spec_instance: MIoTSpecInstance = MIoTSpecInstance(\n            urn=urn,\n            name=urn_strs[3],\n            description=instance['description'],\n            description_trans=(\n                self._std_lib.device_translate(key=':'.join(urn_strs[:5])) or\n                instance['description'] or urn_strs[3]))\n        urn_service_instance = instance.get('services', [])\n        # set spec instance in spec_add.json as not being filtered.\n        custom_service_instance = self._spec_add.get_service_add()\n        if custom_service_instance:\n            for service in custom_service_instance:\n                service['need_filter'] = False\n                if 'properties' in service:\n                    for prop in service['properties']:\n                        prop['need_filter'] = False\n                if 'actions' in service:\n                    for action in service['actions']:\n                        action['need_filter'] = False\n                if 'events' in service:\n                    for event in service['events']:\n                        event['need_filter'] = False\n                urn_service_instance.append(service)\n        # Parse services\n        for service in urn_service_instance:\n            if ('iid' not in service or 'type' not in service or\n                    'description' not in service):\n                _LOGGER.error('invalid service, %s, %s', urn, service)\n                continue\n            type_strs: list[str] = service['type'].split(':')\n            if type_strs[3] == 'device-information':\n                # Ignore device-information service\n                continue\n            spec_service: MIoTSpecService = MIoTSpecService(spec=service)\n            spec_service.name = type_strs[3]\n            # Filter spec service\n            spec_service.need_filter = self._spec_filter.filter_service(\n                siid=service['iid']) if (\n                    'need_filter' not in service) else service['need_filter']\n            if spec_service.need_filter:\n                continue\n            if type_strs[1] != 'miot-spec-v2':\n                spec_service.proprietary = True\n            spec_service.description_trans = (\n                self._multi_lang.translate(f's:{service[\"iid\"]}') or\n                self._std_lib.service_translate(key=':'.join(type_strs[:5])) or\n                service['description'] or spec_service.name)\n            # Parse service property\n            for property_ in service.get('properties', []):\n                if ('iid' not in property_ or 'type' not in property_ or\n                        'description' not in property_ or\n                        'format' not in property_ or 'access' not in property_):\n                    continue\n                p_type_strs: list[str] = property_['type'].split(':')\n                # Handle special property.unit\n                unit = property_.get('unit', None)\n                spec_prop: MIoTSpecProperty = MIoTSpecProperty(\n                    spec=property_,\n                    service=spec_service,\n                    format_=property_['format'],\n                    access=property_['access'],\n                    unit=unit if unit != 'none' else None)\n                spec_prop.name = p_type_strs[3]\n                # Filter spec property\n                spec_prop.need_filter = (\n                    spec_service.need_filter or\n                    (self._spec_filter.filter_property(siid=service['iid'],\n                                                       piid=property_['iid'])\n                     if 'need_filter' not in property_ else\n                     property_['need_filter']))\n                if spec_prop.need_filter:\n                    continue\n                if p_type_strs[1] != 'miot-spec-v2':\n                    spec_prop.proprietary = spec_service.proprietary or True\n                spec_prop.description_trans = (\n                    self._multi_lang.translate(\n                        f'p:{service[\"iid\"]}:{property_[\"iid\"]}') or\n                    self._std_lib.property_translate(\n                        key=':'.join(p_type_strs[:5])) or\n                    property_['description'] or spec_prop.name)\n                # Modify value-list before translation\n                v_list: list[dict] = self._spec_modify.get_prop_value_list(\n                    siid=service['iid'], piid=property_['iid'])\n                if (v_list is None) and ('value-list' in property_):\n                    v_list = property_['value-list']\n                if v_list is not None:\n                    for index, v in enumerate(v_list):\n                        if v['description'].strip() == '':\n                            v['description'] = f'v_{v[\"value\"]}'\n                        v['name'] = v['description']\n                        v['description'] = (self._multi_lang.translate(\n                            f'v:{service[\"iid\"]}:{property_[\"iid\"]}:'\n                            f'{index}') or self._std_lib.value_translate(\n                                key=f'{type_strs[:5]}|{p_type_strs[3]}|'\n                                f'{v[\"description\"]}') or v['name'])\n                    spec_prop.value_list = MIoTSpecValueList.from_spec(v_list)\n                if 'value-range' in property_:\n                    spec_prop.value_range = property_['value-range']\n                elif property_['format'] == 'bool':\n                    v_tag = ':'.join(p_type_strs[:5])\n                    v_descriptions = (await\n                                      self._bool_trans.translate_async(urn=v_tag\n                                                                      ))\n                    if v_descriptions:\n                        # bool without value-list.name\n                        spec_prop.value_list = v_descriptions\n                # Prop modify\n                spec_prop.unit = self._spec_modify.get_prop_unit(\n                    siid=service['iid'],\n                    piid=property_['iid']) or spec_prop.unit\n                spec_prop.expr = self._spec_modify.get_prop_expr(\n                    siid=service['iid'], piid=property_['iid'])\n                spec_prop.icon = self._spec_modify.get_prop_icon(\n                    siid=service['iid'], piid=property_['iid'])\n                spec_service.properties.append(spec_prop)\n                custom_access = self._spec_modify.get_prop_access(\n                    siid=service['iid'], piid=property_['iid'])\n                if custom_access:\n                    spec_prop.access = custom_access\n                custom_format = self._spec_modify.get_prop_format(\n                    siid=service['iid'], piid=property_['iid'])\n                if custom_format:\n                    spec_prop.format_ = custom_format\n                custom_range = self._spec_modify.get_prop_value_range(\n                    siid=service['iid'], piid=property_['iid'])\n                if custom_range:\n                    spec_prop.value_range = custom_range\n                custom_name = self._spec_modify.get_prop_name(\n                    siid=service['iid'], piid=property_['iid'])\n                if custom_name:\n                    spec_prop.name = custom_name\n            # Parse service event\n            for event in service.get('events', []):\n                if ('iid' not in event or 'type' not in event or\n                        'description' not in event or 'arguments' not in event):\n                    continue\n                e_type_strs: list[str] = event['type'].split(':')\n                spec_event: MIoTSpecEvent = MIoTSpecEvent(spec=event,\n                                                          service=spec_service)\n                spec_event.name = e_type_strs[3]\n                # Filter spec event\n                spec_event.need_filter = (\n                    spec_service.need_filter or\n                    (self._spec_filter.filter_event(siid=service['iid'],\n                                                    eiid=event['iid'])\n                     if 'need_filter' not in event else event['need_filter']))\n                if spec_event.need_filter:\n                    continue\n                if e_type_strs[1] != 'miot-spec-v2':\n                    spec_event.proprietary = spec_service.proprietary or True\n                spec_event.description_trans = (\n                    self._multi_lang.translate(\n                        f'e:{service[\"iid\"]}:{event[\"iid\"]}') or\n                    self._std_lib.event_translate(key=':'.join(e_type_strs[:5]))\n                    or event['description'] or spec_event.name)\n                arg_list: list[MIoTSpecProperty] = []\n                for piid in event['arguments']:\n                    for prop in spec_service.properties:\n                        if prop.iid == piid:\n                            arg_list.append(prop)\n                            break\n                spec_event.argument = arg_list\n                spec_service.events.append(spec_event)\n            # Parse service action\n            for action in service.get('actions', []):\n                if ('iid' not in action or 'type' not in action or\n                        'description' not in action or 'in' not in action):\n                    continue\n                a_type_strs: list[str] = action['type'].split(':')\n                spec_action: MIoTSpecAction = MIoTSpecAction(\n                    spec=action, service=spec_service)\n                spec_action.name = a_type_strs[3]\n                # Filter spec action\n                spec_action.need_filter = (\n                    spec_service.need_filter or\n                    (self._spec_filter.filter_action(siid=service['iid'],\n                                                     aiid=action['iid'])\n                     if 'need_filter' not in action else action['need_filter']))\n                if spec_action.need_filter:\n                    continue\n                if a_type_strs[1] != 'miot-spec-v2':\n                    spec_action.proprietary = spec_service.proprietary or True\n                spec_action.description_trans = (\n                    self._multi_lang.translate(\n                        f'a:{service[\"iid\"]}:{action[\"iid\"]}') or\n                    self._std_lib.action_translate(\n                        key=':'.join(a_type_strs[:5])) or\n                    action['description'] or spec_action.name)\n                in_list: list[MIoTSpecProperty] = []\n                for piid in action['in']:\n                    for prop in spec_service.properties:\n                        if prop.iid == piid:\n                            in_list.append(prop)\n                            break\n                spec_action.in_ = in_list\n                out_list: list[MIoTSpecProperty] = []\n                for piid in action['out']:\n                    for prop in spec_service.properties:\n                        if prop.iid == piid:\n                            out_list.append(prop)\n                            break\n                spec_action.out = out_list\n                spec_service.actions.append(spec_action)\n            spec_instance.services.append(spec_service)\n\n        await self.__cache_set(urn=urn, data=spec_instance.dump())\n        return spec_instance\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/miot_storage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT storage and certificate management.\n\"\"\"\nimport os\nimport asyncio\nimport binascii\nimport json\nimport shutil\nimport time\nimport traceback\nimport hashlib\nfrom datetime import datetime, timezone\nfrom enum import Enum, auto\nfrom pathlib import Path\nfrom typing import Any, Optional, Union\nimport logging\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.x509.oid import NameOID\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ed25519\n\n\n# pylint: disable=relative-beyond-top-level\nfrom .const import (\n    MANUFACTURER_EFFECTIVE_TIME,\n    MIHOME_CA_CERT_STR,\n    MIHOME_CA_CERT_SHA256)\nfrom .common import MIoTHttp\nfrom .miot_error import MIoTCertError, MIoTError, MIoTStorageError\n\n_LOGGER = logging.getLogger(__name__)\n\n\nclass MIoTStorageType(Enum):\n    LOAD = auto()\n    LOAD_FILE = auto()\n    SAVE = auto()\n    SAVE_FILE = auto()\n    DEL = auto()\n    DEL_FILE = auto()\n    CLEAR = auto()\n\n\nclass MIoTStorage:\n    \"\"\"File management.\n\n    User data will be stored in the `.storage` directory of Home Assistant.\n    \"\"\"\n    _main_loop: asyncio.AbstractEventLoop\n    _file_future: dict[str, tuple[MIoTStorageType, asyncio.Future]]\n\n    _root_path: str\n\n    def __init__(\n        self, root_path: str,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        \"\"\"Initialize with a root path.\"\"\"\n        self._main_loop = loop or asyncio.get_running_loop()\n        self._file_future = {}\n\n        self._root_path = os.path.abspath(root_path)\n        os.makedirs(self._root_path, exist_ok=True)\n\n        _LOGGER.debug('root path, %s', self._root_path)\n\n    def __get_full_path(self, domain: str, name: str, suffix: str) -> str:\n        return os.path.join(\n            self._root_path, domain, f'{name}.{suffix}')\n\n    def __add_file_future(\n        self, key: str, op_type: MIoTStorageType, fut: asyncio.Future\n    ) -> None:\n        def fut_done_callback(fut: asyncio.Future):\n            del fut\n            self._file_future.pop(key, None)\n\n        fut.add_done_callback(fut_done_callback)\n        self._file_future[key] = op_type, fut\n\n    def __load(\n        self, full_path: str, type_: type = bytes, with_hash_check: bool = True\n    ) -> Union[bytes, str, dict, list, None]:\n        if not os.path.exists(full_path):\n            _LOGGER.debug('load error, file does not exist, %s', full_path)\n            return None\n        if not os.access(full_path, os.R_OK):\n            _LOGGER.error('load error, file not readable, %s', full_path)\n            return None\n        try:\n            with open(full_path, 'rb') as r_file:\n                r_data: bytes = r_file.read()\n                if r_data is None:\n                    _LOGGER.error('load error, empty file, %s', full_path)\n                    return None\n                data_bytes: bytes\n                # Hash check\n                if with_hash_check:\n                    if len(r_data) <= 32:\n                        return None\n                    data_bytes = r_data[:-32]\n                    hash_value = r_data[-32:]\n                    if hashlib.sha256(data_bytes).digest() != hash_value:\n                        _LOGGER.error(\n                            'load error, hash check failed, %s', full_path)\n                        return None\n                else:\n                    data_bytes = r_data\n                if type_ == bytes:\n                    return data_bytes\n                if type_ == str:\n                    return str(data_bytes, 'utf-8')\n                if type_ in [dict, list]:\n                    return json.loads(data_bytes)\n                _LOGGER.error(\n                    'load error, unsupported data type, %s', type_.__name__)\n                return None\n        except (OSError, TypeError) as e:\n            _LOGGER.error('load error, %s, %s', e, traceback.format_exc())\n            return None\n\n    def load(\n        self, domain: str, name: str, type_: type = bytes\n    ) -> Union[bytes, str, dict, list, None]:\n        full_path = self.__get_full_path(\n            domain=domain, name=name, suffix=type_.__name__)\n        return self.__load(full_path=full_path, type_=type_)\n\n    async def load_async(\n        self, domain: str, name: str, type_: type = bytes\n    ) -> Union[bytes, str, dict, list, None]:\n        full_path = self.__get_full_path(\n            domain=domain, name=name, suffix=type_.__name__)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            op_type, fut = self._file_future[full_path]\n            if op_type == MIoTStorageType.LOAD:\n                if not fut.done():\n                    return await fut\n            else:\n                await fut\n        fut = self._main_loop.run_in_executor(\n            None, self.__load, full_path, type_)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.LOAD, fut)\n        return await fut\n\n    def __save(\n        self, full_path: str, data: Union[bytes, str, dict, list, None],\n        cover: bool = True, with_hash: bool = True\n    ) -> bool:\n        if data is None:\n            _LOGGER.error('save error, save data is None')\n            return False\n        if os.path.exists(full_path):\n            if not cover:\n                _LOGGER.error('save error, file exists, cover is False')\n                return False\n            if not os.access(full_path, os.W_OK):\n                _LOGGER.error('save error, file not writeable, %s', full_path)\n                return False\n        else:\n            os.makedirs(os.path.dirname(full_path), exist_ok=True)\n        try:\n            w_bytes: bytes\n            if isinstance(data, bytes):\n                w_bytes = data\n            elif isinstance(data, str):\n                w_bytes = data.encode('utf-8')\n            elif isinstance(data, (dict, list)):\n                w_bytes = json.dumps(data).encode('utf-8')\n            else:\n                _LOGGER.error(\n                    'save error, unsupported data type, %s',\n                    type(data).__name__)\n                return False\n            with open(full_path, 'wb') as w_file:\n                w_file.write(w_bytes)\n                if with_hash:\n                    w_file.write(hashlib.sha256(w_bytes).digest())\n            return True\n        except (OSError, TypeError) as e:\n            _LOGGER.error('save error, %s, %s', e, traceback.format_exc())\n            return False\n\n    def save(\n        self, domain: str, name: str, data: Union[bytes, str, dict, list, None]\n    ) -> bool:\n        full_path = self.__get_full_path(\n            domain=domain, name=name, suffix=type(data).__name__)\n        return self.__save(full_path=full_path, data=data)\n\n    async def save_async(\n        self, domain: str, name: str, data: Union[bytes, str, dict, list, None]\n    ) -> bool:\n        full_path = self.__get_full_path(\n            domain=domain, name=name, suffix=type(data).__name__)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            fut = self._file_future[full_path][1]\n            await fut\n        fut = self._main_loop.run_in_executor(\n            None, self.__save, full_path, data)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.SAVE, fut)\n        return await fut\n\n    def __remove(self, full_path: str) -> bool:\n        item = Path(full_path)\n        if item.is_file() or item.is_symlink():\n            item.unlink()\n        return True\n\n    def remove(self, domain: str, name: str, type_: type) -> bool:\n        full_path = self.__get_full_path(\n            domain=domain, name=name, suffix=type_.__name__)\n        return self.__remove(full_path=full_path)\n\n    async def remove_async(self, domain: str, name: str, type_: type) -> bool:\n        full_path = self.__get_full_path(\n            domain=domain, name=name, suffix=type_.__name__)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            op_type, fut = self._file_future[full_path]\n            if op_type == MIoTStorageType.DEL:\n                if not fut.done():\n                    return await fut\n            else:\n                await fut\n        fut = self._main_loop.run_in_executor(None, self.__remove, full_path)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.DEL, fut)\n        return await fut\n\n    def __remove_domain(self, full_path: str) -> bool:\n        path_obj = Path(full_path)\n        if path_obj.exists():\n            # Recursive deletion\n            shutil.rmtree(path_obj)\n        return True\n\n    def remove_domain(self, domain: str) -> bool:\n        full_path = os.path.join(self._root_path, domain)\n        return self.__remove_domain(full_path=full_path)\n\n    async def remove_domain_async(self, domain: str) -> bool:\n        full_path = os.path.join(self._root_path, domain)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            op_type, fut = self._file_future[full_path]\n            if op_type == MIoTStorageType.DEL:\n                if not fut.done():\n                    return await fut\n            else:\n                await fut\n        # Waiting domain tasks finish\n        for path, value in self._file_future.items():\n            if path.startswith(full_path):\n                await value[1]\n        fut = self._main_loop.run_in_executor(\n            None, self.__remove_domain, full_path)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.DEL, fut)\n        return await fut\n\n    def get_names(self, domain: str, type_: type) -> list[str]:\n        path: str = os.path.join(self._root_path, domain)\n        type_str = f'.{type_.__name__}'\n        names: list[str] = []\n        for item in Path(path).glob(f'*{type_str}'):\n            if not item.is_file() and not item.is_symlink():\n                continue\n            names.append(item.name.replace(type_str, ''))\n        return names\n\n    def file_exists(self, domain: str, name_with_suffix: str) -> bool:\n        return os.path.exists(\n            os.path.join(self._root_path, domain, name_with_suffix))\n\n    def save_file(\n        self, domain: str, name_with_suffix: str, data: bytes\n    ) -> bool:\n        if not isinstance(data, bytes):\n            _LOGGER.error('save file error, file must be bytes')\n            return False\n        full_path = os.path.join(self._root_path, domain, name_with_suffix)\n        return self.__save(full_path=full_path, data=data,  with_hash=False)\n\n    async def save_file_async(\n        self, domain: str, name_with_suffix: str, data: bytes\n    ) -> bool:\n        if not isinstance(data, bytes):\n            _LOGGER.error('save file error, file must be bytes')\n            return False\n        full_path = os.path.join(self._root_path, domain, name_with_suffix)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            fut = self._file_future[full_path][1]\n            await fut\n        fut = self._main_loop.run_in_executor(\n            None, self.__save, full_path, data, True, False)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.SAVE_FILE, fut)\n        return await fut\n\n    def load_file(self, domain: str, name_with_suffix: str) -> Optional[bytes]:\n        full_path = os.path.join(self._root_path, domain, name_with_suffix)\n        return self.__load(\n            full_path=full_path, type_=bytes,\n            with_hash_check=False)  # type: ignore\n\n    async def load_file_async(\n        self, domain: str, name_with_suffix: str\n    ) -> Optional[bytes]:\n        full_path = os.path.join(self._root_path, domain, name_with_suffix)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            op_type, fut = self._file_future[full_path]\n            if op_type == MIoTStorageType.LOAD_FILE:\n                if not fut.done():\n                    return await fut\n            else:\n                await fut\n        fut = self._main_loop.run_in_executor(\n            None, self.__load, full_path, bytes, False)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.LOAD_FILE, fut)\n        return await fut  # type: ignore\n\n    def remove_file(self, domain: str, name_with_suffix: str) -> bool:\n        full_path = os.path.join(self._root_path, domain, name_with_suffix)\n        return self.__remove(full_path=full_path)\n\n    async def remove_file_async(\n        self, domain: str, name_with_suffix: str\n    ) -> bool:\n        full_path = os.path.join(self._root_path, domain, name_with_suffix)\n        if full_path in self._file_future:\n            # Waiting for the last task to be completed\n            op_type, fut = self._file_future[full_path]\n            if op_type == MIoTStorageType.DEL_FILE:\n                if not fut.done():\n                    return await fut\n            else:\n                await fut\n        fut = self._main_loop.run_in_executor(None, self.__remove, full_path)\n        if not fut.done():\n            self.__add_file_future(full_path, MIoTStorageType.DEL_FILE, fut)\n        return await fut\n\n    def clear(self) -> bool:\n        root_path = Path(self._root_path)\n        for item in root_path.iterdir():\n            if item.is_file() or item.is_symlink():\n                item.unlink()\n            elif item.is_dir():\n                shutil.rmtree(item)\n        return True\n\n    async def clear_async(self) -> bool:\n        if self._root_path in self._file_future:\n            op_type, fut = self._file_future[self._root_path]\n            if op_type == MIoTStorageType.CLEAR and not fut.done():\n                return await fut\n        # Waiting all future resolve\n        for value in self._file_future.values():\n            await value[1]\n\n        fut = self._main_loop.run_in_executor(None, self.clear)\n        if not fut.done():\n            self.__add_file_future(\n                self._root_path, MIoTStorageType.CLEAR, fut)\n        return await fut\n\n    def update_user_config(\n        self, uid: str, cloud_server: str, config: Optional[dict[str, Any]],\n        replace: bool = False\n    ) -> bool:\n        if config is not None and len(config) == 0:\n            # Do nothing\n            return True\n\n        config_domain = 'miot_config'\n        config_name = f'{uid}_{cloud_server}'\n        if config is None:\n            # Remove config file\n            return self.remove(\n                domain=config_domain, name=config_name, type_=dict)\n        if replace:\n            # Replace config file\n            return self.save(\n                domain=config_domain, name=config_name, data=config)\n        local_config = (self.load(domain=config_domain,\n                        name=config_name, type_=dict)) or {}\n        local_config.update(config)  # type: ignore\n        return self.save(\n            domain=config_domain, name=config_name, data=local_config)\n\n    async def update_user_config_async(\n        self, uid: str, cloud_server: str, config: Optional[dict[str, Any]],\n        replace: bool = False\n    ) -> bool:\n        \"\"\"Update user configuration.\n\n        Args:\n            uid (str): user_id\n            config (Optional[dict[str]]):\n                remove config file if config is None\n            replace (bool, optional):\n                replace all config item. Defaults to False.\n\n        Returns:\n            bool: result code\n        \"\"\"\n        if config is not None and len(config) == 0:\n            # Do nothing\n            return True\n\n        config_domain = 'miot_config'\n        config_name = f'{uid}_{cloud_server}'\n        if config is None:\n            # Remove config file\n            return await self.remove_async(\n                domain=config_domain, name=config_name, type_=dict)\n        if replace:\n            # Replace config file\n            return await self.save_async(\n                domain=config_domain, name=config_name, data=config)\n        local_config = (await self.load_async(\n            domain=config_domain, name=config_name, type_=dict)) or {}\n        local_config.update(config)  # type: ignore\n        return await self.save_async(\n            domain=config_domain, name=config_name, data=local_config)\n\n    def load_user_config(\n        self, uid: str, cloud_server: str, keys: Optional[list[str]] = None\n    ) -> dict[str, Any]:\n        if isinstance(keys, list) and len(keys) == 0:\n            # Do nothing\n            return {}\n        config_domain = 'miot_config'\n        config_name = f'{uid}_{cloud_server}'\n        local_config = (self.load(domain=config_domain,\n                        name=config_name, type_=dict))\n        if not isinstance(local_config, dict):\n            return {}\n        if keys is None:\n            return local_config\n        return {\n            key: local_config[key] for key in keys\n            if key in local_config}\n\n    async def load_user_config_async(\n        self, uid: str, cloud_server: str, keys: Optional[list[str]] = None\n    ) -> dict:\n        \"\"\"Load user configuration.\n\n        Args:\n            uid (str): user id\n            keys (list[str]):\n                query key list, return all config item if keys is None\n\n        Returns:\n            dict[str, Any]: query result\n        \"\"\"\n        if isinstance(keys, list) and len(keys) == 0:\n            # Do nothing\n            return {}\n        config_domain = 'miot_config'\n        config_name = f'{uid}_{cloud_server}'\n        local_config = (await self.load_async(\n            domain=config_domain, name=config_name, type_=dict))\n        if not isinstance(local_config, dict):\n            return {}\n        if keys is None:\n            return local_config\n        return {\n            key: local_config[key] for key in keys\n            if key in local_config}\n\n    def gen_storage_path(\n        self, domain: Optional[str] = None,\n        name_with_suffix: Optional[str] = None\n    ) -> str:\n        \"\"\"Generate file path.\"\"\"\n        result = self._root_path\n        if domain:\n            result = os.path.join(result, domain)\n            if name_with_suffix:\n                result = os.path.join(result, name_with_suffix)\n        return result\n\n\nclass MIoTCert:\n    \"\"\"MIoT certificate file management.\"\"\"\n    CERT_DOMAIN: str = 'cert'\n    CA_NAME: str = 'mihome_ca.cert'\n    _loop: asyncio.AbstractEventLoop\n    _storage: MIoTStorage\n    _uid: str\n    _cloud_server: str\n\n    _key_name: str\n    _cert_name: str\n\n    def __init__(\n        self, storage: MIoTStorage, uid: str, cloud_server: str,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        if not isinstance(storage, MIoTStorage) or not isinstance(uid, str):\n            raise MIoTError('invalid params')\n        self._loop = loop or asyncio.get_running_loop()\n        self._storage = storage\n        self._uid = uid\n        self._cloud_server = cloud_server\n        self._key_name = f'{uid}_{cloud_server}.key'\n        self._cert_name = f'{uid}_{cloud_server}.cert'\n\n    @property\n    def ca_file(self) -> str:\n        \"\"\"CA certificate file path.\"\"\"\n        return self._storage.gen_storage_path(\n            domain=self.CERT_DOMAIN, name_with_suffix=self.CA_NAME)\n\n    @property\n    def key_file(self) -> str:\n        \"\"\"User private key file file path.\"\"\"\n        return self._storage.gen_storage_path(\n            domain=self.CERT_DOMAIN, name_with_suffix=self._key_name)\n\n    @property\n    def cert_file(self) -> str:\n        \"\"\"User certificate file path.\"\"\"\n        return self._storage.gen_storage_path(\n            domain=self.CERT_DOMAIN, name_with_suffix=self._cert_name)\n\n    async def verify_ca_cert_async(self) -> bool:\n        \"\"\"Verify the integrity of the CA certificate file.\"\"\"\n        ca_data = await self._storage.load_file_async(\n            domain=self.CERT_DOMAIN, name_with_suffix=self.CA_NAME)\n        if ca_data is None:\n            if not await self._storage.save_file_async(\n                    domain=self.CERT_DOMAIN,\n                    name_with_suffix=self.CA_NAME,\n                    data=MIHOME_CA_CERT_STR.encode('utf-8')):\n                raise MIoTStorageError('ca cert save failed')\n            ca_data = await self._storage.load_file_async(\n                domain=self.CERT_DOMAIN, name_with_suffix=self.CA_NAME)\n            if ca_data is None:\n                raise MIoTStorageError('ca cert load failed')\n            _LOGGER.debug('ca cert save success')\n        # Compare the file sha256sum\n        ca_cert_hash = hashlib.sha256(ca_data).digest()\n        hash_str = binascii.hexlify(ca_cert_hash).decode('utf-8')\n        if hash_str != MIHOME_CA_CERT_SHA256:\n            return False\n        return True\n\n    async def user_cert_remaining_time_async(\n        self, cert_data: Optional[bytes] = None, did: Optional[str] = None\n    ) -> int:\n        \"\"\"Get the remaining time of user certificate validity.\n\n        Returns:\n            If the certificate is not valid, return 0.\n        \"\"\"\n        if cert_data is None:\n            cert_data = await self._storage.load_file_async(\n                domain=self.CERT_DOMAIN, name_with_suffix=self._cert_name)\n        if cert_data is None:\n            return 0\n        # Check user cert\n        try:\n            user_cert: x509.Certificate = x509.load_pem_x509_certificate(\n                cert_data, default_backend())\n            cert_info = {}\n            for attribute in user_cert.subject:\n                if attribute.oid == x509.NameOID.COMMON_NAME:\n                    cert_info['CN'] = attribute.value\n                elif attribute.oid == x509.NameOID.COUNTRY_NAME:\n                    cert_info['C'] = attribute.value\n                elif attribute.oid == x509.NameOID.ORGANIZATION_NAME:\n                    cert_info['O'] = attribute.value\n\n            if len(cert_info) != 3:\n                raise MIoTCertError('invalid cert info')\n            if (\n                did and cert_info['CN'] !=\n                    f'mips.{self._uid}.{self.__did_hash(did=did)}.2'\n            ):\n                raise MIoTCertError('invalid COMMON_NAME')\n            if 'C' not in cert_info or cert_info['C'] != 'CN':\n                raise MIoTCertError('invalid COUNTRY_NAME')\n            if 'O' not in cert_info or cert_info['O'] != 'Mijia Device':\n                raise MIoTCertError('invalid ORGANIZATION_NAME')\n            now_utc: datetime = datetime.now(timezone.utc)\n            if (\n                now_utc < user_cert.not_valid_before_utc or\n                    now_utc > user_cert.not_valid_after_utc\n            ):\n                raise MIoTCertError('cert is not valid')\n            return int((user_cert.not_valid_after_utc-now_utc).total_seconds())\n        except (MIoTCertError, ValueError) as error:\n            _LOGGER.error(\n                'load_pem_x509_certificate failed, %s, %s',\n                error, traceback.format_exc())\n            return 0\n\n    def gen_user_key(self) -> str:\n        \"\"\"Generate user private key.\"\"\"\n        private_key = ed25519.Ed25519PrivateKey.generate()\n        return private_key.private_bytes(\n            encoding=serialization.Encoding.PEM,\n            format=serialization.PrivateFormat.PKCS8,\n            encryption_algorithm=serialization.NoEncryption()\n        ).decode('utf-8')\n\n    def gen_user_csr(self, user_key: str, did: str) -> str:\n        \"\"\"Generate CSR of user certificate.\"\"\"\n        private_key = serialization.load_pem_private_key(\n            data=user_key.encode('utf-8'), password=None)\n        did_hash = self.__did_hash(did=did)\n        builder = x509.CertificateSigningRequestBuilder().subject_name(\n            x509.Name([\n                # Central hub gateway service is only supported in China.\n                x509.NameAttribute(NameOID.COUNTRY_NAME, 'CN'),\n                x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'Mijia Device'),\n                x509.NameAttribute(\n                    NameOID.COMMON_NAME, f'mips.{self._uid}.{did_hash}.2'),\n            ]))\n        csr = builder.sign(\n            private_key, algorithm=None,  # type: ignore\n            backend=default_backend())\n        return csr.public_bytes(serialization.Encoding.PEM).decode('utf-8')\n\n    async def load_user_key_async(self) -> Optional[str]:\n        \"\"\"Load user private key.\"\"\"\n        data = await self._storage.load_file_async(\n            domain=self.CERT_DOMAIN, name_with_suffix=self._key_name)\n        return data.decode('utf-8') if data else None\n\n    async def update_user_key_async(self, key: str) -> bool:\n        \"\"\"Update user private key.\"\"\"\n        return await self._storage.save_file_async(\n            domain=self.CERT_DOMAIN,\n            name_with_suffix=self._key_name,\n            data=key.encode('utf-8'))\n\n    async def load_user_cert_async(self) -> Optional[str]:\n        \"\"\"Load user certificate.\"\"\"\n        data = await self._storage.load_file_async(\n            domain=self.CERT_DOMAIN, name_with_suffix=self._cert_name)\n        return data.decode('utf-8') if data else None\n\n    async def update_user_cert_async(self, cert: str) -> bool:\n        \"\"\"Update user certificate.\"\"\"\n        return await self._storage.save_file_async(\n            domain=self.CERT_DOMAIN,\n            name_with_suffix=self._cert_name,\n            data=cert.encode('utf-8'))\n\n    async def remove_ca_cert_async(self) -> bool:\n        \"\"\"Remove CA certificate.\"\"\"\n        return await self._storage.remove_file_async(\n            domain=self.CERT_DOMAIN, name_with_suffix=self.CA_NAME)\n\n    async def remove_user_key_async(self) -> bool:\n        \"\"\"Remove user private key.\"\"\"\n        return await self._storage.remove_file_async(\n            domain=self.CERT_DOMAIN, name_with_suffix=self._key_name)\n\n    async def remove_user_cert_async(self) -> bool:\n        \"\"\"Remove user certificate.\"\"\"\n        return await self._storage.remove_file_async(\n            domain=self.CERT_DOMAIN, name_with_suffix=self._cert_name)\n\n    def __did_hash(self, did: str) -> str:\n        sha1_hash = hashes.Hash(hashes.SHA1(), backend=default_backend())\n        sha1_hash.update(did.encode('utf-8'))\n        return binascii.hexlify(sha1_hash.finalize()).decode('utf-8')\n\n\nclass DeviceManufacturer:\n    \"\"\"Device manufacturer.\"\"\"\n    DOMAIN: str = 'miot_specs'\n    _main_loop: asyncio.AbstractEventLoop\n    _storage: MIoTStorage\n    _data: dict\n\n    def __init__(\n        self, storage: MIoTStorage,\n        loop: Optional[asyncio.AbstractEventLoop] = None\n    ) -> None:\n        self._main_loop = loop or asyncio.get_event_loop()\n        self._storage = storage\n        self._data = {}\n\n    async def init_async(self) -> None:\n        if self._data:\n            return\n        data_cache = await self._storage.load_async(\n            domain=self.DOMAIN, name='manufacturer', type_=dict)\n        if (\n            isinstance(data_cache, dict)\n            and 'data' in data_cache\n            and 'ts' in data_cache\n            and (int(time.time()) - data_cache['ts']) <\n                MANUFACTURER_EFFECTIVE_TIME\n        ):\n            self._data = data_cache['data']\n            _LOGGER.debug('load manufacturer data success')\n            return\n\n        data_cloud = None\n        try:\n            data_cloud = await MIoTHttp.get_json_async(\n                url='https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/'\n                'manufacturer.json',\n                loop=self._main_loop)\n        except Exception as err:  # pylint: disable=broad-exception-caught\n            _LOGGER.error('get manufacturer info failed, %s', err)\n\n        if data_cloud:\n            await self._storage.save_async(\n                domain=self.DOMAIN, name='manufacturer',\n                data={'data': data_cloud, 'ts': int(time.time())})\n            self._data = data_cloud\n            _LOGGER.debug('update manufacturer data success')\n        else:\n            if isinstance(data_cache, dict):\n                self._data = data_cache.get('data', {})\n                _LOGGER.error('load manufacturer data failed, use local data')\n            else:\n                _LOGGER.error('load manufacturer data failed')\n\n    async def deinit_async(self) -> None:\n        self._data.clear()\n\n    def get_name(self, short_name: str) -> str:\n        if not self._data or not short_name or short_name not in self._data:\n            return short_name\n        return self._data[short_name].get('name', None) or short_name\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/resource/oauth_redirect_page.html",
    "content": "<html>\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link rel=\"icon\" href=\"https://cdn.web-global.fds.api.mi-img.com/mcfe--mi-account/static/favicon_new.ico\">\n    <link as=\"style\"\n        href=\"https://font.sec.miui.com/font/css?family=MiSans:300,400,500,600,700:Chinese_Simplify,Chinese_Traditional,Latin&amp;display=swap\"\n        rel=\"preload\">\n    <title>TITLE_PLACEHOLDER</title>\n    <style>\n        body {\n            background: white;\n            color: black;\n            display: flex;\n            justify-content: center;\n            align-items: center;\n            font-family: MiSans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;\n        }\n\n        @media (prefers-color-scheme: dark) {\n            body {\n                background: black;\n                color: white;\n            }\n        }\n\n        .frame {\n            background: rgb(255 255 255 / 5%);\n            width: 360px;\n            padding: 40 45;\n            border-radius: 4px;\n            box-shadow: 0 20px 50px 0 hsla(0, 0%, 64%, .1);\n            text-align: center;\n        }\n\n        .logo-frame {\n            text-align: center;\n        }\n\n        .title-frame {\n            margin: 20px 0 20px 0;\n            font-size: 26px;\n            font-weight: 500;\n            line-height: 40px;\n            opacity: 0.8;\n        }\n\n        .content-frame {\n            font-size: 17px;\n            font-weight: 500;\n            line-height: 20px;\n            opacity: 0.8;\n        }\n\n        button {\n            margin-top: 20px;\n            background-color: #ff5c00;\n            border: none;\n            border-radius: 4px;\n            padding: 0 20px;\n            text-align: center;\n            width: 100%;\n            display: inline-block;\n            font-size: 18px;\n            font-weight: 400;\n            height: 60px;\n            line-height: 60px;\n            overflow: hidden;\n            position: relative;\n            text-overflow: ellipsis;\n            transition: all .3s cubic-bezier(.645, .045, .355, 1);\n            vertical-align: top;\n            white-space: nowrap;\n            cursor: pointer;\n            color: #fff;\n        }\n    </style>\n</head>\n\n<body>\n    <div class=\"frame\">\n        <!-- XIAOMI LOGO-->\n        <div class=\"logo-frame\">\n            <svg width=\"50\" height=\"50\" viewBox=\"0 0 193 193\" xmlns=\"http://www.w3.org/2000/svg\"\n                xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n                <title>编组</title>\n                <desc>Created with Sketch.</desc>\n                <defs>\n                    <polygon id=\"path-1\"\n                        points=\"1.78097075e-14 0.000125324675 192.540685 0.000125324675 192.540685 192.540058 1.78097075e-14 192.540058\">\n                    </polygon>\n                </defs>\n                <g id=\"\\u9875\\u9762-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\">\n                    <g id=\"\\u7F16\\u7EC4\">\n                        <mask id=\"mask-2\" fill=\"white\">\n                            <use xlink:href=\"#path-1\"></use>\n                        </mask>\n                        <g id=\"Clip-2\"></g>\n                        <path\n                            d=\"M172.473071,20.1164903 C154.306633,2.02148701 128.188344,-1.78097075e-14 96.2706558,-1.78097075e-14 C64.312237,-1.78097075e-14 38.155724,2.0452987 19.9974318,20.1872987 C1.84352597,38.3261656 1.78097075e-14,64.4406948 1.78097075e-14,96.3640227 C1.78097075e-14,128.286724 1.84352597,154.415039 20.0049513,172.556412 C38.1638701,190.704052 64.3141169,192.540058 96.2706558,192.540058 C128.225942,192.540058 154.376815,190.704052 172.53636,172.556412 C190.694653,154.409399 192.540685,128.286724 192.540685,96.3640227 C192.540685,64.3999643 190.672721,38.2553571 172.473071,20.1164903\"\n                            id=\"Fill-1\" fill=\"#FF6900\" mask=\"url(#mask-2)\"></path>\n                        <path\n                            d=\"M89.1841721,131.948836 C89.1841721,132.594885 88.640263,133.130648 87.9779221,133.130648 L71.5585097,133.130648 C70.8848896,133.130648 70.338474,132.594885 70.338474,131.948836 L70.338474,89.0100961 C70.338474,88.3584078 70.8848896,87.8251513 71.5585097,87.8251513 L87.9779221,87.8251513 C88.640263,87.8251513 89.1841721,88.3584078 89.1841721,89.0100961 L89.1841721,131.948836 Z\"\n                            id=\"Fill-3\" fill=\"#FFFFFF\" mask=\"url(#mask-2)\"></path>\n                        <path\n                            d=\"M121.332896,131.948836 C121.332896,132.594885 120.786481,133.130648 120.121633,133.130648 L104.492393,133.130648 C103.821906,133.130648 103.275491,132.594885 103.275491,131.948836 L103.275491,131.788421 L103.275491,94.9022357 C103.259198,88.4342292 102.889491,81.7863818 99.5502146,78.445226 C96.6790263,75.5652649 91.3251562,74.9054305 85.7557276,74.7669468 L57.4242049,74.7669468 C56.7555977,74.7669468 56.2154484,75.3045896 56.2154484,75.9512649 L56.2154484,128.074424 L56.2154484,131.948836 C56.2154484,132.594885 55.6640198,133.130648 54.9954127,133.130648 L39.3555198,133.130648 C38.6875393,133.130648 38.1498964,132.594885 38.1498964,131.948836 L38.1498964,60.5996188 C38.1498964,59.9447974 38.6875393,59.4121675 39.3555198,59.4121675 L84.4786692,59.4121675 C96.2717211,59.4121675 108.599909,59.9498104 114.680036,66.0380831 C120.786481,72.1533006 121.332896,84.4595571 121.332896,96.2657682 L121.332896,131.948836 Z\"\n                            id=\"Fill-5\" fill=\"#FFFFFF\" mask=\"url(#mask-2)\"></path>\n                        <path\n                            d=\"M153.53056,131.948836 C153.53056,132.594885 152.978505,133.130648 152.316164,133.130648 L136.678778,133.130648 C136.010797,133.130648 135.467515,132.594885 135.467515,131.948836 L135.467515,60.5996188 C135.467515,59.9447974 136.010797,59.4121675 136.678778,59.4121675 L152.316164,59.4121675 C152.978505,59.4121675 153.53056,59.9447974 153.53056,60.5996188 L153.53056,131.948836 Z\"\n                            id=\"Fill-7\" fill=\"#FFFFFF\" mask=\"url(#mask-2)\"></path>\n                    </g>\n                </g>\n            </svg>\n        </div>\n        <!-- TITLE -->\n        <div class=\"title-frame\">\n            <a id=\"titleArea\">TITLE_PLACEHOLDER</a>\n        </div>\n        <!-- CONTENT -->\n        <div class=\"content-frame\">\n            <a id=\"contentArea\">CONTENT_PLACEHOLDER</a>\n        </div>\n        <!-- BUTTON -->\n        <button onClick=\"window.close();\" id=\"buttonArea\">BUTTON_PLACEHOLDER</button>\n    </div>\n    <script>\n        if (STATUS_PLACEHOLDER) {\n            window.opener = null;\n            window.open('', '_self');\n            window.close();\n        }\n    </script>\n</body>\n\n</html>"
  },
  {
    "path": "custom_components/xiaomi_home/miot/specs/bool_trans.yaml",
    "content": "data:\n  urn:miot-spec-v2:property:air-cooler:000000EB: open_close\n  urn:miot-spec-v2:property:alarm:00000012: open_close\n  urn:miot-spec-v2:property:anion:00000025: open_close\n  urn:miot-spec-v2:property:anti-fake:00000130: yes_no\n  urn:miot-spec-v2:property:arrhythmia:000000B4: yes_no\n  urn:miot-spec-v2:property:auto-cleanup:00000124: open_close\n  urn:miot-spec-v2:property:auto-deodorization:00000125: open_close\n  urn:miot-spec-v2:property:auto-keep-warm:0000002B: open_close\n  urn:miot-spec-v2:property:automatic-feeding:000000F0: open_close\n  urn:miot-spec-v2:property:blow:000000CD: open_close\n  urn:miot-spec-v2:property:card-insertion-state:00000106: yes_no\n  urn:miot-spec-v2:property:contact-state:0000007C: contact_state\n  urn:miot-spec-v2:property:current-physical-control-lock:00000099: open_close\n  urn:miot-spec-v2:property:delay:0000014F: yes_no\n  urn:miot-spec-v2:property:deodorization:000000C6: open_close\n  urn:miot-spec-v2:property:dns-auto-mode:000000DC: open_close\n  urn:miot-spec-v2:property:driving-status:000000B9: yes_no\n  urn:miot-spec-v2:property:dryer:00000027: open_close\n  urn:miot-spec-v2:property:eco:00000024: open_close\n  urn:miot-spec-v2:property:glimmer-full-color:00000089: open_close\n  urn:miot-spec-v2:property:guard-mode:000000B6: open_close\n  urn:miot-spec-v2:property:heater:00000026: open_close\n  urn:miot-spec-v2:property:heating:000000C7: open_close\n  urn:miot-spec-v2:property:horizontal-swing:00000017: open_close\n  urn:miot-spec-v2:property:hot-water-recirculation:0000011C: open_close\n  urn:miot-spec-v2:property:image-distortion-correction:0000010F: open_close\n  urn:miot-spec-v2:property:local-storage:0000011E: yes_no\n  urn:miot-spec-v2:property:motion-detection:00000056: open_close\n  urn:miot-spec-v2:property:motion-state:0000007D: motion_state\n  urn:miot-spec-v2:property:motion-tracking:0000008A: open_close\n  urn:miot-spec-v2:property:motor-reverse:00000072: yes_no\n  urn:miot-spec-v2:property:mute:00000040: open_close\n  urn:miot-spec-v2:property:off-delay:00000053: open_close\n  urn:miot-spec-v2:property:on:00000006: open_close\n  urn:miot-spec-v2:property:physical-controls-locked:0000001D: open_close\n  urn:miot-spec-v2:property:plasma:00000132: yes_no\n  urn:miot-spec-v2:property:preheat:00000103: open_close\n  urn:miot-spec-v2:property:seating-state:000000B8: yes_no\n  urn:miot-spec-v2:property:silent-execution:000000FB: yes_no\n  urn:miot-spec-v2:property:sleep-aid-mode:0000010B: open_close\n  urn:miot-spec-v2:property:sleep-mode:00000028: open_close\n  urn:miot-spec-v2:property:snore-state:0000012A: yes_no\n  urn:miot-spec-v2:property:soft-wind:000000CF: open_close\n  urn:miot-spec-v2:property:speed-control:000000E8: open_close\n  urn:miot-spec-v2:property:submersion-state:0000007E: yes_no\n  urn:miot-spec-v2:property:time-watermark:00000087: open_close\n  urn:miot-spec-v2:property:un-straight-blowing:00000100: open_close\n  urn:miot-spec-v2:property:uv:00000029: open_close\n  urn:miot-spec-v2:property:valve-switch:000000FE: open_close\n  urn:miot-spec-v2:property:ventilation:000000CE: open_close\n  urn:miot-spec-v2:property:vertical-swing:00000018: open_close\n  urn:miot-spec-v2:property:wake-up-mode:00000107: open_close\n  urn:miot-spec-v2:property:water-pump:000000F2: open_close\n  urn:miot-spec-v2:property:watering:000000CC: open_close\n  urn:miot-spec-v2:property:wdr-mode:00000088: open_close\n  urn:miot-spec-v2:property:wet:0000002A: open_close\n  urn:miot-spec-v2:property:wifi-band-combine:000000E0: open_close\n  urn:miot-spec-v2:property:wifi-ssid-hidden:000000E3: yes_no\n  urn:miot-spec-v2:property:wind-reverse:00000117: yes_no\ntranslate:\n  default:\n    de:\n      'false': Falsch\n      'true': Wahr\n    en:\n      'false': 'False'\n      'true': 'True'\n    es:\n      'false': Falso\n      'true': Verdadero\n    fr:\n      'false': Faux\n      'true': Vrai\n    it:\n      'false': Falso\n      'true': Vero\n    ja:\n      'false': 偽\n      'true': 真\n    nl:\n      'false': 'False'\n      'true': 'True'\n    pt:\n      'false': 'False'\n      'true': 'True'\n    pt-BR:\n      'false': 'False'\n      'true': 'True'\n    ru:\n      'false': Ложь\n      'true': Истина\n    tr:\n      'false': Yanlış\n      'true': Doğru\n    zh-Hans:\n      'false': 假\n      'true': 真\n    zh-Hant:\n      'false': 假\n      'true': 真\n  contact_state:\n    de:\n      'false': Kein Kontakt\n      'true': Kontakt\n    en:\n      'false': No Contact\n      'true': Contact\n    es:\n      'false': Sin contacto\n      'true': Contacto\n    fr:\n      'false': Pas de contact\n      'true': Contact\n    it:\n      'false': Nessun contatto\n      'true': Contatto\n    ja:\n      'false': 非接触\n      'true': 接触\n    nl:\n      'false': Geen contact\n      'true': Contact\n    pt:\n      'false': Sem contato\n      'true': Contato\n    pt-BR:\n      'false': Sem contato\n      'true': Contato\n    ru:\n      'false': Нет контакта\n      'true': Контакт\n    tr:\n      'false': Temas Yok\n      'true': Temas\n    zh-Hans:\n      'false': 分离\n      'true': 接触\n    zh-Hant:\n      'false': 分離\n      'true': 接觸\n  motion_state:\n    de:\n      'false': Keine Bewegung erkannt\n      'true': Bewegung erkannt\n    en:\n      'false': No Motion Detected\n      'true': Motion Detected\n    es:\n      'false': No se detecta movimiento\n      'true': Movimiento detectado\n    fr:\n      'false': Aucun mouvement détecté\n      'true': Mouvement détecté\n    it:\n      'false': Nessun Movimento Rilevato\n      'true': Movimento Rilevato\n    ja:\n      'false': 動きが検出されません\n      'true': 動きを検知\n    nl:\n      'false': Geen contact\n      'true': Contact\n    pt:\n      'false': Sem contato\n      'true': Contato\n    pt-BR:\n      'false': Sem contato\n      'true': Contato\n    ru:\n      'false': Движение не обнаружено\n      'true': Обнаружено движение\n    tr:\n      'false': Hareket Algılanmadı\n      'true': Hareket Algılandı\n    zh-Hans:\n      'false': 无人\n      'true': 有人\n    zh-Hant:\n      'false': 無人\n      'true': 有人\n  open_close:\n    de:\n      'false': Schließen\n      'true': Öffnen\n    en:\n      'false': Close\n      'true': Open\n    es:\n      'false': Cerrado\n      'true': Abierto\n    fr:\n      'false': Fermer\n      'true': Ouvert\n    it:\n      'false': Chiuso\n      'true': Aperto\n    ja:\n      'false': 閉じる\n      'true': 開く\n    nl:\n      'false': Dicht\n      'true': Open\n    pt:\n      'false': Fechado\n      'true': Aberto\n    pt-BR:\n      'false': Fechado\n      'true': Aberto\n    ru:\n      'false': Закрыть\n      'true': Открыть\n    tr:\n      'false': Kapalı\n      'true': Açık\n    zh-Hans:\n      'false': 关闭\n      'true': 开启\n    zh-Hant:\n      'false': 關閉\n      'true': 開啟\n  yes_no:\n    de:\n      'false': Nein\n      'true': Ja\n    en:\n      'false': 'No'\n      'true': 'Yes'\n    es:\n      'false': 'No'\n      'true': Sí\n    fr:\n      'false': Non\n      'true': Oui\n    it:\n      'false': 'No'\n      'true': Si\n    ja:\n      'false': いいえ\n      'true': はい\n    nl:\n      'false': Nee\n      'true': Ja\n    pt:\n      'false': Não\n      'true': Sim\n    pt-BR:\n      'false': Não\n      'true': Sim\n    ru:\n      'false': Нет\n      'true': Да\n    tr:\n      'false': Hayır\n      'true': Evet\n    zh-Hans:\n      'false': 否\n      'true': 是\n    zh-Hant:\n      'false': 否\n      'true': 是\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/specs/multi_lang.json",
    "content": "{\n  \"urn:miot-spec-v2:device:bath-heater:0000A028:yeelink-v10\": {\n    \"en\": {\n      \"service:003:property:001:valuelist:000\": \"Idle\",\n      \"service:003:property:001:valuelist:001\": \"Dry\"\n    },\n    \"tr\": {\n      \"service:003:property:001:valuelist:000\": \"Boşta\",\n      \"service:003:property:001:valuelist:001\": \"Kurutma\"\n    }\n  },\n  \"urn:miot-spec-v2:device:electronic-valve:0000A0A7:lxzn-02\": {\n    \"tr\": {\n      \"service:004:property:001\": \"Yüksek Güç - Eşik Ayarı\"\n    },\n    \"zh-Hans\": {\n      \"service:004:property:001\": \"功率过高-阈值设置\"\n    }\n  },\n  \"urn:miot-spec-v2:device:electronic-valve:0000A0A7:ykcn-cbcs\": {\n    \"tr\": {\n      \"service:004:property:001\": \"Yüksek Güç - Eşik Ayarı\",\n      \"service:004:property:009\": \"Düşük Voltaj Alarmı - Eşik Ayarı\"\n    },\n    \"zh-Hans\": {\n      \"service:004:property:001\": \"功率过高-阈值设置\",\n      \"service:004:property:009\": \"欠压告警-阈值设置\"\n    }\n  },\n  \"urn:miot-spec-v2:device:fan:0000A005:zhimi-za1\": {\n    \"tr\": {\n      \"service:002:property:005:valuelist:000\": \"Doğal Rüzgar\",\n      \"service:002:property:005:valuelist:001\": \"Doğrudan Üfleme\"\n    },\n    \"zh-Hans\": {\n      \"service:002:property:005:valuelist:000\": \"自然风\",\n      \"service:002:property:005:valuelist:001\": \"直吹风\"\n    }\n  },\n  \"urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1\": {\n    \"de\": {\n      \"service:001\": \"Geräteinformationen\",\n      \"service:001:property:003\": \"Geräte-ID\",\n      \"service:001:property:005\": \"Seriennummer (SN)\",\n      \"service:002\": \"Gateway\",\n      \"service:002:event:001\": \"Netzwerk geändert\",\n      \"service:002:event:002\": \"Netzwerk geändert\",\n      \"service:002:property:001\": \"Zugriffsmethode\",\n      \"service:002:property:001:valuelist:000\": \"Kabelgebunden\",\n      \"service:002:property:001:valuelist:001\": \"5G Drahtlos\",\n      \"service:002:property:001:valuelist:002\": \"2.4G Drahtlos\",\n      \"service:002:property:002\": \"IP-Adresse\",\n      \"service:002:property:003\": \"WiFi-Netzwerkname\",\n      \"service:002:property:004\": \"Aktuelle Zeit\",\n      \"service:002:property:005\": \"DHCP-Server-MAC-Adresse\",\n      \"service:003\": \"Anzeigelampe\",\n      \"service:003:property:001\": \"Schalter\",\n      \"service:004\": \"Virtueller Dienst\",\n      \"service:004:action:001\": \"Virtuelles Ereignis erzeugen\",\n      \"service:004:event:001\": \"Virtuelles Ereignis aufgetreten\",\n      \"service:004:property:001\": \"Ereignisname\"\n    },\n    \"en\": {\n      \"service:001\": \"Device Information\",\n      \"service:001:property:003\": \"Device ID\",\n      \"service:001:property:005\": \"Serial Number (SN)\",\n      \"service:002\": \"Gateway\",\n      \"service:002:event:001\": \"Network Changed\",\n      \"service:002:event:002\": \"Network Changed\",\n      \"service:002:property:001\": \"Access Method\",\n      \"service:002:property:001:valuelist:000\": \"Wired\",\n      \"service:002:property:001:valuelist:001\": \"5G Wireless\",\n      \"service:002:property:001:valuelist:002\": \"2.4G Wireless\",\n      \"service:002:property:002\": \"IP Address\",\n      \"service:002:property:003\": \"WiFi Network Name\",\n      \"service:002:property:004\": \"Current Time\",\n      \"service:002:property:005\": \"DHCP Server MAC Address\",\n      \"service:003\": \"Indicator Light\",\n      \"service:003:property:001\": \"Switch\",\n      \"service:004\": \"Virtual Service\",\n      \"service:004:action:001\": \"Generate Virtual Event\",\n      \"service:004:event:001\": \"Virtual Event Occurred\",\n      \"service:004:property:001\": \"Event Name\"\n    },\n    \"es\": {\n      \"service:001\": \"Información del dispositivo\",\n      \"service:001:property:003\": \"ID del dispositivo\",\n      \"service:001:property:005\": \"Número de serie (SN)\",\n      \"service:002\": \"Puerta de enlace\",\n      \"service:002:event:001\": \"Cambio de red\",\n      \"service:002:event:002\": \"Cambio de red\",\n      \"service:002:property:001\": \"Método de acceso\",\n      \"service:002:property:001:valuelist:000\": \"Cableado\",\n      \"service:002:property:001:valuelist:001\": \"5G inalámbrico\",\n      \"service:002:property:001:valuelist:002\": \"2.4G inalámbrico\",\n      \"service:002:property:002\": \"Dirección IP\",\n      \"service:002:property:003\": \"Nombre de red WiFi\",\n      \"service:002:property:004\": \"Hora actual\",\n      \"service:002:property:005\": \"Dirección MAC del servidor DHCP\",\n      \"service:003\": \"Luz indicadora\",\n      \"service:003:property:001\": \"Interruptor\",\n      \"service:004\": \"Servicio virtual\",\n      \"service:004:action:001\": \"Generar evento virtual\",\n      \"service:004:event:001\": \"Ocurrió un evento virtual\",\n      \"service:004:property:001\": \"Nombre del evento\"\n    },\n    \"fr\": {\n      \"service:001\": \"Informations sur l'appareil\",\n      \"service:001:property:003\": \"ID de l'appareil\",\n      \"service:001:property:005\": \"Numéro de série (SN)\",\n      \"service:002\": \"Passerelle\",\n      \"service:002:event:001\": \"Changement de réseau\",\n      \"service:002:event:002\": \"Changement de réseau\",\n      \"service:002:property:001\": \"Méthode d'accès\",\n      \"service:002:property:001:valuelist:000\": \"Câblé\",\n      \"service:002:property:001:valuelist:001\": \"Sans fil 5G\",\n      \"service:002:property:001:valuelist:002\": \"Sans fil 2.4G\",\n      \"service:002:property:002\": \"Adresse IP\",\n      \"service:002:property:003\": \"Nom du réseau WiFi\",\n      \"service:002:property:004\": \"Heure actuelle\",\n      \"service:002:property:005\": \"Adresse MAC du serveur DHCP\",\n      \"service:003\": \"Voyant lumineux\",\n      \"service:003:property:001\": \"Interrupteur\",\n      \"service:004\": \"Service virtuel\",\n      \"service:004:action:001\": \"Générer un événement virtuel\",\n      \"service:004:event:001\": \"Événement virtuel survenu\",\n      \"service:004:property:001\": \"Nom de l'événement\"\n    },\n    \"ja\": {\n      \"service:001\": \"デバイス情報\",\n      \"service:001:property:003\": \"デバイスID\",\n      \"service:001:property:005\": \"シリアル番号 (SN)\",\n      \"service:002\": \"ゲートウェイ\",\n      \"service:002:event:001\": \"ネットワークが変更されました\",\n      \"service:002:event:002\": \"ネットワークが変更されました\",\n      \"service:002:property:001\": \"アクセス方法\",\n      \"service:002:property:001:valuelist:000\": \"有線\",\n      \"service:002:property:001:valuelist:001\": \"5G ワイヤレス\",\n      \"service:002:property:001:valuelist:002\": \"2.4G ワイヤレス\",\n      \"service:002:property:002\": \"IPアドレス\",\n      \"service:002:property:003\": \"WiFiネットワーク名\",\n      \"service:002:property:004\": \"現在の時間\",\n      \"service:002:property:005\": \"DHCPサーバーMACアドレス\",\n      \"service:003\": \"インジケータライト\",\n      \"service:003:property:001\": \"スイッチ\",\n      \"service:004\": \"バーチャルサービス\",\n      \"service:004:action:001\": \"バーチャルイベントを生成\",\n      \"service:004:event:001\": \"バーチャルイベントが発生しました\",\n      \"service:004:property:001\": \"イベント名\"\n    },\n    \"ru\": {\n      \"service:001\": \"Информация об устройстве\",\n      \"service:001:property:003\": \"ID устройства\",\n      \"service:001:property:005\": \"Серийный номер (SN)\",\n      \"service:002\": \"Шлюз\",\n      \"service:002:event:001\": \"Сеть изменена\",\n      \"service:002:event:002\": \"Сеть изменена\",\n      \"service:002:property:001\": \"Метод доступа\",\n      \"service:002:property:001:valuelist:000\": \"Проводной\",\n      \"service:002:property:001:valuelist:001\": \"5G Беспроводной\",\n      \"service:002:property:001:valuelist:002\": \"2.4G Беспроводной\",\n      \"service:002:property:002\": \"IP Адрес\",\n      \"service:002:property:003\": \"Название WiFi сети\",\n      \"service:002:property:004\": \"Текущее время\",\n      \"service:002:property:005\": \"MAC адрес DHCP сервера\",\n      \"service:003\": \"Световой индикатор\",\n      \"service:003:property:001\": \"Переключатель\",\n      \"service:004\": \"Виртуальная служба\",\n      \"service:004:action:001\": \"Создать виртуальное событие\",\n      \"service:004:event:001\": \"Произошло виртуальное событие\",\n      \"service:004:property:001\": \"Название события\"\n    },\n    \"tr\": {\n      \"service:001\": \"Cihaz Bilgileri\",\n      \"service:001:property:003\": \"Cihaz ID\",\n      \"service:001:property:005\": \"Seri Numarası (SN)\",\n      \"service:002\": \"Ağ Geçidi\",\n      \"service:002:event:001\": \"Ağ Değişti\",\n      \"service:002:event:002\": \"Ağ Değişti\",\n      \"service:002:property:001\": \"Erişim Yöntemi\",\n      \"service:002:property:001:valuelist:000\": \"Kablolu\",\n      \"service:002:property:001:valuelist:001\": \"5G Kablosuz\",\n      \"service:002:property:001:valuelist:002\": \"2.4G Kablosuz\",\n      \"service:002:property:002\": \"IP Adresi\",\n      \"service:002:property:003\": \"WiFi Ağ Adı\",\n      \"service:002:property:004\": \"Geçerli Zaman\",\n      \"service:002:property:005\": \"DHCP Sunucusu MAC Adresi\",\n      \"service:003\": \"Gösterge Işığı\",\n      \"service:003:property:001\": \"Anahtar\",\n      \"service:004\": \"Sanal Hizmet\",\n      \"service:004:action:001\": \"Sanal Olay Oluştur\",\n      \"service:004:event:001\": \"Sanal Olay Gerçekleşti\",\n      \"service:004:property:001\": \"Olay Adı\"\n    },\n    \"zh-Hant\": {\n      \"service:001\": \"設備信息\",\n      \"service:001:property:003\": \"設備ID\",\n      \"service:001:property:005\": \"序號 (SN)\",\n      \"service:002\": \"網關\",\n      \"service:002:event:001\": \"網路發生變化\",\n      \"service:002:event:002\": \"網路發生變化\",\n      \"service:002:property:001\": \"接入方式\",\n      \"service:002:property:001:valuelist:000\": \"有線\",\n      \"service:002:property:001:valuelist:001\": \"5G 無線\",\n      \"service:002:property:001:valuelist:002\": \"2.4G 無線\",\n      \"service:002:property:002\": \"IP地址\",\n      \"service:002:property:003\": \"WiFi網路名稱\",\n      \"service:002:property:004\": \"當前時間\",\n      \"service:002:property:005\": \"DHCP伺服器MAC地址\",\n      \"service:003\": \"指示燈\",\n      \"service:003:property:001\": \"開關\",\n      \"service:004\": \"虛擬服務\",\n      \"service:004:action:001\": \"產生虛擬事件\",\n      \"service:004:event:001\": \"虛擬事件發生\",\n      \"service:004:property:001\": \"事件名稱\"\n    }\n  },\n  \"urn:miot-spec-v2:device:lock:0000A038:loock-t2pv1\": {\n    \"tr\": {\n      \"service:003:property:1021:valuelist:000\": \"Kilitli\",\n      \"service:003:property:1021:valuelist:001\": \"Kilitli (Çocuk Kilidi)\",\n      \"service:003:property:1021:valuelist:002\": \"Kilitli (İçten Kilitli)\",\n      \"service:003:property:1021:valuelist:003\": \"Kilitli (İçten Kilitli + Çocuk Kilidi)\",\n      \"service:003:property:1021:valuelist:004\": \"Kilidi Açık\",\n      \"service:003:property:1021:valuelist:008\": \"Kapı Kapalı Değil (Zaman Aşımı)\",\n      \"service:003:property:1021:valuelist:012\": \"Kapı Aralık\"\n    },\n    \"zh-Hans\": {\n      \"service:003:property:1021:valuelist:000\": \"已上锁\",\n      \"service:003:property:1021:valuelist:001\": \"已上锁(童锁)\",\n      \"service:003:property:1021:valuelist:002\": \"已上锁(反锁)\",\n      \"service:003:property:1021:valuelist:003\": \"已上锁(反锁+童锁)\",\n      \"service:003:property:1021:valuelist:004\": \"已开锁\",\n      \"service:003:property:1021:valuelist:008\": \"门未关(门超时未关)\",\n      \"service:003:property:1021:valuelist:012\": \"门虚掩\"\n    }\n  },\n  \"urn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1\": {\n    \"en\": {\n      \"service:002:property:001\": \"Soil Moisture\"\n    },\n    \"tr\": {\n      \"service:002:property:001\": \"Toprak Nemi\",\n      \"service:002:property:003\": \"Işık Yoğunluğu\"\n    },\n    \"zh-Hans\": {\n      \"service:002:property:001\": \"土壤湿度\",\n      \"service:002:property:003\": \"光照强度\"\n    }\n  },\n  \"urn:miot-spec-v2:device:switch:0000A003:090615-x1tpm\": {\n    \"en\": {\n      \"service:027:property:001\": \"Fan Switch\",\n      \"service:027:property:003\": \"Light Switch\",\n      \"service:027:property:004\": \"Fan and Light Switch\"\n    },\n    \"tr\": {\n      \"service:027:property:001\": \"Vantilatör Anahtarı\",\n      \"service:027:property:003\": \"Işık Anahtarı\",\n      \"service:027:property:004\": \"Vantilatör ve Işık Anahtarı\"\n    }\n  },\n  \"urn:miot-spec-v2:device:switch:0000A003:lumi-acn040\": {\n    \"en\": {\n      \"service:011\": \"Right Button On and Off\",\n      \"service:011:property:001\": \"Right Button On and Off\",\n      \"service:015:action:001\": \"Left Button Identify\",\n      \"service:016:action:001\": \"Middle Button Identify\",\n      \"service:017:action:001\": \"Right Button Identify\"\n    },\n    \"tr\": {\n      \"service:011\": \"Sağ Düğme Açma ve Kapama\",\n      \"service:011:property:001\": \"Sağ Düğme Açma ve Kapama\",\n      \"service:015:action:001\": \"Sol Düğme Tanımlama\",\n      \"service:016:action:001\": \"Orta Düğme Tanımlama\",\n      \"service:017:action:001\": \"Sağ Düğme Tanımlama\"\n    },\n    \"zh-Hans\": {\n      \"service:015:action:001\": \"左键确认\",\n      \"service:016:action:001\": \"中键确认\",\n      \"service:017:action:001\": \"右键确认\"\n    }\n  },\n  \"urn:miot-spec-v2:device:thermostat:0000A031:cubee-th123e\": {\n    \"ru\": {\n      \"service:002\": \"термостат\",\n      \"service:002:property:001\": \"выключатель\",\n      \"service:002:property:002\": \"режим нагрузки\",\n      \"service:002:property:002:valuelist:000\": \"без подогрева\",\n      \"service:002:property:002:valuelist:001\": \"нагрев\",\n      \"service:002:property:003\": \"неисправность\",\n      \"service:002:property:003:valuelist:000\": \"ошибка датчика\",\n      \"service:002:property:003:valuelist:001\": \"без ошибок\",\n      \"service:002:property:003:valuelist:002\": \"защита от высоких температур\",\n      \"service:002:property:003:valuelist:003\": \"криогенная защита\",\n      \"service:002:property:004\": \"режим\",\n      \"service:002:property:004:valuelist:000\": \"ручной режим\",\n      \"service:002:property:004:valuelist:001\": \"домашний режим\",\n      \"service:002:property:004:valuelist:002\": \"режим выхода из дома\",\n      \"service:002:property:004:valuelist:003\": \"автоматический режим\",\n      \"service:002:property:004:valuelist:004\": \"Режим сна\",\n      \"service:002:property:005\": \"температура цели\",\n      \"service:002:property:007\": \"текущая температура\",\n      \"service:004\": \"Пользовательские услуги\",\n      \"service:004:property:001\": \"детский замок\",\n      \"service:004:property:002\": \"тип датчика\",\n      \"service:004:property:002:valuelist:000\": \"внутренний  датчик\",\n      \"service:004:property:002:valuelist:001\": \"выносной датчик\",\n      \"service:004:property:002:valuelist:002\": \"встроенный и внешний датчик\",\n      \"service:004:property:003\": \"пусковая разность температур\",\n      \"service:004:property:004\": \"компенсационная температура\",\n      \"service:004:property:005\": \"температура выносного датчика\",\n      \"service:004:property:006\": \"максимальная температура цели\",\n      \"service:004:property:007\": \"минимальная температура цели \"\n    },\n    \"tr\": {\n      \"service:002\": \"Termostat\",\n      \"service:002:property:001\": \"Anahtar\",\n      \"service:002:property:002\": \"Yük Modu\",\n      \"service:002:property:002:valuelist:000\": \"Isıtmasız\",\n      \"service:002:property:002:valuelist:001\": \"Isıtma\",\n      \"service:002:property:003\": \"Arıza\",\n      \"service:002:property:003:valuelist:000\": \"Sensör Hatası\",\n      \"service:002:property:003:valuelist:001\": \"Hata Yok\",\n      \"service:002:property:003:valuelist:002\": \"Yüksek Sıcaklık Koruması\",\n      \"service:002:property:003:valuelist:003\": \"Düşük Sıcaklık Koruması\",\n      \"service:002:property:004\": \"Mod\",\n      \"service:002:property:004:valuelist:000\": \"Manuel Mod\",\n      \"service:002:property:004:valuelist:001\": \"Ev Modu\",\n      \"service:002:property:004:valuelist:002\": \"Dışarıda Modu\",\n      \"service:002:property:004:valuelist:003\": \"Otomatik Mod\",\n      \"service:002:property:004:valuelist:004\": \"Uyku Modu\",\n      \"service:002:property:005\": \"Hedef Sıcaklık\",\n      \"service:002:property:007\": \"Geçerli Sıcaklık\",\n      \"service:004\": \"Özel Hizmetler\",\n      \"service:004:property:001\": \"Çocuk Kilidi\",\n      \"service:004:property:002\": \"Sensör Tipi\",\n      \"service:004:property:002:valuelist:000\": \"Dahili Sensör\",\n      \"service:004:property:002:valuelist:001\": \"Harici Sensör\",\n      \"service:004:property:002:valuelist:002\": \"Dahili ve Harici Sensör\",\n      \"service:004:property:003\": \"Başlangıç Sıcaklık Farkı\",\n      \"service:004:property:004\": \"Telafi Sıcaklığı\",\n      \"service:004:property:005\": \"Harici Sensör Sıcaklığı\",\n      \"service:004:property:006\": \"Maksimum Hedef Sıcaklık\",\n      \"service:004:property:007\": \"Minimum Hedef Sıcaklık\"\n    }\n  },\n  \"urn:miot-spec-v2:device:thermostat:0000A031:cubee-th123w\": {\n    \"ru\": {\n      \"service:002\": \"термостат\",\n      \"service:002:property:001\": \"выключатель\",\n      \"service:002:property:002\": \"режим нагрузки\",\n      \"service:002:property:002:valuelist:000\": \"нагрев\",\n      \"service:002:property:002:valuelist:001\": \"без подогрева\",\n      \"service:002:property:003\": \"неисправность\",\n      \"service:002:property:003:valuelist:000\": \"без ошибок\",\n      \"service:002:property:003:valuelist:001\": \"ошибка датчика\",\n      \"service:002:property:003:valuelist:002\": \"защита от высоких температур\",\n      \"service:002:property:003:valuelist:003\": \"криогенная защита\",\n      \"service:002:property:004\": \"режим\",\n      \"service:002:property:004:valuelist:000\": \"ручной режим\",\n      \"service:002:property:004:valuelist:001\": \"домашний режим\",\n      \"service:002:property:004:valuelist:002\": \"режим выхода из дома\",\n      \"service:002:property:004:valuelist:003\": \"автоматический режим\",\n      \"service:002:property:004:valuelist:004\": \"Режим сна\",\n      \"service:002:property:005\": \"температура цели\",\n      \"service:002:property:007\": \"текущая температура\",\n      \"service:004\": \"Пользовательские услуги\",\n      \"service:004:property:001\": \"детский замок\",\n      \"service:004:property:002\": \"тип датчика\",\n      \"service:004:property:002:valuelist:000\": \"внутренний  датчик\",\n      \"service:004:property:002:valuelist:001\": \"выносной датчик\",\n      \"service:004:property:002:valuelist:002\": \"встроенный и внешний датчик\",\n      \"service:004:property:003\": \"пусковая разность температур\",\n      \"service:004:property:004\": \"компенсационная температура\",\n      \"service:004:property:005\": \"температура выносного датчика\",\n      \"service:004:property:006\": \"максимальная температура цели\",\n      \"service:004:property:007\": \"минимальная температура цели \"\n    },\n    \"tr\": {\n      \"service:002\": \"Termostat\",\n      \"service:002:property:001\": \"Anahtar\",\n      \"service:002:property:002\": \"Yük Modu\",\n      \"service:002:property:002:valuelist:000\": \"Isıtma\",\n      \"service:002:property:002:valuelist:001\": \"Isıtmasız\",\n      \"service:002:property:003\": \"Arıza\",\n      \"service:002:property:003:valuelist:000\": \"Hata Yok\",\n      \"service:002:property:003:valuelist:001\": \"Sensör Hatası\",\n      \"service:002:property:003:valuelist:002\": \"Yüksek Sıcaklık Koruması\",\n      \"service:002:property:003:valuelist:003\": \"Düşük Sıcaklık Koruması\",\n      \"service:002:property:004\": \"Mod\",\n      \"service:002:property:004:valuelist:000\": \"Manuel Mod\",\n      \"service:002:property:004:valuelist:001\": \"Ev Modu\",\n      \"service:002:property:004:valuelist:002\": \"Dışarıda Modu\",\n      \"service:002:property:004:valuelist:003\": \"Otomatik Mod\",\n      \"service:002:property:004:valuelist:004\": \"Uyku Modu\",\n      \"service:002:property:005\": \"Hedef Sıcaklık\",\n      \"service:002:property:007\": \"Geçerli Sıcaklık\",\n      \"service:004\": \"Özel Hizmetler\",\n      \"service:004:property:001\": \"Çocuk Kilidi\",\n      \"service:004:property:002\": \"Sensör Tipi\",\n      \"service:004:property:002:valuelist:000\": \"Dahili Sensör\",\n      \"service:004:property:002:valuelist:001\": \"Harici Sensör\",\n      \"service:004:property:002:valuelist:002\": \"Dahili ve Harici Sensör\",\n      \"service:004:property:003\": \"Başlangıç Sıcaklık Farkı\",\n      \"service:004:property:004\": \"Telafi Sıcaklığı\",\n      \"service:004:property:005\": \"Harici Sensör Sıcaklığı\",\n      \"service:004:property:006\": \"Maksimum Hedef Sıcaklık\",\n      \"service:004:property:007\": \"Minimum Hedef Sıcaklık\"\n    }\n  },\n  \"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01\": {\n    \"en\": {\n      \"service:002\": \"Thermostat\",\n      \"service:002:property:002\": \"Air Conditioner Mode\",\n      \"service:004\": \"Air Conditioner\"\n    },\n    \"tr\": {\n      \"service:002\": \"Yerden Isıtma\",\n      \"service:002:property:002\": \"Klima Modu\",\n      \"service:004\": \"Klima\"\n    },\n    \"zh-Hans\": {\n      \"service:002\": \"地暖\",\n      \"service:004\": \"空调\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:ijai-v1\": {\n    \"tr\": {\n      \"service:007:property:005:valuelist:000\": \"Sessiz\",\n      \"service:007:property:005:valuelist:001\": \"Standart\",\n      \"service:007:property:005:valuelist:002\": \"Orta\",\n      \"service:007:property:005:valuelist:003\": \"Güçlü\"\n    },\n    \"zh-Hans\": {\n      \"service:007:property:005:valuelist:000\": \"安静\",\n      \"service:007:property:005:valuelist:001\": \"标准\",\n      \"service:007:property:005:valuelist:002\": \"中档\",\n      \"service:007:property:005:valuelist:003\": \"强力\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:narwa-001\": {\n    \"tr\": {\n      \"service:001\": \"Cihaz Bilgileri\",\n      \"service:002\": \"Pil\",\n      \"service:002:property:001\": \"Pil Seviyesi\",\n      \"service:003\": \"Robot Süpürge\",\n      \"service:003:property:001\": \"Çalışma Durumu\",\n      \"service:004\": \"Temizleme\",\n      \"service:004:property:001\": \"Temizlik Modu\",\n      \"service:004:property:002\": \"Emme Gücü\",\n      \"service:005\": \"Harita Yönetimi\",\n      \"service:005:property:001\": \"Harita Listesi\",\n      \"service:005:property:002\": \"Aktif Harita\",\n      \"service:005:property:003\": \"Harita Adı\",\n      \"service:006\": \"Tüketim Malzemeleri\",\n      \"service:006:property:001\": \"Ana Fırça Ömrü\",\n      \"service:006:property:002\": \"Yan Fırça Ömrü\",\n      \"service:006:property:003\": \"Filtre Ömrü\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:narwa-ax11\": {\n    \"tr\": {\n      \"service:001\": \"Cihaz Bilgileri\",\n      \"service:002\": \"Pil\",\n      \"service:002:property:001\": \"Pil Seviyesi\",\n      \"service:003\": \"Robot Süpürge\",\n      \"service:003:property:001\": \"Çalışma Durumu\",\n      \"service:004\": \"Temizleme\",\n      \"service:004:property:001\": \"Temizlik Modu\",\n      \"service:004:property:002\": \"Emme Gücü\",\n      \"service:005\": \"Harita Yönetimi\",\n      \"service:005:property:001\": \"Harita Listesi\",\n      \"service:005:property:002\": \"Aktif Harita\",\n      \"service:005:property:003\": \"Harita Adı\",\n      \"service:006\": \"Tüketim Malzemeleri\",\n      \"service:006:property:001\": \"Ana Fırça Ömrü\",\n      \"service:006:property:002\": \"Yan Fırça Ömrü\",\n      \"service:006:property:003\": \"Filtre Ömrü\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:roidmi-v60\": {\n    \"tr\": {\n      \"service:001\": \"Cihaz Bilgileri\",\n      \"service:002\": \"Pil\",\n      \"service:002:property:001\": \"Pil Seviyesi\",\n      \"service:003\": \"Robot Süpürge\",\n      \"service:003:property:001\": \"Çalışma Durumu\",\n      \"service:004\": \"Temizleme\",\n      \"service:004:property:001\": \"Temizlik Modu\",\n      \"service:004:property:002\": \"Emme Gücü\",\n      \"service:004:property:003\": \"Temizlik Süresi\",\n      \"service:004:property:004\": \"Temizlenen Alan\",\n      \"service:005\": \"Harita Yönetimi\",\n      \"service:005:property:001\": \"Harita Listesi\",\n      \"service:005:property:002\": \"Aktif Harita\",\n      \"service:005:property:003\": \"Harita Adı\",\n      \"service:006\": \"Tüketim Malzemeleri\",\n      \"service:006:property:001\": \"Ana Fırça Ömrü\",\n      \"service:006:property:002\": \"Yan Fırça Ömrü\",\n      \"service:006:property:003\": \"Filtre Ömrü\",\n      \"service:007\": \"Emme Gücü\",\n      \"service:007:property:001\": \"Güç Seviyesi\",\n      \"service:007:property:001:valuelist:000\": \"Sessiz\",\n      \"service:007:property:001:valuelist:001\": \"Standart\",\n      \"service:007:property:001:valuelist:002\": \"Orta\",\n      \"service:007:property:001:valuelist:003\": \"Güçlü\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:viomi-v38\": {\n    \"tr\": {\n      \"service:001\": \"Cihaz Bilgileri\",\n      \"service:002\": \"Pil\",\n      \"service:002:property:001\": \"Pil Seviyesi\",\n      \"service:002:property:002\": \"Şarj Durumu\",\n      \"service:003\": \"Robot Süpürge\",\n      \"service:003:property:001\": \"Çalışma Durumu\",\n      \"service:003:property:002\": \"Hata Kodu\",\n      \"service:004\": \"Temizleme\",\n      \"service:004:property:001\": \"Temizlik Modu\",\n      \"service:004:property:002\": \"Emme Gücü\",\n      \"service:004:property:003\": \"Su Seviyesi\",\n      \"service:004:property:004\": \"Temizlik Süresi\",\n      \"service:004:property:005\": \"Temizlenen Alan\",\n      \"service:005\": \"Tüketim Malzemeleri\",\n      \"service:005:property:001\": \"Ana Fırça Ömrü\",\n      \"service:005:property:002\": \"Yan Fırça Ömrü\",\n      \"service:005:property:003\": \"Filtre Ömrü\",\n      \"service:005:property:004\": \"Sensör Ömrü\",\n      \"service:005:property:005\": \"Paspas Bezi Ömrü\",\n      \"service:006\": \"Harita Yönetimi\",\n      \"service:006:property:001\": \"Harita Listesi\",\n      \"service:006:property:002\": \"Aktif Harita\",\n      \"service:006:property:003\": \"Harita Adı\",\n      \"service:006:property:004\": \"Harita ID\",\n      \"service:007\": \"Emme Gücü\",\n      \"service:007:property:001\": \"Güç Seviyesi\",\n      \"service:007:property:001:valuelist:000\": \"Sessiz\",\n      \"service:007:property:001:valuelist:001\": \"Standart\",\n      \"service:007:property:001:valuelist:002\": \"Orta\",\n      \"service:007:property:001:valuelist:003\": \"Güçlü\",\n      \"service:007:property:001:valuelist:004\": \"Maksimum\",\n      \"service:008\": \"Su Tankı\",\n      \"service:008:property:001\": \"Su Seviyesi\",\n      \"service:008:property:001:valuelist:000\": \"Düşük\",\n      \"service:008:property:001:valuelist:001\": \"Orta\",\n      \"service:008:property:001:valuelist:002\": \"Yüksek\",\n      \"service:009\": \"Bölge Temizliği\",\n      \"service:009:property:001\": \"Oda Seçimi\",\n      \"service:009:property:002\": \"Bölge Temizliği\",\n      \"service:010\": \"Zamanlama\",\n      \"service:010:property:001\": \"Zamanlayıcı\",\n      \"service:010:property:002\": \"Temizlik Zamanı\",\n      \"service:011\": \"Ses ve Bildirim\",\n      \"service:011:property:001\": \"Ses Seviyesi\",\n      \"service:011:property:002\": \"Sesli Bildirimler\",\n      \"service:012\": \"Gelişmiş Ayarlar\",\n      \"service:012:property:001\": \"Halı Modu\",\n      \"service:012:property:002\": \"Tekrar Geçiş\",\n      \"service:012:property:003\": \"Kenar Temizliği\",\n      \"service:012:property:004\": \"Engel Algılama\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:viomi-v5\": {\n    \"tr\": {\n      \"service:001\": \"Cihaz Bilgileri\",\n      \"service:002\": \"Pil\",\n      \"service:002:property:001\": \"Pil Seviyesi\",\n      \"service:002:property:002\": \"Şarj Durumu\",\n      \"service:003\": \"Robot Süpürge\",\n      \"service:003:property:001\": \"Çalışma Durumu\",\n      \"service:003:property:002\": \"Hata Kodu\",\n      \"service:004\": \"Temizleme\",\n      \"service:004:property:001\": \"Temizlik Modu\",\n      \"service:004:property:002\": \"Emme Gücü\",\n      \"service:004:property:003\": \"Su Seviyesi\",\n      \"service:004:property:004\": \"Temizlik Süresi\",\n      \"service:004:property:005\": \"Temizlenen Alan\",\n      \"service:005\": \"Tüketim Malzemeleri\",\n      \"service:005:property:001\": \"Ana Fırça Ömrü\",\n      \"service:005:property:002\": \"Yan Fırça Ömrü\",\n      \"service:005:property:003\": \"Filtre Ömrü\",\n      \"service:005:property:004\": \"Sensör Ömrü\",\n      \"service:005:property:005\": \"Paspas Bezi Ömrü\",\n      \"service:006\": \"Harita Yönetimi\",\n      \"service:006:property:001\": \"Harita Listesi\",\n      \"service:006:property:002\": \"Aktif Harita\",\n      \"service:006:property:003\": \"Harita Adı\",\n      \"service:006:property:004\": \"Harita ID\",\n      \"service:007\": \"Emme Gücü\",\n      \"service:007:property:001\": \"Güç Seviyesi\",\n      \"service:007:property:001:valuelist:000\": \"Sessiz\",\n      \"service:007:property:001:valuelist:001\": \"Standart\",\n      \"service:007:property:001:valuelist:002\": \"Orta\",\n      \"service:007:property:001:valuelist:003\": \"Güçlü\",\n      \"service:007:property:001:valuelist:004\": \"Maksimum\",\n      \"service:008\": \"Su Tankı\",\n      \"service:008:property:001\": \"Su Seviyesi\",\n      \"service:008:property:001:valuelist:000\": \"Düşük\",\n      \"service:008:property:001:valuelist:001\": \"Orta\",\n      \"service:008:property:001:valuelist:002\": \"Yüksek\",\n      \"service:009\": \"Bölge Temizliği\",\n      \"service:009:property:001\": \"Oda Seçimi\",\n      \"service:009:property:002\": \"Bölge Temizliği\",\n      \"service:010\": \"Zamanlama\",\n      \"service:010:property:001\": \"Zamanlayıcı\",\n      \"service:010:property:002\": \"Temizlik Zamanı\",\n      \"service:011\": \"Ses ve Bildirim\",\n      \"service:011:property:001\": \"Ses Seviyesi\",\n      \"service:011:property:002\": \"Sesli Bildirimler\",\n      \"service:012\": \"Gelişmiş Ayarlar\",\n      \"service:012:property:001\": \"Halı Modu\",\n      \"service:012:property:002\": \"Tekrar Geçiş\",\n      \"service:012:property:003\": \"Kenar Temizliği\",\n      \"service:012:property:004\": \"Engel Algılama\"\n    }\n  },\n  \"urn:miot-spec-v2:device:vacuum:0000A006:xiaomi-ov31cn\": {\n    \"ru\": {\n      \"service:002\": \"РП\",\n      \"service:002:action:001\": \"Начать подметание перед влажной уборкой\",\n      \"service:002:action:002\": \"Остановить уборку\",\n      \"service:002:action:003\": \"Остановить уборку и Домой\",\n      \"service:002:action:004\": \"Начать подметание\",\n      \"service:002:action:005\": \"Начать влажную уборку\",\n      \"service:002:action:006\": \"Начать подметание и влажную уборку\",\n      \"service:002:action:018\": \"Станция Очистка мешка от пыли\",\n      \"service:002:action:019\": \"Станция Очистить губку\",\n      \"service:002:action:020\": \"Станция Сушка воздухом\",\n      \"service:002:property:001\": \"ПО\",\n      \"service:002:property:002\": \"Статус\",\n      \"service:002:property:003\": \"Ошибки\",\n      \"service:002:property:004\": \"Режим уборки\",\n      \"service:002:property:004:valuelist:000\": \"Подметание\",\n      \"service:002:property:004:valuelist:001\": \"Влажная уборка\",\n      \"service:002:property:004:valuelist:002\": \"Подметание и влажная уборка\",\n      \"service:002:property:004:valuelist:003\": \"Подметание перед влажной уборкой\",\n      \"service:002:property:005\": \"Выбор зоны уборки\",\n      \"service:002:property:006\": \"Зона уборки\",\n      \"service:002:property:007\": \"Время уборки\",\n      \"service:002:property:008\": \"Количество уборок\",\n      \"service:002:property:008:valuelist:000\": \"Один раз\",\n      \"service:002:property:008:valuelist:001\": \"Два раза\",\n      \"service:002:property:008:valuelist:002\": \"Три раза\",\n      \"service:002:property:009\": \"Мощность уборки\",\n      \"service:002:property:009:valuelist:000\": \"Бесшумно\",\n      \"service:002:property:009:valuelist:001\": \"Стандарт\",\n      \"service:002:property:009:valuelist:002\": \"Интенсивно\",\n      \"service:002:property:009:valuelist:003\": \"Турбо\",\n      \"service:002:property:010\": \"Уровень смачивания губок\",\n      \"service:002:property:010:valuelist:000\": \"Выключено\",\n      \"service:002:property:010:valuelist:001\": \"Уровень 1\",\n      \"service:002:property:010:valuelist:002\": \"Уровень 2\",\n      \"service:002:property:010:valuelist:003\": \"Уровень 3\",\n      \"service:002:property:020\": \"Ковёр Повышение мощности на коврах\",\n      \"service:002:property:021\": \"Ковёр Избегать ковров\",\n      \"service:002:property:023\": \"Возобновления уборки после зарядки\",\n      \"service:002:property:028\": \"Автоматическая очистка губки\",\n      \"service:002:property:029\": \"Тип возвращения для очистки губки\",\n      \"service:002:property:029:valuelist:000\": \"По комнатам\",\n      \"service:002:property:029:valuelist:001\": \"По зонам\",\n      \"service:002:property:029:valuelist:002\": \"По времени\",\n      \"service:002:property:030\": \"Подача воды для очистки губок\",\n      \"service:002:property:030:valuelist:000\": \"Глубокая очистка губок\",\n      \"service:002:property:030:valuelist:001\": \"Ежедневная очистка губок\",\n      \"service:002:property:030:valuelist:002\": \"Экономия воды\",\n      \"service:002:property:031\": \"Время сушки\",\n      \"service:002:property:031:valuelist:000\": \"2 часа\",\n      \"service:002:property:031:valuelist:001\": \"3 часа\",\n      \"service:002:property:031:valuelist:002\": \"4 часа\",\n      \"service:002:property:032\": \"Автоматическая очистка от пыли\",\n      \"service:002:property:033\": \"Частота очистки от пыли\",\n      \"service:002:property:033:valuelist:000\": \"Каждый раз\",\n      \"service:002:property:033:valuelist:001\": \"Через раз\",\n      \"service:002:property:033:valuelist:002\": \"Каждый третий раз\",\n      \"service:002:property:034\": \"Автоматическая сушка губок\",\n      \"service:002:property:036\": \"Использование моющего средства\",\n      \"service:002:property:041\": \"Мытье губок горячей водой\",\n      \"service:002:property:049\": \"Запуск самоочистки\",\n      \"service:002:property:059\": \"Подача моющего средства\",\n      \"service:002:property:060\": \"Уровень смачивания губок вначале уборки\",\n      \"service:002:property:060:valuelist:000\": \"Ежедневное\",\n      \"service:002:property:060:valuelist:001\": \"Глубокое\",\n      \"service:002:property:061\": \"Частота возвращения для очистки губки по зонам\",\n      \"service:002:property:061:valuelist:000\": \"5 м²\",\n      \"service:002:property:061:valuelist:002\": \"8 м²\",\n      \"service:002:property:061:valuelist:001\": \"10 м²\",\n      \"service:002:property:061:valuelist:003\": \"15 м²\",\n      \"service:002:property:061:valuelist:004\": \"20 м²\",\n      \"service:002:property:061:valuelist:005\": \"25 м²\",\n      \"service:002:property:071\": \"Напоминание об окончании моющего средства\",\n      \"service:002:property:073\": \"Ковёр Параметры очистки\",\n      \"service:002:property:073:valuelist:000\": \"Только уборка пыли на коврах\",\n      \"service:002:property:073:valuelist:001\": \"Обход ковра\",\n      \"service:002:property:073:valuelist:002\": \"Игнорировать\",\n      \"service:002:property:073:valuelist:003\": \"Пересечение ковра\",\n      \"service:002:property:074\": \"Маршрут убоки\",\n      \"service:002:property:074:valuelist:000\": \"Быстрая уборка\",\n      \"service:002:property:074:valuelist:001\": \"Стандартная уборка\",\n      \"service:002:property:074:valuelist:002\": \"Тщательная уборка\",\n      \"service:002:property:075\": \"Стратегия обхода препятствий\",\n      \"service:002:property:075:valuelist:000\": \"Режим `Минимум столкновений`\",\n      \"service:002:property:075:valuelist:001\": \"Режим большого охвата\",\n      \"service:002:property:076\": \"Ковёр Глубокая очистка\",\n      \"service:002:property:077\": \"Ковёр Распознавание ковров\",\n      \"service:002:property:078\": \"Ковёр Сначала очистить ковёр\",\n      \"service:002:property:079\": \"Акцент на краях и углах Вращение губок\",\n      \"service:002:property:080\": \"Акцент на краях и углах\",\n      \"service:002:property:080:valuelist:000\": \"Один раз в 7 дней\",\n      \"service:002:property:080:valuelist:001\": \"Каждую уборку\",\n      \"service:002:property:081\": \"Частота возвращения для очистки губки по времени\",\n      \"service:002:property:082\": \"Мощность опустошения мешка от пыли\",\n      \"service:002:property:082:valuelist:000\": \"Бесшумно\",\n      \"service:002:property:082:valuelist:001\": \"Стандарт\",\n      \"service:002:property:082:valuelist:002\": \"Интенсивно\",\n      \"service:002:property:083\": \"Быстрый доступ\",\n      \"service:002:property:083:valuelist:000\": \"Тихая уборка\",\n      \"service:002:property:083:valuelist:001\": \"Глубокая очистка\",\n      \"service:002:property:083:valuelist:002\": \"Общие настройки\",\n      \"service:002:property:083:valuelist:003\": \"ИИ Режим убощика\",\n      \"service:002:property:083:valuelist:004\": \"Режим домашних животных\",\n      \"service:002:property:084\": \"Температура воды для очистки губок\",\n      \"service:002:property:084:valuelist:000\": \"Комнатная\",\n      \"service:002:property:084:valuelist:001\": \"Теплая\",\n      \"service:002:property:084:valuelist:002\": \"Горячая\",\n      \"service:002:property:084:valuelist:003\": \"Интелектуальная\",\n      \"service:002:property:091\": \"Акцент на краях и углах Вращение боковой щетки\",\n      \"service:002:property:092\": \"Повторное мытье губок\",\n      \"service:003\": \"Батарея\",\n      \"service:003:property:001\": \"Уровень заряда\",\n      \"service:003:property:002\": \"Состояние зарядки\",\n      \"service:003:property:002:valuelist:000\": \"Зарядка\",\n      \"service:003:property:002:valuelist:001\": \"Не заряжается\",\n      \"service:003:property:003\": \"Напряжение\",\n      \"service:003:action:001\": \"Начать зарядку\",\n      \"service:004\": \"Звук\",\n      \"service:004:property:001\": \"Тревога\",\n      \"service:004:property:002\": \"Громкость\",\n      \"service:005\": \"Блокировка кнопок\",\n      \"service:005:property:001\": \"Блокировка от детей\",\n      \"service:005:property:002\": \"Статус блокировки\",\n      \"service:006\": \"Идентификация\",\n      \"service:006:action:001\": \"Найти Робот-Пылесос\",\n      \"service:009\": \"Губка\",\n      \"service:009:property:002\": \"Оставшийся срок службы\",\n      \"service:009:action:001\": \"Сбросить срок службы\",\n      \"service:010\": \"Карта\",\n      \"service:010:property:010\": \"Автопереключение карты\",\n      \"service:010:property:010:valuelist:000\": \"Вручную\",\n      \"service:010:property:010:valuelist:001\": \"Автоматически\",\n      \"service:011\": \"Не беспокоить\",\n      \"service:012\": \"Щетка\",\n      \"service:012:action:001\": \"Сбросить срок службы\",\n      \"service:013\": \"Боковая щетка\",\n      \"service:013:action:001\": \"Сбросить срок службы\",\n      \"service:014\": \"Фильтр\",\n      \"service:014:action:001\": \"Сбросить срок службы\",\n      \"service:015\": \"Озвучка\",\n      \"service:017\": \"ИИ\",\n      \"service:018\": \"Моющее\",\n      \"service:018:property:001\": \"Количество моющего средства\",\n      \"service:018:property:002\": \"Подача моющего средства\",\n      \"service:018:property:003\": \"Уровень подачи моющего средства\",\n      \"service:018:property:003:valuelist:000\": \"Экономия\",\n      \"service:018:property:003:valuelist:001\": \"Нормально\",\n      \"service:018:property:003:valuelist:002\": \"Много\",\n      \"service:018:property:003:valuelist:003\": \"Без ограничений\",\n      \"service:019\": \"Одноразовый мешок\",\n      \"service:019:action:001\": \"Сбросить срок службы\",\n      \"service:020:action:007\": \"Лидар Контроль\",\n      \"service:020:property:011\": \"Лидар Статус\"\n    }\n  }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/specs/spec_add.json",
    "content": "{\n  \"urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:1\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:switch:0000780C:090615-ktf:1\",\n      \"description\": \"AC Switch\",\n      \"properties\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:090615-ktf:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ]\n    },\n    {\n      \"iid\": 4,\n      \"type\": \"urn:miot-spec-v2:service:environment:0000780A:090615-ktf:1\",\n      \"description\": \"Environment\",\n      \"properties\": [\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:temperature:00000020:090615-ktf:1\",\n          \"description\": \"Temperature\",\n          \"format\": \"float\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"unit\": \"celsius\",\n          \"value-range\": [\n            -30,\n            100,\n            1\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:air-conditioner:0000A004:juhl-hvac:1\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:switch:0000780C:juhl-hvac:1\",\n      \"description\": \"AC Switch\",\n      \"properties\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:juhl-hvac:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1\": [\n    {\n      \"iid\": 3,\n      \"type\": \"urn:miot-spec-v2:service:light:00007802:hyd-lyjpro:1\",\n      \"description\": \"Moon Light\",\n      \"properties\": [\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:hyd-lyjpro:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:speaker:0000A015:xiaomi-l05b:1\": [\n    {\n      \"iid\": 3,\n      \"type\": \"urn:miot-spec-v2:service:play:0000781D:xiaomi-l05b:1\",\n      \"description\": \"Play Control\",\n      \"actions\": [\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:action:play:0000280B:xiaomi-l05b:1\",\n          \"description\": \"Play\",\n          \"in\": [],\n          \"out\": []\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:1:0000C822\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:thermostat:0000784A:tofan-wk01:1\",\n      \"description\": \"Thermostat\",\n      \"properties\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:tofan-wk01:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        },\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:mode-a:00000008:tofan-wk01:1\",\n          \"description\": \"Mode\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"Auto\"\n            },\n            {\n              \"value\": 1,\n              \"description\": \"Cool\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Heat\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"Fan\"\n            },\n            {\n              \"value\": 4,\n              \"description\": \"Dry\"\n            }\n          ]\n        },\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:property:fault:00000009:tofan-wk01:1\",\n          \"description\": \"Device Fault\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"No Faults\"\n            }\n          ]\n        },\n        {\n          \"iid\": 4,\n          \"type\": \"urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1\",\n          \"description\": \"Target Temperature\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ],\n          \"unit\": \"celsius\",\n          \"value-range\": [\n            16,\n            35,\n            1\n          ]\n        }\n      ],\n      \"actions\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:action:toggle:00002811:tofan-wk01:1\",\n          \"description\": \"Toggle\",\n          \"in\": [],\n          \"out\": []\n        }\n      ]\n    },\n    {\n      \"iid\": 4,\n      \"type\": \"urn:miot-spec-v2:service:air-conditioner:0000780F:tofan-wk01:1\",\n      \"description\": \"Air Conditioner\",\n      \"properties\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:tofan-wk01:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        },\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:target-temperature:00000021:tofan-wk01:1\",\n          \"description\": \"Target Temperature\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ],\n          \"unit\": \"celsius\",\n          \"value-range\": [\n            16,\n            32,\n            1\n          ]\n        },\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:property:fan-level:00000016:tofan-wk01:1\",\n          \"description\": \"Fan Level\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"Auto\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Low\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"Medium\"\n            },\n            {\n              \"value\": 4,\n              \"description\": \"High\"\n            }\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:vacuum:0000A006:narwa-001:1\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:vacuum:00007810:narwa-001:1\",\n      \"description\": \"Robot Cleaner\",\n      \"properties\": [\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:status:00000007:narwa-001:1\",\n          \"description\": \"Status\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 1,\n              \"description\": \"Idle\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Charging\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"BreakCharging\"\n            },\n            {\n              \"value\": 4,\n              \"description\": \"Sweeping\"\n            },\n            {\n              \"value\": 5,\n              \"description\": \"Paused\"\n            },\n            {\n              \"value\": 6,\n              \"description\": \"Go Charging\"\n            },\n            {\n              \"value\": 7,\n              \"description\": \"GoWash\"\n            },\n            {\n              \"value\": 8,\n              \"description\": \"Remote\"\n            },\n            {\n              \"value\": 9,\n              \"description\": \"Charging\"\n            },\n            {\n              \"value\": 10,\n              \"description\": \"BuildingMap\"\n            },\n            {\n              \"value\": 11,\n              \"description\": \"Updating\"\n            },\n            {\n              \"value\": 12,\n              \"description\": \"Sleeping\"\n            },\n            {\n              \"value\": 13,\n              \"description\": \"Relocation\"\n            },\n            {\n              \"value\": 14,\n              \"description\": \"StationWorking\"\n            },\n            {\n              \"value\": 15,\n              \"description\": \"Error\"\n            },\n            {\n              \"value\": 16,\n              \"description\": \"Sweeping and Mopping\"\n            },\n            {\n              \"value\": 17,\n              \"description\": \"Mopping\"\n            },\n            {\n              \"value\": 18,\n              \"description\": \"Paused\"\n            },\n            {\n              \"value\": 19,\n              \"description\": \"GoChargeBreak\"\n            },\n            {\n              \"value\": 20,\n              \"description\": \"WashBreak\"\n            },\n            {\n              \"value\": 21,\n              \"description\": \"IdleInOutside\"\n            }\n          ]\n        },\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:property:fault:00000009:narwa-001:1\",\n          \"description\": \"Device Fault\",\n          \"format\": \"uint32\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"No Faults\"\n            },\n            {\n              \"value\": 16777248,\n              \"description\": \"16777248\"\n            },\n            {\n              \"value\": 34603008,\n              \"description\": \"34603008\"\n            },\n            {\n              \"value\": 34668608,\n              \"description\": \"34668608\"\n            },\n            {\n              \"value\": 16842848,\n              \"description\": \"16842848\"\n            },\n            {\n              \"value\": 16842849,\n              \"description\": \"16842849\"\n            },\n            {\n              \"value\": 16842850,\n              \"description\": \"16842850\"\n            },\n            {\n              \"value\": 16842851,\n              \"description\": \"16842851\"\n            },\n            {\n              \"value\": 33751106,\n              \"description\": \"33751106\"\n            },\n            {\n              \"value\": 16842852,\n              \"description\": \"16842852\"\n            },\n            {\n              \"value\": 33751105,\n              \"description\": \"33751105\"\n            },\n            {\n              \"value\": 16842853,\n              \"description\": \"16842853\"\n            },\n            {\n              \"value\": 33751104,\n              \"description\": \"33751104\"\n            },\n            {\n              \"value\": 34603121,\n              \"description\": \"34603121\"\n            },\n            {\n              \"value\": 34603120,\n              \"description\": \"34603120\"\n            },\n            {\n              \"value\": 34603123,\n              \"description\": \"34603123\"\n            },\n            {\n              \"value\": 34603122,\n              \"description\": \"34603122\"\n            },\n            {\n              \"value\": 33685523,\n              \"description\": \"33685523\"\n            },\n            {\n              \"value\": 34603125,\n              \"description\": \"34603125\"\n            },\n            {\n              \"value\": 33685524,\n              \"description\": \"33685524\"\n            },\n            {\n              \"value\": 34603124,\n              \"description\": \"34603124\"\n            },\n            {\n              \"value\": 34668592,\n              \"description\": \"34668592\"\n            },\n            {\n              \"value\": 36765959,\n              \"description\": \"36765959\"\n            },\n            {\n              \"value\": 34603126,\n              \"description\": \"34603126\"\n            },\n            {\n              \"value\": 36765952,\n              \"description\": \"36765952\"\n            },\n            {\n              \"value\": 36765954,\n              \"description\": \"36765954\"\n            },\n            {\n              \"value\": 36765953,\n              \"description\": \"36765953\"\n            },\n            {\n              \"value\": 36765956,\n              \"description\": \"36765956\"\n            },\n            {\n              \"value\": 36765955,\n              \"description\": \"36765955\"\n            },\n            {\n              \"value\": 36765958,\n              \"description\": \"36765958\"\n            },\n            {\n              \"value\": 36765957,\n              \"description\": \"36765957\"\n            },\n            {\n              \"value\": 33685525,\n              \"description\": \"33685525\"\n            },\n            {\n              \"value\": 16843026,\n              \"description\": \"16843026\"\n            },\n            {\n              \"value\": 33751122,\n              \"description\": \"33751122\"\n            },\n            {\n              \"value\": 33751121,\n              \"description\": \"33751121\"\n            },\n            {\n              \"value\": 16843024,\n              \"description\": \"16843024\"\n            },\n            {\n              \"value\": 33751120,\n              \"description\": \"33751120\"\n            },\n            {\n              \"value\": 16843025,\n              \"description\": \"16843025\"\n            },\n            {\n              \"value\": 33685602,\n              \"description\": \"33685602\"\n            },\n            {\n              \"value\": 34603104,\n              \"description\": \"34603104\"\n            },\n            {\n              \"value\": 34603105,\n              \"description\": \"34603105\"\n            },\n            {\n              \"value\": 33751078,\n              \"description\": \"33751078\"\n            },\n            {\n              \"value\": 34668576,\n              \"description\": \"34668576\"\n            },\n            {\n              \"value\": 34603107,\n              \"description\": \"34603107\"\n            },\n            {\n              \"value\": 33685600,\n              \"description\": \"33685600\"\n            },\n            {\n              \"value\": 33685601,\n              \"description\": \"33685601\"\n            },\n            {\n              \"value\": 33751076,\n              \"description\": \"33751076\"\n            },\n            {\n              \"value\": 33751075,\n              \"description\": \"33751075\"\n            },\n            {\n              \"value\": 33751073,\n              \"description\": \"33751073\"\n            },\n            {\n              \"value\": 36765745,\n              \"description\": \"36765745\"\n            },\n            {\n              \"value\": 34603089,\n              \"description\": \"34603089\"\n            },\n            {\n              \"value\": 34603088,\n              \"description\": \"34603088\"\n            },\n            {\n              \"value\": 34603090,\n              \"description\": \"34603090\"\n            },\n            {\n              \"value\": 16842832,\n              \"description\": \"16842832\"\n            },\n            {\n              \"value\": 16842833,\n              \"description\": \"16842833\"\n            },\n            {\n              \"value\": 16842834,\n              \"description\": \"16842834\"\n            },\n            {\n              \"value\": 16842841,\n              \"description\": \"16842841\"\n            },\n            {\n              \"value\": 16842835,\n              \"description\": \"16842835\"\n            },\n            {\n              \"value\": 16842836,\n              \"description\": \"16842836\"\n            },\n            {\n              \"value\": 33685568,\n              \"description\": \"33685568\"\n            },\n            {\n              \"value\": 36765995,\n              \"description\": \"36765995\"\n            },\n            {\n              \"value\": 33685569,\n              \"description\": \"33685569\"\n            },\n            {\n              \"value\": 36765994,\n              \"description\": \"36765994\"\n            },\n            {\n              \"value\": 33685570,\n              \"description\": \"33685570\"\n            },\n            {\n              \"value\": 34668545,\n              \"description\": \"34668545\"\n            },\n            {\n              \"value\": 34668544,\n              \"description\": \"34668544\"\n            },\n            {\n              \"value\": 33685585,\n              \"description\": \"33685585\"\n            },\n            {\n              \"value\": 34799648,\n              \"description\": \"34799648\"\n            },\n            {\n              \"value\": 33685586,\n              \"description\": \"33685586\"\n            },\n            {\n              \"value\": 33685584,\n              \"description\": \"33685584\"\n            },\n            {\n              \"value\": 36765733,\n              \"description\": \"36765733\"\n            },\n            {\n              \"value\": 36765735,\n              \"description\": \"36765735\"\n            },\n            {\n              \"value\": 36765734,\n              \"description\": \"36765734\"\n            },\n            {\n              \"value\": 36765736,\n              \"description\": \"36765736\"\n            },\n            {\n              \"value\": 16842805,\n              \"description\": \"16842805\"\n            },\n            {\n              \"value\": 16842806,\n              \"description\": \"16842806\"\n            },\n            {\n              \"value\": 16842801,\n              \"description\": \"16842801\"\n            },\n            {\n              \"value\": 36766016,\n              \"description\": \"36766016\"\n            },\n            {\n              \"value\": 16842802,\n              \"description\": \"16842802\"\n            },\n            {\n              \"value\": 16842804,\n              \"description\": \"16842804\"\n            },\n            {\n              \"value\": 34603040,\n              \"description\": \"34603040\"\n            },\n            {\n              \"value\": 34603042,\n              \"description\": \"34603042\"\n            },\n            {\n              \"value\": 34603041,\n              \"description\": \"34603041\"\n            },\n            {\n              \"value\": 34603044,\n              \"description\": \"34603044\"\n            },\n            {\n              \"value\": 34668641,\n              \"description\": \"34668641\"\n            },\n            {\n              \"value\": 34603043,\n              \"description\": \"34603043\"\n            },\n            {\n              \"value\": 34668640,\n              \"description\": \"34668640\"\n            },\n            {\n              \"value\": 34603045,\n              \"description\": \"34603045\"\n            },\n            {\n              \"value\": 36765969,\n              \"description\": \"36765969\"\n            },\n            {\n              \"value\": 36765968,\n              \"description\": \"36765968\"\n            },\n            {\n              \"value\": 33685544,\n              \"description\": \"33685544\"\n            },\n            {\n              \"value\": 33685545,\n              \"description\": \"33685545\"\n            },\n            {\n              \"value\": 33685552,\n              \"description\": \"33685552\"\n            },\n            {\n              \"value\": 33685553,\n              \"description\": \"33685553\"\n            },\n            {\n              \"value\": 33685554,\n              \"description\": \"33685554\"\n            },\n            {\n              \"value\": 36765996,\n              \"description\": \"36765996\"\n            },\n            {\n              \"value\": 33685555,\n              \"description\": \"33685555\"\n            },\n            {\n              \"value\": 33685556,\n              \"description\": \"33685556\"\n            },\n            {\n              \"value\": 36765993,\n              \"description\": \"36765993\"\n            },\n            {\n              \"value\": 34603029,\n              \"description\": \"34603029\"\n            },\n            {\n              \"value\": 34603031,\n              \"description\": \"34603031\"\n            },\n            {\n              \"value\": 34603030,\n              \"description\": \"34603030\"\n            },\n            {\n              \"value\": 34603033,\n              \"description\": \"34603033\"\n            },\n            {\n              \"value\": 16842899,\n              \"description\": \"16842899\"\n            },\n            {\n              \"value\": 34603032,\n              \"description\": \"34603032\"\n            },\n            {\n              \"value\": 36765696,\n              \"description\": \"36765696\"\n            },\n            {\n              \"value\": 16842897,\n              \"description\": \"16842897\"\n            },\n            {\n              \"value\": 36765992,\n              \"description\": \"36765992\"\n            },\n            {\n              \"value\": 16842771,\n              \"description\": \"16842771\"\n            },\n            {\n              \"value\": 16842769,\n              \"description\": \"16842769\"\n            },\n            {\n              \"value\": 16842770,\n              \"description\": \"16842770\"\n            }\n          ]\n        },\n        {\n          \"iid\": 17,\n          \"type\": \"urn:miot-spec-v2:property:last-clean-time:00000280:narwa-001:1\",\n          \"description\": \"Last Clean Time\",\n          \"format\": \"uint32\",\n          \"access\": [\n            \"notify\",\n            \"read\"\n          ],\n          \"value-range\": [\n            0,\n            4294967295,\n            1\n          ]\n        },\n        {\n          \"iid\": 18,\n          \"type\": \"urn:miot-spec-v2:property:base-station-working-status:00000281:narwa-001:1\",\n          \"description\": \"Base Station Working Status\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"NULL\"\n            },\n            {\n              \"value\": 1,\n              \"description\": \"Mop Clean\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Mop Air Dry\"\n            }\n          ]\n        },\n        {\n          \"iid\": 71,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:narwa-001:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ],\n      \"actions\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:action:start-sweep:00002804:narwa-001:1\",\n          \"description\": \"Start Sweep\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:action:stop-sweeping:00002805:narwa-001:1\",\n          \"description\": \"Stop Sweeping\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:action:stop-and-gocharge:000028B4:narwa-001:1\",\n          \"description\": \"Stop And Gocharge\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 7,\n          \"type\": \"urn:miot-spec-v2:action:pause-sweeping:00002863:narwa-001:1\",\n          \"description\": \"Pause Sweeping\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 8,\n          \"type\": \"urn:miot-spec-v2:action:continue-sweep:000028AA:narwa-001:1\",\n          \"description\": \"Continue Sweep\",\n          \"in\": [],\n          \"out\": []\n        }\n      ]\n    },\n    {\n      \"iid\": 11,\n      \"type\": \"urn:miot-spec-v2:service:battery:00007805:narwa-001:1\",\n      \"description\": \"Battery\",\n      \"properties\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:property:battery-level:00000014:narwa-001:1\",\n          \"description\": \"Battery Level\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"unit\": \"percentage\",\n          \"value-range\": [\n            0,\n            100,\n            1\n          ]\n        },\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:charging-state:00000015:narwa-001:1\",\n          \"description\": \"Charging State\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 1,\n              \"description\": \"Charging\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Not Charging\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"Not Chargeable\"\n            }\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:vacuum:0000A006:narwa-ax11:1\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:vacuum:00007810:narwa-ax11:1\",\n      \"description\": \"Robot Cleaner\",\n      \"properties\": [\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:status:00000007:narwa-ax11:1\",\n          \"description\": \"Status\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 1,\n              \"description\": \"Idle\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Charging\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"BreakCharging\"\n            },\n            {\n              \"value\": 4,\n              \"description\": \"Sweeping\"\n            },\n            {\n              \"value\": 5,\n              \"description\": \"Paused\"\n            },\n            {\n              \"value\": 6,\n              \"description\": \"Go Charging\"\n            },\n            {\n              \"value\": 7,\n              \"description\": \"GoWash\"\n            },\n            {\n              \"value\": 8,\n              \"description\": \"Remote\"\n            },\n            {\n              \"value\": 9,\n              \"description\": \"Charging\"\n            },\n            {\n              \"value\": 10,\n              \"description\": \"BuildingMap\"\n            },\n            {\n              \"value\": 11,\n              \"description\": \"Updating\"\n            },\n            {\n              \"value\": 12,\n              \"description\": \"Sleeping\"\n            },\n            {\n              \"value\": 13,\n              \"description\": \"Relocation\"\n            },\n            {\n              \"value\": 14,\n              \"description\": \"StationWorking\"\n            },\n            {\n              \"value\": 15,\n              \"description\": \"Error\"\n            },\n            {\n              \"value\": 16,\n              \"description\": \"Sweeping and Mopping\"\n            },\n            {\n              \"value\": 17,\n              \"description\": \"Mopping\"\n            },\n            {\n              \"value\": 18,\n              \"description\": \"Paused\"\n            },\n            {\n              \"value\": 19,\n              \"description\": \"GoChargeBreak\"\n            },\n            {\n              \"value\": 20,\n              \"description\": \"WashBreak\"\n            },\n            {\n              \"value\": 21,\n              \"description\": \"IdleInOutside\"\n            }\n          ]\n        },\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:property:fault:00000009:narwa-ax11:1\",\n          \"description\": \"Device Fault\",\n          \"format\": \"uint32\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"No Faults\"\n            },\n            {\n              \"value\": 16777248,\n              \"description\": \"16777248\"\n            },\n            {\n              \"value\": 34603008,\n              \"description\": \"34603008\"\n            },\n            {\n              \"value\": 34668608,\n              \"description\": \"34668608\"\n            },\n            {\n              \"value\": 16842848,\n              \"description\": \"16842848\"\n            },\n            {\n              \"value\": 16842849,\n              \"description\": \"16842849\"\n            },\n            {\n              \"value\": 16842850,\n              \"description\": \"16842850\"\n            },\n            {\n              \"value\": 16842851,\n              \"description\": \"16842851\"\n            },\n            {\n              \"value\": 33751106,\n              \"description\": \"33751106\"\n            },\n            {\n              \"value\": 16842852,\n              \"description\": \"16842852\"\n            },\n            {\n              \"value\": 33751105,\n              \"description\": \"33751105\"\n            },\n            {\n              \"value\": 16842853,\n              \"description\": \"16842853\"\n            },\n            {\n              \"value\": 33751104,\n              \"description\": \"33751104\"\n            },\n            {\n              \"value\": 34603121,\n              \"description\": \"34603121\"\n            },\n            {\n              \"value\": 34603120,\n              \"description\": \"34603120\"\n            },\n            {\n              \"value\": 34603123,\n              \"description\": \"34603123\"\n            },\n            {\n              \"value\": 34603122,\n              \"description\": \"34603122\"\n            },\n            {\n              \"value\": 33685523,\n              \"description\": \"33685523\"\n            },\n            {\n              \"value\": 34603125,\n              \"description\": \"34603125\"\n            },\n            {\n              \"value\": 33685524,\n              \"description\": \"33685524\"\n            },\n            {\n              \"value\": 34603124,\n              \"description\": \"34603124\"\n            },\n            {\n              \"value\": 34668592,\n              \"description\": \"34668592\"\n            },\n            {\n              \"value\": 36765959,\n              \"description\": \"36765959\"\n            },\n            {\n              \"value\": 34603126,\n              \"description\": \"34603126\"\n            },\n            {\n              \"value\": 36765952,\n              \"description\": \"36765952\"\n            },\n            {\n              \"value\": 36765954,\n              \"description\": \"36765954\"\n            },\n            {\n              \"value\": 36765953,\n              \"description\": \"36765953\"\n            },\n            {\n              \"value\": 36765956,\n              \"description\": \"36765956\"\n            },\n            {\n              \"value\": 36765955,\n              \"description\": \"36765955\"\n            },\n            {\n              \"value\": 36765958,\n              \"description\": \"36765958\"\n            },\n            {\n              \"value\": 36765957,\n              \"description\": \"36765957\"\n            },\n            {\n              \"value\": 33685525,\n              \"description\": \"33685525\"\n            },\n            {\n              \"value\": 16843026,\n              \"description\": \"16843026\"\n            },\n            {\n              \"value\": 33751122,\n              \"description\": \"33751122\"\n            },\n            {\n              \"value\": 33751121,\n              \"description\": \"33751121\"\n            },\n            {\n              \"value\": 16843024,\n              \"description\": \"16843024\"\n            },\n            {\n              \"value\": 33751120,\n              \"description\": \"33751120\"\n            },\n            {\n              \"value\": 16843025,\n              \"description\": \"16843025\"\n            },\n            {\n              \"value\": 33685602,\n              \"description\": \"33685602\"\n            },\n            {\n              \"value\": 34603104,\n              \"description\": \"34603104\"\n            },\n            {\n              \"value\": 34603105,\n              \"description\": \"34603105\"\n            },\n            {\n              \"value\": 33751078,\n              \"description\": \"33751078\"\n            },\n            {\n              \"value\": 34668576,\n              \"description\": \"34668576\"\n            },\n            {\n              \"value\": 34603107,\n              \"description\": \"34603107\"\n            },\n            {\n              \"value\": 33685600,\n              \"description\": \"33685600\"\n            },\n            {\n              \"value\": 33685601,\n              \"description\": \"33685601\"\n            },\n            {\n              \"value\": 33751076,\n              \"description\": \"33751076\"\n            },\n            {\n              \"value\": 33751075,\n              \"description\": \"33751075\"\n            },\n            {\n              \"value\": 33751073,\n              \"description\": \"33751073\"\n            },\n            {\n              \"value\": 36765745,\n              \"description\": \"36765745\"\n            },\n            {\n              \"value\": 34603089,\n              \"description\": \"34603089\"\n            },\n            {\n              \"value\": 34603088,\n              \"description\": \"34603088\"\n            },\n            {\n              \"value\": 34603090,\n              \"description\": \"34603090\"\n            },\n            {\n              \"value\": 16842832,\n              \"description\": \"16842832\"\n            },\n            {\n              \"value\": 16842833,\n              \"description\": \"16842833\"\n            },\n            {\n              \"value\": 16842834,\n              \"description\": \"16842834\"\n            },\n            {\n              \"value\": 16842841,\n              \"description\": \"16842841\"\n            },\n            {\n              \"value\": 16842835,\n              \"description\": \"16842835\"\n            },\n            {\n              \"value\": 16842836,\n              \"description\": \"16842836\"\n            },\n            {\n              \"value\": 33685568,\n              \"description\": \"33685568\"\n            },\n            {\n              \"value\": 36765995,\n              \"description\": \"36765995\"\n            },\n            {\n              \"value\": 33685569,\n              \"description\": \"33685569\"\n            },\n            {\n              \"value\": 36765994,\n              \"description\": \"36765994\"\n            },\n            {\n              \"value\": 33685570,\n              \"description\": \"33685570\"\n            },\n            {\n              \"value\": 34668545,\n              \"description\": \"34668545\"\n            },\n            {\n              \"value\": 34668544,\n              \"description\": \"34668544\"\n            },\n            {\n              \"value\": 33685585,\n              \"description\": \"33685585\"\n            },\n            {\n              \"value\": 34799648,\n              \"description\": \"34799648\"\n            },\n            {\n              \"value\": 33685586,\n              \"description\": \"33685586\"\n            },\n            {\n              \"value\": 33685584,\n              \"description\": \"33685584\"\n            },\n            {\n              \"value\": 36765733,\n              \"description\": \"36765733\"\n            },\n            {\n              \"value\": 36765735,\n              \"description\": \"36765735\"\n            },\n            {\n              \"value\": 36765734,\n              \"description\": \"36765734\"\n            },\n            {\n              \"value\": 36765736,\n              \"description\": \"36765736\"\n            },\n            {\n              \"value\": 16842805,\n              \"description\": \"16842805\"\n            },\n            {\n              \"value\": 16842806,\n              \"description\": \"16842806\"\n            },\n            {\n              \"value\": 16842801,\n              \"description\": \"16842801\"\n            },\n            {\n              \"value\": 36766016,\n              \"description\": \"36766016\"\n            },\n            {\n              \"value\": 16842802,\n              \"description\": \"16842802\"\n            },\n            {\n              \"value\": 16842804,\n              \"description\": \"16842804\"\n            },\n            {\n              \"value\": 34603040,\n              \"description\": \"34603040\"\n            },\n            {\n              \"value\": 34603042,\n              \"description\": \"34603042\"\n            },\n            {\n              \"value\": 34603041,\n              \"description\": \"34603041\"\n            },\n            {\n              \"value\": 34603044,\n              \"description\": \"34603044\"\n            },\n            {\n              \"value\": 34668641,\n              \"description\": \"34668641\"\n            },\n            {\n              \"value\": 34603043,\n              \"description\": \"34603043\"\n            },\n            {\n              \"value\": 34668640,\n              \"description\": \"34668640\"\n            },\n            {\n              \"value\": 34603045,\n              \"description\": \"34603045\"\n            },\n            {\n              \"value\": 36765969,\n              \"description\": \"36765969\"\n            },\n            {\n              \"value\": 36765968,\n              \"description\": \"36765968\"\n            },\n            {\n              \"value\": 33685544,\n              \"description\": \"33685544\"\n            },\n            {\n              \"value\": 33685545,\n              \"description\": \"33685545\"\n            },\n            {\n              \"value\": 33685552,\n              \"description\": \"33685552\"\n            },\n            {\n              \"value\": 33685553,\n              \"description\": \"33685553\"\n            },\n            {\n              \"value\": 33685554,\n              \"description\": \"33685554\"\n            },\n            {\n              \"value\": 36765996,\n              \"description\": \"36765996\"\n            },\n            {\n              \"value\": 33685555,\n              \"description\": \"33685555\"\n            },\n            {\n              \"value\": 33685556,\n              \"description\": \"33685556\"\n            },\n            {\n              \"value\": 36765993,\n              \"description\": \"36765993\"\n            },\n            {\n              \"value\": 34603029,\n              \"description\": \"34603029\"\n            },\n            {\n              \"value\": 34603031,\n              \"description\": \"34603031\"\n            },\n            {\n              \"value\": 34603030,\n              \"description\": \"34603030\"\n            },\n            {\n              \"value\": 34603033,\n              \"description\": \"34603033\"\n            },\n            {\n              \"value\": 16842899,\n              \"description\": \"16842899\"\n            },\n            {\n              \"value\": 34603032,\n              \"description\": \"34603032\"\n            },\n            {\n              \"value\": 36765696,\n              \"description\": \"36765696\"\n            },\n            {\n              \"value\": 16842897,\n              \"description\": \"16842897\"\n            },\n            {\n              \"value\": 36765992,\n              \"description\": \"36765992\"\n            },\n            {\n              \"value\": 16842771,\n              \"description\": \"16842771\"\n            },\n            {\n              \"value\": 16842769,\n              \"description\": \"16842769\"\n            },\n            {\n              \"value\": 16842770,\n              \"description\": \"16842770\"\n            }\n          ]\n        },\n        {\n          \"iid\": 4,\n          \"type\": \"urn:miot-spec-v2:property:sweep-mop-type:00000135:narwa-ax11:1\",\n          \"description\": \"Sweep Mop Type\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 1,\n              \"description\": \"Sweep\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Mop\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"Sweep Mop\"\n            },\n            {\n              \"value\": 4,\n              \"description\": \"Sweep Before Mopping\"\n            }\n          ]\n        },\n        {\n          \"iid\": 17,\n          \"type\": \"urn:miot-spec-v2:property:last-clean-time:00000280:narwa-ax11:1\",\n          \"description\": \"Last Clean Time\",\n          \"format\": \"uint32\",\n          \"access\": [\n            \"notify\",\n            \"read\"\n          ],\n          \"value-range\": [\n            0,\n            4294967295,\n            1\n          ]\n        },\n        {\n          \"iid\": 18,\n          \"type\": \"urn:miot-spec-v2:property:base-station-working-status:00000281:narwa-ax11:1\",\n          \"description\": \"Base Station Working Status\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 0,\n              \"description\": \"NULL\"\n            },\n            {\n              \"value\": 1,\n              \"description\": \"Mop Clean\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Mop Air Dry\"\n            }\n          ]\n        },\n        {\n          \"iid\": 71,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:narwa-ax11:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ],\n      \"actions\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:action:start-sweep:00002804:narwa-ax11:1\",\n          \"description\": \"Start Sweep\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:action:stop-sweeping:00002805:narwa-ax11:1\",\n          \"description\": \"Stop Sweeping\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:action:stop-and-gocharge:000028B4:narwa-ax11:1\",\n          \"description\": \"Stop And Gocharge\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 7,\n          \"type\": \"urn:miot-spec-v2:action:pause-sweeping:00002863:narwa-ax11:1\",\n          \"description\": \"Pause Sweeping\",\n          \"in\": [],\n          \"out\": []\n        },\n        {\n          \"iid\": 8,\n          \"type\": \"urn:miot-spec-v2:action:continue-sweep:000028AA:narwa-ax11:1\",\n          \"description\": \"Continue Sweep\",\n          \"in\": [],\n          \"out\": []\n        }\n      ]\n    },\n    {\n      \"iid\": 11,\n      \"type\": \"urn:miot-spec-v2:service:battery:00007805:narwa-ax11:1\",\n      \"description\": \"Battery\",\n      \"properties\": [\n        {\n          \"iid\": 1,\n          \"type\": \"urn:miot-spec-v2:property:battery-level:00000014:narwa-ax11:1\",\n          \"description\": \"Battery Level\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"unit\": \"percentage\",\n          \"value-range\": [\n            0,\n            100,\n            1\n          ]\n        },\n        {\n          \"iid\": 2,\n          \"type\": \"urn:miot-spec-v2:property:charging-state:00000015:narwa-ax11:1\",\n          \"description\": \"Charging State\",\n          \"format\": \"uint8\",\n          \"access\": [\n            \"read\",\n            \"notify\"\n          ],\n          \"value-list\": [\n            {\n              \"value\": 1,\n              \"description\": \"Charging\"\n            },\n            {\n              \"value\": 2,\n              \"description\": \"Not Charging\"\n            },\n            {\n              \"value\": 3,\n              \"description\": \"Not Chargeable\"\n            }\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:vacuum:0000A006:roidmi-v60:3\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:cleaner:00007810:roidmi-v60:1\",\n      \"description\": \"Robot Cleaner\",\n      \"actions\": [\n        {\n          \"iid\": 3,\n          \"type\": \"urn:miot-spec-v2:action:start-room-sweep:00002826:roidmi-v60:1\",\n          \"description\": \"Start Room Sweep\",\n          \"in\": [\n            9\n          ],\n          \"out\": []\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:water-heater:0000A02A:viomi-m1:2\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:switch:0000780C:viomi-m1:1\",\n      \"description\": \"Water Heater\",\n      \"properties\": [\n        {\n          \"iid\": 6,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:viomi-m1:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:water-heater:0000A02A:xiaomi-yms2:1\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:switch:0000780C:xiaomi-yms2:1\",\n      \"description\": \"Switch\",\n      \"properties\": [\n        {\n          \"iid\": 6,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:xiaomi-yms2:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ]\n    }\n  ],\n  \"urn:miot-spec-v2:device:water-heater:0000A02A:zimi-h03:1\": [\n    {\n      \"iid\": 2,\n      \"type\": \"urn:miot-spec-v2:service:switch:0000780C:zimi-h03:1\",\n      \"description\": \"Switch\",\n      \"properties\": [\n        {\n          \"iid\": 6,\n          \"type\": \"urn:miot-spec-v2:property:on:00000006:zimi-h03:1\",\n          \"description\": \"Switch Status\",\n          \"format\": \"bool\",\n          \"access\": [\n            \"read\",\n            \"write\",\n            \"notify\"\n          ]\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "custom_components/xiaomi_home/miot/specs/spec_filter.yaml",
    "content": "urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:\n  services:\n  - '4'\nurn:miot-spec-v2:device:air-conditioner:0000A004:759413-iez:\n  properties:\n  - '2.3'\nurn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:\n  properties:\n  - 9.*\n  - 13.*\n  - 15.*\n  services:\n  - '10'\nurn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:\n  properties:\n  - '3.2'\nurn:miot-spec-v2:device:curtain:0000A00C:lumi-hmcn01:\n  properties:\n  - '5.1'\n  services:\n  - '4'\n  - '7'\n  - '8'\nurn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:\n  events:\n  - '2.1'\nurn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1:\n  services:\n  - '5'\nurn:miot-spec-v2:device:light:0000A001:philips-strip3:\n  properties:\n  - '2.2'\n  services:\n  - '1'\n  - '3'\nurn:miot-spec-v2:device:light:0000A001:yeelink-color2:\n  properties:\n  - 3.*\n  - '2.5'\nurn:miot-spec-v2:device:light:0000A001:yeelink-dnlight2:\n  services:\n  - '3'\nurn:miot-spec-v2:device:light:0000A001:yeelink-mbulb3:\n  services:\n  - '3'\nurn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:\n  services:\n  - '1'\n  - '5'\nurn:miot-spec-v2:device:speaker:0000A015:xiaomi-l15a:\n  properties:\n  - '3.3'\n  - '6.1'\n  - '6.2'\n  - '6.3'\n  - '6.4'\nurn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:\n  services:\n  - '2'\n  - '4'\nurn:miot-spec-v2:device:vacuum:0000A006:narwa-001:\n  services:\n  - '*'\nurn:miot-spec-v2:device:vacuum:0000A006:narwa-ax11:\n  services:\n  - '*'\nurn:miot-spec-v2:device:vacuum:0000A006:roidmi-v60:\n  actions:\n  - '2.3'\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/specs/spec_modify.yaml",
    "content": "urn:miot-spec-v2:device:air-condition-outlet:0000A045:lumi-mcn04:1:\n  prop.3.4:\n    format: uint8\nurn:miot-spec-v2:device:air-conditioner:0000A004:daikin-k2:1:\n  prop.2.1:\n    name: ac-on\n    format: string\n    access:\n    - read\n    - notify\n  prop.2.2:\n    name: ac-mode\n    format: string\n    access:\n    - read\n    - notify\n  prop.3.1:\n    name: ac-fan-level\n    format: string\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:air-conditioner:0000A004:qdhkl-a42:1:\n  prop.2.2:\n    value-list:\n    - value: 1\n      description: Cool\n    - value: 2\n      description: Dry\n    - value: 4\n      description: Fan\n    - value: 8\n      description: Heat\n    - value: 9\n      description: Auto\n    - value: 10\n      description: Heat_cool\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c17:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c17:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c17:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c20:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1:\n  prop.8.6:\n    name: power-consumption\n    unit: kWh\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c24:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-c35:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h09h00:4:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h40h00:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h43h00:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h43h00:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-h43h00:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m16:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m28:5:\n  prop.9.5:\n    unit: days\n  prop.10.6:\n    unit: none\n  prop.12.3:\n    unit: rpm\n  prop.12.11:\n    unit: Hz\n  prop.12.13:\n    unit: A\n  prop.12.14:\n    unit: V\n  prop.12.15:\n    unit: pascal\n  prop.12.17:\n    unit: rpm\n  prop.12.21:\n    unit: celsius\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:1: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:5: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:7: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-m9:6\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1:\n  prop.10.6:\n    unit: none\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-mt0:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1:\n  prop.10.6:\n    unit: none\n  prop.3.12:\n    name: vertical-swing-left-up\n  prop.3.13:\n    name: vertical-swing-left-down\n  prop.3.14:\n    name: vertical-swing-right-up\n  prop.3.15:\n    name: vertical-swing-right-down\n  prop.3.20:\n    name: horizontal-swing-left\n  prop.3.22:\n    name: horizontal-swing-right\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:2: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:3: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1\nurn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:4: urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-rr0r00:1\nurn:miot-spec-v2:device:air-fresh:0000A012:daikin-k33:1:\n  prop.2.1:\n    name: ac-on\n    format: string\n    access:\n    - read\n    - notify\n  prop.2.3:\n    name: ac-mode\n    format: string\n    access:\n    - read\n    - notify\n  prop.2.5:\n    name: ac-fan-level\n    format: string\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:air-monitor:0000A008:cgllc-cgd1st:1:\n  prop.3.7:\n    value-range:\n    - -30\n    - 100\n    - 0.1\nurn:miot-spec-v2:device:air-monitor:0000A008:cgllc-s1:1:\n  prop.2.5:\n    name: voc-density\nurn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:1: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3\nurn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:2: urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3\nurn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ua1a:3:\n  prop.3.5:\n    expr: (src_value*6)\nurn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1:\n  prop.2.3:\n    name: current-position-a\n  prop.2.8:\n    name: target-position-a\n  prop.2.9:\n    name: target-position-b\n  prop.2.11:\n    expr: (100-src_value)\nurn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1:\n  prop.2.3:\n    value-range:\n    - 0\n    - 1\n    - 1\nurn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:2: urn:miot-spec-v2:device:airer:0000A00D:hyd-znlyj5:1\nurn:miot-spec-v2:device:airer:0000A00D:mrbond-m1t:1:\n  prop.2.3:\n    name: current-position-a\nurn:miot-spec-v2:device:airer:0000A00D:mrbond-m33a:1:\n  prop.2.3:\n    name: current-position-a\n  prop.2.11:\n    name: current-position-b\nurn:miot-spec-v2:device:bath-heater:0000A028:mike-2:1:\n  prop.3.1:\n    name: mode-a\n  prop.3.11:\n    name: mode-b\n  prop.3.12:\n    name: mode-c\nurn:miot-spec-v2:device:bath-heater:0000A028:opple-acmoto:1:\n  prop.5.2:\n    value-list:\n    - value: 1\n      description: low\n    - value: 128\n      description: medium\n    - value: 255\n      description: high\nurn:miot-spec-v2:device:bath-heater:0000A028:xiaomi-s1:1:\n  prop.4.4:\n    name: fan-level-ventilation\nurn:miot-spec-v2:device:curtain:0000A00C:bjkcz-kczble:1:0000D031:\n  prop.2.2:\n    name: status-a\nurn:miot-spec-v2:device:dehumidifier:0000A02D:xiaomi-lite:1:\n  prop.3.2:\n    value-range:\n    - -30\n    - 100\n    - 0.1\nurn:miot-spec-v2:device:electronic-valve:0000A0A7:lxzn-02:1:0000C833:\n  prop.3.1:\n    format: float\n    value-range:\n    - 0\n    - 999999\n    - 0.01\n    expr: (src_value/100)\n  prop.3.2:\n    unit: mA\n  prop.3.3:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.1\n    expr: (src_value/10)\n  prop.4.1:\n    unit: kW\nurn:miot-spec-v2:device:electronic-valve:0000A0A7:sanmei-s1:1:\n  prop.3.1:\n    format: float\n    value-range:\n    - 0\n    - 99999999\n    - 0.01\n    expr: round(src_value/100, 2)\n  prop.3.2:\n    format: float\n    value-range:\n    - 0\n    - 16777216\n    - 0.01\n    expr: round(src_value/100, 2)\n  prop.3.3:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.1\n    expr: round(src_value/10, 1)\nurn:miot-spec-v2:device:electronic-valve:0000A0A7:ykcn-cbcs:1:0000C833:\n  prop.3.1:\n    format: float\n    value-range:\n    - 0\n    - 99999999\n    - 0.01\n    expr: round(src_value/100, 2)\n  prop.3.2:\n    unit: mA\n  prop.3.3:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.1\n    expr: round(src_value/10, 1)\n  prop.4.1:\n    unit: kW\nurn:miot-spec-v2:device:fan:0000A005:dmaker-p33:1:\n  prop.2.2:\n    name: fan-level-a\n  prop.2.6:\n    name: fan-level\n    access:\n    - read\n    - write\n    - notify\nurn:miot-spec-v2:device:fan:0000A005:dmaker-p5:1:\n  prop.2.4:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:xiaomi-p43:1:\n  prop.2.2:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:xiaomi-p45:1:0000D062:\n  prop.2.4:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:xiaomi-p51:1:\n  prop.2.2:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:xiaomi-p69:1:0000D062:\n  prop.2.4:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:xiaomi-p70:1:0000D062:\n  prop.2.4:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:xiaomi-p76:1:0000D062:\n  prop.2.4:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:zhimi-sa1:3:\n  prop.2.2:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:zhimi-v3:3:\n  prop.2.2:\n    name: fan-level-a\nurn:miot-spec-v2:device:fan:0000A005:zhimi-za4:3:\n  prop.2.2:\n    name: fan-level-a\nurn:miot-spec-v2:device:gateway:0000A019:lumi-mcn001:1:\n  prop.2.1:\n    access:\n    - read\n    - notify\n  prop.2.2:\n    icon: mdi:ip\n  prop.2.3:\n    access:\n    - read\n    - notify\n  prop.2.5:\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1:\n  prop.2.1:\n    name: access-mode\n    access:\n    - read\n    - notify\n  prop.2.2:\n    name: ip-address\n    icon: mdi:ip\n  prop.2.3:\n    name: wifi-ssid\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:2: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1\nurn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3: urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:1\nurn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:1: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3\nurn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:2: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3\nurn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3:\n  prop.3.1:\n    name: on-ventilation\nurn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:4: urn:miot-spec-v2:device:hood:0000A01B:cykj-jyj22:3\nurn:miot-spec-v2:device:humidifier:0000A00E:zhimi-ca4:2:\n  prop.2.7:\n    unit: percentage\n    expr: round(src_value*0.83)\nurn:miot-spec-v2:device:kettle:0000A009:yunmi-r3:1:\n  prop.3.1:\n    unit: ppm\nurn:miot-spec-v2:device:light:0000A001:shhf-sfla10:1:\n  prop.8.9:\n    name: wind-reverse\nurn:miot-spec-v2:device:light:0000A001:shhf-sfla12:1:\n  prop.8.11:\n    name: on-a\nurn:miot-spec-v2:device:light:0000A001:shhf-sflt11:1:0000C802:\n  prop.11.14:\n    name: on-power\nurn:miot-spec-v2:device:magnet-sensor:0000A016:linp-m1:1:\n  prop.2.1004:\n    name: contact-state\n    value-list:\n    - value: 0\n      description: open\n    - value: 1\n      description: closed\n    expr: (src_value!=1)\nurn:miot-spec-v2:device:motion-sensor:0000A014:lumi-acn001:1:\n  prop.3.2:\n    access:\n    - read\n    - notify\n    unit: mV\nurn:miot-spec-v2:device:motion-sensor:0000A014:lumi-bmgl01:1:\n  prop.2.2:\n    value-list:\n    - value: 0\n      description: 0 Seconds\n    - value: 2\n      description: 2 Minutes\n    - value: 5\n      description: 5 Minutes\nurn:miot-spec-v2:device:motor-controller:0000A01D:adp-adswb4:1:0000C837:\n  prop.2.1:\n    name: motor-switch\nurn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:1: urn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2\nurn:miot-spec-v2:device:occupancy-sensor:0000A0BF:ainice-3b:2:\n  prop.2.5:\n    name: people-number\n  prop.2.8:\n    name: people-number\n  prop.2.11:\n    name: people-number\n  prop.2.14:\n    name: people-number\n  prop.2.17:\n    name: people-number\nurn:miot-spec-v2:device:occupancy-sensor:0000A0BF:izq-24:2:0000C824:\n  prop.2.6:\n    unit: cm\nurn:miot-spec-v2:device:occupancy-sensor:0000A0BF:linp-hb01:2:0000C824:\n  prop.3.3:\n    unit: m\nurn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:1: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3\nurn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:2: urn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3\nurn:miot-spec-v2:device:outlet:0000A002:chuangmi-212a01:3:\n  prop.5.1:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.01\n    expr: round(src_value*6/1000000, 2)\nurn:miot-spec-v2:device:outlet:0000A002:cuco-cp1md:1:\n  prop.2.2:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.01\n    expr: round(src_value/1000, 2)\n  prop.2.3:\n    format: float\n    value-range:\n    - 0\n    - 3000\n    - 0.1\n    expr: round(src_value/10, 1)\n  prop.2.4:\n    unit: mA\nurn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:1: urn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2\nurn:miot-spec-v2:device:outlet:0000A002:cuco-cp2:2:\n  prop.2.3:\n    format: float\n    value-range:\n    - 0\n    - 3000\n    - 0.1\n    expr: round(src_value/10, 1)\n  prop.2.4:\n    unit: mA\n  prop.3.2:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.1\n    expr: round(src_value/10, 1)\nurn:miot-spec-v2:device:outlet:0000A002:cuco-cp2d:1:\n  prop.3.2:\n    unit: mA\nurn:miot-spec-v2:device:outlet:0000A002:cuco-cp7pd:1:\n  prop.11.1:\n    unit: Wh\n    value-range:\n    - 0\n    - 65535\n    - 0.001\n    expr: (src_value*1000)\n  prop.11.4:\n    value-range:\n    - 0\n    - 10000\n    - 0.01\nurn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1:\n  prop.11.1:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.01\n    expr: round(src_value/100, 2)\nurn:miot-spec-v2:device:outlet:0000A002:cuco-v3:2: urn:miot-spec-v2:device:outlet:0000A002:cuco-v3:1\nurn:miot-spec-v2:device:outlet:0000A002:giot-v8icm:1:0000C816:\n  prop.4.1:\n    unit: mWh\nurn:miot-spec-v2:device:outlet:0000A002:qmi-psv3:1:0000C816:\n  prop.3.3:\n    unit: mV\n  prop.3.4:\n    unit: mA\nurn:miot-spec-v2:device:outlet:0000A002:yutai-fsov8m:1:0000C816:\n  prop.4.1:\n    format: float\n    value-range:\n    - 0\n    - 429497\n    - 0.01\n    expr: round(src_value/10000, 2)\nurn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:1:0000C816: urn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816\nurn:miot-spec-v2:device:outlet:0000A002:zimi-zncz01:2:0000C816:\n  prop.3.1:\n    format: float\n    value-range:\n    - 0\n    - 350000\n    - 0.01\n    expr: round(src_value/100, 2)\nurn:miot-spec-v2:device:plant-monitor:0000A030:hhcc-v1:1:\n  prop.2.1:\n    name: soil-moisture\n    icon: mdi:watering-can\n  prop.2.2:\n    name: soil-ec\n    icon: mdi:sprout-outline\n    unit: μS/cm\nurn:miot-spec-v2:device:relay:0000A03D:lumi-c2acn01:1:\n  prop.4.1:\n    unit: kWh\nurn:miot-spec-v2:device:router:0000A036:xiaomi-rd08:1:\n  prop.2.1:\n    name: download-speed\n    icon: mdi:download\n    unit: B/s\n  prop.2.2:\n    name: upload-speed\n    icon: mdi:upload\n    unit: B/s\nurn:miot-spec-v2:device:safe-box:0000A042:loock-v1:1:\n  prop.5.1:\n    name: contact-state\n    expr: (src_value!=1)\nurn:miot-spec-v2:device:speaker:0000A015:xiaomi-l04m:2:\n  prop.3.1:\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:speaker:0000A015:xiaomi-lx04:2:\n  prop.3.1:\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:speaker:0000A015:xiaomi-lx06:2:\n  prop.3.1:\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:speaker:0000A015:xiaomi-x08c:2:\n  prop.2.1:\n    access:\n    - read\n    - notify\nurn:miot-spec-v2:device:speaker:0000A015:xiaomi-x08e:1:\n  prop.3.1:\n    name: playing-state-a\nurn:miot-spec-v2:device:switch:0000A003:090615-x1tpm:1:0000D042:\n  prop.27.3:\n    name: light-on\n  prop.27.4:\n    name: light-fan-on\nurn:miot-spec-v2:device:switch:0000A003:lxzn-cbcsmj:1:0000D00D:\n  prop.3.1:\n    format: float\n    value-range:\n    - 0\n    - 99999999\n    - 0.01\n    expr: round(src_value/100, 2)\n  prop.3.2:\n    unit: mA\n  prop.3.3:\n    format: float\n    value-range:\n    - 0\n    - 65535\n    - 0.1\n    expr: round(src_value/10, 1)\nurn:miot-spec-v2:device:thermostat:0000A031:suittc-wk168:1:\n  prop.2.3:\n    value-list:\n    - value: 1\n      description: '1'\n    - value: 2\n      description: '2'\n    - value: 3\n      description: '3'\n    - value: 4\n      description: '4'\n    - value: 5\n      description: '5'\n    - value: 6\n      description: '6'\n    - value: 7\n      description: '7'\n    - value: 8\n      description: '8'\n    - value: 9\n      description: '9'\n    - value: 10\n      description: '10'\n    - value: 11\n      description: '11'\n    - value: 12\n      description: '12'\n    - value: 13\n      description: '13'\n    - value: 14\n      description: '14'\n    - value: 15\n      description: '15'\n    - value: 16\n      description: '16'\nurn:miot-spec-v2:device:toothbrush:0000A07E:xiaomi-p001:1:\n  prop.4.1041:\n    unit: days\nurn:miot-spec-v2:device:water-purifier:0000A013:roswan-lte01:1:0000D05A:\n  prop.4.1:\n    unit: ppm\n  prop.4.2:\n    unit: ppm\nurn:miot-spec-v2:device:water-purifier:0000A013:xiaomi-s1200g:1:0000D05A:\n  prop.3.2:\n    unit: days\n  prop.3.3:\n    unit: days\n  prop.3.4:\n    unit: L\n  prop.3.5:\n    unit: L\n  prop.5.2:\n    unit: days\n  prop.5.3:\n    unit: days\n  prop.5.4:\n    unit: L\n  prop.5.5:\n    unit: L\nurn:miot-spec-v2:device:water-purifier:0000A013:xiaomi-s1200g:2:0000D05A: urn:miot-spec-v2:device:water-purifier:0000A013:xiaomi-s1200g:1:0000D05A\nurn:miot-spec-v2:device:water-purifier:0000A013:yunmi-s20:1:\n  prop.4.1:\n    unit: ppm\n  prop.4.2:\n    unit: ppm\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/specs/specv2entity.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nConversion rules of MIoT-Spec-V2 instance to Home Assistant entity.\n\"\"\"\nfrom homeassistant.components.sensor import SensorDeviceClass\nfrom homeassistant.components.sensor import SensorStateClass\nfrom homeassistant.components.event import EventDeviceClass\nfrom homeassistant.components.binary_sensor import BinarySensorDeviceClass\n\nfrom homeassistant.const import (CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,\n                                 EntityCategory, LIGHT_LUX, UnitOfEnergy,\n                                 UnitOfPower, UnitOfElectricCurrent,\n                                 UnitOfElectricPotential, UnitOfTemperature,\n                                 UnitOfPressure, PERCENTAGE)\n\n# pylint: disable=pointless-string-statement\n\"\"\"SPEC_DEVICE_TRANS_MAP\n{\n    '<device instance name>':{\n        'required':{\n            '<service instance name>':{\n                'required':{\n                    'properties': {\n                        '<property instance name>': set<property access: str>\n                    },\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                },\n                'optional':{\n                    'properties': set<property instance name: str>,\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                }\n            }\n        },\n        'optional':{\n            '<service instance name>':{\n                'required':{\n                    'properties': {\n                        '<property instance name>': set<property access: str>\n                    },\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                },\n                'optional':{\n                    'properties': set<property instance name: str>,\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                }\n            }\n        },\n        'entity': str\n    }\n}\n\"\"\"\nSPEC_DEVICE_TRANS_MAP: dict = {\n    'humidifier': {\n        'required': {\n            'humidifier': {\n                'required': {\n                    'properties': {\n                        'on': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'mode', 'target-humidity'}\n                }\n            }\n        },\n        'optional': {\n            'environment': {\n                'required': {\n                    'properties': {\n                        'relative-humidity': {'read'}\n                    }\n                }\n            }\n        },\n        'entity': 'humidifier'\n    },\n    'dehumidifier': {\n        'required': {\n            'dehumidifier': {\n                'required': {\n                    'properties': {\n                        'on': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'mode', 'target-humidity'}\n                }\n            }\n        },\n        'optional': {\n            'environment': {\n                'required': {\n                    'properties': {\n                        'relative-humidity': {'read'}\n                    }\n                }\n            }\n        },\n        'entity': 'dehumidifier'\n    },\n    'vacuum': {\n        'required': {\n            'vacuum': {\n                'required': {\n                    'actions': {'start-sweep', 'stop-sweeping'},\n                },\n                'optional': {\n                    'properties': {'status', 'fan-level'},\n                    'actions': {\n                        'pause-sweeping', 'continue-sweep', 'stop-and-gocharge'\n                    }\n                }\n            }\n        },\n        'optional': {\n            'identify': {\n                'required': {\n                    'actions': {'identify'}\n                }\n            },\n            'battery': {\n                'required': {\n                    'actions': {\n                        'start-charge'\n                    }\n                }\n            }\n        },\n        'entity': 'vacuum'\n    },\n    'air-conditioner': {\n        'required': {\n            'air-conditioner': {\n                'required': {\n                    'properties': {\n                        'on': {'read', 'write'},\n                        'mode': {'read', 'write'},\n                        'target-temperature': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'target-humidity'}\n                }\n            }\n        },\n        'optional': {\n            'fan-control': {\n                'required': {},\n                'optional': {\n                    'properties': {\n                        'on', 'fan-level', 'horizontal-swing', 'vertical-swing'\n                    }\n                }\n            },\n            'environment': {\n                'required': {},\n                'optional': {\n                    'properties': {'temperature', 'relative-humidity'}\n                }\n            },\n            'air-condition-outlet-matching': {\n                'required': {},\n                'optional': {\n                    'properties': {'ac-state'}\n                }\n            }\n        },\n        'entity': 'air-conditioner'\n    },\n    'air-condition-outlet': 'air-conditioner',\n    'thermostat': {\n        'required': {\n            'thermostat': {\n                'required': {\n                    'properties': {\n                        'on': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {\n                        'target-temperature', 'mode', 'fan-level', 'temperature'\n                    }\n                }\n            }\n        },\n        'optional': {\n            'environment': {\n                'required': {},\n                'optional': {\n                    'properties': {'temperature', 'relative-humidity'}\n                }\n            }\n        },\n        'entity': 'thermostat'\n    },\n    'heater': {\n        'required': {\n            'heater': {\n                'required': {\n                    'properties': {\n                        'on': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'target-temperature', 'heat-level'}\n                }\n            }\n        },\n        'optional': {\n            'environment': {\n                'required': {},\n                'optional': {\n                    'properties': {'temperature', 'relative-humidity'}\n                }\n            }\n        },\n        'entity': 'heater'\n    },\n    'bath-heater': {\n        'required': {\n            'ptc-bath-heater': {\n                'required': {\n                    'properties': {\n                        'mode': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'target-temperature', 'temperature'}\n                }\n            }\n        },\n        'optional': {\n            'fan-control': {\n                'required': {},\n                'optional': {\n                    'properties': {\n                        'on', 'fan-level', 'horizontal-swing', 'vertical-swing'\n                    }\n                }\n            },\n            'environment': {\n                'required': {},\n                'optional': {\n                    'properties': {'temperature'}\n                }\n            }\n        },\n        'entity': 'bath-heater',\n    },\n    'electric-blanket': {\n        'required': {\n            'electric-blanket': {\n                'required': {\n                    'properties': {\n                        'on': {'read', 'write'},\n                        'target-temperature': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'mode', 'temperature'}\n                }\n            }\n        },\n        'optional': {},\n        'entity': 'electric-blanket'\n    },\n    'speaker': {\n        'required': {\n            'speaker': {\n                'required': {\n                    'properties': {\n                        'volume': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'mute'}\n                }\n            },\n            'play-control': {\n                'required': {\n                    'properties': {\n                        'playing-state': {'read'}\n                    },\n                    'actions': {'play'}\n                },\n                'optional': {\n                    'properties': {'play-loop-mode'},\n                    'actions': {'pause', 'stop', 'next', 'previous'}\n                }\n            }\n        },\n        'optional': {},\n        'entity': 'wifi-speaker'\n    },\n    'television': {\n        'required': {\n            'speaker': {\n                'required': {\n                    'properties': {\n                        'volume': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'mute'}\n                }\n            },\n            'television': {\n                'required': {\n                    'actions': {'turn-off'}\n                },\n                'optional': {\n                    'properties': {'input-control'},\n                    'actions': {'turn-on'}\n                }\n            }\n        },\n        'optional': {\n            'play-control': {\n                'required': {\n                    'properties': {\n                        'playing-state': {'read'}\n                    }\n                },\n                'optional': {\n                    'properties': {'play-loop-mode'},\n                    'actions': {'play', 'pause', 'stop', 'next', 'previous'}\n                }\n            }\n        },\n        'entity': 'television'\n    },\n    'tv-box':{\n        'required': {\n            'speaker': {\n                'required': {\n                    'properties': {\n                        'volume': {'read', 'write'}\n                    }\n                },\n                'optional': {\n                    'properties': {'mute'}\n                }\n            },\n            'tv-box': {\n                'required': {\n                    'actions': {'turn-off'}\n                },\n                'optional': {\n                    'actions': {'turn-on'}\n                }\n            }\n        },\n        'optional': {\n            'play-control': {\n                'required': {\n                    'properties': {\n                        'playing-state': {'read'}\n                    }\n                },\n                'optional': {\n                    'properties': {'play-loop-mode'},\n                    'actions': {'play', 'pause', 'stop', 'next', 'previous'}\n                }\n            }\n        },\n        'entity': 'television'\n    },\n    'watch': {\n        'required': {\n            'watch': {\n                'required': {\n                    'properties': {\n                        'longitude': {'read'},\n                        'latitude': {'read'}\n                    }\n                },\n                'optional': {\n                    'properties': {'area-id'}\n                }\n            }\n        },\n        'optional': {\n            'battery': {\n                'required': {\n                    'properties': {\n                        'battery-level': {'read'}\n                    }\n                }\n            }\n        },\n        'entity': 'device_tracker'\n    }\n}\n\n\"\"\"SPEC_SERVICE_TRANS_MAP\n{\n    '<service instance name>':{\n        'required':{\n            'properties': {\n                '<property instance name>': set<property access: str>\n            },\n            'events': set<event instance name: str>,\n            'actions': set<action instance name: str>\n        },\n        'optional':{\n            'properties': set<property instance name: str>,\n            'events': set<event instance name: str>,\n            'actions': set<action instance name: str>\n        },\n        'entity': str,\n        'entity_category'?: str\n    }\n}\n\"\"\"\nSPEC_SERVICE_TRANS_MAP: dict = {\n    'light': {\n        'required': {\n            'properties': {\n                'on': {'read', 'write'}\n            }\n        },\n        'optional': {\n            'properties': {'mode', 'brightness', 'color', 'color-temperature'}\n        },\n        'entity': 'light'\n    },\n    'ambient-light': 'light',\n    'night-light': 'light',\n    'white-light': 'light',\n    'indicator-light': {\n        'required': {\n            'properties': {\n                'on': {'read', 'write'}\n            }\n        },\n        'optional': {\n            'properties': {\n                'mode',\n                'brightness',\n            }\n        },\n        'entity': 'light',\n        'entity_category': EntityCategory.CONFIG\n    },\n    'fan': {\n        'required': {\n            'properties': {\n                'on': {'read', 'write'},\n                'fan-level': {'read', 'write'}\n            }\n        },\n        'optional': {\n            'properties': {'mode', 'horizontal-swing', 'wind-reverse'}\n        },\n        'entity': 'fan'\n    },\n    'fan-control': 'fan',\n    'ceiling-fan': 'fan',\n    'air-fresh': 'fan',\n    'air-purifier': 'fan',\n    'water-heater': {\n        'required': {\n            'properties': {\n                'on': {'read', 'write'}\n            }\n        },\n        'optional': {\n            'properties': {'temperature', 'target-temperature', 'mode'}\n        },\n        'entity': 'water_heater'\n    },\n    'curtain': {\n        'required': {\n            'properties': {\n                'motor-control': {'write'}\n            }\n        },\n        'optional': {\n            'properties': {'status', 'current-position', 'target-position'}\n        },\n        'entity': 'cover'\n    },\n    'window-opener': 'curtain',\n    'motor-controller': 'curtain',\n    'airer': 'curtain',\n    'air-conditioner': {\n        'required': {\n            'properties': {\n                'on': {'read', 'write'},\n                'mode': {'read', 'write'},\n                'target-temperature': {'read', 'write'}\n            }\n        },\n        'optional': {\n            'properties': {'target-humidity'}\n        },\n        'entity': 'air-conditioner'\n    }\n}\n\n\"\"\"SPEC_PROP_TRANS_MAP\n{\n    'entities':{\n        '<entity name>':{\n            'format': set<str>,\n            'access': set<str>\n        }\n    },\n    'properties': {\n        '<property instance name>':{\n            'device_class': str,\n            'entity': str,\n            'state_class'?: str,\n            'unit_of_measurement'?: str\n        }\n    }\n}\n\"\"\"\nSPEC_PROP_TRANS_MAP: dict = {\n    'entities': {\n        'sensor': {\n            'format': {'int', 'float'},\n            'access': {'read'}\n        },\n        'binary_sensor': {\n            'format': {'bool', 'int'},\n            'access': {'read'}\n        },\n        'switch': {\n            'format': {'bool'},\n            'access': {'read', 'write'}\n        }\n    },\n    'properties': {\n        'submersion-state': {\n            'device_class': BinarySensorDeviceClass.MOISTURE,\n            'entity': 'binary_sensor'\n        },\n        'contact-state': {\n            'device_class': BinarySensorDeviceClass.DOOR,\n            'entity': 'binary_sensor'\n        },\n        'occupancy-status': {\n            'device_class': BinarySensorDeviceClass.OCCUPANCY,\n            'entity': 'binary_sensor',\n        },\n        'temperature': {\n            'device_class': SensorDeviceClass.TEMPERATURE,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfTemperature.CELSIUS\n        },\n        'relative-humidity': {\n            'device_class': SensorDeviceClass.HUMIDITY,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': PERCENTAGE\n        },\n        'air-quality-index': {\n            'device_class': SensorDeviceClass.AQI,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n        },\n        'pm2.5-density': {\n            'device_class': SensorDeviceClass.PM25,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER\n        },\n        'pm10-density': {\n            'device_class': SensorDeviceClass.PM10,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER\n        },\n        'pm1': {\n            'device_class': SensorDeviceClass.PM1,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': CONCENTRATION_MICROGRAMS_PER_CUBIC_METER\n        },\n        'atmospheric-pressure': {\n            'device_class': SensorDeviceClass.ATMOSPHERIC_PRESSURE,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfPressure.PA\n        },\n        'tvoc-density': {\n            'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT\n        },\n        'voc-density': {\n            'device_class': SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT\n        },\n        'battery-level': {\n            'device_class': SensorDeviceClass.BATTERY,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': PERCENTAGE\n        },\n        'voltage': {\n            'device_class': SensorDeviceClass.VOLTAGE,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfElectricPotential.VOLT\n        },\n        'electric-current': {\n            'device_class': SensorDeviceClass.CURRENT,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfElectricCurrent.AMPERE\n        },\n        'illumination': {\n            'device_class': SensorDeviceClass.ILLUMINANCE,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': LIGHT_LUX\n        },\n        'no-one-determine-time': {\n            'device_class': SensorDeviceClass.DURATION,\n            'entity': 'sensor'\n        },\n        'has-someone-duration': 'no-one-determine-time',\n        'no-one-duration': 'no-one-determine-time',\n        'electric-power': {\n            'device_class': SensorDeviceClass.POWER,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfPower.WATT\n        },\n        'surge-power': {\n            'device_class': SensorDeviceClass.POWER,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfPower.WATT\n        },\n        'power-consumption': {\n            'device_class': SensorDeviceClass.ENERGY,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.TOTAL_INCREASING,\n            'unit_of_measurement': UnitOfEnergy.KILO_WATT_HOUR\n        },\n        'power': {\n            'device_class': SensorDeviceClass.POWER,\n            'entity': 'sensor',\n            'state_class': SensorStateClass.MEASUREMENT,\n            'unit_of_measurement': UnitOfPower.WATT\n        }\n    }\n}\n\n\"\"\"SPEC_EVENT_TRANS_MAP\n{\n    '<event instance name>': str\n}\n\"\"\"\nSPEC_EVENT_TRANS_MAP: dict[str, str] = {\n    'click': EventDeviceClass.BUTTON,\n    'double-click': EventDeviceClass.BUTTON,\n    'long-press': EventDeviceClass.BUTTON,\n    'motion-detected': EventDeviceClass.MOTION,\n    'no-motion': EventDeviceClass.MOTION,\n    'doorbell-ring': EventDeviceClass.DOORBELL\n}\n\nSPEC_ACTION_TRANS_MAP = {}\n"
  },
  {
    "path": "custom_components/xiaomi_home/miot/web_pages.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nMIoT redirect web pages.\n\"\"\"\n\nimport os\nimport asyncio\n\nweb_template = ''\n\n\ndef _load_page_template():\n    path = os.path.join(\n        os.path.dirname(os.path.abspath(__file__)),\n        'resource/oauth_redirect_page.html')\n    with open(path, 'r', encoding='utf-8') as f:\n        global web_template\n        web_template = f.read()\n\n\nasync def oauth_redirect_page(\n    title: str, content: str, button: str, success: bool\n) -> str:\n    \"\"\"Return oauth redirect page.\"\"\"\n    if web_template == '':\n        await asyncio.get_running_loop().run_in_executor(\n            None, _load_page_template)\n    web_page = web_template.replace('TITLE_PLACEHOLDER', title)\n    web_page = web_page.replace('CONTENT_PLACEHOLDER', content)\n    web_page = web_page.replace('BUTTON_PLACEHOLDER', button)\n    web_page = web_page.replace(\n        'STATUS_PLACEHOLDER', 'true' if success else 'false')\n    return web_page\n"
  },
  {
    "path": "custom_components/xiaomi_home/notify.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nNotify entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any, Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.notify import NotifyEntity\nfrom homeassistant.util import yaml\nfrom homeassistant.exceptions import HomeAssistantError\n\nfrom .miot.miot_spec import MIoTSpecAction\nfrom .miot.miot_device import MIoTDevice, MIoTActionEntity\nfrom .miot.const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for action in miot_device.action_list.get('notify', []):\n            new_entities.append(Notify(miot_device=miot_device, spec=action))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Notify(MIoTActionEntity, NotifyEntity):\n    \"\"\"Notify entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:\n        \"\"\"Initialize the Notify.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        self._attr_extra_state_attributes = {}\n        action_in: str = ', '.join([\n            f'{prop.description_trans}({prop.format_.__name__})'\n            for prop in self.spec.in_])\n        self._attr_extra_state_attributes['action params'] = f'[{action_in}]'\n\n    async def async_send_message(\n        self, message: str, title: Optional[str] = None\n    ) -> None:\n        \"\"\"Send a message.\"\"\"\n        if not message:\n            _LOGGER.error(\n                'action exec failed, %s(%s), empty action params',\n                self.name, self.entity_id)\n            return\n        in_list: Any = None\n        try:\n            # YAML will convert yes, no, on, off, true, false to the bool type,\n            # and if it is a string, quotation marks need to be added.\n            in_list = yaml.parse_yaml(content=message)\n        except HomeAssistantError:\n            _LOGGER.error(\n                'action exec failed, %s(%s), invalid action params format, %s',\n                self.name, self.entity_id, message)\n            return\n        if len(self.spec.in_) == 1 and not isinstance(in_list, list):\n            in_list = [in_list]\n        if not isinstance(in_list, list) or len(in_list) != len(self.spec.in_):\n            _LOGGER.error(\n                'action exec failed, %s(%s), invalid action params, %s',\n                self.name, self.entity_id, message)\n            return\n        in_value: list[dict] = []\n        for index, prop in enumerate(self.spec.in_):\n            if prop.format_ == str:\n                if isinstance(in_list[index], (bool, int, float, str)):\n                    in_value.append(\n                        {'piid': prop.iid, 'value': str(in_list[index])})\n                    continue\n            elif prop.format_ == bool:\n                if isinstance(in_list[index], (bool, int)):\n                    # yes, no, on, off, true, false and other bool types\n                    # will also be parsed as 0 and 1 of int.\n                    in_value.append(\n                        {'piid': prop.iid, 'value': bool(in_list[index])})\n                    continue\n            elif prop.format_ == float:\n                if isinstance(in_list[index], (int, float)):\n                    in_value.append(\n                        {'piid': prop.iid, 'value': in_list[index]})\n                    continue\n            elif prop.format_ == int:\n                if isinstance(in_list[index], int):\n                    in_value.append(\n                        {'piid': prop.iid, 'value': in_list[index]})\n                    continue\n            # Invalid params type, raise error.\n            _LOGGER.error(\n                'action exec failed, %s(%s), invalid params item, '\n                'which item(%s) in the list must be %s, %s type was %s, %s',\n                self.name, self.entity_id, prop.description_trans,\n                prop.format_, in_list[index], type(\n                    in_list[index]).__name__, message)\n            return\n        await self.action_async(in_list=in_value)\n"
  },
  {
    "path": "custom_components/xiaomi_home/number.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nNumber entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.number import NumberEntity\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.miot_device import MIoTDevice, MIoTPropertyEntity\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for prop in miot_device.prop_list.get('number', []):\n            new_entities.append(Number(miot_device=miot_device, spec=prop))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Number(MIoTPropertyEntity, NumberEntity):\n    \"\"\"Number entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        \"\"\"Initialize the Notify.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        # Set device_class\n        self._attr_device_class = spec.device_class\n        # Set unit\n        if self.spec.external_unit:\n            self._attr_native_unit_of_measurement = self.spec.external_unit\n        # Set icon\n        if self.spec.icon and not self.device_class:\n            self._attr_icon = self.spec.icon\n        # Set value range\n        if self._value_range:\n            self._attr_native_min_value = self._value_range.min_\n            self._attr_native_max_value = self._value_range.max_\n            self._attr_native_step = self._value_range.step\n\n    @property\n    def native_value(self) -> Optional[float]:\n        \"\"\"Return the current value of the number.\"\"\"\n        return self._value\n\n    async def async_set_native_value(self, value: float) -> None:\n        \"\"\"Update the current value.\"\"\"\n        await self.set_property_async(value=value)\n"
  },
  {
    "path": "custom_components/xiaomi_home/select.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nSelect entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.select import SelectEntity\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTPropertyEntity\nfrom .miot.miot_spec import MIoTSpecProperty\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for prop in miot_device.prop_list.get('select', []):\n            new_entities.append(Select(miot_device=miot_device, spec=prop))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Select(MIoTPropertyEntity, SelectEntity):\n    \"\"\"Select entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        \"\"\"Initialize the Select.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        if self._value_list:\n            self._attr_options = self._value_list.descriptions\n\n    async def async_select_option(self, option: str) -> None:\n        \"\"\"Change the selected option.\"\"\"\n        await self.set_property_async(\n            value=self.get_vlist_value(description=option))\n\n    @property\n    def current_option(self) -> Optional[str]:\n        \"\"\"Return the current selected option.\"\"\"\n        return self.get_vlist_description(value=self._value)\n"
  },
  {
    "path": "custom_components/xiaomi_home/sensor.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nSensor entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.sensor import SensorEntity, SensorDeviceClass\nfrom homeassistant.components.sensor import DEVICE_CLASS_UNITS\n\nfrom .miot.miot_device import MIoTDevice, MIoTPropertyEntity\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.const import DOMAIN\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for prop in miot_device.prop_list.get('sensor', []):\n            new_entities.append(Sensor(miot_device=miot_device, spec=prop))\n\n        if miot_device.miot_client.display_binary_text:\n            for prop in miot_device.prop_list.get('binary_sensor', []):\n                if not prop.value_list:\n                    continue\n                new_entities.append(Sensor(miot_device=miot_device, spec=prop))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Sensor(MIoTPropertyEntity, SensorEntity):\n    \"\"\"Sensor entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        \"\"\"Initialize the Sensor.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        # Set device_class\n        if self._value_list:\n            self._attr_device_class = SensorDeviceClass.ENUM\n            self._attr_icon = 'mdi:format-text'\n            self._attr_native_unit_of_measurement = None\n            self._attr_options = self._value_list.descriptions\n        else:\n            self._attr_device_class = spec.device_class\n            if spec.external_unit:\n                self._attr_native_unit_of_measurement = spec.external_unit\n            else:\n                # device_class is not empty but unit is empty.\n                # Set the default unit according to device_class.\n                unit_sets = DEVICE_CLASS_UNITS.get(\n                    self._attr_device_class, None)  # type: ignore\n                self._attr_native_unit_of_measurement = list(\n                    unit_sets)[0] if unit_sets else None\n            # Set suggested precision\n            if spec.format_ == float:\n                self._attr_suggested_display_precision = spec.precision\n            # Set state_class\n            if spec.state_class:\n                self._attr_state_class = spec.state_class\n        # Set icon\n        if spec.icon and not self.device_class:\n            self._attr_icon = spec.icon\n\n    @property\n    def native_value(self) -> Any:\n        \"\"\"Return the current value of the sensor.\"\"\"\n        if self._value_range and isinstance(self._value, (int, float)):\n            if (\n                self._value < self._value_range.min_\n                or self._value > self._value_range.max_\n            ):\n                _LOGGER.info(\n                    '%s, data exception, out of range, %s, %s',\n                    self.entity_id, self._value, self._value_range)\n        if self._value_list:\n            return self.get_vlist_description(self._value)\n        if isinstance(self._value, str):\n            return self._value[:255]\n        return self._value\n"
  },
  {
    "path": "custom_components/xiaomi_home/switch.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nSwitch entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Any\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.switch import SwitchEntity\n\nfrom .miot.miot_device import MIoTDevice\nfrom .miot.miot_spec import MIoTSpecProperty\nfrom .miot.miot_device import MIoTPropertyEntity\nfrom .miot.const import DOMAIN\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for prop in miot_device.prop_list.get('switch', []):\n            new_entities.append(Switch(miot_device=miot_device, spec=prop))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Switch(MIoTPropertyEntity, SwitchEntity):\n    \"\"\"Switch entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        \"\"\"Initialize the Switch.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n        # Set device_class\n        self._attr_device_class = spec.device_class\n\n    @property\n    def is_on(self) -> bool:\n        \"\"\"On/Off state.\"\"\"\n        return self._value is True\n\n    async def async_turn_on(self, **kwargs: Any) -> None:\n        \"\"\"Turn the switch on.\"\"\"\n        await self.set_property_async(value=True)\n\n    async def async_turn_off(self, **kwargs: Any) -> None:\n        \"\"\"Turn the switch off.\"\"\"\n        await self.set_property_async(value=False)\n\n    async def async_toggle(self, **kwargs: Any) -> None:\n        \"\"\"Toggle the switch.\"\"\"\n        await self.set_property_async(value=not self.is_on)\n"
  },
  {
    "path": "custom_components/xiaomi_home/text.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nText entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any, Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.text import TextEntity\nfrom homeassistant.util import yaml\nfrom homeassistant.exceptions import HomeAssistantError\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_spec import MIoTSpecAction, MIoTSpecProperty\nfrom .miot.miot_device import MIoTActionEntity, MIoTDevice, MIoTPropertyEntity\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n    new_entities = []\n    for miot_device in device_list:\n        for prop in miot_device.prop_list.get('text', []):\n            new_entities.append(Text(miot_device=miot_device, spec=prop))\n\n        if miot_device.miot_client.action_debug:\n            for action in miot_device.action_list.get('notify', []):\n                new_entities.append(ActionText(\n                    miot_device=miot_device, spec=action))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Text(MIoTPropertyEntity, TextEntity):\n    \"\"\"Text entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecProperty) -> None:\n        \"\"\"Initialize the Text.\"\"\"\n        super().__init__(miot_device=miot_device, spec=spec)\n\n    @property\n    def native_value(self) -> Optional[str]:\n        \"\"\"Return the current text value.\"\"\"\n        if isinstance(self._value, str):\n            return self._value[:255]\n        return self._value\n\n    async def async_set_value(self, value: str) -> None:\n        \"\"\"Set the text value.\"\"\"\n        await self.set_property_async(value=value)\n\n\nclass ActionText(MIoTActionEntity, TextEntity):\n    \"\"\"Text entities for Xiaomi Home.\"\"\"\n\n    def __init__(self, miot_device: MIoTDevice, spec: MIoTSpecAction) -> None:\n        super().__init__(miot_device=miot_device, spec=spec)\n        self._attr_extra_state_attributes = {}\n        self._attr_native_value = ''\n        action_in: str = ', '.join([\n            f'{prop.description_trans}({prop.format_.__name__})'\n            for prop in self.spec.in_])\n        self._attr_extra_state_attributes['action params'] = f'[{action_in}]'\n\n    async def async_set_value(self, value: str) -> None:\n        if not value:\n            return\n        in_list: Any = None\n        try:\n            in_list = yaml.parse_yaml(content=value)\n        except HomeAssistantError as e:\n            _LOGGER.error(\n                'action exec failed, %s(%s), invalid action params format, %s',\n                self.name, self.entity_id, value)\n            raise ValueError(\n                f'action exec failed, {self.name}({self.entity_id}), '\n                f'invalid action params format, {value}') from e\n        if len(self.spec.in_) == 1 and not isinstance(in_list, list):\n            in_list = [in_list]\n        if not isinstance(in_list, list) or len(in_list) != len(self.spec.in_):\n            _LOGGER.error(\n                'action exec failed, %s(%s), invalid action params, %s',\n                self.name, self.entity_id, value)\n            raise ValueError(\n                f'action exec failed, {self.name}({self.entity_id}), '\n                f'invalid action params, {value}')\n        in_value: list[dict] = []\n        for index, prop in enumerate(self.spec.in_):\n            if prop.format_ == str:\n                if isinstance(in_list[index], (bool, int, float, str)):\n                    in_value.append(\n                        {'piid': prop.iid, 'value': str(in_list[index])})\n                    continue\n            elif prop.format_ == bool:\n                if isinstance(in_list[index], (bool, int)):\n                    # yes, no, on, off, true, false and other bool types\n                    # will also be parsed as 0 and 1 of int.\n                    in_value.append(\n                        {'piid': prop.iid, 'value': bool(in_list[index])})\n                    continue\n            elif prop.format_ == float:\n                if isinstance(in_list[index], (int, float)):\n                    in_value.append(\n                        {'piid': prop.iid, 'value': in_list[index]})\n                    continue\n            elif prop.format_ == int:\n                if isinstance(in_list[index], int):\n                    in_value.append(\n                        {'piid': prop.iid, 'value': in_list[index]})\n                    continue\n            # Invalid params type, raise error.\n            _LOGGER.error(\n                'action exec failed, %s(%s), invalid params item, '\n                'which item(%s) in the list must be %s, %s type was %s, %s',\n                self.name, self.entity_id, prop.description_trans,\n                prop.format_, in_list[index], type(\n                    in_list[index]).__name__, value)\n            raise ValueError(\n                f'action exec failed, {self.name}({self.entity_id}), '\n                f'invalid params item, which item({prop.description_trans}) '\n                f'in the list must be {prop.format_}, {in_list[index]} type '\n                f'was {type(in_list[index]).__name__}, {value}')\n\n        self._attr_native_value = value\n        if await self.action_async(in_list=in_value):\n            self.async_write_ha_state()\n"
  },
  {
    "path": "custom_components/xiaomi_home/translations/de.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Xiaomi Home Integration\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Risikohinweis\",\n                \"description\": \"1. Ihre **Xiaomi-Benutzerinformationen und Geräteinformationen** werden in Ihrem Home Assistant-System gespeichert. **Xiaomi kann die Sicherheit des Home Assistant-Speichermechanismus nicht garantieren**. Sie sind dafür verantwortlich, Ihre Informationen vor Diebstahl zu schützen.\\r\\n2. Diese Integration wird von der Open-Source-Community unterstützt und gewartet. Es können jedoch Stabilitätsprobleme oder andere Probleme auftreten. Wenn Sie auf ein Problem stoßen, das mit dieser Integration zusammenhängt, sollten Sie **die Open-Source-Community um Hilfe bitten, anstatt sich an den Xiaomi Home Kundendienst zu wenden**.\\r\\n3. Sie benötigen bestimmte technische Fähigkeiten, um Ihre lokale Laufzeitumgebung zu warten. Diese Integration ist für Anfänger nicht geeignet.\\r\\n4. Bevor Sie diese Integration verwenden, lesen Sie bitte die **README-Datei sorgfältig durch**.\\r\\n5. Um eine stabile Nutzung der Integration zu gewährleisten und Missbrauch der Schnittstelle zu verhindern, **darf diese Integration nur in Home Assistant verwendet werden. Weitere Informationen finden Sie in der LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Ich habe das oben genannte Risiko zur Kenntnis genommen und übernehme freiwillig die damit verbundenen Risiken durch die Verwendung der Integration.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Grundkonfiguration\",\n                \"description\": \"### Anmeldegebiet\\r\\nWählen Sie das Gebiet, in dem sich Ihr Xiaomi Home-Konto befindet. Sie können es in der Xiaomi Home App unter `Mein (unten im Menü) > Weitere Einstellungen > Über Xiaomi Home` überprüfen.\\r\\n### Sprache\\r\\nWählen Sie die Sprache, in der Geräte und Entitätsnamen angezeigt werden. Teile von Sätzen, die nicht übersetzt sind, werden in Englisch angezeigt.\\r\\n### OAuth2-Authentifizierungs-Umleitungs-URL\\r\\nDie Umleitungs-URL für die OAuth2-Authentifizierung lautet **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant muss im selben lokalen Netzwerk wie das aktuelle Betriebsterminal (z. B. ein persönlicher Computer) und das Betriebsterminal muss über diese Adresse auf die Home Assistant-Homepage zugreifen können. Andernfalls kann die Anmeldeauthentifizierung fehlschlagen.\\r\\n### Integrierte Netzwerkkonfiguration\\r\\nÜberprüfen Sie, ob das lokale Netzwerk normal funktioniert und ob die zugehörigen Netzwerkressourcen zugänglich sind. **Es wird empfohlen, dies beim ersten Hinzufügen auszuwählen.**\\r\\n### Hinweis\\r\\n- Für Benutzer mit Hunderten oder mehr Mi Home-Geräten wird das erste Hinzufügen der Integration einige Zeit in Anspruch nehmen. Bitte haben Sie Geduld.\\r\\n- Wenn Home Assistant in einer Docker-Umgebung läuft, stellen Sie bitte sicher, dass der Docker-Netzwerkmodus auf host eingestellt ist, da sonst die lokale Steuerungsfunktion möglicherweise nicht richtig funktioniert.\\r\\n- Die lokale Steuerungsfunktion der Integration hat einige Abhängigkeiten. Bitte lesen Sie das README sorgfältig.\",\n                \"data\": {\n                    \"cloud_server\": \"Anmeldegebiet\",\n                    \"integration_language\": \"Sprache\",\n                    \"oauth_redirect_url\": \"OAuth2-Authentifizierungs-Umleitungs-URL\",\n                    \"network_detect_config\": \"Integrierte Netzwerkkonfiguration\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Integrierte Netzwerkkonfiguration\",\n                \"description\": \"## Gebrauchsanweisung\\r\\n### Netzwerk-Erkennungsadresse\\r\\nWird verwendet, um zu überprüfen, ob das Netzwerk normal funktioniert. Wenn nicht festgelegt, wird die Standardadresse des Systems verwendet. Wenn die Standardadressprüfung fehlschlägt, können Sie versuchen, eine benutzerdefinierte Adresse einzugeben.\\r\\n- Sie können mehrere Erkennungsadressen eingeben, getrennt durch ein Komma, z. B. `8.8.8.8,https://www.bing.com`\\r\\n- Wenn es sich um eine IP-Adresse handelt, wird die Erkennung durch Ping durchgeführt. Wenn es sich um eine HTTP(s)-Adresse handelt, wird die Erkennung durch einen HTTP GET-Aufruf durchgeführt.\\r\\n- Wenn Sie die Standarderkennungsadresse des Systems wiederherstellen möchten, geben Sie ein Komma `,` ein und klicken Sie auf 'Weiter'.\\r\\n- **Diese Konfiguration ist global und Änderungen wirken sich auf andere Integrationsinstanzen aus. Bitte ändern Sie sie mit Vorsicht.**\\r\\n### Überprüfung der Netzwerkabhängigkeiten\\r\\nÜberprüfen Sie nacheinander, ob die folgenden Netzwerkabhängigkeiten zugänglich sind. Wenn die entsprechenden Adressen nicht zugänglich sind, führt dies zu Integrationsfehlern.\\r\\n- OAuth2-Authentifizierungsadresse: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API-Adresse: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API-Adresse: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker-Adresse: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Netzwerkerkennungsadresse\",\n                    \"check_network_deps\": \"Überprüfung der Netzwerkabhängigkeiten\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Fehler bei der Anmeldung\",\n                \"description\": \"Klicken Sie auf \\\"Weiter\\\", um es erneut zu versuchen.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Familie und Geräte auswählen\",\n                \"description\": \"## Gebrauchsanweisung\\r\\n### Familienimport für importierte Geräte\\r\\nDie Integration fügt Geräte aus den ausgewählten Familien hinzu.\\r\\n### Raumnamensynchronisationsmodus\\r\\nWenn Geräte von der Xiaomi Home App zu Home Assistant synchronisiert werden, wird die Bezeichnung des Bereichs, in dem sich die Geräte in Home Assistant befinden, nach folgenden Regeln benannt. Beachten Sie, dass das Synchronisieren von Geräten den von Xiaomi Home App festgelegten Familien- und Raum-Einstellungen nicht ändert.\\r\\n- Nicht synchronisieren: Das Gerät wird keinem Bereich hinzugefügt.\\r\\n- Andere Optionen: Der Bereich, in den das Gerät aufgenommen wird, wird nach dem Namen der Familie oder des Raums in der Xiaomi Home App benannt.\\r\\n### Erweiterte Einstellungen\\r\\nZeigen Sie erweiterte Einstellungen an, um die professionellen Konfigurationsoptionen der Integration zu ändern.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Hallo! Bitte wählen Sie die Familie aus, zu der Sie das Gerät hinzufügen möchten.\",\n                \"data\": {\n                    \"home_infos\": \"Familienimport für importierte Geräte\",\n                    \"area_name_rule\": \"Raumnamensynchronisationsmodus\",\n                    \"advanced_options\": \"Erweiterte Einstellungen\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Erweiterte Einstellungen\",\n                \"description\": \"## Gebrauchsanweisung\\r\\n### Wenn Sie die Bedeutung der folgenden Optionen nicht genau kennen, belassen Sie sie bitte auf den Standardeinstellungen.\\r\\n### Geräte filtern\\r\\nUnterstützt das Filtern von Geräten nach Raumnamen und Gerätetypen sowie das Filtern nach Gerätedimensionen.\\r\\n\\r\\n### Steuerungsmodus\\r\\n- Automatisch: Wenn ein verfügbarer Xiaomi-Hub im lokalen Netzwerk vorhanden ist, priorisiert Home Assistant das Senden von Steuerbefehlen über den Hub, um eine lokale Steuerung zu ermöglichen. Wenn kein Hub vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden. Nur wenn diese Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Befehle über die Cloud gesendet.\\r\\n- Cloud: Steuerbefehle werden ausschließlich über die Cloud gesendet.\\r\\n### Action-Debug-Modus\\r\\nFür Methoden, die von MIoT-Spec-V2-Geräten definiert werden, wird neben der Benachrichtigungsentität auch eine Texteingabe-Entität erstellt, mit der Sie während des Debuggens Steuerbefehle an das Gerät senden können.\\r\\n### Nicht standardmäßige Entitäten ausblenden\\r\\nBlendet Entitäten aus, die von nicht-standardmäßigen MIoT-Spec-V2-Instanzen generiert werden und deren Name mit „*“ beginnt.\\r\\n### Binärsensor-Anzeigemodus\\r\\nZeigt Binärsensoren in Xiaomi Home als Textsensor-Entität oder Binärsensor-Entität an。\\r\\n### Gerätestatusänderungen anzeigen\\r\\nDetaillierte Anzeige von Gerätestatusänderungen, es werden nur die ausgewählten Benachrichtigungen angezeigt.\",\n                \"data\": {\n                    \"devices_filter\": \"Geräte filtern\",\n                    \"ctrl_mode\": \"Steuerungsmodus\",\n                    \"action_debug\": \"Action-Debug-Modus\",\n                    \"hide_non_standard_entities\": \"Nicht standardmäßige Entitäten ausblenden\",\n                    \"display_binary_mode\": \"Binärsensor-Anzeigemodus\",\n                    \"display_devices_changed_notify\": \"Gerätestatusänderungen anzeigen\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Geräte filtern\",\n                \"description\": \"## Gebrauchsanweisung\\r\\nUnterstützt das Filtern von Geräten nach Raumnamen, Gerätezugriffstyp und Gerätemodell, und unterstützt auch das Filtern nach Gerätegröße. Die Filterlogik ist wie folgt:\\r\\n- Zuerst wird gemäß der statistischen Logik die Vereinigung oder der Schnittpunkt aller eingeschlossenen Elemente ermittelt, dann der Schnittpunkt oder die Vereinigung der ausgeschlossenen Elemente, und schließlich wird das [eingeschlossene Gesamtergebnis] vom [ausgeschlossenen Gesamtergebnis] subtrahiert, um das [Filterergebnis] zu erhalten.\\r\\n- Wenn keine eingeschlossenen Elemente ausgewählt sind, bedeutet dies, dass alle eingeschlossen sind.\\r\\n### Filtermodus\\r\\n- Ausschließen: Unerwünschte Elemente entfernen.\\r\\n- Einschließen: Gewünschte Elemente einschließen.\\r\\n### Statistische Logik\\r\\n- UND-Logik: Den Schnittpunkt aller Elemente im gleichen Modus nehmen.\\r\\n- ODER-Logik: Die Vereinigung aller Elemente im gleichen Modus nehmen.\\r\\n\\r\\nSie können auch zur Seite [Konfiguration > Geräteliste aktualisieren] des Integrationselements gehen, [Geräte filtern] auswählen, um erneut zu filtern.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Familienraum filtern\",\n                    \"room_list\": \"Familienraum\",\n                    \"type_filter_mode\": \"Gerätetyp filtern\",\n                    \"type_list\": \"Gerätetyp\",\n                    \"model_filter_mode\": \"Gerätemodell filtern\",\n                    \"model_list\": \"Gerätemodell\",\n                    \"devices_filter_mode\": \"Geräte filtern\",\n                    \"device_list\": \"Geräteliste\",\n                    \"statistics_logic\": \"Statistiklogik\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Klicken Sie hier, um sich anzumelden{link_right}\\r\\n(Sie werden nach erfolgreicher Anmeldung automatisch zur nächsten Seite weitergeleitet)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Bitte lesen Sie den Risikohinweis.\",\n            \"get_token_error\": \"Fehler beim Abrufen von Anmeldeinformationen (OAuth-Token).\",\n            \"get_homeinfo_error\": \"Fehler beim Abrufen von Familieninformationen.\",\n            \"mdns_discovery_error\": \"Lokaler Geräteerkennungsdienst ist nicht verfügbar.\",\n            \"get_cert_error\": \"Fehler beim Abrufen des Gateway-Zertifikats.\",\n            \"no_family_selected\": \"Keine Familie ausgewählt.\",\n            \"no_devices\": \"Im ausgewählten Haushalt sind keine Geräte vorhanden. Bitte wählen Sie einen Haushalt mit Geräten aus und fahren Sie fort.\",\n            \"no_filter_devices\": \"Gefilterte Geräte sind leer. Bitte wählen Sie gültige Filterkriterien aus und fahren Sie fort.\",\n            \"no_central_device\": \"Im Modus \\\"Xiaomi Central Hub Gateway\\\" muss ein verfügbares Xiaomi Central Hub Gateway im lokalen Netzwerk von Home Assistant vorhanden sein. Stellen Sie sicher, dass die ausgewählte Familie diese Anforderungen erfüllt.\",\n            \"invalid_network_addr\": \"Ungültige IP-Adresse oder HTTP-Adresse vorhanden, bitte geben Sie eine gültige Adresse ein.\",\n            \"invalid_ip_addr\": \"Unzugängliche IP-Adresse vorhanden, bitte geben Sie eine gültige IP-Adresse ein.\",\n            \"invalid_http_addr\": \"Unzugängliche HTTP-Adresse vorhanden, bitte geben Sie eine gültige HTTP-Adresse ein.\",\n            \"invalid_default_addr\": \"Die Standard-Netzwerkerkennungsadresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration oder verwenden Sie eine benutzerdefinierte Netzwerkerkennungsadresse.\",\n            \"unreachable_oauth2_host\": \"OAuth2-Authentifizierungsadresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\",\n            \"unreachable_http_host\": \"Xiaomi HTTP API-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\",\n            \"unreachable_spec_host\": \"Xiaomi SPEC API-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\",\n            \"unreachable_mqtt_broker\": \"Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Fehler beim Abrufen der Home Assistant-UUID.\",\n            \"network_connect_error\": \"Konfiguration fehlgeschlagen. Netzwerkverbindung fehlgeschlagen. Überprüfen Sie die Netzwerkkonfiguration des Geräts.\",\n            \"already_configured\": \"Dieser Benutzer hat die Konfiguration bereits abgeschlossen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \\\"Konfiguration\\\", um die Konfiguration zu ändern.\",\n            \"invalid_auth_info\": \"Authentifizierungsinformationen sind abgelaufen. Gehen Sie zur Integrationsseite und klicken Sie auf die Schaltfläche \\\"Konfiguration\\\", um die Authentifizierung erneut durchzuführen.\",\n            \"config_flow_error\": \"Integrationskonfigurationsfehler: {error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Authentifizierungskonfiguration\",\n                \"description\": \"Lokale Authentifizierungsinformationen sind abgelaufen. Starten Sie die Authentifizierung erneut.\\r\\n### Aktuelles Anmeldegebiet: {cloud_server}\\r\\n### OAuth2-Authentifizierungs-Umleitungs-URL\\r\\nDie Umleitungs-URL für die OAuth2-Authentifizierung lautet **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant muss im selben lokalen Netzwerk wie das aktuelle Betriebsterminal (z. B. ein persönlicher Computer) und das Betriebsterminal muss über diese Adresse auf die Home Assistant-Homepage zugreifen können. Andernfalls kann die Anmeldeauthentifizierung fehlschlagen.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"OAuth2-Authentifizierungs-Umleitungs-URL\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Fehler bei der Anmeldung\",\n                \"description\": \"Klicken Sie auf \\\"Weiter\\\", um es erneut zu versuchen.\"\n            },\n            \"config_options\": {\n                \"title\": \"Konfigurationsoptionen\",\n                \"description\": \"### Hallo {nick_name}!\\r\\n\\r\\nXiaomi Home-Konto-ID: {uid}\\r\\nAktuelles Anmeldegebiet: {cloud_server}\\r\\nIntegrationsinstanz-ID: {instance_id}\\r\\n\\r\\nWählen Sie die Optionen aus, die Sie erneut konfigurieren möchten, und klicken Sie dann auf \\\"Weiter\\\".\",\n                \"data\": {\n                    \"integration_language\": \"Integrationsprache\",\n                    \"update_user_info\": \"Benutzerinformationen aktualisieren\",\n                    \"update_devices\": \"Geräteliste aktualisieren\",\n                    \"action_debug\": \"Action-Debug-Modus\",\n                    \"hide_non_standard_entities\": \"Verstecke Nicht-Standard-Entitäten\",\n                    \"display_binary_mode\": \"Binärsensor-Anzeigemodus\",\n                    \"display_devices_changed_notify\": \"Gerätestatusänderungen anzeigen\",\n                    \"update_trans_rules\": \"Entitätskonvertierungsregeln aktualisieren\",\n                    \"update_lan_ctrl_config\": \"LAN-Steuerungskonfiguration aktualisieren\",\n                    \"network_detect_config\": \"Integrierte Netzwerkkonfiguration\",\n                    \"cover_dead_zone_width\": \"Die Breite des toten Winkels des Vorhangs\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Benutzernamen aktualisieren\",\n                \"description\": \"{nick_name}! Bitte geben Sie unten Ihren Benutzernamen ein.\",\n                \"data\": {\n                    \"nick_name\": \"Benutzername\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Familie und Geräte neu auswählen\",\n                \"description\": \"## Gebrauchsanweisung\\r\\n### Familienimport für importierte Geräte\\r\\nDie Integration fügt Geräte aus den ausgewählten Familien hinzu.\\r\\n### Geräte filtern\\r\\nUnterstützt das Filtern von Geräten nach Raumnamen, Gerätezugriffstyp und Gerätemodell, und unterstützt auch das Filtern nach Gerätegröße. Es wurden **{local_count}** Geräte gefiltert.\\r\\n### Steuerungsmodus\\r\\n- Automatisch: Wenn im lokalen Netzwerk ein verfügbarer Xiaomi-Zentralgateway vorhanden ist, wird Home Assistant bevorzugt Steuerbefehle über den Zentralgateway senden, um eine lokale Steuerung zu ermöglichen. Wenn im lokalen Netzwerk kein Zentralgateway vorhanden ist, wird versucht, Steuerbefehle über das Xiaomi-OT-Protokoll zu senden, um eine lokale Steuerung zu ermöglichen. Nur wenn die oben genannten Bedingungen für die lokale Steuerung nicht erfüllt sind, werden die Steuerbefehle über die Cloud gesendet.\\r\\n- Cloud: Steuerbefehle werden nur über die Cloud gesendet.\",\n                \"data\": {\n                    \"home_infos\": \"Familienimport für importierte Geräte\",\n                    \"devices_filter\": \"Geräte filtern\",\n                    \"ctrl_mode\": \"Steuerungsmodus\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Geräte filtern\",\n                \"description\": \"## Gebrauchsanweisung\\r\\nUnterstützt das Filtern von Geräten nach Raumnamen, Gerätezugriffstyp und Gerätemodell, und unterstützt auch das Filtern nach Gerätegröße. Die Filterlogik ist wie folgt:\\r\\n- Zuerst wird gemäß der statistischen Logik die Vereinigung oder der Schnittpunkt aller eingeschlossenen Elemente ermittelt, dann der Schnittpunkt oder die Vereinigung der ausgeschlossenen Elemente, und schließlich wird das [eingeschlossene Gesamtergebnis] vom [ausgeschlossenen Gesamtergebnis] subtrahiert, um das [Filterergebnis] zu erhalten.\\r\\n- Wenn keine eingeschlossenen Elemente ausgewählt sind, bedeutet dies, dass alle eingeschlossen sind.\\r\\n### Filtermodus\\r\\n- Ausschließen: Unerwünschte Elemente entfernen.\\r\\n- Einschließen: Gewünschte Elemente einschließen.\\r\\n### Statistische Logik\\r\\n- UND-Logik: Den Schnittpunkt aller Elemente im gleichen Modus nehmen.\\r\\n- ODER-Logik: Die Vereinigung aller Elemente im gleichen Modus nehmen.\\r\\n\\r\\nSie können auch zur Seite [Konfiguration > Geräteliste aktualisieren] des Integrationselements gehen, [Geräte filtern] auswählen, um erneut zu filtern.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Familienraum filtern\",\n                    \"room_list\": \"Familienraum\",\n                    \"type_filter_mode\": \"Gerätetyp filtern\",\n                    \"type_list\": \"Gerätetyp\",\n                    \"model_filter_mode\": \"Gerätemodell filtern\",\n                    \"model_list\": \"Gerätemodell\",\n                    \"devices_filter_mode\": \"Geräte filtern\",\n                    \"device_list\": \"Geräteliste\",\n                    \"statistics_logic\": \"Statistiklogik\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Entitätskonvertierungsregeln aktualisieren\",\n                \"description\": \"## Gebrauchsanweisung\\r\\n- Aktualisieren Sie die Entitätsinformationen der Geräte im aktuellen Integrationsinstanz, einschließlich der mehrsprachigen SPEC-Konfiguration, der SPEC-Booleschen Übersetzung und der SPEC-Modellfilterung.\\r\\n- **Warnung: Diese Konfiguration ist eine globale Konfiguration** und aktualisiert direkt den lokalen Cache. Wenn in anderen Integrationsinstanzen Geräte desselben Modells vorhanden sind, werden diese nach dem Neuladen der entsprechenden Instanzen ebenfalls aktualisiert.\\r\\n- Dieser Vorgang kann einige Zeit in Anspruch nehmen, bitte haben Sie Geduld. Wählen Sie \\\"Bestätigen Sie das Update\\\" und klicken Sie auf \\\"Weiter\\\", um **{urn_count}** Regeln zu aktualisieren, andernfalls überspringen Sie das Update.\",\n                \"data\": {\n                    \"confirm\": \"Bestätigen\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"LAN-Steuerungskonfiguration aktualisieren\",\n                \"description\": \"## Gebrauchsanweisung\\r\\nAktualisieren Sie die Konfigurationsinformationen für **LAN-Steuerung von Xiaomi Home-Geräten**. Wenn die Cloud und das zentrale Gateway die Geräte nicht steuern können, versucht die Integration, die Geräte über das LAN zu steuern; wenn keine Netzwerkkarte ausgewählt ist, wird die LAN-Steuerung nicht aktiviert.\\r\\n- Derzeit werden nur **SPEC v2** WiFi-Geräte im LAN unterstützt. Einige ältere Geräte unterstützen möglicherweise keine Steuerung oder Eigenschaftssynchronisierung.\\r\\n- Bitte wählen Sie die Netzwerkkarte(n) im selben Netzwerk wie die Geräte aus (mehrere Auswahlen werden unterstützt). Wenn die ausgewählte Netzwerkkarte zwei oder mehr Verbindungen im selben Netzwerk hat, wird empfohlen, die mit der besten Netzwerkverbindung auszuwählen, da sonst die **normale Verwendung der Geräte beeinträchtigt werden kann**.\\r\\n- **Wenn es im LAN Endgeräte (Gateways, Mobiltelefone usw.) gibt, die lokale Steuerung unterstützen, kann das Aktivieren des LAN-Abonnements lokale Automatisierung oder Geräteanomalien verursachen. Bitte verwenden Sie es mit Vorsicht**.\\r\\n- **Warnung: Diese Konfiguration ist global und Änderungen wirken sich auf andere Integrationsinstanzen aus. Bitte ändern Sie sie mit Vorsicht**.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Bitte wählen Sie die zu verwendende Netzwerkkarte aus\",\n                    \"enable_subscribe\": \"LAN-Abonnement aktivieren\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Integrierte Netzwerkkonfiguration\",\n                \"description\": \"## Gebrauchsanweisung\\r\\n### Netzwerk-Erkennungsadresse\\r\\nWird verwendet, um zu überprüfen, ob das Netzwerk normal funktioniert. Wenn nicht festgelegt, wird die Standardadresse des Systems verwendet. Wenn die Standardadressprüfung fehlschlägt, können Sie versuchen, eine benutzerdefinierte Adresse einzugeben.\\r\\n- Sie können mehrere Erkennungsadressen eingeben, getrennt durch ein Komma, z. B. `8.8.8.8,https://www.bing.com`\\r\\n- Wenn es sich um eine IP-Adresse handelt, wird die Erkennung durch Ping durchgeführt. Wenn es sich um eine HTTP(s)-Adresse handelt, wird die Erkennung durch einen HTTP GET-Aufruf durchgeführt.\\r\\n- Wenn Sie die Standarderkennungsadresse des Systems wiederherstellen möchten, geben Sie ein Komma `,` ein und klicken Sie auf 'Weiter'.\\r\\n- **Diese Konfiguration ist global und Änderungen wirken sich auf andere Integrationsinstanzen aus. Bitte ändern Sie sie mit Vorsicht.**\\r\\n### Überprüfung der Netzwerkabhängigkeiten\\r\\nÜberprüfen Sie nacheinander, ob die folgenden Netzwerkabhängigkeiten zugänglich sind. Wenn die entsprechenden Adressen nicht zugänglich sind, führt dies zu Integrationsfehlern.\\r\\n- OAuth2-Authentifizierungsadresse: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API-Adresse: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API-Adresse: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker-Adresse: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Netzwerkerkennungsadresse\",\n                    \"check_network_deps\": \"Überprüfung der Netzwerkabhängigkeiten\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Bestätigen Sie die Konfiguration\",\n                \"description\": \"**{nick_name}**, bitte bestätigen Sie die neuesten Konfigurationsinformationen und klicken Sie dann auf \\\"Senden\\\". Die Integration wird mit den aktualisierten Konfigurationen erneut geladen.\\r\\n\\r\\nIntegrationsprache:\\t{lang_new}\\r\\nBenutzername:\\t{nick_name_new}\\r\\nAction-Debug-Modus:\\t{action_debug}\\r\\nVerstecke Nicht-Standard-Entitäten:\\t{hide_non_standard_entities}\\r\\nDie Breite des toten Winkels des Vorhangs:\\t{cover_width_new}\\r\\nGerätestatusänderungen anzeigen:\\t{display_devices_changed_notify}\\r\\nGeräteänderungen:\\t{devices_add} neue Geräte hinzufügen, {devices_remove} Geräte entfernen\\r\\nKonvertierungsregeländerungen:\\tInsgesamt {trans_rules_count} Regeln, aktualisiert {trans_rules_count_success} Regeln\",\n                \"data\": {\n                    \"confirm\": \"Änderungen bestätigen\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Klicken Sie hier, um sich erneut anzumelden{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Nicht authentifiziert. Klicken Sie auf den Authentifizierungslink, um die Benutzeridentität zu authentifizieren.\",\n            \"get_token_error\": \"Fehler beim Abrufen von Anmeldeinformationen (OAuth-Token).\",\n            \"get_homeinfo_error\": \"Fehler beim Abrufen von Home-Informationen.\",\n            \"get_cert_error\": \"Fehler beim Abrufen des Zentralzertifikats.\",\n            \"no_family_selected\": \"Keine Familie ausgewählt.\",\n            \"no_devices\": \"Im ausgewählten Haushalt sind keine Geräte vorhanden. Bitte wählen Sie einen Haushalt mit Geräten aus und fahren Sie fort.\",\n            \"no_filter_devices\": \"Gefilterte Geräte sind leer. Bitte wählen Sie gültige Filterkriterien aus und fahren Sie fort.\",\n            \"no_central_device\": \"Der Modus \\\"Zentral Gateway\\\" erfordert ein verfügbares Xiaomi-Zentral-Gateway im lokalen Netzwerk, in dem Home Assistant installiert ist. Überprüfen Sie, ob die ausgewählte Familie diese Anforderung erfüllt.\",\n            \"mdns_discovery_error\": \"Lokaler Geräteerkennungsdienstfehler.\",\n            \"update_config_error\": \"Fehler beim Aktualisieren der Konfigurationsinformationen.\",\n            \"not_confirm\": \"Änderungen wurden nicht bestätigt. Bitte bestätigen Sie die Auswahl, bevor Sie sie einreichen.\",\n            \"invalid_network_addr\": \"Ungültige IP-Adresse oder HTTP-Adresse vorhanden, bitte geben Sie eine gültige Adresse ein.\",\n            \"invalid_ip_addr\": \"Unzugängliche IP-Adresse vorhanden, bitte geben Sie eine gültige IP-Adresse ein.\",\n            \"invalid_http_addr\": \"Unzugängliche HTTP-Adresse vorhanden, bitte geben Sie eine gültige HTTP-Adresse ein.\",\n            \"invalid_default_addr\": \"Die Standard-Netzwerkerkennungsadresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration oder verwenden Sie eine benutzerdefinierte Netzwerkerkennungsadresse.\",\n            \"unreachable_oauth2_host\": \"OAuth2-Authentifizierungsadresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\",\n            \"unreachable_http_host\": \"Xiaomi HTTP API-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\",\n            \"unreachable_spec_host\": \"Xiaomi SPEC API-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\",\n            \"unreachable_mqtt_broker\": \"Xiaomi MQTT Broker-Adresse ist nicht erreichbar, bitte überprüfen Sie die Netzwerkkonfiguration.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Konfiguration fehlgeschlagen. Netzwerkverbindungsfehler. Überprüfen Sie die Netzwerkkonfiguration des Geräts.\",\n            \"options_flow_error\": \"Integrationsneukonfigurationsfehler: {error}\",\n            \"re_add\": \"Fügen Sie die Integration erneut hinzu. Fehlermeldung: {error}\",\n            \"storage_error\": \"Integrations-Speichermodulfehler. Bitte versuchen Sie es erneut oder fügen Sie die Integration erneut hinzu: {error}\",\n            \"inconsistent_account\": \"Kontoinformationen sind inkonsistent. Bitte melden Sie sich mit den richtigen Kontoinformationen an.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/en.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Xiaomi Home Integration\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Risk Notice\",\n                \"description\": \"1. Your Xiaomi user information and device information will be stored in the Home Assistant system. **Xiaomi cannot guarantee the security of the Home Assistant storage mechanism**. You are responsible for preventing your information from being stolen.\\r\\n2. This integration is maintained by the open-source community. There may be stability issues or other problems. When encountering issues or bugs of this integration, **you should seek help from the open-source community rather than contacting Xiaomi customer service**.\\r\\n3. You need some technical ability to maintain your local operating environment. The integration is not user-friendly for beginners.\\r\\n4. Please read the README file before starting.\\n\\n5. To ensure stable use of the integration and prevent interface abuse, **this integration is only allowed to be used in Home Assistant. For details, please refer to the LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"I am aware of the above risks and willing to voluntarily assume any risks associated with the use of the integration.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Basic configuration\",\n                \"description\": \"### Login Region\\r\\nSelect the region of your Xiaomi account. You can find it in the Xiaomi Home APP > Profile (located in the menu at the bottom) > Additional settings > About Xiaomi Home.\\r\\n### Language\\r\\nSelect the language of the device and entity names. Some sentences without translation will be displayed in English.\\r\\n### OAuth2 Redirect URL\\r\\nThe OAuth2 authentication redirect address is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. The Home Assistant needs to be in the same local area network as the current operating terminal (e.g., the personal computer) and the operating terminal can access the Home Assistant home page through this address. Otherwise, the login authentication may fail.\\r\\n### Integrated Network Configuration\\r\\nCheck if the local network is functioning properly and if the related network resources are accessible. **It is recommended to select this when adding for the first time.**\\r\\n### Note\\r\\n- For users with hundreds or more Mi Home devices, the initial addition of the integration will take some time. Please be patient.\\r\\n- If Home Assistant is running in a Docker environment, please ensure that the Docker network mode is set to host, otherwise local control functionality may not work properly.\\r\\n- The local control functionality of the integration has some dependencies. Please read the README carefully.\",\n                \"data\": {\n                    \"cloud_server\": \"Login Region\",\n                    \"integration_language\": \"Language\",\n                    \"oauth_redirect_url\": \"OAuth2 Redirect URL\",\n                    \"network_detect_config\": \"Integrated Network Configuration\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Integrated Network Configuration\",\n                \"description\": \"## Usage Introduction\\r\\n### Network Detection Address\\r\\nUsed to check if the network is functioning properly. If not set, the system default address will be used. If the default address check fails, you can try entering a custom address.\\r\\n- You can enter multiple detection addresses, separated by commas, such as `8.8.8.8,https://www.bing.com`\\r\\n- If it is an IP address, detection will be done via ping. If it is an HTTP(s) address, detection will be done via HTTP GET request.\\r\\n- If you want to restore the system default detection address, please enter a comma `,` and click 'Next'.\\r\\n- **This configuration is global, and changes will affect other integration instances. Please modify with caution.**\\r\\n### Check Network Dependencies\\r\\nCheck the following network dependencies one by one to see if they are accessible. If the related addresses are not accessible, it will cause integration issues.\\r\\n- OAuth2 Authentication Address: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API Address: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API Address: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker Address: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Network Detection Address\",\n                    \"check_network_deps\": \"Check Network Dependencies\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Login Error\",\n                \"description\": \"Click NEXT to try again.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Select Family and Device\",\n                \"description\": \"## Introduction\\r\\n### Import Device's Family\\r\\nThe integration will add devices from the selected family.\\r\\n### Room Name Sync Mode\\r\\nWhen syncing devices from the Mi Home APP to Home Assistant, the naming of the area in Home Assistant will follow the rules below. Note that the sync process will not change the family and room settings in the Mi Home APP.\\r\\n- Do not sync: The device will not be added to any area.\\r\\n- Other options: The area to which the device is added will be named after the family or room name in the Mi Home APP.\\r\\n### Advanced Settings\\r\\nShow advanced settings to modify the professional configuration options of the integration.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Hello! Please select the family to which you want to add the device.\",\n                \"data\": {\n                    \"home_infos\": \"Import Device's Family\",\n                    \"area_name_rule\": \"Room Name Sync Mode\",\n                    \"advanced_options\": \"Advanced Settings\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Advanced Settings\",\n                \"description\": \"## Introduction\\r\\n### Unless you are very clear about the meaning of the following options, please keep the default settings.\\r\\n### Filter Devices\\r\\nSupports filtering devices by room name and device type, and also supports device dimension filtering.\\r\\n### Control Mode\\r\\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi OT protocol to achieve local control. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\\r\\n- Cloud: All control commands are sent through the cloud.\\r\\n### Action Debug Mode\\r\\nFor the methods defined by the device MIoT-Spec-V2, in addition to generating notification entities, a text input box entity will also be generated. You can use it to send control commands to the device during debugging.\\r\\n### Hide Non-Standard Generated Entities\\r\\nHide entities generated by non-standard MIoT-Spec-V2 instances with names starting with \\\"*\\\".\\r\\n### Binary Sensor Display Mode\\r\\nDisplay binary sensors in Xiaomi Home as text sensor entity or binary sensor entity。\\r\\n### Display Device Status Change Notifications\\r\\nDisplay detailed device status change notifications, only showing the selected notifications.\",\n                \"data\": {\n                    \"devices_filter\": \"Filter Devices\",\n                    \"ctrl_mode\": \"Control Mode\",\n                    \"action_debug\": \"Action Debug Mode\",\n                    \"hide_non_standard_entities\": \"Hide Non-Standard Generated Entities\",\n                    \"display_binary_mode\": \"Binary Sensor Display Mode\",\n                    \"display_devices_changed_notify\": \"Display Device Status Change Notifications\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filter Devices\",\n                \"description\": \"## Usage Instructions\\r\\nSupports filtering devices by home room name, device access type, and device model, and also supports device dimension filtering. The filtering logic is as follows:\\r\\n- First, according to the statistical logic, get the union or intersection of all included items, then get the intersection or union of the excluded items, and finally subtract the [included summary result] from the [excluded summary result] to get the [filter result].\\r\\n- If no included items are selected, it means all are included.\\r\\n### Filter Mode\\r\\n- Exclude: Remove unwanted items.\\r\\n- Include: Include desired items.\\r\\n### Statistical Logic\\r\\n- AND logic: Take the intersection of all items in the same mode.\\r\\n- OR logic: Take the union of all items in the same mode.\\r\\n\\r\\nYou can also go to the [Configuration > Update Device List] page of the integration item, check [Filter Devices] to re-filter.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filter Family Rooms\",\n                    \"room_list\": \"Family Rooms\",\n                    \"type_filter_mode\": \"Filter Device Connect Type\",\n                    \"type_list\": \"Device Connect Type\",\n                    \"model_filter_mode\": \"Filter Device Model\",\n                    \"model_list\": \"Device Model\",\n                    \"devices_filter_mode\": \"Filter Devices\",\n                    \"device_list\": \"Device List\",\n                    \"statistics_logic\": \"Statistics Logic\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Click here to login{link_right}\\r\\n(You will be automatically redirected to the next page after a successful login)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Please read the risk notice.\",\n            \"get_token_error\": \"Failed to retrieve login authorization information (OAuth token).\",\n            \"get_homeinfo_error\": \"Failed to retrieve home information.\",\n            \"mdns_discovery_error\": \"Local device discovery service exception.\",\n            \"get_cert_error\": \"Failed to retrieve the central hub gateway certificate.\",\n            \"no_family_selected\": \"No home selected.\",\n            \"no_devices\": \"There are no devices in the selected home. Please select a home with devices and continue.\",\n            \"no_filter_devices\": \"Filtered devices are empty. Please select valid filter criteria and continue.\",\n            \"no_central_device\": \"[Central Hub Gateway Mode] requires a Xiaomi central hub gateway available in the local network where Home Assistant exists. Please check if the selected home meets the requirement.\",\n            \"invalid_network_addr\": \"Invalid IP address or HTTP address detected, please enter a valid address.\",\n            \"invalid_ip_addr\": \"Unreachable IP address detected, please enter a valid IP address.\",\n            \"invalid_http_addr\": \"Unreachable HTTP address detected, please enter a valid HTTP address.\",\n            \"invalid_default_addr\": \"Default network detection address is unreachable, please check network configuration or use a custom network detection address.\",\n            \"unreachable_oauth2_host\": \"Unable to reach OAuth2 authentication address, please check network configuration.\",\n            \"unreachable_http_host\": \"Unable to reach Xiaomi HTTP API address, please check network configuration.\",\n            \"unreachable_spec_host\": \"Unable to reach Xiaomi SPEC API address, please check network configuration.\",\n            \"unreachable_mqtt_broker\": \"Unable to reach Xiaomi MQTT Broker address, please check network configuration.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Failed to get Home Assistant UUID.\",\n            \"network_connect_error\": \"Configuration failed. The network connection is abnormal. Please check the equipment network configuration.\",\n            \"already_configured\": \"Configuration for this user is already completed. Please go to the integration page and click the CONFIGURE button for modifications.\",\n            \"invalid_auth_info\": \"Authentication information has expired. Please go to the integration page and click the CONFIGURE button to re-authenticate.\",\n            \"config_flow_error\": \"Integration configuration error: {error}.\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Authentication Configuration\",\n                \"description\": \"Local authentication information has expired. Please restart the authentication process.\\r\\n### Current Login Region: {cloud_server}\\r\\n### OAuth2 Redirect URL\\r\\nThe OAuth2 authentication redirect address is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. The Home Assistant needs to be in the same local area network as the current operating terminal (e.g., the personal computer) and the operating terminal can access the Home Assistant home page through this address. Otherwise, the login authentication may fail.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"OAuth2 Redirect URL\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"An error occurred during login.\",\n                \"description\": \"Click NEXT to retry.\"\n            },\n            \"config_options\": {\n                \"title\": \"Configuration Options\",\n                \"description\": \"### Hello, {nick_name}\\r\\n\\r\\nXiaomi ID: {uid}\\r\\nCurrent Login Region: {cloud_server}\\r\\nIntegration Instance ID: {instance_id}\\r\\n\\r\\nPlease select the options you need to configure, then click NEXT.\",\n                \"data\": {\n                    \"integration_language\": \"Integration Language\",\n                    \"update_user_info\": \"Update user information\",\n                    \"update_devices\": \"Update device list\",\n                    \"action_debug\": \"Debug mode for action\",\n                    \"hide_non_standard_entities\": \"Hide non-standard created entities\",\n                    \"display_binary_mode\": \"Binary Sensor Display Mode\",\n                    \"display_devices_changed_notify\": \"Display device status change notifications\",\n                    \"update_trans_rules\": \"Update entity conversion rules\",\n                    \"update_lan_ctrl_config\": \"Update LAN control configuration\",\n                    \"network_detect_config\": \"Integrated network configuration\",\n                    \"cover_dead_zone_width\": \"Cover dead zone width\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Update User Nickname\",\n                \"description\": \"Hello {nick_name}, you can modify your custom nickname below.\",\n                \"data\": {\n                    \"nick_name\": \"Nick Name\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Re-select Home and Devices\",\n                \"description\": \"## Usage Instructions\\r\\n### Import devices from home\\r\\nThe integration will add devices from the selected homes.\\r\\n### Filter Devices\\r\\nSupports filtering devices by home room name, device access type, and device model, and also supports device dimension filtering. **{local_count}** devices have been filtered.\\r\\n### Control mode\\r\\n- Auto: When there is an available Xiaomi central hub gateway in the local area network, Home Assistant will prioritize sending device control commands through the central hub gateway to achieve local control. If there is no central hub gateway in the local area network, it will attempt to send control commands through Xiaomi LAN control function. Only when the above local control conditions are not met, the device control commands will be sent through the cloud.\\r\\n- Cloud: All control commands are sent through the cloud.\",\n                \"data\": {\n                    \"home_infos\": \"Import devices from home\",\n                    \"devices_filter\": \"Filter devices\",\n                    \"ctrl_mode\": \"Control mode\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filter Devices\",\n                \"description\": \"## Usage Instructions\\r\\nSupports filtering devices by home room name, device access type, and device model, and also supports device dimension filtering. The filtering logic is as follows:\\r\\n- First, according to the statistical logic, get the union or intersection of all included items, then get the intersection or union of the excluded items, and finally subtract the [included summary result] from the [excluded summary result] to get the [filter result].\\r\\n- If no included items are selected, it means all are included.\\r\\n### Filter Mode\\r\\n- Exclude: Remove unwanted items.\\r\\n- Include: Include desired items.\\r\\n### Statistical Logic\\r\\n- AND logic: Take the intersection of all items in the same mode.\\r\\n- OR logic: Take the union of all items in the same mode.\\r\\n\\r\\nYou can also go to the [Configuration > Update Device List] page of the integration item, check [Filter Devices] to re-filter.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filter Family Rooms\",\n                    \"room_list\": \"Family Rooms\",\n                    \"type_filter_mode\": \"Filter Device Connect Type\",\n                    \"type_list\": \"Device Connect Type\",\n                    \"model_filter_mode\": \"Filter Device Model\",\n                    \"model_list\": \"Device Model\",\n                    \"devices_filter_mode\": \"Filter Devices\",\n                    \"device_list\": \"Device List\",\n                    \"statistics_logic\": \"Statistics Logic\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Update Entities Transformation Rules\",\n                \"description\": \"## Usage Instructions\\r\\n- Update the entity information of devices in the current integration instance, including MIoT-Spec-V2 multilingual configuration, boolean translation, and model filtering.\\r\\n- **Warning**: This is a global configuration and will update the local cache. It will affect all integration instances.\\r\\n- This operation will take some time, please be patient. Check \\\"Confirm Update\\\" and click \\\"Next\\\" to start updating **{urn_count}** rules, otherwise skip the update.\",\n                \"data\": {\n                    \"confirm\": \"Confirm the update\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Update lan control configuration\",\n                \"description\": \"## Usage Instructions\\r\\nUpdate the configurations for Xiaomi LAN control function. When the cloud and the central hub gateway cannot control the devices, the integration will attempt to control the devices through the LAN. If no network card is selected, the LAN control function will not take effect.\\r\\n- Only MIoT-Spec-V2 compatible IP devices in the LAN are supported. Some devices produced before 2020 may not support LAN control or LAN subscription.\\r\\n- Please select the network card(s) on the same network as the devices to be controlled. Multiple network cards can be selected. If Home Assistant have two or more connections to the local area network because of the multiple selection of the network cards, it is recommended to select the one with the best network connection, otherwise it may have bad effect on the devices.\\r\\n- If there are terminal devices (Xiaomi speaker with screen, mobile phone, etc.) in the LAN that support local control, enabling LAN subscription may cause local automation and device anomalies.\\r\\n- **Warning**: This is a global configuration. It will affect all integration instances. Please use it with caution.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Please select the network card to use\",\n                    \"enable_subscribe\": \"Enable LAN subscription\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Integrated Network Configuration\",\n                \"description\": \"## Usage Introduction\\r\\n### Network Detection Address\\r\\nUsed to check if the network is functioning properly. If not set, the system default address will be used. If the default address check fails, you can try entering a custom address.\\r\\n- You can enter multiple detection addresses, separated by commas, such as `8.8.8.8,https://www.bing.com`\\r\\n- If it is an IP address, detection will be done via ping. If it is an HTTP(s) address, detection will be done via HTTP GET request.\\r\\n- If you want to restore the system default detection address, please enter a comma `,` and click 'Next'.\\r\\n- **This configuration is global, and changes will affect other integration instances. Please modify with caution.**\\r\\n### Check Network Dependencies\\r\\nCheck the following network dependencies one by one to see if they are accessible. If the related addresses are not accessible, it will cause integration issues.\\r\\n- OAuth2 Authentication Address: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API Address: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API Address: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker Address: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Network Detection Address\",\n                    \"check_network_deps\": \"Check Network Dependencies\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Confirm Configuration\",\n                \"description\": \"Hello **{nick_name}**, please confirm the latest configuration information and then Click SUBMIT.\\r\\nThe integration will reload using the updated configuration.\\r\\n\\r\\nIntegration Language:\\t{lang_new}\\r\\nNickname:\\t{nick_name_new}\\r\\nDebug mode for action:\\t{action_debug}\\r\\nHide non-standard created entities:\\t{hide_non_standard_entities}\\r\\nCover dead zone width:\\t{cover_width_new}\\r\\nDisplay device status change notifications:\\t{display_devices_changed_notify}\\r\\nDevice Changes:\\tAdd **{devices_add}** devices, Remove **{devices_remove}** devices\\r\\nTransformation rules change:\\tThere are a total of **{trans_rules_count}** rules, and updated **{trans_rules_count_success}** rules\",\n                \"data\": {\n                    \"confirm\": \"Confirm the change\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Please click here to re-login{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Not authenticated. Please click the authentication link to authenticate user identity.\",\n            \"get_token_error\": \"Failed to retrieve login authorization information (OAuth token).\",\n            \"get_homeinfo_error\": \"Failed to retrieve home information.\",\n            \"get_cert_error\": \"Failed to retrieve the central hub gateway certificate.\",\n            \"no_devices\": \"There are no devices in the selected home. Please select a home with devices and continue.\",\n            \"no_filter_devices\": \"Filtered devices are empty. Please select valid filter criteria and continue.\",\n            \"no_family_selected\": \"No home selected.\",\n            \"no_central_device\": \"[Central Hub Gateway Mode] requires a Xiaomi central hub gateway available in the local network where Home Assistant exists. Please check if the selected home meets the requirement.\",\n            \"mdns_discovery_error\": \"Local device discovery service exception.\",\n            \"update_config_error\": \"Failed to update configuration information.\",\n            \"not_confirm\": \"Changes are not confirmed. Please confirm the change before submitting.\",\n            \"invalid_network_addr\": \"Invalid IP address or HTTP address detected, please enter a valid address.\",\n            \"invalid_ip_addr\": \"Unreachable IP address detected, please enter a valid IP address.\",\n            \"invalid_http_addr\": \"Unreachable HTTP address detected, please enter a valid HTTP address.\",\n            \"invalid_default_addr\": \"Default network detection address is unreachable, please check network configuration or use a custom network detection address.\",\n            \"unreachable_oauth2_host\": \"Unable to reach OAuth2 authentication address, please check network configuration.\",\n            \"unreachable_http_host\": \"Unable to reach Xiaomi HTTP API address, please check network configuration.\",\n            \"unreachable_spec_host\": \"Unable to reach Xiaomi SPEC API address, please check network configuration.\",\n            \"unreachable_mqtt_broker\": \"Unable to reach Xiaomi MQTT Broker address, please check network configuration.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Configuration failed. The network connection is abnormal. Please check the equipment network configuration.\",\n            \"options_flow_error\": \"Integration re-configuration error: {error}\",\n            \"re_add\": \"Please re-add the integration. Error message: {error}\",\n            \"storage_error\": \"Integration storage module exception. Please try again or re-add the integration: {error}\",\n            \"inconsistent_account\": \"Account information is inconsistent. Please login with the correct account.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/es.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Integración de Xiaomi Home\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Aviso de riesgo\",\n                \"description\": \"1. Su **información de usuario de Xiaomi e información del dispositivo** se almacenará en su sistema Home Assistant. **Xiaomi no puede garantizar la seguridad del mecanismo de almacenamiento de Home Assistant**. Usted es responsable de evitar que su información sea robada.\\r\\n2. Esta integración es mantenida por la comunidad de código abierto y puede haber problemas de estabilidad u otros problemas. Cuando tenga problemas relacionados con el uso de esta integración, **busque ayuda en la comunidad de código abierto en lugar de contactar al servicio al cliente de Xiaomi**.\\r\\n3. Es necesario tener ciertas habilidades técnicas para mantener su entorno de ejecución local, esta integración no es amigable para los usuarios novatos.\\r\\n4. Antes de utilizar esta integración, por favor **lea detenidamente el archivo README**.\\r\\n5. Para garantizar el uso estable de la integración y prevenir el abuso de la interfaz, **esta integración solo está permitida en Home Assistant. Para más detalles, consulte la LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"He leído y entiendo los riesgos anteriores, y estoy dispuesto a asumir cualquier riesgo relacionado con el uso de esta integración.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Configuración básica\",\n                \"description\": \"### Región de inicio de sesión\\r\\nSeleccione la región donde se encuentra su cuenta de Xiaomi. Puede consultar esta información en `Xiaomi Home APP > Yo (ubicado en el menú inferior) > Más ajustes > Acerca de Xiaomi Home`.\\r\\n### Idioma\\r\\nSeleccione el idioma utilizado para los nombres de los dispositivos y entidades. Las partes de las frases que no están traducidas se mostrarán en inglés.\\r\\n### Dirección de redireccionamiento de autenticación de OAuth2\\r\\nLa dirección de redireccionamiento de autenticación de OAuth2 es **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant debe estar en la misma red local que el terminal de operación actual (por ejemplo, una computadora personal) y el terminal de operación debe poder acceder a la página de inicio de Home Assistant a través de esta dirección, de lo contrario, la autenticación de inicio de sesión podría fallar.\\r\\n### Configuración de Red Integrada\\r\\nVerifique si la red local funciona correctamente y si los recursos de red relacionados son accesibles. **Se recomienda seleccionar esto al agregar por primera vez.**\\r\\n### Nota\\r\\n- Para los usuarios con cientos o más dispositivos Mi Home, la adición inicial de la integración tomará algún tiempo. Por favor, sea paciente.\\r\\n- Si Home Assistant se está ejecutando en un entorno Docker, asegúrese de que el modo de red de Docker esté configurado en host, de lo contrario, la funcionalidad de control local puede no funcionar correctamente.\\r\\n- La funcionalidad de control local de la integración tiene algunas dependencias. Por favor, lea el README cuidadosamente.\",\n                \"data\": {\n                    \"cloud_server\": \"Región de inicio de sesión\",\n                    \"integration_language\": \"Idioma\",\n                    \"oauth_redirect_url\": \"Dirección de redireccionamiento de autenticación de OAuth2\",\n                    \"network_detect_config\": \"Configuración de Red Integrada\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuración de Red Integrada\",\n                \"description\": \"## Introducción al Uso\\r\\n### Dirección de Detección de Red\\r\\nSe utiliza para verificar si la red funciona correctamente. Si no se establece, se utilizará la dirección predeterminada del sistema. Si la verificación de la dirección predeterminada falla, puede intentar ingresar una dirección personalizada.\\r\\n- Puede ingresar varias direcciones de detección, separadas por comas, como `8.8.8.8,https://www.bing.com`\\r\\n- Si es una dirección IP, la detección se realizará mediante ping. Si es una dirección HTTP(s), la detección se realizará mediante una solicitud HTTP GET.\\r\\n- Si desea restaurar la dirección de detección predeterminada del sistema, ingrese una coma `,` y haga clic en 'Siguiente'.\\r\\n- **Esta configuración es global y los cambios afectarán a otras instancias de integración. Modifique con precaución.**\\r\\n### Verificar Dependencias de Red\\r\\nVerifique una por una las siguientes dependencias de red para ver si son accesibles. Si las direcciones relacionadas no son accesibles, causará problemas de integración.\\r\\n- Dirección de Autenticación OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Dirección de API HTTP de Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Dirección de API SPEC de Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Dirección del Broker MQTT de Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Dirección de Detección de Red\",\n                    \"check_network_deps\": \"Verificar Dependencias de Red\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Error de inicio de sesión\",\n                \"description\": \"Haga clic en \\\"Siguiente\\\" para volver a intentarlo\"\n            },\n            \"homes_select\": {\n                \"title\": \"Seleccionar familia y dispositivo\",\n                \"description\": \"## Introducción\\r\\n### Importar la familia del dispositivo\\r\\nLa integración añadirá dispositivos de la familia seleccionada.\\r\\n### Modo de sincronización del nombre de la habitación\\r\\nAl sincronizar dispositivos desde la APP Mi Home a Home Assistant, el nombre del área en Home Assistant seguirá las siguientes reglas. Tenga en cuenta que el proceso de sincronización no cambiará la configuración de la familia y la habitación en la APP Mi Home.\\r\\n- No sincronizar: El dispositivo no se añadirá a ninguna área.\\r\\n- Otras opciones: El área a la que se añade el dispositivo se nombrará según el nombre de la familia o la habitación en la APP Mi Home.\\r\\n### Configuración avanzada\\r\\nMostrar configuración avanzada para modificar las opciones de configuración profesional de la integración.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} ¡Hola! Por favor, seleccione la familia a la que desea añadir el dispositivo.\",\n                \"data\": {\n                    \"home_infos\": \"Importar la familia del dispositivo\",\n                    \"area_name_rule\": \"Modo de sincronización del nombre de la habitación\",\n                    \"advanced_options\": \"Configuración avanzada\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Opciones Avanzadas\",\n                \"description\": \"## Introducción\\r\\n### A menos que entienda claramente el significado de las siguientes opciones, manténgalas en su configuración predeterminada.\\r\\n### Filtrar dispositivos\\r\\nAdmite la filtración de dispositivos por nombre de habitación y tipo de dispositivo, y también admite la filtración por familia.\\r\\n### Modo de Control\\r\\n- Automático: Cuando hay una puerta de enlace central de Xiaomi disponible en la red local, Home Assistant enviará comandos de control de dispositivos a través de la puerta de enlace central para lograr la función de control local. Cuando no hay una puerta de enlace central en la red local, intentará enviar comandos de control a través del protocolo OT de Xiaomi para lograr la función de control local. Solo cuando no se cumplan las condiciones de control local anteriores, los comandos de control de dispositivos se enviarán a través de la nube.\\r\\n- Nube: Los comandos de control solo se envían a través de la nube.\\r\\n### Modo de Depuración de Acciones\\r\\nPara los métodos definidos por el dispositivo MIoT-Spec-V2, además de generar una entidad de notificación, también se generará una entidad de cuadro de texto que se puede utilizar para enviar comandos de control al dispositivo durante la depuración.\\r\\n### Ocultar Entidades Generadas No Estándar\\r\\nOcultar entidades generadas por instancias MIoT-Spec-V2 no estándar que comienzan con \\\"*\\\".\\r\\n### Modo de visualización del sensor binario\\r\\nMuestra los sensores binarios en Xiaomi Home como entidad de sensor de texto o entidad de sensor binario。\\r\\n### Mostrar notificaciones de cambio de estado del dispositivo\\r\\nMostrar notificaciones detalladas de cambio de estado del dispositivo, mostrando solo las notificaciones seleccionadas.\",\n                \"data\": {\n                    \"devices_filter\": \"Filtrar Dispositivos\",\n                    \"ctrl_mode\": \"Modo de Control\",\n                    \"action_debug\": \"Modo de Depuración de Acciones\",\n                    \"hide_non_standard_entities\": \"Ocultar Entidades Generadas No Estándar\",\n                    \"display_binary_mode\": \"Modo de visualización del sensor binario\",\n                    \"display_devices_changed_notify\": \"Mostrar notificaciones de cambio de estado del dispositivo\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrar Dispositivos\",\n                \"description\": \"## Instrucciones de uso\\r\\nAdmite la filtración de dispositivos por nombre de habitación, tipo de acceso del dispositivo y modelo de dispositivo, y también admite la filtración por dimensión del dispositivo. La lógica de filtración es la siguiente:\\r\\n- Primero, de acuerdo con la lógica estadística, obtenga la unión o intersección de todos los elementos incluidos, luego obtenga la intersección o unión de los elementos excluidos, y finalmente reste el [resultado del resumen incluido] del [resultado del resumen excluido] para obtener el [resultado del filtro].\\r\\n- Si no se seleccionan elementos incluidos, significa que todos están incluidos.\\r\\n### Modo de Filtración\\r\\n- Excluir: Eliminar elementos no deseados.\\r\\n- Incluir: Incluir elementos deseados.\\r\\n### Lógica Estadística\\r\\n- Lógica Y: Tomar la intersección de todos los elementos en el mismo modo.\\r\\n- Lógica O: Tomar la unión de todos los elementos en el mismo modo.\\r\\n\\r\\nTambién puede ir a la página [Configuración > Actualizar lista de dispositivos] del elemento de integración, marcar [Filtrar dispositivos] para volver a filtrar.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrar Habitaciones de la Familia\",\n                    \"room_list\": \"Habitaciones de la Familia\",\n                    \"type_filter_mode\": \"Filtrar Tipo de Dispositivo\",\n                    \"type_list\": \"Tipo de Dispositivo\",\n                    \"model_filter_mode\": \"Filtrar Modelo de Dispositivo\",\n                    \"model_list\": \"Modelo de Dispositivo\",\n                    \"devices_filter_mode\": \"Filtrar Dispositivos\",\n                    \"device_list\": \"Lista de Dispositivos\",\n                    \"statistics_logic\": \"Lógica de Estadísticas\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Haga clic aquí para iniciar sesión de nuevo{link_right}\\r\\n(Será redirigido automáticamente a la siguiente página después de un inicio de sesión exitoso)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Lea el texto de aviso de riesgo.\",\n            \"get_token_error\": \"Error al obtener la información de autorización de inicio de sesión (token OAuth).\",\n            \"get_homeinfo_error\": \"Error al obtener la información del hogar.\",\n            \"mdns_discovery_error\": \"Error en el servicio de descubrimiento de dispositivos locales.\",\n            \"get_cert_error\": \"Error al obtener el certificado de la puerta de enlace.\",\n            \"no_family_selected\": \"No se ha seleccionado ningún hogar.\",\n            \"no_devices\": \"No hay dispositivos en el hogar seleccionado. Por favor, seleccione un hogar con dispositivos y continúe.\",\n            \"no_filter_devices\": \"Los dispositivos filtrados están vacíos. Por favor, seleccione criterios de filtro válidos y continúe.\",\n            \"no_central_device\": \"【Modo de puerta de enlace central】Se requiere una puerta de enlace Xiaomi disponible en la red local donde se encuentra Home Assistant. Verifique si el hogar seleccionado cumple con este requisito.\",\n            \"invalid_network_addr\": \"Se detectó una dirección IP o HTTP no válida, por favor ingrese una dirección válida.\",\n            \"invalid_ip_addr\": \"Se detectó una dirección IP inaccesible, por favor ingrese una dirección IP válida.\",\n            \"invalid_http_addr\": \"Se detectó una dirección HTTP inaccesible, por favor ingrese una dirección HTTP válida.\",\n            \"invalid_default_addr\": \"La dirección de detección de red predeterminada no es accesible, por favor verifique la configuración de la red o use una dirección de detección de red personalizada.\",\n            \"unreachable_oauth2_host\": \"No se puede acceder a la dirección de autenticación OAuth2, por favor verifique la configuración de la red.\",\n            \"unreachable_http_host\": \"No se puede acceder a la dirección de la API HTTP de Xiaomi, por favor verifique la configuración de la red.\",\n            \"unreachable_spec_host\": \"No se puede acceder a la dirección de la API SPEC de Xiaomi, por favor verifique la configuración de la red.\",\n            \"unreachable_mqtt_broker\": \"No se puede acceder a la dirección del Broker MQTT de Xiaomi, por favor verifique la configuración de la red.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Error al obtener el UUID de Home Assistant.\",\n            \"network_connect_error\": \"La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.\",\n            \"already_configured\": \"Esta cuenta ya ha finalizado la configuración. Ingrese a la página de integración y haga clic en el botón \\\"Configurar\\\" para modificar la configuración.\",\n            \"invalid_auth_info\": \"La información de autorización ha caducado. Ingrese a la página de integración y haga clic en el botón \\\"Configurar\\\" para volver a autenticarse.\",\n            \"config_flow_error\": \"Error de configuración de integración: {error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Configuración de autorización\",\n                \"description\": \"Se detectó que la información de autenticación local ha caducado, vuelva a autenticarse\\r\\n### Región de inicio de sesión actual: {cloud_server}\\r\\n### Dirección de redireccionamiento de autenticación de OAuth2\\r\\nLa dirección de redireccionamiento de autenticación de OAuth2 es **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant debe estar en la misma red local que el terminal de operación actual (por ejemplo, una computadora personal) y el terminal de operación debe poder acceder a la página de inicio de Home Assistant a través de esta dirección, de lo contrario, la autenticación de inicio de sesión podría fallar.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"Dirección de redireccionamiento de autenticación de OAuth2\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Error de inicio de sesión\",\n                \"description\": \"Haga clic en \\\"Siguiente\\\" para volver a intentarlo\"\n            },\n            \"config_options\": {\n                \"title\": \"Opciones de configuración\",\n                \"description\": \"### ¡Hola, {nick_name}!\\r\\n\\r\\nID de cuenta de Xiaomi: {uid}\\r\\nRegión de inicio de sesión actual: {cloud_server}\\r\\nID de Instancia de Integración: {instance_id}\\r\\n\\r\\nSeleccione las opciones que desea reconfigurar y haga clic en \\\"Siguiente\\\".\",\n                \"data\": {\n                    \"integration_language\": \"Idioma de la integración\",\n                    \"update_user_info\": \"Actualizar información de usuario\",\n                    \"update_devices\": \"Actualizar lista de dispositivos\",\n                    \"action_debug\": \"Modo de depuración de Action\",\n                    \"hide_non_standard_entities\": \"Ocultar entidades generadas no estándar\",\n                    \"display_binary_mode\": \"Modo de visualización del sensor binario\",\n                    \"display_devices_changed_notify\": \"Mostrar notificaciones de cambio de estado del dispositivo\",\n                    \"update_trans_rules\": \"Actualizar reglas de conversión de entidad\",\n                    \"update_lan_ctrl_config\": \"Actualizar configuración de control LAN\",\n                    \"network_detect_config\": \"Configuración de Red Integrada\",\n                    \"cover_dead_zone_width\": \"Anchura del punto ciego de la cortina\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Actualizar apodo de usuario\",\n                \"description\": \"¡Hola, {nick_name}! Modifique su apodo de usuario a continuación.\",\n                \"data\": {\n                    \"nick_name\": \"Apodo de usuario\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Recomendar hogares y dispositivos\",\n                \"description\": \"## Instrucciones de uso\\r\\n### Hogares de dispositivos importados\\r\\nLa integración agregará los dispositivos en los hogares seleccionados.\\r\\n### Filtrar dispositivos\\r\\nAdmite la filtración de dispositivos por nombre de habitación, tipo de acceso del dispositivo y modelo de dispositivo, y también admite la filtración por dimensión del dispositivo. Se han filtrado **{local_count}** dispositivos.\\r\\n### Modo de control\\r\\n- Automático: Cuando hay un gateway central de Xiaomi disponible en la red local, Home Assistant priorizará el envío de comandos de control de dispositivos a través del gateway central para lograr un control localizado. Si no hay un gateway central en la red local, intentará enviar comandos de control a través del protocolo Xiaomi OT para lograr un control localizado. Solo cuando no se cumplan las condiciones anteriores de control localizado, los comandos de control del dispositivo se enviarán a través de la nube.\\r\\n- Nube: Los comandos de control solo se envían a través de la nube.\",\n                \"data\": {\n                    \"home_infos\": \"Hogares de dispositivos importados\",\n                    \"devices_filter\": \"Filtrar dispositivos\",\n                    \"ctrl_mode\": \"Modo de control\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrar Dispositivos\",\n                \"description\": \"## Instrucciones de uso\\r\\nAdmite la filtración de dispositivos por nombre de habitación, tipo de acceso del dispositivo y modelo de dispositivo, y también admite la filtración por dimensión del dispositivo. La lógica de filtración es la siguiente:\\r\\n- Primero, de acuerdo con la lógica estadística, obtenga la unión o intersección de todos los elementos incluidos, luego obtenga la intersección o unión de los elementos excluidos, y finalmente reste el [resultado del resumen incluido] del [resultado del resumen excluido] para obtener el [resultado del filtro].\\r\\n- Si no se seleccionan elementos incluidos, significa que todos están incluidos.\\r\\n### Modo de Filtración\\r\\n- Excluir: Eliminar elementos no deseados.\\r\\n- Incluir: Incluir elementos deseados.\\r\\n### Lógica Estadística\\r\\n- Lógica Y: Tomar la intersección de todos los elementos en el mismo modo.\\r\\n- Lógica O: Tomar la unión de todos los elementos en el mismo modo.\\r\\n\\r\\nTambién puede ir a la página [Configuración > Actualizar lista de dispositivos] del elemento de integración, marcar [Filtrar dispositivos] para volver a filtrar.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrar Habitaciones de la Familia\",\n                    \"room_list\": \"Habitaciones de la Familia\",\n                    \"type_filter_mode\": \"Filtrar Tipo de Dispositivo\",\n                    \"type_list\": \"Tipo de Dispositivo\",\n                    \"model_filter_mode\": \"Filtrar Modelo de Dispositivo\",\n                    \"model_list\": \"Modelo de Dispositivo\",\n                    \"devices_filter_mode\": \"Filtrar Dispositivos\",\n                    \"device_list\": \"Lista de Dispositivos\",\n                    \"statistics_logic\": \"Lógica de Estadísticas\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Actualizar reglas de conversión de entidad\",\n                \"description\": \"## Instrucciones de uso\\r\\n- Actualice la información de la entidad de los dispositivos en la instancia de integración actual, incluida la configuración multilingüe de SPEC, la traducción booleana de SPEC y el filtrado de modelos de SPEC.\\r\\n- **Advertencia: Esta configuración es una configuración global** y actualizará directamente la caché local. Si hay dispositivos del mismo modelo en otras instancias de integración, las instancias relevantes también se actualizarán después de recargarlas.\\r\\n- Esta operación tomará algún tiempo, por favor sea paciente. Marque \\\"Confirmar actualización\\\" y haga clic en \\\"Siguiente\\\" para comenzar a actualizar **{urn_count}** reglas, de lo contrario, omita la actualización.\",\n                \"data\": {\n                    \"confirm\": \"Confirmar actualización\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Actualizar configuración de control LAN\",\n                \"description\": \"## Instrucciones de uso\\r\\nActualice la información de configuración para **control LAN de dispositivos Xiaomi Home**. Cuando la nube y la puerta de enlace central no puedan controlar los dispositivos, la integración intentará controlar los dispositivos a través de la LAN; si no se selecciona ninguna tarjeta de red, el control LAN no se habilitará.\\r\\n- Actualmente, solo se admiten dispositivos WiFi **SPEC v2** en la LAN. Algunos dispositivos más antiguos pueden no admitir el control o la sincronización de propiedades.\\r\\n- Seleccione la(s) tarjeta(s) de red en la misma red que los dispositivos (se admiten múltiples selecciones). Si la tarjeta de red seleccionada tiene dos o más conexiones en la misma red, se recomienda seleccionar la que tenga la mejor conexión de red, de lo contrario, puede **afectar el uso normal de los dispositivos**.\\r\\n- **Si hay dispositivos terminales (puertas de enlace, teléfonos móviles, etc.) en la LAN que admiten el control local, habilitar la suscripción LAN puede causar automatización local o anomalías en los dispositivos. Úselo con precaución**.\\r\\n- **Advertencia: Esta configuración es global y los cambios afectarán a otras instancias de integración. Modifique con precaución**.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Por favor, seleccione la tarjeta de red a utilizar\",\n                    \"enable_subscribe\": \"Habilitar suscripción LAN\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuración de Red Integrada\",\n                \"description\": \"## Introducción al Uso\\r\\n### Dirección de Detección de Red\\r\\nSe utiliza para verificar si la red funciona correctamente. Si no se establece, se utilizará la dirección predeterminada del sistema. Si la verificación de la dirección predeterminada falla, puede intentar ingresar una dirección personalizada.\\r\\n- Puede ingresar varias direcciones de detección, separadas por comas, como `8.8.8.8,https://www.bing.com`\\r\\n- Si es una dirección IP, la detección se realizará mediante ping. Si es una dirección HTTP(s), la detección se realizará mediante una solicitud HTTP GET.\\r\\n- Si desea restaurar la dirección de detección predeterminada del sistema, ingrese una coma `,` y haga clic en 'Siguiente'.\\r\\n- **Esta configuración es global y los cambios afectarán a otras instancias de integración. Modifique con precaución.**\\r\\n### Verificar Dependencias de Red\\r\\nVerifique una por una las siguientes dependencias de red para ver si son accesibles. Si las direcciones relacionadas no son accesibles, causará problemas de integración.\\r\\n- Dirección de Autenticación OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Dirección de API HTTP de Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Dirección de API SPEC de Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Dirección del Broker MQTT de Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Dirección de Detección de Red\",\n                    \"check_network_deps\": \"Verificar Dependencias de Red\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Confirmar configuración\",\n                \"description\": \"¡Hola, **{nick_name}**! Por favor, confirme la última información de configuración y haga clic en \\\"Enviar\\\" para finalizar la configuración.\\r\\nLa integración se volverá a cargar con la nueva configuración.\\r\\n\\r\\nIdioma de la integración:\\t{lang_new}\\r\\nApodo de usuario:\\t{nick_name_new}\\r\\nModo de depuración de Action:\\t{action_debug}\\r\\nOcultar entidades generadas no estándar:\\t{hide_non_standard_entities}\\r\\nAnchura del punto ciego de la cortina:\\t{cover_width_new}\\r\\nMostrar notificaciones de cambio de estado del dispositivo:\\t{display_devices_changed_notify}\\r\\nCambios de dispositivos:\\t{devices_add} dispositivos agregados, {devices_remove} dispositivos eliminados\\r\\nCambios en las reglas de conversión:\\t{trans_rules_count} reglas en total, {trans_rules_count_success} reglas actualizadas\",\n                \"data\": {\n                    \"confirm\": \"Confirmar modificación\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Haga clic aquí para iniciar sesión de nuevo{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Usuario no autenticado. Haga clic en el enlace de autenticación para autenticarse.\",\n            \"get_token_error\": \"Error al obtener la información de autorización de inicio de sesión (token OAuth).\",\n            \"get_homeinfo_error\": \"Error al obtener la información del hogar.\",\n            \"get_cert_error\": \"Error al obtener el certificado de la puerta de enlace.\",\n            \"no_family_selected\": \"No se ha seleccionado ningún hogar.\",\n            \"no_devices\": \"No hay dispositivos en el hogar seleccionado. Por favor, seleccione un hogar con dispositivos y continúe.\",\n            \"no_filter_devices\": \"Los dispositivos filtrados están vacíos. Por favor, seleccione criterios de filtro válidos y continúe.\",\n            \"no_central_device\": \"【Modo de puerta de enlace central】Se requiere una puerta de enlace Xiaomi disponible en la red local donde se encuentra Home Assistant. Verifique si el hogar seleccionado cumple con este requisito.\",\n            \"mdns_discovery_error\": \"Error en el servicio de descubrimiento de dispositivos locales.\",\n            \"update_config_error\": \"Error al actualizar la información de configuración.\",\n            \"not_confirm\": \"No se ha confirmado la opción de modificación. Seleccione y confirme la opción antes de enviar.\",\n            \"invalid_network_addr\": \"Se detectó una dirección IP o HTTP no válida, por favor ingrese una dirección válida.\",\n            \"invalid_ip_addr\": \"Se detectó una dirección IP inaccesible, por favor ingrese una dirección IP válida.\",\n            \"invalid_http_addr\": \"Se detectó una dirección HTTP inaccesible, por favor ingrese una dirección HTTP válida.\",\n            \"invalid_default_addr\": \"La dirección de detección de red predeterminada no es accesible, por favor verifique la configuración de la red o use una dirección de detección de red personalizada.\",\n            \"unreachable_oauth2_host\": \"No se puede acceder a la dirección de autenticación OAuth2, por favor verifique la configuración de la red.\",\n            \"unreachable_http_host\": \"No se puede acceder a la dirección de la API HTTP de Xiaomi, por favor verifique la configuración de la red.\",\n            \"unreachable_spec_host\": \"No se puede acceder a la dirección de la API SPEC de Xiaomi, por favor verifique la configuración de la red.\",\n            \"unreachable_mqtt_broker\": \"No se puede acceder a la dirección del Broker MQTT de Xiaomi, por favor verifique la configuración de la red.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"La configuración ha fallado. Existe un problema con la conexión de red, verifique la configuración de red del dispositivo.\",\n            \"options_flow_error\": \"Error al reconfigurar la integración: {error}\",\n            \"re_add\": \"Agregue la integración de nuevo, mensaje de error: {error}\",\n            \"storage_error\": \"Error en el módulo de almacenamiento de integración. Intente de nuevo o agregue la integración de nuevo: {error}\",\n            \"inconsistent_account\": \"La información de la cuenta no coincide. Inicie sesión con la cuenta correcta.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/fr.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Intégration Xiaomi Home\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Notification de risques\",\n                \"description\": \"1. Vos **informations utilisateur Xiaomi et informations sur l'appareil** seront stockées dans votre système Home Assistant. **Xiaomi ne peut garantir la sécurité du mécanisme de stockage de Home Assistant**. Vous êtes responsable de la protection de vos informations contre le vol.\\r\\n2. Cette intégration est maintenue par la communauté open source et peut rencontrer des problèmes de stabilité ou autres problèmes. Si vous rencontrez des problèmes liés à l'utilisation de cette intégration, vous devez **demander de l'aide à la communauté open source plutôt que de contacter le service client Xiaomi**.\\r\\n3. Vous avez besoin d'un certain niveau de compétences techniques pour maintenir votre environnement d'exécution local. Cette intégration n'est pas conviviale pour les utilisateurs novices.\\r\\n4. Avant d'utiliser cette intégration, veuillez **lire attentivement le README**.\\r\\n5. Pour garantir une utilisation stable de l'intégration et prévenir les abus d'interface, **cette intégration n'est autorisée qu'à être utilisée dans Home Assistant. Pour plus de détails, veuillez consulter la LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Je suis informé des risques ci-dessus et j'accepte volontairement les risques associés à l'utilisation de cette intégration.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Configuration de base\",\n                \"description\": \"### Région de connexion\\r\\nSélectionnez la région où se trouve votre compte Xiaomi. Vous pouvez le trouver dans `Xiaomi Home APP > Mon (situé dans le menu inférieur) > Plus de paramètres > À propos de Xiaomi Home`.\\r\\n### Langue\\r\\nChoisissez la langue utilisée pour les noms de périphériques et d'entités. Les parties de phrases sans traduction seront affichées en anglais.\\r\\n### Adresse de redirection de l'authentification OAuth2\\r\\nL'adresse de redirection de l'authentification OAuth2 est **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant doit être dans le même réseau local que le terminal de l'opération actuelle (par exemple, un ordinateur personnel) et le terminal de l'opération doit pouvoir accéder à la page d'accueil de Home Assistant via cette adresse. Sinon, l'authentification de connexion peut échouer.\\r\\n### Configuration Réseau Intégrée\\r\\nVérifiez si le réseau local fonctionne correctement et si les ressources réseau associées sont accessibles. **Il est recommandé de sélectionner cela lors du premier ajout.**\\r\\n### Remarque\\r\\n- Pour les utilisateurs ayant des centaines ou plus d'appareils Mi Home, l'ajout initial de l'intégration prendra un certain temps. Veuillez être patient.\\r\\n- Si Home Assistant fonctionne dans un environnement Docker, veuillez vous assurer que le mode réseau Docker est réglé sur host, sinon la fonctionnalité de contrôle local peut ne pas fonctionner correctement.\\r\\n- La fonctionnalité de contrôle local de l'intégration a quelques dépendances. Veuillez lire attentivement le README.\",\n                \"data\": {\n                    \"cloud_server\": \"Région de connexion\",\n                    \"integration_language\": \"Langue\",\n                    \"oauth_redirect_url\": \"Adresse de redirection de l'authentification\",\n                    \"network_detect_config\": \"Configuration Réseau Intégrée\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuration Réseau Intégrée\",\n                \"description\": \"## Introduction à l'Utilisation\\r\\n### Adresse de Détection Réseau\\r\\nUtilisé pour vérifier si le réseau fonctionne correctement. Si non défini, l'adresse par défaut du système sera utilisée. Si la vérification de l'adresse par défaut échoue, vous pouvez essayer de saisir une adresse personnalisée.\\r\\n- Vous pouvez entrer plusieurs adresses de détection, séparées par des virgules, telles que `8.8.8.8,https://www.bing.com`\\r\\n- S'il s'agit d'une adresse IP, la détection se fera par ping. S'il s'agit d'une adresse HTTP(s), la détection se fera par une requête HTTP GET.\\r\\n- Si vous souhaitez rétablir l'adresse de détection par défaut du système, veuillez entrer une virgule `,` et cliquer sur 'Suivant'.\\r\\n- **Cette configuration est globale et les modifications affecteront les autres instances d'intégration. Veuillez modifier avec prudence.**\\r\\n### Vérification des Dépendances Réseau\\r\\nVérifiez une par une les dépendances réseau suivantes pour voir si elles sont accessibles. Si les adresses associées ne sont pas accessibles, cela entraînera des problèmes d'intégration.\\r\\n- Adresse d'Authentification OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Adresse de l'API HTTP de Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Adresse de l'API SPEC de Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Adresse du Broker MQTT de Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Adresse de Détection Réseau\",\n                    \"check_network_deps\": \"Vérification des Dépendances Réseau\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Erreur de connexion\",\n                \"description\": \"Cliquez sur \\\"Suivant\\\" pour réessayer\"\n            },\n            \"homes_select\": {\n                \"title\": \"Sélectionner la famille et l'appareil\",\n                \"description\": \"## Introduction\\r\\n### Importer la famille de l'appareil\\r\\nL'intégration ajoutera des appareils de la famille sélectionnée.\\r\\n### Mode de synchronisation du nom de la pièce\\r\\nLors de la synchronisation des appareils de l'application Mi Home avec Home Assistant, le nom de la zone dans Home Assistant suivra les règles ci-dessous. Notez que le processus de synchronisation ne modifiera pas les paramètres de la famille et de la pièce dans l'application Mi Home.\\r\\n- Ne pas synchroniser : L'appareil ne sera ajouté à aucune zone.\\r\\n- Autres options : La zone à laquelle l'appareil est ajouté sera nommée d'après le nom de la famille ou de la pièce dans l'application Mi Home.\\r\\n### Paramètres avancés\\r\\nAfficher les paramètres avancés pour modifier les options de configuration professionnelle de l'intégration.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Bonjour ! Veuillez sélectionner la famille à laquelle vous souhaitez ajouter l'appareil.\",\n                \"data\": {\n                    \"home_infos\": \"Importer la famille de l'appareil\",\n                    \"area_name_rule\": \"Mode de synchronisation du nom de la pièce\",\n                    \"advanced_options\": \"Paramètres avancés\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Paramètres Avancés\",\n                \"description\": \"## Introduction\\r\\n### Sauf si vous comprenez très bien la signification des options suivantes, veuillez les laisser par défaut.\\r\\n### Filtrer les appareils\\r\\nPrend en charge le filtrage des appareils en fonction du nom de la pièce et du type d'appareil, ainsi que le filtrage basé sur les appareils.\\r\\n### Mode de Contrôle\\r\\n- Automatique : Lorsqu'une passerelle Xiaomi est disponible dans le réseau local, Home Assistant enverra les commandes de contrôle des appareils via la passerelle pour permettre le contrôle local. Si aucune passerelle n'est disponible dans le réseau local, Home Assistant essaiera d'envoyer les commandes de contrôle des appareils via le protocole OT Xiaomi pour permettre le contrôle local. Seules si les conditions de contrôle local ci-dessus ne sont pas remplies, les commandes de contrôle des appareils seront envoyées via le cloud.\\r\\n- Cloud : Les commandes de contrôle des appareils sont envoyées uniquement via le cloud.\\r\\n### Mode de Débogage d’Actions\\r\\nPour les méthodes définies par les appareils MIoT-Spec-V2, en plus de générer une entité de notification, une entité de champ de texte sera également générée pour vous permettre d'envoyer des commandes de contrôle aux appareils lors du débogage.\\r\\n### Masquer les Entités Non Standard\\r\\nMasquer les entités générées par des instances MIoT-Spec-V2 non standard et commençant par \\\"*\\\".\\r\\n### Mode d'affichage du capteur binaire\\r\\nAffiche les capteurs binaires dans Xiaomi Home comme entité de capteur de texte ou entité de capteur binaire。\\r\\n### Afficher les notifications de changement d'état de l'appareil\\r\\nAfficher les notifications détaillées de changement d'état de l'appareil, en affichant uniquement les notifications sélectionnées.\",\n                \"data\": {\n                    \"devices_filter\": \"Filtrer les Appareils\",\n                    \"ctrl_mode\": \"Mode de Contrôle\",\n                    \"action_debug\": \"Mode de Débogage d’Actions\",\n                    \"hide_non_standard_entities\": \"Masquer les Entités Non Standard\",\n                    \"display_binary_mode\": \"Mode d'affichage du capteur binaire\",\n                    \"display_devices_changed_notify\": \"Afficher les notifications de changement d'état de l'appareil\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrer les Appareils\",\n                \"description\": \"## Instructions d'utilisation\\r\\nPrend en charge le filtrage des appareils par nom de pièce de la maison, type d'accès de l'appareil et modèle d'appareil, et prend également en charge le filtrage par dimension de l'appareil. La logique de filtrage est la suivante :\\r\\n- Tout d'abord, selon la logique statistique, obtenez l'union ou l'intersection de tous les éléments inclus, puis obtenez l'intersection ou l'union des éléments exclus, et enfin soustrayez le [résultat du résumé inclus] du [résultat du résumé exclu] pour obtenir le [résultat du filtre].\\r\\n- Si aucun élément inclus n'est sélectionné, cela signifie que tous sont inclus.\\r\\n### Mode de Filtrage\\r\\n- Exclure : Supprimer les éléments indésirables.\\r\\n- Inclure : Inclure les éléments souhaités.\\r\\n### Logique Statistique\\r\\n- Logique ET : Prendre l'intersection de tous les éléments dans le même mode.\\r\\n- Logique OU : Prendre l'union de tous les éléments dans le même mode.\\r\\n\\r\\nVous pouvez également aller à la page [Configuration > Mettre à jour la liste des appareils] de l'élément d'intégration, cocher [Filtrer les appareils] pour re-filtrer.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrer les Pièces\",\n                    \"room_list\": \"Pièces\",\n                    \"type_filter_mode\": \"Filtrer les Types d'Appareils\",\n                    \"type_list\": \"Types d'Appareils\",\n                    \"model_filter_mode\": \"Filtrer les Modèles d'Appareils\",\n                    \"model_list\": \"Modèles d'Appareils\",\n                    \"devices_filter_mode\": \"Filtrer les Appareils\",\n                    \"device_list\": \"Liste des Appareils\",\n                    \"statistics_logic\": \"Logique de Statistiques\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Veuillez cliquer ici pour vous reconnecter{link_right}\\r\\n(Vous serez automatiquement redirigé vers la page suivante après une connexion réussie)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Veuillez lire le texte de notification de risques.\",\n            \"get_token_error\": \"Échec de la récupération des informations d'autorisation de connexion (jeton OAuth).\",\n            \"get_homeinfo_error\": \"Échec de la récupération des informations de la maison.\",\n            \"mdns_discovery_error\": \"Le service de découverte de périphériques locaux est anormal.\",\n            \"get_cert_error\": \"Échec de l'obtention du certificat de la passerelle.\",\n            \"no_family_selected\": \"Aucune maison sélectionnée.\",\n            \"no_devices\": \"Il n'y a pas d'appareils dans la maison sélectionnée. Veuillez sélectionner une maison avec des appareils et continuer.\",\n            \"no_filter_devices\": \"Les appareils filtrés sont vides. Veuillez sélectionner des critères de filtre valides et continuer.\",\n            \"no_central_device\": \"Le mode gateway central a besoin d'un Xiaomi Gateway disponible dans le réseau local où se trouve Home Assistant. Veuillez vérifier si la maison sélectionnée répond à cette exigence.\",\n            \"invalid_network_addr\": \"Adresse IP ou HTTP invalide détectée, veuillez entrer une adresse valide.\",\n            \"invalid_ip_addr\": \"Adresse IP inaccessible détectée, veuillez entrer une adresse IP valide.\",\n            \"invalid_http_addr\": \"Adresse HTTP inaccessible détectée, veuillez entrer une adresse HTTP valide.\",\n            \"invalid_default_addr\": \"L'adresse de détection réseau par défaut est inaccessible, veuillez vérifier la configuration réseau ou utiliser une adresse de détection réseau personnalisée.\",\n            \"unreachable_oauth2_host\": \"Impossible d'atteindre l'adresse d'authentification OAuth2, veuillez vérifier la configuration réseau.\",\n            \"unreachable_http_host\": \"Impossible d'atteindre l'adresse de l'API HTTP de Xiaomi, veuillez vérifier la configuration réseau.\",\n            \"unreachable_spec_host\": \"Impossible d'atteindre l'adresse de l'API SPEC de Xiaomi, veuillez vérifier la configuration réseau.\",\n            \"unreachable_mqtt_broker\": \"Impossible d'atteindre l'adresse du Broker MQTT de Xiaomi, veuillez vérifier la configuration réseau.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Échec de l'obtention de l'UUID de Home Assistant.\",\n            \"network_connect_error\": \"La configuration a échoué. Erreur de connexion réseau. Veuillez vérifier la configuration du réseau de l'appareil.\",\n            \"already_configured\": \"Cet utilisateur a déjà terminé la configuration. Veuillez accéder à la page d'intégration et cliquer sur le bouton \\\"Configurer\\\" pour modifier la configuration.\",\n            \"invalid_auth_info\": \"Les informations d'authentification ont expiré. Veuillez accéder à la page d'intégration et cliquer sur le bouton \\\"Configurer\\\" pour vous authentifier à nouveau.\",\n            \"config_flow_error\": \"Erreur de configuration de l'intégration : {error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Configuration d'authentification\",\n                \"description\": \"Les informations d'authentification locales ont expiré. Veuillez recommencer l'authentification\\r\\n### Région de connexion actuelle : {cloud_server}\\r\\n### Adresse de redirection de l'authentification OAuth2\\r\\nL'adresse de redirection de l'authentification OAuth2 est **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant doit être dans le même réseau local que le terminal de l'opération actuelle (par exemple, un ordinateur personnel) et le terminal de l'opération doit pouvoir accéder à la page d'accueil de Home Assistant via cette adresse. Sinon, l'authentification de connexion peut échouer.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"Adresse de redirection de l'authentification\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Erreur de connexion\",\n                \"description\": \"Cliquez sur \\\"Suivant\\\" pour réessayer\"\n            },\n            \"config_options\": {\n                \"title\": \"Options de configuration\",\n                \"description\": \"### {nick_name} Bonjour !\\r\\n\\r\\nID de compte Xiaomi : {uid}\\r\\nRégion de connexion actuelle : {cloud_server}\\r\\nID d'Instance d'Intégration: {instance_id}\\r\\n\\r\\nVeuillez sélectionner les options que vous devez reconfigurer et cliquer sur \\\"Suivant\\\".\",\n                \"data\": {\n                    \"integration_language\": \"Langue d'intégration\",\n                    \"update_user_info\": \"Mettre à jour les informations utilisateur\",\n                    \"update_devices\": \"Mettre à jour la liste des appareils\",\n                    \"action_debug\": \"Mode de débogage d'action\",\n                    \"hide_non_standard_entities\": \"Masquer les entités générées non standard\",\n                    \"display_binary_mode\": \"Mode d'affichage du capteur binaire\",\n                    \"display_devices_changed_notify\": \"Afficher les notifications de changement d'état de l'appareil\",\n                    \"update_trans_rules\": \"Mettre à jour les règles de conversion d'entités\",\n                    \"update_lan_ctrl_config\": \"Mettre à jour la configuration de contrôle LAN\",\n                    \"network_detect_config\": \"Configuration Réseau Intégrée\",\n                    \"cover_dead_zone_width\": \"Largeur de la zone aveugle du rideau\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Mettre à jour le pseudo de l'utilisateur\",\n                \"description\": \"{nick_name} Bonjour ! Veuillez modifier votre pseudo utilisateur ci-dessous.\",\n                \"data\": {\n                    \"nick_name\": \"Pseudo utilisateur\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Re-sélectionner une maison et des appareils\",\n                \"description\": \"## Instructions d'utilisation\\r\\n### Importer une maison pour les appareils\\r\\nL'intégration ajoutera les appareils de la maison sélectionnée.\\r\\n### Filtrer les appareils\\r\\nPrend en charge le filtrage des appareils par nom de pièce de la maison, type d'accès de l'appareil et modèle d'appareil, et prend également en charge le filtrage par dimension de l'appareil. **{local_count}** appareils ont été filtrés.\\r\\n### Mode de contrôle\\r\\n- Automatique: Lorsqu'il y a une passerelle centrale Xiaomi disponible dans le réseau local, Home Assistant priorisera l'envoi des commandes de contrôle des appareils via la passerelle centrale pour réaliser un contrôle localisé. S'il n'y a pas de passerelle centrale dans le réseau local, il tentera d'envoyer des commandes de contrôle via le protocole Xiaomi OT pour réaliser un contrôle localisé. Ce n'est que lorsque les conditions de contrôle localisé ci-dessus ne sont pas remplies que les commandes de contrôle des appareils seront envoyées via le cloud.\\r\\n- Cloud: Les commandes de contrôle ne sont envoyées que via le cloud.\",\n                \"data\": {\n                    \"home_infos\": \"Importer une maison pour les appareils\",\n                    \"devices_filter\": \"Filtrer les Appareils\",\n                    \"ctrl_mode\": \"Mode de contrôle\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrer les Appareils\",\n                \"description\": \"## Instructions d'utilisation\\r\\nPrend en charge le filtrage des appareils par nom de pièce de la maison, type d'accès de l'appareil et modèle d'appareil, et prend également en charge le filtrage par dimension de l'appareil. La logique de filtrage est la suivante :\\r\\n- Tout d'abord, selon la logique statistique, obtenez l'union ou l'intersection de tous les éléments inclus, puis obtenez l'intersection ou l'union des éléments exclus, et enfin soustrayez le [résultat du résumé inclus] du [résultat du résumé exclu] pour obtenir le [résultat du filtre].\\r\\n- Si aucun élément inclus n'est sélectionné, cela signifie que tous sont inclus.\\r\\n### Mode de Filtrage\\r\\n- Exclure : Supprimer les éléments indésirables.\\r\\n- Inclure : Inclure les éléments souhaités.\\r\\n### Logique Statistique\\r\\n- Logique ET : Prendre l'intersection de tous les éléments dans le même mode.\\r\\n- Logique OU : Prendre l'union de tous les éléments dans le même mode.\\r\\n\\r\\nVous pouvez également aller à la page [Configuration > Mettre à jour la liste des appareils] de l'élément d'intégration, cocher [Filtrer les appareils] pour re-filtrer.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrer les Pièces\",\n                    \"room_list\": \"Pièces\",\n                    \"type_filter_mode\": \"Filtrer les Types d'Appareils\",\n                    \"type_list\": \"Types d'Appareils\",\n                    \"model_filter_mode\": \"Filtrer les Modèles d'Appareils\",\n                    \"model_list\": \"Modèles d'Appareils\",\n                    \"devices_filter_mode\": \"Filtrer les Appareils\",\n                    \"device_list\": \"Liste des Appareils\",\n                    \"statistics_logic\": \"Logique de Statistiques\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Mettre à jour les règles de conversion d'entités\",\n                \"description\": \"## Instructions d'utilisation\\r\\n- Mettez à jour les informations d'entité des appareils dans l'instance d'intégration actuelle, y compris la configuration multilingue SPEC, la traduction booléenne SPEC et le filtrage de modèle SPEC.\\r\\n- **Avertissement: Cette configuration est une configuration globale** et mettra directement à jour le cache local. S'il y a des appareils du même modèle dans d'autres instances d'intégration, les instances pertinentes seront également mises à jour après rechargement.\\r\\n- Cette opération prendra du temps, veuillez être patient. Cochez \\\"Confirmer la mise à jour\\\" et cliquez sur \\\"Suivant\\\" pour commencer à mettre à jour **{urn_count}** règles, sinon passez la mise à jour.\",\n                \"data\": {\n                    \"confirm\": \"Confirmer la mise à jour\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Mettre à jour la configuration réseau de contrôle LAN\",\n                \"description\": \"## Instructions d'utilisation\\r\\nMettez à jour les informations de configuration pour **le contrôle LAN des appareils Xiaomi Home**. Lorsque le cloud et la passerelle centrale ne peuvent pas contrôler les appareils, l'intégration tentera de contrôler les appareils via le LAN ; si aucune carte réseau n'est sélectionnée, le contrôle LAN ne sera pas activé.\\r\\n- Actuellement, seuls les appareils WiFi **SPEC v2** dans le LAN sont pris en charge. Certains anciens appareils peuvent ne pas prendre en charge le contrôle ou la synchronisation des propriétés.\\r\\n- Veuillez sélectionner la ou les cartes réseau sur le même réseau que les appareils (plusieurs sélections sont prises en charge). Si la carte réseau sélectionnée a deux ou plusieurs connexions sur le même réseau, il est recommandé de sélectionner celle avec la meilleure connexion réseau, sinon cela peut **affecter l'utilisation normale des appareils**.\\r\\n- **S'il y a des appareils terminaux (passerelles, téléphones mobiles, etc.) dans le LAN qui prennent en charge le contrôle local, l'activation de l'abonnement LAN peut provoquer des automatisations locales ou des anomalies des appareils. Veuillez l'utiliser avec prudence**.\\r\\n- **Avertissement : Cette configuration est globale et les modifications affecteront d'autres instances d'intégration. Veuillez modifier avec prudence**.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Veuillez sélectionner la carte réseau à utiliser\",\n                    \"enable_subscribe\": \"Activer la souscription\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuration Réseau Intégrée\",\n                \"description\": \"## Introduction à l'Utilisation\\r\\n### Adresse de Détection Réseau\\r\\nUtilisé pour vérifier si le réseau fonctionne correctement. Si non défini, l'adresse par défaut du système sera utilisée. Si la vérification de l'adresse par défaut échoue, vous pouvez essayer de saisir une adresse personnalisée.\\r\\n- Vous pouvez entrer plusieurs adresses de détection, séparées par des virgules, telles que `8.8.8.8,https://www.bing.com`\\r\\n- S'il s'agit d'une adresse IP, la détection se fera par ping. S'il s'agit d'une adresse HTTP(s), la détection se fera par une requête HTTP GET.\\r\\n- Si vous souhaitez rétablir l'adresse de détection par défaut du système, veuillez entrer une virgule `,` et cliquer sur 'Suivant'.\\r\\n- **Cette configuration est globale et les modifications affecteront les autres instances d'intégration. Veuillez modifier avec prudence.**\\r\\n### Vérification des Dépendances Réseau\\r\\nVérifiez une par une les dépendances réseau suivantes pour voir si elles sont accessibles. Si les adresses associées ne sont pas accessibles, cela entraînera des problèmes d'intégration.\\r\\n- Adresse d'Authentification OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Adresse de l'API HTTP de Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Adresse de l'API SPEC de Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Adresse du Broker MQTT de Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Adresse de Détection Réseau\",\n                    \"check_network_deps\": \"Vérification des Dépendances Réseau\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Confirmer la configuration\",\n                \"description\": \"**{nick_name}** Bonjour ! Veuillez confirmer les dernières informations de configuration et cliquer sur \\\"Soumettre\\\".\\r\\nL'intégration rechargera avec la nouvelle configuration.\\r\\n\\r\\nLangue d'intégration : {lang_new}\\r\\nPseudo utilisateur : {nick_name_new}\\r\\nMode de débogage d'action : {action_debug}\\r\\nMasquer les entités générées non standard : {hide_non_standard_entities}\\r\\nLargeur de la zone aveugle du rideau:\\t{cover_width_new}\\r\\nAfficher les notifications de changement d'état de l'appareil:\\t{display_devices_changed_notify}\\r\\nModifications des appareils : Ajouter **{devices_add}** appareils, supprimer **{devices_remove}** appareils\\r\\nModifications des règles de conversion : **{trans_rules_count}** règles au total, mise à jour de **{trans_rules_count_success}** règles\",\n                \"data\": {\n                    \"confirm\": \"Confirmer la modification\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Veuillez cliquer ici pour vous reconnecter{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"L'utilisateur n'est pas authentifié. Veuillez cliquer sur le lien d'authentification pour vous identifier.\",\n            \"get_token_error\": \"Impossible d'obtenir les informations d'authentification (jeton OAuth).\",\n            \"get_homeinfo_error\": \"Impossible d'obtenir les informations de la maison.\",\n            \"get_cert_error\": \"Impossible d'obtenir le certificat central.\",\n            \"no_family_selected\": \"Aucune maison sélectionnée.\",\n            \"no_devices\": \"Il n'y a pas d'appareils dans la maison sélectionnée. Veuillez sélectionner une maison avec des appareils et continuer.\",\n            \"no_filter_devices\": \"Les appareils filtrés sont vides. Veuillez sélectionner des critères de filtre valides et continuer.\",\n            \"no_central_device\": \"Le mode passerelle centrale nécessite une passerelle Xiaomi disponible dans le réseau local où est installé Home Assistant. Veuillez vérifier que la maison sélectionnée répond à cette exigence.\",\n            \"mdns_discovery_error\": \"Service de découverte de périphérique local en panne.\",\n            \"update_config_error\": \"Échec de la mise à jour des informations de configuration.\",\n            \"not_confirm\": \"La modification n'a pas été confirmée. Veuillez cocher la case de confirmation avant de soumettre.\",\n            \"invalid_network_addr\": \"Adresse IP ou HTTP invalide détectée, veuillez entrer une adresse valide.\",\n            \"invalid_ip_addr\": \"Adresse IP inaccessible détectée, veuillez entrer une adresse IP valide.\",\n            \"invalid_http_addr\": \"Adresse HTTP inaccessible détectée, veuillez entrer une adresse HTTP valide.\",\n            \"invalid_default_addr\": \"L'adresse de détection réseau par défaut est inaccessible, veuillez vérifier la configuration réseau ou utiliser une adresse de détection réseau personnalisée.\",\n            \"unreachable_oauth2_host\": \"Impossible d'atteindre l'adresse d'authentification OAuth2, veuillez vérifier la configuration réseau.\",\n            \"unreachable_http_host\": \"Impossible d'atteindre l'adresse de l'API HTTP de Xiaomi, veuillez vérifier la configuration réseau.\",\n            \"unreachable_spec_host\": \"Impossible d'atteindre l'adresse de l'API SPEC de Xiaomi, veuillez vérifier la configuration réseau.\",\n            \"unreachable_mqtt_broker\": \"Impossible d'atteindre l'adresse du Broker MQTT de Xiaomi, veuillez vérifier la configuration réseau.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Échec de la configuration. Problème de connexion réseau, veuillez vérifier la configuration du périphérique.\",\n            \"options_flow_error\": \"Erreur de réinitialisation de la configuration de l'intégration : {error}\",\n            \"re_add\": \"Veuillez réajouter l'intégration, message d'erreur : {error}\",\n            \"storage_error\": \"Erreur de stockage pour l'intégration. Veuillez réessayer ou réajouter l'intégration : {error}\",\n            \"inconsistent_account\": \"Les informations de compte sont incohérentes. Veuillez vous connecter avec les informations de compte correctes.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/it.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Integrazione Xiaomi Home\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Avviso sui Rischi\",\n                \"description\": \"1. Le informazioni del tuo utente Xiaomi e le informazioni del dispositivo saranno memorizzate nel sistema Home Assistant. **Xiaomi non può garantire la sicurezza del meccanismo di archiviazione di Home Assistant**. Sei responsabile per prevenire il furto delle tue informazioni.\\r\\n2. Questa integrazione è mantenuta dalla comunità open-source. Potrebbero esserci problemi di stabilità o altri problemi. In caso di problemi o bug con questa integrazione, **dovresti cercare aiuto dalla comunità open-source piuttosto che contattare il servizio clienti Xiaomi**.\\r\\n3. È necessaria una certa abilità tecnica per mantenere il tuo ambiente operativo locale. L'integrazione non è user-friendly per i principianti.\\r\\n4. Si prega di leggere il file README prima di iniziare.\\n\\n5. Per garantire un uso stabile dell'integrazione e prevenire l'abuso dell'interfaccia, **questa integrazione può essere utilizzata solo in Home Assistant. Per i dettagli, consulta il LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Sono consapevole dei rischi sopra indicati e sono disposto ad assumermi volontariamente qualsiasi rischio associato all'uso dell'integrazione.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Configurazione di base\",\n                \"description\": \"### Regione di Login\\r\\nSeleziona la regione del tuo account Xiaomi. Puoi trovarla nell'APP Xiaomi Home > Profilo (nel menu in basso) > Impostazioni aggiuntive > Informazioni su Xiaomi Home.\\r\\n### Lingua\\r\\nSeleziona la lingua dei nomi dei dispositivi e delle entità. Alcune frasi senza traduzione verranno visualizzate in inglese.\\r\\n### URL di reindirizzamento OAuth2\\r\\nL'indirizzo di reindirizzamento dell'autenticazione OAuth2 è **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant deve trovarsi nella stessa rete locale del terminale operativo corrente (ad esempio, il computer personale) e il terminale operativo deve poter accedere alla home page di Home Assistant tramite questo indirizzo. Altrimenti, l'autenticazione del login potrebbe fallire.\\r\\n### Nota\\r\\n- Per gli utenti con centinaia o più dispositivi Mi Home, l'aggiunta iniziale dell'integrazione richiederà del tempo. Si prega di essere pazienti.\\r\\n- Se Home Assistant è in esecuzione in un ambiente Docker, assicurarsi che la modalità di rete Docker sia impostata su host, altrimenti la funzionalità di controllo locale potrebbe non funzionare correttamente.\\r\\n- La funzionalità di controllo locale dell'integrazione ha alcune dipendenze. Si prega di leggere attentamente il README.\",\n                \"data\": {\n                    \"cloud_server\": \"Regione di Login\",\n                    \"integration_language\": \"Lingua\",\n                    \"oauth_redirect_url\": \"URL di reindirizzamento OAuth2\",\n                    \"network_detect_config\": \"Configurazione di rete integrata\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configurazione di Rete Integrata\",\n                \"description\": \"## Introduzione all'uso\\r\\n### Indirizzo di Rilevamento della Rete\\r\\nUtilizzato per verificare se la rete funziona correttamente. Se non impostato, verrà utilizzato l'indirizzo di default del sistema. Se il controllo dell'indirizzo predefinito fallisce, puoi provare a inserire un indirizzo personalizzato.\\r\\n- Puoi inserire più indirizzi di rilevamento, separati da virgole, come `8.8.8.8,https://www.bing.com`\\r\\n- Se è un indirizzo IP, il rilevamento verrà eseguito tramite ping. Se è un indirizzo HTTP(s), il rilevamento verrà eseguito tramite richiesta HTTP GET.\\r\\n- Se desideri ripristinare l'indirizzo di rilevamento predefinito del sistema, inserisci una virgola `,` e fai clic su 'Avanti'.\\r\\n- **Questa configurazione è globale e le modifiche influenzeranno altre istanze di integrazione. Si prega di modificare con cautela.**\\r\\n### Controlla le Dipendenze di Rete\\r\\nControlla una per una le seguenti dipendenze di rete per vedere se sono accessibili. Se gli indirizzi correlati non sono accessibili, causerà problemi di integrazione.\\r\\n- Indirizzo di Autenticazione OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Indirizzo API HTTP di Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Indirizzo API SPEC di Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Indirizzo del Broker MQTT di Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Indirizzo di Rilevamento della Rete\",\n                    \"check_network_deps\": \"Controlla le Dipendenze di Rete\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Errore di Login\",\n                \"description\": \"Clicca AVANTI per riprovare.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Seleziona Famiglia e Dispositivo\",\n                \"description\": \"## Introduzione\\r\\n### Importa la Famiglia del Dispositivo\\r\\nL'integrazione aggiungerà i dispositivi dalla famiglia selezionata.\\r\\n### Modalità di Sincronizzazione del Nome della Stanza\\r\\nQuando si sincronizzano i dispositivi dall'app Mi Home a Home Assistant, la denominazione dell'area in Home Assistant seguirà le regole indicate di seguito. Si noti che il processo di sincronizzazione non modificherà le impostazioni di famiglia e stanza nell'app Mi Home.\\r\\n- Non sincronizzare: Il dispositivo non verrà aggiunto a nessuna area.\\r\\n- Altre opzioni: L'area a cui viene aggiunto il dispositivo verrà denominata come la famiglia o il nome della stanza nell'app Mi Home.\\r\\n### Impostazioni Avanzate\\r\\nMostra le impostazioni avanzate per modificare le opzioni di configurazione professionale dell'integrazione.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Ciao! Seleziona la famiglia a cui desideri aggiungere il dispositivo.\",\n                \"data\": {\n                    \"home_infos\": \"Importa la Famiglia del Dispositivo\",\n                    \"area_name_rule\": \"Modalità di Sincronizzazione del Nome della Stanza\",\n                    \"advanced_options\": \"Impostazioni Avanzate\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Impostazioni Avanzate\",\n                \"description\": \"## Introduzione\\r\\n### A meno che tu non abbia chiaro il significato delle seguenti opzioni, si prega di mantenere le impostazioni predefinite.\\r\\n### Filtra Dispositivi\\r\\nSupporta il filtraggio dei dispositivi per nome della stanza e tipo di dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo.\\r\\n### Modalità di Controllo\\r\\n- Automatico: Quando è disponibile un gateway hub centrale Xiaomi nella rete locale, Home Assistant darà priorità all'invio dei comandi di controllo dei dispositivi tramite il gateway hub centrale per ottenere il controllo locale. Se non è presente un gateway hub centrale nella rete locale, tenterà di inviare comandi di controllo tramite il protocollo OT di Xiaomi per ottenere il controllo locale. Solo quando le condizioni di controllo locale sopra indicate non sono soddisfatte, i comandi di controllo del dispositivo verranno inviati tramite il cloud.\\r\\n- Cloud: Tutti i comandi di controllo vengono inviati tramite il cloud.\\r\\n### Modalità di Debug delle Azioni\\r\\nPer i metodi definiti dal dispositivo MIoT-Spec-V2, oltre a generare entità di notifica, verrà generata anche un'entità di casella di input di testo. È possibile utilizzarla per inviare comandi di controllo al dispositivo durante il debug.\\r\\n### Nascondi Entità Generate Non Standard\\r\\nNasconde le entità generate da istanze non standard MIoT-Spec-V2 con nomi che iniziano con \\\"*\\\".\\r\\n### Modalità di visualizzazione del sensore binario\\r\\nVisualizza i sensori binari in Mi Home come entità del sensore di testo o entità del sensore binario。\\r\\n### Mostra Notifiche di Cambio di Stato del Dispositivo\\r\\nMostra notifiche dettagliate sui cambiamenti di stato del dispositivo, mostrando solo le notifiche selezionate.\",\n                \"data\": {\n                    \"devices_filter\": \"Filtra Dispositivi\",\n                    \"ctrl_mode\": \"Modalità di Controllo\",\n                    \"action_debug\": \"Modalità di Debug delle Azioni\",\n                    \"hide_non_standard_entities\": \"Nascondi Entità Generate Non Standard\",\n                    \"display_binary_mode\": \"Modalità di visualizzazione del sensore binario\",\n                    \"display_devices_changed_notify\": \"Mostra Notifiche di Cambio di Stato del Dispositivo\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtra Dispositivi\",\n                \"description\": \"## Istruzioni per l'uso\\r\\nSupporta il filtraggio dei dispositivi per nome della stanza, tipo di accesso al dispositivo e modello del dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo. La logica di filtraggio è la seguente:\\r\\n- Prima, secondo la logica statistica, ottieni l'unione o l'intersezione di tutti gli elementi inclusi, poi ottieni l'intersezione o l'unione degli elementi esclusi e infine sottrai il [risultato riassuntivo incluso] dal [risultato riassuntivo escluso] per ottenere il [risultato del filtro].\\r\\n- Se non vengono selezionati elementi inclusi, significa che tutti sono inclusi.\\r\\n### Modalità di Filtro\\r\\n- Escludi: Rimuovi gli elementi indesiderati.\\r\\n- Includi: Includi gli elementi desiderati.\\r\\n### Logica Statistica\\r\\n- Logica AND: Prendi l'intersezione di tutti gli elementi nella stessa modalità.\\r\\n- Logica OR: Prendi l'unione di tutti gli elementi nella stessa modalità.\\r\\n\\r\\nPuoi anche andare alla pagina [Configurazione > Aggiorna Elenco Dispositivi] dell'elemento di integrazione e controllare [Filtra Dispositivi] per rifiltrare.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtra Stanze della Famiglia\",\n                    \"room_list\": \"Stanze della Famiglia\",\n                    \"type_filter_mode\": \"Filtra Tipo di Connessione del Dispositivo\",\n                    \"type_list\": \"Tipo di Connessione del Dispositivo\",\n                    \"model_filter_mode\": \"Filtra Modello del Dispositivo\",\n                    \"model_list\": \"Modello del Dispositivo\",\n                    \"devices_filter_mode\": \"Filtra Dispositivi\",\n                    \"device_list\": \"Elenco Dispositivi\",\n                    \"statistics_logic\": \"Logica Statistica\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Clicca qui per accedere{link_right}\\r\\n(Verrai reindirizzato automaticamente alla pagina successiva dopo un accesso riuscito)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Si prega di leggere l'avviso sui rischi.\",\n            \"get_token_error\": \"Impossibile recuperare le informazioni di autorizzazione per il login (token OAuth).\",\n            \"get_homeinfo_error\": \"Impossibile recuperare le informazioni della casa.\",\n            \"mdns_discovery_error\": \"Eccezione del servizio di scoperta dei dispositivi locali.\",\n            \"get_cert_error\": \"Impossibile recuperare il certificato del gateway centrale.\",\n            \"no_family_selected\": \"Nessuna casa selezionata.\",\n            \"no_devices\": \"La casa selezionata non ha dispositivi. Si prega di scegliere una casa che contiene dispositivi e continuare.\",\n            \"no_filter_devices\": \"I dispositivi filtrati sono vuoti. Si prega di selezionare criteri di filtro validi e continuare.\",\n            \"no_central_device\": \"[Modalità Gateway Hub Centrale] richiede un gateway hub centrale Xiaomi disponibile nella rete locale in cui esiste Home Assistant. Si prega di verificare se la casa selezionata soddisfa il requisito.\",\n            \"invalid_network_addr\": \"Rilevato indirizzo IP o indirizzo HTTP non valido, si prega di inserire un indirizzo valido.\",\n            \"invalid_ip_addr\": \"Rilevato indirizzo IP non raggiungibile, si prega di inserire un indirizzo IP valido.\",\n            \"invalid_http_addr\": \"Rilevato indirizzo HTTP non raggiungibile, si prega di inserire un indirizzo HTTP valido.\",\n            \"invalid_default_addr\": \"Indirizzo di rilevamento della rete predefinito non raggiungibile, si prega di verificare la configurazione della rete o utilizzare un indirizzo di rilevamento della rete personalizzato.\",\n            \"unreachable_oauth2_host\": \"Impossibile raggiungere l'indirizzo di autenticazione OAuth2, si prega di verificare la configurazione della rete.\",\n            \"unreachable_http_host\": \"Impossibile raggiungere l'indirizzo API HTTP di Xiaomi, si prega di verificare la configurazione della rete.\",\n            \"unreachable_spec_host\": \"Impossibile raggiungere l'indirizzo API SPEC di Xiaomi, si prega di verificare la configurazione della rete.\",\n            \"unreachable_mqtt_broker\": \"Impossibile raggiungere l'indirizzo del broker MQTT di Xiaomi, si prega di verificare la configurazione della rete.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Impossibile ottenere l'UUID di Home Assistant.\",\n            \"network_connect_error\": \"Configurazione fallita. La connessione di rete è anomala. Si prega di controllare la configurazione della rete del dispositivo.\",\n            \"already_configured\": \"La configurazione per questo utente è già completata. Si prega di andare alla pagina dell'integrazione e cliccare sul pulsante CONFIGURA per le modifiche.\",\n            \"invalid_auth_info\": \"Le informazioni di autenticazione sono scadute. Si prega di andare alla pagina dell'integrazione e cliccare sul pulsante CONFIGURA per ri-autenticarsi.\",\n            \"config_flow_error\": \"Errore di configurazione dell'integrazione: {error}.\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Configurazione dell'Autenticazione\",\n                \"description\": \"Le informazioni di autenticazione locale sono scadute. Si prega di riavviare il processo di autenticazione.\\r\\n### Regione di Login Corrente: {cloud_server}\\r\\n### URL di reindirizzamento OAuth2\\r\\nL'indirizzo di reindirizzamento dell'autenticazione OAuth2 è **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant deve trovarsi nella stessa rete locale del terminale operativo corrente (ad esempio, il computer personale) e il terminale operativo deve poter accedere alla home page di Home Assistant tramite questo indirizzo. Altrimenti, l'autenticazione del login potrebbe fallire.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"URL di reindirizzamento OAuth2\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Si è verificato un errore durante il login.\",\n                \"description\": \"Clicca AVANTI per riprovare.\"\n            },\n            \"config_options\": {\n                \"title\": \"Opzioni di Configurazione\",\n                \"description\": \"### Ciao, {nick_name}\\r\\n\\r\\nID Xiaomi: {uid}\\r\\nRegione di Login Corrente: {cloud_server}\\r\\nID istanza di integrazione: {instance_id}\\r\\n\\r\\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.\",\n                \"data\": {\n                    \"integration_language\": \"Lingua dell'Integrazione\",\n                    \"update_user_info\": \"Aggiorna le informazioni dell'utente\",\n                    \"update_devices\": \"Aggiorna l'elenco dei dispositivi\",\n                    \"action_debug\": \"Modalità debug per azione\",\n                    \"hide_non_standard_entities\": \"Nascondi entità create non standard\",\n                    \"display_binary_mode\": \"Modalità di visualizzazione del sensore binario\",\n                    \"display_devices_changed_notify\": \"Mostra notifiche di cambio stato del dispositivo\",\n                    \"update_trans_rules\": \"Aggiorna le regole di conversione delle entità\",\n                    \"update_lan_ctrl_config\": \"Aggiorna configurazione del controllo LAN\",\n                    \"network_detect_config\": \"Configurazione di Rete Integrata\",\n                    \"cover_dead_zone_width\": \"Larghezza dell’angolo cieco della tenda\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Aggiorna il Nickname dell'Utente\",\n                \"description\": \"Ciao {nick_name}, puoi modificare il tuo nickname personalizzato qui sotto.\",\n                \"data\": {\n                    \"nick_name\": \"Nickname\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Seleziona Nuovamente Casa e Dispositivi\",\n                \"description\": \"## Istruzioni per l'uso\\r\\n### Importa dispositivi da casa\\r\\nL'integrazione aggiungerà dispositivi dalle case selezionate.\\r\\n### Filtra Dispositivi\\r\\nSupporta il filtraggio dei dispositivi per nome della stanza, tipo di accesso al dispositivo e modello del dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo. **{local_count}** dispositivi sono stati filtrati.\\r\\n### Modalità di Controllo\\r\\n- Automatico: Quando è disponibile un gateway hub centrale Xiaomi nella rete locale, Home Assistant darà priorità all'invio dei comandi di controllo dei dispositivi tramite il gateway hub centrale per ottenere il controllo locale. Se non è presente un gateway hub centrale nella rete locale, tenterà di inviare comandi di controllo tramite la funzione di controllo LAN di Xiaomi. Solo quando le condizioni di controllo locale sopra indicate non sono soddisfatte, i comandi di controllo del dispositivo verranno inviati tramite il cloud.\\r\\n- Cloud: Tutti i comandi di controllo vengono inviati tramite il cloud.\",\n                \"data\": {\n                    \"home_infos\": \"Importa dispositivi da casa\",\n                    \"devices_filter\": \"Filtra dispositivi\",\n                    \"ctrl_mode\": \"Modalità di controllo\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtra Dispositivi\",\n                \"description\": \"## Istruzioni per l'uso\\r\\nSupporta il filtraggio dei dispositivi per nome della stanza, tipo di accesso al dispositivo e modello del dispositivo, e supporta anche il filtraggio delle dimensioni del dispositivo. La logica di filtraggio è la seguente:\\r\\n- Prima, secondo la logica statistica, ottieni l'unione o l'intersezione di tutti gli elementi inclusi, poi ottieni l'intersezione o l'unione degli elementi esclusi e infine sottrai il [risultato riassuntivo incluso] dal [risultato riassuntivo escluso] per ottenere il [risultato del filtro].\\r\\n- Se non vengono selezionati elementi inclusi, significa che tutti sono inclusi.\\r\\n### Modalità di Filtro\\r\\n- Escludi: Rimuovi gli elementi indesiderati.\\r\\n- Includi: Includi gli elementi desiderati.\\r\\n### Logica Statistica\\r\\n- Logica AND: Prendi l'intersezione di tutti gli elementi nella stessa modalità.\\r\\n- Logica OR: Prendi l'unione di tutti gli elementi nella stessa modalità.\\r\\n\\r\\nPuoi anche andare alla pagina [Configurazione > Aggiorna Elenco Dispositivi] dell'elemento di integrazione e controllare [Filtra Dispositivi] per rifiltrare.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtra Stanze della Famiglia\",\n                    \"room_list\": \"Stanze della Famiglia\",\n                    \"type_filter_mode\": \"Filtra Tipo di Connessione del Dispositivo\",\n                    \"type_list\": \"Tipo di Connessione del Dispositivo\",\n                    \"model_filter_mode\": \"Filtra Modello del Dispositivo\",\n                    \"model_list\": \"Modello del Dispositivo\",\n                    \"devices_filter_mode\": \"Filtra Dispositivi\",\n                    \"device_list\": \"Elenco Dispositivi\",\n                    \"statistics_logic\": \"Logica Statistica\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Aggiorna le Regole di Trasformazione delle Entità\",\n                \"description\": \"## Istruzioni per l'uso\\r\\n- Aggiorna le informazioni delle entità dei dispositivi nell'istanza dell'integrazione corrente, incluse la configurazione multilingue MIoT-Spec-V2, la traduzione booleana e il filtro dei modelli.\\r\\n- **Avviso**: Questa è una configurazione globale e aggiornerà la cache locale. Influenzando tutte le istanze di integrazione.\\r\\n- Questa operazione richiederà del tempo, si prega di essere pazienti. Seleziona \\\"Conferma Aggiornamento\\\" e clicca \\\"Avanti\\\" per iniziare l'aggiornamento di **{urn_count}** regole, altrimenti salta l'aggiornamento.\",\n                \"data\": {\n                    \"confirm\": \"Conferma l'aggiornamento\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Aggiorna configurazione del controllo LAN\",\n                \"description\": \"## Istruzioni per l'uso\\r\\nAggiorna le configurazioni per la funzione di controllo LAN di Xiaomi. Quando il cloud e il gateway centrale non possono controllare i dispositivi, l'integrazione tenterà di controllare i dispositivi tramite la LAN. Se nessuna scheda di rete è selezionata, la funzione di controllo LAN non avrà effetto.\\r\\n- Solo i dispositivi compatibili con MIoT-Spec-V2 nella LAN sono supportati. Alcuni dispositivi prodotti prima del 2020 potrebbero non supportare il controllo LAN o l'abbonamento LAN.\\r\\n- Seleziona la/le scheda/e di rete nella stessa rete dei dispositivi da controllare. È possibile selezionare più schede di rete. Se Home Assistant ha due o più connessioni alla rete locale a causa della selezione multipla delle schede di rete, si consiglia di selezionare quella con la migliore connessione di rete, altrimenti potrebbe avere un effetto negativo sui dispositivi.\\r\\n- Se ci sono dispositivi terminali (altoparlanti Xiaomi con schermo, telefono cellulare, ecc.) nella LAN che supportano il controllo locale, abilitare l'abbonamento LAN potrebbe causare anomalie nell'automazione locale e nei dispositivi.\\r\\n- **Avviso**: Questa è una configurazione globale. Influenzando tutte le istanze di integrazione. Usala con cautela.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Si prega di selezionare la scheda di rete da utilizzare\",\n                    \"enable_subscribe\": \"Abilita Sottoscrizione LAN\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configurazione di Rete Integrata\",\n                \"description\": \"## Introduzione all'uso\\r\\n### Indirizzo di Rilevamento della Rete\\r\\nUtilizzato per verificare se la rete funziona correttamente. Se non impostato, verrà utilizzato l'indirizzo di default del sistema. Se il controllo dell'indirizzo predefinito fallisce, puoi provare a inserire un indirizzo personalizzato.\\r\\n- Puoi inserire più indirizzi di rilevamento, separati da virgole, come `8.8.8.8,https://www.bing.com`\\r\\n- Se è un indirizzo IP, il rilevamento verrà eseguito tramite ping. Se è un indirizzo HTTP(s), il rilevamento verrà eseguito tramite richiesta HTTP GET.\\r\\n- Se desideri ripristinare l'indirizzo di rilevamento predefinito del sistema, inserisci una virgola `,` e fai clic su 'Avanti'.\\r\\n- **Questa configurazione è globale e le modifiche influenzeranno altre istanze di integrazione. Si prega di modificare con cautela.**\\r\\n### Controlla le Dipendenze di Rete\\r\\nControlla una per una le seguenti dipendenze di rete per vedere se sono accessibili. Se gli indirizzi correlati non sono accessibili, causerà problemi di integrazione.\\r\\n- Indirizzo di Autenticazione OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Indirizzo API HTTP di Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Indirizzo API SPEC di Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Indirizzo del Broker MQTT di Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Indirizzo di Rilevamento della Rete\",\n                    \"check_network_deps\": \"Controlla le Dipendenze di Rete\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Conferma Configurazione\",\n                \"description\": \"Ciao **{nick_name}**, si prega di confermare le informazioni di configurazione più recenti e poi fare clic su INVIA.\\r\\nL'integrazione verrà ricaricata utilizzando la configurazione aggiornata.\\r\\n\\r\\nLingua dell'Integrazione:\\t{lang_new}\\r\\nSoprannome:\\t{nick_name_new}\\r\\nModalità di debug per azione:\\t{action_debug}\\r\\nNascondi entità create non standard:\\t{hide_non_standard_entities}\\r\\nLarghezza dell’angolo cieco della tenda:\\t{cover_width_new}\\r\\nMostra notifiche di cambio stato del dispositivo:\\t{display_devices_changed_notify}\\r\\nCambiamenti del Dispositivo:\\tAggiungi **{devices_add}** dispositivi, Rimuovi **{devices_remove}** dispositivi\\r\\nCambiamenti delle regole di trasformazione:\\tCi sono un totale di **{trans_rules_count}** regole, e aggiornate **{trans_rules_count_success}** regole\",\n                \"data\": {\n                    \"confirm\": \"Conferma la modifica\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Clicca qui per riaccedere{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Non autenticato. Si prega di fare clic sul link di autenticazione per autenticare l'identità dell'utente.\",\n            \"get_token_error\": \"Impossibile recuperare le informazioni di autorizzazione all'accesso (token OAuth).\",\n            \"get_homeinfo_error\": \"Impossibile recuperare le informazioni sulla casa.\",\n            \"get_cert_error\": \"Impossibile recuperare il certificato del gateway hub centrale.\",\n            \"no_devices\": \"Non ci sono dispositivi nella casa selezionata. Si prega di selezionare una casa con dispositivi e continuare.\",\n            \"no_filter_devices\": \"I dispositivi filtrati sono vuoti. Si prega di selezionare criteri di filtro validi e continuare.\",\n            \"no_family_selected\": \"Nessuna casa selezionata.\",\n            \"no_central_device\": \"[Modalità Gateway Hub Centrale] richiede un gateway hub centrale Xiaomi disponibile nella rete locale in cui esiste Home Assistant. Si prega di verificare se la casa selezionata soddisfa il requisito.\",\n            \"mdns_discovery_error\": \"Eccezione nel servizio di rilevamento dei dispositivi locali.\",\n            \"update_config_error\": \"Impossibile aggiornare le informazioni di configurazione.\",\n            \"not_confirm\": \"Le modifiche non sono confermate. Si prega di confermare la modifica prima di inviare.\",\n            \"invalid_network_addr\": \"Rilevato indirizzo IP o indirizzo HTTP non valido, si prega di inserire un indirizzo valido.\",\n            \"invalid_ip_addr\": \"Rilevato indirizzo IP non raggiungibile, si prega di inserire un indirizzo IP valido.\",\n            \"invalid_http_addr\": \"Rilevato indirizzo HTTP non raggiungibile, si prega di inserire un indirizzo HTTP valido.\",\n            \"invalid_default_addr\": \"Indirizzo di rilevamento della rete predefinito non raggiungibile, si prega di verificare la configurazione della rete o utilizzare un indirizzo di rilevamento della rete personalizzato.\",\n            \"unreachable_oauth2_host\": \"Impossibile raggiungere l'indirizzo di autenticazione OAuth2, si prega di verificare la configurazione della rete.\",\n            \"unreachable_http_host\": \"Impossibile raggiungere l'indirizzo API HTTP di Xiaomi, si prega di verificare la configurazione della rete.\",\n            \"unreachable_spec_host\": \"Impossibile raggiungere l'indirizzo API SPEC di Xiaomi, si prega di verificare la configurazione della rete.\",\n            \"unreachable_mqtt_broker\": \"Impossibile raggiungere l'indirizzo del broker MQTT di Xiaomi, si prega di verificare la configurazione della rete.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Configurazione fallita. La connessione di rete è anomala. Si prega di controllare la configurazione della rete del dispositivo.\",\n            \"options_flow_error\": \"Errore di riconfigurazione dell'integrazione: {error}\",\n            \"re_add\": \"Si prega di riaggiungere l'integrazione. Messaggio di errore: {error}\",\n            \"storage_error\": \"Eccezione del modulo di archiviazione dell'integrazione. Si prega di riprovare o riaggiungere l'integrazione: {error}\",\n            \"inconsistent_account\": \"Le informazioni dell'account sono incoerenti.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/ja.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Xiaomi Home インテグレーション\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"リスク告知\",\n                \"description\": \"1. あなたの**Xiaomiユーザー情報とデバイス情報**は、Home Assistantシステムに保存されます。**XiaomiはHome Assistantの保存メカニズムの安全性を保証できません**。情報が盗まれないようにする責任はあなたにあります。\\r\\n2. このインテグレーションはオープンソースコミュニティによって管理されています。安定性の問題やその他の問題が発生する可能性があります。問題が発生した場合は、 **Xiaomi カスタマーサポートに連絡するのではなく、オープンソースコミュニティに助けを求める必要があります**。\\r\\n3. あなたはある程度の技術力を持っている必要があります。このインテグレーションは初心者には友好的ではありません。\\r\\n4. このインテグレーションを使用する前に、 **README をよく読んでください**。\\r\\n5. 統合の安定した使用を確保し、インターフェースの乱用を防ぐために、**この統合はHome Assistantでのみ使用することが許可されています。詳細についてはLICENSEを参照してください**。\",\n                \"data\": {\n                    \"eula\": \"私は上記のリスクを理解し、インテグレーションを使用することによる関連するリスクを自己責任で引き受けます。\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"基本設定\",\n                \"description\": \"### ログインエリア\\r\\nXiaomi アカウントが属する地域を選択します。 `Xiaomi Home アプリ> マイ（ボトムメニューにあります）> その他の設定> Xiaomi Home について` で確認できます。\\r\\n### 言語\\r\\nデバイスおよびエンティティ名に使用される言語を選択します。一部の翻訳が欠落している場合、英語が表示されます。\\r\\n### OAuth2 認証リダイレクトアドレス\\r\\nOAuth2 認証リダイレクトアドレスは **[http://homeassistant.local:8123](http://homeassistant.local:8123)** です。Home Assistant は、現在の操作端末（たとえば、パーソナルコンピュータ）と同じ LAN 内にあり、操作端末がこのアドレスで Home Assistant ホームページにアクセスできる場合にのみログイン認証が成功する場合があります。\\r\\n### 統合ネットワーク構成\\r\\nローカルネットワークが正常に機能しているかどうか、および関連するネットワークリソースにアクセスできるかどうかを確認します。**初めて追加する場合は、これを選択することをお勧めします。**\\r\\n### 注意事項\\r\\n- 数百台以上のMi Homeデバイスをお持ちのユーザーの場合、統合の初回追加には時間がかかります。しばらくお待ちください。\\r\\n- Home AssistantがDocker環境で実行されている場合は、Dockerのネットワークモードがhostに設定されていることを確認してください。そうしないと、ローカル制御機能が正しく動作しない可能性があります。\\r\\n- 統合のローカル制御機能にはいくつかの依存関係があります。READMEを注意深く読んでください。\",\n                \"data\": {\n                    \"cloud_server\": \"ログインエリア\",\n                    \"integration_language\": \"言語\",\n                    \"oauth_redirect_url\": \"認証リダイレクトアドレス\",\n                    \"network_detect_config\": \"統合ネットワーク構成\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"統合ネットワーク構成\",\n                \"description\": \"## 使用方法の紹介\\r\\n### ネットワーク検出アドレス\\r\\nネットワークが正常に機能しているかどうかを確認するために使用されます。設定されていない場合、システムのデフォルトアドレスが使用されます。デフォルトアドレスのチェックが失敗した場合は、カスタムアドレスを入力してみてください。\\r\\n- 複数の検出アドレスを入力できます。アドレスはコンマで区切ります。例：`8.8.8.8,https://www.bing.com`\\r\\n- IPアドレスの場合、pingによる検出が行われます。HTTP(s)アドレスの場合、HTTP GETリクエストによる検出が行われます。\\r\\n- システムのデフォルト検出アドレスを復元する場合は、カンマ `,` を入力して「次へ」をクリックしてください。\\r\\n- **この設定はグローバルであり、変更は他の統合インスタンスに影響を与えます。慎重に変更してください。**\\r\\n### ネットワーク依存関係のチェック\\r\\n次のネットワーク依存関係がアクセス可能かどうかを順番に確認します。関連するアドレスにアクセスできない場合、統合に問題が発生します。\\r\\n- OAuth2 認証アドレス: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API アドレス: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API アドレス: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT ブローカーアドレス: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"ネットワーク検出アドレス\",\n                    \"check_network_deps\": \"ネットワーク依存関係のチェック\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"ログインエラー\",\n                \"description\": \"「次へ」をクリックして再試行してください\"\n            },\n            \"homes_select\": {\n                \"title\": \"家族とデバイスを選択\",\n                \"description\": \"## 紹介\\r\\n### デバイスの家族をインポート\\r\\n統合は選択された家族のデバイスを追加します。\\r\\n### 部屋の名前の同期モード\\r\\nMi HomeアプリからHome Assistantにデバイスを同期する際、Home Assistantのエリアの名前は以下のルールに従います。同期プロセスはMi Homeアプリの家族と部屋の設定を変更しないことに注意してください。\\r\\n- 同期しない：デバイスはどのエリアにも追加されません。\\r\\n- その他のオプション：デバイスが追加されるエリアはMi Homeアプリの家族または部屋の名前にちなんで命名されます。\\r\\n### 高度な設定\\r\\n統合のプロフェッショナルな設定オプションを変更するために高度な設定を表示します。\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} こんにちは！デバイスを追加したい家族を選択してください。\",\n                \"data\": {\n                    \"home_infos\": \"デバイスの家族をインポート\",\n                    \"area_name_rule\": \"部屋の名前の同期モード\",\n                    \"advanced_options\": \"高度な設定\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"高度な設定オプション\",\n                \"description\": \"## 紹介\\r\\n### 以下のオプションの意味がよくわからない場合は、デフォルトのままにしてください。\\r\\n### デバイスのフィルタリング\\r\\n部屋名とデバイスタイプでデバイスをフィルタリングすることができます。デバイスの次元でフィルタリングすることもできます。\\r\\n### コントロールモード\\r\\n- 自動：ローカルネットワーク内に利用可能なXiaomi中央ゲートウェイがある場合、Home Assistantはデバイス制御命令を送信するために優先的に中央ゲートウェイを使用します。ローカルネットワークに中央ゲートウェイがない場合、Xiaomi OTプロトコルを使用してデバイス制御命令を送信し、ローカル制御機能を実現します。上記のローカル制御条件が満たされない場合のみ、デバイス制御命令はクラウドを介して送信されます。\\r\\n- クラウド：制御命令はクラウドを介してのみ送信されます。\\r\\n### Actionデバッグモード\\r\\nデバイスが定義するMIoT-Spec-V2のメソッドに対して、通知エンティティを生成するだけでなく、デバイスに制御命令を送信するためのテキスト入力ボックスエンティティも生成されます。デバッグ時にデバイスに制御命令を送信するために使用できます。\\r\\n### 非標準生成エンティティを隠す\\r\\n「*」で始まる名前の非標準MIoT-Spec-V2インスタンスによって生成されたエンティティを非表示にします。\\r\\n### バイナリセンサー表示モード\\r\\nXiaomi Homeのバイナリセンサーをテキストセンサーエンティティまたはバイナリセンサーエンティティとして表示します。\\r\\n### デバイスの状態変化通知を表示\\r\\nデバイスの状態変化通知を詳細に表示し、選択された通知のみを表示します。\",\n                \"data\": {\n                    \"devices_filter\": \"デバイスをフィルタリング\",\n                    \"ctrl_mode\": \"コントロールモード\",\n                    \"action_debug\": \"Actionデバッグモード\",\n                    \"hide_non_standard_entities\": \"非標準生成エンティティを隠す\",\n                    \"display_binary_mode\": \"バイナリセンサー表示モード\",\n                    \"display_devices_changed_notify\": \"デバイスの状態変化通知を表示\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"デバイスをフィルタリング\",\n                \"description\": \"## 使用方法\\r\\n家庭の部屋の名前、デバイスの接続タイプ、デバイスのモデルでデバイスをフィルタリングすることをサポートし、デバイスの次元フィルタリングもサポートします。フィルタリングロジックは次のとおりです：\\r\\n- まず、統計ロジックに従って、すべての含まれる項目の和集合または交差を取得し、次に除外される項目の交差または和集合を取得し、最後に[含まれる集計結果]から[除外される集計結果]を引いて[フィルタ結果]を取得します。\\r\\n- 含まれる項目が選択されていない場合、すべてが含まれることを意味します。\\r\\n### フィルターモード\\r\\n- 除外：不要な項目を削除します。\\r\\n- 含む：必要な項目を含めます。\\r\\n### 統計ロジック\\r\\n- ANDロジック：同じモードのすべての項目の交差を取ります。\\r\\n- ORロジック：同じモードのすべての項目の和集合を取ります。\\r\\n\\r\\n統合項目の[設定 > デバイスリストの更新]ページに移動し、[デバイスをフィルタリング]を選択して再フィルタリングすることもできます。\",\n                \"data\": {\n                    \"room_filter_mode\": \"家族の部屋をフィルタリング\",\n                    \"room_list\": \"家族の部屋\",\n                    \"type_filter_mode\": \"デバイスタイプをフィルタリング\",\n                    \"type_list\": \"デバイスタイプ\",\n                    \"model_filter_mode\": \"デバイスモデルをフィルタリング\",\n                    \"model_list\": \"デバイスモデル\",\n                    \"devices_filter_mode\": \"デバイスをフィルタリング\",\n                    \"device_list\": \"デバイスリスト\",\n                    \"statistics_logic\": \"統計ロジック\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}ここをクリックして再度ログインしてください{link_right}\\r\\n(ログインに成功すると、自動的に次のページにリダイレクトされます)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"リスク告知文書を読んでください。\",\n            \"get_token_error\": \"ログイン認証情報（OAuth トークン）を取得できませんでした。\",\n            \"get_homeinfo_error\": \"ホーム情報を取得できませんでした。\",\n            \"mdns_discovery_error\": \"ローカルデバイス検出サービスに異常があります。\",\n            \"get_cert_error\": \"ゲートウェイ証明書を取得できませんでした。\",\n            \"no_family_selected\": \"家庭が選択されていません。\",\n            \"no_devices\": \"選択した家庭にはデバイスがありません。デバイスがある家庭を選択して続行してください。\",\n            \"no_filter_devices\": \"フィルタリングされたデバイスが空です。有効なフィルター条件を選択して続行してください。\",\n            \"no_central_device\": \"【中央ゲートウェイモード】Home Assistant が存在する LAN 内に使用可能な Xiaomi 中央ゲートウェイがある必要があります。選択された家庭がこの要件を満たしているかどうかを確認してください。\",\n            \"invalid_network_addr\": \"無効なIPアドレスまたはHTTPアドレスが検出されました。有効なアドレスを入力してください。\",\n            \"invalid_ip_addr\": \"アクセスできないIPアドレスが検出されました。有効なIPアドレスを入力してください。\",\n            \"invalid_http_addr\": \"アクセスできないHTTPアドレスが検出されました。有効なHTTPアドレスを入力してください。\",\n            \"invalid_default_addr\": \"デフォルトのネットワーク検出アドレスにアクセスできません。ネットワーク設定を確認するか、カスタムネットワーク検出アドレスを使用してください。\",\n            \"unreachable_oauth2_host\": \"OAuth2 認証アドレスにアクセスできません。ネットワーク設定を確認してください。\",\n            \"unreachable_http_host\": \"Xiaomi HTTP API アドレスにアクセスできません。ネットワーク設定を確認してください。\",\n            \"unreachable_spec_host\": \"Xiaomi SPEC API アドレスにアクセスできません。ネットワーク設定を確認してください。\",\n            \"unreachable_mqtt_broker\": \"Xiaomi MQTT ブローカーアドレスにアクセスできません。ネットワーク設定を確認してください。\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Home Assistant インスタンスIDを取得できませんでした。\",\n            \"network_connect_error\": \"設定に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク設定を確認してください。\",\n            \"already_configured\": \"このユーザーはすでに設定が完了しています。統合ページにアクセスして、「設定」ボタンをクリックして設定を変更してください。\",\n            \"invalid_auth_info\": \"認証情報が期限切れになりました。統合ページにアクセスして、「設定」ボタンをクリックして再度認証してください。\",\n            \"config_flow_error\": \"統合設定エラー：{error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"認証構成\",\n                \"description\": \"ローカル認証情報が期限切れになっています。認証を再開してください。\\r\\n### 現在のログインエリア：{cloud_server}\\r\\n### OAuth2 認証リダイレクトアドレス\\r\\nOAuth2 認証リダイレクトアドレスは **[http://homeassistant.local:8123](http://homeassistant.local:8123)** です。Home Assistant は、現在の操作端末（たとえば、パーソナルコンピュータ）と同じ LAN 内にあり、操作端末がこのアドレスで Home Assistant ホームページにアクセスできる場合にのみログイン認証が成功する場合があります。\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"認証リダイレクトアドレス\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"ログインエラー\",\n                \"description\": \"「次へ」をクリックして再試行してください\"\n            },\n            \"config_options\": {\n                \"title\": \"設定オプション\",\n                \"description\": \"### {nick_name} さん、こんにちは！\\r\\n\\r\\nXiaomi アカウントID：{uid}\\r\\n現在のログインエリア：{cloud_server}\\r\\n統合インスタンスID: {instance_id}\\r\\n\\r\\n必要な構成オプションを選択して、[次へ] をクリックしてください。\",\n                \"data\": {\n                    \"integration_language\": \"統合言語\",\n                    \"update_user_info\": \"ユーザー情報を更新する\",\n                    \"update_devices\": \"デバイスリストを更新する\",\n                    \"action_debug\": \"Action デバッグモード\",\n                    \"hide_non_standard_entities\": \"非標準生成エンティティを非表示にする\",\n                    \"display_binary_mode\": \"バイナリセンサー表示モード\",\n                    \"display_devices_changed_notify\": \"デバイスの状態変化通知を表示\",\n                    \"update_trans_rules\": \"エンティティ変換ルールを更新する\",\n                    \"update_lan_ctrl_config\": \"LAN制御構成を更新する\",\n                    \"network_detect_config\": \"統合ネットワーク構成\",\n                    \"cover_dead_zone_width\": \"カーテンの死角幅\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"ユーザー名を更新する\",\n                \"description\": \"{nick_name} さん、こんにちは！ 下のボックスからユーザー名を変更してください。\",\n                \"data\": {\n                    \"nick_name\": \"ユーザー名\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"ホームとデバイスを再度選択\",\n                \"description\": \"## 使用方法\\r\\n### 導入されたデバイスのホーム\\r\\n統合は、選択された家庭にあるデバイスを追加します。\\r\\n### デバイスをフィルタリング\\r\\n家庭の部屋の名前、デバイスの接続タイプ、デバイスのモデルでデバイスをフィルタリングすることをサポートし、デバイスの次元フィルタリングもサポートします。**{local_count}** 個のデバイスがフィルタリングされました。\\r\\n### 制御モード\\r\\n- 自動: ローカルエリアネットワーク内に利用可能なXiaomi中央ゲートウェイが存在する場合、Home Assistantは中央ゲートウェイを介してデバイス制御コマンドを優先的に送信し、ローカル制御機能を実現します。ローカルエリアネットワーク内に中央ゲートウェイが存在しない場合、Xiaomi OTプロトコルを介して制御コマンドを送信し、ローカル制御機能を実現しようとします。上記のローカル制御条件が満たされない場合にのみ、デバイス制御コマンドはクラウドを介して送信されます。\\r\\n- クラウド: 制御コマンドはクラウドを介してのみ送信されます。\",\n                \"data\": {\n                    \"home_infos\": \"導入されたデバイスのホーム\",\n                    \"devices_filter\": \"デバイスをフィルタリング\",\n                    \"ctrl_mode\": \"制御モード\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"デバイスをフィルタリング\",\n                \"description\": \"## 使用方法\\r\\n家庭の部屋の名前、デバイスの接続タイプ、デバイスのモデルでデバイスをフィルタリングすることをサポートし、デバイスの次元フィルタリングもサポートします。フィルタリングロジックは次のとおりです：\\r\\n- まず、統計ロジックに従って、すべての含まれる項目の和集合または交差を取得し、次に除外される項目の交差または和集合を取得し、最後に[含まれる集計結果]から[除外される集計結果]を引いて[フィルタ結果]を取得します。\\r\\n- 含まれる項目が選択されていない場合、すべてが含まれることを意味します。\\r\\n### フィルターモード\\r\\n- 除外：不要な項目を削除します。\\r\\n- 含む：必要な項目を含めます。\\r\\n### 統計ロジック\\r\\n- ANDロジック：同じモードのすべての項目の交差を取ります。\\r\\n- ORロジック：同じモードのすべての項目の和集合を取ります。\\r\\n\\r\\n統合項目の[設定 > デバイスリストの更新]ページに移動し、[デバイスをフィルタリング]を選択して再フィルタリングすることもできます。\",\n                \"data\": {\n                    \"room_filter_mode\": \"家族の部屋をフィルタリング\",\n                    \"room_list\": \"家族の部屋\",\n                    \"type_filter_mode\": \"デバイスタイプをフィルタリング\",\n                    \"type_list\": \"デバイスタイプ\",\n                    \"model_filter_mode\": \"デバイスモデルをフィルタリング\",\n                    \"model_list\": \"デバイスモデル\",\n                    \"devices_filter_mode\": \"デバイスをフィルタリング\",\n                    \"device_list\": \"デバイスリスト\",\n                    \"statistics_logic\": \"統計ロジック\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"エンティティ変換ルールを更新する\",\n                \"description\": \"## 使用方法\\r\\n- 現在の統合インスタンス内のデバイスのエンティティ情報を更新します。これには、SPEC多言語設定、SPECブール値翻訳、SPECモデルフィルタリングが含まれます。\\r\\n- **警告: この設定はグローバル設定**であり、ローカルキャッシュを直接更新します。他の統合インスタンスに同じモデルのデバイスがある場合、関連するインスタンスを再読み込みした後に更新されます。\\r\\n- この操作には時間がかかるため、しばらくお待ちください。「更新を確認」を選択し、「次へ」をクリックして **{urn_count}** ルールの更新を開始します。そうでない場合は、更新をスキップします。\",\n                \"data\": {\n                    \"confirm\": \"確認する\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"LAN制御構成を更新する\",\n                \"description\": \"## 使用方法\\r\\n**Xiaomi HomeデバイスのLAN制御**の設定情報を更新します。クラウドと中央ゲートウェイがデバイスを制御できない場合、統合はLANを介してデバイスを制御しようとします。ネットワークカードが選択されていない場合、LAN制御は有効になりません。\\r\\n- 現在、LAN内の**SPEC v2** WiFiデバイスのみがサポートされています。一部の古いデバイスは、制御やプロパティの同期をサポートしていない場合があります。\\r\\n- デバイスと同じネットワーク上のネットワークカードを選択してください（複数選択がサポートされています）。選択したネットワークカードが同じネットワークに2つ以上の接続を持っている場合は、最適なネットワーク接続を持つものを選択することをお勧めします。そうしないと、デバイスの正常な使用に**影響を与える可能性があります**。\\r\\n- **LAN内にローカル制御をサポートする端末デバイス（ゲートウェイ、携帯電話など）が存在する場合、LANサブスクリプションを有効にすると、ローカルオートメーションやデバイスの異常が発生する可能性があります。慎重に使用してください**。\\r\\n- **警告：この設定はグローバル設定であり、変更は他の統合インスタンスに影響を与えます。慎重に変更してください**。\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"使用するネットワークカードを選択してください\",\n                    \"enable_subscribe\": \"LANサブスクリプションを有効にする\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"統合ネットワーク構成\",\n                \"description\": \"## 使用方法の紹介\\r\\n### ネットワーク検出アドレス\\r\\nネットワークが正常に機能しているかどうかを確認するために使用されます。設定されていない場合、システムのデフォルトアドレスが使用されます。デフォルトアドレスのチェックが失敗した場合は、カスタムアドレスを入力してみてください。\\r\\n- 複数の検出アドレスを入力できます。アドレスはコンマで区切ります。例：`8.8.8.8,https://www.bing.com`\\r\\n- IPアドレスの場合、pingによる検出が行われます。HTTP(s)アドレスの場合、HTTP GETリクエストによる検出が行われます。\\r\\n- システムのデフォルト検出アドレスを復元する場合は、カンマ `,` を入力して「次へ」をクリックしてください。\\r\\n- **この設定はグローバルであり、変更は他の統合インスタンスに影響を与えます。慎重に変更してください。**\\r\\n### ネットワーク依存関係のチェック\\r\\n次のネットワーク依存関係がアクセス可能かどうかを順番に確認します。関連するアドレスにアクセスできない場合、統合に問題が発生します。\\r\\n- OAuth2 認証アドレス: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API アドレス: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API アドレス: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT ブローカーアドレス: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"ネットワーク検出アドレス\",\n                    \"check_network_deps\": \"ネットワーク依存関係のチェック\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"構成を確認する\",\n                \"description\": \"**{nick_name}** さん、こんにちは！ 最新の構成情報を確認してください。[送信] をクリックして、更新された構成を使用して再度読み込みます。\\r\\n\\r\\n統合言語：\\t{lang_new}\\r\\nユーザー名：\\t{nick_name_new}\\r\\nAction デバッグモード：\\t{action_debug}\\r\\n非標準生成エンティティを非表示にする：\\t{hide_non_standard_entities}\\r\\nカーテンの死角幅：\\t{cover_width_new}\\r\\nデバイスの状態変化通知を表示:\\t{display_devices_changed_notify}\\r\\nデバイス変更：\\t追加 **{devices_add}** 個のデバイス、削除 **{devices_remove}** 個のデバイス\\r\\n変換ルール変更：\\t合計 **{trans_rules_count}** 個の規則、更新 **{trans_rules_count_success}** 個の規則\",\n                \"data\": {\n                    \"confirm\": \"変更を確認する\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}ここをクリックして再度ログインしてください{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"ユーザーが認証されていません。認証リンクをクリックしてユーザーの身元を確認してください。\",\n            \"get_token_error\": \"ログイン認証情報（OAuthトークン）の取得に失敗しました。\",\n            \"get_homeinfo_error\": \"家庭情報の取得に失敗しました。\",\n            \"get_cert_error\": \"中枢証明書の取得に失敗しました。\",\n            \"no_family_selected\": \"家族が選択されていません。\",\n            \"no_devices\": \"選択した家庭にはデバイスがありません。デバイスがある家庭を選択して続行してください。\",\n            \"no_filter_devices\": \"フィルタリングされたデバイスが空です。有効なフィルター条件を選択して続行してください。\",\n            \"no_central_device\": \"【中枢ゲートウェイモード】には、Home Assistantが存在するローカルネットワークに使用可能なXiaomi Central Hub Gatewayが存在する必要があります。選択された家庭がこの要件を満たしているかどうかを確認してください。\",\n            \"mdns_discovery_error\": \"ローカルデバイス発見サービスが異常です。\",\n            \"update_config_error\": \"構成情報の更新に失敗しました。\",\n            \"not_confirm\": \"変更を確認していません。確認をチェックしてから送信してください。\",\n            \"invalid_network_addr\": \"無効なIPアドレスまたはHTTPアドレスが検出されました。有効なアドレスを入力してください。\",\n            \"invalid_ip_addr\": \"アクセスできないIPアドレスが検出されました。有効なIPアドレスを入力してください。\",\n            \"invalid_http_addr\": \"アクセスできないHTTPアドレスが検出されました。有効なHTTPアドレスを入力してください。\",\n            \"invalid_default_addr\": \"デフォルトのネットワーク検出アドレスにアクセスできません。ネットワーク設定を確認するか、カスタムネットワーク検出アドレスを使用してください。\",\n            \"unreachable_oauth2_host\": \"OAuth2 認証アドレスにアクセスできません。ネットワーク設定を確認してください。\",\n            \"unreachable_http_host\": \"Xiaomi HTTP API アドレスにアクセスできません。ネットワーク設定を確認してください。\",\n            \"unreachable_spec_host\": \"Xiaomi SPEC API アドレスにアクセスできません。ネットワーク設定を確認してください。\",\n            \"unreachable_mqtt_broker\": \"Xiaomi MQTT ブローカーアドレスにアクセスできません。ネットワーク設定を確認してください。\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"構成に失敗しました。ネットワーク接続に異常があります。デバイスのネットワーク構成を確認してください。\",\n            \"options_flow_error\": \"統合の再設定エラー：{error}\",\n            \"re_add\": \"統合を再度追加してください。エラーメッセージ：{error}\",\n            \"storage_error\": \"統合ストレージモジュールに異常があります。再試行するか、統合を再度追加してください：{error}\",\n            \"inconsistent_account\": \"アカウント情報が一致しません。正しいアカウントでログインしてください。\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/nl.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Xiaomi Home Integratie\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Risiconotitie\",\n                \"description\": \"1. Uw Xiaomi-gebruikersinformatie en apparaatinformatie worden opgeslagen in het Home Assistant-systeem. **Xiaomi kan de beveiliging van het opslagmechanisme van Home Assistant niet garanderen**. U bent verantwoordelijk voor het voorkomen dat uw informatie wordt gestolen.\\r\\n2. Deze integratie wordt onderhouden door de open-sourcegemeenschap. Er kunnen stabiliteitsproblemen of andere problemen optreden. Bij problemen of fouten met deze integratie, **moet u hulp zoeken bij de open-sourcegemeenschap in plaats van contact op te nemen met de Xiaomi klantenservice**.\\r\\n3. U heeft enige technische vaardigheden nodig om uw lokale werkomgeving te onderhouden. De integratie is niet gebruiksvriendelijk voor beginners.\\r\\n4. Lees het README-bestand voordat u begint.\\n\\n5. Om een stabiel gebruik van de integratie te waarborgen en misbruik van de interface te voorkomen, **mag deze integratie alleen worden gebruikt in Home Assistant. Voor details, zie de LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Ik ben me bewust van de bovenstaande risico's en bereid om vrijwillig alle risico's die gepaard gaan met het gebruik van de integratie te aanvaarden.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Basisconfiguratie\",\n                \"description\": \"### Inlogregio\\r\\nSelecteer de regio van uw Xiaomi-account. U kunt deze vinden in de Xiaomi Home APP > Profiel (onderin het menu) > Extra instellingen > Over Xiaomi Home.\\r\\n### Taal\\r\\nKies de taal voor de apparaats- en entiteitsnamen. Sommige zinnen zonder vertaling worden in het Engels weergegeven.\\r\\n### OAuth2 Omleidings-URL\\r\\nHet OAuth2 authenticatie omleidingsadres is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant moet zich in hetzelfde lokale netwerk bevinden als de huidige werkterminal (bijv. de persoonlijke computer) en de werkterminal moet toegang hebben tot de startpagina van Home Assistant via dit adres. Anders kan de inlogauthenticatie mislukken.\\r\\n### Geïntegreerde Netwerkconfiguratie\\r\\nControleer of het lokale netwerk correct functioneert en of de gerelateerde netwerkbronnen toegankelijk zijn. **Het wordt aanbevolen om dit te selecteren bij het eerste toevoegen.**\\r\\n### Opmerking\\r\\n- Voor gebruikers met honderden of meer Mi Home-apparaten kan het aanvankelijke toevoegen van de integratie enige tijd duren. Wees geduldig.\\r\\n- Als Home Assistant draait in een Docker-omgeving, zorg er dan voor dat de Docker-netwerkmodus is ingesteld op host, anders werkt de lokale controlefunctionaliteit mogelijk niet correct.\\r\\n- De lokale controlefunctionaliteit van de integratie heeft enkele afhankelijkheden. Lees het README zorgvuldig.\",\n                \"data\": {\n                    \"cloud_server\": \"Inlogregio\",\n                    \"integration_language\": \"Taal\",\n                    \"oauth_redirect_url\": \"OAuth2 Omleidings-URL\",\n                    \"network_detect_config\": \"Geïntegreerde Netwerkconfiguratie\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Geïntegreerde Netwerkconfiguratie\",\n                \"description\": \"## Gebruiksinstructie\\r\\n### Netwerkdetectieadres\\r\\nWordt gebruikt om te controleren of het netwerk correct functioneert. Als dit niet is ingesteld, wordt het standaardadres van het systeem gebruikt. Als de standaardadrescontrole mislukt, kunt u proberen een aangepast adres in te voeren.\\r\\n- U kunt meerdere detectieadressen invoeren, gescheiden door komma's, zoals `8.8.8.8,https://www.bing.com`\\r\\n- Als het een IP-adres is, wordt de detectie uitgevoerd via ping. Als het een HTTP(s)-adres is, wordt de detectie uitgevoerd via een HTTP GET-verzoek.\\r\\n- Als u het standaarddetectieadres van het systeem wilt herstellen, voert u een komma `,` in en klikt u op 'Volgende'.\\r\\n- **Deze configuratie is globaal en wijzigingen zullen andere integratie-instanties beïnvloeden. Wijzig met voorzichtigheid.**\\r\\n### Controleer Netwerkafhankelijkheden\\r\\nControleer een voor een de volgende netwerkafhankelijkheden om te zien of ze toegankelijk zijn. Als de gerelateerde adressen niet toegankelijk zijn, zal dit integratieproblemen veroorzaken.\\r\\n- OAuth2-authenticatieadres: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API-adres: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API-adres: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker-adres: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Netwerkdetectieadres\",\n                    \"check_network_deps\": \"Controleer Netwerkafhankelijkheden\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Inlogfout\",\n                \"description\": \"Klik OP VOLGENDE om het opnieuw te proberen.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Selecteer Familie en Apparaat\",\n                \"description\": \"## Inleiding\\r\\n### Importeer de familie van het apparaat\\r\\nDe integratie voegt apparaten toe uit de geselecteerde familie.\\r\\n### Kamernaam Synchronisatiemodus\\r\\nBij het synchroniseren van apparaten van de Mi Home-app naar Home Assistant, wordt de naam van het gebied in Home Assistant volgens de volgende regels genoemd. Houd er rekening mee dat het synchronisatieproces de instellingen van familie en kamer in de Mi Home-app niet zal wijzigen.\\r\\n- Niet synchroniseren: Het apparaat wordt niet aan een gebied toegevoegd.\\r\\n- Andere opties: Het gebied waaraan het apparaat wordt toegevoegd, wordt genoemd naar de familie- of kamernaam in de Mi Home-app.\\r\\n### Geavanceerde instellingen\\r\\nToon geavanceerde instellingen om de professionele configuratie-opties van de integratie te wijzigen.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Hallo! Selecteer de familie waaraan u het apparaat wilt toevoegen.\",\n                \"data\": {\n                    \"home_infos\": \"Importeer de familie van het apparaat\",\n                    \"area_name_rule\": \"Kamernaam Synchronisatiemodus\",\n                    \"advanced_options\": \"Geavanceerde instellingen\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Geavanceerde Instellingen\",\n                \"description\": \"## Inleiding\\r\\n### Tenzij u zeer goed op de hoogte bent van de betekenis van de volgende opties, houdt u de standaardinstellingen.\\r\\n### Apparaten filteren\\r\\nOndersteunt het filteren van apparaten op basis van kamer- en apparaattypen, en ondersteunt ook apparaatdimensiefiltering.\\r\\n### Besturingsmodus\\r\\n- Automatisch: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, zal Home Assistant eerst apparaatbesturingsinstructies via de centrale hubgateway verzenden om lokale controlefunctionaliteit te bereiken. Als er geen centrale hub in het lokale netwerk is, zal het proberen om besturingsinstructies via het Xiaomi OT-protocol te verzenden om lokale controlefunctionaliteit te bereiken. Alleen als de bovenstaande lokale controlevoorwaarden niet worden vervuld, worden apparaatbesturingsinstructies via de cloud verzonden.\\r\\n- Cloud: Besturingsinstructies worden alleen via de cloud verzonden.\\r\\n### Actie-debugmodus\\r\\nVoor methoden die zijn gedefinieerd in de MIoT-Spec-V2 van het apparaat, wordt naast het genereren van een meldingsentiteit ook een tekstinvoerveldentiteit gegenereerd. U kunt dit gebruiken om besturingsinstructies naar het apparaat te sturen tijdens het debuggen.\\r\\n### Niet-standaard entiteiten verbergen\\r\\nVerberg entiteiten die zijn gegenereerd door niet-standaard MIoT-Spec-V2-instanties die beginnen met \\\"*\\\".\\r\\n### Binaire sensorweergavemodus\\r\\nToont binaire sensoren in Xiaomi Home als tekstsensor-entiteit of binairesensor-entiteit。\\r\\n### Apparaatstatuswijzigingen weergeven\\r\\nGedetailleerde apparaatstatuswijzigingen weergeven, alleen de geselecteerde meldingen weergeven.\",\n                \"data\": {\n                    \"devices_filter\": \"Apparaten filteren\",\n                    \"ctrl_mode\": \"Besturingsmodus\",\n                    \"action_debug\": \"Actie-debugmodus\",\n                    \"hide_non_standard_entities\": \"Niet-standaard entiteiten verbergen\",\n                    \"display_binary_mode\": \"Binaire sensorweergavemodus\",\n                    \"display_devices_changed_notify\": \"Apparaatstatuswijzigingen weergeven\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Apparaten filteren\",\n                \"description\": \"## Gebruiksinstructies\\r\\nOndersteunt het filteren van apparaten op kamernaam, apparaattoegangstype en apparaattype, en ondersteunt ook het filteren op apparaatdimensie. De filterlogica is als volgt:\\r\\n- Eerst, volgens de statistische logica, de vereniging of doorsnede van alle opgenomen items verkrijgen, vervolgens de doorsnede of vereniging van de uitgesloten items verkrijgen, en ten slotte het [opgenomen samenvattingsresultaat] aftrekken van het [uitgesloten samenvattingsresultaat] om het [filterresultaat] te verkrijgen.\\r\\n- Als er geen opgenomen items zijn geselecteerd, betekent dit dat alles is opgenomen.\\r\\n### Filtermodus\\r\\n- Uitsluiten: Ongewenste items verwijderen.\\r\\n- Opnemen: Gewenste items opnemen.\\r\\n### Statistische Logica\\r\\n- EN-logica: Neem de doorsnede van alle items in dezelfde modus.\\r\\n- OF-logica: Neem de vereniging van alle items in dezelfde modus.\\r\\n\\r\\nU kunt ook naar de pagina [Configuratie > Apparaatlijst bijwerken] van het integratie-item gaan, [Apparaten filteren] aanvinken om opnieuw te filteren.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Kamerfiltermodus\",\n                    \"room_list\": \"Kamers\",\n                    \"type_filter_mode\": \"Apparaattypen filteren\",\n                    \"type_list\": \"Apparaattypen\",\n                    \"model_filter_mode\": \"Apparaatmodel filteren\",\n                    \"model_list\": \"Apparaatmodellen\",\n                    \"devices_filter_mode\": \"Apparaten filteren\",\n                    \"device_list\": \"Apparaatlijst\",\n                    \"statistics_logic\": \"Statistische logica\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Klik hier om in te loggen{link_right}\\r\\n(U wordt automatisch doorgestuurd naar de volgende pagina na succesvolle inlog)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Lees de risiconotitie.\",\n            \"get_token_error\": \"Mislukt bij het ophalen van inlogautorisatie-informatie (OAuth-token).\",\n            \"get_homeinfo_error\": \"Mislukt bij het ophalen van huisinformatie.\",\n            \"mdns_discovery_error\": \"Lokaal apparaatsontdekkingsservice-exceptie.\",\n            \"get_cert_error\": \"Mislukt bij het ophalen van het certificaat van de centrale hubgateway.\",\n            \"no_family_selected\": \"Geen huis geselecteerd.\",\n            \"no_devices\": \"Er zijn geen apparaten in het geselecteerde huis. Selecteer een huis met apparaten en ga verder.\",\n            \"no_filter_devices\": \"Gefilterde apparaten zijn leeg. Selecteer geldige filtercriteria en ga verder.\",\n            \"no_central_device\": \"[Centrale Hub Gateway Modus] vereist een beschikbare Xiaomi centrale hubgateway in het lokale netwerk waar Home Assistant zich bevindt. Controleer of het geselecteerde huis aan deze vereiste voldoet.\",\n            \"invalid_network_addr\": \"Ongeldig IP-adres of HTTP-adres gedetecteerd, voer een geldig adres in.\",\n            \"invalid_ip_addr\": \"Onbereikbaar IP-adres gedetecteerd, voer een geldig IP-adres in.\",\n            \"invalid_http_addr\": \"Onbereikbaar HTTP-adres gedetecteerd, voer een geldig HTTP-adres in.\",\n            \"invalid_default_addr\": \"Standaard netwerkdetectieadres is onbereikbaar, controleer de netwerkconfiguratie of gebruik een aangepast netwerkdetectieadres.\",\n            \"unreachable_oauth2_host\": \"Kan OAuth2-authenticatieadres niet bereiken, controleer de netwerkconfiguratie.\",\n            \"unreachable_http_host\": \"Kan Xiaomi HTTP API-adres niet bereiken, controleer de netwerkconfiguratie.\",\n            \"unreachable_spec_host\": \"Kan Xiaomi SPEC API-adres niet bereiken, controleer de netwerkconfiguratie.\",\n            \"unreachable_mqtt_broker\": \"Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Mislukt bij het ophalen van Home Assistant UUID.\",\n            \"network_connect_error\": \"Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.\",\n            \"already_configured\": \"Configuratie voor deze gebruiker is al voltooid. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om wijzigingen aan te brengen.\",\n            \"invalid_auth_info\": \"Authenticatie-informatie is verlopen. Ga naar de integratiepagina en klik op de CONFIGUREER-knop om opnieuw te authentiseren.\",\n            \"config_flow_error\": \"Integratie configuratiefout: {error}.\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Authenticatieconfiguratie\",\n                \"description\": \"Lokale authenticatie-informatie is verlopen. Begin alstublieft het authenticatieproces opnieuw.\\r\\n### Huidige inlogregio: {cloud_server}\\r\\n### OAuth2 Omleidings-URL\\r\\nHet OAuth2 authenticatie omleidingsadres is **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. Home Assistant moet zich in hetzelfde lokale netwerk bevinden als de huidige werkterminal (bijv. de persoonlijke computer) en de werkterminal moet toegang hebben tot de startpagina van Home Assistant via dit adres. Anders kan de inlogauthenticatie mislukken.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"OAuth2 Omleidings-URL\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Er is een fout opgetreden tijdens het inloggen.\",\n                \"description\": \"Klik OP VOLGENDE om opnieuw te proberen.\"\n            },\n            \"config_options\": {\n                \"title\": \"Configuratie-opties\",\n                \"description\": \"### Hallo, {nick_name}\\r\\n\\r\\nXiaomi ID: {uid}\\r\\nHuidige inlogregio: {cloud_server}\\r\\nIntegratie-instantie-ID: {instance_id}\\r\\n\\r\\nKies de opties die u wilt configureren en klik vervolgens op VOLGENDE.\",\n                \"data\": {\n                    \"integration_language\": \"Integratietaal\",\n                    \"update_user_info\": \"Werk gebruikersinformatie bij\",\n                    \"update_devices\": \"Werk apparatenlijst bij\",\n                    \"action_debug\": \"Debugmodus voor actie\",\n                    \"hide_non_standard_entities\": \"Verberg niet-standaard gemaakte entiteiten\",\n                    \"display_binary_mode\": \"Binaire sensorweergavemodus\",\n                    \"display_devices_changed_notify\": \"Apparaatstatuswijzigingen weergeven\",\n                    \"update_trans_rules\": \"Werk entiteitsconversieregels bij\",\n                    \"update_lan_ctrl_config\": \"Werk LAN controleconfiguratie bij\",\n                    \"network_detect_config\": \"Geïntegreerde Netwerkconfiguratie\",\n                    \"cover_dead_zone_width\": \"Breedte van de dode hoek van het gordijn\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Bijwerken van gebruikersnickname\",\n                \"description\": \"Hallo {nick_name}, u kunt uw aangepaste bijnaam hieronder wijzigen.\",\n                \"data\": {\n                    \"nick_name\": \"Bijnaam\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Huis en Apparaten opnieuw selecteren\",\n                \"description\": \"## Gebruiksinstructies\\r\\n### Importeer apparaten uit huis\\r\\nDe integratie voegt apparaten toe van de geselecteerde huizen.\\r\\n### Apparaten filteren\\r\\nOndersteunt het filteren van apparaten op kamernaam, apparaattoegangstype en apparaattype, en ondersteunt ook het filteren op apparaatdimensie. **{local_count}** apparaten zijn gefilterd.\\r\\n### Controlemodus\\r\\n- Auto: Wanneer er een beschikbare Xiaomi centrale hubgateway in het lokale netwerk is, geeft Home Assistant de voorkeur aan het verzenden van apparaatbedieningscommando's via de centrale hubgateway om lokale controle te bereiken. Als er geen centrale hubgateway in het lokale netwerk is, zal het proberen bedieningscommando's te verzenden via de Xiaomi LAN-controlefunctie. Alleen wanneer de bovenstaande lokale controlevoorwaarden niet zijn vervuld, worden de apparaatbedieningscommando's via de cloud verzonden.\\r\\n- Cloud: Alle bedieningscommando's worden via de cloud verzonden.\",\n                \"data\": {\n                    \"home_infos\": \"Importeer apparaten uit huis\",\n                    \"devices_filter\": \"Apparaten filteren\",\n                    \"ctrl_mode\": \"Controlemodus\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Apparaten filteren\",\n                \"description\": \"## Gebruiksinstructies\\r\\nOndersteunt het filteren van apparaten op kamernaam, apparaattoegangstype en apparaattype, en ondersteunt ook het filteren op apparaatdimensie. De filterlogica is als volgt:\\r\\n- Eerst, volgens de statistische logica, de vereniging of doorsnede van alle opgenomen items verkrijgen, vervolgens de doorsnede of vereniging van de uitgesloten items verkrijgen, en ten slotte het [opgenomen samenvattingsresultaat] aftrekken van het [uitgesloten samenvattingsresultaat] om het [filterresultaat] te verkrijgen.\\r\\n- Als er geen opgenomen items zijn geselecteerd, betekent dit dat alles is opgenomen.\\r\\n### Filtermodus\\r\\n- Uitsluiten: Ongewenste items verwijderen.\\r\\n- Opnemen: Gewenste items opnemen.\\r\\n### Statistische Logica\\r\\n- EN-logica: Neem de doorsnede van alle items in dezelfde modus.\\r\\n- OF-logica: Neem de vereniging van alle items in dezelfde modus.\\r\\n\\r\\nU kunt ook naar de pagina [Configuratie > Apparaatlijst bijwerken] van het integratie-item gaan, [Apparaten filteren] aanvinken om opnieuw te filteren.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Kamerfiltermodus\",\n                    \"room_list\": \"Kamers\",\n                    \"type_filter_mode\": \"Apparaattypen filteren\",\n                    \"type_list\": \"Apparaattypen\",\n                    \"model_filter_mode\": \"Apparaatmodel filteren\",\n                    \"model_list\": \"Apparaatmodellen\",\n                    \"devices_filter_mode\": \"Apparaten filteren\",\n                    \"device_list\": \"Apparaatlijst\",\n                    \"statistics_logic\": \"Statistische logica\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Bijwerken van entiteiten transformateregels\",\n                \"description\": \"## Gebruiksinstructies\\r\\n- Werk de entiteitsinformatie van apparaten in de huidige integratie-instantie bij, inclusief MIoT-Spec-V2 meertalige configuratie, booleanvertaling en modelfiltering.\\r\\n- **Waarschuwing**: Dit is een globale configuratie en zal de lokale cache bijwerken. Dit zal alle integratie-instanties beïnvloeden.\\r\\n- Deze handeling duurt enige tijd, wees geduldig. Vink \\\"Bevestig bijwerken\\\" aan en klik op \\\"Volgende\\\" om **{urn_count}** regels bij te werken, anders overslaan.\",\n                \"data\": {\n                    \"confirm\": \"Bevestig de update\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Update LAN controleconfiguratie\",\n                \"description\": \"## Gebruiksinstructies\\r\\nWerk de configuraties voor de Xiaomi LAN controlefunctie bij. Wanneer de cloud en de centrale hubgateway de apparaten niet kunnen bedienen, zal de integratie proberen de apparaten via het LAN te bedienen. Als er geen netwerkkaart is geselecteerd, zal de LAN controlefunctie niet werken.\\r\\n- Alleen MIoT-Spec-V2 compatibele IP-apparaten in het LAN worden ondersteund. Sommige apparaten die vóór 2020 zijn geproduceerd, ondersteunen mogelijk geen LAN controle of LAN abonnement.\\r\\n- Selecteer de netwerkkaart(en) op hetzelfde netwerk als de te bedienen apparaten. Meerdere netwerkkaarten kunnen worden geselecteerd. Als Home Assistant vanwege de meervoudige selectie van de netwerkkaarten twee of meer verbindingen heeft met het lokale netwerk, wordt aanbevolen om de verbinding met de beste netwerkverbinding te selecteren, anders kan dit een negatief effect hebben op de apparaten.\\r\\n- Als er terminalapparaten (Xiaomi-luidsprekers met scherm, mobiele telefoons, enz.) in het LAN zijn die lokale controle ondersteunen, kan het inschakelen van LAN-abonnement leiden tot lokale automatisering- en apparaatanomalieën.\\r\\n- **Waarschuwing**: Dit is een globale configuratie. Het zal alle integratie-instanties beïnvloeden. Gebruik het met voorzichtigheid.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Selecteer alstublieft de te gebruiken netwerkkaart\",\n                    \"enable_subscribe\": \"Zet LAN-abonnement aan\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Geïntegreerde Netwerkconfiguratie\",\n                \"description\": \"## Gebruiksinstructie\\r\\n### Netwerkdetectieadres\\r\\nWordt gebruikt om te controleren of het netwerk correct functioneert. Als dit niet is ingesteld, wordt het standaardadres van het systeem gebruikt. Als de standaardadrescontrole mislukt, kunt u proberen een aangepast adres in te voeren.\\r\\n- U kunt meerdere detectieadressen invoeren, gescheiden door komma's, zoals `8.8.8.8,https://www.bing.com`\\r\\n- Als het een IP-adres is, wordt de detectie uitgevoerd via ping. Als het een HTTP(s)-adres is, wordt de detectie uitgevoerd via een HTTP GET-verzoek.\\r\\n- Als u het standaarddetectieadres van het systeem wilt herstellen, voert u een komma `,` in en klikt u op 'Volgende'.\\r\\n- **Deze configuratie is globaal en wijzigingen zullen andere integratie-instanties beïnvloeden. Wijzig met voorzichtigheid.**\\r\\n### Controleer Netwerkafhankelijkheden\\r\\nControleer een voor een de volgende netwerkafhankelijkheden om te zien of ze toegankelijk zijn. Als de gerelateerde adressen niet toegankelijk zijn, zal dit integratieproblemen veroorzaken.\\r\\n- OAuth2-authenticatieadres: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API-adres: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API-adres: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker-adres: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Netwerkdetectieadres\",\n                    \"check_network_deps\": \"Controleer Netwerkafhankelijkheden\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Bevestig Configuratie\",\n                \"description\": \"Hallo **{nick_name}**, bevestig alstublieft de nieuwste configuratie-informatie en klik vervolgens op INDENKEN.\\r\\nDe integratie zal opnieuw laden met de bijgewerkte configuratie.\\r\\n\\r\\nIntegratietaal:\\t{lang_new}\\r\\nBijnaam:\\t{nick_name_new}\\r\\nDebugmodus voor actie:\\t{action_debug}\\r\\nVerberg niet-standaard gemaakte entiteiten:\\t{hide_non_standard_entities}\\r\\nBreedte van de dode hoek van het gordijn:\\t{cover_width_new}\\r\\nApparaatstatuswijzigingen weergeven:\\t{display_devices_changed_notify}\\r\\nWijzigingen in apparaten:\\tVoeg **{devices_add}** apparaten toe, Verwijder **{devices_remove}** apparaten\\r\\nWijzigingen in transformateregels:\\tEr zijn in totaal **{trans_rules_count}** regels, en **{trans_rules_count_success}** regels zijn bijgewerkt\",\n                \"data\": {\n                    \"confirm\": \"Bevestig de wijziging\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Klik hier om opnieuw in te loggen{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Niet geauthenticeerd. Klik op de authenticatielink om de gebruikersidentiteit te verifiëren.\",\n            \"get_token_error\": \"Mislukt bij het ophalen van inlogautorisatie-informatie (OAuth-token).\",\n            \"get_homeinfo_error\": \"Mislukt bij het ophalen van huisinformatie.\",\n            \"get_cert_error\": \"Mislukt bij het ophalen van het certificaat van de centrale hubgateway.\",\n            \"no_devices\": \"Er zijn geen apparaten in het geselecteerde huis. Selecteer een huis met apparaten en ga verder.\",\n            \"no_filter_devices\": \"Gefilterde apparaten zijn leeg. Selecteer geldige filtercriteria en ga verder.\",\n            \"no_family_selected\": \"Geen huis geselecteerd.\",\n            \"no_central_device\": \"[Centrale Hub Gateway Modus] vereist een beschikbare Xiaomi centrale hubgateway in het lokale netwerk waar Home Assistant zich bevindt. Controleer of het geselecteerde huis aan deze vereiste voldoet.\",\n            \"mdns_discovery_error\": \"Lokaal apparaatsontdekkingsservice-exceptie.\",\n            \"update_config_error\": \"Mislukt bij het bijwerken van configuratie-informatie.\",\n            \"not_confirm\": \"Wijzigingen zijn niet bevestigd. Bevestig de wijziging voordat u deze indient.\",\n            \"invalid_network_addr\": \"Ongeldig IP-adres of HTTP-adres gedetecteerd, voer een geldig adres in.\",\n            \"invalid_ip_addr\": \"Onbereikbaar IP-adres gedetecteerd, voer een geldig IP-adres in.\",\n            \"invalid_http_addr\": \"Onbereikbaar HTTP-adres gedetecteerd, voer een geldig HTTP-adres in.\",\n            \"invalid_default_addr\": \"Standaard netwerkdetectieadres is onbereikbaar, controleer de netwerkconfiguratie of gebruik een aangepast netwerkdetectieadres.\",\n            \"unreachable_oauth2_host\": \"Kan OAuth2-authenticatieadres niet bereiken, controleer de netwerkconfiguratie.\",\n            \"unreachable_http_host\": \"Kan Xiaomi HTTP API-adres niet bereiken, controleer de netwerkconfiguratie.\",\n            \"unreachable_spec_host\": \"Kan Xiaomi SPEC API-adres niet bereiken, controleer de netwerkconfiguratie.\",\n            \"unreachable_mqtt_broker\": \"Kan Xiaomi MQTT Broker-adres niet bereiken, controleer de netwerkconfiguratie.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Configuratie mislukt. De netwerkverbinding is abnormaal. Controleer de netwerkinstellingen van de apparatuur.\",\n            \"options_flow_error\": \"Integratie herconfiguratiefout: {error}\",\n            \"re_add\": \"Voeg de integratie opnieuw toe. Foutmelding: {error}\",\n            \"storage_error\": \"Integratie opslagmodule-exceptie. Probeer het opnieuw of voeg de integratie opnieuw toe: {error}\",\n            \"inconsistent_account\": \"Accountinformatie is inconsistent. Log in met het juiste account.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/pt-BR.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Integração Xiaomi Home\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Aviso de risco\",\n                \"description\": \"1. Suas informações de usuário Xiaomi e informações dos dispositivos serão armazenadas no sistema Home Assistant. **A Xiaomi não pode garantir a segurança do mecanismo de armazenamento do Home Assistant**. Você é responsável por evitar que suas informações sejam roubadas.\\r\\n2. Esta integração é mantida pela comunidade open-source. Podem haver problemas de estabilidade ou outros problemas. Ao encontrar falhas ou erros nesta integração, **você deve buscar ajuda da comunidade open-source em vez de contatar o suporte da Xiaomi**.\\r\\n3. Você precisará de certa habilidade técnica para manter seu ambiente operacional local. Esta integração não é amigável para iniciantes.\\r\\n4. Por favor, leia o arquivo README antes de começar.\\n\\n5. Para garantir o uso estável da integração e evitar abusos da interface, **esta integração só é permitida para uso no Home Assistant. Para mais detalhes, consulte a LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Estou ciente dos riscos acima e disposto(a) a assumi-los voluntariamente ao utilizar a integração.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Configuração básica\",\n                \"description\": \"### Região de Login\\r\\nSelecione a região da sua conta Xiaomi. Você pode encontrá-la no aplicativo Xiaomi Home > Perfil (localizado no menu inferior) > Configurações adicionais > Sobre o Xiaomi Home.\\r\\n### Idioma\\r\\nSelecione o idioma dos nomes dos dispositivos e entidades. Algumas frases sem tradução serão exibidas em inglês.\\r\\n### URL de Redirecionamento OAuth2\\r\\nO endereço de redirecionamento da autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant precisa estar na mesma rede local que o terminal atual (por exemplo, o computador pessoal) e o terminal precisa acessar a página inicial do Home Assistant através desse endereço. Caso contrário, a autenticação de login pode falhar.\\r\\n### Configuração de Rede Integrada\\r\\nVerifique se a rede local está funcionando corretamente e se os recursos de rede relacionados estão acessíveis. **Recomenda-se selecionar isso ao adicionar pela primeira vez.**\\r\\n### Observações\\r\\n- Para usuários com centenas ou mais dispositivos Mi Home, a adição inicial da integração levará algum tempo. Seja paciente.\\r\\n- Se o Home Assistant estiver sendo executado em um ambiente Docker, certifique-se de que o modo de rede do Docker esteja definido como host, caso contrário a funcionalidade de controle local pode não funcionar corretamente.\\r\\n- A funcionalidade de controle local da integração tem algumas dependências. Por favor, leia o README atentamente.\",\n                \"data\": {\n                    \"cloud_server\": \"Região de Login\",\n                    \"integration_language\": \"Idioma\",\n                    \"oauth_redirect_url\": \"URL de Redirecionamento OAuth2\",\n                    \"network_detect_config\": \"Configuração de Rede Integrada\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuração de Detecção de Rede\",\n                \"description\": \"## Introdução ao Uso\\r\\n### Endereço de Detecção de Rede\\r\\nUsado para verificar se a rede está funcionando corretamente. Se não for definido, o endereço padrão do sistema será usado. Se a verificação do endereço padrão falhar, você pode tentar inserir um endereço personalizado.\\r\\n- Você pode inserir vários endereços de detecção, separados por vírgulas, como `8.8.8.8,https://www.bing.com`\\r\\n- Se for um endereço IP, a detecção será feita via ping. Se for um endereço HTTP(s), a detecção será feita via solicitação HTTP GET.\\r\\n- Se você deseja restaurar o endereço de detecção padrão do sistema, insira uma vírgula `,` e clique em 'Próximo'.\\r\\n- **Esta configuração é global e as alterações afetarão outras instâncias de integração. Modifique com cautela.**\\r\\n### Verificar Dependências de Rede\\r\\nVerifique uma por uma as seguintes dependências de rede para ver se são acessíveis. Se os endereços relacionados não forem acessíveis, isso causará problemas de integração.\\r\\n- Endereço de Autenticação OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Endereço da API HTTP da Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Endereço da API SPEC da Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Endereço do Broker MQTT da Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Endereço de Detecção de Rede\",\n                    \"check_network_deps\": \"Verificar Dependências de Rede\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Erro de Login\",\n                \"description\": \"Clique em AVANÇAR para tentar novamente.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Selecionar Família e Dispositivo\",\n                \"description\": \"## Introdução\\r\\n### Importar a Família do Dispositivo\\r\\nA integração adicionará dispositivos da família selecionada.\\r\\n### Modo de Sincronização do Nome da Sala\\r\\nAo sincronizar dispositivos do APP Mi Home para o Home Assistant, a nomeação da área no Home Assistant seguirá as regras abaixo. Observe que o processo de sincronização não alterará as configurações de família e sala no APP Mi Home.\\r\\n- Não sincronizar: O dispositivo não será adicionado a nenhuma área.\\r\\n- Outras opções: A área à qual o dispositivo é adicionado será nomeada de acordo com o nome da família ou da sala no APP Mi Home.\\r\\n### Configurações Avançadas\\r\\nMostrar configurações avançadas para modificar as opções de configuração profissional da integração.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Olá! Por favor, selecione a família à qual você deseja adicionar o dispositivo.\",\n                \"data\": {\n                    \"home_infos\": \"Importar a Família do Dispositivo\",\n                    \"area_name_rule\": \"Modo de Sincronização do Nome da Sala\",\n                    \"advanced_options\": \"Configurações Avançadas\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Configurações Avançadas\",\n                \"description\": \"## Introdução\\r\\n### A menos que você entenda claramente o significado das opções a seguir, mantenha as configurações padrão.\\r\\n### Filtrar Dispositivos\\r\\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\\r\\n### Modo de Controle\\r\\n- Automático: Quando um gateway central Xiaomi disponível na rede local está disponível, o Home Assistant enviará comandos de controle de dispositivo através do gateway central para realizar a função de controle local. Quando não há gateway central na rede local, ele tentará enviar comandos de controle através do protocolo OT da Xiaomi para realizar a função de controle local. Somente quando as condições de controle local acima não forem atendidas, os comandos de controle do dispositivo serão enviados através da nuvem.\\r\\n- Nuvem: Os comandos de controle são enviados apenas através da nuvem.\\r\\n### Modo de Depuração de Ações\\r\\nPara métodos definidos pelo MIoT-Spec-V2 do dispositivo, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para você enviar comandos de controle ao dispositivo durante a depuração.\\r\\n### Ocultar Entidades Geradas Não Padrão\\r\\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão que começam com \\\"*\\\".\\r\\n### Modo de exibição do sensor binário\\r\\nExibe sensores binários no Xiaomi Home como entidade de sensor de texto ou entidade de sensor binário。\\r\\n### Exibir notificações de mudança de status do dispositivo\\r\\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.\",\n                \"data\": {\n                    \"devices_filter\": \"Filtrar Dispositivos\",\n                    \"ctrl_mode\": \"Modo de Controle\",\n                    \"action_debug\": \"Modo de Depuração de Ações\",\n                    \"hide_non_standard_entities\": \"Ocultar Entidades Geradas Não Padrão\",\n                    \"display_binary_mode\": \"Modo de exibição do sensor binário\",\n                    \"display_devices_changed_notify\": \"Exibir notificações de mudança de status do dispositivo\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrar Dispositivos\",\n                \"description\": \"## Instruções de Uso\\r\\nSuporta a filtragem de dispositivos por nome da sala, tipo de acesso do dispositivo e modelo do dispositivo, e também suporta a filtragem por dimensão do dispositivo. A lógica de filtragem é a seguinte:\\r\\n- Primeiro, de acordo com a lógica estatística, obtenha a união ou interseção de todos os itens incluídos, depois obtenha a interseção ou união dos itens excluídos, e finalmente subtraia o [resultado do resumo incluído] do [resultado do resumo excluído] para obter o [resultado do filtro].\\r\\n- Se nenhum item incluído for selecionado, significa que todos estão incluídos.\\r\\n### Modo de Filtragem\\r\\n- Excluir: Remover itens indesejados.\\r\\n- Incluir: Incluir itens desejados.\\r\\n### Lógica Estatística\\r\\n- Lógica E: Pegue a interseção de todos os itens no mesmo modo.\\r\\n- Lógica OU: Pegue a união de todos os itens no mesmo modo.\\r\\n\\r\\nVocê também pode ir para a página [Configuração > Atualizar Lista de Dispositivos] do item de integração, marcar [Filtrar Dispositivos] para refiltrar.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrar por Sala\",\n                    \"room_list\": \"Salas\",\n                    \"type_filter_mode\": \"Filtrar por Tipo de Dispositivo\",\n                    \"type_list\": \"Tipos de Dispositivo\",\n                    \"model_filter_mode\": \"Filtrar por Modelo de Dispositivo\",\n                    \"model_list\": \"Modelos de Dispositivo\",\n                    \"devices_filter_mode\": \"Filtrar Dispositivos\",\n                    \"device_list\": \"Lista de Dispositivos\",\n                    \"statistics_logic\": \"Lógica de Estatísticas\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Clique aqui para fazer login{link_right}\\r\\n(Você será redirecionado automaticamente para a próxima página após um login bem-sucedido)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Por favor, leia o aviso de risco.\",\n            \"get_token_error\": \"Falha ao obter as informações de autorização de login (token OAuth).\",\n            \"get_homeinfo_error\": \"Falha ao obter as informações da casa.\",\n            \"mdns_discovery_error\": \"Exceção no serviço de descoberta de dispositivos locais.\",\n            \"get_cert_error\": \"Falha ao obter o certificado do gateway central.\",\n            \"no_family_selected\": \"Nenhuma casa selecionada.\",\n            \"no_devices\": \"Não há dispositivos na casa selecionada. Por favor, selecione uma casa com dispositivos e continue.\",\n            \"no_filter_devices\": \"Os dispositivos filtrados estão vazios. Por favor, selecione critérios de filtro válidos e continue.\",\n            \"no_central_device\": \"[Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada atende a esse requisito.\",\n            \"invalid_network_addr\": \"Endereço IP ou HTTP inválido detectado, por favor insira um endereço válido.\",\n            \"invalid_ip_addr\": \"Endereço IP inacessível detectado, por favor insira um endereço IP válido.\",\n            \"invalid_http_addr\": \"Endereço HTTP inacessível detectado, por favor insira um endereço HTTP válido.\",\n            \"invalid_default_addr\": \"O endereço de detecção de rede padrão está inacessível, por favor verifique a configuração da rede ou use um endereço de detecção de rede personalizado.\",\n            \"unreachable_oauth2_host\": \"Não é possível acessar o endereço de autenticação OAuth2, verifique a configuração da rede.\",\n            \"unreachable_http_host\": \"Não é possível acessar o endereço da API HTTP da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_spec_host\": \"Não é possível acessar o endereço da API SPEC da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_mqtt_broker\": \"Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Falha ao obter o UUID do Home Assistant.\",\n            \"network_connect_error\": \"Configuração falhou. A conexão de rede está anormal. Verifique a configuração de rede do equipamento.\",\n            \"already_configured\": \"A configuração para este usuário já foi concluída. Vá para a página de integrações e clique no botão CONFIGURAR para modificações.\",\n            \"invalid_auth_info\": \"As informações de autenticação expiraram. Vá para a página de integrações e clique em CONFIGURAR para reautenticar.\",\n            \"config_flow_error\": \"Erro na configuração da integração: {error}.\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Configuração de Autenticação\",\n                \"description\": \"As informações de autenticação local expiraram. Por favor, reinicie o processo de autenticação.\\r\\n### Região de Login Atual: {cloud_server}\\r\\n### URL de Redirecionamento OAuth2\\r\\nO endereço de redirecionamento da autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant precisa estar na mesma rede local que o terminal atual (por exemplo, o computador pessoal) e o terminal precisa acessar a página inicial do Home Assistant através desse endereço. Caso contrário, a autenticação de login pode falhar.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"URL de Redirecionamento OAuth2\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Ocorreu um erro durante o login.\",\n                \"description\": \"Clique em AVANÇAR para tentar novamente.\"\n            },\n            \"config_options\": {\n                \"title\": \"Opções de Configuração\",\n                \"description\": \"### Olá, {nick_name}\\r\\n\\r\\nID Xiaomi: {uid}\\r\\nRegião de Login Atual: {cloud_server}\\r\\nID da Instância de Integração: {instance_id}\\r\\n\\r\\nSelecione as opções que você deseja configurar e clique em AVANÇAR.\",\n                \"data\": {\n                    \"integration_language\": \"Idioma da Integração\",\n                    \"update_user_info\": \"Atualizar informações do usuário\",\n                    \"update_devices\": \"Atualizar lista de dispositivos\",\n                    \"action_debug\": \"Modo de depuração para ação\",\n                    \"hide_non_standard_entities\": \"Ocultar entidades não padrão criadas\",\n                    \"display_binary_mode\": \"Modo de exibição do sensor binário\",\n                    \"display_devices_changed_notify\": \"Exibir notificações de mudança de status do dispositivo\",\n                    \"update_trans_rules\": \"Atualizar regras de conversão de entidades\",\n                    \"update_lan_ctrl_config\": \"Atualizar configuração de controle LAN\",\n                    \"network_detect_config\": \"Configuração de Rede Integrada\",\n                    \"cover_dead_zone_width\": \"Largura da área cega da cortina\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Atualizar Apelido do Usuário\",\n                \"description\": \"Olá {nick_name}, você pode modificar seu apelido personalizado abaixo.\",\n                \"data\": {\n                    \"nick_name\": \"Apelido\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Selecionar novamente Casa e Dispositivos\",\n                \"description\": \"## Instruções de Uso\\r\\n### Importar dispositivos da casa\\r\\nA integração adicionará dispositivos das casas selecionadas.\\r\\n### Filtrar Dispositivos\\r\\nSuporta a filtragem de dispositivos por nome da sala, tipo de acesso do dispositivo e modelo do dispositivo, e também suporta a filtragem por dimensão do dispositivo. **{local_count}** dispositivos foram filtrados.\\r\\n### Modo de controle\\r\\n- Auto: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos através dele para obter controle local. Caso não haja, tentará enviar comandos através da função de controle LAN da Xiaomi. Somente se as condições anteriores não forem atendidas, o controle será feito pela nuvem.\\r\\n- Nuvem: Todos os comandos de controle são enviados pela nuvem.\",\n                \"data\": {\n                    \"home_infos\": \"Importar dispositivos da casa\",\n                    \"devices_filter\": \"Filtrar Dispositivos\",\n                    \"ctrl_mode\": \"Modo de controle\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrar Dispositivos\",\n                \"description\": \"## Instruções de Uso\\r\\nSuporta a filtragem de dispositivos por nome da sala, tipo de acesso do dispositivo e modelo do dispositivo, e também suporta a filtragem por dimensão do dispositivo. A lógica de filtragem é a seguinte:\\r\\n- Primeiro, de acordo com a lógica estatística, obtenha a união ou interseção de todos os itens incluídos, depois obtenha a interseção ou união dos itens excluídos, e finalmente subtraia o [resultado do resumo incluído] do [resultado do resumo excluído] para obter o [resultado do filtro].\\r\\n- Se nenhum item incluído for selecionado, significa que todos estão incluídos.\\r\\n### Modo de Filtragem\\r\\n- Excluir: Remover itens indesejados.\\r\\n- Incluir: Incluir itens desejados.\\r\\n### Lógica Estatística\\r\\n- Lógica E: Pegue a interseção de todos os itens no mesmo modo.\\r\\n- Lógica OU: Pegue a união de todos os itens no mesmo modo.\\r\\n\\r\\nVocê também pode ir para a página [Configuração > Atualizar Lista de Dispositivos] do item de integração, marcar [Filtrar Dispositivos] para refiltrar.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrar por Sala\",\n                    \"room_list\": \"Salas\",\n                    \"type_filter_mode\": \"Filtrar por Tipo de Dispositivo\",\n                    \"type_list\": \"Tipos de Dispositivo\",\n                    \"model_filter_mode\": \"Filtrar por Modelo de Dispositivo\",\n                    \"model_list\": \"Modelos de Dispositivo\",\n                    \"devices_filter_mode\": \"Filtrar Dispositivos\",\n                    \"device_list\": \"Lista de Dispositivos\",\n                    \"statistics_logic\": \"Lógica de Estatísticas\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Atualizar Regras de Transformação de Entidades\",\n                \"description\": \"## Instruções de Uso\\r\\n- Atualiza as informações das entidades dos dispositivos na instância atual da integração, incluindo configuração multilíngue MIoT-Spec-V2, tradução de booleanos e filtragem de modelos.\\r\\n- **Aviso**: Esta é uma configuração global e atualizará o cache local. Ela afetará todas as instâncias da integração.\\r\\n- Esta operação levará algum tempo, seja paciente. Marque \\\"Confirmar atualização\\\" e clique em \\\"Avançar\\\" para iniciar a atualização de **{urn_count}** regras, caso contrário, pule.\\r\\n\",\n                \"data\": {\n                    \"confirm\": \"Confirmar a atualização\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Atualizar configuração de controle LAN\",\n                \"description\": \"## Instruções de Uso\\r\\nAtualize as configurações para a função de controle LAN da Xiaomi. Quando a nuvem e o gateway central não puderem controlar os dispositivos, a integração tentará controlá-los através da LAN. Se nenhuma placa de rede for selecionada, o controle LAN não terá efeito.\\r\\n- Somente dispositivos compatíveis com MIoT-Spec-V2 conectados via IP na LAN são suportados. Alguns dispositivos produzidos antes de 2020 podem não suportar controle LAN ou assinatura LAN.\\r\\n- Selecione a(s) placa(s) de rede que estão na mesma rede que os dispositivos a serem controlados. É possível selecionar várias placas. Se o Home Assistant tiver duas ou mais conexões com a rede local devido a múltiplas placas, recomenda-se selecionar a que tiver melhor conexão de rede. Caso contrário, isso pode afetar o desempenho.\\r\\n- Se houver dispositivos terminais (alto-falantes Xiaomi com tela, celular, etc.) na LAN que suportem controle local, habilitar a assinatura LAN pode causar comportamentos anormais em automações e dispositivos locais.\\r\\n- **Aviso**: Esta é uma configuração global. Afetará todas as instâncias da integração. Use com cautela.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Selecione a placa de rede a ser usada\",\n                    \"enable_subscribe\": \"Habilitar assinatura LAN\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuração de Detecção de Rede\",\n                \"description\": \"## Introdução ao Uso\\r\\n### Endereço de Detecção de Rede\\r\\nUsado para verificar se a rede está funcionando corretamente. Se não for definido, o endereço padrão do sistema será usado. Se a verificação do endereço padrão falhar, você pode tentar inserir um endereço personalizado.\\r\\n- Você pode inserir vários endereços de detecção, separados por vírgulas, como `8.8.8.8,https://www.bing.com`\\r\\n- Se for um endereço IP, a detecção será feita via ping. Se for um endereço HTTP(s), a detecção será feita via solicitação HTTP GET.\\r\\n- Se você deseja restaurar o endereço de detecção padrão do sistema, insira uma vírgula `,` e clique em 'Próximo'.\\r\\n- **Esta configuração é global e as alterações afetarão outras instâncias de integração. Modifique com cautela.**\\r\\n### Verificar Dependências de Rede\\r\\nVerifique uma por uma as seguintes dependências de rede para ver se são acessíveis. Se os endereços relacionados não forem acessíveis, isso causará problemas de integração.\\r\\n- Endereço de Autenticação OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Endereço da API HTTP da Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Endereço da API SPEC da Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Endereço do Broker MQTT da Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Endereço de Detecção de Rede\",\n                    \"check_network_deps\": \"Verificar Dependências de Rede\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Confirmar Configuração\",\n                \"description\": \"Olá **{nick_name}**, confirme as informações da configuração mais recente e depois clique em ENVIAR.\\r\\nA integração será recarregada com a configuração atualizada.\\r\\n\\r\\nIdioma da Integração:\\t{lang_new}\\r\\nApelido:\\t{nick_name_new}\\r\\nModo de depuração para ação:\\t{action_debug}\\r\\nOcultar entidades não padrão criadas:\\t{hide_non_standard_entities}\\r\\nLargura da área cega da cortina:\\t{cover_width_new}\\r\\nExibir notificações de mudança de status do dispositivo:\\t{display_devices_changed_notify}\\r\\nAlterações de Dispositivos:\\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\\r\\nAlteração nas Regras de Transformação:\\tUm total de **{trans_rules_count}** regras, e **{trans_rules_count_success}** regras atualizadas\",\n                \"data\": {\n                    \"confirm\": \"Confirmar a mudança\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Por favor, clique aqui para relogar{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Não autenticado. Por favor, clique no link de autenticação para autenticar sua identidade.\",\n            \"get_token_error\": \"Falha ao obter as informações de autorização de login (token OAuth).\",\n            \"get_homeinfo_error\": \"Falha ao obter as informações da casa.\",\n            \"get_cert_error\": \"Falha ao obter o certificado do gateway central.\",\n            \"no_devices\": \"Não há dispositivos na casa selecionada. Por favor, selecione uma casa com dispositivos e continue.\",\n            \"no_filter_devices\": \"Os dispositivos filtrados estão vazios. Por favor, selecione critérios de filtro válidos e continue.\",\n            \"no_family_selected\": \"Nenhuma casa selecionada.\",\n            \"no_central_device\": \"[Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada atende a esse requisito.\",\n            \"mdns_discovery_error\": \"Exceção no serviço de descoberta de dispositivos locais.\",\n            \"update_config_error\": \"Falha ao atualizar as informações de configuração.\",\n            \"not_confirm\": \"As alterações não foram confirmadas. Por favor, confirme a mudança antes de enviar.\",\n            \"invalid_network_addr\": \"Endereço IP ou HTTP inválido detectado, por favor insira um endereço válido.\",\n            \"invalid_ip_addr\": \"Endereço IP inacessível detectado, por favor insira um endereço IP válido.\",\n            \"invalid_http_addr\": \"Endereço HTTP inacessível detectado, por favor insira um endereço HTTP válido.\",\n            \"invalid_default_addr\": \"O endereço de detecção de rede padrão está inacessível, por favor verifique a configuração da rede ou use um endereço de detecção de rede personalizado.\",\n            \"unreachable_oauth2_host\": \"Não é possível acessar o endereço de autenticação OAuth2, verifique a configuração da rede.\",\n            \"unreachable_http_host\": \"Não é possível acessar o endereço da API HTTP da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_spec_host\": \"Não é possível acessar o endereço da API SPEC da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_mqtt_broker\": \"Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Configuração falhou. A conexão de rede está anormal. Verifique a configuração da rede do equipamento.\",\n            \"options_flow_error\": \"Erro na reconfiguração da integração: {error}\",\n            \"re_add\": \"Por favor, adicione novamente a integração. Mensagem de erro: {error}\",\n            \"storage_error\": \"Exceção no módulo de armazenamento da integração. Tente novamente ou readicione a integração: {error}\",\n            \"inconsistent_account\": \"As informações da conta são inconsistentes. Por favor, faça login com a conta correta.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/pt.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Integração Xiaomi Home\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Aviso de Risco\",\n                \"description\": \"1. As informações da sua conta Xiaomi e dos seus dispositivos serão armazenadas no sistema do Home Assistant. **A Xiaomi não pode garantir a segurança do mecanismo de armazenamento do Home Assistant**. É da sua responsabilidade impedir que a sua informação seja roubada.\\r\\n2. Esta integração é mantida pela comunidade open-source. Podem ocorrer problemas de estabilidade ou outros. Ao encontrar problemas ou falhas nesta integração, **deverá procurar ajuda junto da comunidade open-source, em vez de contactar o apoio ao cliente da Xiaomi**.\\r\\n3. Necessitará de algumas competências técnicas para manter o seu ambiente de operação local. Esta integração não é intuitiva para utilizadores iniciantes.\\r\\n4. Leia o ficheiro README antes de começar.\\n\\n5. Para garantir uma utilização estável da integração e prevenir uso indevido, **esta integração só pode ser utilizada no Home Assistant. Para mais detalhes, consulte a LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Estou ciente dos riscos acima e disposto(a) a assumi-los voluntariamente ao utilizar a integração.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Configuração Básica\",\n                \"description\": \"### Região de Login\\r\\nSelecione a região da sua conta Xiaomi. Pode encontrá-la na aplicação Xiaomi Home > Perfil (menu inferior) > Configurações adicionais > Sobre o Xiaomi Home.\\r\\n### Idioma\\r\\nSelecione o idioma para os nomes de dispositivos e entidades. Algumas frases sem tradução serão apresentadas em inglês.\\r\\n### URL de Redirecionamento OAuth2\\r\\nO endereço de redirecionamento para a autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant deve estar na mesma rede local que o terminal atual (por exemplo, o computador pessoal) e esse terminal deve conseguir aceder à página inicial do Home Assistant através deste endereço. Caso contrário, a autenticação de login pode falhar.\\r\\n### Configuração de Rede Integrada\\r\\nVerifique se a rede local está funcionando corretamente e se os recursos de rede relacionados estão acessíveis. **Recomenda-se selecionar isso ao adicionar pela primeira vez.**\\r\\n### Notas\\r\\n- Para utilizadores com centenas (ou mais) de dispositivos Mi Home, a adição inicial da integração demorará algum tempo. Seja paciente.\\r\\n- Se o Home Assistant estiver a ser executado num ambiente Docker, assegure-se de que o modo de rede do Docker está configurado como host; caso contrário, a funcionalidade de controlo local pode não funcionar corretamente.\\r\\n- A funcionalidade de controlo local da integração tem algumas dependências. Leia cuidadosamente o README.\",\n                \"data\": {\n                    \"cloud_server\": \"Região de Login\",\n                    \"integration_language\": \"Idioma\",\n                    \"oauth_redirect_url\": \"URL de Redirecionamento OAuth2\",\n                    \"network_detect_config\": \"Configuração de Rede Integrada\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuração de Rede Integrada\",\n                \"description\": \"## Introdução ao Uso\\r\\n### Endereço de Detecção de Rede\\r\\nUsado para verificar se a rede está funcionando corretamente. Se não for definido, o endereço padrão do sistema será usado. Se a verificação do endereço padrão falhar, você pode tentar inserir um endereço personalizado.\\r\\n- Você pode inserir vários endereços de detecção, separados por vírgulas, como `8.8.8.8,https://www.bing.com`\\r\\n- Se for um endereço IP, a detecção será feita via ping. Se for um endereço HTTP(s), a detecção será feita via solicitação HTTP GET.\\r\\n- Se você deseja restaurar o endereço de detecção padrão do sistema, insira uma vírgula `,` e clique em 'Próximo'.\\r\\n- **Esta configuração é global e as alterações afetarão outras instâncias de integração. Modifique com cautela.**\\r\\n### Verificar Dependências de Rede\\r\\nVerifique uma por uma as seguintes dependências de rede para ver se são acessíveis. Se os endereços relacionados não forem acessíveis, isso causará problemas de integração.\\r\\n- Endereço de Autenticação OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Endereço da API HTTP da Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Endereço da API SPEC da Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Endereço do Broker MQTT da Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Endereço de Detecção de Rede\",\n                    \"check_network_deps\": \"Verificar Dependências de Rede\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Erro de Login\",\n                \"description\": \"Clique em SEGUINTE para tentar novamente.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Selecionar Família e Dispositivo\",\n                \"description\": \"## Introdução\\r\\n### Importar a Família do Dispositivo\\r\\nA integração adicionará dispositivos da família selecionada.\\r\\n### Modo de Sincronização do Nome da Sala\\r\\nAo sincronizar dispositivos do APP Mi Home para o Home Assistant, a nomeação da área no Home Assistant seguirá as regras abaixo. Observe que o processo de sincronização não alterará as configurações de família e sala no APP Mi Home.\\r\\n- Não sincronizar: O dispositivo não será adicionado a nenhuma área.\\r\\n- Outras opções: A área à qual o dispositivo é adicionado será nomeada de acordo com o nome da família ou da sala no APP Mi Home.\\r\\n### Configurações Avançadas\\r\\nMostrar configurações avançadas para modificar as opções de configuração profissional da integração.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Olá! Por favor, selecione a família à qual você deseja adicionar o dispositivo.\",\n                \"data\": {\n                    \"home_infos\": \"Importar a Família do Dispositivo\",\n                    \"area_name_rule\": \"Modo de Sincronização do Nome da Sala\",\n                    \"advanced_options\": \"Configurações Avançadas\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Opções Avançadas\",\n                \"description\": \"## Introdução\\r\\n### A menos que você entenda claramente o significado das opções abaixo, mantenha as configurações padrão.\\r\\n### Filtrar Dispositivos\\r\\nSuporte para filtrar dispositivos por nome da sala e tipo de dispositivo, bem como filtragem por família.\\r\\n### Modo de Controle\\r\\n- Automático: Quando um gateway central Xiaomi está disponível na rede local, o Home Assistant enviará comandos de controlo de dispositivos através do gateway central para realizar o controlo local. Quando não há gateway central na rede local, tentará enviar comandos de controlo através do protocolo Xiaomi OT para realizar o controlo local. Apenas quando as condições de controlo local acima não são atendidas, os comandos de controlo de dispositivos serão enviados através da nuvem.\\r\\n- Nuvem: Os comandos de controlo são enviados apenas através da nuvem.\\r\\n### Modo de Depuração de Ações\\r\\nPara métodos definidos pelo MIoT-Spec-V2, além de gerar uma entidade de notificação, também será gerada uma entidade de caixa de texto para depuração de controlo de dispositivos.\\r\\n### Ocultar Entidades Geradas Não Padrão\\r\\nOcultar entidades geradas por instâncias MIoT-Spec-V2 não padrão, cujos nomes começam com \\\"*\\\".\\r\\n### Modo de exibição do sensor binário\\r\\nExibe sensores binários no Xiaomi Home como entidade de sensor de texto ou entidade de sensor binário。\\r\\n### Exibir notificações de mudança de status do dispositivo\\r\\nExibir notificações detalhadas de mudança de status do dispositivo, mostrando apenas as notificações selecionadas.\",\n                \"data\": {\n                    \"devices_filter\": \"Filtrar Dispositivos\",\n                    \"ctrl_mode\": \"Modo de Controlo\",\n                    \"action_debug\": \"Modo de Depuração de Ações\",\n                    \"hide_non_standard_entities\": \"Ocultar Entidades Geradas Não Padrão\",\n                    \"display_binary_mode\": \"Modo de exibição do sensor binário\",\n                    \"display_devices_changed_notify\": \"Exibir notificações de mudança de status do dispositivo\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrar Dispositivos\",\n                \"description\": \"## Instruções de Utilização\\r\\nSuporta a filtragem de dispositivos por nome da sala, tipo de acesso do dispositivo e modelo do dispositivo, e também suporta a filtragem por dimensão do dispositivo. A lógica de filtragem é a seguinte:\\r\\n- Primeiro, de acordo com a lógica estatística, obtenha a união ou interseção de todos os itens incluídos, depois obtenha a interseção ou união dos itens excluídos, e finalmente subtraia o [resultado do resumo incluído] do [resultado do resumo excluído] para obter o [resultado do filtro].\\r\\n- Se nenhum item incluído for selecionado, significa que todos estão incluídos.\\r\\n### Modo de Filtragem\\r\\n- Excluir: Remover itens indesejados.\\r\\n- Incluir: Incluir itens desejados.\\r\\n### Lógica Estatística\\r\\n- Lógica E: Pegue a interseção de todos os itens no mesmo modo.\\r\\n- Lógica OU: Pegue a união de todos os itens no mesmo modo.\\r\\n\\r\\nVocê também pode ir para a página [Configuração > Atualizar Lista de Dispositivos] do item de integração, marcar [Filtrar Dispositivos] para refiltrar.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrar por Sala\",\n                    \"room_list\": \"Salas\",\n                    \"type_filter_mode\": \"Filtrar por Tipo de Dispositivo\",\n                    \"type_list\": \"Tipos de Dispositivo\",\n                    \"model_filter_mode\": \"Filtrar por Modelo de Dispositivo\",\n                    \"model_list\": \"Modelos de Dispositivo\",\n                    \"devices_filter_mode\": \"Filtrar Dispositivos\",\n                    \"device_list\": \"Lista de Dispositivos\",\n                    \"statistics_logic\": \"Lógica de Estatísticas\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Clique aqui para iniciar sessão{link_right}\\r\\n(Será automaticamente redirecionado após um login bem-sucedido)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Por favor, leia o aviso de risco.\",\n            \"get_token_error\": \"Não foi possível obter a informação de autorização de login (token OAuth).\",\n            \"get_homeinfo_error\": \"Não foi possível obter a informação da casa.\",\n            \"mdns_discovery_error\": \"Exceção no serviço de descoberta de dispositivos locais.\",\n            \"get_cert_error\": \"Não foi possível obter o certificado do gateway central.\",\n            \"no_family_selected\": \"Nenhuma casa selecionada.\",\n            \"no_devices\": \"Não há dispositivos na casa selecionada. Por favor, selecione uma casa com dispositivos e continue.\",\n            \"no_filter_devices\": \"Os dispositivos filtrados estão vazios. Por favor, selecione critérios de filtro válidos e continue.\",\n            \"no_central_device\": \"O [Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada cumpre este requisito.\",\n            \"invalid_network_addr\": \"Endereço IP ou HTTP inválido detectado, por favor insira um endereço válido.\",\n            \"invalid_ip_addr\": \"Endereço IP inacessível detectado, por favor insira um endereço IP válido.\",\n            \"invalid_http_addr\": \"Endereço HTTP inacessível detectado, por favor insira um endereço HTTP válido.\",\n            \"invalid_default_addr\": \"O endereço de detecção de rede padrão está inacessível, por favor verifique a configuração da rede ou use um endereço de detecção de rede personalizado.\",\n            \"unreachable_oauth2_host\": \"Não é possível acessar o endereço de autenticação OAuth2, verifique a configuração da rede.\",\n            \"unreachable_http_host\": \"Não é possível acessar o endereço da API HTTP da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_spec_host\": \"Não é possível acessar o endereço da API SPEC da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_mqtt_broker\": \"Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Não foi possível obter o UUID do Home Assistant.\",\n            \"network_connect_error\": \"A configuração falhou. A ligação de rede é anormal. Verifique a configuração de rede do equipamento.\",\n            \"already_configured\": \"A configuração para este utilizador já foi concluída. Vá à página de integrações e clique em CONFIGURAR para efetuar alterações.\",\n            \"invalid_auth_info\": \"A informação de autenticação expirou. Vá à página de integrações e clique em CONFIGURAR para reautenticar.\",\n            \"config_flow_error\": \"Erro na configuração da integração: {error}.\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Configuração de Autenticação\",\n                \"description\": \"A informação de autenticação local expirou. Por favor, reinicie o processo de autenticação.\\r\\n### Região de Login Atual: {cloud_server}\\r\\n### URL de Redirecionamento OAuth2\\r\\nO endereço de redirecionamento para a autenticação OAuth2 é **[http://homeassistant.local:8123](http://homeassistant.local:8123)**. O Home Assistant deve estar na mesma rede local que o terminal atual (por exemplo, o computador) e esse terminal deve conseguir aceder à página inicial do Home Assistant através deste endereço. Caso contrário, a autenticação poderá falhar.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"URL de Redirecionamento OAuth2\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Ocorreu um erro durante o login.\",\n                \"description\": \"Clique em SEGUINTE para tentar novamente.\"\n            },\n            \"config_options\": {\n                \"title\": \"Opções de Configuração\",\n                \"description\": \"### Olá, {nick_name}\\r\\n\\r\\nID Xiaomi: {uid}\\r\\nRegião de Login Atual: {cloud_server}\\r\\nID da Instância de Integração: {instance_id}\\r\\n\\r\\nSelecione as opções que pretende configurar e depois clique em SEGUINTE.\",\n                \"data\": {\n                    \"integration_language\": \"Idioma da Integração\",\n                    \"update_user_info\": \"Atualizar informação do utilizador\",\n                    \"update_devices\": \"Atualizar lista de dispositivos\",\n                    \"action_debug\": \"Modo de depuração de ação\",\n                    \"hide_non_standard_entities\": \"Ocultar entidades não padrão\",\n                    \"display_binary_mode\": \"Modo de exibição do sensor binário\",\n                    \"display_devices_changed_notify\": \"Exibir notificações de mudança de status do dispositivo\",\n                    \"update_trans_rules\": \"Atualizar regras de conversão de entidades\",\n                    \"update_lan_ctrl_config\": \"Atualizar configuração de controlo LAN\",\n                    \"network_detect_config\": \"Configuração de Rede Integrada\",\n                    \"cover_dead_zone_width\": \"Largura da zona cega da cortina\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Atualizar Alcunha do Utilizador\",\n                \"description\": \"Olá {nick_name}, pode modificar a sua alcunha personalizada abaixo.\",\n                \"data\": {\n                    \"nick_name\": \"Alcunha\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Selecionar novamente a Casa e os Dispositivos\",\n                \"description\": \"## Instruções de Utilização\\r\\n### Importar dispositivos da casa\\r\\nA integração adicionará dispositivos das casas selecionadas.\\r\\n### Filtrar Dispositivos\\r\\nSuporta a filtragem de dispositivos por nome da sala, tipo de acesso do dispositivo e modelo do dispositivo, e também suporta a filtragem por dimensão do dispositivo. **{local_count}** dispositivos foram filtrados.\\r\\n### Modo de Controlo\\r\\n- Automático: Quando houver um gateway central Xiaomi disponível na rede local, o Home Assistant priorizará o envio de comandos através dele para obter controlo local. Se não existir um gateway central, tentará enviar comandos através da função de controlo LAN da Xiaomi. Apenas se estas condições não forem satisfeitas, os comandos serão enviados pela nuvem.\\r\\n- Nuvem: Todos os comandos de controlo são enviados através da nuvem.\",\n                \"data\": {\n                    \"home_infos\": \"Importar dispositivos da casa\",\n                    \"devices_filter\": \"Filtrar Dispositivos\",\n                    \"ctrl_mode\": \"Modo de Controlo\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Filtrar Dispositivos\",\n                \"description\": \"## Instruções de Utilização\\r\\nSuporta a filtragem de dispositivos por nome da sala, tipo de acesso do dispositivo e modelo do dispositivo, e também suporta a filtragem por dimensão do dispositivo. A lógica de filtragem é a seguinte:\\r\\n- Primeiro, de acordo com a lógica estatística, obtenha a união ou interseção de todos os itens incluídos, depois obtenha a interseção ou união dos itens excluídos, e finalmente subtraia o [resultado do resumo incluído] do [resultado do resumo excluído] para obter o [resultado do filtro].\\r\\n- Se nenhum item incluído for selecionado, significa que todos estão incluídos.\\r\\n### Modo de Filtragem\\r\\n- Excluir: Remover itens indesejados.\\r\\n- Incluir: Incluir itens desejados.\\r\\n### Lógica Estatística\\r\\n- Lógica E: Pegue a interseção de todos os itens no mesmo modo.\\r\\n- Lógica OU: Pegue a união de todos os itens no mesmo modo.\\r\\n\\r\\nVocê também pode ir para a página [Configuração > Atualizar Lista de Dispositivos] do item de integração, marcar [Filtrar Dispositivos] para refiltrar.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Filtrar por Sala\",\n                    \"room_list\": \"Salas\",\n                    \"type_filter_mode\": \"Filtrar por Tipo de Dispositivo\",\n                    \"type_list\": \"Tipos de Dispositivo\",\n                    \"model_filter_mode\": \"Filtrar por Modelo de Dispositivo\",\n                    \"model_list\": \"Modelos de Dispositivo\",\n                    \"devices_filter_mode\": \"Filtrar Dispositivos\",\n                    \"device_list\": \"Lista de Dispositivos\",\n                    \"statistics_logic\": \"Lógica de Estatísticas\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Atualizar Regras de Transformação de Entidades\",\n                \"description\": \"## Instruções de Utilização\\r\\n- Atualiza a informação das entidades dos dispositivos na instância atual da integração, incluindo configuração multilingue MIoT-Spec-V2, tradução de booleanos e filtragem de modelos.\\r\\n- **Aviso**: Esta é uma configuração global e atualizará a cache local, afetando todas as instâncias da integração.\\r\\n- Esta operação levará algum tempo, seja paciente. Selecione \\\"Confirmar a atualização\\\" e clique em \\\"SEGUINTE\\\" para iniciar a atualização de **{urn_count}** regras, caso contrário, ignore esta etapa.\\r\\n\",\n                \"data\": {\n                    \"confirm\": \"Confirmar a atualização\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Atualizar Configuração de Controlo LAN\",\n                \"description\": \"## Instruções de Utilização\\r\\nAtualize as configurações para a funcionalidade de controlo LAN da Xiaomi. Quando a nuvem e o gateway central não puderem controlar os dispositivos, a integração tentará controlá-los através da LAN. Se não selecionar nenhuma interface de rede, o controlo LAN não terá efeito.\\r\\n- Apenas dispositivos compatíveis com MIoT-Spec-V2 ligados via IP na LAN são suportados. Alguns dispositivos produzidos antes de 2020 podem não suportar controlo LAN ou subscrição LAN.\\r\\n- Selecione a(s) placa(s) de rede que estejam na mesma rede que os dispositivos a controlar. Pode selecionar várias placas. Se o Home Assistant tiver duas ou mais ligações à rede local devido à seleção de várias placas, é recomendado selecionar a que tiver melhor ligação, caso contrário poderá afetar negativamente o desempenho dos dispositivos.\\r\\n- Se houver dispositivos terminais (colunas Xiaomi com ecrã, telemóveis, etc.) na LAN que suportem controlo local, a ativação da subscrição LAN pode causar anomalias em automações e dispositivos locais.\\r\\n- **Aviso**: Esta é uma configuração global, afetando todas as instâncias da integração. Utilize com cautela.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Selecione a(s) interface(s) de rede a utilizar\",\n                    \"enable_subscribe\": \"Ativar subscrição LAN\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Configuração de Rede Integrada\",\n                \"description\": \"## Introdução ao Uso\\r\\n### Endereço de Detecção de Rede\\r\\nUsado para verificar se a rede está funcionando corretamente. Se não for definido, o endereço padrão do sistema será usado. Se a verificação do endereço padrão falhar, você pode tentar inserir um endereço personalizado.\\r\\n- Você pode inserir vários endereços de detecção, separados por vírgulas, como `8.8.8.8,https://www.bing.com`\\r\\n- Se for um endereço IP, a detecção será feita via ping. Se for um endereço HTTP(s), a detecção será feita via solicitação HTTP GET.\\r\\n- Se você deseja restaurar o endereço de detecção padrão do sistema, insira uma vírgula `,` e clique em 'Próximo'.\\r\\n- **Esta configuração é global e as alterações afetarão outras instâncias de integração. Modifique com cautela.**\\r\\n### Verificar Dependências de Rede\\r\\nVerifique uma por uma as seguintes dependências de rede para ver se são acessíveis. Se os endereços relacionados não forem acessíveis, isso causará problemas de integração.\\r\\n- Endereço de Autenticação OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Endereço da API HTTP da Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Endereço da API SPEC da Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Endereço do Broker MQTT da Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Endereço de Detecção de Rede\",\n                    \"check_network_deps\": \"Verificar Dependências de Rede\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Confirmar Configuração\",\n                \"description\": \"Olá **{nick_name}**, confirme a informação da configuração mais recente e depois clique em SUBMETER.\\r\\nA integração será recarregada com a configuração atualizada.\\r\\n\\r\\nIdioma da Integração:\\t{lang_new}\\r\\nAlcunha:\\t{nick_name_new}\\r\\nModo de depuração de ação:\\t{action_debug}\\r\\nOcultar entidades não padrão:\\t{hide_non_standard_entities}\\r\\nLargura da zona cega da cortina:\\t{cover_width_new}\\r\\nExibir notificações de mudança de status do dispositivo:\\t{display_devices_changed_notify}\\r\\nAlterações aos Dispositivos:\\tAdicionar **{devices_add}** dispositivos, Remover **{devices_remove}** dispositivos\\r\\nAlteração das Regras de Transformação:\\tExistem **{trans_rules_count}** regras no total, com **{trans_rules_count_success}** regras atualizadas\",\n                \"data\": {\n                    \"confirm\": \"Confirmar a alteração\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Por favor, clique aqui para voltar a iniciar sessão{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Não autenticado. Por favor, clique no link de autenticação para confirmar a sua identidade.\",\n            \"get_token_error\": \"Não foi possível obter a informação de autorização de login (token OAuth).\",\n            \"get_homeinfo_error\": \"Não foi possível obter a informação da casa.\",\n            \"get_cert_error\": \"Não foi possível obter o certificado do gateway central.\",\n            \"no_devices\": \"Não há dispositivos na casa selecionada. Por favor, selecione uma casa com dispositivos e continue.\",\n            \"no_filter_devices\": \"Os dispositivos filtrados estão vazios. Por favor, selecione critérios de filtro válidos e continue.\",\n            \"no_family_selected\": \"Nenhuma casa selecionada.\",\n            \"no_central_device\": \"O [Modo Gateway Central] requer um gateway central Xiaomi disponível na rede local onde o Home Assistant está. Verifique se a casa selecionada cumpre este requisito.\",\n            \"mdns_discovery_error\": \"Exceção no serviço de descoberta de dispositivos locais.\",\n            \"update_config_error\": \"Não foi possível atualizar a informação de configuração.\",\n            \"not_confirm\": \"As alterações não foram confirmadas. Por favor, confirme a alteração antes de submeter.\",\n            \"invalid_network_addr\": \"Endereço IP ou HTTP inválido detectado, por favor insira um endereço válido.\",\n            \"invalid_ip_addr\": \"Endereço IP inacessível detectado, por favor insira um endereço IP válido.\",\n            \"invalid_http_addr\": \"Endereço HTTP inacessível detectado, por favor insira um endereço HTTP válido.\",\n            \"invalid_default_addr\": \"O endereço de detecção de rede padrão está inacessível, por favor verifique a configuração da rede ou use um endereço de detecção de rede personalizado.\",\n            \"unreachable_oauth2_host\": \"Não é possível acessar o endereço de autenticação OAuth2, verifique a configuração da rede.\",\n            \"unreachable_http_host\": \"Não é possível acessar o endereço da API HTTP da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_spec_host\": \"Não é possível acessar o endereço da API SPEC da Xiaomi, verifique a configuração da rede.\",\n            \"unreachable_mqtt_broker\": \"Não é possível acessar o endereço do Broker MQTT da Xiaomi, verifique a configuração da rede.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"A configuração falhou. A ligação de rede é anormal. Verifique a configuração da rede do equipamento.\",\n            \"options_flow_error\": \"Erro na reconfiguração da integração: {error}\",\n            \"re_add\": \"Por favor, volte a adicionar a integração. Mensagem de erro: {error}\",\n            \"storage_error\": \"Exceção no módulo de armazenamento da integração. Tente novamente ou volte a adicionar a integração: {error}\",\n            \"inconsistent_account\": \"A informação da conta é inconsistente. Por favor, inicie sessão com a conta correta.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/ru.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Ми Дома Интеграция\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Риск-информирование\",\n                \"description\": \"1. Ваша **информация о пользователе Xiaomi и информация об устройстве** будет храниться в вашей системе Home Assistant. **Xiaomi не может гарантировать безопасность механизма хранения Home Assistant**. Вы несете ответственность за предотвращение кражи вашей информации.\\r\\n2. Эта интеграция поддерживается сообществом Open Source и может иметь проблемы с устойчивостью или другими проблемами, если вы столкнулись с проблемами при использовании этой интеграции, **вы должны обратиться за помощью к сообществу Open Source, а не к службе поддержки Xiaomi**.\\r\\n3. Вам нужно иметь определенные технические навыки для поддержания вашей локальной рабочей среды, эта интеграция не является дружественной для новичков.\\r\\n4. Перед использованием этой интеграции, **внимательно прочитайте README**.\\r\\n5. Чтобы обеспечить стабильное использование интеграции и предотвратить злоупотребление интерфейсом, **эта интеграция разрешена только для использования в Home Assistant. Для получения подробной информации, пожалуйста, обратитесь к LICENSE**.\",\n                \"data\": {\n                    \"eula\": \"Я ознакомлен с вышеуказанными рисками и добровольно несу связанные с использованием интеграции риски.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Основные настройки\",\n                \"description\": \"### Регион входа в систему\\r\\nВыберите регион, в котором находится ваша учетная запись Xiaomi. Вы можете узнать об этом в `Xiaomi Home> Мой (в нижнем меню)> Дополнительные настройки> О Xiaomi Home`.\\r\\n### Язык\\r\\nВыберите язык, используемый для имен устройств и сущностей. Части предложений, которые не имеют перевода, будут отображаться на английском языке.\\r\\n### Адрес перенаправления для аутентификации OAuth2\\r\\nАдрес перенаправления для аутентификации OAuth2 - ** [http: //homeassistant.local: 8123] (http: //homeassistant.local: 8123) **, Home Assistant должен находиться в одной локальной сети с текущим терминалом (например, персональный компьютер), и терминал должен иметь доступ к домашней странице Home Assistant по этому адресу, в противном случае аутентификация входа может завершиться неудачно.\\r\\n### Интегрированная Сетевая Конфигурация\\r\\nПроверьте, нормально ли функционирует локальная сеть и доступны ли связанные сетевые ресурсы. **Рекомендуется выбрать это при первом добавлении.**\\r\\n### Примечание\\r\\n- Для пользователей с сотнями или более устройств Mi Home первоначальное добавление интеграции займет некоторое время. Пожалуйста, будьте терпеливы.\\r\\n- Если Home Assistant работает в среде Docker, убедитесь, что сетевой режим Docker установлен на host, иначе функция локального управления может работать неправильно.\\r\\n- Функция локального управления интеграции имеет некоторые зависимости. Пожалуйста, внимательно прочитайте README.\",\n                \"data\": {\n                    \"cloud_server\": \"Регион входа в систему\",\n                    \"integration_language\": \"Язык\",\n                    \"oauth_redirect_url\": \"Адрес перенаправления для аутентификации OAuth2\",\n                    \"network_detect_config\": \"Интегрированная Сетевая Конфигурация\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Интегрированная Сетевая Конфигурация\",\n                \"description\": \"## Введение в Использование\\r\\n### Адрес Обнаружения Сети\\r\\nИспользуется для проверки работоспособности сети. Если не задано, будет использоваться адрес по умолчанию. Если проверка адреса по умолчанию не удалась, вы можете попробовать ввести пользовательский адрес.\\r\\n- Вы можете ввести несколько адресов для проверки, разделенных запятыми, например `8.8.8.8,https://www.bing.com`\\r\\n- Если это IP-адрес, проверка будет выполняться с помощью ping. Если это HTTP(s)-адрес, проверка будет выполняться с помощью HTTP GET запроса.\\r\\n- Если вы хотите восстановить адрес обнаружения по умолчанию, введите запятую `,` и нажмите 'Далее'.\\r\\n- **Эта конфигурация является глобальной, и изменения повлияют на другие экземпляры интеграции. Пожалуйста, изменяйте с осторожностью.**\\r\\n### Проверка Сетевых Зависимостей\\r\\nПроверьте поочередно следующие сетевые зависимости, чтобы убедиться, что они доступны. Если соответствующие адреса недоступны, это приведет к проблемам с интеграцией.\\r\\n- Адрес аутентификации OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Адрес HTTP API Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Адрес SPEC API Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Адрес MQTT брокера Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Адрес Обнаружения Сети\",\n                    \"check_network_deps\": \"Проверка Сетевых Зависимостей\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Ошибка входа в систему\",\n                \"description\": \"Нажмите кнопку «Далее», чтобы повторить попытку\"\n            },\n            \"homes_select\": {\n                \"title\": \"Выберите семью и устройство\",\n                \"description\": \"## Введение\\r\\n### Импорт семьи устройства\\r\\nИнтеграция добавит устройства из выбранной семьи.\\r\\n### Режим синхронизации имени комнаты\\r\\nПри синхронизации устройств из приложения Mi Home с Home Assistant, название области в Home Assistant будет следовать следующим правилам. Обратите внимание, что процесс синхронизации не изменит настройки семьи и комнаты в приложении Mi Home.\\r\\n- Не синхронизировать: Устройство не будет добавлено ни в одну область.\\r\\n- Другие варианты: Область, в которую добавляется устройство, будет названа в честь имени семьи или комнаты в приложении Mi Home.\\r\\n### Расширенные настройки\\r\\nПоказать расширенные настройки для изменения профессиональных параметров конфигурации интеграции.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Здравствуйте! Пожалуйста, выберите семью, в которую вы хотите добавить устройство.\",\n                \"data\": {\n                    \"home_infos\": \"Импорт семьи устройства\",\n                    \"area_name_rule\": \"Режим синхронизации имени комнаты\",\n                    \"advanced_options\": \"Расширенные настройки\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Расширенные настройки\",\n                \"description\": \"## Введение\\r\\n### Если вы не очень хорошо понимаете значение следующих параметров, оставьте их по умолчанию.\\r\\n### Фильтрация устройств\\r\\nПоддерживает фильтрацию устройств по названию комнаты и типу устройства, а также фильтрацию по уровню устройства.\\r\\n### Режим управления\\r\\n- Автоматически: при наличии доступного центрального шлюза Xiaomi в локальной сети Home Assistant Home Assistant будет отправлять команды управления устройствами через центральный шлюз для локального управления. Если центрального шлюза нет в локальной сети, Home Assistant попытается отправить команды управления устройствами через протокол OT Xiaomi для локального управления. Только если вышеуказанные условия локального управления не выполняются, команды управления устройствами будут отправляться через облако.\\r\\n- Облако: команды управления отправляются только через облако.\\r\\n### Режим отладки действий\\r\\nДля методов, определенных устройством MIoT-Spec-V2, помимо создания уведомления, будет создана сущность текстового поля, которую вы можете использовать для отправки команд управления устройством во время отладки.\\r\\n### Скрыть нестандартные сущности\\r\\nСкрыть сущности, созданные нестандартными экземплярами MIoT-Spec-V2, имена которых начинаются с «*».\\r\\n### Режим отображения бинарного датчика\\r\\nОтображает бинарные датчики в Xiaomi Home как сущность текстового датчика или сущность бинарного датчика。\\r\\n### Отображать уведомления о изменении состояния устройства\\r\\nОтображать подробные уведомления о изменении состояния устройства, показывая только выбранные уведомления.\",\n                \"data\": {\n                    \"devices_filter\": \"Фильтрация устройств\",\n                    \"ctrl_mode\": \"Режим управления\",\n                    \"action_debug\": \"Режим отладки действий\",\n                    \"hide_non_standard_entities\": \"Скрыть нестандартные сущности\",\n                    \"display_binary_mode\": \"Режим отображения бинарного датчика\",\n                    \"display_devices_changed_notify\": \"Отображать уведомления о изменении состояния устройства\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Фильтрация устройств\",\n                \"description\": \"## Инструкция по использованию\\r\\nПоддерживает фильтрацию устройств по названию комнаты, типу доступа устройства и модели устройства, а также поддерживает фильтрацию по размеру устройства. Логика фильтрации следующая:\\r\\n- Сначала, согласно статистической логике, получите объединение или пересечение всех включенных элементов, затем получите пересечение или объединение исключенных элементов, и, наконец, вычтите [включенный итоговый результат] из [исключенного итогового результата], чтобы получить [результат фильтрации].\\r\\n- Если не выбраны включенные элементы, это означает, что все включены.\\r\\n### Режим фильтрации\\r\\n- Исключить: Удалить ненужные элементы.\\r\\n- Включить: Включить нужные элементы.\\r\\n### Статистическая логика\\r\\n- Логика И: Взять пересечение всех элементов в одном режиме.\\r\\n- Логика ИЛИ: Взять объединение всех элементов в одном режиме.\\r\\n\\r\\nВы также можете перейти на страницу [Конфигурация > Обновить список устройств] элемента интеграции, установить флажок [Фильтровать устройства], чтобы повторно отфильтровать.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Фильтрация по комнатам семьи\",\n                    \"room_list\": \"Комнаты семьи\",\n                    \"type_filter_mode\": \"Фильтрация по типу устройства\",\n                    \"type_list\": \"Типы устройств\",\n                    \"model_filter_mode\": \"Фильтрация по модели устройства\",\n                    \"model_list\": \"Модели устройств\",\n                    \"devices_filter_mode\": \"Фильтрация устройств\",\n                    \"device_list\": \"Список устройств\",\n                    \"statistics_logic\": \"Логика статистики\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Пожалуйста, нажмите здесь, чтобы войти в систему{link_right}\\r\\n(После успешного входа вы будете автоматически перенаправлены на следующую страницу)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Пожалуйста, ознакомьтесь с текстом рискового информирования.\",\n            \"get_token_error\": \"Не удалось получить информацию об авторизации входа в систему (OAuth token).\",\n            \"get_homeinfo_error\": \"Не удалось получить информацию о домашнем устройстве.\",\n            \"mdns_discovery_error\": \"Сервис обнаружения локальных устройств недоступен.\",\n            \"get_cert_error\": \"Не удалось получить сертификат центрального шлюза.\",\n            \"no_family_selected\": \"Не выбрана домашняя сеть.\",\n            \"no_devices\": \"В выбранном доме нет устройств. Пожалуйста, выберите дом с устройствами и продолжите.\",\n            \"no_filter_devices\": \"Список устройств после фильтрации пуст. Пожалуйста, выберите действительные условия фильтрации и продолжите.\",\n            \"no_central_device\": \"Для режима центрального шлюза Xiaomi необходимо наличие доступного центрального шлюза Xiaomi в локальной сети Home Assistant. Проверьте, соответствует ли выбранная домашняя сеть этому требованию.\",\n            \"invalid_network_addr\": \"Обнаружен недействительный IP-адрес или HTTP-адрес, пожалуйста, введите действительный адрес.\",\n            \"invalid_ip_addr\": \"Обнаружен недоступный IP-адрес, пожалуйста, введите действительный IP-адрес.\",\n            \"invalid_http_addr\": \"Обнаружен недоступный HTTP-адрес, пожалуйста, введите действительный HTTP-адрес.\",\n            \"invalid_default_addr\": \"Адрес обнаружения сети по умолчанию недоступен, пожалуйста, проверьте конфигурацию сети или используйте пользовательский адрес обнаружения сети.\",\n            \"unreachable_oauth2_host\": \"Не удается подключиться к адресу аутентификации OAuth2, проверьте настройки сети.\",\n            \"unreachable_http_host\": \"Не удается подключиться к адресу HTTP API Xiaomi, проверьте настройки сети.\",\n            \"unreachable_spec_host\": \"Не удается подключиться к адресу SPEC API Xiaomi, проверьте настройки сети.\",\n            \"unreachable_mqtt_broker\": \"Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Не удалось получить UUID Home Assistant.\",\n            \"network_connect_error\": \"Ошибка настройки. Сетевое подключение недоступно. Проверьте настройки сети устройства.\",\n            \"already_configured\": \"Этот пользователь уже настроен. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы изменить настройки.\",\n            \"invalid_auth_info\": \"Информация об авторизации истекла. Перейдите на страницу интеграции и нажмите кнопку «Настроить», чтобы переавторизоваться.\",\n            \"config_flow_error\": \"Ошибка настройки интеграции: {error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Настройка аутентификации\",\n                \"description\": \"Обнаружено, что информация об аутентификации локальной сети устарела, повторите аутентификацию.\\r\\n### Текущий регион входа в систему: {cloud_server}\\r\\n### Адрес перенаправления для аутентификации OAuth2\\r\\nАдрес перенаправления для аутентификации OAuth2 - ** [http: //homeassistant.local: 8123] (http: //homeassistant.local: 8123) **, Home Assistant должен находиться в одной локальной сети с текущим терминалом (например, персональный компьютер), и терминал должен иметь доступ к домашней странице Home Assistant по этому адресу, в противном случае аутентификация входа может завершиться неудачно.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"Адрес перенаправления для аутентификации OAuth2\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Ошибка входа в систему\",\n                \"description\": \"Нажмите кнопку «Далее», чтобы повторить попытку\"\n            },\n            \"config_options\": {\n                \"title\": \"Параметры настройки\",\n                \"description\": \"### {nick_name} Здравствуйте!\\r\\n\\r\\nID учетной записи Xiaomi: {uid}\\r\\nТекущий регион входа в систему: {cloud_server}\\r\\nID экземпляра интеграции: {instance_id}\\r\\n\\r\\nВыберите параметры, которые нужно настроить заново, а затем нажмите «Далее».\",\n                \"data\": {\n                    \"integration_language\": \"Язык интеграции\",\n                    \"update_user_info\": \"Обновить информацию о пользователе\",\n                    \"update_devices\": \"Обновить список устройств\",\n                    \"action_debug\": \"Режим отладки Action\",\n                    \"hide_non_standard_entities\": \"Скрыть нестандартные сущности\",\n                    \"display_binary_mode\": \"Режим отображения бинарного датчика\",\n                    \"display_devices_changed_notify\": \"Отображать уведомления о изменении состояния устройства\",\n                    \"update_trans_rules\": \"Обновить правила преобразования сущностей\",\n                    \"update_lan_ctrl_config\": \"Обновить конфигурацию управления LAN\",\n                    \"network_detect_config\": \"Интегрированная Сетевая Конфигурация\",\n                    \"cover_dead_zone_width\": \"Ширина «мертвой зоны» шторы\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Обновить имя пользователя\",\n                \"description\": \"{nick_name} Здравствуйте! Пожалуйста, введите свое имя пользователя ниже.\",\n                \"data\": {\n                    \"nick_name\": \"Имя пользователя\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Выберите дом и устройства\",\n                \"description\": \"## Инструкция по использованию\\r\\n### Импорт домашнего устройства\\r\\nИнтеграция добавит устройства из выбранных домов.\\r\\n### Фильтрация устройств\\r\\nПоддерживает фильтрацию устройств по названию комнаты, типу доступа устройства и модели устройства, а также поддерживает фильтрацию по размеру устройства. Отфильтровано **{local_count}** устройств.\\r\\n### Режим управления\\r\\n- Авто: Когда в локальной сети доступен центральный шлюз Xiaomi, Home Assistant будет в первую очередь отправлять команды управления устройствами через центральный шлюз для достижения локализованного управления. Если в локальной сети нет центрального шлюза, он попытается отправить команды управления через протокол Xiaomi OT для достижения локализованного управления. Только если вышеуказанные условия локализованного управления не выполняются, команды управления устройствами будут отправляться через облако.\\r\\n- Облако: Команды управления отправляются только через облако.\",\n                \"data\": {\n                    \"home_infos\": \"Импорт домашнего устройства\",\n                    \"devices_filter\": \"Фильтрация устройств\",\n                    \"ctrl_mode\": \"Режим управления\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Фильтрация устройств\",\n                \"description\": \"## Инструкция по использованию\\r\\nПоддерживает фильтрацию устройств по названию комнаты, типу доступа устройства и модели устройства, а также поддерживает фильтрацию по размеру устройства. Логика фильтрации следующая:\\r\\n- Сначала, согласно статистической логике, получите объединение или пересечение всех включенных элементов, затем получите пересечение или объединение исключенных элементов, и, наконец, вычтите [включенный итоговый результат] из [исключенного итогового результата], чтобы получить [результат фильтрации].\\r\\n- Если не выбраны включенные элементы, это означает, что все включены.\\r\\n### Режим фильтрации\\r\\n- Исключить: Удалить ненужные элементы.\\r\\n- Включить: Включить нужные элементы.\\r\\n### Статистическая логика\\r\\n- Логика И: Взять пересечение всех элементов в одном режиме.\\r\\n- Логика ИЛИ: Взять объединение всех элементов в одном режиме.\\r\\n\\r\\nВы также можете перейти на страницу [Конфигурация > Обновить список устройств] элемента интеграции, установить флажок [Фильтровать устройства], чтобы повторно отфильтровать.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Фильтрация по комнатам семьи\",\n                    \"room_list\": \"Комнаты семьи\",\n                    \"type_filter_mode\": \"Фильтрация по типу устройства\",\n                    \"type_list\": \"Типы устройств\",\n                    \"model_filter_mode\": \"Фильтрация по модели устройства\",\n                    \"model_list\": \"Модели устройств\",\n                    \"devices_filter_mode\": \"Фильтрация устройств\",\n                    \"device_list\": \"Список устройств\",\n                    \"statistics_logic\": \"Логика статистики\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Обновить правила преобразования сущностей\",\n                \"description\": \"## Инструкция по использованию\\r\\n- Обновите информацию об объектах устройств в текущем экземпляре интеграции, включая многоязычную конфигурацию SPEC, булевый перевод SPEC и фильтрацию моделей SPEC.\\r\\n- **Предупреждение: Эта конфигурация является глобальной конфигурацией** и напрямую обновит локальный кэш. Если в других экземплярах интеграции есть устройства той же модели, соответствующие экземпляры также будут обновлены после перезагрузки.\\r\\n- Эта операция займет некоторое время, пожалуйста, будьте терпеливы. Установите флажок \\\"Подтвердить обновление\\\" и нажмите \\\"Далее\\\", чтобы начать обновление **{urn_count}** правил, иначе пропустите обновление.\",\n                \"data\": {\n                    \"confirm\": \"Подтвердить обновление\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"Обновить конфигурацию управления LAN\",\n                \"description\": \"## Инструкция по использованию\\r\\nОбновите информацию о конфигурации для **LAN-управления устройствами Xiaomi Home**. Когда облако и центральный шлюз не могут управлять устройствами, интеграция попытается управлять устройствами через LAN; если сетевая карта не выбрана, управление через LAN не будет включено.\\r\\n- В настоящее время поддерживаются только устройства WiFi **SPEC v2** в локальной сети. Некоторые старые устройства могут не поддерживать управление или синхронизацию свойств.\\r\\n- Пожалуйста, выберите сетевую карту(и) в той же сети, что и устройства (поддерживается множественный выбор). Если выбранная сетевая карта имеет два или более соединений в одной сети, рекомендуется выбрать ту, которая имеет наилучшее сетевое соединение, иначе это может **повлиять на нормальное использование устройств**.\\r\\n- **Если в локальной сети есть терминальные устройства (шлюзы, мобильные телефоны и т. д.), поддерживающие локальное управление, включение подписки на LAN может вызвать локальную автоматизацию или аномалии устройств. Пожалуйста, используйте с осторожностью**.\\r\\n- **Предупреждение: Эта конфигурация является глобальной, и изменения повлияют на другие экземпляры интеграции. Пожалуйста, изменяйте с осторожностью**.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Пожалуйста, выберите сетевую карту для использования\",\n                    \"enable_subscribe\": \"Включить подписку LAN\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Интегрированная Сетевая Конфигурация\",\n                \"description\": \"## Введение в Использование\\r\\n### Адрес Обнаружения Сети\\r\\nИспользуется для проверки работоспособности сети. Если не задано, будет использоваться адрес по умолчанию. Если проверка адреса по умолчанию не удалась, вы можете попробовать ввести пользовательский адрес.\\r\\n- Вы можете ввести несколько адресов для проверки, разделенных запятыми, например `8.8.8.8,https://www.bing.com`\\r\\n- Если это IP-адрес, проверка будет выполняться с помощью ping. Если это HTTP(s)-адрес, проверка будет выполняться с помощью HTTP GET запроса.\\r\\n- Если вы хотите восстановить адрес обнаружения по умолчанию, введите запятую `,` и нажмите 'Далее'.\\r\\n- **Эта конфигурация является глобальной, и изменения повлияют на другие экземпляры интеграции. Пожалуйста, изменяйте с осторожностью.**\\r\\n### Проверка Сетевых Зависимостей\\r\\nПроверьте поочередно следующие сетевые зависимости, чтобы убедиться, что они доступны. Если соответствующие адреса недоступны, это приведет к проблемам с интеграцией.\\r\\n- Адрес аутентификации OAuth2: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Адрес HTTP API Xiaomi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Адрес SPEC API Xiaomi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Адрес MQTT брокера Xiaomi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Адрес Обнаружения Сети\",\n                    \"check_network_deps\": \"Проверка Сетевых Зависимостей\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Подтверждение настройки\",\n                \"description\": \"**{nick_name}** Здравствуйте! Подтвердите последнюю информацию о настройке и нажмите «Отправить». Интеграция будет перезагружена с использованием обновленных настроек.\\r\\n\\r\\nЯзык интеграции:\\t{lang_new}\\r\\nИмя пользователя:\\t{nick_name_new}\\r\\nРежим отладки Action:\\t{action_debug}\\r\\nСкрыть непроизводственные сущности:\\t{hide_non_standard_entities}\\r\\nШирина «мертвой зоны» шторы:\\t{cover_width_new}\\r\\nОтображать уведомления о изменении состояния устройства:\\t{display_devices_changed_notify}\\r\\nИзменение устройства:\\tДобавлено **{devices_add}** устройство, удалено **{devices_remove}** устройства\\r\\nИзменение правил преобразования:\\tВсего **{trans_rules_count}** правил, обновлено **{trans_rules_count_success}** правил\",\n                \"data\": {\n                    \"confirm\": \"Подтвердить изменения\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Пожалуйста, нажмите здесь, чтобы повторно войти в систему{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Пользователь не аутентифицирован. Нажмите ссылку для аутентификации пользователя.\",\n            \"get_token_error\": \"Не удалось получить информацию об авторизации входа (OAuth token).\",\n            \"get_homeinfo_error\": \"Не удалось получить информацию о домашней сети.\",\n            \"get_cert_error\": \"Не удалось получить центральный сертификат.\",\n            \"no_family_selected\": \"Не выбрана семья.\",\n            \"no_devices\": \"В выбранном доме нет устройств. Пожалуйста, выберите дом с устройствами и продолжите.\",\n            \"no_filter_devices\": \"Список устройств после фильтрации пуст. Пожалуйста, выберите действительные условия фильтрации и продолжите.\",\n            \"no_central_device\": \"Для режима центрального шлюза необходим существующий в локальной сети Home Assistant с доступным Xiaomi-шлюзом. Пожалуйста, проверьте, соответствует ли выбранная семья этому требованию.\",\n            \"mdns_discovery_error\": \"Ошибка сервиса поиска локальных устройств.\",\n            \"update_config_error\": \"Не удалось обновить информацию о конфигурации.\",\n            \"not_confirm\": \"Изменение не подтверждено. Пожалуйста, отметьте для подтверждения и отправки.\",\n            \"invalid_network_addr\": \"Обнаружен недействительный IP-адрес или HTTP-адрес, пожалуйста, введите действительный адрес.\",\n            \"invalid_ip_addr\": \"Обнаружен недоступный IP-адрес, пожалуйста, введите действительный IP-адрес.\",\n            \"invalid_http_addr\": \"Обнаружен недоступный HTTP-адрес, пожалуйста, введите действительный HTTP-адрес.\",\n            \"invalid_default_addr\": \"Адрес обнаружения сети по умолчанию недоступен, пожалуйста, проверьте конфигурацию сети или используйте пользовательский адрес обнаружения сети.\",\n            \"unreachable_oauth2_host\": \"Не удается подключиться к адресу аутентификации OAuth2, проверьте настройки сети.\",\n            \"unreachable_http_host\": \"Не удается подключиться к адресу HTTP API Xiaomi, проверьте настройки сети.\",\n            \"unreachable_spec_host\": \"Не удается подключиться к адресу SPEC API Xiaomi, проверьте настройки сети.\",\n            \"unreachable_mqtt_broker\": \"Не удается подключиться к адресу MQTT брокера Xiaomi, проверьте настройки сети.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Ошибка конфигурации. Сбой сетевого подключения. Проверьте настройки сети устройства.\",\n            \"options_flow_error\": \"Ошибка повторной настройки интеграции: {error}\",\n            \"re_add\": \"Пожалуйста, добавьте интеграцию снова. Информация об ошибке: {error}\",\n            \"storage_error\": \"Ошибка хранения интеграции. Пожалуйста, повторите попытку или добавьте интеграцию снова: {error}\",\n            \"inconsistent_account\": \"Информация об аккаунте не соответствует. Пожалуйста, используйте правильный аккаунт для входа.\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/tr.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"Xiaomi Home Entegrasyonu\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"Risk Bildirimi\",\n                \"description\": \"1. Xiaomi kullanıcı bilgileriniz ve cihaz bilgileriniz Home Assistant sisteminde saklanacaktır. **Xiaomi, Home Assistant depolama mekanizmasının güvenliğini garanti edemez**. Bilgilerinizin çalınmasını önlemekten siz sorumlusunuz.\\r\\n2. Bu entegrasyon açık kaynak topluluğu tarafından sürdürülmektedir. Kararlılık sorunları veya diğer sorunlar olabilir. Bu entegrasyonla ilgili sorunlar veya hatalarla karşılaştığınızda, **Xiaomi müşteri hizmetleriyle iletişime geçmek yerine açık kaynak topluluğundan yardım istemelisiniz**.\\r\\n3. Yerel işletim ortamınızı sürdürmek için teknik beceriye ihtiyacınız vardır. Entegrasyon yeni başlayanlar için kullanıcı dostu değildir.\\r\\n4. Lütfen başlamadan önce README dosyasını okuyun.\\n\\n5. Entegrasyonun kararlı kullanımını sağlamak ve arayüz kötüye kullanımını önlemek için, **bu entegrasyonun yalnızca Home Assistant'ta kullanılmasına izin verilir. Ayrıntılar için lütfen LICENSE'a bakın**.\",\n                \"data\": {\n                    \"eula\": \"Yukarıdaki risklerin farkındayım ve entegrasyonun kullanımıyla ilişkili tüm riskleri gönüllü olarak üstlenmeye razıyım.\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"Temel yapılandırma\",\n                \"description\": \"### Giriş Bölgesi\\r\\nXiaomi hesabınızın bölgesini seçin. Bunu Xiaomi Home UYGULAMASI > Profil (alttaki menüde bulunur) > Ek ayarlar > Xiaomi Home Hakkında bölümünde bulabilirsiniz.\\r\\n### Dil\\r\\nCihaz ve varlık adlarının dilini seçin. Çevirisi olmayan bazı cümleler İngilizce olarak görüntülenecektir.\\r\\n### OAuth2 Yönlendirme URL'si\\r\\nOAuth2 kimlik doğrulama yönlendirme adresi **[http://homeassistant.local:8123](http://homeassistant.local:8123)** şeklindedir. Home Assistant'ın mevcut işletim terminali (örn. kişisel bilgisayar) ile aynı yerel ağda olması ve işletim terminalinin bu adres üzerinden Home Assistant ana sayfasına erişebilmesi gerekir. Aksi takdirde giriş kimlik doğrulaması başarısız olabilir.\\r\\n### Entegre Ağ Yapılandırması\\r\\nYerel ağın düzgün çalışıp çalışmadığını ve ilgili ağ kaynaklarının erişilebilir olup olmadığını kontrol edin. **İlk kez eklerken bunu seçmeniz önerilir.**\\r\\n### Not\\r\\n- Yüzlerce veya daha fazla Mi Home cihazına sahip kullanıcılar için, entegrasyonun ilk eklenmesi biraz zaman alacaktır. Lütfen sabırlı olun.\\r\\n- Home Assistant bir Docker ortamında çalışıyorsa, lütfen Docker ağ modunun host olarak ayarlandığından emin olun, aksi takdirde yerel kontrol işlevi düzgün çalışmayabilir.\\r\\n- Entegrasyonun yerel kontrol işlevinin bazı bağımlılıkları vardır. Lütfen README'yi dikkatlice okuyun.\",\n                \"data\": {\n                    \"cloud_server\": \"Giriş Bölgesi\",\n                    \"integration_language\": \"Dil\",\n                    \"oauth_redirect_url\": \"OAuth2 Yönlendirme URL'si\",\n                    \"network_detect_config\": \"Entegre Ağ Yapılandırması\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Entegre Ağ Yapılandırması\",\n                \"description\": \"## Kullanım Tanıtımı\\r\\n### Ağ Algılama Adresi\\r\\nAğın düzgün çalışıp çalışmadığını kontrol etmek için kullanılır. Ayarlanmazsa, sistem varsayılan adresi kullanılacaktır. Varsayılan adres kontrolü başarısız olursa, özel bir adres girmeyi deneyebilirsiniz.\\r\\n- Virgülle ayrılmış birden fazla algılama adresi girebilirsiniz, örneğin `8.8.8.8,https://www.bing.com`\\r\\n- Bir IP adresi ise, algılama ping üzerinden yapılacaktır. Bir HTTP(s) adresi ise, algılama HTTP GET isteği üzerinden yapılacaktır.\\r\\n- Sistem varsayılan algılama adresini geri yüklemek istiyorsanız, lütfen bir virgül `,` girin ve 'İleri'ye tıklayın.\\r\\n- **Bu yapılandırma geneldir ve değişiklikler diğer entegrasyon örneklerini etkileyecektir. Lütfen dikkatli değiştirin.**\\r\\n### Ağ Bağımlılıklarını Kontrol Et\\r\\nAşağıdaki ağ bağımlılıklarının erişilebilir olup olmadığını tek tek kontrol edin. İlgili adresler erişilebilir değilse, entegrasyon sorunlarına neden olacaktır.\\r\\n- OAuth2 Kimlik Doğrulama Adresi: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API Adresi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API Adresi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker Adresi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Ağ Algılama Adresi\",\n                    \"check_network_deps\": \"Ağ Bağımlılıklarını Kontrol Et\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Giriş Hatası\",\n                \"description\": \"Tekrar denemek için İLERİ'ye tıklayın.\"\n            },\n            \"homes_select\": {\n                \"title\": \"Ev ve Cihaz Seçimi\",\n                \"description\": \"## Tanıtım\\r\\n### Cihazın Evini İçe Aktar\\r\\nEntegrasyon, seçilen evden cihazları ekleyecektir.\\r\\n### Oda Adı Senkronizasyon Modu\\r\\nMi Home UYGULAMASINDAN Home Assistant'a cihazlar senkronize edilirken, Home Assistant'taki alanın adlandırılması aşağıdaki kurallara uyacaktır. Senkronizasyon işleminin Mi Home UYGULAMASINDAKI ev ve oda ayarlarını değiştirmeyeceğini unutmayın.\\r\\n- Senkronize etme: Cihaz herhangi bir alana eklenmeyecektir.\\r\\n- Diğer seçenekler: Cihazın eklendiği alan, Mi Home UYGULAMASINDAKI ev veya oda adına göre adlandırılacaktır.\\r\\n### Gelişmiş Ayarlar\\r\\nEntegrasyonun profesyonel yapılandırma seçeneklerini değiştirmek için gelişmiş ayarları gösterin.\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} Merhaba! Lütfen cihaz eklemek istediğiniz evi seçin.\",\n                \"data\": {\n                    \"home_infos\": \"Cihazın Evini İçe Aktar\",\n                    \"area_name_rule\": \"Oda Adı Senkronizasyon Modu\",\n                    \"advanced_options\": \"Gelişmiş Ayarlar\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"Gelişmiş Ayarlar\",\n                \"description\": \"## Tanıtım\\r\\n### Aşağıdaki seçeneklerin anlamını çok net bir şekilde bilmiyorsanız, lütfen varsayılan ayarları koruyun.\\r\\n### Cihazları Filtrele\\r\\nCihazları oda adı ve cihaz türüne göre filtrelemeyi destekler, ayrıca cihaz boyutu filtrelemesini de destekler.\\r\\n### Kontrol Modu\\r\\n- Otomatik: Yerel ağda kullanılabilir bir Xiaomi merkezi hub ağ geçidi olduğunda, Home Assistant cihaz kontrol komutlarını öncelikle merkezi hub ağ geçidi üzerinden göndererek yerel kontrolü sağlar. Yerel ağda merkezi hub ağ geçidi yoksa, Xiaomi OT protokolü üzerinden kontrol komutları göndermeye çalışır. Yalnızca yukarıdaki yerel kontrol koşulları karşılanmadığında, cihaz kontrol komutları bulut üzerinden gönderilir.\\r\\n- Bulut: Tüm kontrol komutları bulut üzerinden gönderilir.\\r\\n### Eylem Hata Ayıklama Modu\\r\\nCihaz MIoT-Spec-V2 tarafından tanımlanan yöntemler için, bildirim varlıkları oluşturmaya ek olarak, bir metin girişi kutusu varlığı da oluşturulacaktır. Hata ayıklama sırasında cihaza kontrol komutları göndermek için bunu kullanabilirsiniz.\\r\\n### Standart Olmayan Oluşturulan Varlıkları Gizle\\r\\nAdı \\\"*\\\" ile başlayan standart olmayan MIoT-Spec-V2 örnekleri tarafından oluşturulan varlıkları gizle.\\r\\n### İkili Sensör Görüntüleme Modu\\r\\nXiaomi Home'daki ikili sensörleri metin sensör varlığı veya ikili sensör varlığı olarak görüntüleyin.\\r\\n### Cihaz Durum Değişikliği Bildirimlerini Göster\\r\\nDetaylı cihaz durum değişikliği bildirimlerini görüntüleyin, yalnızca seçilen bildirimleri gösterin.\",\n                \"data\": {\n                    \"devices_filter\": \"Cihazları Filtrele\",\n                    \"ctrl_mode\": \"Kontrol Modu\",\n                    \"action_debug\": \"Eylem Hata Ayıklama Modu\",\n                    \"hide_non_standard_entities\": \"Standart Olmayan Oluşturulan Varlıkları Gizle\",\n                    \"display_binary_mode\": \"İkili Sensör Görüntüleme Modu\",\n                    \"display_devices_changed_notify\": \"Cihaz Durum Değişikliği Bildirimlerini Göster\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Cihazları Filtrele\",\n                \"description\": \"## Kullanım Talimatları\\r\\nCihazları ev oda adı, cihaz erişim türü ve cihaz modeline göre filtrelemeyi destekler, ayrıca cihaz boyutu filtrelemesini de destekler. Filtreleme mantığı aşağıdaki gibidir:\\r\\n- İlk olarak, istatistik mantığına göre, dahil edilen tüm öğelerin birleşimini veya kesişimini alın, ardından hariç tutulan öğelerin kesişimini veya birleşimini alın ve son olarak [hariç tutulan özet sonucu]ndan [dahil edilen özet sonucu]nu çıkararak [filtre sonucu]nu elde edin.\\r\\n- Dahil edilen öğe seçilmemişse, tümünün dahil edildiği anlamına gelir.\\r\\n### Filtre Modu\\r\\n- Hariç tut: İstenmeyen öğeleri kaldırın.\\r\\n- Dahil et: İstenen öğeleri dahil edin.\\r\\n### İstatistik Mantığı\\r\\n- VE mantığı: Aynı moddaki tüm öğelerin kesişimini alın.\\r\\n- VEYA mantığı: Aynı moddaki tüm öğelerin birleşimini alın.\\r\\n\\r\\nAyrıca entegrasyon öğesinin [Yapılandırma > Cihaz Listesini Güncelle] sayfasına gidip, yeniden filtrelemek için [Cihazları Filtrele]'yi işaretleyebilirsiniz.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Ev Odalarını Filtrele\",\n                    \"room_list\": \"Ev Odaları\",\n                    \"type_filter_mode\": \"Cihaz Bağlantı Türünü Filtrele\",\n                    \"type_list\": \"Cihaz Bağlantı Türü\",\n                    \"model_filter_mode\": \"Cihaz Modelini Filtrele\",\n                    \"model_list\": \"Cihaz Modeli\",\n                    \"devices_filter_mode\": \"Cihazları Filtrele\",\n                    \"device_list\": \"Cihaz Listesi\",\n                    \"statistics_logic\": \"İstatistik Mantığı\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Giriş yapmak için buraya tıklayın{link_right}\\r\\n(Başarılı girişten sonra otomatik olarak bir sonraki sayfaya yönlendirileceksiniz)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"Lütfen risk bildirimini okuyun.\",\n            \"get_token_error\": \"Giriş yetkilendirme bilgileri (OAuth token) alınamadı.\",\n            \"get_homeinfo_error\": \"Ev bilgileri alınamadı.\",\n            \"mdns_discovery_error\": \"Yerel cihaz keşif hizmeti istisnası.\",\n            \"get_cert_error\": \"Merkezi hub ağ geçidi sertifikası alınamadı.\",\n            \"no_family_selected\": \"Ev seçilmedi.\",\n            \"no_devices\": \"Seçilen evde cihaz yok. Lütfen cihazı olan bir ev seçin ve devam edin.\",\n            \"no_filter_devices\": \"Filtrelenmiş cihazlar boş. Lütfen geçerli filtre kriterleri seçin ve devam edin.\",\n            \"no_central_device\": \"[Merkezi Hub Ağ Geçidi Modu], Home Assistant'ın bulunduğu yerel ağda kullanılabilir bir Xiaomi merkezi hub ağ geçidi gerektirir. Lütfen seçilen evin gereksinimi karşılayıp karşılamadığını kontrol edin.\",\n            \"invalid_network_addr\": \"Geçersiz IP adresi veya HTTP adresi algılandı, lütfen geçerli bir adres girin.\",\n            \"invalid_ip_addr\": \"Ulaşılamayan IP adresi algılandı, lütfen geçerli bir IP adresi girin.\",\n            \"invalid_http_addr\": \"Ulaşılamayan HTTP adresi algılandı, lütfen geçerli bir HTTP adresi girin.\",\n            \"invalid_default_addr\": \"Varsayılan ağ algılama adresi ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin veya özel bir ağ algılama adresi kullanın.\",\n            \"unreachable_oauth2_host\": \"OAuth2 kimlik doğrulama adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\",\n            \"unreachable_http_host\": \"Xiaomi HTTP API adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\",\n            \"unreachable_spec_host\": \"Xiaomi SPEC API adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\",\n            \"unreachable_mqtt_broker\": \"Xiaomi MQTT Broker adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"Home Assistant UUID alınamadı.\",\n            \"network_connect_error\": \"Yapılandırma başarısız oldu. Ağ bağlantısı anormal. Lütfen ekipman ağ yapılandırmasını kontrol edin.\",\n            \"already_configured\": \"Bu kullanıcı için yapılandırma zaten tamamlandı. Lütfen entegrasyon sayfasına gidin ve değişiklikler için YAPILANDIRMA düğmesine tıklayın.\",\n            \"invalid_auth_info\": \"Kimlik doğrulama bilgileri süresi doldu. Lütfen entegrasyon sayfasına gidin ve yeniden kimlik doğrulaması yapmak için YAPILANDIRMA düğmesine tıklayın.\",\n            \"config_flow_error\": \"Entegrasyon yapılandırma hatası: {error}.\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"Kimlik Doğrulama Yapılandırması\",\n                \"description\": \"Yerel kimlik doğrulama bilgileri süresi doldu. Lütfen kimlik doğrulama işlemini yeniden başlatın.\\r\\n### Geçerli Giriş Bölgesi: {cloud_server}\\r\\n### OAuth2 Yönlendirme URL'si\\r\\nOAuth2 kimlik doğrulama yönlendirme adresi **[http://homeassistant.local:8123](http://homeassistant.local:8123)** şeklindedir. Home Assistant'ın mevcut işletim terminali (örn. kişisel bilgisayar) ile aynı yerel ağda olması ve işletim terminalinin bu adres üzerinden Home Assistant ana sayfasına erişebilmesi gerekir. Aksi takdirde giriş kimlik doğrulaması başarısız olabilir.\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"OAuth2 Yönlendirme URL'si\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"Giriş sırasında bir hata oluştu.\",\n                \"description\": \"Tekrar denemek için İLERİ'ye tıklayın.\"\n            },\n            \"config_options\": {\n                \"title\": \"Yapılandırma Seçenekleri\",\n                \"description\": \"### Merhaba, {nick_name}\\r\\n\\r\\nXiaomi ID: {uid}\\r\\nGeçerli Giriş Bölgesi: {cloud_server}\\r\\nEntegrasyon Örnek Kimliği: {instance_id}\\r\\n\\r\\nLütfen yapılandırmak istediğiniz seçenekleri seçin, ardından İLERİ'ye tıklayın.\",\n                \"data\": {\n                    \"integration_language\": \"Entegrasyon Dili\",\n                    \"update_user_info\": \"Kullanıcı bilgilerini güncelle\",\n                    \"update_devices\": \"Cihaz listesini güncelle\",\n                    \"action_debug\": \"Eylem için hata ayıklama modu\",\n                    \"hide_non_standard_entities\": \"Standart olmayan oluşturulan varlıkları gizle\",\n                    \"display_binary_mode\": \"İkili Sensör Görüntüleme Modu\",\n                    \"display_devices_changed_notify\": \"Cihaz durum değişikliği bildirimlerini göster\",\n                    \"update_trans_rules\": \"Varlık dönüştürme kurallarını güncelle\",\n                    \"update_lan_ctrl_config\": \"LAN kontrol yapılandırmasını güncelle\",\n                    \"network_detect_config\": \"Entegre ağ yapılandırması\",\n                    \"cover_dead_zone_width\": \"Perde ölü bölge genişliği\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"Kullanıcı Takma Adını Güncelle\",\n                \"description\": \"Merhaba {nick_name}, aşağıda özel takma adınızı değiştirebilirsiniz.\",\n                \"data\": {\n                    \"nick_name\": \"Takma Ad\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"Evi ve Cihazları Yeniden Seç\",\n                \"description\": \"## Kullanım Talimatları\\r\\n### Evden cihazları içe aktar\\r\\nEntegrasyon, seçilen evlerden cihazları ekleyecektir.\\r\\n### Cihazları Filtrele\\r\\nCihazları ev oda adı, cihaz erişim türü ve cihaz modeline göre filtrelemeyi destekler, ayrıca cihaz boyutu filtrelemesini de destekler. **{local_count}** cihaz filtrelendi.\\r\\n### Kontrol modu\\r\\n- Otomatik: Yerel ağda kullanılabilir bir Xiaomi merkezi hub ağ geçidi olduğunda, Home Assistant cihaz kontrol komutlarını öncelikle merkezi hub ağ geçidi üzerinden göndererek yerel kontrolü sağlar. Yerel ağda merkezi hub ağ geçidi yoksa, Xiaomi LAN kontrol işlevi aracılığıyla kontrol komutları göndermeye çalışır. Yalnızca yukarıdaki yerel kontrol koşulları karşılanmadığında, cihaz kontrol komutları bulut üzerinden gönderilir.\\r\\n- Bulut: Tüm kontrol komutları bulut üzerinden gönderilir.\",\n                \"data\": {\n                    \"home_infos\": \"Evden cihazları içe aktar\",\n                    \"devices_filter\": \"Cihazları filtrele\",\n                    \"ctrl_mode\": \"Kontrol modu\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"Cihazları Filtrele\",\n                \"description\": \"## Kullanım Talimatları\\r\\nCihazları ev oda adı, cihaz erişim türü ve cihaz modeline göre filtrelemeyi destekler, ayrıca cihaz boyutu filtrelemesini de destekler. Filtreleme mantığı aşağıdaki gibidir:\\r\\n- İlk olarak, istatistik mantığına göre, dahil edilen tüm öğelerin birleşimini veya kesişimini alın, ardından hariç tutulan öğelerin kesişimini veya birleşimini alın ve son olarak [hariç tutulan özet sonucu]ndan [dahil edilen özet sonucu]nu çıkararak [filtre sonucu]nu elde edin.\\r\\n- Dahil edilen öğe seçilmemişse, tümünün dahil edildiği anlamına gelir.\\r\\n### Filtre Modu\\r\\n- Hariç tut: İstenmeyen öğeleri kaldırın.\\r\\n- Dahil et: İstenen öğeleri dahil edin.\\r\\n### İstatistik Mantığı\\r\\n- VE mantığı: Aynı moddaki tüm öğelerin kesişimini alın.\\r\\n- VEYA mantığı: Aynı moddaki tüm öğelerin birleşimini alın.\\r\\n\\r\\nAyrıca entegrasyon öğesinin [Yapılandırma > Cihaz Listesini Güncelle] sayfasına gidip, yeniden filtrelemek için [Cihazları Filtrele]'yi işaretleyebilirsiniz.\",\n                \"data\": {\n                    \"room_filter_mode\": \"Ev Odalarını Filtrele\",\n                    \"room_list\": \"Ev Odaları\",\n                    \"type_filter_mode\": \"Cihaz Bağlantı Türünü Filtrele\",\n                    \"type_list\": \"Cihaz Bağlantı Türü\",\n                    \"model_filter_mode\": \"Cihaz Modelini Filtrele\",\n                    \"model_list\": \"Cihaz Modeli\",\n                    \"devices_filter_mode\": \"Cihazları Filtrele\",\n                    \"device_list\": \"Cihaz Listesi\",\n                    \"statistics_logic\": \"İstatistik Mantığı\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"Varlık Dönüştürme Kurallarını Güncelle\",\n                \"description\": \"## Kullanım Talimatları\\r\\n- Mevcut entegrasyon örneğindeki cihazların varlık bilgilerini güncelleyin, MIoT-Spec-V2 çok dilli yapılandırma, boolean çeviri ve model filtreleme dahil.\\r\\n- **Uyarı**: Bu genel bir yapılandırmadır ve yerel önbelleği güncelleyecektir. Tüm entegrasyon örneklerini etkileyecektir.\\r\\n- Bu işlem biraz zaman alacaktır, lütfen sabırlı olun. \\\"Güncellemeyi Onayla\\\"yı işaretleyin ve **{urn_count}** kuralı güncellemeye başlamak için \\\"İleri\\\"ye tıklayın, aksi takdirde güncellemeyi atlayın.\",\n                \"data\": {\n                    \"confirm\": \"Güncellemeyi onayla\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"LAN kontrol yapılandırmasını güncelle\",\n                \"description\": \"## Kullanım Talimatları\\r\\nXiaomi LAN kontrol işlevi için yapılandırmaları güncelleyin. Bulut ve merkezi hub ağ geçidi cihazları kontrol edemediğinde, entegrasyon LAN üzerinden cihazları kontrol etmeye çalışacaktır. Hiçbir ağ kartı seçilmezse, LAN kontrol işlevi etkin olmayacaktır.\\r\\n- Yalnızca LAN'daki MIoT-Spec-V2 uyumlu IP cihazları desteklenir. 2020'den önce üretilen bazı cihazlar LAN kontrolünü veya LAN aboneliğini desteklemeyebilir.\\r\\n- Lütfen kontrol edilecek cihazlarla aynı ağdaki ağ kartlarını seçin. Birden fazla ağ kartı seçilebilir. Home Assistant'ın birden fazla ağ kartı seçimi nedeniyle yerel ağa iki veya daha fazla bağlantısı varsa, en iyi ağ bağlantısına sahip olanı seçmeniz önerilir, aksi takdirde cihazlar üzerinde kötü etkisi olabilir.\\r\\n- LAN'da yerel kontrolü destekleyen terminal cihazlar (ekranlı Xiaomi hoparlör, cep telefonu, vb.) varsa, LAN aboneliğini etkinleştirmek yerel otomasyon ve cihaz anormalliklerine neden olabilir.\\r\\n- **Uyarı**: Bu genel bir yapılandırmadır. Tüm entegrasyon örneklerini etkileyecektir. Lütfen dikkatli kullanın.\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"Lütfen kullanılacak ağ kartını seçin\",\n                    \"enable_subscribe\": \"LAN aboneliğini etkinleştir\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"Entegre Ağ Yapılandırması\",\n                \"description\": \"## Kullanım Tanıtımı\\r\\n### Ağ Algılama Adresi\\r\\nAğın düzgün çalışıp çalışmadığını kontrol etmek için kullanılır. Ayarlanmazsa, sistem varsayılan adresi kullanılacaktır. Varsayılan adres kontrolü başarısız olursa, özel bir adres girmeyi deneyebilirsiniz.\\r\\n- Virgülle ayrılmış birden fazla algılama adresi girebilirsiniz, örneğin `8.8.8.8,https://www.bing.com`\\r\\n- Bir IP adresi ise, algılama ping üzerinden yapılacaktır. Bir HTTP(s) adresi ise, algılama HTTP GET isteği üzerinden yapılacaktır.\\r\\n- Sistem varsayılan algılama adresini geri yüklemek istiyorsanız, lütfen bir virgül `,` girin ve 'İleri'ye tıklayın.\\r\\n- **Bu yapılandırma geneldir ve değişiklikler diğer entegrasyon örneklerini etkileyecektir. Lütfen dikkatli değiştirin.**\\r\\n### Ağ Bağımlılıklarını Kontrol Et\\r\\nAşağıdaki ağ bağımlılıklarının erişilebilir olup olmadığını tek tek kontrol edin. İlgili adresler erişilebilir değilse, entegrasyon sorunlarına neden olacaktır.\\r\\n- OAuth2 Kimlik Doğrulama Adresi: `https://account.xiaomi.com/oauth2/authorize`.\\r\\n- Xiaomi HTTP API Adresi: `https://{http_host}/app/v2/ha/oauth/get_token`.\\r\\n- Xiaomi SPEC API Adresi: `https://miot-spec.org/miot-spec-v2/template/list/device`.\\r\\n- Xiaomi MQTT Broker Adresi: `mqtts://{broker_host}`.\",\n                \"data\": {\n                    \"network_detect_addr\": \"Ağ Algılama Adresi\",\n                    \"check_network_deps\": \"Ağ Bağımlılıklarını Kontrol Et\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"Yapılandırmayı Onayla\",\n                \"description\": \"Merhaba **{nick_name}**, lütfen en son yapılandırma bilgilerini onaylayın ve ardından GÖNDER'e tıklayın.\\r\\nEntegrasyon güncellenmiş yapılandırmayla yeniden yüklenecektir.\\r\\n\\r\\nEntegrasyon Dili:\\t{lang_new}\\r\\nTakma Ad:\\t{nick_name_new}\\r\\nEylem için hata ayıklama modu:\\t{action_debug}\\r\\nStandart olmayan oluşturulan varlıkları gizle:\\t{hide_non_standard_entities}\\r\\nPerde ölü bölge genişliği:\\t{cover_width_new}\\r\\nCihaz durum değişikliği bildirimlerini göster:\\t{display_devices_changed_notify}\\r\\nCihaz Değişiklikleri:\\t**{devices_add}** cihaz ekle, **{devices_remove}** cihaz kaldır\\r\\nDönüştürme kuralları değişikliği:\\tToplam **{trans_rules_count}** kural var ve **{trans_rules_count_success}** kural güncellendi\",\n                \"data\": {\n                    \"confirm\": \"Değişikliği onayla\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}Yeniden giriş yapmak için lütfen buraya tıklayın{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"Kimlik doğrulanmadı. Lütfen kullanıcı kimliğini doğrulamak için kimlik doğrulama bağlantısına tıklayın.\",\n            \"get_token_error\": \"Giriş yetkilendirme bilgileri (OAuth token) alınamadı.\",\n            \"get_homeinfo_error\": \"Ev bilgileri alınamadı.\",\n            \"get_cert_error\": \"Merkezi hub ağ geçidi sertifikası alınamadı.\",\n            \"no_devices\": \"Seçilen evde cihaz yok. Lütfen cihazı olan bir ev seçin ve devam edin.\",\n            \"no_filter_devices\": \"Filtrelenmiş cihazlar boş. Lütfen geçerli filtre kriterleri seçin ve devam edin.\",\n            \"no_family_selected\": \"Ev seçilmedi.\",\n            \"no_central_device\": \"[Merkezi Hub Ağ Geçidi Modu], Home Assistant'ın bulunduğu yerel ağda kullanılabilir bir Xiaomi merkezi hub ağ geçidi gerektirir. Lütfen seçilen evin gereksinimi karşılayıp karşılamadığını kontrol edin.\",\n            \"mdns_discovery_error\": \"Yerel cihaz keşif hizmeti istisnası.\",\n            \"update_config_error\": \"Yapılandırma bilgileri güncellenemedi.\",\n            \"not_confirm\": \"Değişiklikler onaylanmadı. Lütfen göndermeden önce değişikliği onaylayın.\",\n            \"invalid_network_addr\": \"Geçersiz IP adresi veya HTTP adresi algılandı, lütfen geçerli bir adres girin.\",\n            \"invalid_ip_addr\": \"Ulaşılamayan IP adresi algılandı, lütfen geçerli bir IP adresi girin.\",\n            \"invalid_http_addr\": \"Ulaşılamayan HTTP adresi algılandı, lütfen geçerli bir HTTP adresi girin.\",\n            \"invalid_default_addr\": \"Varsayılan ağ algılama adresi ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin veya özel bir ağ algılama adresi kullanın.\",\n            \"unreachable_oauth2_host\": \"OAuth2 kimlik doğrulama adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\",\n            \"unreachable_http_host\": \"Xiaomi HTTP API adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\",\n            \"unreachable_spec_host\": \"Xiaomi SPEC API adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\",\n            \"unreachable_mqtt_broker\": \"Xiaomi MQTT Broker adresine ulaşılamıyor, lütfen ağ yapılandırmasını kontrol edin.\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"Yapılandırma başarısız oldu. Ağ bağlantısı anormal. Lütfen ekipman ağ yapılandırmasını kontrol edin.\",\n            \"options_flow_error\": \"Entegrasyon yeniden yapılandırma hatası: {error}\",\n            \"re_add\": \"Lütfen entegrasyonu yeniden ekleyin. Hata mesajı: {error}\",\n            \"storage_error\": \"Entegrasyon depolama modülü istisnası. Lütfen tekrar deneyin veya entegrasyonu yeniden ekleyin: {error}\",\n            \"inconsistent_account\": \"Hesap bilgileri tutarsız. Lütfen doğru hesapla giriş yapın.\"\n        }\n    }\n}\n"
  },
  {
    "path": "custom_components/xiaomi_home/translations/zh-Hans.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"米家集成\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"风险告知\",\n                \"description\": \"1. 您的小米用户信息和设备信息将会存储在您的 Home Assistant 系统中，**小米无法保证 Home Assistant 存储机制的安全性**。您需要负责防止您的信息被窃取。\\r\\n2. 此集成由开源社区维护，可能会出现稳定性问题或其它问题，使用此集成遇到相关问题时，您应当**向开源社区寻求帮助，而不是联系小米客服**。\\r\\n3. 您需要有一定的技术能力来维护您的本地运行环境，此集成对新手用户来说并不友好。\\r\\n4. 在使用此集成前，请仔细阅读README。\\r\\n5. 为了用户能够稳定地使用集成，避免接口被滥用，**此集成仅允许在 Home Assistant 中使用，详情请参考LICENSE**。\",\n                \"data\": {\n                    \"eula\": \"我已悉知以上风险并自愿承担因使用集成所带来的相关风险。\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"基础配置\",\n                \"description\": \"### 登录地区\\r\\n选择小米账号所在的地区。您可以在 `米家APP > 我的（位于底部菜单） > 更多设置 > 关于米家` 中查看。\\r\\n### 语言\\r\\n选择设备及实体名称所用的语言。缺少翻译的部分语句将使用英文显示。\\r\\n### OAuth2 认证跳转地址\\r\\nOAuth2 认证跳转地址为 **[http://homeassistant.local:8123](http://homeassistant.local:8123)**，Home Assistant 需要与当前操作终端（例如，个人电脑）在同一局域网内，且操作终端能通过该地址访问 Home Assistant 首页，否则登录认证可能会失败。\\r\\n### 集成网络配置\\r\\n检测本地网络是否正常，相关网络资源是否可访问。**首次添加时建议勾选。**\\r\\n### 注意事项\\r\\n- 对于数百个及以上米家设备的用户，首次添加集成会耗费一些时间，请耐心等待。\\r\\n- 如果 Home Assistant 运行在docker环境下，请确保docker网络模式为host，否则会导致本地控制功能异常。\\r\\n- 集成本地控制功能存在一些依赖项，请仔细阅读README。\",\n                \"data\": {\n                    \"cloud_server\": \"登录地区\",\n                    \"integration_language\": \"语言\",\n                    \"oauth_redirect_url\": \"认证跳转地址\",\n                    \"network_detect_config\": \"集成网络配置\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"网络检测配置\",\n                \"description\": \"## 使用介绍\\r\\n### 网络检测地址\\r\\n用于检测网络是否正常，未设置时将使用系统默认地址检测。如果默认地址检测异常时，可尝试输入可用的自定义地址检测。\\r\\n- 可输入多个检测地址，地址之间使用`,`号间隔，如`8.8.8.8,https://www.bing.com`\\r\\n- 如果为IP地址，将采用ping方式检测，如果为http(s)地址，将采用 HTTP GET 访问该地址检测。\\r\\n- 如果想恢复系统默认检测地址，请输入`,`号，然后点击'下一步'。\\r\\n- **该配置为全局配置，修改会影响其它集成实例的网络检测，请谨慎修改。**\\r\\n### 检测网络依赖项\\r\\n依次检查下述网络依赖项是否可访问。如果相关地址无法访问，将会导致集成异常。\\r\\n- OAuth2 认证地址：`https://account.xiaomi.com/oauth2/authorize`。\\r\\n- 小米 HTTP API 地址：`https://{http_host}/app/v2/ha/oauth/get_token`。\\r\\n- 小米 SPEC API 地址：`https://miot-spec.org/miot-spec-v2/template/list/device`。\\r\\n- 小米 MQTT Broker 地址：`mqtts://{broker_host}`。\",\n                \"data\": {\n                    \"network_detect_addr\": \"网络检测地址\",\n                    \"check_network_deps\": \"检测网络依赖项\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"登录出现错误\",\n                \"description\": \"点击“下一步”重试\"\n            },\n            \"homes_select\": {\n                \"title\": \"选择家庭与设备\",\n                \"description\": \"## 使用介绍\\r\\n### 导入设备的家庭\\r\\n集成将添加已选中家庭中的设备。\\r\\n### 房间名同步模式\\r\\n将设备从米家APP同步到 Home Assistant 时，设备在 Home Assistant 中所处区域的名称的命名方式将遵循以下规则。注意，设备同步过程不会改变米家APP中家庭和房间的设置。\\r\\n- 不同步：设备不会被添加至任何区域。\\r\\n- 其它选项：设备所添加到的区域以米家APP中的家庭或房间名称命名。\\r\\n### 高级设置选项\\r\\n展示高级设置选项，对集成的专业配置选项进行修改。\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} 您好！请选择您想要添加的设备所处家庭。\",\n                \"data\": {\n                    \"home_infos\": \"导入设备的家庭\",\n                    \"area_name_rule\": \"房间名同步模式\",\n                    \"advanced_options\": \"高级设置选项\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"高级设置选项\",\n                \"description\": \"## 使用介绍\\r\\n### 除非您非常清楚下列选项的含义，否则请保持默认。\\r\\n### 筛选设备\\r\\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备，同时也支持设备维度筛选。\\r\\n### 控制模式\\r\\n- 自动：本地局域网内存在可用的小米中枢网关时， Home Assistant 会优先通过中枢网关发送设备控制指令，以实现本地化控制功能。本地局域网不存在中枢时，会尝试通过小米OT协议发送控制指令，以实现本地化控制功能。只有当上述本地化控制条件不满足时，设备控制指令才会通过云端发送。\\r\\n- 云端：控制指令仅通过云端发送。\\r\\n### Action 调试模式\\r\\n对于设备 MIoT-Spec-V2 定义的方法，在生成通知实体之外，还会生成一个文本输入框实体，您可以在调试时用它向设备发送控制指令。\\r\\n### 隐藏非标准生成实体\\r\\n隐藏名称以“*”开头的非标准 MIoT-Spec-V2 实例生成的实体。\\r\\n### 二进制传感器显示模式\\r\\n将米家中的二进制传感器显示为文本传感器实体或者二进制传感器实体。\\r\\n### 显示设备状态变化通知\\r\\n细化显示设备状态变化通知，只显示勾选的通知消息。\",\n                \"data\": {\n                    \"devices_filter\": \"筛选设备\",\n                    \"ctrl_mode\": \"控制模式\",\n                    \"action_debug\": \"Action 调试模式\",\n                    \"hide_non_standard_entities\": \"隐藏非标准生成实体\",\n                    \"display_binary_mode\": \"二进制传感器显示模式\",\n                    \"display_devices_changed_notify\": \"显示设备状态变化通知\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"筛选设备\",\n                \"description\": \"## 使用介绍\\r\\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备，同时也支持设备维度筛选，筛选逻辑如下：\\r\\n- 会先根据统计逻辑获取所有包含项的并集或者交集，然后再获取排除项的交集或者并集，最后将【包含汇总结果】减去【排除汇总结果】得到【筛选结果】\\r\\n- 如未选择包含项，表示包含全部。\\r\\n### 筛选模式\\r\\n- 排除：移除不需要的项。\\r\\n- 包含：包含需要的项。\\r\\n### 统计逻辑\\r\\n- 与逻辑：取所有同模式筛选项的交集。\\r\\n- 或逻辑：取所有同模式筛选项的并集。\\r\\n\\r\\n您也可以进入集成项的【配置>更新设备列表】页面，勾选【筛选设备】重新筛选。\",\n                \"data\": {\n                    \"room_filter_mode\": \"筛选家庭房间\",\n                    \"room_list\": \"家庭房间\",\n                    \"type_filter_mode\": \"筛选设备接入类型\",\n                    \"type_list\": \"设备接入类型\",\n                    \"model_filter_mode\": \"筛选设备型号\",\n                    \"model_list\": \"设备型号\",\n                    \"devices_filter_mode\": \"筛选设备\",\n                    \"device_list\": \"设备列表\",\n                    \"statistics_logic\": \"统计逻辑\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}请点击此处进行登录{link_right}\\r\\n（登录成功后，将会自动跳转至下一页面）\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"请阅读风险告知文本。\",\n            \"get_token_error\": \"获取登录授权信息（OAuth token）失败。\",\n            \"get_homeinfo_error\": \"获取家庭信息失败。\",\n            \"mdns_discovery_error\": \"本地设备发现服务异常。\",\n            \"get_cert_error\": \"获取中枢证书失败。\",\n            \"no_family_selected\": \"未选择家庭。\",\n            \"no_devices\": \"选择的家庭中没有设备。请选择有设备的家庭，然后继续。\",\n            \"no_filter_devices\": \"筛选设备为空。请选择有效的筛选条件，然后继续。\",\n            \"no_central_device\": \"【中枢网关模式】需要 Home Assistant 所在的局域网中存在可用的小米中枢网关。请检查选择的家庭是否符合该要求。\",\n            \"invalid_network_addr\": \"存在无效的IP地址或者HTTP地址，请输入有效的地址。\",\n            \"invalid_ip_addr\": \"存在无法访问的IP地址，请输入有效的IP地址。\",\n            \"invalid_http_addr\": \"存在无法访问的HTTP地址，请输入有效的HTTP地址。\",\n            \"invalid_default_addr\": \"默认网络检测地址无法访问，请检查网络配置或者使用自定义网络检测地址。\",\n            \"unreachable_oauth2_host\": \"无法访问 OAuth2 认证地址，请检查网络配置。\",\n            \"unreachable_http_host\": \"无法访问小米 HTTP API 地址，请检查网络配置。\",\n            \"unreachable_spec_host\": \"无法访问小米 SPEC API 地址，请检查网络配置。\",\n            \"unreachable_mqtt_broker\": \"无法访问小米 MQTT Broker 地址，请检查网络配置。\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"获取 Home Assistant UUID 失败。\",\n            \"network_connect_error\": \"配置失败。网络连接异常，请检查设备网络配置。\",\n            \"already_configured\": \"该用户已配置完成。请进入集成页面，点击“配置”按钮修改配置。\",\n            \"invalid_auth_info\": \"认证信息已过期。请进入集成页面，点击“配置”按钮重新认证。\",\n            \"config_flow_error\": \"集成配置错误：{error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"认证配置\",\n                \"description\": \"检测到本地认证信息过期，请重新开始认证\\r\\n### 当前登录地区: {cloud_server}\\r\\n### OAuth2 认证跳转地址\\r\\nOAuth2 认证跳转地址为 **[http://homeassistant.local:8123](http://homeassistant.local:8123)**，Home Assistant 需要与当前操作终端（例如，个人电脑）在同一局域网内，且操作终端能通过该地址访问 Home Assistant 首页，否则登录认证可能会失败。\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"认证跳转地址\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"登录出现错误\",\n                \"description\": \"点击“下一步”重试\"\n            },\n            \"config_options\": {\n                \"title\": \"配置选项\",\n                \"description\": \"### {nick_name} 您好！\\r\\n\\r\\n小米账号ID：{uid}\\r\\n当前登录区域：{cloud_server}\\r\\n集成实例ID：{instance_id}\\r\\n\\r\\n请勾选需要重新配置的选项，然后点击“下一步”。\",\n                \"data\": {\n                    \"integration_language\": \"集成语言\",\n                    \"update_user_info\": \"更新用户信息\",\n                    \"update_devices\": \"更新设备列表\",\n                    \"action_debug\": \"Action 调试模式\",\n                    \"hide_non_standard_entities\": \"隐藏非标准生成实体\",\n                    \"display_binary_mode\": \"二进制传感器显示模式\",\n                    \"display_devices_changed_notify\": \"显示设备状态变化通知\",\n                    \"update_trans_rules\": \"更新实体转换规则\",\n                    \"update_lan_ctrl_config\": \"更新局域网控制配置\",\n                    \"network_detect_config\": \"集成网络配置\",\n                    \"cover_dead_zone_width\": \"窗帘盲区宽度\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"更新用户昵称\",\n                \"description\": \"{nick_name} 您好！请在下方修改您的用户昵称。\",\n                \"data\": {\n                    \"nick_name\": \"用户昵称\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"重新选择家庭与设备\",\n                \"description\": \"## 使用介绍\\r\\n### 导入设备的家庭\\r\\n集成将添加已选中家庭中的设备。\\r\\n### 筛选设备\\r\\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备，同时也支持设备维度筛选，已筛选出 **{local_count}** 个设备。\\r\\n### 控制模式\\r\\n- 自动：本地局域网内存在可用的小米中枢网关时， Home Assistant 会优先通过中枢网关发送设备控制指令，以实现本地化控制功能。本地局域网不存在中枢时，会尝试通过小米OT协议发送控制指令，以实现本地化控制功能。只有当上述本地化控制条件不满足时，设备控制指令才会通过云端发送。\\r\\n- 云端：控制指令仅通过云端发送。\",\n                \"data\": {\n                    \"home_infos\": \"导入设备的家庭\",\n                    \"devices_filter\": \"筛选设备\",\n                    \"ctrl_mode\": \"控制模式\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"筛选设备\",\n                \"description\": \"## 使用介绍\\r\\n支持按照家庭房间名称、设备接入类型、设备型号筛选设备，同时也支持设备维度筛选，筛选逻辑如下：\\r\\n- 会先根据统计逻辑获取所有包含项的并集或者交集，然后再获取排除项的交集或者并集，最后将【包含汇总结果】减去【排除汇总结果】得到【筛选结果】\\r\\n- 如未选择包含项，表示包含全部。\\r\\n### 筛选模式\\r\\n- 排除：移除不需要的项。\\r\\n- 包含：包含需要的项。\\r\\n### 统计逻辑\\r\\n- 与逻辑：取所有同模式筛选项的交集。\\r\\n- 或逻辑：取所有同模式筛选项的并集。\\r\\n\\r\\n您也可以进入集成项的【配置>更新设备列表】页面，勾选【筛选设备】重新筛选。\",\n                \"data\": {\n                    \"room_filter_mode\": \"筛选家庭房间\",\n                    \"room_list\": \"家庭房间\",\n                    \"type_filter_mode\": \"筛选设备接入类型\",\n                    \"type_list\": \"设备接入类型\",\n                    \"model_filter_mode\": \"筛选设备型号\",\n                    \"model_list\": \"设备型号\",\n                    \"devices_filter_mode\": \"筛选设备\",\n                    \"device_list\": \"设备列表\",\n                    \"statistics_logic\": \"统计逻辑\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"更新实体转换规则\",\n                \"description\": \"## 使用介绍\\r\\n- 更新当前集成实例中设备的实体信息，包含 MIoT-Spec-V2 多语言配置、布尔值翻译、过滤规则。\\r\\n- **警告**：该配置为全局配置，将会更新本地缓存，会影响所有集成实例。\\r\\n- 该操作会耗费一定时间，请耐心等待，勾选“确认更新”，点击“下一步”开始更新 **{urn_count}** 条规则，否则跳过更新。\",\n                \"data\": {\n                    \"confirm\": \"确认更新\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"更新局域网控制配置\",\n                \"description\": \"## 使用介绍\\r\\n更新小米局域网控制功能的配置信息。当云端和中枢网关均无法控制设备时，集成会尝试通过局域网控制设备。如果未选择网卡，局域网控制将不会生效。\\r\\n- 目前只支持控制局域网内的兼容 MIoT-Spec-V2 的 IP 设备，部分2020年之前生产的旧设备可能不支持局域网控制或者不支持局域网订阅。\\r\\n- 请选择和被控设备同一局域网的网卡（支持多选）。如果选择多个网卡导致 Home Assistant 到同一局域网存在多个连接，建议只保留最优的网络连接，否则可能会影响设备的正常使用。\\r\\n- 如果局域网内存在支持本地控制的终端设备（带屏音箱、手机等），启用局域网订阅可能会导致本地自动化或者设备异常。\\r\\n- **警告**：该配置为全局配置，会影响所有集成实例，请谨慎修改。\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"请选择使用的网卡\",\n                    \"enable_subscribe\": \"启用局域网订阅\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"网络检测配置\",\n                \"description\": \"## 使用介绍\\r\\n### 网络检测地址\\r\\n用于检测网络是否正常，未设置时将使用系统默认地址检测。如果默认地址检测异常时，可尝试输入可用的自定义地址检测。\\r\\n- 可输入多个检测地址，地址之间使用`,`号间隔，如`8.8.8.8,https://www.bing.com`\\r\\n- 如果为IP地址，将采用ping方式检测，如果为http(s)地址，将采用 HTTP GET 访问该地址检测。\\r\\n- 如果想恢复系统默认检测地址，请输入`,`号，然后点击'下一步'。\\r\\n- **该配置为全局配置，修改会影响其它集成实例的网络检测，请谨慎修改。**\\r\\n### 检测网络依赖项\\r\\n依次检查下述网络依赖项是否可访问。如果相关地址无法访问，将会导致集成异常。\\r\\n- OAuth2 认证地址：`https://account.xiaomi.com/oauth2/authorize`。\\r\\n- 小米 HTTP API 地址：`https://{http_host}/app/v2/ha/oauth/get_token`。\\r\\n- 小米 SPEC API 地址：`https://miot-spec.org/miot-spec-v2/template/list/device`。\\r\\n- 小米 MQTT Broker 地址：`mqtts://{broker_host}`。\",\n                \"data\": {\n                    \"network_detect_addr\": \"网络检测地址\",\n                    \"check_network_deps\": \"检测网络依赖项\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"确认配置\",\n                \"description\": \"**{nick_name}** 您好！请确认最新的配置信息，然后点击“提交”。\\r\\n集成将会使用更新后的配置重新载入。\\r\\n\\r\\n集成语言：\\t{lang_new}\\r\\n用户昵称：\\t{nick_name_new}\\r\\nAction 调试模式：\\t{action_debug}\\r\\n隐藏非标准生成实体：\\t{hide_non_standard_entities}\\r\\n窗帘盲区宽度：\\t{cover_width_new}\\r\\n显示设备状态变化通知：\\t{display_devices_changed_notify}\\r\\n设备变化：\\t新增 **{devices_add}** 个设备，移除 **{devices_remove}** 个设备\\r\\n转换规则变化：\\t共条 **{trans_rules_count}** 规则，更新 **{trans_rules_count_success}** 条规则\",\n                \"data\": {\n                    \"confirm\": \"确认修改\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}请点击此处重新登录{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"用户未认证。请点击认证链接以认证用户身份。\",\n            \"get_token_error\": \"获取登录授权信息（OAuth token）失败。\",\n            \"get_homeinfo_error\": \"获取家庭信息失败。\",\n            \"get_cert_error\": \"获取中枢证书失败。\",\n            \"no_family_selected\": \"未选择家庭。\",\n            \"no_devices\": \"选择的家庭中没有设备，请选择有设备的家庭，然后继续。\",\n            \"no_filter_devices\": \"筛选设备为空。请选择有效的筛选条件，然后继续。\",\n            \"no_central_device\": \"【中枢网关模式】需要 Home Assistant 所在的局域网中存在可用的小米中枢网关。请检查选择的家庭是否符合该要求。\",\n            \"mdns_discovery_error\": \"本地设备发现服务异常。\",\n            \"update_config_error\": \"配置信息更新失败。\",\n            \"not_confirm\": \"未确认修改项。请勾选确认后再提交。\",\n            \"invalid_network_addr\": \"存在无效的IP地址或者HTTP地址，请输入有效的地址。\",\n            \"invalid_ip_addr\": \"存在无法访问的IP地址，请输入有效的IP地址。\",\n            \"invalid_http_addr\": \"存在无法访问的HTTP地址，请输入有效的HTTP地址。\",\n            \"invalid_default_addr\": \"默认网络检测地址无法访问，请检查网络配置或者使用自定义网络检测地址。\",\n            \"unreachable_oauth2_host\": \"无法访问 OAuth2 认证地址，请检查网络配置。\",\n            \"unreachable_http_host\": \"无法访问小米 HTTP API 地址，请检查网络配置。\",\n            \"unreachable_spec_host\": \"无法访问小米 SPEC API 地址，请检查网络配置。\",\n            \"unreachable_mqtt_broker\": \"无法访问小米 MQTT Broker 地址，请检查网络配置。\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"配置失败。网络连接异常，请检查设备网络配置。\",\n            \"options_flow_error\": \"集成重新配置错误：{error}\",\n            \"re_add\": \"请重新添加集成，错误信息：{error}\",\n            \"storage_error\": \"集成存储模块异常。请重试或者重新添加集成：{error}\",\n            \"inconsistent_account\": \"账号信息不一致。请使用正确的账号登录。\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/translations/zh-Hant.json",
    "content": "{\n    \"config\": {\n        \"flow_title\": \"米家集成\",\n        \"step\": {\n            \"eula\": {\n                \"title\": \"風險告知\",\n                \"description\": \"1. 您的**小米用戶信息和設備信息**將會存儲在您的 Home Assistant 系統中，**小米無法保證 Home Assistant 存儲機制的安全性**。您需要負責防止您的信息被竊取。\\r\\n2. 此集成由開源社區維護，可能會出現穩定性問題或其他問題，使用此集成遇到相關問題時，您應當**向開源社區尋求幫助，而不是聯繫小米客服**。\\r\\n3. 您需要有一定的技術能力來維護您的本地運行環境，此集成對新手用戶來說並不友好。\\r\\n4. 在使用此集成前，請**仔細閱讀README**。\\r\\n5. 為了用戶能夠穩定地使用集成，避免接口被濫用，**此集成僅允許在 Home Assistant 中使用，詳情請參考LICENSE**。\",\n                \"data\": {\n                    \"eula\": \"我已悉知以上風險並自願承擔因使用集成所帶來的相關風險。\"\n                }\n            },\n            \"auth_config\": {\n                \"title\": \"基礎配置\",\n                \"description\": \"### 登錄地區\\r\\n選擇小米帳號所在的地區。您可以在 `米家APP > 我的（位於底部菜單） > 更多設置 > 關於米家` 中查看。\\r\\n### 語言\\r\\n選擇設備及實體名稱所用的語言。缺少翻譯的部分語句將使用英文顯示。\\r\\n### OAuth2 認證跳轉地址\\r\\nOAuth2 認證跳轉地址為 **[http://homeassistant.local:8123](http://homeassistant.local:8123)**，Home Assistant 需要與當前操作終端（例如，個人電腦）在同一局域網內，且操作終端能通過該地址訪問 Home Assistant 首頁，否則登錄認證可能會失敗。\\r\\n### 集成網絡配置\\r\\n檢測本地網絡是否正常，相關網絡資源是否可訪問。**首次添加時建議勾選。**\\r\\n### 注意事項\\r\\n- 對於數百個及以上米家設備的用戶，首次添加集成會耗費一些時間，請耐心等待。\\r\\n- 如果 Home Assistant 運行在docker環境下，請確保docker網絡模式為host，否則會導致本地控制功能異常。\\r\\n- 集成本地控制功能存在一些依賴項，請仔細閱讀README。\",\n                \"data\": {\n                    \"cloud_server\": \"登錄地區\",\n                    \"integration_language\": \"語言\",\n                    \"oauth_redirect_url\": \"認證跳轉地址\",\n                    \"network_detect_config\": \"集成網絡配置\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"集成網絡配置\",\n                \"description\": \"## 使用介紹\\r\\n### 網絡檢測地址\\r\\n用於檢測網絡是否正常，未設置時將使用系統默認地址檢測。如果默認地址檢測異常時，可嘗試輸入可用的自定義地址檢測。\\r\\n- 可輸入多個檢測地址，地址之間使用`,`號間隔，如`8.8.8.8,https://www.bing.com`\\r\\n- 如果為IP地址，將採用ping方式檢測，如果為http(s)地址，將採用 HTTP GET 訪問該地址檢測。\\r\\n- 如果想恢復系統默認檢測地址，請輸入`,`號，然後點擊'下一步'。\\r\\n- **該配置為全局配置，修改會影響其它集成實例的網絡檢測，請謹慎修改。**\\r\\n### 檢測網絡依賴項\\r\\n依次檢查下述網絡依賴項是否可訪問。如果相關地址無法訪問，將會導致集成異常。\\r\\n- OAuth2 認證地址：`https://account.xiaomi.com/oauth2/authorize`。\\r\\n- 小米 HTTP API 地址：`https://{http_host}/app/v2/ha/oauth/get_token`。\\r\\n- 小米 SPEC API 地址：`https://miot-spec.org/miot-spec-v2/template/list/device`。\\r\\n- 小米 MQTT Broker 地址：`mqtts://{broker_host}`。\",\n                \"data\": {\n                    \"network_detect_addr\": \"網絡檢測地址\",\n                    \"check_network_deps\": \"檢測網絡依賴項\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"登錄出現錯誤\",\n                \"description\": \"點擊“下一步”重試\"\n            },\n            \"homes_select\": {\n                \"title\": \"選擇家庭與設備\",\n                \"description\": \"## 使用介紹\\r\\n### 導入設備的家庭\\r\\n集成將添加已選中家庭中的設備。\\r\\n### 房間名同步模式\\r\\n將設備從米家APP同步到 Home Assistant 時，設備在 Home Assistant 中所處區域的名稱的命名方式將遵循以下規則。注意，設備同步過程不會改變米家APP中家庭和房間的設置。\\r\\n- 不同步：設備不會被添加至任何區域。\\r\\n- 其它選項：設備所添加到的區域以米家APP中的家庭或房間名稱命名。\\r\\n### 高級設置選項\\r\\n展示高級設置選項，對集成的專業配置選項進行修改。\\r\\n\\r\\n&emsp;\\r\\n### {nick_name} 您好！請選擇您想要添加的設備所處家庭。\",\n                \"data\": {\n                    \"home_infos\": \"導入設備的家庭\",\n                    \"area_name_rule\": \"房間名同步模式\",\n                    \"advanced_options\": \"高級設置選項\"\n                }\n            },\n            \"advanced_options\": {\n                \"title\": \"高級設置選項\",\n                \"description\": \"## 使用介紹\\r\\n### 除非您非常清楚下列選項的含義，否則請保持默認。\\r\\n### 篩選設備\\r\\n支持按照房間名稱和設備類型篩選設備，同時也支持設備維度篩選。\\r\\n### 控制模式\\r\\n- 自動：本地局域網內存在可用的小米中樞網關時， Home Assistant 會優先通過中樞網關發送設備控制指令，以實現本地化控制功能。本地局域網不存在中樞時，會嘗試通過小米OT協議發送控制指令，以實現本地化控制功能。只有當上述本地化控制條件不滿足時，設備控制指令才會通過雲端發送。\\r\\n- 雲端：控制指令僅通過雲端發送。\\r\\n### Action 調試模式\\r\\n對於設備 MIoT-Spec-V2 定義的方法，在生成通知實體之外，還會生成一個文本輸入框實體，您可以在調試時用它向設備發送控制指令。\\r\\n### 隱藏非標準生成實體\\r\\n隱藏名稱以“*”開頭的非標準 MIoT-Spec-V2 實例生成的實體。\\r\\n### 二進制傳感器顯示模式\\r\\n將米家中的二進制傳感器顯示為文本傳感器實體或者二進制傳感器實體。\\r\\n### 顯示設備狀態變化通知\\r\\n細化顯示設備狀態變化通知，只顯示勾選的通知消息。\",\n                \"data\": {\n                    \"devices_filter\": \"篩選設備\",\n                    \"ctrl_mode\": \"控制模式\",\n                    \"action_debug\": \"Action 調試模式\",\n                    \"hide_non_standard_entities\": \"隱藏非標準生成實體\",\n                    \"display_binary_mode\": \"二進制傳感器顯示模式\",\n                    \"display_devices_changed_notify\": \"顯示設備狀態變化通知\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"篩選設備\",\n                \"description\": \"## 使用介紹\\r\\n支持按照家庭房間名稱、設備接入類型、設備型號篩選設備，同時也支持設備維度篩選，篩選邏輯如下：\\r\\n- 會先根據統計邏輯獲取所有包含項的並集或者交集，然後再獲取排除項的交集或者並集，最後將【包含匯總結果】減去【排除匯總結果】得到【篩選結果】\\r\\n- 如未選擇包含項，表示包含全部。\\r\\n### 篩選模式\\r\\n- 排除：移除不需要的項。\\r\\n- 包含：包含需要的項。\\r\\n### 統計邏輯\\r\\n- 與邏輯：取所有同模式篩選項的交集。\\r\\n- 或邏輯：取所有同模式篩選項的並集。\\r\\n\\r\\n您也可以進入集成項的【配置>更新設備列表】頁面，勾選【篩選設備】重新篩選。\",\n                \"data\": {\n                    \"room_filter_mode\": \"篩選家庭房間\",\n                    \"room_list\": \"家庭房間\",\n                    \"type_filter_mode\": \"篩選設備接入類型\",\n                    \"type_list\": \"設備接入類型\",\n                    \"model_filter_mode\": \"篩選設備型號\",\n                    \"model_list\": \"設備型號\",\n                    \"devices_filter_mode\": \"篩選設備\",\n                    \"device_list\": \"設備列表\",\n                    \"statistics_logic\": \"統計邏輯\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}請點擊此處進行登錄{link_right}\\r\\n(登錄成功後，將會自動跳轉至下一頁面)\"\n        },\n        \"error\": {\n            \"eula_not_agree\": \"請閱讀風險告知文本。\",\n            \"get_token_error\": \"獲取登錄授權信息（OAuth token）失敗。\",\n            \"get_homeinfo_error\": \"獲取家庭信息失敗。\",\n            \"mdns_discovery_error\": \"本地設備發現服務異常。\",\n            \"get_cert_error\": \"獲取中樞證書失敗。\",\n            \"no_family_selected\": \"未選擇家庭。\",\n            \"no_devices\": \"選擇的家庭中沒有設備。請選擇有設備的家庭，然後繼續。\",\n            \"no_filter_devices\": \"篩選設備為空。請選擇有效的篩選條件，然後繼續。\",\n            \"no_central_device\": \"【中樞網關模式】需要 Home Assistant 所在的局域網中存在可用的小米中樞網關。請檢查選擇的家庭是否符合該要求。\",\n            \"invalid_network_addr\": \"存在無效的IP地址或者HTTP地址，請輸入有效的地址。\",\n            \"invalid_ip_addr\": \"存在無法訪問的IP地址，請輸入有效的IP地址。\",\n            \"invalid_http_addr\": \"存在無法訪問的HTTP地址，請輸入有效的HTTP地址。\",\n            \"invalid_default_addr\": \"默認網絡檢測地址無法訪問，請檢查網絡配置或者使用自定義網絡檢測地址。\",\n            \"unreachable_oauth2_host\": \"無法訪問 OAuth2 認證地址，請檢查網絡配置。\",\n            \"unreachable_http_host\": \"無法訪問小米 HTTP API 地址，請檢查網絡配置。\",\n            \"unreachable_spec_host\": \"無法訪問小米 SPEC API 地址，請檢查網絡配置。\",\n            \"unreachable_mqtt_broker\": \"無法訪問小米 MQTT Broker 地址，請檢查網絡配置。\"\n        },\n        \"abort\": {\n            \"ha_uuid_get_failed\": \"獲取 Home Assistant UUID 失敗。\",\n            \"network_connect_error\": \"配置失敗。網絡連接異常，請檢查設備網絡配置。\",\n            \"already_configured\": \"該用戶已配置完成。請進入集成頁面，點擊“配置”按鈕修改配置。\",\n            \"invalid_auth_info\": \"認證信息已過期。請進入集成頁面，點擊“配置”按鈕重新認證。\",\n            \"config_flow_error\": \"集成配置錯誤：{error}\"\n        }\n    },\n    \"options\": {\n        \"step\": {\n            \"auth_config\": {\n                \"title\": \"認證配置\",\n                \"description\": \"檢測到本地認證信息過期，請重新開始認證\\r\\n### 當前登錄地區: {cloud_server}\\r\\n### OAuth2 認證跳轉地址\\r\\nOAuth2 認證跳轉地址為 **[http://homeassistant.local:8123](http://homeassistant.local:8123)**，Home Assistant 需要與當前操作終端（例如，個人電腦）在同一局域網內，且操作終端能通過該地址訪問 Home Assistant 首頁，否則登錄認證可能會失敗。\",\n                \"data\": {\n                    \"oauth_redirect_url\": \"認證跳轉地址\"\n                }\n            },\n            \"oauth_error\": {\n                \"title\": \"登錄出現錯誤\",\n                \"description\": \"點擊“下一步”重試\"\n            },\n            \"config_options\": {\n                \"title\": \"配置選項\",\n                \"description\": \"### {nick_name} 您好！\\r\\n\\r\\n小米帳號ID：{uid}\\r\\n當前登錄區域：{cloud_server}\\r\\n集成實例ID：{instance_id}\\r\\n\\r\\n請勾選需要重新配置的選項，然後點擊“下一步”。\",\n                \"data\": {\n                    \"integration_language\": \"集成語言\",\n                    \"update_user_info\": \"更新用戶信息\",\n                    \"update_devices\": \"更新設備列表\",\n                    \"action_debug\": \"Action 調試模式\",\n                    \"hide_non_standard_entities\": \"隱藏非標準生成實體\",\n                    \"display_binary_mode\": \"二進制傳感器顯示模式\",\n                    \"display_devices_changed_notify\": \"顯示設備狀態變化通知\",\n                    \"update_trans_rules\": \"更新實體轉換規則\",\n                    \"update_lan_ctrl_config\": \"更新局域網控制配置\",\n                    \"network_detect_config\": \"集成網絡配置\",\n                    \"cover_dead_zone_width\": \"窗簾盲區寬度\"\n                }\n            },\n            \"update_user_info\": {\n                \"title\": \"更新用戶暱稱\",\n                \"description\": \"{nick_name} 您好！請在下方修改您的用戶暱稱。\",\n                \"data\": {\n                    \"nick_name\": \"用戶暱稱\"\n                }\n            },\n            \"homes_select\": {\n                \"title\": \"重新選擇家庭與設備\",\n                \"description\": \"## 使用介紹\\r\\n### 導入設備的家庭\\r\\n集成將添加已選中家庭中的設備。\\r\\n### 篩選設備\\r\\n支持按照家庭房間名稱、設備接入類型、設備型號篩選設備，同時也支持設備維度篩選，已篩選出 **{local_count}** 個設備。\\r\\n### 控制模式\\r\\n- 自動：本地局域網內存在可用的小米中樞網關時， Home Assistant 會優先通過中樞網關發送設備控制指令，以實現本地化控制功能。只有當本地化控制條件不滿足時，設備控制指令才會通過雲端發送。\\r\\n- 雲端：控制指令強制通過雲端發送。\",\n                \"data\": {\n                    \"home_infos\": \"導入設備的家庭\",\n                    \"devices_filter\": \"篩選設備\",\n                    \"ctrl_mode\": \"控制模式\"\n                }\n            },\n            \"devices_filter\": {\n                \"title\": \"篩選設備\",\n                \"description\": \"## 使用介紹\\r\\n支持按照家庭房間名稱、設備接入類型、設備型號篩選設備，同時也支持設備維度篩選，篩選邏輯如下：\\r\\n- 會先根據統計邏輯獲取所有包含項的並集或者交集，然後再獲取排除項的交集或者並集，最後將【包含匯總結果】減去【排除匯總結果】得到【篩選結果】\\r\\n- 如未選擇包含項，表示包含全部。\\r\\n### 篩選模式\\r\\n- 排除：移除不需要的項。\\r\\n- 包含：包含需要的項。\\r\\n### 統計邏輯\\r\\n- 與邏輯：取所有同模式篩選項的交集。\\r\\n- 或邏輯：取所有同模式篩選項的並集。\\r\\n\\r\\n您也可以進入集成項的【配置>更新設備列表】頁面，勾選【篩選設備】重新篩選。\",\n                \"data\": {\n                    \"room_filter_mode\": \"篩選家庭房間\",\n                    \"room_list\": \"家庭房間\",\n                    \"type_filter_mode\": \"篩選設備接入類型\",\n                    \"type_list\": \"設備接入類型\",\n                    \"model_filter_mode\": \"篩選設備型號\",\n                    \"model_list\": \"設備型號\",\n                    \"devices_filter_mode\": \"篩選設備\",\n                    \"device_list\": \"設備列表\",\n                    \"statistics_logic\": \"統計邏輯\"\n                }\n            },\n            \"update_trans_rules\": {\n                \"title\": \"更新實體轉換規則\",\n                \"description\": \"## 使用介紹\\r\\n- 更新當前集成實例中設備的實體信息，包含SPEC多語言配置、SPEC布爾值翻譯、SPEC模型過濾。\\r\\n- **警告：該配置為全局配置**，會直接更新本地緩存，如果其它集成實例中有相同型號設備，相關實例重載後也會更新。\\r\\n- 該操作會耗費一定時間，請耐心等待，勾選“確認更新”，點擊“下一步”開始更新 **{urn_count}** 條規則，否則跳過更新。\",\n                \"data\": {\n                    \"confirm\": \"確認更新\"\n                }\n            },\n            \"update_lan_ctrl_config\": {\n                \"title\": \"更新局域網控制配置\",\n                \"description\": \"## 使用介紹\\r\\n更新**局域網控制米家設備**時的配置信息，當雲端和中樞網關無法控制設備時，集成會嘗試通過局域網控制設備；如果未選擇網卡，局域網控制將不會啟用。\\r\\n- 目前只支持控制局域網內 **SPEC v2** WiFi 設備，部分舊設備可能不支持控制或者不支持屬性同步。\\r\\n- 請選擇和設備同一網絡的網卡（支持多選），如果選擇的網卡存在兩個及以上連接在同一網絡中，建議選擇網絡連接最優的，否則可能會**影響設備正常使用**。\\r\\n- **如果局域網中存在支持本地控制的終端設備（網關、手機等），啟用局域網訂閱可能會導致本地自動化或者設備異常，請謹慎使用**。\\r\\n- **警告：該配置為全局配置，修改會影響其他集成實例，請謹慎修改**。\\r\\n{notice_net_dup}\",\n                \"data\": {\n                    \"net_interfaces\": \"請選擇使用的網卡\",\n                    \"enable_subscribe\": \"啟用局域網訂閱\"\n                }\n            },\n            \"network_detect_config\": {\n                \"title\": \"集成網絡配置\",\n                \"description\": \"## 使用介紹\\r\\n### 網絡檢測地址\\r\\n用於檢測網絡是否正常，未設置時將使用系統默認地址檢測。如果默認地址檢測異常時，可嘗試輸入可用的自定義地址檢測。\\r\\n- 可輸入多個檢測地址，地址之間使用`,`號間隔，如`8.8.8.8,https://www.bing.com`\\r\\n- 如果為IP地址，將採用ping方式檢測，如果為http(s)地址，將採用 HTTP GET 訪問該地址檢測。\\r\\n- 如果想恢復系統默認檢測地址，請輸入`,`號，然後點擊'下一步'。\\r\\n- **該配置為全局配置，修改會影響其它集成實例的網絡檢測，請謹慎修改。**\\r\\n### 檢測網絡依賴項\\r\\n依次檢查下述網絡依賴項是否可訪問。如果相關地址無法訪問，將會導致集成異常。\\r\\n- OAuth2 認證地址：`https://account.xiaomi.com/oauth2/authorize`。\\r\\n- 小米 HTTP API 地址：`https://{http_host}/app/v2/ha/oauth/get_token`。\\r\\n- 小米 SPEC API 地址：`https://miot-spec.org/miot-spec-v2/template/list/device`。\\r\\n- 小米 MQTT Broker 地址：`mqtts://{broker_host}`。\",\n                \"data\": {\n                    \"network_detect_addr\": \"網絡檢測地址\",\n                    \"check_network_deps\": \"檢測網絡依賴項\"\n                }\n            },\n            \"config_confirm\": {\n                \"title\": \"確認配置\",\n                \"description\": \"**{nick_name}** 您好！請確認最新的配置信息，然後點擊“提交”。\\r\\n集成將會使用更新後的配置重新載入。\\r\\n\\r\\n集成語言：\\t{lang_new}\\r\\n用戶暱稱：\\t{nick_name_new}\\r\\nAction 調試模式：\\t{action_debug}\\r\\n隱藏非標準生成實體：\\t{hide_non_standard_entities}\\r\\n窗簾盲區寬度：\\t{cover_width_new}\\r\\n顯示設備狀態變化通知：\\t{display_devices_changed_notify}\\r\\n設備變化：\\t新增 **{devices_add}** 個設備，移除 **{devices_remove}** 個設備\\r\\n轉換規則變化：\\t共條 **{trans_rules_count}** 規則，更新 **{trans_rules_count_success}** 條規則\",\n                \"data\": {\n                    \"confirm\": \"確認修改\"\n                }\n            }\n        },\n        \"progress\": {\n            \"oauth\": \"### {link_left}請點擊此處重新登錄{link_right}\"\n        },\n        \"error\": {\n            \"not_auth\": \"用戶未認證。請點擊認證鏈接以認證用戶身份。\",\n            \"get_token_error\": \"獲取登錄授權信息（OAuth token）失敗。\",\n            \"get_homeinfo_error\": \"獲取家庭信息失敗。\",\n            \"get_cert_error\": \"獲取中樞證書失敗。\",\n            \"no_family_selected\": \"未選擇家庭。\",\n            \"no_devices\": \"選擇的家庭中沒有設備。請選擇有設備的家庭，然後繼續。\",\n            \"no_filter_devices\": \"篩選設備為空。請選擇有效的篩選條件，然後繼續。\",\n            \"no_central_device\": \"【中樞網關模式】需要 Home Assistant 所在的局域網中存在可用的小米中樞網關。請檢查選擇的家庭是否符合該要求。\",\n            \"mdns_discovery_error\": \"本地設備發現服務異常。\",\n            \"update_config_error\": \"配置信息更新失敗。\",\n            \"not_confirm\": \"未確認修改項。請勾選確認後再提交。\",\n            \"invalid_network_addr\": \"存在無效的IP地址或者HTTP地址，請輸入有效的地址。\",\n            \"invalid_ip_addr\": \"存在無法訪問的IP地址，請輸入有效的IP地址。\",\n            \"invalid_http_addr\": \"存在無法訪問的HTTP地址，請輸入有效的HTTP地址。\",\n            \"invalid_default_addr\": \"默認網絡檢測地址無法訪問，請檢查網絡配置或者使用自定義網絡檢測地址。\",\n            \"unreachable_oauth2_host\": \"無法訪問 OAuth2 認證地址，請檢查網絡配置。\",\n            \"unreachable_http_host\": \"無法訪問小米 HTTP API 地址，請檢查網絡配置。\",\n            \"unreachable_spec_host\": \"無法訪問小米 SPEC API 地址，請檢查網絡配置。\",\n            \"unreachable_mqtt_broker\": \"無法訪問小米 MQTT Broker 地址，請檢查網絡配置。\"\n        },\n        \"abort\": {\n            \"network_connect_error\": \"配置失敗。網絡連接異常，請檢查設備網絡配置。\",\n            \"options_flow_error\": \"集成重新配置錯誤：{error}\",\n            \"re_add\": \"請重新添加集成，錯誤信息：{error}\",\n            \"storage_error\": \"集成存儲模塊異常。請重試或者重新添加集成：{error}\",\n            \"inconsistent_account\": \"帳號信息不一致。請使用正確的帳號登錄。\"\n        }\n    }\n}"
  },
  {
    "path": "custom_components/xiaomi_home/vacuum.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nVacuum entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nfrom typing import Any, Optional\nimport re\nimport logging\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.vacuum import (StateVacuumEntity,\n                                             VacuumEntityFeature)\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData\nfrom .miot.miot_spec import (MIoTSpecAction, MIoTSpecProperty)\n\ntry:  # VacuumActivity is introduced in HA core 2025.1.0\n    from homeassistant.components.vacuum import VacuumActivity\n    HA_CORE_HAS_ACTIVITY = True\nexcept ImportError:\n    HA_CORE_HAS_ACTIVITY = False\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('vacuum', []):\n            new_entities.append(\n                Vacuum(miot_device=miot_device, entity_data=data))\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass Vacuum(MIoTServiceEntity, StateVacuumEntity):\n    \"\"\"Vacuum entities for Xiaomi Home.\"\"\"\n    # pylint: disable=unused-argument\n    _prop_status: Optional[MIoTSpecProperty]\n    _prop_fan_level: Optional[MIoTSpecProperty]\n    _prop_status_cleaning: Optional[list[int]]\n    _prop_status_docked: Optional[list[int]]\n    _prop_status_paused: Optional[list[int]]\n    _prop_status_returning: Optional[list[int]]\n    _prop_status_error: Optional[list[int]]\n\n    _action_start_sweep: Optional[MIoTSpecAction]\n    _action_stop_sweeping: Optional[MIoTSpecAction]\n    _action_pause_sweeping: Optional[MIoTSpecAction]\n    _action_continue_sweep: Optional[MIoTSpecAction]\n    _action_stop_and_gocharge: Optional[MIoTSpecAction]\n    _action_identify: Optional[MIoTSpecAction]\n\n    _status_map: Optional[dict[int, str]]\n    _fan_level_map: Optional[dict[int, str]]\n\n    _device_name: str\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._device_name = miot_device.name\n        self._attr_supported_features = VacuumEntityFeature(0)\n\n        self._prop_status = None\n        self._prop_fan_level = None\n        self._prop_status_cleaning = []\n        self._prop_status_docked = []\n        self._prop_status_paused = []\n        self._prop_status_returning = []\n        self._prop_status_error = []\n        self._action_start_sweep = None\n        self._action_stop_sweeping = None\n        self._action_pause_sweeping = None\n        self._action_continue_sweep = None\n        self._action_stop_and_gocharge = None\n        self._action_identify = None\n        self._status_map = None\n        self._fan_level_map = None\n\n        # properties\n        for prop in entity_data.props:\n            if prop.name == 'status':\n                if not prop.value_list:\n                    _LOGGER.error('invalid status value_list, %s',\n                                  self.entity_id)\n                    continue\n                self._status_map = prop.value_list.to_map()\n                self._attr_supported_features |= VacuumEntityFeature.STATE\n                self._prop_status = prop\n                for item in prop.value_list.items:\n                    item_str: str = item.name\n                    item_name: str = re.sub(r'[^a-z]', '', item_str)\n                    if item_name in {\n                            'charging', 'charged', 'chargingcompleted',\n                            'fullcharge', 'fullpower', 'findchargerpause',\n                            'drying', 'washing', 'wash', 'inthewash',\n                            'inthedry', 'stationworking', 'dustcollecting',\n                            'upgrade', 'upgrading', 'updating'\n                    }:\n                        self._prop_status_docked.append(item.value)\n                    elif item_name in {'paused', 'pause'}:\n                        self._prop_status_paused.append(item.value)\n                    elif item_name in {\n                            'gocharging', 'cleancompletegocharging',\n                            'findchargewash', 'backtowashmop', 'gowash',\n                            'gowashing', 'summon'\n                    }:\n                        self._prop_status_returning.append(item.value)\n                    elif item_name in {\n                            'error', 'breakcharging', 'gochargebreak'\n                    }:\n                        self._prop_status_error.append(item.value)\n                    elif (item_name.find('sweeping') != -1) or (\n                            item_name.find('mopping') != -1) or (item_name in {\n                                'cleaning', 'remoteclean', 'continuesweep',\n                                'busy', 'building', 'buildingmap', 'mapping'\n                            }):\n                        self._prop_status_cleaning.append(item.value)\n            elif prop.name == 'fan-level':\n                if not prop.value_list:\n                    _LOGGER.error('invalid fan-level value_list, %s',\n                                  self.entity_id)\n                    continue\n                self._fan_level_map = prop.value_list.to_map()\n                self._attr_fan_speed_list = list(self._fan_level_map.values())\n                self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED\n                self._prop_fan_level = prop\n        # action\n        for action in entity_data.actions:\n            if action.name == 'start-sweep':\n                self._attr_supported_features |= VacuumEntityFeature.START\n                self._action_start_sweep = action\n            elif action.name == 'stop-sweeping':\n                self._attr_supported_features |= VacuumEntityFeature.STOP\n                self._action_stop_sweeping = action\n            elif action.name == 'pause-sweeping':\n                self._attr_supported_features |= VacuumEntityFeature.PAUSE\n                self._action_pause_sweeping = action\n            elif action.name == 'continue-sweep':\n                self._action_continue_sweep = action\n            elif action.name == 'stop-and-gocharge':\n                self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME\n                self._action_stop_and_gocharge = action\n            elif action.name == 'identify':\n                self._attr_supported_features |= VacuumEntityFeature.LOCATE\n                self._action_identify = action\n\n        # Use start-charge from battery service as fallback\n        # if stop-and-gocharge is not available\n        if self._action_stop_and_gocharge is None:\n            for action in entity_data.actions:\n                if action.name == 'start-charge':\n                    self._attr_supported_features |= (\n                        VacuumEntityFeature.RETURN_HOME)\n                    self._action_stop_and_gocharge = action\n                    break\n\n    async def async_start(self) -> None:\n        \"\"\"Start or resume the cleaning task.\"\"\"\n        if self._prop_status is not None:\n            status = self.get_prop_value(prop=self._prop_status)\n            if (status in self._prop_status_paused\n               ) and self._action_continue_sweep:\n                await self.action_async(action=self._action_continue_sweep)\n                return\n        await self.action_async(action=self._action_start_sweep)\n\n    async def async_stop(self, **kwargs: Any) -> None:\n        \"\"\"Stop the vacuum cleaner, do not return to base.\"\"\"\n        await self.action_async(action=self._action_stop_sweeping)\n\n    async def async_pause(self) -> None:\n        \"\"\"Pause the cleaning task.\"\"\"\n        await self.action_async(action=self._action_pause_sweeping)\n\n    async def async_return_to_base(self, **kwargs: Any) -> None:\n        \"\"\"Set the vacuum cleaner to return to the dock.\"\"\"\n        await self.action_async(action=self._action_stop_and_gocharge)\n\n    async def async_locate(self, **kwargs: Any) -> None:\n        \"\"\"Locate the vacuum cleaner.\"\"\"\n        await self.action_async(action=self._action_identify)\n\n    async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:\n        \"\"\"Set fan speed.\"\"\"\n        fan_level_value = self.get_map_key(map_=self._fan_level_map,\n                                           value=fan_speed)\n        await self.set_property_async(prop=self._prop_fan_level,\n                                      value=fan_level_value)\n\n    @property\n    def name(self) -> Optional[str]:\n        \"\"\"Name of the vacuum entity.\"\"\"\n        return self._device_name\n\n    @property\n    def fan_speed(self) -> Optional[str]:\n        \"\"\"The current fan speed of the vacuum cleaner.\"\"\"\n        return self.get_map_value(\n            map_=self._fan_level_map,\n            key=self.get_prop_value(prop=self._prop_fan_level))\n\n    if HA_CORE_HAS_ACTIVITY:\n\n        @property\n        def activity(self) -> Optional[str]:\n            \"\"\"The current vacuum activity.\n        To fix the HA warning below:\n            Detected that custom integration 'xiaomi_home' is setting state\n            directly.Entity XXX(<class 'custom_components.xiaomi_home.vacuum\n            .Vacuum'>)should implement the 'activity' property and return\n            its state using the VacuumActivity enum.This will stop working in\n            Home Assistant 2026.1.\n\n        Refer to\n        https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property\n\n        There are only 6 states in VacuumActivity enum. To be compatible with\n        more constants, try get matching VacuumActivity enum first, return state\n        string as before if there is no match. In Home Assistant 2026.1, every\n        state should map to a VacuumActivity enum.\n            \"\"\"\n            status = self.get_prop_value(prop=self._prop_status)\n            if status is None:\n                return None\n            if status in self._prop_status_cleaning:\n                return VacuumActivity.CLEANING\n            if status in self._prop_status_docked:\n                return VacuumActivity.DOCKED\n            if status in self._prop_status_paused:\n                return VacuumActivity.PAUSED\n            if status in self._prop_status_returning:\n                return VacuumActivity.RETURNING\n            if status in self._prop_status_error:\n                return VacuumActivity.ERROR\n            return VacuumActivity.IDLE\n\n    else:\n\n        @property\n        def state(self) -> Optional[str]:\n            \"\"\"The current state of the vacuum.\"\"\"\n            status = self.get_prop_value(prop=self._prop_status)\n            return None if (status is None) else self.get_map_value(\n                map_=self._status_map, key=status)\n"
  },
  {
    "path": "custom_components/xiaomi_home/water_heater.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nCopyright (C) 2024 Xiaomi Corporation.\n\nThe ownership and intellectual property rights of Xiaomi Home Assistant\nIntegration and related Xiaomi cloud service API interface provided under this\nlicense, including source code and object code (collectively, \"Licensed Work\"),\nare owned by Xiaomi. Subject to the terms and conditions of this License, Xiaomi\nhereby grants you a personal, limited, non-exclusive, non-transferable,\nnon-sublicensable, and royalty-free license to reproduce, use, modify, and\ndistribute the Licensed Work only for your use of Home Assistant for\nnon-commercial purposes. For the avoidance of doubt, Xiaomi does not authorize\nyou to use the Licensed Work for any other purpose, including but not limited\nto use Licensed Work to develop applications (APP), Web services, and other\nforms of software.\n\nYou may reproduce and distribute copies of the Licensed Work, with or without\nmodifications, whether in source or object form, provided that you must give\nany other recipients of the Licensed Work a copy of this License and retain all\ncopyright and disclaimers.\n\nXiaomi provides the Licensed Work on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied, including, without\nlimitation, any warranties, undertakes, or conditions of TITLE, NO ERROR OR\nOMISSION, CONTINUITY, RELIABILITY, NON-INFRINGEMENT, MERCHANTABILITY, or\nFITNESS FOR A PARTICULAR PURPOSE. In any event, you are solely responsible\nfor any direct, indirect, special, incidental, or consequential damages or\nlosses arising from the use or inability to use the Licensed Work.\n\nXiaomi reserves all rights not expressly granted to you in this License.\nExcept for the rights expressly granted by Xiaomi under this License, Xiaomi\ndoes not authorize you in any form to use the trademarks, copyrights, or other\nforms of intellectual property rights of Xiaomi and its affiliates, including,\nwithout limitation, without obtaining other written permission from Xiaomi, you\nshall not use \"Xiaomi\", \"Mijia\" and other words related to Xiaomi or words that\nmay make the public associate with Xiaomi in any form to publicize or promote\nthe software or hardware devices that use the Licensed Work.\n\nXiaomi has the right to immediately terminate all your authorization under this\nLicense in the event:\n1. You assert patent invalidation, litigation, or other claims against patents\nor other intellectual property rights of Xiaomi or its affiliates; or,\n2. You make, have made, manufacture, sell, or offer to sell products that knock\noff Xiaomi or its affiliates' products.\n\nWater heater entities for Xiaomi Home.\n\"\"\"\nfrom __future__ import annotations\nimport logging\nfrom typing import Any, Optional\n\nfrom homeassistant.config_entries import ConfigEntry\nfrom homeassistant.core import HomeAssistant\nfrom homeassistant.helpers.entity_platform import AddEntitiesCallback\nfrom homeassistant.components.water_heater import (STATE_ON, STATE_OFF,\n                                                   ATTR_TEMPERATURE,\n                                                   WaterHeaterEntity,\n                                                   WaterHeaterEntityFeature)\n\nfrom .miot.const import DOMAIN\nfrom .miot.miot_device import MIoTDevice, MIoTEntityData, MIoTServiceEntity\nfrom .miot.miot_spec import MIoTSpecProperty\n\n_LOGGER = logging.getLogger(__name__)\n\n\nasync def async_setup_entry(\n    hass: HomeAssistant,\n    config_entry: ConfigEntry,\n    async_add_entities: AddEntitiesCallback,\n) -> None:\n    \"\"\"Set up a config entry.\"\"\"\n    device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][\n        config_entry.entry_id]\n\n    new_entities = []\n    for miot_device in device_list:\n        for data in miot_device.entity_list.get('water_heater', []):\n            new_entities.append(\n                WaterHeater(miot_device=miot_device, entity_data=data))\n\n    if new_entities:\n        async_add_entities(new_entities)\n\n\nclass WaterHeater(MIoTServiceEntity, WaterHeaterEntity):\n    \"\"\"Water heater entities for Xiaomi Home.\"\"\"\n    _prop_on: Optional[MIoTSpecProperty]\n    _prop_temp: Optional[MIoTSpecProperty]\n    _prop_target_temp: Optional[MIoTSpecProperty]\n    _prop_mode: Optional[MIoTSpecProperty]\n\n    _mode_map: Optional[dict[Any, Any]]\n\n    def __init__(self, miot_device: MIoTDevice,\n                 entity_data: MIoTEntityData) -> None:\n        \"\"\"Initialize the Water heater.\"\"\"\n        super().__init__(miot_device=miot_device, entity_data=entity_data)\n        self._attr_temperature_unit = None\n        self._attr_supported_features = WaterHeaterEntityFeature(0)\n        self._prop_on = None\n        self._prop_temp = None\n        self._prop_target_temp = None\n        self._prop_mode = None\n        self._mode_map = None\n\n        # properties\n        for prop in entity_data.props:\n            # on\n            if prop.name == 'on':\n                self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF\n                self._prop_on = prop\n            # temperature\n            if prop.name == 'temperature':\n                if not prop.value_range:\n                    _LOGGER.error('invalid temperature value_range format, %s',\n                                  self.entity_id)\n                    continue\n                if prop.external_unit:\n                    self._attr_temperature_unit = prop.external_unit\n                self._prop_temp = prop\n            # target-temperature\n            if prop.name == 'target-temperature':\n                if not prop.value_range:\n                    _LOGGER.error(\n                        'invalid target-temperature value_range format, %s',\n                        self.entity_id)\n                    continue\n                self._attr_min_temp = prop.value_range.min_\n                self._attr_max_temp = prop.value_range.max_\n                self._attr_target_temperature_step = prop.value_range.step\n                if self._attr_temperature_unit is None and prop.external_unit:\n                    self._attr_temperature_unit = prop.external_unit\n                self._attr_supported_features |= (\n                    WaterHeaterEntityFeature.TARGET_TEMPERATURE)\n                self._prop_target_temp = prop\n            # mode\n            if prop.name == 'mode':\n                if not prop.value_list:\n                    _LOGGER.error('mode value_list is None, %s', self.entity_id)\n                    continue\n                self._mode_map = prop.value_list.to_map()\n                self._attr_operation_list = list(self._mode_map.values())\n                self._prop_mode = prop\n        if not self._attr_operation_list:\n            self._attr_operation_list = [STATE_ON]\n        self._attr_operation_list.append(STATE_OFF)\n        self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE\n\n    async def async_turn_on(self) -> None:\n        \"\"\"Turn the water heater on.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=True)\n\n    async def async_turn_off(self) -> None:\n        \"\"\"Turn the water heater off.\"\"\"\n        await self.set_property_async(prop=self._prop_on, value=False)\n\n    async def async_set_temperature(self, **kwargs: Any) -> None:\n        \"\"\"Set the target temperature.\"\"\"\n        await self.set_property_async(prop=self._prop_target_temp,\n                                      value=kwargs[ATTR_TEMPERATURE])\n\n    async def async_set_operation_mode(self, operation_mode: str) -> None:\n        \"\"\"Set the operation mode of the water heater.\"\"\"\n        if operation_mode == STATE_OFF:\n            await self.set_property_async(prop=self._prop_on, value=False)\n            return\n        if operation_mode == STATE_ON:\n            await self.set_property_async(prop=self._prop_on, value=True)\n            return\n        if self.get_prop_value(prop=self._prop_on) is not True:\n            await self.set_property_async(prop=self._prop_on,\n                                          value=True,\n                                          write_ha_state=False)\n        await self.set_property_async(prop=self._prop_mode,\n                                      value=self.get_map_key(\n                                          map_=self._mode_map,\n                                          value=operation_mode))\n\n    @property\n    def current_temperature(self) -> Optional[float]:\n        \"\"\"The current temperature.\"\"\"\n        return (None if self._prop_temp is None else self.get_prop_value(\n            prop=self._prop_temp))\n\n    @property\n    def target_temperature(self) -> Optional[float]:\n        \"\"\"The target temperature.\"\"\"\n        return (None if self._prop_target_temp is None else self.get_prop_value(\n            prop=self._prop_target_temp))\n\n    @property\n    def current_operation(self) -> Optional[str]:\n        \"\"\"The current mode.\"\"\"\n        if self.get_prop_value(prop=self._prop_on) is False:\n            return STATE_OFF\n        if not self._prop_mode and self.get_prop_value(prop=self._prop_on):\n            return STATE_ON\n        return (None if self._prop_mode is None else self.get_map_value(\n            map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)))\n"
  },
  {
    "path": "doc/CONTRIBUTING_zh.md",
    "content": "# 贡献指南\n\n[English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)\n\n感谢您考虑为我们的项目做出贡献！您的努力将使我们的项目变得更好。\n\n在您开始贡献之前，请花一点时间阅读以下准则：\n\n## 我可以如何贡献？\n\n### 报告问题\n\n如果您在项目中遇到错误，请在 GitHub 上[报告问题](https://github.com/XiaoMi/ha_xiaomi_home/issues/new/)，并提供关于错误的详细信息，包括复现步骤、 debug 级日志以及错误出现的时间。\n\n集成开启 debug 级日志的[方法](https://www.home-assistant.io/integrations/logger/#log-filters)：\n\n```\n# configuration.yaml 设置打印日志等级\n\nlogger:\n  default: critical\n  logs:\n    custom_components.xiaomi_home: debug\n```\n\n### 建议增强功能\n\n如果您有增强或新功能的想法，欢迎您在 GitHub 讨论区[创建想法](https://github.com/XiaoMi/ha_xiaomi_home/discussions/new?category=ideas) 。我们期待您的建议！\n\n### 贡献代码\n\n1. Fork 该仓库并从 `main` 创建您的分支。\n2. 确保您的代码符合项目的编码规范。\n3. 确保您的提交消息描述清晰。\n4. 提交请求应附有明确的问题描述和解决方案。\n5. 如果必要，请更新文档。\n6. 请运行测试并确保测试通过。\n\n## 拉取请求准则\n\n在提交拉取请求之前，请确保满足以下要求：\n\n- 您的拉取请求解决了单个问题或功能。\n- 您已在本地测试过您的更改。\n- 您的代码遵循项目的[代码规范](#代码规范)，已运行 [`pylint`](https://github.com/google/pyink) 搭配本项目的 [pylintrc](../.pylintrc) 检查代码。\n- 所有现有测试都通过，并且如果适用，您已添加了新的测试。\n- 任何依赖更改都有文档说明。\n\n## 代码规范\n\n本项目的代码格式遵循 [Google Style](https://google.github.io/styleguide/pyguide.html) 。请确保您的贡献符合该指南。\n\n## Commit Message 格式\n\n```\n<type>: <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\ntype ：有以下几种变更类型\n\n- feat：新增功能。\n- fix：修复问题。\n- docs：仅仅修改了文档。\n- style：仅仅是对格式进行修改，如逗号、缩进、空格等，不改变代码逻辑。\n- refactor：代码重构，没有新增功能或修复问题。\n- perf：优化性能。\n- test：增加或修改测试用例。\n- chore：修改编译流程，或变更依赖库和工具等。\n- revert：版本回滚。\n\nsubject ：简洁的标题，描述本次提交的概要。使用祈使句、现在时态，首字母小写，结尾不加句号。\n\nbody ：描述本次提交的详细内容，解释为什么需要这些变更。除 docs 之外的变更类型必须包含 body。\n\nfooter ：（可选）关联的 issue 或 pull request 编号。\n\n## 命名规范\n\n### 米家命名规范\n\n- 描述“小米”时必须使用“Xiaomi”，变量名称可以使用“xiaomi”或“mi”。\n- 描述“米家”时必须使用“Xiaomi Home”，变量名称可以使用“mihome”或“MiHome”。\n- 描述“小米IoT”时必须使用“MIoT”，变量名称可以使用“miot”或“MIoT”。\n\n### 第三方平台命名规范\n\n- 描述“Home Assistant”时必须使用“Home Assistant”，变量可以使用“hass”或“hass_xxx”。\n\n### 其它命名规范\n\n- 文档中的中文语句包含英文时，如果英文没有被中文引号括起来，那么中文与英文之间必须有一个空格。（最好代码注释也这么写）\n\n## 许可\n\n在为本项目做出贡献时，您同意您的贡献遵循本项目的[许可证](../LICENSE.md) 。\n\n当您第一次提交拉取请求时，GitHub Action 会提示您签署贡献者许可协议（Contributor License Agreement，CLA）。只有签署了 CLA ，本项目才会合入您的拉取请求。\n\n## 获取帮助\n\n如果您需要帮助或有疑问，可在 GitHub 的[讨论区](https://github.com/XiaoMi/ha_xiaomi_home/discussions/)询问。\n\n您还可以联系 ha_xiaomi_home@xiaomi.com\n"
  },
  {
    "path": "doc/README_zh.md",
    "content": "# Home Assistant 米家集成\n\n[English](../README.md) | [简体中文](./README_zh.md)\n\n米家集成是一个由小米官方提供支持的 Home Assistant 的集成组件，它可以让您在 Home Assistant 中使用小米 IoT 智能设备。\n\n## 安装\n\n> Home Assistant 版本要求：\n>\n> - Core $\\geq$ 2024.4.4\n> - Operating System $\\geq$ 13.0\n\n### 方法 1：使用 git clone 命令从 GitHub 下载\n\n```bash\ncd config\ngit clone https://github.com/XiaoMi/ha_xiaomi_home.git\ncd ha_xiaomi_home\n./install.sh /config\n```\n\n推荐使用此方法安装米家集成。当您想要更新至特定版本时，只需要切换至相应的 Tag 。\n\n例如，更新米家集成版本至 v1.0.0\n\n```bash\ncd config/ha_xiaomi_home\ngit fetch\ngit checkout v1.0.0\n./install.sh /config\n```\n\n### 方法 2: [HACS](https://hacs.xyz/)\n\n一键从 HACS 安装米家集成：\n\n[![打开您的 Home Assistant 实例并打开 Home Assistant 社区商店内的米家集成。](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=XiaoMi&repository=ha_xiaomi_home&category=integration)\n\n或者，HACS > 在搜索框中输入 **Xiaomi Home** > 点击 **Xiaomi Home** ，进入集成详情页  > DOWNLOAD\n\n### 方法 3：通过 [Samba](https://github.com/home-assistant/addons/tree/master/samba) 或 [FTPS](https://github.com/hassio-addons/addon-ftp) 手动安装\n\n下载并将 `custom_components/xiaomi_home` 文件夹复制到 Home Assistant 的 `config/custom_components` 文件夹下。\n\n## 配置\n\n### 登录\n\n[设置 > 设备与服务 > 添加集成](https://my.home-assistant.io/redirect/brand/?brand=xiaomi_home) > 搜索“`Xiaomi Home`” > 下一步 > 请点击此处进行登录 > 使用小米账号登录\n\n[![打开您的 Home Assistant 实例并开始配置一个新的米家集成实例。](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=xiaomi_home)\n\n### 添加 MIoT 设备\n\n登录成功后，会弹出会话框“选择家庭与设备”。您可以选择需要添加的米家家庭，该家庭内的所有设备将导入 Home Assistant 。\n\n### 多账号登录\n\n用一个小米账号登录并配置完成后，您可以在 Xiaomi Home Integration 页面中继续添加其他小米账号。\n\n方法：[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 添加中枢 > 下一步 > 请点击此处进行登录 > 使用小米账号登录\n\n[![打开您的 Home Assistant 实例并显示米家集成。](https://my.home-assistant.io/badges/integration.svg)](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home)\n\n### 修改配置项\n\n在会话框“配置选项”中，可选择需要变更的配置项。您可以修改用户昵称或更新从米家 APP 导入的设备列表。\n\n方法：[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 选择需要变更的配置项\n\n### Action 调试模式\n\n开启该模式后，您可手动向设备发送带参数的 Action 控制指令。发送带参数的 Action 控制指令的用户入口显示为一个文本实体。\n\n方法：[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > Action 调试模式\n\n## 安全性\n\n米家集成及其使用的云端接口由小米官方提供。您需要使用小米账号登录以获取设备列表。米家集成使用 OAuth 2.0 的登录方式，不会在 Home Assistant 中保存您的小米账号密码。但由于 Home Assistant 平台的限制，登录成功后，您的小米用户信息（包括设备信息、证书、 token 等）会明文保存在 Home Assistant 的配置文件中。因此，您需要保管好自己 Home Assistant 配置文件。一旦该文件泄露，其他人可能会冒用您的身份登录。\n\n> 如果您怀疑您的 OAuth 2.0 令牌已泄露，您可以通过以下步骤取消小米账号的登录授权： 米家 APP -> 我的 -> 点击用户名进入小米账号页面 -> 应用授权 -> Xiaomi Home (Home Assistant Integration) -> 取消授权\n\n## 常见问题\n\n- 米家集成是否支持所有的小米米家设备？\n\n  米家集成目前支持大部分米家设备品类，但仍有一小部分设备品类（蓝牙、红外及虚拟设备）并不支持。\n\n- 米家集成是否可以同时使用多个小米账号？\n\n  是的，米家集成支持多个小米账号同时登录。另外，米家集成还支持不同账号的米家设备添加至同一个 Home Assistant 区域。\n\n- 米家集成是否支持本地化控制？\n\n  米家集成支持通过[小米中枢网关](https://www.mi.com/shop/buy/detail?product_id=15755&cfrom=search)（固件版本 3.3.0_0023 及以上）或[内置中枢网关](https://github.com/XiaoMi/ha_xiaomi_home/wiki/Central-hub-gateway-device-models)（软件版本 0.8.9 及以上）的米家设备实现本地化控制。如果没有小米中枢网关或其他带中枢网关功能的设备，那么所有控制指令都会通过小米云发送。支持 Home Assistant 本地化控制的小米中枢网关（含内置中枢网关）的固件已发布。\n\n  小米中枢网关仅在中国大陆可用，在其他地区不可用。\n\n  米家集成也能通过开启小米局域网控制功能实现部分本地化控制效果。小米局域网控制功能只能控制与 Home Assistant 处于同一局域网内的 IP 设备（使用 WiFi、网线连接路由器的设备），无法控制蓝牙 Mesh、ZigBee 等协议接入的设备。该功能可能会引起一些异常，我们建议不要使用该功能。小米局域网控制功能开启方法：[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新局域网控制配置。\n\n  小米局域网控制功能不受地区限制，在全球范围内均可用。如果 Home Assistant 所在的局域网内存在中枢网关，那么即便米家集成开启了小米局域网控制功能，该功能也不会生效。\n\n- 米家集成在哪些地区可用？\n\n  米家集成所用的云服务接口已部署在中国大陆、欧洲、印度、俄罗斯、新加坡、美国共六个地区的机房。由于用户数据在不同地区的小米云上相互隔离，您需要在配置 Home Assistant 时选择用户所在地区，才能导入相应的米家设备。米家集成支持将不同地区的米家设备添加至同一个 Home Assistant 区域。\n\n## 消息收发原理\n\n### 云端控制\n\n<div align=center>\n<img src=\"./images/cloud_control_zh.jpg\" width=300>\n\n图 1：云端控制架构\n\n</div>\n\n米家集成向小米云 MQTT Broker 订阅关注的设备消息。当设备属性发生改变或产生设备事件时，设备向小米云发送上行消息， MQTT Broker 向米家集成推送订阅的设备消息。由于米家集成不需要向云端轮询以获取设备当前的属性值，因此米家集成能第一时间获知设备属性变化或事件发生。得益于消息订阅机制，米家集成只在配置完成时向云端查询一次所有的设备属性，对云端产生的访问压力很小。\n\n米家集成需要控制设备时，通过小米云 HTTP 接口向设备发送控制消息。设备收到小米云发来的下行消息后做出响应。\n\n### 本地控制\n\n<div align=center>\n<img src=\"./images/local_control_zh.jpg\" width=300>\n\n图 2：本地控制架构\n\n</div>\n\n小米中枢网关内包含一个标准的 MQTT Broker ，实现了完整的订阅发布机制。米家集成向小米中枢网关订阅关注的设备消息。当设备属性发生改变或产生设备事件时，设备向小米中枢网关发送上行消息， MQTT Broker 向米家集成推送订阅的设备消息。\n\n米家集成需要控制设备时，向 MQTT Broker 发布设备控制消息，再经由小米中枢网关转发给设备。设备收到小米中枢网关发来的下行消息后做出响应。\n\n## MIoT-Spec-V2 与 Home Assistant 实体的映射关系\n\n[MIoT-Spec-V2](https://iot.mi.com/v2/new/doc/introduction/knowledge/spec) 的全称为 MIoT Specification Version 2 ，是小米 IoT 平台制订的物联网协议，用于对 IoT 设备进行规范化的功能性描述，其中包含功能定义（其他 IoT 平台称之为物模型）、交互模型、消息格式以及编码。\n\n在 MIoT-Spec-V2 中，一个产品定义为一个设备，一个设备包含若干服务，一个服务包含若干属性、方法和事件。米家集成根据 MIoT-Spec-V2 生成对应的 Home Assistant 实体。\n\n具体的转换关系如下：\n\n### 一般转换规则\n\n- 属性（Property）\n\n| access（访问方式） | format（数据格式） | value-list（取值列表） | value-range（取值范围） | 转换后的实体 |\n| ------------------ | ------------------ | ---------------------- | ----------------------- | ------------ |\n| 可写               | string             | -                      | -                       | Text         |\n| 可写               | bool               | -                      | -                       | Switch       |\n| 可写               | 非 string、非 bool | 有                     | -                       | Select       |\n| 可写               | 非 string、非 bool | 无                     | 有                      | Number       |\n| 不可写             | -                  | -                      | -                       | Sensor       |\n\n- 事件（Event）\n\n转换后的实体为 Event，事件参数同时传递给实体的 `_trigger_event` 。\n\nMIoT-Spec-V2 事件的 arguments 字段是事件的参数列表，列表元素代表同服务下属性的 piid 。例如，小米智能无线开关（双开）的 [MIoT-Spec-V2](http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:remote-control:0000A021:xiaomi-mcn002:1:0000D057)的 siid=2 无线开关服务下包含 eiid=1014 长按事件，该事件触发时会携带一个 piid=2 的按键类型属性作为事件参数， debug 等级日志会打印 `长按, attributes: {'按键类型': 1}` (日志示例，按键类型为 1 表示右键触发了长按事件)。\n\n- 方法（Action）\n\n| in（输入参数列表） | 转换后的实体 |\n| ------------------ | ------------ |\n| 空                 | Button       |\n| 非空               | Notify       |\n\n如果开启了“Action 调试模式”，方法的 in 字段为非空时，还会生成 Text 实体。\n\nNotify 实体详情页中 Attributes 会显示输入参数的格式。输入参数为有序的列表，英文中括号[]包括。字符串元素由英文双引号\"\"包括。\n\n例如， xiaomi.wifispeaker.s12 siid=5 aiid=5 方法（ Intelligent Speaker Execute Text Directive ）在 Notify 实体详情页显示的输入参数格式为 `[Text Content(str), Silent Execution(bool)]` ，使用的输入参数可以是 `[\"Hello\", true]` 。\n\n### 特殊转换规则\n\nMIoT-Spec-V2 定义类型使用的 URN 格式为 `urn:<namespace>:<type>:<name>:<value>[:<vendor-product>:<version>]`，其中 `name` 是用于描述实例（设备、服务、属性、事件、方法）的有意义的单词或词组。米家集成先用实例名称（ name ）判断是否将 MIoT-Spec-V2 实例转换成特定的 Home Assistant 实体。对于不符合特殊转换规则的 MIoT-Spec-V2 实例，再使用一般转换规则进行转换。\n\n`namespace` 用于描述实例所属的命名空间，取值为“miot-spec-v2”表示小米定义的规范， 取值为“bluetooth-spec”表示蓝牙联盟定义的规范，其他则为厂商自定义的规范。当 `namespace` 不是“miot-spec-v2”时，转换后的实体名称前会显示一个星号\\*。\n\n- 设备（Device）\n\n转换规则为 `SPEC_DEVICE_TRANS_MAP` ：\n\n```\n{\n    '<device instance name>':{\n        'required':{\n            '<service instance name>':{\n                'required':{\n                    'properties': {\n                        '<property instance name>': set<property access: str>\n                    },\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                },\n                'optional':{\n                    'properties': set<property instance name: str>,\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                }\n            }\n        },\n        'optional':{\n            '<service instance name>':{\n                'required':{\n                    'properties': {\n                        '<property instance name>': set<property access: str>\n                    },\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                },\n                'optional':{\n                    'properties': set<property instance name: str>,\n                    'events': set<event instance name: str>,\n                    'actions': set<action instance name: str>\n                }\n            }\n        },\n        'entity': str\n    }\n}\n```\n\ndevice instance name 下的 required 表示必须包含的服务， optional 表示可选的服务， entity 表示转换后的实体。 service instance name 下的 required 表示必须包含的属性、事件、方法，optional 表示可选的属性、事件、方法。 required 、 properties 域下的 property instance name 的值表示属性的访问方式，匹配成功的条件是 property instance name 的值是相应 MIoT-Spec-V2 属性实例的访问方式的子集。\n\n如果 MIoT-Spec-V2 设备实例缺少必选的服务、属性、事件、方法，则不会生成 Home Assistant 实体。\n\n- 服务（Service）\n\n转换规则为 `SPEC_SERVICE_TRANS_MAP` ：\n\n```\n{\n    '<service instance name>':{\n        'required':{\n            'properties': {\n                '<property instance name>': set<property access: str>\n            },\n            'events': set<event instance name: str>,\n            'actions': set<action instance name: str>\n        },\n        'optional':{\n            'properties': set<property instance name: str>,\n            'events': set<event instance name: str>,\n            'actions': set<action instance name: str>\n        },\n        'entity': str\n    }\n}\n```\n\nservice instance name 下的 required 表示必须包含的属性、事件、方法，optional 表示可选的属性、事件、方法， entity 表示转换后的实体。 required 、 properties 域下的 property instance name 的值表示属性的访问方式，匹配成功的条件是 property instance name 的值是相应 MIoT-Spec-V2 属性实例的访问方式的子集。\n\n如果 MIoT-Spec-V2 服务实例缺少必选的属性、事件、方法，则不会生成 Home Assistant 实体。\n\n- 属性（Property）\n\n转换规则为 `SPEC_PROP_TRANS_MAP` ：\n\n```\n{\n    'entities':{\n        '<entity name>':{\n            'format': set<str>,\n            'access': set<str>\n        }\n    },\n    'properties': {\n        '<property instance name>':{\n            'device_class': str,\n            'entity': str\n        }\n    }\n}\n```\n\nentity name 下的 format 表示属性的数据格式，匹配上一个值即为匹配成功； access 表示属性的访问方式，匹配上所有值才算匹配成功。\n\nproperty instance name 下的 entity 表示转换后的实体，取值为 entities 下的 entity name ； device_class 表示转换后实体所用的 `_attr_device_class` 。\n\n- 事件（Event）\n\n转换规则为 `SPEC_EVENT_TRANS_MAP` ：\n\n```\n{\n    '<event instance name>': str\n}\n```\n\nevent instance name 下的值表示转换后实体所用的 `_attr_device_class` 。\n\n### MIoT-Spec-V2 过滤规则\n\n`spec_filter.yaml` 用于过滤掉不需要的 MIoT-Spec-V2 实例，过滤掉的实例不会转换成 Home Assistant 实体。\n\n`spec_filter.yaml`的格式如下：\n\n```yaml\n<MIoT-Spec-V2 device instance urn without the version field>:\n    services: list<service_iid: str>\n    properties: list<service_iid.property_iid: str>\n    events: list<service_iid.event_iid: str>\n    actions: list<service_iid.action_iid: str>\n```\n\n`spec_filter.yaml` 的键值为 MIoT-Spec-V2 设备实例的 urn （不含版本号“version”字段）。一个产品的不同版本的固件可能会关联不同版本的 MIoT-Spec-V2 设备实例。 MIoT 平台要求厂商定义产品的 MIoT-Spec-V2 时，高版本的 MIoT-Spec-V2 实例必须包含全部低版本的 MIoT-Spec-V2 实例。因此， `spec_filter.yaml` 的键值不需要指定设备实例的版本号。\n\n设备实例下的 services 、 properties 、 events 、 actions 域的值表示需要过滤掉的服务、属性、事件、方法的实例号（ iid ，即 instance id ）。支持通配符匹配。\n\n示例：\n\n```yaml\nurn:miot-spec-v2:device:television:0000A010:xiaomi-rmi1:\n    services:\n    - '*'   # 排除所有服务，相当于排除拥有该 MIoT-Spec-V2 的设备。\nurn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:\n    services:\n    - '3'   # 排除 siid=3 的服务。\n    properties:\n    - '4.*' # 排除 siid=4 服务的所有属性。\n    events:\n    - '4.1' # 排除 siid=4 服务的 eiid=1 的事件。\n    actions:\n    - '4.1' # 排除 siid=4 服务的 aiid=1 的方法。\n```\n\n所有设备的设备信息服务（ urn:miot-spec-v2:service:device-information:00007801 ）均不会生成 Home Assistant 实体。\n\n## 多语言支持\n\n米家集成配置选项中可选择的集成使用的语言有简体中文、繁体中文、英文、西班牙语、俄语、法语、德语、日语、意大利语、荷兰语、葡萄牙语、巴西葡萄牙语、土耳其语这十三种语言。目前，米家集成配置页面的简体中文和英文已经过人工校审，其他语言由机器翻译或社区贡献。如果您希望修改配置页面的词句，则需要修改 `custom_components/xiaomi_home/translations/` 以及 `custom_components/xiaomi_home/miot/i18n/` 目录下相应语言的 json 文件。\n\n在显示 Home Assistant 实体名称时，米家集成会从小米云下载设备厂商为设备配置的多语言文件，该文件包含设备 MIoT-Spec-V2 实例的多语言翻译。 `multi_lang.json` 是本地维护的多语言配置字典，其优先级高于从云端获取的多语言文件，可用于补充或修改设备的多语言翻译。\n\n`multi_lang.json` 的格式如下：\n\n```\n{\n    \"<MIoT-Spec-V2 device instance>\": {\n        \"<language code>\": {\n            \"<instance code>\": <translation: str>\n        }\n    }\n}\n```\n\n`multi_lang.json` 的键值为 MIoT-Spec-V2 设备实例的 urn （不含版本号“version”字段）。\n\nlanguage code 为语言代码，取值为 zh-Hans、zh-Hant、en、es、ru、fr、de、ja、it、nl、pt、pt-BR、tr（对应上述米家集成可选的十三种语言）。\n\ninstance code 为 MIoT-Spec-V2 实例代码，格式如下：\n\n```\nservice:<siid>                  # 服务\nservice:<siid>:property:<piid>  # 属性\nservice:<siid>:property:<piid>:valuelist:<value> # 属性取值列表的索引值\nservice:<siid>:event:<eiid>     # 事件\nservice:<siid>:action:<aiid>    # 方法\n```\n\nsiid、piid、eiid、aiid、value 均为十进制三位整数。\n\n示例：\n\n```\n{\n    \"urn:miot-spec-v2:device:health-pot:0000A051:chunmi-a1\": {\n        \"zh-Hant\": {\n            \"service:002\": \"養生壺\",\n            \"service:002:property:001\": \"工作狀態\",\n            \"service:002:property:001:valuelist:000\": \"待機中\",\n            \"service:002:action:002\": \"停止烹飪\",\n            \"service:005:event:001\": \"烹飪完成\"\n        }\n    }\n}\n```\n\n> 在 Home Assistant 中修改了 `custom_components/xiaomi_home/miot/specs` 路径下的任何文件（`spec_filter.yaml`、`spec_modify.yaml`、`multi_lang.json`等），需要在集成配置中更新实体转换规则才能生效。方法：[设置 > 设备与服务 > 已配置 > Xiaomi Home](https://my.home-assistant.io/redirect/integration/?domain=xiaomi_home) > 配置 > 更新实体转换规则\n\n## 文档\n\n- [许可证](../LICENSE.md)\n- 贡献指南： [English](../CONTRIBUTING.md) | [简体中文](./CONTRIBUTING_zh.md)\n- [更新日志](../CHANGELOG.md)\n- 开发文档： https://developers.home-assistant.io/docs/creating_component_index\n- [常见问题](https://github.com/XiaoMi/ha_xiaomi_home/wiki)\n\n## 目录结构\n\n- miot：核心代码。\n- miot/miot_client：每添加一个用户需要增加一个 miot_client 实例。\n- miot/miot_cloud：云服务相关功能，包括 OAuth 登录、 HTTP 接口功能（获取用户信息、发送设备控制指令等）。\n- miot/miot_device：设备实体，包含设备信息以及属性、事件、方法的处理逻辑。\n- miot/miot_mips：消息总线，用于订阅和发布消息。\n- miot/miot_spec：解析 MIoT-Spec-V2 。\n- miot/miot_lan: 设备局域网控制，包括设备发现、设备控制等。\n- miot/miot_mdns: 中枢网关服务局域网发现。\n- miot/miot_network：获取网络状态和网络信息。\n- miot/miot_storage: 集成文件存储。\n- miot/test：测试脚本。\n- config_flow：配置流程。\n"
  },
  {
    "path": "hacs.json",
    "content": "{\n    \"name\": \"Xiaomi Home\",\n    \"homeassistant\": \"2024.4.4\",\n    \"hacs\": \"1.34.0\"\n}\n"
  },
  {
    "path": "install.sh",
    "content": "#!/bin/bash\nset -e\n\n# Check the number of input parameters.\nif [ $# -ne 1 ]; then\n    echo \"usage: $0 [config_path]\"\n    exit 1\nfi\n# Get the config path.\nconfig_path=$1\n# Check if config path exists.\nif [ ! -d \"$config_path\" ]; then\n    echo \"$config_path does not exist\"\n    exit 1\nfi\n\n# Get the script path.\nscript_path=$(dirname \"$0\")\n\n# Set source and target\ncomponent_name=xiaomi_home\nsource_path=\"$script_path/custom_components/$component_name\"\ntarget_root=\"$config_path/custom_components\"\ntarget_path=\"$target_root/$component_name\"\n\n# Remove the old version.\nrm -rf \"$target_path\"\n\n# Copy the new version.\nmkdir -p \"$target_root\"\ncp -r \"$source_path\" \"$target_path\"\n\n# Done.\necho \"Xiaomi Home installation is completed. Please restart Home Assistant.\"\nexit 0\n"
  },
  {
    "path": "test/.gitignore",
    "content": "miot\ntest_cache"
  },
  {
    "path": "test/check_rule_format.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Test rule format.\"\"\"\nimport json\nimport logging\nfrom os import listdir, path\nfrom typing import Optional\nimport pytest\nimport yaml\n\n_LOGGER = logging.getLogger(__name__)\n\nROOT_PATH: str = path.dirname(path.abspath(__file__))\nTRANS_RELATIVE_PATH: str = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/translations')\nMIOT_I18N_RELATIVE_PATH: str = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n')\nSPEC_BOOL_TRANS_FILE = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/bool_trans.yaml')\nSPEC_FILTER_FILE = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_filter.yaml')\nSPEC_MULTI_LANG_FILE = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/multi_lang.json')\nSPEC_ADD_FILE = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_add.json')\nSPEC_MODIFY_FILE = path.join(\n    ROOT_PATH, '../custom_components/xiaomi_home/miot/specs/spec_modify.yaml')\n\n\ndef load_json_file(file_path: str) -> Optional[dict]:\n    try:\n        with open(file_path, 'r', encoding='utf-8') as file:\n            return json.load(file)\n    except FileNotFoundError:\n        _LOGGER.info('%s is not found.', file_path)\n        return None\n    except json.JSONDecodeError:\n        _LOGGER.info('%s is not a valid JSON file.', file_path)\n        return None\n\n\ndef save_json_file(file_path: str, data: dict) -> None:\n    with open(file_path, 'w', encoding='utf-8') as file:\n        json.dump(data, file, ensure_ascii=False, indent=2)\n\n\ndef load_yaml_file(file_path: str) -> Optional[dict]:\n    try:\n        with open(file_path, 'r', encoding='utf-8') as file:\n            return yaml.safe_load(file)\n    except FileNotFoundError:\n        _LOGGER.info('%s is not found.', file_path)\n        return None\n    except yaml.YAMLError:\n        _LOGGER.info('%s, is not a valid YAML file.', file_path)\n        return None\n\n\ndef save_yaml_file(file_path: str, data: dict) -> None:\n    with open(file_path, 'w', encoding='utf-8') as file:\n        yaml.safe_dump(data,\n                       file,\n                       default_flow_style=False,\n                       allow_unicode=True,\n                       indent=2,\n                       sort_keys=False)\n\n\ndef dict_str_str(d: dict) -> bool:\n    \"\"\"restricted format: dict[str, str]\"\"\"\n    if not isinstance(d, dict):\n        return False\n    for k, v in d.items():\n        if not isinstance(k, str) or not isinstance(v, str):\n            return False\n    return True\n\n\ndef dict_str_dict(d: dict) -> bool:\n    \"\"\"restricted format: dict[str, dict]\"\"\"\n    if not isinstance(d, dict):\n        return False\n    for k, v in d.items():\n        if not isinstance(k, str) or not isinstance(v, dict):\n            return False\n    return True\n\n\ndef nested_2_dict_str_str(d: dict) -> bool:\n    \"\"\"restricted format: dict[str, dict[str, str]]\"\"\"\n    if not dict_str_dict(d):\n        return False\n    for v in d.values():\n        if not dict_str_str(v):\n            return False\n    return True\n\n\ndef nested_3_dict_str_str(d: dict) -> bool:\n    \"\"\"restricted format: dict[str, dict[str, dict[str, str]]]\"\"\"\n    if not dict_str_dict(d):\n        return False\n    for v in d.values():\n        if not nested_2_dict_str_str(v):\n            return False\n    return True\n\n\ndef spec_filter(d: dict) -> bool:\n    \"\"\"restricted format: dict[str, dict[str, list<str>]]\"\"\"\n    if not dict_str_dict(d):\n        return False\n    for value in d.values():\n        for k, v in value.items():\n            if not isinstance(k, str) or not isinstance(v, list):\n                return False\n            if not all(isinstance(i, str) for i in v):\n                return False\n    return True\n\n\ndef bool_trans(d: dict) -> bool:\n    \"\"\"dict[str,  dict[str, str] | dict[str, dict[str, str]] ]\"\"\"\n    if not isinstance(d, dict):\n        return False\n    if 'data' not in d or 'translate' not in d:\n        return False\n    if not dict_str_str(d['data']):\n        return False\n    if not nested_3_dict_str_str(d['translate']):\n        return False\n    default_trans: dict = d['translate'].pop('default')\n    if not default_trans:\n        _LOGGER.info('default trans is empty')\n        return False\n    default_keys: set[str] = set(default_trans.keys())\n    for key, trans in d['translate'].items():\n        trans_keys: set[str] = set(trans.keys())\n        if set(trans.keys()) != default_keys:\n            _LOGGER.info('bool trans inconsistent, %s, %s, %s', key,\n                         default_keys, trans_keys)\n            return False\n    return True\n\n\ndef multi_lang(data: dict) -> bool:\n    \"\"\"dict[str, dict[str, dict[str, str]]]\"\"\"\n    for key in data.keys():\n        if key.count(':') != 5:\n            return False\n    return nested_3_dict_str_str(data)\n\n\ndef spec_add(data: dict) -> bool:\n    \"\"\"dict[str, list[dict[str, int| str | list]]]\"\"\"\n    if not isinstance(data, dict):\n        return False\n    for urn, content in data.items():\n        if not isinstance(urn, str) or not isinstance(content, (list, str)):\n            return False\n        if isinstance(content, str):\n            continue\n        for service in content:\n            if ('iid' not in service) or ('type' not in service) or (\n                    'description'\n                    not in service) or (('properties' not in service) and\n                                        ('actions' not in service) and\n                                        ('events' not in service)):\n                return False\n            type_strs: list[str] = service['type'].split(':')\n            if type_strs[1] != 'miot-spec-v2':\n                return False\n            if 'properties' in service:\n                if not isinstance(service['properties'], list):\n                    return False\n                for prop in service['properties']:\n                    if ('iid' not in prop) or ('type' not in prop) or (\n                            'description' not in prop) or (\n                                'format' not in prop) or ('access' not in prop):\n                        return False\n                    if not isinstance(prop['iid'], int) or not isinstance(\n                            prop['type'], str) or not isinstance(\n                                prop['description'], str) or not isinstance(\n                                    prop['format'], str) or not isinstance(\n                                        prop['access'], list):\n                        return False\n                    type_strs = prop['type'].split(':')\n                    if type_strs[1] != 'miot-spec-v2':\n                        return False\n                    for access in prop['access']:\n                        if access not in ['read', 'write', 'notify']:\n                            return False\n                    if 'value-range' in prop:\n                        if not isinstance(prop['value-range'], list):\n                            return False\n                        for value in prop['value-range']:\n                            if not isinstance(value, (int, float)):\n                                return False\n                    if 'value-list' in prop:\n                        if not isinstance(prop['value-list'], list):\n                            return False\n                        for item in prop['value-list']:\n                            if 'value' not in item or 'description' not in item:\n                                return False\n                            if not isinstance(item['value'],\n                                              int) or not isinstance(\n                                                  item['description'], str):\n                                return False\n            if 'actions' in service:\n                if not isinstance(service['actions'], list):\n                    return False\n                for action in service['actions']:\n                    if ('iid' not in action) or ('type' not in action) or (\n                            'description' not in action) or (\n                                'in' not in action) or ('out' not in action):\n                        return False\n                    if not isinstance(action['iid'], int) or not isinstance(\n                            action['type'], str) or not isinstance(\n                                action['description'], str) or not isinstance(\n                                    action['in'], list) or not isinstance(\n                                        action['out'], list):\n                        return False\n                    type_strs = action['type'].split(':')\n                    if type_strs[1] != 'miot-spec-v2':\n                        return False\n                    for param in action['in']:\n                        if not isinstance(param, int):\n                            return False\n                    for param in action['out']:\n                        if not isinstance(param, int):\n                            return False\n            if 'events' in service:\n                if not isinstance(service['events'], list):\n                    return False\n                for event in service['events']:\n                    if ('iid' not in event) or ('type' not in event) or (\n                            'description' not in event) or ('arguments'\n                                                            not in event):\n                        return False\n                    if not isinstance(event['iid'], int) or not isinstance(\n                            event['type'], str) or not isinstance(\n                                event['description'], str) or not isinstance(\n                                    event['arguments'], list):\n                        return False\n                    type_strs = event['type'].split(':')\n                    if type_strs[1] != 'miot-spec-v2':\n                        return False\n                    for param in event['arguments']:\n                        if not isinstance(param, int):\n                            return False\n    return True\n\n\ndef spec_modify(data: dict) -> bool:\n    \"\"\"dict[str, str | dict[str, dict]]\"\"\"\n    if not isinstance(data, dict):\n        return False\n    for urn, content in data.items():\n        if not isinstance(urn, str) or not isinstance(content, (dict, str)):\n            return False\n        if isinstance(content, str):\n            continue\n        for key, value in content.items():\n            if not isinstance(key, str) or not isinstance(value, dict):\n                return False\n    return True\n\n\ndef compare_dict_structure(dict1: dict, dict2: dict) -> bool:\n    if not isinstance(dict1, dict) or not isinstance(dict2, dict):\n        _LOGGER.info('invalid type')\n        return False\n    if dict1.keys() != dict2.keys():\n        _LOGGER.info('inconsistent key values, %s, %s', dict1.keys(),\n                     dict2.keys())\n        return False\n    for key in dict1:\n        if isinstance(dict1[key], dict) and isinstance(dict2[key], dict):\n            if not compare_dict_structure(dict1[key], dict2[key]):\n                _LOGGER.info('inconsistent key values, dict, %s', key)\n                return False\n        elif isinstance(dict1[key], list) and isinstance(dict2[key], list):\n            if not all(\n                    isinstance(i, type(j))\n                    for i, j in zip(dict1[key], dict2[key])):\n                _LOGGER.info('inconsistent key values, list, %s', key)\n                return False\n        elif not isinstance(dict1[key], type(dict2[key])):\n            _LOGGER.info('inconsistent key values, type, %s', key)\n            return False\n    return True\n\n\ndef sort_bool_trans(file_path: str):\n    trans_data = load_yaml_file(file_path=file_path)\n    assert isinstance(trans_data, dict), f'{file_path} format error'\n    trans_data['data'] = dict(sorted(trans_data['data'].items()))\n    for key, trans in trans_data['translate'].items():\n        trans_data['translate'][key] = dict(sorted(trans.items()))\n    return trans_data\n\n\ndef sort_spec_filter(file_path: str):\n    filter_data = load_yaml_file(file_path=file_path)\n    assert isinstance(filter_data, dict), f'{file_path} format error'\n    filter_data = dict(sorted(filter_data.items()))\n    for urn, spec in filter_data.items():\n        filter_data[urn] = dict(sorted(spec.items()))\n    return filter_data\n\n\ndef sort_spec_add(file_path: str):\n    filter_data = load_json_file(file_path=file_path)\n    assert isinstance(filter_data, dict), f'{file_path} format error'\n    return dict(sorted(filter_data.items()))\n\n\ndef sort_multi_lang(file_path: str):\n    return sort_spec_add(file_path)\n\n\ndef sort_spec_modify(file_path: str):\n    filter_data = load_yaml_file(file_path=file_path)\n    assert isinstance(filter_data, dict), f'{file_path} format error'\n    return dict(sorted(filter_data.items()))\n\n\n@pytest.mark.github\ndef test_bool_trans():\n    data = load_yaml_file(SPEC_BOOL_TRANS_FILE)\n    assert isinstance(data, dict)\n    assert data, f'load {SPEC_BOOL_TRANS_FILE} failed'\n    assert bool_trans(data), f'{SPEC_BOOL_TRANS_FILE} format error'\n\n\n@pytest.mark.github\ndef test_spec_filter():\n    data = load_yaml_file(SPEC_FILTER_FILE)\n    assert isinstance(data, dict)\n    assert data, f'load {SPEC_FILTER_FILE} failed'\n    assert spec_filter(data), f'{SPEC_FILTER_FILE} format error'\n\n\n@pytest.mark.github\ndef test_multi_lang():\n    data = load_json_file(SPEC_MULTI_LANG_FILE)\n    assert isinstance(data, dict)\n    assert data, f'load {SPEC_MULTI_LANG_FILE} failed'\n    assert multi_lang(data), f'{SPEC_MULTI_LANG_FILE} format error'\n\n\n@pytest.mark.github\ndef test_spec_add():\n    data = load_json_file(SPEC_ADD_FILE)\n    assert isinstance(data, dict)\n    assert data, f'load {SPEC_ADD_FILE} failed'\n    assert spec_add(data), f'{SPEC_ADD_FILE} format error'\n\n\n@pytest.mark.github\ndef test_spec_modify():\n    data = load_yaml_file(SPEC_MODIFY_FILE)\n    assert isinstance(data, dict)\n    assert data, f'load {SPEC_MODIFY_FILE} failed'\n    assert spec_modify(data), f'{SPEC_MODIFY_FILE} format error'\n\n\n@pytest.mark.github\ndef test_miot_i18n():\n    for file_name in listdir(MIOT_I18N_RELATIVE_PATH):\n        file_path: str = path.join(MIOT_I18N_RELATIVE_PATH, file_name)\n        data = load_json_file(file_path)\n        assert isinstance(data, dict)\n        assert data, f'load {file_path} failed'\n        assert nested_3_dict_str_str(data), f'{file_path} format error'\n\n\n@pytest.mark.github\ndef test_translations():\n    for file_name in listdir(TRANS_RELATIVE_PATH):\n        file_path: str = path.join(TRANS_RELATIVE_PATH, file_name)\n        data = load_json_file(file_path)\n        assert isinstance(data, dict)\n        assert data, f'load {file_path} failed'\n        assert dict_str_dict(data), f'{file_path} format error'\n\n\n@pytest.mark.github\ndef test_miot_lang_integrity():\n    # pylint: disable=import-outside-toplevel\n    from miot.const import INTEGRATION_LANGUAGES\n    integration_lang_list: list[str] = [\n        f'{key}.json' for key in list(INTEGRATION_LANGUAGES.keys())\n    ]\n    translations_names: set[str] = set(listdir(TRANS_RELATIVE_PATH))\n    assert len(translations_names) == len(integration_lang_list)\n    assert translations_names == set(integration_lang_list)\n    i18n_names: set[str] = set(listdir(MIOT_I18N_RELATIVE_PATH))\n    assert len(i18n_names) == len(translations_names)\n    assert i18n_names == translations_names\n    bool_trans_data = load_yaml_file(SPEC_BOOL_TRANS_FILE)\n    assert isinstance(bool_trans_data, dict)\n    bool_trans_names: set[str] = set(\n        bool_trans_data['translate']['default'].keys())\n    assert len(bool_trans_names) == len(translations_names)\n    # Check translation files structure\n    default_dict = load_json_file(\n        path.join(TRANS_RELATIVE_PATH, integration_lang_list[0]))\n    for name in list(integration_lang_list)[1:]:\n        compare_dict = load_json_file(path.join(TRANS_RELATIVE_PATH, name))\n        if not compare_dict_structure(default_dict, compare_dict):\n            _LOGGER.info('compare_dict_structure failed /translations, %s',\n                         name)\n            assert False\n    # Check i18n files structure\n    default_dict = load_json_file(\n        path.join(MIOT_I18N_RELATIVE_PATH, integration_lang_list[0]))\n    for name in list(integration_lang_list)[1:]:\n        compare_dict = load_json_file(path.join(MIOT_I18N_RELATIVE_PATH, name))\n        if not compare_dict_structure(default_dict, compare_dict):\n            _LOGGER.info('compare_dict_structure failed /miot/i18n, %s', name)\n            assert False\n\n\n@pytest.mark.github\ndef test_miot_data_sort():\n    # pylint: disable=import-outside-toplevel\n    from miot.const import INTEGRATION_LANGUAGES\n    sort_langs: dict = dict(sorted(INTEGRATION_LANGUAGES.items()))\n    assert list(INTEGRATION_LANGUAGES.keys()) == list(sort_langs.keys()), (\n        'INTEGRATION_LANGUAGES not sorted, correct order\\r\\n'\n        f'{list(sort_langs.keys())}')\n    assert json.dumps(\n        load_yaml_file(file_path=SPEC_BOOL_TRANS_FILE)) == json.dumps(\n            sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)), (\n                f'{SPEC_BOOL_TRANS_FILE} not sorted, goto project root path'\n                ' and run the following command sorting, ',\n                'pytest -s -v -m update ./test/check_rule_format.py')\n    assert json.dumps(load_yaml_file(file_path=SPEC_FILTER_FILE)) == json.dumps(\n        sort_spec_filter(file_path=SPEC_FILTER_FILE)), (\n            f'{SPEC_FILTER_FILE} not sorted, goto project root path'\n            ' and run the following command sorting, ',\n            'pytest -s -v -m update ./test/check_rule_format.py')\n    assert json.dumps(\n        load_json_file(file_path=SPEC_MULTI_LANG_FILE)) == json.dumps(\n            sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)), (\n                f'{SPEC_MULTI_LANG_FILE} not sorted, goto project root path'\n                ' and run the following command sorting, ',\n                'pytest -s -v -m update ./test/check_rule_format.py')\n    assert json.dumps(load_json_file(file_path=SPEC_ADD_FILE)) == json.dumps(\n        sort_spec_add(file_path=SPEC_ADD_FILE)), (\n            f'{SPEC_ADD_FILE} not sorted, goto project root path'\n            ' and run the following command sorting, ',\n            'pytest -s -v -m update ./test/check_rule_format.py')\n    assert json.dumps(load_yaml_file(file_path=SPEC_MODIFY_FILE)) == json.dumps(\n        sort_spec_modify(file_path=SPEC_MODIFY_FILE)), (\n            f'{SPEC_MODIFY_FILE} not sorted, goto project root path'\n            ' and run the following command sorting, ',\n            'pytest -s -v -m update ./test/check_rule_format.py')\n\n\n@pytest.mark.update\ndef test_sort_spec_data():\n    sort_data: dict = sort_bool_trans(file_path=SPEC_BOOL_TRANS_FILE)\n    save_yaml_file(file_path=SPEC_BOOL_TRANS_FILE, data=sort_data)\n    _LOGGER.info('%s formatted.', SPEC_BOOL_TRANS_FILE)\n    sort_data = sort_spec_filter(file_path=SPEC_FILTER_FILE)\n    save_yaml_file(file_path=SPEC_FILTER_FILE, data=sort_data)\n    _LOGGER.info('%s formatted.', SPEC_FILTER_FILE)\n    sort_data = sort_multi_lang(file_path=SPEC_MULTI_LANG_FILE)\n    save_json_file(file_path=SPEC_MULTI_LANG_FILE, data=sort_data)\n    sort_data = sort_spec_add(file_path=SPEC_ADD_FILE)\n    save_json_file(file_path=SPEC_ADD_FILE, data=sort_data)\n    _LOGGER.info('%s formatted.', SPEC_ADD_FILE)\n    sort_data = sort_spec_modify(file_path=SPEC_MODIFY_FILE)\n    save_yaml_file(file_path=SPEC_MODIFY_FILE, data=sort_data)\n    _LOGGER.info('%s formatted.', SPEC_MODIFY_FILE)\n"
  },
  {
    "path": "test/conftest.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Pytest fixtures.\"\"\"\nimport logging\nimport random\nimport shutil\nimport pytest\nfrom os import path, makedirs\nfrom uuid import uuid4\n\nTEST_ROOT_PATH: str = path.dirname(path.abspath(__file__))\nTEST_FILES_PATH: str = path.join(TEST_ROOT_PATH, 'miot')\nTEST_CACHE_PATH: str = path.join(TEST_ROOT_PATH, 'test_cache')\nTEST_OAUTH2_REDIRECT_URL: str = 'http://homeassistant.local:8123'\nTEST_LANG: str = 'zh-Hans'\nTEST_UID: str = '123456789'\nTEST_CLOUD_SERVER: str = 'cn'\n\nDOMAIN_CLOUD_CACHE: str = 'cloud_cache'\n\n_LOGGER = logging.getLogger(__name__)\n\n\n@pytest.fixture(scope='session', autouse=True)\ndef set_logger():\n    logger = logging.getLogger()\n    logger.setLevel(logging.INFO)\n    console_handler = logging.StreamHandler()\n    console_handler.setLevel(logging.INFO)\n    formatter = logging.Formatter(\n        '%(asctime)s - %(name)s - %(levelname)s - %(message)s')\n    console_handler.setFormatter(formatter)\n    logger.addHandler(console_handler)\n    _LOGGER.info('set logger, %s', logger)\n\n\n@pytest.fixture(scope='session', autouse=True)\ndef load_py_file():\n    # Copy py file to test folder\n    file_list = [\n        'common.py',\n        'const.py',\n        'miot_cloud.py',\n        'miot_error.py',\n        'miot_i18n.py',\n        'miot_lan.py',\n        'miot_mdns.py',\n        'miot_mips.py',\n        'miot_network.py',\n        'miot_spec.py',\n        'miot_storage.py']\n    makedirs(TEST_CACHE_PATH, exist_ok=True)\n    makedirs(TEST_FILES_PATH, exist_ok=True)\n    for file_name in file_list:\n        shutil.copyfile(\n            path.join(\n                TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot',\n                file_name),\n            path.join(TEST_FILES_PATH, file_name))\n    _LOGGER.info('\\nloaded test py files, %s', file_list)\n    # Copy spec files to test folder\n    shutil.copytree(\n        src=path.join(\n            TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/specs'),\n        dst=path.join(TEST_FILES_PATH, 'specs'),\n        dirs_exist_ok=True)\n    _LOGGER.info('loaded spec test folder, specs')\n    # Copy lan files to test folder\n    shutil.copytree(\n        src=path.join(\n            TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/lan'),\n        dst=path.join(TEST_FILES_PATH, 'lan'),\n        dirs_exist_ok=True)\n    _LOGGER.info('loaded lan test folder, lan')\n    # Copy i18n files to test folder\n    shutil.copytree(\n        src=path.join(\n            TEST_ROOT_PATH, '../custom_components/xiaomi_home/miot/i18n'),\n        dst=path.join(TEST_FILES_PATH, 'i18n'),\n        dirs_exist_ok=True)\n    _LOGGER.info('loaded i18n test folder, i18n')\n\n    yield\n\n    # NOTICE: All test files and data (tokens, device information, etc.) will\n    # be deleted after the test is completed. For some test cases that\n    # require caching data, you can comment out the following code.\n\n    if path.exists(TEST_FILES_PATH):\n        shutil.rmtree(TEST_FILES_PATH)\n        print('\\nremoved test files, ', TEST_FILES_PATH)\n\n    if path.exists(TEST_CACHE_PATH):\n        shutil.rmtree(TEST_CACHE_PATH)\n        print('removed test cache, ', TEST_CACHE_PATH)\n\n\n@pytest.fixture(scope='session')\ndef test_root_path() -> str:\n    return TEST_ROOT_PATH\n\n\n@pytest.fixture(scope='session')\ndef test_cache_path() -> str:\n    makedirs(TEST_CACHE_PATH, exist_ok=True)\n    return TEST_CACHE_PATH\n\n\n@pytest.fixture(scope='session')\ndef test_oauth2_redirect_url() -> str:\n    return TEST_OAUTH2_REDIRECT_URL\n\n\n@pytest.fixture(scope='session')\ndef test_lang() -> str:\n    return TEST_LANG\n\n\n@pytest.fixture(scope='session')\ndef test_uid() -> str:\n    return TEST_UID\n\n\n@pytest.fixture(scope='session')\ndef test_random_did() -> str:\n    # Gen random did\n    return str(random.getrandbits(64))\n\n\n@pytest.fixture(scope='session')\ndef test_uuid() -> str:\n    # Gen uuid\n    return uuid4().hex\n\n\n@pytest.fixture(scope='session')\ndef test_cloud_server() -> str:\n    return TEST_CLOUD_SERVER\n\n\n@pytest.fixture(scope='session')\ndef test_domain_cloud_cache() -> str:\n    return DOMAIN_CLOUD_CACHE\n\n\n@pytest.fixture(scope='session')\ndef test_name_oauth2_info() -> str:\n    return f'{TEST_CLOUD_SERVER}_oauth2_info'\n\n\n@pytest.fixture(scope='session')\ndef test_name_uid() -> str:\n    return f'{TEST_CLOUD_SERVER}_uid'\n\n\n@pytest.fixture(scope='session')\ndef test_name_uuid() -> str:\n    return f'{TEST_CLOUD_SERVER}_uuid'\n\n\n@pytest.fixture(scope='session')\ndef test_name_rd_did() -> str:\n    return f'{TEST_CLOUD_SERVER}_rd_did'\n\n\n@pytest.fixture(scope='session')\ndef test_name_homes() -> str:\n    return f'{TEST_CLOUD_SERVER}_homes'\n\n\n@pytest.fixture(scope='session')\ndef test_name_devices() -> str:\n    return f'{TEST_CLOUD_SERVER}_devices'\n"
  },
  {
    "path": "test/pytest.ini",
    "content": "[pytest]\nmarkers:\n    github: tests for github actions\n    update: update or re-sort config file"
  },
  {
    "path": "test/test_cloud.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_cloud.py.\"\"\"\nimport asyncio\nimport logging\nimport time\nimport webbrowser\nimport pytest\n\n# pylint: disable=import-outside-toplevel, unused-argument\n_LOGGER = logging.getLogger(__name__)\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_oauth_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_oauth2_redirect_url: str,\n    test_uuid: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_uuid: str\n) -> dict:\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTOauthClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    local_uuid = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_uuid, type_=str)\n    uuid = str(local_uuid or test_uuid)\n    _LOGGER.info('uuid: %s', uuid)\n    miot_oauth = MIoTOauthClient(\n        client_id=OAUTH2_CLIENT_ID,\n        redirect_url=test_oauth2_redirect_url,\n        cloud_server=test_cloud_server,\n        uuid=uuid)\n\n    oauth_info = None\n    load_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    if (\n        isinstance(load_info, dict)\n        and 'access_token' in load_info\n        and 'expires_ts' in load_info\n        and load_info['expires_ts'] > int(time.time())\n    ):\n        _LOGGER.info('load oauth info, %s', load_info)\n        oauth_info = load_info\n    if oauth_info is None:\n        # gen oauth url\n        auth_url: str = miot_oauth.gen_auth_url()\n        assert isinstance(auth_url, str)\n        _LOGGER.info('auth url: %s', auth_url)\n        # get code\n        webbrowser.open(auth_url)\n        code: str = input('input code: ')\n        assert code is not None\n        # get access_token\n        res_obj = await miot_oauth.get_access_token_async(code=code)\n        assert res_obj is not None\n        oauth_info = res_obj\n        _LOGGER.info('get_access_token result: %s', res_obj)\n        rc = await miot_storage.save_async(\n            test_domain_cloud_cache, test_name_oauth2_info, oauth_info)\n        assert rc\n        _LOGGER.info('save oauth info')\n        rc = await miot_storage.save_async(\n            test_domain_cloud_cache, test_name_uuid, uuid)\n        assert rc\n        _LOGGER.info('save uuid')\n\n    access_token = oauth_info.get('access_token', None)\n    assert isinstance(access_token, str)\n    _LOGGER.info('access_token: %s', access_token)\n    refresh_token = oauth_info.get('refresh_token', None)\n    assert isinstance(refresh_token, str)\n    _LOGGER.info('refresh_token: %s', refresh_token)\n\n    await miot_oauth.deinit_async()\n    return oauth_info\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency(on=['test_miot_oauth_async'])\nasync def test_miot_oauth_refresh_token(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_oauth2_redirect_url: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_uuid: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTOauthClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    uuid = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_uuid, type_=str)\n    assert isinstance(uuid, str)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict)\n    assert 'access_token' in oauth_info\n    assert 'refresh_token' in oauth_info\n    assert 'expires_ts' in oauth_info\n    remaining_time = oauth_info['expires_ts'] - int(time.time())\n    _LOGGER.info('token remaining valid time: %ss', remaining_time)\n    # Refresh token\n    miot_oauth = MIoTOauthClient(\n        client_id=OAUTH2_CLIENT_ID,\n        redirect_url=test_oauth2_redirect_url,\n        cloud_server=test_cloud_server,\n        uuid=uuid)\n    refresh_token = oauth_info.get('refresh_token', None)\n    assert refresh_token\n    update_info = await miot_oauth.refresh_access_token_async(\n        refresh_token=refresh_token)\n    assert update_info\n    assert 'access_token' in update_info\n    assert 'refresh_token' in update_info\n    assert 'expires_ts' in update_info\n    remaining_time = update_info['expires_ts'] - int(time.time())\n    assert remaining_time > 0\n    _LOGGER.info('refresh token, remaining valid time: %ss', remaining_time)\n    # Save oauth2 info\n    rc = await miot_storage.save_async(\n        test_domain_cloud_cache, test_name_oauth2_info, update_info)\n    assert rc\n    _LOGGER.info('refresh token success, %s', update_info)\n\n    await miot_oauth.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_nickname_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Get nickname\n    user_info = await miot_http.get_user_info_async()\n    assert isinstance(user_info, dict) and 'miliaoNick' in user_info\n    nickname = user_info['miliaoNick']\n    _LOGGER.info('your nickname: %s', nickname)\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_uid_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_uid: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    uid = await miot_http.get_uid_async()\n    assert isinstance(uid, str)\n    _LOGGER.info('your uid: %s', uid)\n    # Save uid\n    rc = await miot_storage.save_async(\n        domain=test_domain_cloud_cache, name=test_name_uid, data=uid)\n    assert rc\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_homeinfos_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_uid: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Get homeinfos\n    homeinfos = await miot_http.get_homeinfos_async()\n    assert isinstance(homeinfos, dict)\n    assert 'uid' in homeinfos and isinstance(homeinfos['uid'], str)\n    assert 'home_list' in homeinfos and isinstance(\n        homeinfos['home_list'], dict)\n    assert 'share_home_list' in homeinfos and isinstance(\n        homeinfos['share_home_list'], dict)\n    # Get uid\n    uid = homeinfos.get('uid', '')\n    # Compare uid with uid in storage\n    uid2 = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_uid, type_=str)\n    assert uid == uid2\n    _LOGGER.info('your uid: %s', uid)\n    # Get homes\n    home_list = homeinfos.get('home_list', {})\n    _LOGGER.info('your home_list: ,%s', home_list)\n    # Get share homes\n    share_home_list = homeinfos.get('share_home_list', {})\n    _LOGGER.info('your share_home_list: %s', share_home_list)\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_devices_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_uid: str,\n    test_name_homes: str,\n    test_name_devices: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Get devices\n    devices = await miot_http.get_devices_async()\n    assert isinstance(devices, dict)\n    assert 'uid' in devices and isinstance(devices['uid'], str)\n    assert 'homes' in devices and isinstance(devices['homes'], dict)\n    assert 'devices' in devices and isinstance(devices['devices'], dict)\n    # Compare uid with uid in storage\n    uid = devices.get('uid', '')\n    uid2 = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_uid, type_=str)\n    assert uid == uid2\n    _LOGGER.info('your uid: %s', uid)\n    # Get homes\n    homes = devices['homes']\n    _LOGGER.info('your homes: %s', homes)\n    # Get devices\n    devices = devices['devices']\n    _LOGGER.info('your devices count: %s', len(devices))\n    # Storage homes and devices\n    rc = await miot_storage.save_async(\n        domain=test_domain_cloud_cache, name=test_name_homes, data=homes)\n    assert rc\n    rc = await miot_storage.save_async(\n        domain=test_domain_cloud_cache, name=test_name_devices, data=devices)\n    assert rc\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_devices_with_dids_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_devices: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Load devices\n    local_devices = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)\n    assert isinstance(local_devices, dict)\n    did_list = list(local_devices.keys())\n    assert len(did_list) > 0\n    # Get device with dids\n    test_list = did_list[:6]\n    devices_info = await miot_http.get_devices_with_dids_async(\n        dids=test_list)\n    assert isinstance(devices_info, dict)\n    _LOGGER.info('test did list, %s, %s', len(test_list), test_list)\n    _LOGGER.info(\n        'test result: %s, %s', len(devices_info), list(devices_info.keys()))\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\nasync def test_miot_cloud_get_cert(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_random_did: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_uid: str,\n    test_name_rd_did: str\n):\n    \"\"\"\n    NOTICE: Currently, only certificate acquisition in the CN region is \n    supported.\n    \"\"\"\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTCert, MIoTStorage\n\n    if test_cloud_server.lower() != 'cn':\n        _LOGGER.info('only support CN region')\n        return\n\n    miot_storage = MIoTStorage(test_cache_path)\n    uid = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_uid, type_=str)\n    assert isinstance(uid, str)\n    _LOGGER.info('your uid: %s', uid)\n    random_did = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_rd_did, type_=str)\n    if not random_did:\n        random_did = test_random_did\n        rc = await miot_storage.save_async(\n            domain=test_domain_cloud_cache, name=test_name_rd_did,\n            data=random_did)\n        assert rc\n    assert isinstance(random_did, str)\n    _LOGGER.info('your random_did: %s', random_did)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict)\n    assert 'access_token' in oauth_info\n    access_token = oauth_info['access_token']\n\n    # Get certificates\n    miot_cert = MIoTCert(storage=miot_storage, uid=uid, cloud_server='CN')\n    assert await miot_cert.verify_ca_cert_async(), 'invalid ca cert'\n    remaining_time: int = await miot_cert.user_cert_remaining_time_async()\n    if remaining_time > 0:\n        _LOGGER.info(\n            'user cert is valid, remaining time, %ss', remaining_time)\n        _LOGGER.info((\n            'if you want to obtain it again, please delete the '\n            'key, csr, and cert files in %s.'), test_cache_path)\n        return\n\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server,\n        client_id=OAUTH2_CLIENT_ID,\n        access_token=access_token)\n\n    user_key = miot_cert.gen_user_key()\n    assert isinstance(user_key, str)\n    _LOGGER.info('user_key str, %s', user_key)\n    user_csr = miot_cert.gen_user_csr(user_key=user_key, did=random_did)\n    assert isinstance(user_csr, str)\n    _LOGGER.info('user_csr str, %s', user_csr)\n    cert_str = await miot_http.get_central_cert_async(csr=user_csr)\n    assert isinstance(cert_str, str)\n    _LOGGER.info('user_cert str, %s', cert_str)\n    rc = await miot_cert.update_user_key_async(key=user_key)\n    assert rc\n    rc = await miot_cert.update_user_cert_async(cert=cert_str)\n    assert rc\n    # verify user certificates\n    remaining_time = await miot_cert.user_cert_remaining_time_async(\n        cert_data=cert_str.encode('utf-8'), did=random_did)\n    assert remaining_time > 0\n    _LOGGER.info('user cert remaining time, %ss', remaining_time)\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_prop_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_devices: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Load devices\n    local_devices = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)\n    assert isinstance(local_devices, dict)\n    did_list = list(local_devices.keys())\n    assert len(did_list) > 0\n    # Get prop\n    test_list = did_list[:6]\n    for did in test_list:\n        prop_value = await miot_http.get_prop_async(did=did, siid=2, piid=1)\n        device_name = local_devices[did]['name']\n        _LOGGER.info('%s(%s), prop.2.1: %s', device_name, did, prop_value)\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_get_props_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_devices: str\n):\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Load devices\n    local_devices = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)\n    assert isinstance(local_devices, dict)\n    did_list = list(local_devices.keys())\n    assert len(did_list) > 0\n    # Get props\n    test_list = did_list[:6]\n    prop_values = await miot_http.get_props_async(params=[\n        {'did': did, 'siid': 2, 'piid': 1} for did in test_list])\n\n    _LOGGER.info('test did list, %s, %s', len(test_list), test_list)\n    _LOGGER.info('test result, %s, %s', len(prop_values), prop_values)\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.skip(reason='skip danger operation')\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_set_prop_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_devices: str\n):\n    \"\"\"\n    WARNING: This test case will control the actual device and is not enabled\n    by default. You can uncomment @pytest.mark.skip to enable it.\n    \"\"\"\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Load devices\n    local_devices = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)\n    assert isinstance(local_devices, dict)\n    assert len(local_devices) > 0\n    # Set prop\n    # Find central hub gateway, control its indicator light switch\n    # You can replace it with the device you want to control.\n    test_did = ''\n    for did, dev in local_devices.items():\n        if dev['model'] == 'xiaomi.gateway.hub1':\n            test_did = did\n            break\n    assert test_did != '', 'no central hub gateway found'\n    result = await miot_http.set_prop_async(params=[{\n        'did': test_did, 'siid': 3, 'piid': 1, 'value': False}])\n    _LOGGER.info('test did, %s, prop.3.1=False -> %s', test_did, result)\n    await asyncio.sleep(1)\n    result = await miot_http.set_prop_async(params=[{\n        'did': test_did, 'siid': 3, 'piid': 1, 'value': True}])\n    _LOGGER.info('test did, %s, prop.3.1=True -> %s', test_did, result)\n\n    await miot_http.deinit_async()\n\n\n@pytest.mark.skip(reason='skip danger operation')\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_miot_cloud_action_async(\n    test_cache_path: str,\n    test_cloud_server: str,\n    test_domain_cloud_cache: str,\n    test_name_oauth2_info: str,\n    test_name_devices: str\n):\n    \"\"\"\n    WARNING: This test case will control the actual device and is not enabled\n    by default. You can uncomment @pytest.mark.skip to enable it.\n    \"\"\"\n    from miot.const import OAUTH2_CLIENT_ID\n    from miot.miot_cloud import MIoTHttpClient\n    from miot.miot_storage import MIoTStorage\n\n    miot_storage = MIoTStorage(test_cache_path)\n    oauth_info = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\n    miot_http = MIoTHttpClient(\n        cloud_server=test_cloud_server, client_id=OAUTH2_CLIENT_ID,\n        access_token=oauth_info['access_token'])\n\n    # Load devices\n    local_devices = await miot_storage.load_async(\n        domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)\n    assert isinstance(local_devices, dict)\n    assert len(local_devices) > 0\n    # Action\n    # Find central hub gateway, trigger its virtual events\n    # You can replace it with the device you want to control.\n    test_did = ''\n    for did, dev in local_devices.items():\n        if dev['model'] == 'xiaomi.gateway.hub1':\n            test_did = did\n            break\n    assert test_did != '', 'no central hub gateway found'\n    result = await miot_http.action_async(\n        did=test_did, siid=4, aiid=1,\n        in_list=[{'piid': 1, 'value': 'hello world.'}])\n    _LOGGER.info('test did, %s, action.4.1 -> %s', test_did, result)\n\n    await miot_http.deinit_async()\n"
  },
  {
    "path": "test/test_common.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_common.py.\"\"\"\nimport pytest\n\n# pylint: disable=import-outside-toplevel, unused-argument\n\n\n@pytest.mark.github\ndef test_miot_matcher():\n    from miot.common import MIoTMatcher\n\n    matcher: MIoTMatcher = MIoTMatcher()\n    # Add\n    for l1 in range(1, 11):\n        matcher[f'test/{l1}/#'] = f'test/{l1}/#'\n        for l2 in range(1, 11):\n            matcher[f'test/{l1}/{l2}'] = f'test/{l1}/{l2}'\n            if not matcher.get(topic=f'test/+/{l2}'):\n                matcher[f'test/+/{l2}'] = f'test/+/{l2}'\n    # Match\n    match_result: list[str] = list(matcher.iter_all_nodes())\n    assert len(match_result) == 120\n    match_result: list[str] = list(matcher.iter_match(topic='test/1/1'))\n    assert len(match_result) == 3\n    assert set(match_result) == set(['test/1/1', 'test/+/1', 'test/1/#'])\n    # Delete\n    if matcher.get(topic='test/1/1'):\n        del matcher['test/1/1']\n    assert len(list(matcher.iter_all_nodes())) == 119\n    match_result: list[str] = list(matcher.iter_match(topic='test/1/1'))\n    assert len(match_result) == 2\n    assert set(match_result) == set(['test/+/1', 'test/1/#'])\n"
  },
  {
    "path": "test/test_lan.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_lan.py.\"\"\"\nimport logging\nfrom typing import Any\nimport pytest\nimport asyncio\nfrom zeroconf import IPVersion\nfrom zeroconf.asyncio import AsyncZeroconf\n\n_LOGGER = logging.getLogger(__name__)\n\n# pylint: disable=import-outside-toplevel, unused-argument\n\n\n@pytest.mark.parametrize('test_devices', [{\n    # specv2 model\n    '123456': {\n        'token': '11223344556677d9a03d43936fc384205',\n        'model': 'xiaomi.gateway.hub1'\n    },\n    # profile model\n    '123457': {\n        'token': '11223344556677d9a03d43936fc384205',\n        'model': 'yeelink.light.lamp9'\n    },\n    '123458': {\n        'token': '11223344556677d9a03d43936fc384205',\n        'model': 'zhimi.heater.ma1'\n    },\n    # Non -digital did\n    'group.123456': {\n        'token': '11223344556677d9a03d43936fc384205',\n        'model': 'mijia.light.group3'\n    },\n    'proxy.123456.1': {\n        'token': '11223344556677d9a03d43936fc384205',\n        'model': 'xiaomi.light.p1'\n    },\n    'miwifi_123456': {\n        'token': '11223344556677d9a03d43936fc384205',\n        'model': 'xiaomi.light.p1'\n    }\n}])\n@pytest.mark.asyncio\nasync def test_lan_async(test_devices: dict):\n    \"\"\"\n    Use the central hub gateway as a test equipment, and through the local area \n    network control central hub gateway indicator light switch. Please replace \n    it for your own device information (did, token) during testing.\n    xiaomi.gateway.hub1 spec define: \n    http://poc.miot-spec.srv/miot-spec-v2/instance?type=urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3\n    \"\"\"\n    from miot.miot_network import MIoTNetwork\n    from miot.miot_lan import MIoTLan\n    from miot.miot_mdns import MipsService\n\n    # Your central hub gateway did\n    test_did = '111111'\n    # Your central hub gateway token\n    test_token = '11223344556677d9a03d43936fc384205'\n    test_model = 'xiaomi.gateway.hub1'\n    # Your computer interface list, such as enp3s0, wlp5s0\n    test_if_names = ['enp3s0', 'wlp5s0']\n\n    # Check test params\n    assert int(test_did) > 0\n\n    evt_push_available: asyncio.Event\n    evt_push_unavailable: asyncio.Event\n\n    miot_network = MIoTNetwork()\n    await miot_network.init_async()\n    _LOGGER.info('miot_network, %s', miot_network.network_info)\n    mips_service = MipsService(\n        aiozc=AsyncZeroconf(ip_version=IPVersion.V4Only))\n    await mips_service.init_async()\n    miot_lan = MIoTLan(\n        net_ifs=test_if_names,\n        network=miot_network,\n        mips_service=mips_service,\n        enable_subscribe=True)\n    evt_push_available = asyncio.Event()\n    evt_push_unavailable = asyncio.Event()\n    await miot_lan.vote_for_lan_ctrl_async(key='test', vote=True)\n\n    async def device_state_change(did: str, state: dict, ctx: Any):\n        _LOGGER.info('device state change, %s, %s', did, state)\n        if did != test_did:\n            return\n        if (\n            state.get('online', False)\n            and state.get('push_available', False)\n        ):\n            # Test sub prop\n            miot_lan.sub_prop(\n                did=did, siid=3, piid=1, handler=lambda msg, ctx:\n                    _LOGGER.info('sub prop.3.1 msg, %s=%s', did, msg))\n            miot_lan.sub_prop(\n                did=did, handler=lambda msg, ctx:\n                    _LOGGER.info('sub all device msg, %s=%s', did, msg))\n            evt_push_available.set()\n        else:\n            # miot_lan.unsub_prop(did=did, siid=3, piid=1)\n            # miot_lan.unsub_prop(did=did)\n            evt_push_unavailable.set()\n\n    async def lan_state_change(state: bool):\n        _LOGGER.info('lan state change, %s', state)\n        if not state:\n            return\n        miot_lan.update_devices(devices={\n            test_did: {\n                'token': test_token,\n                'model': test_model\n            },\n            **test_devices\n        })\n\n        # Test sub device state\n        miot_lan.sub_device_state(\n            'test', device_state_change)\n\n    miot_lan.sub_lan_state('test', lan_state_change)\n    if miot_lan.init_done:\n        await lan_state_change(True)\n\n    await evt_push_available.wait()\n    result = await miot_lan.get_dev_list_async()\n    assert test_did in result\n    result = await miot_lan.set_prop_async(\n        did=test_did, siid=3, piid=1, value=True)\n    assert result.get('code', -1) == 0\n    await asyncio.sleep(0.2)\n    result = await miot_lan.set_prop_async(\n        did=test_did, siid=3, piid=1, value=False)\n    assert result.get('code', -1) == 0\n    await asyncio.sleep(0.2)\n\n    evt_push_unavailable = asyncio.Event()\n    await miot_lan.update_subscribe_option(enable_subscribe=False)\n\n    await evt_push_unavailable.wait()\n    result = await miot_lan.get_dev_list_async()\n    assert test_did in result\n    result = await miot_lan.set_prop_async(\n        did=test_did, siid=3, piid=1, value=True)\n    assert result.get('code', -1) == 0\n    await asyncio.sleep(0.2)\n    result = await miot_lan.set_prop_async(\n        did=test_did, siid=3, piid=1, value=False)\n    assert result.get('code', -1) == 0\n    await asyncio.sleep(0.2)\n\n    await miot_lan.deinit_async()\n    await mips_service.deinit_async()\n    await miot_network.deinit_async()\n"
  },
  {
    "path": "test/test_mdns.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_mdns.py.\"\"\"\nimport asyncio\nimport logging\nimport pytest\nfrom zeroconf import IPVersion\nfrom zeroconf.asyncio import AsyncZeroconf\n\n_LOGGER = logging.getLogger(__name__)\n\n# pylint: disable=import-outside-toplevel, unused-argument\n\n\n@pytest.mark.asyncio\nasync def test_service_loop_async():\n    from miot.miot_mdns import MipsService, MipsServiceState\n\n    async def on_service_state_change(\n            group_id: str, state: MipsServiceState, data: dict):\n        _LOGGER.info(\n            'on_service_state_change, %s, %s, %s', group_id, state, data)\n\n    async with AsyncZeroconf(ip_version=IPVersion.V4Only) as aiozc:\n        mips_service = MipsService(aiozc)\n        mips_service.sub_service_change('test', '*', on_service_state_change)\n        await mips_service.init_async()\n        # Wait for service to discover\n        await asyncio.sleep(3)\n        services_detail = mips_service.get_services()\n        _LOGGER.info('get all service, %s', list(services_detail.keys()))\n        for name, data in services_detail.items():\n            _LOGGER.info(\n                '\\tinfo, %s, %s, %s, %s',\n                name, data['did'], data['addresses'], data['port'])\n        await mips_service.deinit_async()\n"
  },
  {
    "path": "test/test_mips.py",
    "content": "# -*- coding: utf-8 -*-\r\n\"\"\"Unit test for miot_mips.py.\r\nNOTICE: When running this test case, you need to run test_cloud.py first to \r\nobtain the token and certificate information, and at the same time avoid data \r\ndeletion.\r\n\"\"\"\r\nimport ipaddress\r\nfrom typing import Any, Tuple\r\nimport pytest\r\nimport asyncio\r\nimport logging\r\n\r\n_LOGGER = logging.getLogger(__name__)\r\n\r\n\r\n# pylint: disable = import-outside-toplevel, unused-argument\r\n\r\n@pytest.mark.parametrize('central_info', [\r\n    ('<Group id>', 'Gateway did', 'Gateway ip', 8883),\r\n])\r\n@pytest.mark.asyncio\r\nasync def test_mips_local_async(\r\n    test_cache_path: str,\r\n    test_domain_cloud_cache: str,\r\n    test_name_uid: str,\r\n    test_name_rd_did: str,\r\n    central_info: Tuple[str, str, str, int]\r\n):\r\n    \"\"\"\r\n    NOTICE:\r\n    - Mips local is used to connect to the central gateway and is only \r\n    supported in the Chinese mainland region.\r\n    - Before running this test case, you need to run test_mdns.py first to \r\n    obtain the group_id, did, ip, and port of the hub, and then fill in this \r\n    information in the parametrize. you can enter multiple central connection \r\n    information items for separate tests.\r\n    - This test case requires running test_cloud.py first to obtain the\r\n    central connection certificate.\r\n    - This test case will control the indicator light switch of the central\r\n    gateway.\r\n    \"\"\"\r\n    from miot.miot_storage import MIoTStorage, MIoTCert\r\n    from miot.miot_mips import MipsLocalClient\r\n\r\n    central_group_id: str = central_info[0]\r\n    assert isinstance(central_group_id, str)\r\n    central_did: str = central_info[1]\r\n    assert central_did.isdigit()\r\n    central_ip: str = central_info[2]\r\n    assert ipaddress.ip_address(central_ip)\r\n    central_port: int = central_info[3]\r\n    assert isinstance(central_port, int)\r\n\r\n    miot_storage = MIoTStorage(test_cache_path)\r\n    uid = await miot_storage.load_async(\r\n        domain=test_domain_cloud_cache, name=test_name_uid, type_=str)\r\n    assert isinstance(uid, str)\r\n    random_did = await miot_storage.load_async(\r\n        domain=test_domain_cloud_cache, name=test_name_rd_did, type_=str)\r\n    assert isinstance(random_did, str)\r\n    miot_cert = MIoTCert(storage=miot_storage, uid=uid, cloud_server='CN')\r\n    assert miot_cert.ca_file\r\n    assert miot_cert.cert_file\r\n    assert miot_cert.key_file\r\n    _LOGGER.info(\r\n        'cert info, %s, %s, %s', miot_cert.ca_file, miot_cert.cert_file,\r\n        miot_cert.key_file)\r\n\r\n    mips_local = MipsLocalClient(\r\n        did=random_did,\r\n        host=central_ip,\r\n        group_id=central_group_id,\r\n        ca_file=miot_cert.ca_file,\r\n        cert_file=miot_cert.cert_file,\r\n        key_file=miot_cert.key_file,\r\n        port=central_port,\r\n        home_name='mips local test')\r\n    mips_local.enable_logger(logger=_LOGGER)\r\n    mips_local.enable_mqtt_logger(logger=_LOGGER)\r\n\r\n    async def on_mips_state_changed_async(key: str, state: bool):\r\n        _LOGGER.info('on mips state changed, %s, %s', key, state)\r\n\r\n    async def on_dev_list_changed_async(\r\n        mips: MipsLocalClient, did_list: list[str]\r\n    ):\r\n        _LOGGER.info('dev list changed, %s', did_list)\r\n\r\n    def on_prop_changed(payload: dict, ctx: Any):\r\n        _LOGGER.info('prop changed, %s=%s', ctx, payload)\r\n\r\n    def on_event_occurred(payload: dict, ctx: Any):\r\n        _LOGGER.info('event occurred, %s=%s', ctx, payload)\r\n\r\n    # Reg mips state\r\n    mips_local.sub_mips_state(\r\n        key='mips_local', handler=on_mips_state_changed_async)\r\n    mips_local.on_dev_list_changed = on_dev_list_changed_async\r\n    # Connect\r\n    await mips_local.connect_async()\r\n    await asyncio.sleep(0.5)\r\n    # Get device list\r\n    device_list = await mips_local.get_dev_list_async()\r\n    assert isinstance(device_list, dict)\r\n    _LOGGER.info(\r\n        'get_dev_list, %d, %s', len(device_list), list(device_list.keys()))\r\n    # Sub Prop\r\n    mips_local.sub_prop(\r\n        did=central_did, handler=on_prop_changed,\r\n        handler_ctx=f'{central_did}.*')\r\n    # Sub Event\r\n    mips_local.sub_event(\r\n        did=central_did, handler=on_event_occurred,\r\n        handler_ctx=f'{central_did}.*')\r\n    # Get/set prop\r\n    test_siid = 3\r\n    test_piid = 1\r\n    # mips_local.sub_prop(\r\n    #     did=central_did, siid=test_siid, piid=test_piid,\r\n    #     handler=on_prop_changed,\r\n    #     handler_ctx=f'{central_did}.{test_siid}.{test_piid}')\r\n    result1 = await mips_local.get_prop_async(\r\n        did=central_did, siid=test_siid, piid=test_piid)\r\n    assert isinstance(result1, bool)\r\n    _LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result1)\r\n    result2 = await mips_local.set_prop_async(\r\n        did=central_did, siid=test_siid, piid=test_piid, value=not result1)\r\n    _LOGGER.info(\r\n        'set prop.%s.%s=%s, result=%s',\r\n        test_siid, test_piid, not result1, result2)\r\n    assert isinstance(result2, dict)\r\n    result3 = await mips_local.get_prop_async(\r\n        did=central_did, siid=test_siid, piid=test_piid)\r\n    assert isinstance(result3, bool)\r\n    _LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result3)\r\n    # Action\r\n    test_siid = 4\r\n    test_aiid = 1\r\n    in_list = [{'piid': 1, 'value': 'hello world.'}]\r\n    result4 = await mips_local.action_async(\r\n        did=central_did, siid=test_siid, aiid=test_aiid,\r\n        in_list=in_list)\r\n    assert isinstance(result4, dict)\r\n    _LOGGER.info(\r\n        'action.%s.%s=%s, result=%s', test_siid, test_piid, in_list, result4)\r\n    # Disconnect\r\n    await mips_local.disconnect_async()\r\n    await mips_local.deinit_async()\r\n\r\n\r\n@pytest.mark.asyncio\r\nasync def test_mips_cloud_async(\r\n    test_cache_path: str,\r\n    test_name_uuid: str,\r\n    test_cloud_server: str,\r\n    test_domain_cloud_cache: str,\r\n    test_name_oauth2_info: str,\r\n    test_name_devices: str\r\n):\r\n    \"\"\"\r\n    NOTICE:\r\n    - This test case requires running test_cloud.py first to obtain the\r\n    central connection certificate.\r\n    - This test case will control the indicator light switch of the central\r\n    gateway.\r\n    \"\"\"\r\n    from miot.const import OAUTH2_CLIENT_ID\r\n    from miot.miot_storage import MIoTStorage\r\n    from miot.miot_mips import MipsCloudClient\r\n    from miot.miot_cloud import MIoTHttpClient\r\n\r\n    miot_storage = MIoTStorage(test_cache_path)\r\n    uuid = await miot_storage.load_async(\r\n        domain=test_domain_cloud_cache, name=test_name_uuid, type_=str)\r\n    assert isinstance(uuid, str)\r\n    oauth_info = await miot_storage.load_async(\r\n        domain=test_domain_cloud_cache, name=test_name_oauth2_info, type_=dict)\r\n    assert isinstance(oauth_info, dict) and 'access_token' in oauth_info\r\n    access_token = oauth_info['access_token']\r\n    _LOGGER.info('connect info, %s, %s', uuid, access_token)\r\n    mips_cloud = MipsCloudClient(\r\n        uuid=uuid,\r\n        cloud_server=test_cloud_server,\r\n        app_id=OAUTH2_CLIENT_ID,\r\n        token=access_token)\r\n    mips_cloud.enable_logger(logger=_LOGGER)\r\n    mips_cloud.enable_mqtt_logger(logger=_LOGGER)\r\n    miot_http = MIoTHttpClient(\r\n        cloud_server=test_cloud_server,\r\n        client_id=OAUTH2_CLIENT_ID,\r\n        access_token=access_token)\r\n\r\n    async def on_mips_state_changed_async(key: str, state: bool):\r\n        _LOGGER.info('on mips state changed, %s, %s', key, state)\r\n\r\n    def on_prop_changed(payload: dict, ctx: Any):\r\n        _LOGGER.info('prop changed, %s=%s', ctx, payload)\r\n\r\n    def on_event_occurred(payload: dict, ctx: Any):\r\n        _LOGGER.info('event occurred, %s=%s', ctx, payload)\r\n\r\n    await mips_cloud.connect_async()\r\n    await asyncio.sleep(0.5)\r\n\r\n    # Sub mips state\r\n    mips_cloud.sub_mips_state(\r\n        key='mips_cloud', handler=on_mips_state_changed_async)\r\n    # Load devices\r\n    local_devices = await miot_storage.load_async(\r\n        domain=test_domain_cloud_cache, name=test_name_devices, type_=dict)\r\n    assert isinstance(local_devices, dict)\r\n    central_did = ''\r\n    for did, info in local_devices.items():\r\n        if info['model'] != 'xiaomi.gateway.hub1':\r\n            continue\r\n        central_did = did\r\n        break\r\n    if central_did:\r\n        # Sub Prop\r\n        mips_cloud.sub_prop(\r\n            did=central_did, handler=on_prop_changed,\r\n            handler_ctx=f'{central_did}.*')\r\n        # Sub Event\r\n        mips_cloud.sub_event(\r\n            did=central_did, handler=on_event_occurred,\r\n            handler_ctx=f'{central_did}.*')\r\n        # Get/set prop\r\n        test_siid = 3\r\n        test_piid = 1\r\n        # mips_cloud.sub_prop(\r\n        #     did=central_did, siid=test_siid, piid=test_piid,\r\n        #     handler=on_prop_changed,\r\n        #     handler_ctx=f'{central_did}.{test_siid}.{test_piid}')\r\n        result1 = await miot_http.get_prop_async(\r\n            did=central_did, siid=test_siid, piid=test_piid)\r\n        assert isinstance(result1, bool)\r\n        _LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result1)\r\n        result2 = await miot_http.set_prop_async(params=[{\r\n            'did': central_did, 'siid': test_siid, 'piid': test_piid,\r\n            'value': not result1}])\r\n        _LOGGER.info(\r\n            'set prop.%s.%s=%s, result=%s',\r\n            test_siid, test_piid, not result1, result2)\r\n        assert isinstance(result2, list)\r\n        result3 = await miot_http.get_prop_async(\r\n            did=central_did, siid=test_siid, piid=test_piid)\r\n        assert isinstance(result3, bool)\r\n        _LOGGER.info('get prop.%s.%s, value=%s', test_siid, test_piid, result3)\r\n        # Action\r\n        test_siid = 4\r\n        test_aiid = 1\r\n        in_list = [{'piid': 1, 'value': 'hello world.'}]\r\n        result4 = await miot_http.action_async(\r\n            did=central_did, siid=test_siid, aiid=test_aiid,\r\n            in_list=in_list)\r\n        assert isinstance(result4, dict)\r\n        _LOGGER.info(\r\n            'action.%s.%s=%s, result=%s',\r\n            test_siid, test_piid, in_list, result4)\r\n        await asyncio.sleep(1)\r\n    # Disconnect\r\n    await mips_cloud.disconnect_async()\r\n    await mips_cloud.deinit_async()\r\n    await miot_http.deinit_async()\r\n"
  },
  {
    "path": "test/test_network.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_network.py.\"\"\"\nimport logging\nimport pytest\nimport asyncio\n\n_LOGGER = logging.getLogger(__name__)\n\n# pylint: disable=import-outside-toplevel, unused-argument\n\n\n@pytest.mark.asyncio\nasync def test_network_monitor_loop_async():\n    from miot.miot_network import MIoTNetwork, InterfaceStatus, NetworkInfo\n    miot_net = MIoTNetwork()\n\n    async def on_network_status_changed(status: bool):\n        _LOGGER.info('on_network_status_changed, %s', status)\n    miot_net.sub_network_status(key='test', handler=on_network_status_changed)\n\n    async def on_network_info_changed(\n            status: InterfaceStatus, info: NetworkInfo):\n        _LOGGER.info('on_network_info_changed, %s, %s', status, info)\n    miot_net.sub_network_info(key='test', handler=on_network_info_changed)\n\n    await miot_net.init_async()\n    await asyncio.sleep(3)\n    _LOGGER.info('net status: %s', miot_net.network_status)\n    _LOGGER.info('net info: %s', miot_net.network_info)\n    await miot_net.deinit_async()\n"
  },
  {
    "path": "test/test_spec.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_spec.py.\"\"\"\nimport json\nimport logging\nimport random\nimport time\nfrom urllib.request import Request, urlopen\nimport pytest\n\n_LOGGER = logging.getLogger(__name__)\n\n# pylint: disable=import-outside-toplevel, unused-argument\n\n\n@pytest.mark.parametrize('urn', [\n    'urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3',\n    'urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802',\n    'urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-ar03r1:1',\n    'urn:miot-spec-v2:device:air-purifier:0000A007:xiaomi-va5:1:0000D050',\n    'urn:miot-spec-v2:device:humidifier:0000A00E:xiaomi-p800:1',\n    'urn:miot-spec-v2:device:curtain:0000A00C:xiaomi-acn010:1:0000D031',\n    'urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:2',\n    'urn:miot-spec-v2:device:light:0000A001:philips-strip3:2'])\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_spec_parse_async(test_cache_path, test_lang, urn):\n    from miot.miot_spec import MIoTSpecParser\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    spec_parser = MIoTSpecParser(lang=test_lang, storage=storage)\n    await spec_parser.init_async()\n    assert await spec_parser.parse(urn=urn)\n\n\n@pytest.mark.parametrize('urn_list', [[\n    'urn:miot-spec-v2:device:gateway:0000A019:xiaomi-hub1:3',\n    'urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802',\n    'urn:miot-spec-v2:device:air-conditioner:0000A004:xiaomi-ar03r1:1',\n    'urn:miot-spec-v2:device:air-purifier:0000A007:xiaomi-va5:1:0000D050',\n    'urn:miot-spec-v2:device:humidifier:0000A00E:xiaomi-p800:1',\n    'urn:miot-spec-v2:device:curtain:0000A00C:xiaomi-acn010:1:0000D031',\n    'urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:2',\n    'urn:miot-spec-v2:device:light:0000A001:philips-strip3:2']])\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_spec_refresh_async(test_cache_path, test_lang, urn_list):\n    from miot.miot_spec import MIoTSpecParser\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    spec_parser = MIoTSpecParser(lang=test_lang, storage=storage)\n    await spec_parser.init_async()\n    assert await spec_parser.refresh_async(urn_list=urn_list) == len(urn_list)\n\n\n@pytest.mark.asyncio\n@pytest.mark.dependency()\nasync def test_spec_random_parse_async(test_cache_path, test_lang):\n    from miot.miot_spec import MIoTSpecParser\n    from miot.miot_storage import MIoTStorage\n\n    test_count = 10\n    # get test data\n\n    def get_release_instance() -> list[str]:\n        request = Request(\n            'https://miot-spec.org/miot-spec-v2/instances?status=released',\n            method='GET')\n        with urlopen(request) as response:\n            content = response.read()\n            res_obj = json.loads(str(content, 'utf-8'))\n            result: list[str] = []\n            for item in res_obj['instances']:\n                result.append(item['type'])\n            return result\n    test_urns: list[str] = get_release_instance()\n    test_urn_index: list[int] = random.sample(\n        list(range(len(test_urns))), test_count)\n\n    # get local cache\n    storage = MIoTStorage(test_cache_path)\n    spec_parser = MIoTSpecParser(lang=test_lang, storage=storage)\n    await spec_parser.init_async()\n    start_ts = time.time()*1000\n    for index in test_urn_index:\n        urn: str = test_urns[int(index)]\n        result = await spec_parser.parse(urn=urn, skip_cache=True)\n        assert result is not None\n    end_ts = time.time()*1000\n    _LOGGER.info('takes time, %s, %s', test_count, end_ts-start_ts)\n"
  },
  {
    "path": "test/test_storage.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Unit test for miot_storage.py.\"\"\"\nimport asyncio\nimport logging\nfrom os import path\nimport pytest\n\n_LOGGER = logging.getLogger(__name__)\n\n# pylint: disable=import-outside-toplevel, unused-argument\n\n\n@pytest.mark.asyncio\n@pytest.mark.github\n@pytest.mark.dependency()\nasync def test_variable_async(test_cache_path):\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    test_domain = 'variable'\n    test_count = 50\n\n    for index in range(test_count):\n        # bytes\n        var_name = f'bytes_var{index}'\n        write_value: bytes = b'bytes value->{index}\\n\\n\\n'\n        assert await storage.save_async(test_domain, var_name, write_value)\n        read_value: bytes = await storage.load_async(\n            test_domain, var_name, type_=bytes)\n        assert read_value == write_value\n        if index > test_count/2:\n            assert await storage.remove_async(\n                test_domain, var_name, type_=bytes)\n        # str\n        var_name = f'str_var{index}'\n        write_value: str = f'str value->{index}\\n\\n\\n'\n        assert await storage.save_async(test_domain, var_name, write_value)\n        read_value: str = await storage.load_async(\n            test_domain, var_name, type_=str)\n        assert read_value == write_value\n        if index >= test_count/2:\n            assert await storage.remove_async(test_domain, var_name, type_=str)\n        # list\n        var_name = f'list_var{index}'\n        write_value: list = [1, 2, 3, 4, 5, 'test_list', index]\n        assert await storage.save_async(test_domain, var_name, write_value)\n        read_value: list = await storage.load_async(\n            test_domain, var_name, type_=list)\n        assert read_value == write_value\n        if index >= test_count/2:\n            assert await storage.remove_async(test_domain, var_name, type_=list)\n        # dict\n        var_name = f'dict_var{index}'\n        write_value: dict = {'k1': 'v1', 'k2': 'v2', 'index': f'index-{index}'}\n        assert await storage.save_async(test_domain, var_name, write_value)\n        read_value: dict = await storage.load_async(\n            test_domain, var_name, type_=dict)\n        assert read_value == write_value\n        if index >= test_count/2:\n            assert await storage.remove_async(test_domain, var_name, type_=dict)\n\n    # Delete all bytes\n    names: list[str] = storage.get_names(domain=test_domain, type_=bytes)\n    for name in names:\n        assert await storage.remove_async(\n            domain=test_domain, name=name, type_=bytes)\n    assert len(storage.get_names(domain=test_domain, type_=bytes)) == 0\n    assert len(storage.get_names(\n        domain=test_domain, type_=str)) == test_count/2\n    assert len(storage.get_names(\n        domain=test_domain, type_=list)) == test_count/2\n    assert len(storage.get_names(\n        domain=test_domain, type_=dict)) == test_count/2\n\n\n@pytest.mark.asyncio\n@pytest.mark.github\n@pytest.mark.dependency()\nasync def test_load_domain_async(test_cache_path):\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    test_domain = 'variable'\n    names: list[str] = storage.get_names(domain=test_domain, type_=dict)\n    assert len(names) > 0\n    for name in names:\n        r_data = await storage.load_async(test_domain, name=name, type_=dict)\n        assert r_data\n\n\n@pytest.mark.asyncio\n@pytest.mark.github\n@pytest.mark.dependency()\nasync def test_multi_task_load_async(test_cache_path):\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    test_domain = 'variable'\n    task_count = 50\n\n    names: list[str] = storage.get_names(domain=test_domain, type_=dict)\n    task_list: list = []\n    for name in names:\n        for _ in range(task_count):\n            task_list.append(asyncio.create_task(storage.load_async(\n                domain=test_domain, name=name, type_=dict)))\n    _LOGGER.info('task count, %s', len(task_list))\n    result: list = await asyncio.gather(*task_list)\n    assert None not in result\n\n\n@pytest.mark.asyncio\n@pytest.mark.github\n@pytest.mark.dependency()\nasync def test_file_save_load_async(test_cache_path):\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    test_count = 50\n    test_domain = 'file'\n    for index in range(test_count):\n        file_name = f'test-{index}.txt'\n        file_content = f'this is a test file, the index={index}\\r\\r\\r'.encode(\n            'utf-8')\n        assert await storage.save_file_async(\n            test_domain, file_name, file_content)\n        read_content = await storage.load_file_async(test_domain, file_name)\n        assert file_content == read_content\n        # Read the contents of the file directly\n        with open(\n            path.join(test_cache_path, test_domain, file_name), 'rb'\n        ) as r_file:\n            data = r_file.read()\n            assert data == file_content\n        if index > test_count/2:\n            assert await storage.remove_file_async(\n                domain=test_domain, name_with_suffix=file_name)\n    # Delete domain path\n    assert await storage.remove_domain_async(test_domain)\n\n\n@pytest.mark.asyncio\n@pytest.mark.github\n@pytest.mark.dependency()\nasync def test_user_config_async(\n        test_cache_path, test_uid, test_cloud_server):\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    config_base = {\n        'str': 'test string',\n        'list': ['test', 'list'],\n        'dict': {\n            'test': 'dict',\n            'key1': 'value1'\n        },\n        'bool': False,\n        'number_int': 123456,\n        'number_float': 123.456\n    }\n    config = config_base.copy()\n    assert await storage.update_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server, config=config)\n    # Test load all\n    assert (await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server)) == config\n    # Test update\n    config_update = {\n        'test_str': 'test str',\n        'number_float': 456.123\n    }\n    assert await storage.update_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server, config=config_update)\n    config.update(config_update)\n    assert (await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server)) == config\n    # Test replace\n    config_replace = None\n    assert await storage.update_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server,\n        config=config_update, replace=True)\n    assert (config_replace := await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server)) == config_update\n    _LOGGER.info('replace result, %s', config_replace)\n    # Test query\n    query_keys = list(config_base.keys())\n    _LOGGER.info('query keys, %s', query_keys)\n    query_result = await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server, keys=query_keys)\n    _LOGGER.info('query result 1, %s', query_result)\n    assert await storage.update_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server,\n        config=config_base, replace=True)\n    query_result = await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server, keys=query_keys)\n    _LOGGER.info('query result 2, %s', query_result)\n    query_result = await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server)\n    _LOGGER.info('query result all, %s', query_result)\n    # Remove config\n    assert await storage.update_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server, config=None)\n    query_result = await storage.load_user_config_async(\n        uid=test_uid, cloud_server=test_cloud_server)\n    _LOGGER.info('remove result, %s', query_result)\n    # Remove domain\n    assert await storage.remove_domain_async(domain='miot_config')\n\n\n@pytest.mark.asyncio\n@pytest.mark.skip(reason='clean')\n@pytest.mark.dependency()\nasync def test_clear_async(test_cache_path):\n    from miot.miot_storage import MIoTStorage\n\n    storage = MIoTStorage(test_cache_path)\n    assert await storage.clear_async()\n"
  },
  {
    "path": "tools/common.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Common functions.\"\"\"\nimport json\nimport yaml\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\n\n\ndef load_yaml_file(yaml_file: str) -> dict:\n    with open(yaml_file, 'r', encoding='utf-8') as file:\n        return yaml.safe_load(file)\n\n\ndef save_yaml_file(yaml_file: str, data: dict) -> None:\n    with open(yaml_file, 'w', encoding='utf-8') as file:\n        yaml.safe_dump(\n            data=data, stream=file, allow_unicode=True)\n\n\ndef load_json_file(json_file: str) -> dict:\n    with open(json_file, 'r', encoding='utf-8') as file:\n        return json.load(file)\n\n\ndef save_json_file(json_file: str, data: dict) -> None:\n    with open(json_file, 'w', encoding='utf-8') as file:\n        json.dump(data, file, ensure_ascii=False, indent=4)\n\n\ndef http_get(\n    url: str, params: dict = None, headers: dict = None\n) -> dict:\n    if params:\n        encoded_params = urlencode(params)\n        full_url = f'{url}?{encoded_params}'\n    else:\n        full_url = url\n    request = Request(full_url, method='GET', headers=headers or {})\n    content: bytes = None\n    with urlopen(request) as response:\n        content = response.read()\n    return (\n        json.loads(str(content, 'utf-8'))\n        if content is not None else None)\n"
  },
  {
    "path": "tools/update_lan_rule.py",
    "content": "\"\"\" Update LAN rule.\"\"\"\n# -*- coding: utf-8 -*-\n# pylint: disable=relative-beyond-top-level\nfrom os import path\nfrom common import (\n    http_get,\n    load_yaml_file,\n    save_yaml_file)\n\n\nROOT_PATH: str = path.dirname(path.abspath(__file__))\nLAN_PROFILE_MODELS_FILE: str = path.join(\n    ROOT_PATH,\n    '../custom_components/xiaomi_home/miot/lan/profile_models.yaml')\n\n\nSPECIAL_MODELS: list[str] = [\n    # model2class-v2\n    'chuangmi.camera.ipc007b', 'chuangmi.camera.ipc019b',\n    'chuangmi.camera.ipc019e', 'chuangmi.camera.ipc020',\n    'chuangmi.camera.v2', 'chuangmi.camera.v5',\n    'chuangmi.camera.v6', 'chuangmi.camera.xiaobai',\n    'chuangmi.radio.v1', 'chuangmi.radio.v2',\n    'hith.foot_bath.q2', 'imou99.camera.tp2',\n    'isa.camera.hl5', 'isa.camera.isc5',\n    'jiqid.mistory.pro', 'jiqid.mistory.v1',\n    'lumi.airrtc.tcpco2ecn01', 'lumi.airrtc.tcpecn02',\n    'lumi.camera.gwagl01', 'miir.light.ir01',\n    'miir.projector.ir01', 'miir.tv.hir01',\n    'miir.tvbox.ir01', 'roome.bhf_light.yf6002',\n    'smith.waterpuri.jnt600', 'viomi.fridge.u2',\n    'xiaovv.camera.lamp', 'xiaovv.camera.ptz',\n    'xiaovv.camera.xva3', 'xiaovv.camera.xvb4',\n    'xiaovv.camera.xvsnowman', 'zdeer.ajh.a8',\n    'zdeer.ajh.a9', 'zdeer.ajh.zda10',\n    'zdeer.ajh.zda9', 'zdeer.ajh.zjy', 'zimi.clock.myk01',\n    # specialModels\n    'chuangmi.camera.ipc004b', 'chuangmi.camera.ipc009',\n    'chuangmi.camera.ipc010', 'chuangmi.camera.ipc013',\n    'chuangmi.camera.ipc013d', 'chuangmi.camera.ipc016',\n    'chuangmi.camera.ipc017', 'chuangmi.camera.ipc019',\n    'chuangmi.camera.ipc021', 'chuangmi.camera.v3',\n    'chuangmi.camera.v4', 'isa.camera.df3',\n    'isa.camera.hlc6', 'lumi.acpartner.v1',\n    'lumi.acpartner.v2', 'lumi.acpartner.v3',\n    'lumi.airrtc.tcpecn01', 'lumi.camera.aq1',\n    'miir.aircondition.ir01', 'miir.aircondition.ir02',\n    'miir.fan.ir01', 'miir.stb.ir01',\n    'miir.tv.ir01', 'mijia.camera.v1',\n    'mijia.camera.v3', 'roborock.sweeper.s5v2',\n    'roborock.vacuum.c1', 'roborock.vacuum.e2',\n    'roborock.vacuum.m1s', 'roborock.vacuum.s5',\n    'rockrobo.vacuum.v1', 'xiaovv.camera.xvd5']\n\n\ndef update_profile_model(file_path: str):\n    profile_rules: dict = http_get(\n        url='https://miot-spec.org/instance/translate/models')\n    if not profile_rules and 'models' not in profile_rules and not isinstance(\n            profile_rules['models'], dict):\n        raise ValueError('Failed to get profile rule')\n    local_rules: dict = load_yaml_file(\n        yaml_file=file_path) or {}\n    for rule, ts in profile_rules['models'].items():\n        if rule not in local_rules:\n            local_rules[rule] = {'ts': ts}\n        else:\n            local_rules[rule]['ts'] = ts\n    for mode in SPECIAL_MODELS:\n        if mode not in local_rules:\n            local_rules[mode] = {'ts': 1531108800}\n        else:\n            local_rules[mode]['ts'] = 1531108800\n    local_rules = dict(sorted(local_rules.items()))\n    save_yaml_file(\n        yaml_file=file_path, data=local_rules)\n\n\nupdate_profile_model(file_path=LAN_PROFILE_MODELS_FILE)\nprint('profile model list updated.')\n"
  }
]