pax_global_header00006660000000000000000000000064150145147600014515gustar00rootroot0000000000000052 comment=ea966ca250276b9662aba256673647ad41268642 zigpy-0.80.1/000077500000000000000000000000001501451476000127455ustar00rootroot00000000000000zigpy-0.80.1/.github/000077500000000000000000000000001501451476000143055ustar00rootroot00000000000000zigpy-0.80.1/.github/workflows/000077500000000000000000000000001501451476000163425ustar00rootroot00000000000000zigpy-0.80.1/.github/workflows/ci.yml000066400000000000000000000005511501451476000174610ustar00rootroot00000000000000name: CI on: push: pull_request: ~ jobs: shared-ci: uses: zigpy/workflows/.github/workflows/ci.yml@main with: CODE_FOLDER: zigpy CACHE_VERSION: 3 PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit PYTHON_VERSION_DEFAULT: 3.9.15 MINIMUM_COVERAGE_PERCENTAGE: 99 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} zigpy-0.80.1/.github/workflows/matchers/000077500000000000000000000000001501451476000201505ustar00rootroot00000000000000zigpy-0.80.1/.github/workflows/matchers/codespell.json000066400000000000000000000004001501451476000230070ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "codespell", "severity": "warning", "pattern": [ { "regexp": "^(.+):(\\d+):\\s(.+)$", "file": 1, "line": 2, "message": 3 } ] } ] } zigpy-0.80.1/.github/workflows/matchers/flake8.json000066400000000000000000000011011501451476000222060ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "flake8-error", "severity": "error", "pattern": [ { "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", "file": 1, "line": 2, "column": 3, "message": 4 } ] }, { "owner": "flake8-warning", "severity": "warning", "pattern": [ { "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", "file": 1, "line": 2, "column": 3, "message": 4 } ] } ] } zigpy-0.80.1/.github/workflows/matchers/python.json000066400000000000000000000005201501451476000223610ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "python", "pattern": [ { "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", "file": 1, "line": 2 }, { "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", "message": 2 } ] } ] } zigpy-0.80.1/.github/workflows/matchers/ruff.json000066400000000000000000000011661501451476000220110ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "ruff-error", "severity": "error", "pattern": [ { "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", "file": 1, "line": 2, "column": 3, "message": 4 } ] }, { "owner": "ruff-warning", "severity": "warning", "pattern": [ { "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", "file": 1, "line": 2, "column": 3, "message": 4 } ] } ] }zigpy-0.80.1/.github/workflows/publish-to-pypi.yml000066400000000000000000000003621501451476000221330ustar00rootroot00000000000000name: Publish distributions to PyPI on: release: types: - published jobs: shared-build-and-publish: uses: zigpy/workflows/.github/workflows/publish-to-pypi.yml@main secrets: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} zigpy-0.80.1/.github/workflows/stale.yml000066400000000000000000000057141501451476000202040ustar00rootroot00000000000000name: Stale # yamllint disable-line rule:truthy on: schedule: - cron: "0 * * * *" workflow_dispatch: jobs: stale: runs-on: ubuntu-latest steps: # The 180 day stale policy # Used for: # - Issues & PRs # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 180 days stale issues & PRs policy uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 180 days-before-close: 7 operations-per-run: 150 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no stale,help wanted" stale-issue-message: > There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions. stale-pr-label: "stale" exempt-pr-labels: "no stale" stale-pr-message: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. Thank you for your contributions. # The 60 day stale policy for issues # Used for: # - Issues that are pending more information (incomplete issues) # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs more information" days-before-stale: 60 days-before-close: 7 days-before-pr-close: -1 operations-per-run: 50 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no stale,help wanted" stale-issue-message: > There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions. zigpy-0.80.1/.gitignore000066400000000000000000000016171501451476000147420ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache coverage.xml *,cover .pytest_cache/ # Translations *.mo *.pot # Sphinx documentation docs/_build/ # PyBuilder target/ # pyenv .python-version # dotenv .env # virtualenv .venv/ venv/ ENV/ # Editor temp files .*.swp # Visual Studio Code .vscode .DS_Store # Don't keep track of downloaded OTA files tests/ota/files/external/dlzigpy-0.80.1/.pre-commit-config.yaml000066400000000000000000000011121501451476000172210ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.2 hooks: - id: ruff args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"] - id: ruff-format args: ["--config", "pyproject.toml"] - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell additional_dependencies: [tomli] args: ["--toml", "pyproject.toml"] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.11.2 hooks: - id: mypy additional_dependencies: [attrs] zigpy-0.80.1/CODE_OF_CONDUCT.md000066400000000000000000000132631501451476000155510ustar00rootroot00000000000000# Contributor Covenant Code of Conduct for zigpy ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [safety@home-assistant.io][email] or by using the report/flag feature of the medium used. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available [here][version]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][mozilla]. ## Adoption This Code of Conduct was first adopted on January 21st, 2017, and announced in [this][coc-blog] blog post and has been updated on May 25th, 2020 to version 2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog] blog post. For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . [coc-blog]: https://www.home-assistant.io/blog/2017/01/21/home-assistant-governance/ [coc2-blog]: https://www.home-assistant.io/blog/2020/05/25/code-of-conduct-updated/ [email]: mailto:safety@home-assistant.io [homepage]: http://contributor-covenant.org [mozilla]: https://github.com/mozilla/diversity [version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html zigpy-0.80.1/CONTRIBUTING.md000066400000000000000000000434541501451476000152100ustar00rootroot00000000000000# Contribute to the zigpy project This file contains information for end-users, testers and developers on how-to contribute to the zigpy project. It will include guides on how to how to install, use, troubleshoot, debug, code and more. You can contribute to this project either as an normal end-user, a tester (advanced user contributing constructive issue/bug-reports) or as a developer contributing enhancing code. ## How to contribute as an end-user If you think that you are having problems due to a bug then please see the section below on reporting issues as a tester, but be aware that reporting issues put higher responsibility on your active involvement on your part as a tester. Some developers might be also interested in receiving donations in the form of money or hardware such as Zigbee modules and devices, and even if such donations are most often donated with no strings attached it could in many cases help the developers motivation and indirectly improve the development of this project. Sometimes it might just be simpler to just donate money earmarked to specifically let a willing developer buy the exact same type Zigbee device that you are having issues with to be able to replicate the issue themselves in order to troubleshoot and hopefully also solve the problem. Consider submitting a post on GitHub projects issues tracker about willingness to making a donation (please see section below on posing issues). ### How to report issues or bugs as a tester Issues or bugs are normally first to be submitted upstream to the software/project that is utilizing zigpy and its radio libraries, (like for example Home Assistant), however if and when the issue is determined to be in the zigpy or underlying radio library then you should continue by submitting a detailed issue/bug report via the GitHub projects issues tracker. Always be sure to first check if there is not already an existing issue posted with the same description before posting a new issue. - https://help.github.com/en/github/managing-your-work-on-github/creating-an-issue - https://guides.github.com/features/issues/ ### Testing new releases Testing a new release of the zigpy library before it is released in Home Assistant. If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro): - Add https://github.com/home-assistant/hassio-addons-development as "add-on" repository - Install "Custom deps deployment" addon - Update config like: ``` pypi: - zigpy==0.20.0 apk: [] ``` where 0.20.0 is the new version - Start the addon If you are instead using some custom python installation of Home Assistant then do this: - Activate your python virtual env - Update package with ``pip`` ``` pip install zigpy==0.20.0 ### Troubleshooting For troubleshooting with Home Assistant, the general recommendation is to first only enable DEBUG logging for homeassistant.core and homeassistant.components.zha in Home Assistant, then look in the home-assistant.log file and try to get the Home Assistant community to exhausted their combined troubleshooting knowledge of the ZHA component before posting issue directly to a radio library, like example zigpy-deconz or zigpy-xbee. That is, begin with checking debug logs for Home Assistant core and the ZHA component first, (troubleshooting/debugging from the top down instead of from the bottom up), trying to getting help via Home Assistant community forum before moving on to posting debug logs to zigpy and radio libraries. This is a general suggestion to help filter away common problems and not flood the zigpy-cc developer(s) with too many logs. Please also try the very latest versions of zigpy and the radio library, (see the section above about "Testing new releases"), and only if you still have the same issues with the latest versions then enable debug logging for zigpy and the radio libraries in Home Assistant in addition to core and zha. Once enabled debug logging for all those libraries in Home Assistant you should try to reproduce the problem and then raise an issue to the zigpy repo (or to a specific radio library) repo with a copy of those logs. To enable debugging in Home Assistant to get debug logs, either update logger configuration section in configuration.yaml or call logger.set_default_level service with {"level": "debug"} data. Check logger component configuration where you want something this in your configuration.yaml logger: default: info logs: asyncio: debug homeassistant.core: debug homeassistant.components.zha: debug zigpy: debug bellows: debug zigpy_znp: debug zigpy_xbee: debug zigpy_deconz: debug zigpy_zigate: debug ## How to contribute as a developer If you are looking to make a contribution as a developer to this project we suggest that you follow the steps in these guides: - https://github.com/firstcontributions/first-contributions/blob/master/README.md - https://github.com/firstcontributions/first-contributions/blob/master/github-desktop-tutorial.md Code changes or additions can then be submitted to this project on GitHub via pull requests: - https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests - https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request In general when contributing code to this project it is encouraged that you try to follow the coding standards: - First [raise issues on GitHub](https://github.com/zigpy/zigpy/issues) before working on an enhancement to provide coordination with other contributors. - Try to keep each pull request short and only a single PR per enhancement as this makes tracking and reviewing easier. - All code is formatted with black. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with black. Instructions for installing black in many editors can be found here: https://github.com/psf/black#editor-integration - Ideally, you should aim to achieve full coverage of any code changes with tests. - Recommend read and follow [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). - Recommend read and follow [Clifford Programming Style](http://www.clifford.at/style.html). - Recommend code style use [standard naming conventions for Python](https://medium.com/@dasagrivamanu/python-naming-conventions-the-10-points-you-should-know-149a9aa9f8c7). - Recommend use [Semantic Versioning](http://semver.org/) for libraries and dependencies if possible. - Contributions must be your own and you must agree with the license. - All code for this project should aim to be licensed under [GNU GENERAL PUBLIC LICENSE Version 3](https://raw.githubusercontent.com/zigpy/zigpy/dev/LICENSE). ### Installation for use in a new project #### Prerequicites It is recommended that code is formatted with `black` and sorted with `isort`. The check format script that runs in CI will ensure that code meets this requirement and that it is correctly formatted with black. Instructions for installing black in many editors can be found here: https://github.com/psf/black#editor-integration - https://github.com/psf/black - https://github.com/PyCQA/isort #### Setup To setup a development environment, fork the repository and create a virtual environment: ```shell $ git clone git@github.com:youruser/zigpy.git $ cd zigpy $ virtualenv -p python3.8 venv $ source venv/bin/activate (venv) $ pip install --upgrade pip pre-commit tox (venv) $ pre-commit install # install pre-commit as a Git hook (venv) $ pip install -e '.[testing]' # installs zigpy+testing deps into the venv in dev mode ``` At this point `black` and `isort` will be run by the pre-commit hook, reformatting your code automatically to match the rest of the project. ### Unit testing Run `pytest -lv`, which will show you a stack trace and all the local variables when something breaks. It is recommended that you install Python 3.8, 3.9, 3.10 and 3.11 so that you can run `tox` from the root project folder and see exactly what the CI system will tell you without having to wait for Github Actions or Coveralls. Code coverage information will be written by tox to `htmlcov/index.html`. ### The zigpy API This section is meant to describe the zigpy API (Application Programming Interface) and how-to to use it. #### Application * raw_device_initialized * device_initialized * device_removed * device_joined * device_left #### Device * node_descriptor_updated * device_init_failure * device_relays_updated #### Endpoint * unknown_cluster_message * member_added * member_removed #### Group * group_added * group_member_added * group_removed * group_removed #### ZCL Commands * cluster_command * general_command * attribute_updated * device_announce * permit_duration ### Developer references Reference collections for different hardware specific Zigbee Stack and related manufacturer documentation. - https://github.com/zigpy/zigpy/discussions/595 Silicon Labs video playlist of Zigbee Concepts: Architecture basics, MAC/PHY, node types, and application profiles - https://www.youtube.com/playlist?list=PL-awFRrdECXvAs1mN2t2xaI0_bQRh2AqD ### zigpy wiki and communication channels - https://github.com/zigpy/zigpy/wiki - https://github.com/zigpy/zigpy/discussions - https://github.com/zigpy/zigpy/issues ### Zigbee specifications - [Zigbee PRO 2017 (R22) Protocol Specification](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-05-3474-21-0csg-zigbee-specification.pdf) - [Zigbee Cluster Library (R8)](https://zigbeealliance.org/wp-content/uploads/2021/10/07-5123-08-Zigbee-Cluster-Library.pdf) - [Zigbee Base Device Behavior Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/zip/zigbee-base-device-behavior-bdb-v1-0.zip) - [Zigbee Lighting & Occupancy Device Specification (V1.0)](https://zigbeealliance.org/wp-content/uploads/2019/11/docs-15-0014-05-0plo-Lighting-OccupancyDevice-Specification-V1.0.pdf) - [Zigbee Primer](https://docs.smartthings.com/en/latest/device-type-developers-guide/zigbee-primer.html) ## Official release packages available via PyPI New packages of tagged versions are also released via the "zigpy" project on PyPI - https://pypi.org/project/zigpy/ - https://pypi.org/project/zigpy/#history - https://pypi.org/project/zigpy/#files Older packages of tagged versions are still available on the "zigpy-homeassistant" project on PyPI - https://pypi.org/project/zigpy-homeassistant/ Packages of tagged versions of the radio libraries are released via separate projects on PyPI - https://pypi.org/project/zigpy/ - https://pypi.org/project/zha-quirks/ - https://pypi.org/project/bellows/ - https://pypi.org/project/zigpy-znp/ - https://pypi.org/project/zigpy-deconz/ - https://pypi.org/project/zigpy-xbee/ - https://pypi.org/project/zigpy-zigate/ - https://pypi.org/project/zigpy-cc/ (obsolete as replaced by zigpy-znp) ## Related projects ### zigpy-cli (zigpy command line interface) [zigpy-cli](https://github.com/zigpy/zigpy-cli) is a unified command line interface for zigpy radios. The goal of this project is to allow low-level network management from an intuitive command line interface and to group useful Zigbee tools into a single binary. ### ZHA Device Handlers ZHA deviation handling in Home Assistant relies on the third-party [ZHA Device Handlers](https://github.com/zigpy/zha-device-handlers) project (also known unders zha-quirks package name on PyPI). Zigbee devices that deviate from or do not fully conform to the standard specifications set by the [Zigbee Alliance](https://www.zigbee.org) may require the development of custom [ZHA Device Handlers](https://github.com/zigpy/zha-device-handlers) (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. These ZHA Device Handlers for Home Assistant can thus be used to parse custom messages to and from non-compliant Zigbee devices. The custom quirks implementations for zigpy implemented as ZHA Device Handlers for Home Assistant are a similar concept to that of [Hub-connected Device Handlers for the SmartThings platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well as that of [zigbee-herdsman converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are each virtual representations of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. ### ZHA integration component for Home Assistant [ZHA integration component for Home Assistant](https://www.home-assistant.io/integrations/zha/) is a reference implementation of the zigpy library as integrated into the core of **[Home Assistant](https://www.home-assistant.io)** (a Python based open source home automation software). There are also other GUI and non-GUI projects for Home Assistant's ZHA components which builds on or depends on its features and functions to enhance or improve its user-experience, some of those are listed and linked below. #### ZHA Toolkit [ZHA Toolkit](https://github.com/mdeweerd/zha-toolkit) is a custom service for "rare" Zigbee operations using the [ZHA integration component](https://www.home-assistant.io/integrations/zha) in [Home Assistant](https://www.home-assistant.io/). The purpose of ZHA Toolkit and its Home Assistant 'Services' feature, is to provide direct control over low level zigbee commands provided in ZHA or zigpy that are not otherwise available or too limited for some use cases. ZHA Toolkit can also; serve as a framework to do local low level coding (the modules are reloaded on each call), provide access to some higher level commands such as ZNP backup (and restore), make it easier to perform one-time operations where (some) Zigbee knowledge is sufficient and avoiding the need to understand the inner workings of ZHA or Zigpy (methods, quirks, etc). #### ZHA Custom [zha_custom](https://github.com/Adminiuga/zha_custom) (unmaintained project) is a custom component package for Home Assistant (with its ZHA component for zigpy integration) that acts as zigpy commands service wrapper, when installed it allows you to enter custom commands via to zigy to example change advanced configuration and settings that are not available in the UI. #### ZHA Map [zha-map](https://github.com/zha-ng/zha-map) for Home Assistant's ZHA component can build a Zigbee network topology map. #### ZHA Network Visualization Card [zha-network-visualization-card](https://github.com/dmulcahey/zha-network-visualization-card) was a custom Lovelace element for Home Assistant which visualize the Zigbee network for the ZHA component. #### ZHA Network Card [zha-network-card](https://github.com/dmulcahey/zha-network-card) was a custom Lovelace card for Home Assistant that displays ZHA component Zigbee network and device information in Home Assistant #### Zigzag [Zigzag](https://github.com/Samantha-uk/zigzag-v1) was a custom card/panel for [Home Assistant](https://www.home-assistant.io/) that displays a graphical layout of Zigbee devices and the connections between them. Zigzag could be installed as a panel or a custom card and relies on the data provided by the [zha-map](https://github.com/zha-ng/zha-map) integration component. #### ZHA Device Exporter [zha-device-exporter](https://github.com/dmulcahey/zha-device-exporter) is a custom component for Home Assistant to allow the ZHA component to export lists of Zigbee devices. #### ZHA Custom Radios [zha-custom-radios](https://github.com/zha-ng/zha-custom-radios) A now obsolete custom component package for Home Assistant (with its ZHA component for zigpy integration) that allows users to test out new zigpy radio libraries and hardware modules before they have officially been integrated into ZHA. This enables developers and testers to test new or updated zigpy radio modules without having to modify the Home Assistant source code. #### Zigpy Deconz Parser [zigpy-deconz-parser](https://github.com/zha-ng/zigpy-deconz-parser) allow you to parse Home Assistant's ZHA component debug log using `zigpy-deconz` library if you are using a deCONZ based adapter like ConBee or RaspBee. ### Zigbee for Domoticz Plugin [Zigbee for Domoticz Plugin](https://www.domoticz.com/wiki/ZigbeeForDomoticz) is and addon for [Domoticz home automation software](https://www.domoticz.com/) with hardware independent Zigbee Coordinator support achieved via dependency on [zigpy], with the exception of Zigate (which it still continues to manage and handle in native mode as this plugin was originally the mature "Zigate plugin" for Domoticz). Domoticz-Zigbee project available at https://github.com/zigbeefordomoticz/Domoticz-Zigbee and wiki at https://zigbeefordomoticz.github.io/wiki/ ### Zigbee for Jeedom [Zigbee plugin for Jeedom](https://doc.jeedom.com/en_US/plugins/automation%20protocol/zigbee/) is and official addon for [Jeedom home automation software]([https://www.domoticz.com/](https://jeedom.com/en/)) which depends on [zigpy] for hardware independent Zigbee Coordinator support. While free and open source licensed the source code for this Zigbee plugin is currently not available for direct download on a public website, as instead independent developers and users of Jeedom can only download the code by installing Jeedom and [purchasing the plugin from their Jeedom online marketplace for around €6](https://market.jeedom.com/index.php?v=d&p=market_display&id=4050) (at least as it stands in May in 2022). ### ZigCoHTTP [ZigCoHTTP](https://github.com/daniel17903/ZigCoHTTP) (unmaintained and now abandoned) was a stand-alone python application project that creates a Zigbee network using zigpy and bellows. Zigbee devices joining this network can be controlled via a HTTP API. It was developed for a Raspberry Pi using a [Matrix Creator Board](https://www.matrix.one/products/creator) but should also work with other computers with Silicon Labs Zigbee hardware, or with other Zigbee hardware if replace bellows with other radio library for zigpy. zigpy-0.80.1/COPYING000066400000000000000000000012151501451476000137770ustar00rootroot00000000000000zigpy Copyright (C) 2018 Russell Cloran This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . zigpy-0.80.1/Contributors.md000066400000000000000000000021071501451476000157640ustar00rootroot00000000000000# Contributors - [Russell Cloran] (https://github.com/rcloran) - [Alexei Chetroi] (https://github.com/Adminiuga) - [damarco] (https://github.com/damarco) - [Andreas Bomholtz] (https://github.com/AndreasBomholtz) - [puddly] (https://github.com/puddly) - [presslab-us] (https://github.com/presslab-us) - [Igor Bernstein] (https://github.com/igorbernstein2) - [David F. Mulcahey] (https://github.com/dmulcahey) - [Yoda-x] (https://github.com/Yoda-x) - [Solomon_M] (https://github.com/zalke) - [Pascal Vizeli] (https://github.com/pvizeli) - [prairiesnpr] (https://github.com/prairiesnpr) - [Jurriaan Pruis] (https://github.com/jurriaan) - [Marcel Hoppe] (https://github.com/hobbypunk90) - [felixstorm] (https://github.com/felixstorm) - [Dinko Bajric] (https://github.com/dbajric) - [Abílio Costa] (https://github.com/abmantis) - [https://github.com/SchaumburgM] (https://github.com/SchaumburgM) - [https://github.com/Nemesis24] (https://github.com/Nemesis24) - [Hedda] (https://github.com/Hedda) - [Andreas Setterlind] (https://github.com/Gamester17) - [lisongjun] (https://github.com/lisongjun12) zigpy-0.80.1/LICENSE000066400000000000000000001045131501451476000137560ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . zigpy-0.80.1/README.md000066400000000000000000000170571501451476000142360ustar00rootroot00000000000000# zigpy [![Build](https://github.com/zigpy/zigpy/workflows/CI/badge.svg?branch=dev)](https://github.com/zigpy/zigpy/workflows/CI/badge.svg?branch=dev) [![Coverage Status](https://codecov.io/gh/zigpy/zigpy/branch/dev/graph/badge.svg)](https://codecov.io/gh/zigpy/zigpy) **[zigpy](https://github.com/zigpy/zigpy)** is a hardware independent **[Zigbee protocol stack](https://en.wikipedia.org/wiki/Zigbee)** integration project to implement **[Zigbee](https://www.zigbee.org/)** standard specifications as a Python 3 library. Zigbee integration via zigpy allows you to connect one of many off-the-shelf Zigbee Coordinator adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), lights, switches, buttons, covers, fans, climate control equipment, locks, and intruder alarm system devices. Note that Zigbee Green Power devices [currently are unsupported](https://github.com/zigpy/zigpy/issues/341). Zigbee stacks and hardware from many different hardware chip manufacturers are supported via radio libraries which translate their proprietary communication protocol into a common API which is shared among all radio libraries for zigpy. If some Zigbee stack or Zigbee Coordinator hardware for other manufacturers is not supported by yet zigpy it is possible for any independent developer to step-up and develop a new radio library for zigpy which translates its proprietary communication protocol into the common API that zigpy can understand. zigpy contains common code implementing ZCL (Zigbee Cluster Library) and ZDO (Zigbee Device Object) application state management which is being used by various radio libraries implementing the actual interface with the radio modules from different manufacturers. The separate radio libraries interface with radio hardware adapters/modules over USB and GPIO using different native UART serial protocols. The **[ZHA integration component for Home Assistant](https://www.home-assistant.io/integrations/zha/)**, the [Zigbee Plugin for Domoticz](https://www.domoticz.com/wiki/ZigbeeForDomoticz), and the [Zigbee Plugin for Jeedom](https://doc.jeedom.com/en_US/plugins/automation%20protocol/zigbee/) (competing open-source home automation software) are all using [zigpy libraries](https://github.com/zigpy/) as dependencies, as such they could be used as references of different implementations if looking to integrate a Zigbee solution into your application. [![Zigpy - A library from the Open Home Foundation](https://www.openhomefoundation.org/badges/zigpy.png)](https://www.openhomefoundation.org/) ### Zigbee device OTA updates zigpy have ability to download and perform Zigbee OTAU (Over-The-Air Updates) of Zigbee devices firmware. The Zigbee OTA update firmware image files should conform to standard Zigbee OTA format and OTA provider source URLs need to be published for public availability. Updates from a local OTA update directory also is also supported and can be used as an option for offline firmware updates if user provide correct Zigbee OTA formatted firmware files themselves. Support for automatic download from existing online OTA providers in zigpy OTA provider code is currently only available for IKEA, Inovelli, LEDVANCE/OSRAM, and SONOFF/ITEAD devices. Support for additional OTA providers for other manufacturers devices could be added to zigpy in the future, if device manufacturers publish their firmware images publicly and developers contribute the needed download code for them. ## How to install and test, report bugs, or contribute to this project For specific instructions on how-to install and test zigpy or contribute bug-reports and code to this project please see the guidelines in the CONTRIBUTING.md file: - [Guidelines in CONTRIBUTING.md](./CONTRIBUTING.md) This CONTRIBUTING.md file will contain information about using zigpy, testing new releases, troubleshooting and bug-reporting as, as well as library + code instructions for developers and more. This file also contain short summaries and links to other related projects that directly or indirectly depends in zigpy libraries. You can contribute to this project either as an end-user, a tester (advanced user contributing constructive issue/bug-reports) or as a developer contributing code. ## Compatible Zigbee coordinator hardware Radio libraries for zigpy are separate projects with their own repositories and include **[bellows](https://github.com/zigpy/bellows)** (for communicating with Silicon Labs EmberZNet based radios), **[zigpy-deconz](https://github.com/zigpy/zigpy-deconz)** (for communicating with deCONZ based radios from Dresden Elektronik), and **[zigpy-xbee](https://github.com/zigpy/zigpy-xbee)** (for communicating with XBee based Zigbee radios), **[zigpy-zigate](https://github.com/zigpy/zigpy-zigate)** for communicating with ZiGate based radios, **[zigpy-znp](https://github.com/zha-ng/zigpy-znp)** or **[zigpy-cc](https://github.com/zigpy/zigpy-cc)** for communicating with Texas Instruments based radios that have Z-Stack ZNP coordinator firmware. Note! Zigbee 3.0 support or not in zigpy depends primarily on your Zigbee coordinator hardware and its firmware. Some Zigbee coordinator hardware support Zigbee 3.0 but might be shipped with an older firmware which does not, in which case may want to upgrade the firmware manually yourself. Some other Zigbee coordinator hardware may not support a firmware that is capable of Zigbee 3.0 at all but can still be fully functional and feature complete for your needs, (this is very common as many if not most Zigbee devices do not yet Zigbee 3.0 or are backwards-compable with a Zigbee profile that is support by your Zigbee coordinator hardware and its firmware). As a general rule, newer Zigbee coordinator hardware released can normally support Zigbee 3.0 firmware and it is up to its manufacturer to make such firmware available for them. ### Compatible zigpy radio libraries - **Digi XBee** based Zigbee radios via the [zigpy-xbee](https://github.com/zigpy/zigpy-xbee) library for zigpy. - **dresden elektronik** deCONZ based Zigbee radios via the [zigpy-deconz](https://github.com/zigpy/zigpy-deconz) library for zigpy. - **Silicon Labs** (EmberZNet) based Zigbee radios using the EZSP protocol via the [bellows](https://github.com/zigpy/bellows) library for zigpy. - **Texas Instruments** based Zigbee radios with all compatible Z-Stack firmware via the [zigpy-znp](https://github.com/zha-ng/zigpy-znp) library for zigpy. - **ZiGate** based Zigbee radios via the [zigpy-zigate](https://github.com/zigpy/zigpy-zigate) library for zigpy. ### Legacy or obsolete zigpy radio libraries - Texas Instruments with Z-Stack legacy firmware via the [zigpy-cc](https://github.com/zigpy/zigpy-cc) library for zigpy. ## Release packages available via PyPI New packages of tagged versions are also released via the "zigpy" project on PyPI - https://pypi.org/project/zigpy/ - https://pypi.org/project/zigpy/#history - https://pypi.org/project/zigpy/#files Older packages of tagged versions are still available on the "zigpy-homeassistant" project on PyPI - https://pypi.org/project/zigpy-homeassistant/ Packages of tagged versions of the radio libraries are released via separate projects on PyPI - https://pypi.org/project/zigpy/ - https://pypi.org/project/bellows/ - https://pypi.org/project/zigpy-cc/ - https://pypi.org/project/zigpy-deconz/ - https://pypi.org/project/zigpy-xbee/ - https://pypi.org/project/zigpy-zigate/ - https://pypi.org/project/zigpy-znp/ zigpy-0.80.1/pyproject.toml000066400000000000000000000213241501451476000156630ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0.0", "wheel", "setuptools-git-versioning<2"] build-backend = "setuptools.build_meta" [project] name = "zigpy" dynamic = ["version"] description = "Library implementing a Zigbee stack" urls = {repository = "https://github.com/zigpy/zigpy"} authors = [ {name = "Russell Cloran", email = "rcloran@gmail.com"} ] readme = "README.md" license = {text = "GPL-3.0"} requires-python = ">=3.9" dependencies = [ "attrs", "aiohttp", "aiosqlite>=0.20.0", "crccheck", "cryptography", 'async-timeout; python_version<"3.11"', "voluptuous", "jsonschema", 'pyserial-asyncio; platform_system!="Windows"', 'pyserial-asyncio!=0.5; platform_system=="Windows"', "typing_extensions", "frozendict", ] [tool.setuptools.packages.find] exclude = ["tests", "tests.*"] [tool.setuptools.package-data] "*" = ["appdb_schemas/schema_v*.sql"] [project.optional-dependencies] testing = [ "tomli", "asynctest", "coveralls", "coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-timeout", "freezegun", 'pysqlite3-binary; platform_system=="Linux" and python_version<"3.12"', ] [tool.setuptools-git-versioning] enabled = true [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" [tool.ruff] required-version = ">=0.5.0" target-version = "py39" line-length = 88 exclude = [ ".venv", ".git", ".tox", "docs", "venv", "bin", "lib", "deps", "build", ] [tool.ruff.lint] select = [ "A001", # Variable {name} is shadowing a Python builtin "ASYNC210", # Async functions should not call blocking HTTP methods "ASYNC220", # Async functions should not create subprocesses with blocking methods "ASYNC221", # Async functions should not run processes with blocking methods "ASYNC222", # Async functions should not wait on processes with blocking methods "ASYNC230", # Async functions should not open files with blocking methods like open "ASYNC251", # Async functions should not call time.sleep "B002", # Python does not support the unary prefix increment "B005", # Using .strip() with multi-character strings is misleading "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "B015", # Pointless comparison. Did you mean to assign a value? Otherwise, prepend assert or remove it. "B017", # pytest.raises(BaseException) should be considered evil "B018", # Found useless attribute access. Either assign it to a variable or remove it. "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged "B032", # Possible unintentional type annotation (using :). Did you mean to assign (using =)? "B904", # Use raise from to specify exception cause "B905", # zip() without an explicit strict= parameter "BLE", "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format "I", # isort "INP", # flake8-no-pep420 "ISC", # flake8-implicit-str-concat "ICN001", # import concentions; {name} should be imported as {asname} "LOG", # flake8-logging "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase "PERF", # Perflint "PGH", # pygrep-hooks "PIE", # flake8-pie "PL", # pylint "PT", # flake8-pytest-style "PYI", # flake8-pyi "RET", # flake8-return "RSE", # flake8-raise "RUF005", # Consider iterable unpacking instead of concatenation "RUF006", # Store a reference to the return value of asyncio.create_task "RUF010", # Use explicit conversion flag "RUF013", # PEP 484 prohibits implicit Optional "RUF017", # Avoid quadratic list summation "RUF018", # Avoid assignment expressions in assert statements "RUF019", # Unnecessary key check before dictionary access # "RUF100", # Unused `noqa` directive; temporarily every now and then to clean them up "S102", # Use of exec detected "S103", # bad-file-permissions "S108", # hardcoded-temp-file "S306", # suspicious-mktemp-usage "S307", # suspicious-eval-usage "S313", # suspicious-xmlc-element-tree-usage "S314", # suspicious-xml-element-tree-usage "S315", # suspicious-xml-expat-reader-usage "S316", # suspicious-xml-expat-builder-usage "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage "S319", # suspicious-xml-pull-dom-usage "S320", # suspicious-xmle-tree-usage "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection "SIM", # flake8-simplify "SLF", # flake8-self "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print "TID251", # Banned imports "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle ] ignore = [ # TODO: these are reasonable and should be fixed! "D101", # Missing docstring in public class "D102", # Missing docstring in public method "D103", # Missing docstring in public function "SLF001", # Private member access "D106", # Undocumented public nested class "D401", # Non-imperative mood "D400", # Ends in period "D415", "D405", "D100", "D107", "D105", "B007", "D104", "D205", # Personal preference "SIM117", "S608", # We have few SQL queries and they're all safe "RET505", "RET506", "RET507", "C901", "PERF203", "D203", "D213", # From Home Assistant "D202", # No blank lines allowed after function docstring "E501", # line too long "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "PT004", # Fixture {fixture} does not return anything, add leading underscore "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception "PT018", # Assertion should be broken down into multiple parts "SIM102", # Use a single if statement instead of nested if statements "SIM103", # Return the condition {condition} directly "SIM108", # Use ternary operator {contents} instead of if-else-block "TRY003", # Avoid specifying long messages outside the exception class "TRY400", # Use `logging.exception` instead of `logging.error` # Ignored due to performance: https://github.com/charliermarsh/ruff/issues/2923 # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", "E114", "E117", "D206", "D300", "Q", "COM812", "COM819", "ISC001", # temporarily disabled "PYI024", # Use typing.NamedTuple instead of collections.namedtuple "RET503", "TRY002", ] [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["F811"] [tool.ruff.lint.isort] force-sort-within-sections = true known-first-party = [ "zigpy", ] combine-as-imports = true split-on-trailing-comma = false [tool.codespell] ignore-words-list = ["ser", "nd", "hass", "checkin", "socio-economic", "IntStruct"] skip = ["./.*", "tests/*", "pyproject.toml"] quiet-level = 2 [tool.mypy] ignore_missing_imports = true install_types = true non_interactive = true show_error_codes = true show_error_context = true error_summary = true disable_error_code = [ # Only a few notifications left: "type-arg", # Only a few notifications left: "return-value", # operator breaks in CI (zigpy.types.basic), but not locally "operator", "valid-type", "misc", "attr-defined", "assignment", "arg-type" ] # Only report on selected files follow_imports = "silent" [tool.coverage.run] source = ["zigpy"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", "raise NotImplementedError", "raise NotImplementedError()", ] zigpy-0.80.1/requirements_test.txt000066400000000000000000000003131501451476000172650ustar00rootroot00000000000000# Test dependencies asynctest coveralls coverage[toml] pytest pytest-asyncio pytest-cov pytest-timeout freezegun tomli aioresponses pysqlite3-binary; platform_system=="Linux" and python_version<"3.12" zigpy-0.80.1/ruff.toml000066400000000000000000000055411501451476000146110ustar00rootroot00000000000000target-version = "py38" select = [ "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "C", # complexity "D", # docstrings "E", # pycodestyle "F", # pyflakes/autoflake "ICN001", # import concentions; {name} should be imported as {asname} "PGH004", # Use specific rule codes when using noqa "PLC0414", # Useless import alias. Import alias does not rename original package. "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass "SIM117", # Merge with-statements that use the same scope "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() "SIM201", # Use {left} != {right} instead of not {left} == {right} "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. "SIM401", # Use get from dict with default instead of an if block "T20", # flake8-print "TRY004", # Prefer TypeError exception for invalid type "RUF006", # Store a reference to the return value of asyncio.create_task "UP", # pyupgrade "W", # pycodestyle ] ignore = [ "D100", # Missing docstring in public module "D101", # Missing docstring in public class "D102", # Missing docstring in public method "D103", # Missing docstring in public function "D104", # Missing docstring in public package "D105", # Missing docstring in magic method "D106", # Missing docstring in public nested class "D107", # Missing docstring in `__init__` "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D205", # 1 blank line required between summary line and description "D213", # Multi-line docstring summary should start at the second line "D400", # First line should end with a period "D401", # First line of docstring should be in imperative mood: "D406", # Section name should end with a newline "D407", # Section name underlining "D415", # First line should end with a period, question mark, or exclamation point "E501", # line too long # the rules below this line should be corrected "E731", # do not assign a lambda expression, use a def "B007", # Loop control variable `id_` not used within loop body "PGH004", # Use specific rule codes when using `noqa` "TRY004", # Prefer `TypeError` exception for invalid type ] extend-exclude = [ "tests" ] [flake8-pytest-style] fixture-parentheses = false [pyupgrade] keep-runtime-typing = true [isort] # will group `import x` and `from x import` of the same module. force-sort-within-sections = true known-first-party = [ "zigpy", "tests", ] forced-separate = ["tests"] combine-as-imports = true [mccabe] max-complexity = 25 zigpy-0.80.1/setup.py000066400000000000000000000001051501451476000144530ustar00rootroot00000000000000import setuptools if __name__ == "__main__": setuptools.setup() zigpy-0.80.1/tests/000077500000000000000000000000001501451476000141075ustar00rootroot00000000000000zigpy-0.80.1/tests/__init__.py000066400000000000000000000000251501451476000162150ustar00rootroot00000000000000"""Tests modules.""" zigpy-0.80.1/tests/async_mock.py000066400000000000000000000017221501451476000166110ustar00rootroot00000000000000"""Mock utilities that are async aware.""" from unittest.mock import * # noqa: F401, F403 class _IntSentinelObject(int): """Sentinel-like object that is also an integer subclass. Allows sentinels to be used in loggers that perform int-specific string formatting. """ def __new__(cls, name): instance = super().__new__(cls, 0) instance.name = name return instance def __repr__(self): return f"int_sentinel.{self.name}" def __hash__(self): return hash((int(self), self.name)) def __eq__(self, other): return self is other __str__ = __reduce__ = __repr__ class _IntSentinel: def __init__(self): self._sentinels = {} def __getattr__(self, name): if name == "__bases__": raise AttributeError return self._sentinels.setdefault(name, _IntSentinelObject(name)) def __reduce__(self): return "int_sentinel" int_sentinel = _IntSentinel() zigpy-0.80.1/tests/conftest.py000066400000000000000000000176571501451476000163260ustar00rootroot00000000000000"""Common fixtures.""" from __future__ import annotations import asyncio import copy import logging import threading import typing from unittest.mock import Mock import pytest import zigpy.application from zigpy.config import ( CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH, CONF_OTA, CONF_OTA_ENABLED, ) import zigpy.state as app_state import zigpy.types as t import zigpy.zdo.types as zdo_t from .async_mock import AsyncMock, MagicMock if typing.TYPE_CHECKING: import zigpy.device _LOGGER = logging.getLogger(__name__) NCP_IEEE = t.EUI64.convert("aa:11:22:bb:33:44:be:ef") class FailOnBadFormattingHandler(logging.Handler): def emit(self, record): try: record.msg % record.args except Exception as e: # noqa: BLE001 pytest.fail( f"Failed to format log message {record.msg!r} with {record.args!r}: {e}" ) @pytest.fixture(autouse=True) def raise_on_bad_log_formatting(): handler = FailOnBadFormattingHandler() root = logging.getLogger() root.addHandler(handler) root.setLevel(logging.DEBUG) try: yield finally: root.removeHandler(handler) class App(zigpy.application.ControllerApplication): async def send_packet(self, packet): pass async def connect(self): pass async def disconnect(self): pass async def start_network(self): dev = add_initialized_device( app=self, nwk=self.state.node_info.nwk, ieee=self.state.node_info.ieee ) dev.model = "Coordinator Model" dev.manufacturer = "Coordinator Manufacturer" dev.zdo.Mgmt_NWK_Update_req = AsyncMock( return_value=[ zdo_t.Status.SUCCESS, t.Channels.ALL_CHANNELS, 0, 0, [80] * 16, ] ) async def force_remove(self, dev): pass async def add_endpoint(self, descriptor): pass async def permit_ncp(self, time_s=60): pass async def permit_with_link_key(self, node, link_key, time_s=60): pass async def reset_network_info(self): pass async def write_network_info(self, *, network_info, node_info): pass async def load_network_info(self, *, load_devices=False): self.state.network_info.channel = 15 async def _network_scan(self, channels, duration_exp): if False: yield async def _packet_capture(self, channel): if False: yield async def _packet_capture_change_channel(self, channel): pass def recursive_dict_merge( obj: dict[str, typing.Any], updates: dict[str, typing.Any] ) -> dict[str, typing.Any]: result = copy.deepcopy(obj) for key, update in updates.items(): if isinstance(update, dict) and key in result: result[key] = recursive_dict_merge(result[key], update) else: result[key] = update return result def make_app( config_updates: dict[str, typing.Any], app_base: zigpy.application.ControllerApplication = App, ) -> zigpy.application.ControllerApplication: config = recursive_dict_merge( { CONF_DATABASE: None, CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/null"}, CONF_OTA: { CONF_OTA_ENABLED: False, }, }, config_updates, ) app = app_base(config) app.state.node_info = app_state.NodeInfo( nwk=t.NWK(0x0000), ieee=NCP_IEEE, logical_type=zdo_t.LogicalType.Coordinator ) app.device_initialized = Mock(wraps=app.device_initialized) app.listener_event = Mock(wraps=app.listener_event) app.get_sequence = MagicMock(wraps=app.get_sequence, return_value=123) app.send_packet = AsyncMock(wraps=app.send_packet) app.write_network_info = AsyncMock(wraps=app.write_network_info) return app @pytest.fixture def app(): """ControllerApplication Mock.""" return make_app({}) @pytest.fixture def app_mock(): """ControllerApplication Mock.""" return make_app({}) def make_ieee(start=0): return t.EUI64(map(t.uint8_t, range(start, start + 8))) def make_node_desc( *, logical_type: zdo_t.LogicalType = zdo_t.LogicalType.Router ) -> zdo_t.NodeDescriptor: return zdo_t.NodeDescriptor( logical_type=logical_type, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=zdo_t.NodeDescriptor.FrequencyBand.Freq2400MHz, mac_capability_flags=zdo_t.NodeDescriptor.MACCapabilityFlags.AllocateAddress, manufacturer_code=4174, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=0, maximum_outgoing_transfer_size=82, descriptor_capability_field=zdo_t.NodeDescriptor.DescriptorCapability.NONE, ) def add_initialized_device(app, nwk, ieee): dev = app.add_device(nwk=nwk, ieee=ieee) dev.node_desc = make_node_desc(logical_type=zdo_t.LogicalType.Router) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = zigpy.profiles.zha.DeviceType.PUMP return dev @pytest.fixture def make_initialized_device(): count = 1 def inner(app): nonlocal count dev = add_initialized_device(app, nwk=0x1000 + count, ieee=make_ieee(count)) count += 1 return dev return inner def make_neighbor( *, ieee: t.EUI64, nwk: t.NWK, device_type: zdo_t.Neighbor.DeviceType = zdo_t.Neighbor.DeviceType.Router, rx_on_when_idle=True, relationship: zdo_t.Neighbor.Relationship = zdo_t.Neighbor.Relationship.Child, ) -> zdo_t.Neighbor: return zdo_t.Neighbor( extended_pan_id=make_ieee(start=0), ieee=ieee, nwk=nwk, device_type=device_type, rx_on_when_idle=int(rx_on_when_idle), relationship=relationship, reserved1=0, permit_joining=0, reserved2=0, depth=15, lqi=250, ) def make_neighbor_from_device( device: zigpy.device.Device, *, relationship: zdo_t.Neighbor.Relationship = zdo_t.Neighbor.Relationship.Child, ): assert device.node_desc is not None return make_neighbor( ieee=device.ieee, nwk=device.nwk, device_type=zdo_t.Neighbor.DeviceType(int(device.node_desc.logical_type)), rx_on_when_idle=device.node_desc.is_receiver_on_when_idle, relationship=relationship, ) def make_route( *, dest_nwk: t.NWK, next_hop: t.NWK, status: zdo_t.RouteStatus = zdo_t.RouteStatus.Active, ) -> zdo_t.Route: return zdo_t.Route( DstNWK=dest_nwk, RouteStatus=status, MemoryConstrained=0, ManyToOne=0, RouteRecordRequired=0, Reserved=0, NextHop=next_hop, ) # Taken from Home Assistant's `conftest.py` @pytest.fixture(autouse=True) def verify_cleanup( event_loop: asyncio.AbstractEventLoop, ) -> typing.Generator[None, None, None]: """Verify that the test has cleaned up resources correctly.""" threads_before = frozenset(threading.enumerate()) tasks_before = asyncio.all_tasks(event_loop) yield event_loop.run_until_complete(event_loop.shutdown_default_executor()) # Warn and clean-up lingering tasks and timers # before moving on to the next test. tasks = asyncio.all_tasks(event_loop) - tasks_before for task in tasks: _LOGGER.warning("Linger task after test %r", task) task.cancel() if tasks: event_loop.run_until_complete(asyncio.wait(tasks)) for handle in event_loop._scheduled: # type: ignore[attr-defined] if not handle.cancelled(): _LOGGER.warning("Lingering timer after test %r", handle) handle.cancel() # Verify no threads where left behind. threads = frozenset(threading.enumerate()) - threads_before for thread in threads: assert isinstance(thread, threading._DummyThread) zigpy-0.80.1/tests/databases/000077500000000000000000000000001501451476000160365ustar00rootroot00000000000000zigpy-0.80.1/tests/databases/bad_attrs_v3.db000066400000000000000000006500001501451476000207210ustar00rootroot00000000000000SQLite format 3@ 5).C mM 3  3N=J!iindexrelays_idxrelaysCREATE UNIQUE INDEX relays_idx ON relays(ieee)wtablerelaysrelaysCREATE TABLE relays (ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE)~/'3indexgroup_members_idxgroup_membersCREATE UNIQUE INDEX group_members_idx ON group_members(group_id, ieee, endpoint_id)3''%tablegroup_membersgroup_membersCREATE TABLE group_members (group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE)Loindexgroup_idxgroupsCREATE UNIQUE INDEX group_idx ON groups(group_id)<UtablegroupsgroupsCREATE TABLE groups (group_id, name)w'!3indexattribute_idxattributesCREATE UNIQUE INDEX attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid)' !!tableattributesattributesCREATE TABLE attributes (ieee ieee, endpoint_id, cluster, attrid, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE) 1+7indexoutput_cluster_idxoutput_clusters CREATE UNIQUE INDEX output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster)C ++=tableoutput_clustersoutput_clusters CREATE TABLE output_clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE)s 5-indexnode_descriptors_idxnode_descriptors CREATE UNIQUE INDEX node_descriptors_idx ON node_descriptors(ieee) --itablenode_descriptorsnode_descriptors CREATE TABLE node_descriptors (ieee ieee, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE)V'uindexneighbors_idxneighbors CREATE INDEX neighbors_idx ON neighbors(device_ieee)F[tableneighborsneighborsCREATE TABLE neighbors (device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL,ieee ieee NOT NULL, nwk INTEGER NOT NULL, struct INTEGER NOT NULL, permit_joining INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices(ieee) ON DELETE CASCADE)g#indexcluster_idxclustersCREATE UNIQUE INDEX cluster_idx ON clusters(ieee, endpoint_id, cluster)./tableclustersclustersCREATE TABLE clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE)b% indexendpoint_idxendpointsCREATE UNIQUE INDEX endpoint_idx ON endpoints(ieee, endpoint_id)9AtableendpointsendpointsCREATE TABLE endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE)Hgindexieee_idxdevicesCREATE UNIQUE INDEX ieee_idx ON devices(ieee)GgtabledevicesdevicesCREATE TABLE devices (ieee ieee, nwk, status)   w _ :>  V 5   q P 0{[ !;60:a4:23:ff:fe:02:34:91 ;60:a4:23:ff:fe:02:36:24M;80:4b:50:ff:fe:41:67:d4;68:0a:e2:ff:fe:70:00:69;58:8e:81:ff:fe:15:e3:ffH;ec:1b:bd:ff:fe:37:72:7eG ;ec:1b:bd:ff:fe:33:a0:04S;00:15:8d:00:05:1e:13:46;00:15:8d:00:05:4a:73:c3u6;00:15:8d:00:05:1e:0e:32;60:a4:23:ff:fe:02:32:30;60:a4:23:ff:fe:02:36:93_;60:a4:23:ff:fe:02:30:39;60:a4:23:ff:fe:02:54:1eq;60:a4:23:ff:fe:02:38:60K;60:a4:23:ff:fe:02:2f:4d=;60:a4:23:ff:fe:02:38:59XM;60:a4:23:ff:fe:02:2f:96m ;80:4b:50:ff:fe:41:58:f3;80:4b:50:ff:fe:41:59:63;60:a4:23:ff:fe:02:36:a0G ;60:a4:23:ff:fe:02:3b:b4 ;ec:1b:bd:ff:fe:94:18:a4;60:a4:23:ff:fe:02:36:9c;60:a4:23:ff:fe:02:51:70Ջ;60:a4:23:ff:fe:02:38:af;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:2f:42_ ;cc:cc:cc:ff:fe:a5:f2:83  a } %Ay !u E=Y]  ) ;68:0a:e2:ff:fe:70:00:69;58:8e:81:ff:fe:15:e3:ff;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:33:a0:04;60:a4:23:ff:fe:02:38:59;00:15:8d:00:05:1e:13:46;00:15:8d:00:05:4a:73:c3;00:15:8d:00:05:1e:0e:32;60:a4:23:ff:fe:02:32:30;60:a4:23:ff:fe:02:34:91;60:a4:23:ff:fe:02:30:39;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:2f:42;60:a4:23:ff:fe:02:2f:4d;60:a4:23:ff:fe:02:51:70;60:a4:23:ff:fe:02:2f:96;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:59:63;80:4b:50:ff:fe:41:58:f3 ;60:a4:23:ff:fe:02:3b:b4 ;ec:1b:bd:ff:fe:94:18:a4 ;60:a4:23:ff:fe:02:36:9c;60:a4:23:ff:fe:02:38:60;60:a4:23:ff:fe:02:38:af;60:a4:23:ff:fe:02:36:a0;60:a4:23:ff:fe:02:36:24;60:a4:23:ff:fe:02:36:93;60:a4:23:ff:fe:02:39:7b; cc:cc:cc:ff:fe:a5:f2:83 6 p K )  t Q . | Z 8   oJ &K& r M o r N ) \ &6"; 60:a4:23:ff:fe:02:36:a0  #; 80:4b:50:ff:fe:41:58:f3a#; 80:4b:50:ff:fe:41:59:63a"; 80:4b:50:ff:fe:41:59:63  #; 80:4b:50:ff:fe:41:67:d4a#@; 68:0a:e2:ff:fe:70:00:69a"?; 68:0a:e2:ff:fe:70:00:69  >; 58:8e:81:ff:fe:15:e3:ff =; 58:8e:81:ff:fe:15:e3:ff <; 58:8e:81:ff:fe:15:e3:ff;;  58:8e:81:ff:fe:15:e3:ff"s;  ec:1b:bd:ff:fe:37:72:7e^"x;  ec:1b:bd:ff:fe:33:a0:04^#|; 60:a4:23:ff:fe:02:38:59a"{; 60:a4:23:ff:fe:02:38:59  !3;  00:15:8d:00:05:1e:13:46!2;  00:15:8d:00:05:4a:73:c3!1;  00:15:8d:00:05:1e:0e:32#0; 60:a4:23:ff:fe:02:32:30a"/; 60:a4:23:ff:fe:02:32:30  #J; 60:a4:23:ff:fe:02:34:91a"I; 60:a4:23:ff:fe:02:34:91  #z; 60:a4:23:ff:fe:02:30:39a"y; 60:a4:23:ff:fe:02:30:39  #u; 60:a4:23:ff:fe:02:54:1ea"t; 60:a4:23:ff:fe:02:54:1e  #\; 60:a4:23:ff:fe:02:2f:42a"[; 60:a4:23:ff:fe:02:2f:42  #; 60:a4:23:ff:fe:02:2f:4da"; 60:a4:23:ff:fe:02:2f:4d  #j; 60:a4:23:ff:fe:02:51:70a"i; 60:a4:23:ff:fe:02:51:70  #n; 60:a4:23:ff:fe:02:2f:96a"m; 60:a4:23:ff:fe:02:2f:96  "; 80:4b:50:ff:fe:41:67:d4  #"; 80:4b:50:ff:fe:41:58:f3  #; 60:a4:23:ff:fe:02:3b:b4a"; 60:a4:23:ff:fe:02:3b:b4  #r; ec:1b:bd:ff:fe:94:18:a4f"q;  ec:1b:bd:ff:fe:94:18:a4^ #; 60:a4:23:ff:fe:02:36:9ca"; 60:a4:23:ff:fe:02:36:9c  #f; 60:a4:23:ff:fe:02:38:60a"e; 60:a4:23:ff:fe:02:38:60  #p; 60:a4:23:ff:fe:02:38:afa"o; 60:a4:23:ff:fe:02:38:af  ##; 60:a4:23:ff:fe:02:36:a0a#^; 60:a4:23:ff:fe:02:36:24a"]; 60:a4:23:ff:fe:02:36:24  #V; 60:a4:23:ff:fe:02:36:93a"U; 60:a4:23:ff:fe:02:36:93  #`; 60:a4:23:ff:fe:02:39:7ba"_; 60:a4:23:ff:fe:02:39:7b  ";  cc:cc:cc:ff:fe:a5:f2:83 i6 C d * G x Z < u K ,  L-jX9 C v  W 8  b   ;60:a4:23:ff:fe:02:36:a0 ;80:4b:50:ff:fe:41:58:f3;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:59:63;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4 ;68:0a:e2:ff:fe:70:00:69@;68:0a:e2:ff:fe:70:00:69 ?;58:8e:81:ff:fe:15:e3:ff>;58:8e:81:ff:fe:15:e3:ff=;58:8e:81:ff:fe:15:e3:ff<; 58:8e:81:ff:fe:15:e3:ff;; ec:1b:bd:ff:fe:37:72:7es; ec:1b:bd:ff:fe:33:a0:04x;60:a4:23:ff:fe:02:38:59|;60:a4:23:ff:fe:02:38:59 {; 00:15:8d:00:05:1e:13:463; 00:15:8d:00:05:4a:73:c32; 00:15:8d:00:05:1e:0e:321;60:a4:23:ff:fe:02:32:300;60:a4:23:ff:fe:02:32:30 /;60:a4:23:ff:fe:02:34:91J;60:a4:23:ff:fe:02:34:91 I;60:a4:23:ff:fe:02:30:39z;60:a4:23:ff:fe:02:30:39 y;60:a4:23:ff:fe:02:54:1eu;60:a4:23:ff:fe:02:54:1e t;60:a4:23:ff:fe:02:2f:42\;60:a4:23:ff:fe:02:2f:42 [;60:a4:23:ff:fe:02:2f:4d;60:a4:23:ff:fe:02:2f:4d ;60:a4:23:ff:fe:02:51:70j;60:a4:23:ff:fe:02:51:70 i;60:a4:23:ff:fe:02:2f:96n;60:a4:23:ff:fe:02:2f:96 m ;80:4b:50:ff:fe:41:59:63 ;60:a4:23:ff:fe:02:3b:b4;60:a4:23:ff:fe:02:3b:b4 ;ec:1b:bd:ff:fe:94:18:a4r; ec:1b:bd:ff:fe:94:18:a4q;60:a4:23:ff:fe:02:36:9c;60:a4:23:ff:fe:02:36:9c ;60:a4:23:ff:fe:02:38:60f;60:a4:23:ff:fe:02:38:60 e;60:a4:23:ff:fe:02:38:afp;60:a4:23:ff:fe:02:38:af o;60:a4:23:ff:fe:02:36:a0;60:a4:23:ff:fe:02:36:24^;60:a4:23:ff:fe:02:36:24 ];60:a4:23:ff:fe:02:36:93V;60:a4:23:ff:fe:02:36:93 U;60:a4:23:ff:fe:02:39:7b`;60:a4:23:ff:fe:02:39:7b _; cc:cc:cc:ff:fe:a5:f2:83*.\!;60:a4:23:ff:fe:02:36:a0 ( +/@14h05!8%m.<#d,A(;68:0a:e2:ff:fe:70:00:69';60:a4:23:ff:fe:02:38:af&;60:a4:23:ff:fe:02:36:935  [f  G \ =  ( Q p z [3'E;&80:4b:50:ff:fe:41:58:f3@ORR,R'D;&80:4b:50:ff:fe:41:59:63@ORR,R'C;&80:4b:50:ff:fe:41:67:d4@ORR,R'!;&68:0a:e2:ff:fe:70:00:69@RR,R' ;&58:8e:81:ff:fe:15:e3:ff@$RR,R'<;&ec:1b:bd:ff:fe:37:72:7e@|RRR'?;&ec:1b:bd:ff:fe:33:a0:04@|RRR'A;&60:a4:23:ff:fe:02:38:59@RR,R';&00:15:8d:00:05:1e:13:46@7dd';&00:15:8d:00:05:4a:73:c3@7dd'6;&00:15:8d:00:05:1e:0e:32@7dd';&60:a4:23:ff:fe:02:32:30@ORR,R'&;&60:a4:23:ff:fe:02:34:91@ORR,R'@;&60:a4:23:ff:fe:02:30:39@RR,R'=;&60:a4:23:ff:fe:02:54:1e@RR,R'/;&60:a4:23:ff:fe:02:2f:42@RR,R';&60:a4:23:ff:fe:02:2f:4d@RR,R'7;&60:a4:23:ff:fe:02:51:70@RR,R'9;&60:a4:23:ff:fe:02:2f:96@RR,R' ;&60:a4:23:ff:fe:02:3b:b4@RR,R';;&ec:1b:bd:ff:fe:94:18:a4@$RR,R' ;&60:a4:23:ff:fe:02:36:9c@RR,R'4;&60:a4:23:ff:fe:02:38:60@RR,R':;&60:a4:23:ff:fe:02:38:af@RR,R'F;&60:a4:23:ff:fe:02:36:a0@RR,R'0;&60:a4:23:ff:fe:02:36:24@RR,R',;&60:a4:23:ff:fe:02:36:93@RR,R'1;&60:a4:23:ff:fe:02:39:7b@RR,R';&cc:cc:cc:ff:fe:a5:f2:83@ͫRA,  EyA %!u =Y] )  } a;80:4b:50:ff:fe:41:58:f3E;80:4b:50:ff:fe:41:59:63D;80:4b:50:ff:fe:41:67:d4C;68:0a:e2:ff:fe:70:00:69!;58:8e:81:ff:fe:15:e3:ff ;ec:1b:bd:ff:fe:37:72:7e<;ec:1b:bd:ff:fe:33:a0:04?;60:a4:23:ff:fe:02:38:59A;00:15:8d:00:05:1e:13:46;00:15:8d:00:05:4a:73:c3;00:15:8d:00:05:1e:0e:326;60:a4:23:ff:fe:02:32:30;60:a4:23:ff:fe:02:34:91&;60:a4:23:ff:fe:02:30:39@;60:a4:23:ff:fe:02:54:1e=;60:a4:23:ff:fe:02:2f:42/;60:a4:23:ff:fe:02:2f:4d;60:a4:23:ff:fe:02:51:707;60:a4:23:ff:fe:02:2f:969;60:a4:23:ff:fe:02:3b:b4 ;ec:1b:bd:ff:fe:94:18:a4;;60:a4:23:ff:fe:02:36:9c ;60:a4:23:ff:fe:02:38:604;60:a4:23:ff:fe:02:38:af:;60:a4:23:ff:fe:02:36:a0F;60:a4:23:ff:fe:02:36:240;60:a4:23:ff:fe:02:36:93,;60:a4:23:ff:fe:02:39:7b1; cc:cc:cc:ff:fe:a5:f2:83 9  d G )  a C $ jK+ pQ1vW7 Y 9 l L 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9\;68:0a:e2:ff:fe:70:00:69![;68:0a:e2:ff:fe:70:00:69 Z;68:0a:e2:ff:fe:70:00:69 Y;58:8e:81:ff:fe:15:e3:ffX;58:8e:81:ff:fe:15:e3:ffW;58:8e:81:ff:fe:15:e3:ffV;58:8e:81:ff:fe:15:e3:ffU;58:8e:81:ff:fe:15:e3:ffT;58:8e:81:ff:fe:15:e3:ffS;58:8e:81:ff:fe:15:e3:ffR;58:8e:81:ff:fe:15:e3:ffQ;58:8e:81:ff:fe:15:e3:ffP;58:8e:81:ff:fe:15:e3:ffO;58:8e:81:ff:fe:15:e3:ffN;58:8e:81:ff:fe:15:e3:ffM;58:8e:81:ff:fe:15:e3:ffL;58:8e:81:ff:fe:15:e3:ffK;58:8e:81:ff:fe:15:e3:ffJ;58:8e:81:ff:fe:15:e3:ffI;58:8e:81:ff:fe:15:e3:ffH;58:8e:81:ff:fe:15:e3:ffG;58:8e:81:ff:fe:15:e3:ffF;58:8e:81:ff:fe:15:e3:ffE;58:8e:81:ff:fe:15:e3:ffD;58:8e:81:ff:fe:15:e3:ffC;58:8e:81:ff:fe:15:e3:ffB;58:8e:81:ff:fe:15:e3:ffA; 58:8e:81:ff:fe:15:e3:ff@; 58:8e:81:ff:fe:15:e3:ff?; 58:8e:81:ff:fe:15:e3:ff>; 58:8e:81:ff:fe:15:e3:ff=; 58:8e:81:ff:fe:15:e3:ff<; 58:8e:81:ff:fe:15:e3:ff;; 58:8e:81:ff:fe:15:e3:ff:; 58:8e:81:ff:fe:15:e3:ff xUi;80:4b:50:ff:fe:41:58:f3!h;80:4b:50:ff:fe:41:58:f3 g;80:4b:50:ff:fe:41:59:63!f;80:4b:50:ff:fe:41:59:63 e;80:4b:50:ff:fe:41:67:d4!d;80:4b:50:ff:fe:41:67:d4 +; 00:15:8d:00:05:1e:13:46*; 00:15:8d:00:05:1e:13:46); 00:15:8d:00:05:1e:13:46(; 00:15:8d:00:05:4a:73:c3'; 00:15:8d:00:05:4a:73:c3&; 00:15:8d:00:05:1e:0e:32%; 00:15:8d:00:05:1e:0e:32$; 00:15:8d:00:05:1e:0e:32#;60:a4:23:ff:fe:02:32:30!";60:a4:23:ff:fe:02:32:30 c;60:a4:23:ff:fe:02:34:91!b;60:a4:23:ff:fe:02:34:91  `;60:a4:23:ff:fe:02:2f:4d! ;60:a4:23:ff:fe:02:3b:b4!!>;60:a4:23:ff:fe:02:36:9c!; cc:cc:cc:ff:fe:a5:f2:83 9e | !  ^ ? i J + lL, jJ* hH( < e ?  _  C C C C C C  ;68:0a:e2:ff:fe:70:00:69!\;68:0a:e2:ff:fe:70:00:69 [;68:0a:e2:ff:fe:70:00:69 Z ;58:8e:81:ff:fe:15:e3:ffY ;58:8e:81:ff:fe:15:e3:ffX;58:8e:81:ff:fe:15:e3:ffW;58:8e:81:ff:fe:15:e3:ffV;58:8e:81:ff:fe:15:e3:ffU;58:8e:81:ff:fe:15:e3:ffT;58:8e:81:ff:fe:15:e3:ffS;58:8e:81:ff:fe:15:e3:ffR ;58:8e:81:ff:fe:15:e3:ffQ ;58:8e:81:ff:fe:15:e3:ffP;58:8e:81:ff:fe:15:e3:ffO;58:8e:81:ff:fe:15:e3:ffN;58:8e:81:ff:fe:15:e3:ffM;58:8e:81:ff:fe:15:e3:ffL;58:8e:81:ff:fe:15:e3:ffK;58:8e:81:ff:fe:15:e3:ffJ ;58:8e:81:ff:fe:15:e3:ffI ;58:8e:81:ff:fe:15:e3:ffH;58:8e:81:ff:fe:15:e3:ffG;58:8e:81:ff:fe:15:e3:ffF;58:8e:81:ff:fe:15:e3:ffE;58:8e:81:ff:fe:15:e3:ffD;58:8e:81:ff:fe:15:e3:ffC;58:8e:81:ff:fe:15:e3:ffB; 58:8e:81:ff:fe:15:e3:ffA; 58:8e:81:ff:fe:15:e3:ff@; 58:8e:81:ff:fe:15:e3:ff?; 58:8e:81:ff:fe:15:e3:ff>; 58:8e:81:ff:fe:15:e3:ff=; 58:8e:81:ff:fe:15:e3:ff<; 58:8e:81:ff:fe:15:e3:ff;; 58:8e:81:ff:fe:15:e3:ff: \X ;80:4b:50:ff:fe:41:58:f3!i;80:4b:50:ff:fe:41:58:f3 h ;80:4b:50:ff:fe:41:59:63!g;80:4b:50:ff:fe:41:59:63 f ;80:4b:50:ff:fe:41:67:d4!e;80:4b:50:ff:fe:41:67:d4 d ; 00:15:8d:00:05:1e:13:46+; 00:15:8d:00:05:1e:13:46*; 00:15:8d:00:05:1e:13:46); 00:15:8d:00:05:4a:73:c3(; 00:15:8d:00:05:4a:73:c3' ; 00:15:8d:00:05:1e:0e:32&; 00:15:8d:00:05:1e:0e:32%; 00:15:8d:00:05:1e:0e:32$ ;60:a4:23:ff:fe:02:32:30!#;60:a4:23:ff:fe:02:32:30 " ;60:a4:23:ff:fe:02:34:91!c;60:a4:23:ff:fe:02:34:91 b c ;60:a4:23:ff:fe:02:2f:4d! ;60:a4:23:ff:fe:02:3b:b4! @ ;60:a4:23:ff:fe:02:36:9c!;  cc:cc:cc:ff:fe:a5:f2:83$Y $ c"c#; 60:a4:23:ff:fe:02:38:59 $;60:a4:23:ff:fe:02:54:1e @ q#$;60:a4:23:ff:fe:02:30:39 $ TeT#Living Room;Default Lightlink Group)Bedroom Lights'Office Lights#Hood Lights#Corner Lamp5Guest Bedroom Lights9Kitchen Ceiling Lights     S4 'reF  z [ <  :;60:a4:23:ff:fe:02:38:59 9;60:a4:23:ff:fe:02:3b:b4 8;80:4b:50:ff:fe:41:58:f3 7;60:a4:23:ff:fe:02:36:9c 6;80:4b:50:ff:fe:41:59:63 5;80:4b:50:ff:fe:41:67:d4 4;80:4b:50:ff:fe:41:59:63 /;60:a4:23:ff:fe:02:51:70 ;60:a4:23:ff:fe:02:32:30 &;60:a4:23:ff:fe:02:34:91 );60:a4:23:ff:fe:02:38:af $;60:a4:23:ff:fe:02:39:7b #;60:a4:23:ff:fe:02:36:93 *; ec:1b:bd:ff:fe:37:72:7e0; ec:1b:bd:ff:fe:33:a0:043;80:4b:50:ff:fe:41:58:f3 2;80:4b:50:ff:fe:41:67:d4 +;60:a4:23:ff:fe:02:2f:42 (;60:a4:23:ff:fe:02:38:60 -;60:a4:23:ff:fe:02:54:1e ,;60:a4:23:ff:fe:02:2f:96 "; cc:cc:cc:ff:fe:a5:f2:83;60:a4:23:ff:fe:02:2f:4d ';60:a4:23:ff:fe:02:36:24  a A! ffF&  & F F;60:a4:23:ff:fe:02:38:59 :;60:a4:23:ff:fe:02:3b:b4 9;80:4b:50:ff:fe:41:58:f3 8;60:a4:23:ff:fe:02:36:9c 7;80:4b:50:ff:fe:41:59:63 6;80:4b:50:ff:fe:41:67:d4 5;80:4b:50:ff:fe:41:59:63 4;60:a4:23:ff:fe:02:51:70 /;60:a4:23:ff:fe:02:36:24 ';60:a4:23:ff:fe:02:32:30 ;60:a4:23:ff:fe:02:34:91 &;60:a4:23:ff:fe:02:38:af );60:a4:23:ff:fe:02:39:7b $;60:a4:23:ff:fe:02:36:93 #; ec:1b:bd:ff:fe:37:72:7e*; ec:1b:bd:ff:fe:33:a0:040;80:4b:50:ff:fe:41:58:f3 3;80:4b:50:ff:fe:41:67:d4 2;60:a4:23:ff:fe:02:2f:42 +;60:a4:23:ff:fe:02:38:60 (;60:a4:23:ff:fe:02:54:1e -;60:a4:23:ff:fe:02:2f:96 ,; cc:cc:cc:ff:fe:a5:f2:83" ;60:a4:23:ff:fe:02:2f:4d    ~_@ pO . Q#.# ;80:4b:50:ff:fe:41:67:d4w;60:a4:23:ff:fe:02:36:a0;ec:1b:bd:ff:fe:94:18:a4KE;60:a4:23:ff:fe:02:36:24!;80:4b:50:ff:fe:41:59:63G;60:a4:23:ff:fe:02:51:70K;80:4b:50:ff:fe:41:58:f3GD;ec:1b:bd:ff:fe:33:a0:04q;00:15:8d:00:05:1e:13:46K;68:0a:e2:ff:fe:70:00:69s;58:8e:81:ff:fe:15:e3:ff%m;60:a4:23:ff:fe:02:36:93;60:a4:23:ff:fe:02:38:60;60:a4:23:ff:fe:02:38:afL;00:15:8d:00:05:4a:73:c3TK.;60:a4:23:ff:fe:02:39:7ba;00:15:8d:00:05:1e:0e:32  \z>L.jj \ jj    Z e9 e e G - Q79< jj;80:4b:50:ff:fe:41:58:f3&;00:15:8d:00:05:1e:0e:32 ";60:a4:23:ff:fe:02:38:af ݏ;00:15:8d:00:05:4a:73:c3 ;58:8e:81:ff:fe:15:e3:ff s;00:15:8d:00:05:1e:13:46&;60:a4:23:ff:fe:02:36:a0&w;80:4b:50:ff:fe:41:59:63&;60:a4:23:ff:fe:02:36:93 m;60:a4:23:ff:fe:02:38:60 ݓ;68:0a:e2:ff:fe:70:00:69 ;60:a4:23:ff:fe:02:51:70&;ec:1b:bd:ff:fe:94:18:a4&;60:a4:23:ff:fe:02:36:24&E;ec:1b:bd:ff:fe:33:a0:04%D;80:4b:50:ff:fe:41:67:d4&;60:a4:23:ff:fe:02:39:7b #. ;09u|V1sY2   Nc r L X -  ` 9  s M (8sHL%X000000000000000000%%%%%%tR"_>{T<$Y;60:a4:23:ff:fe:02:36:9c @ $X;60:a4:23:ff:fe:02:36:9c @ < ; Iec:1b:bd:ff:fe:33:a0:04TRADFRI bulb E14 W op/ch 400lmTu ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%HTt ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%STr ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%>TU ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6558:8e:81:ff:fe:15:e3:ffHV ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30%U~ ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%xU} ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%=U| ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91%UU{ ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eݢ%MUz ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%CVy ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%Uw ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%+Uv ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%4Uu ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e%6Ut ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%iTs ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59}%5Tr ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%>Tq ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04d{%6Tp ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39Q%-Uo ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24MUn ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0Mx%Tm ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%*Ul ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%Qk ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$M q)^<xY: v W 7  s S 2  m L + K g G '  d C " )o|[:{Z9rQ0iH'`?wX8vV5m!;60:a4:23:ff:fe:02:2f:96 !;60:a4:23:ff:fe:02:2f:42 w;60:a4:23:ff:fe:02:30:39 " ;60:a4:23:ff:fe:02:36:a0 ' ;60:a4:23:ff:fe:02:36:a0 & ;60:a4:23:ff:fe:02:36:a0 % ;60:a4:23:ff:fe:02:36:a0 $ ;60:a4:23:ff:fe:02:36:a0 #;60:a4:23:ff:fe:02:36:a0 " ;60:a4:23:ff:fe:02:36:9c 8 ;60:a4:23:ff:fe:02:36:9c 7;60:a4:23:ff:fe:02:36:9c 6;60:a4:23:ff:fe:02:36:9c 5;60:a4:23:ff:fe:02:36:9c 4;60:a4:23:ff:fe:02:36:9c 3;60:a4:23:ff:fe:02:36:9c 2;60:a4:23:ff:fe:02:36:9c 1!;60:a4:23:ff:fe:02:36:93 _!;60:a4:23:ff:fe:02:36:93 ^ ;60:a4:23:ff:fe:02:36:93 ] ;60:a4:23:ff:fe:02:36:93 \ ;60:a4:23:ff:fe:02:36:93 [ ;60:a4:23:ff:fe:02:36:93 Z ;60:a4:23:ff:fe:02:36:93 Y;60:a4:23:ff:fe:02:36:93 X!;60:a4:23:ff:fe:02:36:24 !;60:a4:23:ff:fe:02:36:24 ~ ;60:a4:23:ff:fe:02:36:24 } ;60:a4:23:ff:fe:02:36:24 | ;60:a4:23:ff:fe:02:36:24 { ;60:a4:23:ff:fe:02:36:24 z ;60:a4:23:ff:fe:02:36:24 y;60:a4:23:ff:fe:02:36:24 x!;60:a4:23:ff:fe:02:34:91 /!;60:a4:23:ff:fe:02:34:91 . ;60:a4:23:ff:fe:02:34:91 - ;60:a4:23:ff:fe:02:34:91 , ;60:a4:23:ff:fe:02:34:91 + ;60:a4:23:ff:fe:02:34:91 * ;60:a4:23:ff:fe:02:34:91 );60:a4:23:ff:fe:02:34:91 (!;60:a4:23:ff:fe:02:32:30 !;60:a4:23:ff:fe:02:32:30  ;60:a4:23:ff:fe:02:32:30  ;60:a4:23:ff:fe:02:32:30  ;60:a4:23:ff:fe:02:32:30  ;60:a4:23:ff:fe:02:32:30  ;60:a4:23:ff:fe:02:32:30 ;60:a4:23:ff:fe:02:32:30 :!;60:a4:23:ff:fe:02:30:39 !;60:a4:23:ff:fe:02:30:39  ;60:a4:23:ff:fe:02:30:39  ;60:a4:23:ff:fe:02:30:39  ;60:a4:23:ff:fe:02:30:39  ;60:a4:23:ff:fe:02:30:39  ;60:a4:23:ff:fe:02:30:39 !;60:a4:23:ff:fe:02:2f:96  ;60:a4:23:ff:fe:02:2f:96  ;60:a4:23:ff:fe:02:2f:96  ;60:a4:23:ff:fe:02:2f:96  ;60:a4:23:ff:fe:02:2f:96  ;60:a4:23:ff:fe:02:2f:96 ;60:a4:23:ff:fe:02:2f:96  ;60:a4:23:ff:fe:02:2f:4d } ;60:a4:23:ff:fe:02:2f:4d |;60:a4:23:ff:fe:02:2f:4d {;60:a4:23:ff:fe:02:2f:4d z;60:a4:23:ff:fe:02:2f:4d y;60:a4:23:ff:fe:02:2f:4d x;60:a4:23:ff:fe:02:2f:4d w;60:a4:23:ff:fe:02:2f:4d v!;60:a4:23:ff:fe:02:2f:42 v ;60:a4:23:ff:fe:02:2f:42 u ;60:a4:23:ff:fe:02:2f:42 t ;60:a4:23:ff:fe:02:2f:42 s ;60:a4:23:ff:fe:02:2f:42 r ;60:a4:23:ff:fe:02:2f:42 q;60:a4:23:ff:fe:02:2f:42 p!;58:8e:81:ff:fe:15:e3:ff  ;58:8e:81:ff:fe:15:e3:ff; 58:8e:81:ff:fe:15:e3:ff;58:8e:81:ff:fe:15:e3:ff!;58:8e:81:ff:fe:15:e3:ff  ;58:8e:81:ff:fe:15:e3:ff; 58:8e:81:ff:fe:15:e3:ff;58:8e:81:ff:fe:15:e3:ff!;58:8e:81:ff:fe:15:e3:ff  ;58:8e:81:ff:fe:15:e3:ff; 58:8e:81:ff:fe:15:e3:ff;58:8e:81:ff:fe:15:e3:ff ; 58:8e:81:ff:fe:15:e3:ff ; 58:8e:81:ff:fe:15:e3:ff; 58:8e:81:ff:fe:15:e3:ff; 58:8e:81:ff:fe:15:e3:ff!; 00:15:8d:00:05:4a:73:c3 ; 00:15:8d:00:05:4a:73:c3 ; 00:15:8d:00:05:4a:73:c3 ; 00:15:8d:00:05:4a:73:c3; 00:15:8d:00:05:4a:73:c3; 00:15:8d:00:05:4a:73:c3; 00:15:8d:00:05:4a:73:c3!; 00:15:8d:00:05:1e:13:46 ; 00:15:8d:00:05:1e:13:46 ; 00:15:8d:00:05:1e:13:46 ; 00:15:8d:00:05:1e:13:46; 00:15:8d:00:05:1e:13:46; 00:15:8d:00:05:1e:13:46!; 00:15:8d:00:05:1e:0e:32 ; 00:15:8d:00:05:1e:0e:32 ; 00:15:8d:00:05:1e:0e:32 ; 00:15:8d:00:05:1e:0e:32; 00:15:8d:00:05:1e:0e:32; 00:15:8d:00:05:1e:0e:32 o}\;tS2 k J )  b A { [ ;  y X 7  p O . gF%xW6pO. `?iI) hH(gG' ; ec:1b:bd:ff:fe:94:18:a4 ; ec:1b:bd:ff:fe:94:18:a4  ; ec:1b:bd:ff:fe:94:18:a4; ec:1b:bd:ff:fe:94:18:a4; ec:1b:bd:ff:fe:94:18:a4; ec:1b:bd:ff:fe:94:18:a4; ec:1b:bd:ff:fe:94:18:a4; ec:1b:bd:ff:fe:94:18:a4; ec:1b:bd:ff:fe:94:18:a4 ; ec:1b:bd:ff:fe:37:72:7e ; ec:1b:bd:ff:fe:37:72:7e ; ec:1b:bd:ff:fe:37:72:7e; ec:1b:bd:ff:fe:37:72:7e; ec:1b:bd:ff:fe:37:72:7e; ec:1b:bd:ff:fe:37:72:7e; ec:1b:bd:ff:fe:37:72:7e; ec:1b:bd:ff:fe:37:72:7e ; ec:1b:bd:ff:fe:33:a0:04 ; ec:1b:bd:ff:fe:33:a0:04 ; ec:1b:bd:ff:fe:33:a0:04; ec:1b:bd:ff:fe:33:a0:04; ec:1b:bd:ff:fe:33:a0:04; ec:1b:bd:ff:fe:33:a0:04; ec:1b:bd:ff:fe:33:a0:04; ec:1b:bd:ff:fe:33:a0:04!;80:4b:50:ff:fe:41:59:63 !;80:4b:50:ff:fe:41:59:63  ;80:4b:50:ff:fe:41:59:63  ;80:4b:50:ff:fe:41:59:63  ;80:4b:50:ff:fe:41:59:63  ;80:4b:50:ff:fe:41:59:63  ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63  !;80:4b:50:ff:fe:41:58:f3 !!;80:4b:50:ff:fe:41:58:f3  ;80:4b:50:ff:fe:41:58:f3  ;80:4b:50:ff:fe:41:58:f3  ;80:4b:50:ff:fe:41:58:f3  ;80:4b:50:ff:fe:41:58:f3  ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 !;80:4b:50:ff:fe:41:67:d4 !;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;68:0a:e2:ff:fe:70:00:69  ;68:0a:e2:ff:fe:70:00:69  ;68:0a:e2:ff:fe:70:00:69  ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 !;60:a4:23:ff:fe:02:54:1e !;60:a4:23:ff:fe:02:54:1e  ;60:a4:23:ff:fe:02:54:1e  ;60:a4:23:ff:fe:02:54:1e  ;60:a4:23:ff:fe:02:54:1e  ;60:a4:23:ff:fe:02:54:1e  ;60:a4:23:ff:fe:02:54:1e ;60:a4:23:ff:fe:02:54:1e !;60:a4:23:ff:fe:02:51:70 !;60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:51:70 ;60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:3b:b4 J ;60:a4:23:ff:fe:02:3b:b4 I;60:a4:23:ff:fe:02:3b:b4 H;60:a4:23:ff:fe:02:3b:b4 G;60:a4:23:ff:fe:02:3b:b4 F;60:a4:23:ff:fe:02:3b:b4 E;60:a4:23:ff:fe:02:3b:b4 D;60:a4:23:ff:fe:02:3b:b4 C!;60:a4:23:ff:fe:02:39:7b !;60:a4:23:ff:fe:02:39:7b  ;60:a4:23:ff:fe:02:39:7b  ;60:a4:23:ff:fe:02:39:7b  ;60:a4:23:ff:fe:02:39:7b  ;60:a4:23:ff:fe:02:39:7b  ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b !;60:a4:23:ff:fe:02:38:af !;60:a4:23:ff:fe:02:38:af  ;60:a4:23:ff:fe:02:38:af  ;60:a4:23:ff:fe:02:38:af  ;60:a4:23:ff:fe:02:38:af  ;60:a4:23:ff:fe:02:38:af  ;60:a4:23:ff:fe:02:38:af ;60:a4:23:ff:fe:02:38:af !;60:a4:23:ff:fe:02:38:60 !;60:a4:23:ff:fe:02:38:60  ;60:a4:23:ff:fe:02:38:60  ;60:a4:23:ff:fe:02:38:60  ;60:a4:23:ff:fe:02:38:60  ;60:a4:23:ff:fe:02:38:60  ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 !;60:a4:23:ff:fe:02:38:59 !;60:a4:23:ff:fe:02:38:59  ;60:a4:23:ff:fe:02:38:59  ;60:a4:23:ff:fe:02:38:59  ;60:a4:23:ff:fe:02:38:59  ;60:a4:23:ff:fe:02:38:59  ;60:a4:23:ff:fe:02:38:59 ;60:a4:23:ff:fe:02:38:59  !;60:a4:23:ff:fe:02:36:a0 ) 3 q Q 1gH(W8 x_>  _ A {[; | | | | | | | | | | | | | | | | | | | | | | | | | | | | { { { { { { { { { { { { { { { {         C; 00:15:8d:00:05:1e:13:46B; 00:15:8d:00:05:4a:73:c3A; 00:15:8d:00:05:4a:73:c3@; 00:15:8d:00:05:4a:73:c3?; 00:15:8d:00:05:4a:73:c3>; 00:15:8d:00:05:4a:73:c3mJ;60:a4:23:ff:fe:02:3b:b4 I;60:a4:23:ff:fe:02:3b:b4 H;60:a4:23:ff:fe:02:3b:b4 G;60:a4:23:ff:fe:02:3b:b4 F;60:a4:23:ff:fe:02:3b:b4 E;60:a4:23:ff:fe:02:3b:b4 D;60:a4:23:ff:fe:02:3b:b4 C;60:a4:23:ff:fe:02:3b:b4 ux;60:a4:23:ff:fe:02:2f:4d w;60:a4:23:ff:fe:02:2f:4d v;60:a4:23:ff:fe:02:2f:4d *8;60:a4:23:ff:fe:02:36:9c 7;60:a4:23:ff:fe:02:36:9c 6;60:a4:23:ff:fe:02:36:9c 5;60:a4:23:ff:fe:02:36:9c 4;60:a4:23:ff:fe:02:36:9c 3;60:a4:23:ff:fe:02:36:9c 2;60:a4:23:ff:fe:02:36:9c 1;60:a4:23:ff:fe:02:36:9c RH; 00:15:8d:00:05:1e:13:46G; 00:15:8d:00:05:1e:13:46F; 00:15:8d:00:05:1e:13:46E; 00:15:8d:00:05:1e:13:46D; 00:15:8d:00:05:1e:13:46 \};60:a4:23:ff:fe:02:2f:4d |;60:a4:23:ff:fe:02:2f:4d {;60:a4:23:ff:fe:02:2f:4d z;60:a4:23:ff:fe:02:2f:4d y;60:a4:23:ff:fe:02:2f:4d  =; 00:15:8d:00:05:4a:73:c3<; 00:15:8d:00:05:4a:73:c3;; 00:15:8d:00:05:1e:0e:32:; 00:15:8d:00:05:1e:0e:329; 00:15:8d:00:05:1e:0e:328; 00:15:8d:00:05:1e:0e:327; 00:15:8d:00:05:1e:0e:326; 00:15:8d:00:05:1e:0e:325;60:a4:23:ff:fe:02:32:30 4;60:a4:23:ff:fe:02:32:30 3;60:a4:23:ff:fe:02:32:30 2;60:a4:23:ff:fe:02:32:30 1;60:a4:23:ff:fe:02:32:30 0;60:a4:23:ff:fe:02:32:30 /;60:a4:23:ff:fe:02:32:30 .;60:a4:23:ff:fe:02:32:30 K u V 6  y Y 8 zZ:`@ _?|\<}]=|\<?~] =``````````````````````````````````````````````v;60:a4:23:ff:fe:02:2f:42 u;60:a4:23:ff:fe:02:2f:42 t;60:a4:23:ff:fe:02:2f:42 s;60:a4:23:ff:fe:02:2f:42 r;60:a4:23:ff:fe:02:2f:42 q;60:a4:23:ff:fe:02:2f:42 p;60:a4:23:ff:fe:02:2f:42 ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:39:7b ;60:a4:23:ff:fe:02:36:24 ~;60:a4:23:ff:fe:02:36:24 };60:a4:23:ff:fe:02:36:24 |;60:a4:23:ff:fe:02:36:24 {;60:a4:23:ff:fe:02:36:24 z;60:a4:23:ff:fe:02:36:24 y;60:a4:23:ff:fe:02:36:24 x;60:a4:23:ff:fe:02:36:24 w;60:a4:23:ff:fe:02:2f:42 _;60:a4:23:ff:fe:02:36:93 ^;60:a4:23:ff:fe:02:36:93 ];60:a4:23:ff:fe:02:36:93 \;60:a4:23:ff:fe:02:36:93 [;60:a4:23:ff:fe:02:36:93 Z;60:a4:23:ff:fe:02:36:93 Y;60:a4:23:ff:fe:02:36:93 X;60:a4:23:ff:fe:02:36:93 /;60:a4:23:ff:fe:02:34:91 .;60:a4:23:ff:fe:02:34:91 -;60:a4:23:ff:fe:02:34:91 ,;60:a4:23:ff:fe:02:34:91 +;60:a4:23:ff:fe:02:34:91 *;60:a4:23:ff:fe:02:34:91 );60:a4:23:ff:fe:02:34:91 (;60:a4:23:ff:fe:02:34:91 ;60:a4:23:ff:fe:02:39:7b ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;58:8e:81:ff:fe:15:e3:ff ;58:8e:81:ff:fe:15:e3:ff~; 58:8e:81:ff:fe:15:e3:ff};58:8e:81:ff:fe:15:e3:ff|;58:8e:81:ff:fe:15:e3:ff {;58:8e:81:ff:fe:15:e3:ffz; 58:8e:81:ff:fe:15:e3:ffy;58:8e:81:ff:fe:15:e3:ffx;58:8e:81:ff:fe:15:e3:ff w;58:8e:81:ff:fe:15:e3:ffv; 58:8e:81:ff:fe:15:e3:ffu;58:8e:81:ff:fe:15:e3:fft; 58:8e:81:ff:fe:15:e3:ff s; 58:8e:81:ff:fe:15:e3:ffr; 58:8e:81:ff:fe:15:e3:ffq; 58:8e:81:ff:fe:15:e3:ff \);60:a4:23:ff:fe:02:51:70  ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 ;60:a4:23:ff:fe:02:38:60 .;60:a4:23:ff:fe:02:51:70 -;60:a4:23:ff:fe:02:51:70 ,;60:a4:23:ff:fe:02:51:70 +;60:a4:23:ff:fe:02:51:70 *;60:a4:23:ff:fe:02:51:70  _ wkF! xT0U <  $e iE! 2 ` :M(G  ^ O  O+l@ os 5 \ 7  zL)r Z* ^: !#; 60:a4:23:ff:fe:02:38:60 l#;60:a4:23:ff:fe:02:38:60 k";60:a4:23:ff:fe:02:38:60 m";60:a4:23:ff:fe:02:38:60 l";60:a4:23:ff:fe:02:38:60  n ;60:a4:23:ff:fe:02:38:60 a ";60:a4:23:ff:fe:02:39:7b ";60:a4:23:ff:fe:02:39:7b  m$;60:a4:23:ff:fe:02:38:60 f";60:a4:23:ff:fe:02:54:1e  n";60:a4:23:ff:fe:02:51:70  n";60:a4:23:ff:fe:02:3b:b4  n#; 60:a4:23:ff:fe:02:54:1e %w#;60:a4:23:ff:fe:02:54:1e %v#; 60:a4:23:ff:fe:02:3b:b4  #;60:a4:23:ff:fe:02:3b:b4  #; 60:a4:23:ff:fe:02:51:70 %5#;60:a4:23:ff:fe:02:51:70 %4$;60:a4:23:ff:fe:02:38:af %g$;60:a4:23:ff:fe:02:38:af V";60:a4:23:ff:fe:02:39:7b $;60:a4:23:ff:fe:02:39:7b $;60:a4:23:ff:fe:02:39:7b $;60:a4:23:ff:fe:02:54:1e ^$;60:a4:23:ff:fe:02:54:1e %z";60:a4:23:ff:fe:02:38:af %h$;60:a4:23:ff:fe:02:38:60 g$;60:a4:23:ff:fe:02:38:af ";60:a4:23:ff:fe:02:54:1e ";60:a4:23:ff:fe:02:54:1e ";60:a4:23:ff:fe:02:51:70 ";60:a4:23:ff:fe:02:51:70 $;60:a4:23:ff:fe:02:51:70 _$;60:a4:23:ff:fe:02:51:70 `$;60:a4:23:ff:fe:02:38:60 %9";60:a4:23:ff:fe:02:38:af %i$;60:a4:23:ff:fe:02:39:7b $;60:a4:23:ff:fe:02:3b:b4 $$;60:a4:23:ff:fe:02:3b:b4 $";60:a4:23:ff:fe:02:3b:b4 $";60:a4:23:ff:fe:02:3b:b4 $$;60:a4:23:ff:fe:02:51:70 a#;60:a4:23:ff:fe:02:38:af @ s#;60:a4:23:ff:fe:02:38:af @ r#;60:a4:23:ff:fe:02:38:af @ t "%;60:a4:23:ff:fe:02:38:af @$$;60:a4:23:ff:fe:02:54:1e %y";60:a4:23:ff:fe:02:38:af  n";60:a4:23:ff:fe:02:39:7b  m$;60:a4:23:ff:fe:02:3b:b4 $#; 60:a4:23:ff:fe:02:38:af #;60:a4:23:ff:fe:02:38:af %;60:a4:23:ff:fe:02:54:1e @%{ ;60:a4:23:ff:fe:02:38:af [#;60:a4:23:ff:fe:02:38:60 @ c#;60:a4:23:ff:fe:02:38:60 @ b#;60:a4:23:ff:fe:02:38:60 @ d#;60:a4:23:ff:fe:02:39:7b %;60:a4:23:ff:fe:02:51:70 @b%;60:a4:23:ff:fe:02:3b:b4 @$";60:a4:23:ff:fe:02:3b:b4  4!";60:a4:23:ff:fe:02:38:60  4 ";60:a4:23:ff:fe:02:39:7b  m#; 60:a4:23:ff:fe:02:39:7b %;60:a4:23:ff:fe:02:39:7b @%;60:a4:23:ff:fe:02:38:60 @$a ";60:a4:23:ff:fe:02:38:af  4&!;60:a4:23:ff:fe:02:54:1e a!;60:a4:23:ff:fe:02:54:1e `$;60:a4:23:ff:fe:02:51:70 @ [$;60:a4:23:ff:fe:02:51:70 @ Z$;60:a4:23:ff:fe:02:51:70 @ \ 2 $;60:a4:23:ff:fe:02:38:60 $_$;60:a4:23:ff:fe:02:3b:b4 $$;60:a4:23:ff:fe:02:38:af $$;60:a4:23:ff:fe:02:39:7b $;60:a4:23:ff:fe:02:51:70 %^$;60:a4:23:ff:fe:02:54:1e %x!;60:a4:23:ff:fe:02:51:70 R!;60:a4:23:ff:fe:02:51:70 Q$;60:a4:23:ff:fe:02:3b:b4 @ $;60:a4:23:ff:fe:02:3b:b4 @ $;60:a4:23:ff:fe:02:3b:b4 @ /%;60:a4:23:ff:fe:02:39:7b @ m/$;60:a4:23:ff:fe:02:38:59 %N$;60:a4:23:ff:fe:02:38:59 %M%;60:a4:23:ff:fe:02:39:7b @ m%;60:a4:23:ff:fe:02:39:7b @ m!;60:a4:23:ff:fe:02:3b:b4 $;60:a4:23:ff:fe:02:38:59 @ 1$;60:a4:23:ff:fe:02:38:59 @ 0$;60:a4:23:ff:fe:02:38:59 @ 2%;60:a4:23:ff:fe:02:38:59 @$$;60:a4:23:ff:fe:02:38:59 $$;60:a4:23:ff:fe:02:38:59  B _ 3nP, ?  ! n ` y \ 4 UBb: J  lHp(b[33333nh(&&& $; 60:a4:23:ff:fe:02:39:7b  #;60:a4:23:ff:fe:02:39:7b &F4h; 900:15:8d:00:05:4a:73:c3lumi.sensor_motion.aq2#;60:a4:23:ff:fe:02:36:93 @ $;60:a4:23:ff:fe:02:36:93 @  ; ec:1b:bd:ff:fe:94:18:a4 ; 58:8e:81:ff:fe:15:e3:ff!W(e;60:a4:23:ff:fe:02:36:a0 GL-B-008P#^;60:a4:23:ff:fe:02:36:24 @ $];60:a4:23:ff:fe:02:36:24 @ $\;60:a4:23:ff:fe:02:36:24 @ H;60:a4:23:ff:fe:02:36:24 'G;60:a4:23:ff:fe:02:36:24 GLEDOPTO(F;60:a4:23:ff:fe:02:36:24 GL-B-008P(n;60:a4:23:ff:fe:02:36:93 GL-B-008P 'f;60:a4:23:ff:fe:02:36:a0 GLEDOPTO ;  cc:cc:cc:ff:fe:29:2d:ab ; cc:cc:cc:ff:fe:29:2d:ab $i;60:a4:23:ff:fe:02:36:a0 @ $h;60:a4:23:ff:fe:02:36:a0 @ g;60:a4:23:ff:fe:02:36:a0  a; 80:4b:50:ff:fe:41:59:63  `;80:4b:50:ff:fe:41:59:63 'o;60:a4:23:ff:fe:02:36:93 GLEDOPTO&#j;60:a4:23:ff:fe:02:36:a0 @ p;60:a4:23:ff:fe:02:36:93 t ;; 60:a4:23:ff:fe:02:36:93  :;60:a4:23:ff:fe:02:36:93  <[ r;80:4b:50:ff:fe:41:67:d4  f; 80:4b:50:ff:fe:41:58:f3  e;80:4b:50:ff:fe:41:58:f3  $$";80:4b:50:ff:fe:41:59:63 @ (!;80:4b:50:ff:fe:41:59:63 GL-B-001P $H$;60:a4:23:ff:fe:02:36:93 @ a; ec:1b:bd:ff:fe:37:72:7e ;'D;80:4b:50:ff:fe:41:58:f3 GLEDOPTO$#;80:4b:50:ff:fe:41:59:63 @ !B;60:a4:23:ff:fe:02:38:59 A;60:a4:23:ff:fe:02:38:59 #?;60:a4:23:ff:fe:02:2f:96  9#H;80:4b:50:ff:fe:41:58:f3 @ $G;80:4b:50:ff:fe:41:58:f3 @ $F;80:4b:50:ff:fe:41:58:f3 @ (E;80:4b:50:ff:fe:41:58:f3 GL-B-001P0; 58:8e:81:ff:fe:15:e3:ff3 /; 58:8e:81:ff:fe:15:e3:ff1 9 [;60:a4:23:ff:fe:02:38:59 A ; N00:15:8d:00:05:4a:73:c3! (!!G$ !=d ! L \; 60:a4:23:ff:fe:02:38:59 ;  cc:cc:cc:ff:fe:29:2d:abi s; 80:4b:50:ff:fe:41:67:d4  y; cc:cc:cc:ff:fe:29:27:01 x; cc:cc:cc:ff:fe:29:4d:f4v;  cc:cc:cc:ff:fe:29:27:01u;  cc:cc:cc:ff:fe:29:4d:f4 4; 60:a4:23:ff:fe:02:2f:4d  3;60:a4:23:ff:fe:02:2f:4d !!;  ec:1b:bd:ff:fe:94:18:a4 ; 60:a4:23:ff:fe:02:34:91  ~;60:a4:23:ff:fe:02:34:91 p ; 60:a4:23:ff:fe:02:36:9c  ;60:a4:23:ff:fe:02:36:9c $ l; 60:a4:23:ff:fe:02:38:60  k;60:a4:23:ff:fe:02:38:60  A; 60:a4:23:ff:fe:02:38:af  @;60:a4:23:ff:fe:02:38:af  x , { " q  e YMG>4*xxxxxxxxxx"ffffK@ U8 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%.U7 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%MU6 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%`U5 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%-T4 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%0T3 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%?U2 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%U1 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%T0 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%CT/ ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%FU. ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%T- ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%,T, ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%CR+ ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$V* ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30%U) ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%`U( ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91%_U' ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%GU& ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%GV% ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%V$ ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%U# ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%PU" ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%XU! ;;;cc:cc:cc:ff:fe:a5:f2:837f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S% E "usN(3 qK' qK L&jY/  h& u S / } [ 9   o M +  [ 7 n#; 80:4b:50:ff:fe:41:58:f3 #;80:4b:50:ff:fe:41:58:f3 #; 80:4b:50:ff:fe:41:67:d4 #;80:4b:50:ff:fe:41:67:d4 ";80:4b:50:ff:fe:41:67:d4 %~%;80:4b:50:ff:fe:41:58:f3 @U$;80:4b:50:ff:fe:41:58:f3 $;80:4b:50:ff:fe:41:58:f3 %L$;80:4b:50:ff:fe:41:58:f3 % ";80:4b:50:ff:fe:41:58:f3 P";80:4b:50:ff:fe:41:58:f3 %#; 80:4b:50:ff:fe:41:59:63 #;80:4b:50:ff:fe:41:59:63 %;80:4b:50:ff:fe:41:59:63 @1$;80:4b:50:ff:fe:41:59:63 $;80:4b:50:ff:fe:41:59:63 %K$;80:4b:50:ff:fe:41:59:63 %J";80:4b:50:ff:fe:41:59:63 2";80:4b:50:ff:fe:41:59:63 %;80:4b:50:ff:fe:41:67:d4 @$;80:4b:50:ff:fe:41:67:d4 %b";80:4b:50:ff:fe:41:59:63  ";80:4b:50:ff:fe:41:67:d4 %$;80:4b:50:ff:fe:41:67:d4 %d$;80:4b:50:ff:fe:41:67:d4 %c$;80:4b:50:ff:fe:41:67:d4 #; cc:cc:cc:ff:fe:29:2d:ab";80:4b:50:ff:fe:41:59:63 !#; ec:1b:bd:ff:fe:94:18:a4%+!; ec:1b:bd:ff:fe:37:72:7e 4A!; ec:1b:bd:ff:fe:94:18:a4 4!; 68:0a:e2:ff:fe:70:00:69 %|";68:0a:e2:ff:fe:70:00:69 %q";68:0a:e2:ff:fe:70:00:69  o";68:0a:e2:ff:fe:70:00:69 ";68:0a:e2:ff:fe:70:00:69  4$; ec:1b:bd:ff:fe:94:18:a4@ u$; ec:1b:bd:ff:fe:94:18:a4@ t $; ec:1b:bd:ff:fe:94:18:a4@ v#; ec:1b:bd:ff:fe:94:18:a4%}#; ec:1b:bd:ff:fe:94:18:a4%]#; ec:1b:bd:ff:fe:94:18:a4%\";  ec:1b:bd:ff:fe:94:18:a4!"; ec:1b:bd:ff:fe:94:18:a4 !; ec:1b:bd:ff:fe:94:18:a4%B!; ec:1b:bd:ff:fe:94:18:a4!; ec:1b:bd:ff:fe:94:18:a4 n!; ec:1b:bd:ff:fe:94:18:a4c!; ec:1b:bd:ff:fe:37:72:7e$!; ec:1b:bd:ff:fe:37:72:7e$!; ec:1b:bd:ff:fe:37:72:7e ^a ; ec:1b:bd:ff:fe:37:72:7e !!; ec:1b:bd:ff:fe:33:a0:04!; ec:1b:bd:ff:fe:33:a0:04!; ec:1b:bd:ff:fe:33:a0:04 ^]!; ec:1b:bd:ff:fe:33:a0:04!; ec:1b:bd:ff:fe:33:a0:04 ki %;80:4b:50:ff:fe:41:59:63 @ #%;80:4b:50:ff:fe:41:59:63 @ "#; cc:cc:cc:ff:fe:29:4d:f4!#; cc:cc:cc:ff:fe:29:4d:f4 !; cc:cc:cc:ff:fe:29:4d:f4!; cc:cc:cc:ff:fe:29:4d:f4 $;80:4b:50:ff:fe:41:59:63 %;80:4b:50:ff:fe:41:58:f3 @ G%;80:4b:50:ff:fe:41:58:f3 @ F";80:4b:50:ff:fe:41:58:f3 E";80:4b:50:ff:fe:41:58:f3 D#; cc:cc:cc:ff:fe:29:2d:ab%#; cc:cc:cc:ff:fe:29:2d:ab$!; cc:cc:cc:ff:fe:29:2d:ab!; cc:cc:cc:ff:fe:29:2d:abQ$;80:4b:50:ff:fe:41:58:f3 %f%;80:4b:50:ff:fe:41:58:f3 @ H%;80:4b:50:ff:fe:41:67:d4 @ ";80:4b:50:ff:fe:41:67:d4 ";80:4b:50:ff:fe:41:67:d4 #; cc:cc:cc:ff:fe:29:27:01##; cc:cc:cc:ff:fe:29:27:01"!; cc:cc:cc:ff:fe:29:27:01!; cc:cc:cc:ff:fe:29:27:01%;80:4b:50:ff:fe:41:67:d4 @ %;80:4b:50:ff:fe:41:67:d4 @ $;60:a4:23:ff:fe:02:54:1e @ p$;60:a4:23:ff:fe:02:54:1e @ o ,MB 6 / ~ % r  f ZRI>2&shQd ;;;60:a4:23:ff:fe:02:38:af7f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$cVc ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30%Ub ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%kUa ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%EU` ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%NV_ ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91U^ ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%BU] ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%9V\ ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%U[ ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%IUZ ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%;VY ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%UX ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%,UW ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%aUV ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%?UU ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%;TT ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%5US ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%TR ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%LTQ ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%5RP ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$VO ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30%UN ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%@UM ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%?VL ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91%UK ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%?VJ ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%UI ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%=VH ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%UG ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%IUF ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%ITE ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%BUD ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%TC ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%PRB ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$UA ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6500:15:8d:00:05:1e:0e:32U@ ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30}V? ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%U> ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%KU= ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91%cU< ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%OU; ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%8U: ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%5V9 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%% bmI> > ^ s`:  [ 5 a j L  `    %uNiB7G$ p`9sLwP+*   ;% D &+# w; 60:a4:23:ff:fe:02:54:1e  v;60:a4:23:ff:fe:02:54:1e  c; 60:a4:23:ff:fe:02:32:30  b;60:a4:23:ff:fe:02:32:30  h;60:a4:23:ff:fe:02:38:af !X; 00:15:8d:00:05:1e:0e:32\!W; 00:15:8d:00:05:1e:0e:32k V; 00:15:8d:00:05:1e:0e:32!U; 00:15:8d:00:05:1e:0e:32 ET; V00:15:8d:00:05:1e:0e:32! !!@$d)ke!\f+= !#o;60:a4:23:ff:fe:02:2f:4d j$n;60:a4:23:ff:fe:02:2f:4d  B; ec:1b:bd:ff:fe:94:18:a4 |; 68:0a:e2:ff:fe:70:00:69 B"}; ec:1b:bd:ff:fe:94:18:a4#d;80:4b:50:ff:fe:41:67:d4 j$c;80:4b:50:ff:fe:41:67:d4 !b; 80:4b:50:ff:fe:41:67:d4 ;60:a4:23:ff:fe:02:36:9c !;80:4b:50:ff:fe:41:67:d4 ~;80:4b:50:ff:fe:41:67:d4 !i;60:a4:23:ff:fe:02:38:af #g;60:a4:23:ff:fe:02:38:af c#f;80:4b:50:ff:fe:41:58:f3 "a;60:a4:23:ff:fe:02:38:60 @#;;60:a4:23:ff:fe:02:36:24 "_;60:a4:23:ff:fe:02:38:60 $z;60:a4:23:ff:fe:02:54:1e #]; ec:1b:bd:ff:fe:94:18:a4#\; ec:1b:bd:ff:fe:94:18:a4!G;60:a4:23:ff:fe:02:34:91 !;60:a4:23:ff:fe:02:36:9c  C;60:a4:23:ff:fe:02:2f:42 Lq;68:0a:e2:ff:fe:70:00:69 #y;60:a4:23:ff:fe:02:54:1e #N;60:a4:23:ff:fe:02:38:59 j$M;60:a4:23:ff:fe:02:38:59 #F;60:a4:23:ff:fe:02:2f:96 ;80:4b:50:ff:fe:41:58:f3 #L;80:4b:50:ff:fe:41:58:f3 j#E;60:a4:23:ff:fe:02:30:39  k;60:a4:23:ff:fe:02:2f:4d L#K;80:4b:50:ff:fe:41:59:63 j$J;80:4b:50:ff:fe:41:59:63  A;60:a4:23:ff:fe:02:2f:96 L#m;60:a4:23:ff:fe:02:2f:4d !+; ec:1b:bd:ff:fe:94:18:a4 E"2;60:a4:23:ff:fe:02:34:91 @#Q;60:a4:23:ff:fe:02:34:91 "0;60:a4:23:ff:fe:02:34:91  {; ec:1b:bd:ff:fe:37:72:7ez;  ec:1b:bd:ff:fe:37:72:7e ""^;60:a4:23:ff:fe:02:51:70 &; 00:15:8d:00:05:4a:73:c3 "Z;60:a4:23:ff:fe:02:3b:b4 @#Y;60:a4:23:ff:fe:02:3b:b4 j$X;60:a4:23:ff:fe:02:3b:b4 #W;60:a4:23:ff:fe:02:3b:b4 !V; 60:a4:23:ff:fe:02:3b:b4 !S;60:a4:23:ff:fe:02:3b:b4 Q;60:a4:23:ff:fe:02:3b:b4 V; 58:8e:81:ff:fe:15:e3:ff!W"s;60:a4:23:ff:fe:02:38:af @"q;60:a4:23:ff:fe:02:38:af  7!$?;60:a4:23:ff:fe:02:2f:96 $ ;80:4b:50:ff:fe:41:58:f3 "{;60:a4:23:ff:fe:02:54:1e @"x;60:a4:23:ff:fe:02:54:1e !v; 60:a4:23:ff:fe:02:38:59 ";60:a4:23:ff:fe:02:32:30 @"p;60:a4:23:ff:fe:02:2f:4d @"l;60:a4:23:ff:fe:02:2f:4d  '; 00:15:8d:00:05:4a:73:c3!#@;60:a4:23:ff:fe:02:2f:96 j"y;60:a4:23:ff:fe:02:38:59 @ "j;60:a4:23:ff:fe:02:2f:96 @"f;60:a4:23:ff:fe:02:2f:96  N; 58:8e:81:ff:fe:15:e3:ff!W'Y; 00:15:8d:00:05:1e:0e:32@##9;60:a4:23:ff:fe:02:38:60  !t; 60:a4:23:ff:fe:02:36:9c A%; N00:15:8d:00:05:4a:73:c3! (!!G$ !=d !W; 58:8e:81:ff:fe:15:e3:ff  5; 60:a4:23:ff:fe:02:51:70  4;60:a4:23:ff:fe:02:51:70 "u;60:a4:23:ff:fe:02:36:9c @"#;60:a4:23:ff:fe:02:30:39 @"!;60:a4:23:ff:fe:02:30:39 b =; 60:a4:23:ff:fe:02:30:39  <;60:a4:23:ff:fe:02:30:39  [; 60:a4:23:ff:fe:02:36:24  Z;60:a4:23:ff:fe:02:36:24  Y; 60:a4:23:ff:fe:02:36:a0  X;60:a4:23:ff:fe:02:36:a0  [; 60:a4:23:ff:fe:02:2f:42  Z;60:a4:23:ff:fe:02:2f:42  P; 60:a4:23:ff:fe:02:2f:96  O;60:a4:23:ff:fe:02:2f:96  -dQF < 2 * w  k _RKC:.z ldTm ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%Tl ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4% Tk ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%dVj ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%Vi ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%Vh ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%Vg ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%Vf ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%Ue ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%(Vd ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%Uc ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%kUb ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%Ta ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%GT` ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%kT_ ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%\T^ ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%8T] ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%@T\ ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%tQ[ ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$VZ ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%UY ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%MVX ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%VW ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%UV ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%UU ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%FVT ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%US ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%TUR ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%FVQ ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%VP ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%UO ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%sTN ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%wTM ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%ATL ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%* tV 4    vXf l N 0   2 2X;60:a4:23:ff:fe:02:51:70T;60:a4:23:ff:fe:02:3b:b41;60:a4:23:ff:fe:02:3b:b40;60:a4:23:ff:fe:02:3b:b4/;60:a4:23:ff:fe:02:3b:b4.;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:51:70W;60:a4:23:ff:fe:02:51:70V;60:a4:23:ff:fe:02:51:70U;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:51:70X;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;60:a4:23:ff:fe:02:51:70[;60:a4:23:ff:fe:02:51:70Z;60:a4:23:ff:fe:02:51:70Y;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:3b:b45;60:a4:23:ff:fe:02:3b:b44;60:a4:23:ff:fe:02:3b:b43;60:a4:23:ff:fe:02:3b:b42;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:51:70\;60:a4:23:ff:fe:02:3b:b46;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:51:70^;60:a4:23:ff:fe:02:51:70];60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:3b:b48;60:a4:23:ff:fe:02:3b:b47;60:a4:23:ff:fe:02:51:70_;60:a4:23:ff:fe:02:3b:b49;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:3b:b4;;60:a4:23:ff:fe:02:3b:b4:;60:a4:23:ff:fe:02:51:70c;60:a4:23:ff:fe:02:51:70b;60:a4:23:ff:fe:02:51:70a;60:a4:23:ff:fe:02:51:70`;60:a4:23:ff:fe:02:3b:b4@;60:a4:23:ff:fe:02:3b:b4?;60:a4:23:ff:fe:02:3b:b4>;60:a4:23:ff:fe:02:3b:b4=;60:a4:23:ff:fe:02:3b:b4<;68:0a:e2:ff:fe:70:00:69;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;60:a4:23:ff:fe:02:39:7b;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69 ;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;68:0a:e2:ff:fe:70:00:69;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:54:1e;60:a4:23:ff:fe:02:51:70h;60:a4:23:ff:fe:02:51:70g;60:a4:23:ff:fe:02:51:70f;60:a4:23:ff:fe:02:51:70e;60:a4:23:ff:fe:02:51:70d;68:0a:e2:ff:fe:70:00:6932%+, c~^> } ] =  ~ _ @ !  e F '  j J * jK, oO/ i I ) nN.mM- lL, _?!;80:4b:50:ff:fe:41:58:f3  ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:58:f3 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:59:63 ;80:4b:50:ff:fe:41:67:d4 ;80:4b:50:ff:fe:41:67:d4 ;80:4b:50:ff:fe:41:67:d4 ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4  ;80:4b:50:ff:fe:41:67:d4 y;60:a4:23:ff:fe:02:30:39 x;60:a4:23:ff:fe:02:30:39 w;60:a4:23:ff:fe:02:30:39 v;60:a4:23:ff:fe:02:30:39 u;60:a4:23:ff:fe:02:30:39 t;60:a4:23:ff:fe:02:30:39 s;60:a4:23:ff:fe:02:30:39 r;60:a4:23:ff:fe:02:30:39 q; ec:1b:bd:ff:fe:33:a0:04p; ec:1b:bd:ff:fe:33:a0:04 o; ec:1b:bd:ff:fe:33:a0:04n; ec:1b:bd:ff:fe:33:a0:04m; ec:1b:bd:ff:fe:33:a0:04l; ec:1b:bd:ff:fe:33:a0:04k; ec:1b:bd:ff:fe:33:a0:04j; ec:1b:bd:ff:fe:33:a0:04;60:a4:23:ff:fe:02:38:59 ;60:a4:23:ff:fe:02:38:59 ;60:a4:23:ff:fe:02:38:59 ~;60:a4:23:ff:fe:02:38:59 };60:a4:23:ff:fe:02:38:59 |;60:a4:23:ff:fe:02:38:59 {;60:a4:23:ff:fe:02:38:59 z;60:a4:23:ff:fe:02:38:59 a;60:a4:23:ff:fe:02:54:1e `;60:a4:23:ff:fe:02:54:1e _;60:a4:23:ff:fe:02:54:1e ^;60:a4:23:ff:fe:02:54:1e ];60:a4:23:ff:fe:02:54:1e \;60:a4:23:ff:fe:02:54:1e [;60:a4:23:ff:fe:02:54:1e Z;60:a4:23:ff:fe:02:54:1e Y; ec:1b:bd:ff:fe:37:72:7eX; ec:1b:bd:ff:fe:37:72:7e W; ec:1b:bd:ff:fe:37:72:7eV; ec:1b:bd:ff:fe:37:72:7eU; ec:1b:bd:ff:fe:37:72:7eT; ec:1b:bd:ff:fe:37:72:7eS; ec:1b:bd:ff:fe:37:72:7eR; ec:1b:bd:ff:fe:37:72:7eQ; ec:1b:bd:ff:fe:94:18:a4P; ec:1b:bd:ff:fe:94:18:a4 O; ec:1b:bd:ff:fe:94:18:a4N; ec:1b:bd:ff:fe:94:18:a4M; ec:1b:bd:ff:fe:94:18:a4L; ec:1b:bd:ff:fe:94:18:a4K; ec:1b:bd:ff:fe:94:18:a4J; ec:1b:bd:ff:fe:94:18:a4I; ec:1b:bd:ff:fe:94:18:a4H;60:a4:23:ff:fe:02:38:af G;60:a4:23:ff:fe:02:38:af F;60:a4:23:ff:fe:02:38:af E;60:a4:23:ff:fe:02:38:af D;60:a4:23:ff:fe:02:38:af C;60:a4:23:ff:fe:02:38:af B;60:a4:23:ff:fe:02:38:af A;60:a4:23:ff:fe:02:38:af @;60:a4:23:ff:fe:02:2f:96 ?;60:a4:23:ff:fe:02:2f:96 >;60:a4:23:ff:fe:02:2f:96 =;60:a4:23:ff:fe:02:2f:96 <;60:a4:23:ff:fe:02:2f:96 ;;60:a4:23:ff:fe:02:2f:96 :;60:a4:23:ff:fe:02:2f:96 9;60:a4:23:ff:fe:02:2f:96 );60:a4:23:ff:fe:02:36:a0 (;60:a4:23:ff:fe:02:36:a0 ';60:a4:23:ff:fe:02:36:a0 &;60:a4:23:ff:fe:02:36:a0 %;60:a4:23:ff:fe:02:36:a0 $;60:a4:23:ff:fe:02:36:a0 #;60:a4:23:ff:fe:02:36:a0 ";60:a4:23:ff:fe:02:36:a0 0;60:a4:23:ff:fe:02:51:70 /;60:a4:23:ff:fe:02:51:70  )MB 7 , | $ s  f [OF=/}#pUC ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%|UB ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%kVA ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%U@ ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%IV? ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63U> ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%MU= ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%dV< ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%V; ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%V: ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%T9 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%vU8 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%T7 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%\T6 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%sU5 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%T4 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%\V3 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%U2 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%jU1 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%pU0 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%HU/ ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%  l N 0 dF( tVD&r6jL.X:8  ~ ` B $  p R 4 bfH* v P 2 n n;80:4b:50:ff:fe:41:58:f3C;80:4b:50:ff:fe:41:58:f3B;cc:cc:cc:ff:fe:a5:f2:83;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:94:18:a4+;ec:1b:bd:ff:fe:94:18:a4*;ec:1b:bd:ff:fe:94:18:a4);ec:1b:bd:ff:fe:94:18:a4(;ec:1b:bd:ff:fe:94:18:a4';ec:1b:bd:ff:fe:94:18:a4&;ec:1b:bd:ff:fe:94:18:a4%;ec:1b:bd:ff:fe:94:18:a4$;ec:1b:bd:ff:fe:94:18:a4#;ec:1b:bd:ff:fe:94:18:a4";ec:1b:bd:ff:fe:94:18:a4!;ec:1b:bd:ff:fe:94:18:a4 ;ec:1b:bd:ff:fe:94:18:a4;ec:1b:bd:ff:fe:94:18:a4;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;80:4b:50:ff:fe:41:59:63";80:4b:50:ff:fe:41:67:d4!;80:4b:50:ff:fe:41:67:d4 ;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;80:4b:50:ff:fe:41:67:d4;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:37:72:7e;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:33:a0:04;ec:1b:bd:ff:fe:94:18:a4-;ec:1b:bd:ff:fe:94:18:a4,;80:4b:50:ff:fe:41:58:f3A;80:4b:50:ff:fe:41:58:f3@;80:4b:50:ff:fe:41:58:f3?;80:4b:50:ff:fe:41:58:f3>;80:4b:50:ff:fe:41:58:f3=;80:4b:50:ff:fe:41:58:f3<;80:4b:50:ff:fe:41:58:f3;;80:4b:50:ff:fe:41:58:f3:;80:4b:50:ff:fe:41:58:f39;80:4b:50:ff:fe:41:58:f38;80:4b:50:ff:fe:41:58:f37;80:4b:50:ff:fe:41:58:f36;80:4b:50:ff:fe:41:58:f35;80:4b:50:ff:fe:41:58:f34;80:4b:50:ff:fe:41:59:633;80:4b:50:ff:fe:41:59:632;80:4b:50:ff:fe:41:59:631;80:4b:50:ff:fe:41:59:630;80:4b:50:ff:fe:41:59:63/;80:4b:50:ff:fe:41:59:63.;80:4b:50:ff:fe:41:59:63-;80:4b:50:ff:fe:41:59:63,;80:4b:50:ff:fe:41:59:63+;80:4b:50:ff:fe:41:59:63*;80:4b:50:ff:fe:41:59:63);80:4b:50:ff:fe:41:59:63(;80:4b:50:ff:fe:41:59:63';80:4b:50:ff:fe:41:59:63&;80:4b:50:ff:fe:41:59:63%;80:4b:50:ff:fe:41:59:63$;80:4b:50:ff:fe:41:59:63#;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83;cc:cc:cc:ff:fe:a5:f2:83)))3 ,PC 7 + w  m  c VI<1&uhZV< ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%V; ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%V: ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%V9 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%V8 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%V7 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%V6 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%U5 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%U4 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%T3 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%OU2 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%U1 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%U0 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%U/ ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%U. ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%U- ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%nV, ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%U+ ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%`V* ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%V) ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%V( ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%U' ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%oV& ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%V% ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%U$ ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%T# ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%aU" ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%U! ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%U ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%T ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%}U ;;;ec:1b:bd:ff:fe:94:18:a47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%V ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%V ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%V ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%U ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91%GU ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%uU ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%jV ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%U ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%`V ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%V ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%U ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%T ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%NT ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%r ,PE 8 - z ! n  f \OB6(z"pe V@ ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30%U? ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%^U> ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%mU= ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%IU< ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%JU; ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%U: ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%T9 ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%8Q8 ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83[U7 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%\V6 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%V5 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%V4 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%V3 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%U2 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%BU1 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%tV0 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%U/ ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%KV. ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%V- ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%U, ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%PV+ ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%U* ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%T) ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%SU( ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%T' ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%XT& ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%tT% ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%vU$ ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%xV# ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%U" ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%hV! ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%7U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%]U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%oU ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%[V ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%V ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%]V ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%T ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%uT ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%3T ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%5 ,OG ; . { " n  b YNE7*vof U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%T ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%~T ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%^U ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%Q ;;;60:a4:23:ff:fe:02:54:1e7f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$BU ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%^U ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%zV ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%V ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%U ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%RV ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%V ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%V ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%V ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%V ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%U ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%T ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%HT ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%DU ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%U ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%U ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%T~ ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%hU} ;;;60:a4:23:ff:fe:02:2f:427f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%T| ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6500:15:8d:00:05:4a:73:c3u6U{ ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%sVz ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%Uy ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69%kVx ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%Vw ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%Uv ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%DUu ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%pVt ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%Us ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%AVr ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%Vq ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%Up ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%kVo ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%Un ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%Tm ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%vTl ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%RTk ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%mTj ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%oUi ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39% ,PG < / { ! n  e ]TG:,ylf U ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%U ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%U ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%T ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%fQ ;;;60:a4:23:ff:fe:02:36:9c7f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83aU ;;;60:a4:23:ff:fe:02:38:607f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30% ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69U= ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91%G -ePG : / | $ s  k c WLA5,|$qeU ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%8U ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%ZV ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%V ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%U ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%jT ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%UT ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%jT ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%zU ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%T ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%FT ;;;80:4b:50:ff:fe:41:67:d47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%~V ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%fU ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%6U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%]U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b%6U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%/U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7eG%1U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93%%-U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4%/U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4|V ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%U ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%KT ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%XT ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%kT ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M%'T ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%8T ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%\T~ ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%ZU} ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%T| ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4%aT{ ;;;68:0a:e2:ff:fe:70:00:697f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%wUz ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af%Vy ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42_%Ux ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%{Uw ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1eq%GUv ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63%Vu ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4%Ut ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c%TVs ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04S%Ur ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m%Tq ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%&Tp ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G%To ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%kTn ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3%"zigpy-0.80.1/tests/databases/simple_v3.sql000066400000000000000000000121501501451476000204570ustar00rootroot00000000000000PRAGMA foreign_keys=OFF; PRAGMA user_version=3; BEGIN TRANSACTION; CREATE TABLE devices (ieee ieee, nwk, status); CREATE TABLE endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE neighbors (device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL,ieee ieee NOT NULL, nwk INTEGER NOT NULL, struct INTEGER NOT NULL, permit_joining INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE node_descriptors (ieee ieee, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE output_clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE attributes (ieee ieee, endpoint_id, cluster, attrid, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE groups (group_id, name); CREATE TABLE group_members (group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE relays (ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE UNIQUE INDEX ieee_idx ON devices(ieee); CREATE UNIQUE INDEX endpoint_idx ON endpoints(ieee, endpoint_id); CREATE UNIQUE INDEX cluster_idx ON clusters(ieee, endpoint_id, cluster); CREATE INDEX neighbors_idx ON neighbors(device_ieee); CREATE UNIQUE INDEX node_descriptors_idx ON node_descriptors(ieee); CREATE UNIQUE INDEX output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX group_idx ON groups(group_id); CREATE UNIQUE INDEX group_members_idx ON group_members(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX relays_idx ON relays(ieee); INSERT INTO attributes VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,4,'IKEA of Sweden'); INSERT INTO attributes VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,5,'TRADFRI control outlet'); INSERT INTO attributes VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,4,'con'); INSERT INTO attributes VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,5,'ZBT-CCTLight-GLS0109'); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,3); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4096); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,5); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,6); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,64636); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,8); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',242,33); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,2821); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,3); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,4); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,4096); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,5); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,6); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,64642); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,768); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,8); INSERT INTO devices VALUES('00:0d:6f:ff:fe:a6:11:7a',48461,2); INSERT INTO devices VALUES('ec:1b:bd:ff:fe:54:4f:40',27932,2); INSERT INTO endpoints VALUES('00:0d:6f:ff:fe:a6:11:7a',1,260,266,1); INSERT INTO endpoints VALUES('00:0d:6f:ff:fe:a6:11:7a',242,41440,97,1); INSERT INTO endpoints VALUES('ec:1b:bd:ff:fe:54:4f:40',1,260,268,1); INSERT INTO endpoints VALUES('ec:1b:bd:ff:fe:54:4f:40',242,41440,97,1); INSERT INTO neighbors VALUES('00:0d:6f:ff:fe:a6:11:7a','81:b1:12:dc:9f:bd:f4:b6','ec:1b:bd:ff:fe:54:4f:40',27932,37,2,15,130); INSERT INTO neighbors VALUES('ec:1b:bd:ff:fe:54:4f:40','81:b1:12:dc:9f:bd:f4:b6','00:0d:6f:ff:fe:a6:11:7a',48461,37,2,15,132); INSERT INTO node_descriptors VALUES('00:0d:6f:ff:fe:a6:11:7a',X'01408e7c11525200002c520000'); INSERT INTO node_descriptors VALUES('ec:1b:bd:ff:fe:54:4f:40',X'01408e6811525200002c520000'); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,25); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,32); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4096); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,5); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',242,33); INSERT INTO output_clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,10); INSERT INTO output_clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,25); INSERT INTO output_clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',242,33); INSERT INTO relays VALUES('00:0d:6f:ff:fe:a6:11:7a',X'00'); INSERT INTO relays VALUES('ec:1b:bd:ff:fe:54:4f:40',X'00'); COMMIT;zigpy-0.80.1/tests/databases/simple_v3_to_v4.sql000066400000000000000000000161421501451476000215770ustar00rootroot00000000000000PRAGMA foreign_keys=OFF; PRAGMA user_version=4; BEGIN TRANSACTION; CREATE TABLE devices (ieee ieee, nwk, status); INSERT INTO devices VALUES('00:0d:6f:ff:fe:a6:11:7a',48461,2); INSERT INTO devices VALUES('ec:1b:bd:ff:fe:54:4f:40',27932,2); CREATE TABLE endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); INSERT INTO endpoints VALUES('00:0d:6f:ff:fe:a6:11:7a',1,260,266,1); INSERT INTO endpoints VALUES('00:0d:6f:ff:fe:a6:11:7a',242,41440,97,1); INSERT INTO endpoints VALUES('ec:1b:bd:ff:fe:54:4f:40',1,260,268,1); INSERT INTO endpoints VALUES('ec:1b:bd:ff:fe:54:4f:40',242,41440,97,1); CREATE TABLE clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,3); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4096); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,5); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,6); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,64636); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,8); INSERT INTO clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',242,33); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,2821); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,3); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,4); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,4096); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,5); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,6); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,64642); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,768); INSERT INTO clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,8); CREATE TABLE neighbors (device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL,ieee ieee NOT NULL, nwk INTEGER NOT NULL, struct INTEGER NOT NULL, permit_joining INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices(ieee) ON DELETE CASCADE); INSERT INTO neighbors VALUES('00:0d:6f:ff:fe:a6:11:7a','81:b1:12:dc:9f:bd:f4:b6','ec:1b:bd:ff:fe:54:4f:40',27932,37,2,15,130); INSERT INTO neighbors VALUES('ec:1b:bd:ff:fe:54:4f:40','81:b1:12:dc:9f:bd:f4:b6','00:0d:6f:ff:fe:a6:11:7a',48461,37,2,15,132); CREATE TABLE node_descriptors (ieee ieee, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); INSERT INTO node_descriptors VALUES('00:0d:6f:ff:fe:a6:11:7a',X'01408e7c11525200002c520000'); INSERT INTO node_descriptors VALUES('ec:1b:bd:ff:fe:54:4f:40',X'01408e6811525200002c520000'); CREATE TABLE output_clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,25); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,32); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4096); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',1,5); INSERT INTO output_clusters VALUES('00:0d:6f:ff:fe:a6:11:7a',242,33); INSERT INTO output_clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,10); INSERT INTO output_clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',1,25); INSERT INTO output_clusters VALUES('ec:1b:bd:ff:fe:54:4f:40',242,33); CREATE TABLE attributes (ieee ieee, endpoint_id, cluster, attrid, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); INSERT INTO attributes VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,4,'IKEA of Sweden'); INSERT INTO attributes VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,5,'TRADFRI control outlet'); INSERT INTO attributes VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,4,'con'); INSERT INTO attributes VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,5,'ZBT-CCTLight-GLS0109'); CREATE TABLE groups (group_id, name); CREATE TABLE group_members (group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE relays (ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); INSERT INTO relays VALUES('00:0d:6f:ff:fe:a6:11:7a',X'00'); INSERT INTO relays VALUES('ec:1b:bd:ff:fe:54:4f:40',X'00'); CREATE TABLE node_descriptors_v4 ( ieee ieee, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE ); INSERT INTO node_descriptors_v4 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,0,0,0,8,142,4476,82,82,11264,82,0); INSERT INTO node_descriptors_v4 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,0,0,0,8,142,4456,82,82,11264,82,0); CREATE TABLE neighbors_v4 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL ); INSERT INTO neighbors_v4 VALUES('00:0d:6f:ff:fe:a6:11:7a','81:b1:12:dc:9f:bd:f4:b6','ec:1b:bd:ff:fe:54:4f:40',27932,1,1,2,0,2,0,15,130); INSERT INTO neighbors_v4 VALUES('ec:1b:bd:ff:fe:54:4f:40','81:b1:12:dc:9f:bd:f4:b6','00:0d:6f:ff:fe:a6:11:7a',48461,1,1,2,0,2,0,15,132); CREATE UNIQUE INDEX ieee_idx ON devices(ieee); CREATE UNIQUE INDEX endpoint_idx ON endpoints(ieee, endpoint_id); CREATE UNIQUE INDEX cluster_idx ON clusters(ieee, endpoint_id, cluster); CREATE INDEX neighbors_idx ON neighbors(device_ieee); CREATE UNIQUE INDEX node_descriptors_idx ON node_descriptors(ieee); CREATE UNIQUE INDEX output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX group_idx ON groups(group_id); CREATE UNIQUE INDEX group_members_idx ON group_members(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX relays_idx ON relays(ieee); CREATE UNIQUE INDEX node_descriptors_idx_v4 ON node_descriptors_v4(ieee); CREATE INDEX neighbors_idx_v4 ON neighbors_v4(device_ieee); COMMIT;zigpy-0.80.1/tests/databases/simple_v5.sql000066400000000000000000000157101501451476000204660ustar00rootroot00000000000000PRAGMA foreign_keys=OFF; PRAGMA user_version=5; BEGIN TRANSACTION; CREATE TABLE devices_v5 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL ); INSERT INTO devices_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',48461,2); INSERT INTO devices_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',27932,2); CREATE TABLE endpoints_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); INSERT INTO endpoints_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,260,266,1); INSERT INTO endpoints_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',242,41440,97,1); INSERT INTO endpoints_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,260,268,1); INSERT INTO endpoints_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',242,41440,97,1); CREATE TABLE in_clusters_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,3); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4096); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,5); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,6); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,64636); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,8); INSERT INTO in_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',242,33); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,2821); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,3); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,4); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,4096); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,5); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,6); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,64642); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,768); INSERT INTO in_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,8); CREATE TABLE neighbors_v5 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); INSERT INTO neighbors_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a','81:b1:12:dc:9f:bd:f4:b6','ec:1b:bd:ff:fe:54:4f:40',27932,1,1,2,0,2,0,15,130); INSERT INTO neighbors_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40','81:b1:12:dc:9f:bd:f4:b6','00:0d:6f:ff:fe:a6:11:7a',48461,1,1,2,0,2,0,15,132); CREATE TABLE node_descriptors_v5 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); INSERT INTO node_descriptors_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,0,0,0,8,142,4476,82,82,11264,82,0); INSERT INTO node_descriptors_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,0,0,0,8,142,4456,82,82,11264,82,0); CREATE TABLE out_clusters_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); INSERT INTO out_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,25); INSERT INTO out_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,32); INSERT INTO out_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,4096); INSERT INTO out_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,5); INSERT INTO out_clusters_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',242,33); INSERT INTO out_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,10); INSERT INTO out_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,25); INSERT INTO out_clusters_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',242,33); CREATE TABLE attributes_cache_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters that won't be present in the DB but whose -- values still need to be cached FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); INSERT INTO attributes_cache_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,4,'IKEA of Sweden'); INSERT INTO attributes_cache_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',1,0,5,'TRADFRI control outlet'); INSERT INTO attributes_cache_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,4,'con'); INSERT INTO attributes_cache_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',1,0,5,'ZBT-CCTLight-GLS0109'); CREATE TABLE groups_v5 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE TABLE group_members_v5 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v5(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); CREATE TABLE relays_v5 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); INSERT INTO relays_v5 VALUES('00:0d:6f:ff:fe:a6:11:7a',X'00'); INSERT INTO relays_v5 VALUES('ec:1b:bd:ff:fe:54:4f:40',X'00'); CREATE UNIQUE INDEX devices_idx_v5 ON devices_v5(ieee); CREATE UNIQUE INDEX endpoint_idx_v5 ON endpoints_v5(ieee, endpoint_id); CREATE UNIQUE INDEX in_clusters_idx_v5 ON in_clusters_v5(ieee, endpoint_id, cluster); CREATE INDEX neighbors_idx_v5 ON neighbors_v5(device_ieee); CREATE UNIQUE INDEX node_descriptors_idx_v5 ON node_descriptors_v5(ieee); CREATE UNIQUE INDEX out_clusters_idx_v5 ON out_clusters_v5(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX attributes_idx_v5 ON attributes_cache_v5(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX groups_idx_v5 ON groups_v5(group_id); CREATE UNIQUE INDEX group_members_idx_v5 ON group_members_v5(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX relays_idx_v5 ON relays_v5(ieee); COMMIT; zigpy-0.80.1/tests/databases/simple_v8.sql000066400000000000000000000266541501451476000205020ustar00rootroot00000000000000PRAGMA user_version=8; PRAGMA foreign_keys=OFF; BEGIN TRANSACTION; CREATE TABLE devices_v8 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen unix_timestamp NOT NULL ); INSERT INTO devices_v8 VALUES('00:12:4b:00:1c:a1:b8:46',0,2,1651119833288); INSERT INTO devices_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',44170,2,1651119836445); INSERT INTO devices_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',50064,2,1651119839551); INSERT INTO devices_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',57374,2,1651119830048); CREATE TABLE endpoints_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); INSERT INTO endpoints_v8 VALUES('00:12:4b:00:1c:a1:b8:46',1,260,48879,1); INSERT INTO endpoints_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,260,268,1); INSERT INTO endpoints_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',242,41440,97,1); INSERT INTO endpoints_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,260,268,1); INSERT INTO endpoints_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',242,41440,97,1); INSERT INTO endpoints_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,260,2080,1); CREATE TABLE in_clusters_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v8(ieee, endpoint_id) ON DELETE CASCADE ); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,0); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,3); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,4); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,5); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,6); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,8); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,2821); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,4096); INSERT INTO in_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,64642); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,0); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,3); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,4); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,5); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,6); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,8); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,2821); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,4096); INSERT INTO in_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,64642); INSERT INTO in_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,0); INSERT INTO in_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,1); INSERT INTO in_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,3); INSERT INTO in_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,32); INSERT INTO in_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,4096); CREATE TABLE neighbors_v8 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); INSERT INTO neighbors_v8 VALUES('00:12:4b:00:1c:a1:b8:46','bd:27:0b:38:37:95:dc:87','ec:1b:bd:ff:fe:2f:41:a4',44170,1,1,2,0,2,0,15,255); INSERT INTO neighbors_v8 VALUES('00:12:4b:00:1c:a1:b8:46','bd:27:0b:38:37:95:dc:87','cc:cc:cc:ff:fe:e6:8e:ca',50064,1,1,2,0,2,0,15,255); INSERT INTO neighbors_v8 VALUES('00:12:4b:00:1c:a1:b8:46','bd:27:0b:38:37:95:dc:87','00:0b:57:ff:fe:2b:d4:57',57374,2,0,1,0,0,0,1,255); INSERT INTO neighbors_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4','bd:27:0b:38:37:95:dc:87','00:12:4b:00:1c:a1:b8:46',0,0,1,2,0,2,0,0,253); INSERT INTO neighbors_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4','bd:27:0b:38:37:95:dc:87','cc:cc:cc:ff:fe:e6:8e:ca',50064,1,1,0,0,2,0,15,255); INSERT INTO neighbors_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca','bd:27:0b:38:37:95:dc:87','00:12:4b:00:1c:a1:b8:46',0,0,1,0,0,2,0,0,255); INSERT INTO neighbors_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca','bd:27:0b:38:37:95:dc:87','ec:1b:bd:ff:fe:2f:41:a4',44170,1,1,2,0,2,0,15,255); CREATE TABLE node_descriptors_v8 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); INSERT INTO node_descriptors_v8 VALUES('00:12:4b:00:1c:a1:b8:46',0,0,0,0,0,8,143,43981,82,128,11329,128,0); INSERT INTO node_descriptors_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,0,0,0,0,8,142,4688,82,82,11264,82,0); INSERT INTO node_descriptors_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,0,0,0,0,8,142,4688,82,82,11264,82,0); INSERT INTO node_descriptors_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',2,0,0,0,0,8,128,4476,82,82,11264,82,0); CREATE TABLE out_clusters_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v8(ieee, endpoint_id) ON DELETE CASCADE ); INSERT INTO out_clusters_v8 VALUES('00:12:4b:00:1c:a1:b8:46',1,1280); INSERT INTO out_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,10); INSERT INTO out_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,25); INSERT INTO out_clusters_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',242,33); INSERT INTO out_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,10); INSERT INTO out_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,25); INSERT INTO out_clusters_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',242,33); INSERT INTO out_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,3); INSERT INTO out_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,4); INSERT INTO out_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,6); INSERT INTO out_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,8); INSERT INTO out_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,25); INSERT INTO out_clusters_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,4096); CREATE TABLE attributes_cache_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,0,4,'The Home Depot'); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,0,5,'Ecosmart-ZBT-A19-CCT-Bulb'); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,6,0,1); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,6,16387,1); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,8,0,254); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,16395,153); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,16396,370); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,16394,16); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,0,4,'The Home Depot'); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,0,5,'Ecosmart-ZBT-A19-CCT-Bulb'); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,3,30002); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,4,26876); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,7,370); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,6,0,1); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,8,0,254); INSERT INTO attributes_cache_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,8,2); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,8,2); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,7,370); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,3,30002); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,4,26876); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,6,16387,1); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,16395,153); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,16396,370); INSERT INTO attributes_cache_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,16394,16); INSERT INTO attributes_cache_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,0,4,'IKEA of Sweden'); INSERT INTO attributes_cache_v8 VALUES('00:0b:57:ff:fe:2b:d4:57',1,0,5,'TRADFRI wireless dimmer'); CREATE TABLE groups_v8 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); INSERT INTO groups_v8 VALUES(0,'Default Lightlink Group'); CREATE TABLE group_members_v8 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v8(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v8(ieee, endpoint_id) ON DELETE CASCADE ); INSERT INTO group_members_v8 VALUES(0,'00:12:4b:00:1c:a1:b8:46',1); CREATE TABLE relays_v8 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); INSERT INTO relays_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',X'00'); INSERT INTO relays_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',X'00'); CREATE TABLE unsupported_attributes_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v8(ieee, endpoint_id, cluster) ON DELETE CASCADE ); INSERT INTO unsupported_attributes_v8 VALUES('ec:1b:bd:ff:fe:2f:41:a4',1,768,16386); INSERT INTO unsupported_attributes_v8 VALUES('cc:cc:cc:ff:fe:e6:8e:ca',1,768,16386); CREATE UNIQUE INDEX devices_idx_v8 ON devices_v8(ieee); CREATE UNIQUE INDEX endpoint_idx_v8 ON endpoints_v8(ieee, endpoint_id); CREATE UNIQUE INDEX in_clusters_idx_v8 ON in_clusters_v8(ieee, endpoint_id, cluster); CREATE INDEX neighbors_idx_v8 ON neighbors_v8(device_ieee); CREATE UNIQUE INDEX node_descriptors_idx_v8 ON node_descriptors_v8(ieee); CREATE UNIQUE INDEX out_clusters_idx_v8 ON out_clusters_v8(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX attributes_idx_v8 ON attributes_cache_v8(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX groups_idx_v8 ON groups_v8(group_id); CREATE UNIQUE INDEX group_members_idx_v8 ON group_members_v8(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX relays_idx_v8 ON relays_v8(ieee); CREATE UNIQUE INDEX unsupported_attributes_idx_v8 ON unsupported_attributes_v8(ieee, endpoint_id, cluster, attrid); COMMIT;zigpy-0.80.1/tests/databases/zigbee_20190417_v0.db000066400000000000000000002600001501451476000212040ustar00rootroot00000000000000SQLite format 3@ Vl Vl.,P m7 c r w '!3indexattribute_idxattributes CREATE UNIQUE INDEX attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid)i !!tableattributesattributes CREATE TABLE attributes (ieee ieee, endpoint_id, cluster, attrid, value)1+7indexoutput_cluster_idxoutput_clusters CREATE UNIQUE INDEX output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster)i++ tableoutput_clustersoutput_clustersCREATE TABLE output_clusters (ieee ieee, endpoint_id, cluster)g#indexcluster_idxclustersCREATE UNIQUE INDEX cluster_idx ON clusters(ieee, endpoint_id, cluster)S{tableclustersclustersCREATE TABLE clusters (ieee ieee, endpoint_id, cluster)b% indexendpoint_idxendpointsCREATE UNIQUE INDEX endpoint_idx ON endpoints(ieee, endpoint_id){EtableendpointsendpointsCREATE TABLE endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status)Hgindexieee_idxdevicesCREATE UNIQUE INDEX ieee_idx ON devices(ieee)GgtabledevicesdevicesCREATE TABLE devices (ieee ieee, nwk, status)  m]<zY9  s R~ 1  m-;00:15:8d:00:01:eb:71:ecO,;84:18:26:00:00:d9:86:e7A+;84:18:26:00:00:01:30:50);00:15:8d:00:02:05:a6:41ԧ&;84:18:26:00:00:00:d1:dfz%;84:18:26:00:00:00:d0:fa $;84:18:26:00:00:02:44:33!;84:18:26:00:00:04:a7:c9 ;00:15:8d:00:02:b8:bb:71.;00:15:8d:00:02:c3:af:b1;00:15:8d:00:02:c3:95:8a;00:0d:6f:00:0d:2e:8d:72;00:15:8d:00:02:04:a0:62|;94:10:3e:f6:bf:42:8a:ady;00:15:8d:00:02:36:84:85}5;00:15:8d:00:02:36:91:2f?;00:15:8d:00:02:04:54:405 ;00:0d:6f:00:0c:a7:42:a6YF ;00:15:8d:00:02:b5:f7:cdY&;00:15:8d:00:02:b5:2d:b5k;00:12:4b:00:19:36:95:c1TJ;00:0d:6f:00:0b:12:4b:62;00:0d:6f:00:0b:1c:f1:4a ";00:0d:6f:00:0d:2e:8d:e91;00:0d:6f:ff:fe:7a:d3:7aˣ;00:0d:6f:00:05:76:14:80#;84:18:26:00:00:02:b7:13/  YuA = ) y!% a E } ];84:18:26:00:00:d9:86:e7,;84:18:26:00:00:01:30:50+;00:15:8d:00:01:eb:71:ec-;84:18:26:00:00:00:d1:df&;84:18:26:00:00:00:d0:fa%;84:18:26:00:00:02:44:33$;84:18:26:00:00:02:b7:13#;00:0d:6f:00:0d:2e:8d:e9";84:18:26:00:00:04:a7:c9!;00:15:8d:00:02:05:a6:41);00:15:8d:00:02:c3:af:b1;00:15:8d:00:02:c3:95:8a;00:0d:6f:00:0d:2e:8d:72;94:10:3e:f6:bf:42:8a:ad;00:15:8d:00:02:36:84:85;00:15:8d:00:02:36:91:2f;00:15:8d:00:02:04:54:40;00:0d:6f:00:0c:a7:42:a6 ;00:15:8d:00:02:04:a0:62;00:15:8d:00:02:b5:f7:cd;00:15:8d:00:02:b5:2d:b5;00:12:4b:00:19:36:95:c1;00:0d:6f:00:0b:12:4b:62;00:0d:6f:00:0b:1c:f1:4a;00:15:8d:00:02:b8:bb:71 ;00:0d:6f:ff:fe:7a:d3:7a;00:0d:6f:00:05:76:14:80 u! -qO*zW5 c  @ p L (  Q - - -"0; 84:18:26:00:00:d9:86:e7"/; 84:18:26:00:00:01:30:50H!1;  00:15:8d:00:01:eb:71:ec_"(; 84:18:26:00:00:00:d1:df"'; 84:18:26:00:00:00:d0:fa"&; 84:18:26:00:00:02:44:33"%; 84:18:26:00:00:02:b7:13"$; 00:0d:6f:00:0d:2e:8d:e9 +; 00:15:8d:00:02:05:a6:41!;  00:15:8d:00:02:c3:af:b1_!;  00:15:8d:00:02:c3:95:8a_"; 00:0d:6f:00:0d:2e:8d:72 !;  00:0d:6f:00:0d:2e:8d:72!;  94:10:3e:f6:bf:42:8a:ad!;  00:15:8d:00:02:36:84:85!;  00:15:8d:00:02:36:91:2f!;  00:15:8d:00:02:04:54:40_ ;  00:0d:6f:00:0c:a7:42:a6Q!;  00:15:8d:00:02:04:a0:62_"; 00:15:8d:00:02:b5:f7:cd_ ;  00:15:8d:00:02:b5:f7:cd ! ;  00:15:8d:00:02:b5:2d:b5_" ; 00:12:4b:00:19:36:95:c1! ;  00:0d:6f:00:0b:12:4b:62! ;  00:0d:6f:00:0b:1c:f1:4a"!; 00:15:8d:00:02:b8:bb:71_ ; 00:15:8d:00:02:b8:bb:71 #; 00:0d:6f:ff:fe:7a:d3:7aa ;  00:0d:6f:ff:fe:7a:d3:7a"; 00:0d:6f:00:05:76:14:80 !;  00:0d:6f:00:05:76:14:80!#;  00:0d:6f:00:0d:2e:8d:e9""; 84:18:26:00:00:04:a7:c9 2! ' x kL n D ! a/ [ >   ;84:18:26:00:00:d9:86:e70;84:18:26:00:00:01:30:50/<; 00:15:8d:00:01:eb:71:ec1;84:18:26:00:00:00:d1:df(;84:18:26:00:00:00:d0:fa';84:18:26:00:00:02:44:33&;84:18:26:00:00:02:b7:13%;00:0d:6f:00:0d:2e:8d:e9$; 00:15:8d:00:02:05:a6:41+; 00:15:8d:00:02:c3:af:b1; 00:15:8d:00:02:c3:95:8a;00:0d:6f:00:0d:2e:8d:72; 00:0d:6f:00:0d:2e:8d:72; 94:10:3e:f6:bf:42:8a:ad; 00:15:8d:00:02:36:84:85; 00:15:8d:00:02:36:91:2f; 00:15:8d:00:02:04:54:40; 00:0d:6f:00:0c:a7:42:a6; 00:15:8d:00:02:04:a0:62;00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:2d:b5 ;00:12:4b:00:19:36:95:c1 ; 00:0d:6f:00:0b:12:4b:62 ; 00:0d:6f:00:0b:1c:f1:4a ;00:15:8d:00:02:b8:bb:71!; 00:15:8d:00:02:b8:bb:71 ;00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a;00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:0d:2e:8d:e9#;84:18:26:00:00:04:a7:c9"; 00:15:8d:00:02:b5:2d:b52 P=hH d D ' q R 3  ~ ` @ "  n N 1  + uV7 x Y : 1X;84:18:26:00:00:d9:86:e7W;84:18:26:00:00:01:30:50Y; 00:15:8d:00:01:eb:71:ecG;84:18:26:00:00:00:d1:dfF;84:18:26:00:00:00:d0:faE;84:18:26:00:00:02:44:33D;84:18:26:00:00:02:b7:13C;00:0d:6f:00:0d:2e:8d:e9B; 00:0d:6f:00:0d:2e:8d:e9A;84:18:26:00:00:04:a7:c94; 00:15:8d:00:02:c3:af:b13; 00:15:8d:00:02:c3:af:b12; 00:15:8d:00:02:c3:af:b11; 00:15:8d:00:02:c3:95:8a0; 00:15:8d:00:02:c3:95:8a/; 00:15:8d:00:02:c3:95:8a.;00:0d:6f:00:0d:2e:8d:72-; 00:0d:6f:00:0d:2e:8d:726; 94:10:3e:f6:bf:42:8a:ad+; 00:15:8d:00:02:36:84:85*; 00:15:8d:00:02:36:91:2f); 00:15:8d:00:02:04:54:40(; 00:15:8d:00:02:04:54:40'; 00:15:8d:00:02:04:54:40&; 00:0d:6f:00:0c:a7:42:a6%; 00:15:8d:00:02:04:a0:62$; 00:15:8d:00:02:04:a0:62#; 00:15:8d:00:02:04:a0:62";00:15:8d:00:02:b5:f7:cd!;00:15:8d:00:02:b5:f7:cd ;00:15:8d:00:02:b5:f7:cd;00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:2d:b5; 00:15:8d:00:02:b5:2d:b5; 00:15:8d:00:02:b5:2d:b5;00:12:4b:00:19:36:95:c1;00:12:4b:00:19:36:95:c1; 00:0d:6f:00:0b:12:4b:62; 00:0d:6f:00:0b:1c:f1:4a@;00:15:8d:00:02:b8:bb:71?;00:15:8d:00:02:b8:bb:71>;00:15:8d:00:02:b8:bb:71=;00:15:8d:00:02:b8:bb:71<; 00:15:8d:00:02:b8:bb:71;; 00:15:8d:00:02:b8:bb:71:; 00:15:8d:00:02:b8:bb:719; 00:15:8d:00:02:b8:bb:718; 00:15:8d:00:02:b8:bb:717; 00:15:8d:00:02:b8:bb:71;00:0d:6f:ff:fe:7a:d3:7a!; 00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a ; 00:0d:6f:ff:fe:7a:d3:7a;00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80 = w } bdC  Y :  n O .  t 4 T  %Ii) _ @    " B  ;84:18:26:00:00:d9:86:e7X;84:18:26:00:00:01:30:50W; 00:15:8d:00:01:eb:71:ecY;84:18:26:00:00:00:d1:dfG;84:18:26:00:00:00:d0:faF;84:18:26:00:00:02:44:33E;84:18:26:00:00:02:b7:13D;00:0d:6f:00:0d:2e:8d:e9C; 00:0d:6f:00:0d:2e:8d:e9B;84:18:26:00:00:04:a7:c9A ; 00:15:8d:00:02:c3:af:b14; 00:15:8d:00:02:c3:af:b13; 00:15:8d:00:02:c3:af:b12 ; 00:15:8d:00:02:c3:95:8a1; 00:15:8d:00:02:c3:95:8a0; 00:15:8d:00:02:c3:95:8a/;00:0d:6f:00:0d:2e:8d:72.; 00:0d:6f:00:0d:2e:8d:72-; 94:10:3e:f6:bf:42:8a:ad6; 00:15:8d:00:02:36:84:85+; 00:15:8d:00:02:36:91:2f* ; 00:15:8d:00:02:04:54:40); 00:15:8d:00:02:04:54:40(; 00:15:8d:00:02:04:54:40'; 00:0d:6f:00:0c:a7:42:a6& ; 00:15:8d:00:02:04:a0:62%; 00:15:8d:00:02:04:a0:62$; 00:15:8d:00:02:04:a0:62#;00:15:8d:00:02:b5:f7:cd";00:15:8d:00:02:b5:f7:cd!;00:15:8d:00:02:b5:f7:cd ;00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd; 00:15:8d:00:02:b5:f7:cd ; 00:15:8d:00:02:b5:2d:b5; 00:15:8d:00:02:b5:2d:b5; 00:15:8d:00:02:b5:2d:b5;00:12:4b:00:19:36:95:c1;00:12:4b:00:19:36:95:c1; 00:0d:6f:00:0b:12:4b:62; 00:0d:6f:00:0b:1c:f1:4a;00:15:8d:00:02:b8:bb:71@;00:15:8d:00:02:b8:bb:71?;00:15:8d:00:02:b8:bb:71>;00:15:8d:00:02:b8:bb:71=; 00:15:8d:00:02:b8:bb:71<; 00:15:8d:00:02:b8:bb:71;; 00:15:8d:00:02:b8:bb:71:; 00:15:8d:00:02:b8:bb:719; 00:15:8d:00:02:b8:bb:718; 00:15:8d:00:02:b8:bb:717 ;00:0d:6f:ff:fe:7a:d3:7a!; 00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a ; 00:0d:6f:ff:fe:7a:d3:7a;00:0d:6f:00:05:76:14:80;  00:0d:6f:00:05:76:14:80 a ";84:18:26:00:00:00:d0:fa!9 ; 00:15:8d:00:02:04:a0:62 H^0`7 { V +  b A  m G %  w V  O oM*{H$}[$\1[8pL)f>pgg"-;84:18:26:00:00:04:a7:c9Nv; h00:15:8d:00:02:b8:bb:71! (!C!$! !FY!(!%M $|; 00:15:8d:00:02:b8:bb:71; 00:15:8d:00:02:b8:bb:71; 00:15:8d:00:02:b5:f7:cd0'; 000:15:8d:00:02:b5:f7:cdlumi.vibration.aq10(; 000:15:8d:00:02:b8:bb:71lumi.vibration.aq1'$; 00:15:8d:00:02:b8:bb:71 O""; 00:15:8d:00:02:b8:bb:71;  00:15:8d:00:02:b8:bb:71 ;  00:15:8d:00:02:b5:f7:cd !j; 00:15:8d:00:02:b5:f7:cd3{; 600:15:8d:00:02:36:84:85lumi.sensor_wleak.aq13n; 600:15:8d:00:02:36:91:2flumi.sensor_wleak.aq1+;  00:15:8d:00:02:b5:f7:cd45; 800:15:8d:00:02:c3:95:8alumi.sensor_magnet.aq2"U; 00:15:8d:00:02:c3:af:b1LUMI@; 00:15:8d:00:02:c3:95:8a"<; 00:15:8d:00:02:c3:95:8aLUMI ^;  00:0d:6f:00:0d:2e:8d:72/\;,00:0d:6f:00:0d:2e:8d:72Contact Sensor-A)[; 00:0d:6f:00:0d:2e:8d:72CentraLite.Z; ,00:0d:6f:00:0d:2e:8d:72Contact Sensor-A(Y;  00:0d:6f:00:0d:2e:8d:72CentraLite!; 00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80!; 00:0d:6f:00:05:76:14:80 j; 00:0d:6f:00:05:76:14:80>M;  00:0d:6f:00:05:76:14:80!J; 00:0d:6f:00:05:76:14:80$I; 00:0d:6f:00:05:76:14:801 G;00:0d:6f:00:05:76:14:801c; 100:15:8d:00:02:b5:f7:cdbattery_voltage_mV 1 ; 100:15:8d:00:02:36:84:85battery_voltage_mV 1s; 100:15:8d:00:02:36:91:2fbattery_voltage_mV 1H; 100:15:8d:00:02:04:54:40battery_voltage_mV 1 ; 100:15:8d:00:02:04:a0:62battery_voltage_mV 4/; 800:15:8d:00:02:04:a0:62lumi.sensor_magnet.aq2';  00:15:8d:00:02:04:a0:621$; 100:15:8d:00:02:b5:2d:b5battery_voltage_mV ;  00:15:8d:00:02:b5:2d:b54; 800:15:8d:00:02:b5:2d:b5lumi.sensor_magnet.aq2h; 00:15:8d:00:02:04:54:40T; 00:0d:6f:00:05:76:14:80#3; 94:10:3e:f6:bf:42:8a:adMZ100"2; 94:10:3e:f6:bf:42:8a:adMRVLq; 00:15:8d:00:02:36:84:85 m;  00:15:8d:00:02:36:84:85*"k; 00:15:8d:00:02:36:84:85LUMIZ; 00:15:8d:00:02:36:91:2f U;  00:15:8d:00:02:36:91:2f*L;  00:15:8d:00:02:36:91:2f"H; 00:15:8d:00:02:36:91:2fLUMI{;  00:15:8d:00:02:04:54:404z; 800:15:8d:00:02:04:54:40lumi.sensor_magnet.aq2"[; 00:15:8d:00:02:04:54:40LUMI$ ; 00:0d:6f:00:0c:a7:42:a63210-L(;  00:0d:6f:00:0c:a7:42:a6CentraLite"; 00:15:8d:00:02:04:a0:62LUMI"; 00:15:8d:00:02:b5:f7:cdLUMI"{; 00:15:8d:00:02:b5:2d:b5LUMI*R;"00:12:4b:00:19:36:95:c1lumi.router#Q;00:12:4b:00:19:36:95:c1LUMI ;;  00:0d:6f:00:0b:12:4b:62'.; 00:0d:6f:00:0b:12:4b:62MCT-340 E%-; 00:0d:6f:00:0b:12:4b:62Visonic %;  00:0d:6f:00:0b:1c:f1:4a'#; 00:0d:6f:00:0b:1c:f1:4aMCT-340 E%"; 00:0d:6f:00:0b:1c:f1:4aVisonic5; :00:0d:6f:ff:fe:7a:d3:7aTRADFRI signal repeater,; (00:0d:6f:ff:fe:7a:d3:7aIKEA of Sweden%;00:0d:6f:00:05:76:14:803320-L); 00:0d:6f:00:05:76:14:80CentraLite$; 00:0d:6f:00:05:76:14:803320-L(;  00:0d:6f:00:05:76:14:80CentraLite ErP-d LoK'|O G xr@oG wS-sKw P l ! p M * * * * * * * * * * * * * * *nI_;U/!6;84:18:26:00:00:02:b7:13''O; 00:15:8d:00:02:b5:f7:cdK"N; 00:15:8d:00:02:b5:f7:cd0!M; 00:15:8d:00:02:b5:f7:cdU; 00:15:8d:00:02:b5:f7:cd$*; 00:15:8d:00:02:b5:f7:cd ; 84:18:26:00:00:02:b7:13#;84:18:26:00:00:02:b7:13r9;@84:18:26:00:00:02:b7:13LIGHTIFY A19 Tunable White$;84:18:26:00:00:02:b7:13OSRAMF <;84:18:26:00:00:02:b7:13 S;  00:15:8d:00:02:b8:bb:71U!; 00:15:8d:00:02:b5:f7:cd ; 00:15:8d:00:02:b5:f7:cd  ; 00:15:8d:00:02:b5:f7:cd!N ; h00:15:8d:00:02:b5:f7:cd! (!3!$! !FY!(!%K ##;84:18:26:00:00:00:d1:dfr$;84:18:26:00:00:00:d1:dfOSRAM!;84:18:26:00:00:00:d0:fa w"#<;84:18:26:00:00:00:d0:far99;@84:18:26:00:00:00:d0:faLIGHTIFY A19 Tunable White$8;84:18:26:00:00:00:d0:faOSRAM$*;84:18:26:00:00:02:44:33  %!$;84:18:26:00:00:02:44:33#; 84:18:26:00:00:02:44:33!;84:18:26:00:00:02:44:33!;84:18:26:00:00:02:44:33"N;84:18:26:00:00:02:44:33 J;84:18:26:00:00:02:44:33I; 84:18:26:00:00:02:44:33"F;84:18:26:00:00:02:44:335; 84:18:26:00:00:02:44:33#4;84:18:26:00:00:02:44:33r93;@84:18:26:00:00:02:44:33LIGHTIFY A19 Tunable White$2;84:18:26:00:00:02:44:33OSRAM$;84:18:26:00:00:02:b7:13  %!1;84:18:26:00:00:02:b7:130; 84:18:26:00:00:02:b7:13!B;84:18:26:00:00:02:b7:133; 84:18:26:00:00:04:a7:c9 2;84:18:26:00:00:04:a7:c9 0;84:18:26:00:00:04:a7:c9"-;84:18:26:00:00:04:a7:c99+;@84:18:26:00:00:04:a7:c9LIGHTIFY A19 Tunable White$*;84:18:26:00:00:04:a7:c9OSRAMNv; h00:15:8d:00:02:b8:bb:71! (!C!$! !FY!(!%M $|; 00:15:8d:00:02:b8:bb:71 j G!a;84:18:26:00:00:00:d1:df9 ;@84:18:26:00:00:00:d1:dfLIGHTIFY A19 Tunable White#.;84:18:26:00:00:04:a7:c9j; 00:15:8d:00:02:c3:af:b1 ; 00:0d:6f:00:0d:2e:8d:e9>.; 00:15:8d:00:02:36:84:85 Cya; 00:0d:6f:00:0b:12:4b:62`; 00:0d:6f:00:0d:2e:8d:e9!_; 00:0d:6f:00:0d:2e:8d:e9$[; 00:0d:6f:00:0d:2e:8d:e91N; 84:18:26:00:00:00:d0:faI; 00:12:4b:00:19:36:95:c1G; 84:18:26:00:00:04:a7:c9F;  00:0d:6f:00:0c:a7:42:a6[y; 00:0d:6f:00:0d:2e:8d:72>4k; 800:15:8d:00:02:c3:af:b1lumi.sensor_magnet.aq2 ;  00:0d:6f:00:0d:2e:8d:e9/;,00:0d:6f:00:0d:2e:8d:e9Contact Sensor-A); 00:0d:6f:00:0d:2e:8d:e9CentraLite.; ,00:0d:6f:00:0d:2e:8d:e9Contact Sensor-A(;  00:0d:6f:00:0d:2e:8d:e9CentraLite"W;84:18:26:00:00:04:a7:c9 V^=sQ1 f C " z X 6  i G % z X 6  f D !wU3`=vS3h6kI'mI'lH(}[)t#; !; 00:15:8d:00:01:eb:71:ecU!; 00:15:8d:00:01:eb:71:ecU ; 00:15:8d:00:02:04:a0:62;  00:15:8d:00:02:04:a0:62"; 00:15:8d:00:02:04:54:40U1; 100:15:8d:00:02:04:54:40battery_voltage_mV!; 00:15:8d:00:02:04:54:40!U!; 00:15:8d:00:02:04:54:40 U#; 00:15:8d:00:02:04:54:40U ; 00:15:8d:00:02:04:54:40h ; 00:15:8d:00:02:04:54:40 ; 00:15:8d:00:02:04:54:40;  00:15:8d:00:02:04:54:40";00:12:4b:00:19:36:95:c1U";00:12:4b:00:19:36:95:c1K ;00:12:4b:00:19:36:95:c1R ;00:12:4b:00:19:36:95:c1Q!; 00:0d:6f:ff:fe:7a:d3:7aQ; 00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a";00:0d:6f:00:0d:2e:8d:e9K";00:0d:6f:00:0d:2e:8d:e9";00:0d:6f:00:0d:2e:8d:e9#; 00:0d:6f:00:0d:2e:8d:e9K";  00:0d:6f:00:0d:2e:8d:e9"; 00:0d:6f:00:0d:2e:8d:e9K"; 00:0d:6f:00:0d:2e:8d:e9U!; 00:0d:6f:00:0d:2e:8d:e9>B!; 00:0d:6f:00:0d:2e:8d:e91K!; 00:0d:6f:00:0d:2e:8d:e9!Uo!; 00:0d:6f:00:0d:2e:8d:e9 Un!; 00:0d:6f:00:0d:2e:8d:e9K!; 00:0d:6f:00:0d:2e:8d:e9!; 00:0d:6f:00:0d:2e:8d:e9";00:0d:6f:00:0d:2e:8d:72L !;00:0d:6f:00:0d:2e:8d:72r!;00:0d:6f:00:0d:2e:8d:72r#; 00:0d:6f:00:0d:2e:8d:72L !;  00:0d:6f:00:0d:2e:8d:72r"; 00:0d:6f:00:0d:2e:8d:72L "; 00:0d:6f:00:0d:2e:8d:72U!; 00:0d:6f:00:0d:2e:8d:72>E!; 00:0d:6f:00:0d:2e:8d:721L!; 00:0d:6f:00:0d:2e:8d:72!U!; 00:0d:6f:00:0d:2e:8d:72 U!; 00:0d:6f:00:0d:2e:8d:72L ; 00:0d:6f:00:0d:2e:8d:72r ; 00:0d:6f:00:0d:2e:8d:72r$; 00:0d:6f:00:0c:a7:42:a6  U!; 00:0d:6f:00:0c:a7:42:a6U!; 00:0d:6f:00:0c:a7:42:a6K ; 00:0d:6f:00:0c:a7:42:a6 ; 00:0d:6f:00:0c:a7:42:a6#; 00:0d:6f:00:0b:1c:f1:4aK ;  00:0d:6f:00:0b:1c:f1:4a%"; 00:0d:6f:00:0b:1c:f1:4aK"; 00:0d:6f:00:0b:1c:f1:4aU!; 00:0d:6f:00:0b:1c:f1:4a1K!; 00:0d:6f:00:0b:1c:f1:4a!L!; 00:0d:6f:00:0b:1c:f1:4a L!; 00:0d:6f:00:0b:1c:f1:4aK; 00:0d:6f:00:0b:1c:f1:4a#; 00:0d:6f:00:0b:1c:f1:4a"#; 00:0d:6f:00:0b:12:4b:62K ;  00:0d:6f:00:0b:12:4b:62;"; 00:0d:6f:00:0b:12:4b:62K"; 00:0d:6f:00:0b:12:4b:62U!; 00:0d:6f:00:0b:12:4b:621K!; 00:0d:6f:00:0b:12:4b:62!K!; 00:0d:6f:00:0b:12:4b:62 K!; 00:0d:6f:00:0b:12:4b:62K; 00:0d:6f:00:0b:12:4b:62.; 00:0d:6f:00:0b:12:4b:62-!;00:0d:6f:00:05:76:14:80 ;00:0d:6f:00:05:76:14:80 ;00:0d:6f:00:05:76:14:80"; 00:0d:6f:00:05:76:14:80!; 00:0d:6f:00:05:76:14:80!; 00:0d:6f:00:05:76:14:80> ; 00:0d:6f:00:05:76:14:80>7j ; 00:0d:6f:00:05:76:14:801 ; 00:0d:6f:00:05:76:14:80!> ; 00:0d:6f:00:05:76:14:80 > ; 00:0d:6f:00:05:76:14:80T; 00:0d:6f:00:05:76:14:80;  00:0d:6f:00:05:76:14:80 sM Ngpa?O"-`>D #  } [ 9  { X 4  ~ ] ;  b A ) S 1 pMaaaaaaaaaa5~[8d@mK)-!NP!; 00:15:8d:00:02:36:91:2f!U!; 00:15:8d:00:02:36:84:85!T#; 00:15:8d:00:02:36:84:85T!#; 00:15:8d:00:02:36:91:2fU!; 00:15:8d:00:02:c3:af:b1!T#; 00:15:8d:00:02:36:84:85L'"; 00:15:8d:00:02:36:84:85T!; 00:15:8d:00:02:36:84:85 T!; 00:15:8d:00:02:36:91:2f U!; 00:15:8d:00:02:04:a0:62!R7E"; 00:15:8d:00:02:36:91:2fU#; 00:15:8d:00:02:36:91:2fM!; 00:15:8d:00:02:36:91:2fM-"; 00:15:8d:00:02:c3:95:8aU2!; 00:15:8d:00:02:c3:95:8a U1!; 00:15:8d:00:02:c3:95:8a!U0#; 00:15:8d:00:02:c3:95:8aU/"; 00:15:8d:00:02:c3:af:b1T!; 00:15:8d:00:02:c3:af:b1 T#; 00:15:8d:00:02:c3:af:b1T!; 00:15:8d:00:02:36:91:2f Z!; 00:15:8d:00:02:04:a0:62 R1; 100:15:8d:00:02:36:91:2fbattery_voltage_mV!!; 00:15:8d:00:02:04:a0:62N"; 00:15:8d:00:02:04:a0:62R#; 00:15:8d:00:02:04:a0:62R!; 00:15:8d:00:02:36:91:2fjn ; 00:15:8d:00:02:36:91:2f H;  00:15:8d:00:02:36:91:2f L#; 00:15:8d:00:02:b5:2d:b5R!;  00:15:8d:00:02:36:84:85 m!; 00:15:8d:00:02:36:84:85 q!; 00:15:8d:00:02:b5:2d:b5!R1; 100:15:8d:00:02:36:84:85battery_voltage_mV !; 00:15:8d:00:02:b5:2d:b5N"; 00:15:8d:00:02:b5:2d:b5R!; 00:15:8d:00:02:b5:2d:b5 R!; 00:15:8d:00:02:36:84:85A!; 00:15:8d:00:02:36:84:85j{ ; 00:15:8d:00:02:36:84:85 k#1; 100:15:8d:00:02:04:a0:62battery_voltage_mV h";84:18:26:00:00:00:d0:fa!8 t!; 00:15:8d:00:02:c3:af:b1 !; 00:15:8d:00:02:c3:af:b1 ; 00:15:8d:00:02:c3:af:b1sU ; 00:15:8d:00:02:c3:95:8as@ ; 00:15:8d:00:02:c3:95:8av ; 00:15:8d:00:02:c3:95:8as<$; 00:15:8d:00:02:b8:bb:71$; 00:15:8d:00:02:b8:bb:71|$; 00:15:8d:00:02:b8:bb:71#; 00:15:8d:00:02:b8:bb:71US#; 00:15:8d:00:02:b8:bb:71v!; 00:15:8d:00:02:b8:bb:71T!; 00:15:8d:00:02:b8:bb:71 ;  00:15:8d:00:02:b8:bb:71"; 00:15:8d:00:02:b5:f7:cd$; 00:15:8d:00:02:b5:f7:cdO$; 00:15:8d:00:02:b5:f7:cd*$; 00:15:8d:00:02:b5:f7:cdN#; 00:15:8d:00:02:b5:f7:cdUM#; 00:15:8d:00:02:b5:f7:cdj"; 00:15:8d:00:02:b5:f7:cd1; 100:15:8d:00:02:b5:f7:cdbattery_voltage_mVc!; 00:15:8d:00:02:b5:f7:cd!!; 00:15:8d:00:02:b5:f7:cd #; 00:15:8d:00:02:b5:f7:cd #; 00:15:8d:00:02:b5:f7:cd !; 00:15:8d:00:02:b5:f7:cdT!; 00:15:8d:00:02:b5:f7:cd' ; 00:15:8d:00:02:b5:f7:cd ;  00:15:8d:00:02:b5:f7:cdj+#1; 100:15:8d:00:02:b5:2d:b5battery_voltage_mV ; 00:15:8d:00:02:b5:2d:b5; 00:15:8d:00:02:b5:2d:b5{;  00:15:8d:00:02:b5:2d:b5E$!;  00:15:8d:00:02:36:91:2f U yWgG' X w 8 2 q Q  % iI(aA"Z;xX7}hI^>.1Pp`A  MMMMMM k L , c#B dC"y<[z!;00:0d:6f:00:0d:2e:8d:e9  ;00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9;00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9  ; 00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9 ; 00:15:8d:00:02:36:84:85O ; 00:15:8d:00:02:04:a0:62=; 00:15:8d:00:02:04:a0:62>; 00:15:8d:00:02:04:a0:62<; 00:15:8d:00:02:04:a0:62; ; 00:15:8d:00:02:04:54:40J; 00:15:8d:00:02:04:54:40K; 00:15:8d:00:02:04:54:40I; 00:15:8d:00:02:04:54:40H;00:12:4b:00:19:36:95:c10;00:12:4b:00:19:36:95:c1/ ;00:0d:6f:ff:fe:7a:d3:7a! ; 00:0d:6f:ff:fe:7a:d3:7a|; 00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a ; 00:0d:6f:ff:fe:7a:d3:7a ";00:0d:6f:00:0d:2e:8d:e9!;00:0d:6f:00:0d:2e:8d:72d ;00:0d:6f:00:0d:2e:8d:72 c;00:0d:6f:00:0d:2e:8d:72b; 00:0d:6f:00:0d:2e:8d:72a;00:0d:6f:00:0d:2e:8d:72`; 00:0d:6f:00:0d:2e:8d:72 _; 00:0d:6f:00:0d:2e:8d:72^; 00:0d:6f:00:0d:2e:8d:72]; 00:0d:6f:00:0d:2e:8d:72 \; 00:0d:6f:00:0d:2e:8d:72[; 00:0d:6f:00:0d:2e:8d:72Z; 00:0d:6f:00:0d:2e:8d:72Yk; 00:15:8d:00:01:eb:71:ec; 00:15:8d:00:02:b5:2d:b51 C; 00:15:8d:00:01:eb:71:ec; 00:15:8d:00:01:eb:71:ec; 00:15:8d:00:01:eb:71:ec; 00:15:8d:00:02:36:91:2fM; 00:15:8d:00:02:36:91:2fN; 00:15:8d:00:02:36:91:2fL; 00:15:8d:00:02:36:84:85P; 00:15:8d:00:02:36:84:85Q ; 00:0d:6f:00:0c:a7:42:a6G; 00:0d:6f:00:0c:a7:42:a6 F; 00:0d:6f:00:0c:a7:42:a6 E; 00:0d:6f:00:0c:a7:42:a6D; 00:0d:6f:00:0c:a7:42:a6C; 00:0d:6f:00:0c:a7:42:a6B; 00:0d:6f:00:0c:a7:42:a6A; 00:0d:6f:00:0c:a7:42:a6@; 00:0d:6f:00:0c:a7:42:a6? ; 00:0d:6f:00:0b:12:4b:62 .; 00:0d:6f:00:0b:12:4b:62 -; 00:0d:6f:00:0b:12:4b:62,; 00:0d:6f:00:0b:12:4b:62+; 00:0d:6f:00:0b:12:4b:62*; 00:0d:6f:00:0b:12:4b:62); 00:0d:6f:00:0b:12:4b:62(; 00:0d:6f:00:0b:1c:f1:4a  ; 00:0d:6f:00:0b:1c:f1:4a ; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a]; 00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a !;00:0d:6f:00:05:76:14:80 ;00:0d:6f:00:05:76:14:80  ;00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80 ;00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80;  00:0d:6f:00:05:76:14:80 \ 3  v V 8  { \ =  !|Z8qP. bA M, dAyX7m  ^ =  sxW6c@ U 6  ;84:18:26:00:00:d9:86:e7 ;84:18:26:00:00:d9:86:e7 ;84:18:26:00:00:d9:86:e7 ;84:18:26:00:00:d9:86:e7;84:18:26:00:00:d9:86:e7";84:18:26:00:00:01:30:50!;84:18:26:00:00:01:30:50 !;84:18:26:00:00:01:30:50 ;84:18:26:00:00:01:30:50 ;84:18:26:00:00:01:30:50 ;84:18:26:00:00:01:30:50 ;84:18:26:00:00:01:30:50 ;84:18:26:00:00:01:30:50;84:18:26:00:00:01:30:50 R|";84:18:26:00:00:d9:86:e7!;84:18:26:00:00:d9:86:e7 !;84:18:26:00:00:d9:86:e7 ;84:18:26:00:00:d9:86:e7 ;84:18:26:00:00:d9:86:e7";84:18:26:00:00:00:d1:df!;84:18:26:00:00:00:d1:df !;84:18:26:00:00:00:d1:df ;84:18:26:00:00:00:d1:df ;84:18:26:00:00:00:d1:df ;84:18:26:00:00:00:d1:df ;84:18:26:00:00:00:d1:df ;84:18:26:00:00:00:d1:df;84:18:26:00:00:00:d1:df";84:18:26:00:00:00:d0:fa!;84:18:26:00:00:00:d0:fa !;84:18:26:00:00:00:d0:fa ;84:18:26:00:00:00:d0:fa ;84:18:26:00:00:00:d0:fa ;84:18:26:00:00:00:d0:fa ;84:18:26:00:00:00:d0:fa ;84:18:26:00:00:00:d0:fa;84:18:26:00:00:00:d0:fa";84:18:26:00:00:02:44:33!;84:18:26:00:00:02:44:33 !;84:18:26:00:00:02:44:33 ;84:18:26:00:00:02:44:33 ;84:18:26:00:00:02:44:33 ;84:18:26:00:00:02:44:33 ;84:18:26:00:00:02:44:33 ;84:18:26:00:00:02:44:33;84:18:26:00:00:02:44:33";84:18:26:00:00:02:b7:13!;84:18:26:00:00:02:b7:13 !;84:18:26:00:00:02:b7:13 ;84:18:26:00:00:02:b7:13 ;84:18:26:00:00:02:b7:13 ;84:18:26:00:00:02:b7:13 ;84:18:26:00:00:02:b7:13 ;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13 ; 94:10:3e:f6:bf:42:8a:adz; 94:10:3e:f6:bf:42:8a:adw; 94:10:3e:f6:bf:42:8a:adv; 94:10:3e:f6:bf:42:8a:adx; 94:10:3e:f6:bf:42:8a:ady; 94:10:3e:f6:bf:42:8a:adu; 94:10:3e:f6:bf:42:8a:adt";84:18:26:00:00:04:a7:c9!;84:18:26:00:00:04:a7:c9 !;84:18:26:00:00:04:a7:c9 ;84:18:26:00:00:04:a7:c9 ;84:18:26:00:00:04:a7:c9 ;84:18:26:00:00:04:a7:c9 ;84:18:26:00:00:04:a7:c9 ;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9 ; 00:15:8d:00:02:c3:af:b1k; 00:15:8d:00:02:c3:af:b1l; 00:15:8d:00:02:c3:af:b1j; 00:15:8d:00:02:c3:af:b1i ; 00:15:8d:00:02:c3:95:8ag; 00:15:8d:00:02:c3:95:8ah; 00:15:8d:00:02:c3:95:8af; 00:15:8d:00:02:c3:95:8ae ;00:15:8d:00:02:b8:bb:71;00:15:8d:00:02:b8:bb:71; 00:15:8d:00:02:b8:bb:71~; 00:15:8d:00:02:b8:bb:71}; 00:15:8d:00:02:b8:bb:71|; 00:15:8d:00:02:b8:bb:71{;00:15:8d:00:02:b5:f7:cd:;00:15:8d:00:02:b5:f7:cd9; 00:15:8d:00:02:b5:f7:cd8; 00:15:8d:00:02:b5:f7:cd7; 00:15:8d:00:02:b5:f7:cd6; 00:15:8d:00:02:b5:f7:cd5 ; 00:15:8d:00:02:b5:2d:b53; 00:15:8d:00:02:b5:2d:b54 } kL-tV8 n P 1  } ^ @ !  m O 0  y \ > gJ, y[>N1z\=fH( !k  a B # jJ)lL, kL, ;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13;00:0d:6f:00:0d:2e:8d:e9;00:0d:6f:00:0d:2e:8d:e9 ;00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9;00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9; 00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9  ; 00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9 ; 00:0d:6f:00:0d:2e:8d:e9 ;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9 ;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9;84:18:26:00:00:04:a7:c9;00:15:8d:00:02:b8:bb:71l; 00:15:8d:00:02:c3:af:b1k; 00:15:8d:00:02:c3:af:b1j; 00:15:8d:00:02:c3:af:b1i; 00:15:8d:00:02:c3:af:b1h; 00:15:8d:00:02:c3:95:8ag; 00:15:8d:00:02:c3:95:8af; 00:15:8d:00:02:c3:95:8ae; 00:15:8d:00:02:c3:95:8ad;00:0d:6f:00:0d:2e:8d:72c;00:0d:6f:00:0d:2e:8d:72 b;00:0d:6f:00:0d:2e:8d:72a; 00:0d:6f:00:0d:2e:8d:72`;00:0d:6f:00:0d:2e:8d:72_; 00:0d:6f:00:0d:2e:8d:72 ^; 00:0d:6f:00:0d:2e:8d:72]; 00:0d:6f:00:0d:2e:8d:72\; 00:0d:6f:00:0d:2e:8d:72 [; 00:0d:6f:00:0d:2e:8d:72Z; 00:0d:6f:00:0d:2e:8d:72Y; 00:0d:6f:00:0d:2e:8d:72z; 94:10:3e:f6:bf:42:8a:ady; 94:10:3e:f6:bf:42:8a:adx; 94:10:3e:f6:bf:42:8a:adw; 94:10:3e:f6:bf:42:8a:adv; 94:10:3e:f6:bf:42:8a:adu; 94:10:3e:f6:bf:42:8a:adt; 94:10:3e:f6:bf:42:8a:adQ; 00:15:8d:00:02:36:84:85P; 00:15:8d:00:02:36:84:85O; 00:15:8d:00:02:36:84:85N; 00:15:8d:00:02:36:91:2fM; 00:15:8d:00:02:36:91:2fL; 00:15:8d:00:02:36:91:2fK; 00:15:8d:00:02:04:54:40J; 00:15:8d:00:02:04:54:40I; 00:15:8d:00:02:04:54:40H; 00:15:8d:00:02:04:54:40G; 00:0d:6f:00:0c:a7:42:a6F; 00:0d:6f:00:0c:a7:42:a6 E; 00:0d:6f:00:0c:a7:42:a6 D; 00:0d:6f:00:0c:a7:42:a6C; 00:0d:6f:00:0c:a7:42:a6B; 00:0d:6f:00:0c:a7:42:a6A; 00:0d:6f:00:0c:a7:42:a6@; 00:0d:6f:00:0c:a7:42:a6?; 00:0d:6f:00:0c:a7:42:a6>; 00:15:8d:00:02:04:a0:62=; 00:15:8d:00:02:04:a0:62<; 00:15:8d:00:02:04:a0:62;; 00:15:8d:00:02:04:a0:62:;00:15:8d:00:02:b5:f7:cd9;00:15:8d:00:02:b5:f7:cd8; 00:15:8d:00:02:b5:f7:cd7; 00:15:8d:00:02:b5:f7:cd6; 00:15:8d:00:02:b5:f7:cd5; 00:15:8d:00:02:b5:f7:cd4; 00:15:8d:00:02:b5:2d:b53; 00:15:8d:00:02:b5:2d:b52; 00:15:8d:00:02:b5:2d:b51; 00:15:8d:00:02:b5:2d:b50;00:12:4b:00:19:36:95:c1/;00:12:4b:00:19:36:95:c1.; 00:0d:6f:00:0b:12:4b:62 -; 00:0d:6f:00:0b:12:4b:62 ,; 00:0d:6f:00:0b:12:4b:62+; 00:0d:6f:00:0b:12:4b:62*; 00:0d:6f:00:0b:12:4b:62); 00:0d:6f:00:0b:12:4b:62(; 00:0d:6f:00:0b:12:4b:62 ; 00:0d:6f:00:0b:1c:f1:4a ; 00:0d:6f:00:0b:1c:f1:4a ; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a; 00:0d:6f:00:0b:1c:f1:4a;84:18:26:00:00:04:a7:c9;00:15:8d:00:02:b8:bb:71~; 00:15:8d:00:02:b8:bb:71}; 00:15:8d:00:02:b8:bb:71|; 00:15:8d:00:02:b8:bb:71{; 00:15:8d:00:02:b8:bb:71;00:0d:6f:ff:fe:7a:d3:7a!; 00:0d:6f:ff:fe:7a:d3:7a|; 00:0d:6f:ff:fe:7a:d3:7a; 00:0d:6f:ff:fe:7a:d3:7a ; 00:0d:6f:ff:fe:7a:d3:7a ; 00:0d:6f:ff:fe:7a:d3:7a ; 00:0d:6f:ff:fe:7a:d3:7a ;00:0d:6f:00:05:76:14:80 ;00:0d:6f:00:05:76:14:80  ;00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80;00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80 ; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80; 00:0d:6f:00:05:76:14:80 7q^<}\; z Z 9  w W 7  x X 8  u U 5 q w YqqqqqT;84:18:26:00:00:d9:86:e7S;84:18:26:00:00:d9:86:e7 R;84:18:26:00:00:d9:86:e7Q;84:18:26:00:00:d9:86:e7P;84:18:26:00:00:d9:86:e7O;84:18:26:00:00:d9:86:e7N;84:18:26:00:00:d9:86:e7M;84:18:26:00:00:d9:86:e7L;84:18:26:00:00:d9:86:e7K;84:18:26:00:00:01:30:50J;84:18:26:00:00:01:30:50 I;84:18:26:00:00:01:30:50H;84:18:26:00:00:01:30:50G;84:18:26:00:00:01:30:50F;84:18:26:00:00:01:30:50E;84:18:26:00:00:01:30:50D;84:18:26:00:00:01:30:50C;84:18:26:00:00:01:30:50X; 00:15:8d:00:01:eb:71:ecW; 00:15:8d:00:01:eb:71:ecV; 00:15:8d:00:01:eb:71:ecU; 00:15:8d:00:01:eb:71:ec9;84:18:26:00:00:00:d1:df8;84:18:26:00:00:00:d1:df 7;84:18:26:00:00:00:d1:df6;84:18:26:00:00:00:d1:df5;84:18:26:00:00:00:d1:df4;84:18:26:00:00:00:d1:df3;84:18:26:00:00:00:d1:df2;84:18:26:00:00:00:d1:df1;84:18:26:00:00:00:d1:df0;84:18:26:00:00:00:d0:fa/;84:18:26:00:00:00:d0:fa .;84:18:26:00:00:00:d0:fa-;84:18:26:00:00:00:d0:fa,;84:18:26:00:00:00:d0:fa+;84:18:26:00:00:00:d0:fa*;84:18:26:00:00:00:d0:fa);84:18:26:00:00:00:d0:fa(;84:18:26:00:00:00:d0:fa';84:18:26:00:00:02:44:33&;84:18:26:00:00:02:44:33 %;84:18:26:00:00:02:44:33$;84:18:26:00:00:02:44:33#;84:18:26:00:00:02:44:33";84:18:26:00:00:02:44:33!;84:18:26:00:00:02:44:33 ;84:18:26:00:00:02:44:33;84:18:26:00:00:02:44:33;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13 ;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13;84:18:26:00:00:02:b7:13 ^ v S 0  x U +  y Z3c<OxS O+hI$ 4  @  x o L 'lI$.P!^,E*!; 00:15:8d:00:02:04:54:40; 00:15:8d:00:02:04:54:40  B; 00:15:8d:00:02:04:a0:62!=A; F00:15:8d:00:02:04:a0:62! (!!?$ !d+$A;84:18:26:00:00:00:d1:df  %$;84:18:26:00:00:00:d0:fa  %; 00:15:8d:00:02:04:a0:62!&; 00:0d:6f:00:0d:2e:8d:e9KB!; 00:0d:6f:00:0b:1c:f1:4a!8;84:18:26:00:00:00:d0:fa7; 84:18:26:00:00:00:d0:fa$M;84:18:26:00:00:04:a7:c9  %!!; 00:0d:6f:00:0b:12:4b:62K;00:12:4b:00:19:36:95:c1"N; 00:0d:6f:00:0c:a7:42:a6  A*; N00:15:8d:00:02:36:91:2f! (!!Q$! !d!;84:18:26:00:00:00:d1:df; 84:18:26:00:00:00:d1:df ; 00:0d:6f:00:0d:2e:8d:72!; 00:0d:6f:00:0d:2e:8d:72 !s;84:18:26:00:00:04:a7:c9r;84:18:26:00:00:04:a7:c9 o; 00:0d:6f:00:0d:2e:8d:e9!n; 00:0d:6f:00:0d:2e:8d:e9 !{;84:18:26:00:00:01:30:50u; 84:18:26:00:00:01:30:50!J; 00:0d:6f:00:0d:2e:8d:72 +; 00:15:8d:00:02:36:91:2f!B;  00:0d:6f:00:0c:a7:42:a6 h; 00:15:8d:00:02:b5:2d:b5!,; 00:15:8d:00:02:36:91:2f  ; 00:15:8d:00:02:36:91:2f; 00:15:8d:00:02:36:91:2f4.2; ,00:15:8d:00:01:eb:71:eclumi.sensor_swit"1; 00:15:8d:00:01:eb:71:ecLUMI!-; 00:15:8d:00:02:36:91:2f!O;84:18:26:00:00:d9:86:e7M; 84:18:26:00:00:d9:86:e7$;84:18:26:00:00:d9:86:e7  %g; 84:18:26:00:00:d9:86:e7#b;84:18:26:00:00:d9:86:e7r9a;@84:18:26:00:00:d9:86:e7LIGHTIFY A19 Tunable White$`;84:18:26:00:00:d9:86:e7OSRAM G;84:18:26:00:00:01:30:50 F;84:18:26:00:00:01:30:50; 84:18:26:00:00:01:30:50#;84:18:26:00:00:01:30:50r9;@84:18:26:00:00:01:30:50LIGHTIFY A19 Tunable White$;84:18:26:00:00:01:30:50OSRAM; 94:10:3e:f6:bf:42:8a:ad c; 94:10:3e:f6:bf:42:8a:adb;  94:10:3e:f6:bf:42:8a:ad!2; 00:15:8d:00:02:c3:95:8a 1; 00:15:8d:00:02:c3:95:8a  0; 00:15:8d:00:02:c3:95:8a!=/; F00:15:8d:00:02:c3:95:8a! (!!~$ !JTd!@; 00:15:8d:00:02:c3:af:b1?; 00:15:8d:00:02:c3:af:b1  >; 00:15:8d:00:02:c3:af:b1!==; F00:15:8d:00:02:c3:af:b1! (!!$ !JTd1; 84:18:26:00:00:00:d1:df '; 00:15:8d:00:02:36:84:85!(; 00:15:8d:00:02:36:84:85@'; 00:15:8d:00:02:36:84:85  &; 00:15:8d:00:02:36:84:85!A%; N00:15:8d:00:02:36:84:85! (!!0$! !:d ;  00:0d:6f:00:0d:2e:8d:72 ;00:0d:6f:00:0d:2e:8d:72! ; 00:0d:6f:00:0d:2e:8d:72$; 00:0d:6f:00:0d:2e:8d:721; 00:0d:6f:00:0d:2e:8d:72&; 00:0d:6f:00:0b:1c:f1:4a!@V8\); 00:0d:6f:00:0b:1c:f1:4a ;  00:0d:6f:00:0b:1c:f1:4a!~; 00:0d:6f:00:0b:1c:f1:4a |; 00:0d:6f:00:0b:1c:f1:4a1{; 00:0d:6f:00:0b:1c:f1:4a&k; 00:0d:6f:00:0b:12:4b:62!@[ =pj; 00:0d:6f:00:0b:12:4b:62 i;  00:0d:6f:00:0b:12:4b:62 h;00:0d:6f:00:0d:2e:8d:e9!e; 00:0d:6f:00:0b:12:4b:62 d;  00:0d:6f:00:0d:2e:8d:e9b; 00:0d:6f:00:0b:12:4b:621l!j; 00:15:8d:00:02:b5:2d:b5i; 00:15:8d:00:02:b5:2d:b5 &!D; 00:15:8d:00:02:04:a0:62_;  00:0d:6f:ff:fe:7a:d3:7a$u;84:18:26:00:00:01:30:50  %r =g; F00:15:8d:00:02:b5:2d:b5! (!!$ !dP; 00:15:8d:00:02:b5:2d:b5 ; 00:15:8d:00:02:04:54:40!=; F00:15:8d:00:02:04:54:40! (!!$ !:dC; 00:15:8d:00:02:04:a0:62  FBsN(yU0 [8`< ~ [ 8  _ 9  f B  j G $  q L &hB ~%;84:18:26:00:00:d9:86:e7  P";84:18:26:00:00:d9:86:e7MO";84:18:26:00:00:d9:86:e7MM";84:18:26:00:00:d9:86:e7L$;84:18:26:00:00:d9:86:e7L";84:18:26:00:00:d9:86:e7L";84:18:26:00:00:d9:86:e7L#;84:18:26:00:00:01:30:50L#;84:18:26:00:00:01:30:50L";84:18:26:00:00:01:30:50P{%;84:18:26:00:00:01:30:50  P";84:18:26:00:00:01:30:50Pu";84:18:26:00:00:01:30:50L$;84:18:26:00:00:01:30:50L";84:18:26:00:00:01:30:50L";84:18:26:00:00:01:30:50L!; 94:10:3e:f6:bf:42:8a:adQ!; 94:10:3e:f6:bf:42:8a:adQ!; 94:10:3e:f6:bf:42:8a:adL ; 94:10:3e:f6:bf:42:8a:ad3 ; 94:10:3e:f6:bf:42:8a:ad2%;84:18:26:00:00:04:a7:c9  U$;84:18:26:00:00:04:a7:c9 #;84:18:26:00:00:04:a7:c9-#;84:18:26:00:00:04:a7:c90#;84:18:26:00:00:04:a7:c9W#;84:18:26:00:00:04:a7:c92"; 84:18:26:00:00:04:a7:c93";84:18:26:00:00:04:a7:c9Us";84:18:26:00:00:04:a7:c9Ur";84:18:26:00:00:04:a7:c9K";84:18:26:00:00:04:a7:c9";84:18:26:00:00:04:a7:c9%;84:18:26:00:00:02:b7:13   $;84:18:26:00:00:02:b7:13#;84:18:26:00:00:02:b7:13#;84:18:26:00:00:02:b7:13#;84:18:26:00:00:02:b7:13";84:18:26:00:00:02:b7:13";84:18:26:00:00:02:b7:13";84:18:26:00:00:02:b7:13";84:18:26:00:00:02:b7:13";84:18:26:00:00:02:b7:13%;84:18:26:00:00:02:44:33  !*$;84:18:26:00:00:02:44:33 4#;84:18:26:00:00:02:44:33 #;84:18:26:00:00:02:44:33!#;84:18:26:00:00:02:44:33!#;84:18:26:00:00:02:44:33 #;84:18:26:00:00:02:44:33 "; 84:18:26:00:00:02:44:33 ";84:18:26:00:00:02:44:33!$";84:18:26:00:00:02:44:33!#";84:18:26:00:00:02:44:33 5";84:18:26:00:00:02:44:33 3";84:18:26:00:00:02:44:33 2%;84:18:26:00:00:00:d1:df  U$;84:18:26:00:00:00:d1:df!#;84:18:26:00:00:00:d1:df#a";84:18:26:00:00:00:d1:dfU";84:18:26:00:00:00:d1:dfU";84:18:26:00:00:00:d1:dfL1";84:18:26:00:00:00:d1:df!";84:18:26:00:00:00:d1:df!%;84:18:26:00:00:00:d0:fa  P$;84:18:26:00:00:00:d0:fa!<#;84:18:26:00:00:00:d0:fa!";84:18:26:00:00:00:d0:faP8";84:18:26:00:00:00:d0:faP7";84:18:26:00:00:00:d0:faKzigpy-0.80.1/tests/ota/000077500000000000000000000000001501451476000146725ustar00rootroot00000000000000zigpy-0.80.1/tests/ota/__init__.py000066400000000000000000000000001501451476000167710ustar00rootroot00000000000000zigpy-0.80.1/tests/ota/files/000077500000000000000000000000001501451476000157745ustar00rootroot00000000000000zigpy-0.80.1/tests/ota/files/external/000077500000000000000000000000001501451476000176165ustar00rootroot00000000000000zigpy-0.80.1/tests/ota/files/external/urls.json000066400000000000000000000023431501451476000215000ustar00rootroot00000000000000{ "dl/ikea/mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota": { "url": "https://fw.ota.homesmart.ikea.com/files/mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota", "checksum": "sha3-256:e68e61bd57291e0b6358242e72ee2dfe098cb8b769f572b5b8f8e7a34dbcfaca" }, "dl/ikea/10039874-1.0-TRADFRI-motion-sensor-2-2.0.022.ota.ota.signed": { "url": "https://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10039874-1.0-TRADFRI-motion-sensor-2-2.0.022.ota.ota.signed", "checksum": "sha3-256:105f6c3670822282b949a4a13886d6eac6befbb67dea8ad20546816acbbc6864" }, "dl/local_provider/1135-0000-201000A0-FLS-PP3_RGBW.zigbee": { "url": "https://deconz.dresden-elektronik.de/otau/1135-0000-201000A0-FLS-PP3_RGBW.zigbee", "checksum": "sha3-256:23415a1c54353219bb7de3e72ba6050d9e849be0954eebda9b5783d34d0723d1" }, "dl/local_provider/RDL2016091_1_E11-G13_V0.0.9_20170921_release.ota": { "url": "https://us-fm.cloud.sengled.com/sengled/zigbee/firmware/RDL2016091_1_E11-G13_V0.0.9_20170921_release.ota", "checksum": "sha3-256:542122a6f0f48075c3f089afcf9a4f86af2741672bc8c9cc7d5759ceed5c0e63" } }zigpy-0.80.1/tests/ota/files/ikea_version_info_dirigera.json000066400000000000000000000273511501451476000242360ustar00rootroot00000000000000[{"fw_image_type":10242,"fw_type":2,"fw_sha3_256":"e68e61bd57291e0b6358242e72ee2dfe098cb8b769f572b5b8f8e7a34dbcfaca","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota"},{"fw_image_type":40766,"fw_type":2,"fw_sha3_256":"b79150a538dfa413e784ff095e504afdd4e720554122f598dbbb13510448c652","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/inspelning-smart-plug-soc_release_prod_v33816645_02579ff4-6fec-42f6-8957-4048def87def.ota"},{"fw_major_version":2,"fw_minor_version":753,"size":272193571,"fw_type":3,"fw_hotfix_version":0,"fw_sha3_256":"be9a00de94b2eec3f8d5c067cd626047d45343ef1df9dfd3abd46996070fc0f2","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/DIRIGERA_release_prod_v2.753.0_ed340033-e1c2-4a7f-abe5-4696581080e4.raucb"},{"fw_image_type":6456,"fw_type":2,"fw_sha3_256":"ad623ed146226dc17b76b71a7a8414c6e550cfe29513580bd4d9269bc7578b2b","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/motionsensor-lds-mg21a_release_prod_v16777316_3a00064a-cc29-4aac-bdb6-c4fa1fb445d5.ota"},{"fw_image_type":4545,"fw_type":2,"fw_sha3_256":"d36803d6dea5f86bc3aea3b3ff7fd46d5d2187b61209cf68ceea2474e85a5f48","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-controller_release_prod_v604241925_abef4451-762a-4ef9-8c11-dfd88c3e98f7.ota"},{"fw_image_type":4554,"fw_type":2,"fw_sha3_256":"3c8cd9a5eee7e1b35a187e84a91db5d2bc91fd98c27f349548ae211eb233720d","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-dimmer_release_prod_v604241925_ecbb4451-ce85-4e6c-ab9f-e7ce32cd0c1e.ota"},{"fw_image_type":4548,"fw_type":2,"fw_sha3_256":"81edaa25a0e5c31a594b41551f1ebdede6c5f94b58e2dfe5462915fc0d4fc637","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-motion-sensor_release_prod_v604241925_e006dcd0-d37e-49db-9a61-6575cb5c44d3.ota"},{"fw_image_type":16897,"fw_type":2,"fw_sha3_256":"87b25a4ca55b1920d50defffd1bd459f0c90a9af0adc6e51609ebd384d6e78ac","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-driver-lp_release_prod_v587757105_b28cda41-22d2-446b-b3bf-5ca11d866719.ota"},{"fw_image_type":4550,"fw_type":2,"fw_sha3_256":"ce7b04ec3ccaa18e0b8b6a085a61c9526e3bacdc6b3513352ac62d2689b066eb","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-shortcut-button_release_prod_v604241926_56f5d8d1-78b1-4088-afc1-05d3b7e3314b.ota"},{"fw_image_type":8449,"fw_type":2,"fw_sha3_256":"6c543711e4e7cc88392097588c0d1a05228ae089e9bd2a9b5d1fb1b0b3ce76bb","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-w-1000lm_release_prod_v587810353_5a161508-742d-4eab-8761-286c80d116eb.ota"},{"fw_image_type":16898,"fw_type":2,"fw_sha3_256":"c60d3ced6fc1a5c0d32f8044f28f19c4e324c2b105a39d1666871036ced68222","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-driver-hp_release_prod_v587757105_b42c57bf-cb9c-478c-8327-5812a3286a64.ota"},{"fw_image_type":8708,"fw_type":2,"fw_sha3_256":"406ffad6d70c5d59faeaaaea63d42abfa9399cf5a7f3d91be10673fd37a2766a","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-kt-bulb-hwpwmcs-ws_release_prod_v16842784_32dc5ff1-4fa4-4960-a847-5e548a81047c.ota"},{"fw_image_type":4555,"fw_type":2,"fw_sha3_256":"967dc9224f8ea9b06f5f5f7d0cc93cdcad2b9a4c7424689fd24b1deaaf637db0","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-kt-styrbar-remote_release_prod_v33816598_c00d5422-e816-48ec-87a6-40198661d2d5.ota"},{"fw_image_type":4552,"fw_type":2,"fw_sha3_256":"1b5fbea79c5b41864352a938a90ad25d9a0118054bf1cdc0314ef9636a60143a","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-motion-sensor2_release_prod_v604241925_8afa2f7c-19c3-4ddf-a96c-233714179022.ota"},{"fw_image_type":15112,"fw_type":2,"fw_sha3_256":"fbabe810f1105152fab706f115c4c2fa21b05e0d61a0bdf06552851c634d5345","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/rodret-shortcut-soc_release_prod_v16777249_d89ffc33-55d1-4d47-acac-42365e5d9dd8.ota"},{"fw_image_type":4367,"fw_type":2,"fw_sha3_256":"029b49416e475ddfb6bd2abfdd47ba279062646cb88940d647cae51b295f5a7f","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-ikea-vindstyrka_release_prod_v16777233_c0532d00-594a-4e10-bb82-1b80d5b6ed87.ota"},{"fw_image_type":4487,"fw_type":2,"fw_sha3_256":"0bf36e39770f508fa96aeb6c5ed6de2d062351197b783cbe72dad720048cd20f","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-connected-blind_release_prod_v604241939_89e61475-8999-4074-842a-e04efac9c857.ota"},{"fw_image_type":16902,"fw_type":2,"fw_sha3_256":"13a15addaef498471f2d4475847769d798f4f14e210874faa02a2f1a1ba1b2a5","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-cv-cct-unified_release_prod_v587757105_33e34452-9267-4665-bc5a-844c8f61f063.ota"},{"fw_image_type":16901,"fw_type":2,"fw_sha3_256":"8951ac2ab9584249b7edf1ca245032f0f9d6fb65e121d268230f49b76ce04dad","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-sy5882-unified_release_prod_v587757105_d478807c-f16e-4989-a948-82818fb545b9.ota"},{"fw_image_type":51017,"fw_type":2,"fw_sha3_256":"6733ab9abdec5a9a7f303e3b9d8ad4cf847aa346270cc0f5526fb61471eb2544","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-jetstrom-cws_release_prod_v16777268_023f19b9-f55b-4c94-a3fe-00c97755eb78.ota"},{"fw_image_type":16643,"fw_type":2,"fw_sha3_256":"2ca4e43497f274d43e340ac6721e221739743a1332ebdcfe1dfd1c2282e14f00","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-light-unified-w_release_prod_v587806257_147c8812-e7f3-4999-b7eb-3da91009ab65.ota"},{"fw_image_type":8450,"fw_type":2,"fw_sha3_256":"53b63d510bd868078bf53f345b31e88167ef10890ea843a20f7a28c7ed5815e7","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-lds-bulb-hwpwm-ww_release_prod_v16842756_529d7965-cee3-4c32-bd28-e5dd17ddc256.ota"},{"fw_image_type":4354,"fw_type":2,"fw_sha3_256":"8401788edc74c2e417011f1b913fa3391f5cc3f12487c4647089e19fbde20381","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-signal-repeater_release_prod_v587753009_3ce8f096-bd12-4b2c-b66e-51dc9f2637dc.ota"},{"fw_image_type":16645,"fw_type":2,"fw_sha3_256":"c1b55ce8de192c7e8c9510f3668fb1a3bc0c1811f946012060054870ca383c24","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-lds-stoftmoln_release_prod_v65542_3388635f-aa73-40e0-8edb-50cd242b72f1.ota"},{"fw_image_type":8707,"fw_type":2,"fw_sha3_256":"64f4627df80227872bfc65cda8e64c42288e75e2e22fd784e5eb5674f546c356","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-ws-gu10_release_prod_v587757105_25ac125d-5723-4b92-aa02-404fd5008a55.ota"},{"fw_image_type":8710,"fw_type":2,"fw_sha3_256":"38ab4524f5da1cc0ff8bca1be726c631aea38b1fbdc9f43c9a0ff42d80dca0da","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-ws-soc_release_prod_v50331683_f909cf22-3452-47e3-b8b2-c59d82102e17.ota"},{"fw_image_type":16649,"fw_type":2,"fw_sha3_256":"3815222125490d9ad331e967dba34700733d12ad6ec3776f19b1a7092c3bbc2c","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-driver-zingo_release_prod_v16777220_1fd2b92c-45c5-44d0-97c5-5c71c2ecb8d2.ota"},{"fw_image_type":8709,"fw_type":2,"fw_sha3_256":"a9a4d6814c2302ba46cd71e7a1e80c192a17accf78ab17eeaa31bb1a8a39459e","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-lds-bulb-hwpwmcs-ws_release_prod_v65554_5d50205c-63c3-426d-bfba-4839c4b55bba.ota"},{"fw_image_type":8448,"fw_type":2,"fw_sha3_256":"2bfd7a648dbe954812a72ab16fc9201dcf2efdb7ff0d9d178ee5a9f00f65a151","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-ww_release_prod_v16777282_a39f1e76-af87-4f04-bd7f-0faf71baf3e4.ota"},{"fw_image_type":4366,"fw_type":2,"fw_sha3_256":"b72a646849f3494adc0690b799b017eb8e78079bae8bf71df692c5d89bc7219f","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/symfonisk-sound-remote-zingo_release_prod_v16777269_1fcbd170-5b54-49e8-896e-16a73ca72011.ota"},{"fw_image_type":4353,"fw_type":2,"fw_sha3_256":"01a742d63d0b247259068b729edace2929af8f946a8df2334e3025f59cb4cff1","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-control-outlet_release_prod_v587765297_20061876-85d6-4b39-8c9c-eb95620baa97.ota"},{"fw_image_type":4357,"fw_type":2,"fw_sha3_256":"725e953c3a9507a921a533b04ae1231d0c16ccc57080d81acfdc130e18066426","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-jetstrom-ws_release_prod_v33816584_33813e5d-0cc9-4b4a-9e93-9000ee44cbe4.ota"},{"fw_image_type":16900,"fw_type":2,"fw_sha3_256":"754eb2e6b4867d4972e43441cbef9306537eaf4dc41b30bfd3163b1b69c67050","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-sy5882-bulb-ws_release_prod_v587814449_185b3c4d-da1b-4867-8c16-2cee1fc5c11d.ota"},{"fw_image_type":4546,"fw_type":2,"fw_sha3_256":"b39c08132728c048ebcc90093f74fd355cefce64f84d061560aa1c57ed38b39c","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-wireless-dimmer_release_prod_v587367985_87ff9a75-c4e3-4999-a654-09bb8638f4cc.ota"},{"fw_image_type":16899,"fw_type":2,"fw_sha3_256":"cc22b85ca4ba06acf8047b31234b137fcd084d45660bb7373d2c67c3ed8b0329","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-sy5882-driver-ws_release_prod_v587798065_479fc716-673b-4731-bbb7-86833c456e4c.ota"},{"fw_image_type":16641,"fw_type":2,"fw_sha3_256":"95c154a0a97f1276ae5f53b50ee5ee99e31dcf96d7199b2708262a40707e5771","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-transformer_release_prod_v587753009_17d32c23-151f-44cb-a947-4c13a78f36e6.ota"},{"fw_image_type":10245,"fw_type":2,"fw_sha3_256":"0ee2edd8c4efbba03c02900c75336a472010599c97aeb2e539c4821c5e31ee6c","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-cws_release_prod_v16777272_89b37a10-683a-4183-9ed7-4973bc34994b.ota"},{"fw_image_type":8705,"fw_type":2,"fw_sha3_256":"394c0836a5e77883c2710c6c0a1f1a33d34583f4502300aab3fc471bb6aba03f","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-ws-e14_release_prod_v587757105_963ac72a-97be-4f7e-b12c-46759bb91d6e.ota"},{"fw_image_type":8706,"fw_type":2,"fw_sha3_256":"9318a9cfbb6aef7bc50586d9b16a3a39b470fa6c88e4fd05f5f77834f9ceeceb","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-ws-1000lm_release_prod_v587814449_2b13625e-17a8-40d9-bad3-47a9a96ab002.ota"},{"fw_image_type":8704,"fw_type":2,"fw_sha3_256":"b3fec5e75bb4c529b84bd2e92a40146d992672d6afe943aef8349ccfaa0695cd","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-ws_release_prod_v50331681_969bee21-eca6-4f10-a4c0-9685c0cd5d52.ota"},{"fw_image_type":4365,"fw_type":2,"fw_sha3_256":"2512a74ba20f445494e75f9f05c4debf051cff14496af2abad7a2074d635b6d3","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-lds-plugin-unit_release_prod_v65538_b29c3768-a102-4414-a84d-405c99654e74.ota"},{"fw_image_type":4557,"fw_type":2,"fw_sha3_256":"55a69e55d4edff076e0702dd260011f26a32352404a3227afa4f274e3c9958a2","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/rodret-dimmer-soc_release_prod_v16777303_0a78457a-950c-4903-bfd1-67902aa66cf9.ota"},{"fw_image_type":4364,"fw_type":2,"fw_sha3_256":"80c8b788ff094b3943d8b74bebefba8b73df2c526602bca0da126e777dca3f73","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-lds-starkvind_release_prod_v69633_2044addf-0a35-4845-b851-74df04ab3a76.ota"},{"fw_image_type":4549,"fw_type":2,"fw_sha3_256":"499f08c42fdcd5019c8d48b3a17c2f3406cdc97b4bfb41db064a1487e695596c","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-onoff-controller_release_prod_v604241926_3c2e5569-667c-49ed-a286-78a0e031935d.ota"},{"fw_image_type":16644,"fw_type":2,"fw_sha3_256":"f546464079b5e3c03862acfa03dcba693e03155681cd07c67c3f0b12558d7987","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/zingo-sigma-driver-silverglans-ww_release_prod_v65569_73466dc4-1320-4242-add6-717432538a77.ota"},{"fw_image_type":10241,"fw_type":2,"fw_sha3_256":"e718abba1958fe7e00297a1c143033d8505b26b67d2668faf08bc137cc27ba00","fw_binary_url":"https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-cws-zll_release_prod_v587753009_ec8b1193-0fa2-440e-b921-2412a8688b74.ota"}]zigpy-0.80.1/tests/ota/files/ikea_version_info_old.json000066400000000000000000000304331501451476000232210ustar00rootroot00000000000000[{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10005777-TRADFRI-control-outlet-2.3.089.ota.ota.signed","fw_file_version_LSB":38449,"fw_file_version_MSB":8968,"fw_filesize":209136,"fw_image_type":4353,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10005778-tradfri_onoff_controller-24.4.6-prod.ota.ota.signed","fw_file_version_LSB":6,"fw_file_version_MSB":9220,"fw_filesize":205560,"fw_image_type":4549,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10032198-2.2-TRADFRI-gateway-1.21.31.p.elf.sig.ota.signed","fw_filesize":891248,"fw_hotfix_version":31,"fw_major_version":1,"fw_minor_version":21,"fw_req_hotfix_version":32,"fw_req_major_version":9,"fw_req_minor_version":9,"fw_type":0,"fw_update_prio":5,"fw_weblink_relnote":"https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html"},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10035514-2.1-TRADFRI-bulb-ws-2.3.087.ota.ota.signed","fw_file_version_LSB":30257,"fw_file_version_MSB":8968,"fw_filesize":215596,"fw_image_type":8705,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10035515-TRADFRI-bulb-cws-2.3.093.ota.ota.signed","fw_file_version_LSB":13873,"fw_file_version_MSB":8969,"fw_filesize":227500,"fw_image_type":10243,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10035515-TRADFRI-bulb-cws-step-2.3.086.ota.ota.signed","fw_file_version_LSB":26161,"fw_file_version_MSB":8968,"fw_filesize":226244,"fw_image_type":10241,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10035534-2.1-TRADFRI-bulb-ws-gu10-2.3.087.ota.ota.signed","fw_file_version_LSB":30257,"fw_file_version_MSB":8968,"fw_filesize":215340,"fw_image_type":8707,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10037585-tradfri_connected_blind-24.4.13-prod.ota.ota.signed","fw_file_version_LSB":19,"fw_file_version_MSB":9220,"fw_filesize":219816,"fw_image_type":4487,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10037603-3.1-TRADFRI-signal-repeater-2.3.086.ota.ota.signed","fw_file_version_LSB":26161,"fw_file_version_MSB":8968,"fw_filesize":197052,"fw_image_type":4354,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10038562-2.1-TRADFRI-sy5882-bulb-ws-2.3.095.ota.ota.signed","fw_file_version_LSB":22065,"fw_file_version_MSB":8969,"fw_filesize":214716,"fw_image_type":16900,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10039874-1.0-TRADFRI-motion-sensor-2-2.0.022.ota.ota.signed","fw_file_version_LSB":9763,"fw_file_version_MSB":8194,"fw_filesize":186814,"fw_image_type":4552,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10046695-1.1-TRADFRI-light-unified-w-2.3.093.ota.ota.signed","fw_file_version_LSB":13873,"fw_file_version_MSB":8969,"fw_filesize":211572,"fw_image_type":16643,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10047227-1.2-TRADFRI-cv-cct-unified-2.3.087.ota.ota.signed","fw_file_version_LSB":30257,"fw_file_version_MSB":8968,"fw_filesize":215884,"fw_image_type":16902,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10050896-TRADFRI-sy5882-unified-2.3.087.ota.ota.signed","fw_file_version_LSB":30257,"fw_file_version_MSB":8968,"fw_filesize":215656,"fw_image_type":16901,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10054470-tradfri_shortcut_button-24.4.6-prod.ota.ota.signed","fw_file_version_LSB":6,"fw_file_version_MSB":9220,"fw_filesize":205004,"fw_image_type":4550,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10057275-mgm210l_light_cws_cv_rgbw_1.0.021.ota.ota.signed","fw_file_version_LSB":5717,"fw_file_version_MSB":4098,"fw_filesize":228582,"fw_image_type":10242,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10075356-zingo_sigma_driver_silverglans_ww-1.0.021.ota.ota.signed","fw_file_version_LSB":33,"fw_file_version_MSB":1,"fw_filesize":230566,"fw_image_type":16644,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10076691-zingo_lds_bulb_hwpwm_ww-0x2102-1.1.006-prod.ota.ota.signed","fw_file_version_LSB":4102,"fw_file_version_MSB":1,"fw_filesize":236274,"fw_image_type":8450,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10076692-zingo_lds_bulb_hwpwmcs_ws-1.0.012.ota.ota.signed","fw_file_version_LSB":18,"fw_file_version_MSB":1,"fw_filesize":236794,"fw_image_type":8709,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10077684-zingo_ikea_driver_hwpwm_ww-0x4109-1.0.4-prod.ota.ota.signed","fw_file_version_LSB":4,"fw_file_version_MSB":256,"fw_filesize":257262,"fw_image_type":16649,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10078247-zingo_lds_plugin_unit-1.0.002.ota.ota.signed","fw_file_version_LSB":2,"fw_file_version_MSB":1,"fw_filesize":220362,"fw_image_type":4365,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10080506-zingo_kt_bulb_hwpwmcs_ws-1.1.003.ota.ota.signed","fw_file_version_LSB":4099,"fw_file_version_MSB":1,"fw_filesize":259294,"fw_image_type":8708,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10082261-zingo_lds_starkvind-1.1.001.ota.ota.signed","fw_file_version_LSB":4097,"fw_file_version_MSB":1,"fw_filesize":231282,"fw_image_type":4364,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10082264-zingo_lds_stoftmoln-1.0.006.ota.ota.signed","fw_file_version_LSB":6,"fw_file_version_MSB":1,"fw_filesize":212454,"fw_image_type":16645,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10115366-zingo_lds_bulb_jetstrom_ws-0x1105-2.4.5-prod.ota.ota.signed","fw_file_version_LSB":5,"fw_file_version_MSB":516,"fw_filesize":249490,"fw_image_type":4357,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159495-TRADFRI-transformer-2.3.086.ota.ota.signed","fw_file_version_LSB":26161,"fw_file_version_MSB":8968,"fw_filesize":215340,"fw_image_type":16641,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159695-2.1-TRADFRI-bulb-ws-1000lm-2.3.095.ota.ota.signed","fw_file_version_LSB":22065,"fw_file_version_MSB":8969,"fw_filesize":216620,"fw_image_type":8706,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159696-TRADFRI-bulb-w-1000lm-2.3.094.ota.ota.signed","fw_file_version_LSB":17969,"fw_file_version_MSB":8969,"fw_filesize":207340,"fw_image_type":8449,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159697-TRADFRI-driver-hp-2.3.087.ota.ota.signed","fw_file_version_LSB":30257,"fw_file_version_MSB":8968,"fw_filesize":215852,"fw_image_type":16898,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159698-TRADFRI-driver-lp-2.3.087.ota.ota.signed","fw_file_version_LSB":30257,"fw_file_version_MSB":8968,"fw_filesize":215596,"fw_image_type":16897,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159699-5.1-TRADFRI-remote-control-24.4.5.ota.ota.signed","fw_file_version_LSB":5,"fw_file_version_MSB":9220,"fw_filesize":207084,"fw_image_type":4545,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159700-TRADFRI-motion-sensor-24.4.5.ota.ota.signed","fw_file_version_LSB":5,"fw_file_version_MSB":9220,"fw_filesize":214316,"fw_image_type":4548,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/159701-2.1-TRADFRI-wireless-dimmer-2.3.028.ota.ota.signed","fw_file_version_LSB":34353,"fw_file_version_MSB":8962,"fw_filesize":179390,"fw_image_type":4546,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/190579-ncp_572_445.ebl.ota.ota.signed","fw_build_version":445,"fw_file_version_LSB":445,"fw_file_version_MSB":5720,"fw_filesize":169662,"fw_hotfix_version":2,"fw_image_type":2,"fw_major_version":5,"fw_manufacturer_id":4476,"fw_minor_version":7,"fw_type":1},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/191100-4.1-TRADFRI-sy5882-driver-ws-2.3.091.ota.ota.signed","fw_file_version_LSB":5681,"fw_file_version_MSB":8969,"fw_filesize":214572,"fw_image_type":16899,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/motionsensor_lds_mg21a-0x1938-1.0.64-prod.ota.ota.signed","fw_file_version_LSB":100,"fw_file_version_MSB":256,"fw_filesize":267650,"fw_image_type":6456,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/rodret_dimmer_soc-0x11cd-1.0.57-prod.ota.ota.signed","fw_file_version_LSB":87,"fw_file_version_MSB":256,"fw_filesize":268502,"fw_image_type":4557,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/rodret_shortcut_soc-0x3b08-1.0.21-prod.ota.ota.signed","fw_file_version_LSB":33,"fw_file_version_MSB":256,"fw_filesize":269430,"fw_image_type":15112,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/styrbar_dimmer_zingo-0x11cb-2.4.11-prod.ota.ota.signed","fw_file_version_LSB":17,"fw_file_version_MSB":516,"fw_filesize":269638,"fw_image_type":4555,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/symfonisk_sound_remote_zingo-0x110e-1.0.35-prod.ota.ota.signed","fw_file_version_LSB":53,"fw_file_version_MSB":256,"fw_filesize":305574,"fw_image_type":4366,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/tradfri_dimmer-24.4.5-prod.ota.ota.signed","fw_file_version_LSB":5,"fw_file_version_MSB":9220,"fw_filesize":214692,"fw_image_type":4554,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/tretakt_smart_plug_soc-0x1100-2.4.25-prod.ota.ota.signed","fw_file_version_LSB":37,"fw_file_version_MSB":516,"fw_filesize":280318,"fw_image_type":4352,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/zingo_jetstrom_cws-0xc749-1.0.34-prod.ota.ota.signed","fw_file_version_LSB":52,"fw_file_version_MSB":256,"fw_filesize":329014,"fw_image_type":51017,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/zingo_ws-0x2200-3.0.10-prod.ota.ota.signed","fw_file_version_LSB":16,"fw_file_version_MSB":768,"fw_filesize":307830,"fw_image_type":8704,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/zingo_ws_soc-0x2206-3.0.10-prod.ota.ota.signed","fw_file_version_LSB":16,"fw_file_version_MSB":768,"fw_filesize":306074,"fw_image_type":8710,"fw_manufacturer_id":4476,"fw_type":2},{"fw_binary_url":"http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/zingo_ww-0x2100-1.0.36-prod.ota.ota.signed","fw_file_version_LSB":54,"fw_file_version_MSB":256,"fw_filesize":287290,"fw_image_type":8448,"fw_manufacturer_id":4476,"fw_type":2}]zigpy-0.80.1/tests/ota/files/ikea_version_info_old_test.json000066400000000000000000000212111501451476000242520ustar00rootroot00000000000000[ { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10032198-2.1-TRADFRI-gateway-1.11.47.elf.sig.ota.signed", "fw_filesize": 802816, "fw_hotfix_version": 47, "fw_major_version": 1, "fw_minor_version": 11, "fw_req_hotfix_version": 48, "fw_req_major_version": 9, "fw_req_minor_version": 9, "fw_type": 0, "fw_update_prio": 5, "fw_weblink_relnote": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10047227-1.2-TRADFRI-cv-cct-unified-2.3.050.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8965, "fw_filesize": 207670, "fw_image_type": 16902, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10038562-2.1-TRADFRI-sy5882-bulb-ws-2.0.029.ota.ota.signed", "fw_file_version_LSB": 38435, "fw_file_version_MSB": 8194, "fw_filesize": 208242, "fw_image_type": 16900, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10037603-3.1-TRADFRI-signal-repeater-2.2.005.ota.ota.signed", "fw_file_version_LSB": 22065, "fw_file_version_MSB": 8704, "fw_filesize": 189822, "fw_image_type": 4354, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159696-TRADFRI-bulb-w-1000lm-2.3.023.ota.ota.signed", "fw_file_version_LSB": 13873, "fw_file_version_MSB": 8962, "fw_filesize": 200446, "fw_image_type": 8449, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10040611-3.2-TRADFRI-sy5882-unified-2.3.050.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8965, "fw_filesize": 207438, "fw_image_type": 16901, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10005778-10.1-TRADFRI-onoff-shortcut-control-2.2.010.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8705, "fw_filesize": 178838, "fw_image_type": 4549, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10043101-3.1-TRADFRI-dimmer-2.1.024.ota.ota.signed", "fw_file_version_LSB": 17969, "fw_file_version_MSB": 8450, "fw_filesize": 179886, "fw_image_type": 4554, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10046695-1.1-TRADFRI-light-unified-w-2.3.050.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8965, "fw_filesize": 203378, "fw_image_type": 16643, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159697-TRADFRI-driver-hp-1.2.224.ota.ota.signed", "fw_file_version_LSB": 17779, "fw_file_version_MSB": 4642, "fw_filesize": 174206, "fw_image_type": 16898, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10035534-2.1-TRADFRI-bulb-ws-gu10-2.3.050.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8965, "fw_filesize": 207422, "fw_image_type": 8707, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159695-2.1-TRADFRI-bulb-ws-1000lm-2.3.050.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8965, "fw_filesize": 208254, "fw_image_type": 8706, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/191100-4.1-TRADFRI-sy5882-driver-ws-2.0.029.ota.ota.signed", "fw_file_version_LSB": 38435, "fw_file_version_MSB": 8194, "fw_filesize": 208294, "fw_image_type": 16899, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159700-TRADFRI-motion-sensor-1.2.214.ota.ota.signed", "fw_file_version_LSB": 17778, "fw_file_version_MSB": 4641, "fw_filesize": 157822, "fw_image_type": 4548, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159495-TRADFRI-transformer-1.2.245.ota.ota.signed", "fw_file_version_LSB": 21874, "fw_file_version_MSB": 4644, "fw_filesize": 181118, "fw_image_type": 16641, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10035515-TRADFRI-bulb-cws-1.3.013.ota.ota.signed", "fw_file_version_LSB": 13682, "fw_file_version_MSB": 4865, "fw_filesize": 179326, "fw_image_type": 10241, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159699-5.1-TRADFRI-remote-control-2.3.014.ota.ota.signed", "fw_file_version_LSB": 17969, "fw_file_version_MSB": 8961, "fw_filesize": 182590, "fw_image_type": 4545, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159701-2.1-TRADFRI-wireless-dimmer-2.3.028.ota.ota.signed", "fw_file_version_LSB": 34353, "fw_file_version_MSB": 8962, "fw_filesize": 179390, "fw_image_type": 4546, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/190579-ncp572b444.ebl.ota.ota.signed", "fw_build_version": 444, "fw_file_version_LSB": 444, "fw_file_version_MSB": 5720, "fw_filesize": 166270, "fw_hotfix_version": 2, "fw_image_type": 2, "fw_major_version": 5, "fw_manufacturer_id": 4476, "fw_minor_version": 7, "fw_type": 1 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/159698-TRADFRI-driver-lp-1.2.224.ota.ota.signed", "fw_file_version_LSB": 17779, "fw_file_version_MSB": 4642, "fw_filesize": 174206, "fw_image_type": 16897, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10005777-4.1-TRADFRI-control-outlet-2.0.024.ota.ota.signed", "fw_file_version_LSB": 17955, "fw_file_version_MSB": 8194, "fw_filesize": 201030, "fw_image_type": 4353, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10039874-1.0-TRADFRI-motion-sensor-2-2.0.022.ota.ota.signed", "fw_file_version_LSB": 9763, "fw_file_version_MSB": 8194, "fw_filesize": 186814, "fw_image_type": 4552, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10035514-2.1-TRADFRI-bulb-ws-2.3.050.ota.ota.signed", "fw_file_version_LSB": 1585, "fw_file_version_MSB": 8965, "fw_filesize": 207486, "fw_image_type": 8705, "fw_manufacturer_id": 4476, "fw_type": 2 }, { "fw_binary_url": "http://fw.test.ota.homesmart.ikea.net/global/GW1.0/01.11.047/bin/10037585-5.1-TRADFRI-connected-blind-2.2.009.ota.ota.signed", "fw_file_version_LSB": 38449, "fw_file_version_MSB": 8704, "fw_filesize": 186942, "fw_image_type": 4487, "fw_manufacturer_id": 4476, "fw_type": 2 } ]zigpy-0.80.1/tests/ota/files/inovelli_firmware-zha.json000066400000000000000000000060201501451476000231620ustar00rootroot00000000000000{ "VZM31-SN": [ { "version": "0000000B", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/1.11/VZM31-SN_1.11.ota", "manufacturer_id": 4655, "image_type": 257 }, { "version": "16842764", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/1.12/VZM31-SN_1.12.ota", "manufacturer_id": 4655, "image_type": 257 }, { "version": "16843021", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/1.13/VZM31-SN_1.13.ota", "manufacturer_id": 4655, "image_type": 257 }, { "version": "16908805", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/2.05/VZM31-SN_2.05.ota", "manufacturer_id": 4655, "image_type": 257 }, { "version": "16908806", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/2.06/VZM31-SN_2.06.ota", "manufacturer_id": 4655, "image_type": 257 }, { "version": "16908807", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/2.07/VZM31-SN_2.07.ota", "manufacturer_id": 4655, "image_type": 257 }, { "version": "16908808", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM31-SN/Beta/2.08/VZM31-SN_2.08.ota", "manufacturer_id": 4655, "image_type": 257 } ], "VZM35-SN": [ { "version": "33685506", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM35-SN/Beta/0.02/VZM35-SN_0.02.ota", "manufacturer_id": 4655, "image_type": 258 }, { "version": "33685760", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM35-SN/1.00/VZM35-SN_1.00.ota", "manufacturer_id": 4655, "image_type": 258 }, { "version": "33685762", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM35-SN/1.02/VZM35-SN_1.02.ota", "manufacturer_id": 4655, "image_type": 258 }, { "version": "33685764", "channel": "beta", "firmware": "https://files.inovelli.com/firmware/VZM35-SN/1.04/VZM35-SN_1.04.ota", "manufacturer_id": 4655, "image_type": 258 } ], "VZM36": [ { "version": "67174402", "channel": "beta", "firmware": "https://inov.li/IRbxhx1646F/VZM36_0.02.ota", "manufacturer_id": 4655, "image_type": 1025 }, { "version": "67174403", "channel": "beta", "firmware": "https://inov.li/IRbxhx1646F/VZM36_0.03.ota", "manufacturer_id": 4655, "image_type": 1025 }, { "version": "67174404", "channel": "beta", "firmware": "https://inov.li/IRbxhx1646F/VZM36_0.04.ota", "manufacturer_id": 4655, "image_type": 1025 } ] } zigpy-0.80.1/tests/ota/files/ledvance_firmwares.json000066400000000000000000003177601501451476000225450ustar00rootroot00000000000000{"firmwares":[{"blob":null,"identity":{"company":4489,"product":25,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"ffe0298312f63fa0be5e568886e419d714146652ff4747a8afed2de221ad43ee","name":"A19_RGBW_IMG0019_00102428-encrypted.ota","productName":"A19 RGBW","fullName":"A19 RGBW/00102428/A19_RGBW_IMG0019_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:36:28","salesRegion":"us","length":180052},{"blob":null,"identity":{"company":4489,"product":13,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"fa5ab550bde3e8c877cf40aa460fc9836405a7843df040e75bfdb2fb582c22fb","name":"A19_TW_10_year_IMG000D_00102428-encrypted.ota","productName":"A19 TW 10 year","fullName":"A19 TW 10 year/00102428/A19_TW_10_year_IMG000D_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:42:50","salesRegion":"us","length":170800},{"blob":null,"identity":{"company":4489,"product":12,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"0c46f738bb173478d2e018558547437838d89d723834d551677a7eaf27d89e5c","name":"A19_W_10_year_IMG000C_00102428-encrypted.ota","productName":"A19 W 10 year","fullName":"A19 W 10 year/00102428/A19_W_10_year_IMG000C_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:44:04","salesRegion":"us","length":170140},{"blob":null,"identity":{"company":4489,"product":205,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"15fa80b3873c3602f6e9601fc2d958a07a0c247238a92bc899bcb877ab4c5101","name":"DIM-A60_DIM_T-0x00CD-0x03203660.OTA","productName":"A60 DIM T","fullName":"A60 DIM T/03203660/DIM-A60_DIM_T-0x00CD-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:05:02","salesRegion":"eu","length":188384},{"blob":null,"identity":{"company":4489,"product":61,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"1c243996b76c27c3f13b277e38d3748199e2ab01733a48276dc3bd74ebf86679","name":"A60_DIM_Z3_IM003D_01056400-encrypted_202129091152_withoutMF.ota","productName":"A60 DIM Z3","fullName":"A60 DIM Z3/01056400/A60_DIM_Z3_IM003D_01056400-encrypted_202129091152_withoutMF.ota","extension":".ota","released":"2021-10-21T05:26:51","salesRegion":"eu","length":185112},{"blob":null,"identity":{"company":4489,"product":61,"version":{"major":1,"minor":3,"build":100,"revision":0}},"releaseNotes":"1.Support for turn on/off fading time configurations\r\n2.Support for ZLO commands\r\n3.OTA improvements, rollback protection enabled","shA256":"7e25053d47bccd75c215707600265fa90d5fecdd1a55a8668a7bef3e4d7e3ddc","name":"A60_DIM_Z3_IM003D_01036400-encrypted_202110060418_withoutMF.ota","productName":"A60 DIM Z3","fullName":"A60 DIM Z3/01036400/A60_DIM_Z3_IM003D_01036400-encrypted_202110060418_withoutMF.ota","extension":".ota","released":"2021-07-14T08:51:24","salesRegion":"eu","length":183392},{"blob":null,"identity":{"company":4489,"product":61,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"19a44c1c048192b05038229628f3807df9d86bdb1f8bba0c432aac027ece0887","name":"A60_DIM_Z3_IM003D_00103101-encrypted_11_20_2018_Tue_122925_01_withoutMF.ota","productName":"A60 DIM Z3","fullName":"A60 DIM Z3/00103101/A60_DIM_Z3_IM003D_00103101-encrypted_11_20_2018_Tue_122925_01_withoutMF.ota","extension":".ota","released":"2019-03-22T08:08:57","salesRegion":"eu","length":182876},{"blob":null,"identity":{"company":4489,"product":208,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"f53af0b255d589c795913081560de211b162b491dcb5ea865546dec15f433a78","name":"DIM-A60_FIL_DIM_T-0x00D0-0x03203660.OTA","productName":"A60 FIL DIM T","fullName":"A60 FIL DIM T/03203660/DIM-A60_FIL_DIM_T-0x00D0-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:06:46","salesRegion":"eu","length":188416},{"blob":null,"identity":{"company":4489,"product":138,"version":{"major":2,"minor":3,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported \r\n- On with time off Command supported ","shA256":"d93a15155f2cd4621bb6904d73f7ba884aace4ee9f0b97aecccc22357f5be26e","name":"RGBW-A60_RGBW_Value_II-0x1189-0x008A-0x02036550-MF_DIS-20201104140534.ota","productName":"A60 RGBW Value II","fullName":"A60 RGBW Value II/02036550/RGBW-A60_RGBW_Value_II-0x1189-0x008A-0x02036550-MF_DIS-20201104140534.ota","extension":".ota","released":"2020-11-13T05:37:28","salesRegion":"eu","length":210550},{"blob":null,"identity":{"company":4489,"product":139,"version":{"major":2,"minor":3,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported \r\n- On with time off Command supported ","shA256":"9bf800de608f53968e4710b883fc56ab72d28957f4a3b0da21422f9fa8daba4a","name":"TW-A60_TW_Value_II-0x1189-0x008B-0x02036550-MF_DIS-20201104114113.ota","productName":"A60 TW Value II","fullName":"A60 TW Value II/02036550/TW-A60_TW_Value_II-0x1189-0x008B-0x02036550-MF_DIS-20201104114113.ota","extension":".ota","released":"2020-11-13T06:26:45","salesRegion":"eu","length":196574},{"blob":null,"identity":{"company":4489,"product":60,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"95dedba17bc113be00ae8f410ce9ec93cd608cea1f192060c1fa19875f73a6d7","name":"A60_TW_Z3_IM003C_01056400-encrypted_202129091149_withoutMF.ota","productName":"A60 TW Z3","fullName":"A60 TW Z3/01056400/A60_TW_Z3_IM003C_01056400-encrypted_202129091149_withoutMF.ota","extension":".ota","released":"2021-10-21T05:27:33","salesRegion":"eu","length":185972},{"blob":null,"identity":{"company":4489,"product":60,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"43c3daffcb761170e0fe66672067869acf1ff9c017038ace2c80e6a1e3bd7639","name":"A60_TW_Z3_IM003C_00103101-encrypted_11_20_2018_Tue_103138_93_withoutMF.ota","productName":"A60 TW Z3","fullName":"A60 TW Z3/00103101/A60_TW_Z3_IM003C_00103101-encrypted_11_20_2018_Tue_103138_93_withoutMF.ota","extension":".ota","released":"2019-03-22T08:09:43","salesRegion":"eu","length":183628},{"blob":null,"identity":{"company":4489,"product":138,"version":{"major":2,"minor":20,"build":101,"revision":80}},"releaseNotes":"1.ZLO gap fix\r\n2.RGBW color calibration\r\n3.Disable touch-link function","shA256":"30f54c9e0e12c01db2a05c39e12860a965f39583b4b4bd2cf8985ae061bbcf26","name":"RGBW-A60_RGBW_Value_II-0x1189-0x008A-0x02146550-MF_DIS-20211203083445-3221010102432.ota","productName":"A60 RGBW Value II","fullName":"A60_RGBW_Value_II/02146550/RGBW-A60_RGBW_Value_II-0x1189-0x008A-0x02146550-MF_DIS-20211203083445-3221010102432.ota","extension":".ota","released":"2022-03-02T07:50:54","salesRegion":"eu","length":213058},{"blob":null,"identity":{"company":4489,"product":160,"version":{"major":2,"minor":20,"build":101,"revision":80}},"releaseNotes":"1.ZLO gap fix\r\n2.RGBW color calibration\r\n3.Disable touch-link function","shA256":"3cfe3293cba4eef0a95b59e56491d103737d51e3f9a41d47281e084f811d69aa","name":"RGBW-A60S_RGBW-0x1189-0x00A0-0x02146550-MF_DIS-20211203082926-3221010102432.ota","productName":"A60S RGBW","fullName":"A60S_RGBW/02146550/RGBW-A60S_RGBW-0x1189-0x00A0-0x02146550-MF_DIS-20211203082926-3221010102432.ota","extension":".ota","released":"2022-03-02T07:53:12","salesRegion":"eu","length":213154},{"blob":null,"identity":{"company":4489,"product":162,"version":{"major":2,"minor":19,"build":101,"revision":80}},"releaseNotes":"1. ZLO gap fixed.\r\n2. V02136550 is production firmware.","shA256":"4be1fba85364f3df5a5f4c2b9e8df9098a731f0f8f0c9989b36ef568e9bb8031","name":"TW-A60S_TW-0x1189-0x00A2-0x02136550-MF_DIS-20211011050926-3221010102432.ota","productName":"A60S TW","fullName":"A60S_TW/02136550/TW-A60S_TW-0x1189-0x00A2-0x02136550-MF_DIS-20211011050926-3221010102432.ota","extension":".ota","released":"2023-01-19T07:42:19","salesRegion":"eu","length":198254},{"blob":null,"identity":{"company":4489,"product":184,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"b16cbbe97e118416111959f7dd839a68f8d216ad23b5a661b0f2d8e17e501117","name":"DIM-B40_DIM_T-0x00B8-0x03203660.OTA","productName":"B40 DIM T","fullName":"B40 DIM T/03203660/DIM-B40_DIM_T-0x00B8-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:07:25","salesRegion":"eu","length":188384},{"blob":null,"identity":{"company":4489,"product":52,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"703487accb7f301a258ead8efb0834dded18cad16c0c0f05e31cc64753bd3d00","name":"B40_DIM_Z3_IM0034_01056400-encrypted_202129091146_withoutMF.ota","productName":"B40 DIM Z3","fullName":"B40 DIM Z3/01056400/B40_DIM_Z3_IM0034_01056400-encrypted_202129091146_withoutMF.ota","extension":".ota","released":"2021-10-21T05:28:28","salesRegion":"eu","length":185112},{"blob":null,"identity":{"company":4489,"product":52,"version":{"major":1,"minor":3,"build":100,"revision":0}},"releaseNotes":"1.Support for turn on/off fading time configurations\r\n2.Support for ZLO commands\r\n3.OTA improvements, rollback protection enabled","shA256":"15f85ef4c2fc2a717a28079a933ce2a2ce9204e61f6966b8d26316385276644d","name":"B40_DIM_Z3_IM0034_01036400-encrypted_202110060421_withoutMF.ota","productName":"B40 DIM Z3","fullName":"B40 DIM Z3/01036400/B40_DIM_Z3_IM0034_01036400-encrypted_202110060421_withoutMF.ota","extension":".ota","released":"2021-07-14T08:55:19","salesRegion":"eu","length":183392},{"blob":null,"identity":{"company":4489,"product":52,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"b47c8647372197fb4a4788da6a6d6559e9e7302c9258e9e976565d13d52d5c00","name":"B40_DIM_Z3_IM0034_00103101-encrypted_11_26_2018_Mon_174522_20_withoutMF.ota","productName":"B40 DIM Z3","fullName":"B40 DIM Z3/00103101/B40_DIM_Z3_IM0034_00103101-encrypted_11_26_2018_Mon_174522_20_withoutMF.ota","extension":".ota","released":"2019-03-22T08:10:20","salesRegion":"eu","length":182876},{"blob":null,"identity":{"company":4489,"product":140,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported \r\n- On with time off Command supported","shA256":"f2f4278ddd8c63796dbf5f456fa38ef487eb56e0358c874e0069397f4a04636b","name":"TW-B40_TW_Value-0x1189-0x008C-0x02056550-MF_DIS-20201207183007.ota","productName":"B40 TW Value","fullName":"B40 TW Value/02056550/TW-B40_TW_Value-0x1189-0x008C-0x02056550-MF_DIS-20201207183007.ota","extension":".ota","released":"2020-12-17T05:18:10","salesRegion":"eu","length":196574},{"blob":null,"identity":{"company":4489,"product":51,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"8c1f3154b1b7d3f7d0b78b8b5b69abde36aa73cf43af53a591e6652f7b8e6522","name":"B40_TW_Z3_IM0033_01056400-encrypted_202129091140_withoutMF.ota","productName":"B40 TW Z3","fullName":"B40 TW Z3/01056400/B40_TW_Z3_IM0033_01056400-encrypted_202129091140_withoutMF.ota","extension":".ota","released":"2021-10-21T05:29:08","salesRegion":"eu","length":185968},{"blob":null,"identity":{"company":4489,"product":51,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"6498b9375a37f66197f7034206c99e11b1b36759cc97d6c0e0ab6c8d6a0f4283","name":"B40_TW_Z3_IM0033_00103101-encrypted_11_23_2018_Fri_160706_13_withoutMF.ota","productName":"B40 TW Z3","fullName":"B40 TW Z3/00103101/B40_TW_Z3_IM0033_00103101-encrypted_11_23_2018_Fri_160706_13_withoutMF.ota","extension":".ota","released":"2019-03-22T08:24:49","salesRegion":"eu","length":183624},{"blob":null,"identity":{"company":4489,"product":163,"version":{"major":2,"minor":19,"build":101,"revision":80}},"releaseNotes":"1. ZLO gap fixed.\r\n2. V02136550 is production firmware.","shA256":"bbe9ed0002c9f7669d1bc08063f6b94196c2a08b838f6a591ecf587146ea0523","name":"TW-B40S_TW-0x1189-0x00A3-0x02136550-MF_DIS-20211011051433-3221010102432.ota","productName":"B40S TW","fullName":"B40S_TW/02136550/TW-B40S_TW-0x1189-0x00A3-0x02136550-MF_DIS-20211011051433-3221010102432.ota","extension":".ota","released":"2023-01-19T07:44:31","salesRegion":"eu","length":198246},{"blob":null,"identity":{"company":4489,"product":27,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"fdb1bd99559e4fc9f67a5ffb0a9db34a3e2aa2f801a43717886759ce49de4108","name":"BR30_RGBW_IMG001B_00102428-encrypted.ota","productName":"BR30 RGBW","fullName":"BR30 RGBW/00102428/BR30_RGBW_IMG001B_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:44:56","salesRegion":"us","length":179100},{"blob":null,"identity":{"company":4489,"product":26,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"a41d7cc0583607f4c4bb3cb773ee798de28c4c80b95bf54489a9b503471e0fe8","name":"BR30_TW_IMG001A_00102428-encrypted.ota","productName":"BR30 TW","fullName":"BR30 TW/00102428/BR30_TW_IMG001A_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:45:45","salesRegion":"us","length":170776},{"blob":null,"identity":{"company":4489,"product":15,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"b7a27f56dc661c715eaeea4645b658a9837bc3013eb2c4eda1a88522aa9ca768","name":"BR30_W_10_year_IMG000F_00102428-encrypted.ota","productName":"BR30 W","fullName":"BR30 W/00102428/BR30_W_10_year_IMG000F_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:46:22","salesRegion":"us","length":170120},{"blob":null,"identity":{"company":4364,"product":107,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"79306151743191f66c774eef83a5857aa953760af5633249d543515685a08821","name":"ZLL_MK_0x01020510_CEILING_TW_OSRAM.ota","productName":"CEILING TW OSRAM","fullName":"CEILING TW OSRAM/01020510/ZLL_MK_0x01020510_CEILING_TW_OSRAM.ota","extension":".ota","released":"2019-03-13T09:41:07","salesRegion":null,"length":132672},{"blob":null,"identity":{"company":4364,"product":98,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"3e0dda44c5d78d84b9378dcd4272eb9d0819ca55b6d43f0563503893cc3ced11","name":"ZLL_MK_0x01020510_CLA60_RGBW_OSRAM.ota","productName":"CLA60 RGBW OSRAM","fullName":"CLA60 RGBW OSRAM/01020510/ZLL_MK_0x01020510_CLA60_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-13T09:42:55","salesRegion":null,"length":142972},{"blob":null,"identity":{"company":4489,"product":17,"version":{"major":1,"minor":6,"build":100,"revision":0}},"releaseNotes":"1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement ","shA256":"3a7ff7cc174fe2f22e880f13ad335245531bbf111eeebb14f4b879cbab9db5d3","name":"CLA60_RGBW_Z3_IM0011_01066400-encrypted_202126110358_withoutMF.ota","productName":"CLA60 RGBW Z3","fullName":"CLA60 RGBW Z3/01066400/CLA60_RGBW_Z3_IM0011_01066400-encrypted_202126110358_withoutMF.ota","extension":".ota","released":"2022-04-15T05:17:56","salesRegion":"eu","length":193900},{"blob":null,"identity":{"company":4489,"product":17,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"db8f41fc00e9f4548610cd735d2a28d8ab38f94a4851f126320f89058297d38d","name":"CLA60_RGBW_Z3_IM0011_00103101-encrypted_11_27_2018_Tue_133608_15_withoutMF.ota","productName":"CLA60 RGBW Z3","fullName":"CLA60 RGBW Z3/00103101/CLA60_RGBW_Z3_IM0011_00103101-encrypted_11_27_2018_Tue_133608_15_withoutMF.ota","extension":".ota","released":"2019-03-22T08:13:52","salesRegion":"eu","length":191128},{"blob":null,"identity":{"company":4364,"product":99,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"6f10f07c341eac14e0791c17ffef453182fe3a5fff81933b8387579388375195","name":"ZLL_MK_0x01020510_CLA60_TW_OSRAM.ota","productName":"CLA60 TW OSRAM","fullName":"CLA60 TW OSRAM/01020510/ZLL_MK_0x01020510_CLA60_TW_OSRAM.ota","extension":".ota","released":"2019-03-13T09:44:08","salesRegion":null,"length":132672},{"blob":null,"identity":{"company":4364,"product":8,"version":{"major":1,"minor":2,"build":5,"revision":9}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"65a5c2a55429aae1a89e9db20dcc1a647d34cc686526bd36509cda12afdcaf2b","name":"ZLL_MK_0x01020509_CLA60_TW.ota","productName":"CLA60 TW","fullName":"CLA60 TW/01020509/ZLL_MK_0x01020509_CLA60_TW.ota","extension":".ota","released":"2019-03-13T09:40:11","salesRegion":null,"length":133444},{"blob":null,"identity":{"company":4364,"product":19,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"173e9e77434118ebb5a0d83032063cf594cc201b42277a90e080b196497cf442","name":"ZLL_MK_0x01020510_CLA60_W_CLEAR.ota","productName":"CLA60 W CLEAR","fullName":"CLA60 W CLEAR/01020510/ZLL_MK_0x01020510_CLA60_W_CLEAR.ota","extension":".ota","released":"2019-03-13T09:45:06","salesRegion":null,"length":123884},{"blob":null,"identity":{"company":4364,"product":6,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"cebff1b5578ab897151eb654bbd28547c9213739ad257c576105611bf087ac84","name":"ZLL_MK_0x01020510_CLASSIC_A60_RGBW.ota","productName":"CLASSIC A60 RGBW","fullName":"CLASSIC A60 RGBW/01020510/ZLL_MK_0x01020510_CLASSIC_A60_RGBW.ota","extension":".ota","released":"2019-03-13T09:46:08","salesRegion":null,"length":144008},{"blob":null,"identity":{"company":4364,"product":20,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade\r\n2. Improve network performance","shA256":"609a1352b0bea23d58096b75d2d182e828cf4c2fafc780a78856fee9ce76953b","name":"ZLL_MK_0x01020510_CLASSIC_B40_TW.ota","productName":"CLASSIC B40 TW","fullName":"CLASSIC B40 TW/01020510/ZLL_MK_0x01020510_CLASSIC_B40_TW.ota","extension":".ota","released":"2019-03-13T09:47:06","salesRegion":null,"length":132928},{"blob":null,"identity":{"company":4489,"product":33,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"aad1d240107f795b4fcef0acc08e52f854d81a0e1df41a69bc90958454c3834f","name":"Conv_Under_Cabinet_TW_IMG0021_00102428-encrypted.ota","productName":"Convertible Undercabinet Light TW","fullName":"Convertible Undercabinet Light TW/00102428/Conv_Under_Cabinet_TW_IMG0021_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:47:26","salesRegion":"us","length":170776},{"blob":null,"identity":{"company":4489,"product":150,"version":{"major":1,"minor":10,"build":100,"revision":0}},"releaseNotes":"1. Fix bug that Biolux Gen I luminaire lost HCL mode occasionally.","shA256":"48ed58089edd912f692c2fcdf410d3c4959a31182ff21ef86302088c9af73b34","name":"DL_HCL_DN150_01_IM0096_010A6400-encrypted_202331101115_withoutMF.OTA","productName":"DL HCL DN150 01","fullName":"DL_HCL_DN150_01/010a6400/DL_HCL_DN150_01_IM0096_010A6400-encrypted_202331101115_withoutMF.OTA","extension":".OTA","released":"2023-12-15T05:06:04","salesRegion":"eu","length":179616},{"blob":null,"identity":{"company":4489,"product":150,"version":{"major":1,"minor":9,"build":100,"revision":0}},"releaseNotes":"\r\n1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix leave network issue when update link key fail.","shA256":"c7fa163f113ec5905532df87023a697d9d95328a1cf8aa67be464ccc24339b6d","name":"DL_HCL_DN150_01_IM0096_01096400-encrypted_202301071154_withoutMF.OTA","productName":"DL HCL DN150 01","fullName":"DL_HCL_DN150_01/01096400/DL_HCL_DN150_01_IM0096_01096400-encrypted_202301071154_withoutMF.OTA","extension":".OTA","released":"2023-07-18T04:35:26","salesRegion":"eu","length":179648},{"blob":null,"identity":{"company":4489,"product":187,"version":{"major":2,"minor":35,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"cd11aaebf253a3a82c9fbb9be880c264c7f439bd02aab4b7a59c5861136dc670","name":"TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02236550-MF_DIS-20230609102634-3221010102432.ota","productName":"DL HCL ND150 02","fullName":"DL_HCL_ND150_02/02236550/TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02236550-MF_DIS-20230609102634-3221010102432.ota","extension":".ota","released":"2023-08-31T08:26:56","salesRegion":"eu","length":208078},{"blob":null,"identity":{"company":4489,"product":187,"version":{"major":2,"minor":34,"build":101,"revision":80}},"releaseNotes":"1. Enable permanent beacon request before Luminaire finish network paring.\r\n2. Patch to pass Zigbee3.0 certification.\r\n3. Improve fading function.","shA256":"84444093b49ce71fd2164b7e1759e7d4291784c55b30d119dc0f1e5a7c2a4f8e","name":"TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02226550-MF_DIS-20220922093310-3221010102432.ota","productName":"DL HCL ND150 02","fullName":"DL_HCL_ND150_02/02226550/TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02226550-MF_DIS-20220922093310-3221010102432.ota","extension":".ota","released":"2022-11-08T09:29:06","salesRegion":"eu","length":208198},{"blob":null,"identity":{"company":4489,"product":187,"version":{"major":2,"minor":33,"build":101,"revision":80}},"releaseNotes":"1. Improve CCT accuracy\r\n2. Fix bug that identify fail when Lums are turned off.\r\n3. Add overtemperature protection function.\r\n4. Production line support function in manufacture mode.\r\n5. EMMA feature supported.\r\n6. V02216550 is the PP FW.","shA256":"4a195f3df00b26fe9f3b4f381bba61043038d42dfb76bfdf7bde68d2a72c2a61","name":"TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02216550-MF_DIS-20220722105237-3221010102432.ota","productName":"DL HCL ND150 02","fullName":"DL_HCL_ND150_02/02216550/TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02216550-MF_DIS-20220722105237-3221010102432.ota","extension":".ota","released":"2022-09-02T10:16:19","salesRegion":"eu","length":207722},{"blob":null,"identity":{"company":4489,"product":135,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"14a6b9cf37bbcb26c9d9bef7ce3984d5066d47964b49640df3f25c92a45f6ef3","name":"DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x02116550-MF_DIS-20230710044635-3221010102432.ota","productName":"DL PFM155UGR 04","fullName":"DL_PFM155UGR_04/02116550/DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x02116550-MF_DIS-20230710044635-3221010102432.ota","extension":".ota","released":"2023-08-31T09:02:30","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":135,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0X0B04, attribute 0X0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"ee47b7edd569f7191930e6dc3c893125cfaca6ba25f1c8b0bd14ca218fe06ad3","name":"DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x020E6550-MF_DIS-20220124052131-....ota","productName":"DL PFM155UGR 04","fullName":"DL_PFM155UGR_04/020e6550/DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x020E6550-MF_DIS-20220124052131-....ota","extension":".ota","released":"2022-04-28T07:08:45","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":135,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is DL_PFM155UGR_04.\r\n4.This is the same version as PP firmware.","shA256":"906e7ba30f792d63cb81b9b38396d0d9dc50c7099a9f66ef06adf36df49c598c","name":"DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x02056550-MF_DIS-20201225135054.ota","productName":"DL PFM155UGR 04","fullName":"DL_PFM155UGR_04/02056550/DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x02056550-MF_DIS-20201225135054.ota","extension":".ota","released":"2021-04-20T09:59:03","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":136,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"5baec8fb3a56becc890cbcd5defba313dfc880941c284d823216d11deac41f59","name":"DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x02116550-MF_DIS-20230708054143-3221010102432.ota","productName":"DL PFM195UGR 04","fullName":"DL_PFM195UGR_04/02116550/DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x02116550-MF_DIS-20230708054143-3221010102432.ota","extension":".ota","released":"2023-08-31T09:03:31","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":136,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0X0B04, attribute 0X0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"7a6aa8983b02abe71ff0ff23d2f58ec3c5ef6e5a77bcd50d73401bfdd45d9aaf","name":"DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x020E6550-MF_DIS-20220124052644-....ota","productName":"DL PFM195UGR 04","fullName":"DL_PFM195UGR_04/020e6550/DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x020E6550-MF_DIS-20220124052644-....ota","extension":".ota","released":"2022-04-28T07:09:14","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":136,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.This is the same version as PP firmware version.","shA256":"d9ba3fffb9abc229c3c24c7078b6c7f4775ff147b082877691c0dbfac514337a","name":"DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x02056550-MF_DIS-20201225140825.ota","productName":"DL PFM195UGR 04","fullName":"DL_PFM195UGR_04/02056550/DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x02056550-MF_DIS-20201225140825.ota","extension":".ota","released":"2021-03-04T07:35:30","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":101,"version":{"major":0,"minor":16,"build":50,"revision":1}},"releaseNotes":"1. Level curving algorithm update","shA256":"ce209589ab40082e334018112d656d99f4514447aa9a4b03b81cc4650cd3d637","name":"Downlight_TW_HCL_IM0065_00103201-encrypted_09_20_2019_Fri_142050_70_withoutMF.ota","productName":"Downlight TW HCL","fullName":"Downlight TW HCL/00103201/Downlight_TW_HCL_IM0065_00103201-encrypted_09_20_2019_Fri_142050_70_withoutMF.ota","extension":".ota","released":"2019-10-17T09:00:40","salesRegion":"eu","length":179976},{"blob":null,"identity":{"company":4489,"product":35,"version":{"major":0,"minor":16,"build":36,"revision":17}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"50bc458e53bcd831188a052ef054b4bd07d08f3b5371f6aab1ee8fdbfaf669d4","name":"Edge_Lit_Under_Cabinet_IMG0023_00102411-encrypted.ota","productName":"Edge-Lit Under Cabinet","fullName":"Edge-Lit Under Cabinet/00102411/Edge_Lit_Under_Cabinet_IMG0023_00102411-encrypted.ota","extension":".ota","released":"2019-02-28T16:48:24","salesRegion":"us","length":170492},{"blob":null,"identity":{"company":4489,"product":209,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"8446e368c5d64c9c366066a1dc95b8df798b6c67f5f38de8992cfedf6d1e916c","name":"DIM-EDISON60_FIL_DIM_T-0x00D1-0x03203660.OTA","productName":"EDISON60 FIL DIM T","fullName":"EDISON60 FIL DIM T/03203660/DIM-EDISON60_FIL_DIM_T-0x00D1-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:08:20","salesRegion":"eu","length":188416},{"blob":null,"identity":{"company":4489,"product":31,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"d05e7dc1346e790d62ad77163540741ad733148851b1bd92e4cd320a9b3984b6","name":"FLEX_Outdoor_RGBW_IMG001F_00102428-encrypted.ota","productName":"FLEX Outdoor RGBW","fullName":"FLEX Outdoor RGBW/00102428/FLEX_Outdoor_RGBW_IMG001F_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:49:09","salesRegion":"us","length":179960},{"blob":null,"identity":{"company":4489,"product":42,"version":{"major":1,"minor":6,"build":100,"revision":0}},"releaseNotes":"1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement ","shA256":"ba6c104741c556ef9c6bd90d7b3f01385e7395dff7690d23917ee7017e5e20b7","name":"Flex_RGBW_Z3_IM002A_01066400-encrypted_202216020348_withoutMF.ota","productName":"Flex RGBW Z3","fullName":"Flex RGBW Z3/01066400/Flex_RGBW_Z3_IM002A_01066400-encrypted_202216020348_withoutMF.ota","extension":".ota","released":"2022-03-17T12:40:45","salesRegion":"eu","length":193948},{"blob":null,"identity":{"company":4489,"product":42,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"de351df72f48498a3599bce37da773a46a33724bfb85f6236234404d7b8faab6","name":"Flex_RGBW_Z3_IM002A_00103101-encrypted_11_27_2018_Tue_134318_76_withoutMF.ota","productName":"Flex RGBW Z3","fullName":"Flex RGBW Z3/00103101/Flex_RGBW_Z3_IM002A_00103101-encrypted_11_27_2018_Tue_134318_76_withoutMF.ota","extension":".ota","released":"2019-03-22T08:14:28","salesRegion":"eu","length":191068},{"blob":null,"identity":{"company":4489,"product":30,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"50c5ffb8970051ec919985d06ee1e3e2fa86b65173d806ae6fe7b87434c01106","name":"FLEX_RGBW_IMG001E_00102428-encrypted.ota","productName":"FLEX RGBW","fullName":"FLEX RGBW/00102428/FLEX_RGBW_IMG001E_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:49:51","salesRegion":"us","length":178908},{"blob":null,"identity":{"company":4364,"product":108,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance","shA256":"3487f715eecf2c68d6e4b119f85158f7a9736b52f1373f00fa6571f6b6b81eb1","name":"ZLL_MK_0x01020510_FLOOD_LIGHT_RGBW_OSRAM.ota","productName":"FLOOD LIGHT RGBW OSRAM","fullName":"FLOOD LIGHT RGBW OSRAM/01020510/ZLL_MK_0x01020510_FLOOD_LIGHT_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-14T05:30:05","salesRegion":"eu","length":142972},{"blob":null,"identity":{"company":4489,"product":34,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"6418a705d2fa74719299a8e282ce362c6d3754573883c02628728aa167cc8956","name":"Flushmount_TW_IMG0022_00102428-encrypted.ota","productName":"Flushmount TW","fullName":"Flushmount TW/00102428/Flushmount_TW_IMG0022_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:50:35","salesRegion":"us","length":170776},{"blob":null,"identity":{"company":4364,"product":103,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance","shA256":"254f5a65469f3a8a16a7c231572a5284e24afaef225bb621757da4e2823d26a7","name":"ZLL_MK_0x01020510_GARDENPOLE_MINI_RGBW_OSRAM.ota","productName":"GARDENPOLE MINI RGBW OSRAM","fullName":"GARDENPOLE MINI RGBW OSRAM/01020510/ZLL_MK_0x01020510_GARDENPOLE_MINI_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-14T05:31:27","salesRegion":"eu","length":142968},{"blob":null,"identity":{"company":4489,"product":64,"version":{"major":1,"minor":6,"build":100,"revision":0}},"releaseNotes":"1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement ","shA256":"b33801388c41194193a2e8c2f1a0ffd8289932382d1f32485050757189539678","name":"Gardenpole_Mini_RGBW_Z3_IM0040_01066400-encrypted_202210011005_withoutMF.ota","productName":"Gardenpole Mini RGBW Z3","fullName":"Gardenpole Mini RGBW Z3/01066400/Gardenpole_Mini_RGBW_Z3_IM0040_01066400-encrypted_202210011005_withoutMF.ota","extension":".ota","released":"2022-03-17T12:42:53","salesRegion":"eu","length":193948},{"blob":null,"identity":{"company":4364,"product":90,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance","shA256":"171ffe1d5cdede576cc20593715bc786f33cb0fecff586ec8130a7c54cf309ab","name":"ZLL_MK_0x01020510_GARDENPOLE_RGBW_LIGHTIFY.ota","productName":"GARDENPOLE RGBW LIGHTIFY","fullName":"GARDENPOLE RGBW LIGHTIFY/01020510/ZLL_MK_0x01020510_GARDENPOLE_RGBW_LIGHTIFY.ota","extension":".ota","released":"2019-03-14T05:40:54","salesRegion":"eu","length":142968},{"blob":null,"identity":{"company":4489,"product":59,"version":{"major":1,"minor":6,"build":100,"revision":0}},"releaseNotes":"1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement ","shA256":"d6a1cec950dbe3f1792741c4ed1d836d7a16f642d3607226de498cb42ee650a7","name":"Gardenpole_RGBW_Z3_IM003B_01066400-encrypted_202216020351_withoutMF.ota","productName":"Gardenpole RGBW Z3","fullName":"Gardenpole RGBW Z3/01066400/Gardenpole_RGBW_Z3_IM003B_01066400-encrypted_202216020351_withoutMF.ota","extension":".ota","released":"2022-03-17T12:43:36","salesRegion":"eu","length":193940},{"blob":null,"identity":{"company":4489,"product":59,"version":{"major":0,"minor":16,"build":49,"revision":3}},"releaseNotes":"1. updated to the latest stack","shA256":"93e5182fdacd81a231f72df97de6e94723884661472056158562afa9ad0222fe","name":"Gardenpole_RGBW_Z3_IM003B_00103103-encrypted_02_27_2019_Wed_150725_31_withoutMF.ota","productName":"Gardenpole RGBW Z3","fullName":"Gardenpole RGBW Z3/00103103/Gardenpole_RGBW_Z3_IM003B_00103103-encrypted_02_27_2019_Wed_150725_31_withoutMF.ota","extension":".ota","released":"2019-10-24T03:46:27","salesRegion":"eu","length":191068},{"blob":null,"identity":{"company":4489,"product":64,"version":{"major":0,"minor":16,"build":49,"revision":3}},"releaseNotes":"1. updated to the latest stack","shA256":"a4fe10d0578fe47aba9b76a2fd4a119bf594ab82658ab3d7ff5090512dd511a1","name":"Gardenpole_Mini_RGBW_Z3_IM0040_00103103-encrypted_02_27_2019_Wed_151557_92_withoutMF.ota","productName":"GardenpoleMini RGBW Z3","fullName":"GardenpoleMini RGBW Z3/00103103/Gardenpole_Mini_RGBW_Z3_IM0040_00103103-encrypted_02_27_2019_Wed_151557_92_withoutMF.ota","extension":".ota","released":"2019-10-24T03:45:21","salesRegion":"eu","length":191068},{"blob":null,"identity":{"company":4364,"product":5,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"8fc54b848bb4f740c5b16a6b5161be4a21c924046b35aef8785b9c78abf3f5d9","name":"ZLL_MK_0x01020510_GARDENSPOT_RGB.ota","productName":"GARDENSPOT RGB","fullName":"GARDENSPOT RGB/01020510/ZLL_MK_0x01020510_GARDENSPOT_RGB.ota","extension":".ota","released":"2019-03-14T05:45:51","salesRegion":"eu","length":140550},{"blob":null,"identity":{"company":4364,"product":7,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"8db3aaac1e56f5be4d08d7e783176273e083ae7a5c528a367db10b42e1cee8c8","name":"ZLL_MK_0x01020510_GARDENSPOT_W.ota","productName":"GARDENSPOT W","fullName":"GARDENSPOT W/01020510/ZLL_MK_0x01020510_GARDENSPOT_W.ota","extension":".ota","released":"2019-03-14T05:47:37","salesRegion":"eu","length":123884},{"blob":null,"identity":{"company":4489,"product":210,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"dba3a537651754b9a67023271c241d3d5d4734bd8ef73f068239aff087837a41","name":"DIM-GLOBE60_FIL_DIM_T-0x00D2-0x03203660.OTA","productName":"GLOBE60 FIL DIM T","fullName":"GLOBE60 FIL DIM T/03203660/DIM-GLOBE60_FIL_DIM_T-0x00D2-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:09:23","salesRegion":"eu","length":188416},{"blob":null,"identity":{"company":4489,"product":111,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported \r\n- On with time off Command supported ","shA256":"9f28ed05b273eca0ff0c59dc0b06eb8c6d8922579f0233dd12519face12efb47","name":"DIM-LEDVANCE_DIM-0x1189-0x006F-0x02056550-MF_DIS-20201201111102.ota","productName":"LEDVANCE DIM","fullName":"LEDVANCE DIM/02056550/DIM-LEDVANCE_DIM-0x1189-0x006F-0x02056550-MF_DIS-20201201111102.ota","extension":".ota","released":"2020-12-10T06:34:40","salesRegion":"eu","length":190758},{"blob":null,"identity":{"company":4364,"product":92,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"7a1401d133fbe36eddddb3ad97379988918475d3f734dc908b4e22e89783cc72","name":"ZLL_MK_0x01020510_LIGHTIFY_INDOOR_FLEX.ota","productName":"LIGHTIFY INDOOR FLEX","fullName":"LIGHTIFY INDOOR FLEX/01020510/ZLL_MK_0x01020510_LIGHTIFY_INDOOR_FLEX.ota","extension":".ota","released":"2019-03-14T05:49:21","salesRegion":"eu","length":142972},{"blob":null,"identity":{"company":4364,"product":91,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"24a774035cf9ffabd1f8b4fb792abaf7e9c2795aaf786707c7bda10b39bafc9b","name":"ZLL_MK_0x01020510_LIGHTIFY_OUTDOOR_FLEX.ota","productName":"LIGHTIFY OUTDOOR FLEX","fullName":"LIGHTIFY OUTDOOR FLEX/01020510/ZLL_MK_0x01020510_LIGHTIFY_OUTDOOR_FLEX.ota","extension":".ota","released":"2019-03-14T05:52:08","salesRegion":"eu","length":143228},{"blob":null,"identity":{"company":4489,"product":132,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"30a782e9fc631726f686af2855491f803440a30f557ffe62ee7bcbd33a542b4b","name":"DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x02116550-MF_DIS-20230710043240-3221010102432.ota","productName":"LN Indivi1200 04","fullName":"LN_Indivi1200_04/02116550/DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x02116550-MF_DIS-20230710043240-3221010102432.ota","extension":".ota","released":"2023-08-31T09:04:25","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":132,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0X0B04, attribute 0X0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"55ca6aa32c6fafe834519a37e711c90865937be84b48e0eaeac03f4f06723229","name":"DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x020E6550-MF_DIS-20220124050546....ota","productName":"LN Indivi1200 04","fullName":"LN_Indivi1200_04/020e6550/DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x020E6550-MF_DIS-20220124050546....ota","extension":".ota","released":"2022-04-28T07:10:08","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":132,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is LN_Indivi1200_04.\r\n4.This is the same version as PP firmware.","shA256":"8f76fc9bef872528697cc21014794daa92afff9c04e89054785b7d14487b9010","name":"DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x02056550-MF_DIS-20201230144342.ota","productName":"LN Indivi1200 04","fullName":"LN_Indivi1200_04/02056550/DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x02056550-MF_DIS-20201230144342.ota","extension":".ota","released":"2021-04-20T10:16:14","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":133,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"7b6a0cbce37c9a601bca953718cfd1139e20b1fa8602087ed15d72da06d9d5ed","name":"DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x02116550-MF_DIS-20230708053449-3221010102432.ota","productName":"LN Indivi1500 04","fullName":"LN_Indivi1500_04/02116550/DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x02116550-MF_DIS-20230708053449-3221010102432.ota","extension":".ota","released":"2023-08-31T09:05:06","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":133,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0X0B04, attribute 0X0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"fa3cc34812d8619f2a673a6ec1acdc7283da710c90b691aa40d177489c4a74cb","name":"DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x020E6550-MF_DIS-20220124051101....ota","productName":"LN Indivi1500 04","fullName":"LN_Indivi1500_04/020e6550/DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x020E6550-MF_DIS-20220124051101....ota","extension":".ota","released":"2022-04-28T07:12:05","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":133,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is LN_Indivi1500_04.\r\n4.This is the same version as PP firmware.","shA256":"47c8ce17da159db59c621f5fc7d320ebf3aad496092d3615c2777ab22dac913b","name":"DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x02056550-MF_DIS-20201230155133.ota","productName":"LN Indivi1500 04","fullName":"LN_Indivi1500_04/02056550/DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x02056550-MF_DIS-20201230155133.ota","extension":".ota","released":"2021-04-20T10:17:10","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4364,"product":101,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"53f12c37e8ef0271aa0b6e1f3e8b82c4f78f52231431d4b903d6e24e4a973fc2","name":"ZLL_MK_0x01020510_MR16_TW_OSRAM.ota","productName":"MR16 TW OSRAM","fullName":"MR16 TW OSRAM/01020510/ZLL_MK_0x01020510_MR16_TW_OSRAM.ota","extension":".ota","released":"2019-03-14T05:55:27","salesRegion":"eu","length":132676},{"blob":null,"identity":{"company":4489,"product":32,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"69af9ff20d7893051317df1018dc4b2ff7300162418b7783154d32d70f2af184","name":"Outdoor_Accent_Light_RGB_IMG0020_00102428-encrypted.ota","productName":"Outdoor Accent Light RGB","fullName":"Outdoor Accent Light RGB/00102428/Outdoor_Accent_Light_RGB_IMG0020_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:51:14","salesRegion":"us","length":178524},{"blob":null,"identity":{"company":4489,"product":92,"version":{"major":1,"minor":6,"build":100,"revision":0}},"releaseNotes":"1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement ","shA256":"4d1ecf5673ae4fb6f247ac9fc5d84c4317f59ad24462ae5bb95c6da91edb66cf","name":"Outdoor_FLEX_RGBW_Z3_IM005C_01066400-encrypted_202210011002_withoutMF.ota","productName":"Outdoor FLEX RGBW Z3","fullName":"Outdoor FLEX RGBW Z3/01066400/Outdoor_FLEX_RGBW_Z3_IM005C_01066400-encrypted_202210011002_withoutMF.ota","extension":".ota","released":"2022-03-17T12:45:35","salesRegion":"eu","length":193920},{"blob":null,"identity":{"company":4489,"product":92,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"9d07611fca08cd7fac4490e8adf1424f53a251aa7857aa88cd5caea4bf4bf9ed","name":"Outdoor_FLEX_RGBW_Z3_IM005C_00103101-encrypted_11_27_2018_Tue_135739_87_withoutMF.ota","productName":"Outdoor FLEX RGBW Z3","fullName":"Outdoor FLEX RGBW Z3/00103101/Outdoor_FLEX_RGBW_Z3_IM005C_00103101-encrypted_11_27_2018_Tue_135739_87_withoutMF.ota","extension":".ota","released":"2019-03-22T08:15:07","salesRegion":"eu","length":191048},{"blob":null,"identity":{"company":4364,"product":105,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"7052d5cea090a674a16c98cf190f777f6556ad424e2f86a1449d344066a253b9","name":"ZLL_MK_0x01020510_OUTDOOR_LANTERN_B50_RGBW_OSRAM.ota","productName":"OUTDOOR LANTERN B50 RGBW OSRAM","fullName":"OUTDOOR LANTERN B50 RGBW OSRAM/01020510/ZLL_MK_0x01020510_OUTDOOR_LANTERN_B50_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-14T06:04:01","salesRegion":"eu","length":142968},{"blob":null,"identity":{"company":4364,"product":110,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"586d7921b21280ac8698c9e8818f090f303977a5c7c293c03846bd015efe3d2f","name":"ZLL_MK_0x01020510_OUTDOOR_LANTERN_B90_RGBW_OSRAM.ota","productName":"OUTDOOR LANTERN B90 RGBW OSRAM","fullName":"OUTDOOR LANTERN B90 RGBW OSRAM/01020510/ZLL_MK_0x01020510_OUTDOOR_LANTERN_B90_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-14T06:07:55","salesRegion":"eu","length":142968},{"blob":null,"identity":{"company":4364,"product":104,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"8348f367ab69581e7cffc6361930b639dce24f3f37596491748f4e369b95b0b2","name":"ZLL_MK_0x01020510_OUTDOOR_LANTERN_W_RGBW_OSRAM.ota","productName":"OUTDOOR LANTERN W RGBW OSRAM","fullName":"OUTDOOR LANTERN W RGBW OSRAM/01020510/ZLL_MK_0x01020510_OUTDOOR_LANTERN_W_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-14T06:11:12","salesRegion":"eu","length":142968},{"blob":null,"identity":{"company":4489,"product":206,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"fb0c264961c05f3d93bf6e99660b3d5929e918aa90efc56e00f9db7295e80eaf","name":"DIM-P40_DIM_T-0x00CE-0x03203660.OTA","productName":"P40 DIM T","fullName":"P40 DIM T/03203660/DIM-P40_DIM_T-0x00CE-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:10:45","salesRegion":"eu","length":188384},{"blob":null,"identity":{"company":4489,"product":141,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported \r\n- On with time off Command supported","shA256":"55a3f2310fbe0798dd89b52bfcf046edff6bf33a339962cc036fa063d72a7176","name":"TW-P40_TW_Value-0x1189-0x008D-0x02056550-MF_DIS-20201207182023.ota","productName":"P40 TW Value","fullName":"P40 TW Value/02056550/TW-P40_TW_Value-0x1189-0x008D-0x02056550-MF_DIS-20201207182023.ota","extension":".ota","released":"2020-12-17T05:18:41","salesRegion":"eu","length":196574},{"blob":null,"identity":{"company":4489,"product":164,"version":{"major":2,"minor":19,"build":101,"revision":80}},"releaseNotes":"1. ZLO gap fixed.\r\n2. V02136550 is production firmware.","shA256":"116ff8ba3b83912c1ca16eb1cd307c2e0c7fdce534662ad8a77225f656ab7eb5","name":"TW-P40S_TW-0x1189-0x00A4-0x02136550-MF_DIS-20211011051950-3221010102432.ota","productName":"P40S TW","fullName":"P40S_TW/02136550/TW-P40S_TW-0x1189-0x00A4-0x02136550-MF_DIS-20211011051950-3221010102432.ota","extension":".ota","released":"2023-01-19T07:45:36","salesRegion":"eu","length":198246},{"blob":null,"identity":{"company":4364,"product":106,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"f59aac7ac741b09f610fa26821b05aef403fb14b970797a7d060416e1c187a24","name":"ZLL_MK_0x01020510_PANEL_RGBW_OSRAM.ota","productName":"PANEL RGBW OSRAM","fullName":"PANEL RGBW OSRAM/01020510/ZLL_MK_0x01020510_PANEL_RGBW_OSRAM.ota","extension":".ota","released":"2019-03-14T06:13:30","salesRegion":"eu","length":142968},{"blob":null,"identity":{"company":4489,"product":99,"version":{"major":0,"minor":16,"build":50,"revision":1}},"releaseNotes":"1. Level curving algorithm update","shA256":"53eaa0a1d14e1538abd5f68341e5f36efa50a9fdb19c0619dd9fc47deaf004ba","name":"Panel_TW_HCL_IM0063_00103201-encrypted_09_18_2019_Wed_113705_07_withoutMF.ota","productName":"Panel TW HCL","fullName":"Panel TW HCL/00103201/Panel_TW_HCL_IM0063_00103201-encrypted_09_18_2019_Wed_113705_07_withoutMF.ota","extension":".ota","released":"2019-10-17T08:59:21","salesRegion":"eu","length":179964},{"blob":null,"identity":{"company":4489,"product":90,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"9562ee5fede44c42ffffed75f52f7c5ad7aa1291c03a9785c3d47a106d2acea0","name":"Panel_TW_Z3_IM005A_01056400-encrypted_202129091207_withoutMF.ota","productName":"Panel TW Z3","fullName":"Panel TW Z3/01056400/Panel_TW_Z3_IM005A_01056400-encrypted_202129091207_withoutMF.ota","extension":".ota","released":"2021-10-21T05:29:41","salesRegion":"eu","length":185972},{"blob":null,"identity":{"company":4489,"product":90,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"3665fee14b9fe3370281c40f2db382b0a887e610955b56a8a5b3d363be5f0887","name":"Panel_TW_Z3_IM005A_00103101-encrypted_11_23_2018_Fri_161331_81_withoutMF.ota","productName":"Panel TW Z3","fullName":"Panel TW Z3/00103101/Panel_TW_Z3_IM005A_00103101-encrypted_11_23_2018_Fri_161331_81_withoutMF.ota","extension":".ota","released":"2019-03-22T08:15:49","salesRegion":"eu","length":183628},{"blob":null,"identity":{"company":4364,"product":3,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"60d6b811185d33393db8fa64a1e17ab4db86c479c1a1be575aef780ab455ffb4","name":"ZLL_MK_0x01020510_PAR16_50_TW.ota","productName":"PAR16 50 TW","fullName":"PAR16 50 TW/01020510/ZLL_MK_0x01020510_PAR16_50_TW.ota","extension":".ota","released":"2019-03-14T06:15:26","salesRegion":"eu","length":132672},{"blob":null,"identity":{"company":4489,"product":207,"version":{"major":3,"minor":32,"build":54,"revision":96}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255","shA256":"3b7f8b4475c46400d2d65c98892d8b9bf022aa214e307a9c98ab8225ebe93dca","name":"DIM-PAR16_DIM_T-0x00CF-0x03203660.OTA","productName":"PAR16 DIM T","fullName":"PAR16 DIM T/03203660/DIM-PAR16_DIM_T-0x00CF-0x03203660.OTA","extension":".OTA","released":"2022-09-01T06:11:31","salesRegion":"eu","length":188384},{"blob":null,"identity":{"company":4489,"product":49,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"1ba243b0d67916ada2d8f41a7155c4def963905d133dda7738d0add34378ebdf","name":"PAR16_DIM_Z3_IM0031_01056400-encrypted_202129091143_withoutMF.ota","productName":"PAR16 DIM Z3","fullName":"PAR16 DIM Z3/01056400/PAR16_DIM_Z3_IM0031_01056400-encrypted_202129091143_withoutMF.ota","extension":".ota","released":"2021-10-21T05:30:17","salesRegion":"eu","length":185112},{"blob":null,"identity":{"company":4489,"product":49,"version":{"major":1,"minor":3,"build":100,"revision":0}},"releaseNotes":"1.Support for turn on/off fading time configurations\r\n2.Support for ZLO commands\r\n3.OTA improvements, rollback protection enabled","shA256":"f39e6a0b77726f3e33c22e6d846dbfc946e5d0425d86ecd6cb823e3ae7e0e76f","name":"PAR16_DIM_Z3_IM0031_01036400-encrypted_202110060424_withoutMF.ota","productName":"PAR16 DIM Z3","fullName":"PAR16 DIM Z3/01036400/PAR16_DIM_Z3_IM0031_01036400-encrypted_202110060424_withoutMF.ota","extension":".ota","released":"2021-07-14T09:11:24","salesRegion":"eu","length":183392},{"blob":null,"identity":{"company":4489,"product":49,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"430afa8d2f094af21134ca83d67ffc8c2292b11081e5785f5870394d0e9fca32","name":"PAR16_DIM_Z3_IM0031_00103101-encrypted_11_26_2018_Mon_175052_32_withoutMF.ota","productName":"PAR16 DIM Z3","fullName":"PAR16 DIM Z3/00103101/PAR16_DIM_Z3_IM0031_00103101-encrypted_11_26_2018_Mon_175052_32_withoutMF.ota","extension":".ota","released":"2019-03-22T08:16:37","salesRegion":"eu","length":182876},{"blob":null,"identity":{"company":4489,"product":142,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported\r\n- On with time off Command supported","shA256":"7e260f86a452b9de851f80460cefdc32e174035c787b566233b51a15d1d98636","name":"RGBW-PAR16_RGBW_Value-0x1189-0x008E-0x02056550-MF_DIS-20201216114033.ota","productName":"PAR16 RGBW Value","fullName":"PAR16 RGBW Value/02056550/RGBW-PAR16_RGBW_Value-0x1189-0x008E-0x02056550-MF_DIS-20201216114033.ota","extension":".ota","released":"2021-01-05T07:03:29","salesRegion":"eu","length":210510},{"blob":null,"identity":{"company":4489,"product":48,"version":{"major":1,"minor":6,"build":100,"revision":0}},"releaseNotes":"1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement ","shA256":"d0db5be8632413bcb2079e1d9f1b9927598b71712c2abf20d63a04449b2f155e","name":"PAR16_RGBW_Z3_IM0030_01066400-encrypted_202126110402_withoutMF.ota","productName":"PAR16 RGBW Z3","fullName":"PAR16 RGBW Z3/01066400/PAR16_RGBW_Z3_IM0030_01066400-encrypted_202126110402_withoutMF.ota","extension":".ota","released":"2022-04-15T05:19:24","salesRegion":"eu","length":193956},{"blob":null,"identity":{"company":4489,"product":48,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"d29318d6b4f70379741a1e6c12c01e2d61ebdce2936090575573b45f344667f4","name":"PAR16_RGBW_Z3_IM0030_00103101-encrypted_11_27_2018_Tue_140612_79_withoutMF.ota","productName":"PAR16 RGBW Z3","fullName":"PAR16 RGBW Z3/00103101/PAR16_RGBW_Z3_IM0030_00103101-encrypted_11_27_2018_Tue_140612_79_withoutMF.ota","extension":".ota","released":"2019-03-22T08:17:10","salesRegion":"eu","length":191080},{"blob":null,"identity":{"company":4364,"product":17,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"256d795fcfb1fe84073a7a148c0ad87d5c872d6017d383f6118b2866e6fc2aa2","name":"ZLL_MK_0x01020510_Par16Rgbw.ota","productName":"PAR16 RGBW","fullName":"PAR16 RGBW/01020510/ZLL_MK_0x01020510_Par16Rgbw.ota","extension":".ota","released":"2019-03-14T06:17:28","salesRegion":"eu","length":142086},{"blob":null,"identity":{"company":4489,"product":46,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"7771fbcfa4da97944f0daef253534627231b476f25b96c27281e5f2c544f5d5f","name":"PAR16_TW_Z3_IM002E_01056400-encrypted_202129091137_withoutMF.ota","productName":"PAR16 TW Z3","fullName":"PAR16 TW Z3/01056400/PAR16_TW_Z3_IM002E_01056400-encrypted_202129091137_withoutMF.ota","extension":".ota","released":"2021-10-21T05:30:51","salesRegion":"eu","length":185968},{"blob":null,"identity":{"company":4489,"product":46,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"64b4ed1beeabc7cd03b664a1cf57b4024ba306ff13f56681a4f34f025361a46c","name":"PAR16_TW_Z3_IM002E_00103101-encrypted_11_23_2018_Fri_162418_58_withoutMF.ota","productName":"PAR16 TW Z3","fullName":"PAR16 TW Z3/00103101/PAR16_TW_Z3_IM002E_00103101-encrypted_11_23_2018_Fri_162418_58_withoutMF.ota","extension":".ota","released":"2019-03-22T08:17:42","salesRegion":"eu","length":183624},{"blob":null,"identity":{"company":4489,"product":143,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported \r\n- On with time off Command supported ","shA256":"f70f0ec1f41a1dffdc37f7c3a7c254d1d9b4dee675a0eb336a456c5214c002dc","name":"TW-PAR16_TW_Value-0x1189-0x008F-0x02056550-MF_DIS-20201202092118.ota","productName":"PAR16 TW","fullName":"PAR16 TW/02056550/TW-PAR16_TW_Value-0x1189-0x008F-0x02056550-MF_DIS-20201202092118.ota","extension":".ota","released":"2020-12-10T06:35:08","salesRegion":"eu","length":196574},{"blob":null,"identity":{"company":4489,"product":142,"version":{"major":2,"minor":20,"build":101,"revision":80}},"releaseNotes":"1.ZLO gap fix\r\n2.RGBW color calibration\r\n3.Disable touch-link function","shA256":"4a2a81543945eaee9a1dfb3643ba5c2f4de730e2632f87c6e5e80d20ac36e155","name":"RGBW-PAR16_RGBW_Value-0x1189-0x008E-0x02146550-MF_DIS-20211203084009-3221010102432.ota","productName":"PAR16 RGBW Value","fullName":"PAR16_RGBW_Value/02146550/RGBW-PAR16_RGBW_Value-0x1189-0x008E-0x02146550-MF_DIS-20211203084009-3221010102432.ota","extension":".ota","released":"2022-03-02T07:54:23","salesRegion":"eu","length":213082},{"blob":null,"identity":{"company":4489,"product":166,"version":{"major":2,"minor":20,"build":101,"revision":80}},"releaseNotes":"1.ZLO gap fix\r\n2.RGBW color calibration\r\n3.Disable touch-link function","shA256":"914dc04e898323ad3f66d0808b5f7d7213d705a9975007d999b280d1f30bbeea","name":"RGBW-PAR16S_RGBW-0x1189-0x00A6-0x02146550-MF_DIS-20211203084525-3221010102432.ota","productName":"PAR16S RGBW","fullName":"PAR16S_RGBW/02146550/RGBW-PAR16S_RGBW-0x1189-0x00A6-0x02146550-MF_DIS-20211203084525-3221010102432.ota","extension":".ota","released":"2022-03-02T08:04:30","salesRegion":"eu","length":213142},{"blob":null,"identity":{"company":4489,"product":165,"version":{"major":2,"minor":19,"build":101,"revision":80}},"releaseNotes":"1. ZLO gap fixed.\r\n2. V02136550 is production firmware.","shA256":"e1f727947c5199cb4ff6b06729b65f14d3c3d361198a48aac59400b8e9047824","name":"TW-PAR16S_TW-0x1189-0x00A5-0x02136550-MF_DIS-20211011052510-322101010243....ota","productName":"PAR16S TW","fullName":"PAR16S_TW/02136550/TW-PAR16S_TW-0x1189-0x00A5-0x02136550-MF_DIS-20211011052510-322101010243....ota","extension":".ota","released":"2023-01-19T07:48:06","salesRegion":"eu","length":198246},{"blob":null,"identity":{"company":4489,"product":16,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"6a1f3d50c7bc361f5690faa3bf3b37cd17e72004fd5b365a9283b178f8dfb617","name":"PAR38_W_10_year_IMG0010_00102428-encrypted.ota","productName":"PAR38 W 10 year","fullName":"PAR38 W 10 year/00102428/PAR38_W_10_year_IMG0010_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:51:51","salesRegion":"us","length":170120},{"blob":null,"identity":{"company":4489,"product":128,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PFM600UGR_04.\r\n4.This is the same version as PP firmware.","shA256":"ec9588be5f25e01d5a8e6d1f34b3f1672d3b3ffab680511023f9ea489b3c27c9","name":"DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x02056550-MF_DIS-20201225172901.ota","productName":"PFM600UGR 04","fullName":"PFM600UGR_04/02056550/DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x02056550-MF_DIS-20201225172901.ota","extension":".ota","released":"2021-04-20T10:25:37","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":134,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"8fb33e300080c31c112f7397512cda382518a452608616e6cdcf1af3631d45ef","name":"DIM_UART-PL_DI1200_04-0x1189-0x0086-0x02116550-MF_DIS-20230710043941-3221010102432.ota","productName":"PL DI1200 04","fullName":"PL_DI1200_04/02116550/DIM_UART-PL_DI1200_04-0x1189-0x0086-0x02116550-MF_DIS-20230710043941-3221010102432.ota","extension":".ota","released":"2023-08-31T09:05:52","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":134,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"c445c6d305ad2d5fc8c8165fef3879d36f8016c2a2bfd9c67b4aee7de81fcae8","name":"DIM_UART-PL_DI1200_04-0x1189-0x0086-0x020E6550-MF_DIS-20220124051610-322....ota","productName":"PL DI1200 04","fullName":"PL_DI1200_04/020e6550/DIM_UART-PL_DI1200_04-0x1189-0x0086-0x020E6550-MF_DIS-20220124051610-322....ota","extension":".ota","released":"2022-04-28T07:12:59","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":134,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PL_DI1200_04.\r\n4.This is the same version as PP firmware.","shA256":"a9bb62250a76c540cbf122b8fe27708c396dc935748c07c5c5c30c661234b47f","name":"DIM_UART-PL_DI1200_04-0x1189-0x0086-0x02056550-MF_DIS-20201230160535.ota","productName":"PL DI1200 04","fullName":"PL_DI1200_04/02056550/DIM_UART-PL_DI1200_04-0x1189-0x0086-0x02056550-MF_DIS-20201230160535.ota","extension":".ota","released":"2021-04-26T02:38:56","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":188,"version":{"major":2,"minor":35,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"e6e75a497aa3a4c55b0299c9e86be2011855212920e9fd38c77e421982190060","name":"TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02236550-MF_DIS-20230609103517-3221010102432.ota","productName":"PL HCL300x1200 01","fullName":"PL_HCL300x1200_01/02236550/TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02236550-MF_DIS-20230609103517-3221010102432.ota","extension":".ota","released":"2023-08-31T08:32:06","salesRegion":"eu","length":208054},{"blob":null,"identity":{"company":4489,"product":188,"version":{"major":2,"minor":34,"build":101,"revision":80}},"releaseNotes":"1. Enable permanent beacon request before Luminaire finish network paring.\r\n2. Patch for Zigbee3.0 certification.\r\n3. Improve fading function.","shA256":"0590951cd2ab3ce851a9e35a34fd3c8acacdd312f219a1b239254a15074950ed","name":"TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02226550-MF_DIS-20220922093955-3221010102432.ota","productName":"PL HCL300x1200 01","fullName":"PL_HCL300x1200_01/02226550/TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02226550-MF_DIS-20220922093955-3221010102432.ota","extension":".ota","released":"2022-11-08T09:32:08","salesRegion":"eu","length":208174},{"blob":null,"identity":{"company":4489,"product":188,"version":{"major":2,"minor":33,"build":101,"revision":80}},"releaseNotes":"1. Improve CCT accuracy\r\n2. Fix bug that identify fail when Lums are turned off.\r\n3. Add overtemperature protection function.\r\n4. Production line support function in manufacture mode.\r\n5. EMMA feature supported.\r\n6. V02216550 is the PP FW.","shA256":"38d1d8c80b94ba58e2b38c3191a38e2fc90823f5d70fcdaaf1a4ccecd40ffa1e","name":"TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02216550-MF_DIS-20220722105857-3221010102432.ota","productName":"PL HCL300x1200 01","fullName":"PL_HCL300x1200_01/02216550/TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02216550-MF_DIS-20220722105857-3221010102432.ota","extension":".ota","released":"2022-09-02T10:19:05","salesRegion":"eu","length":207706},{"blob":null,"identity":{"company":4489,"product":149,"version":{"major":1,"minor":10,"build":100,"revision":0}},"releaseNotes":"1. Fix bug that Biolux Gen I luminaire lost HCL mode occasionally.","shA256":"82d6c7821c27f7b7d61e3a09fbd80be945e824b8a934ba4325b03558741b0509","name":"PL_HCL600_01_IM0095_010A6400-encrypted_202331101118_withoutMF.OTA","productName":"PL HCL600 01","fullName":"PL_HCL600_01/010a6400/PL_HCL600_01_IM0095_010A6400-encrypted_202331101118_withoutMF.OTA","extension":".OTA","released":"2023-12-15T05:08:33","salesRegion":"eu","length":179680},{"blob":null,"identity":{"company":4489,"product":149,"version":{"major":1,"minor":9,"build":100,"revision":0}},"releaseNotes":"\r\n1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix leave network issue when update link key fail.","shA256":"9e2f9f7ac3f9e8d9f487618f3ad3e7ed77456b1258562d7137501e61de4ceece","name":"PL_HCL600_01_IM0095_01096400-encrypted_202301071159_withoutMF.OTA","productName":"PL HCL600 01","fullName":"PL_HCL600_01/01096400/PL_HCL600_01_IM0095_01096400-encrypted_202301071159_withoutMF.OTA","extension":".OTA","released":"2023-07-18T04:36:22","salesRegion":"eu","length":179712},{"blob":null,"identity":{"company":4489,"product":185,"version":{"major":2,"minor":35,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"464bf793bfe678fc7485ec0e585a842de344c1cbf4a7aa4a247eac1ed3414dd6","name":"TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02236550-MF_DIS-20230609100910-3221010102432.ota","productName":"PL HCL600 02","fullName":"PL_HCL600_02/02236550/TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02236550-MF_DIS-20230609100910-3221010102432.ota","extension":".ota","released":"2023-08-31T08:33:37","salesRegion":"eu","length":208078},{"blob":null,"identity":{"company":4489,"product":185,"version":{"major":2,"minor":34,"build":101,"revision":80}},"releaseNotes":"1. Enable permanent beacon request before Luminaire finish network paring.\r\n2. Patch for Zigbee3.0 certification.\r\n3. Improve fading function.","shA256":"d6fe85d7dad9983b39dc1453f0696dea74236f55d847889d8f9b15a155b3b44f","name":"TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02226550-MF_DIS-20220922091954-3221010102432.ota","productName":"PL HCL600 02","fullName":"PL_HCL600_02/02226550/TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02226550-MF_DIS-20220922091954-3221010102432.ota","extension":".ota","released":"2022-11-08T09:33:47","salesRegion":"eu","length":208198},{"blob":null,"identity":{"company":4489,"product":185,"version":{"major":2,"minor":33,"build":101,"revision":80}},"releaseNotes":"1. Improve CCT accuracy\r\n2. Fix bug that identify fail when Lums are turned off.\r\n3. Add overtemperature protection function.\r\n4. Production line support function in manufacture mode.\r\n5. EMMA feature supported.\r\n6. V02216550 is the PP FW.","shA256":"4e853f65b202516e2dfbc56d7b0c81ceb8be61c4b6c2f9e836d2dc0271176a91","name":"TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02216550-MF_DIS-20220722103908-3221010102432.ota","productName":"PL HCL600 02","fullName":"PL_HCL600_02/02216550/TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02216550-MF_DIS-20220722103908-3221010102432.ota","extension":".ota","released":"2022-09-02T10:20:11","salesRegion":"eu","length":207722},{"blob":null,"identity":{"company":4489,"product":148,"version":{"major":1,"minor":10,"build":100,"revision":0}},"releaseNotes":"1. Fix bug that Biolux Gen I luminaire lost HCL mode occasionally.","shA256":"0103884bd46b3d1363458ecc04819d725c57998e23378647e91ea86780334ec0","name":"PL_HCL625_01_IM0094_010A6400-encrypted_202331101122_withoutMF.OTA","productName":"PL HCL625 01","fullName":"PL_HCL625_01/010a6400/PL_HCL625_01_IM0094_010A6400-encrypted_202331101122_withoutMF.OTA","extension":".OTA","released":"2023-12-15T05:10:28","salesRegion":"eu","length":179680},{"blob":null,"identity":{"company":4489,"product":148,"version":{"major":1,"minor":9,"build":100,"revision":0}},"releaseNotes":"\r\n1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix leave network issue when update link key fail.","shA256":"bc43d143a533e49e65f0301b285df83cec2c37b18d16a63eeeec3c7c2b5af067","name":"PL_HCL625_01_IM0094_01096400-encrypted_202301071204_withoutMF.OTA","productName":"PL HCL625 01","fullName":"PL_HCL625_01/01096400/PL_HCL625_01_IM0094_01096400-encrypted_202301071204_withoutMF.OTA","extension":".OTA","released":"2023-07-18T04:37:07","salesRegion":"eu","length":179712},{"blob":null,"identity":{"company":4489,"product":186,"version":{"major":2,"minor":35,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"1e42003a6937abae895c20060dff1115ac78f08a2235c9958174082e53520ba9","name":"TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02236550-MF_DIS-20230609101747-3221010102432.ota","productName":"PL HCL625 02","fullName":"PL_HCL625_02/02236550/TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02236550-MF_DIS-20230609101747-3221010102432.ota","extension":".ota","released":"2023-08-31T08:34:35","salesRegion":"eu","length":208078},{"blob":null,"identity":{"company":4489,"product":186,"version":{"major":2,"minor":34,"build":101,"revision":80}},"releaseNotes":"1. Enable permanent beacon request before Luminaire finish network paring.\r\n2. Patch for Zigbee3.0 certification.\r\n3. Improve fading function.","shA256":"3cbab41851519ec0010ab1ae9d4b6827e0baccbfc05f5baf93d52244802293f5","name":"TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02226550-MF_DIS-20220922092614-3221010102432.ota","productName":"PL HCL625 02","fullName":"PL_HCL625_02/02226550/TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02226550-MF_DIS-20220922092614-3221010102432.ota","extension":".ota","released":"2022-11-08T09:35:16","salesRegion":"eu","length":208198},{"blob":null,"identity":{"company":4489,"product":186,"version":{"major":2,"minor":33,"build":101,"revision":80}},"releaseNotes":"1. Improve CCT accuracy\r\n2. Fix bug that identify fail when Lums are turned off.\r\n3. Add overtemperature protection function.\r\n4. Production line support function in manufacture mode.\r\n5. EMMA feature supported.\r\n6. V02216550 is the PP FW.","shA256":"48e2a7c41c38f72f816a48d550d558ddfce45c584b7e755b9d5a461bcf3a759b","name":"TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02216550-MF_DIS-20220722104616-3221010102432.ota","productName":"PL HCL625 02","fullName":"PL_HCL625_02/02216550/TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02216550-MF_DIS-20220722104616-3221010102432.ota","extension":".ota","released":"2022-09-02T10:21:24","salesRegion":"eu","length":207722},{"blob":null,"identity":{"company":4489,"product":130,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"33e9a03c18ec013f79a195af7ba5ae4c958ea4fec55dcd7d1560af1d7e50947b","name":"DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x02116550-MF_DIS-20230710041844-3221010102432.ota","productName":"PL Indivi600 04","fullName":"PL_Indivi600_04/02116550/DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x02116550-MF_DIS-20230710041844-3221010102432.ota","extension":".ota","released":"2023-08-31T09:07:27","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":130,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"61ec89560eb1ce2df7bd5497a97e241dda7ff7d5bd60052d38760c3ee65517b5","name":"DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x020E6550-MF_DIS-20220124045523-....ota","productName":"PL Indivi600 04","fullName":"PL_Indivi600_04/020e6550/DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x020E6550-MF_DIS-20220124045523-....ota","extension":".ota","released":"2022-04-28T07:13:54","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":130,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PL_Indivi600_04.\r\n4.This is the same version as PP firmware.","shA256":"470e5abbc91580fb847fd8aa77e89b9e8c33bd0cc153346f429d8cf68fdf00e5","name":"DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x02056550-MF_DIS-20201225180539.ota","productName":"PL Indivi600 04","fullName":"PL_Indivi600_04/02056550/DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x02056550-MF_DIS-20201225180539.ota","extension":".ota","released":"2021-04-20T10:22:28","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":131,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"f84f49c7a6fd068d8a58607f751d69e1d0fb5dafbda9e040c64c3883ce1c8748","name":"DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x02116550-MF_DIS-20230710042543-3221010102432.ota","productName":"PL Indivi625 04","fullName":"PL_Indivi625_04/02116550/DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x02116550-MF_DIS-20230710042543-3221010102432.ota","extension":".ota","released":"2023-08-31T09:08:27","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":131,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"4037ef83214b6c89e7332069c383d2538e010f2ad569bde4d1491893e93f8f34","name":"DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x020E6550-MF_DIS-20220124050038-....ota","productName":"PL Indivi625 04","fullName":"PL_Indivi625_04/020e6550/DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x020E6550-MF_DIS-20220124050038-....ota","extension":".ota","released":"2022-04-28T07:14:22","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":131,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PL_Indivi625_04.\r\n4.This is the same version as PP firmware.","shA256":"739e83adecc39772211ffbaacf1911e73e1331ee4d0d343ba91d0d1c7b337b26","name":"DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x02056550-MF_DIS-20201225181957.ota","productName":"PL Indivi625 04","fullName":"PL_Indivi625_04/02056550/DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x02056550-MF_DIS-20201225181957.ota","extension":".ota","released":"2021-04-20T10:23:10","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":137,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"f1ed515e8694923270d4f077900540ec8e1b38fc3a3e74c9c06522b83cda04f8","name":"DIM_UART-PL_PFM600_04-0x1189-0x0089-0x02116550-MF_DIS-20230710045334-3221010102432.ota","productName":"PL PFM600 04","fullName":"PL_PFM600_04/02116550/DIM_UART-PL_PFM600_04-0x1189-0x0089-0x02116550-MF_DIS-20230710045334-3221010102432.ota","extension":".ota","released":"2023-08-31T09:09:25","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":137,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"e8542cfaa58a5a8a83bd25a0065c4e641c0817af390f5dae8341c4b5af82aa91","name":"DIM_UART-PL_PFM600_04-0x1189-0x0089-0x020E6550-MF_DIS-20220124053204-322....ota","productName":"PL PFM600 04","fullName":"PL_PFM600_04/020e6550/DIM_UART-PL_PFM600_04-0x1189-0x0089-0x020E6550-MF_DIS-20220124053204-322....ota","extension":".ota","released":"2022-04-28T07:14:50","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":137,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PL_PFM600_04.\r\n4.This is the same version as PP firmware.","shA256":"c46b4736e39cfcfa5a0b59638b5dee1b0687ce5224a8d35912c675779885c5a8","name":"DIM_UART-PL_PFM600_04-0x1189-0x0089-0x02056550-MF_DIS-20201225162930.ota","productName":"PL PFM600 04","fullName":"PL_PFM600_04/02056550/DIM_UART-PL_PFM600_04-0x1189-0x0089-0x02056550-MF_DIS-20201225162930.ota","extension":".ota","released":"2021-04-20T10:23:57","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":128,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"01d40254c63cbe9c00c8a6b35675e871774cadefb2a8d44ccd00933fed3f2c13","name":"DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x02116550-MF_DIS-20230710040457-3221010102432.ota","productName":"PL PFM600UGR 04","fullName":"PL_PFM600UGR_04/02116550/DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x02116550-MF_DIS-20230710040457-3221010102432.ota","extension":".ota","released":"2023-08-31T09:10:15","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":128,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"aceee18ace08f78873e62afce832d2d14fc18960917a225aada16aea4e688a1a","name":"DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x020E6550-MF_DIS-20220124044457-....ota","productName":"PL PFM600UGR 04","fullName":"PL_PFM600UGR_04/020e6550/DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x020E6550-MF_DIS-20220124044457-....ota","extension":".ota","released":"2022-04-28T07:18:08","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":127,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"7c6fe5b6194ac34ddb11dad3404b8d4fe7c1698f032dcbfe54b281a035235172","name":"DIM_UART-PL_PFM625_04-0x1189-0x007F-0x02116550-MF_DIS-20230710035807-3221010102432.ota","productName":"PL PFM625 04","fullName":"PL_PFM625_04/02116550/DIM_UART-PL_PFM625_04-0x1189-0x007F-0x02116550-MF_DIS-20230710035807-3221010102432.ota","extension":".ota","released":"2023-08-31T09:11:44","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":127,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"f368b1fb16c31f7009f6697a01917b2382d2235199952bc128694c0a59f757f9","name":"DIM_UART-PL_PFM625_04-0x1189-0x007F-0x020E6550-MF_DIS-20220124043947-322....ota","productName":"PL PFM625 04","fullName":"PL_PFM625_04/020e6550/DIM_UART-PL_PFM625_04-0x1189-0x007F-0x020E6550-MF_DIS-20220124043947-322....ota","extension":".ota","released":"2022-04-28T07:20:14","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":127,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PL_PFM625_04.\r\n4.This is the same version as PP firmware.","shA256":"730af105ec5349c8037efbdcd3898738bbba96ce827c3b4b750ae9ff32c1335a","name":"DIM_UART-PL_PFM625_04-0x1189-0x007F-0x02056550-MF_DIS-20201225170032.ota","productName":"PL PFM625 04","fullName":"PL_PFM625_04/02056550/DIM_UART-PL_PFM625_04-0x1189-0x007F-0x02056550-MF_DIS-20201225170032.ota","extension":".ota","released":"2021-04-20T10:26:35","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":129,"version":{"major":2,"minor":17,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement.","shA256":"189a03bc5fe419127649cd09a333fd96051367e90e7feeea0937c931ec861101","name":"DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x02116550-MF_DIS-20230710041154-3221010102432.ota","productName":"PL PFM625UGR 04","fullName":"PL_PFM625UGR_04/02116550/DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x02116550-MF_DIS-20230710041154-3221010102432.ota","extension":".ota","released":"2023-08-31T09:12:20","salesRegion":"eu","length":197986},{"blob":null,"identity":{"company":4489,"product":129,"version":{"major":2,"minor":14,"build":101,"revision":80}},"releaseNotes":"1.Power report using cluster 0x0B04, attribute 0x0304\r\n2.Maximum group number is 12.\r\n3.Reply group capacity with meaningful value.\r\n4.Support ZLO command.\r\n5.GTIN report for EMMA.","shA256":"781ade587d3ee140dcc7cf795be35b091ab9ee2b0afbf57654eea4ad990fa418","name":"DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x020E6550-MF_DIS-20220124045012-....ota","productName":"PL PFM625UGR 04","fullName":"PL_PFM625UGR_04/020e6550/DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x020E6550-MF_DIS-20220124045012-....ota","extension":".ota","released":"2022-04-28T07:21:56","salesRegion":"eu","length":197778},{"blob":null,"identity":{"company":4489,"product":129,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1.Support move to level time value 0xffff. \r\n2.Keep on-off status when identify done.\r\n3.Zigbee device name is PL_PFM625UGR_04.\r\n4.This is the same version as PP firmware.","shA256":"9e7454e4f7514e2f73cbbff98d28cd4abd1e51fb2d0893b770c1ae8a0e796fe9","name":"DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x02056550-MF_DIS-20201225174925.ota","productName":"PL PFM625UGR 04","fullName":"PL_PFM625UGR_04/02056550/DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x02056550-MF_DIS-20201225174925.ota","extension":".ota","released":"2021-04-20T10:27:29","salesRegion":"eu","length":192006},{"blob":null,"identity":{"company":4489,"product":194,"version":{"major":3,"minor":32,"build":54,"revision":114}},"releaseNotes":"1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB","shA256":"1511499e7eca38651df0e36cbd70a2e315a3a1e26d3043db926146e6de9fce52","name":"PLUG_OUTDOOR_EU_T-0x00C2-0x03203672.OTA","productName":"PLUG OUTDOOR EU T","fullName":"PLUG OUTDOOR EU T/03203672/PLUG_OUTDOOR_EU_T-0x00C2-0x03203672.OTA","extension":".OTA","released":"2022-09-01T06:12:24","salesRegion":"eu","length":182480},{"blob":null,"identity":{"company":4489,"product":103,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"- LQI attribute reporting improved\r\n- Turn On/Off fading time configuration supported\r\n- On with time off Command supported","shA256":"e613a60110f62983690644f80d5ffa51635c3b41b9e43485a684c5790c48490e","name":"PLUG-Plug_Value-0x1189-0x0067-0x02056550-MF_DIS-20201216170637.ota","productName":"Plug Value","fullName":"Plug Value/02056550/PLUG-Plug_Value-0x1189-0x0067-0x02056550-MF_DIS-20201216170637.ota","extension":".ota","released":"2021-01-05T07:01:04","salesRegion":"eu","length":190306},{"blob":null,"identity":{"company":4489,"product":45,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. upgrade zigbee stack;","shA256":"0b68057c3821554f1aa1d54b56d5c72f24d5a52c20dbda09feb5ae2f024736f9","name":"Plug_Z3_IM002D_00103101-encrypted_12_07_2018_Fri_103650_94_withoutMF.ota","productName":"Plug Z3","fullName":"Plug Z3/00103101/Plug_Z3_IM002D_00103101-encrypted_12_07_2018_Fri_103650_94_withoutMF.ota","extension":".ota","released":"2019-09-25T07:30:47","salesRegion":"eu","length":178996},{"blob":null,"identity":{"company":4364,"product":39,"version":{"major":1,"minor":2,"build":5,"revision":9}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"c70126e43666f0a83077e8d93c587b7c0cefcd37d4719ebbeab1e33090b7c3b5","name":"ZLL_Plug01_OnOff_MK_0x01020509.ota","productName":"Plug01 OnOff MK","fullName":"Plug01 OnOff MK/01020509/ZLL_Plug01_OnOff_MK_0x01020509.ota","extension":".ota","released":"2019-03-14T06:21:58","salesRegion":"eu","length":121680},{"blob":null,"identity":{"company":4489,"product":29,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"f32aeb4548212d48411356a64771f7d8157349349e27dc213f13deb557658905","name":"RT_RGBW_IMG001D_00102428-encrypted.ota","productName":"RT RGBW","fullName":"RT RGBW/00102428/RT_RGBW_IMG001D_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:52:23","salesRegion":"us","length":179088},{"blob":null,"identity":{"company":4489,"product":28,"version":{"major":0,"minor":16,"build":36,"revision":40}},"releaseNotes":"• Fix Move command bugs\r\n• OTA improvements\r\n• Wakeup/Sunrise feature improvements\r\n• Attribute reporting improvements","shA256":"00dde1abbf25798b6dfb7353ff5db2cca0e33eaeb711706e751d1ef0481eba26","name":"RT_TW_IMG001C_00102428-encrypted.ota","productName":"RT TW","fullName":"RT TW/00102428/RT_TW_IMG001C_00102428-encrypted.ota","extension":".ota","released":"2019-02-28T16:52:51","salesRegion":"us","length":170776},{"blob":null,"identity":{"company":4364,"product":46,"version":{"major":1,"minor":2,"build":5,"revision":9}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"92aeef4fe61e7e31680831cfa0a0cdbfeb5ab7a21c4997be79240463b986f371","name":"ZLL_SubstiTube_W_MK_0x01020509.ota","productName":"SubstiTube W MK","fullName":"SubstiTube W MK/01020509/ZLL_SubstiTube_W_MK_0x01020509.ota","extension":".ota","released":"2019-03-14T06:23:04","salesRegion":"eu","length":123440},{"blob":null,"identity":{"company":4364,"product":4,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"95e525ff236fd4541cdfffb861711748a365fe61c78b43bf9764181f662c899e","name":"ZLL_MK_0x01020510_Surface_Light_TW.ota","productName":"Surface Light TW","fullName":"Surface Light TW/01020510/ZLL_MK_0x01020510_Surface_Light_TW.ota","extension":".ota","released":"2019-03-14T06:19:06","salesRegion":"eu","length":131904},{"blob":null,"identity":{"company":4364,"product":9,"version":{"major":1,"minor":2,"build":5,"revision":16}},"releaseNotes":"1. Stack upgrade;\r\n2. Improve network performance.","shA256":"5b4e2a485c0bfb4efbf4607f22d698f0dd1b802ef3b56da1d0c912f02113087f","name":"ZLL_MK_0x01020510_Surface_Light_W.ota","productName":"Surface Light W","fullName":"Surface Light W/01020510/ZLL_MK_0x01020510_Surface_Light_W.ota","extension":".ota","released":"2019-03-14T06:20:20","salesRegion":"eu","length":123884},{"blob":null,"identity":{"company":4489,"product":44,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"b7eb75a66b7963eebcc20515cff7bf2743d541b18baa885abe69a1ff0057a0cc","name":"Tibea_TW_Z3_IM002C_01056400-encrypted_202129091201_withoutMF.ota","productName":"Tibea TW Z3","fullName":"Tibea TW Z3/01056400/Tibea_TW_Z3_IM002C_01056400-encrypted_202129091201_withoutMF.ota","extension":".ota","released":"2021-10-21T05:31:17","salesRegion":"eu","length":185972},{"blob":null,"identity":{"company":4489,"product":44,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"98f7dbed74edd1a969881e02773f1aad1029a04a9cffbcfab4420ce286cdfeea","name":"Tibea_TW_Z3_IM002C_00103101-encrypted_11_23_2018_Fri_163423_97_withoutMF.ota","productName":"Tibea TW Z3","fullName":"Tibea TW Z3/00103101/Tibea_TW_Z3_IM002C_00103101-encrypted_11_23_2018_Fri_163423_97_withoutMF.ota","extension":".ota","released":"2019-03-22T08:19:21","salesRegion":"eu","length":183628},{"blob":null,"identity":{"company":4489,"product":222,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"cf4a53be8bd1157ee74a73fd469239dbd10ae9924802cc77e13651a72771d99e","name":"DIM_ENERGY-TUBE_T8_CON_1200_16W_830ZBVR-0x1189-0x00DE-0x02056550-MF_DIS-20230612083031-322101076832.ota","productName":"TUBE T8 CON 1200 16W 830ZBVR","fullName":"TUBE_T8_CON_1200_16W_830ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_1200_16W_830ZBVR-0x1189-0x00DE-0x02056550-MF_DIS-20230612083031-322101076832.ota","extension":".ota","released":"2023-08-31T10:20:44","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":199,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"4f15d2212066986da15cd671b4e28267ee3674dded69055e8b9360b255b7ba4f","name":"DIM_ENERGY-TUBE_T8_CON_1200_16W_840ZBVR-0x1189-0x00C7-0x02056550-MF_DIS-20230612075626-322101076832.ota","productName":"TUBE T8 CON 1200 16W 840ZBVR","fullName":"TUBE_T8_CON_1200_16W_840ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_1200_16W_840ZBVR-0x1189-0x00C7-0x02056550-MF_DIS-20230612075626-322101076832.ota","extension":".ota","released":"2023-08-31T10:21:43","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":200,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"edd7eb070a3f89adcbba2fbeed5fbfd7e6109a4802ba9c4d60f6bdd101147cd7","name":"DIM_ENERGY-TUBE_T8_CON_1200_16W_865ZBVR-0x1189-0x00C8-0x02056550-MF_DIS-20230612080310-322101076832.ota","productName":"TUBE T8 CON 1200 16W 865ZBVR","fullName":"TUBE_T8_CON_1200_16W_865ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_1200_16W_865ZBVR-0x1189-0x00C8-0x02056550-MF_DIS-20230612080310-322101076832.ota","extension":".ota","released":"2023-08-31T10:22:29","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":221,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"c8c3eee6728b47cdb92c89b6f07cced72de67646e435febeb67dbc64bbb5cefd","name":"DIM_ENERGY-TUBE_T8_CON_1500_24W_830ZBVR-0x1189-0x00DD-0x02056550-MF_DIS-20230612083725-322101076832.ota","productName":"TUBE T8 CON 1500 24W 830ZBVR","fullName":"TUBE_T8_CON_1500_24W_830ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_1500_24W_830ZBVR-0x1189-0x00DD-0x02056550-MF_DIS-20230612083725-322101076832.ota","extension":".ota","released":"2023-08-31T10:23:22","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":201,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"c08466e304ec92437785ce01c8ca38535987b53c29be2f4539e050adff6ed0c3","name":"DIM_ENERGY-TUBE_T8_CON_1500_24W_840ZBVR-0x1189-0x00C9-0x02056550-MF_DIS-20230612081003-322101076832.ota","productName":"TUBE T8 CON 1500 24W 840ZBVR","fullName":"TUBE_T8_CON_1500_24W_840ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_1500_24W_840ZBVR-0x1189-0x00C9-0x02056550-MF_DIS-20230612081003-322101076832.ota","extension":".ota","released":"2023-08-31T10:24:07","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":202,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"ac618fff2722d17e2441726b66db44fe07188094495b5bd3bec99b4624c10df8","name":"DIM_ENERGY-TUBE_T8_CON_1500_24W_865ZBVR-0x1189-0x00CA-0x02056550-MF_DIS-20230612081654-322101076832.ota","productName":"TUBE T8 CON 1500 24W 865ZBVR","fullName":"TUBE_T8_CON_1500_24W_865ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_1500_24W_865ZBVR-0x1189-0x00CA-0x02056550-MF_DIS-20230612081654-322101076832.ota","extension":".ota","released":"2023-08-31T10:24:38","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":223,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"5315cae79bc6c95d651ddffe7a8799878da7590aa0f67137240f17c3687b8665","name":"DIM_ENERGY-TUBE_T8_CON_600_7_5W_830ZBVR-0x1189-0x00DF-0x02056550-MF_DIS-20230704080741-322101076832.ota","productName":"TUBE T8 CON 600 7 5W 830ZBVR","fullName":"TUBE_T8_CON_600_7_5W_830ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_600_7_5W_830ZBVR-0x1189-0x00DF-0x02056550-MF_DIS-20230704080741-322101076832.ota","extension":".ota","released":"2023-08-31T10:02:46","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":203,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"8833aa8e31e590334c88a1b98c8ad41e7bb816aede85c6970ad24ad96f511037","name":"DIM_ENERGY-TUBE_T8_CON_600_7_5W_840ZBVR-0x1189-0x00CB-0x02056550-MF_DIS-20230704075326-322101076832.ota","productName":"TUBE T8 CON 600 7 5W 840ZBVR","fullName":"TUBE_T8_CON_600_7_5W_840ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_600_7_5W_840ZBVR-0x1189-0x00CB-0x02056550-MF_DIS-20230704075326-322101076832.ota","extension":".ota","released":"2023-08-31T10:18:46","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":204,"version":{"major":2,"minor":5,"build":101,"revision":80}},"releaseNotes":"1. Support GP switch.\r\n2. Improve network performance by fine tune router table.","shA256":"ea5aabe0e8d4993e4ea947f1b4b1da3d1d8ae0008c32e8f2b94a431c372d3671","name":"DIM_ENERGY-TUBE_T8_CON_600_7_5W_865ZBVR-0x1189-0x00CC-0x02056550-MF_DIS-20230704080035-322101076832.ota","productName":"TUBE T8 CON 600 7 5W 865ZBVR","fullName":"TUBE_T8_CON_600_7_5W_865ZBVR/02056550/DIM_ENERGY-TUBE_T8_CON_600_7_5W_865ZBVR-0x1189-0x00CC-0x02056550-MF_DIS-20230704080035-322101076832.ota","extension":".ota","released":"2023-08-31T10:19:46","salesRegion":"eu","length":197266},{"blob":null,"identity":{"company":4489,"product":70,"version":{"major":1,"minor":5,"build":100,"revision":0}},"releaseNotes":"Support ZLO","shA256":"a5f108a6b901b93822354d7a1f4929434538c6a033937050acef5552ee2e3654","name":"Undercabinet_TW_Z3_IM0046_01056400-encrypted_202129091204_withoutMF.ota","productName":"Undercabinet TW Z3","fullName":"Undercabinet TW Z3/01056400/Undercabinet_TW_Z3_IM0046_01056400-encrypted_202129091204_withoutMF.ota","extension":".ota","released":"2021-10-21T05:31:51","salesRegion":"eu","length":185980},{"blob":null,"identity":{"company":4489,"product":70,"version":{"major":0,"minor":16,"build":49,"revision":1}},"releaseNotes":"1. Faster Joining\r\n2. Network performance improvement\r\n","shA256":"af295b1890e63625b123c4ba241563d676e26f86bb25511bd01c25a76e8a1166","name":"Undercabinet_TW_Z3_IM0046_00103101-encrypted_11_20_2018_Tue_101550_96_withoutMF.ota","productName":"Undercabinet TW Z3","fullName":"Undercabinet TW Z3/00103101/Undercabinet_TW_Z3_IM0046_00103101-encrypted_11_20_2018_Tue_101550_96_withoutMF.ota","extension":".ota","released":"2019-03-22T08:19:56","salesRegion":"eu","length":183636},{"blob":null,"identity":{"company":4489,"product":152,"version":{"major":16,"minor":19,"build":37,"revision":3}},"releaseNotes":"Fix router request to avoid route table full.","shA256":"ef0addeb9d24d10e6b0ca3244f605b9a3fdd788812590dd88a72d9ea1f370bdd","name":"VIVARES_PBC4_01_0X0098_0x10132503.ota","productName":"VIVARES PBC4 01","fullName":"VIVARES_PBC4_01/10132503/VIVARES_PBC4_01_0X0098_0x10132503.ota","extension":".ota","released":"2023-07-18T04:30:48","salesRegion":"eu","length":158673},{"blob":null,"identity":{"company":4489,"product":152,"version":{"major":16,"minor":16,"build":37,"revision":3}},"releaseNotes":"1.Add reset function by 10s long press prog button\r\n2.Fix network pairig issue. (The issue is that only 6 beacon request sent out and then PBC stop beacon request)\r\n3.Fix the issue that push button configure for 4 channels missing after OTA upgrade.\r\n","shA256":"fe0b9723e5a409ec19c74ae36051d75534bbf173fb3021d80d8947303520d9d7","name":"VIVARES_PBC4_01_0x10102503.ota","productName":"VIVARES PBC4 01","fullName":"VIVARES_PBC4_01/10102503/VIVARES_PBC4_01_0x10102503.ota","extension":".ota","released":"2021-09-29T12:19:43","salesRegion":"eu","length":231538},{"blob":null,"identity":{"company":4489,"product":152,"version":{"major":16,"minor":4,"build":37,"revision":3}},"releaseNotes":"1. Reset by 5 times power recycle\r\n2. Fix clear channel bug\r\n3. Send device annouce after reboot","shA256":"8ec8084e7b1678211c8d3e907f6bdde931a866df75049cb55c5e806bc2119cb6","name":"VIVARES_PBC4_01_0x10042503.ota","productName":"VIVARES PBC4 01","fullName":"VIVARES_PBC4_01/10042503/VIVARES_PBC4_01_0x10042503.ota","extension":".ota","released":"2021-05-26T13:19:27","salesRegion":"eu","length":232054},{"blob":null,"identity":{"company":4489,"product":124,"version":{"major":17,"minor":67,"build":102,"revision":48}},"releaseNotes":"1. Fix bug that sensor freeze after long time running in big system.\r\n2. Fix bug that sensor automatic left network occasionally.","shA256":"9f297b89035f6f01ffde8000f7a79271a9748b983f83a0c7f6f3113a7aeaa221","name":"1189_007c_11436630_Release.ota","productName":"VIVARES SENS 00","fullName":"VIVARES_SENS_00/11436630/1189_007c_11436630_Release.ota","extension":".ota","released":"2024-02-06T11:37:07","salesRegion":"eu","length":238322},{"blob":null,"identity":{"company":4489,"product":124,"version":{"major":17,"minor":49,"build":102,"revision":48}},"releaseNotes":" 1. Blink red LED indicator when sensor receives identify command.\r\n 2. Fix network paring bug that related with network link key.\r\n 3. Add Zigbee cluster for Zigbee certification test.\r\n 4. Support GTIN reporting for EMMA.","shA256":"0a7b5d495d7fc8452877d4e8d98260920fb131be596de1713389b5179f9fe57a","name":"VIVARES_SENS_00-0x1189-007C-0x11316630-upgradeMe.ota","productName":"VIVARES SENS 00","fullName":"VIVARES_SENS_00/11316630/VIVARES_SENS_00-0x1189-007C-0x11316630-upgradeMe.ota","extension":".ota","released":"2022-05-27T09:26:33","salesRegion":"eu","length":238110},{"blob":null,"identity":{"company":4489,"product":124,"version":{"major":17,"minor":29,"build":102,"revision":48}},"releaseNotes":"1. Fix traffic storm issue\r\n2. Reset of 5 times power recycle\r\n3. Network joining parameter fine tune.","shA256":"77ebb0e7c8c1b278bbda18c57fc2917c169fb157b6a2e8cde6ee69fcf5bbfae1","name":"1189-007C-0x111D6630-upgradeMe-U150B150-SD5.ota","productName":"VIVARES SENS 00","fullName":"VIVARES_SENS_00/111d6630/1189-007C-0x111D6630-upgradeMe-U150B150-SD5.ota","extension":".ota","released":"2021-06-02T08:38:51","salesRegion":"eu","length":235489},{"blob":null,"identity":{"company":4489,"product":124,"version":{"major":17,"minor":16,"build":102,"revision":48}},"releaseNotes":"Solve rippler issue.\r\nThe Zigbee device name is VIVARES_SENSOR_00.\r\nV11106630 is PP firmware of VIVARES Combined Sensor.","shA256":"22d3a310e7cac9493e2cecaf81a469664c5e7926ef529220ea840298bc9409e8","name":"VIVARES_SENS_00-0x1189-0x007C-0x11106630-upgradeMe.ota","productName":"VIVARES SENS 00","fullName":"VIVARES_SENS_00/11106630/VIVARES_SENS_00-0x1189-0x007C-0x11106630-upgradeMe.ota","extension":".ota","released":"2021-04-20T06:10:44","salesRegion":"eu","length":235694},{"blob":null,"identity":{"company":4489,"product":151,"version":{"major":17,"minor":67,"build":102,"revision":48}},"releaseNotes":"1. Fix bug that sensor freezing after long time running.\r\n2. Fix bug that sensor automatic leave network occasionally.\r\n3. Fix manual reset fail if press more than 15 seconds.","shA256":"229999a73ca6ef971c455d7c405bd6c7a2c8aecfdfd95710ef6f0258d49e51d0","name":"1189_0097_11436630_Release.ota","productName":"VIVARES SENS 01","fullName":"VIVARES_SENS_01/11436630/1189_0097_11436630_Release.ota","extension":".ota","released":"2024-02-06T11:38:52","salesRegion":"eu","length":240006},{"blob":null,"identity":{"company":4489,"product":151,"version":{"major":17,"minor":40,"build":102,"revision":48}},"releaseNotes":" 1. Blink red LED indicator when sensor receives identify command.\r\n 2. Fix network paring bug that related with network link key.\r\n 3. Add Zigbee cluster for Zigbee certification test.\r\n 4. Support GTIN reporting for EMMA.","shA256":"79c247bf0e4642672dbb9587ab3a0e61545f0d5fd40fc116fb0c5d26825a122e","name":"VIVARES_SENS_01-0x1189-0097-0x11286630-upgradeMe.ota","productName":"VIVARES SENS 01","fullName":"VIVARES_SENS_01/11286630/VIVARES_SENS_01-0x1189-0097-0x11286630-upgradeMe.ota","extension":".ota","released":"2022-05-27T09:27:44","salesRegion":"eu","length":239750},{"blob":null,"identity":{"company":4489,"product":151,"version":{"major":17,"minor":6,"build":102,"revision":48}},"releaseNotes":"1.Reset by 5 times power recycle, time interval is 2s~7s.\r\n2.Network joining parameter fine tune.\r\n3.Beacon request every 15 second after power up 30 minutes.","shA256":"984a1dfd897e1d6319ed063c6825dddc68351e905bbf665538d08d11a28d5044","name":"VIVARES_SENS_01-1189-0097-0x11066630-upgradeMe.ota","productName":"VIVARES SENS 01","fullName":"VIVARES_SENS_01/11066630/VIVARES_SENS_01-1189-0097-0x11066630-upgradeMe.ota","extension":".ota","released":"2021-06-28T14:01:33","salesRegion":"eu","length":237174},{"blob":null,"identity":{"company":4364,"product":57374,"version":{"major":16,"minor":14,"build":101,"revision":91}},"releaseNotes":"1. Fix bug that endpoint changes.\r\n2. Supportdim down control from push button coupler.","shA256":"665666b472af72cc3a596b3ee89d4dd2d11107926706b48e10bab21d6aa2b0d6","name":"Zigbee3toDALI_100E655B.ota","productName":"Zigbee 3.0 DALI CONV LI","fullName":"Zigbee 3.0 DALI CONV LI/100e655b/Zigbee3toDALI_100E655B.ota","extension":".ota","released":"2024-02-06T09:54:21","salesRegion":"eu","length":200928},{"blob":null,"identity":{"company":4364,"product":57374,"version":{"major":16,"minor":9,"build":101,"revision":91}},"releaseNotes":"Optimized broadcast and multicast message forwarding in large networks","shA256":"cafca76cb2bc4d25d9bacbb5c4c9b0f34058574d27eba86ae3f8e9dfed9d3668","name":"Zigbee3toDALI_1009655B.ota","productName":"Zigbee 3.0 DALI CONV LI","fullName":"Zigbee 3.0 DALI CONV LI/1009655b/Zigbee3toDALI_1009655B.ota","extension":".ota","released":"2023-08-18T13:39:55","salesRegion":"eu","length":200036},{"blob":null,"identity":{"company":4364,"product":57374,"version":{"major":16,"minor":8,"build":101,"revision":91}},"releaseNotes":"- Reset to Factory, sent via radio, now shows a feedback on the ballast side.","shA256":"1f1c5e623576e1e06af5bcece4cdb85c5d16ab5b55aa85427382d8f510f1e64d","name":"Zigbee3toDALI_1008655B.ota","productName":"Zigbee 3.0 DALI CONV LI","fullName":"Zigbee 3.0 DALI CONV LI/1008655b/Zigbee3toDALI_1008655B.ota","extension":".ota","released":"2022-10-09T05:10:44","salesRegion":"eu","length":200036}]}zigpy-0.80.1/tests/ota/files/local_index.json000066400000000000000000000014401501451476000211470ustar00rootroot00000000000000{ "firmwares": [ { "path": "external/dl/local_provider/1135-0000-201000A0-FLS-PP3_RGBW.zigbee", "file_version": 604050705, "file_size": 291142, "image_type": 559, "manufacturer_names": ["Test Manuf 1", "Test Manuf 2"], "model_names": ["Test Model 1", "Test Model 2"], "manufacturer_id": 4454, "changelog": "A changelog", "release_notes": "Long release notes", "checksum": "sha3-256:23415a1c54353219bb7de3e72ba6050d9e849be0954eebda9b5783d34d0723d1", "min_hardware_version": 0, "max_hardware_version": 257, "min_current_file_version": 0, "max_current_file_version": 257, "specificity": 999999 } ] }zigpy-0.80.1/tests/ota/files/remote_index.json000066400000000000000000000014041501451476000213500ustar00rootroot00000000000000{ "firmwares": [ { "binary_url": "https://example.org/fw/test.ota", "file_version": 604050705, "file_size": 291142, "image_type": 559, "manufacturer_names": ["Test Manuf 1", "Test Manuf 2"], "model_names": ["Test Model 1", "Test Model 2"], "manufacturer_id": 4454, "changelog": "A changelog", "release_notes": "Long release notes", "checksum": "sha3-256:28d4883705ac932160b51f09d2c4697f288d5bb11174677c6e43247288f6c794", "min_hardware_version": 0, "max_hardware_version": 257, "min_current_file_version": 0, "max_current_file_version": 257, "specificity": 999999 } ] }zigpy-0.80.1/tests/ota/files/sonoff_upgrade.json000066400000000000000000000013221501451476000216660ustar00rootroot00000000000000[ { "fw_binary_url": "https://zigbee-ota.sonoff.tech/releases/86-0001-00001101.zigbee", "fw_file_version": 4353, "fw_filesize": 131086, "fw_image_type": 1, "fw_manufacturer_id": 4742, "model_id": "ZBMINI-L" }, { "fw_binary_url": "https://zigbee-ota.sonoff.tech/releases/zigbeeminil2_100E_stand_ota_file.ota", "fw_file_version": 4110, "fw_filesize": 259018, "fw_image_type": 4, "fw_manufacturer_id": 4742, "model_id": "ZBMINIL2" }, { "fw_binary_url": "https://zigbee-ota.sonoff.tech/releases/snzb-06p_v1.0.5.ota", "fw_file_version": 4101, "fw_filesize": 258206, "fw_image_type": 2060, "fw_manufacturer_id": 4742, "model_id": "SNZB-06P" } ]zigpy-0.80.1/tests/ota/files/thirdreality_firmware.json000066400000000000000000000102331501451476000232660ustar00rootroot00000000000000{ "versions": [ { "modelId": "3RAP0149BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/Air_Pressure_Sensor_PROD_OTA_V6_v1.00.06.ota", "version": "1.00.06", "imageType": 54190, "manufacturerId": 4659, "fileVersion": 6 }, { "modelId": "3RSB22BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/Button_PROD_OTA_V28_v1.00.28.ota", "version": "1.00.28", "imageType": 54184, "manufacturerId": 4659, "fileVersion": 28 }, { "modelId": "3RDS17BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/Door_Sensor_PROD_OTA_V48_v1.00.48.ota", "version": "1.00.48", "imageType": 54178, "manufacturerId": 4659, "fileVersion": 48 }, { "modelId": "3RSNL02043Z", "url": "https://tr-zha.s3.amazonaws.com/firmware/FW_ha_v0.00.50.ota", "version": "0.00.50", "imageType": 0, "manufacturerId": 4877, "fileVersion": 50 }, { "modelId": "3RMS16BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/Motion_Sensor_PROD_OTA_V51_v1.00.51.ota", "version": "1.00.51", "imageType": 54177, "manufacturerId": 4659, "fileVersion": 51 }, { "modelId": "TRZB3", "url": "https://tr-zha.s3.amazonaws.com/firmware/SmartCurtainModule_Zigbee_PROD_OTA_V12_v1.00.12.ota", "version": "1.00.12", "imageType": 54186, "manufacturerId": 4659, "fileVersion": 12 }, { "modelId": "3RSB015BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/SmartCurtain_PROD_OTA_V68_v1.00.68.ota", "version": "1.00.68", "imageType": 54183, "manufacturerId": 4659, "fileVersion": 68 }, { "modelId": "3RSP019BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/SmartPlug_Zigbee_PROD_OTA_V74_v1.00.74.ota", "version": "1.00.74", "imageType": 54182, "manufacturerId": 4659, "fileVersion": 268513355 }, { "modelId": "3RSP02028BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/SmartPlug_Zigbee_PROD_OTA_V74_v1.00.74.ota", "version": "1.00.74", "imageType": 54182, "manufacturerId": 4659, "fileVersion": 268513355 }, { "modelId": "3RSPE01044BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/SmartPlug_Zigbee_PROD_OTA_V74_v1.00.74.ota", "version": "1.00.74", "imageType": 54182, "manufacturerId": 4659, "fileVersion": 268513355 }, { "modelId": "3RSS009Z", "url": "https://tr-zha.s3.amazonaws.com/firmware/SmartSwitchGen3_PROD_OTA_V18_v1.00.18.ota", "version": "1.00.18", "imageType": 54181, "manufacturerId": 4659, "fileVersion": 18 }, { "modelId": "3RTHS24BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/TRTL_ThermalSensor_PROD_OTA_V23_1.00.23.ota", "version": "1.00.23", "imageType": 54185, "manufacturerId": 4659, "fileVersion": 23 }, { "modelId": "3RVS01031Z", "url": "https://tr-zha.s3.amazonaws.com/firmware/Vibrate_Sensor_PROD_OTA_V40_v1.00.40.ota", "version": "1.00.40", "imageType": 54187, "manufacturerId": 4659, "fileVersion": 40 }, { "modelId": "3RWS18BZ", "url": "https://tr-zha.s3.amazonaws.com/firmware/Water_Leak_Sensor_PROD_OTA_V56_v1.00.56.ota", "version": "1.00.56", "imageType": 54179, "manufacturerId": 4659, "fileVersion": 56 } ] }zigpy-0.80.1/tests/ota/files/z2m_index.json000066400000000000000000015745421501451476000206100ustar00rootroot00000000000000[ { "fileName": "MainsPowerOutlet_JN5169_PCB_ARC_OTA_0x1409_v22.ota", "fileVersion": 22, "fileSize": 173726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Aurora/MainsPowerOutlet_JN5169_PCB_ARC_OTA_0x1409_v22.ota", "imageType": 5129, "manufacturerCode": 4636, "sha512": "7b1c6733fbf0f081e5e4ebb946616d88ccd482790a5365d0cfa9c2d050a6dddef56291bb749a33c903c57944782d69100562614755202e3be013eaf58ed70e38", "otaHeaderString": "DoubleSocket50AU--UNENC000JN5169" }, { "fileName": "0x1209_0x300e_0x02086a30.ota", "fileVersion": 34105904, "fileSize": 258058, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Bosch/0x1209_0x300e_0x02086a30.ota", "imageType": 12302, "manufacturerCode": 4617, "sha512": "901d7b98b3448df06ba0e87465b9140d80e0ef41cdb2c1880c9788a562bcfefd3cd306f2f737007c7f96ad02b3d230391d0d88940ef9c9e976c792bc80d935d2", "otaHeaderString": "RTH2_230\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "0x1209_0x3011_0x03076a30.ota", "fileVersion": 50817584, "fileSize": 264354, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Bosch/0x1209_0x3011_0x03076a30.ota", "imageType": 12305, "manufacturerCode": 4617, "sha512": "63f232232252af0fbb3c4cc2efe2ce2564101405de4923238c71ccef2e66e6abef022deee45399a886af4019dfce64fdea969ff60f5b4b854e222b5ffcf7bcc2", "otaHeaderString": "RTH2_230\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ota_t0x3002_m0x1209_v0x2a006a30.ota", "fileVersion": 704670256, "fileSize": 254482, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Bosch/ota_t0x3002_m0x1209_v0x2a006a30.ota", "imageType": 12290, "manufacturerCode": 4617, "sha512": "e321384272c1cb5b11bd259ecc8f06d64ae50b193d6057e5221c2d56c55871d1387fdd72c11356f8b9d85f2f68f3906f3b11a49e7140848078ac60ce73723dc3", "otaHeaderString": "PLUG_EU\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "RBSH-SP-ZB-EU" }, { "fileName": "ota_t0x300a_m0x1209_v0x37041514.ota", "fileVersion": 923014420, "fileSize": 168518, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Bosch/ota_t0x300a_m0x1209_v0x37041514.ota", "imageType": 12298, "manufacturerCode": 4617, "sha512": "834531102c17686c831a3e0e0b78b402643a50c42264fa326fbe32213a0c104bc036befcdf51f330cbbf5c6c95d38a028365c3bbaf7feaa453e4d11792f79f36", "otaHeaderString": "RBSH-TRV0-ZB-EU\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "RBSH-TRV0-ZB-EU", "releaseNotes": "1. Fix error E03, which can happen on some radiators after some time" }, { "fileName": "12128_OTA_3.18.ZIGBEE", "fileVersion": 318, "fileSize": 176624, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ClimaxTechnology/12128_OTA_3.18.ZIGBEE", "imageType": 2017, "manufacturerCode": 10132, "sha512": "5e36ceaa44c4b3e12f4ff415b00fb16a3d64aaa3d8bebaecc0590480f4850e8ff226be20356417da223679c4210b5c69e8159ae15628c098c59d074e86d97258", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "PRS3CH1_00.00.05.11TC.zigbee", "fileVersion": 511, "fileSize": 201260, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ClimaxTechnology/PRS3CH1_00.00.05.11TC.zigbee", "imageType": 1025, "manufacturerCode": 10132, "sha512": "76a327ab014803b6f18376343b1f75de474e7e5ad1b6a0e276668052362e6aff2bccf44bf92683217228cd4049fa1b273231284f4a25aac7858b363f2b09e05b", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "PRS3CH1_00.00.05.11TC" }, { "fileName": "PRS3CH2_00.00.05.12TC.zigbee", "fileVersion": 512, "fileSize": 204164, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ClimaxTechnology/PRS3CH2_00.00.05.12TC.zigbee", "imageType": 1025, "manufacturerCode": 10132, "sha512": "904609129ff2445cc36e0a01033d584797bd784a2033defc2dd1b2f269e9318b6f6bd31f8b1d07e308c4281243bfbde72219de347b6c23e3a1fd4300bfe17aa9", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "PRS3CH2_00.00.05.12TC" }, { "fileName": "db15-0203-11003001-z03mmc.zigbee", "fileVersion": 285224961, "fileSize": 131362, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/DIY/db15-0203-11003001-z03mmc.zigbee", "imageType": 515, "manufacturerCode": 56085, "sha512": "2a7a17f348f6631e10217d689456ddeceaf768e06267a76a93335a8fb0ee57dfda88e84830536eb478977e423d3add9ff0590839b8736e465e216d0d81f474e9", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "115C-0004-11040000-ZigbeeXM_101-029_E1_DanalockV3_17.4.0_20221213143911.ota", "fileVersion": 285474816, "fileSize": 204794, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Danalock/115C-0004-11040000-ZigbeeXM_101-029_E1_DanalockV3_17.4.0_20221213143911.ota", "imageType": 4, "manufacturerCode": 4444, "sha512": "44e421aecf5b0c5793a1ba836257f8601408e1dcc2832846b0093e7c2f2a4c100632b8a71ef346b9c611be1d15602bcc3b693002994830b8e857da6e931f6722", "otaHeaderString": "Danalock V3 BTZBE XM\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "V3-BTZBE" }, { "fileName": "115C-0004-11070000-ZigbeeXM_101-029_E1_DanalockV3_17.7.0_20240201155008.ota", "fileVersion": 285671424, "fileSize": 204522, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Danalock/115C-0004-11070000-ZigbeeXM_101-029_E1_DanalockV3_17.7.0_20240201155008.ota", "imageType": 4, "manufacturerCode": 4444, "sha512": "9e45e56b0aef871e1280c678729fdce0a57628d40e569fbc3c26bca5acc2a5c4088a90cbd59c9b6ea378d5c450310e04766b133333b23e7b4520e5c80282e334", "otaHeaderString": "Danalock V3 BTZBE XM\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "PSoC4_1246-0100-01280128.0002_(4CA01CD1).ota", "fileVersion": 284, "fileSize": 391402, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Danfoss/PSoC4_1246-0100-01280128.0002_(4CA01CD1).ota", "imageType": 256, "manufacturerCode": 4678, "sha512": "63dafd3332dd2f8901a5094c5d8437861244b78c6348bc3be2b3e7a135e33765cad0001dd842f22a992978e34afe4f7674e082092f03d35040dc0e3fda5f6ae6", "otaHeaderString": "Thu 11/09/2023 V.011C.011C\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "PSoC6_1246-0120-00280028.0002_(90215AC0).ota", "fileVersion": 28, "fileSize": 463202, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Danfoss/PSoC6_1246-0120-00280028.0002_(90215AC0).ota", "imageType": 288, "manufacturerCode": 4678, "sha512": "2be3ce5ff9e9b378a447285a7f2f099313a8ec067c5565b0ceb9fcb917ba663d022b05ceaeddd20d0205d9b98b1c2986570699d749031f4b54ffd04d14d593c8", "otaHeaderString": "Thu 11/09/2023 V.001C.001C\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ctm_mains_power_outlet.ota", "fileVersion": 268462640, "fileSize": 306294, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/ctm_mains_power_outlet.ota", "imageType": 4099, "manufacturerCode": 4919, "sha512": "4b161a3ea4586657f249ff6de7e8134b1fee6658b99574e1e5bc34a71ab3647513e86dcc38c1b7af0eba1810c5f0d9773c1a69cf64129c34bf567384e5b1cc17", "otaHeaderString": "EBL ctm_mains_power_outlet\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ctm_mbd-s_2_5.ota", "fileVersion": 256920, "fileSize": 269370, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/ctm_mbd-s_2_5.ota", "imageType": 4109, "manufacturerCode": 4919, "sha512": "583cb7e4829174f225d8f99d82e49b764de30c4f7b35cb9002d183ad237c58305eca1436abff6607671b66a549807b77d45558d204868bb5bc9eed6132929123", "otaHeaderString": "EBL ctm_mbd\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "dimmerpille_v2_0_v_4_combined_OTA.ota", "fileVersion": 206920, "fileSize": 297862, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/dimmerpille_v2_0_v_4_combined_OTA.ota", "imageType": 4114, "manufacturerCode": 4919, "sha512": "a4c805a4769d38f6ed9ffad358573aeb4e3dceaba2d3b2cf693f39e64fa94a5cc7a8efdc6c5363521c54d9e6c10901fd1606af674fde2e60bcbb96ad4c79daf0", "otaHeaderString": "EBL ctm_mtouch_dim\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "eva_meter_reader-2.0_MG21.ota", "fileVersion": 536898096, "fileSize": 241258, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/eva_meter_reader-2.0_MG21.ota", "imageType": 8224, "manufacturerCode": 4919, "sha512": "2dd671e994f521d602f083e4f3a7720e9d251672e1b4068333a0025a9de9519f8b6d629c4b55d0ef8cc44735d8ce98cea9b477c1a92526fe5a72eb762b70dacc", "otaHeaderString": "EBL eva_meter_reader\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "han_adapter-0.7_MG13_nonreworked.ota", "fileVersion": 117531984, "fileSize": 197946, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/han_adapter-0.7_MG13_nonreworked.ota", "imageType": 8205, "manufacturerCode": 4919, "sha512": "d5e28bcae888e97f6cc59c18fb9e406dbd53bf2b796b3807e1247a6d831a2bece433f8961702fe7437b5a26fe26b30ddcc802cc4d15660d27fd2be31b1311ff5", "otaHeaderString": "EBL han_adapter\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "han_adapter-2.0_MG13_reworked.ota", "fileVersion": 536898096, "fileSize": 235962, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/han_adapter-2.0_MG13_reworked.ota", "imageType": 8226, "manufacturerCode": 4919, "sha512": "516f3a1b31f6018cc794c3959adb0eac84e6477db2a60b8eba63f10ac9527e5d9368d9cbbd5e4257ea7587f1b58c3132c43f0bd788dac93819ab24caa1671150", "otaHeaderString": "EBL han_adapter\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "mTouch_Dim_v3_1_v35_combined_OTA.ota", "fileVersion": 316920, "fileSize": 462058, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/mTouch_Dim_v3_1_v35_combined_OTA.ota", "imageType": 4113, "manufacturerCode": 4919, "sha512": "0c175bed4bef94a26541d6a1fc8cfdff6c78d2234d479fa3b2a542fe642b41324282feebfad1140ca5d3aa8221d12a8fffb385eaf0e3c41372b663bdadbcd126", "otaHeaderString": "EBL ctm_mtouch_dim\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "mTouch_One_v3_8_v72_combined_OTA.ota", "fileVersion": 386920, "fileSize": 471258, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/mTouch_One_v3_8_v72_combined_OTA.ota", "imageType": 4102, "manufacturerCode": 4919, "sha512": "11e455bb88f247c8b7e99db34c5d760a23da7097214cc2cb15f369617e91a94da1189fe81b5ecfadbb9fcd22f8ec66a1a5087bc9aac3e0af47be34f888416062", "otaHeaderString": "EBL mTouch_One\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "mains_power_outlet_2.7.ota", "fileVersion": 654468432, "fileSize": 253278, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Datek/mains_power_outlet_2.7.ota", "imageType": 0, "manufacturerCode": 4919, "sha512": "431e3fb76a5bde82947a925099d5b5b8639c2e0ba1ad6fe85e7dcd598a8f43c41a86bb78c34ee2fdbe4ae5465e35bb2e24065408fe4aa13c492881848214b6ab", "otaHeaderString": "EBL mains_power_outlet\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ED - HA - VOC Sensor-SSIG 4.0.1.zigbee", "fileVersion": 262145, "fileSize": 194471, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/ED%20-%20HA%20-%20VOC%20Sensor-SSIG%204.0.1.zigbee", "imageType": 800, "manufacturerCode": 4117, "sha512": "d6779d7e0879c86d8a7479f352d187cb854cc8586b57245dc96f6c202ccf455a19acb5d360b9ae62510e5b7ae2ea67a95841ff822a5a3a69199d5841dbcf2dbe", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "EMI2LED_3.1.2.zigbee", "fileVersion": 196866, "fileSize": 213826, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/EMI2LED_3.1.2.zigbee", "imageType": 976, "manufacturerCode": 4117, "sha512": "fa53c339b61a8980c3d8d42319be4f57aa088d98b8f87393a6fce84c2bd224079c26ca2b097f2a3bf042f629adf32bac90460e17d0b7a28864f244453e262baf", "otaHeaderString": "EMI 2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "EMI2P1_3.1.7.zigbee", "fileVersion": 196871, "fileSize": 226882, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/EMI2P1_3.1.7.zigbee", "imageType": 977, "manufacturerCode": 4117, "sha512": "4ad740da6eaba7cbfbc3847f101c264a6df99b3bed6a6223721d4ce28cb39ef28a8a655d94aa9a33721fb0c1621d26d8c2b4f2b1968e3b8016b6125964e99a14", "otaHeaderString": "EMI 2 - P1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "EmiNorwegianHan_4.0.7.zigbee", "fileVersion": 262151, "fileSize": 204077, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/EmiNorwegianHan_4.0.7.zigbee", "imageType": 832, "manufacturerCode": 4117, "sha512": "cbc9158bc45d7a819d56ed72dc9b53c717371ccc25a17e84ea13819ce62c5a50cc549602b4b7979dd70561be5e1c4c0a31ba2899a15a20730d894165f3c4a487", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "EntrySensor2_2.0.6.zigbee", "fileVersion": 131078, "fileSize": 217058, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/EntrySensor2_2.0.6.zigbee", "imageType": 928, "manufacturerCode": 4117, "sha512": "49b5c712b9e5ef91440bece5512cfc66d112e99c4d43c2f9d9755617d0ce302ffad2cd7b981769457d060c58e7e3950f9eb208583be87c98d638ac47fb1dda1d", "otaHeaderString": "WindowVibrationSensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "EntrySensor_4.0.2.zigbee", "fileVersion": 262146, "fileSize": 200121, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/EntrySensor_4.0.2.zigbee", "imageType": 576, "manufacturerCode": 4117, "sha512": "839eff218970eb619c9f15ae63f435da50d2cf93f44569de4fea56d63d49c888031b1ee0714e447b39f90664eaa490bf619b8eac641c296a27b609e78c61fc56", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "FireAlarm_4.0.8.zigbee", "fileVersion": 262152, "fileSize": 201716, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/FireAlarm_4.0.8.zigbee", "imageType": 593, "manufacturerCode": 4117, "sha512": "07bc44d6510a22f99b4ce5baf56a23aa29a8c34f213dbc5c2407fc89c6c7c3542e43082e15285aeaaa94eff4579002da8e6cc30cf35455f57dbfaf7d3db5bf2c", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "HumiditySensor_4.0.1.zigbee", "fileVersion": 262145, "fileSize": 189614, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/HumiditySensor_4.0.1.zigbee", "imageType": 784, "manufacturerCode": 4117, "sha512": "a5bae0c769d58e788ba146b40c65ccd32d26d4934f6291a48f5bf23e13266c9ddd5afe310a47dcc5592c1865572e723da69171f1d43f1c669ed153d670b3afb5", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "IntelligentKeyPad_2.0.5.zigbee", "fileVersion": 131077, "fileSize": 217046, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/IntelligentKeyPad_2.0.5.zigbee", "imageType": 915, "manufacturerCode": 4117, "sha512": "59b62aab3326b662f6e21a0327440db4836e55a618c7ce7b2cf5578c7b847cd9d08d8e6c431feb714f87a3556c1951069466f3986effa8dd5e1f91b39f3edd53", "otaHeaderString": "KeyPad\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "MotionSensor2_2.0.6.zigbee", "fileVersion": 131078, "fileSize": 216102, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/MotionSensor2_2.0.6.zigbee", "imageType": 387, "manufacturerCode": 4117, "sha512": "4e78aa73ceec45e03606a67ba5e05d930862e01afc8fc81c86fa3cfa4b5433d3b2a785187afb1983c2b1874db5a9ab10d1a22876ddb9cf6a15252cc600c518d0", "otaHeaderString": "MotionSensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "MotionSensor_4.0.6.zigbee", "fileVersion": 262150, "fileSize": 210750, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/MotionSensor_4.0.6.zigbee", "imageType": 386, "manufacturerCode": 4117, "sha512": "6ed82ead9881ae40347b51127aef48309cd54b78f0be3d46b42337cd34a095b49d9fe0bce434c06059136292bd33d6f8e44cd46a9d7a6d1c9c9934d10ab64af3", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "SmartButton_2.0.2.zigbee", "fileVersion": 131074, "fileSize": 208318, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/SmartButton_2.0.2.zigbee", "imageType": 913, "manufacturerCode": 4117, "sha512": "79a014db8559dd8ec42d3d867d3330eee0208904eb5169a3fe47a072b341486864dd4a62bfb7bb42f00aa62a7c2d36ef861842441cbd46e44fca37b98b28097c", "otaHeaderString": "PanicButton\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "SmartPlug2_2.0.5.zigbee", "fileVersion": 131077, "fileSize": 234566, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/SmartPlug2_2.0.5.zigbee", "imageType": 737, "manufacturerCode": 4117, "sha512": "3c91179db3e65ff0bb361a84844ef1e096249af586f0e548bb72ac9bf27096113f90f14ffd0d222fdb792865a28368c58c072a895f87598bd44543275d6a766f", "otaHeaderString": "Router - Smart Plug\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "WaterLeakDetector_4.0.4.zigbee", "fileVersion": 262148, "fileSize": 201108, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/WaterLeakDetector_4.0.4.zigbee", "imageType": 768, "manufacturerCode": 4117, "sha512": "382a13ab21fa9aed73f136b42f565444ba4ebc115d437a1af6868cad0fca86376fb2f48407056aa511cd77321897981a52bec890b9810aff1af15ac6b072d385", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZR_Smartplug_SSIG_3.12.16.zigbee", "fileVersion": 199696, "fileSize": 181164, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/ZR_Smartplug_SSIG_3.12.16.zigbee", "imageType": 736, "manufacturerCode": 4117, "sha512": "eaf4e925dbdf32f171fb8323c52b8652620dea5d62bc007e61ee61d276bdd90f097400a1c1bc48980d78c276117415e274c34529b481be82c28cd3695e2be817", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZigbeeRangeExtender_2.0.2.zigbee", "fileVersion": 131074, "fileSize": 230466, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Develco/ZigbeeRangeExtender_2.0.2.zigbee", "imageType": 916, "manufacturerCode": 4117, "sha512": "bec200a43bc7a51c58e70caa9d057006cbab54e5b058a8191a52d40e9aa8d1b979d2695ce0b4b0d4d1db6e926cce95bc1ae0a9e0810773b6c28ddb6705f4e248", "otaHeaderString": "Router - Range Extender\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1135-0000-201000F5-FLS-PP3_RGBW_16Mhz.zigbee", "fileVersion": 537919733, "fileSize": 202911, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/DresdenElektronik/1135-0000-201000F5-FLS-PP3_RGBW_16Mhz.zigbee", "imageType": 0, "manufacturerCode": 4405, "sha512": "5aa0a6156c8fe672b0fdfcc6e97bb21244cc5497ca86b75c2d371db85c0712334f88886a0903723f9230c21c6f3ee3a19a0e6ebdcdfc2ac5390fd4c8830be402", "otaHeaderString": "�w}6@\u0000`>@\u0000\u0013p@\u0000\u0001\u0000\u0000\u0000�6@\u0000�\u0015@\u0000 �@\u0000��" }, { "fileName": "1135-0100-1000002A-Kobold.zigbee", "fileVersion": 268435498, "fileSize": 244094, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/DresdenElektronik/1135-0100-1000002A-Kobold.zigbee", "imageType": 256, "manufacturerCode": 4405, "sha512": "0905f0e6a469be3893f2c665156f18077258e24439e283c9f4b121553a94e8eb142e6250e721585ddd9aceef75da3c10a03defaee89885d7cb8f08449a20912d", "otaHeaderString": "`\u0000�.@\u0000\u0010/@\u0000\u0000\u0000�n`\n�t�\u0015@\u0000\u0000\u0016@\u0000 �@\u0000��", "originalUrl": "https://deconz.dresden-elektronik.de/otau/1135-0100-1000002A-Kobold.zigbee" }, { "fileName": "1135-0101-0020001B-Hive.zigbee", "fileVersion": 2097179, "fileSize": 491614, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/DresdenElektronik/1135-0101-0020001B-Hive.zigbee", "imageType": 257, "manufacturerCode": 4405, "sha512": "2fdcef8bb3d69f5f534d60982fbfba4078b15a963948e1549367732df431f0076abe0bc8f8183585e850cdc55826509aa2d431001026d1aa242cd8c7a93af8af", "otaHeaderString": "`\u0000�.@\u0000\u0010/@\u0000\u0000\u0000�n`\n!v�\u0015@\u0000\u0000\u0016@\u0000 �@\u0000��" }, { "fileName": "ZB21S3_HZC_Dimmer1_EcoDim-Zigbee 3.0_1.01_20230908.ota", "fileVersion": 4, "fileSize": 286646, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/EcoDim/ZB21S3_HZC_Dimmer1_EcoDim-Zigbee%203.0_1.01_20230908.ota", "imageType": 1000, "manufacturerCode": 4714, "sha512": "9b7835e32ef5af35385710332838099c5dce99a398ae04a1a769d67935fb0752ca4da58892f13bc842cbf8f9ae20516142fe9b66eff3e8ebef6577a71a1d9b0b", "otaHeaderString": "EBL D581_ZG\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "TICMeter.ota", "fileVersion": 197133, "fileSize": 1140668, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/GammaTroniques/TICMeter.ota", "imageType": 200, "manufacturerCode": 65535, "sha512": "67f0bfdbb7d2cdeee4d54d2ad44dc04b42ac9401f02c92fcde5db3010d5706027d793405a601bcf18bb789cfc46cacc4887bee1b59f42c234177135d1ea9ffb0", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://update.gammatroniques.fr/ticmeter/V3.2.13/download/TICMeter.ota", "modelId": "TICMeter" }, { "fileName": "GL-B-007P_V17_OTAV7_20210305_100%.ota", "fileVersion": 7, "fileSize": 291702, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-B-007P_V17_OTAV7_20210305_100%25.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "8c3431b4d60d31e3ac16468601b64b3f8da0c2cf122c638a654b71543de901bfaf2e3afe5a0a8084d20d32478ad61738c86f33a2548211ea8f2367900fb1b620", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-B-007P" }, { "fileName": "GL-B-007P_V20451233_20240425.ota", "fileVersion": 604057601, "fileSize": 209330, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-B-007P_V20451233_20240425.ota", "imageType": 5154, "manufacturerCode": 4687, "sha512": "5edf31594046cd881aa47ba966909ead5e065f3c6816a30cbeea6499845313d24f27fbdc491ab02b181a8015134949cb5fb78ef76418a5cfcc982241c9e401ac", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-B-007P" }, { "fileName": "GL-B-008P_V17A1_OTAV7.ota", "fileVersion": 7, "fileSize": 291726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-B-008P_V17A1_OTAV7.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "788341b48089076a6d1c6f020f34699428adc5103ba18d54fe3b0b00bbee2212bef13ba853fe3f1c1fb247f6d17d29608a937ed897fdd9e5b5294b4ceee13ebe", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-B-008P" }, { "fileName": "GL-C-008P_V17A1_OTAV7_20210303--V1-4.ota", "fileVersion": 7, "fileSize": 291702, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-C-008P_V17A1_OTAV7_20210303--V1-4.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "8eb197dc7e6f92afca457ce585e754ac3bf39e53cfb3f2e2433bf6317c51e0987cfe9224db046bf24522069651a85defe3bf62531c9eb4f2955b402ab55e9002", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-C-008P" }, { "fileName": "GL-D-004P_V105_20220427(1).ota", "fileVersion": 352399361, "fileSize": 209138, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-D-004P_V105_20220427(1).ota", "imageType": 5202, "manufacturerCode": 4687, "sha512": "64df9200d6f1a48c8561dd6bfb8b0949fcb0b30a87c740674205e9a0f383844d66686479ed7480076a6ea3cd3c42fe4f1c7af55356132fdc9d4c571096604696", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "GL-D-005P_V11076801_OTAV12_20211108_60%.ota", "fileVersion": 18, "fileSize": 291826, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-D-005P_V11076801_OTAV12_20211108_60%25.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "966bbc8a44693eb5a91c4cf64bf59b91d291335d223cec1ffc65a6d7b3fe339be4e70b90908aea0bc0a6f7ea4ff130898b4492e0818d7f05220582097a543f9d", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-D-005P" }, { "fileName": "GL-D-006P_V105_20220511.ota", "fileVersion": 352399361, "fileSize": 209138, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-D-006P_V105_20220511.ota", "imageType": 5204, "manufacturerCode": 4687, "sha512": "0a6ee5e0d447ebe278f3c5c594a3850185f8dd04ec41f5618ba58b0c28eae31e9145f77c4ddf173d1a3d9e48a4306232fa6a264512aa38c395a28ae72501bd85", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "GL-D-007P.ota", "fileVersion": 402731009, "fileSize": 209202, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-D-007P.ota", "imageType": 5155, "manufacturerCode": 4687, "sha512": "fbeba0b73f91a865a5200c77b75589b53369b44d97d3ed445e0f36f0d581372bf4b414cff7dd32eadfb9e57d4c431639f76344cba62d6125044be1b564d995ec", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "GL-D-007P_V105_20220418(1).ota", "fileVersion": 352399361, "fileSize": 209138, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-D-007P_V105_20220418(1).ota", "imageType": 5205, "manufacturerCode": 4687, "sha512": "8d51a793c2591aa494cda1bfe16eb5095bebdd1338604222c0ca5ac44006959ee740692eb193c8416c92d6607602a741745ec3f76b8ed1dabea3d548685bdcf3", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "GL-FL-005P_V14_OTAV4_20210119_100%.ota", "fileVersion": 4, "fileSize": 291546, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-FL-005P_V14_OTAV4_20210119_100%25.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "35815df1ef7ec3f492076f118c77bf21a846ea68c07766f574eeb67d34cb00cee8e374edabfba47c1b66763c222c1ce2133a06cae8bece483b0be9d90f54a2b8", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-FL-005P" }, { "fileName": "GL-FL-006P_V14_OTAV4_20210119_100%.ota", "fileVersion": 4, "fileSize": 291546, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-FL-006P_V14_OTAV4_20210119_100%25.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "fec3c2e153d30a3a01193b0b7a96e3ef2e6613d1de58e5ee01d6d66baf7d2d4e8c1bd94b3aece0effc94145e8b5b783f192ff752a9ae640044b895faabbbcd63", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-FL-006P" }, { "fileName": "GL-G-001P_V11076801_OTAV12_20211028.ota", "fileVersion": 18, "fileSize": 291802, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-G-001P_V11076801_OTAV12_20211028.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "111b3620c7bc4f989d9814ed35b968708048f445cf87d04a2432c460ff7892001c18e9f899d7504916a2005d99fd116751fc4c6cee65db80c29d297cfbbe5a84", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-G-001P" }, { "fileName": "GL-S-006P_V107_20220906.ota", "fileVersion": 385953793, "fileSize": 209202, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-S-006P_V107_20220906.ota", "imageType": 5172, "manufacturerCode": 4687, "sha512": "e71026da82d79c789b859a82792b7a45b91e58175a6fe6ac2a77deb9c4c295656647866b513ebc3fbfe38f58c2a72598de36b34abb2b6294788ccc2791fcd456", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "GL-S-007P_V15_A1_OTAV5_20210201_90%.ota", "fileVersion": 5, "fileSize": 291622, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Gledopto/GL-S-007P_V15_A1_OTAV5_20210201_90%25.ota", "imageType": 0, "manufacturerCode": 4687, "sha512": "ce512a11f0d4e603edd3a93686cf5d666ee447bee1d24a6e06ce544e2ebf941a4a4e3fcc737cdf8c65821f8e008366b269d9a221fb55cfa54065380442c5f432", "otaHeaderString": "EBL ARGBW_V_0_1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "GL-S-007P" }, { "fileName": "100B-010C-01001A02-ConfLight-Lamps_0012.zigbee", "fileVersion": 16783874, "fileSize": 267452, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-010C-01001A02-ConfLight-Lamps_0012.zigbee", "imageType": 268, "manufacturerCode": 4107, "sha512": "c4591fe155bef8500779c36c7792f3960c4f83dde9dd47aa367113229c5bd73161f14cc92e6d6a0960e807c54626ce2ab0ce0d18c76d0206770dcda3a4776862", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010C/2ef158a5-ffb4-43ac-9d59-3cb71078f6f7/100B-010C-01001A02-ConfLight-Lamps_0012.zigbee", "maxFileVersion": 16783873 }, { "fileName": "100B-010C-01002800-ConfLight-Lamps_0012.zigbee", "fileVersion": 16787456, "fileSize": 266684, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-010C-01002800-ConfLight-Lamps_0012.zigbee", "imageType": 268, "manufacturerCode": 4107, "sha512": "30c754504fed42ce12b4243fbf70a8207675f02cba1efbe2e454270049b472e400578c316602978deadb39166b196cb21aaf0f5cbb527fd2491fd78d4a14b620", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010C/9ee7aed8-faed-43eb-b7f7-712a5b578dba/100B-010C-01002800-ConfLight-Lamps_0012.zigbee", "minFileVersion": 16783874 }, { "fileName": "100B-010E-01001904-ConfLight-ModuLum_0012.zigbee", "fileVersion": 16783620, "fileSize": 271050, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-010E-01001904-ConfLight-ModuLum_0012.zigbee", "imageType": 270, "manufacturerCode": 4107, "sha512": "5843552ab361d2d063e36be24785afcb8af34491ae721c2426da6afec94967acd4005d5e2abfcca5ebd10f3c9e39656524775b50cef115f67c3f636b9609d3c2", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010E/3e979745-cc00-43cf-a51c-73a3d9d91430/100B-010E-01001904-ConfLight-ModuLum_0012.zigbee", "maxFileVersion": 16783619 }, { "fileName": "100B-010E-01002600-ConfLight-ModuLum_0012.zigbee", "fileVersion": 16786944, "fileSize": 269002, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-010E-01002600-ConfLight-ModuLum_0012.zigbee", "imageType": 270, "manufacturerCode": 4107, "sha512": "f22b61f43ec9a98991825ba492d5373c1e617d62508fa538ef0ae6b5e51ec3419e2b9b15328770918b06391bf3b0adb18d747251bb615e189c43171abf0b3f07", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010E/05e4122d-0d51-41df-91af-7d1aae4cc0a0/100B-010E-01002600-ConfLight-ModuLum_0012.zigbee", "minFileVersion": 16783620 }, { "fileName": "100B-010F-01000A02-ConfLight-LedStrips_0012.zigbee", "fileVersion": 16779778, "fileSize": 250762, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-010F-01000A02-ConfLight-LedStrips_0012.zigbee", "imageType": 271, "manufacturerCode": 4107, "sha512": "af6c7538574a11d11f055563dd396f6f3d2fbe1312f8cd8e4b722d877fdd5272e665ad665a90a1bc87c88c0a60e9b1ee8ebbd39d132fdfae1f2b143c263dd171", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010F/7e46b10c-6cfe-462c-a587-5c1cf48a8418/100B-010F-01000A02-ConfLight-LedStrips_0012.zigbee", "maxFileVersion": 16779777 }, { "fileName": "100B-010F-01001700-ConfLight-LedStrips_0012.zigbee", "fileVersion": 16783104, "fileSize": 250762, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-010F-01001700-ConfLight-LedStrips_0012.zigbee", "imageType": 271, "manufacturerCode": 4107, "sha512": "34a42cd9185602bf76559425d2e4655ec33c2fe3159a09d866cd3425a0d56535ee9a807b86b21a66083fc7578287538c7e1b8e9aebd53923269aedb99b6090f7", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010F/0ed8b3ac-0870-486e-9f8b-5ba6e9b035a1/100B-010F-01001700-ConfLight-LedStrips_0012.zigbee", "minFileVersion": 16779778 }, { "fileName": "100B-0110-01000400-ConfLight-Lamps-EFR32MG13_0012_inclBL.zigbee", "fileVersion": 16778240, "fileSize": 281920, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0110-01000400-ConfLight-Lamps-EFR32MG13_0012_inclBL.zigbee", "imageType": 272, "manufacturerCode": 4107, "sha512": "840c55cf8239e7fb3cbe2a055758c3b06adad6316bd09f1d94884b19f2b9ebfa5a844daaaee12713cbd1e8926e227565c50e268e7c70fa6f24ca63af4adf3bf9", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0110/e8a0d0b9-1ce1-4f1a-934f-04ecd04a7080/100B-0110-01000400-ConfLight-Lamps-EFR32MG13_0012_inclBL.zigbee", "maxFileVersion": 16778239 }, { "fileName": "100B-0110-01002602-ConfLight-Lamps-EFR32MG13.zigbee", "fileVersion": 16786946, "fileSize": 328956, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0110-01002602-ConfLight-Lamps-EFR32MG13.zigbee", "imageType": 272, "manufacturerCode": 4107, "sha512": "4d15669f586c39da05fdfa95506fa085e297e177e5f2e962ce5f8ec2788f1d4488f880c35c2f2a1e0279ffe66272c99d27ca6da5d80512b88fcf50a574647a64", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0110/77cabadf-422e-4b65-a971-b40f6422ca6f/100B-0110-01002602-ConfLight-Lamps-EFR32MG13.zigbee", "minFileVersion": 16778240 }, { "fileName": "100B-0111-01001D00-ConfLight-ModuLum-EFR32MG13.zigbee", "fileVersion": 16784640, "fileSize": 468744, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0111-01001D00-ConfLight-ModuLum-EFR32MG13.zigbee", "imageType": 273, "manufacturerCode": 4107, "sha512": "d7f6adc33b7d1d165e1aab6975825a789e3daa83d8d86fe20b057fb603926548a6eeb6a0af09f20108d87a71aacfed654dc273eb79d4dcf917f254aa13876027", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0111/ad34031b-3c49-420f-9782-f37e205db2a9/100B-0111-01001D00-ConfLight-ModuLum-EFR32MG13.zigbee" }, { "fileName": "100B-0112-01002902-ConfLightBLE-Lamps-EFR32MG13.zigbee", "fileVersion": 16787714, "fileSize": 477486, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0112-01002902-ConfLightBLE-Lamps-EFR32MG13.zigbee", "imageType": 274, "manufacturerCode": 4107, "sha512": "5e7c52ee30fcdca12b875499923ede2078efec650cbbb0a1267874fc88acde456e439b53e6abc69501ed058d6abfeb031f7dec6df888c79b44fc6a7a13927d7b", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0112/fd7eef13-74d9-4920-8315-45adcf652102/100B-0112-01002902-ConfLightBLE-Lamps-EFR32MG13.zigbee" }, { "fileName": "100B-0114-01001200-ConfLightBLE-Lamps-EFR32MG21.zigbee", "fileVersion": 16781824, "fileSize": 336644, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0114-01001200-ConfLightBLE-Lamps-EFR32MG21.zigbee", "imageType": 276, "manufacturerCode": 4107, "sha512": "0f3cef1daeff4f25eed56b82be28c8f8bbb26f13d562aee8fcd86b718b9bcdd8d183b2ad86db7ca75e5ffbeaad32c8c8b1355bfdb3bcac7f4d62f7da574f48cb", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0114/db6d96de-5792-4b3c-a04f-3a773bf45124/100B-0114-01001200-ConfLightBLE-Lamps-EFR32MG21.zigbee", "maxFileVersion": 16781823 }, { "fileName": "100B-0114-01001300-ConfLightBLE-Lamps-EFR32MG21.zigbee", "fileVersion": 16782080, "fileSize": 353220, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0114-01001300-ConfLightBLE-Lamps-EFR32MG21.zigbee", "imageType": 276, "manufacturerCode": 4107, "sha512": "49a40a59ab73f2222b760319f93243aaf82e2a88eef1e6ec78e5ccc17be24f8d3d6fba5f808837813fa1199154508722989391f7f9e41a2f308fd84573c2ad45", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0114/423ed640-a522-4d4e-92f9-5f99c679195c/100B-0114-01001300-ConfLightBLE-Lamps-EFR32MG21.zigbee", "maxFileVersion": 16782079, "minFileVersion": 16781824 }, { "fileName": "100B-0114-01001304-ConfLightBLE-Lamps-EFR32MG21.zigbee", "fileVersion": 16782084, "fileSize": 353296, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0114-01001304-ConfLightBLE-Lamps-EFR32MG21.zigbee", "imageType": 276, "manufacturerCode": 4107, "sha512": "460470ea628716ede44a160f48a34a6dd6b7288fc23f2525d7a8d9210730800fa7842f8309a7bc36c2584b282072cd50b1b2bf9cb40f9f32e30fd1884dc84542", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0114/28309b46-1ae1-4493-a0bb-f7f26f3ff1ad/100B-0114-01001304-ConfLightBLE-Lamps-EFR32MG21.zigbee", "maxFileVersion": 16782083, "minFileVersion": 16782080 }, { "fileName": "100B-0114-01002502-ConfLightBLE-Lamps-EFR32MG21.zigbee", "fileVersion": 16786690, "fileSize": 534968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0114-01002502-ConfLightBLE-Lamps-EFR32MG21.zigbee", "imageType": 276, "manufacturerCode": 4107, "sha512": "a4dec4e9d3bb2561218cf370fb9306d85f00464f908a029cf4dc779050fe03fabac041dafe257d6088698c6854dc4326a446b017ef183a06037160bb81e5af33", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0114/b9ca4597-f893-4658-99ad-ed61050ab741/100B-0114-01002502-ConfLightBLE-Lamps-EFR32MG21.zigbee", "minFileVersion": 16782084 }, { "fileName": "100B-0115-01001402-SmartPlug-EFR32MG13.zigbee", "fileVersion": 16782338, "fileSize": 413406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0115-01001402-SmartPlug-EFR32MG13.zigbee", "imageType": 277, "manufacturerCode": 4107, "sha512": "d2b85f5575c9e5f93966ae3d918a9ace2c725549c7e55c58993217c7bc131ceee20a717912c9b32e697ea840f2c747afdd0a5861b581c4d6bc30416ac26d8360", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0115/84e91e20-7715-4bd0-9a41-2f6dcca94be8/100B-0115-01001402-SmartPlug-EFR32MG13.zigbee" }, { "fileName": "100B-0116-02001300-Switch-EFR32MG13.zigbee", "fileVersion": 33559296, "fileSize": 243610, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0116-02001300-Switch-EFR32MG13.zigbee", "imageType": 278, "manufacturerCode": 4107, "sha512": "615c6b6d88bac398c7e01fe857ada5c2a95f2e201cd98f4483fa4220de8c045cefbd7203db006eb7fe5bac4353666bde88987764848e51d452ae6bc854b79a6d", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0116/b1cfb2a9-0bf1-4eb3-b9ec-9ca7fa3f11b8/100B-0116-02001300-Switch-EFR32MG13.zigbee", "maxFileVersion": 33559295 }, { "fileName": "100B-0116-02004D27-Switch-EFR32MG13.zigbee", "fileVersion": 33574183, "fileSize": 235434, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0116-02004D27-Switch-EFR32MG13.zigbee", "imageType": 278, "manufacturerCode": 4107, "sha512": "aa7c8ffcc189cf32f7a2096fe2e9fdb6c35765d0a08f9558ffff646de8d33b8233c161491be2f959f11e1b60111746406a5f27b0f8aa805589ce784656b5c5df", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0116/37d5e444-b304-423e-a2bb-27e74a263726/100B-0116-02004D27-Switch-EFR32MG13.zigbee", "minFileVersion": 33559296 }, { "fileName": "100B-0117-01000B00-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "fileVersion": 16780032, "fileSize": 300890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0117-01000B00-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "imageType": 279, "manufacturerCode": 4107, "sha512": "549e1c1e9251d1e2253526a40d18ec0f00f99274bcac73106dda9c3a148921f4cd091eb2fdd0037999f5535f661fdce9b47e306be9483959a9552d989202ec5f", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0117/5cf15788-8127-4557-b873-55a3283d2807/100B-0117-01000B00-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "maxFileVersion": 16780031 }, { "fileName": "100B-0117-01000C00-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "fileVersion": 16780288, "fileSize": 317506, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0117-01000C00-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "imageType": 279, "manufacturerCode": 4107, "sha512": "f9fcd5312ec92d5b0d79f03248c268ef8b5c6a663e666dcb6009a53a75d2d459413414c401596a86e30d5d1686c6e210db9fcaf390271c9202534fdab006b942", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0117/46638111-17d3-47f0-b9c6-67453ac8f299/100B-0117-01000C00-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "maxFileVersion": 16780287, "minFileVersion": 16780032 }, { "fileName": "100B-0117-01000C04-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "fileVersion": 16780292, "fileSize": 317618, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0117-01000C04-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "imageType": 279, "manufacturerCode": 4107, "sha512": "3c3b4349089377adc67a209e2241b7817768506588272eecf94692b7fa15c610987496a61619d8b635f7b057be46d66b990a81ff864466066e324ed72f55770f", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0117/a31ccf48-0a8f-4e99-912f-4c3b9fef60f0/100B-0117-01000C04-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "maxFileVersion": 16780291, "minFileVersion": 16780288 }, { "fileName": "100B-0117-01001D0C-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "fileVersion": 16784652, "fileSize": 420784, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0117-01001D0C-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "imageType": 279, "manufacturerCode": 4107, "sha512": "b17faa044694f3b9a3f28653ed0a42441797f60dd390a9507eed8d4baa95368ee4ca9accef6698b0092cc196c99c89129372e2dcba8383473f13d522b02488f1", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0117/a8a50281-4075-4d41-928d-85aec9f4ef33/100B-0117-01001D0C-ConfLightBLE-ModuLum-EFR32MG21.zigbee", "minFileVersion": 16780292 }, { "fileName": "100B-0118-01001802-PixelLum-EFR32MG21.zigbee", "fileVersion": 16783362, "fileSize": 453438, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0118-01001802-PixelLum-EFR32MG21.zigbee", "imageType": 280, "manufacturerCode": 4107, "sha512": "9acb1a53c13fedcdf1528cf402971eea7bd7b5c81a8ab4569f86757392fdd5bc305bde30df7c967be63a03ff2a8c961b598ae7eeb561835d56f91be7833fc81b", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0118/de21c7c9-cdfa-4f95-90a6-b0e01bb62e82/100B-0118-01001802-PixelLum-EFR32MG21.zigbee" }, { "fileName": "100B-0119-02002100-Switch-EFR32MG22.zigbee", "fileVersion": 33562880, "fileSize": 169450, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0119-02002100-Switch-EFR32MG22.zigbee", "imageType": 281, "manufacturerCode": 4107, "sha512": "b89acc3a4146c033ab96a9f11d07d11f9dcf8e432a357fe063c83065a49e588ec92031bc560462c1e1db2242f49823e1e1528a7660b6970ba576f20135a5b54e", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0119/bc0fab3b-4307-4005-a6b8-ea8beb0f57e9/100B-0119-02002100-Switch-EFR32MG22.zigbee", "maxFileVersion": 33562879 }, { "fileName": "100B-0119-02004D27-Switch-EFR32MG22.zigbee", "fileVersion": 33574183, "fileSize": 202986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0119-02004D27-Switch-EFR32MG22.zigbee", "imageType": 281, "manufacturerCode": 4107, "sha512": "0c4bc737a5ea23a9c4fac34f2a2b90308fad1a6ca2e9068519ecaeb623eb3a540e61e96c18b803c46aca968be03d4ad03ed65d70a51f9a2eb16c2dce15ce4beb", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0119/d9e42e82-ed2d-4b98-82fa-f0217e5895d2/100B-0119-02004D27-Switch-EFR32MG22.zigbee", "minFileVersion": 33562880 }, { "fileName": "100B-011A-01000400-SmartPlug-EFR32MG21.zigbee", "fileVersion": 16778240, "fileSize": 272454, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011A-01000400-SmartPlug-EFR32MG21.zigbee", "imageType": 282, "manufacturerCode": 4107, "sha512": "96f12964daa049df95a3087cf96bf765e4f4ddf81877e5d76e9ddc865cb45aa207a27a5288d502c2d4a15a5e95a0934d4fc0c0a893984d0cf7d778c7f269dced", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011A/64f5cda8-1e98-4d34-885b-08ad58b9f702/100B-011A-01000400-SmartPlug-EFR32MG21.zigbee", "maxFileVersion": 16778239 }, { "fileName": "100B-011A-01000500-SmartPlug-EFR32MG21.zigbee", "fileVersion": 16778496, "fileSize": 289102, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011A-01000500-SmartPlug-EFR32MG21.zigbee", "imageType": 282, "manufacturerCode": 4107, "sha512": "e41998d10b6ffdf3276b9acc942cf5cb7468bdae02de337a430b701a817a9c8c5a03a6437e52a263a4b82cd3a0907b60f818ce121f87936b6c3f44812830425c", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011A/c0d186d5-aab0-42bd-a71e-fa29b850aaaa/100B-011A-01000500-SmartPlug-EFR32MG21.zigbee", "maxFileVersion": 16778495, "minFileVersion": 16778240 }, { "fileName": "100B-011A-01000504-SmartPlug-EFR32MG21.zigbee", "fileVersion": 16778500, "fileSize": 289166, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011A-01000504-SmartPlug-EFR32MG21.zigbee", "imageType": 282, "manufacturerCode": 4107, "sha512": "6b30e7a6ee5be633cf0b2f7e0695be952bc621bfc431d2ac8e8697a242839ae116dc62f8be1fca97104db9651734b80345751cd0225ee5c21659ef74d4ffedda", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011A/e3332645-c1b2-41c7-b76b-3ce0631401cb/100B-011A-01000504-SmartPlug-EFR32MG21.zigbee", "maxFileVersion": 16778499, "minFileVersion": 16778496 }, { "fileName": "100B-011A-01000F04-SmartPlug-EFR32MG21.zigbee", "fileVersion": 16781060, "fileSize": 348032, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011A-01000F04-SmartPlug-EFR32MG21.zigbee", "imageType": 282, "manufacturerCode": 4107, "sha512": "82ba5d9ce6d0b590e85458b796a1c8d1375d2ac1a24432bc83504f54bf56ca62f96b27b27fe4f867d1ade6c8813045118078aa4b98a6532f8f5a2aeeeaa23c16", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011A/0aafab7f-56c0-4c7b-b0f8-19cfe1f02602/100B-011A-01000F04-SmartPlug-EFR32MG21.zigbee", "minFileVersion": 16778500 }, { "fileName": "100B-011B-02004D23-Sensor-EFR32MG22.zigbee", "fileVersion": 33574179, "fileSize": 200814, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011B-02004D23-Sensor-EFR32MG22.zigbee", "imageType": 283, "manufacturerCode": 4107, "sha512": "eab283cabf8d9f55c84b9a0e0ee2da9c6d3eacaebcb3ef8973cb01448df091d437855e7dd6f01e8d8d8bc2db26aa729db52f2c8531d8d0e6d05cce2f4a875d5c", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011B/126cdb96-9758-45c3-98dc-ae35386bc960/100B-011B-02004D23-Sensor-EFR32MG22.zigbee" }, { "fileName": "100B-011C-02004D23-SwitchModule-EFR32MG13.zigbee", "fileVersion": 33574179, "fileSize": 227550, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011C-02004D23-SwitchModule-EFR32MG13.zigbee", "imageType": 284, "manufacturerCode": 4107, "sha512": "aa5b10bdf5910581f1c02e29ef994546d14b7d9be536977d666fcf0af7392db86783478a5d57802e1f320c2235a80d9cf959f6ca2f117c64534b6f24e76292b2", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011C/f8e8feb9-b5b5-4e46-b09d-59c2f2f23efb/100B-011C-02004D23-SwitchModule-EFR32MG13.zigbee" }, { "fileName": "100B-011D-01002504-ConfLight-ModuLumV2-EFR32MG13.zigbee", "fileVersion": 16786692, "fileSize": 485542, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011D-01002504-ConfLight-ModuLumV2-EFR32MG13.zigbee", "imageType": 285, "manufacturerCode": 4107, "sha512": "ca172fda58aac17c731cb55fdf4c4856596917725d1a127c77d64e344b68dbafc063984772e408f94b4dfd8b4a815dc259e48234e55314124b8ca75f4c457c13", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011D/6f9c541e-b1d2-42c2-8098-fc2ce7017cfc/100B-011D-01002504-ConfLight-ModuLumV2-EFR32MG13.zigbee" }, { "fileName": "100B-011E-01002404-ConfLight-PortableV2-EFR32MG13.zigbee", "fileVersion": 16786436, "fileSize": 450454, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011E-01002404-ConfLight-PortableV2-EFR32MG13.zigbee", "imageType": 286, "manufacturerCode": 4107, "sha512": "77c8e3a4953fa7c1b376f63968df11f7b053a55c895d178cbfaecc5b394684f734fe2f9188abd1afcff7eb96da0daea0803b88397b311a198817bda944543ecf", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011E/42d2db88-6252-4a0f-8c10-ab3fe9a0e8b0/100B-011E-01002404-ConfLight-PortableV2-EFR32MG13.zigbee" }, { "fileName": "100B-011F-01002402-ConfLightBLE-ModuLumV3-EFR32MG21.zigbee", "fileVersion": 16786434, "fileSize": 446260, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-011F-01002402-ConfLightBLE-ModuLumV3-EFR32MG21.zigbee", "imageType": 287, "manufacturerCode": 4107, "sha512": "8ec63076c58fb6c3870234aec98ff86ea7043bd25601e155c69f7e864f6bf858e950c76b5bad77386025a79a4fa9a126d87db7ca61e707a0f605fff06b212b3d", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_011F/f88abe86-a753-417f-b6e9-772a7a15e84a/100B-011F-01002402-ConfLightBLE-ModuLumV3-EFR32MG21.zigbee" }, { "fileName": "100B-0120-01002402-ConfLightBLE-PortableV3-EFR32MG21.zigbee", "fileVersion": 16786434, "fileSize": 400336, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0120-01002402-ConfLightBLE-PortableV3-EFR32MG21.zigbee", "imageType": 288, "manufacturerCode": 4107, "sha512": "4c61e5be6488454554dbc62006d63111a121ad864c42827d7dba634630ade6b3098167b92b271a0ff3793223943df4ed2cf08d55dfdc3c56b7a91b552d8a8912", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0120/ca5124f9-b9b5-4474-ba8d-3f431d713eb7/100B-0120-01002402-ConfLightBLE-PortableV3-EFR32MG21.zigbee" }, { "fileName": "100B-0121-02004D27-Switch-EFR32MG22-40xf.zigbee", "fileVersion": 33574183, "fileSize": 208106, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0121-02004D27-Switch-EFR32MG22-40xf.zigbee", "imageType": 289, "manufacturerCode": 4107, "sha512": "3ea0778f4eb4c2494e0b81d7ef85bd957fece8ca051289bd99dc5de1a6fb344d92ae1f481be64a516f97f72a6e947d0c0d88408c6b0945bda63dd81e7adc6618", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0121/c4cba7cc-7784-4b20-88db-df8e82ddb487/100B-0121-02004D27-Switch-EFR32MG22-40xf.zigbee" }, { "fileName": "100B-0122-02004D23-SwitchModule-EFR32MG22.zigbee", "fileVersion": 33574179, "fileSize": 197206, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0122-02004D23-SwitchModule-EFR32MG22.zigbee", "imageType": 290, "manufacturerCode": 4107, "sha512": "12c72c104444768ad8af52eb699e51f72726da24338fa6b24df0a0f2bd042a5cb4e193e838b9c38826d1debe81f612e27130e5b5dd75de31a9d55ebd99ec4045", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0122/b839c65f-9ee0-4cb5-94e4-8063b92bcb01/100B-0122-02004D23-SwitchModule-EFR32MG22.zigbee" }, { "fileName": "100B-0123-01000C02-PixelLumXL-EFR32MG21.zigbee", "fileVersion": 16780290, "fileSize": 423298, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0123-01000C02-PixelLumXL-EFR32MG21.zigbee", "imageType": 291, "manufacturerCode": 4107, "sha512": "a4317039a4fa26845b743d8ab2aa037b3589b4506e35e9462f1a1475351ab7eee74c3705ea9b1bff92d7f76cb72011a13d92423d65bebfbcc7524b57e0a04e29", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0123/567ce2cd-671c-4072-8485-2b19d250e9c7/100B-0123-01000C02-PixelLumXL-EFR32MG21.zigbee" }, { "fileName": "100B-0125-02004301-ContactSensor-EFR32MG22.zigbee", "fileVersion": 33571585, "fileSize": 171126, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0125-02004301-ContactSensor-EFR32MG22.zigbee", "imageType": 293, "manufacturerCode": 4107, "sha512": "a2a0076275a1dcdb94a9bea8c1591209f31f76fe847d5fd46456dad63247d0c302e438776c179dcc86f93471b3965e884f3455c06c332313fdc408df71a58c11", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://firmware.meethue.com/storage/100b-125/33571585/17803020-a40e-4015-b9e3-3454e43998bb/100B-0125-02004301-ContactSensor-EFR32MG22.zigbee", "maxFileVersion": 33571584 }, { "fileName": "100B-0125-02004D23-ContactSensor-EFR32MG22.zigbee", "fileVersion": 33574179, "fileSize": 193862, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0125-02004D23-ContactSensor-EFR32MG22.zigbee", "imageType": 293, "manufacturerCode": 4107, "sha512": "0e9f6fa8f476e0f2e7a8b7bad8066ffc65368ae753b0e4df7061f0b7c750c7a1f9241e69c522f43ef3268db45eb3ffdcf82560d6a2c33556c0e9f12001420762", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0125/7ef867a4-88e3-4aae-a94a-3f2940081717/100B-0125-02004D23-ContactSensor-EFR32MG22.zigbee", "minFileVersion": 33571585 }, { "fileName": "100B-0127-01000D02-MSD-EFR32MG21.zigbee", "fileVersion": 16780546, "fileSize": 477268, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0127-01000D02-MSD-EFR32MG21.zigbee", "imageType": 295, "manufacturerCode": 4107, "sha512": "9d7e340b79e0a82e16a7873760762e374be8b96267c4f643aedfdac7f4710bbce66beef51b1d4e353743a09cdbb22474508f419f124aae937d2bdd71cbc780d3", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://firmware.meethue.com/storage/100b-127/16780546/96200b09-9d10-4272-adeb-b89203fffc41/100B-0127-01000D02-MSD-EFR32MG21.zigbee" }, { "fileName": "ConnectedLamp-Atmel-Target_0012.sbl-ota", "fileVersion": 1124103171, "fileSize": 256696, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/ConnectedLamp-Atmel-Target_0012.sbl-ota", "imageType": 260, "manufacturerCode": 4107, "sha512": "eb9e81b28ea8128831c0f656e65be2821b6d06207bd44adbc31b050ed41e3656edc10e1c0a27cf73a2817c257208f9002c3e219913903ecdaacf7857f799b001", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0104/33cbfd3d-3b58-43e2-a6a0-1e6fe50f2bee/ConnectedLamp-Atmel-Target_0012.sbl-ota", "minFileVersion": 1107326256 }, { "fileName": "ConnectedLamp-Atmel_0104_5.130.1.30000_0012.sbl-ota", "fileVersion": 1107326256, "fileSize": 256632, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/ConnectedLamp-Atmel_0104_5.130.1.30000_0012.sbl-ota", "imageType": 260, "manufacturerCode": 4107, "sha512": "d2bf330b9a23114efb6a613ccefce691e4a67a98175033e65d3eaf6841312b5a542bd538ae19c06c3804aa06224d35acc184f4b37a6198b457df2a173a490f21", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0104/631d2194-554e-4016-b954-f3c226482f04/ConnectedLamp-Atmel_0104_5.130.1.30000_0012.sbl-ota", "maxFileVersion": 1107326255 }, { "fileName": "ConnectedLamp-TI-Target_0012.sbl-ota", "fileVersion": 1124103171, "fileSize": 258104, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/ConnectedLamp-TI-Target_0012.sbl-ota", "imageType": 256, "manufacturerCode": 4107, "sha512": "c63a1eb02ac030f3a76d9e81a4d48695796457d263bb1dae483688134e550d9846c37a3fd0eab2d4670f12f11b79691a5cf2789af0dbd90d703512496190a0a5", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0100/2dcfe6e6-0177-4c81-a1d9-4d2bd2ea1fb7/ConnectedLamp-TI-Target_0012.sbl-ota" }, { "fileName": "LivingColors-Atmel-Target_0012.sbl-ota", "fileVersion": 1124103171, "fileSize": 256696, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/LivingColors-Atmel-Target_0012.sbl-ota", "imageType": 264, "manufacturerCode": 4107, "sha512": "5c0736a0d4f191a214a209fca6a1984a5ca2caa073b79dccc3ea62cfb0dd4b6755d92770f49bdc904ddafaa586a65d8b71160f74420ff937007f79f5cc477389", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0108/a2470745-062a-4159-adc5-5162080aacb5/LivingColors-Atmel-Target_0012.sbl-ota", "minFileVersion": 1107326256 }, { "fileName": "LivingColors-Hue-Target_0012.sbl-ota", "fileVersion": 1124103171, "fileSize": 258104, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/LivingColors-Hue-Target_0012.sbl-ota", "imageType": 259, "manufacturerCode": 4107, "sha512": "f1c9b5f0cc779bcf01fb1f7e5bffc0112aa82e60972dad9264f87484a571d13710572c2f5fedf1dd2b5deb62fa45d4c0e41d107e2fd2fb544fb5a9235d21ee3a", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0103/e14480ff-2661-4abb-8dfd-275b77d876c2/LivingColors-Hue-Target_0012.sbl-ota" }, { "fileName": "LivingColors-Target_0108_5.130.1.30000_0012.sbl-ota", "fileVersion": 1107326256, "fileSize": 256632, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/LivingColors-Target_0108_5.130.1.30000_0012.sbl-ota", "imageType": 264, "manufacturerCode": 4107, "sha512": "7d6166daf46ad68275ada764d17d9fde78b364c4ebb0f81664fb8159efc81a225790d5f670e036489be80fa2a9fedf92201990336559d2299c16faeb396a46b6", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0108/fb2b4e6e-f8c4-44b0-88cb-aac2e88c9fa0/LivingColors-Target_0108_5.130.1.30000_0012.sbl-ota", "maxFileVersion": 1107326255 }, { "fileName": "ModuLum-ATmega_0012.sbl-ota", "fileVersion": 1124103171, "fileSize": 256696, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/ModuLum-ATmega_0012.sbl-ota", "imageType": 267, "manufacturerCode": 4107, "sha512": "9c5b28be12dd8299774f0d0515131156ee9882f683537553fcf878198b4da198270c79a5dab0cfb81b80e35db055dff850835da4e069d27a7e8eb2de0d461d1b", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010B/1cfec25a-f2f5-4e84-a80f-96548a95d6c3/ModuLum-ATmega_0012.sbl-ota", "minFileVersion": 1107326256 }, { "fileName": "ModuLum-ATmega_010B_5.130.1.30000_0012.sbl-ota", "fileVersion": 1107326256, "fileSize": 256632, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/ModuLum-ATmega_010B_5.130.1.30000_0012.sbl-ota", "imageType": 267, "manufacturerCode": 4107, "sha512": "903dc359ddab530136e2aced646633627555a4317696f9a1c61300ff2006109e316443f7807b03397021e9162698dc9ba7fcbb3749f6f5a83883b9aafc78eb10", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010B/e3d57ccf-94b9-4786-8b3f-569c5c8883f8/ModuLum-ATmega_010B_5.130.1.30000_0012.sbl-ota", "maxFileVersion": 1107326255 }, { "fileName": "Sensor-ATmega_0012.sbl-ota", "fileVersion": 1124102917, "fileSize": 240760, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/Sensor-ATmega_0012.sbl-ota", "imageType": 269, "manufacturerCode": 4107, "sha512": "ba7cc0e3632c1f6c50ccb6f3a33ee44947de643425743c35657ce34dae3c0c9c45d48b05479b1183fcb9f1572df68d9d5b8c6f2f0a86c72d095773e817e8147f", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_010D/fa14f094-f99f-497d-9bd5-cc2742b2cb69/Sensor-ATmega_0012.sbl-ota" }, { "fileName": "Superman_v3_08_ProdKey_3080.ota", "fileVersion": 3080, "fileSize": 232594, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/Superman_v3_08_ProdKey_3080.ota", "imageType": 0, "manufacturerCode": 4420, "sha512": "eb1e76825aca6a6418042d71821921d4c073aa1ada0d52eaffd967ce2ccba7a5dda07a6caab70f19b3a2f9630d43b055c06d16050101b655413ba271375bea57", "otaHeaderString": "EBL Z3SwitchSoc\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_1144_0000/04071b69-217b-4d73-8cf3-367ed2dc7ca8/Superman_v3_08_ProdKey_3080.ota" }, { "fileName": "Switch-ATmega_0012.sbl-ota", "fileVersion": 1124102917, "fileSize": 240760, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/Switch-ATmega_0012.sbl-ota", "imageType": 265, "manufacturerCode": 4107, "sha512": "6bec6b6dce7ef9bb47c4467643222871788256d5c3f0aa88ded80be24fc002dbdda525ca2cafa78b996ff7fe0c18d7c9194288e4f6b40f0f37d147bc1724dd4e", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0109/3a1c8cf8-3f4c-4464-93f0-24cc7f67f0d7/Switch-ATmega_0012.sbl-ota" }, { "fileName": "WhiteLamp-Atmel-Target_0012.sbl-ota", "fileVersion": 1124103171, "fileSize": 256696, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/WhiteLamp-Atmel-Target_0012.sbl-ota", "imageType": 261, "manufacturerCode": 4107, "sha512": "aacf086c482e149e916a12a344d0d2a2b1489e47f5d4d5ef9d9ebb308b976c5d8d266a19792a53ee64a108d8f39b56c800815191d4454311124fe85fe32392a2", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0105/a74b4fe6-805b-4113-8f08-2f6585cb8f5d/WhiteLamp-Atmel-Target_0012.sbl-ota", "minFileVersion": 1107326256 }, { "fileName": "WhiteLamp-Atmel-Target_0105_5.130.1.30000_0012.sbl-ota", "fileVersion": 1107326256, "fileSize": 256632, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/WhiteLamp-Atmel-Target_0105_5.130.1.30000_0012.sbl-ota", "imageType": 261, "manufacturerCode": 4107, "sha512": "a3492bec9fd9b3149be9135ea9175d6161617188c04428230bbea161660c0cef2f9c83ecc185b758e1337d4f99e08f3978fd9102eec67843bac66e6dbc1e39a9", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://otau.meethue.com/storage/ZGB_100B_0105/6b0b2e69-652d-4941-9da9-a4e7ff0fc70c/WhiteLamp-Atmel-Target_0105_5.130.1.30000_0012.sbl-ota", "maxFileVersion": 1107326255 }, { "fileName": "10035515-TRADFRI-bulb-cws-2.3.093.ota.ota.signed", "fileVersion": 587806257, "fileSize": 227500, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/10035515-TRADFRI-bulb-cws-2.3.093.ota.ota.signed", "imageType": 10243, "manufacturerCode": 4476, "sha512": "fcdcc6198cd5f41d3602000207fa4a4c7678279dce4c2e8467d2044293e27d8d9d63e0aa71b5125af62a74b599bfb0dd28645961fb034c803722b732d20802d0", "otaHeaderString": "EBL tradfri_wrgb\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10035515-TRADFRI-bulb-cws-2.3.093.ota.ota.signed", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "10082264-zingo_lds_stoftmoln-1.1.7.ota.ota.signed", "fileVersion": 16842759, "fileSize": 287178, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/10082264-zingo_lds_stoftmoln-1.1.7.ota.ota.signed", "imageType": 16645, "manufacturerCode": 4476, "sha512": "c656737a4fd3464ed09d47887cde2b7390b13fe6423c06d5ceba5aa92d40dade89c49b4feabf89546585936c40543113bfcf720a4b09158e7e5fa924f7b385b8", "otaHeaderString": "GBL zingo_lds_stoftmoln\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/10082264-zingo_lds_stoftmoln-1.1.7.ota.ota.signed", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "inspelning-smart-plug-soc_release_prod_v33816645_02579ff4-6fec-42f6-8957-4048def87def.ota", "fileVersion": 33816645, "fileSize": 294530, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/inspelning-smart-plug-soc_release_prod_v33816645_02579ff4-6fec-42f6-8957-4048def87def.ota", "imageType": 40766, "manufacturerCode": 4476, "sha512": "76f16f4c2ca48a2b6a66693c3a2d4f85d2f52ff440cc09a565b5856d46a872435b28c5a9b6746d50cb2425555db9bdf41ae05e1a17b0292095198af53552e5eb", "otaHeaderString": "GBL inspelning_smart_plug_soc\u0000\u0000\u0000" }, { "fileName": "mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota", "fileVersion": 268572245, "fileSize": 228582, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota", "imageType": 10242, "manufacturerCode": 4476, "sha512": "d00cb8207813feb220c7a907644304b1cb39e4ee239de835c5c73e3bc41a0baaea2d58823ec14ed6887ee442e7014c4617d3c3354dedd11184f83cd26f9dfd8b", "otaHeaderString": "GBL Signed OTA\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "motionsensor-lds-mg21a_release_prod_v16777316_3a00064a-cc29-4aac-bdb6-c4fa1fb445d5.ota", "fileVersion": 16777316, "fileSize": 267650, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/motionsensor-lds-mg21a_release_prod_v16777316_3a00064a-cc29-4aac-bdb6-c4fa1fb445d5.ota", "imageType": 6456, "manufacturerCode": 4476, "sha512": "39a5d1f8a626c02f3648799b27fb2367e246af3a28a798c90b5374b715f637a13c47fba2ce288ea64a7fc8e17ae98fc1ed64daf05d3629b3f56ad8eee0bd9100", "otaHeaderString": "GBL motionsensor_lds_mg21a\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/motionsensor-lds-mg21a_release_prod_v16777316_3a00064a-cc29-4aac-bdb6-c4fa1fb445d5.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "ota_t0x110e_m0x117c_v0x01000035.ota", "fileVersion": 16777269, "fileSize": 305574, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/ota_t0x110e_m0x117c_v0x01000035.ota", "imageType": 4366, "manufacturerCode": 4476, "sha512": "771af218764fa9ec4a6f5fcda73c46afc3ac478e81cbf309e9356b093c6305db4d7d257d17adbcc4b91976171443e85a785cd4025ca160e5e716f43b3369d7e0", "otaHeaderString": "GBL symfonisk_sound_remote_zingo" }, { "fileName": "ota_t0x110f_m0x117c_v0x01000011.ota", "fileVersion": 16777233, "fileSize": 283326, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/ota_t0x110f_m0x117c_v0x01000011.ota", "imageType": 4367, "manufacturerCode": 4476, "sha512": "995c25c09706dd39498d1adb6f0aca700c193dc1d5b18069f2057f5d2f05096527731330278f4ca15350d4e72489e0349e285af5d5befae25c5e992c847a50b6", "otaHeaderString": "GBL zingo_ikea_vindstyrka\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "rodret-dimmer-soc_release_prod_v16777303_0a78457a-950c-4903-bfd1-67902aa66cf9.ota", "fileVersion": 16777303, "fileSize": 268502, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/rodret-dimmer-soc_release_prod_v16777303_0a78457a-950c-4903-bfd1-67902aa66cf9.ota", "imageType": 4557, "manufacturerCode": 4476, "sha512": "0f248c6036c323f5d56158b9451d1e73b1a7935b8628bdff6a58590ce2e6625b4bdd0772c5a1318eb8302ab6601170dfeed1ebc0e8eafe6de686e5b98f56c883", "otaHeaderString": "GBL rodret_dimmer_soc\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/rodret-dimmer-soc_release_prod_v16777303_0a78457a-950c-4903-bfd1-67902aa66cf9.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "rodret-shortcut-soc_release_prod_v16777249_d89ffc33-55d1-4d47-acac-42365e5d9dd8.ota", "fileVersion": 16777249, "fileSize": 269430, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/rodret-shortcut-soc_release_prod_v16777249_d89ffc33-55d1-4d47-acac-42365e5d9dd8.ota", "imageType": 15112, "manufacturerCode": 4476, "sha512": "2a6553362120c82385f9d49666907b8b194a43e4420bbad251c9d31f09ee6a4a583fab0f374b571f3b3f29a50614f956be7ae23147e85a26a5602dbbf9107671", "otaHeaderString": "GBL rodret_shortcut_soc\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/rodret-shortcut-soc_release_prod_v16777249_d89ffc33-55d1-4d47-acac-42365e5d9dd8.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-bulb-cws-zll_release_prod_v587753009_ec8b1193-0fa2-440e-b921-2412a8688b74.ota", "fileVersion": 587753009, "fileSize": 226244, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-bulb-cws-zll_release_prod_v587753009_ec8b1193-0fa2-440e-b921-2412a8688b74.ota", "imageType": 10241, "manufacturerCode": 4476, "sha512": "faa374df605e1708c1e2b48568ca657c1a3e2dc43f72991ebad0fa869d23e613b2faeb3e124d9a364b29cb4ee179b04824bd54dab48a0f5d67c60cea14c5be5d", "otaHeaderString": "EBL tradfri_wrgb\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-cws-zll_release_prod_v587753009_ec8b1193-0fa2-440e-b921-2412a8688b74.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-bulb-w-1000lm_release_prod_v587810353_5a161508-742d-4eab-8761-286c80d116eb.ota", "fileVersion": 587810353, "fileSize": 207340, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-bulb-w-1000lm_release_prod_v587810353_5a161508-742d-4eab-8761-286c80d116eb.ota", "imageType": 8449, "manufacturerCode": 4476, "sha512": "d383dc0ad22c96e6876298758979f14917b9972aa68b18e772b103a08969f262ed71d30daed9de40d486f13b8a0df686340b946bafe23621e1fb1ce2866d0572", "otaHeaderString": "EBL tradfri_light_basic\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-w-1000lm_release_prod_v587810353_5a161508-742d-4eab-8761-286c80d116eb.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-bulb-ws-1000lm_release_prod_v587814449_2b13625e-17a8-40d9-bad3-47a9a96ab002.ota", "fileVersion": 587814449, "fileSize": 216620, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-bulb-ws-1000lm_release_prod_v587814449_2b13625e-17a8-40d9-bad3-47a9a96ab002.ota", "imageType": 8706, "manufacturerCode": 4476, "sha512": "c8436256d91deb8d88218b57d1d365906ad73320b6db34b22ea318a4c712b16588b059dc0899bb3cf879de303e9a76ddbc572334bcf1a16ed0269d75bf40bf4f", "otaHeaderString": "EBL tradfri_light_1000ml\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-ws-1000lm_release_prod_v587814449_2b13625e-17a8-40d9-bad3-47a9a96ab002.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-bulb-ws-e14_release_prod_v587757105_963ac72a-97be-4f7e-b12c-46759bb91d6e.ota", "fileVersion": 587757105, "fileSize": 215596, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-bulb-ws-e14_release_prod_v587757105_963ac72a-97be-4f7e-b12c-46759bb91d6e.ota", "imageType": 8705, "manufacturerCode": 4476, "sha512": "b1ed830781ba1dc18c6bf41adf02b6653a918f3ed05396110e6054b4a6c2fdca2a9ff39e82e0776339f63aaeaefaf5393e4b6a87a6d3bfa7a68cd222b26a4007", "otaHeaderString": "EBL tradfri_light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-ws-e14_release_prod_v587757105_963ac72a-97be-4f7e-b12c-46759bb91d6e.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-bulb-ws-gu10_release_prod_v587757105_25ac125d-5723-4b92-aa02-404fd5008a55.ota", "fileVersion": 587757105, "fileSize": 215340, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-bulb-ws-gu10_release_prod_v587757105_25ac125d-5723-4b92-aa02-404fd5008a55.ota", "imageType": 8707, "manufacturerCode": 4476, "sha512": "8da9e1bc6cee95cad20acb6d4a60cfb73b3f213f935451c66ed1d165fa0350de662856f8eaf1611723cf22dd03f91e6b6582cb8285edd63ec2a52c9d35b1b6bc", "otaHeaderString": "EBL tradfri_light_gu10\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-bulb-ws-gu10_release_prod_v587757105_25ac125d-5723-4b92-aa02-404fd5008a55.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-connected-blind_release_prod_v604241939_89e61475-8999-4074-842a-e04efac9c857.ota", "fileVersion": 604241939, "fileSize": 219816, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-connected-blind_release_prod_v604241939_89e61475-8999-4074-842a-e04efac9c857.ota", "imageType": 4487, "manufacturerCode": 4476, "sha512": "6fded116c93dc2bb20443f11bbc11ba84bd9803255f2ed4ae076ff6661f840b7c3e8e6b67a1671625f081faf8613f0be87b5764c57163a1c2585d777d2481f77", "otaHeaderString": "GBL GBL_tradfri_connected_blind\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-connected-blind_release_prod_v604241939_89e61475-8999-4074-842a-e04efac9c857.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-control-outlet_release_prod_v587765297_20061876-85d6-4b39-8c9c-eb95620baa97.ota", "fileVersion": 587765297, "fileSize": 209136, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-control-outlet_release_prod_v587765297_20061876-85d6-4b39-8c9c-eb95620baa97.ota", "imageType": 4353, "manufacturerCode": 4476, "sha512": "83d8427b1fbc4701673185fc02c846680ce83b9270bb75896377469c69eb1be37648fdf019a39b340a1f928a1ece21e394dc94b926245b3aa5a59c7147f73a3b", "otaHeaderString": "GBL GBL_tradfri_control_outlet\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-control-outlet_release_prod_v587765297_20061876-85d6-4b39-8c9c-eb95620baa97.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-controller_release_prod_v604241925_abef4451-762a-4ef9-8c11-dfd88c3e98f7.ota", "fileVersion": 604241925, "fileSize": 207084, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-controller_release_prod_v604241925_abef4451-762a-4ef9-8c11-dfd88c3e98f7.ota", "imageType": 4545, "manufacturerCode": 4476, "sha512": "513c5a4bff7d64c014d11c8f8ca776e57b73ee6e18227be80be179e513a853e2eccda5acedd294f94a2f2e123ccc41d64d871f29c5e88cbc2be9d6350b6ffc77", "otaHeaderString": "EBL tradfri_controller\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-controller_release_prod_v604241925_abef4451-762a-4ef9-8c11-dfd88c3e98f7.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-cv-cct-unified_release_prod_v587757105_33e34452-9267-4665-bc5a-844c8f61f063.ota", "fileVersion": 587757105, "fileSize": 215884, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-cv-cct-unified_release_prod_v587757105_33e34452-9267-4665-bc5a-844c8f61f063.ota", "imageType": 16902, "manufacturerCode": 4476, "sha512": "98d8be5b4dcd9692cb6ff87531513023c0116cb9137c5690105ce2077765f35ae2651bdd2aa80ce0363f6db4ef3b9795449252c6c0ff8a8b0e6e05789fdb03be", "otaHeaderString": "GBL GBL_tradfri_cv_cct_unified\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-cv-cct-unified_release_prod_v587757105_33e34452-9267-4665-bc5a-844c8f61f063.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-dimmer_release_prod_v604241925_ecbb4451-ce85-4e6c-ab9f-e7ce32cd0c1e.ota", "fileVersion": 604241925, "fileSize": 214692, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-dimmer_release_prod_v604241925_ecbb4451-ce85-4e6c-ab9f-e7ce32cd0c1e.ota", "imageType": 4554, "manufacturerCode": 4476, "sha512": "69b8a35e926eab95831df7ddf91f607cf3462fe02d01eab77ad2f63e0dde5be2ac118bcd136c41d383cf74d50fc26911561649b042c997d7c3043a20e22ba7e4", "otaHeaderString": "GBL GBL_tradfri_dimmer\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-dimmer_release_prod_v604241925_ecbb4451-ce85-4e6c-ab9f-e7ce32cd0c1e.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-driver-hp_release_prod_v587757105_b42c57bf-cb9c-478c-8327-5812a3286a64.ota", "fileVersion": 587757105, "fileSize": 215852, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-driver-hp_release_prod_v587757105_b42c57bf-cb9c-478c-8327-5812a3286a64.ota", "imageType": 16898, "manufacturerCode": 4476, "sha512": "13109c4d517f16aa069af63c3c8257767a98c22fd21dd964e54c36deabb62494b2ee5fd2cbda1e925389277db9deadd95a0eaa173b0f6f35aee65159a97dcff3", "otaHeaderString": "EBL tradfri_light_hp\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-driver-hp_release_prod_v587757105_b42c57bf-cb9c-478c-8327-5812a3286a64.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-driver-lp_release_prod_v587757105_b28cda41-22d2-446b-b3bf-5ca11d866719.ota", "fileVersion": 587757105, "fileSize": 215596, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-driver-lp_release_prod_v587757105_b28cda41-22d2-446b-b3bf-5ca11d866719.ota", "imageType": 16897, "manufacturerCode": 4476, "sha512": "5a7718b7c56e00cae0dc2396a339dde9d837f0ea0d023d653d93044eaf8841a9799f64301d09b3ad7fa0e6b81b76ca4b704291a30a8943c79f14ff0bfa05554e", "otaHeaderString": "EBL tradfri_light_lp\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-driver-lp_release_prod_v587757105_b28cda41-22d2-446b-b3bf-5ca11d866719.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-driver-zingo_release_prod_v16777220_1fd2b92c-45c5-44d0-97c5-5c71c2ecb8d2.ota", "fileVersion": 16777220, "fileSize": 257262, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-driver-zingo_release_prod_v16777220_1fd2b92c-45c5-44d0-97c5-5c71c2ecb8d2.ota", "imageType": 16649, "manufacturerCode": 4476, "sha512": "9e2560406937f8e342c391ef18ad8aefdea62adcc0c59fc163c3daf2f7dd91626e47466388a8a7b7e85ac0508d0eb6bce4891d1622fde52f508764921e3cd79d", "otaHeaderString": "GBL zingo_ikea_driver_hwpwm_ww\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-driver-zingo_release_prod_v16777220_1fd2b92c-45c5-44d0-97c5-5c71c2ecb8d2.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-light-unified-w_release_prod_v587806257_147c8812-e7f3-4999-b7eb-3da91009ab65.ota", "fileVersion": 587806257, "fileSize": 211572, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-light-unified-w_release_prod_v587806257_147c8812-e7f3-4999-b7eb-3da91009ab65.ota", "imageType": 16643, "manufacturerCode": 4476, "sha512": "e9ca743c2309cee9f12b481ac959da0d0ad095f009049c133c003447095a2295fac56969a682070cea75037e74ef37df7b93e39eda1fc4a3f4efe15fa9bace21", "otaHeaderString": "GBL GBL_tradfri_light_unified_w\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-light-unified-w_release_prod_v587806257_147c8812-e7f3-4999-b7eb-3da91009ab65.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-motion-sensor2_release_prod_v604241925_8afa2f7c-19c3-4ddf-a96c-233714179022.ota", "fileVersion": 604241925, "fileSize": 217548, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-motion-sensor2_release_prod_v604241925_8afa2f7c-19c3-4ddf-a96c-233714179022.ota", "imageType": 4552, "manufacturerCode": 4476, "sha512": "178bc4070db85787b2a8bc154a79ed10feb0a3dc06debc61f01f3e870effd36763569e601b75a2f64a9f5a409387dd5e18b7675e280966cdb0e57db0590c0cbc", "otaHeaderString": "GBL GBL_tradfri_motion_sensor2\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-motion-sensor2_release_prod_v604241925_8afa2f7c-19c3-4ddf-a96c-233714179022.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-onoff-controller_release_prod_v604241926_3c2e5569-667c-49ed-a286-78a0e031935d.ota", "fileVersion": 604241926, "fileSize": 205488, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-onoff-controller_release_prod_v604241926_3c2e5569-667c-49ed-a286-78a0e031935d.ota", "imageType": 4549, "manufacturerCode": 4476, "sha512": "41adf236facf5176dd7cd7aad4e8fcdfc9294e07ac5db0b0aad9f79d914817a04fb5c647d08d61188c10e52819ffc6058cfe93611d2ccf35234b9167a96e839a", "otaHeaderString": "GBL GBL_tradfri_onoff_controller", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-onoff-controller_release_prod_v604241926_3c2e5569-667c-49ed-a286-78a0e031935d.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-shortcut-button_release_prod_v604241926_56f5d8d1-78b1-4088-afc1-05d3b7e3314b.ota", "fileVersion": 604241926, "fileSize": 204932, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-shortcut-button_release_prod_v604241926_56f5d8d1-78b1-4088-afc1-05d3b7e3314b.ota", "imageType": 4550, "manufacturerCode": 4476, "sha512": "8527aa46afbf93cfaec68845cfc5092ca533b87ef0d162741eee82b32185a9d2966235edaae8127d9ee81a70f5247a23d75907b2387f520d797f3da1e9fdc3db", "otaHeaderString": "GBL GBL_tradfri_shortcut_button\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-shortcut-button_release_prod_v604241926_56f5d8d1-78b1-4088-afc1-05d3b7e3314b.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-signal-repeater_release_prod_v587753009_3ce8f096-bd12-4b2c-b66e-51dc9f2637dc.ota", "fileVersion": 587753009, "fileSize": 197052, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-signal-repeater_release_prod_v587753009_3ce8f096-bd12-4b2c-b66e-51dc9f2637dc.ota", "imageType": 4354, "manufacturerCode": 4476, "sha512": "f5166c7fb478b574269092b1086424cbc726bb05fa5512d5dcbb62f53b0fa2f184533d9467977e1e6ebc83e1ec6998b5097bfb65238b226b24a13ab54a9de731", "otaHeaderString": "GBL GBL_tradfri_zigbee_repeater\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-signal-repeater_release_prod_v587753009_3ce8f096-bd12-4b2c-b66e-51dc9f2637dc.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-sy5882-bulb-ws_release_prod_v587814449_185b3c4d-da1b-4867-8c16-2cee1fc5c11d.ota", "fileVersion": 587814449, "fileSize": 214716, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-sy5882-bulb-ws_release_prod_v587814449_185b3c4d-da1b-4867-8c16-2cee1fc5c11d.ota", "imageType": 16900, "manufacturerCode": 4476, "sha512": "a1ae0dd420adf21515a772631117c867712998d123c4c4a2c4655038aa5fc6104fc1cabc01fa18b8c8a9522efda86a53f2cfbc9557976a30b72c0068c4ded7d5", "otaHeaderString": "GBL GBL_tradfri_sy5882_bulb_ws\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-sy5882-bulb-ws_release_prod_v587814449_185b3c4d-da1b-4867-8c16-2cee1fc5c11d.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-sy5882-driver-ws_release_prod_v587798065_479fc716-673b-4731-bbb7-86833c456e4c.ota", "fileVersion": 587798065, "fileSize": 214572, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-sy5882-driver-ws_release_prod_v587798065_479fc716-673b-4731-bbb7-86833c456e4c.ota", "imageType": 16899, "manufacturerCode": 4476, "sha512": "a40d614191c884b2cfc50761c5d40b66b95a30194140154d2839110bb78ca034ee3b51dd33ecf146eac122743cbd157a6e77486dc1d8ec42b3aed72b8603ff09", "otaHeaderString": "GBL GBL_tradfri_sy5882_driver_ws", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-sy5882-driver-ws_release_prod_v587798065_479fc716-673b-4731-bbb7-86833c456e4c.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-sy5882-unified_release_prod_v587757105_d478807c-f16e-4989-a948-82818fb545b9.ota", "fileVersion": 587757105, "fileSize": 215656, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-sy5882-unified_release_prod_v587757105_d478807c-f16e-4989-a948-82818fb545b9.ota", "imageType": 16901, "manufacturerCode": 4476, "sha512": "5cf9948f9a048dcd9d4dbbdc9ef0f9ff64687c3d64fc8077b6368ee3c27ede31916f3132d4f2e86aadd13f1d8ba145cb90ff166e7b4b59951317d13747b3c2f3", "otaHeaderString": "GBL GBL_tradfri_sy5882_unified\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-sy5882-unified_release_prod_v587757105_d478807c-f16e-4989-a948-82818fb545b9.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-transformer_release_prod_v587753009_17d32c23-151f-44cb-a947-4c13a78f36e6.ota", "fileVersion": 587753009, "fileSize": 215340, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-transformer_release_prod_v587753009_17d32c23-151f-44cb-a947-4c13a78f36e6.ota", "imageType": 16641, "manufacturerCode": 4476, "sha512": "ab7efe310564f5e48bd122281092fe6e9be727a78825308d5597e5ac30995381d9b911e7f984ebccba2156da35450ec4c85854d51349e09acee23bb01bddc97a", "otaHeaderString": "EBL tradfri_light_ansluta_basic\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-transformer_release_prod_v587753009_17d32c23-151f-44cb-a947-4c13a78f36e6.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tradfri-wireless-dimmer_release_prod_v587367985_87ff9a75-c4e3-4999-a654-09bb8638f4cc.ota", "fileVersion": 587367985, "fileSize": 179390, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tradfri-wireless-dimmer_release_prod_v587367985_87ff9a75-c4e3-4999-a654-09bb8638f4cc.ota", "imageType": 4546, "manufacturerCode": 4476, "sha512": "d3691fa7980da249e6bd8ad416ca3ccec7e385ebcb49ed24408e985a4e5561a9bcd71918666c37e4f72f6aed672121b1fec98f2ee8718b69a5d575d8a5d02e6c", "otaHeaderString": "EBL tradfri_switch_basic\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/tradfri-wireless-dimmer_release_prod_v587367985_87ff9a75-c4e3-4999-a654-09bb8638f4cc.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "tretakt_smart_plug_soc-0x1100-2.4.25-prod.ota.ota.signed", "fileVersion": 33816613, "fileSize": 280318, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/tretakt_smart_plug_soc-0x1100-2.4.25-prod.ota.ota.signed", "imageType": 4352, "manufacturerCode": 4476, "sha512": "cd2f88f3f47f459218dd23d1317e76f413cea8a4eedee047a7a6627a595e742f47ec2783eeb2b33e634435cda3f219fa7b540dde6d67876f05d3fdf6feb636d2", "otaHeaderString": "GBL tretakt_smart_plug_soc\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/tretakt_smart_plug_soc-0x1100-2.4.25-prod.ota.ota.signed", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-jetstrom-cws_release_prod_v16777268_023f19b9-f55b-4c94-a3fe-00c97755eb78.ota", "fileVersion": 16777268, "fileSize": 329014, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-jetstrom-cws_release_prod_v16777268_023f19b9-f55b-4c94-a3fe-00c97755eb78.ota", "imageType": 51017, "manufacturerCode": 4476, "sha512": "9563658eca45bba7f6a91528694b8e6db4e78b8b72160310120bc11b48f76708d53328dfe6a0b26797a11ad4f1c4cae832515b9fb4c4135735bf0959e641b6d3", "otaHeaderString": "GBL zingo_jetstrom_cws\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-jetstrom-cws_release_prod_v16777268_023f19b9-f55b-4c94-a3fe-00c97755eb78.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-jetstrom-ws_release_prod_v33816584_33813e5d-0cc9-4b4a-9e93-9000ee44cbe4.ota", "fileVersion": 33816584, "fileSize": 249938, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-jetstrom-ws_release_prod_v33816584_33813e5d-0cc9-4b4a-9e93-9000ee44cbe4.ota", "imageType": 4357, "manufacturerCode": 4476, "sha512": "95d187ca0a3c4edba376571ea9631a3c04f0518b3593d83863b1f40ce5f3a51f9aa7459091f59e5e7c7e6f6cbea7d476d7d396424336bf96bd0e8586595f0cca", "otaHeaderString": "GBL zingo_lds_bulb_jetstrom_ws\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-jetstrom-ws_release_prod_v33816584_33813e5d-0cc9-4b4a-9e93-9000ee44cbe4.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-kt-bulb-hwpwmcs-ws_release_prod_v16842784_32dc5ff1-4fa4-4960-a847-5e548a81047c.ota", "fileVersion": 16842784, "fileSize": 307886, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-kt-bulb-hwpwmcs-ws_release_prod_v16842784_32dc5ff1-4fa4-4960-a847-5e548a81047c.ota", "imageType": 8708, "manufacturerCode": 4476, "sha512": "8053c97e5a587a806f24a2006eecc7ad43a968052d58b62b968bfb5fdf38c40f12e24a0908412cf8cc9cb59d2e92488ff6424fda4bdd0175f756a42c6724979e", "otaHeaderString": "GBL zingo_kt_bulb_hwpwmcs_ws\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-kt-bulb-hwpwmcs-ws_release_prod_v16842784_32dc5ff1-4fa4-4960-a847-5e548a81047c.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-kt-styrbar-remote_release_prod_v33816598_c00d5422-e816-48ec-87a6-40198661d2d5.ota", "fileVersion": 33816598, "fileSize": 218698, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-kt-styrbar-remote_release_prod_v33816598_c00d5422-e816-48ec-87a6-40198661d2d5.ota", "imageType": 4555, "manufacturerCode": 4476, "sha512": "b9bdc4022897cfcd826dd8f537bcef0a83929d3334f9838498e45fa81265ba2bc5182df0f022f92e283cab0dfaea897a3e2452ddf9d7b8a32405aa327b652973", "otaHeaderString": "GBL zingo_kt_styrbar_remote\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-kt-styrbar-remote_release_prod_v33816598_c00d5422-e816-48ec-87a6-40198661d2d5.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-lds-bulb-hwpwm-ww_release_prod_v16842756_529d7965-cee3-4c32-bd28-e5dd17ddc256.ota", "fileVersion": 16842756, "fileSize": 287186, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-lds-bulb-hwpwm-ww_release_prod_v16842756_529d7965-cee3-4c32-bd28-e5dd17ddc256.ota", "imageType": 8450, "manufacturerCode": 4476, "sha512": "cfd6b7b521b41aefd57eb0e474d07f22fad59aae07d2d23db8c0d1f59f5c7354b80162a2f95e3310a8f6923475a6e215a450b31db6551c0adba4ce8603dae9c6", "otaHeaderString": "GBL zingo_lds_bulb_hwpwm_ww\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-lds-bulb-hwpwm-ww_release_prod_v16842756_529d7965-cee3-4c32-bd28-e5dd17ddc256.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-lds-bulb-hwpwmcs-ws_release_prod_v65554_5d50205c-63c3-426d-bfba-4839c4b55bba.ota", "fileVersion": 65554, "fileSize": 236794, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-lds-bulb-hwpwmcs-ws_release_prod_v65554_5d50205c-63c3-426d-bfba-4839c4b55bba.ota", "imageType": 8709, "manufacturerCode": 4476, "sha512": "58bb981b05c1a1ba4c86f8c02facf1474a2d7877fd7f7155e15e7a03a91a38f5b070e27fcf1e53fd238bafb12b771e869655e864fd2385e2a83b5282055de8c9", "otaHeaderString": "GBL zingo-lds-bulb-hwpwmcs-ws\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-lds-bulb-hwpwmcs-ws_release_prod_v65554_5d50205c-63c3-426d-bfba-4839c4b55bba.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-lds-plugin-unit_release_prod_v65538_b29c3768-a102-4414-a84d-405c99654e74.ota", "fileVersion": 65538, "fileSize": 220362, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-lds-plugin-unit_release_prod_v65538_b29c3768-a102-4414-a84d-405c99654e74.ota", "imageType": 4365, "manufacturerCode": 4476, "sha512": "6a4eb16fbb01847b2487b6f8479b5f0681b3d53dce135919638c0f5d1ebc9cc29ea3cd86cd9472b61d2119102897059d593ffb5d6d2be6e7282a26599ecd5366", "otaHeaderString": "GBL 10078247-zingo-lds-plugin-un", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-lds-plugin-unit_release_prod_v65538_b29c3768-a102-4414-a84d-405c99654e74.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-lds-starkvind_release_prod_v69633_2044addf-0a35-4845-b851-74df04ab3a76.ota", "fileVersion": 69633, "fileSize": 231282, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-lds-starkvind_release_prod_v69633_2044addf-0a35-4845-b851-74df04ab3a76.ota", "imageType": 4364, "manufacturerCode": 4476, "sha512": "39dd95e0252a1b3a945528577e1246f6dbce9303505bf2b5d9bed467c903056e6ba399b90a765bc1595c67676d5c483976c221660113ea67390e5a83911fe811", "otaHeaderString": "GBL 10082261-zingo-lds-starkvind", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-lds-starkvind_release_prod_v69633_2044addf-0a35-4845-b851-74df04ab3a76.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-sigma-driver-silverglans-ww_release_prod_v65569_73466dc4-1320-4242-add6-717432538a77.ota", "fileVersion": 65569, "fileSize": 230566, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-sigma-driver-silverglans-ww_release_prod_v65569_73466dc4-1320-4242-add6-717432538a77.ota", "imageType": 16644, "manufacturerCode": 4476, "sha512": "e06f08a6091d7c5712d8c99997b7aa81cac20386ad4ae08b55ca75127e9156e4cddebfdf6cb7f71d6a3b64c672a550f7e46488c81a5281aa135793680221b026", "otaHeaderString": "GBL Signed OTA\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-sigma-driver-silverglans-ww_release_prod_v65569_73466dc4-1320-4242-add6-717432538a77.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-ws-soc_release_prod_v50331683_f909cf22-3452-47e3-b8b2-c59d82102e17.ota", "fileVersion": 50331683, "fileSize": 306706, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-ws-soc_release_prod_v50331683_f909cf22-3452-47e3-b8b2-c59d82102e17.ota", "imageType": 8710, "manufacturerCode": 4476, "sha512": "8458fac2bb64a28167c0c9b95d19d953a77e5ba839a218cd1eea338909d3e25dd204db0a3b43466182d71732c0d99ce6aa6a55b5f48b4207a999528daa371329", "otaHeaderString": "GBL zingo_ws_soc\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-ws-soc_release_prod_v50331683_f909cf22-3452-47e3-b8b2-c59d82102e17.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-ws_release_prod_v50331681_969bee21-eca6-4f10-a4c0-9685c0cd5d52.ota", "fileVersion": 50331681, "fileSize": 307758, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-ws_release_prod_v50331681_969bee21-eca6-4f10-a4c0-9685c0cd5d52.ota", "imageType": 8704, "manufacturerCode": 4476, "sha512": "079742b178c6a667e8965466fd9ff7554864a40c761a0dbc13cae68900088bf49883ab27f466ed4a47201f8fda697763ff354d0d0edfc85af6f8b417a74852c8", "otaHeaderString": "GBL zingo_ws\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-ws_release_prod_v50331681_969bee21-eca6-4f10-a4c0-9685c0cd5d52.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo-ww_release_prod_v16777282_a39f1e76-af87-4f04-bd7f-0faf71baf3e4.ota", "fileVersion": 16777282, "fileSize": 287186, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo-ww_release_prod_v16777282_a39f1e76-af87-4f04-bd7f-0faf71baf3e4.ota", "imageType": 8448, "manufacturerCode": 4476, "sha512": "346c769cb5a81638d38391afd8fa8acdbb3fbbe3f43994c6a09fc5551b551b7a84e13b27823cbe533d28a860b0286bd3fa47b768719d1568c5deb6ef1aec1706", "otaHeaderString": "GBL zingo_ww\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.ota.homesmart.ikea.com/files/zingo-ww_release_prod_v16777282_a39f1e76-af87-4f04-bd7f-0faf71baf3e4.ota", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "zingo_cws-0x2805-1.0.44-prod.ota.ota.signed", "fileVersion": 16777284, "fileSize": 335354, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/IKEA/zingo_cws-0x2805-1.0.44-prod.ota.ota.signed", "imageType": 10245, "manufacturerCode": 4476, "sha512": "c41e5f5c5caba112c83765055ace9794c2a53f92a7574727d94cfcef353abf6c84e69d53ffbeedb9d83205d534a272d34b9311a6d19e62a6e0cd19d7070af292", "otaHeaderString": "GBL zingo_cws\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fw.ota.homesmart.ikea.net/global/GW1.0/01.21.031/bin/zingo_cws-0x2805-1.0.44-prod.ota.ota.signed", "releaseNotes": "https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html" }, { "fileName": "1166-0109-17103685-rb262-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0109-17103685-rb262-1.7.16.ota", "imageType": 265, "manufacturerCode": 4454, "sha512": "63c1f5676ed175002c18466970f434f71ffc729f758f4d2b001fbbbc08d8241a13e710e310da5afdc42d83baf9be9dabd4df2581c2d6b70c20ca81d802a1f084", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-010a-17103685-rb267-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-010a-17103685-rb267-1.7.16.ota", "imageType": 266, "manufacturerCode": 4454, "sha512": "07213630168af80893093522bd122c521e6f8b08d44ec7d566adf267a7204e2ea3c32a1c3120980460d57ab5cdfdab753527c0e78f93edf51725ad8732656e3d", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-010b-17103685-rb243-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-010b-17103685-rb243-1.7.16.ota", "imageType": 267, "manufacturerCode": 4454, "sha512": "60fc7967ef0702d4a06ce3ce3cbeae6e9388d495c8506f849d2143b1c8f2ba181ea6d20d050b5dfc87e25f8d34e8aedafbced076dfdf3bd00598a74c9b74fc43", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-010d-17103685-rf262-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-010d-17103685-rf262-1.7.16.ota", "imageType": 269, "manufacturerCode": 4454, "sha512": "9c856d7f1aa28af576849cdb46c8e4d175f9ca3fc5d974b34d332e1ea2ab2c744f3ab9a5711a34d2f1a217b2b25f1363be59273bac7fbaeb20c5e5f101e0f876", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0112-10096610-RS_226v1-upgradeMe.ota", "fileVersion": 269051408, "fileSize": 230710, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0112-10096610-RS_226v1-upgradeMe.ota", "imageType": 274, "manufacturerCode": 4454, "sha512": "215e9c69557798b17153a921f76313c71bff860a569a8188e0b8d5db18f97eb5b7d92c36860509d7c2a6de2a4fe08f909a8390f1623b47654aa6290407b7a994", "otaHeaderString": "EBL ZBTDimmableLight_MG21A010F76" }, { "fileName": "1166-0115-20046A30-RS_230_C-upgradeMe.ota", "fileVersion": 537160240, "fileSize": 236978, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0115-20046A30-RS_230_C-upgradeMe.ota", "imageType": 277, "manufacturerCode": 4454, "sha512": "e6dbd3ab1d4b457cd6d8f102835ecb569364455de465d4068ea927551111137ed68da30d4b61c4c9908cd6cb701487cf50fb316ac2b1113c901daad5c13bd7c6", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-011A-20086A30-RS_229_T-upgradeMe.ota", "fileVersion": 537422384, "fileSize": 236066, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-011A-20086A30-RS_229_T-upgradeMe.ota", "imageType": 282, "manufacturerCode": 4454, "sha512": "0d21ed79d6c102790ec2017ec768c334cf175f381df3556505f6aa078d3b26373411b4ee0b656fb505923a178c94e13f4c6c0c2f0c09b3e72d227113aceed55f", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-011B-10116610-RB_245_v2-upgradeMe.ota", "fileVersion": 269575696, "fileSize": 230886, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-011B-10116610-RB_245_v2-upgradeMe.ota", "imageType": 283, "manufacturerCode": 4454, "sha512": "7f0201c41d7010261a363497694dda3b0cc03dd2a103c82bd5d6e13e85ea305c47ba8d62e65e21c5e0cb3a58da3ea52e04cbafb8b4aca057157a525d0871be20", "otaHeaderString": "EBL ZBTDimmableLight_MG21A010F76" }, { "fileName": "1166-011C-20026A30-RB_248_T_v2-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 236070, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-011C-20026A30-RB_248_T_v2-upgradeMe.ota", "imageType": 284, "manufacturerCode": 4454, "sha512": "74994baf707f7300353c63ab595a33ba4f1f8a8cb3a58b04d6fa8b68098e36e8cc863881c5491d480d037960ce8c0ce8543fafe74c409de9ffca64987cc893cc", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0120-20026A30-RF_263-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 231406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0120-20026A30-RF_263-upgradeMe.ota", "imageType": 288, "manufacturerCode": 4454, "sha512": "83410df888984666b83ecf87ee4fc0059082ac30acfbbadd8a3cdce62302501cc927aabd631dc741f3e351526caac506e7b80d5b26601c36b7064a1210ed2064", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0121-20026A30-RF_265-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 231406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0121-20026A30-RF_265-upgradeMe.ota", "imageType": 289, "manufacturerCode": 4454, "sha512": "c2f46b6db03e16664479f0293e7dfa88b6d92f3620bc277e86bf5e75f552fe7077f64fa95899748cd140f9a073c567c69e5fc5591e2377409fad2e0ae819b0ee", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0122-20026A30-RF_261-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 231406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0122-20026A30-RF_261-upgradeMe.ota", "imageType": 290, "manufacturerCode": 4454, "sha512": "3ddad977b5bfe38a842a0e1ab49c50769d76375ba2db2bbe5379f58e9116a0bea0e49f35d257b25ea895be39024ce21f54d31923311bc2c7d66ce1eca9a5dbd6", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0124-20046A30-RF_264-upgradeMe.ota", "fileVersion": 537160240, "fileSize": 231406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0124-20046A30-RF_264-upgradeMe.ota", "imageType": 292, "manufacturerCode": 4454, "sha512": "7d883752d7d3b215a90f1add7bc5b14a2c468dd72bf4936722627339c48d579be505c747e80cebf6669362f25c48423b523ce6fc78ab3a8d157d4b48a4a8332a", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0128-24031511-upgradeMe-RS 226.zigbee", "fileVersion": 604181777, "fileSize": 262013, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0128-24031511-upgradeMe-RS%20226.zigbee", "imageType": 296, "manufacturerCode": 4454, "sha512": "8d239c44c73d403c15448d5c7e4c15d1b25463bbb4681fdfd184cdb6e5f72b4797d71b746097b14ce0e09b631a9e6a4b4d103caa1175ccd257b9193fe7261120", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0129-24031511-upgradeMe-RS 227 T.zigbee", "fileVersion": 604181777, "fileSize": 268720, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0129-24031511-upgradeMe-RS%20227%20T.zigbee", "imageType": 297, "manufacturerCode": 4454, "sha512": "c7365c9576886a4fc4ddb6e1256d7dcb75f788ec92631e5cb13b8ae317b876954988632010fbe6604a5d1fd5f25936f61387dd6dda20ca61eb5848999278b964", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-012A-24031511-upgradeMe-RB 245.zigbee", "fileVersion": 604181777, "fileSize": 262013, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-012A-24031511-upgradeMe-RB%20245.zigbee", "imageType": 298, "manufacturerCode": 4454, "sha512": "a6606afeb0ce56b20d7aa839d40ec079851fa6b39a80a65707d4dbd900fb2ac2582db40b5dc519c473fc51d1e630bb81aded66545283b77f2ac5e91699771cac", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-012B-24031511-upgradeMe-RB 249 T.zigbee", "fileVersion": 604181777, "fileSize": 268720, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-012B-24031511-upgradeMe-RB%20249%20T.zigbee", "imageType": 299, "manufacturerCode": 4454, "sha512": "d3c86737e3a9de6102c013b00b69ea37c551debb898c25f5ce67bfdab4c52b8acca5932e34953d06087dd078d77d83d5d8654c695af6a18440fc2acea0c07f5f", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-012C-24031511-upgradeMe-RB 251 C.zigbee", "fileVersion": 604181777, "fileSize": 318314, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-012C-24031511-upgradeMe-RB%20251%20C.zigbee", "imageType": 300, "manufacturerCode": 4454, "sha512": "7be7ca7b54e3f404f164f0ae22386e6180f30a72334909648240a45490002b291e4031a6604d3ed7b894a1105a49f6d088df4a085f434a8b1be512a8552a995d", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-012D-24031511-upgradeMe-RB 266.zigbee", "fileVersion": 604181777, "fileSize": 262013, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-012D-24031511-upgradeMe-RB%20266.zigbee", "imageType": 301, "manufacturerCode": 4454, "sha512": "317a109be00c0f7b96e7bdb1d60eab57d68149e3a9da78b7fe21db165d22165fef389b8eb4c0463abe27d5e9680a48975a72687900f5baa5e2df5db16eda1348", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-012E-24031511-upgradeMe-RB 279 T.zigbee", "fileVersion": 604181777, "fileSize": 268720, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-012E-24031511-upgradeMe-RB%20279%20T.zigbee", "imageType": 302, "manufacturerCode": 4454, "sha512": "96c85aab44c85dfaecf0e5728b87de5920aa6d1030e18831cbfd737b5e0772937f55f0e7da1c5356ef6e6756f7c4cffbfc7d0ba095e63d053bde2535a450b72d", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-012F-24021511-upgradeMe -RB 286 C.zigbee", "fileVersion": 604116241, "fileSize": 302698, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-012F-24021511-upgradeMe%20-RB%20286%20C.zigbee", "imageType": 303, "manufacturerCode": 4454, "sha512": "3195366686f563306fa7fd9d41656498fb6d15e085d4f691d8464f6de074c1042ed33a6706deea6d9dd001ef535f74cd8dceae1deec38194ab1e3d92a596a11f", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0130-22151511-upgradeMe RS 232 C 20230714.ota", "fileVersion": 571806993, "fileSize": 319518, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0130-22151511-upgradeMe%20%20RS%20232%20C%2020230714.ota", "imageType": 304, "manufacturerCode": 4454, "sha512": "cfe7aab8c926345f38aa817a6ac652519cd891eeba8162e680b98dc91e6025ff150a04e618c8abc5528110965d1bf036f324619269d33bb97a535ce766b71a5f", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0131-24081511-upgradeMe RB 255 C 20230714.ota", "fileVersion": 604509457, "fileSize": 319414, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0131-24081511-upgradeMe%20%20RB%20255%20C%2020230714.ota", "imageType": 305, "manufacturerCode": 4454, "sha512": "0d1374689790b7d92eecc068303f87ef0d5df1aadddb0a498c9dca73db9313b2239be14b4b63420660da8512a692909cce64b6ff74769fa62bc4fef610c9c366", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0132-17103685-rb272t-1.7.16.ota", "fileVersion": 386938501, "fileSize": 285314, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0132-17103685-rb272t-1.7.16.ota", "imageType": 306, "manufacturerCode": 4454, "sha512": "62b9b55d90e15816f611ea1d174a1d1c05e8d854f635ff10ab9e4921e35926992efe7da605f558ed3172c81b91913cb836202a9a3d1649157b2fdcd1566e0261", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0133-17103685-rb247t-1.7.16.ota", "fileVersion": 386938501, "fileSize": 285314, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0133-17103685-rb247t-1.7.16.ota", "imageType": 307, "manufacturerCode": 4454, "sha512": "4da46054eea1c538f7382ea20ab2df708b64d2ac55c5732fe9e3ef9495a1dbd5f047a066952dd36143860d13ccb325e13c60c342c97faf70c5784608eb5fedc7", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0134-17103685-rf273t-1.7.16.ota", "fileVersion": 386938501, "fileSize": 285282, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0134-17103685-rf273t-1.7.16.ota", "imageType": 308, "manufacturerCode": 4454, "sha512": "e8978a70c31814c2b720280e1152198416df59d48ce200bcb942c5e1abd14ca55be76d5373df26d77ec28d1eeb4bbc7972fc9c2f420f4941901c7415023a0f31", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0135-17103685-rf274t-1.7.16.ota", "fileVersion": 386938501, "fileSize": 285282, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0135-17103685-rf274t-1.7.16.ota", "imageType": 309, "manufacturerCode": 4454, "sha512": "ee7f62c9a13949e69e1190088eda29ba0900ee0c6c90b00f90513d55975c8f1ae8b619d2ef4ba566b4e665d9d32a5601395540d9ab39350d3f99959f3e83f4ad", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0136-17103685-rf271t-1.7.16.ota", "fileVersion": 386938501, "fileSize": 285282, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0136-17103685-rf271t-1.7.16.ota", "imageType": 310, "manufacturerCode": 4454, "sha512": "a337dc7dcef398bda0ef273fde885162862e8e99a8e337c931b77cbc3dc9edb6fd6af3aaa133a756eb30edd525781838c3437c3d7b44dfeb710b2c5b1c154f84", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0209-17103685-bb262-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0209-17103685-bb262-1.7.16.ota", "imageType": 521, "manufacturerCode": 4454, "sha512": "8fcb605c73d677e38f48072562d39d0311deb56bd2ccd87324acefeea405b35e2945df929e8a3c753869acbbee89386258c49c9694adca40a9a3fa6dd7f3f7d2", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0220-20026A30-BF_263-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 231406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0220-20026A30-BF_263-upgradeMe.ota", "imageType": 544, "manufacturerCode": 4454, "sha512": "1fb89b2184d2564e0ecec1adbc2a839fb64bd7ebb0eecc53ea7158c23941546a0946873056c38807b0700c0d2822636576f86404371fff64204a1b06f0d5c010", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0221-20026A30-BF_265-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 231406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0221-20026A30-BF_265-upgradeMe.ota", "imageType": 545, "manufacturerCode": 4454, "sha512": "29c01a141853afb877ce7ba9df86b0eba2ddb067164d996333c71128fe15d7d3d7dcc4e0bd7662cb7dd440b1705d157aaf30479b75bec445c2b3e7c4a646f9d3", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-022D-24031511-upgradeMe-BY 266.zigbee", "fileVersion": 604181777, "fileSize": 262013, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-022D-24031511-upgradeMe-BY%20266.zigbee", "imageType": 557, "manufacturerCode": 4454, "sha512": "e3a75926cd6e19441dac4aec5a6c81aba32c0fc09fb74481cb1140e4284954497167ae2a897346d53b5cd14bb80bb52b6f14bf7356d5de300b1868f62b6ca8e7", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-022F-24021511-upgradeMe BY 286 C.zigbee", "fileVersion": 604116241, "fileSize": 302698, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-022F-24021511-upgradeMe%20BY%20286%20C.zigbee", "imageType": 559, "manufacturerCode": 4454, "sha512": "a6fdd9ab0f770665e126920143da7c0f66ef5d9fed5f2b76f030c4d21a51ae02d881ca31aa6cb80730246311f8bbaac2f9196b7840ac8bc99a6f22db738e9a00", "otaHeaderString": "Light\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0311-27016A30-SP_222-upgradeMe.ota", "fileVersion": 654404144, "fileSize": 235306, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0311-27016A30-SP_222-upgradeMe.ota", "imageType": 785, "manufacturerCode": 4454, "sha512": "cc6096c0d0f29ef27b883b2fe77672e1956fdcf8fbe028e61052f103d68af0d082d61e32dc41a583c9165186924b727e45c3e83e0b17f1077410cbea3d588720", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0312-27016A30-upgradeMe.zigbee", "fileVersion": 654404144, "fileSize": 235306, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0312-27016A30-upgradeMe.zigbee", "imageType": 786, "manufacturerCode": 4454, "sha512": "e04ad772d6e3c263adc8108eb80c811cd1aad5e750241bc77e44067f4b9530098fa31b7b03abec412d9d28e732f9d25ffaf96f7f0ec619e1a18b51cc90b5a35b", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0313-31016610-upgradeMe.ota", "fileVersion": 822175248, "fileSize": 229422, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0313-31016610-upgradeMe.ota", "imageType": 787, "manufacturerCode": 4454, "sha512": "91605b5a7f965ac34a1e72bd2e2adba7abea877182b043b6d21e78ef1aa68005eba7011c548116ef2df74d29c7036e6a2616c65597705fd64d01d2f528ceb986", "otaHeaderString": "GBL Smart Home Plug\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0401-17103685-ae262-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0401-17103685-ae262-1.7.16.ota", "imageType": 1025, "manufacturerCode": 4454, "sha512": "c7bfbb09c6ee55816575099d875b46ad3009329cc651dddf23bb24b8b5b9fdc5a87374f18d54deb9f7ae6e7e012d300969fbd56cf18a8618635b5fc64888a60c", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0402-17103685-ae264-1.7.16.ota", "fileVersion": 386938501, "fileSize": 280658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0402-17103685-ae264-1.7.16.ota", "imageType": 1026, "manufacturerCode": 4454, "sha512": "fdff1b901c31d0e443cab9465cf3d7941e318878079bd25f2dcadd697fd304b7eb6e876f123ad4a55a1da7f4dfb8d5619d87ed00dce36bcfbc7ed050073a1f61", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0414-20026A30-AE_280_C-upgradeMe.ota", "fileVersion": 537029168, "fileSize": 236826, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0414-20026A30-AE_280_C-upgradeMe.ota", "imageType": 1044, "manufacturerCode": 4454, "sha512": "9e80707893222af177ceb158353d1829f5df6b86dab5edda769f62fd60bb4ae8289e63c1d3f139ea4db4a21a6c0779de36767645ee6a51ce6dd9a04d6af81958", "otaHeaderString": "EBL C688_Freelocate\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0430-17103685-ae270t-1.7.16.ota", "fileVersion": 386938501, "fileSize": 285282, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0430-17103685-ae270t-1.7.16.ota", "imageType": 1072, "manufacturerCode": 4454, "sha512": "a1821008181b2dca54ffb61a99f49fe8c8d8b39fd984d57423e34691de32666d9b2f8bbd3f7f6a929d67229c239b55fcfeb490f352cc0ccc0cb361408bb17b75", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0980-20036A30-RCL_240_T-upgradeMe.ota", "fileVersion": 537094704, "fileSize": 235482, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0980-20036A30-RCL_240_T-upgradeMe.ota", "imageType": 2432, "manufacturerCode": 4454, "sha512": "9acfaa6978264a588a742a65148536269737c46f81b337709b2cd61f2fdf8ba8e67b94445dd3f85348dfe7d5bb167e03ebef941cde23384b0d1ca17b93d0e5a3", "otaHeaderString": "EBL C610_light_mg1p_ZBTColorTemp" }, { "fileName": "DZM32-SN_1.13.ota", "fileVersion": 50397203, "fileSize": 294682, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/DZM32-SN_1.13.ota", "imageType": 4660, "manufacturerCode": 4655, "sha512": "e09e2d301520bf618dd7511ec6df4ac0d659a8a515014d89326610b303df17b992c3ae125ce918debda459c4e783c71546ffe6270c4ae47f89c2673e420c8ab3", "otaHeaderString": "EFR32MG21_Z3\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://files.inovelli.com/firmware/DZM32-SN/Beta/1.13/DZM32-SN_1.13.ota" }, { "fileName": "VZM30-SN_0.03.ota", "fileVersion": 17825795, "fileSize": 228406, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/VZM30-SN_0.03.ota", "imageType": 272, "manufacturerCode": 4655, "sha512": "e3e4d75124c74d0595af42e9371b590098d63e86bbda515ca9a309c39894388d8fcf378969c1038d57806809464aa8a975e4f32a18d1f6836605252b58f1a9ea", "otaHeaderString": "VZM30-SN_OnOff\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://files.inovelli.com/firmware/VZM30-SN/Beta/0.03/VZM30-SN_0.03.ota" }, { "fileName": "VZM31-SN_2.18-Production.ota", "fileVersion": 16908818, "fileSize": 310258, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/VZM31-SN_2.18-Production.ota", "imageType": 257, "manufacturerCode": 4655, "sha512": "6d480a5d621a16bb3a57fcc1af09071fc528dab2a8f3e479620c3bd75ddfa9e8f624c32b1dc35d5c1bc8db0f67a70ee150ce3a516a2026f717076dcbeba23df7", "otaHeaderString": "EBL VM_SWITCH\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "VZM35-SN_1.07.ota", "fileVersion": 33685767, "fileSize": 232994, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/VZM35-SN_1.07.ota", "imageType": 513, "manufacturerCode": 4655, "sha512": "cc2e1639debcebb42e8cb8e48559258378112a94eaf6480721ee9ce5b5ab8d562a2b8c0c6df8819b1db9b3a539178f1b1bbf039243cf1184efe1024e4c8f05d0", "otaHeaderString": "VZM35_MG24\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://files.inovelli.com/firmware/VZM35-SN/Beta/1.07/VZM35-SN_1.07.ota" }, { "fileName": "VZM36_1.01-Beta.ota", "fileVersion": 67174657, "fileSize": 237938, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/VZM36_1.01-Beta.ota", "imageType": 1025, "manufacturerCode": 4655, "sha512": "daa5570379960cc959d867160bc21d3cf651926af63c4c3076a02810cee1a4b8a4def1a097b2bdaea818c712f39fb93c37ccf84aeb0ada4eaedebd9b1c4eb882", "otaHeaderString": "VMZB_Canopy\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://github.com/InovelliUSA/Firmware/raw/6608d75c73b5200b20ed197653c9ad3af1f5c1e7/Blue-Series/Zigbee/VZM36-Fan-Plus-Light/Beta/1.01/VZM36_1.01-Beta.ota" }, { "fileName": "V00.00.52.12.ota", "fileVersion": 5212, "fileSize": 578278, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Insta/V00.00.52.12.ota", "imageType": 529, "manufacturerCode": 4474, "sha512": "ab539a1d694d9488a0deeaa2b0b809bc19b88ba7323eecba7f8abff7e989cee43112b29eb39a8548c0da480d258d7be0f681adb5b848bece0b2a55692c3b2b95", "otaHeaderString": "EBL Zigbee_System'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_HS_4f_GJ_Release_10.03.32.02.zigbee", "fileVersion": 36832016, "fileSize": 133287, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Insta/ZLL_HS_4f_GJ_Release_10.03.32.02.zigbee", "imageType": 53249, "manufacturerCode": 4474, "sha512": "a1d34e74b0f65e6808e403f930c58c365622ee6ad157fa0cd3b6e93043797cae1d45ac3f72b7c43dfce046bca3cf6bb3848043bbb29fae1e5b49237d6d403d9d", "otaHeaderString": "450727 " }, { "fileName": "ZLL_WS_4f_J_Release_10.03.32.02.zigbee", "fileVersion": 36832016, "fileSize": 134487, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Insta/ZLL_WS_4f_J_Release_10.03.32.02.zigbee", "imageType": 53250, "manufacturerCode": 4474, "sha512": "53c6cadc6a83d39f2cfa2e29d3d7f15e48ead17f5e4d46ae81466c263696cbe9ec7656818a900f6d963d8bc081e0c0671662a12504e53ec73751e151c84c410a", "otaHeaderString": "450728 " }, { "fileName": "20190408_EUROTRONIC_Spirit_Zigbee_0x00122C380.ota", "fileVersion": 19055488, "fileSize": 185294, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/20190408_EUROTRONIC_Spirit_Zigbee_0x00122C380.ota", "imageType": 4364, "manufacturerCode": 4151, "sha512": "4e870adf42d60f196f353f87a5c16d4782f38a2fc3cd977a4cddea237789eb7847363f4810216b434a5fc269971e25ad0671cfc4e6aa5c4a3c7946f01702dbf4", "otaHeaderString": "DR1175r1v2--JN5179--ENCRYPTED000" }, { "fileName": "BSM300Z_OTA_ENC_V4_ENC.ota", "fileVersion": 4, "fileSize": 174558, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/BSM300Z_OTA_ENC_V4_ENC.ota", "imageType": 4097, "manufacturerCode": 4151, "sha512": "1d20a4c887bb3aef48819c4d9d1608f82a8e5ea58d37e98788288d717278681aeef870555a8cb08a45dd9e7e558f8eb9c3a9330c1911e5beb15939226b772fae", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "CSM300Z_TOF_OTA_ENC_V15_ENC.ota", "fileVersion": 15, "fileSize": 191534, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/CSM300Z_TOF_OTA_ENC_V15_ENC.ota", "imageType": 4105, "manufacturerCode": 4151, "sha512": "8b99e6ca27ee6c4f6ffdeb9f569fb7bfcd724d8b06897495fe6bd59aeaf1634886aa161b99159f8883ef4fd224d32adfb4489bcd8ef7c1ea87071a51a28f6d9c", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "DIO300Z_OTA_ENC_V5_ENC.ota", "fileVersion": 5, "fileSize": 188702, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/DIO300Z_OTA_ENC_V5_ENC.ota", "imageType": 4120, "manufacturerCode": 4151, "sha512": "f5a51866ce803e15aed135d81e0a101cabcda00701a73aeaff3d0248dfbab27fc7412559d6b12f7ded8511200f5eda846eaad32b75c2d4bb60434afd379a6c52", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "DLM300Z_OTA_ENC_V8_ENC.ota", "fileVersion": 8, "fileSize": 173246, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/DLM300Z_OTA_ENC_V8_ENC.ota", "imageType": 4101, "manufacturerCode": 4151, "sha512": "e54631f1ed754cd5025ec9f0493a59ad6b03987968cc2f7013cd85f6e09d734652bb81500c62a0ae671bc70029db368719524f03929a514e981a7cf66ed6a19e", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "DMS300Z_OTA_ENC_V3_ENC.ota", "fileVersion": 3, "fileSize": 177790, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/DMS300Z_OTA_ENC_V3_ENC.ota", "imageType": 4111, "manufacturerCode": 4151, "sha512": "1551dbfdb952f8b6e93b62c6c895d59b82d3cd8d3b5ebf80e5c4347cea85f341d4fe726223d4b6330fbbbe25df76c5172c2ed2b788d2ee06aa4125503656e932", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "DSM300Z_OTA_ENC_V5_ENC.ota", "fileVersion": 5, "fileSize": 175902, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/DSM300Z_OTA_ENC_V5_ENC.ota", "imageType": 4098, "manufacturerCode": 4151, "sha512": "b672359b105c3803386743e71d71908a3c582e807fcf68ec0ad89e264f51e555ac27b4ba67b51116b8c0812aa1ba84c321876892e47a8e7062df3a54cca79d7f", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "GCM300Z_OTA_ENC_V1_ENC.ota", "fileVersion": 1, "fileSize": 171086, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/GCM300Z_OTA_ENC_V1_ENC.ota", "imageType": 4115, "manufacturerCode": 4151, "sha512": "03a2af79ecb80faf4595fca429dfd70af6581d9d62868049873d2f05c633d9445051c0663ca87f394c59fe70104ee6ca94b0de19d970acd637e7f528e9aa7e83", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "ISM300Z_OTA_ENC_V3_ENC.ota", "fileVersion": 3, "fileSize": 192926, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/ISM300Z_OTA_ENC_V3_ENC.ota", "imageType": 4110, "manufacturerCode": 4151, "sha512": "fb3df739fb53d5ccc316be0889dac0dfd47f8b1a85ddd9bcd657724ef767ddde6b7cdffb935afdf5ccaad2218d21e043cb1a909efdbca1acdc52dd3b1c2ee330", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "MSM300Z_OTA_ENC_V4_ENC.ota", "fileVersion": 4, "fileSize": 175486, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/MSM300Z_OTA_ENC_V4_ENC.ota", "imageType": 4102, "manufacturerCode": 4151, "sha512": "bf4e464bcfeb6356b31909a5e9d2097345ec47fd44b4f24afb10039412a545c09e2dc88f131c774cbd686304dde7feb2b0e9469c90342ee4e7e3bc1623760460", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "PMM300Z2_OTA_ENC_V6_ENC.ota", "fileVersion": 6, "fileSize": 212478, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/PMM300Z2_OTA_ENC_V6_ENC.ota", "imageType": 4107, "manufacturerCode": 4151, "sha512": "10b0cffcf7dc90804694a52be1ff3ca8178a8685172e309da9eacc71109403fd53be9a127b881341a095cb3ae2284eaedd5bec49a15d7b9d8badd76e382be4b5", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "SBM300ZB_OTA_ENC_V4_ENC.ota", "fileVersion": 4, "fileSize": 178798, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/SBM300ZB_OTA_ENC_V4_ENC.ota", "imageType": 4104, "manufacturerCode": 4151, "sha512": "0199440a282e299fa026c44645fe2612cb5bf29edaf55587ad2f9e6b7b064a82b137feef0c6e202091cac0febc465c0ed51153dd3d620fdae2286e9d3057120b", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "TCM300Z_OTA_ENC_V5_ENC.ota", "fileVersion": 5, "fileSize": 221166, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/TCM300Z_OTA_ENC_V5_ENC.ota", "imageType": 4119, "manufacturerCode": 4151, "sha512": "e1414ec9cde43992fd8e22d39419fdf2c87f4383da677e3499587cf004e70ee7fbe7894816c50c640a0717a59c087ba167cea3c96ee579862f40e73b0d66ee1d", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "TSM300Z_OTA_ENC_V6_ENC.ota", "fileVersion": 6, "fileSize": 184686, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/TSM300Z_OTA_ENC_V6_ENC.ota", "imageType": 4100, "manufacturerCode": 4151, "sha512": "3c625264337a918802322eb48225ee41e4490466c4d10a1172a52fcbd4aee2184a7c5eae832d1ba0a281e71dc9df1edb0a008611e40e825a2393d29bec062ec8", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "USM300Z_OTA_ENC_V6_ENC.ota", "fileVersion": 6, "fileSize": 185246, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/USM300Z_OTA_ENC_V6_ENC.ota", "imageType": 4103, "manufacturerCode": 4151, "sha512": "55de851ccec9042d8fcc2c9de66c4a53f8d7584119efcaf299021be9efe5007c05bad27691c396c453ea67fb0f15f4a1d7cb48bf5819ccd85913636d4831d555", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00" }, { "fileName": "jethome_zigbee_release_15_zigbee.ota.zigbee", "fileVersion": 15, "fileSize": 160242, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/JetHome/jethome_zigbee_release_15_zigbee.ota.zigbee", "imageType": 61441, "manufacturerCode": 61731, "sha512": "53efac6c622306df81fd6d36d52eca301df138aedfc1bf3366a5d77b685cf2c3e20016eff83d1cf62ab8257dfefa91af05e6086429c6b56517e342d07d0a27b8", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://fw.jethome.ru/media/firmwares/jethome/zigbee/release/15/jethome_zigbee_release_15_zigbee.ota.zigbee", "manufacturerName": [ "JetHome" ], "releaseNotes": "https://fw.jethome.ru/devices/jethome/WS7/fw/release/latest/CHANGELOG.md" }, { "fileName": "1189_007c_11436630_Release.ota", "fileVersion": 289629744, "fileSize": 238322, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/1189_007c_11436630_Release.ota", "imageType": 124, "manufacturerCode": 4489, "sha512": "e501377b05e04dc72f2b220ce19002f4a3b34a3f132b9dcc047e2b498e8c70999d93e7d7e5703b42fbb787876bb89ebde43dd8dc50bb64d201000a3f9294447a", "otaHeaderString": "EBL AcSensor_Y\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=124&version=17.67.102.48", "releaseNotes": "1. Fix bug that sensor freeze after long time running in big system.\r\n2. Fix bug that sensor automatic left network occasionally." }, { "fileName": "1189_0097_11436630_Release.ota", "fileVersion": 289629744, "fileSize": 240006, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/1189_0097_11436630_Release.ota", "imageType": 151, "manufacturerCode": 4489, "sha512": "e477bea2476d14f8ea6c639fec2a90f01d631dab3e0184ecfe11cfb5f0453a1cd0348eee3a72950d33ca370df573c8f71716cdcde97ffe272272158a4c1a2fe6", "otaHeaderString": "EBL AcSensor_Y\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=151&version=17.67.102.48", "releaseNotes": "1. Fix bug that sensor freezing after long time running.\r\n2. Fix bug that sensor automatic leave network occasionally.\r\n3. Fix manual reset fail if press more than 15 seconds." }, { "fileName": "A19_RGBW_IMG0019_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 180052, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A19_RGBW_IMG0019_00102428-encrypted.ota", "imageType": 25, "manufacturerCode": 4489, "sha512": "1a269383342ad612e3a30eafdb2363a4d2feda40718bae10a6eeb0970d2771df670287b9741e37cce14db1d40a39b5bb334226836171565cb9cf23d5349cd04d", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "A19_TW_10_year_IMG000D_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170800, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A19_TW_10_year_IMG000D_00102428-encrypted.ota", "imageType": 13, "manufacturerCode": 4489, "sha512": "60a0a7447a209707257775697c16150844641442c4e192b89fe1858c50e3df54f77a8102d113de8cd75b3c3ea64eeeb68211d5affdb9e605cf830c119dcf4415", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "A19_W_10_year_IMG000C_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170140, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A19_W_10_year_IMG000C_00102428-encrypted.ota", "imageType": 12, "manufacturerCode": 4489, "sha512": "4efd3c4d802dc32489b1de0aca342bf6e7c55f2ce488987889707d7a513538f6901e39f0fdfdd461622ecaa8c2322237aaf73a5a50523b55f60b4efdb49fc70b", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "A60S_RGBW-0x1189-0x00A0-0x03197310-MF_DIS-20240523095111-3221010102432.ota", "fileVersion": 51999504, "fileSize": 314466, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A60S_RGBW-0x1189-0x00A0-0x03197310-MF_DIS-20240523095111-3221010102432.ota", "imageType": 160, "manufacturerCode": 4489, "sha512": "d0ccca81ba3598d79b70ab4ee836c230d725cf02e5ee4b6feff7d4301c3c86afbe2b30504e96cee88b5333aec580d2c1e1b09cfbc4d4e359f744b86deb6f26cc", "otaHeaderString": "A60S_RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=160&version=3.25.115.16", "releaseNotes": "(1) Security patch\r\n(2) Refine RGBW color control.\r\n(3) Support sleep mode in Hue automation settings. " }, { "fileName": "A60S_TW-0x1189-0x00A2-0x03177310-MF_DIS-20240426153518-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A60S_TW-0x1189-0x00A2-0x03177310-MF_DIS-20240426153518-3221010102432.ota", "imageType": 162, "manufacturerCode": 4489, "sha512": "ed88b8e69fbae0d071962bd9d65b0d54353c20f8824e5a07ddc5a3dcc586ad8aea8a809e97aff6d49bc41140c5a0bbac7e3519b19562bdb98d5ecfc01225c703", "otaHeaderString": "A60S_TW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=162&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "A60_DIM_Z3_IM003D_01056400-encrypted_202129091152_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185112, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A60_DIM_Z3_IM003D_01056400-encrypted_202129091152_withoutMF.ota", "imageType": 61, "manufacturerCode": 4489, "sha512": "393017c8b6fe46366e9cbfdec965caf1abaa9db310d6cbcd61017f5341000066e38cf848f7fa6ddb0d259542299a008a955dc22381702925b5cd989715339173", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=61&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "A60_RGBW_Value_II-0x1189-0x008A-0x03197310-MF_DIS-20240523093911-3221010102432.ota", "fileVersion": 51999504, "fileSize": 314402, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A60_RGBW_Value_II-0x1189-0x008A-0x03197310-MF_DIS-20240523093911-3221010102432.ota", "imageType": 138, "manufacturerCode": 4489, "sha512": "c1506be6d427dff52ef7cc3d1d877fcab87d413f22866af878b3e0e801a8ba7bf863061052387091bb14a126dff04d6dc2897b38be2598c20056fc56b580aee0", "otaHeaderString": "A60_RGBW_Value_II\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=138&version=3.25.115.16", "releaseNotes": "(1) Security patch\r\n(2) Refine RGBW color control.\r\n(3) Support sleep mode in Hue automation settings. " }, { "fileName": "A60_TW_Value_II-0x1189-0x008B-0x03177310-MF_DIS-20240426150951-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A60_TW_Value_II-0x1189-0x008B-0x03177310-MF_DIS-20240426150951-3221010102432.ota", "imageType": 139, "manufacturerCode": 4489, "sha512": "8366767213db679d702c51e9130554afcd55730ff8a1203d08d4d43729b2f9905f061f6a89b51bfce7d8da85bc5102ee38948680c002861822683cc451ed4211", "otaHeaderString": "A60_TW_Value_II\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=139&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "A60_TW_Z3_IM003C_01056400-encrypted_202129091149_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185972, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/A60_TW_Z3_IM003C_01056400-encrypted_202129091149_withoutMF.ota", "imageType": 60, "manufacturerCode": 4489, "sha512": "6f9a19ff9388d9db2ed7b769db40f156c425fc46a1b67b24ec2eb47ae40b21e2a9b4cc7cc717f24847e134d28c88a545909cd18aad012058eef3bfca7fec3981", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=60&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "B40S_TW-0x1189-0x00A3-0x03177310-MF_DIS-20240426154101-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/B40S_TW-0x1189-0x00A3-0x03177310-MF_DIS-20240426154101-3221010102432.ota", "imageType": 163, "manufacturerCode": 4489, "sha512": "1600634bb26a61cb275d022c3a234f4ce62a77b75ac38b36bba8c45363ccfe639c0e77890473133acb3448c7ed70387c3945967d411af5f58b21c513f915e8c0", "otaHeaderString": "B40S_TW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=163&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "B40_DIM_Z3_IM0034_01056400-encrypted_202129091146_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185112, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/B40_DIM_Z3_IM0034_01056400-encrypted_202129091146_withoutMF.ota", "imageType": 52, "manufacturerCode": 4489, "sha512": "b2b40193c7536afef6cc1364a075d2546c1f812ae395addf37b96c4532ee2d1e43cd871ce254a1f16c499f77717e96e2d7bc07ef72c3d71d3a7451031c822b36", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=52&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "B40_TW_Value-0x1189-0x008C-0x03177310-MF_DIS-20240426151750-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/B40_TW_Value-0x1189-0x008C-0x03177310-MF_DIS-20240426151750-3221010102432.ota", "imageType": 140, "manufacturerCode": 4489, "sha512": "e7d3fad4b806240d3a8016dd69b356038549f20130e154a5ed763e2e6b4c0954833d9f452c1bb9b37b0ae0fa5a5c616cb83563e57f628ee2b62c1449378888ae", "otaHeaderString": "B40_TW_Value\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=140&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "B40_TW_Z3_IM0033_01056400-encrypted_202129091140_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/B40_TW_Z3_IM0033_01056400-encrypted_202129091140_withoutMF.ota", "imageType": 51, "manufacturerCode": 4489, "sha512": "8bcea29b2c059391bbfcd0b9998db6c2328ee1cc988226049462d164373cc0b2a84178e0ae4a9331d022b35cf8f2bf93d800ece0bbabbf0554298fa69a665256", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=51&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "BR30_RGBW_IMG001B_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 179100, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/BR30_RGBW_IMG001B_00102428-encrypted.ota", "imageType": 27, "manufacturerCode": 4489, "sha512": "68bfb341e4a3327bfe6c3ac2a469bf9c8497298ef4ea05a956058bc306bf33e1fd664068521d9f99704eeb95f1a1c6ade63a03f142cc4f4ee45c6a30220da247", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "BR30_TW_IMG001A_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170776, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/BR30_TW_IMG001A_00102428-encrypted.ota", "imageType": 26, "manufacturerCode": 4489, "sha512": "8fffbd0b82ecdabdd76890c8def88c431116a9daf7c0fa6cc7bbab14dc893be235fbc092375e70fe0679a1a710aba246bdf7f47536242256148a6a11987f8d70", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "BR30_W_10_year_IMG000F_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170120, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/BR30_W_10_year_IMG000F_00102428-encrypted.ota", "imageType": 15, "manufacturerCode": 4489, "sha512": "928f699ccb59d763a442e0d00ec842c5c95b59c0354d2b447b172f98ef9410a90e8315a460c122d3ba4f8e502fe8a6ec8e40b44c798855a995ff536b405232f4", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "CLA60_RGBW_Z3_IM0011_01066400-encrypted_202126110358_withoutMF.ota", "fileVersion": 17196032, "fileSize": 193900, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/CLA60_RGBW_Z3_IM0011_01066400-encrypted_202126110358_withoutMF.ota", "imageType": 17, "manufacturerCode": 4489, "sha512": "e6667216432104472db651baec065ecb2cd6a7e00efa5ea56ca57ad0c5b8e3c67ad07feb8fced88dde2c4fbdcd4b90cdc41343ca9edbf69df37b35395596dbf0", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=17&version=1.6.100.0", "releaseNotes": "1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement " }, { "fileName": "Conv_Under_Cabinet_TW_IMG0021_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170776, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Conv_Under_Cabinet_TW_IMG0021_00102428-encrypted.ota", "imageType": 33, "manufacturerCode": 4489, "sha512": "4857e26de657a952f7878edfb03b58020e3b64715bfd3c00d7f0f78bb9c8e8f3002ae2d10bd3c8cbc2e071b7909ae27436cdfe82fb3c4b3f360963a49e5b3806", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "DIM-A60_DIM_T-0x00CD-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188384, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-A60_DIM_T-0x00CD-0x03203660.OTA", "imageType": 205, "manufacturerCode": 4489, "sha512": "a7be48d6c9e1473748af00bf8696ead99497c7dc5006f76a888ca6e386ed0d266047ff08c4303b85b1f56ea4edaca4f03a0c2aab3842a24707c2fd4fcd73ec8a", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=205&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM-A60_FIL_DIM_T-0x00D0-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188416, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-A60_FIL_DIM_T-0x00D0-0x03203660.OTA", "imageType": 208, "manufacturerCode": 4489, "sha512": "3854e452e13ff0c973531e99a842d36fcbff6a4d43f2edd57a472bde48b32eb590759fcaf96ff0824845e1b9c63666adc21d4070c367ff3b18505ff7cab0d5f3", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=208&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM-B40_DIM_T-0x00B8-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188384, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-B40_DIM_T-0x00B8-0x03203660.OTA", "imageType": 184, "manufacturerCode": 4489, "sha512": "d50a9ebeb0a3d3eefabaf593b71aa229213b44d90046220e753be73e5c72a51c7fb419db27fb667bc1a949f6c9a159edf68ce5bf24f49c7056a668c6d0ee0504", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=184&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM-EDISON60_FIL_DIM_T-0x00D1-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188416, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-EDISON60_FIL_DIM_T-0x00D1-0x03203660.OTA", "imageType": 209, "manufacturerCode": 4489, "sha512": "92741c25cf6da726cf1d83d45b3d8420546a52c3db0b6c8237551e8d71b8badb6bd3023348d58b94118eebb454cafc06e7a778c3899555b9af5f3e4cef81ae37", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=209&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM-GLOBE60_FIL_DIM_T-0x00D2-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188416, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-GLOBE60_FIL_DIM_T-0x00D2-0x03203660.OTA", "imageType": 210, "manufacturerCode": 4489, "sha512": "e686f9d515678dcf10bba87545f2588c39354635c2b5e95ac060b5447600819c733c436ce16dcf6122c35d4b24cb4d7a4288c8fc9e71985f88d23ee111bb23ed", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=210&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM-P40_DIM_T-0x00CE-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188384, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-P40_DIM_T-0x00CE-0x03203660.OTA", "imageType": 206, "manufacturerCode": 4489, "sha512": "b3867cfd8f681ae951ac614b822ad4c0368d4eb5fcf4055d643fb1abf07aebdc763e32f5bc06aa3d491202b4db2621db4bd9810189d2af8bb1c6d85e4c5ac3dd", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=206&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM-PAR16_DIM_T-0x00CF-0x03203660.OTA", "fileVersion": 52442720, "fileSize": 188384, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM-PAR16_DIM_T-0x00CF-0x03203660.OTA", "imageType": 207, "manufacturerCode": 4489, "sha512": "2085b60c00c66103d3d178822f210df1f1243c445f2dcd582d854ba244a7ac5993f88f1621fd33eade6282c9df9411304ce8cf6301689309d88e7a95128e573e", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=207&version=3.32.54.96", "releaseNotes": "1. Support maximum 30 groups\r\n2. Enable the watchdog\r\n3. Set the Tx power to 9.8dB\r\n4. For Filament dimmable bulbs only, set the minimum level to 3%(according to APP) = the minimum PWM duty cycle is 15/255" }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_1200_16W_830ZBVR-0x1189-0x00DE-0x02056550-MF_DIS-20230612083031-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_1200_16W_830ZBVR-0x1189-0x00DE-0x02056550-MF_DIS-20230612083031-322101076832.ota", "imageType": 222, "manufacturerCode": 4489, "sha512": "393c6424ec18ebb05b0c1d4aa9b8c7ebffcf2eb27201dd9e66462623456af1b95ddc70436bfb9d5b19ff9665cecab3a419cbf543f38008cbd6c0d2da541facff", "otaHeaderString": "TUBE_T8_CON_1200_16W_830ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=222&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_1200_16W_840ZBVR-0x1189-0x00C7-0x02056550-MF_DIS-20230612075626-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_1200_16W_840ZBVR-0x1189-0x00C7-0x02056550-MF_DIS-20230612075626-322101076832.ota", "imageType": 199, "manufacturerCode": 4489, "sha512": "a5fe0c1a70dbf225591ba8cfde1dbdd49d8997e5e0a850a9f06d83781a45247572e4ace263e93e3381843c2f42076f6a0266b040aff2ee9e7bd513d31592cb19", "otaHeaderString": "TUBE_T8_CON_1200_16W_840ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=199&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_1200_16W_865ZBVR-0x1189-0x00C8-0x02056550-MF_DIS-20230612080310-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_1200_16W_865ZBVR-0x1189-0x00C8-0x02056550-MF_DIS-20230612080310-322101076832.ota", "imageType": 200, "manufacturerCode": 4489, "sha512": "f31f82039d767be2f7b85eb9d0f7dd9c2573a1558b6745eb588a8191ff0edd6d7ba36400ecee5697845c28910360635521922667ccd49ced2ab7fa37d55612f6", "otaHeaderString": "TUBE_T8_CON_1200_16W_865ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=200&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_1500_24W_830ZBVR-0x1189-0x00DD-0x02056550-MF_DIS-20230612083725-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_1500_24W_830ZBVR-0x1189-0x00DD-0x02056550-MF_DIS-20230612083725-322101076832.ota", "imageType": 221, "manufacturerCode": 4489, "sha512": "b69c54bc99b7ecf1b1a13ac41ef52963f0ab41e8fdf0f2b032aa28fbbe1ab069caf3ec235f5619767a8478f5858f831961ddaa2cf8ce8edc9704acb08d132cd1", "otaHeaderString": "TUBE_T8_CON_1500_24W_830ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=221&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_1500_24W_840ZBVR-0x1189-0x00C9-0x02056550-MF_DIS-20230612081003-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_1500_24W_840ZBVR-0x1189-0x00C9-0x02056550-MF_DIS-20230612081003-322101076832.ota", "imageType": 201, "manufacturerCode": 4489, "sha512": "f23ea8d9035ae309dbe1b444316ea8a19436c5f0c2f100e8ea7399e4d31a6c4eee5751b8d65bf09eaeb5321cc2963075b4e4c89204cf4b2350926c59d3eb9bfd", "otaHeaderString": "TUBE_T8_CON_1500_24W_840ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=201&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_1500_24W_865ZBVR-0x1189-0x00CA-0x02056550-MF_DIS-20230612081654-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_1500_24W_865ZBVR-0x1189-0x00CA-0x02056550-MF_DIS-20230612081654-322101076832.ota", "imageType": 202, "manufacturerCode": 4489, "sha512": "2dfb53a2e8f3db70d12485bcf0c563251c3593bff503b677e5b3988092a28ed782558d0976237da3145d5998c005ce55cf43e60ebc54701cbb1c6139b221a290", "otaHeaderString": "TUBE_T8_CON_1500_24W_865ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=202&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_600_7_5W_830ZBVR-0x1189-0x00DF-0x02056550-MF_DIS-20230704080741-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_600_7_5W_830ZBVR-0x1189-0x00DF-0x02056550-MF_DIS-20230704080741-322101076832.ota", "imageType": 223, "manufacturerCode": 4489, "sha512": "6510d671f8a216cbb08ecfea2033318fcc8e5c55c3290c576c614e7f98f948b9a40d2cc5c720bc0b3d754a087ba3cc053a6780f5fc8af6317145177df0ee1cba", "otaHeaderString": "TUBE_T8_CON_600_7_5W_830ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=223&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_600_7_5W_840ZBVR-0x1189-0x00CB-0x02056550-MF_DIS-20230704075326-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_600_7_5W_840ZBVR-0x1189-0x00CB-0x02056550-MF_DIS-20230704075326-322101076832.ota", "imageType": 203, "manufacturerCode": 4489, "sha512": "10cccaed0ea69674eeffa6df77eea4873be3d9e8c29fa21a6c1ff1d45305e74741942f47ea23aad6a1f3385fdf4b482e681265ae82bbde4c6acb52f4ad75530c", "otaHeaderString": "TUBE_T8_CON_600_7_5W_840ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=203&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_ENERGY-TUBE_T8_CON_600_7_5W_865ZBVR-0x1189-0x00CC-0x02056550-MF_DIS-20230704080035-322101076832.ota", "fileVersion": 33908048, "fileSize": 197266, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_ENERGY-TUBE_T8_CON_600_7_5W_865ZBVR-0x1189-0x00CC-0x02056550-MF_DIS-20230704080035-322101076832.ota", "imageType": 204, "manufacturerCode": 4489, "sha512": "678cb88d600622f7c62c55c5728c6dd2dcb384210550dd88dcd71355425197ca2ff76c6f541b6f7aba474cd029ee8a06a75236f6174deed229678742b33dede0", "otaHeaderString": "TUBE_T8_CON_600_7_5W_865ZBVR\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=204&version=2.5.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x02116550-MF_DIS-20230710044635-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-DL_PFM155UGR_04-0x1189-0x0087-0x02116550-MF_DIS-20230710044635-3221010102432.ota", "imageType": 135, "manufacturerCode": 4489, "sha512": "b4c7cb37a7cee34df7d6935222cbf56f8a25b5a5ff5831bd4a906336b4fa9cfab30d8d5841d642c3ac6fe47067773173921119ace67cf9326e98de20b74d0eae", "otaHeaderString": "DL_PFM155UGR_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=135&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x02116550-MF_DIS-20230708054143-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-DL_PFM195UGR_04-0x1189-0x0088-0x02116550-MF_DIS-20230708054143-3221010102432.ota", "imageType": 136, "manufacturerCode": 4489, "sha512": "df38535a415e0db5e7eafb32c81b6ee04068cdcdf78d658073649197661b157335f0dc7cec4e26369a99ed688461db045aa4ea8e48c8007b2e2fc337734b353b", "otaHeaderString": "DL_PFM195UGR_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=136&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x02116550-MF_DIS-20230710043240-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-LN_Indivi1200_04-0x1189-0x0084-0x02116550-MF_DIS-20230710043240-3221010102432.ota", "imageType": 132, "manufacturerCode": 4489, "sha512": "c240d8f145914f61aaf701d1a772f7cc72a1a778df368261cf6175ae685f1354f4ab86df50d682547f0e54a80db02efbf6ddaa39583086800563a7c585ee67f3", "otaHeaderString": "LN_Indivi1200_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=132&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x02116550-MF_DIS-20230708053449-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-LN_Indivi1500_04-0x1189-0x0085-0x02116550-MF_DIS-20230708053449-3221010102432.ota", "imageType": 133, "manufacturerCode": 4489, "sha512": "d84bc210d6a8a1f0e7f625d68ae9f9d6d69f37248084e025ba520bd9192090b2f6e9f20233e16478dfa9e8316631e490dda02fa7aabfa184033a75bbf69ff49e", "otaHeaderString": "LN_Indivi1500_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=133&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_DI1200_04-0x1189-0x0086-0x02116550-MF_DIS-20230710043941-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_DI1200_04-0x1189-0x0086-0x02116550-MF_DIS-20230710043941-3221010102432.ota", "imageType": 134, "manufacturerCode": 4489, "sha512": "ce82c7e358c5a5b23112939da57d220f94f58f4ca5a644827c9dc35ae1c31b6c563e3e914bdcbdb80b84a51c0a2cb21d19d22aae9c82b6cca0693e0aaf6a444e", "otaHeaderString": "PL_DI1200_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=134&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x02116550-MF_DIS-20230710041844-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_Indivi600_04-0x1189-0x0082-0x02116550-MF_DIS-20230710041844-3221010102432.ota", "imageType": 130, "manufacturerCode": 4489, "sha512": "5412293f3bb6338d8423aa7e6e33760da3b465dc4e6459305f819c7388fd983ff51103f8bc5ad48d63941eaa3b581a278eefd88bf7c32fbaa8bc9275a54703a4", "otaHeaderString": "PL_Indivi600_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=130&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x02116550-MF_DIS-20230710042543-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_Indivi625_04-0x1189-0x0083-0x02116550-MF_DIS-20230710042543-3221010102432.ota", "imageType": 131, "manufacturerCode": 4489, "sha512": "b6d8215e1fe268f953bc5989f46d4d7ff894b661f7bdf5103e7d015fe35b77987b37c57869b5a9cc2865bdc0b616be5466d0a667be322cb0518321fdf4056d63", "otaHeaderString": "PL_Indivi625_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=131&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x02116550-MF_DIS-20230710040457-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_PFM600UGR_04-0x1189-0x0080-0x02116550-MF_DIS-20230710040457-3221010102432.ota", "imageType": 128, "manufacturerCode": 4489, "sha512": "678c6940cc738d658658c2bd759b3e831c55c28a9cb121f4dcd2ab6c99e388e0d341af886cb5ab5c58d77847d72f6c2d1c34c10cdcedebcf29532045e955fdc8", "otaHeaderString": "PL_PFM600UGR_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=128&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_PFM600_04-0x1189-0x0089-0x02116550-MF_DIS-20230710045334-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_PFM600_04-0x1189-0x0089-0x02116550-MF_DIS-20230710045334-3221010102432.ota", "imageType": 137, "manufacturerCode": 4489, "sha512": "31f8d55de2ed328c6c00195499c0cecd8ec8eeb86a147deac7b7f92719600e7fe1390fd78dd03d5bb5e5e1521dec63bba760eea757662a6d50f5b813fcc2df64", "otaHeaderString": "PL_PFM600_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=137&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x02116550-MF_DIS-20230710041154-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_PFM625UGR_04-0x1189-0x0081-0x02116550-MF_DIS-20230710041154-3221010102432.ota", "imageType": 129, "manufacturerCode": 4489, "sha512": "f568beab5a4f3266d22a658ea9bdd877f7192004560252682ad4e74ad56e27b4a59bbb8869f615aa8e80c6b21f7cb9e6f52123a8729886bb7e1de188ac924aa6", "otaHeaderString": "PL_PFM625UGR_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=129&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DIM_UART-PL_PFM625_04-0x1189-0x007F-0x02116550-MF_DIS-20230710035807-3221010102432.ota", "fileVersion": 34694480, "fileSize": 197986, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DIM_UART-PL_PFM625_04-0x1189-0x007F-0x02116550-MF_DIS-20230710035807-3221010102432.ota", "imageType": 127, "manufacturerCode": 4489, "sha512": "7573438156a9b63831d2a804d00e83c551b510337a664a261e63baef996d91c9f6a7c47206a23a8f7f962d1a375ce43ac489f1e4c4419f0b09ff7ca8e6e43224", "otaHeaderString": "PL_PFM625_04\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=127&version=2.17.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table.\r\n3. Fix abnormal device announcement." }, { "fileName": "DL_HCL_DN150_01_IM0096_010A6400-encrypted_202331101115_withoutMF.OTA", "fileVersion": 17458176, "fileSize": 179616, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/DL_HCL_DN150_01_IM0096_010A6400-encrypted_202331101115_withoutMF.OTA", "imageType": 150, "manufacturerCode": 4489, "sha512": "bcabc6ddc1c94263acab14bb699b01082dd013d2617269fdb8ca3f0e192fd2f9f0a854dec96917d8684259598c26276238073f0daf4ea008eae4db5e991e68b7", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=150&version=1.10.100.0", "releaseNotes": "1. Fix bug that Biolux Gen I luminaire lost HCL mode occasionally." }, { "fileName": "Downlight_TW_HCL_IM0065_00103201-encrypted_09_20_2019_Fri_142050_70_withoutMF.ota", "fileVersion": 1061377, "fileSize": 179976, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Downlight_TW_HCL_IM0065_00103201-encrypted_09_20_2019_Fri_142050_70_withoutMF.ota", "imageType": 101, "manufacturerCode": 4489, "sha512": "0e9ad28f4e950d73417489ec5f046d72c93f71d327eb23123d16941a5e6b42e0ca8b0c75cf56a443ab2d0d7d6d23433bfed4533dde65ccb07c177802872ddb0c", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Edge_Lit_Under_Cabinet_IMG0023_00102411-encrypted.ota", "fileVersion": 1057809, "fileSize": 170492, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Edge_Lit_Under_Cabinet_IMG0023_00102411-encrypted.ota", "imageType": 35, "manufacturerCode": 4489, "sha512": "4b657dd79631031aac8fd8baa1dd7ad1f1c7d1ba34aa40849bd4fb543ea0917dfee11c579aaa31f2c5eb8bdaa725b2140dd68c021cb7252492f79cf0040e4fa4", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "FLEX_Outdoor_RGBW_IMG001F_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 179960, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/FLEX_Outdoor_RGBW_IMG001F_00102428-encrypted.ota", "imageType": 31, "manufacturerCode": 4489, "sha512": "6bb62d30849317b975d5f614a5a9f9a7d8e2b82519584b1b9ef69555b77646f9082dcde22c6212ed8cc184b35dee64e31d486a739b023efe81a9624dde69b89a", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "FLEX_RGBW_IMG001E_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 178908, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/FLEX_RGBW_IMG001E_00102428-encrypted.ota", "imageType": 30, "manufacturerCode": 4489, "sha512": "4e49ac2d15af8e28157db8d70f782d1a2e2eb783f0a11a0a2da623d2c8d5b29dd5165971de241fa1c2b21d9bebb2b13289336203cc2d509705937c31b9838648", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Flex_RGBW_Z3_IM002A_01066400-encrypted_202216020348_withoutMF.ota", "fileVersion": 17196032, "fileSize": 193948, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Flex_RGBW_Z3_IM002A_01066400-encrypted_202216020348_withoutMF.ota", "imageType": 42, "manufacturerCode": 4489, "sha512": "511af78337e0e0a68140c062733b852fe03c9ff67f15e4a93dccb9ee64eb37bad8f184e3edfb4ac4d587ff8abb6a00c24121993f8a20d6a365b7218423acaaf0", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=42&version=1.6.100.0", "releaseNotes": "1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement " }, { "fileName": "Flushmount_TW_IMG0022_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170776, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Flushmount_TW_IMG0022_00102428-encrypted.ota", "imageType": 34, "manufacturerCode": 4489, "sha512": "e73eeb57bb577cb3a365583d3e0cb44524941f25ac5f69e7bb68f707a68fb397e075598b79edaea2267cf488ba731ea13731833508c5d3ed60c00e8d7d4dab84", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Gardenpole_Mini_RGBW_Z3_IM0040_01066400-encrypted_202210011005_withoutMF.ota", "fileVersion": 17196032, "fileSize": 193948, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Gardenpole_Mini_RGBW_Z3_IM0040_01066400-encrypted_202210011005_withoutMF.ota", "imageType": 64, "manufacturerCode": 4489, "sha512": "7a34a152205947dc93ae9748099140f20fb87dbc47c6fea09ddc8bb8cf53b594aa47dc7f7e3fac92f7db9b8ed8eeaaad7adc03f47b90501959fb76e72652d940", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=64&version=1.6.100.0", "releaseNotes": "1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement " }, { "fileName": "Gardenpole_RGBW_Z3_IM003B_01066400-encrypted_202216020351_withoutMF.ota", "fileVersion": 17196032, "fileSize": 193940, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Gardenpole_RGBW_Z3_IM003B_01066400-encrypted_202216020351_withoutMF.ota", "imageType": 59, "manufacturerCode": 4489, "sha512": "130bf1100ebe5d9225a110e8446e9555929380358ee9a7d726a00562b46c0a9ce50b66f94fb1cfb9d6fdf82547c0773512d6f5d78a070a46183b07ddcd3d0cc6", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=59&version=1.6.100.0", "releaseNotes": "1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement " }, { "fileName": "LEDVANCE_DIM-0x1189-0x006F-0x03177310-MF_DIS-20240428033446-3221010102432.ota", "fileVersion": 51868432, "fileSize": 297066, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/LEDVANCE_DIM-0x1189-0x006F-0x03177310-MF_DIS-20240428033446-3221010102432.ota", "imageType": 111, "manufacturerCode": 4489, "sha512": "f4cb4724cca17e70883068bd886fa785be6c2e091270663712a97c019446eccd21c25a821cf0035d55a39cc2efdaac16419da47945e4be5faaa1bcf483892c09", "otaHeaderString": "LEDVANCE_DIM\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=111&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "Outdoor_Accent_Light_RGB_IMG0020_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 178524, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Outdoor_Accent_Light_RGB_IMG0020_00102428-encrypted.ota", "imageType": 32, "manufacturerCode": 4489, "sha512": "a3284885687a2424b61d05285e3021e91ad5627bfe50566eaabb5b2bf55444c0d1064b66292d7679f6a3f320281c4dfb7262d2466492d4b54dcd7237dbf5aebf", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Outdoor_FLEX_RGBW_Z3_IM005C_01066400-encrypted_202210011002_withoutMF.ota", "fileVersion": 17196032, "fileSize": 193920, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Outdoor_FLEX_RGBW_Z3_IM005C_01066400-encrypted_202210011002_withoutMF.ota", "imageType": 92, "manufacturerCode": 4489, "sha512": "0f6b59d25e4ca7047c898b9b74358f4efbf89ae37f7b8f706dccac741d36023a71624addac0ed7e4a60da8c96995395a10dea15cbafaa8030130261a1f9f5110", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=92&version=1.6.100.0", "releaseNotes": "1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement " }, { "fileName": "P40S_TW-0x1189-0x00A4-0x03177310-MF_DIS-20240426154635-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/P40S_TW-0x1189-0x00A4-0x03177310-MF_DIS-20240426154635-3221010102432.ota", "imageType": 164, "manufacturerCode": 4489, "sha512": "b6cda1628d320f1644420100c79c5a1888ae4257a53f4dc490f18e83a3f69120b33944b4faa3613f80a3c9ebff3749e9fa74af1506c04254b8d11d4e1801ccb4", "otaHeaderString": "P40S_TW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=164&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "P40_TW_Value-0x1189-0x008D-0x03177310-MF_DIS-20240426152357-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/P40_TW_Value-0x1189-0x008D-0x03177310-MF_DIS-20240426152357-3221010102432.ota", "imageType": 141, "manufacturerCode": 4489, "sha512": "840e07b58c0ca4df0285e8d49ef192ce05d005ed189e619bd3342a4579ab9f464ead3ae0bd941e9ed29c2df05e68e520de7d2f07102753ea634089dbe9459ab0", "otaHeaderString": "P40_TW_Value\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=141&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "PAR16S_RGBW-0x1189-0x00A6-0x03197310-MF_DIS-20240523095706-3221010102432.ota", "fileVersion": 51999504, "fileSize": 314462, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16S_RGBW-0x1189-0x00A6-0x03197310-MF_DIS-20240523095706-3221010102432.ota", "imageType": 166, "manufacturerCode": 4489, "sha512": "fb4132facbb0d4c2db44a5f1a55a062516fd95bfd157ae8184710b785f488655174eca264a47da8dc2d05f145d80beee4b07adfc40e41546fa3ed73fd88be7ef", "otaHeaderString": "PAR16S_RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=166&version=3.25.115.16", "releaseNotes": "(1) Security patch\r\n(2) Refine RGBW color control.\r\n(3) Support sleep mode in Hue automation settings. " }, { "fileName": "PAR16S_TW-0x1189-0x00A5-0x03177310-MF_DIS-20240426155214-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16S_TW-0x1189-0x00A5-0x03177310-MF_DIS-20240426155214-3221010102432.ota", "imageType": 165, "manufacturerCode": 4489, "sha512": "bbd0f303f2f47b5a4c99e13188860e18a7b2a21ea808b035356182bf3e6d42306a34312c8126d9c6f505884332a1b72bfac6a4049e41da04b843992d09ab0839", "otaHeaderString": "PAR16S_TW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=165&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "PAR16_DIM_Z3_IM0031_01056400-encrypted_202129091143_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185112, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16_DIM_Z3_IM0031_01056400-encrypted_202129091143_withoutMF.ota", "imageType": 49, "manufacturerCode": 4489, "sha512": "9ead17873dd80c19bd7c5ed9647f15a02f66d314a9ead8f142f93e1bc32139badc403e33fa0a2fd4d74d35defd0ed1df41eebba7abed88e1519e9d4e88f97927", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "PAR16_RGBW_Value-0x1189-0x008E-0x03197310-MF_DIS-20240523094504-3221010102432.ota", "fileVersion": 51999504, "fileSize": 314422, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16_RGBW_Value-0x1189-0x008E-0x03197310-MF_DIS-20240523094504-3221010102432.ota", "imageType": 142, "manufacturerCode": 4489, "sha512": "805309879e74f225530aebe850d4f86a43b48273dfe24046addbb263b8f580b69d44e9b2c712098e6c8386e7865122cb14564b0e17d0ce1c24397384bf4d0031", "otaHeaderString": "PAR16_RGBW_Value\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=142&version=3.25.115.16", "releaseNotes": "(1) Security patch\r\n(2) Refine RGBW color control.\r\n(3) Support sleep mode in Hue automation settings. " }, { "fileName": "PAR16_RGBW_Z3_IM0030_01066400-encrypted_202126110402_withoutMF.ota", "fileVersion": 17196032, "fileSize": 193956, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16_RGBW_Z3_IM0030_01066400-encrypted_202126110402_withoutMF.ota", "imageType": 48, "manufacturerCode": 4489, "sha512": "46ecf61b702216a68504b404ec6bcadb69a79aa42658a440cad157cf023d5d01e7d3cc36f2d095e9b9b2eef93a0d1a1e5827f1ebebb0b8353af6a3defba30bdd", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=48&version=1.6.100.0", "releaseNotes": "1. Rollback Protection enabled \r\n2. Fade off feature removed\r\n3. ZLO commands support\r\n4. Color Improvement " }, { "fileName": "PAR16_TW_Value-0x1189-0x008F-0x03177310-MF_DIS-20240426152944-3221010102432.ota", "fileVersion": 51868432, "fileSize": 307510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16_TW_Value-0x1189-0x008F-0x03177310-MF_DIS-20240426152944-3221010102432.ota", "imageType": 143, "manufacturerCode": 4489, "sha512": "8e68bc40a53736aac839cb57b6b4424b893a826e8a4f755dc9286b16ce06b1fea9307938ca5f4e68695ea3c43b76b6e0c34479b269f0c80e19b1379be5c2d5b1", "otaHeaderString": "PAR16_TW_Value\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=143&version=3.23.115.16", "releaseNotes": "(1) Add security patch. " }, { "fileName": "PAR16_TW_Z3_IM002E_01056400-encrypted_202129091137_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR16_TW_Z3_IM002E_01056400-encrypted_202129091137_withoutMF.ota", "imageType": 46, "manufacturerCode": 4489, "sha512": "d330be4e977f4ebfc5cbf6357c0eb0d36c957b5972d2e45c1b2ed74960d09df4bd65f9d745161e84dae65ecf24a911d0c3a17a1113e82ab0ae2fe831d6f70ae1", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=46&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "PAR38_W_10_year_IMG0010_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170120, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PAR38_W_10_year_IMG0010_00102428-encrypted.ota", "imageType": 16, "manufacturerCode": 4489, "sha512": "584fef08d2cc42006e14f9c3b632c6536355932beab0d8166fd7a32096ad3d7cd515c946e48195c356b175f08653bc80a589ce656dd1c98ce888043edbc4a95d", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "PLUG_COMPACT_EU_T-0x00D6-0x032B3674-MF_DIS.OTA", "fileVersion": 53163636, "fileSize": 197136, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PLUG_COMPACT_EU_T-0x00D6-0x032B3674-MF_DIS.OTA", "imageType": 214, "manufacturerCode": 4489, "sha512": "bc45a149b84113718eee4a127fc0bcb4c25c905efa805140de278adc7730461717b256bebe2befdce7e777078e498aae4369e48e9f899526630c4f5a658d933c", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=214&version=3.43.54.116", "releaseNotes": "1. Add the \"startuponoff\" attribute.\r\n2. After a successful OTA, the socket maintains its previous state." }, { "fileName": "PLUG_EU_T-0x00D4-0x032B3674-MF_DIS.OTA", "fileVersion": 53163636, "fileSize": 197136, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PLUG_EU_T-0x00D4-0x032B3674-MF_DIS.OTA", "imageType": 212, "manufacturerCode": 4489, "sha512": "de0dbabc38386eef03c3a49330a72e21be11cc16f1254da44c5b2d1d828e18150d3ae80bb05a357ae4f1f7188d621dac4f1263244eeab944ffd271d8dc158c96", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=212&version=3.43.54.116", "releaseNotes": "1. Add the \"startuponoff\" attribute.\r\n2. After a successful OTA, the socket maintains its previous state." }, { "fileName": "PLUG_OUTDOOR_EU_T-0x00C2-0x032B3674-MF_DIS.OTA", "fileVersion": 53163636, "fileSize": 197136, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PLUG_OUTDOOR_EU_T-0x00C2-0x032B3674-MF_DIS.OTA", "imageType": 194, "manufacturerCode": 4489, "sha512": "249ec36e24e3a8858093d04967bee25eb350870993fa033a2e04a123242aa52d1f276526fb893abd39e4a85409f5d364e5c3ad0bbb555479604ee4edf4bf0843", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=194&version=3.43.54.116", "releaseNotes": "1. Add the \"startuponoff\" attribute.\r\n2. After a successful OTA, the socket maintains its previous state." }, { "fileName": "PLUG_UK_T-0x00D5-0x032B3674-MF_DIS.OTA", "fileVersion": 53163636, "fileSize": 197136, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PLUG_UK_T-0x00D5-0x032B3674-MF_DIS.OTA", "imageType": 213, "manufacturerCode": 4489, "sha512": "db2ed859306e90874012e305e984dedd5d437410ae920c71d0dfb6814ce74c65c41ade1d6c63892c91c6ae6d648ac5be253c35344bbec270d08b462eb940e6cb", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=213&version=3.43.54.116", "releaseNotes": "1. Add the \"startuponoff\" attribute.\r\n2. After a successful OTA, the socket maintains its previous state." }, { "fileName": "PL_HCL600_01_IM0095_010A6400-encrypted_202331101118_withoutMF.OTA", "fileVersion": 17458176, "fileSize": 179680, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PL_HCL600_01_IM0095_010A6400-encrypted_202331101118_withoutMF.OTA", "imageType": 149, "manufacturerCode": 4489, "sha512": "7b56e97da0f534b598d03c753d19ce041f1a0831b5a61b6f004485923d542c6048bfaf612f4c4e025f22aec9a790b7c94098b4bdd783b0c8d2c79f171037f05e", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=149&version=1.10.100.0", "releaseNotes": "1. Fix bug that Biolux Gen I luminaire lost HCL mode occasionally." }, { "fileName": "PL_HCL625_01_IM0094_010A6400-encrypted_202331101122_withoutMF.OTA", "fileVersion": 17458176, "fileSize": 179680, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/PL_HCL625_01_IM0094_010A6400-encrypted_202331101122_withoutMF.OTA", "imageType": 148, "manufacturerCode": 4489, "sha512": "600b8867605e9cfb1f6aa6e5907617d687e68d333ef27fe2cb7c970297f9e95041d601f65dca315f3a90c30cd62d7f0ab12dff78a297c8a66ba4f56f935a3ac6", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=148&version=1.10.100.0", "releaseNotes": "1. Fix bug that Biolux Gen I luminaire lost HCL mode occasionally." }, { "fileName": "Panel_TW_HCL_IM0063_00103201-encrypted_09_18_2019_Wed_113705_07_withoutMF.ota", "fileVersion": 1061377, "fileSize": 179964, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Panel_TW_HCL_IM0063_00103201-encrypted_09_18_2019_Wed_113705_07_withoutMF.ota", "imageType": 99, "manufacturerCode": 4489, "sha512": "bfb076e5ba37c99b06b9da01938c165a54f5f5a61559969b27e2ebcccbba31954d176c501346275124fe856e0ebab6660f1276af7de4302046e4dcf91d79fee2", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Panel_TW_Z3_IM005A_01056400-encrypted_202129091207_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185972, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Panel_TW_Z3_IM005A_01056400-encrypted_202129091207_withoutMF.ota", "imageType": 90, "manufacturerCode": 4489, "sha512": "4d197641675c06b65e3fde9b65cf805772bb0e249066b74d35644fea1d20c53f967234e8128357b82a8ba960227fe20f2f3bb077a3a1cdfbb30c8e9c53673ad9", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=90&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "Plug_Value-0x1189-0x0067-0x031F7310-MF_DIS-20240710124454-3221010102432.ota", "fileVersion": 52392720, "fileSize": 292762, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Plug_Value-0x1189-0x0067-0x031F7310-MF_DIS-20240710124454-3221010102432.ota", "imageType": 103, "manufacturerCode": 4489, "sha512": "80ce3557353d90aa106fecb237befd780861dcc710431826444f65c98edb120f5f7517c2bf9b52cce72cadc9f275a2e7f2b963dd3a1c831adadf55acebe58f38", "otaHeaderString": "Plug_Value\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=103&version=3.31.115.16", "releaseNotes": "(1) Add security patch.\r\n(2) Fix reset button bug in smart outdoor plug." }, { "fileName": "Plug_Z3_IM002D_00103101-encrypted_12_07_2018_Fri_103650_94_withoutMF.ota", "fileVersion": 1061121, "fileSize": 178996, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Plug_Z3_IM002D_00103101-encrypted_12_07_2018_Fri_103650_94_withoutMF.ota", "imageType": 45, "manufacturerCode": 4489, "sha512": "3ebc116780b1b69bed344d94937b1e6280bd7ca477f49958fbb28cbf634f747f4358f307126652ef1c46b94a7f964ccba44e0da62b1acb7a19d10a7f477c7da2", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "RT_RGBW_IMG001D_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 179088, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/RT_RGBW_IMG001D_00102428-encrypted.ota", "imageType": 29, "manufacturerCode": 4489, "sha512": "41c07ffddaa00c8665b4cdef34c34f0a1f1701ad62c6ffee00d5bc408d08865cf163cda2ab382c9770e00bd6c5b296873abc2bfe85f0473d926905c2024c6286", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "RT_TW_IMG001C_00102428-encrypted.ota", "fileVersion": 1057832, "fileSize": 170776, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/RT_TW_IMG001C_00102428-encrypted.ota", "imageType": 28, "manufacturerCode": 4489, "sha512": "deb1f04253eeff026532a9732f9e544847be88d01b866316babea77e1f36d0125cd1033124af15c0cbf5014eb38cc46927165ce0ba99620117d132c6fa1221f7", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02236550-MF_DIS-20230609102634-3221010102432.ota", "fileVersion": 35874128, "fileSize": 208078, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/TW_UART_ENERGY-DL_HCL_ND150_02-0x1189-0x00BB-0x02236550-MF_DIS-20230609102634-3221010102432.ota", "imageType": 187, "manufacturerCode": 4489, "sha512": "e1719c9df94e390b00f670fcf1864cad8273fb0554986ffa0281f3fdc76a65bc37304793d937fea7555fd01de69cc8ce5cca8bd9f6780a6ca91ab5b0d04073de", "otaHeaderString": "DL_HCL_ND150_02\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=187&version=2.35.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02236550-MF_DIS-20230609103517-3221010102432.ota", "fileVersion": 35874128, "fileSize": 208054, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/TW_UART_ENERGY-PL_HCL300x1200_01-0x1189-0x00BC-0x02236550-MF_DIS-20230609103517-3221010102432.ota", "imageType": 188, "manufacturerCode": 4489, "sha512": "c12b7a8695472aa0c3ebf3e43f504bdbf58c65ef81c137d2718644416214de45054f440eb341c486767f9a4b9fdfaa665575066ad153c56ddbdf50a08a2f863e", "otaHeaderString": "PL_HCL300x1200_01\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=188&version=2.35.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02236550-MF_DIS-20230609100910-3221010102432.ota", "fileVersion": 35874128, "fileSize": 208078, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/TW_UART_ENERGY-PL_HCL600_02-0x1189-0x00B9-0x02236550-MF_DIS-20230609100910-3221010102432.ota", "imageType": 185, "manufacturerCode": 4489, "sha512": "23655edfb8be126dd44463e0e4cd7c0cda3e804166ef559a98c2ec8ae70ca0d49a177e2a10f6823af736160aea52f2efb3f983057220abe1f894d69c8cd55ad8", "otaHeaderString": "PL_HCL600_02\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=185&version=2.35.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02236550-MF_DIS-20230609101747-3221010102432.ota", "fileVersion": 35874128, "fileSize": 208078, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/TW_UART_ENERGY-PL_HCL625_02-0x1189-0x00BA-0x02236550-MF_DIS-20230609101747-3221010102432.ota", "imageType": 186, "manufacturerCode": 4489, "sha512": "38e3242e350178d74c2447851c5314db9b522ba380dc26850bc66b8fc9fac367afae2e65a62daf398bb5baf06851cc3b2065af7c0782a6dda3709fee712455f2", "otaHeaderString": "PL_HCL625_02\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=186&version=2.35.101.80", "releaseNotes": "1. Support GP switch.\r\n2. Improve network performance by fine tune router table." }, { "fileName": "Tibea_TW_Z3_IM002C_01056400-encrypted_202129091201_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185972, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Tibea_TW_Z3_IM002C_01056400-encrypted_202129091201_withoutMF.ota", "imageType": 44, "manufacturerCode": 4489, "sha512": "61c9b3727efccff6ba214bda917257e68164c4f276f979d2ac09057be5a9d37129236545022c4c93afc27c2e33a4909f21b8e92cf616795135913f56bcb22b97", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=44&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "Undercabinet_TW_Z3_IM0046_01056400-encrypted_202129091204_withoutMF.ota", "fileVersion": 17130496, "fileSize": 185980, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Undercabinet_TW_Z3_IM0046_01056400-encrypted_202129091204_withoutMF.ota", "imageType": 70, "manufacturerCode": 4489, "sha512": "b2d9f6a1618d68bc97e07a737f02f7b8be4d4f8fc4f08f9e8030c6312b75baa83394490fd37525676102e39e39c59f4003c06521a8b8bbbc727ebf41dcb9f10c", "otaHeaderString": "EBL RGBW\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=70&version=1.5.100.0", "releaseNotes": "Support ZLO" }, { "fileName": "VIVARES_PBC4_01_0X0098_0x10132503.ota", "fileVersion": 269690115, "fileSize": 158673, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/VIVARES_PBC4_01_0X0098_0x10132503.ota", "imageType": 152, "manufacturerCode": 4489, "sha512": "127394c14ca09515c29686e159cb139b1c8e3874339b9c96515a9a0423bb0f249281e3f5b250afab20ed257998b4766dd9665c8bcf1d1154520d242f8ca84cb9", "otaHeaderString": "Encrypted GBL Z3SwitchSoc_sdk650", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4489&product=152&version=16.19.37.3", "releaseNotes": "Fix router request to avoid route table full." }, { "fileName": "Zigbee3toDALI_100E655B.ota", "fileVersion": 269378907, "fileSize": 200928, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LEDVANCE/Zigbee3toDALI_100E655B.ota", "imageType": 57374, "manufacturerCode": 4364, "sha512": "af7d59bf9775eaaa80fbed6d2b19365e29fc3a1cd813dbbab19d6e1dae2293112c64d09ce10436b24c441e27ca6faf44016ae9e79fc81cc93e988e59ad639657", "otaHeaderString": "Zigbee3toDALIConverter\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://api.update.ledvance.com/v1/zigbee/firmwares/download?company=4364&product=57374&version=16.14.101.91", "releaseNotes": "1. Fix bug that endpoint changes.\r\n2. Supportdim down control from push button coupler." }, { "fileName": "1021-000e-004c4203-NLF.zigbee", "fileVersion": 4997635, "fileSize": 255207, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-000e-004c4203-NLF.zigbee", "imageType": 14, "manufacturerCode": 4129, "sha512": "9d76e88d757a086c8d7004daa3de8ff513f911c3d06e6afed374e49ba12e6339f208362a434021c247246a7574c683460c00fa0d90de12d8b54ed351a4990c29", "otaHeaderString": " " }, { "fileName": "1021-000f-00414203-NLL.zigbee", "fileVersion": 4276739, "fileSize": 254391, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-000f-00414203-NLL.zigbee", "imageType": 15, "manufacturerCode": 4129, "sha512": "2705e190fc0752d7778071c497edc76e193e2782b2f65e0da1f9aa2dace4eb35dc280b4c0afdeaef8345615354a27bf3b30d5958d9248025d72e9e35fae926a5", "otaHeaderString": " " }, { "fileName": "1021-0010-00434203-NLM.zigbee", "fileVersion": 4407811, "fileSize": 245527, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0010-00434203-NLM.zigbee", "imageType": 16, "manufacturerCode": 4129, "sha512": "6331d017f16c58e0894fb6adb87b1e5123dbd893349ccce00f0dcdb5d32d5589d07b6b9ac99be940af1a8a3c22dcecf707ce5d87d0983c3f863edafa39fc0360", "otaHeaderString": " " }, { "fileName": "1021-0011-00654203-NLP.zigbee", "fileVersion": 6636035, "fileSize": 250967, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0011-00654203-NLP.zigbee", "imageType": 17, "manufacturerCode": 4129, "sha512": "1a2712eb0c01325fca91d2d3e646d796f136cc757c02bec07f8c87e9c97b8b0f8028100a440b32f675466115a15b3fa35bb509ec8270c5ab5009add84321fbee", "otaHeaderString": " " }, { "fileName": "1021-0012-004e4203-NLT.zigbee", "fileVersion": 5128707, "fileSize": 205415, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0012-004e4203-NLT.zigbee", "imageType": 18, "manufacturerCode": 4129, "sha512": "f09cb4a036232b96c9f0adbf6de215820973f9211cd2bc1d5089ccc9c399dcb3bcd5a4ec452ef05f69df9eb956dcae8d1a4f6b75676f7226902f4870261ccbac", "otaHeaderString": " " }, { "fileName": "1021-0013-003d4203-NLV.zigbee", "fileVersion": 4014595, "fileSize": 254583, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0013-003d4203-NLV.zigbee", "imageType": 19, "manufacturerCode": 4129, "sha512": "50118724b35b04f5aa4a19cd9aa804505f91dd330e075c6373b9b626aa34d369bd99974e7c18394eba3ad63e22b754b8d70c1d82ef384885bfaf9aee9d52a211", "otaHeaderString": " " }, { "fileName": "1021-0015-00264203-NLC.zigbee", "fileVersion": 2507267, "fileSize": 251447, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0015-00264203-NLC.zigbee", "imageType": 21, "manufacturerCode": 4129, "sha512": "9c5f83c5213b71746b293730630e9f186a85ecce7a2876d88adc878ec9614a619630e8240980c88e6aa2555c7605454b3ead9ccb494940096e2fbfb5c07401db", "otaHeaderString": " " }, { "fileName": "1021-0016-002f4203-NLD.zigbee", "fileVersion": 3097091, "fileSize": 204503, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0016-002f4203-NLD.zigbee", "imageType": 22, "manufacturerCode": 4129, "sha512": "868f2a191d778c6562f0596d34f9f2de7b1afbb2e5b1a7484b07f6b6d2d231933c611a8e1f77c2e89cf6d2cd654be0f74e26c7f04f101bacc73071f85ab8a303", "otaHeaderString": " " }, { "fileName": "1021-0018-00204203-NLTS.zigbee", "fileVersion": 2114051, "fileSize": 199911, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0018-00204203-NLTS.zigbee", "imageType": 24, "manufacturerCode": 4129, "sha512": "a7e48921d70cf3007eee77dc2ccaeb3ead2c1f094f94ccc4467bc1278b9abe6404cb96cbc5958194e469cf78dc1eddee8538e72768c11546eb84ffbb7f7d3d54", "otaHeaderString": " " }, { "fileName": "1021-0019-00254203-NLFN.zigbee", "fileVersion": 2441731, "fileSize": 240407, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0019-00254203-NLFN.zigbee", "imageType": 25, "manufacturerCode": 4129, "sha512": "e88473d0afd0549a9168a8fbc0c0d6419827e31ae175a8e07712ed204bb820875c5d9f9110b357ba3240e51702a80cfd7a66f20dbe8a14a373c65afb803efe64", "otaHeaderString": " " }, { "fileName": "1021-001c-00214203-NLFE.zigbee", "fileVersion": 2179587, "fileSize": 240791, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-001c-00214203-NLFE.zigbee", "imageType": 28, "manufacturerCode": 4129, "sha512": "18baa1b5244ce4e72ffaff672bfaa58e19da461cefa13a0918c0fe6488e59d4d6e3ceb4d3573cfe1cbe22684b586cefedd03aba2bde94f9ad3737ea40214662b", "otaHeaderString": " " }, { "fileName": "1021-001d-002d4203-NLUI-8090D0-Boot-universal-Switch_2_16.zigbee", "fileVersion": 2966019, "fileSize": 248919, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-001d-002d4203-NLUI-8090D0-Boot-universal-Switch_2_16.zigbee", "imageType": 29, "manufacturerCode": 4129, "sha512": "896695b95a14ba76b3156c382b84fb099bcc98bb395ab9bec3bbbe9f17783b5057927084744753c57239ae41e14550f8330c4a786392a644372294f4e01f94cb", "otaHeaderString": " " }, { "fileName": "1021-001e-002d4203-NLUF-8190D7-Boot-universal-Dimmer_2_23.zigbee", "fileVersion": 2966019, "fileSize": 262215, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-001e-002d4203-NLUF-8190D7-Boot-universal-Dimmer_2_23.zigbee", "imageType": 30, "manufacturerCode": 4129, "sha512": "69086803abb9d13475d677fba160b8c2aa1466599c90f6bb3806eb6eab6feaea691510af237ea89ef2f0ab3ca4223a9980985f9cf514694cde7c4c669aebd5b6", "otaHeaderString": " " }, { "fileName": "1021-001f-002d4203-NLUP-8080C9-Boot-plug-Switch_2_09.zigbee", "fileVersion": 2966019, "fileSize": 247863, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-001f-002d4203-NLUP-8080C9-Boot-plug-Switch_2_09.zigbee", "imageType": 31, "manufacturerCode": 4129, "sha512": "c901ee9159b992658e1f78876cb42b2a98b366adb07729160441ddf0e99d05ab002f4ad8111a2353e87041f7401b28c23bcc220c92991b432c61d3333a8120a7", "otaHeaderString": " " }, { "fileName": "1021-0020-002d4203-NLUO-8190D7-Boot-universal-Dimmer_2_23.zigbee", "fileVersion": 2966019, "fileSize": 262215, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0020-002d4203-NLUO-8190D7-Boot-universal-Dimmer_2_23.zigbee", "imageType": 32, "manufacturerCode": 4129, "sha512": "798e1a024c5293360af60ac2cc202455ef073e9029e5a7b5dbe93a3f58c630c694ddabba8733119d43111ece1ce2548caafdd32992b48115391989d1cdf63c8d", "otaHeaderString": " " }, { "fileName": "1021-0024-001a4203-NLIS.zigbee", "fileVersion": 1720835, "fileSize": 252871, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0024-001a4203-NLIS.zigbee", "imageType": 36, "manufacturerCode": 4129, "sha512": "b03dc2238643ea24496d354161478bc7f32d45f679689f004f0d33835d8b29c2d6ad24ac14221550be805328aa936da554a068c564498d2ab45055f577881c15", "otaHeaderString": " " }, { "fileName": "1021-002a-00184203-NLW.zigbee", "fileVersion": 1589763, "fileSize": 198023, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-002a-00184203-NLW.zigbee", "imageType": 42, "manufacturerCode": 4129, "sha512": "9a616dbc836db6a154b95eb11e015629033b19617b48d868646fe2c73ded188756444d7a0eaf5bacca61dd276b2c586f953fe9c2d419063d9dbc2f4f9cdea752", "otaHeaderString": " " }, { "fileName": "1021-002e-001d4203-NLIV.zigbee", "fileVersion": 1917443, "fileSize": 252823, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-002e-001d4203-NLIV.zigbee", "imageType": 46, "manufacturerCode": 4129, "sha512": "3a1d1bfc96613d48964bb02ca39ceb60f9d288446d9e0c79d1a6e0fda11993011cee075943cd6b38f861666bdb75f16d6916a457c1a2e9597ae7ecb6e418c1cf", "otaHeaderString": " " }, { "fileName": "1021-002f-00104203-NLH.zigbee", "fileVersion": 1065475, "fileSize": 215735, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-002f-00104203-NLH.zigbee", "imageType": 47, "manufacturerCode": 4129, "sha512": "b742fab60dbe4c03e75d89bd6a33af39cbd4211957e93e1037311144234df5b1e806d5e182f804db3a8378197483001c45f3367db30f262c12a0714f661c60d6", "otaHeaderString": " " }, { "fileName": "1021-0033-000b4203-NLJ.zigbee", "fileVersion": 737795, "fileSize": 241671, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0033-000b4203-NLJ.zigbee", "imageType": 51, "manufacturerCode": 4129, "sha512": "0239b592bc9f25a5b0e7e056953055b6dcf4f259bb9729a0f70c03e11e12db9de21574d0ff676f17dff606df691ce63dc6d2f61fd32ceceb5fc4dd12a70c4172", "otaHeaderString": " " }, { "fileName": "1021-0034-00074203-NLY.zigbee", "fileVersion": 475651, "fileSize": 219751, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Legrand/1021-0034-00074203-NLY.zigbee", "imageType": 52, "manufacturerCode": 4129, "sha512": "e8c70969fa566a0d5f509bb2cab20b876b61ec72f17a5455a9bc366f65b42a16d21e58b2ee31f7938df69b51eb80cb17cb70d52c8c2a7298cd3756816b63dd58", "otaHeaderString": " " }, { "fileName": "ZLinky_router_v16.ota", "fileVersion": 16, "fileSize": 245886, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LiXee/ZLinky_router_v16.ota", "imageType": 1, "manufacturerCode": 4151, "sha512": "562b9598ad436ff31fc4b981c1406cdb99822e472689229ca5f0c8687d17e28df32d07ec7238562a3a22f1495a97ae722e1012aad1735eeba97a6d8b6185379f", "otaHeaderString": "OM15081-RTR-JN5189-0000000000000", "originalUrl": "https://github.com/fairecasoimeme/Zlinky_TIC/releases/download/v16.0/ZLinky_router_v16.ota", "manufacturerName": [ "LiXee" ], "releaseNotes": "https://github.com/fairecasoimeme/Zlinky_TIC/releases/tag/v16.0" }, { "fileName": "ZLinky_router_v16_limited.ota", "fileVersion": 16, "fileSize": 245886, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/LiXee/ZLinky_router_v16_limited.ota", "imageType": 2, "manufacturerCode": 4151, "sha512": "8e3143c62442cb0ccab768b94e791f651a8c40c951e033473c52eb8e34e6d3dd22f4f3e57e42281e0ce72a73d60e956b256414462ce402c02a40a932eb3a0217", "otaHeaderString": "OM15081-RTR-LIMITED-JN5189-00000", "originalUrl": "https://github.com/fairecasoimeme/Zlinky_TIC/releases/download/v16.0/ZLinky_router_v16_limited.ota", "manufacturerName": [ "LiXee" ], "releaseNotes": "https://github.com/fairecasoimeme/Zlinky_TIC/releases/tag/v16.0" }, { "fileName": "20211122110505_OTA_lumi.curtain.hagl07_V48_20211119_2C5A.ota", "fileVersion": 48, "fileSize": 284750, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20211122110505_OTA_lumi.curtain.hagl07_V48_20211119_2C5A.ota", "imageType": 4104, "manufacturerCode": 4447, "sha512": "41788737db26cfe01890a1c5f400b5f3ffb01f4d8ac81606e55a7bfbc0c1d8a22ae8725ff9ff2d9a4bff890fce3271a6fbc808c2a43f3bddf07c7ea75d630432", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.curtain.hagl07" }, { "fileName": "20211124154453_OTA_lumi.switch.n1aeu1_0.0.0_1123_20211110.ota", "fileVersion": 2839, "fileSize": 294136, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20211124154453_OTA_lumi.switch.n1aeu1_0.0.0_1123_20211110.ota", "imageType": 5404, "manufacturerCode": 4447, "sha512": "140be0a40f40f84fcb1878ba623ad5830bfab2049f489ee7acf8ae22c1db1e4991fbf39832482a55fee2fc5b20681d9ab5592d457d1e5bf705b0c75af45eff1f", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.n1aeu1" }, { "fileName": "20211228121851_OTA_lumi.switch.b1nacn02_0.0.0_0065_20211223_EB5B32.ota", "fileVersion": 65, "fileSize": 187230, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20211228121851_OTA_lumi.switch.b1nacn02_0.0.0_0065_20211223_EB5B32.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "8945e228c2e8cff55d312e7592a01f93b6772334fbe46177c7966c1a896b0e9fa70ee62a70ac4a667ad4dbe9221f1d73b0048b754e3fdcfd1ed32b94fc1a69ab", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.switch.b1nacn02" }, { "fileName": "20211228180917_OTA_lumi.switch.b1naus01_0.0.0_0031_20211228_5689C8.ota", "fileVersion": 31, "fileSize": 270126, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20211228180917_OTA_lumi.switch.b1naus01_0.0.0_0031_20211228_5689C8.ota", "imageType": 276, "manufacturerCode": 4447, "sha512": "7c43a39a05825af48d125be2ca24118e286f2b52c5912ed07b9c18fac9fee9681800e1815b3c294a109fc9b220587e6aac62d94be27bd994a0be67a47141fd57", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b1naus01" }, { "fileName": "20220104175358_OTA_lumi.plug.maus01_0.0.0_0017_20211224_83991F.ota", "fileVersion": 17, "fileSize": 186494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220104175358_OTA_lumi.plug.maus01_0.0.0_0017_20211224_83991F.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "391074dec14591fea2f947701c75908c21802430c45a902109f698601136ac9f591373d180b25edc27c44a45f683ff373f101387047bfb100d700a9f1e847afa", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.plug.maus01" }, { "fileName": "20220106191002_OTA_lumi.switch.b2nacn02_0.0.0_0066_20211223_7588AC.ota", "fileVersion": 66, "fileSize": 189374, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220106191002_OTA_lumi.switch.b2nacn02_0.0.0_0066_20211223_7588AC.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "5e1f09ef670a25902760c951d88ea417a3ddb888f18330355d04c9a61776a4b8551275d7488f9beecc81767cfc2726c7bcb8c26f198c10f0247eeb1d89f9d752", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.switch.b2nacn02" }, { "fileName": "20220212141304_OTA_lumi.curtain.aq2_0.0.0_0033_20220124_2C69FB.ota", "fileVersion": 33, "fileSize": 189758, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220212141304_OTA_lumi.curtain.aq2_0.0.0_0033_20220124_2C69FB.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "07592976767a2cb8dd0faa643a15c4e022e0af06ac2dc6f0ba86bd5a3b6126dfefa794c4bfd899ad9883445bcc3b0a7df649341324469437cd74b64a49a6f168", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.curtain.aq2" }, { "fileName": "20220222162717_OTA_lumi.light.rgbac1_0.0.0_0028_20220222_BFF63B.ota", "fileVersion": 28, "fileSize": 294846, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220222162717_OTA_lumi.light.rgbac1_0.0.0_0028_20220222_BFF63B.ota", "imageType": 2057, "manufacturerCode": 4447, "sha512": "72c0dcb313361f780eeb093dedbcb6f4089b43cdd0e37f87ae8e3782874f9fada121b376a1715c44002f962c86c70d2da923b8b11e96f1cf08693a0da5939acb", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.light.rgbac1" }, { "fileName": "20220222202427_OTA_lumi.airmonitor.acn01_0.0.0_0029_20220222_17EC2C.ota", "fileVersion": 29, "fileSize": 244350, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220222202427_OTA_lumi.airmonitor.acn01_0.0.0_0029_20220222_17EC2C.ota", "imageType": 9480, "manufacturerCode": 4447, "sha512": "6810b06a486f7689c87186e593dfdd6f5e0900ac9da154d515c6dd8881c6688ffbaac82e370940ebf133098f26c9a2df1aaf81b6fe0bc2815ee6ac2958b424d5", "otaHeaderString": "OM15082-TEMP-JN5180--ENCRYPTED01", "modelId": "lumi.airmonitor.acn01" }, { "fileName": "20220316103100_OTA_lumi.curtain.v1_0.0.0_0036_20220125_BEEC32.ota", "fileVersion": 36, "fileSize": 191070, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220316103100_OTA_lumi.curtain.v1_0.0.0_0036_20220125_BEEC32.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "ea99469ae0b63556455784b95de50e5143f593186d9a3fc4ad577bc5d75f8e465bcff88e9996f2ff690db687a7984a1a33a95ce7fdf7d79bf0a3cc23651378e8", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.curtain" }, { "fileName": "20220402154955_OTA_lumi.light.aqcn02_0.0.0_34_20220331_2215F2.ota", "fileVersion": 34, "fileSize": 200446, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220402154955_OTA_lumi.light.aqcn02_0.0.0_34_20220331_2215F2.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "1c7f550fd7318778b9bb53d8296f108e0fade70a34b0735964bdfa22efd87c987c39a7a89e48cf9d360a6eb0b8def6fe3c2ea5d36f11de9ffc7c648a05f5d312", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.light.aqcn02" }, { "fileName": "20220506181329_lumi.curtain.agl001_Multi_JN5189_FMSH_0.0.0_2424_20220422_1aa302.ota", "fileVersion": 6168, "fileSize": 288213, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220506181329_lumi.curtain.agl001_Multi_JN5189_FMSH_0.0.0_2424_20220422_1aa302.ota", "imageType": 4105, "manufacturerCode": 4447, "sha512": "0d1bae25759420898d381b98e97858d2e47f48cd3495abd8754661d0e1957eb5b570a2ae7435acc435d5a18665409e07148abae6f1f4a190c9af786d94ec725d", "otaHeaderString": "CURTAIN-OCC-JN5189---ENCRYPTED00", "modelId": "lumi.curtain.agl001" }, { "fileName": "20220524105221_OTA_lumi.motion.ac01_0.0.0_0054_20220509_EB279B.ota", "fileVersion": 54, "fileSize": 317658, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220524105221_OTA_lumi.motion.ac01_0.0.0_0054_20220509_EB279B.ota", "imageType": 8347, "manufacturerCode": 4447, "sha512": "32174d5bc4abbb7295e7619a1684b0a37a9622dfc4cebbc6a39174e18fa3b9bde9b562cbb34e1eb9c61169c0f1f879245d4e452890dfc0645cd22e6ad36f468d", "otaHeaderString": "\u0014OTA_lumi.motion.ac01\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.motion.ac01" }, { "fileName": "20220607175331_OTA_lumi.switch.acn029_0.0.0_211551_20220217_08B622.ota", "fileVersion": 1380147, "fileSize": 322672, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220607175331_OTA_lumi.switch.acn029_0.0.0_211551_20220217_08B622.ota", "imageType": 2572, "manufacturerCode": 4447, "sha512": "61b68fa353b90d1e4e390fb04d466dd70f1f4ec03145a9d45ea9efb79099c27f81d89e6ef907f163f06bf1ed167e0d69178e3843b5f0155583dbb74b0f57c3d6", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.acn029" }, { "fileName": "20220607175754_OTA_lumi.switch.acn030_0.0.0_211551_20220217_BBD96B.ota", "fileVersion": 1380147, "fileSize": 324960, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220607175754_OTA_lumi.switch.acn030_0.0.0_211551_20220217_BBD96B.ota", "imageType": 2700, "manufacturerCode": 4447, "sha512": "26af30dab1549a41d96e9c7a952bb8222f4ececea5197bb88839deb3c1ed2aa5397839af1e0593bf334601386f55b122b76345670a2e0611608586d3903d92a5", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.acn030" }, { "fileName": "20220607180259_OTA_lumi.switch.acn031_0.0.0_211551_20220217_C7F604.ota", "fileVersion": 1380147, "fileSize": 326064, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220607180259_OTA_lumi.switch.acn031_0.0.0_211551_20220217_C7F604.ota", "imageType": 2828, "manufacturerCode": 4447, "sha512": "b46f4d4870bbe38de40dcaad10a7d44cbe684f49608c5a849a5e00490fe77f9bf1b899a5b036ce9e39f5b212239d3b7689eac449181ec07dd0e631e6c2ff0dbf", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.acn031" }, { "fileName": "20220607183723_OTA_lumi.remote.cagl02_V1.0.25_20220602_44D1AF.ota", "fileVersion": 25, "fileSize": 266926, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220607183723_OTA_lumi.remote.cagl02_V1.0.25_20220602_44D1AF.ota", "imageType": 8840, "manufacturerCode": 4447, "sha512": "c92357df7603f9e33f7c818eabb42ba301886fed9a0f2c70df0d909932c024ea772929f62dce10a71deef723ecb69f67346f487260d9bcea76eb96b436569c9e", "otaHeaderString": "OM15082-CUBE-JN5180-ENCRYPTED000", "modelId": "lumi.remote.cagl02" }, { "fileName": "20220701175804_OTA_lumi.switch.b2lc04_0.0.0_0023_20220701_E4CE08.ota", "fileVersion": 23, "fileSize": 291662, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220701175804_OTA_lumi.switch.b2lc04_0.0.0_0023_20220701_E4CE08.ota", "imageType": 6664, "manufacturerCode": 4447, "sha512": "1823d8c227a419d5f128169c9e61baa18822e828e02e2ef368927546850dfacb154d38f523e27e80ab6e7cd6c1783fba8abc207c8e80371759c14fbfc9bf152f", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b2lc04" }, { "fileName": "20220701180138_OTA_lumi.switch.b1lc04_0.0.0_0023_20220701_1ED980.ota", "fileVersion": 23, "fileSize": 289838, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220701180138_OTA_lumi.switch.b1lc04_0.0.0_0023_20220701_1ED980.ota", "imageType": 6536, "manufacturerCode": 4447, "sha512": "c40e00c75ab07fdb8956e1f169b1652bb1ea2141e68eee56ae728f164391956ac7c069c0aac086c215db3f74520e64afb9d9019809321b98ccc0118018869a72", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b1lc04" }, { "fileName": "20220718113235_OTA_lumi.sensor_smoke.acn03_0.0.0_0017_20220617_CB9276.ota", "fileVersion": 17, "fileSize": 222094, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220718113235_OTA_lumi.sensor_smoke.acn03_0.0.0_0017_20220617_CB9276.ota", "imageType": 9728, "manufacturerCode": 4447, "sha512": "d5a8e5c323acd99ceaf02ab061e7145dbcf2da028c9d73a6042d02536f0b8aaefbc798b9b47e8fb03345721c0231374c8654e3979e73b10ba51997ad9d41cf0a", "otaHeaderString": "OM15082-WIN-JN5180--ENCRYPTED000", "modelId": "lumi.sensor_smoke.acn03" }, { "fileName": "20220718144406_OTA_lumi.switch.n3acn3_0.0.0_0033_20211108_E03E80.ota", "fileVersion": 33, "fileSize": 291022, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220718144406_OTA_lumi.switch.n3acn3_0.0.0_0033_20211108_E03E80.ota", "imageType": 1164, "manufacturerCode": 4447, "sha512": "a01487436dd5bc830b74851fc7360c541543e684b11e263e888551439b7a309beb9d61b808866fc98356ea45c1b5ca61bfb8af5e7d47784c9c505667154d2362", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.n3acn3" }, { "fileName": "20220728184843_OTA_lumi.ctrl_86plug.aq1_0.0.0_0094_20220722_42FE08.ota", "fileVersion": 94, "fileSize": 189934, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220728184843_OTA_lumi.ctrl_86plug.aq1_0.0.0_0094_20220722_42FE08.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "2a481a36e2aa077c110d7efc30f5f3709f799469eba7dde9defa57c80d327b645c74bc332ff22e004d64ec67fa88abd8f1f1572efbfd48c582292cfaf2cf07a0", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_86plug.aq1" }, { "fileName": "20220920171525_OTA_lumi.airm.fhac01_0.0.0_0026_20220919_BEDF49.ota", "fileVersion": 26, "fileSize": 274750, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20220920171525_OTA_lumi.airm.fhac01_0.0.0_0026_20220919_BEDF49.ota", "imageType": 7432, "manufacturerCode": 4447, "sha512": "89a392b29fab601cb7d17980806dbf131ebfdbd807fa12031fb92caa71f48f204b989f4477621cd309fcdb23c961ff1adefaff698d4bf0e97383c480fa1a7eba", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.airm.fhac01" }, { "fileName": "20221009111923_OTA_lumi.curtain.acn002_0.0.0_1530_20221009_6C9C3D.ota", "fileVersion": 3870, "fileSize": 296304, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20221009111923_OTA_lumi.curtain.acn002_0.0.0_1530_20221009_6C9C3D.ota", "imageType": 14976, "manufacturerCode": 4447, "sha512": "2436508af75c9b0139d304f8b62ce5579ad721fffec74967db7256bb030f47f8172a2b2ef2003eede1c868410b007ac6c89b3936dd44dffd07fdff155bcb7451", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.curtain.acn002" }, { "fileName": "20221123140517_OTA_lumi.remote.b1acn02_0.0.0_0031_20221010_8D956D.ota", "fileVersion": 31, "fileSize": 211150, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20221123140517_OTA_lumi.remote.b1acn02_0.0.0_0031_20221010_8D956D.ota", "imageType": 8584, "manufacturerCode": 4447, "sha512": "2012c8d42ad631e5bb8e95a8a6fe2eab308c2379d52880636b956cdf4a01aa9d68101ca5b76c5dcdb2f485edfb67361fe641b54ac838f0b5a7b4cf8f6c71c486", "otaHeaderString": "OM15082-SWITCH-JN5180--ENCRYPTED", "modelId": "lumi.remote.b1acn02" }, { "fileName": "20221213122302_OTA_aqara.feeder.acn001_0.0.0_3833_20220914_03DFD1.ota", "fileVersion": 9761, "fileSize": 324224, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20221213122302_OTA_aqara.feeder.acn001_0.0.0_3833_20220914_03DFD1.ota", "imageType": 4873, "manufacturerCode": 4447, "sha512": "20955a8996973680d9b1f110c9d5583c072bcb99032a1070fc0c6663323539928d82d46f34a21109e68882272a459d205eecc0ae3b3bae66a8866dc0c2094b2f", "otaHeaderString": "empower mcu\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "aqara.feeder.acn001" }, { "fileName": "20230110152017_OTA_lumi.sensor_ht.agl02_0.0.0_0029_20230109_88D8F4.ota", "fileVersion": 29, "fileSize": 227710, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230110152017_OTA_lumi.sensor_ht.agl02_0.0.0_0029_20230109_88D8F4.ota", "imageType": 8456, "manufacturerCode": 4447, "sha512": "a786eccd33e389777f731f08590dc1d69c6192a03d0ac52cb1901bdf03707a30ac8bdd083beeed75c79923282542ec460d54b0b59a15be371fa2ace1c815cf6b", "otaHeaderString": "OM15082-TEMP-JN5180--ENCRYPTED02", "modelId": "lumi.sensor_ht.agl02" }, { "fileName": "20230110152619_OTA_lumi.magnet.agl02_0.0.0_0030_20230109_D7EBD0.ota", "fileVersion": 30, "fileSize": 214798, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230110152619_OTA_lumi.magnet.agl02_0.0.0_0030_20230109_D7EBD0.ota", "imageType": 8200, "manufacturerCode": 4447, "sha512": "b8ebb6f7d216870587e7ff91f26116452d9fb2a47b5b1766992f02ec31bd43e59329920dd60e1754cb388ec7a9e3b41c2f979a749fb73dcc6cd16215e230d060", "otaHeaderString": "OM15082-WIN-JN5180--ENCRYPTED000", "modelId": "lumi.magnet.agl02" }, { "fileName": "20230112205436_OTA_lumi.motion.agl04_0.0.0_0027_20230109_4D9D5F.ota", "fileVersion": 27, "fileSize": 217470, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230112205436_OTA_lumi.motion.agl04_0.0.0_0027_20230109_4D9D5F.ota", "imageType": 9352, "manufacturerCode": 4447, "sha512": "c7d5486e4cf6bd592b2e66e5a0d005b1813b182da2f88c871fc20b797db7cc3633a426d80ee968d225e1918831914864f54eb39a117abf8aa82e21f40ece2bed", "otaHeaderString": "OM15082-OCC-JN5180--ENCRYPTED0V2", "modelId": "lumi.motion.agl04" }, { "fileName": "20230130180718_OTA_lumi.motion.ac02_0.0.0_0010_20230104_390E3D.ota", "fileVersion": 10, "fileSize": 196238, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230130180718_OTA_lumi.motion.ac02_0.0.0_0010_20230104_390E3D.ota", "imageType": 11528, "manufacturerCode": 4447, "sha512": "22fb7b610d169f433ef0bad5a6c6e5f0abfb5b09a1ed2155bc5da23b6cfd204659441136947270b98514581e97233cd2db33002ec277796e1e2ae1572abf2853", "otaHeaderString": "BAQMSP1-OCC-JN5189---ENCRYPTED00", "modelId": "lumi.motion.ac02" }, { "fileName": "20230202185209_OTA_lumi.ctrl_ln2.aq1_0.0.0_0095_20220725_0B0798.ota", "fileVersion": 95, "fileSize": 190334, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230202185209_OTA_lumi.ctrl_ln2.aq1_0.0.0_0095_20220725_0B0798.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "e2a0d6d247b177a34471d2098e5069c0752cec764871e9b9e1f368427af2a66d54f0d57fa9c2ef135f4ce4f76f86dc38f7c9992040c09ab9bbf291a47c357ff2", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_ln2.aq1" }, { "fileName": "20230202185525_OTA_lumi.ctrl_ln1.aq1_0.0.0_0095_20220804_59D5CD.ota", "fileVersion": 95, "fileSize": 188030, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230202185525_OTA_lumi.ctrl_ln1.aq1_0.0.0_0095_20220804_59D5CD.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "79ab01c83ded9ad1116f8314e1464ebfa0a87947bbabb26904fca72a05e07864cb153330badcc8c9fb0dde834fa31f5707cd2e887f79021dbee98a90e26b1abd", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_ln1.aq1" }, { "fileName": "20230209143954_OTA_lumi.motion.agl02_0.0.0_0037_20230209_78D49C.ota", "fileVersion": 37, "fileSize": 215918, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230209143954_OTA_lumi.motion.agl02_0.0.0_0037_20230209_78D49C.ota", "imageType": 8328, "manufacturerCode": 4447, "sha512": "082c60b0481de354ecb1580fc78618990dc32bd19f512694e4ba3fbd90a1fbcc0b49fae17cd3bfa476b10c4a2e304d9faaecdf38e8f55574afca8388c2e13fb7", "otaHeaderString": "OM15082-OCC-JN5180--ENCRYPTED0V2", "modelId": "lumi.motion.agl02" }, { "fileName": "20230221165005_OTA_lumi.airrtc.agl001_0.0.0_1030_20230220_712488.ota", "fileVersion": 2590, "fileSize": 264144, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230221165005_OTA_lumi.airrtc.agl001_0.0.0_1030_20230220_712488.ota", "imageType": 5017, "manufacturerCode": 4447, "sha512": "8e360ede89d13ee7f046cebca8b0d6d46806617f91907c44270dd478f86cadc6e521f774113a3c4d434b1ff27e375f5a586e78cdca421363f84feaa6e36346fa", "otaHeaderString": "BAQMSP1-OCC-JN5189---ENCRYPTED00", "modelId": "lumi.airrtc.agl001" }, { "fileName": "20230425180824_OTA_lumi.sensor_gas.acn02_0.0.0_0017_20230423_3F1EAA.ota", "fileVersion": 17, "fileSize": 274110, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230425180824_OTA_lumi.sensor_gas.acn02_0.0.0_0017_20230423_3F1EAA.ota", "imageType": 6156, "manufacturerCode": 4447, "sha512": "552e0562121fed5cfce3d0a64f66750ed68f597b3fdd3ff4e53277cb742540accb23b0a52ee4a4d64c83590370609aab90ac34c711b3027f6ea6785f04610108", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.sensor_gas.acn02" }, { "fileName": "20230610160234_lumi.light.acn132_mcu_0.0.0_2627_20230606_DA1C86.ota", "fileVersion": 672539, "fileSize": 942962, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230610160234_lumi.light.acn132_mcu_0.0.0_2627_20230606_DA1C86.ota", "imageType": 141, "manufacturerCode": 4447, "sha512": "25007f79ccb030715cd9fa1a0acd447554aa1b8c0627116b0ae06e5135e6c5922e64eace80452e9a05423487d499df1fa0a23a30aead31fac2f15c51f9d82895", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn132" }, { "fileName": "20230704151031_OTA_lumi.curtain.vagl02_V41_20230703_563B.ota", "fileVersion": 41, "fileSize": 285742, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230704151031_OTA_lumi.curtain.vagl02_V41_20230703_563B.ota", "imageType": 4616, "manufacturerCode": 4447, "sha512": "5c154c263bddba0ab0f5ffc0fb80d68f1c56c833b9018ad2a1942f30cec1efee977d6e2f431f8e06d617b241b1311ce4dc8d04f68757e889314e18075e485e5e", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.curtain.vagl02" }, { "fileName": "20230905121119_OTA_lumi.light.acn003_0.0.0_0029_20230712_1D4A6E.ota", "fileVersion": 29, "fileSize": 315524, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20230905121119_OTA_lumi.light.acn003_0.0.0_0029_20230712_1D4A6E.ota", "imageType": 131, "manufacturerCode": 4447, "sha512": "25a205565f0ba9400cf57e7f633e9fb16fe473d23ac79897269d07ead4a2507a1585abcbb55994be551fbf130c28d4bf34a705d9d7d3083a21e2441cfcbd4a20", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn003" }, { "fileName": "20231222182930_lumi.light.acn128_0.0.0_0022_20231219_46D8F0.ota", "fileVersion": 22, "fileSize": 313700, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231222182930_lumi.light.acn128_0.0.0_0022_20231219_46D8F0.ota", "imageType": 139, "manufacturerCode": 4447, "sha512": "c0e7a4ea751f2cfb715aee5c3cd4d5cf458f133a76c88e16080a0442652ebb6ae555bcd1336b609a2a9eecc53869d21182fe4b093cc349ef326bf37b6809cf11", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn128" }, { "fileName": "20231222183430_lumi.light.acn014_0.0.0_0040_20231220_1E2291.ota", "fileVersion": 40, "fileSize": 482528, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231222183430_lumi.light.acn014_0.0.0_0040_20231220_1E2291.ota", "imageType": 129, "manufacturerCode": 4447, "sha512": "18b7eb6a76e610645b078103276462991897c02da3825ca92be54dd10260477932eb69300e988b7017e16b74e5c75c707dee56cd9996227de1f252c5fa1aba40", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn014" }, { "fileName": "20231222194652_lumi.dimmer.acn004_0.0.0_0024_9FD15B.ota", "fileVersion": 24, "fileSize": 440714, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231222194652_lumi.dimmer.acn004_0.0.0_0024_9FD15B.ota", "imageType": 6028, "manufacturerCode": 4447, "sha512": "56ecdbb4940e48ab4abeb2ca7a168f23e4c9c116818f842a36b3a28c935a3b8f6e99c42cdef4b2db1dcb9ad2a244548591a1d112d9c37bd4bc568b5c20a3fa07", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.dimmer.acn004" }, { "fileName": "20231222195203_lumi.light.acn024_0.0.0_0041_20231220_20F906.ota", "fileVersion": 41, "fileSize": 317900, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231222195203_lumi.light.acn024_0.0.0_0041_20231220_20F906.ota", "imageType": 138, "manufacturerCode": 4447, "sha512": "dea351b8a1208cb6703530a78966cc940a5d67c2a8990df18a3fcb7d55304a82d713a656efd27705a8ff51b43028ddef4b765dc3692be2f9cdaf47795ab15b11", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn024" }, { "fileName": "20231222195338_lumi.light.acn026_0.0.0_0041_20231220_20F906.ota", "fileVersion": 41, "fileSize": 317900, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231222195338_lumi.light.acn026_0.0.0_0041_20231220_20F906.ota", "imageType": 138, "manufacturerCode": 4447, "sha512": "dea351b8a1208cb6703530a78966cc940a5d67c2a8990df18a3fcb7d55304a82d713a656efd27705a8ff51b43028ddef4b765dc3692be2f9cdaf47795ab15b11", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn026" }, { "fileName": "20231227113728_lumi.light.acn031_0.0.0_0026_20231226_064840.ota", "fileVersion": 26, "fileSize": 462258, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231227113728_lumi.light.acn031_0.0.0_0026_20231226_064840.ota", "imageType": 143, "manufacturerCode": 4447, "sha512": "e221d3bb6d2a28011463b259ab94e1de40953b2bafd66730b7349e00b88591afe3972f02fea6f8a839d86d92015964976ad61da797de312688696bbeecc718a2", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn031" }, { "fileName": "20231227113844_lumi.light.acn032_0.0.0_0026_20231226_064840.ota", "fileVersion": 26, "fileSize": 462258, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20231227113844_lumi.light.acn032_0.0.0_0026_20231226_064840.ota", "imageType": 143, "manufacturerCode": 4447, "sha512": "e221d3bb6d2a28011463b259ab94e1de40953b2bafd66730b7349e00b88591afe3972f02fea6f8a839d86d92015964976ad61da797de312688696bbeecc718a2", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.light.acn032" }, { "fileName": "20240103161114_OTA_lumi.switch.n4acn4_V62_20240103_776DCB.ota", "fileVersion": 62, "fileSize": 377918, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240103161114_OTA_lumi.switch.n4acn4_V62_20240103_776DCB.ota", "imageType": 3848, "manufacturerCode": 4447, "sha512": "b5927a2c270ba68d1d9db4bd61b7354d48cf9063aa2bbbf5d0866a91731de5c11478830e283d0fe01bdd32ae3941db725b88e40ad2ef8439d901ea41cf747ec6", "otaHeaderString": "ROUTERX-----JN5189--ENCRYPTED000", "modelId": "lumi.switch.n4acn4" }, { "fileName": "20240112203928_lumi.switch.n0agl1_0.0.0.0030_20240112_C892CC.ota", "fileVersion": 30, "fileSize": 288146, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240112203928_lumi.switch.n0agl1_0.0.0.0030_20240112_C892CC.ota", "imageType": 6169, "manufacturerCode": 4447, "sha512": "796c6dc424faf0be42aa98482ab178ce3e429a80b47f0ba90ec91d479c6c3205865b666ca57de4814e4e806d1e4bbe8d594956fea30443ba25a67093e46a52be", "otaHeaderString": "lumi.switch.n0agl1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.n0agl1" }, { "fileName": "20240119171828_OTA_lumi.dimmer.rcbac1_0.0.0_0034_20240119_C8C27C.ota", "fileVersion": 34, "fileSize": 326270, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240119171828_OTA_lumi.dimmer.rcbac1_0.0.0_0034_20240119_C8C27C.ota", "imageType": 6024, "manufacturerCode": 4447, "sha512": "ec8b02bda1c6c6ff53dcdb3d8e4cf7754aef2000335635c236cf88772dff11975a13d19e40c19206bc4d0ff8153c41bd16538b2929c6b8b748c9640b1b0dd239", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.dimmer.rcbac1" }, { "fileName": "20240204163339_OTA_lumi.switch.acn059_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163339_OTA_lumi.switch.acn059_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn059" }, { "fileName": "20240204163449_OTA_lumi.switch.acn058_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163449_OTA_lumi.switch.acn058_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn058" }, { "fileName": "20240204163527_OTA_lumi.switch.acn057_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163527_OTA_lumi.switch.acn057_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn057" }, { "fileName": "20240204163609_OTA_lumi.switch.acn056_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163609_OTA_lumi.switch.acn056_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn056" }, { "fileName": "20240204163658_OTA_lumi.switch.acn055_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163658_OTA_lumi.switch.acn055_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn055" }, { "fileName": "20240204163742_OTA_lumi.switch.acn054_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163742_OTA_lumi.switch.acn054_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn054" }, { "fileName": "20240204163834_OTA_lumi.switch.acn049_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163834_OTA_lumi.switch.acn049_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn049" }, { "fileName": "20240204163918_OTA_lumi.switch.acn048_0.0.0_0034_20240204_80D522.ota", "fileVersion": 34, "fileSize": 579494, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240204163918_OTA_lumi.switch.acn048_0.0.0_0034_20240204_80D522.ota", "imageType": 6424, "manufacturerCode": 4447, "sha512": "8bed0ef08684be7c182c00bb6226d301e94f73052c997b1adef7ed4379d953c54e870175eca3596e1a5fb3db91ebf8bacd6fa9d9ba65eb3c00556ff2406c1622", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.acn048" }, { "fileName": "20240312112538_OTA_lumi.switch.n1acn1_0.0.0_58_20240311_02B313.ota", "fileVersion": 58, "fileSize": 288030, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240312112538_OTA_lumi.switch.n1acn1_0.0.0_58_20240311_02B313.ota", "imageType": 2572, "manufacturerCode": 4447, "sha512": "f5d7b9af469e2f627dd341ba70b677400e3e25b43118b8d32f8970f8674cbb8948c195b692a8f425d78fd178e931ca2d0572ca45cd4419a4e6367b47208a9410", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.n1acn1" }, { "fileName": "20240411111850_OTA_lumi.light.cbacn1_0.0.0_0043_20240407_AE8330.ota", "fileVersion": 43, "fileSize": 288846, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240411111850_OTA_lumi.light.cbacn1_0.0.0_0043_20240407_AE8330.ota", "imageType": 2440, "manufacturerCode": 4447, "sha512": "1329724912da4ec269e06349fd11f5e32a9133fa7d8e3ce0b755887da314022de320f19ba1320b1a4eb700b6233eb1bf384c20d9bf80e88aa5f941353665c296", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.light.cbacn1" }, { "fileName": "20240415175402_OTA_lumi.light.acn004_0.0.0_0031_20240319_BB204F.ota", "fileVersion": 31, "fileSize": 288510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240415175402_OTA_lumi.light.acn004_0.0.0_0031_20240319_BB204F.ota", "imageType": 4361, "manufacturerCode": 4447, "sha512": "a342cb05509ab10892a695de2947c66e92109c90cdd9a36c38fbce282971723dbe5502095353e45aace651bf4ff3dbe39c179d3fd32cf148a5f194254e9efd72", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.light.acn004" }, { "fileName": "20240620152417_lumi.curtain.acn003_Multi_JN5189_FMSH_0.0.0_3331_20240613_792592.ota", "fileVersion": 8479, "fileSize": 296034, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240620152417_lumi.curtain.acn003_Multi_JN5189_FMSH_0.0.0_3331_20240613_792592.ota", "imageType": 4105, "manufacturerCode": 4447, "sha512": "6491f7755f15ff586607f10530051b70fa6cdde4ce7285dc05aa06a59a2e206dd1c8d62cc2c838fc98dab5266e6f526c1d6d87e0329d06495831c4a3130c6e98", "otaHeaderString": "CURTAIN-OCC-JN5189---ENCRYPTED00", "originalUrl": "https://cdn.aqara.com/cdn/opencloud-product/mainland/product-firmware/prd/lumi.curtain.acn003/20240620152417_lumi.curtain.acn003_Multi_JN5189_FMSH_0.0.0_3331_20240613_792592.ota", "modelId": "lumi.curtain.acn003", "releaseNotes": "Fix known bugs" }, { "fileName": "20240719174346_OTA_lumi.plug.macn01_0.0.0_0036_20240719_40C5BB.ota", "fileVersion": 36, "fileSize": 292672, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240719174346_OTA_lumi.plug.macn01_0.0.0_0036_20240719_40C5BB.ota", "imageType": 12, "manufacturerCode": 4447, "sha512": "64136bca741924f865e105685efd1eb2d9c8bcf9b7a53ac3ade32f8927c3e49412a7f4afc0212ab8faa0018abcda2881c5251643faa168e3a04782f94c74e7e1", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.plug.macn01" }, { "fileName": "20240719174937_OTA_lumi.plug.maeu01_0.0.0_0045_20240719_958E71.ota", "fileVersion": 45, "fileSize": 280672, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240719174937_OTA_lumi.plug.maeu01_0.0.0_0045_20240719_958E71.ota", "imageType": 24, "manufacturerCode": 4447, "sha512": "a1bd7164edbb76eeb44ee05f6a5f85cf39d33b2262d8cc7786e1cc1e009baf1e8450a3dcbf7f1e61f85039146aa0c57f7f2a2175a3e3dd6cfa8f170647b2dd09", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.plug.maeu01" }, { "fileName": "20240722115628_OTA_lumi.plug.sacn03_0.0.0_0038_20240719_39CCF1.ota", "fileVersion": 38, "fileSize": 281550, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240722115628_OTA_lumi.plug.sacn03_0.0.0_0038_20240719_39CCF1.ota", "imageType": 5128, "manufacturerCode": 4447, "sha512": "ef8fea3c72398b645242f2da9d5ff015da5296ab714e49ba86aa7aab42897c427c735cb173555b861e59d06d5ed00fc500c3603904dea140f968ef86862e7ec7", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.plug.sacn03" }, { "fileName": "20240722115926_OTA_lumi.plug.sacn02_0.0.0_0052_20240719_986BEB.ota", "fileVersion": 52, "fileSize": 280352, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240722115926_OTA_lumi.plug.sacn02_0.0.0_0052_20240719_986BEB.ota", "imageType": 140, "manufacturerCode": 4447, "sha512": "7400a508ca6bcd198488d9be60104b88569348c57abc339e7e815c1fa9cff7cb003dfba8062269b172f3ecd4f82dfd00836f3598d5d48cef145359ba12dcd9a8", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.plug.sacn02" }, { "fileName": "20240724195823_OTA_lumi.switch.b3n01_0.0.0_0028_20240723_6A0037.ota", "fileVersion": 28, "fileSize": 281808, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240724195823_OTA_lumi.switch.b3n01_0.0.0_0028_20240723_6A0037.ota", "imageType": 4108, "manufacturerCode": 4447, "sha512": "2f6ecd75ba984271093a3d3c6002a85776bf6c0b035c19af60530b7303c09ca4a30c00bd5d81aa69d42913c6850ac7797be2b902b966373dc381b067a6732398", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b3n01" }, { "fileName": "20240726113823_OTA_lumi.switch.n2acn1_0.0.0_59_20240724_F96EFF.ota", "fileVersion": 59, "fileSize": 292144, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240726113823_OTA_lumi.switch.n2acn1_0.0.0_59_20240724_F96EFF.ota", "imageType": 2700, "manufacturerCode": 4447, "sha512": "11c332a852aacc96ecc9a440d914b51025c3fce3bab61014624744436b171852496334c967eeb574fb5ffcacae013bcf4856338a5cfb6b33fabf154395d28c02", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "originalUrl": "https://cdn.aqara.com/cdn/opencloud-product/mainland/product-firmware/prd/lumi.switch.n2acn1/20240726113823_OTA_lumi.switch.n2acn1_0.0.0_59_20240724_F96EFF.ota", "modelId": "lumi.switch.n2acn1", "releaseNotes": "Fix known issues" }, { "fileName": "20240726114727_OTA_lumi.switch.n3acn1_0.0.0_59_20240724_1B883A.ota", "fileVersion": 59, "fileSize": 293248, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240726114727_OTA_lumi.switch.n3acn1_0.0.0_59_20240724_1B883A.ota", "imageType": 2828, "manufacturerCode": 4447, "sha512": "bbed6dca39141ac0e55c62c6b5b6168dd105794b76d33630e4126d83c9f831783ce5eb556a9bc97d442cf6e623ab88d2028bfdc6fb423cdd04c92fc71e18e536", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "originalUrl": "https://cdn.aqara.com/cdn/opencloud-product/mainland/product-firmware/prd/lumi.switch.n3acn1/20240726114727_OTA_lumi.switch.n3acn1_0.0.0_59_20240724_1B883A.ota", "modelId": "lumi.switch.n3acn1", "releaseNotes": "Fix known issues" }, { "fileName": "20240826103128_20240723105743_OTA_lumi.switch.b2nacn01_0.0.0_0028_20240722_296A66.ota", "fileVersion": 28, "fileSize": 277598, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240826103128_20240723105743_OTA_lumi.switch.b2nacn01_0.0.0_0028_20240722_296A66.ota", "imageType": 396, "manufacturerCode": 4447, "sha512": "5605d6bb5b9c0f9a627e1b25f83abade2fd24284aa9a23435e0c39ba5d5d09ecc84dd18a55dc44013f37583f3673f46bf928e35559cca8fd7e702704986edf2f", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b2nacn01" }, { "fileName": "20240830115847_OTA_lumi.switch.acn047_0.0.0_0032_20240823_D8294E.ota", "fileVersion": 32, "fileSize": 597122, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240830115847_OTA_lumi.switch.acn047_0.0.0_0032_20240823_D8294E.ota", "imageType": 6416, "manufacturerCode": 4447, "sha512": "a135dcf96e1086d30e945cb3b40885fce60840b98ca6dee595dbed97e702e3583336d21948a19f37d20a942ac9daa84abc57da39465611ff86628d825ce6f5c0", "otaHeaderString": "Aqara OTA Image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://cdn.aqara.com/cdn/opencloud-product/mainland/product-firmware/prd/lumi.switch.acn047/20240830115847_OTA_lumi.switch.acn047_0.0.0_0032_20240823_D8294E.ota", "modelId": "lumi.switch.acn047", "releaseNotes": "1.Fix known bugs" }, { "fileName": "LM15_86SP_aq_V1.0.11_20170302_OTA_v11_withCRC.20170417201259.ota", "fileVersion": 11, "fileSize": 204434, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/LM15_86SP_aq_V1.0.11_20170302_OTA_v11_withCRC.20170417201259.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "87aa835321545ec7144d2fec17f51122f8a130f26ed4260301d07839000ff192649865f450302ced4aec9b19fcb309420b13c2a12a78d994d1a84676ffbbafab", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_86plug" }, { "fileName": "LM15_SP_aq_V1.3.30_20180724_v30_withCRC.20180724160524.ota", "fileVersion": 30, "fileSize": 191714, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/LM15_SP_aq_V1.3.30_20180724_v30_withCRC.20180724160524.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "8fd5a04c36991ed8b7e4de3a842e15162b6c8e07e3b0dce5e74ef6295cd653b2e69d44ab5e5e0274c6baf7b9633b4639319036b57e3a383606e70bf174229531", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.plug.aq1" }, { "fileName": "LM15_ln1_AQ_V1.0.32_20180625_v32.20181008194104.ota", "fileVersion": 32, "fileSize": 193374, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/LM15_ln1_AQ_V1.0.32_20180625_v32.20181008194104.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "c89e575b65906603c6fde9e77424a8ea91924d279de679bbadd1bfa496988b4cbec7b2faa9632ff09f78e044706dd2ee26ed68e624d4b980705fd49f5d5f580b", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_ln1" }, { "fileName": "LM15_ln2_V1.0.32_20180625_v32.20181008194246.ota", "fileVersion": 32, "fileSize": 195630, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/LM15_ln2_V1.0.32_20180625_v32.20181008194246.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "14f037e10da56a544f6534678cdcbd2c875a512d9dfb1169fec0c21a31326f7f4bc5041a22969ff8b4a9997cf8f2593fb52bd5eafc3b93098162e1a93d663758", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_ln2" }, { "fileName": "LM19_BatteryCurtain_V1.0.24_20200803_Enc_F3D9.20200903160047.ota", "fileVersion": 24, "fileSize": 240014, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/LM19_BatteryCurtain_V1.0.24_20200803_Enc_F3D9.20200903160047.ota", "imageType": 9224, "manufacturerCode": 4447, "sha512": "fc2fc192a1e41f551bb5f8a5c5293035b2d5d75be43adfd922ec5948c56409afe837672245bf0785d4913b9e44c008e197e5c7d8a8e1ea00645457386b2cbb3e", "otaHeaderString": "OM15082-CURTIN-JN5180--ENCRYPTED", "modelId": "lumi.curtain.hagl04" }, { "fileName": "OTA_LM15_LNN_V2.6.22_20180503_neutral1_19ms_DIO19Led.20181011142357.ota", "fileVersion": 22, "fileSize": 169746, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_LM15_LNN_V2.6.22_20180503_neutral1_19ms_DIO19Led.20181011142357.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "715205fd8efbac551b4483e1eb28c7767eb693aa602928c1d2400a94a3fe816f8330e76216c957874557f0399318159cbdd1cb686e89529bd213a4b79a19bb78", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_neutral1" }, { "fileName": "OTA_LM15_LNN_V2.6.22_20180503_neutral2_19ms_DIO19Led.20181011142447.ota", "fileVersion": 22, "fileSize": 171330, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_LM15_LNN_V2.6.22_20180503_neutral2_19ms_DIO19Led.20181011142447.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "bed8ed339344a5fc66a8e599949e83f9a1d99b36ede2e4b5a9bbb09a855fb695be89e280f1ab4522299c63d3fc7856e4458688fa596ef7eb0c8e548143aa8bdc", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_neutral2" }, { "fileName": "OTA_LMACN02_DoorLock_V4.1.09_20180317.20180411152155.ota", "fileVersion": 9, "fileSize": 175378, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_LMACN02_DoorLock_V4.1.09_20180317.20180411152155.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "6c32a2bc8a4146766650a7208a7cd43a57d2a912907173c8c5994e1d4ea8906699c2ddbd33f31c260aaf05b3d74cf3210159dbfa7d6f4e86c692aa21e479c80b", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.lock.acn02" }, { "fileName": "OTA_LMAQ_DoorLock_V2.2.19_20171108.20180129142422.ota", "fileVersion": 19, "fileSize": 166066, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_LMAQ_DoorLock_V2.2.19_20171108.20180129142422.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "199e6a1fbee2d754b640d82acfd5e00f2799567abd8dec04176c4535aef97d2fa91ddce40346b556284134b9ebc123411a9f970361a81522160c997f94372c24", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.lock.aq1" }, { "fileName": "OTA_LM_WM_DoorLock_V2.3.13_20180409.20180412161023.ota", "fileVersion": 13, "fileSize": 156530, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_LM_WM_DoorLock_V2.3.13_20180409.20180412161023.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "077d99acbf7f4e0bb540b586d62f2274556d55a9c654ee6a3397b3d60dbe4032e7bdc6d27b8c409be7d58a24d56609d9975d31ba7b9c8b852f6650e7e6c20115", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.lock" }, { "fileName": "OTA_WithCRC_LMES_RGBController_V1.2.30_20170801.20170920100827.ota", "fileVersion": 30, "fileSize": 197826, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_WithCRC_LMES_RGBController_V1.2.30_20170801.20170920100827.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "cd1f30464a1cef4ceb99aa4c32255cd11957e21d4461dd05b5d171f6967b5f5f4808c99bddd1b0041848ffd85a3a182fad1b4adead585c4960305b24cee5f712", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_rgb.es1" }, { "fileName": "OTA_lumi.airrtc.tcpco2ecn01_OTA_v12.20180828161433.ota", "fileVersion": 12, "fileSize": 193726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.airrtc.tcpco2ecn01_OTA_v12.20180828161433.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "e111297f40388931b58c2fb1caa29627d6d9f6230c6eab274fad645ce8de0f92856644f63e0b8f941eb288ecaad9ac65c757a84b118178e75b36a7acbfbb2c9a", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.airrtc.tcpco2ecn01" }, { "fileName": "OTA_lumi.airrtc.tcpecn02_OTA_v12.20180828161528.ota", "fileVersion": 12, "fileSize": 193726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.airrtc.tcpecn02_OTA_v12.20180828161528.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "4d09285e0e5e84c97bc9d33added3034aca02dae8dbbaefd1b61de4e2ab384c0d7884fbabbcee76e9512d43686304935d97c88a30387419e20fc5e50331966b6", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.airrtc.tcpecn02" }, { "fileName": "OTA_lumi.flood.agl02_V1.0.18_20190814.20191008104903.ota", "fileVersion": 18, "fileSize": 207358, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.flood.agl02_V1.0.18_20190814.20191008104903.ota", "imageType": 8712, "manufacturerCode": 4447, "sha512": "8d8615f9f2d4e24f99fb860de82618799767061bf57ceab0370ca51ae3cea00684f374e8826b62f04fcf9c8ef1ce7128d299ae3066b1f85658b6a5450e3aac48", "otaHeaderString": "OM15082-WATER-JN5180--ENCRYPTED0", "modelId": "lumi.flood.agl02" }, { "fileName": "OTA_lumi.light.cwopcn01_V25_20200328_86DF8E.20200702155802.ota", "fileVersion": 25, "fileSize": 285038, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.light.cwopcn01_V25_20200328_86DF8E.20200702155802.ota", "imageType": 1800, "manufacturerCode": 4447, "sha512": "bdd19e7caac673df5546f97fe7b68d5c815bf4d2d5083cac5b2cd0407befdc488149dd20bbe33bd88e62802d5aa76c08f5ddbae92bf1c4a44638bc29d468c190", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.light.cwopcn01" }, { "fileName": "OTA_lumi.light.cwopcn02_V25_20200328_6C8C9C.20200702155957.ota", "fileVersion": 25, "fileSize": 285038, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.light.cwopcn02_V25_20200328_6C8C9C.20200702155957.ota", "imageType": 1928, "manufacturerCode": 4447, "sha512": "bffc6ff8f2017693e3608f9e8bc6a5447eee176f44821a8bdf20ede738ada69252b88472d67a3d2c17a7eefe9f40ef44205b25cf318a9d2ea73395ed0b2ebe49", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.light.cwopcn02" }, { "fileName": "OTA_lumi.light.cwopcn03_V25_20200328_0022DA.20200702160124.ota", "fileVersion": 25, "fileSize": 285038, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.light.cwopcn03_V25_20200328_0022DA.20200702160124.ota", "imageType": 2056, "manufacturerCode": 4447, "sha512": "1bbcc31a494f0c1ee2442cd4ace4843c16292ea626bab0a4038505eb7c6d1aefb864cfcda5d89d85bc8f8995bc3ababee6c7feb51ad0ba45fe4be08853eb6ee0", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.light.cwopcn03" }, { "fileName": "OTA_lumi.plug.mmeu01_V22_20190906_D32362.20191008105750.ota", "fileVersion": 22, "fileSize": 276030, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.plug.mmeu01_V22_20190906_D32362.20191008105750.ota", "imageType": 16408, "manufacturerCode": 4447, "sha512": "a7fec7851a60696fb4f482f8fbbcfd638631bc460dca328427351baaf3a0c65a85191d450dab96700dd46c2ca9228efcd9a30c909d598718fe60210ee551d4f8", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.plug.mmeu01" }, { "fileName": "OTA_lumi.relay.c4acn01_V2.1.20_201900821_8FFEC9.20190906162416.ota", "fileVersion": 20, "fileSize": 184398, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.relay.c4acn01_V2.1.20_201900821_8FFEC9.20190906162416.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "d4abf6afd408978d7ae937d399c1bd1de2ca6eb58b84aa5bd498958496952c66818344cc5757b2c5b60df4fd5aa505202147d05d51afb448340141affa0d21da", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.relay.c4acn01" }, { "fileName": "OTA_lumi.remote.b286acn03_V1.0.21_20191127.20200310172748.ota", "fileVersion": 21, "fileSize": 209006, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.remote.b286acn03_V1.0.21_20191127.20200310172748.ota", "imageType": 8584, "manufacturerCode": 4447, "sha512": "61ffabff02870c62fbd1ac20290248d3d506da1545a96ce044e32b0b8b370321947bd26048a1188a9bcabfdbb0411eec5acb73a07f6527c39e3fc43d2b2fea28", "otaHeaderString": "OM15082-SW_AQ02-JN5180-ENCRYPTED", "modelId": "lumi.remote.b286acn03" }, { "fileName": "OTA_lumi.sen_ill.agl01_V1.0.27_20200312.20200507152054.ota", "fileVersion": 27, "fileSize": 215886, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.sen_ill.agl01_V1.0.27_20200312.20200507152054.ota", "imageType": 9096, "manufacturerCode": 4447, "sha512": "8beb3aac5eeb05cec5d6d4299066a929b2b4268d318864c407d8ed29f7a99a0d876c96afa01ca58b603203e6b8e7eff813d8616bfb69e9e4d391b35030bd17fd", "otaHeaderString": "OM15082-LUX-JN5180-AQ-ENCRYPTED0", "modelId": "lumi.sen_ill.agl01" }, { "fileName": "OTA_lumi.sen_ill.mgl01_V1.0.18_20190814.20191008105225.ota", "fileVersion": 18, "fileSize": 212206, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.sen_ill.mgl01_V1.0.18_20190814.20191008105225.ota", "imageType": 9096, "manufacturerCode": 4447, "sha512": "b64d5670271617c06695049aee0a832f3667aa2a15ba84ee84c879dcc5089e046b570918ec5ad0e509b195bf3c7a315d1984939256b386c6766f471f8b7ef922", "otaHeaderString": "OM15082-LUX-JN5180-MI-ENCRYPTED0", "modelId": "lumi.sen_ill.mgl01" }, { "fileName": "OTA_lumi.switch.b1laus01_0.0.0_0032_20200609_0C501F.20200811124320.ota", "fileVersion": 32, "fileSize": 268222, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.switch.b1laus01_0.0.0_0032_20200609_0C501F.20200811124320.ota", "imageType": 528, "manufacturerCode": 4447, "sha512": "5c0ef08e8a09d8611d05f9816556f92aaf142854a36564f44a8583928fb87ad4d6870080fde39d29a0daca8f767ddc7382de23b9689a1a9c8baa86f85e62d0af", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b1laus01" }, { "fileName": "OTA_lumi.switch.b1nacn01_0.0.0_0026_20211101_E70B9E.ota", "fileVersion": 26, "fileSize": 286350, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.switch.b1nacn01_0.0.0_0026_20211101_E70B9E.ota", "imageType": 268, "manufacturerCode": 4447, "sha512": "45cc317c491ba0d3cf84d9d16572920009481d9be2527d43be156af8cedb263ebe76320c1e55bcbf114578967efa30d2abec840adfe8394cbc850931c5e4b258", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b1nacn01" }, { "fileName": "OTA_lumi.switch.b2laus01_0.0.0_0032_20200609_CC225A.20200811140905.ota", "fileVersion": 32, "fileSize": 270014, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.switch.b2laus01_0.0.0_0032_20200609_CC225A.20200811140905.ota", "imageType": 656, "manufacturerCode": 4447, "sha512": "07666cf47deba2f10574cc52a39e7855fb3961d52272b29958ee51d48b375e407a5fabc755f737b44c113622e9b136d3650a6251b761a59d06eb3ba376d09294", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b2laus01" }, { "fileName": "OTA_lumi.vibration.agl01_V1.0.25_20200528_F8C40C.20200529185424.ota", "fileVersion": 25, "fileSize": 242382, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi.vibration.agl01_V1.0.25_20200528_F8C40C.20200529185424.ota", "imageType": 8968, "manufacturerCode": 4447, "sha512": "7580343aa5334c23561d0843ea6e8983358e35e3a2d5ebad03d9862632011753f3c72f4c93efcd60a65c1781da59d11217e144aab00d5aa597a70895433e65eb", "otaHeaderString": "OM15082-SWITCH-JN5180--ENCRYPTED", "modelId": "lumi.vibration.agl01" }, { "fileName": "OTA_lumi_switch_l3acn3_0_0_0_0027_20200619_283DA8_20200702151504.ota", "fileVersion": 27, "fileSize": 271982, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_lumi_switch_l3acn3_0_0_0_0027_20200619_283DA8_20200702151504.ota", "imageType": 1288, "manufacturerCode": 4447, "sha512": "dc09d2a451cf89927342a27b70e6e823b23f5ad118cf6a1b4c444dd4af1508ae2c52810ca96d309d4a1aea17dccb2402b356b3177831fa510e30d9d5d09091bc", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.l3acn3" }, { "fileName": "OTA_withCRC_LMES_Dimmer3Controller_V1.2.30_20170801.20170818101543.ota", "fileVersion": 30, "fileSize": 189730, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_withCRC_LMES_Dimmer3Controller_V1.2.30_20170801.20170818101543.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "d6fb4c4ec58fb1618056e3bd1d338f1ee45ef69942b0a98993bdf98898d8c62f2b6104bf618524cabe2b998b9d5237afae978c3d7dfa3a14325ba3c31dc5b076", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_dimmer3.es1" }, { "fileName": "OTA_withCRC_LMES_DualController_V1.3.30_20170801.20170818100757.ota", "fileVersion": 30, "fileSize": 184002, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_withCRC_LMES_DualController_V1.3.30_20170801.20170818100757.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "02abd66413de6a4f3bf5633b1e82b66f4b8a1fa9b488d1f7a3aeac5f76a097e83a207920f47cce90fa5f12cf9a39ef6ea7c0e629a5be32bd49b0cbc4bc95a7bd", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_dualchn.es1" }, { "fileName": "OTA_withCRC_LMES_HVACController_V1.2.30_20170710.20181024102131.ota", "fileVersion": 30, "fileSize": 185138, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_withCRC_LMES_HVACController_V1.2.30_20170710.20181024102131.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "c9587bd3318aa040473a4fd8d0c20f89c169f83902476ab28c738aa3b1d3b5a4bfb2a2663447135761518c86c91c35152290d9e0ef67385e09c5094c675b5f41", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.airrtc.tcpecn01" }, { "fileName": "OTA_withCRC_LMES_HVACController_V1_2_30_20170710_20170818101250.ota", "fileVersion": 30, "fileSize": 185138, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/OTA_withCRC_LMES_HVACController_V1_2_30_20170710_20170818101250.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "c9587bd3318aa040473a4fd8d0c20f89c169f83902476ab28c738aa3b1d3b5a4bfb2a2663447135761518c86c91c35152290d9e0ef67385e09c5094c675b5f41", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.ctrl_hvac.es1" }, { "fileName": "lumi.plug_20211224_v92.ota", "fileVersion": 92, "fileSize": 185918, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.plug_20211224_v92.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "d9c53f674955832d3b31903cf6f8812791380339cfebd4ffd801638f781ca3fb9d43b74e4bc78d96ce684c23fa3875f7451448ba48e701d6b0a4dbcbb5dcc0e5", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.plug" }, { "fileName": "lumi.relay.c2acn01_20211201_v0047.ota", "fileVersion": 47, "fileSize": 182206, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.relay.c2acn01_20211201_v0047.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "66380db41c6f9cc8d4c74809caff46c0cb1b691046c3e490fe8d98f2fa2a5c6acceb1d27456a3d7efd50cde92ef150c5adbc0287538a7218540400385bd2956d", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.relay.c2acn01" }, { "fileName": "lumi.switch.b1lacn01_0.0.0_0032_20200414.ota", "fileVersion": 32, "fileSize": 267726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.switch.b1lacn01_0.0.0_0032_20200414.ota", "imageType": 520, "manufacturerCode": 4447, "sha512": "1c7e4d51802bd4196e6ea0cf432b5bb40117ae71d20035341be25834417f2c12873539ba99953039c468a2d801b7c59b8a6290619b36bdf195b2161ca1cf9322", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b1lacn01" }, { "fileName": "lumi.switch.b2lacn01_0.0.0_0032_20200414.ota", "fileVersion": 32, "fileSize": 269454, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.switch.b2lacn01_0.0.0_0032_20200414.ota", "imageType": 648, "manufacturerCode": 4447, "sha512": "b0d9595aab6d9b9b6ad9acd8b0bb9f0d6577dbbf4c96f90cd6465e8c6a487d999f6b0d8f378472580c19e072c029dbe21d9f18cd010f67ecac8ec4dad21cb0a3", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b2lacn01" }, { "fileName": "lumi.switch.b3l01_0.0.0_0033_20200414.ota", "fileVersion": 33, "fileSize": 270894, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.switch.b3l01_0.0.0_0033_20200414.ota", "imageType": 4232, "manufacturerCode": 4447, "sha512": "b2ef1b4a93225ef8d0400a039fb67a124fc188b90d9bc9d1b9444fcf780465bcfcc78c9c2df8987034487d8bd9b12e5296b307b9088bfec2049c86a2a43a2091", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b3l01" }, { "fileName": "lumi.switch.n0acn2_0.0.0_0039_20211230_6DCD12.ota", "fileVersion": 39, "fileSize": 287646, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.switch.n0acn2_0.0.0_0039_20211230_6DCD12.ota", "imageType": 1409, "manufacturerCode": 4447, "sha512": "c830f68703a1556c619000b8b67c055676953d40ac32090f3ca4fa8abc7c30902614a2890dcd926b9e8d496be769e103ef32c40c9dbbc4caec15417d00465de6", "otaHeaderString": "lumi.switch.n0acn2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "lumi.switch.n0acn2" }, { "fileName": "lumi.zzjq_1.1.35_20180824_v35.20180824161828.ota", "fileVersion": 35, "fileSize": 176238, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/lumi.zzjq_1.1.35_20180824_v35.20180824161828.ota", "imageType": 257, "manufacturerCode": 4447, "sha512": "c7b739ab5dce9a15e1b7019503bde9d0809577cbd701b4c03cdf47cad4bbdb8490016f1d52cd2c9af37e1a9cc805fe997ea0f75e91149bea26575f75b10b750b", "otaHeaderString": "DR1175r1v1UNENCRYPTED00000JN5169", "modelId": "lumi.eemeter.zbtecn01" }, { "fileName": "4512700-Firmware-35.ota", "fileVersion": 56, "fileSize": 187759, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512700-Firmware-35.ota", "imageType": 103, "manufacturerCode": 4644, "sha512": "a26bc37b7dfad1fa473eef13677505df70ec6ae5e3012351dec1344bac53756b4423ae2fbfdb21056b11da8b762455df15481500f7b505f5e2892c33629e0a21", "otaHeaderString": "4512700\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "4512703-Firmware-35.ota", "fileVersion": 25, "fileSize": 160153, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512703-Firmware-35.ota", "imageType": 1008, "manufacturerCode": 4644, "sha512": "b4d6e2ee31ad7a90ec0d87d3a00ca08a587ac5ca276484a2ffdc8ff94dcb6b63199f679e6659a2b5217aa4a6bf067c8bc10692ca541ddec354184939a673cb71", "otaHeaderString": "4512701&4512703&4512719\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "4512703" }, { "fileName": "4512704-Firmware-35.ota", "fileVersion": 47, "fileSize": 241342, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512704-Firmware-35.ota", "imageType": 2004, "manufacturerCode": 4644, "sha512": "0aeb77ba2d00b84af63582a3c71e98fda84f9bfdd079a19c203e34b18ff1f722417484e03e0be841793c14f9e3eb50e77412f4918d3b50ada1c1e85bba4c2d7a", "otaHeaderString": "4512704\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "4512704" }, { "fileName": "4512705-Firmware-35.ota", "fileVersion": 25, "fileSize": 160153, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512705-Firmware-35.ota", "imageType": 1008, "manufacturerCode": 4644, "sha512": "b4d6e2ee31ad7a90ec0d87d3a00ca08a587ac5ca276484a2ffdc8ff94dcb6b63199f679e6659a2b5217aa4a6bf067c8bc10692ca541ddec354184939a673cb71", "otaHeaderString": "4512701&4512703&4512719\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "4512705" }, { "fileName": "4512719-Firmware-35.ota", "fileVersion": 25, "fileSize": 160153, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512719-Firmware-35.ota", "imageType": 1008, "manufacturerCode": 4644, "sha512": "b4d6e2ee31ad7a90ec0d87d3a00ca08a587ac5ca276484a2ffdc8ff94dcb6b63199f679e6659a2b5217aa4a6bf067c8bc10692ca541ddec354184939a673cb71", "otaHeaderString": "4512701&4512703&4512719\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "4512719" }, { "fileName": "4512721-Firmware-35.ota", "fileVersion": 25, "fileSize": 160154, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512721-Firmware-35.ota", "imageType": 1008, "manufacturerCode": 4644, "sha512": "af27753c8e1c53e38f3330e161143f01492a9d124156a9e1a8ead138143695e1496a5d0266494892e8a6d1055f6b8a7272ea5327d6d8061ba4be94f74c39c418", "otaHeaderString": "4512721&4512728&4512729\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "4512721" }, { "fileName": "4512726-Firmware-35.ota", "fileVersion": 22, "fileSize": 144312, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512726-Firmware-35.ota", "imageType": 1234, "manufacturerCode": 4644, "sha512": "e338a39c7e4d9c902d0c36e9d83c14e75e562acfa26c87a2e7c730bfe310700a3f2504f2c2ee828ebac2f69db0fc32ec3ebd80de97ef9357244846c7ad7d7baa", "otaHeaderString": "Encrypted GBL Z3SwitchSoc_sdk676", "modelId": "4512726" }, { "fileName": "4512729-Firmware-35.ota", "fileVersion": 25, "fileSize": 160154, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512729-Firmware-35.ota", "imageType": 1008, "manufacturerCode": 4644, "sha512": "af27753c8e1c53e38f3330e161143f01492a9d124156a9e1a8ead138143695e1496a5d0266494892e8a6d1055f6b8a7272ea5327d6d8061ba4be94f74c39c418", "otaHeaderString": "4512721&4512728&4512729\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "4512729" }, { "fileName": "4512772-Firmware-35.ota", "fileVersion": 26, "fileSize": 146034, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/4512772-Firmware-35.ota", "imageType": 166, "manufacturerCode": 4644, "sha512": "345b089dffdf63057757afecd2808a99b8c3c420721117b7ae234153e820bf2a12f8ae993106dda478196248d7db728caaca32320d110c42c365b678546ae181", "otaHeaderString": "Encrypted GBL Z3SwitchSoc_sdk676", "originalUrl": "https://www.elektroimportoren.no/docs/lib/4512772-Firmware-35.ota" }, { "fileName": "5401392-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401392-Firmware-35.ota", "imageType": 6207, "manufacturerCode": 4644, "sha512": "77238bf33e618f65d5651235836277bfa985ab14db09caf1f7be6b97cf962e4b89edacb204249c427392d24e3a63b5b5bcb2ee41af053524d2ccba8575853c13", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401393-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401393-Firmware-35.ota", "imageType": 6206, "manufacturerCode": 4644, "sha512": "d7c57d581cfdff58c7ecd6201bad199b9e30e9d96e692767a4435718ad67853933ccc5779f0ab60dc13ee947d8d78d071c9d1445e3c1aff18b6e58cda5fdf776", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401394-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401394-Firmware-35.ota", "imageType": 6205, "manufacturerCode": 4644, "sha512": "aced4b4f5e579e65201476e41d98708fc09b1cefbaee7bf2dc87e9e95f1bbc0a6ecdfe1e3fde89495d666c2b6648ac9c0a78e7f3cba68eeac195847a019e14cb", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401395-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401395-Firmware-35.ota", "imageType": 6204, "manufacturerCode": 4644, "sha512": "0d73ecc7b2d8b92c7508da39a1db5a52b3cb0deb73cc2ffb58b6d7522f8843910330488b574885d8d87065fd6eb2e9be6b361d5be53dd76aee4d13835492ebb4", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401396-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401396-Firmware-35.ota", "imageType": 6211, "manufacturerCode": 4644, "sha512": "9acfb847048dbf5de2eb022e09a6187dc1201849119a8c1d59c87a352769ca440627abc9ca903177a281fd4f015b7600ad6bb9c890e1caee0692411f92ae9daa", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401397-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401397-Firmware-35.ota", "imageType": 6210, "manufacturerCode": 4644, "sha512": "2bd9a7db0c606e54fcd1cb8ca69301569652b0ae859ba1dc1cd15205e305ec1f2c4ec19a71c7d851032903386199053c0927d224e1215b78ee9b1a2648bb1082", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401398-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401398-Firmware-35.ota", "imageType": 6209, "manufacturerCode": 4644, "sha512": "e6b59973e7659f8f2d1c5e237129e184acd9bc0509acac302905604bba38588de9e681aa8b8a9a4a59f3573d4845e65ff7505ae0038079df025135c1bad34ee0", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "5401399-Firmware-35.ota", "fileVersion": 25, "fileSize": 255890, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/5401399-Firmware-35.ota", "imageType": 6208, "manufacturerCode": 4644, "sha512": "81a8aa05bd2ac259e7ff21bc70588b5a56f4e9a9e04c5a48a66b07534604e2836e8210b406ea582a3f1781173e0692c63a38db59d6e20f2e95815b4ba852bf69", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router_ZG9" }, { "fileName": "NAMRON_AS_4512737(6001) V22.ota", "fileVersion": 22, "fileSize": 245148, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/NAMRON_AS_4512737(6001)%20V22.ota", "imageType": 6001, "manufacturerCode": 4644, "sha512": "ce40cfc670692384590fc58b84fd9b0d1a6f7b1fa28c6a34ea46fd404b536135151e0fa70bef5ff0c1eeca3fe0d27174806fd1b4863e5bc10afaeca49e0b14f0", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router\u0000\u0000\u0000\u0000", "modelId": "4512737" }, { "fileName": "NAMRON_AS_4512738(6002) V22.ota", "fileVersion": 22, "fileSize": 245148, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/NAMRON_AS_4512738(6002)%20V22.ota", "imageType": 6002, "manufacturerCode": 4644, "sha512": "c6eb11b68bbbacee5ee8dc483a93d07db60003a8345fdd512e05603b9ab1b3dd06f692e01a3be71f3411fdd43a3446198fc7dc806c9b80cab3c86226a41941a7", "otaHeaderString": "Encrypted GBL Z3_HVAC_Router\u0000\u0000\u0000\u0000", "modelId": "4512738" }, { "fileName": "128b-0002-0305-700_nodon_sin_4_2_20_fw_V0305.zigbee", "fileVersion": 773, "fileSize": 415960, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0002-0305-700_nodon_sin_4_2_20_fw_V0305.zigbee", "imageType": 2, "manufacturerCode": 4747, "sha512": "5353005ba068d46aa3c32aa31a1e4958c801e467f65ada46fe438a4f70402c711a7ce22ac5fe3f0b3882b3d8222b08dd43d89fca3babdc30f38121b625b96f04", "otaHeaderString": "nodon_sin_efr32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0005-0305-700_nodon_sin_4_1_21_fw_V0305.zigbee", "fileVersion": 773, "fileSize": 399708, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0005-0305-700_nodon_sin_4_1_21_fw_V0305.zigbee", "imageType": 5, "manufacturerCode": 4747, "sha512": "a5eaf2e4faf5b2ce3330f51ee738a65499209cf754784b025626f683c95b7b018a57b6fb55ff47ff86de5ca7dcf7f1a36ada0307252c4119368c5c7bad125261", "otaHeaderString": "nodon_sin_efr32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0006-0305-700_nodon_sin_4_fp_21_fw_V0305.zigbee", "fileVersion": 773, "fileSize": 396656, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0006-0305-700_nodon_sin_4_fp_21_fw_V0305.zigbee", "imageType": 6, "manufacturerCode": 4747, "sha512": "9ed0f44d8cd338a53e9be2b9fe136213e9a8ff886f78d94fe992b02ef6fee412fe58c6145c2facf2bf5b516b7d000e8997ee5b680d7c2acf5ff96fdb15d4a82e", "otaHeaderString": "nodon_sin_efr32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0009-0305-700_nodon_sin_4_rs_20_fw_V0305.zigbee", "fileVersion": 773, "fileSize": 398752, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0009-0305-700_nodon_sin_4_rs_20_fw_V0305.zigbee", "imageType": 9, "manufacturerCode": 4747, "sha512": "88bdfeadcbae70c3fe541f98af1dfbd9b437600ede7d7d27ca8e21a5b6e86dc8fa6ffbf9921cafda9a0f19809e909d1126b71cc221b53e39f5f62d252e4e407e", "otaHeaderString": "nodon_sin_efr32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-000A-0305-700_nodon_sin_4_1_20_V0305.zigbee", "fileVersion": 773, "fileSize": 412984, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-000A-0305-700_nodon_sin_4_1_20_V0305.zigbee", "imageType": 10, "manufacturerCode": 4747, "sha512": "95d2b3dbcf41e5212e63ff92270a9fa8ebb1cdabf84b3fa94bc4207bd55ce86ac4e110c1c150d78affef2d003ad4a306ad5947a55705d1ff1637cc4df3df29cf", "otaHeaderString": "nodon_sin_efr32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0102-10101-700_nodon_sin_2_fm_stm32_V10101.zigbee", "fileVersion": 65793, "fileSize": 27162, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0102-10101-700_nodon_sin_2_fm_stm32_V10101.zigbee", "imageType": 258, "manufacturerCode": 4747, "sha512": "4e9eaa29e4bd1277595d6bc779f4f45b9e70dc5407d7fe200c08884e533e0f377f054021cfb89c7e6fad5ffd1dcd35ec22ae90e39ebc94874c95ab2acdb2e77c", "otaHeaderString": "nodon_sin_stm32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0105-010500-700_nodon_sin_xx_met_fm_stm32_V010500.zigbee", "fileVersion": 66816, "fileSize": 39464, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0105-010500-700_nodon_sin_xx_met_fm_stm32_V010500.zigbee", "imageType": 261, "manufacturerCode": 4747, "sha512": "fc47d5f4e6a709cf3ced8b0cbc97dce6f7626a185ea86f807827afd29c87a380ba4f9bbc200f7a19f41033e4d1609ffc2c77f0ac4fe486c6756fa7cfe1b0713e", "otaHeaderString": "nodon_sinMet_stm32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0106-010500-700_nodon_sin_xx_met_fm_stm32_V010500.zigbee", "fileVersion": 66816, "fileSize": 39464, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0106-010500-700_nodon_sin_xx_met_fm_stm32_V010500.zigbee", "imageType": 262, "manufacturerCode": 4747, "sha512": "214f6e1793a739a1822ddbe53a5c68a32b0ee48ba19fff974b8e607bfaea4baeb3596dde070e7aec06e0a7eddad25a67db62ee8a7396c77fe5ac71f34e7fb35a", "otaHeaderString": "nodon_sin_stm32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-0109-10102-700_nodon_sin_rs_fm_stm32_V10102.zigbee", "fileVersion": 65794, "fileSize": 30206, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-0109-10102-700_nodon_sin_rs_fm_stm32_V10102.zigbee", "imageType": 265, "manufacturerCode": 4747, "sha512": "54f663c338f26aa89245edc79bbbdc911763897f2ca6ba92d72ce447f03f23d0d33a669af68e87ef84d95fa0df0231441f56fc45cb768a5e12ca2f1e77c2f1b9", "otaHeaderString": "nodon_sin_stm32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-010A-010500-700_nodon_sin_xx_met_fm_stm32_V010500.zigbee", "fileVersion": 66816, "fileSize": 39456, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-010A-010500-700_nodon_sin_xx_met_fm_stm32_V010500.zigbee", "imageType": 266, "manufacturerCode": 4747, "sha512": "d866e1dd0614b33a8d24b0d66228d53f4cca93c26a2613f75dcf73edb311e7bf6e3c791fae93c3b9633a4c40dbb6380aee5e6134fdfb9cee7bce70386fb7eb1d", "otaHeaderString": "nodon_sin1_stm32_ota\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-2002-040300-700_p2022_HSP_tphu_fw_efr32_V040300.zigbee", "fileVersion": 262912, "fileSize": 292140, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-2002-040300-700_p2022_HSP_tphu_fw_efr32_V040300.zigbee", "imageType": 8194, "manufacturerCode": 4747, "sha512": "ef49e08f08c592887d2cbc24ca95c3e9ee8e9396be1e32dec66b919075b302680954383eb2465da9023faf7ab88078884b9d8cd59d17c8261a4cf4e05ce4e428", "otaHeaderString": "p2022_HSP_tphu_fw_efr32\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-2003-040300-700_p2022_HSP_do_fw_efr32_V040300.zigbee", "fileVersion": 262912, "fileSize": 289364, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-2003-040300-700_p2022_HSP_do_fw_efr32_V040300.zigbee", "imageType": 8195, "manufacturerCode": 4747, "sha512": "aa642f6949ae15848a2ce0ba5f46867caf6dda4e04d46e116fd5df0cf7d7db324d0150cfd3ee43db0245e491756c100051217e5aba4c8c5709780c2c2f9378ad", "otaHeaderString": "p2022_HSP_do_fw_efr32\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-2004-040300-700_p2022_HSP_dc_fw_efr32_V040300.zigbee", "fileVersion": 262912, "fileSize": 289288, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-2004-040300-700_p2022_HSP_dc_fw_efr32_V040300.zigbee", "imageType": 8196, "manufacturerCode": 4747, "sha512": "11344fa4dce9633aba9a83a49d48fea2cd1f7aa0c9622e783bd3df345b41b564bd9db0c600845a69170c59e1c1c1f37416cf429c7257e58cd942033ecb6fd995", "otaHeaderString": "p2022_HSP_dc_fw_efr32\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "128b-4002-010600-nodon_irb-4-1-00_fw_V010600.zigbee", "fileVersion": 17197616, "fileSize": 155734, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/NodOn/128b-4002-010600-nodon_irb-4-1-00_fw_V010600.zigbee", "imageType": 16386, "manufacturerCode": 4747, "sha512": "d2a350be20dd1d3c46a1d26d505b6dbbe5c6fd0894d048f99cd09946ed109163bab8883cded8a9882af6f19dff726232cc2c1066b26ea98b131b7b4d53d9d717", "otaHeaderString": "EBL Remotec_IR\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020509_CLA60_TW.ota", "fileVersion": 16909577, "fileSize": 133444, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020509_CLA60_TW.ota", "imageType": 8, "manufacturerCode": 4364, "sha512": "1cba1e813a0264143e082c9288922309b098c2490548b74a6f63bd2703057afab2f80b16500805417b20ad2c02746cd64c17e3a91c0f338c5dac051a23e43c37", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_CEILING_TW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 132672, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_CEILING_TW_OSRAM.ota", "imageType": 107, "manufacturerCode": 4364, "sha512": "3222b2998a5d587db758fd0ea0185cb0c60324276e30227828d976d469b5ee7461ccc7ccf24e7e9fc7790352e94bf547ff85a44b341a9ce80356c5ba737af1a4", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_CLA60_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142972, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_CLA60_RGBW_OSRAM.ota", "imageType": 98, "manufacturerCode": 4364, "sha512": "f48a5a3b4d0e636e40e82b219eb6ab64431b98b92ad0737a4de42193d64dd5638d38c13273835b2f0959be98b424c0a6ee47c3eb8b7aa18de11d96e84ad5b0d5", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_CLA60_TW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 132672, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_CLA60_TW_OSRAM.ota", "imageType": 99, "manufacturerCode": 4364, "sha512": "c084961dd6117cfd1dc1dab4fd99e6f95c3f0fb2a6b9a847acd52e7b2ab091b32800898477d04871db32cfc7c96eec5afdbf4564cc27ca63283bbeeaec63d06f", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_CLA60_W_CLEAR.ota", "fileVersion": 16909584, "fileSize": 123884, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_CLA60_W_CLEAR.ota", "imageType": 19, "manufacturerCode": 4364, "sha512": "7f4bb423aed7d3d81a43c054897796f76e05c049911fee9ffc36d4c4a4baa44369bb35a8d44d8e3630e26ec65a3a59e5ada97d75109887422c7e6a75b2a9bcf5", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_CLASSIC_A60_RGBW.ota", "fileVersion": 16909584, "fileSize": 144008, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_CLASSIC_A60_RGBW.ota", "imageType": 6, "manufacturerCode": 4364, "sha512": "dbdab10f00722f7b9be2b7b4e6ce8302edee2ad02c9b75da9adf754779ad003ed8f5b07225ea547be14c600989baae56cf1e420b11b264d8c31d3f856e5a9892", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_CLASSIC_B40_TW.ota", "fileVersion": 16909584, "fileSize": 132928, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_CLASSIC_B40_TW.ota", "imageType": 20, "manufacturerCode": 4364, "sha512": "2faa40b833154aaee844b6ab6e393c76b327b1122bb88ae6f3036d95858cfdcdae35fad7c5f6578c166be25fe6978eb02858ae4e09c955debcf26b11b23b79de", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_FLOOD_LIGHT_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142972, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_FLOOD_LIGHT_RGBW_OSRAM.ota", "imageType": 108, "manufacturerCode": 4364, "sha512": "a3b64a5183bc23617afa528edffbe83721547f203f3eca3f043c39d42eacee94cd0a15f8096c136d3c738dd87945f15f9822b0ba265249e1892e739a2b491904", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_GARDENPOLE_MINI_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_GARDENPOLE_MINI_RGBW_OSRAM.ota", "imageType": 103, "manufacturerCode": 4364, "sha512": "c2eabeb89c104b3d398ac9bcd8ff7f8a10eb2e6352050f0404939f7b6c7391ea005b1975ff3e8367775ce2f4b40a1fc197b390564e75389e71f61a2f6d3b7a5e", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_GARDENPOLE_RGBW_LIGHTIFY.ota", "fileVersion": 16909584, "fileSize": 142968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_GARDENPOLE_RGBW_LIGHTIFY.ota", "imageType": 90, "manufacturerCode": 4364, "sha512": "35242c676acf7426fbe89562a5199a665778ef95a2e001141fd5d4927e3a8d3724c0cea5874f36cefdb536bbae3356dd99bff8e6a3666c6205141e3d34d66f57", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_GARDENSPOT_RGB.ota", "fileVersion": 16909584, "fileSize": 140550, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_GARDENSPOT_RGB.ota", "imageType": 5, "manufacturerCode": 4364, "sha512": "20f450221e2915f84dc9ac50d3649f8df219240a35a770c2185d270ccfd4619968f8fe82cb917e181b1e688c6ea6623dc59798575716d2af7f0f660842ca6821", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_GARDENSPOT_W.ota", "fileVersion": 16909584, "fileSize": 123884, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_GARDENSPOT_W.ota", "imageType": 7, "manufacturerCode": 4364, "sha512": "dab7a5430e9a1bd12d15b0a524c0d715ba3de5800fe528478ccdd147a6923fcbe85fa124f0afd120a9a8e4f2d6f458763c88cd58dc1716d44040665a4ccc034d", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_LIGHTIFY_INDOOR_FLEX.ota", "fileVersion": 16909584, "fileSize": 142972, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_LIGHTIFY_INDOOR_FLEX.ota", "imageType": 92, "manufacturerCode": 4364, "sha512": "c40a4c6a58409f325d1409536737b11bdbb6ffd1176d67aea0423778af8b513c4663d8ece9666363950b12e24507d2a8ff345645eb62ca2e881223b94cb3c42f", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_LIGHTIFY_OUTDOOR_FLEX.ota", "fileVersion": 16909584, "fileSize": 143228, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_LIGHTIFY_OUTDOOR_FLEX.ota", "imageType": 91, "manufacturerCode": 4364, "sha512": "b21e18b34bb70d9904adf05e2697769760ada4c1b1ed30aa8c647e99442c35090727f35e823e3dad16ea1593967c790c683895f1c33184eb80055cf322e640ab", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_MR16_TW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 132676, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_MR16_TW_OSRAM.ota", "imageType": 101, "manufacturerCode": 4364, "sha512": "c3773bcba343db437a051be87cbdfb4055412875760d00257e0fc74b7d34d13b496b078d6b0211013101a358f3d5bf3fee17fb6c84a175662b147b32e15f9b62", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_OUTDOOR_LANTERN_B50_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_OUTDOOR_LANTERN_B50_RGBW_OSRAM.ota", "imageType": 105, "manufacturerCode": 4364, "sha512": "032d9bfa1155bc78b5c010070f0b89e26227da3ada96cec9edb83ad783c2babcad4ec5ab9d8ab8491768e0826effa6f051d5206492dda9669002fdef0320cb97", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_OUTDOOR_LANTERN_B90_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_OUTDOOR_LANTERN_B90_RGBW_OSRAM.ota", "imageType": 110, "manufacturerCode": 4364, "sha512": "8f54df92306ebd864022d368fbb00f87d33c04111c2482e436564a778cc4d3de3c129a355a712d96c028c3ff599134bbf9cfdd41386e2e767e8814b72b9e948b", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_OUTDOOR_LANTERN_W_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_OUTDOOR_LANTERN_W_RGBW_OSRAM.ota", "imageType": 104, "manufacturerCode": 4364, "sha512": "7674274798b0eab2f4023f0732f56fdcf131c108ed1080f8a752aa58542839edae468980c51dddf87a1452c331e4bd1f793bb792c2d6e73a4183e792e478f4a8", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_PANEL_RGBW_OSRAM.ota", "fileVersion": 16909584, "fileSize": 142968, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_PANEL_RGBW_OSRAM.ota", "imageType": 106, "manufacturerCode": 4364, "sha512": "0fdc276f1620a676118245f7744a1cb079e5ac5686106d29c744e6aa6b494421dcf438ff12212723844f95b82612ad837275b7f8c67b8403eb11b39ee3bc03af", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_PAR16_50_TW.ota", "fileVersion": 16909584, "fileSize": 132672, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_PAR16_50_TW.ota", "imageType": 3, "manufacturerCode": 4364, "sha512": "ce267106c201cfca6f94e1fdfa4a256bdd8eede8610bc0f5ccebbc70c5df50acf37d71f1779fd454069a6708caa2c858c6510bc627ced7b60b9dcb05bceab240", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_Par16Rgbw.ota", "fileVersion": 16909584, "fileSize": 142086, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_Par16Rgbw.ota", "imageType": 17, "manufacturerCode": 4364, "sha512": "e19e81931f7716a02aabbe696630d5bf48e13ec46f4fd1353c59ff29f683ca44ff5b8417387b1e811e3db32a1b2dcc87d88989874de08790ed304410bd7c322c", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_Surface_Light_TW.ota", "fileVersion": 16909584, "fileSize": 131904, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_Surface_Light_TW.ota", "imageType": 4, "manufacturerCode": 4364, "sha512": "e11551fedbf0617d1133fd4cbc03a05231569ebc3ff006fd49a1e2fc7e6821eaf6d5f4bb19d39381e67f13c74caf3269b8ae42c49e3accadc4a0b029d2cd625d", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_MK_0x01020510_Surface_Light_W.ota", "fileVersion": 16909584, "fileSize": 123884, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_MK_0x01020510_Surface_Light_W.ota", "imageType": 9, "manufacturerCode": 4364, "sha512": "83c54292e2da84e377175f06dc3860028cc37676b51e6669d13a888ff2bdf1545cf215c8990fd6915bbb3359a450c10cd03a30108366fc8b2ccbb60588c1ca69", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_Plug01_OnOff_MK_0x01020509.ota", "fileVersion": 16909577, "fileSize": 121680, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_Plug01_OnOff_MK_0x01020509.ota", "imageType": 39, "manufacturerCode": 4364, "sha512": "65aa917e46d1f31cc1a148a2d0cbe9d24bac3267deb6d6567e27f584b9124ca0fabe0d2be26aa185461605194c1ab0ab68a99dd0e71148e35e21ddc4815e2d21", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZLL_SubstiTube_W_MK_0x01020509.ota", "fileVersion": 16909577, "fileSize": 123440, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Osram/ZLL_SubstiTube_W_MK_0x01020509.ota", "imageType": 46, "manufacturerCode": 4364, "sha512": "2b5de5c152ea81b5cd0f565b325703edfa2e781911366b2f462b982001885876663cab1f7635eba7b6972fea8ba9fc55a1bcad43c7fc5653c2c22a78bd0b2a86", "otaHeaderString": "NULL\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "LDS_MotionSensor_1168-0402-41030002.zigbee", "fileVersion": 1090715650, "fileSize": 158446, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Perenio/LDS_MotionSensor_1168-0402-41030002.zigbee", "imageType": 1026, "manufacturerCode": 4456, "sha512": "e0cc45970c75311a3a4b0f592c023583f8224d6ca0b2c0612539131b7210e095903fd139c50c4f54c9f97b4b40bd0e0637969aa54b229dbba00e1e40333a4edc", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "ZHA-PirSensor" }, { "fileName": "LeakSensor_v5.OTA.zigbee", "fileVersion": 5, "fileSize": 131742, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Perenio/LeakSensor_v5.OTA.zigbee", "imageType": 1026, "manufacturerCode": 4151, "sha512": "6b2bd58cef1936e5a93399619ada984e2e0aa783e6d9cd193f622c3ce8c45a8d9ca86d4e65032303f38a50f8dc2b8373d324be5b006595b650d3425f431f58b3", "otaHeaderString": "YF_LeakSensor_NA772_JN5169000000", "modelId": "PECLS01" }, { "fileName": "CSB600_20170209.ota", "fileVersion": 538378761, "fileSize": 185320, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/CSB600_20170209.ota", "imageType": 47, "manufacturerCode": 4216, "sha512": "81e9a7d13a7ea5ae72b8a3fedc4d1df4133775b9bf7c0ecdf778ed8a63816553fabb342b4353cef7d38925c89160549eb653219149af98e50ba812b11ff86c83", "otaHeaderString": "image build\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/00f1cffd-2aa9-4f5e-8503-59beddc86ba3/CSB600_20170209.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "ECM600_09160525_V16.ota", "fileVersion": 152438053, "fileSize": 340782, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/ECM600_09160525_V16.ota", "imageType": 37, "manufacturerCode": 4216, "sha512": "8fa0103ba223f257e8de01a2bd2e4edd571a3e3f2056a788f50679f29a5bc73ae120ae24c0fad3e640cee1a53a7c6fafdec5efae27139be9a9371c77b083e66c", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/AN2X0Z_xsQeexhO012ZUUbFdDk5QFVECSHNGH4oWm20/ECM600_09160525.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "HS1SA_EM-SALUS-0621-V14-190907.ota", "fileVersion": 20, "fileSize": 139006, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/HS1SA_EM-SALUS-0621-V14-190907.ota", "imageType": 8320, "manufacturerCode": 4619, "sha512": "aeaf6f9e27df16b3d2eb398c457448bdfc88a65cf668884f7ce99eb94a2a5bf8a995e0906b595071fc8c069b4ec5e946a20f05ae4f613d7616d45553497a5c42", "otaHeaderString": "General Upgrede File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/1deaffef-7a69-4579-93f8-351a8df644d8/SmokeSensor-EM_00000014.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "Jasco_5_0_1_Dimmer_45857_v6.ota", "fileVersion": 6, "fileSize": 165182, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/Jasco_5_0_1_Dimmer_45857_v6.ota", "imageType": 0, "manufacturerCode": 4388, "sha512": "57f2774b28229f81a83415ca9e51a89ddfcedec527beb5b582e5ff72d45ef5295ba14ab54f077c62e5b1b92870bcaa6a2b5d02cd3ba830dbacc395f7421794b9", "otaHeaderString": "Jasco 45857 image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/3319b501-98f3-4337-afbe-8d04bb9938bc/45857_00000006.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "Jasco_5_0_1_OnOff_45856_v6.ota", "fileVersion": 6, "fileSize": 162302, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/Jasco_5_0_1_OnOff_45856_v6.ota", "imageType": 2, "manufacturerCode": 4388, "sha512": "3306332e001eab9d71c9360089d450ea21e2c08bac957b523643c042707887e85db0c510f3480bdbcfcfe2398eeaad88d455f346f1e07841e1d690d8c16dc211", "otaHeaderString": "Jasco 45856 image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/a65779cd-13cd-41e5-a7e0-5346f24a0f62/45856_00000006.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "PumpWC_20170614V11_CRC.ota", "fileVersion": 538379796, "fileSize": 184766, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/PumpWC_20170614V11_CRC.ota", "imageType": 55, "manufacturerCode": 4216, "sha512": "b2940f263757fefb5f4f80a5a1c1d7ac8ffd56954c2350782a99444cfb71d23253f6a7f7dcb102cacf0412bd0eeb2a792036bc3ab54a8e023f8a6aa09e6b40c9", "otaHeaderString": "SAA6SK(J)1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/206b5a66-cca9-4886-95cd-1f1278bb293d/IT600PumpWC_20170614.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "Repeater_20190415.ota", "fileVersion": 538510357, "fileSize": 171006, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/Repeater_20190415.ota", "imageType": 62, "manufacturerCode": 4216, "sha512": "c837f2ec9f4deaddbb94adfa60b9d85fe9de59d760b10a968ff97356b174118d05b0f3bcfe961604a9c964ac3fb55346643a87145aca2d4777f7e07a7aad59f2", "otaHeaderString": "Repeater\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/777f6d6a-b55c-4536-89ef-4c96eac79854/RE600_20190415.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAA6UT1_00060962_20230628_1619.ota", "fileVersion": 395618, "fileSize": 368822, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAA6UT1_00060962_20230628_1619.ota", "imageType": 70, "manufacturerCode": 4216, "sha512": "f76716c7f615afba13d57df08d7b01fefe5995a00f91768f42dfe81a56470800208665ed5fb3b3cdc88bef09ed41632455f50c4a47b05600308d3481f0521fd1", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/firmware/UG-SYS/WRT/2023-0629/AWRT10RF_00060962.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL2PE1_02015120_OTA.ota", "fileVersion": 33640736, "fileSize": 155838, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL2PE1_02015120_OTA.ota", "imageType": 39, "manufacturerCode": 4216, "sha512": "3bc7aa979f20ae79a65cf2b190b15a91b87fbbe91cac217daf32000b88089cb7e5aad70aace225ab8ddb75e00653c195936fbfc967dbc304ab6332899a1d4213", "otaHeaderString": "SAL2PE1_02015120_OTA ota image f", "originalUrl": "http://eu.salusconnect.io/download/firmware/259311ba-800e-423e-80fc-c3e1d8adc77c/SPE600_02015120.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL2PU1_02015120_OTA.ota", "fileVersion": 33640736, "fileSize": 155774, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL2PU1_02015120_OTA.ota", "imageType": 38, "manufacturerCode": 4216, "sha512": "52b0afaa165eb039288158ada1f5ca93bc61e4b454c6f20394159396f9af2fce4f7a037d8e7141722181e93344da04cb77cab4d6989c0fcee8a4e3eeff49184d", "otaHeaderString": "SAL2PU1_02015120_OTA ota image f", "originalUrl": "http://eu.salusconnect.io/download/firmware/dd12520a-7100-4968-9483-d995a8d18d40/SP600_02015120.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL2SA1_20190415_OTA.ota", "fileVersion": 538510357, "fileSize": 191814, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL2SA1_20190415_OTA.ota", "imageType": 61, "manufacturerCode": 4216, "sha512": "3dfcccb36767ddb07dac12fc8170b8452d682cbbb523ffe0f2c7adbec8a984b0fd492607427f858615a59d1ebf1f98d37378475da1f38558e028eaab0728f195", "otaHeaderString": "SR600\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/a6ab008c-d375-4ce4-a083-d1b152cbd571/SR600_20190415.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL2WB1_181214.ota", "fileVersion": 538448404, "fileSize": 239826, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL2WB1_181214.ota", "imageType": 75, "manufacturerCode": 4216, "sha512": "92943e8156981049b536016cc6c27b0d5b7d1401e939633e5e15032d98c06e59d8ac5a74d45e7933b593b3c55bb3fd009a5b31a7bcc8726d6422c906780ce6c9", "otaHeaderString": "EBL WaterBugSensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/3ea71e61-bbf7-4b93-a3b3-127ea6c3407f/WLS600_20181214.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6DB1_20190410_V2A_CRC.ota", "fileVersion": 538510352, "fileSize": 174590, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6DB1_20190410_V2A_CRC.ota", "imageType": 19, "manufacturerCode": 4216, "sha512": "625da57746569197b61f92e15512dbbc409ae60290dc98866eb39fc2aa4c2c2ad9441ff14511332745fb5a7bec36b71a0ac373c83a35cf206ed7e9dd8e9cd708", "otaHeaderString": "SAL6DB1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/315e4681-e0c8-44ff-bb77-f0bf0629203c/it600Receiver_20190410.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6DF1_003E0029_20221215_1652.ota", "fileVersion": 4063273, "fileSize": 361238, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6DF1_003E0029_20221215_1652.ota", "imageType": 44, "manufacturerCode": 4216, "sha512": "c319412eec498d890ec5bc43a9e1ccb0ec0fbc942cff08d867be36718a57e3d9abfa4aa97677214a13e1270c418d14ea40e20b22ea210d3818ed760141b5bb3d", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/36ab7f9e-6497-4efa-8ece-efb645af9128/FC600_003E0029.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6DFA_0066003E_20230909_0953.ota", "fileVersion": 6684734, "fileSize": 455534, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6DFA_0066003E_20230909_0953.ota", "imageType": 138, "manufacturerCode": 4216, "sha512": "2cbbbd01f15b73c939f1ae4e48ab116f8567434d63cafa8d8d9bcb2aac65935f01a1017ab0f86b2a464065daba8cd612a5cd71bb396a5625e32c585e9e7b50fa", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/debug/OTA_Test/FC600NH/FC600NH_0066003E.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6DT1_009A74A4_20180627_1225.ota", "fileVersion": 10122404, "fileSize": 429878, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6DT1_009A74A4_20180627_1225.ota", "imageType": 17, "manufacturerCode": 4216, "sha512": "3a1aff87a716a06d562b00eaad6f6c271a68dd7eb0256773f9568708e1f2112577ce3e2b2e37fdfabc2f061cb2ef58f957129bf212d851e66c9d5183e8315397", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/7c6f40e8-c433-4976-bc05-fa4de99ec4bf/it600HW-AC_009A74A4.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6DW1_20230222.ota", "fileVersion": 539165218, "fileSize": 152766, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6DW1_20230222.ota", "imageType": 18, "manufacturerCode": 4216, "sha512": "8d370d10b99495e8dd14795dc3ce9185a6d0a653ce10b0c6352c898b7706590703c31309365c2566020c187a130781a9ca7b4a0dea41ec5cde52b9e0af900a68", "otaHeaderString": "EBL IT600_Tstat\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/8a2d1183-9af1-46c5-b760-64824b11b920/it600WC_20230222.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6EM1_00910040_20210928_1336.ota", "fileVersion": 9502784, "fileSize": 280878, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6EM1_00910040_20210928_1336.ota", "imageType": 27, "manufacturerCode": 4216, "sha512": "667d520a968e5205ec1e427b696da1895c24ece53993ecdbc6f4c67845b34a1e2ca52fbba5b345007178c75948bda703bd3ea0224f8d08b49c120345dbd724f7", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/448bbf25-0ec6-4639-8755-bd614fea203d/it600MINITRV_00910040.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6ET1_009A50A4_20180627_1222.ota", "fileVersion": 10113188, "fileSize": 429878, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6ET1_009A50A4_20180627_1222.ota", "imageType": 16, "manufacturerCode": 4216, "sha512": "477be966bee03ccaa28f7d3994f259d7833db7fea82c9e82d7136dabfa9967da1503914d73f4972f60c9fba423a89f99473468ec9b5e5d62b3139c55f7a886b9", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/c0f1f26c-fe08-400d-ba6a-b04e8dd77413/it600HW_009A50A4.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6EV1_005F004F_20180228_1806.ota", "fileVersion": 6225999, "fileSize": 273742, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6EV1_005F004F_20180228_1806.ota", "imageType": 20, "manufacturerCode": 4216, "sha512": "cc6d5ec80ab6ae113243c7874beb5864ad87c6c13cc52e162387b1c5c4eca17f56d0e073ef7222ac7d3b5d05e413e058183db94062faac8f33d89a11f730945a", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/4d6f705a-b2c5-4a7c-b1e3-e7f7480dd31a/it600TRV_005F004F.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAL6RS1_190612.ota", "fileVersion": 538510866, "fileSize": 189950, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAL6RS1_190612.ota", "imageType": 76, "manufacturerCode": 4216, "sha512": "f14f6895ebf681b0345613c591cff0972b5a9898fc5f5006b1999431706a875e989a960e757426ad5ef7818255eba3686e0274ec3d561c71c9e17dc0df75a2d5", "otaHeaderString": "EBL RollerShutter\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/aa8c3733-72bb-4bcf-8500-1a93bfde2fc2/RS600_20190612.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAR70WA_20231008_V72.ota", "fileVersion": 72, "fileSize": 432526, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAR70WA_20231008_V72.ota", "imageType": 123, "manufacturerCode": 4216, "sha512": "480bb667a5345da8cf057b3952f305b8a442c4bd6be952935faa4d37be1bfb0b17d71273f83fef53bfc676e1cccb7d0f60225debadd38886dfb25bd63c49d582", "otaHeaderString": "SQ610\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/1b0ed51a-5a02-423c-8e93-fc124110546c/SQ610NH_00000048.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAR70ZA_20231008_V70.ota", "fileVersion": 70, "fileSize": 438346, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAR70ZA_20231008_V70.ota", "imageType": 122, "manufacturerCode": 4216, "sha512": "bac59af2a8daefae4a4cff84e8721872e4847063290ac315e2998a479bfdc207e3e25dfc7d0127b2c2b2ba11588676c56ff21c8b73159681e96cfcde29017310", "otaHeaderString": "SQ610RF\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/357da06e-3c53-41fe-9d6c-e75b67f46c15/SQ610RFNH_00000046.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU20T1_0064004E_20211113_1334.ota", "fileVersion": 6553678, "fileSize": 418198, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU20T1_0064004E_20211113_1334.ota", "imageType": 40, "manufacturerCode": 4216, "sha512": "08fa85778408631612bc2ea0df6ad18cd1cba530c5b10ab387743daad00cbda41eb7304ee4050714c76c48c563259ee3324687aaa0f79be9e6480092cc9c36dc", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/4737e511-6fbc-465a-9581-2610d31c71aa/ST898ZB_0064004E.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU21T1_00270009_20230108_1655.ota", "fileVersion": 2555913, "fileSize": 342750, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU21T1_00270009_20230108_1655.ota", "imageType": 82, "manufacturerCode": 4216, "sha512": "eea5e7dd0b6c88e21c6aca8ff68d2dbabd864cd5bdd32432c39a80854c5dc232ea94575861f0f7b24334a5bd5e324049f37bbb808c106bd58d1d656ac9c6ffaa", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/6ed08ad1-9a09-47cc-8b0d-40cf4709c853/ST899ZB_00270009.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU22T1_00640050_20211113_1337.ota", "fileVersion": 6553680, "fileSize": 418198, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU22T1_00640050_20211113_1337.ota", "imageType": 89, "manufacturerCode": 4216, "sha512": "f601e84bb7d831faba15c5e252529401f70c87a6bb9e8f5cd3fe84c2daea0bc187d9c9c44e4fc7ff4d0930d09506b697c7f9e8911dad72e9a85dca0752314361", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/88179668-8112-4ce0-af1b-266a1d8adc08/ST898ZBR_00640050.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU2AD1_170211.ota", "fileVersion": 538378769, "fileSize": 168704, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU2AD1_170211.ota", "imageType": 36, "manufacturerCode": 4216, "sha512": "78c13fb0ba5350b03beb33a704ad0abbc4c61c54613989747a301571607b60f78e1580417c4acd765b87953d451a5d64297f89d9d9ee167d43cab561813e7812", "otaHeaderString": "SAU2AD1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/4a1d7cf3-75b5-46eb-93bc-f38dc1c0f8c8/SS881ZB_20170211.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU2AG1-ZC_20240531.ota", "fileVersion": 539231537, "fileSize": 211906, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU2AG1-ZC_20240531.ota", "imageType": 21, "manufacturerCode": 4216, "sha512": "af72dc9e4e07649550673c86c2491a9fa6b4e0ad0930ea1fcabfb036dc1f39134673b2249f6361a4f128092feb85a50c4e305c3d2413b1fafc8d7158c59d920e", "otaHeaderString": "SAU2AG1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/firmware/UG-SYS/UG600/ZC/SAU2AG1-ZC_20240531.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU2BD1_170206.ota", "fileVersion": 538378758, "fileSize": 168804, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU2BD1_170206.ota", "imageType": 32, "manufacturerCode": 4216, "sha512": "be85661199e446f335f47eb0c273078eaed939de0f3a6244b2b7984c82468c42c96b926e4383b674aff67a733d26f9618b82073d5d472965582b3ffa20f75e81", "otaHeaderString": "SAU2BD1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/dedf71b6-9da3-4c4c-aa5b-bd5b29253ada/SS882ZB_20170206.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU2DA1_EFR32_APP_V20200508.ota", "fileVersion": 538969352, "fileSize": 250510, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU2DA1_EFR32_APP_V20200508.ota", "imageType": 60, "manufacturerCode": 4216, "sha512": "1f12058f8c138d8858a48cec80493f59689c51d1f7f3bc1ea01d29c8723de135e8e9e6b6ab9e537c1db8ed35bfcc5430af97916acf33f57eaf560ccccb8ecf2e", "otaHeaderString": "EBL ZigBeeHabiTemperatureSensor\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/d4213f30-3457-4b53-b68a-7fd929ff650a/PS600_20200508.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU2WB1_180918.ota", "fileVersion": 538446104, "fileSize": 239826, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU2WB1_180918.ota", "imageType": 71, "manufacturerCode": 4216, "sha512": "e52ee1d3800c0a93c10420545fa1e3cde5ecd01e027642671de06b40953f115dfcc1e61b23f78cbd7d712dfb872781e018a0a2b328b1950b8b34018cdbe22b25", "otaHeaderString": "EBL WaterBugSensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/bcf0af73-83ef-4140-99ab-b382c6464b50/SS901ZB_20180918.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU51R1_20190619.ota", "fileVersion": 538510873, "fileSize": 184638, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU51R1_20190619.ota", "imageType": 77, "manufacturerCode": 4216, "sha512": "97130e20edc3d8f3db63c07a2562b9318422733f6399500b6fcf3d01b6dedb4c80444a05ab50fb0511a351d9b720ec67df1bbe2c6448d19ead3205eddad43db9", "otaHeaderString": "EBL SmartRelay_US\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/0ed65137-2c58-4ebb-970c-df456d697cb3/SC824ZB_20190619.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU61T1_00310012_20231120_1046.ota", "fileVersion": 3211282, "fileSize": 372518, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU61T1_00310012_20231120_1046.ota", "imageType": 59, "manufacturerCode": 4216, "sha512": "3d2cbc54ecbae7e9eafdd2d74a0f1b58f0bdcb0252461c26c83ad1c053587afd234d55497a2c6122f182f33f90929397977813ddd4b80465f135287ed5bbcb1b", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/firmware/UG-SYS/ST100/2023-1121/ST100ZB_00310012.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU62C1_00310023_20231121_0942.ota", "fileVersion": 3211299, "fileSize": 420246, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU62C1_00310023_20231121_0942.ota", "imageType": 67, "manufacturerCode": 4216, "sha512": "1330a948f53a81e239796b75dfb29df1f7cfcfcde7bf6b0639275d837ce95d0a876c1eb1fb9e276cd190c52029064459ab23dd518f5c84b9bdecec3d238ffbfc", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/debug/OTA_Test/SAU62X1/SC102ZB_00310023.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SAU62T1_0031001C_20231121_0940.ota", "fileVersion": 3211292, "fileSize": 348726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SAU62T1_0031001C_20231121_0940.ota", "imageType": 68, "manufacturerCode": 4216, "sha512": "4f32a467f8b4199020e2300e68bb5f6d3463c66571128b0f2df03abb1227dbe1f9c63080fad998cc0bef488864962b16a1e990499b1160d6b5d7a227516a32cf", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/debug/OTA_Test/SAU62X1/ST103ZB_0031001C.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SB600_20170209.ota", "fileVersion": 538378761, "fileSize": 173288, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SB600_20170209.ota", "imageType": 46, "manufacturerCode": 4216, "sha512": "8d9a2449b4cd5dd8412fe3b4c7cf14371a1fddd51d95e8f971c5bb97a55b3ae4b2f06f4d9d987dcc23d75b18cb73ddff284931657a8b1de73fa622ac6f2c3bd8", "otaHeaderString": "image build\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/16b82cfc-b764-4e56-a869-857ae5036478/SB600_20170209.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SC904ZB_18012801.ota", "fileVersion": 402728961, "fileSize": 180216, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SC904ZB_18012801.ota", "imageType": 65, "manufacturerCode": 4216, "sha512": "f18234eb5ed1875f50d682791cfb9a58648ba6624db3497930482aa6f8cb670bf77aff7c1794d8f1051574790e517cedbe95a586505dbd14931ad6bd80987dc3", "otaHeaderString": "SAU2BW1_18012801 ota image file\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/93151e6f-a739-4b47-b0e6-bfef7b4c0ad2/SC904ZB_18012801.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SLG2CF1_20180116_v00160501.ota", "fileVersion": 1443073, "fileSize": 180862, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SLG2CF1_20180116_v00160501.ota", "imageType": 56, "manufacturerCode": 4216, "sha512": "b74f8bbae15764b5c4562f8f44154641f76092d20d6bce30358a0adc4627f04d9c72263d12a8da478a1f1fc6dab0c1f2933e25075db26dd40ac9d4d746275b38", "otaHeaderString": "SLG2CF1_20180116_v00160501 ota f", "originalUrl": "http://eu.salusconnect.io/download/firmware/2366de25-7629-4968-9e73-9f154549e5db/HTR-RF(20)_00160501.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SLG5CF1_00370009.ota", "fileVersion": 3604489, "fileSize": 429230, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SLG5CF1_00370009.ota", "imageType": 58, "manufacturerCode": 4216, "sha512": "033f0137aedc765cad80ada61364801d7fad9f40ed4ca5855dddf5a471609e69bd100d3b8ec46fd38ff864313e829dce69cc57b8acaa663affc9258873a197cb", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/38abc21e-f0dd-4aee-8659-cc5c35595711/HTRP-RF(50)_00370009.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SND2DB1_001F0019_20230108_0849.ota", "fileVersion": 2031641, "fileSize": 403270, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SND2DB1_001F0019_20230108_0849.ota", "imageType": 64, "manufacturerCode": 4216, "sha512": "1c9730e93b9eaf0ed382abe0ee5dd08135e9df0179142b00f76ac7eb11556802b2aa7314daa5807a1b842756f4aefdb89596b896ab8519670d05d9464530e468", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/68914d00-d860-474a-8d28-97b8f7f4a665/NTVS41_001F0019.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SQ605RF_V2.3_20201119.ota", "fileVersion": 23, "fileSize": 233262, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SQ605RF_V2.3_20201119.ota", "imageType": 95, "manufacturerCode": 4216, "sha512": "68e8a74f6fb9ce8d26ab0e598966e13054b786d013fb301f4018907d6263424bb1b1cc03970c50ae85f129485703746c68cf0825ab044122329e6b58b3359fdb", "otaHeaderString": "EBL DialThermostat\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/f021e8a8-06f0-4a4f-8b13-bd39adcc80e3/AJSQ605RF_00000017.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SQ610RF_APP_V3.8_20221212.ota", "fileVersion": 38, "fileSize": 446454, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SQ610RF_APP_V3.8_20221212.ota", "imageType": 78, "manufacturerCode": 4216, "sha512": "ab589119b72cc450cad75be72ba2457273c5d60a28b0a0a6a3d699e2b4b650d22ee94bded49176f9b57bb75e3388feb5d3d9834a2978150fbcc1185430027ccb", "otaHeaderString": "EBL SQT\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/bb3683be-6c01-49fb-91fe-beadc65561fc/SQ610RF_00000026.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "SQ610_APP_V3.5_20230403.ota", "fileVersion": 35, "fileSize": 450442, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/SQ610_APP_V3.5_20230403.ota", "imageType": 79, "manufacturerCode": 4216, "sha512": "0996fc7f14ecc4b5b3455fb30a1b5ba853f08f32a8c9963a9bd1578349150ff16c528b349870cc0003cf04cf174351977c48f5a78298837214368a822fa99b11", "otaHeaderString": "SQ610\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/9e996b03-ef2d-423b-b420-0ce9531876c1/SQ610_00000023.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "ST880ZB_EM357V92_MCUV78.ota", "fileVersion": 811335758, "fileSize": 536134, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/ST880ZB_EM357V92_MCUV78.ota", "imageType": 5, "manufacturerCode": 4216, "sha512": "242ba2399de1be3f200cc9408cc6d1e94eb3b3928fea1687354bb5873897c3b077dabacca624e588436e9e50cc9ee34811be3749a3fa6142341a1dedc84ec986", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/125434a7-b9ad-454d-ae82-5832f2ad0f71/ST880ZB_305C004E.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "Salus_Smart_Plug_SX885ZB_21030500.ota", "fileVersion": 553846016, "fileSize": 213220, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/Salus_Smart_Plug_SX885ZB_21030500.ota", "imageType": 31, "manufacturerCode": 4216, "sha512": "4123beb0d9b83f79636168bf153a247a30f013fba1bb8339de937702af81440435f79e182d71fad0316b41df02e2e5517e92f43182c4c18a3f05c46302d53519", "otaHeaderString": "Salus Smart Plug OTA File\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/8315dcc4-0cd8-49b6-bee2-cf0537776b6a/SX885ZB_21030500.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "StreamTRV_00910040_20210928_1427.ota", "fileVersion": 9502784, "fileSize": 280878, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/StreamTRV_00910040_20210928_1427.ota", "imageType": 66, "manufacturerCode": 4216, "sha512": "d5c12f065dc55ea3c63ed4230e03f62a9443539d2814115715ffbc2d214d937e8b977a3540900e2fa50cb55d5686de4e7f68f9aee55faeadb943ebd0f3047278", "otaHeaderString": "OTA Image File\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/3261c5ca-9817-42d2-a30b-a52e6c3a87cf/AVA10M30RF_00910040.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "WindowSensor_20240103.ota", "fileVersion": 539230467, "fileSize": 264290, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/WindowSensor_20240103.ota", "imageType": 63, "manufacturerCode": 4216, "sha512": "64a5fc3fe95770ca5c8a947a9a19665389374296a055671cdd9fcf1ac98d5f25cc287fc6196c16e1c7fdf69771c67f01bb6bb1d8baf34e05e41667a723906e19", "otaHeaderString": "EBL WindowSensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/firmware/UG-SYS/SW600/2024-0103/NTSW600_20240103.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "it600WCNH_00090005.ota", "fileVersion": 589829, "fileSize": 329540, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/it600WCNH_00090005.ota", "imageType": 132, "manufacturerCode": 4216, "sha512": "3d3f023faf6408cf1cab1f3ca6ffceadd590e0bc8447563362de5167346ce00c9c839866290685e4184c854ffce55490fe9431fea199ef8c224d3d1847eb3fda", "otaHeaderString": "EBL SAL6DIA\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://ec2-18-194-20-66.eu-central-1.compute.amazonaws.com/firmware/UG-SYS/WC/NH/2023-0527/it600WCNH_00090005.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "sau2aw1_V19121102.ota", "fileVersion": 420614402, "fileSize": 172484, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/SalusControls/sau2aw1_V19121102.ota", "imageType": 43, "manufacturerCode": 4216, "sha512": "9e3150426fbe269325e0f69be3f85825e36f2eceed7e7208c8cdf834e91c400e6e2d7265a27400d64f94c169be6c5822d4666ff88737e2420f4a0b961a1d7807", "otaHeaderString": "Encrypted EBL sau2aw1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://eu.salusconnect.io/download/firmware/d4c48f8b-4165-4691-ac2e-3c7ade8b776d/SC906ZB_19121102.tar.gz", "manufacturerName": [ "SalusControls" ] }, { "fileName": "1555679540244_RDS2017009_E11_U2E_V42_20190418_release.ota", "fileVersion": 42, "fileSize": 134334, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/1555679540244_RDS2017009_E11_U2E_V42_20190418_release.ota", "imageType": 1017, "manufacturerCode": 4448, "sha512": "4bbcd1c236161a3bb2f50eaa633205087dbbc6ff28e3136a8280e7822e4cd3662b4fd072e08ad2f245da59cd2fef2a52ff3cbefdbc3bf80bfc5b7cfabbcbc26b", "otaHeaderString": "EBL E11_U2E_1017\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1586412538195_RDS2017007_E11-N1EA_V0.0.71_20200311_release.ota", "fileVersion": 71, "fileSize": 142014, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/1586412538195_RDS2017007_E11-N1EA_V0.0.71_20200311_release.ota", "imageType": 1004, "manufacturerCode": 4448, "sha512": "c60da6dd092997e06ea81d543ad5097fa4b65e00f1ee4f9faf9c7fec36c10b1e144a0a039f4ed291c31fd4e7437041faff0bcc61cb8b9d13a22c465aa8d813fa", "otaHeaderString": "EBL rgb_lamp\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1594189489604_RDS2017039_E12_N1E_V0.0.30_20200630_SVN396.ota", "fileVersion": 30, "fileSize": 140414, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/1594189489604_RDS2017039_E12_N1E_V0.0.30_20200630_SVN396.ota", "imageType": 1016, "manufacturerCode": 4448, "sha512": "deca704c355a895b9d4c4b292e581866d7d0198ce2ecb63922465312e2a0062d9ca7596555d8becb6d7cc23f2be72b0081ce74437f6c6f6bdc608b439c6b0362", "otaHeaderString": "EBL E12_N1E_1016\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1594881275531_RDSE2020003C0511_E1G-G8E_05m_10m_V0.0.14_20200711_release.ota", "fileVersion": 14, "fileSize": 142654, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/1594881275531_RDSE2020003C0511_E1G-G8E_05m_10m_V0.0.14_20200711_release.ota", "imageType": 5, "manufacturerCode": 4448, "sha512": "2942ab766c6542094a9564f2f8565f602b0440fa28b83b8f98addc8f69d8f99a97a08ea6381b69cc3ffd0a62add6feed1266742f0ea5e35bc87cfa425140a20e", "otaHeaderString": "EBL RGB_LampTape\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "RDL2016091_1_E11-G13_V0.0.9_20170921_release.ota", "fileVersion": 9, "fileSize": 116478, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/RDL2016091_1_E11-G13_V0.0.9_20170921_release.ota", "imageType": 3, "manufacturerCode": 4448, "sha512": "a925acbe2bd50f597a04ceda2810a60e651a1c2ac0369f5d1aadca89c15d6de763c679f7b6549a8001b03d3e9ac9319aca3983911e8f5a81b36420eac070660f", "otaHeaderString": "EBL ElementClassic_E11GX3\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "RDS2014011_Z01-A19_V0.0.46_20171028_release.ota", "fileVersion": 46, "fileSize": 121086, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/RDS2014011_Z01-A19_V0.0.46_20171028_release.ota", "imageType": 1, "manufacturerCode": 4448, "sha512": "656319d7933e0bcef63a76698d42c5067844503788fd68443149965e03ecd6cce7ad4c08fddadaafff489b0b4d183738feeba3aff8633030d8501ac60c750937", "otaHeaderString": "EBL Z01_A19_HV2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "RDS2017028_E1C-NB6_V0.0.22_20180314.ota", "fileVersion": 22, "fileSize": 136062, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sengled/RDS2017028_E1C-NB6_V0.0.22_20180314.ota", "imageType": 2100, "manufacturerCode": 4448, "sha512": "6a55736c9e858898bef68ad794c77290698227a3eb0db325f9cd750ae7a801ccd7780fd8282a60a7a40478c324e4e7c9b83e4b67cd85032926d80daaba93c1eb", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "s40zbLite_v1.0.3.ota", "fileVersion": 4099, "fileSize": 156778, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/s40zbLite_v1.0.3.ota", "imageType": 65, "manufacturerCode": 4742, "sha512": "38bc090584b844a0fe75900d6c0efb34a57d776b8d549fa3938cda437de4a454885f557a43ba51c7fa524f79de279a17c9da1d6c656c351f82b6eb46db1cf2b7", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "snzb-05p_v1.0.2.ota", "fileVersion": 4098, "fileSize": 267632, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-05p_v1.0.2.ota", "imageType": 2059, "manufacturerCode": 4742, "sha512": "bb64ae86d9ac2dc398d4520f97554515990ecef5568eebdaa7cdb2be19fe2471b35f53583adc8d8e9e70189a90add27556581bca444690155972ffa6b341b81e", "otaHeaderString": "ota-file-test\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "snzb-06p_v1.0.6.ota", "fileVersion": 4102, "fileSize": 258306, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-06p_v1.0.6.ota", "imageType": 2060, "manufacturerCode": 4742, "sha512": "e722a7058439abba22f2d392f9308c0eb22dcb89ad32c3409983b74a28724713e9709cd2b6b61622dac57487227d2f902dd38291b9116a87b9fc52981669cf46", "otaHeaderString": "vers: ZigBee:00001006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zbmicro_v1.0.5.ota", "fileVersion": 4101, "fileSize": 269220, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/zbmicro_v1.0.5.ota", "imageType": 7, "manufacturerCode": 4742, "sha512": "dd1d67ba721740c3eec4da80a2396c6fa09b981bcf2ed9fc712790e45ec9fb392d58d33e36a1bc19899a77a3421a206c1c9c2f7512ed3c8c571e1344f7d996d8", "otaHeaderString": "ota-file-test\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zbmini-l_v1.1.1.ota", "fileVersion": 4353, "fileSize": 131086, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/zbmini-l_v1.1.1.ota", "imageType": 1, "manufacturerCode": 4742, "sha512": "b136d2656ea1197ae84f5e9c2244a9d9697bccab2c448324d9176bac791aff0161afc73c8a1574bbd51c0ea2e318840ca58a0a99da32669d3e5fc776bdf3473a", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zbminil2_v1.0.14.ota", "fileVersion": 4110, "fileSize": 259018, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/zbminil2_v1.0.14.ota", "imageType": 4, "manufacturerCode": 4742, "sha512": "c80d29e84e3019a84f7a3bdb3e84a3b462e2516c7de5120cdbb68a0e474fcb8b1c5d8eb79b204ad2d9c5af2bed4e544588b192b7884d493a11f82d4ac8625177", "otaHeaderString": "ota-file-test\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zbminir2_v1.0.4.ota", "fileVersion": 4100, "fileSize": 276694, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/zbminir2_v1.0.4.ota", "imageType": 8, "manufacturerCode": 4742, "sha512": "66b781d8aa2bf5f6cdae2679382bce99b73887f1588008c7d43b9a4ff5ba284463206354b4b8dc01b0a916887c05619350b73ae68c4d1db7a1c76afc1d9975f3", "otaHeaderString": "ota-file-test\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zbswv_v1.0.4.ota", "fileVersion": 4100, "fileSize": 272354, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/zbswv_v1.0.4.ota", "imageType": 8202, "manufacturerCode": 4742, "sha512": "49ef75a2d1dd6f706d2182d6cd88c6d98c0aa1e70fdb37e739e6e970266884579794faab361f976aed84c6f9c3c547f7d32f28940f16b72e3f7e454bd5001b1b", "otaHeaderString": "ota-file-test\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "wb_msw3_0_061.ota", "fileVersion": 61, "fileSize": 412542, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sprut.device/wb_msw3_0_061.ota", "imageType": 1, "manufacturerCode": 26214, "sha512": "9cca5fd3f7b910f26cb2fb8c65bd6b62fc421fdcbb1579ed246a93b876337e8e356ed47724e0a023582d51da85dde38a8a991b64f0051dadd81607c9ff696672", "otaHeaderString": "EBL zb_wb_msw\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "wb_msw3_A_061.ota", "fileVersion": 61, "fileSize": 412538, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sprut.device/wb_msw3_A_061.ota", "imageType": 2, "manufacturerCode": 26214, "sha512": "a6d952c38effabb14419080d2989b218e41b1eff337967708bfe1f1a62054c8c465f9e6dca945e90c46bc7b8185d122a08a504619934619dbaedaccb9bbd4be9", "otaHeaderString": "EBL zb_wb_msw\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zb_wb_msw4_mg21_065.ota", "fileVersion": 65, "fileSize": 270102, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sprut.device/zb_wb_msw4_mg21_065.ota", "imageType": 15, "manufacturerCode": 26214, "sha512": "4701a4296faa914e00a4211f14cdac4bde01a1ba8708049a3ce393471ef33a22a1abb7015b9cb6a5ffa5b7e77879e938d0133445476478af40fdc45ea80c1c3a", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "zb_wb_msw4_mgm21_065.ota", "fileVersion": 65, "fileSize": 271050, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sprut.device/zb_wb_msw4_mgm21_065.ota", "imageType": 13, "manufacturerCode": 26214, "sha512": "ff1df29fbfcf62cbce4608243600598fcf1066ed573efcf1d40a1717a0c06ea1e0d59f606e3d03429ef769776f29f6f01111c662561fe51fae2f9b4844d88cc7", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1141-0208-01143001-ZMHOC401N.zigbee", "fileVersion": 18100225, "fileSize": 127922, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Telink/1141-0208-01143001-ZMHOC401N.zigbee", "imageType": 520, "manufacturerCode": 4417, "sha512": "3976e5a67ec50a54d05dc2b5ed69f94fe1bd989663e8855511cacf00c4472cf904a2f51d63909374258fc7a2943e6b412a3c47cd5cb7282690c7a5c4d7c24828", "otaHeaderString": "Telink OTA BLE device\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1141-0208-01233001-ZMHOC401N.zigbee", "fileVersion": 19083265, "fileSize": 128690, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Telink/1141-0208-01233001-ZMHOC401N.zigbee", "imageType": 520, "manufacturerCode": 4417, "sha512": "4db812502a8043c85eaa86f53dfc0a8857dd5cde23e5f55dd84bf4a6072f5b37f9de6a0cf4fca4b1b0c74ae91e5d9d92f3a7edcd6c21dde6923d81d2e7d56f3e", "otaHeaderString": "ZigbeeTLc OTA\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "MHO-C401N" }, { "fileName": "1663234985-oem_ztu_dimmer1_klt_OTA_1.0.6.bin", "fileVersion": 70, "fileSize": 256770, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Telink/1663234985-oem_ztu_dimmer1_klt_OTA_1.0.6.bin", "imageType": 54179, "manufacturerCode": 4417, "sha512": "0d2b6be4f0e8b0efdc2f581aee73055a4b3f05a26888e2c3fd0527f81a2f01af445001a91935f2c95ecbda9f83254ef80a9a51c18e592ac9ed00daedb686cbc8", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "manufacturerName": [ "_TZ3210_ngqk6jia" ] }, { "fileName": "1686137326-oem_zg_tl8258_plug_OTA_1.0.14.bin", "fileVersion": 78, "fileSize": 309954, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Telink/1686137326-oem_zg_tl8258_plug_OTA_1.0.14.bin", "imageType": 54179, "manufacturerCode": 4417, "sha512": "6a0861ee46e659b2b4167aa66b958064b57dbbf25c1dc0dcd848b455f012db5f6d0eb2f9099c3905c9cbfcb0ea75eacb12493a1d31e8fcbb896903a31d5cb646", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "manufacturerName": [ "_TZ3000_cjrngdr3" ] }, { "fileName": "1718263020-oem_zg_tl8258_plug_OTA_1.1.0.bin", "fileVersion": 80, "fileSize": 310402, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Telink/1718263020-oem_zg_tl8258_plug_OTA_1.1.0.bin", "imageType": 54179, "manufacturerCode": 4417, "sha512": "c541878e792620ad16cc70967e0458c03de21876ad8ed3c3b1eb90c8ecf7bcdfe6bfe711460a5888bb033def141e7dd22d78fe24c44a4e0b9707afb473a2d2a2", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "manufacturerName": [ "_TZ3000_w0qqde0g", "_TZ3000_dksbtrzs", "_TZ3000_fukaa7nc" ] }, { "fileName": "TERNCY-SD01_v46.OTA", "fileVersion": 26, "fileSize": 131522, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Terncy/TERNCY-SD01_v46.OTA", "imageType": 0, "manufacturerCode": 4648, "sha512": "71d600af15976934d28ae0be550a3641967c6b54045f110071ddcf69729e7586c490b723e92eac2825156c07a3126244189af2dd45d5d60a565cd2aa23ad3a4d", "otaHeaderString": "TERNCY-SD01\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Button_PROD_OTA_V35_v1.00.35.ota", "fileVersion": 35, "fileSize": 129698, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Button_PROD_OTA_V35_v1.00.35.ota", "imageType": 54184, "manufacturerCode": 4659, "sha512": "431aa68788f8e4150ed7b80f0655e234ab34e06afa1b94dce287625991845496d6191578bf335ef56a0c13853616e7ce6feb00b3a2604d60d11384d7dc69f28a", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/Button_PROD_OTA_V35_v1.00.35.ota" }, { "fileName": "Door_Sensor_PROD_OTA_V63_v1.00.63.ota", "fileVersion": 63, "fileSize": 131682, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Door_Sensor_PROD_OTA_V63_v1.00.63.ota", "imageType": 54178, "manufacturerCode": 4659, "sha512": "ef7dd6575b60532af41b63957ccfe750fd8ab188d3c896d2c005bdb19b0c0df2ddf3d68100bf887449ad9085cca410c1bfc446c2aa632ee13d5bb2fb62761440", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/Door_Sensor_PROD_OTA_V63_v1.00.63.ota" }, { "fileName": "FW_ha_v1.00.82.ota", "fileVersion": 82, "fileSize": 297858, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/FW_ha_v1.00.82.ota", "imageType": 0, "manufacturerCode": 4877, "sha512": "ada3886bc02da299648240f71c6d44950937cb6ab29a9bc115a255eeda0d4b3cf061772a60f186361f4c9afddbf90882a28f83370478a561b31eeebf9180e4f2", "otaHeaderString": "test\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/FW_ha_v1.00.82.ota" }, { "fileName": "Motion_Sensor_PROD_OTA_V79_v1.00.79.ota", "fileVersion": 79, "fileSize": 132610, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Motion_Sensor_PROD_OTA_V79_v1.00.79.ota", "imageType": 54177, "manufacturerCode": 4659, "sha512": "e6baeb11feef7c8d0fb2e47bee2c2fd79b155af2a0d68e445b3dd6d55a9dde15fa81517c11f72190d691e20eb0ec4f090235f7697f9e2aebd7a73a5d8ba88803", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/Motion_Sensor_PROD_OTA_V79_v1.00.79.ota" }, { "fileName": "SmartCurtain_PROD_OTA_V76_v1.00.76.ota", "fileVersion": 76, "fileSize": 136738, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartCurtain_PROD_OTA_V76_v1.00.76.ota", "imageType": 54183, "manufacturerCode": 4659, "sha512": "4e071cbdb4e440d37c820ecb844571e6acd0d7c6d3f9c06cbb22dde2f0d2387d5cee0f67ed294333ba5c6b31addd2d1772a3cc2aaf0de582b28fefe3ca42ff8b", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/SmartCurtain_PROD_OTA_V76_v1.00.76.ota" }, { "fileName": "SmartPlug_Zigbee_PROD_OTA_V92_v1.00.92.ota", "fileVersion": 268513372, "fileSize": 197458, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V92_v1.00.92.ota", "imageType": 54182, "manufacturerCode": 4659, "sha512": "99c840f67606ddbdb872768e1ec015b023904d40b0de574d63cae165a76b73fa01d56f01d0e3d43a123a404c2deb713914556c86ada709e2b2a14c061a2c8b2c", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "SmartSwitchGen3_PROD_OTA_V30_v1.00.30.ota", "fileVersion": 30, "fileSize": 135218, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartSwitchGen3_PROD_OTA_V30_v1.00.30.ota", "imageType": 54181, "manufacturerCode": 4659, "sha512": "02dbf23109067616e3bd2cc8b1efc820c4fcf48d56cc3ecb34117d1519d935a886845db73a7d59b56c65cb0a6e4b43c352311f2af24c8c1ddf15164baa438132", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Soil_Moisture_Sensor_PROD_OTA_V31_v1.00.31.ota", "fileVersion": 31, "fileSize": 139026, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Soil_Moisture_Sensor_PROD_OTA_V31_v1.00.31.ota", "imageType": 54191, "manufacturerCode": 5127, "sha512": "18aa9d1118fc0a1bf39a037036c9f6229a789a4c4e20f8403838ede9aa771ccfacbf0eff8ca1b52b265f564a4f555613264556f9e1a1884a6f1fd932dc4e4478", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "TRTL_ThermalSensor_PROD_OTA_V37_v1.00.37.ota", "fileVersion": 37, "fileSize": 129090, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/TRTL_ThermalSensor_PROD_OTA_V37_v1.00.37.ota", "imageType": 54185, "manufacturerCode": 4659, "sha512": "b78cc4b15a1fb1ebedc04143bd9001265df6f380953cec30d50bdd6ed2da062fb260e3a981f1dc8cee0536076c656c033b31327dfc27702c9735fa3f1496bb81", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/TRTL_ThermalSensor_PROD_OTA_V37_v1.00.37.ota" }, { "fileName": "ThermalLiteSensor_PROD_OTA_V29_1.00.29.ota", "fileVersion": 29, "fileSize": 256174, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/ThermalLiteSensor_PROD_OTA_V29_1.00.29.ota", "imageType": 54185, "manufacturerCode": 5127, "sha512": "3d450453147e414e9998cb000183a22ac6fca656a1a3e11e37b31d7c4fa1ec3f301b28d0b144301630ec33aa1f81afda56d6abfb4191f78d04cd7b9ba27b87b7", "otaHeaderString": "temp_humi_sensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/ThermalLiteSensor_PROD_OTA_V29_1.00.29.ota" }, { "fileName": "Water_Leak_Sensor_PROD_OTA_V66_v1.00.66.ota", "fileVersion": 66, "fileSize": 133234, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Water_Leak_Sensor_PROD_OTA_V66_v1.00.66.ota", "imageType": 54179, "manufacturerCode": 4659, "sha512": "daba6b3ad69bb070aa46ddae95bf8fa4a3eed151063b9e14f07a5f8937baac5edc922f99b0f6207b9ece62ee65a4ba896721383947c552f94a82c89d4bd04f1d", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://tr-zha.s3.amazonaws.com/z2m_firmware/Water_Leak_Sensor_PROD_OTA_V66_v1.00.66.ota" }, { "fileName": "Zigbee_A19_Bulb_OTA_V56_1.00.56.ota", "fileVersion": 56, "fileSize": 283666, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Zigbee_A19_Bulb_OTA_V56_1.00.56.ota", "imageType": 54188, "manufacturerCode": 5127, "sha512": "4481f292368d350836798cd3c97e226d111ec31e994729896890aa87dbd75a574255600cd87ad8b117a8f3a6ce1dde3634ad9210db94e453d66e50cfe4e8db08", "otaHeaderString": "temp_humi_sensor\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1615190575-si32_zg_uart_connect_sleep_ZS5_ty_OTA_1.1.7.bin", "fileVersion": 87, "fileSize": 234174, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/1615190575-si32_zg_uart_connect_sleep_ZS5_ty_OTA_1.1.7.bin", "imageType": 5634, "manufacturerCode": 4098, "sha512": "1113486a67403d922fc4a462b7477e0a0986fa8862a89b74ab8c5eac7b4ecb31c30f1da9ae1bbc1d6d50ae1fed66d38c37a1834ab71fd384c0555002783aba13", "otaHeaderString": "EBL sdk_route\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "manufacturerName": [ "_TZE200_kds0pmmv", "_TZE200_ckud7u2l", "_TZE200_cwnjrr72", "_TZE200_ywdxldoj", "_TZE200_cpmgn2cf" ] }, { "fileName": "1622438235e5764b7a861.zigbee", "fileVersion": 33753087, "fileSize": 340031, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/1622438235e5764b7a861.zigbee", "imageType": 55, "manufacturerCode": 4190, "sha512": "18b1d8028a3de3cf5c28fb442779daa74d5d13bb1f782021db03b945bd02aaa85cf20de17cd23f3d9fbf73224c68a68caa2ad3527ce20913844668161f653c70", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/ay1557480848074dO17I/1622438235e5764b7a861.zigbee" }, { "fileName": "1662545193-oem_zg_tl8258_plug_OTA_3.0.0.bin", "fileVersion": 192, "fileSize": 307682, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/1662545193-oem_zg_tl8258_plug_OTA_3.0.0.bin", "imageType": 54179, "manufacturerCode": 4417, "sha512": "01939ca4fc790432d2c233e19b2440c1e0248d2ce85c9299e0b88928cb2341de675350ac7b78187a25f06a2768f93db0a17c4ba950b60c82c072e0c0833cfcfb", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/20220907/1662545193-oem_zg_tl8258_plug_OTA_3.0.0.bin", "modelId": "TS011F" }, { "fileName": "1662693814-oem_mg21_zg_nh_win_cover_relay_OTA_1.0.7.bin", "fileVersion": 71, "fileSize": 222762, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/1662693814-oem_mg21_zg_nh_win_cover_relay_OTA_1.0.7.bin", "imageType": 5634, "manufacturerCode": 4098, "sha512": "41ebf9932d11708b81144232b2c3fccc7a58d76116be63785b978b91afb1180e32d8ba4f4c8f2f0960637f1662ec9ac28aa42b74e5ab57574000889671d2b8b9", "otaHeaderString": "EBL sdk_route\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "manufacturerName": [ "_TZ3000_1dd0d5yi" ] }, { "fileName": "16781822109542aac900a.zigbee", "fileVersion": 34212095, "fileSize": 337195, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/16781822109542aac900a.zigbee", "imageType": 53, "manufacturerCode": 4190, "sha512": "15188628b13c26b577aa4a9f82a4a411debc7125e19cb13d3d1612095c87c51b9f186e01b33a0f798c3a6875dc68c9f36a614372fe6523cda8e47b045a1a76ba", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/ay1557480848074dO17I/16781822109542aac900a.zigbee" }, { "fileName": "1678411825b98a1530a8c.zigbee", "fileVersion": 33884671, "fileSize": 337211, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/1678411825b98a1530a8c.zigbee", "imageType": 54, "manufacturerCode": 4190, "sha512": "50abcc9a6f6351a9bf45994c09b6681cfb255ce617ec299539a937b70fac16110824600269062c5a086b9749a29caf4efe5b3d694e91b04b099efa84b6ba356c", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/ay1557480848074dO17I/1678411825b98a1530a8c.zigbee" }, { "fileName": "16819629247ee0445a5f4.zigbee", "fileVersion": 33686015, "fileSize": 341010, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/16819629247ee0445a5f4.zigbee", "imageType": 79, "manufacturerCode": 4190, "sha512": "4b373c33ca742bdb85d9bd7665545be34e4e9660c94218f02c3754f2c9eb4129a428e51b4fe7ccab29f3ee53537b9301cc43b1dbf4c12dae4377025a23848eb8", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/ay1557480848074dO17I/16819629247ee0445a5f4.zigbee" }, { "fileName": "16844823767736d4b2cbc.ota", "fileVersion": 16845314, "fileSize": 230798, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/16844823767736d4b2cbc.ota", "imageType": 62, "manufacturerCode": 4190, "sha512": "95e5c61e2a683a901bc320f5af59330ed6b753808a3efffb8bb8a457c5360fd16403accb4e7f2736b418d3a5ad195df967d72a13ac5d4ffd1951abd05e1844e0", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/ay1557480848074dO17I/16844823767736d4b2cbc.ota" }, { "fileName": "16855062966c6d846535c.zigbee", "fileVersion": 34341631, "fileSize": 357015, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/16855062966c6d846535c.zigbee", "imageType": 13, "manufacturerCode": 4190, "sha512": "befd51ad8ebb145b7d41b5709ace1a6dfe2af9174c161bb7057db570698cd166f10807b9a418440051c03f53c09f41a1d19219be8d0bb95abee7068edc795773", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://images.tuyaeu.com/smart/firmware/upgrade/ay1557480848074dO17I/16855062966c6d846535c.zigbee" }, { "fileName": "ZB21S3_HZC_Dimmer1_ECO_1.01_20221021.ota", "fileVersion": 10, "fileSize": 287022, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/ZB21S3_HZC_Dimmer1_ECO_1.01_20221021.ota", "imageType": 44, "manufacturerCode": 4098, "sha512": "a663c437d505b34b08c6a049c999ea6af4ed99db9192f0f7ec8b284644d518f6dafbb889e648bc216581e1a05a918811594fdcc4af66ab1c1178205c895038c7", "otaHeaderString": "EBL Zigbee_Dimmer_1_Gang\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "ZPS_CS5490_039.ota", "fileVersion": 57, "fileSize": 155646, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/ZPS_CS5490_039.ota", "imageType": 24256, "manufacturerCode": 4098, "sha512": "69f0bd7ecf971546743b5422265a1713741df87a6c0344831ee09b938b751c948bf2251f2c76c0dff09d048b3ce785c3d7f07295afde9acb02255263d275322f", "otaHeaderString": "peanut Power Plug\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "home_control_as_hc_slm_1_00.02.zigbee", "fileVersion": 10205, "fileSize": 224634, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Tuya/home_control_as_hc_slm_1_00.02.zigbee", "imageType": 0, "manufacturerCode": 4098, "sha512": "17357f99b497d42ea28e6c0ec994fea250cbcd27c833e730876efdbef4f2764fb796500cc3ee532135f067634ae94c0b2bb6da59871a6c3a612fec668e350a2d", "otaHeaderString": "EBL HomeControl_DoorLock\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "HC-SLM-1" }, { "fileName": "007B-0141-00002867-good_image_PL_1_3_43.zigbee", "fileVersion": 10343, "fileSize": 408418, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/UHome/007B-0141-00002867-good_image_PL_1_3_43.zigbee", "imageType": 321, "manufacturerCode": 123, "sha512": "b27714b307f6e6ac8fb1c3dacf3ebda25afd7c36fe7cea72637aedd90c1164807035d51e00743069ef4bd165787feab4ce3c678808dddf109e5e76847b695843", "otaHeaderString": "\ngood_image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "modelId": "PEHPL0X" }, { "fileName": "007B-0141-010E0101-good_image.zigbee", "fileVersion": 17694977, "fileSize": 333712, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/UHome/007B-0141-010E0101-good_image.zigbee", "imageType": 321, "manufacturerCode": 123, "sha512": "254e4cbc885512a4dc03bd0c9d9d0a2f080c8f8a78dbf2b1e18872a7edfa6474fcefaf6a52f8fa78eec807e5b6c53c229825fdb4c714800bccabc9fe25c8f465", "otaHeaderString": "\ngood_image\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "10F2-7B01-0000-0006-0192020D-spo-fmd.ota1.zigbee", "fileVersion": 26345997, "fileSize": 263324, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B01-0000-0006-0192020D-spo-fmd.ota1.zigbee", "imageType": 31489, "manufacturerCode": 4338, "sha512": "0dee3908a98d434c56d624cee7c49e46315a7571b355b656454c5ad69a38f43ac8b3fce7a3b59b596f91ddb58d151dd6b329753156e8b5acf1d7eb55a50fd659", "otaHeaderString": "ubisys D1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B01-0000-0006-0192020D-spo-fmd.ota1.zigbee", "hardwareVersionMax": 6, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B02-0000-0001-0192020D-spo-fms.ota1.zigbee", "fileVersion": 26345997, "fileSize": 256474, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B02-0000-0001-0192020D-spo-fms.ota1.zigbee", "imageType": 31490, "manufacturerCode": 4338, "sha512": "20ff31830a17149b353db48afb9ccd356ac75dec04ea0241ef996918471f4b5bc786916caeae79ba75fffbebd6412f06361032a01fd675e85bb931a5fefd7bf3", "otaHeaderString": "ubisys S1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B02-0000-0001-0192020D-spo-fms.ota1.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B03-0000-0006-0191020D-spo-fms2.ota1.zigbee", "fileVersion": 26280461, "fileSize": 256202, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B03-0000-0006-0191020D-spo-fms2.ota1.zigbee", "imageType": 31491, "manufacturerCode": 4338, "sha512": "4bbab66842186f273c8d18d17dda73e6e3c56b519a77b60e994361bd607aad8cb6d1396bb2a63d97327d8e7d850ac38052870fa9f1d4b035db640accae0c7eaa", "otaHeaderString": "ubisys S2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B03-0000-0006-0191020D-spo-fms2.ota1.zigbee", "hardwareVersionMax": 6, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B04-0000-0007-0191020D-spo-fmsh.ota1.zigbee", "fileVersion": 26280461, "fileSize": 261134, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B04-0000-0007-0191020D-spo-fmsh.ota1.zigbee", "imageType": 31492, "manufacturerCode": 4338, "sha512": "d6739de2706120c4be329588933024326db31ee4fae775d60990fe69e934a7a0d51bba000f3091ba1423a40d973494713fef2c598e8813ce07fa6e6fddbf24f1", "otaHeaderString": "ubisys J1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B04-0000-0007-0191020D-spo-fmsh.ota1.zigbee", "hardwareVersionMax": 7, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B05-0000-0004-0191020D-spo-rms.ota1.zigbee", "fileVersion": 26280461, "fileSize": 256474, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B05-0000-0004-0191020D-spo-rms.ota1.zigbee", "imageType": 31493, "manufacturerCode": 4338, "sha512": "05c0e45c29f614ea50e9c9c4752856b35ddff5fdd82dd046cc076ccc1779cd76b39a2fffae9bdedcb1aee378b71d56068f5cbfb7309bb181d33a6ccaf4837dfb", "otaHeaderString": "ubisys S1-R\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B05-0000-0004-0191020D-spo-rms.ota1.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B06-0000-0004-0191020D-spo-rms2.ota1.zigbee", "fileVersion": 26280461, "fileSize": 256202, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B06-0000-0004-0191020D-spo-rms2.ota1.zigbee", "imageType": 31494, "manufacturerCode": 4338, "sha512": "eab0f8bbddcd0c7a27ebf65891853a2de218f2243c8e25be57df8db524448c1cafd1b5c4b4585a70811362a7a411968c926a12aece4fa0d2e12ea86d8556b887", "otaHeaderString": "ubisys S2-R\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B06-0000-0004-0191020D-spo-rms2.ota1.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B07-0000-0004-0191020D-spo-rmsh.ota1.zigbee", "fileVersion": 26280461, "fileSize": 261134, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B07-0000-0004-0191020D-spo-rmsh.ota1.zigbee", "imageType": 31495, "manufacturerCode": 4338, "sha512": "448ccbbbe62ffd097c804dd36e358363aad0d6f66699284c427ec11944e7dd5c355fa03b83f4b0af8bcd16a4f9a5d75b948634a1b57447ebed7d4888d2197266", "otaHeaderString": "ubisys J1-R\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B07-0000-0004-0191020D-spo-rmsh.ota1.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B08-0000-0004-0192020D-spo-rmd.ota1.zigbee", "fileVersion": 26345997, "fileSize": 263324, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B08-0000-0004-0192020D-spo-rmd.ota1.zigbee", "imageType": 31496, "manufacturerCode": 4338, "sha512": "0fa396f05a629567107a33591c1b76908ca076d6e5db3a04550f2f3f6f9c3ba4ea0f0ba35dea641b10077c22be15b8227427b0b01ef8f57ef0e6bf3c8fc7207d", "otaHeaderString": "ubisys D1-R\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B08-0000-0004-0192020D-spo-rmd.ota1.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B09-0000-0004-0192020D-spo-fmi4.ota1.zigbee", "fileVersion": 26345997, "fileSize": 208802, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B09-0000-0004-0192020D-spo-fmi4.ota1.zigbee", "imageType": 31497, "manufacturerCode": 4338, "sha512": "c56422aa4072d57a159b52edf8b45552b206bf0581e491d27b5c12b1b78137e7e6d0d3b3139c29344626bd1091d6de354d9830efb55e545a7dbd444252629e1b", "otaHeaderString": "ubisys C4\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B09-0000-0004-0192020D-spo-fmi4.ota1.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B0A-0000-0005-0193020D-m7b-r0.ota1.zigbee", "fileVersion": 26411533, "fileSize": 201398, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B0A-0000-0005-0193020D-m7b-r0.ota1.zigbee", "imageType": 31498, "manufacturerCode": 4338, "sha512": "c404999420e0e366e87dbbc9a798937d5df1b3d1a5f97d2fa81f34fb2c244d85e98d32a73f66aecf129c07a13b276bc3af1eac0fc59ad45589885c28eaa2f633", "otaHeaderString": "ubisys R0 1.9.3\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B0A-0000-0005-0193020D-m7b-r0.ota1.zigbee", "hardwareVersionMax": 5, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B0B-0000-0001-01900210-m7b-h10.ota1.zigbee", "fileVersion": 26214928, "fileSize": 256192, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B0B-0000-0001-01900210-m7b-h10.ota1.zigbee", "imageType": 31499, "manufacturerCode": 4338, "sha512": "4196c923f4751d3d6d9719a0f0f66f344d1b8d1f1df991e47fb796d8b6f07fe1f408605f1e7c78f0df8ae0301c5a09c596ec6cfa0fe22b47e663241c98d67bb5", "otaHeaderString": "ubisys H10\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B0B-0000-0001-01900210-m7b-h10.ota1.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B0C-0000-0000-01000206-m7b-wd1.ota.zigbee", "fileVersion": 16777734, "fileSize": 207730, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B0C-0000-0000-01000206-m7b-wd1.ota.zigbee", "imageType": 31500, "manufacturerCode": 4338, "sha512": "1cd9a601a1fb34edeb00b7b76b4de2792bd416826c2522377304de7139c52484449d6af4bd2b4262eee31a479352d0aaeb1c4f646c9155086d50db668e6cdf1c", "otaHeaderString": "ubisys WD1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B0C-0000-0000-01000206-m7b-wd1.ota.zigbee", "hardwareVersionMax": 0, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B0D-0000-0001-01500427-m7b-h1.ota.zigbee", "fileVersion": 22021159, "fileSize": 180734, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B0D-0000-0001-01500427-m7b-h1.ota.zigbee", "imageType": 31501, "manufacturerCode": 4338, "sha512": "773d51705c7b0666c3322e4a15100b258b8d8a085962e5f49c0bcfe5cd65345c46dd9bf8eb84aad7fbc2647401fb5612ea27549956ec7e31b2f1cdce2de2b6b5", "otaHeaderString": "ubisys H1 1.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B0D-0000-0001-01500427-m7b-h1.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B11-0000-0001-00940240-m7b-q95.ota.zigbee", "fileVersion": 9699904, "fileSize": 476640, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B11-0000-0001-00940240-m7b-q95.ota.zigbee", "imageType": 31505, "manufacturerCode": 4338, "sha512": "613ed06ed20a29f762fc0f838c1921914f924e169531fd292c364525486348f362ccbc1757a405cf49b5a78a318ded9bda82419f1b53fbdbaabd60edc07c9de1", "otaHeaderString": "ubisys LD6 0.9.4\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B11-0000-0001-00940240-m7b-q95.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B21-0000-0006-0194020E-spo-fmd.ota.zigbee", "fileVersion": 26477070, "fileSize": 132350, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B21-0000-0006-0194020E-spo-fmd.ota.zigbee", "imageType": 31521, "manufacturerCode": 4338, "sha512": "e2dae4e58b792c57bd7b6d79457f4992d6825bc600055f269caaba6db8f1eda407648201f7a3aa25a7b090991aaf725dba0cca4ef0bb06e1cb49eeab94896388", "otaHeaderString": "ubisys D1 1.9.4\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B21-0000-0006-0194020E-spo-fmd.ota.zigbee", "hardwareVersionMax": 6, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B22-0000-0001-0193020D-spo-fms-rev0-1.ota.zigbee", "fileVersion": 26411533, "fileSize": 129278, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B22-0000-0001-0193020D-spo-fms-rev0-1.ota.zigbee", "imageType": 31522, "manufacturerCode": 4338, "sha512": "100b4381b86f8f87bd7af465e5b21598a5d5db96af0184c04dc8ef357b753ebef1b13fe4fb3c4ef497d57d491c9ca9b3b1c65cc433fd422656a5139a1ec5d791", "otaHeaderString": "ubisys S1 1.9.3\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B22-0000-0001-0193020D-spo-fms-rev0-1.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B23-0000-0006-0192020D-spo-fms2.ota.zigbee", "fileVersion": 26345997, "fileSize": 129022, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B23-0000-0006-0192020D-spo-fms2.ota.zigbee", "imageType": 31523, "manufacturerCode": 4338, "sha512": "6b59596d69c0049ac8111ddec0edee16c7fe3353900e9d04eef7a3d42a7cda10152883e743b8114b2140957fc417fa64c62b15fc236a4d8d86a10cb786742cf2", "otaHeaderString": "ubisys S2 1.9.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B23-0000-0006-0192020D-spo-fms2.ota.zigbee", "hardwareVersionMax": 6, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B24-0000-0007-0192020D-spo-fmsh.ota.zigbee", "fileVersion": 26345997, "fileSize": 132094, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B24-0000-0007-0192020D-spo-fmsh.ota.zigbee", "imageType": 31524, "manufacturerCode": 4338, "sha512": "cc99d8c41fd80730fcca3cd09b61e352b5dd62e35d666e2b8bee9a449ff85dd0b7820258154ff1d748945a59cd9d3163874a906ef80684058f836a7e4d7abdc6", "otaHeaderString": "ubisys J1 1.9.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B24-0000-0007-0192020D-spo-fmsh.ota.zigbee", "hardwareVersionMax": 7, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B25-0000-0004-0192020D-spo-rms.ota.zigbee", "fileVersion": 26345997, "fileSize": 129278, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B25-0000-0004-0192020D-spo-rms.ota.zigbee", "imageType": 31525, "manufacturerCode": 4338, "sha512": "d549288431cc6d66f78df33e8160d2310f375154675f5563291798edda18e1b75480c0160b8aafd24e9c1f8b105d73f2553a771e378d218c0ff3998639170ff0", "otaHeaderString": "ubisys S1-R 1.9.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B25-0000-0004-0192020D-spo-rms.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B26-0000-0004-0192020D-spo-rms2.ota.zigbee", "fileVersion": 26345997, "fileSize": 129022, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B26-0000-0004-0192020D-spo-rms2.ota.zigbee", "imageType": 31526, "manufacturerCode": 4338, "sha512": "2af49c67b63c7d42450091c3a8d5682f3c3d06f11470b61ffa194b3da67fffc9151f653e0b54c886ee78482313b12e2a7893c0b465e577f541e830c598f6408d", "otaHeaderString": "ubisys S2-R 1.9.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B26-0000-0004-0192020D-spo-rms2.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B27-0000-0004-0192020D-spo-rmsh.ota.zigbee", "fileVersion": 26345997, "fileSize": 132094, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B27-0000-0004-0192020D-spo-rmsh.ota.zigbee", "imageType": 31527, "manufacturerCode": 4338, "sha512": "305edd6055e2b23c0e48751cc4dcf95d436d05fec604903e102e6333b9f930c603d531bb223f65b2d744bd460275283c85338925daf199c706ac0152d8d481ea", "otaHeaderString": "ubisys J1-R 1.9.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B27-0000-0004-0192020D-spo-rmsh.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B28-0000-0004-0195020E-spo-rmd.ota.zigbee", "fileVersion": 26542606, "fileSize": 132350, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B28-0000-0004-0195020E-spo-rmd.ota.zigbee", "imageType": 31528, "manufacturerCode": 4338, "sha512": "bf4afd298edad1a2976b02169c9d84880c11d65b10ff48a55fbb168156f1a2cc0014b6f79dd02fc5d117962b57d0127653f5142f65d9653609c3bc2e54350f68", "otaHeaderString": "ubisys D1-R 1.9.5\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B28-0000-0004-0195020E-spo-rmd.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B29-0000-0004-01940221-spo-fmi4.ota.zigbee", "fileVersion": 26477089, "fileSize": 117758, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B29-0000-0004-01940221-spo-fmi4.ota.zigbee", "imageType": 31529, "manufacturerCode": 4338, "sha512": "9eff1e73b9d1eb25e6f0f39fad8c15ee505fcca8fb64d737e87bf4c0a4a88c915cc32d0198d5e7893a694b67869cb4365958a9ddff0da957c1a7ceea1849e595", "otaHeaderString": "ubisys C4 1.9.4\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B29-0000-0004-01940221-spo-fmi4.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B2A-0000-0005-02010230-m7b-r0.ota.zigbee", "fileVersion": 33620528, "fileSize": 114174, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B2A-0000-0005-02010230-m7b-r0.ota.zigbee", "imageType": 31530, "manufacturerCode": 4338, "sha512": "e87af94a95f4d6d8d3eb15c183fee5383bbf6d6a9a1340516e6a95e7ff7d71f06d08227bdaaec140b186e45366e17a6f4fae769266f8531cbfbc4cbea300c029", "otaHeaderString": "ubisys R0 2.0.1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B2A-0000-0005-02010230-m7b-r0.ota.zigbee", "hardwareVersionMax": 5, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B2B-0000-0001-01920210-m7b-h10.ota.zigbee", "fileVersion": 26346000, "fileSize": 134398, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B2B-0000-0001-01920210-m7b-h10.ota.zigbee", "imageType": 31531, "manufacturerCode": 4338, "sha512": "438a8ea59ae39f1d4416bd0a1267ed72657b3966fea4badd1febc248df1dafc85742f6449bc553287e3a905b2ddd268da16680662159d091cdec0a9b05088d3d", "otaHeaderString": "ubisys H10 1.9.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B2B-0000-0001-01920210-m7b-h10.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B2C-0000-0001-0150044D-ld6.ota.zigbee", "fileVersion": 22021197, "fileSize": 239358, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B2C-0000-0001-0150044D-ld6.ota.zigbee", "imageType": 31532, "manufacturerCode": 4338, "sha512": "6545ebd2a0a8ce5cdcf39a2146d2ac9f648632c889ba82b2adf2d285c4a83f42ec8ce1abd215466f7d5692738e66d1f9753572b541fd8e4c7f18ede723cbd041", "otaHeaderString": "ubisys LD6 1.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B2C-0000-0001-0150044D-ld6.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B2D-0000-0001-0172044D-m7b-h1.ota.zigbee", "fileVersion": 24249421, "fileSize": 182526, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B2D-0000-0001-0172044D-m7b-h1.ota.zigbee", "imageType": 31533, "manufacturerCode": 4338, "sha512": "b348e455e1b608890db1c06c23f911b6ada09f96610586c304c42af00f1c1c70025df532d46983e1fac9ec699bb27d0f74eb57c8a9c6adf6edfee7a2f2100dec", "otaHeaderString": "ubisys H1 1.7.2\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B2D-0000-0001-0172044D-m7b-h1.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B31-0000-0006-02500447-spo-fmd.ota.zigbee", "fileVersion": 38798407, "fileSize": 152574, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B31-0000-0006-02500447-spo-fmd.ota.zigbee", "imageType": 31537, "manufacturerCode": 4338, "sha512": "9d8be9b4d07a5f46f0183179d552f73bc2d97aeab37dc01007d76783efeb593b3c0bd8945166bb0ebc5468fb9f91907650dbd1bf3f7f5baaa96c0cd078d9a79c", "otaHeaderString": "ubisys D1 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B31-0000-0006-02500447-spo-fmd.ota.zigbee", "hardwareVersionMax": 6, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B32-0000-0001-02500447-spo-fms-rev0-1.ota.zigbee", "fileVersion": 38798407, "fileSize": 149758, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B32-0000-0001-02500447-spo-fms-rev0-1.ota.zigbee", "imageType": 31538, "manufacturerCode": 4338, "sha512": "ce65e5c5e1d9ba39092f4696ad2a69795aa55050e58a9075cf57b44ea8347383050ace7a7c02409498108ea87eaf755ce2016a836f45519678381248ff19681e", "otaHeaderString": "ubisys S1 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B32-0000-0001-02500447-spo-fms-rev0-1.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B33-0000-0006-02500447-spo-fms2.ota.zigbee", "fileVersion": 38798407, "fileSize": 149502, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B33-0000-0006-02500447-spo-fms2.ota.zigbee", "imageType": 31539, "manufacturerCode": 4338, "sha512": "3072cb363ae9861567279d6ed965fb248c30acf0e59193321eae325732ac8aa0b4ab3d5a8a0b4eb60bb13f8a753195ee356505c710f0309a62f00095aff37420", "otaHeaderString": "ubisys S2 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B33-0000-0006-02500447-spo-fms2.ota.zigbee", "hardwareVersionMax": 6, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B34-0000-0007-02500447-spo-fmsh.ota.zigbee", "fileVersion": 38798407, "fileSize": 151806, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B34-0000-0007-02500447-spo-fmsh.ota.zigbee", "imageType": 31540, "manufacturerCode": 4338, "sha512": "49d9be6554ae3e0d4fa40bfafa23cf87635c02353d37be5a2a35f0a4c83ad3f342afb29d746bcb3ded7dac5bae4797d289bfecdd2aa496d76aa8049ac4a67aed", "otaHeaderString": "ubisys J1 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B34-0000-0007-02500447-spo-fmsh.ota.zigbee", "hardwareVersionMax": 7, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B35-0000-0004-02500447-spo-rms.ota.zigbee", "fileVersion": 38798407, "fileSize": 149758, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B35-0000-0004-02500447-spo-rms.ota.zigbee", "imageType": 31541, "manufacturerCode": 4338, "sha512": "bfc44b028618db233045a54827e6a5dba9ff24d6b284d32f33e7e2d71aa1b3a7a1d4cd0f78b8ce004de885f9a596f5448364293633714b542608b13d2bf380ea", "otaHeaderString": "ubisys S1-R 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B35-0000-0004-02500447-spo-rms.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B36-0000-0004-02500447-spo-rms2.ota.zigbee", "fileVersion": 38798407, "fileSize": 149502, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B36-0000-0004-02500447-spo-rms2.ota.zigbee", "imageType": 31542, "manufacturerCode": 4338, "sha512": "8f4a3869a43cfc9e4d35c9640633d0c1b0678e0aca6edbee1e5aa91f714d9d786dd4585627a919a423011b41dc66661cb6cf2beb1d8421bf7c3609cb3409c35f", "otaHeaderString": "ubisys S2-R 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B36-0000-0004-02500447-spo-rms2.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B37-0000-0004-02500447-spo-rmsh.ota.zigbee", "fileVersion": 38798407, "fileSize": 151806, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B37-0000-0004-02500447-spo-rmsh.ota.zigbee", "imageType": 31543, "manufacturerCode": 4338, "sha512": "b553db5f1cc27a6830e04b1d4f4fee2fd8c4477d9dbe44404e38ab563b5134b183555f04258306843578262ac75e136fa37f98cc1dea1b1fcebe473df8d886ca", "otaHeaderString": "ubisys J1-R 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B37-0000-0004-02500447-spo-rmsh.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B38-0000-0004-02500447-spo-rmd.ota.zigbee", "fileVersion": 38798407, "fileSize": 152574, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B38-0000-0004-02500447-spo-rmd.ota.zigbee", "imageType": 31544, "manufacturerCode": 4338, "sha512": "02ed2e254494a9242b2f132df1e87396cfcddb3fa15fe3f8960a70ccc57417355ff184abf9af13455d07f7b3c91d2afe94ef1ec26961a916d270f5f3c8712bbb", "otaHeaderString": "ubisys D1-R 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B38-0000-0004-02500447-spo-rmd.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B39-0000-0004-02500447-spo-fmi4.ota.zigbee", "fileVersion": 38798407, "fileSize": 128766, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B39-0000-0004-02500447-spo-fmi4.ota.zigbee", "imageType": 31545, "manufacturerCode": 4338, "sha512": "86c71aa053f61d20ee3a553c012d3c16afea557d2c6e28da8b30019a5633aced20959ad26f48655487682d06d105fdd293283ec37f828e168f596d2123c143ca", "otaHeaderString": "ubisys C4 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B39-0000-0004-02500447-spo-fmi4.ota.zigbee", "hardwareVersionMax": 4, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B3A-0000-0005-02500447-m7b-r0.ota.zigbee", "fileVersion": 38798407, "fileSize": 123902, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B3A-0000-0005-02500447-m7b-r0.ota.zigbee", "imageType": 31546, "manufacturerCode": 4338, "sha512": "aace578a76c957613c25fa47f533af0dadaa7150ad1d62ef019ab081d186bc6fe79cb2f6e008015b7f5aa890ad542b71c8c95beafe78480b76e399aa68783b2f", "otaHeaderString": "ubisys R0 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B3A-0000-0005-02500447-m7b-r0.ota.zigbee", "hardwareVersionMax": 5, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B3B-0000-0001-02500447-m7b-h10.ota.zigbee", "fileVersion": 38798407, "fileSize": 157950, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B3B-0000-0001-02500447-m7b-h10.ota.zigbee", "imageType": 31547, "manufacturerCode": 4338, "sha512": "7aa6186e680f62447d8bb226d1902affe940b3d070ae228d545a172d75d6e9552febef60959762704003af5dcd319dce06acf33f19ce7cc7bdb90e5aad95e601", "otaHeaderString": "ubisys H10 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B3B-0000-0001-02500447-m7b-h10.ota.zigbee", "hardwareVersionMax": 1, "hardwareVersionMin": 0 }, { "fileName": "10F2-7B45-0100-0100-0250044D-ubisys-s1r-qpg6105.ota.zigbee", "fileVersion": 38798413, "fileSize": 233726, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B45-0100-0100-0250044D-ubisys-s1r-qpg6105.ota.zigbee", "imageType": 31557, "manufacturerCode": 4338, "sha512": "6fe7a4899d98399804f88951aa069314b9df8a531966b79065027f359ca02b2ed3e7b0ff460f48e6411001acea91618216c67b0c69b4569aea50c47f9175e793", "otaHeaderString": "ubisys S1-R 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B45-0100-0100-0250044D-ubisys-s1r-qpg6105.ota.zigbee", "hardwareVersionMax": 256, "hardwareVersionMin": 256 }, { "fileName": "10F2-7B49-0100-0100-0250044D-ubisys-c4-qpg6105.ota.zigbee", "fileVersion": 38798413, "fileSize": 228094, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B49-0100-0100-0250044D-ubisys-c4-qpg6105.ota.zigbee", "imageType": 31561, "manufacturerCode": 4338, "sha512": "1f5ac51f84fee69c5a7be2f4d309b9bea81ff01af7f65b0d5819b637ceee251130fcc39b7580c662489afc41caae44b7c8f0522db166d64c9e01b2bb0a20ca55", "otaHeaderString": "ubisys C4 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B49-0100-0100-0250044D-ubisys-c4-qpg6105.ota.zigbee", "hardwareVersionMax": 256, "hardwareVersionMin": 256 }, { "fileName": "10F2-7B4A-0100-0100-0250044D-ubisys-r0-qpg6105.ota.zigbee", "fileVersion": 38798413, "fileSize": 223998, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B4A-0100-0100-0250044D-ubisys-r0-qpg6105.ota.zigbee", "imageType": 31562, "manufacturerCode": 4338, "sha512": "1e15d52adaa4cbefa0c1dafe59e119804ce12f234db4ba6ef127f3a3e6a455e8731c79ffdfdc4fb24b3e4a7d7f9d86312407f2ee979e337ade9d682b6e986a8a", "otaHeaderString": "ubisys R0 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B4A-0100-0100-0250044D-ubisys-r0-qpg6105.ota.zigbee", "hardwareVersionMax": 256, "hardwareVersionMin": 256 }, { "fileName": "ZigUSB_C6.ota", "fileVersion": 407, "fileSize": 416645, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/xyzroe/ZigUSB_C6.ota", "imageType": 4113, "manufacturerCode": 13379, "sha512": "ec392dc3cde87ac2a6f39d848f38094f7a8a3a2df4818772ec866fdf9af03bcff3d0ec1985a00874a8b9599aaedc89bc7c26ac4c2b5732dcd7d40f6fff8a3a80", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "originalUrl": "https://github.com/xyzroe/ZigUSB_C6/releases/download/407/ZigUSB_C6.ota", "modelId": "ZigUSB_C6", "releaseNotes": "https://github.com/xyzroe/ZigUSB_C6/releases/tag/407" }, { "fileName": "Vibrate_Sensor_PROD_OTA_V55_v1.00.55.ota", "fileVersion": 55, "fileSize": 132610, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Vibrate_Sensor_PROD_OTA_V55_v1.00.55.ota", "imageType": 54187, "manufacturerCode": 4659, "sha512": "5bd3a87405696100395e93f3f72a512f704c82a1c46775631a4497ffd847d822cb13fdb552213734bac2f46b60027f8939dd793283a2fec60a43e3717afeb502", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0331-191d3685-sp240-1.9.29.ota", "fileVersion": 421344901, "fileSize": 285058, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0331-191d3685-sp240-1.9.29.ota", "imageType": 817, "manufacturerCode": 4454, "sha512": "041ac46c276770f7179aed8222a52813d50d981776208fa4fa76bf432f0fa73dc8a09aabfe61894aa734f18fdc997a6842aaee7ab6a4d3554914a42e3fcdca00", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0332-191d3685-sp242-1.9.29.ota", "fileVersion": 421344901, "fileSize": 285058, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0332-191d3685-sp242-1.9.29.ota", "imageType": 818, "manufacturerCode": 4454, "sha512": "d3f1728a8b6e70501b21414cb0bbbd17934a56a64d2249500db36e29821a3b5f0551b42c63abbe837426cf657d25fa497585cf86ba6f41209ce40a56cadb9319", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0333-191d3685-sp244-1.9.29.ota", "fileVersion": 421344901, "fileSize": 285058, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0333-191d3685-sp244-1.9.29.ota", "imageType": 819, "manufacturerCode": 4454, "sha512": "447f8b22c7c9ed1b2d0d4266c9d20087f62c73a5e1f677388747d6050044512f884266565097a9dcc37893345e5d39cf79b99a63ef036296174c8054f7f372e6", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0334-191e3685-sp240v2-1.9.30.ota", "fileVersion": 421410437, "fileSize": 285042, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0334-191e3685-sp240v2-1.9.30.ota", "imageType": 820, "manufacturerCode": 4454, "sha512": "72e94cc57ae49c53e11e98bdd4a24dd08c640e19d3e31b059e1df458aea80b1cc95a8d4edff30ba6b4e173d8d5e70607a833533656e6475bfa51d7c9edb6bc7b", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0335-191e3685-sp242v2-1.9.30.ota", "fileVersion": 421410437, "fileSize": 285042, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0335-191e3685-sp242v2-1.9.30.ota", "imageType": 821, "manufacturerCode": 4454, "sha512": "b85050cf4f8352967966df3c65cb550fbaac54b4cd987fc8a0ca176acf42f30b38a0fcfe3b28562d670e630191247d2095b732a2230596feb5c7e8c9599a5044", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "1166-0336-191e3685-sp244v2-1.9.30.ota", "fileVersion": 421410437, "fileSize": 285042, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Innr/1166-0336-191e3685-sp244v2-1.9.30.ota", "imageType": 822, "manufacturerCode": 4454, "sha512": "857d8d4b4b179f92120072d8084d1f2b5b3e20a94fca6cf76ccc6afbbc8ba8f9e091a30144ab35e6edc6984fb45427289aaf8def4838a065b98cf9728716109d", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "Namron_4512750_v32_2025_02_14.ota", "fileVersion": 32, "fileSize": 241774, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Namron/Namron_4512750_v32_2025_02_14.ota", "imageType": 44, "manufacturerCode": 4714, "sha512": "28662b8c0495b7d495b4dc4c4eeccb47f54da664419daa950053b0ef041df747856f6e10f5c06c590e74679a9bc4672208280050dc5a15ca38bdcdc8182dd104", "otaHeaderString": "EBL Shyugj_Dimmer_mg22_ext_flash" }, { "fileName": "trvzb_v1.3.0.ota", "fileVersion": 4864, "fileSize": 329856, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/trvzb_v1.3.0.ota", "imageType": 8199, "manufacturerCode": 4742, "sha512": "9bb3374caba58ac72d4ffb1f601a4b35ec7ffa74a45bb03ff9892919929399ea4484fafd681f27368646650929dd7a56c6bf230c4d39971658d21b89c382073e", "otaHeaderString": "vers:00001300,00001204\n\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "20240731114104_OTA_lumi.switch.b1nc01_0.0.0_0029_20240729_69827E.ota", "fileVersion": 29, "fileSize": 289744, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240731114104_OTA_lumi.switch.b1nc01_0.0.0_0029_20240729_69827E.ota", "imageType": 6280, "manufacturerCode": 4447, "sha512": "b5db413acf7818dfc878444b1f550e57e6890e426c68981b33886d3398771b079d44cd85f10790f45f8223d306bf02c7f75f50c5b46c3660d31186fa612abd54", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b1nc01" }, { "fileName": "20240731114846_OTA_lumi.switch.b2nc01_0.0.0_0029_20240729_4EDD09.ota", "fileVersion": 29, "fileSize": 291552, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Lumi/20240731114846_OTA_lumi.switch.b2nc01_0.0.0_0029_20240729_4EDD09.ota", "imageType": 6408, "manufacturerCode": 4447, "sha512": "9fce4e4bccca34831c2ab1cc989013068bc92a9d67264d17764fcea556ba1baa9cacdacbae6f59836a680f39b81df7cd574ba8a7604e0a4c9aebee07ad459b8c", "otaHeaderString": "ROUTERX-----JN5180--ENCRYPTED000", "modelId": "lumi.switch.b2nc01" }, { "fileName": "Radar_PROD_OTA_V6_v1.00.06.ota", "fileVersion": 6, "fileSize": 136354, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/Radar_PROD_OTA_V6_v1.00.06.ota", "imageType": 54194, "manufacturerCode": 5127, "sha512": "ef965246281a1b3da9f908b8c0d45e597c459b7db1e4b98a0ac1b3f3fab67058216eaf8c31ef5b6d9fd413c0cbb0030b38062581a2daa21c3ef42d09afcf5af8", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "GarageDoorSensor_PROD_OTA_V36_v1.00.36.ota", "fileVersion": 36, "fileSize": 137698, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/GarageDoorSensor_PROD_OTA_V36_v1.00.36.ota", "imageType": 54193, "manufacturerCode": 5127, "sha512": "b68cbc5385270c5196daaec55e5175d8203006ed16672ed3e860b3a630dc23965994123a27bb3a2248ae2f0d9871c81c8b38259f2d7aca28651758f936515535", "otaHeaderString": "Telink OTA Sample Usage\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "PMM300Z3_OTA_ENC_V8_ENC.ota", "fileVersion": 8, "fileSize": 224686, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Jennic/PMM300Z3_OTA_ENC_V8_ENC.ota", "imageType": 4108, "manufacturerCode": 4151, "sha512": "8baf64a84928848a8e50889c9f147eec3af40fe98a08fdae0c81211137e97f41497d14fa9cb1760f166bc7d08febf757f2d6fa58aee91ecc9c0cc32402a8e469", "otaHeaderString": "DR1175r1v02--JN5169--ENCRYPTED00", "releaseNotes": "The acCurrentDivisor has been changed from 100 to 10 to allow acCurrent values to exceed 655.35A. Note, however, that the unit precision will change from two decimal places to one." }, { "fileName": "100B-0129-01000D06-Light-EFR32MG26.zigbee", "fileVersion": 16780550, "fileSize": 833604, "originalUrl": "https://firmware.meethue.com/storage/100b-129/16780550/d44f751c-9def-4483-aeb9-940f59ed3aa1/100B-0129-01000D06-Light-EFR32MG26.zigbee", "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Hue/100B-0129-01000D06-Light-EFR32MG26.zigbee", "imageType": 297, "manufacturerCode": 4107, "sha512": "c21b60bca9276fb9c1cecfe4b61b91a4cc90ca9a48cd75c93fb2ed8fc33853e81b0f4e618afbc7ba6538b7a868669765698611c3db2430389aa5fae86c28c5d3", "otaHeaderString": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "mmwave_module_fw_V3_14_3.ota", "fileVersion": 100863491, "fileSize": 50238, "originalUrl": "https://inov.li/IRbxhx1646F/mmwave_module_fw_V3_14_3.ota", "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/mmwave_module_fw_V3_14_3.ota", "imageType": 260, "manufacturerCode": 4655, "sha512": "f89ead312763061ca44dd3cd917c223087877ce235a2ac41ef22b31c43b6566baab267a3d90a649812364a7b4c065757fa4117b64fbed74857bae3d2493cbf27", "otaHeaderString": "LD6002B\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "snzb-02d_v2.3.0.ota", "fileVersion": 8960, "fileSize": 264118, "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Sonoff/snzb-02d_v2.3.0.ota", "imageType": 2053, "manufacturerCode": 4742, "sha512": "1ff8bc7d3d38adc87b4b2050fc6241efdd989c6410b6a73f6f8dde999450f9468bc08ca62f7223adc61a2d655a746c257002af4c2c36f3c089c182cc4a58aaea", "otaHeaderString": "FWSN_SNZB02D\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "VZM32-SN_0.03.ota", "fileVersion": 16973827, "fileSize": 247342, "originalUrl": "https://files.inovelli.com/firmware/VZM32-SN/Beta/0.03/VZM32-SN_0.03.ota", "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Inovelli/VZM32-SN_0.03.ota", "imageType": 259, "manufacturerCode": 4655, "sha512": "7e24a756766c6e8c6d83a14c4d875c7d149ed5c717100897883d96d06c496e9d69bf25a03c4dd2b74cef5bebaf9edd19ce4a6e23dd7e81cb6144ef3c5786bc84", "otaHeaderString": "vzm32-sn_mmWave\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000" }, { "fileName": "10F2-7B02-0002-0007-0192020D-spo-fms.ota1.zigbee", "fileVersion": 26345997, "fileSize": 256200, "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B02-0002-0007-0192020D-spo-fms.ota1.zigbee", "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B02-0002-0007-0192020D-spo-fms.ota1.zigbee", "imageType": 31490, "manufacturerCode": 4338, "sha512": "27743f96962964d3670c7cc1cf90356d1e77b1277d4cc97a501b01e0600768fd9335f2a9b94efc7c8e4ae892eeebc7a5a2c99ab4b223d0d957282ac66e11c7ac", "otaHeaderString": "ubisys S1\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "hardwareVersionMin": 2, "hardwareVersionMax": 7 }, { "fileName": "10F2-7B22-0002-0007-0193020D-spo-fms.ota.zigbee", "fileVersion": 26411533, "fileSize": 129278, "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B22-0002-0007-0193020D-spo-fms.ota.zigbee", "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B22-0002-0007-0193020D-spo-fms.ota.zigbee", "imageType": 31522, "manufacturerCode": 4338, "sha512": "94d2430f2c4ddd9ecbad6199608958715ce48269f352e18636e62bf7f6d7c17b1d9fb444fc9f8b4dea26da265f4cc34d4df27557b97095a30140e0470a9f9c65", "otaHeaderString": "ubisys S1 1.9.3\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "hardwareVersionMin": 2, "hardwareVersionMax": 7 }, { "fileName": "10F2-7B32-0002-0007-02500447-spo-fms.ota.zigbee", "fileVersion": 38798407, "fileSize": 149758, "originalUrl": "http://fwu.ubisys.de/smarthome/OTA/release/10F2-7B32-0002-0007-02500447-spo-fms.ota.zigbee", "url": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/Ubisys/10F2-7B32-0002-0007-02500447-spo-fms.ota.zigbee", "imageType": 31538, "manufacturerCode": 4338, "sha512": "9d3e6e375c865024a29de5760645be70cf1b549442b5f9b97d417bf4a423275076b16eb8a2e9bec91ea364e7e318701069516be71bbc7b629eea1455130c1dac", "otaHeaderString": "ubisys S1 2.5.0\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", "hardwareVersionMin": 2, "hardwareVersionMax": 7 } ]zigpy-0.80.1/tests/ota/test_ota_config.py000066400000000000000000000227431501451476000204230ustar00rootroot00000000000000from __future__ import annotations import asyncio import pathlib from unittest.mock import patch import pytest import voluptuous as vol from tests.conftest import make_app from zigpy import config import zigpy.device import zigpy.ota import zigpy.types as t @pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_ota_disabled_legacy(tmp_path: pathlib.Path) -> None: (tmp_path / "index.json").write_text("{}") # Enable all the providers ota = zigpy.ota.OTA( config=config.SCHEMA_OTA( { config.CONF_OTA_ENABLED: False, # But disable OTA config.CONF_OTA_ADVANCED_DIR: tmp_path, config.CONF_OTA_ALLOW_ADVANCED_DIR: config.CONF_OTA_ALLOW_ADVANCED_DIR_STRING, config.CONF_OTA_IKEA: True, config.CONF_OTA_INOVELLI: True, config.CONF_OTA_LEDVANCE: True, config.CONF_OTA_SALUS: True, config.CONF_OTA_SONOFF: True, config.CONF_OTA_THIRDREALITY: True, config.CONF_OTA_PROVIDERS: [], config.CONF_OTA_EXTRA_PROVIDERS: [], config.CONF_OTA_REMOTE_PROVIDERS: [ { config.CONF_OTA_PROVIDER_URL: "https://example.org/remote_index.json", config.CONF_OTA_PROVIDER_MANUF_IDS: [0x1234, 4476], } ], config.CONF_OTA_Z2M_LOCAL_INDEX: tmp_path / "index.json", config.CONF_OTA_Z2M_REMOTE_INDEX: "https://example.org/z2m_index.json", } ), application=None, ) # None are actually enabled assert not ota._providers @pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_ota_enabled_legacy(tmp_path: pathlib.Path) -> None: (tmp_path / "index.json").write_text("{}") # Enable all the providers ota = zigpy.ota.OTA( config=config.SCHEMA_OTA( { config.CONF_OTA_ENABLED: True, config.CONF_OTA_BROADCAST_ENABLED: False, config.CONF_OTA_ADVANCED_DIR: tmp_path, config.CONF_OTA_ALLOW_ADVANCED_DIR: config.CONF_OTA_ALLOW_ADVANCED_DIR_STRING, config.CONF_OTA_IKEA: True, config.CONF_OTA_INOVELLI: True, config.CONF_OTA_LEDVANCE: True, config.CONF_OTA_SALUS: True, config.CONF_OTA_SONOFF: True, config.CONF_OTA_THIRDREALITY: True, config.CONF_OTA_PROVIDERS: [], config.CONF_OTA_EXTRA_PROVIDERS: [], config.CONF_OTA_REMOTE_PROVIDERS: [ { config.CONF_OTA_PROVIDER_URL: "https://example.org/remote_index.json", config.CONF_OTA_PROVIDER_MANUF_IDS: [0x1234, 4476], } ], config.CONF_OTA_Z2M_LOCAL_INDEX: tmp_path / "index.json", config.CONF_OTA_Z2M_REMOTE_INDEX: "https://example.org/z2m_index.json", } ), application=None, ) # All are enabled assert len(ota._providers) == 9 async def test_ota_config(tmp_path: pathlib.Path) -> None: # Enable all the providers ota = zigpy.ota.OTA( config=config.SCHEMA_OTA( { config.CONF_OTA_ENABLED: True, config.CONF_OTA_BROADCAST_ENABLED: False, config.CONF_OTA_EXTRA_PROVIDERS: [ { config.CONF_OTA_PROVIDER_TYPE: "ikea", config.CONF_OTA_PROVIDER_OVERRIDE_PREVIOUS: True, } ], } ), application=None, ) assert ota._providers == [ zigpy.ota.providers.Ledvance(), zigpy.ota.providers.Sonoff(), zigpy.ota.providers.Inovelli(), zigpy.ota.providers.ThirdReality(), zigpy.ota.providers.Tradfri(), ] async def test_ota_config_invalid_message(tmp_path: pathlib.Path) -> None: with pytest.raises(vol.Invalid): zigpy.ota.OTA( config=config.SCHEMA_OTA( { config.CONF_OTA_ENABLED: True, config.CONF_OTA_BROADCAST_ENABLED: False, config.CONF_OTA_PROVIDERS: [ { config.CONF_OTA_PROVIDER_TYPE: "advanced", config.CONF_OTA_PROVIDER_WARNING: "oops", config.CONF_OTA_PROVIDER_PATH: tmp_path, } ], } ), application=None, ) async def test_ota_config_invalid_provider(tmp_path: pathlib.Path) -> None: with pytest.raises(vol.Invalid): zigpy.ota.OTA( config=config.SCHEMA_OTA( { config.CONF_OTA_ENABLED: True, config.CONF_OTA_BROADCAST_ENABLED: False, config.CONF_OTA_PROVIDERS: [ { config.CONF_OTA_PROVIDER_TYPE: "oops", } ], } ), application=None, ) async def test_ota_config_complex(tmp_path: pathlib.Path) -> None: # Enable all the providers (tmp_path / "index.json").write_text("{}") ota = zigpy.ota.OTA( config=config.SCHEMA_OTA( { config.CONF_OTA_ENABLED: True, config.CONF_OTA_BROADCAST_ENABLED: False, config.CONF_OTA_DISABLE_DEFAULT_PROVIDERS: [ "ikea", "sonoff", "ledvance", ], config.CONF_OTA_EXTRA_PROVIDERS: [ # test salus provider stub { config.CONF_OTA_PROVIDER_TYPE: "salus", config.CONF_OTA_PROVIDER_URL: "https://salus.example.org/", }, { config.CONF_OTA_PROVIDER_TYPE: "ikea", config.CONF_OTA_PROVIDER_URL: "https://ikea1.example.org/", }, { config.CONF_OTA_PROVIDER_TYPE: "ikea", config.CONF_OTA_PROVIDER_URL: "https://ikea2.example.org/", config.CONF_OTA_PROVIDER_MANUF_IDS: [0x1234, 0x5678], }, { config.CONF_OTA_PROVIDER_TYPE: "z2m", config.CONF_OTA_PROVIDER_URL: "https://z2m.example.org/", }, { config.CONF_OTA_PROVIDER_TYPE: "ikea", config.CONF_OTA_PROVIDER_OVERRIDE_PREVIOUS: True, config.CONF_OTA_PROVIDER_URL: "https://ikea3.example.org/", config.CONF_OTA_PROVIDER_MANUF_IDS: [0xABCD, 0xDCBA], }, { config.CONF_OTA_PROVIDER_TYPE: "advanced", config.CONF_OTA_PROVIDER_PATH: tmp_path, config.CONF_OTA_PROVIDER_WARNING: config.CONF_OTA_ALLOW_ADVANCED_DIR_STRING, }, { config.CONF_OTA_PROVIDER_TYPE: "z2m_local", config.CONF_OTA_PROVIDER_INDEX_FILE: tmp_path / "index.json", }, { config.CONF_OTA_PROVIDER_TYPE: "zigpy_local", config.CONF_OTA_PROVIDER_INDEX_FILE: tmp_path / "index.json", }, ], } ), application=None, ) assert ota._providers == [ # zigpy.ota.providers.Ledvance(), # zigpy.ota.providers.Sonoff(), zigpy.ota.providers.Inovelli(), zigpy.ota.providers.ThirdReality(), zigpy.ota.providers.Salus(url="https://salus.example.org/"), zigpy.ota.providers.RemoteZ2MProvider(url="https://z2m.example.org/"), zigpy.ota.providers.Tradfri( url="https://ikea3.example.org/", manufacturer_ids=[0xABCD, 0xDCBA], ), zigpy.ota.providers.AdvancedFileProvider(path=tmp_path), zigpy.ota.providers.LocalZ2MProvider(index_file=tmp_path / "index.json"), zigpy.ota.providers.LocalZigpyProvider(index_file=tmp_path / "index.json"), ] async def test_ota_broadcast_loop() -> None: app = make_app( { config.CONF_OTA: { config.CONF_OTA_ENABLED: True, config.CONF_OTA_BROADCAST_ENABLED: True, config.CONF_OTA_BROADCAST_INITIAL_DELAY: 0.1, config.CONF_OTA_BROADCAST_INTERVAL: 0.2, } } ) with patch.object( app.ota, "broadcast_notify", side_effect=[None, None, RuntimeError(), None, None, None], ) as mock_broadcast_notify: await app.startup() assert app.ota._broadcast_loop_task is not None await asyncio.sleep(1) await app.shutdown() assert app.ota._broadcast_loop_task is None assert len(mock_broadcast_notify.mock_calls) == 5 async def test_ota_broadcast() -> None: app = make_app({config.CONF_OTA: {config.CONF_OTA_ENABLED: True}}) await app.startup() app.send_packet.reset_mock() await app.ota.broadcast_notify() await app.shutdown() assert len(app.send_packet.mock_calls) == 1 assert app.send_packet.mock_calls[0].args[0].dst.addr_mode == t.AddrMode.Broadcast zigpy-0.80.1/tests/ota/test_ota_image.py000066400000000000000000000272311501451476000202350ustar00rootroot00000000000000import hashlib from unittest import mock import pytest import zigpy.ota.image as firmware import zigpy.types as t MANUFACTURER_ID = mock.sentinel.manufacturer_id IMAGE_TYPE = mock.sentinel.image_type @pytest.fixture def image(): img = firmware.OTAImage() img.header = firmware.OTAImageHeader( upgrade_file_id=firmware.OTAImageHeader.MAGIC_VALUE, header_version=256, header_length=56, field_control=0, manufacturer_id=9876, image_type=123, file_version=12345, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 4, ) img.subelements = [firmware.SubElement(tag_id=0x0000, data=b"data")] return img def test_image_serialization_bad_length(image): assert image.serialize() image.header.image_size += 1 with pytest.raises(ValueError): image.serialize() image.header.image_size -= 1 assert image.serialize() image.header.image_size -= 1 with pytest.raises(ValueError): image.serialize() def test_hw_version(): hw = firmware.HWVersion(0x0A01) assert hw.version == 10 assert hw.revision == 1 assert "version=10" in repr(hw) assert "revision=1" in repr(hw) def _test_ota_img_header(field_control, hdr_suffix=b"", extra=b""): d = b"\x1e\xf1\xee\x0b\x00\x018\x00" d += field_control d += ( b"|\x11\x01!rE!\x12\x02\x00EBL tradfri_light_basic\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00~\x91\x02\x00" ) d += hdr_suffix hdr, rest = firmware.OTAImageHeader.deserialize(d + extra) assert hdr.header_version == 0x0100 assert hdr.header_length == 0x0038 assert hdr.manufacturer_id == 4476 assert hdr.image_type == 0x2101 assert hdr.file_version == 0x12214572 assert hdr.stack_version == 0x0002 assert hdr.image_size == 0x0002917E assert hdr.serialize() == d return hdr, rest def test_ota_image_header(): hdr = firmware.OTAImageHeader() assert hdr.security_credential_version_present is None assert hdr.device_specific_file is None assert hdr.hardware_versions_present is None extra = b"abcdefghklmnpqr" hdr, rest = _test_ota_img_header(b"\x00\x00", extra=extra) assert rest == extra assert hdr.security_credential_version_present is False assert hdr.device_specific_file is False assert hdr.hardware_versions_present is False def test_ota_image_header_security(): extra = b"abcdefghklmnpqr" creds = t.uint8_t(0xAC) hdr, rest = _test_ota_img_header(b"\x01\x00", creds.serialize(), extra) assert rest == extra assert hdr.security_credential_version_present is True assert hdr.security_credential_version == creds assert hdr.device_specific_file is False assert hdr.hardware_versions_present is False def test_ota_image_header_hardware_versions(): extra = b"abcdefghklmnpqr" hw_min = firmware.HWVersion(0xBEEF) hw_max = firmware.HWVersion(0xABCD) hdr, rest = _test_ota_img_header( b"\x04\x00", hw_min.serialize() + hw_max.serialize(), extra ) assert rest == extra assert hdr.security_credential_version_present is False assert hdr.device_specific_file is False assert hdr.hardware_versions_present is True assert hdr.minimum_hardware_version == hw_min assert hdr.maximum_hardware_version == hw_max def test_ota_image_destination(): extra = b"abcdefghklmnpqr" dst = t.EUI64.deserialize(b"12345678")[0] hdr, rest = _test_ota_img_header(b"\x02\x00", dst.serialize(), extra) assert rest == extra assert hdr.security_credential_version_present is False assert hdr.device_specific_file is True assert hdr.upgrade_file_destination == dst assert hdr.hardware_versions_present is False def test_ota_img_wrong_header(): d = b"\x1e\xf0\xee\x0b\x00\x018\x00\x00\x00" d += ( b"|\x11\x01!rE!\x12\x02\x00EBL tradfri_light_basic\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00~\x91\x02\x00" ) with pytest.raises(ValueError): firmware.OTAImageHeader.deserialize(d) with pytest.raises(ValueError): firmware.OTAImageHeader.deserialize(d + b"123abc") def test_header_string(): size = 32 header_string = "This is a header String" data = header_string.encode("utf8").ljust(size, b"\x00") extra = b"cdef123" hdr_str, rest = firmware.HeaderString.deserialize(data + extra) assert rest == extra with pytest.raises(ValueError): firmware.HeaderString(b"foo") with pytest.raises(ValueError): firmware.HeaderString(b"a" * 33) hdr_str, rest = firmware.HeaderString.deserialize(data) assert rest == b"" assert header_string in str(hdr_str) assert firmware.HeaderString(header_string).serialize() == data def test_header_string_roundtrip_invalid(): data = bytes.fromhex( "5a757d364000603e400013704000010000009f364000b015400020904000ffff" ) hdr_str, rest = firmware.HeaderString.deserialize(data) assert not rest assert hdr_str == firmware.HeaderString(data) assert hdr_str.serialize() == data assert data.hex() in str(hdr_str) def test_header_string_too_short(): header_string = "This is a header String" data = header_string.encode("utf8") with pytest.raises(ValueError): firmware.HeaderString.deserialize(data) def test_subelement(): payload = b"\x00payload\xff" data = b"\x01\x00" + t.uint32_t(len(payload)).serialize() + payload extra = b"extra" e, rest = firmware.SubElement.deserialize(data + extra) assert rest == extra assert e.tag_id == firmware.ElementTagId.ECDSA_SIGNATURE_CRYPTO_SUITE_1 assert e.data == payload assert len(e.data) == len(payload) assert e.serialize() == data def test_subelement_too_short(): for i in range(1, 5): with pytest.raises(ValueError): firmware.SubElement.deserialize(b"".ljust(i, b"\x00")) e, rest = firmware.SubElement.deserialize(b"\x00\x00\x00\x00\x00\x00") assert e.data == b"" assert rest == b"" with pytest.raises(ValueError): firmware.SubElement.deserialize(b"\x00\x02\x02\x00\x00\x00a") def test_subelement_repr(): sub1 = firmware.SubElement( tag_id=firmware.ElementTagId.UPGRADE_IMAGE, data=b"\x00" * 32 ) assert ( "32:0000000000000000000000000000000000000000000000000000000000000000" in repr(sub1) ) sub2 = firmware.SubElement( tag_id=firmware.ElementTagId.UPGRADE_IMAGE, data=b"\x00" * 33 ) assert ( "33:00000000000000000000000000000000000000000000000000...00000000000000" in repr(sub2) ) @pytest.fixture def raw_header(): def data(elements_size=0): d = b"\x1e\xf1\xee\x0b\x00\x018\x00\x00\x00" d += b"|\x11\x01!rE!\x12\x02\x00EBL tradfri_light_basic\x00\x00\x00" d += b"\x00\x00\x00\x00\x00\x00" d += t.uint32_t(elements_size + 56).serialize() return d return data @pytest.fixture def raw_sub_element(): def data(tag_id, payload=b""): r = t.uint16_t(tag_id).serialize() r += t.uint32_t(len(payload)).serialize() return r + payload return data def test_ota_image(raw_header, raw_sub_element): el1_payload = b"abcd" el2_payload = b"4321" el1 = raw_sub_element(0, el1_payload) el2 = raw_sub_element(1, el2_payload) extra = b"edbc321" img, rest = firmware.OTAImage.deserialize( raw_header(len(el1 + el2)) + el1 + el2 + extra ) assert rest == extra assert len(img.subelements) == 2 assert img.subelements[0].tag_id == 0 assert img.subelements[0].data == el1_payload assert img.subelements[1].tag_id == 1 assert img.subelements[1].data == el2_payload assert img.serialize() == raw_header(len(el1 + el2)) + el1 + el2 with pytest.raises(ValueError): firmware.OTAImage.deserialize(raw_header(len(el1 + el2)) + el1 + el2[:-1]) def wrap_ikea(data): header = bytearray(100) header[0:4] = b"NGIS" header[16:20] = len(header).to_bytes(4, "little") header[20:24] = len(data).to_bytes(4, "little") return header + data + b"F" * 512 def test_parse_ota_normal(image): assert firmware.parse_ota_image(image.serialize()) == (image, b"") def test_parse_ota_ikea(image): data = wrap_ikea(image.serialize()) assert firmware.parse_ota_image(data) == (image, b"") def test_parse_ota_ikea_trailing(image): data = wrap_ikea(image.serialize() + b"trailing") parsed, remaining = firmware.parse_ota_image(data) assert not remaining assert parsed.header.image_size == len(image.serialize() + b"trailing") assert parsed.subelements[0].data == b"data" + b"trailing" parsed2, remaining2 = firmware.OTAImage.deserialize(parsed.serialize()) assert not remaining2 @pytest.mark.parametrize( "data", [ b"NGIS" + b"truncated", b"NGIS" + b"long enough to container header but not actual image", ], ) def test_parse_ota_ikea_truncated(data): with pytest.raises(ValueError): firmware.parse_ota_image(data) def create_hue_ota(data): data = b"\x2a\x00\x01" + data header, _ = firmware.OTAImageHeader.deserialize( bytes.fromhex( "1ef1ee0b0001380000000b100301d5670042020000000000000000000000000000000000000000" "0000000000000000000000000038f00300" ) ) header.image_size = len(header.serialize()) + len(data) return header.serialize() + data def test_parse_ota_hue(): data = create_hue_ota(b"test") + b"rest" img, rest = firmware.parse_ota_image(data) assert isinstance(img, firmware.HueSBLOTAImage) assert rest == b"rest" assert img.data == b"\x2a\x00\x01" + b"test" assert img.serialize() + b"rest" == data def test_parse_ota_hue_invalid(): data = create_hue_ota(b"test") firmware.parse_ota_image(data) with pytest.raises(ValueError): firmware.parse_ota_image(data[:-1]) header, rest = firmware.OTAImageHeader.deserialize(data) assert data == header.serialize() + rest with pytest.raises(ValueError): # Three byte sequence must be the first thing after the header firmware.parse_ota_image(header.serialize() + b"\xff" + rest[1:]) with pytest.raises(ValueError): # Only Hue is known to use these images firmware.parse_ota_image(header.replace(manufacturer_id=12).serialize() + rest) def test_legrand_container_unwrapping(image): # Unwrapped size prefix and 1 + 16 byte suffix data = ( t.uint32_t(len(image.serialize())).serialize() + image.serialize() + b"\x01" + b"abcdabcdabcdabcd" ) with pytest.raises(ValueError): firmware.parse_ota_image(data[:-1]) with pytest.raises(ValueError): firmware.parse_ota_image(b"\xff" + data[1:]) img, rest = firmware.parse_ota_image(data) assert not rest assert img == image def test_thirdreality_container(image): image_bytes = image.serialize() # There's little useful information in the header subcontainer = ( t.uint32_t(16).serialize() # Total length of image, excluding SHA512 prefix + t.uint32_t(len(image_bytes) + 152 - 64).serialize() + t.uint32_t(152).serialize() # Unknown four byte prefix/suffix and what looks like a second SHA512 hash + b"?" * (64 + 4) + t.uint32_t(0).serialize() + t.uint32_t(0).serialize() + image_bytes ) data = hashlib.sha512(subcontainer).digest() + subcontainer assert data.index(image_bytes) == 152 img, rest = firmware.parse_ota_image(data) assert not rest assert img == image with pytest.raises(ValueError): firmware.parse_ota_image(data[:-1]) with pytest.raises(ValueError): firmware.parse_ota_image(b"\xff" + data[1:]) zigpy-0.80.1/tests/ota/test_ota_manager.py000066400000000000000000000670751501451476000205770ustar00rootroot00000000000000import itertools from unittest.mock import AsyncMock, MagicMock, call, patch import pytest from tests.conftest import add_initialized_device, make_app, make_node_desc from tests.ota.test_ota_metadata import image_with_metadata # noqa: F401 import zigpy.application import zigpy.device import zigpy.exceptions from zigpy.exceptions import DeliveryError from zigpy.ota import OtaImageWithMetadata import zigpy.ota.image from zigpy.ota.manager import update_firmware import zigpy.state import zigpy.types as t import zigpy.util from zigpy.zcl import foundation from zigpy.zcl.clusters import Cluster from zigpy.zcl.clusters.general import Ota from zigpy.zdo import types as zdo_t import zigpy.zdo.types as zdo_t def lcg(*, x: int = 0, a: int, c: int, m: int): while True: x = (a * x + c) % m yield x FW_IMAGE = zigpy.ota.OtaImageWithMetadata( metadata=zigpy.ota.providers.BaseOtaImageMetadata( file_version=0x12345678, manufacturer_id=0x1234, image_type=0x90, ), firmware=zigpy.ota.image.OTAImage( header=zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=0x12345678, image_type=0x90, manufacturer_id=0x1234, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=2048 + 56 + 2 + 4, ), subelements=[ zigpy.ota.image.SubElement( tag_id=0x0000, data=bytes( [ x & 0xFF for x in itertools.islice( lcg(x=1, a=16807, c=0, m=7**5), 2048, ) ] ), ) ], ), ) def make_packet(dev: zigpy.device.Device, cluster: Cluster, cmd_name: str, **kwargs): req_hdr, req_cmd = cluster._create_request( general=False, command_id=cluster.commands_by_name[cmd_name].id, schema=cluster.commands_by_name[cmd_name].schema, disable_default_response=False, direction=foundation.Direction.Client_to_Server, args=(), kwargs=kwargs, ) return t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=req_hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) @patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1) async def test_ota_manger_stall(image_with_metadata: OtaImageWithMetadata) -> None: img = image_with_metadata app = make_app({}) dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("00:11:22:33:44:55:66:77")) dev.node_desc = make_node_desc(logical_type=zdo_t.LogicalType.Router) dev.model = "model1" dev.manufacturer = "manufacturer1" ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = zigpy.profiles.zha.DeviceType.PUMP ota = ep.add_output_cluster(Ota.cluster_id) async def send_packet(packet: t.ZigbeePacket): assert img.firmware is not None if packet.cluster_id == Ota.cluster_id: hdr, cmd = ota.deserialize(packet.data.serialize()) if isinstance(cmd, Ota.ImageNotifyCommand): dev.application.packet_received( make_packet( dev, ota, "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=img.firmware.header.manufacturer_id, image_type=img.firmware.header.image_type, current_file_version=img.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance( cmd, Ota.ClientCommandDefs.query_next_image_response.schema ): # Do nothing, just let it time out pass dev.application.send_packet = AsyncMock(side_effect=send_packet) status = await dev.update_firmware(img) assert status == foundation.Status.TIMEOUT @patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1) async def test_ota_manger_device_reject( image_with_metadata: OtaImageWithMetadata, ) -> None: img = image_with_metadata app = make_app({}) dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("00:11:22:33:44:55:66:77")) dev.node_desc = make_node_desc(logical_type=zdo_t.LogicalType.Router) dev.model = "model1" dev.manufacturer = "manufacturer1" ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = zigpy.profiles.zha.DeviceType.PUMP ota = ep.add_output_cluster(Ota.cluster_id) async def send_packet(packet: t.ZigbeePacket): assert img.firmware is not None if packet.cluster_id == Ota.cluster_id: hdr, cmd = ota.deserialize(packet.data.serialize()) if isinstance(cmd, Ota.ImageNotifyCommand): dev.application.packet_received( make_packet( dev, ota, "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=img.firmware.header.manufacturer_id, image_type=img.firmware.header.image_type, # We claim our current version is higher than the file version current_file_version=img.firmware.header.file_version + 10, hardware_version=1, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) status = await dev.update_firmware(img) assert status == foundation.Status.NO_IMAGE_AVAILABLE async def test_ota_manager(): """Test that device firmware updates execute the expected calls.""" app = make_app({}) dev = add_initialized_device( app, nwk=0x1234, ieee=t.EUI64.convert("00:11:22:33:44:55:66:77") ) cluster = dev.endpoints[1].add_output_cluster(Ota.cluster_id) await dev.initialize() # Stop the general cluster handler from interfering dev.ota_in_progress = True reconstructed_firmware = bytearray() async def send_packet(packet: t.ZigbeePacket): if packet.cluster_id != Ota.cluster_id: return hdr, cmd = cluster.deserialize(packet.data.serialize()) assert FW_IMAGE.firmware is not None if isinstance(cmd, Ota.ImageNotifyCommand): assert cmd.query_jitter == 100 # Ask for the next image dev.application.packet_received( make_packet( dev, cluster, "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, current_file_version=FW_IMAGE.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.query_next_image_response.schema): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert cmd.image_size == FW_IMAGE.firmware.header.image_size # Ask for the first block to get things started dev.application.packet_received( make_packet( dev, cluster, "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, file_offset=0, maximum_data_size=40, request_node_addr=dev.ieee, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.image_block_response.schema): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert len(cmd.image_data) > 0 reconstructed_firmware[ cmd.file_offset : cmd.file_offset + len(cmd.image_data) ] = cmd.image_data if cmd.file_offset + len(cmd.image_data) == len( FW_IMAGE.firmware.serialize() ): # End the upgrade dev.application.packet_received( make_packet( dev, cluster, "upgrade_end", status=foundation.Status.SUCCESS, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, ) ) else: # Keep going dev.application.packet_received( make_packet( dev, cluster, "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, file_offset=cmd.file_offset + 40, maximum_data_size=40, request_node_addr=dev.ieee, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.upgrade_end_response.schema): assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert cmd.current_time == 0 assert cmd.upgrade_time == 0 elif isinstance( cmd, foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].schema, ): assert cmd.attribute_ids == [Ota.AttributeDefs.current_file_version.id] req_hdr, req_cmd = cluster._create_request( general=True, command_id=foundation.GeneralCommand.Read_Attributes_rsp, schema=foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes_rsp ].schema, tsn=hdr.tsn, disable_default_response=True, direction=foundation.Direction.Server_to_Client, args=(), kwargs={ "status_records": [ foundation.ReadAttributeRecord( attrid=Ota.AttributeDefs.current_file_version.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DATA_TYPES.pytype_to_datatype_id( t.uint32_t ), value=FW_IMAGE.firmware.header.file_version, ), ) ] }, ) dev.application.packet_received( t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback = MagicMock() result = await update_firmware(dev, FW_IMAGE, progress_callback) image_size = FW_IMAGE.firmware.header.image_size assert progress_callback.mock_calls == [ call(i, image_size, pytest.approx(i * 100 / image_size)) for i in range(40, image_size + 1, 40) ] + [call(image_size, image_size, 100.0)] assert result == foundation.Status.SUCCESS assert bytes(reconstructed_firmware) == FW_IMAGE.firmware.serialize() async def test_ota_manager_image_page(): """Test that device firmware updates execute the expected calls.""" app = make_app({}) dev = add_initialized_device( app, nwk=0x1234, ieee=t.EUI64.convert("00:11:22:33:44:55:66:77") ) cluster = dev.endpoints[1].add_output_cluster(Ota.cluster_id) await dev.initialize() # Stop the general cluster handler from interfering dev.ota_in_progress = True reconstructed_firmware = bytearray() async def send_packet(packet: t.ZigbeePacket): if packet.cluster_id != Ota.cluster_id: return hdr, cmd = cluster.deserialize(packet.data.serialize()) assert FW_IMAGE.firmware is not None if isinstance(cmd, Ota.ImageNotifyCommand): assert cmd.query_jitter == 100 # Ask for the next image dev.application.packet_received( make_packet( dev, cluster, "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, current_file_version=FW_IMAGE.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.query_next_image_response.schema): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert cmd.image_size == FW_IMAGE.firmware.header.image_size # Ask for the first page to get things started dev.application.packet_received( make_packet( dev, cluster, "image_page", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, file_offset=0, maximum_data_size=5, page_size=40, response_spacing=0, request_node_addr=dev.ieee, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.image_block_response.schema): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert len(cmd.image_data) > 0 if cmd.file_offset + len(cmd.image_data) > len(reconstructed_firmware): reconstructed_firmware.extend( b"\x00" * ( cmd.file_offset + len(cmd.image_data) - len(reconstructed_firmware) ) ) reconstructed_firmware[ cmd.file_offset : cmd.file_offset + len(cmd.image_data) ] = cmd.image_data if cmd.file_offset + len(cmd.image_data) == len( FW_IMAGE.firmware.serialize() ): # End the upgrade dev.application.packet_received( make_packet( dev, cluster, "upgrade_end", status=foundation.Status.SUCCESS, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, ) ) else: current_page_start = (cmd.file_offset // 40) * 40 current_page = reconstructed_firmware[ current_page_start : current_page_start + 40 ] # Only ask for another page if the current one has been filled if ( current_page_start + 40 >= len(FW_IMAGE.firmware.serialize()) and len(current_page) == len(FW_IMAGE.firmware.serialize()) - current_page_start ) or len(current_page) == 40: # Keep going dev.application.packet_received( make_packet( dev, cluster, "image_page", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, file_offset=cmd.file_offset + 5, maximum_data_size=5, page_size=40, response_spacing=0, request_node_addr=dev.ieee, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.upgrade_end_response.schema): assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert cmd.current_time == 0 assert cmd.upgrade_time == 0 elif isinstance( cmd, foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].schema, ): assert cmd.attribute_ids == [Ota.AttributeDefs.current_file_version.id] req_hdr, req_cmd = cluster._create_request( general=True, command_id=foundation.GeneralCommand.Read_Attributes_rsp, schema=foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes_rsp ].schema, tsn=hdr.tsn, disable_default_response=True, direction=foundation.Direction.Server_to_Client, args=(), kwargs={ "status_records": [ foundation.ReadAttributeRecord( attrid=Ota.AttributeDefs.current_file_version.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DATA_TYPES.pytype_to_datatype_id( t.uint32_t ), value=FW_IMAGE.firmware.header.file_version, ), ) ] }, ) dev.application.packet_received( t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback = MagicMock() result = await update_firmware(dev, FW_IMAGE, progress_callback) assert result == foundation.Status.SUCCESS image_size = FW_IMAGE.firmware.header.image_size assert progress_callback.mock_calls == [ call(i, image_size, pytest.approx(i / image_size * 100)) for i in range(5, image_size + 1, 5) ] async def test_ota_manager_image_page_invalid_size(): """Test that the OTA manager fails properly with invalid image page requests.""" app = make_app({}) dev = add_initialized_device( app, nwk=0x1234, ieee=t.EUI64.convert("00:11:22:33:44:55:66:77") ) cluster = dev.endpoints[1].add_output_cluster(Ota.cluster_id) await dev.initialize() # Stop the general cluster handler from interfering dev.ota_in_progress = True async def send_packet(packet: t.ZigbeePacket): if packet.cluster_id != Ota.cluster_id: return hdr, cmd = cluster.deserialize(packet.data.serialize()) assert FW_IMAGE.firmware is not None if isinstance(cmd, Ota.ImageNotifyCommand): assert cmd.query_jitter == 100 # Ask for the next image dev.application.packet_received( make_packet( dev, cluster, "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, current_file_version=FW_IMAGE.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.query_next_image_response.schema): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert cmd.image_size == FW_IMAGE.firmware.header.image_size # Ask for the first page to get things started dev.application.packet_received( make_packet( dev, cluster, "image_page", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, file_offset=FW_IMAGE.firmware.header.image_size, maximum_data_size=5, page_size=40, response_spacing=0, request_node_addr=dev.ieee, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback = MagicMock() result = await update_firmware(dev, FW_IMAGE, progress_callback) assert result == foundation.Status.MALFORMED_COMMAND @patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1) async def test_ota_manager_image_page_failure(): """Test that the OTA manager fails properly with invalid image page requests.""" app = make_app({}) dev = add_initialized_device( app, nwk=0x1234, ieee=t.EUI64.convert("00:11:22:33:44:55:66:77") ) cluster = dev.endpoints[1].add_output_cluster(Ota.cluster_id) await dev.initialize() # Stop the general cluster handler from interfering dev.ota_in_progress = True start_failing = False async def send_packet(packet: t.ZigbeePacket): nonlocal start_failing if start_failing: raise DeliveryError("Broken") if packet.cluster_id != Ota.cluster_id: return hdr, cmd = cluster.deserialize(packet.data.serialize()) assert FW_IMAGE.firmware is not None if isinstance(cmd, Ota.ImageNotifyCommand): assert cmd.query_jitter == 100 # Ask for the next image dev.application.packet_received( make_packet( dev, cluster, "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, current_file_version=FW_IMAGE.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.query_next_image_response.schema): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == FW_IMAGE.firmware.header.manufacturer_id assert cmd.image_type == FW_IMAGE.firmware.header.image_type assert cmd.file_version == FW_IMAGE.firmware.header.file_version assert cmd.image_size == FW_IMAGE.firmware.header.image_size # Ask for the first page to get things started dev.application.packet_received( make_packet( dev, cluster, "image_page", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=FW_IMAGE.firmware.header.manufacturer_id, image_type=FW_IMAGE.firmware.header.image_type, file_version=FW_IMAGE.firmware.header.file_version, file_offset=0, maximum_data_size=5, page_size=40, response_spacing=0, request_node_addr=dev.ieee, ) ) start_failing = True dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback = MagicMock() result = await update_firmware(dev, FW_IMAGE, progress_callback) assert result != foundation.Status.SUCCESS zigpy-0.80.1/tests/ota/test_ota_matching.py000066400000000000000000000342351501451476000207470ustar00rootroot00000000000000from __future__ import annotations import asyncio import pathlib import typing from unittest.mock import patch import aiohttp import attrs from tests.ota.test_ota_providers import SelfContainedOtaImageMetadata, make_device from zigpy import config import zigpy.device import zigpy.ota from zigpy.ota.image import FieldControl from zigpy.ota.providers import BaseOtaImageMetadata, BaseOtaProvider from zigpy.zcl.clusters.general import Ota class SelfContainedProvider(BaseOtaProvider): def __init__( self, index: list[SelfContainedOtaImageMetadata], load_index_delay: float = 0 ) -> None: super().__init__() self._index = index self._load_index_delay = load_index_delay def compatible_with_device(self, device: zigpy.device.Device) -> bool: return True async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: await asyncio.sleep(self._load_index_delay) for meta in self._index: yield meta class BrokenProvider(SelfContainedProvider): async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: if False: yield raise Exception("Broken provider") @attrs.define(frozen=True, kw_only=True) class BrokenOtaImageMetadata(BaseOtaImageMetadata): async def _fetch(self) -> bytes: raise RuntimeError("Some problem") async def test_ota_matching_priority(tmp_path: pathlib.Path) -> None: device = make_device(model="device model", manufacturer_id=0x1234) query_cmd = Ota.ServerCommandDefs.query_next_image.schema( field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, manufacturer_code=0x1234, image_type=0xABCD, current_file_version=1, hardware_version=1, ) ota_hdr = zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, manufacturer_id=query_cmd.manufacturer_code, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 8, ) ota_subelements = [zigpy.ota.image.SubElement(tag_id=0x0000, data=b"fw_image")] index = [ # Manufacturer ID SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=ota_subelements, ).serialize(), ), # Image type SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=ota_subelements, ).serialize(), ), # Model string SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, model_names=(device.model,), test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=ota_subelements, ).serialize(), ), # Model string *and* more specific HW version: this is the right image to pick SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, model_names=(device.model,), test_data=zigpy.ota.image.OTAImage( header=ota_hdr.replace( minimum_hardware_version=1, maximum_hardware_version=1, ), subelements=ota_subelements, ).serialize(), ), # Nothing to exclude but we can't be sure SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=ota_subelements, ).serialize(), ), # Irrelevant image SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version - 1, test_data=zigpy.ota.image.OTAImage( header=ota_hdr.replace(file_version=query_cmd.current_file_version - 1), subelements=ota_subelements, ).serialize(), ), # Broken image that won't download BrokenOtaImageMetadata( file_version=query_cmd.current_file_version + 1, ), ] ota = zigpy.ota.OTA(config={config.CONF_OTA_ENABLED: False}, application=None) ota.register_provider(BrokenProvider(index)) ota.register_provider(SelfContainedProvider(index)) ota.register_provider(BrokenProvider(index)) images1 = await ota.get_ota_images(device, query_cmd) # The image that will be chosen is the correct one, others with less specificity # will still be present but they will be deprioritized assert images1.upgrades[0] == zigpy.ota.OtaImageWithMetadata( metadata=index[3], firmware=zigpy.ota.image.OTAImage.deserialize(index[3].test_data)[0], ) images2 = await ota.get_ota_images(device, query_cmd) assert images2 == images1 async def test_ota_matching_ambiguous_error() -> None: device = make_device(model="device model", manufacturer_id=0x1234) query_cmd = Ota.ServerCommandDefs.query_next_image.schema( field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, manufacturer_code=0x1234, image_type=0xABCD, current_file_version=1, hardware_version=1, ) ota_hdr = zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, manufacturer_id=query_cmd.manufacturer_code, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 10, ) index = [ SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 1") ], ).serialize(), ), SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 2") ], ).serialize(), ), ] ota = zigpy.ota.OTA(config={config.CONF_OTA_ENABLED: False}, application=None) ota.register_provider(SelfContainedProvider(index)) # No image will be provided if there is ambiguity images = await ota.get_ota_images(device, query_cmd) assert not images.upgrades async def test_ota_matching_ambiguous_specificity_tie_breaker() -> None: device = make_device(model="device model", manufacturer_id=0x1234) query_cmd = Ota.ServerCommandDefs.query_next_image.schema( field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, manufacturer_code=0x1234, image_type=0xABCD, current_file_version=1, hardware_version=1, ) ota_hdr = zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, manufacturer_id=query_cmd.manufacturer_code, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 10, ) index = [ SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 1") ], ).serialize(), ), SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr, subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 2") ], ).serialize(), # Break the tie by boosting the image's specificity specificity=1, ), ] ota = zigpy.ota.OTA(config={config.CONF_OTA_ENABLED: False}, application=None) ota.register_provider(SelfContainedProvider(index)) # No image will be provided if there is ambiguity but specificity is enough to break # the tie images = await ota.get_ota_images(device, query_cmd) assert len(images.upgrades) == 2 assert images.upgrades[0] == zigpy.ota.OtaImageWithMetadata( metadata=index[1], firmware=zigpy.ota.image.OTAImage.deserialize(index[1].test_data)[0], ) async def test_ota_concurrent_fetching() -> None: device = make_device(model="device model", manufacturer_id=0x1234) query_cmd = Ota.ServerCommandDefs.query_next_image.schema( field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, manufacturer_code=0x1234, image_type=0xABCD, current_file_version=1, hardware_version=1, ) index = [ SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, manufacturer_id=query_cmd.manufacturer_code, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 10, ), subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 1") ], ).serialize(), ) ] provider = SelfContainedProvider(index, load_index_delay=0.1) ota = zigpy.ota.OTA(config={config.CONF_OTA_ENABLED: False}, application=None) ota.register_provider(provider) with patch.object( provider, "_load_index", wraps=provider._load_index ) as load_index: images1, images2 = await asyncio.gather( ota.get_ota_images(device, query_cmd), ota.get_ota_images(device, query_cmd), ) # Concurrent requests were combined assert len(load_index.mock_calls) == 1 assert images1 == images2 async def test_ota_matching_hardware_version_changes_after_download() -> None: device = make_device(model="device model", manufacturer_id=0x1234) query_cmd = Ota.ServerCommandDefs.query_next_image.schema( field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, manufacturer_code=0x1234, image_type=0xABCD, current_file_version=1, hardware_version=1, ) ota_hdr_01 = zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, manufacturer_id=query_cmd.manufacturer_code, header_version=256, header_length=60, field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, minimum_hardware_version=0, maximum_hardware_version=1, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 4 + 10, ) ota_hdr_27 = zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=query_cmd.current_file_version + 1, image_type=query_cmd.image_type, manufacturer_id=query_cmd.manufacturer_code, header_version=256, header_length=60, field_control=FieldControl.HARDWARE_VERSIONS_PRESENT, minimum_hardware_version=2, maximum_hardware_version=7, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 4 + 10, ) index = [ SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr_01, subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 1") ], ).serialize(), ), SelfContainedOtaImageMetadata( file_version=query_cmd.current_file_version + 1, manufacturer_id=query_cmd.manufacturer_code, test_data=zigpy.ota.image.OTAImage( header=ota_hdr_27, subelements=[ zigpy.ota.image.SubElement(tag_id=0x0000, data=b"Firmware 2") ], ).serialize(), ), ] ota = zigpy.ota.OTA(config={config.CONF_OTA_ENABLED: False}, application=None) ota.register_provider(SelfContainedProvider(index)) # Only the first image is considered images = await ota.get_ota_images(device, query_cmd) assert images.upgrades == ( zigpy.ota.OtaImageWithMetadata( metadata=index[0], firmware=zigpy.ota.image.OTAImage.deserialize(index[0].test_data)[0], ), ) zigpy-0.80.1/tests/ota/test_ota_metadata.py000066400000000000000000000156231501451476000207350ustar00rootroot00000000000000import hashlib from unittest.mock import AsyncMock, patch import pytest from tests.conftest import make_app from zigpy.ota import OtaImageWithMetadata import zigpy.ota.image from zigpy.ota.providers import BaseOtaImageMetadata from zigpy.zcl.clusters.general import Ota @pytest.fixture def image_with_metadata() -> OtaImageWithMetadata: firmware = zigpy.ota.image.OTAImage( header=zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=0x12345678, image_type=0x5678, manufacturer_id=0x1234, header_version=256, header_length=60, field_control=zigpy.ota.image.FieldControl.HARDWARE_VERSIONS_PRESENT, minimum_hardware_version=1, maximum_hardware_version=5, stack_version=2, header_string="This is a test header!", image_size=60 + 2 + 4 + 8, ), subelements=[zigpy.ota.image.SubElement(tag_id=0x0000, data=b"fw_image")], ) metadata = BaseOtaImageMetadata( file_version=0x12345678, manufacturer_id=0x1234, image_type=0x5678, checksum="sha256:" + hashlib.sha256(firmware.serialize()).hexdigest(), file_size=len(firmware.serialize()), manufacturer_names=("manufacturer1", "manufacturer2"), model_names=("model1", "model2"), changelog="Some simple changelog", min_hardware_version=1, max_hardware_version=5, min_current_file_version=0x12345678 - 10, max_current_file_version=0x12345678 - 2, specificity=0, ) return OtaImageWithMetadata(metadata=metadata, firmware=firmware) def test_ota_mirrored_metadata(image_with_metadata: OtaImageWithMetadata) -> None: assert image_with_metadata._min_hardware_version == 1 assert image_with_metadata._max_hardware_version == 5 assert image_with_metadata._manufacturer_id == 0x1234 assert image_with_metadata._image_type == 0x5678 # Metadata info is preferred so the firmware file itself isn't necessary image_with_no_firmware = image_with_metadata.replace(firmware=None) assert image_with_no_firmware._min_hardware_version == 1 assert image_with_no_firmware._max_hardware_version == 5 assert image_with_no_firmware._manufacturer_id == 0x1234 assert image_with_no_firmware._image_type == 0x5678 # But we can use it image_with_no_metadata_hw_versions = image_with_metadata.replace( metadata=image_with_metadata.metadata.replace( min_hardware_version=None, max_hardware_version=None, manufacturer_id=None, image_type=None, ) ) assert image_with_no_metadata_hw_versions._min_hardware_version == 1 assert image_with_no_metadata_hw_versions._max_hardware_version == 5 assert image_with_no_metadata_hw_versions._manufacturer_id == 0x1234 assert image_with_no_metadata_hw_versions._image_type == 0x5678 # Only if all are missing will the properties be `None` image_with_no_hw_versions = image_with_metadata.replace( metadata=image_with_metadata.metadata.replace( min_hardware_version=None, max_hardware_version=None, manufacturer_id=None, image_type=None, ), firmware=None, ) assert image_with_no_hw_versions._min_hardware_version is None assert image_with_no_hw_versions._max_hardware_version is None assert image_with_no_hw_versions._manufacturer_id is None assert image_with_no_hw_versions._image_type is None def test_metadata_specificity(image_with_metadata: OtaImageWithMetadata) -> None: def replace_meta(**kwargs): return image_with_metadata.replace( metadata=image_with_metadata.metadata.replace(**kwargs) ) # If we lose useful metadata, the specificity decreases assert ( 0 < replace_meta(manufacturer_names=(), model_names=()).specificity < replace_meta(manufacturer_names=()).specificity < replace_meta(max_current_file_version=None).specificity < image_with_metadata.specificity ) async def test_metadata_compatibility( image_with_metadata: OtaImageWithMetadata, make_initialized_device, ) -> None: app = make_app({}) await app.initialize() dev = make_initialized_device(app) dev.model = "model1" dev.manufacturer = "manufacturer1" assert image_with_metadata.version == 0x12345678 query_cmd = Ota.ServerCommandDefs.query_next_image.schema( field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=0x1234, image_type=0x5678, current_file_version=0x12345678 - 5, hardware_version=3, ) assert image_with_metadata.check_compatibility(dev, query_cmd) # The file version is ignored when checking compatibility assert image_with_metadata.check_compatibility( dev, query_cmd.replace(current_file_version=0x12345678) ) # The min and max current file versions are respected assert image_with_metadata.check_version(0x12345678 - 10) assert image_with_metadata.check_version(0x12345678 - 2) assert not image_with_metadata.check_version(0x12345678 - 11) assert not image_with_metadata.check_version(0x12345678 - 1) assert not image_with_metadata.check_version(0x12345678) assert not image_with_metadata.check_compatibility( dev, query_cmd.replace(image_type=0xAAAA) ) assert not image_with_metadata.check_compatibility( dev, query_cmd.replace(manufacturer_code=0xAAAA) ) with patch.object(dev, attribute="_model", new="model3"): assert not image_with_metadata.check_compatibility(dev, query_cmd) with patch.object(dev, attribute="_manufacturer", new="manufacturer3"): assert not image_with_metadata.check_compatibility(dev, query_cmd) assert not image_with_metadata.check_compatibility( dev, query_cmd.replace(hardware_version=0) ) assert not image_with_metadata.check_compatibility( dev, query_cmd.replace(hardware_version=100) ) # The image is super well-specified: if anything is missing, it becomes incompatible assert not image_with_metadata.check_compatibility( dev, query_cmd.replace( field_control=Ota.QueryNextImageCommand.FieldControl(0), hardware_version=None, ), ) await app.shutdown() async def test_metadata_fetch(image_with_metadata: OtaImageWithMetadata) -> None: image_without_firmware = image_with_metadata.replace(firmware=None) assert image_with_metadata.firmware is not None # Pretend we download the image contents object.__setattr__( image_without_firmware.metadata, "_fetch", AsyncMock(return_value=image_with_metadata.firmware.serialize()), ) # New image is identical new_img = await image_without_firmware.fetch() assert new_img == image_with_metadata zigpy-0.80.1/tests/ota/test_ota_providers.py000066400000000000000000000565351501451476000212010ustar00rootroot00000000000000from __future__ import annotations import asyncio import hashlib import json import pathlib from unittest.mock import Mock import aiohttp from aioresponses import aioresponses import attrs import pytest from tests.conftest import make_node_desc from tests.ota.test_ota_metadata import image_with_metadata # noqa: F401 import zigpy.device from zigpy.ota import OtaImageWithMetadata, providers import zigpy.types as t FILES_DIR = pathlib.Path(__file__).parent / "files" @pytest.fixture(scope="module", autouse=True) def download_external_files(): urls = json.loads((FILES_DIR / "external/urls.json").read_text()) for path, obj in urls.items(): path = FILES_DIR / "external" / path path.parent.mkdir(parents=True, exist_ok=True) if not path.is_file(): async def download(path: pathlib.Path = path, obj: dict = obj) -> None: async with aiohttp.ClientSession() as session: async with session.get( obj["url"], ssl=False, raise_for_status=True, ) as resp: data = await resp.read() path.write_bytes(data) asyncio.run(download()) algorithm, digest = obj["checksum"].split(":") assert hashlib.new(algorithm, path.read_bytes()).hexdigest() == digest def make_device( model: str | None = None, manufacturer: str | None = None, manufacturer_id: int | None = None, ) -> zigpy.device.Device: dev = zigpy.device.Device( application=Mock(), ieee=t.EUI64.convert("00:11:22:33:44:55:66:77"), nwk=0x1234, ) dev.node_desc = make_node_desc() if manufacturer_id is not None: dev.node_desc.manufacturer_code = manufacturer_id if model is not None: dev.model = model if manufacturer is not None: dev.manufacturer = manufacturer return dev @attrs.define(frozen=True, kw_only=True) class SelfContainedOtaImageMetadata(providers.BaseOtaImageMetadata): test_data: bytes async def _fetch(self) -> bytes: return self.test_data def _test_z2m_index_entry(obj: dict, meta: providers.BaseOtaImageMetadata) -> bool: assert meta.checksum == "sha512:" + obj.pop("sha512") assert meta.image_type == obj.pop("imageType") assert meta.file_size == obj.pop("fileSize") assert meta.file_version == obj.pop("fileVersion") assert meta.manufacturer_id == obj.pop("manufacturerCode") assert meta.min_current_file_version == obj.pop("minFileVersion", None) assert meta.max_current_file_version == obj.pop("maxFileVersion", None) assert meta.min_hardware_version == obj.pop("hardwareVersionMin", None) assert meta.max_hardware_version == obj.pop("hardwareVersionMax", None) assert meta.changelog == obj.pop("releaseNotes", None) if "modelId" in obj: assert meta.model_names == (obj.pop("modelId"),) else: assert meta.model_names == () if "manufacturerName" in obj: assert meta.manufacturer_names == tuple(obj.pop("manufacturerName")) else: assert meta.manufacturer_names == () return True async def test_local_z2m_provider(): index_json = (FILES_DIR / "z2m_index.json").read_text() index_obj = json.loads(index_json) provider = providers.LocalZ2MProvider(FILES_DIR / "z2m_index.json") # Test equality assert provider == providers.LocalZ2MProvider(FILES_DIR / "z2m_index.json") assert provider != providers.LocalZ2MProvider(FILES_DIR / "z2m_index2.json") assert provider != providers.LocalZigpyProvider(FILES_DIR / "z2m_index.json") # Compatible with all devices assert provider.compatible_with_device(make_device(manufacturer_id=1234)) assert provider.compatible_with_device(make_device(manufacturer_id=5678)) index = await provider.load_index() assert len(index) == len(index_obj) for obj, meta in zip(index_obj, index): assert _test_z2m_index_entry(obj, meta) if isinstance(meta, providers.RemoteOtaImageMetadata): assert meta.url == obj.pop("url") elif isinstance(meta, providers.LocalOtaImageMetadata): assert meta.path == FILES_DIR / obj.pop("path") obj.pop("url") else: pytest.fail(f"Unexpected metadata type: {meta!r}") obj.pop("fileName", None) obj.pop("otaHeaderString", None) obj.pop("originalUrl", None) assert not obj async def test_remote_z2m_provider(): index_json = (FILES_DIR / "z2m_index.json").read_text() index_obj = json.loads(index_json) index_url = "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json" provider = providers.RemoteZ2MProvider(index_url) # Compatible with all devices assert provider.compatible_with_device(make_device(manufacturer_id=1234)) assert provider.compatible_with_device(make_device(manufacturer_id=5678)) with aioresponses() as mock_http: mock_http.get( index_url, body=index_json, content_type="text/plain; charset=utf-8", ) index = await provider.load_index() assert len(index) == len(index_obj) for obj, meta in zip(index_obj, index): assert _test_z2m_index_entry(obj, meta) assert isinstance(meta, providers.RemoteOtaImageMetadata) assert meta.url == obj.pop("url") obj.pop("path", None) obj.pop("fileName", None) obj.pop("otaHeaderString", None) obj.pop("originalUrl", None) assert not obj async def test_tradfri_provider_dirigera(): index_json = (FILES_DIR / "ikea_version_info_dirigera.json").read_text() index_obj = json.loads(index_json) provider = providers.Tradfri() # Compatible only with IKEA devices assert provider.compatible_with_device(make_device(manufacturer_id=4476)) assert not provider.compatible_with_device(make_device(manufacturer_id=4477)) with aioresponses() as mock_http: mock_http.get( "https://fw.ota.homesmart.ikea.com/DIRIGERA/version_info.json", headers={"Location": "https://fw.ota.homesmart.ikea.com/check/update/prod"}, status=302, ) mock_http.get( "https://fw.ota.homesmart.ikea.com/check/update/prod", body=index_json, content_type="application/json", ) index = await provider.load_index() # The provider will not allow itself to be loaded a second time this quickly with aioresponses() as mock_http: assert (await provider.load_index()) is None mock_http.assert_not_called() # Skip the gateway firmware filtered_version_info_obj = [ obj for obj in index_obj if obj["fw_type"] == 2 and obj["fw_image_type"] not in (8710, 8704) ] assert len(index) == len(index_obj) - 3 == len(filtered_version_info_obj) for obj, meta in zip(filtered_version_info_obj, index): assert isinstance(meta, providers.RemoteOtaImageMetadata) assert meta.file_version == int( obj["fw_binary_url"].split("_v", 1)[1].split("_", 1)[0] ) assert meta.image_type == obj.pop("fw_image_type") assert meta.checksum == "sha3-256:" + obj.pop("fw_sha3_256") assert meta.url == obj.pop("fw_binary_url") assert meta.manufacturer_id == providers.Tradfri.MANUFACTURER_IDS[0] == 4476 obj.pop("fw_type") assert not obj meta = index[0] assert meta.image_type == 10242 ota_contents = ( FILES_DIR / "external/dl/ikea/mgm210l-light-cws-cv-rgbw_release_prod_v268572245_3ae78af7-14fd-44df-bca2-6d366f2e9d02.ota" ).read_bytes() with aioresponses() as mock_http: mock_http.get( meta.url, body=ota_contents, content_type="binary/octet-stream", ) img = await meta.fetch() assert img.serialize() == ota_contents @pytest.mark.parametrize( ("index_url", "index_file"), [ ( "http://fw.ota.homesmart.ikea.net/feed/version_info.json", "ikea_version_info_old.json", ), ( "http://fw.test.ota.homesmart.ikea.net/feed/version_info.json", "ikea_version_info_old_test.json", ), ], ) async def test_tradfri_provider_old(index_url: str, index_file: str) -> None: index_json = (FILES_DIR / index_file).read_text() index_obj = json.loads(index_json) provider = providers.Tradfri(index_url) # Compatible only with IKEA devices assert provider.compatible_with_device(make_device(manufacturer_id=4476)) assert not provider.compatible_with_device(make_device(manufacturer_id=4477)) with aioresponses() as mock_http: mock_http.get(index_url, body=index_json, content_type="application/json") index = await provider.load_index() # The provider will not allow itself to be loaded a second time this quickly with aioresponses() as mock_http: assert (await provider.load_index()) is None mock_http.assert_not_called() # Skip the gateway firmware filtered_version_info_obj = [ obj for obj in index_obj if obj["fw_type"] == 2 and obj["fw_image_type"] not in (8710, 8704) ] assert index assert len(index) == len(filtered_version_info_obj) for obj, meta in zip(filtered_version_info_obj, index): assert isinstance(meta, providers.RemoteOtaImageMetadata) assert meta.file_version == ( (obj.pop("fw_file_version_MSB") << 16) | (obj.pop("fw_file_version_LSB") << 0) ) assert meta.manufacturer_id == obj.pop("fw_manufacturer_id") assert meta.image_type == obj.pop("fw_image_type") assert meta.file_size == obj.pop("fw_filesize") assert meta.url == obj.pop("fw_binary_url").replace("http://", "https://", 1) obj.pop("fw_type") assert not obj # Pick one of the images common to both feeds meta = next(m for m in index if "TRADFRI-motion-sensor-2-" in m.url) assert meta.image_type == 4552 ota_contents = ( FILES_DIR / "external/dl/ikea/10039874-1.0-TRADFRI-motion-sensor-2-2.0.022.ota.ota.signed" ).read_bytes() with aioresponses() as mock_http: mock_http.get( meta.url, body=ota_contents, content_type="binary/octet-stream", ) img = await meta.fetch() assert img.serialize() in ota_contents async def test_tradfri_provider_bad_image() -> None: index_json = (FILES_DIR / "ikea_version_info_old.json").read_text() provider = providers.Tradfri( "http://fw.ota.homesmart.ikea.net/feed/version_info.json" ) with aioresponses() as mock_http: mock_http.get( "http://fw.ota.homesmart.ikea.net/feed/version_info.json", body=index_json, content_type="application/json", ) index = await provider.load_index() assert index is not None meta = next(m for m in index if "TRADFRI-motion-sensor-2-" in m.url) assert meta.image_type == 4552 ota_contents = ( FILES_DIR / "external/dl/ikea/10039874-1.0-TRADFRI-motion-sensor-2-2.0.022.ota.ota.signed" ).read_bytes() # Flip a bit with aioresponses() as mock_http: flipped_contents = bytearray(ota_contents) flipped_contents[50000] ^= 0b00010000 mock_http.get( meta.url, body=bytes(flipped_contents), content_type="binary/octet-stream", ) with pytest.raises(ValueError, match="Block 3 has invalid checksum"): await meta.fetch() # Mess with the header with aioresponses() as mock_http: bad_contents = bytearray(ota_contents) bad_contents[0:4] = b" None: files = list((FILES_DIR / "external/dl/local_provider").glob("[!.]*")) files.sort(key=lambda f: f.name) (tmp_path / "foo/bar").mkdir(parents=True) (tmp_path / "foo/bar" / files[0].name).write_bytes(files[0].read_bytes()) (tmp_path / "foo" / files[1].name).write_bytes(files[1].read_bytes()) (tmp_path / "empty").mkdir(parents=True) (tmp_path / "bad.ota").write_bytes(b"This is not an OTA file") provider = providers.AdvancedFileProvider(tmp_path) # Test equality assert provider == providers.AdvancedFileProvider(tmp_path) assert provider != providers.AdvancedFileProvider(tmp_path / "foo") assert provider != providers.LocalZigpyProvider(tmp_path) # The provider is compatible with all devices assert provider.compatible_with_device(make_device(manufacturer_id=4476)) assert provider.compatible_with_device(make_device(manufacturer_id=4454)) index = await provider.load_index() assert index is not None index.sort(key=lambda m: m.path.name) assert len(index) == len(files) for path, meta in zip(files, index): data = path.read_bytes() assert isinstance(meta, providers.LocalOtaImageMetadata) assert meta.path.name == path.name assert meta.checksum == "sha1:" + hashlib.sha1(data).hexdigest() assert meta.file_size == len(data) fw = await meta.fetch() assert fw.serialize() == data async def test_ota_fetch_size_and_checksum_validation( image_with_metadata: OtaImageWithMetadata, ) -> None: assert image_with_metadata.firmware is not None meta = SelfContainedOtaImageMetadata( file_version=image_with_metadata.metadata.file_version, checksum=image_with_metadata.metadata.checksum, file_size=image_with_metadata.metadata.file_size, test_data=image_with_metadata.firmware.serialize(), ) fw = await meta.fetch() assert fw == image_with_metadata.firmware with pytest.raises(ValueError): await meta.replace(file_size=meta.file_size + 1).fetch() assert meta.checksum is not None assert not meta.checksum.endswith("c") with pytest.raises(ValueError): await meta.replace(checksum=meta.checksum[:-1] + "c").fetch() zigpy-0.80.1/tests/ota/test_ota_validators.py000066400000000000000000000173161501451476000213260ustar00rootroot00000000000000from unittest import mock import zlib import pytest from zigpy.ota import validators from zigpy.ota.image import ElementTagId, OTAImage, SubElement from zigpy.ota.validators import ValidationError, ValidationResult def create_ebl_image(tags): # All images start with a 140-byte "0x0000" header tags = [(b"\x00\x00", b"jklm" * 35), *tags] assert all(len(tag) == 2 for tag, value in tags) image = b"".join(tag + len(value).to_bytes(2, "big") + value for tag, value in tags) # And end with a checksum image += b"\xfc\x04\x00\x04" + zlib.crc32(image + b"\xfc\x04\x00\x04").to_bytes( 4, "little" ) if len(image) % 64 != 0: image += b"\xff" * (64 - len(image) % 64) assert list(validators.parse_silabs_ebl(image)) return image def create_gbl_image(tags): # All images start with an 8-byte header tags = [(b"\xeb\x17\xa6\x03", b"\x00\x00\x00\x03\x01\x01\x00\x00"), *tags] assert all(len(tag) == 4 for tag, value in tags) image = b"".join( tag + len(value).to_bytes(4, "little") + value for tag, value in tags ) # And end with a checksum image += (b"\xfc\x04\x04\xfc" b"\x04\x00\x00\x00") + zlib.crc32( image + b"\xfc\x04\x04\xfc" + b"\x04\x00\x00\x00" ).to_bytes(4, "little") assert list(validators.parse_silabs_gbl(image)) return image VALID_EBL_IMAGE = create_ebl_image([(b"ab", b"foo")]) VALID_GBL_IMAGE = create_gbl_image([(b"test", b"foo")]) def create_subelement(tag_id, value): return SubElement.deserialize( tag_id.serialize() + len(value).to_bytes(4, "little") + value )[0] def test_parse_silabs_ebl(): list(validators.parse_silabs_ebl(VALID_EBL_IMAGE)) image = create_ebl_image([(b"AA", b"test"), (b"BB", b"foo" * 20)]) header, tag1, tag2, checksum = validators.parse_silabs_ebl(image) assert len(image) % 64 == 0 assert header[0] == b"\x00\x00" and len(header[1]) == 140 assert tag1 == (b"AA", b"test") assert tag2 == (b"BB", b"foo" * 20) assert checksum[0] == b"\xfc\x04" and len(checksum[1]) == 4 # Padding needs to be a multiple of 64 bytes with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(image[:-1])) with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(image + b"\xff")) # Nothing can come after the padding assert list(validators.parse_silabs_ebl(image[:-1] + b"\xff")) with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(image[:-1] + b"\xab")) # Truncated images are detected with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(image[: image.index(b"test")] + b"\xff" * 44)) # As are corrupted images of the correct length but with bad tag lengths index = image.index(b"test") bad_image = image[: index - 2] + b"\xff\xff" + image[index:] with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(bad_image)) # Truncated but at a 64-byte boundary, missing CRC footer bad_image = create_ebl_image([(b"AA", b"test" * 11)]) bad_image = bad_image[: bad_image.rindex(b"test") + 4] with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(bad_image)) # Corrupted images are detected corrupted_image = image.replace(b"foo", b"goo", 1) assert image != corrupted_image with pytest.raises(ValidationError): list(validators.parse_silabs_ebl(corrupted_image)) def test_parse_silabs_gbl(): list(validators.parse_silabs_gbl(VALID_GBL_IMAGE)) image = create_gbl_image([(b"AAAA", b"test"), (b"BBBB", b"foo" * 20)]) header, tag1, tag2, checksum = validators.parse_silabs_gbl(image) assert header[0] == b"\xeb\x17\xa6\x03" and len(header[1]) == 8 assert tag1 == (b"AAAA", b"test") assert tag2 == (b"BBBB", b"foo" * 20) assert checksum[0] == b"\xfc\x04\x04\xfc" and len(checksum[1]) == 4 # Arbitrary padding is allowed parsed_image = [header, tag1, tag2, checksum] assert list(validators.parse_silabs_gbl(image + b"\x00")) == parsed_image assert list(validators.parse_silabs_gbl(image + b"\xab\xcd\xef")) == parsed_image # Normal truncated images are detected with pytest.raises(ValidationError): list(validators.parse_silabs_gbl(image[-10:])) # Structurally sound but truncated images are detected offset = image.index(b"test") bad_image = image[: offset - 8] with pytest.raises(ValidationError): list(validators.parse_silabs_gbl(bad_image)) # Corrupted images are detected corrupted_image = image.replace(b"foo", b"goo", 1) assert image != corrupted_image with pytest.raises(ValidationError): list(validators.parse_silabs_gbl(corrupted_image)) def test_validate_firmware(): assert validators.validate_firmware(VALID_EBL_IMAGE) == ValidationResult.VALID with pytest.raises(ValidationError): validators.validate_firmware(VALID_EBL_IMAGE[:-1]) with pytest.raises(ValidationError): validators.validate_firmware(VALID_EBL_IMAGE + b"\xff") assert validators.validate_firmware(VALID_GBL_IMAGE) == ValidationResult.VALID with pytest.raises(ValidationError): validators.validate_firmware(VALID_GBL_IMAGE[:-1]) assert validators.validate_firmware(b"UNKNOWN") == ValidationResult.UNKNOWN def test_validate_ota_image_simple_valid(): image = OTAImage() image.subelements = [ create_subelement(ElementTagId.UPGRADE_IMAGE, VALID_EBL_IMAGE), ] assert validators.validate_ota_image(image) == ValidationResult.VALID def test_validate_ota_image_complex_valid(): image = OTAImage() image.subelements = [ create_subelement(ElementTagId.ECDSA_SIGNATURE_CRYPTO_SUITE_1, b"asd"), create_subelement(ElementTagId.UPGRADE_IMAGE, VALID_EBL_IMAGE), create_subelement(ElementTagId.UPGRADE_IMAGE, VALID_GBL_IMAGE), create_subelement(ElementTagId.ECDSA_SIGNING_CERTIFICATE_CRYPTO_SUITE_1, b"ab"), ] assert validators.validate_ota_image(image) == ValidationResult.VALID def test_validate_ota_image_invalid(): image = OTAImage() image.subelements = [ create_subelement(ElementTagId.UPGRADE_IMAGE, VALID_EBL_IMAGE[:-1]), ] with pytest.raises(ValidationError): validators.validate_ota_image(image) def test_validate_ota_image_mixed_invalid(): image = OTAImage() image.subelements = [ create_subelement(ElementTagId.UPGRADE_IMAGE, b"unknown"), create_subelement(ElementTagId.UPGRADE_IMAGE, VALID_EBL_IMAGE[:-1]), ] with pytest.raises(ValidationError): validators.validate_ota_image(image) def test_validate_ota_image_mixed_valid(): image = OTAImage() image.subelements = [ create_subelement(ElementTagId.UPGRADE_IMAGE, b"unknown1"), create_subelement(ElementTagId.UPGRADE_IMAGE, VALID_EBL_IMAGE), ] assert validators.validate_ota_image(image) == ValidationResult.UNKNOWN def test_validate_ota_image_empty(): image = OTAImage() image.subelements = [] assert validators.validate_ota_image(image) == ValidationResult.UNKNOWN def test_check_invalid_unknown(): image = mock.Mock() assert validators.validate_ota_image(image) == ValidationResult.UNKNOWN def test_check_invalid(): image = OTAImage() with mock.patch("zigpy.ota.validators.validate_ota_image") as m: m.side_effect = [ValidationResult.VALID] assert not validators.check_invalid(image) with mock.patch("zigpy.ota.validators.validate_ota_image") as m: m.side_effect = [ValidationResult.UNKNOWN] assert not validators.check_invalid(image) with mock.patch("zigpy.ota.validators.validate_ota_image") as m: m.side_effect = [ValidationError("error")] assert validators.check_invalid(image) zigpy-0.80.1/tests/test_app_state.py000066400000000000000000000126321501451476000175040ustar00rootroot00000000000000"""Test unit for app status and counters.""" import pytest import zigpy.state as app_state COUNTER_NAMES = ["counter_1", "counter_2", "some random name"] @pytest.fixture def counters(): """Counters fixture.""" counters = app_state.CounterGroup("ezsp_counters") for name in COUNTER_NAMES: counters[name] return counters def test_counter(): """Test basic counter.""" counter = app_state.Counter("mock_counter") assert counter.value == 0 counter = app_state.Counter("mock_counter", 5) assert counter.value == 5 assert counter.reset_count == 0 counter.update(5) assert counter.value == 5 assert counter.reset_count == 0 counter.update(8) assert counter.value == 8 assert counter.reset_count == 0 counter.update(9) assert counter.value == 9 assert counter.reset_count == 0 counter.reset() assert counter.value == 9 assert counter._raw_value == 0 assert counter.reset_count == 1 # new value after a counter was reset/clear counter.update(12) assert counter.value == 21 assert counter.reset_count == 1 counter.update(15) assert counter.value == 24 assert counter.reset_count == 1 # new counter value is less than previously reported. # assume counter was reset counter.update(14) assert counter.value == 24 + 14 assert counter.reset_count == 2 counter.reset_and_update(14) assert counter.value == 38 + 14 assert counter.reset_count == 3 def test_counter_str(): """Test counter str representation.""" counter = app_state.Counter("some_counter", 8) assert str(counter) == "some_counter = 8" def test_counters_init(): """Test counters initialization.""" counter_groups = app_state.CounterGroups() assert len(counter_groups) == 0 counters = counter_groups["ezsp_counters"] assert len(counter_groups) == 1 assert len(counters) == 0 assert counters.name == "ezsp_counters" for name in COUNTER_NAMES: counters[name] assert len(counters) == 3 cnt_1, cnt_2, cnt_3 = (counter for counter in counters.counters()) assert cnt_1.name == "counter_1" assert cnt_2.name == "counter_2" assert cnt_3.name == "some random name" assert cnt_1.value == 0 assert cnt_2.value == 0 assert cnt_3.value == 0 counters["some random name"].update(2) assert cnt_3.value == 2 assert counters["some random name"].value == 2 assert counters["some random name"] == 2 assert counters["some random name"] == cnt_3 assert int(cnt_3) == 2 assert "counter_2" in counters assert [counter.name for counter in counters.counters()] == COUNTER_NAMES counters.reset() for counter in counters.counters(): assert counter.reset_count == 1 def test_counters_str_and_repr(counters): """Test counters str and repr.""" counters["counter_1"].update(22) counters["counter_2"].update(33) assert ( str(counters) == "ezsp_counters: [counter_1 = 22, counter_2 = 33, some random name = 0]" ) assert ( repr(counters) == """CounterGroup('ezsp_counters', {Counter('counter_1', 22), """ """Counter('counter_2', 33), Counter('some random name', 0)})""" ) def test_state(): """Test state structure.""" state = app_state.State() assert state assert state.counters == {} assert state.counters["new_collection"]["counter_2"] == 0 assert state.counters["new_collection"]["counter_2"].reset_count == 0 assert state.counters["new_collection"]["counter_3"].reset_count == 0 state.counters["new_collection"]["counter_2"] = 2 def test_counters_reset(counters): """Test counter resetting.""" counter = counters["counter_1"] assert counter.reset_count == 0 counters["counter_1"].update(22) assert counter.value == 22 assert counter.reset_count == 0 counters.reset() assert counter.reset_count == 1 counter.update(22) assert counter.value == 44 assert counter.reset_count == 1 def test_counter_incr(): """Test counter increment.""" counter = app_state.Counter("counter_name", 42) assert counter == 42 counter.increment() assert counter == 43 counter.increment(5) assert counter == 48 assert counter.value == 48 with pytest.raises(AssertionError): counter.increment(-1) def test_counter_nested_groups_increment(): """Test nested counters.""" counters = app_state.CounterGroup("device_counters") assert len(counters) == 0 counters.increment("reply", "rx", "zdo", 0x8031) counters.increment("total", "rx", 3, 0x0006) counters.increment("total", "rx", 3, 0x0008) counters.increment("total", "rx", 3, 0x0300) tags = set(counters.tags()) assert {"rx"} == tags tags = set(counters["rx"].tags()) assert {"zdo", 3} == tags assert counters["rx"]["reply"] == 1 assert counters["rx"]["zdo"]["reply"] == 1 assert counters["rx"]["zdo"][0x8031]["reply"] == 1 assert counters["rx"]["total"] == 3 assert counters["rx"][3]["total"] == 3 assert counters["rx"][3][0x0006]["total"] == 1 assert counters["rx"][3][0x0008]["total"] == 1 assert counters["rx"][3][0x0300]["total"] == 1 def test_counter_groups(): """Test CounterGroups.""" groups = app_state.CounterGroups() assert not list(groups) counter_group = groups["ezsp_counters"] new_groups = list(groups) assert new_groups == [counter_group] zigpy-0.80.1/tests/test_appdb.py000066400000000000000000001136401501451476000166130ustar00rootroot00000000000000import asyncio import contextlib from datetime import datetime, timedelta, timezone import pathlib import sqlite3 import sys import threading import time import aiosqlite import freezegun import pytest from tests.async_mock import AsyncMock, MagicMock, call, patch from tests.conftest import make_app, make_ieee, make_node_desc from tests.test_backups import backup_factory # noqa: F401 from zigpy import profiles import zigpy.appdb import zigpy.application import zigpy.config as conf from zigpy.const import SIG_ENDPOINTS, SIG_MANUFACTURER, SIG_MODEL from zigpy.device import Device, Status import zigpy.endpoint import zigpy.ota from zigpy.quirks import CustomDevice import zigpy.types as t import zigpy.zcl from zigpy.zcl.clusters.general import Basic from zigpy.zcl.foundation import Status as ZCLStatus from zigpy.zdo import types as zdo_t @pytest.fixture(autouse=True) def auto_kill_aiosqlite(): """Aiosqlite's background thread does not let pytest exit when a failure occurs""" yield for thread in threading.enumerate(): if not isinstance(thread, aiosqlite.core.Connection): continue try: conn = thread._conn except ValueError: pass else: with contextlib.suppress(zigpy.appdb.sqlite3.ProgrammingError): conn.close() thread._running = False async def make_app_with_db(database_file): if isinstance(database_file, pathlib.Path): database_file = str(database_file) app = make_app({conf.CONF_DATABASE: database_file}) await app._load_db() return app class FakeCustomDevice(CustomDevice): replacement = { "endpoints": { # Endpoint exists on original device 1: { "input_clusters": [0, 1, 3, 0x0008], "output_clusters": [6], }, # Endpoint is created only at runtime by the quirk 99: { "input_clusters": [0, 1, 3, 0x0008], "output_clusters": [6], "profile_id": 65535, "device_type": 123, }, } } def mock_dev_init(initialize: bool): """Device schedule_initialize mock factory.""" def _initialize(self): if initialize: self.node_desc = zdo_t.NodeDescriptor(0, 1, 2, 3, 4, 5, 6, 7, 8) return _initialize def _mk_rar(attrid, value, status=0): r = zigpy.zcl.foundation.ReadAttributeRecord() r.attrid = attrid r.status = status r.value = zigpy.zcl.foundation.TypeValue() r.value.value = value return r def fake_get_device(device): if device.endpoints.get(1) is not None and device[1].profile_id == 65535: return FakeCustomDevice(device.application, device.ieee, device.nwk, device) return device async def test_no_database(tmp_path): with patch("zigpy.appdb.PersistingListener.new", AsyncMock()) as db_mock: db_mock.return_value.load.side_effect = AsyncMock() await make_app_with_db(None) assert db_mock.return_value.load.call_count == 0 db = tmp_path / "test.db" with patch("zigpy.appdb.PersistingListener.new", AsyncMock()) as db_mock: db_mock.return_value.load.side_effect = AsyncMock() await make_app_with_db(db) assert db_mock.return_value.load.call_count == 1 @patch("zigpy.device.Device.schedule_initialize", new=mock_dev_init(True)) async def test_database(tmp_path): db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() relays_1 = [t.NWK(0x1234), t.NWK(0x2345)] relays_2 = [t.NWK(0x3456), t.NWK(0x4567)] app.handle_join(99, ieee, 0) app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP ep = dev.add_endpoint(2) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = 0xFFFD # Invalid in_clus = ep.add_input_cluster(0) out_clus = ep.add_output_cluster(0) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 49246 ep.device_type = profiles.zll.DeviceType.COLOR_LIGHT app.device_initialized(dev) in_clus.update_attribute(0, 99) in_clus.update_attribute(4, bytes("Custom", "ascii")) in_clus.update_attribute(5, bytes("Model", "ascii")) in_clus.listener_event("cluster_command", 0) in_clus.listener_event("general_command") out_clus.update_attribute(0, 99) dev.relays = relays_1 signature = dev.get_signature() assert ep.endpoint_id in signature[SIG_ENDPOINTS] assert SIG_MANUFACTURER not in signature assert SIG_MODEL not in signature dev.manufacturer = "Custom" dev.model = "Model" assert dev.get_signature()[SIG_MANUFACTURER] == "Custom" assert dev.get_signature()[SIG_MODEL] == "Model" ts = time.time() dev.last_seen = ts dev_last_seen = dev.last_seen assert isinstance(dev.last_seen, float) assert abs(dev.last_seen - ts) < 0.01 # Test a CustomDevice custom_ieee = make_ieee(1) app.handle_join(199, custom_ieee, 0) dev = app.get_device(custom_ieee) app.device_initialized(dev) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.device_type = profiles.zll.DeviceType.COLOR_LIGHT ep.profile_id = 65535 with patch("zigpy.quirks.get_device", fake_get_device): app.device_initialized(dev) assert isinstance(app.get_device(custom_ieee), FakeCustomDevice) assert isinstance(app.get_device(custom_ieee), CustomDevice) dev = app.get_device(custom_ieee) app.device_initialized(dev) dev.relays = relays_2 dev.endpoints[1].level.update_attribute(0x0011, 17) dev.endpoints[99].level.update_attribute(0x0011, 17) assert dev.endpoints[1].in_clusters[0x0008]._attr_cache[0x0011] == 17 assert dev.endpoints[99].in_clusters[0x0008]._attr_cache[0x0011] == 17 custom_dev_last_seen = dev.last_seen assert isinstance(custom_dev_last_seen, float) await app.shutdown() # Everything should've been saved - check that it re-loads with patch("zigpy.quirks.get_device", fake_get_device): app2 = await make_app_with_db(db) dev = app2.get_device(ieee) assert dev.endpoints[1].device_type == profiles.zha.DeviceType.PUMP assert dev.endpoints[2].device_type == 0xFFFD assert dev.endpoints[2].in_clusters[0]._attr_cache[0] == 99 assert dev.endpoints[2].in_clusters[0]._attr_cache[4] == bytes("Custom", "ascii") assert dev.endpoints[2].in_clusters[0]._attr_cache[5] == bytes("Model", "ascii") assert dev.endpoints[2].out_clusters[0].cluster_id == 0x0000 assert dev.endpoints[2].out_clusters[0]._attr_cache[0] == 99 assert dev.endpoints[2].manufacturer == "Custom" assert dev.endpoints[2].model == "Model" assert dev.endpoints[3].device_type == profiles.zll.DeviceType.COLOR_LIGHT assert dev.relays == relays_1 # The timestamp won't be restored exactly but it is more than close enough assert abs(dev.last_seen - dev_last_seen) < 0.01 dev = app2.get_device(custom_ieee) # This virtual attribute is added by the quirk, there is no corresponding cluster # stored in the database, nor is there a corresponding endpoint 99 assert dev.endpoints[1].in_clusters[0x0008]._attr_cache[0x0011] == 17 assert dev.endpoints[99].in_clusters[0x0008]._attr_cache[0x0011] == 17 assert dev.relays == relays_2 assert abs(dev.last_seen - custom_dev_last_seen) < 0.01 dev.relays = None app.handle_leave(99, ieee) await app2.shutdown() app3 = await make_app_with_db(db) assert ieee in app3.devices async def mockleave(*args, **kwargs): return [0] app3.devices[ieee].zdo.leave = mockleave await app3.remove(ieee) for _i in range(1, 20): await asyncio.sleep(0) assert ieee not in app3.devices await app3.shutdown() app4 = await make_app_with_db(db) assert ieee not in app4.devices dev = app4.get_device(custom_ieee) assert dev.relays is None await app4.shutdown() @patch("zigpy.device.Device.schedule_group_membership_scan", MagicMock()) async def _test_null_padded(tmp_path, test_manufacturer=None, test_model=None): db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() with patch( "zigpy.device.Device.schedule_initialize", new=mock_dev_init(True), ): app.handle_join(99, ieee, 0) app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0) ep.add_output_cluster(1) app.device_initialized(dev) clus.update_attribute(4, test_manufacturer) clus.update_attribute(5, test_model) clus.listener_event("cluster_command", 0) clus.listener_event("zdo_command") await app.shutdown() # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) dev = app2.get_device(ieee) assert dev.endpoints[3].device_type == profiles.zha.DeviceType.PUMP assert dev.endpoints[3].in_clusters[0]._attr_cache[4] == test_manufacturer assert dev.endpoints[3].in_clusters[0]._attr_cache[5] == test_model await app2.shutdown() return dev async def test_appdb_load_null_padded_manuf(tmp_path): manufacturer = b"Mock Manufacturer\x00\x04\\\x00\\\x00\x00\x00\x00\x00\x07" model = b"Mock Model" dev = await _test_null_padded(tmp_path, manufacturer, model) assert dev.manufacturer == "Mock Manufacturer" assert dev.model == "Mock Model" assert dev.endpoints[3].manufacturer == "Mock Manufacturer" assert dev.endpoints[3].model == "Mock Model" async def test_appdb_load_null_padded_model(tmp_path): manufacturer = b"Mock Manufacturer" model = b"Mock Model\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" dev = await _test_null_padded(tmp_path, manufacturer, model) assert dev.manufacturer == "Mock Manufacturer" assert dev.model == "Mock Model" assert dev.endpoints[3].manufacturer == "Mock Manufacturer" assert dev.endpoints[3].model == "Mock Model" async def test_appdb_load_null_padded_manuf_model(tmp_path): manufacturer = b"Mock Manufacturer\x00\x04\\\x00\\\x00\x00\x00\x00\x00\x07" model = b"Mock Model\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" dev = await _test_null_padded(tmp_path, manufacturer, model) assert dev.manufacturer == "Mock Manufacturer" assert dev.model == "Mock Model" assert dev.endpoints[3].manufacturer == "Mock Manufacturer" assert dev.endpoints[3].model == "Mock Model" async def test_appdb_str_model(tmp_path): manufacturer = "Mock Manufacturer" model = "Mock Model" dev = await _test_null_padded(tmp_path, manufacturer, model) assert dev.manufacturer == "Mock Manufacturer" assert dev.model == "Mock Model" assert dev.endpoints[3].manufacturer == "Mock Manufacturer" assert dev.endpoints[3].model == "Mock Model" @patch.object(Device, "schedule_initialize", new=mock_dev_init(True)) @patch("zigpy.zcl.Cluster.request", new_callable=AsyncMock) async def test_groups(mock_request, tmp_path): """Test group adding/removing.""" group_id, group_name = 0x1221, "app db Test Group 0x1221" mock_request.return_value = [ZCLStatus.SUCCESS, group_id] db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP ep.add_input_cluster(4) app.device_initialized(dev) ieee_b = make_ieee(2) app.handle_join(100, ieee_b, 0) dev_b = app.get_device(ieee_b) ep_b = dev_b.add_endpoint(2) ep_b.status = zigpy.endpoint.Status.ZDO_INIT ep_b.profile_id = 260 ep_b.device_type = profiles.zha.DeviceType.PUMP ep_b.add_input_cluster(4) app.device_initialized(dev_b) await ep.add_to_group(group_id, group_name) await ep_b.add_to_group(group_id, group_name) assert group_id in app.groups group = app.groups[group_id] assert group.name == group_name assert (dev.ieee, ep.endpoint_id) in group assert (dev_b.ieee, ep_b.endpoint_id) in group assert group_id in ep.member_of assert group_id in ep_b.member_of await app.shutdown() del app, dev, dev_b, ep, ep_b # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) dev2 = app2.get_device(ieee) assert group_id in app2.groups group = app2.groups[group_id] assert group.name == group_name assert (dev2.ieee, 1) in group assert group_id in dev2.endpoints[1].member_of dev2_b = app2.get_device(ieee_b) assert (dev2_b.ieee, 2) in group assert group_id in dev2_b.endpoints[2].member_of # check member removal await dev2_b.remove_from_group(group_id) await app2.shutdown() del app2, dev2, dev2_b app3 = await make_app_with_db(db) dev3 = app3.get_device(ieee) assert group_id in app3.groups group = app3.groups[group_id] assert group.name == group_name assert (dev3.ieee, 1) in group assert group_id in dev3.endpoints[1].member_of dev3_b = app3.get_device(ieee_b) assert (dev3_b.ieee, 2) not in group assert group_id not in dev3_b.endpoints[2].member_of # check group removal await dev3.remove_from_group(group_id) await app3.shutdown() del app3, dev3, dev3_b app4 = await make_app_with_db(db) dev4 = app4.get_device(ieee) assert group_id in app4.groups assert not app4.groups[group_id] assert group_id not in dev4.endpoints[1].member_of app4.groups.pop(group_id) await app4.shutdown() del app4, dev4 app5 = await make_app_with_db(db) assert not app5.groups await app5.shutdown() @pytest.mark.parametrize("dev_init", [True, False]) async def test_attribute_update(tmp_path, dev_init): """Test attribute update for initialized and uninitialized devices.""" db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() with patch( "zigpy.device.Device.schedule_initialize", new=mock_dev_init(initialize=dev_init), ): app.handle_join(99, ieee, 0) test_manufacturer = "Test Manufacturer" test_model = "Test Model" dev = app.get_device(ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0x0000) ep.add_output_cluster(0x0001) clus.update_attribute(0x0004, test_manufacturer) clus.update_attribute(0x0005, test_model) app.device_initialized(dev) await app.shutdown() attr_update_time = clus._attr_last_updated[0x0004] # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) dev = app2.get_device(ieee) assert dev.is_initialized == dev_init assert dev.endpoints[3].device_type == profiles.zha.DeviceType.PUMP clus = dev.endpoints[3].in_clusters[0x0000] assert clus._attr_cache[0x0004] == test_manufacturer assert clus._attr_cache[0x0005] == test_model assert (attr_update_time - clus._attr_last_updated[0x0004]) < timedelta(seconds=0.1) await app2.shutdown() @patch.object(Device, "schedule_initialize", new=mock_dev_init(True)) async def test_attribute_update_short_interval(tmp_path): """Test updating an attribute twice in a short interval.""" db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0x0000) ep.add_output_cluster(0x0001) clus.update_attribute(0x0004, "Custom") clus.update_attribute(0x0005, "Model") app.device_initialized(dev) # wait for the device initialization to write attribute cache to db await asyncio.sleep(0.01) # update an attribute twice in a short interval clus.update_attribute(0x4000, "1.0") attr_update_time_first = clus._attr_last_updated[0x4000] # update attribute again 10 seconds later fake_time = datetime.now(timezone.utc) + timedelta(seconds=10) with freezegun.freeze_time(fake_time): clus.update_attribute(0x4000, "2.0") await app.shutdown() # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) dev = app2.get_device(ieee) clus = dev.endpoints[3].in_clusters[0x0000] assert clus._attr_cache[0x4000] == "2.0" # verify second attribute update was saved # verify the first update attribute time was not overwritten, as it was within the short interval assert (attr_update_time_first - clus._attr_last_updated[0x0004]) < timedelta( seconds=0.1 ) await app2.shutdown() @patch("zigpy.topology.REQUEST_DELAY", (0, 0)) @patch.object(Device, "schedule_initialize", new=mock_dev_init(True)) async def test_topology(tmp_path): """Test neighbor loading.""" ext_pid = t.EUI64.convert("aa:bb:cc:dd:ee:ff:01:02") neighbor1 = zdo_t.Neighbor( extended_pan_id=ext_pid, ieee=make_ieee(1), nwk=0x1111, device_type=zdo_t.Neighbor.DeviceType.EndDevice, rx_on_when_idle=1, relationship=zdo_t.Neighbor.Relationship.Child, reserved1=0, permit_joining=0, reserved2=0, depth=15, lqi=250, ) neighbor2 = zdo_t.Neighbor( extended_pan_id=ext_pid, ieee=make_ieee(2), nwk=0x1112, device_type=zdo_t.Neighbor.DeviceType.EndDevice, rx_on_when_idle=1, relationship=zdo_t.Neighbor.Relationship.Child, reserved1=0, permit_joining=0, reserved2=0, depth=15, lqi=250, ) route1 = zdo_t.Route( DstNWK=0x1234, RouteStatus=zdo_t.RouteStatus.Active, MemoryConstrained=0, ManyToOne=0, RouteRecordRequired=0, Reserved=0, NextHop=0x6789, ) route2 = zdo_t.Route( DstNWK=0x1235, RouteStatus=zdo_t.RouteStatus.Active, MemoryConstrained=0, ManyToOne=0, RouteRecordRequired=0, Reserved=0, NextHop=0x6790, ) ieee = make_ieee(0) nwk = 0x9876 db = tmp_path / "test.db" app = await make_app_with_db(db) app.handle_join(nwk, ieee, 0x0000) dev = app.get_device(ieee) dev.node_desc = zdo_t.NodeDescriptor( logical_type=zdo_t.LogicalType.Router, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=zdo_t.NodeDescriptor.FrequencyBand.Freq2400MHz, mac_capability_flags=zdo_t.NodeDescriptor.MACCapabilityFlags.AllocateAddress, manufacturer_code=4174, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=0, maximum_outgoing_transfer_size=82, descriptor_capability_field=zdo_t.NodeDescriptor.DescriptorCapability.NONE, ) ep1 = dev.add_endpoint(1) ep1.status = zigpy.endpoint.Status.ZDO_INIT ep1.profile_id = 260 ep1.device_type = 0x1234 app.device_initialized(dev) p1 = patch.object( app.topology, "_scan_neighbors", new=AsyncMock(return_value=[neighbor1, neighbor2]), ) p2 = patch.object( app.topology, "_scan_routes", new=AsyncMock(return_value=[route1, route2]), ) with p1, p2: await app.topology.scan() assert len(app.topology.neighbors[ieee]) == 2 assert neighbor1 in app.topology.neighbors[ieee] assert neighbor2 in app.topology.neighbors[ieee] assert len(app.topology.routes[ieee]) == 2 assert route1 in app.topology.routes[ieee] assert route2 in app.topology.routes[ieee] await app.shutdown() del dev # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) app2.get_device(ieee) assert len(app2.topology.neighbors[ieee]) == 2 assert neighbor1 in app2.topology.neighbors[ieee] assert neighbor2 in app2.topology.neighbors[ieee] assert len(app2.topology.routes[ieee]) == 2 assert route1 in app2.topology.routes[ieee] assert route2 in app2.topology.routes[ieee] await app2.shutdown() @patch("zigpy.device.Device.schedule_initialize", new=mock_dev_init(True)) async def test_device_rejoin(tmp_path): db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() nwk = 199 app.handle_join(nwk, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 65535 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0) ep.add_output_cluster(1) app.device_initialized(dev) clus.update_attribute(4, "Custom") clus.update_attribute(5, "Model") await app.shutdown() # Everything should've been saved - check that it re-loads with patch("zigpy.quirks.get_device", fake_get_device): app2 = await make_app_with_db(db) dev = app2.get_device(ieee) assert dev.nwk == nwk assert dev.endpoints[1].device_type == profiles.zha.DeviceType.PUMP assert dev.endpoints[1].in_clusters[0]._attr_cache[4] == "Custom" assert dev.endpoints[1].in_clusters[0]._attr_cache[5] == "Model" assert dev.endpoints[1].manufacturer == "Custom" assert dev.endpoints[1].model == "Model" # device rejoins dev.nwk = nwk + 1 with patch("zigpy.quirks.get_device", fake_get_device): app2.device_initialized(dev) await app2.shutdown() app3 = await make_app_with_db(db) dev = app3.get_device(ieee) assert dev.nwk == nwk + 1 assert dev.endpoints[1].device_type == profiles.zha.DeviceType.PUMP assert 0 in dev.endpoints[1].in_clusters assert dev.endpoints[1].manufacturer == "Custom" assert dev.endpoints[1].model == "Model" await app3.shutdown() @patch("zigpy.device.Device.schedule_initialize", new=mock_dev_init(True)) async def test_stopped_appdb_listener(tmp_path): db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0) ep.add_output_cluster(1) app.device_initialized(dev) with patch("zigpy.appdb.PersistingListener._save_attribute") as mock_attr_save: clus.update_attribute(0, 99) clus.update_attribute(4, bytes("Custom", "ascii")) clus.update_attribute(5, bytes("Model", "ascii")) await app.shutdown() assert mock_attr_save.call_count == 3 clus.update_attribute(0, 100) for _i in range(100): await asyncio.sleep(0) assert mock_attr_save.call_count == 3 @patch.object(Device, "schedule_initialize", new=mock_dev_init(True)) async def test_invalid_node_desc(tmp_path): """Devices without a valid node descriptor should not save the node descriptor.""" ieee_1 = make_ieee(1) nwk_1 = 0x1111 db = tmp_path / "test.db" app = await make_app_with_db(db) app.handle_join(nwk_1, ieee_1, 0) dev_1 = app.get_device(ieee_1) dev_1.node_desc = None ep = dev_1.add_endpoint(1) ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP ep.status = zigpy.endpoint.Status.ZDO_INIT app.device_initialized(dev_1) await app.shutdown() # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) dev_2 = app2.get_device(ieee=ieee_1) assert dev_2.node_desc is None assert dev_2.nwk == dev_1.nwk assert dev_2.ieee == dev_1.ieee assert dev_2.status == dev_1.status await app2.shutdown() async def test_appdb_worker_exception(tmp_path): """Exceptions should not kill the appdb worker.""" app_mock = MagicMock(name="ControllerApplication") db = tmp_path / "test.db" ieee_1 = make_ieee(1) dev_1 = zigpy.device.Device(app_mock, ieee_1, 0x1111) dev_1.status = Status.ENDPOINTS_INIT dev_1.node_desc = MagicMock() dev_1.node_desc.is_valid = True dev_1.node_desc.serialize.side_effect = AttributeError with patch( "zigpy.appdb.PersistingListener._save_device", wraps=zigpy.appdb.PersistingListener._save_device, ) as save_mock: db_listener = await zigpy.appdb.PersistingListener.new(db, app_mock) for _ in range(3): db_listener.raw_device_initialized(dev_1) await db_listener.shutdown() assert save_mock.await_count == 3 @pytest.mark.parametrize("dev_init", [True, False]) async def test_unsupported_attribute(tmp_path, dev_init): """Test adding unsupported attributes for initialized and uninitialized devices.""" db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() with patch( "zigpy.device.Device.schedule_initialize", new=mock_dev_init(initialize=dev_init), ): app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP in_clus = ep.add_input_cluster(0) in_clus.update_attribute(4, "Custom") in_clus.update_attribute(5, "Model") app.device_initialized(dev) in_clus.add_unsupported_attribute(0x0010) in_clus.add_unsupported_attribute("physical_env") out_clus = ep.add_output_cluster(0) out_clus.add_unsupported_attribute(0x0010) await app.shutdown() # Everything should've been saved - check that it re-loads app2 = await make_app_with_db(db) dev = app2.get_device(ieee) assert dev.is_initialized == dev_init assert dev.endpoints[3].device_type == profiles.zha.DeviceType.PUMP assert 0x0010 in dev.endpoints[3].in_clusters[0].unsupported_attributes assert 0x0010 in dev.endpoints[3].out_clusters[0].unsupported_attributes assert "location_desc" in dev.endpoints[3].in_clusters[0].unsupported_attributes assert "location_desc" in dev.endpoints[3].out_clusters[0].unsupported_attributes assert 0x0011 in dev.endpoints[3].in_clusters[0].unsupported_attributes assert "physical_env" in dev.endpoints[3].in_clusters[0].unsupported_attributes await app2.shutdown() async def mockrequest( is_general_req, command, schema, args, manufacturer=None, **kwargs ): assert is_general_req is True assert command == 0 rar0010 = _mk_rar(0x0010, "Not Removed", zigpy.zcl.foundation.Status.SUCCESS) return [[rar0010]] # Now lets remove an unsupported attribute and make sure it is removed app3 = await make_app_with_db(db) dev = app3.get_device(ieee) assert dev.is_initialized == dev_init assert dev.endpoints[3].device_type == profiles.zha.DeviceType.PUMP in_cluster = dev.endpoints[3].in_clusters[0] assert 0x0010 in in_cluster.unsupported_attributes in_cluster.request = mockrequest await in_cluster.read_attributes([0x0010], allow_cache=False) assert 0x0010 not in in_cluster.unsupported_attributes assert "location_desc" not in in_cluster.unsupported_attributes assert in_cluster.get(0x0010) == "Not Removed" assert 0x0011 in in_cluster.unsupported_attributes assert "physical_env" in in_cluster.unsupported_attributes out_cluster = dev.endpoints[3].out_clusters[0] out_cluster.remove_unsupported_attribute(0x0010) await app3.shutdown() # Everything should've been saved - check that it re-loads app4 = await make_app_with_db(db) dev = app4.get_device(ieee) assert dev.is_initialized == dev_init assert dev.endpoints[3].device_type == profiles.zha.DeviceType.PUMP assert 0x0010 not in dev.endpoints[3].in_clusters[0].unsupported_attributes assert 0x0010 not in dev.endpoints[3].out_clusters[0].unsupported_attributes assert dev.endpoints[3].in_clusters[0].get(0x0010) == "Not Removed" assert "location_desc" not in dev.endpoints[3].in_clusters[0].unsupported_attributes assert 0x0011 in dev.endpoints[3].in_clusters[0].unsupported_attributes assert "physical_env" in dev.endpoints[3].in_clusters[0].unsupported_attributes await app4.shutdown() @patch.object(Device, "schedule_initialize", new=mock_dev_init(True)) async def test_load_unsupp_attr_wrong_cluster(tmp_path): """Test loading unsupported attribute from the wrong cluster.""" db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0) ep.add_output_cluster(1) clus.update_attribute(4, "Custom") clus.update_attribute(5, "Model") app.device_initialized(dev) await app.shutdown() del clus del ep del dev # add unsupported attr for missing endpoint app = await make_app_with_db(db) dev = app.get_device(ieee) ep = dev.endpoints[3] clus = ep.add_input_cluster(2) clus.add_unsupported_attribute(0) await app.shutdown() del clus del ep del dev # reload app = await make_app_with_db(db) await app.shutdown() @patch.object(Device, "schedule_initialize", new=mock_dev_init(True)) async def test_load_unsupp_attr_missing_endpoint(tmp_path): """Test loading unsupported attribute from the wrong cluster.""" db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() app.handle_join(99, ieee, 0) dev = app.get_device(ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0x0000) ep.add_output_cluster(0x0001) clus.update_attribute(0x0004, "Custom") clus.update_attribute(0x0005, "Model") ep = dev.add_endpoint(4) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0x0006) app.device_initialized(dev) # Make an attribute unsupported clus.add_unsupported_attribute(0x0000) await app.shutdown() del clus del ep del dev def remove_cluster(device): device.endpoints.pop(4) return device # Simulate a quirk that removes the entire endpoint with patch("zigpy.quirks.get_device", side_effect=remove_cluster): # The application should still load app = await make_app_with_db(db) dev = app.get_device(ieee) assert 4 not in dev.endpoints await app.shutdown() async def test_last_seen(tmp_path): db = tmp_path / "test.db" app = await make_app_with_db(db) ieee = make_ieee() app.handle_join(99, ieee, 0) dev = app.get_device(ieee=ieee) ep = dev.add_endpoint(3) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP clus = ep.add_input_cluster(0) ep.add_output_cluster(1) clus.update_attribute(4, "Custom") clus.update_attribute(5, "Model") app.device_initialized(dev) old_last_seen = dev.last_seen await app.shutdown() # The `last_seen` of a joined device persists app = await make_app_with_db(db) dev = app.get_device(ieee=ieee) await app.shutdown() next_last_seen = dev.last_seen assert abs(next_last_seen - old_last_seen) < 0.01 app = await make_app_with_db(db) dev = app.get_device(ieee=ieee) # Last-seen is only written to the db every 30s (no write case) now = datetime.fromtimestamp(dev.last_seen + 5, timezone.utc) with freezegun.freeze_time(now): dev.last_seen = datetime.now(timezone.utc) await app.shutdown() app = await make_app_with_db(db) dev = app.get_device(ieee=ieee) assert dev.last_seen == next_last_seen # no change await app.shutdown() app = await make_app_with_db(db) dev = app.get_device(ieee=ieee) # Last-seen is only written to the db every 30s (write case) now = datetime.fromtimestamp(dev.last_seen + 35, timezone.utc) with freezegun.freeze_time(now): dev.last_seen = datetime.now(timezone.utc) await app.shutdown() # And it will be updated when the database next loads app = await make_app_with_db(db) dev = app.get_device(ieee=ieee) assert dev.last_seen >= next_last_seen + 35 # updated await app.shutdown() @pytest.mark.parametrize( ("stdlib_version", "use_sqlite"), [ ((1, 0, 0), False), ((2, 0, 0), False), ((3, 0, 0), False), ((3, 24, 0), True), ((4, 0, 0), True), ], ) def test_pysqlite_load_success(stdlib_version, use_sqlite): """Test that the internal import SQLite helper picks the correct module.""" pysqlite3 = MagicMock() pysqlite3.sqlite_version_info = (3, 30, 0) with ( patch.dict(sys.modules, {"pysqlite3": pysqlite3}), patch.object(sys.modules["sqlite3"], "sqlite_version_info", new=stdlib_version), ): module = zigpy.appdb._import_compatible_sqlite3(zigpy.appdb.MIN_SQLITE_VERSION) if use_sqlite: assert module is sqlite3 else: assert module is pysqlite3 @pytest.mark.parametrize( ("stdlib_version", "pysqlite3_version"), [ ((1, 0, 0), None), ((1, 0, 0), (1, 0, 1)), ], ) def test_pysqlite_load_failure(stdlib_version, pysqlite3_version): """Test that the internal import SQLite helper will throw an error when no compatible module can be found. """ if pysqlite3_version is not None: pysqlite3 = MagicMock() pysqlite3.sqlite_version_info = pysqlite3_version pysqlite3_patch = patch.dict(sys.modules, {"pysqlite3": pysqlite3}) else: pysqlite3_patch = patch.dict(sys.modules, {"pysqlite3": None}) with ( pysqlite3_patch, patch.object(sys.modules["sqlite3"], "sqlite_version_info", new=stdlib_version), ): with pytest.raises(RuntimeError): zigpy.appdb._import_compatible_sqlite3(zigpy.appdb.MIN_SQLITE_VERSION) async def test_appdb_network_backups(tmp_path, backup_factory): # noqa: F811 db = tmp_path / "test.db" backup = backup_factory() app1 = await make_app_with_db(db) app1.backups.add_backup(backup) await app1.shutdown() # The backup is reloaded from the database as well app2 = await make_app_with_db(db) assert len(app2.backups.backups) == 1 assert app2.backups.backups[0] == backup new_backup = backup_factory() new_backup.network_info.network_key.tx_counter += 10000 app2.backups.add_backup(new_backup) await app2.shutdown() # The database will contain only the single backup app3 = await make_app_with_db(db) assert len(app3.backups.backups) == 1 assert app3.backups.backups[0] == new_backup assert app3.backups.backups[0] != backup await app3.shutdown() async def test_appdb_network_backups_format_change(tmp_path, backup_factory): # noqa: F811 db = tmp_path / "test.db" backup = backup_factory() backup.as_dict = MagicMock(return_value={"some new key": 1, **backup.as_dict()}) app1 = await make_app_with_db(db) app1.backups.add_backup(backup) await app1.shutdown() # The backup is reloaded from the database as well app2 = await make_app_with_db(db) assert len(app2.backups.backups) == 1 assert app2.backups.backups[0] == backup new_backup = backup_factory() new_backup.network_info.network_key.tx_counter += 10000 app2.backups.add_backup(new_backup) await app2.shutdown() # The database will contain only the single backup with patch("zigpy.backups.BackupManager.add_backup") as mock_add_backup: app3 = await make_app_with_db(db) await app3.shutdown() assert mock_add_backup.mock_calls == [call(new_backup, suppress_event=True)] async def test_appdb_persist_coordinator_info(tmp_path): # noqa: F811 db = tmp_path / "test.db" with patch( "zigpy.appdb.PersistingListener._save_attribute_cache", wraps=zigpy.appdb.PersistingListener._save_attribute_cache, ) as mock_save_attr_cache: app = await make_app_with_db(db) await app.initialize() await app.shutdown() assert mock_save_attr_cache.mock_calls == [call(app._device.endpoints[1])] async def test_appdb_attribute_clear(tmp_path): db = tmp_path / "test.db" app = await make_app_with_db(db) dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44")) dev.node_desc = make_node_desc(logical_type=zdo_t.LogicalType.Router) ep = dev.add_endpoint(1) ep.status = zigpy.endpoint.Status.ZDO_INIT ep.profile_id = 260 ep.device_type = profiles.zha.DeviceType.PUMP basic = ep.add_input_cluster(Basic.cluster_id) app.device_initialized(dev) basic.update_attribute(Basic.AttributeDefs.zcl_version.id, 0x12) await app.shutdown() # Upon reload, the attribute exists and is in the cache app2 = await make_app_with_db(db) dev2 = app2.get_device(ieee=dev.ieee) assert ( dev2.endpoints[1].basic._attr_cache[Basic.AttributeDefs.zcl_version.id] == 0x12 ) # Clear an existing attribute dev2.endpoints[1].basic.update_attribute(Basic.AttributeDefs.zcl_version.id, None) # Clear an attribute not in the cache dev2.endpoints[1].basic.update_attribute(Basic.AttributeDefs.manufacturer.id, None) assert Basic.AttributeDefs.zcl_version.id not in dev2.endpoints[1].basic._attr_cache await asyncio.sleep(0.1) await app2.shutdown() # The attribute has been removed from the database app3 = await make_app_with_db(db) dev3 = app3.get_device(ieee=dev.ieee) assert Basic.AttributeDefs.zcl_version.id not in dev3.endpoints[1].basic._attr_cache await app3.shutdown() zigpy-0.80.1/tests/test_appdb_migration.py000066400000000000000000000372101501451476000206620ustar00rootroot00000000000000from datetime import datetime, timezone import logging import pathlib from sqlite3.dump import _iterdump as iterdump from aiosqlite.context import contextmanager import pytest from tests.async_mock import AsyncMock, MagicMock, patch from tests.conftest import app # noqa: F401 from tests.test_appdb import auto_kill_aiosqlite, make_app_with_db # noqa: F401 import zigpy.appdb from zigpy.appdb import sqlite3 import zigpy.appdb_schemas import zigpy.types as t from zigpy.zdo import types as zdo_t @pytest.fixture def test_db(tmp_path): def inner(filename): databases = pathlib.Path(__file__).parent / "databases" db_path = tmp_path / filename if filename.endswith(".db"): db_path.write_bytes((databases / filename).read_bytes()) return str(db_path) conn = sqlite3.connect(str(db_path)) sql = (databases / filename).read_text() conn.executescript(sql) conn.commit() conn.close() return str(db_path) return inner def dump_db(path): with sqlite3.connect(path) as conn: cur = conn.cursor() cur.execute("PRAGMA user_version") (user_version,) = cur.fetchone() sql = "\n".join(iterdump(conn)) return user_version, sql @pytest.mark.parametrize("open_twice", [False, True]) async def test_migration_from_3_to_4(open_twice, test_db): test_db_v3 = test_db("simple_v3.sql") with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() neighbors_before = list(cur.execute("SELECT * FROM neighbors")) assert len(neighbors_before) == 2 assert all(len(row) == 8 for row in neighbors_before) node_descs_before = list(cur.execute("SELECT * FROM node_descriptors")) assert len(node_descs_before) == 2 assert all(len(row) == 2 for row in node_descs_before) # Ensure migration works on first run, and after shutdown if open_twice: app = await make_app_with_db(test_db_v3) await app.shutdown() app = await make_app_with_db(test_db_v3) dev1 = app.get_device(nwk=0xBD4D) assert dev1.node_desc == zdo_t.NodeDescriptor( logical_type=zdo_t.LogicalType.Router, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=zdo_t.NodeDescriptor.FrequencyBand.Freq2400MHz, mac_capability_flags=142, manufacturer_code=4476, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=0, ) assert len(app.topology.neighbors[dev1.ieee]) == 1 assert app.topology.neighbors[dev1.ieee][0] == zdo_t.Neighbor( extended_pan_id=t.ExtendedPanId.convert("81:b1:12:dc:9f:bd:f4:b6"), ieee=t.EUI64.convert("ec:1b:bd:ff:fe:54:4f:40"), nwk=0x6D1C, reserved1=0, device_type=zdo_t.Neighbor.DeviceType.Router, rx_on_when_idle=1, relationship=zdo_t.Neighbor.RelationShip.Sibling, reserved2=0, permit_joining=2, depth=15, lqi=130, ) dev2 = app.get_device(nwk=0x6D1C) assert dev2.node_desc == dev1.node_desc.replace(manufacturer_code=4456) assert len(app.topology.neighbors[dev2.ieee]) == 1 assert app.topology.neighbors[dev2.ieee][0] == zdo_t.Neighbor( extended_pan_id=t.ExtendedPanId.convert("81:b1:12:dc:9f:bd:f4:b6"), ieee=t.EUI64.convert("00:0d:6f:ff:fe:a6:11:7a"), nwk=0xBD4D, reserved1=0, device_type=zdo_t.Neighbor.DeviceType.Router, rx_on_when_idle=1, relationship=zdo_t.Neighbor.RelationShip.Sibling, reserved2=0, permit_joining=2, depth=15, lqi=132, ) await app.shutdown() with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() # Old tables are untouched assert neighbors_before == list(cur.execute("SELECT * FROM neighbors")) assert node_descs_before == list(cur.execute("SELECT * FROM node_descriptors")) # New tables exist neighbors_after = list(cur.execute("SELECT * FROM neighbors_v4")) assert len(neighbors_after) == 2 assert all(len(row) == 12 for row in neighbors_after) node_descs_after = list(cur.execute("SELECT * FROM node_descriptors_v4")) assert len(node_descs_after) == 2 assert all(len(row) == 14 for row in node_descs_after) async def test_migration_0_to_5(test_db): test_db_v0 = test_db("zigbee_20190417_v0.db") with sqlite3.connect(test_db_v0) as conn: cur = conn.cursor() cur.execute("SELECT count(*) FROM devices") (num_devices_before_migration,) = cur.fetchone() assert num_devices_before_migration == 27 app1 = await make_app_with_db(test_db_v0) await app1.shutdown() assert len(app1.devices) == 27 app2 = await make_app_with_db(test_db_v0) await app2.shutdown() # All 27 devices migrated assert len(app2.devices) == 27 async def test_migration_missing_neighbors_v3(test_db): test_db_v3 = test_db("simple_v3.sql") with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() cur.execute("DROP TABLE neighbors") # Ensure the table doesn't exist with pytest.raises(sqlite3.OperationalError): cur.execute("SELECT * FROM neighbors") # Migration won't fail even though the database version number is 3 app = await make_app_with_db(test_db_v3) await app.shutdown() # Version was upgraded with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() cur.execute("PRAGMA user_version") assert cur.fetchone() == (zigpy.appdb.DB_VERSION,) @pytest.mark.parametrize("corrupt_device", [False, True]) async def test_migration_bad_attributes(test_db, corrupt_device): test_db_bad_attrs = test_db("bad_attrs_v3.db") with sqlite3.connect(test_db_bad_attrs) as conn: cur = conn.cursor() cur.execute("SELECT count(*) FROM devices") (num_devices_before_migration,) = cur.fetchone() cur.execute("SELECT count(*) FROM endpoints") (num_ep_before_migration,) = cur.fetchone() if corrupt_device: with sqlite3.connect(test_db_bad_attrs) as conn: cur = conn.cursor() cur.execute("DELETE FROM endpoints WHERE ieee='60:a4:23:ff:fe:02:39:7b'") cur.execute("SELECT changes()") (deleted_eps,) = cur.fetchone() else: deleted_eps = 0 # Migration will handle invalid attributes entries app = await make_app_with_db(test_db_bad_attrs) await app.shutdown() assert len(app.devices) == num_devices_before_migration assert ( sum(len(d.non_zdo_endpoints) for d in app.devices.values()) == num_ep_before_migration - deleted_eps ) app2 = await make_app_with_db(test_db_bad_attrs) await app2.shutdown() # All devices still exist assert len(app2.devices) == num_devices_before_migration assert ( sum(len(d.non_zdo_endpoints) for d in app2.devices.values()) == num_ep_before_migration - deleted_eps ) with sqlite3.connect(test_db_bad_attrs) as conn: cur = conn.cursor() cur.execute("PRAGMA user_version") # Ensure the final database schema version number does not decrease assert cur.fetchone()[0] >= zigpy.appdb.DB_VERSION async def test_migration_missing_node_descriptor(test_db, caplog): test_db_v3 = test_db("simple_v3.sql") ieee = "ec:1b:bd:ff:fe:54:4f:40" with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() cur.execute("DELETE FROM node_descriptors WHERE ieee=?", [ieee]) with caplog.at_level(logging.WARNING): # The invalid device will still be loaded, for now app = await make_app_with_db(test_db_v3) assert len(app.devices) == 2 bad_dev = app.devices[t.EUI64.convert(ieee)] assert bad_dev.node_desc is None caplog.clear() # Saving the device should cause the node descriptor to not be saved await app._dblistener._save_device(bad_dev) await app.shutdown() # The node descriptor is not in the database with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() cur.execute( f"SELECT * FROM node_descriptors{zigpy.appdb.DB_V} WHERE ieee=?", [ieee] ) assert not cur.fetchall() @pytest.mark.parametrize( ("fail_on_sql", "fail_on_count"), [ ("INSERT INTO node_descriptors_v4 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", 0), ("INSERT INTO neighbors_v4 VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", 5), ("SELECT * FROM output_clusters", 0), ("INSERT INTO neighbors_v5 VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", 5), ], ) async def test_migration_failure(fail_on_sql, fail_on_count, test_db): test_db_bad_attrs = test_db("bad_attrs_v3.db") before = dump_db(test_db_bad_attrs) assert before[0] == 3 count = 0 sql_seen = False execute = zigpy.appdb.PersistingListener.execute def patched_execute(self, sql, *args, **kwargs): nonlocal count, sql_seen if sql == fail_on_sql: sql_seen = True if count == fail_on_count: raise sqlite3.ProgrammingError("Uh oh") count += 1 return execute(self, sql, *args, **kwargs) with patch("zigpy.appdb.PersistingListener.execute", new=patched_execute): with pytest.raises(sqlite3.ProgrammingError): await make_app_with_db(test_db_bad_attrs) assert sql_seen after = dump_db(test_db_bad_attrs) assert before == after async def test_migration_failure_version_mismatch(test_db): """Test migration failure when the `user_version` and table versions don't match.""" test_db_v3 = test_db("simple_v3.sql") # Migrate it to the latest version app = await make_app_with_db(test_db_v3) await app.shutdown() # Downgrade it back to v7 with sqlite3.connect(test_db_v3) as conn: conn.execute("PRAGMA user_version=7") # Startup now fails due to the version mismatch with pytest.raises(zigpy.exceptions.CorruptDatabase): await make_app_with_db(test_db_v3) async def test_migration_downgrade_warning(test_db, caplog): """Test V4 re-migration which was forcibly downgraded to v3.""" test_db_v3 = test_db("simple_v3.sql") # Migrate it to the latest version app = await make_app_with_db(test_db_v3) await app.shutdown() # Upgrade it beyond our current version with sqlite3.connect(test_db_v3) as conn: conn.execute("CREATE TABLE future_table_v100(column)") conn.execute("PRAGMA user_version=100") # Startup now logs an error due to the "downgrade" with caplog.at_level(logging.ERROR): app2 = await make_app_with_db(test_db_v3) await app2.shutdown() assert "Downgrading zigpy" in caplog.text # Ensure the version was not touched with sqlite3.connect(test_db_v3) as conn: user_version = conn.execute("PRAGMA user_version").fetchone()[0] assert user_version == 100 @pytest.mark.parametrize("with_bad_neighbor", [False, True]) async def test_v4_to_v5_migration_bad_neighbors(test_db, with_bad_neighbor): """V4 migration has no `neighbors_v4` foreign key and no `ON DELETE CASCADE`""" test_db_v4 = test_db("simple_v3_to_v4.sql") with sqlite3.connect(test_db_v4) as conn: cur = conn.cursor() if with_bad_neighbor: # Row refers to an invalid device, left behind by a bad `DELETE` cur.execute( """ INSERT INTO neighbors_v4 VALUES ( '11:aa:bb:cc:dd:ee:ff:00', '22:aa:bb:cc:dd:ee:ff:00', '33:aa:bb:cc:dd:ee:ff:00', 12345, 1,1,2,0,2,0,15,132 ) """ ) (num_v4_neighbors,) = cur.execute( "SELECT count(*) FROM neighbors_v4" ).fetchone() app = await make_app_with_db(test_db_v4) await app.shutdown() with sqlite3.connect(test_db_v4) as conn: (num_new_neighbors,) = cur.execute( f"SELECT count(*) FROM neighbors{zigpy.appdb.DB_V}" ).fetchone() # Only the invalid row was not migrated if with_bad_neighbor: assert num_new_neighbors == num_v4_neighbors - 1 else: assert num_new_neighbors == num_v4_neighbors @pytest.mark.parametrize("with_quirk_attribute", [False, True]) async def test_v4_to_v6_migration_missing_endpoints(test_db, with_quirk_attribute): """V5's schema was too rigid and failed to migrate endpoints created by quirks""" test_db_v3 = test_db("simple_v3.sql") if with_quirk_attribute: with sqlite3.connect(test_db_v3) as conn: cur = conn.cursor() cur.execute( """ INSERT INTO attributes VALUES ( '00:0d:6f:ff:fe:a6:11:7a', 123, 456, 789, 'test' ) """ ) def get_device(dev): if dev.ieee == t.EUI64.convert("00:0d:6f:ff:fe:a6:11:7a"): ep = dev.add_endpoint(123) ep.add_input_cluster(456) return dev # Migrate to v5 and then v6 with patch("zigpy.quirks.get_device", get_device): app = await make_app_with_db(test_db_v3) if with_quirk_attribute: dev = app.get_device(ieee=t.EUI64.convert("00:0d:6f:ff:fe:a6:11:7a")) assert dev.endpoints[123].in_clusters[456]._attr_cache[789] == "test" await app.shutdown() async def test_v5_to_v7_migration(test_db): test_db_v5 = test_db("simple_v5.sql") app = await make_app_with_db(test_db_v5) await app.shutdown() async def test_migration_missing_tables(app): conn = MagicMock() conn.close = AsyncMock() appdb = zigpy.appdb.PersistingListener(conn, app) appdb._get_table_versions = AsyncMock( return_value={"table1_v1": "1", "table1": "", "table2_v1": "1"} ) mock_execute = AsyncMock() appdb.execute = contextmanager(mock_execute) appdb._db._execute = AsyncMock() # Migrations must explicitly specify all old tables, even if they will be untouched with pytest.raises(RuntimeError): await appdb._migrate_tables( { "table1_v1": "table1_v2", # "table2_v1": "table2_v2", } ) # The untouched table will never be queried await appdb._migrate_tables({"table1_v1": "table1_v2", "table2_v1": None}) mock_execute.assert_called_once_with("SELECT * FROM table1_v1") with pytest.raises(AssertionError): mock_execute.assert_called_once_with("SELECT * FROM table2_v1") await appdb.shutdown() async def test_last_seen_initial_migration(test_db): test_db_v5 = test_db("simple_v5.sql") # To preserve the old behavior, `0` will not be exposed to ZHA, only `None` app = await make_app_with_db(test_db_v5) dev = app.get_device(nwk=0xBD4D) assert dev.last_seen is None dev.last_seen = datetime.now(timezone.utc) assert isinstance(dev.last_seen, float) await app.shutdown() # But the device's `last_seen` will still update properly when it's actually set app = await make_app_with_db(test_db_v5) assert isinstance(app.get_device(nwk=0xBD4D).last_seen, float) await app.shutdown() def test_db_version_is_latest_schema_version(): assert max(zigpy.appdb_schemas.SCHEMAS.keys()) == zigpy.appdb.DB_VERSION async def test_last_seen_migration_v8_to_v9(test_db): test_db_v8 = test_db("simple_v8.sql") app = await make_app_with_db(test_db_v8) assert int(app.get_device(nwk=0xE01E).last_seen) == 1651119830 await app.shutdown() zigpy-0.80.1/tests/test_appdb_pysqlite.py000066400000000000000000000021231501451476000205360ustar00rootroot00000000000000import sqlite3 import pytest from tests.async_mock import patch try: import pysqlite3 except ImportError: pass else: @pytest.fixture(scope="module", autouse=True) def force_use_pysqlite3(): # Make the sqlite3 module "be" pysqlite3 with patch.multiple( target=sqlite3, **{ attr: getattr(pysqlite3, attr) for attr in dir(pysqlite3) if hasattr(sqlite3, attr) }, ): # Ensure the module was patched assert sqlite3.connect is pysqlite3.connect # Directly replace it as well in `zigpy.appdb` with patch("zigpy.appdb.sqlite3", pysqlite3): yield # Ensure the module is unpatched assert sqlite3.connect is not pysqlite3.connect # Re-run most of the appdb tests from tests.test_appdb import * # noqa: F401,F403 from tests.test_appdb_migration import * # type:ignore[no-redef] # noqa: F401,F403 del test_pysqlite_load_success # noqa: F821 del test_pysqlite_load_failure # noqa: F821 zigpy-0.80.1/tests/test_application.py000066400000000000000000001402661501451476000200340ustar00rootroot00000000000000import asyncio from datetime import datetime, timezone import errno import logging from unittest import mock from unittest.mock import ANY, PropertyMock, call import pytest import zigpy.application import zigpy.config as conf from zigpy.exceptions import ( DeliveryError, NetworkNotFormed, NetworkSettingsInconsistent, TransientConnectionError, ) import zigpy.ota import zigpy.quirks import zigpy.types as t from zigpy.zcl import clusters, foundation import zigpy.zdo.types as zdo_t from .async_mock import AsyncMock, MagicMock, patch, sentinel from .conftest import ( NCP_IEEE, App, make_app, make_ieee, make_neighbor, make_neighbor_from_device, make_node_desc, ) @pytest.fixture def ieee(): return make_ieee() async def test_permit(app, ieee): app.devices[ieee] = MagicMock() app.devices[ieee].zdo.permit = AsyncMock() app.permit_ncp = AsyncMock() await app.permit(node=(1, 1, 1, 1, 1, 1, 1, 1)) assert app.devices[ieee].zdo.permit.call_count == 0 assert app.permit_ncp.call_count == 0 await app.permit(node=ieee) assert app.devices[ieee].zdo.permit.call_count == 1 assert app.permit_ncp.call_count == 0 await app.permit(node=NCP_IEEE) assert app.devices[ieee].zdo.permit.call_count == 1 assert app.permit_ncp.call_count == 1 async def test_permit_delivery_failure(app, ieee): def zdo_permit(*args, **kwargs): raise DeliveryError("Failed") app.devices[ieee] = MagicMock() app.devices[ieee].zdo.permit = zdo_permit app.permit_ncp = AsyncMock() await app.permit(node=ieee) assert app.permit_ncp.call_count == 0 async def test_permit_broadcast(app): app.permit_ncp = AsyncMock() app.send_packet = AsyncMock() await app.permit(time_s=30) assert app.send_packet.call_count == 1 assert app.permit_ncp.call_count == 1 assert app.send_packet.mock_calls[0].args[0].dst.addr_mode == t.AddrMode.Broadcast @patch("zigpy.device.Device.initialize", new_callable=AsyncMock) async def test_join_handler_skip(init_mock, app, ieee): node_desc = make_node_desc() app.handle_join(1, ieee, None) app.get_device(ieee).node_desc = node_desc app.handle_join(1, ieee, None) assert app.get_device(ieee).node_desc == node_desc async def test_join_handler_change_id(app, ieee): app.handle_join(1, ieee, None) app.handle_join(2, ieee, None) assert app.devices[ieee].nwk == 2 async def test_unknown_device_left(app, ieee): with patch.object(app, "listener_event", wraps=app.listener_event): app.handle_leave(0x1234, ieee) app.listener_event.assert_not_called() async def test_known_device_left(app, ieee): dev = app.add_device(ieee, 0x1234) with patch.object(app, "listener_event", wraps=app.listener_event): app.handle_leave(0x1234, ieee) app.listener_event.assert_called_once_with("device_left", dev) async def _remove( app, ieee, retval, zdo_reply=True, delivery_failure=True, has_node_desc=True ): async def leave(*args, **kwargs): if zdo_reply: return retval elif delivery_failure: raise DeliveryError("Error") else: raise asyncio.TimeoutError device = MagicMock() device.ieee = ieee device.zdo.leave.side_effect = leave if has_node_desc: device.node_desc = zdo_t.NodeDescriptor(1, 64, 142, 4388, 82, 255, 0, 255, 0) else: device.node_desc = None app.devices[ieee] = device await app.remove(ieee) for _i in range(1, 20): await asyncio.sleep(0) assert ieee not in app.devices async def test_remove(app, ieee): """Test remove with successful zdo status.""" with patch.object(app, "_remove_device", wraps=app._remove_device) as remove_device: await _remove(app, ieee, [0]) assert remove_device.await_count == 1 async def test_remove_with_failed_zdo(app, ieee): """Test remove with unsuccessful zdo status.""" with patch.object(app, "_remove_device", wraps=app._remove_device) as remove_device: await _remove(app, ieee, [1]) assert remove_device.await_count == 1 async def test_remove_nonexistent(app, ieee): with patch.object(app, "_remove_device", AsyncMock()) as remove_device: await app.remove(ieee) for _i in range(1, 20): await asyncio.sleep(0) assert ieee not in app.devices assert remove_device.await_count == 0 async def test_remove_with_unreachable_device(app, ieee): with patch.object(app, "_remove_device", wraps=app._remove_device) as remove_device: await _remove(app, ieee, [0], zdo_reply=False) assert remove_device.await_count == 1 async def test_remove_with_reply_timeout(app, ieee): with patch.object(app, "_remove_device", wraps=app._remove_device) as remove_device: await _remove(app, ieee, [0], zdo_reply=False, delivery_failure=False) assert remove_device.await_count == 1 async def test_remove_without_node_desc(app, ieee): with patch.object(app, "_remove_device", wraps=app._remove_device) as remove_device: await _remove(app, ieee, [0], has_node_desc=False) assert remove_device.await_count == 1 def test_add_device(app, ieee): app.add_device(ieee, 8) app.add_device(ieee, 9) assert app.get_device(ieee).nwk == 9 def test_get_device_nwk(app, ieee): dev = app.add_device(ieee, 8) assert app.get_device(nwk=8) is dev def test_get_device_ieee(app, ieee): dev = app.add_device(ieee, 8) assert app.get_device(ieee=ieee) is dev def test_get_device_both(app, ieee): dev = app.add_device(ieee, 8) assert app.get_device(ieee=ieee, nwk=8) is dev def test_get_device_missing(app, ieee): with pytest.raises(KeyError): app.get_device(nwk=8) def test_device_property(app): app.add_device(nwk=0x0000, ieee=NCP_IEEE) assert app._device is app.get_device(ieee=NCP_IEEE) def test_ieee(app): assert app.state.node_info.ieee def test_nwk(app): assert app.state.node_info.nwk is not None def test_config(app): assert app.config == app._config def test_deserialize(app, ieee): dev = MagicMock() app.deserialize(dev, 1, 1, b"") assert dev.deserialize.call_count == 1 @pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_handle_message_shim(app): dev = MagicMock() dev.nwk = 0x1234 app.packet_received = MagicMock(spec_set=app.packet_received) app.handle_message(dev, 260, 1, 2, 3, b"data") assert app.packet_received.mock_calls == [ call( t.ZigbeePacket( profile_id=260, cluster_id=1, src_ep=2, dst_ep=3, data=t.SerializableBytes(b"data"), src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=0x1234, ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=0x0000, ), ) ) ] @patch("zigpy.device.Device.is_initialized", new_callable=PropertyMock) @patch("zigpy.quirks.handle_message_from_uninitialized_sender", new=MagicMock()) async def test_handle_message_uninitialized_dev(is_init_mock, app, ieee): dev = app.add_device(ieee, 0x1234) dev.packet_received = MagicMock() is_init_mock.return_value = False assert not dev.initializing def make_packet( profile_id: int, cluster_id: int, src_ep: int, dst_ep: int, data: bytes ) -> t.ZigbeePacket: return t.ZigbeePacket( profile_id=profile_id, cluster_id=cluster_id, src_ep=src_ep, dst_ep=dst_ep, data=t.SerializableBytes(data), src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=dev.nwk, ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=0x0000, ), ) # Power Configuration cluster not allowed, no endpoints app.packet_received( make_packet(profile_id=260, cluster_id=0x0001, src_ep=1, dst_ep=1, data=b"test") ) assert dev.packet_received.call_count == 0 assert zigpy.quirks.handle_message_from_uninitialized_sender.call_count == 1 # Device should be completing initialization assert dev.initializing # ZDO is allowed app.packet_received( make_packet(profile_id=260, cluster_id=0x0000, src_ep=0, dst_ep=0, data=b"test") ) assert dev.packet_received.call_count == 1 # Endpoint is uninitialized but Basic attribute read responses still work ep = dev.add_endpoint(1) app.packet_received( make_packet(profile_id=260, cluster_id=0x0000, src_ep=1, dst_ep=1, data=b"test") ) assert dev.packet_received.call_count == 2 # Others still do not app.packet_received( make_packet(profile_id=260, cluster_id=0x0001, src_ep=1, dst_ep=1, data=b"test") ) assert dev.packet_received.call_count == 2 assert zigpy.quirks.handle_message_from_uninitialized_sender.call_count == 2 # They work after the endpoint is initialized ep.status = zigpy.endpoint.Status.ZDO_INIT app.packet_received( make_packet(profile_id=260, cluster_id=0x0001, src_ep=1, dst_ep=1, data=b"test") ) assert dev.packet_received.call_count == 3 assert zigpy.quirks.handle_message_from_uninitialized_sender.call_count == 2 def test_get_dst_address(app): r = app.get_dst_address(MagicMock()) assert r.addrmode == 3 assert r.endpoint == 1 def test_props(app): assert app.state.network_info.channel is not None assert app.state.network_info.channel_mask is not None assert app.state.network_info.extended_pan_id is not None assert app.state.network_info.pan_id is not None assert app.state.network_info.nwk_update_id is not None @pytest.mark.filterwarnings( "ignore::DeprecationWarning" ) # TODO: migrate `handle_message_from_uninitialized_sender` away from `handle_message` async def test_uninitialized_message_handlers(app, ieee): """Test uninitialized message handlers.""" handler_1 = MagicMock(return_value=None) handler_2 = MagicMock(return_value=True) zigpy.quirks.register_uninitialized_device_message_handler(handler_1) zigpy.quirks.register_uninitialized_device_message_handler(handler_2) device = app.add_device(ieee, 0x1234) app.handle_message(device, 0x0260, 0x0000, 0, 0, b"123abcd23") assert handler_1.call_count == 0 assert handler_2.call_count == 0 app.handle_message(device, 0x0260, 0x0000, 1, 1, b"123abcd23") assert handler_1.call_count == 1 assert handler_2.call_count == 1 handler_1.return_value = True app.handle_message(device, 0x0260, 0x0000, 1, 1, b"123abcd23") assert handler_1.call_count == 2 assert handler_2.call_count == 1 async def test_remove_parent_devices(app, make_initialized_device): """Test removing an end device with parents.""" end_device = make_initialized_device(app) end_device.node_desc.logical_type = zdo_t.LogicalType.EndDevice router_1 = make_initialized_device(app) router_1.node_desc.logical_type = zdo_t.LogicalType.Router router_2 = make_initialized_device(app) router_2.node_desc.logical_type = zdo_t.LogicalType.Router parent = make_initialized_device(app) app.topology.neighbors[router_1.ieee] = [ make_neighbor_from_device(router_2), make_neighbor_from_device(parent), ] app.topology.neighbors[router_2.ieee] = [ make_neighbor_from_device(parent), make_neighbor_from_device(router_1), ] app.topology.neighbors[parent.ieee] = [ make_neighbor_from_device(router_2), make_neighbor_from_device(router_1), make_neighbor_from_device(end_device), make_neighbor(ieee=make_ieee(123), nwk=0x9876), ] p1 = patch.object(end_device.zdo, "leave", AsyncMock()) p2 = patch.object(end_device.zdo, "request", AsyncMock()) p3 = patch.object(parent.zdo, "leave", AsyncMock()) p4 = patch.object(parent.zdo, "request", AsyncMock()) p5 = patch.object(router_1.zdo, "leave", AsyncMock()) p6 = patch.object(router_1.zdo, "request", AsyncMock()) p7 = patch.object(router_2.zdo, "leave", AsyncMock()) p8 = patch.object(router_2.zdo, "request", AsyncMock()) with p1, p2, p3, p4, p5, p6, p7, p8: await app.remove(end_device.ieee) for _i in range(1, 60): await asyncio.sleep(0) assert end_device.zdo.leave.await_count == 1 assert end_device.zdo.request.await_count == 0 assert router_1.zdo.leave.await_count == 0 assert router_1.zdo.request.await_count == 0 assert router_2.zdo.leave.await_count == 0 assert router_2.zdo.request.await_count == 0 assert parent.zdo.leave.await_count == 0 assert parent.zdo.request.await_count == 1 @patch("zigpy.device.Device.schedule_initialize", new_callable=MagicMock) @patch("zigpy.device.Device.schedule_group_membership_scan", new_callable=MagicMock) @patch("zigpy.device.Device.is_initialized", new_callable=PropertyMock) async def test_device_join_rejoin(is_init_mock, group_scan_mock, init_mock, app, ieee): app.listener_event = MagicMock() is_init_mock.return_value = False # First join is treated as a new join app.handle_join(0x0001, ieee, None) app.listener_event.assert_called_once_with("device_joined", ANY) app.listener_event.reset_mock() init_mock.assert_called_once() init_mock.reset_mock() # Second join with the same NWK is just a reset, not a join app.handle_join(0x0001, ieee, None) app.listener_event.assert_not_called() group_scan_mock.assert_not_called() # Since the device is still partially initialized, re-initialize it init_mock.assert_called_once() init_mock.reset_mock() # Another join with the same NWK but initialized will trigger a group re-scan is_init_mock.return_value = True app.handle_join(0x0001, ieee, None) is_init_mock.return_value = True app.listener_event.assert_not_called() group_scan_mock.assert_called_once() group_scan_mock.reset_mock() init_mock.assert_not_called() # Join with a different NWK but the same IEEE is a re-join app.handle_join(0x0002, ieee, None) app.listener_event.assert_called_once_with("device_joined", ANY) group_scan_mock.assert_not_called() init_mock.assert_called_once() async def test_get_device(app): """Test get_device.""" await app.startup() app.add_device(t.EUI64.convert("11:11:11:11:22:22:22:22"), 0x0000) dev_2 = app.add_device(app.state.node_info.ieee, 0x0000) app.add_device(t.EUI64.convert("11:11:11:11:22:22:22:33"), 0x0000) assert app.get_device(nwk=0x0000) is dev_2 async def test_probe_success(): config = {"path": "/dev/test"} with ( patch.object(App, "connect") as connect, patch.object(App, "disconnect") as disconnect, ): result = await App.probe(config) assert set(config.items()) <= set(result.items()) assert connect.await_count == 1 assert disconnect.await_count == 1 async def test_probe_failure(): config = {"path": "/dev/test"} with ( patch.object(App, "connect", side_effect=asyncio.TimeoutError) as connect, patch.object(App, "disconnect") as disconnect, ): result = await App.probe(config) assert result is False assert connect.await_count == 1 assert disconnect.await_count == 1 async def test_form_network(app): with patch.object(app, "write_network_info") as write1: await app.form_network() with patch.object(app, "write_network_info") as write2: await app.form_network() nwk_info1 = write1.mock_calls[0].kwargs["network_info"] node_info1 = write1.mock_calls[0].kwargs["node_info"] nwk_info2 = write2.mock_calls[0].kwargs["network_info"] node_info2 = write2.mock_calls[0].kwargs["node_info"] assert node_info1 == node_info2 # Critical network settings are randomized assert nwk_info1.extended_pan_id != nwk_info2.extended_pan_id assert nwk_info1.pan_id != nwk_info2.pan_id assert nwk_info1.network_key != nwk_info2.network_key # The well-known TCLK is used assert ( nwk_info1.tc_link_key.key == nwk_info2.tc_link_key.key == t.KeyData(b"ZigBeeAlliance09") ) assert nwk_info1.channel in (11, 15, 20, 25) @mock.patch("zigpy.util.pick_optimal_channel", mock.Mock(return_value=22)) async def test_form_network_find_best_channel(app): orig_start_network = app.start_network async def start_network(*args, **kwargs): start_network.await_count += 1 if start_network.await_count == 1: raise NetworkNotFormed return await orig_start_network(*args, **kwargs) start_network.await_count = 0 app.start_network = start_network with patch.object(app, "write_network_info") as write: with patch.object( app.backups, "create_backup", wraps=app.backups.create_backup ) as create_backup: await app.form_network() assert start_network.await_count == 2 # A temporary network will be formed first nwk_info1 = write.mock_calls[0].kwargs["network_info"] assert nwk_info1.channel == 11 # Then, after the scan, a better channel is chosen nwk_info2 = write.mock_calls[1].kwargs["network_info"] assert nwk_info2.channel == 22 # Only a single backup will be present assert create_backup.await_count == 1 async def test_startup_formed(): app = make_app({}) app.start_network = AsyncMock(wraps=app.start_network) app.form_network = AsyncMock() app.permit = AsyncMock() await app.startup(auto_form=False) assert app.start_network.await_count == 1 assert app.form_network.await_count == 0 assert app.permit.await_count == 1 async def test_startup_not_formed(): app = make_app({}) app.start_network = AsyncMock(wraps=app.start_network) app.form_network = AsyncMock() app.load_network_info = AsyncMock( side_effect=[NetworkNotFormed(), NetworkNotFormed(), None] ) app.permit = AsyncMock() app.backups.backups = [] app.backups.restore_backup = AsyncMock() with pytest.raises(NetworkNotFormed): await app.startup(auto_form=False) assert app.start_network.await_count == 0 assert app.form_network.await_count == 0 assert app.permit.await_count == 0 await app.startup(auto_form=True) assert app.start_network.await_count == 1 assert app.form_network.await_count == 1 assert app.permit.await_count == 1 assert app.backups.restore_backup.await_count == 0 async def test_startup_not_formed_with_backup(): app = make_app({}) app.start_network = AsyncMock(wraps=app.start_network) app.load_network_info = AsyncMock(side_effect=[NetworkNotFormed(), None]) app.permit = AsyncMock() app.backups.restore_backup = AsyncMock() app.backups.backups = [sentinel.OLD_BACKUP, sentinel.NEW_BACKUP] await app.startup(auto_form=True) assert app.start_network.await_count == 1 app.backups.restore_backup.assert_called_once_with(sentinel.NEW_BACKUP) async def test_startup_backup(): app = make_app({conf.CONF_NWK_BACKUP_ENABLED: True}) with patch("zigpy.backups.BackupManager.start_periodic_backups") as p: await app.startup() p.assert_called_once() async def test_startup_no_backup(): app = make_app({conf.CONF_NWK_BACKUP_ENABLED: False}) with patch("zigpy.backups.BackupManager.start_periodic_backups") as p: await app.startup() p.assert_not_called() def with_attributes(obj, **attrs): for k, v in attrs.items(): setattr(obj, k, v) return obj @pytest.mark.parametrize( "error", [ with_attributes(OSError("Network is unreachable"), errno=errno.ENETUNREACH), ConnectionRefusedError(), ], ) async def test_startup_failure_transient_error(error): app = make_app({conf.CONF_NWK_BACKUP_ENABLED: False}) with patch.object(app, "connect", side_effect=[error]): with pytest.raises(TransientConnectionError): await app.startup() @patch("zigpy.backups.BackupManager.from_network_state") @patch("zigpy.backups.BackupManager.most_recent_backup") async def test_initialize_compatible_backup( mock_most_recent_backup, mock_backup_from_state ): app = make_app({conf.CONF_NWK_VALIDATE_SETTINGS: True}) mock_backup_from_state.return_value.is_compatible_with.return_value = True await app.initialize() mock_backup_from_state.return_value.is_compatible_with.assert_called_once() mock_most_recent_backup.assert_called_once() @patch("zigpy.backups.BackupManager.from_network_state") @patch("zigpy.backups.BackupManager.most_recent_backup") async def test_initialize_incompatible_backup( mock_most_recent_backup, mock_backup_from_state ): app = make_app({conf.CONF_NWK_VALIDATE_SETTINGS: True}) mock_backup_from_state.return_value.is_compatible_with.return_value = False with pytest.raises(NetworkSettingsInconsistent) as exc: await app.initialize() mock_backup_from_state.return_value.is_compatible_with.assert_called_once() mock_most_recent_backup.assert_called_once() assert exc.value.old_state is mock_most_recent_backup() assert exc.value.new_state is mock_backup_from_state.return_value async def test_relays_received_device_exists(app): device = MagicMock() app._discover_unknown_device = AsyncMock(spec_set=app._discover_unknown_device) app.get_device = MagicMock(spec_set=app.get_device, return_value=device) app.handle_relays(nwk=0x1234, relays=[0x5678, 0xABCD]) app.get_device.assert_called_once_with(nwk=0x1234) assert device.relays == [0x5678, 0xABCD] assert app._discover_unknown_device.call_count == 0 async def test_relays_received_device_does_not_exist(app): app._discover_unknown_device = AsyncMock(spec_set=app._discover_unknown_device) app.get_device = MagicMock(wraps=app.get_device) app.handle_relays(nwk=0x1234, relays=[0x5678, 0xABCD]) app.get_device.assert_called_once_with(nwk=0x1234) app._discover_unknown_device.assert_called_once_with(nwk=0x1234) async def test_request_concurrency(): current_concurrency = 0 peak_concurrency = 0 class SlowApp(App): async def send_packet(self, packet): nonlocal current_concurrency, peak_concurrency async with self._limit_concurrency(): current_concurrency += 1 peak_concurrency = max(peak_concurrency, current_concurrency) await asyncio.sleep(0.1) current_concurrency -= 1 if packet % 10 == 7: # Fail randomly raise DeliveryError("Failure") app = make_app({conf.CONF_MAX_CONCURRENT_REQUESTS: 16}, app_base=SlowApp) assert current_concurrency == 0 assert peak_concurrency == 0 await asyncio.gather( *[app.send_packet(i) for i in range(100)], return_exceptions=True ) assert current_concurrency == 0 assert peak_concurrency == 16 @pytest.fixture def device(): device = MagicMock() device.nwk = 0xABCD device.ieee = t.EUI64.convert("aa:bb:cc:dd:11:22:33:44") return device @pytest.fixture def packet(app, device): return t.ZigbeePacket( src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=app.state.node_info.nwk ), src_ep=0x9A, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk), dst_ep=0xBC, tsn=0xDE, profile_id=0x1234, cluster_id=0x0006, data=t.SerializableBytes(b"test data"), source_route=None, extended_timeout=False, tx_options=t.TransmitOptions.NONE, ) async def test_request(app, device, packet): app.build_source_route_to = MagicMock(spec_set=app.build_source_route_to) async def send_request(app, **kwargs): kwargs = { "device": device, "profile": 0x1234, "cluster": 0x0006, "src_ep": 0x9A, "dst_ep": 0xBC, "sequence": 0xDE, "data": b"test data", "expect_reply": True, "use_ieee": False, "extended_timeout": False, **kwargs, } return await app.request(**kwargs) # Test sending with NWK status, msg = await send_request(app) assert status == zigpy.zcl.foundation.Status.SUCCESS assert isinstance(msg, str) app.send_packet.assert_called_once_with(packet) app.send_packet.reset_mock() # Test sending with IEEE await send_request(app, use_ieee=True) app.send_packet.assert_called_once_with( packet.replace( src=t.AddrModeAddress( addr_mode=t.AddrMode.IEEE, address=app.state.node_info.ieee, ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.IEEE, address=device.ieee, ), ) ) app.send_packet.reset_mock() # Test sending with source route app.build_source_route_to.return_value = [0x000A, 0x000B] with patch.dict(app.config, {conf.CONF_SOURCE_ROUTING: True}): await send_request(app) app.build_source_route_to.assert_called_once_with(dest=device) app.send_packet.assert_called_once_with( packet.replace(source_route=[0x000A, 0x000B]) ) app.send_packet.reset_mock() # Test sending without waiting for a reply status, msg = await send_request(app, expect_reply=False) app.send_packet.assert_called_once_with( packet.replace(tx_options=t.TransmitOptions.ACK) ) app.send_packet.reset_mock() # Test explicit ACK control (enabled) status, msg = await send_request(app, ask_for_ack=True) app.send_packet.assert_called_once_with( packet.replace(tx_options=t.TransmitOptions.ACK) ) app.send_packet.reset_mock() # Test explicit ACK control (disabled) status, msg = await send_request(app, ask_for_ack=False) app.send_packet.assert_called_once_with( packet.replace(tx_options=t.TransmitOptions(0)) ) app.send_packet.reset_mock() async def test_request_retrying_success(app, device, packet) -> None: app.send_packet.side_effect = [ DeliveryError("Failure"), DeliveryError("Failure"), None, ] await app.request( device=device, profile=0x1234, cluster=0x0006, src_ep=0x9A, dst_ep=0xBC, sequence=0xDE, data=b"test data", expect_reply=True, use_ieee=False, extended_timeout=False, ) assert app.send_packet.mock_calls == [ call(packet), call( packet.replace( tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY ) ), call( packet.replace( tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY ) ), ] async def test_request_retrying_failure(app, device, packet) -> None: app.send_packet.side_effect = [ DeliveryError("Failure"), DeliveryError("Failure"), DeliveryError("Failure"), ] with pytest.raises(DeliveryError): await app.request( device=device, profile=0x1234, cluster=0x0006, src_ep=0x9A, dst_ep=0xBC, sequence=0xDE, data=b"test data", expect_reply=True, use_ieee=False, extended_timeout=False, ) assert app.send_packet.mock_calls == [ call(packet), call( packet.replace( tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY ) ), call( packet.replace( tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY ) ), ] def test_build_source_route_has_relays(app): device = MagicMock() device.relays = [0x1234, 0x5678] assert app.build_source_route_to(device) == [0x5678, 0x1234] def test_build_source_route_no_relays(app): device = MagicMock() device.relays = None assert app.build_source_route_to(device) is None async def test_send_mrequest(app, packet): status, msg = await app.mrequest( group_id=0xABCD, profile=0x1234, cluster=0x0006, src_ep=0x9A, sequence=0xDE, data=b"test data", hops=12, non_member_radius=34, ) assert status == zigpy.zcl.foundation.Status.SUCCESS assert isinstance(msg, str) app.send_packet.assert_called_once_with( packet.replace( dst=t.AddrModeAddress(addr_mode=t.AddrMode.Group, address=0xABCD), dst_ep=None, radius=12, non_member_radius=34, tx_options=t.TransmitOptions.NONE, ) ) async def test_send_broadcast(app, packet): status, msg = await app.broadcast( profile=0x1234, cluster=0x0006, src_ep=0x9A, dst_ep=0xBC, grpid=0x0000, # unused radius=12, sequence=0xDE, data=b"test data", broadcast_address=t.BroadcastAddress.RX_ON_WHEN_IDLE, ) assert status == zigpy.zcl.foundation.Status.SUCCESS assert isinstance(msg, str) app.send_packet.assert_called_once_with( packet.replace( dst=t.AddrModeAddress( addr_mode=t.AddrMode.Broadcast, address=t.BroadcastAddress.RX_ON_WHEN_IDLE, ), radius=12, tx_options=t.TransmitOptions.NONE, ) ) @pytest.fixture def zdo_packet(app, device): return t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=app.state.node_info.nwk ), src_ep=0x00, # ZDO dst_ep=0x00, tsn=0xDE, profile_id=0x0000, cluster_id=0x0000, data=t.SerializableBytes(b""), source_route=None, extended_timeout=False, tx_options=t.TransmitOptions.ACK, lqi=123, rssi=-80, ) @patch("zigpy.device.Device.initialize", AsyncMock()) async def test_packet_received_new_device_zdo_announce(app, device, zdo_packet): app.handle_join = MagicMock(wraps=app.handle_join) zdo_data = zigpy.zdo.ZDO(None)._serialize( zdo_t.ZDOCmd.Device_annce, *{ "NWKAddr": device.nwk, "IEEEAddr": device.ieee, "Capability": 0x00, }.values(), ) zdo_packet.cluster_id = zdo_t.ZDOCmd.Device_annce zdo_packet.data = t.SerializableBytes( t.uint8_t(zdo_packet.tsn).serialize() + zdo_data ) app.packet_received(zdo_packet) app.handle_join.assert_called_once_with( nwk=device.nwk, ieee=device.ieee, parent_nwk=None ) zigpy_device = app.get_device(ieee=device.ieee) assert zigpy_device.lqi == zdo_packet.lqi assert zigpy_device.rssi == zdo_packet.rssi @patch("zigpy.device.Device.initialize", AsyncMock()) async def test_packet_received_new_device_discovery(app, device, zdo_packet): app.handle_join = MagicMock(wraps=app.handle_join) async def send_packet(packet): if packet.dst_ep != 0x00 or packet.cluster_id != zdo_t.ZDOCmd.IEEE_addr_req: return hdr, args = zigpy.zdo.ZDO(None).deserialize( packet.cluster_id, packet.data.serialize() ) assert args == list( { "NWKAddrOfInterest": device.nwk, "RequestType": zdo_t.AddrRequestType.Single, "StartIndex": 0, }.values() ) zdo_data = zigpy.zdo.ZDO(None)._serialize( zdo_t.ZDOCmd.IEEE_addr_rsp, *{ "Status": zdo_t.Status.SUCCESS, "IEEEAddr": device.ieee, "NWKAddr": device.nwk, "NumAssocDev": 0, "StartIndex": 0, "NWKAddrAssocDevList": [], }.values(), ) # Receive the IEEE address reply zdo_packet.data = t.SerializableBytes( t.uint8_t(zdo_packet.tsn).serialize() + zdo_data ) zdo_packet.cluster_id = zdo_t.ZDOCmd.IEEE_addr_rsp app.packet_received(zdo_packet) app.send_packet = AsyncMock(side_effect=send_packet) # Receive a bogus packet first, to trigger device discovery bogus_packet = zdo_packet.replace(dst_ep=0x01, src_ep=0x01) app.packet_received(bogus_packet) await asyncio.sleep(0.1) app.handle_join.assert_called_once_with( nwk=device.nwk, ieee=device.ieee, parent_nwk=None, handle_rejoin=False ) zigpy_device = app.get_device(ieee=device.ieee) assert zigpy_device.lqi == zdo_packet.lqi assert zigpy_device.rssi == zdo_packet.rssi @patch("zigpy.device.Device.initialize", AsyncMock()) async def test_packet_received_ieee_no_rejoin(app, device, zdo_packet, caplog): device.is_initialized = True app.devices[device.ieee] = device app.handle_join = MagicMock(wraps=app.handle_join) zdo_data = zigpy.zdo.ZDO(None)._serialize( zdo_t.ZDOCmd.IEEE_addr_rsp, *{ "Status": zdo_t.Status.SUCCESS, "IEEEAddr": device.ieee, "NWKAddr": device.nwk, }.values(), ) zdo_packet.cluster_id = zdo_t.ZDOCmd.IEEE_addr_rsp zdo_packet.data = t.SerializableBytes( t.uint8_t(zdo_packet.tsn).serialize() + zdo_data ) app.packet_received(zdo_packet) assert "joined the network" not in caplog.text app.handle_join.assert_called_once_with( nwk=device.nwk, ieee=device.ieee, parent_nwk=None, handle_rejoin=False ) assert len(device.schedule_group_membership_scan.mock_calls) == 0 assert len(device.schedule_initialize.mock_calls) == 0 @patch("zigpy.device.Device.initialize", AsyncMock()) async def test_packet_received_ieee_rejoin(app, device, zdo_packet, caplog): device.is_initialized = True app.devices[device.ieee] = device app.handle_join = MagicMock(wraps=app.handle_join) zdo_data = zigpy.zdo.ZDO(None)._serialize( zdo_t.ZDOCmd.IEEE_addr_rsp, *{ "Status": zdo_t.Status.SUCCESS, "IEEEAddr": device.ieee, "NWKAddr": device.nwk + 1, # NWK has changed }.values(), ) zdo_packet.cluster_id = zdo_t.ZDOCmd.IEEE_addr_rsp zdo_packet.data = t.SerializableBytes( t.uint8_t(zdo_packet.tsn).serialize() + zdo_data ) app.packet_received(zdo_packet) assert "joined the network" not in caplog.text app.handle_join.assert_called_once_with( nwk=device.nwk, ieee=device.ieee, parent_nwk=None, handle_rejoin=False ) assert len(device.schedule_initialize.mock_calls) == 1 async def test_bad_zdo_packet_received(app, device): device.is_initialized = True app.devices[device.ieee] = device bogus_zdo_packet = t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=0, # bad destination endpoint tsn=180, profile_id=260, cluster_id=6, data=t.SerializableBytes(b"\x08n\n\x00\x00\x10\x00"), lqi=255, rssi=-30, ) app.packet_received(bogus_zdo_packet) assert len(device.packet_received.mock_calls) == 1 def test_get_device_with_address_nwk(app, device): app.devices[device.ieee] = device assert ( app.get_device_with_address( t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk) ) is device ) assert ( app.get_device_with_address( t.AddrModeAddress(addr_mode=t.AddrMode.IEEE, address=device.ieee) ) is device ) with pytest.raises(ValueError): app.get_device_with_address( t.AddrModeAddress(addr_mode=t.AddrMode.Group, address=device.nwk) ) with pytest.raises(KeyError): app.get_device_with_address( t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk + 1) ) async def test_request_future_matching(app, make_initialized_device): device = make_initialized_device(app) device._packet_debouncer.filter = MagicMock(return_value=False) ota = device.endpoints[1].add_output_cluster(clusters.general.Ota.cluster_id) req_hdr, req_cmd = ota._create_request( general=False, command_id=ota.commands_by_name["query_next_image"].id, schema=ota.commands_by_name["query_next_image"].schema, disable_default_response=False, direction=foundation.Direction.Client_to_Server, args=(), kwargs={ "field_control": 0, "manufacturer_code": 0x1234, "image_type": 0x5678, "current_file_version": 0x11112222, }, ) packet = t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=req_hdr.tsn, profile_id=260, cluster_id=ota.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) assert not app._req_listeners[device] with app.wait_for_response( device, [ota.commands_by_name["query_next_image"].schema()] ) as rsp_fut: # Attach two listeners with app.wait_for_response( device, [ota.commands_by_name["query_next_image"].schema()] ) as rsp_fut2: assert app._req_listeners[device] # Listeners are resolved FIFO app.packet_received(packet) assert rsp_fut.done() assert not rsp_fut2.done() app.packet_received(packet) assert rsp_fut.done() assert rsp_fut2.done() # Unhandled packets are ignored app.packet_received(packet) rsp_hdr, rsp_cmd = await rsp_fut assert rsp_hdr == req_hdr assert rsp_cmd == req_cmd assert rsp_cmd.current_file_version == 0x11112222 assert not app._req_listeners[device] async def test_request_callback_matching(app, make_initialized_device): device = make_initialized_device(app) device._packet_debouncer.filter = MagicMock(return_value=False) ota = device.endpoints[1].add_output_cluster(clusters.general.Ota.cluster_id) req_hdr, req_cmd = ota._create_request( general=False, command_id=ota.commands_by_name["query_next_image"].id, schema=ota.commands_by_name["query_next_image"].schema, disable_default_response=False, direction=foundation.Direction.Client_to_Server, args=(), kwargs={ "field_control": 0, "manufacturer_code": 0x1234, "image_type": 0x5678, "current_file_version": 0x11112222, }, ) packet = t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=req_hdr.tsn, profile_id=260, cluster_id=ota.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) mock_callback = mock.Mock() assert not app._req_listeners[device] with app.callback_for_response( device, [ota.commands_by_name["query_next_image"].schema()], mock_callback ): assert app._req_listeners[device] asyncio.get_running_loop().call_soon(app.packet_received, packet) asyncio.get_running_loop().call_soon(app.packet_received, packet) asyncio.get_running_loop().call_soon(app.packet_received, packet) await asyncio.sleep(0.1) assert len(mock_callback.mock_calls) == 3 assert mock_callback.mock_calls == [mock.call(req_hdr, req_cmd)] * 3 assert not app._req_listeners[device] async def test_energy_scan_default(app): await app.startup() raw_scan_results = [ 170, 191, 181, 165, 179, 169, 196, 163, 174, 162, 190, 186, 191, 178, 204, 187, ] coordinator = app._device coordinator.zdo.Mgmt_NWK_Update_req = AsyncMock( return_value=[ zdo_t.Status.SUCCESS, t.Channels.ALL_CHANNELS, 29, 10, raw_scan_results, ] ) results = await app.energy_scan( channels=t.Channels.ALL_CHANNELS, duration_exp=2, count=1 ) assert len(results) == 16 assert results == dict(zip(range(11, 26 + 1), raw_scan_results)) async def test_energy_scan_not_implemented(app): """Energy scanning still "works" even when the radio doesn't implement it.""" await app.startup() app._device.zdo.Mgmt_NWK_Update_req.side_effect = asyncio.TimeoutError() results = await app.energy_scan( channels=t.Channels.ALL_CHANNELS, duration_exp=2, count=1 ) assert results == {c: 0 for c in range(11, 26 + 1)} async def test_startup_broadcast_failure_due_to_interference(app, caplog): err = DeliveryError( "Failed to deliver packet: ", 225 ) with mock.patch.object(app, "permit", side_effect=err): with caplog.at_level(logging.WARNING): await app.startup() # The application will still start up, however assert "Failed to send startup broadcast" in caplog.text assert "interference" in caplog.text async def test_startup_broadcast_failure_other(app, caplog): with mock.patch.object(app, "permit", side_effect=DeliveryError("Error", 123)): with pytest.raises(DeliveryError, match="^Error$"): await app.startup() @patch("zigpy.application.CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S", 0.1) @patch("zigpy.application.CHANNEL_CHANGE_BROADCAST_DELAY_S", 0.01) async def test_move_network_to_new_channel(app): async def nwk_update(*args, **kwargs): async def inner(): await asyncio.sleep( zigpy.application.CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S * 5 ) NwkUpdate = args[0] app.state.network_info.channel = list(NwkUpdate.ScanChannels)[0] app.state.network_info.nwk_update_id = NwkUpdate.nwkUpdateId asyncio.create_task(inner()) # noqa: RUF006 await app.startup() assert app.state.network_info.channel != 26 with patch.object( app._device.zdo, "Mgmt_NWK_Update_req", side_effect=nwk_update ) as mock_update: await app.move_network_to_channel(new_channel=26, num_broadcasts=10) assert app.state.network_info.channel == 26 assert len(mock_update.mock_calls) == 1 async def test_move_network_to_new_channel_noop(app): await app.startup() old_channel = app.state.network_info.channel with patch("zigpy.zdo.broadcast") as mock_broadcast: await app.move_network_to_channel(new_channel=old_channel) assert app.state.network_info.channel == old_channel assert len(mock_broadcast.mock_calls) == 0 async def test_startup_multiple_dblistener(app): app._dblistener = AsyncMock() app.connect = AsyncMock(side_effect=RuntimeError()) with pytest.raises(RuntimeError): await app.startup() with pytest.raises(RuntimeError): await app.startup() # The database listener will not be shut down automatically assert len(app._dblistener.shutdown.mock_calls) == 0 async def test_connection_lost(app): exc = RuntimeError() listener = MagicMock() app.add_listener(listener) app.connection_lost(exc) listener.connection_lost.assert_called_with(exc) async def test_watchdog(app): error = RuntimeError() app = make_app({}) app._watchdog_period = 0.1 app._watchdog_feed = AsyncMock(side_effect=[None, None, error]) app.connection_lost = MagicMock() assert app._watchdog_task is None await app.startup() assert app._watchdog_task is not None # We call it once during startup synchronously assert app._watchdog_feed.mock_calls == [call()] assert app.connection_lost.mock_calls == [] await asyncio.sleep(0.5) assert app._watchdog_feed.mock_calls == [call(), call(), call()] assert app.connection_lost.mock_calls == [call(error)] assert app._watchdog_task.done() @pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_permit_with_key(app): app = make_app({}) app.permit_with_link_key = AsyncMock() with pytest.raises(ValueError): await app.permit_with_key( node=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), code=b"invalid code that is far too long and of the wrong parity", time_s=60, ) assert app.permit_with_link_key.mock_calls == [] await app.permit_with_key( node=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), code=bytes.fromhex("11223344556677884AF7"), time_s=60, ) assert app.permit_with_link_key.mock_calls == [ call( node=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), link_key=t.KeyData.convert("41618FC0C83B0E14A589954B16E31466"), time_s=60, ) ] async def test_probe(app): class BaudSpecificApp(App): _probe_configs = [ {conf.CONF_DEVICE_BAUDRATE: 57600}, {conf.CONF_DEVICE_BAUDRATE: 115200}, ] async def connect(self): if self._config[conf.CONF_DEVICE][conf.CONF_DEVICE_BAUDRATE] != 115200: raise asyncio.TimeoutError # Only one baudrate is valid assert (await BaudSpecificApp.probe({conf.CONF_DEVICE_PATH: "/dev/null"})) == { conf.CONF_DEVICE_PATH: "/dev/null", conf.CONF_DEVICE_BAUDRATE: 115200, conf.CONF_DEVICE_FLOW_CONTROL: None, } class NeverConnectsApp(App): async def connect(self): raise asyncio.TimeoutError # No settings will work assert (await NeverConnectsApp.probe({conf.CONF_DEVICE_PATH: "/dev/null"})) is False async def test_network_scan(app) -> None: beacons = [ t.NetworkBeacon( pan_id=t.NWK(0x1234), extended_pan_id=t.EUI64.convert("11:22:33:44:55:66:77:88"), channel=11, nwk_update_id=1, permit_joining=True, stack_profile=2, lqi=255, rssi=-80, ), t.NetworkBeacon( pan_id=t.NWK(0xABCD), extended_pan_id=t.EUI64.convert("11:22:33:44:55:66:77:88"), channel=15, nwk_update_id=2, permit_joining=False, stack_profile=2, lqi=255, rssi=-40, ), ] with patch.object(app, "_network_scan") as mock_scan: mock_scan.return_value.__aiter__.return_value = beacons results = [ b async for b in app.network_scan( channels=t.Channels.from_channel_list([11, 15]), duration_exp=1, ) ] assert results == beacons assert mock_scan.mock_calls == [ call( channels=t.Channels.from_channel_list([11, 15]), duration_exp=1, ), call().__aiter__(), ] async def test_packet_capture(app) -> None: packets = [ t.CapturedPacket( timestamp=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc), rssi=-60, lqi=250, channel=15, data=bytes.fromhex("02007f"), ), t.CapturedPacket( timestamp=datetime(2021, 1, 1, 0, 0, 1, tzinfo=timezone.utc), rssi=-70, lqi=240, channel=15, data=bytes.fromhex( "61886fefbe445600004802653c00001e1228eea3dd0046b8a11c004b120000631ea30c" "f9079829433d9b6165c3b56171df2557407024" ), ), ] with patch.object(app, "_packet_capture") as mock_capture: mock_capture.return_value.__aiter__.return_value = packets results = [p async for p in app.packet_capture(channel=15)] assert results == packets assert packets[0].compute_fcs() == b"\xc8\x3e" assert packets[1].compute_fcs() == b"\x63\x7d" with patch.object(app, "_packet_capture_change_channel"): await app.packet_capture_change_channel(channel=25) assert app._packet_capture_change_channel.mock_calls == [call(channel=25)] zigpy-0.80.1/tests/test_backports.py000066400000000000000000000013031501451476000175050ustar00rootroot00000000000000"""Test zigpy backports.""" from enum import auto import pytest from zigpy.backports.enum import StrEnum def test_strenum() -> None: """Test StrEnum.""" class TestEnum(StrEnum): Test = "test" assert str(TestEnum.Test) == "test" assert TestEnum.Test == "test" assert TestEnum("test") is TestEnum.Test assert TestEnum(TestEnum.Test) is TestEnum.Test with pytest.raises(ValueError): TestEnum(42) with pytest.raises(ValueError): TestEnum("str but unknown") with pytest.raises(TypeError): class FailEnum(StrEnum): Test = 42 with pytest.raises(TypeError): class FailEnum2(StrEnum): Test = auto() zigpy-0.80.1/tests/test_backups.py000066400000000000000000000302401501451476000171470ustar00rootroot00000000000000from datetime import datetime, timedelta, timezone import json import pytest from tests.async_mock import AsyncMock from tests.conftest import app # noqa: F401 import zigpy.backups import zigpy.state as app_state import zigpy.types as t import zigpy.zdo.types as zdo_t @pytest.fixture def backup_factory(): def inner(): return zigpy.backups.NetworkBackup( backup_time=datetime(2021, 2, 8, 19, 35, 24, 761000, tzinfo=timezone.utc), node_info=app_state.NodeInfo( nwk=t.NWK(0x0000), ieee=t.EUI64.convert("93:2C:A9:34:D9:D0:5D:12"), logical_type=zdo_t.LogicalType.Coordinator, model="Coordinator Model", manufacturer="Coordinator Manufacturer", version="1.2.3.4", ), network_info=app_state.NetworkInfo( extended_pan_id=t.ExtendedPanId.convert("0D:49:91:99:AE:CD:3C:35"), pan_id=t.PanId(0x9BB0), nwk_update_id=0x12, nwk_manager_id=t.NWK(0x0000), channel=t.uint8_t(15), channel_mask=t.Channels.from_channel_list([15, 20, 25]), security_level=t.uint8_t(5), network_key=app_state.Key( key=t.KeyData.convert( "9A:79:D6:9A:DA:EC:45:C6:F2:EF:EB:AF:DA:A3:07:B6" ), seq=108, tx_counter=39009277, ), tc_link_key=app_state.Key( key=t.KeyData(b"ZigBeeAlliance09"), partner_ieee=t.EUI64.convert("93:2C:A9:34:D9:D0:5D:12"), tx_counter=8712428, ), key_table=[ app_state.Key( key=t.KeyData.convert( "85:7C:05:00:3E:76:1A:F9:68:9A:49:41:6A:60:5C:76" ), tx_counter=3792973670, rx_counter=1083290572, seq=147, partner_ieee=t.EUI64.convert("69:0C:07:52:AA:D7:7D:71"), ), app_state.Key( key=t.KeyData.convert( "CA:02:E8:BB:75:7C:94:F8:93:39:D3:9C:B3:CD:A7:BE" ), tx_counter=2597245184, rx_counter=824424412, seq=19, partner_ieee=t.EUI64.convert("A3:1A:F6:8E:19:95:23:BE"), ), ], children=[ # Has a key t.EUI64.convert("A3:1A:F6:8E:19:95:23:BE"), # Random device with no NWK address or key t.EUI64.convert("A4:02:A0:DC:17:D8:17:DF"), # Does not have a key t.EUI64.convert("C6:DF:28:F9:60:33:DB:03"), ], # If exposed by the stack, NWK addresses of other connected devices on the network nwk_addresses={ # Two children above t.EUI64.convert("A3:1A:F6:8E:19:95:23:BE"): t.NWK(0x2C59), t.EUI64.convert("C6:DF:28:F9:60:33:DB:03"): t.NWK(0x1CA0), # Random devices on the network t.EUI64.convert("7A:BF:38:A9:59:21:A0:7A"): t.NWK(0x16B5), t.EUI64.convert("10:55:FE:67:24:EA:96:D3"): t.NWK(0xBFB9), t.EUI64.convert("9A:0E:10:50:00:1B:1A:5F"): t.NWK(0x1AF6), t.EUI64.convert("AA:BB:CC:DD:11:22:33:44"): t.NWK(0x0ABC), }, stack_specific={ "zstack": {"tclk_seed": "71e31105bb92a2d15747a0d0a042dbfd"} }, metadata={"zstack": {"version": "20220102"}}, ), ) return inner @pytest.fixture def backup(backup_factory): return backup_factory() @pytest.fixture def z2m_backup_json(): return { "metadata": { "format": "zigpy/open-coordinator-backup", "version": 1, "source": "zigbee-herdsman@0.13.65", "internal": {"date": "2021-02-08T19:35:24.761Z", "znpVersion": 2}, }, "stack_specific": {"zstack": {"tclk_seed": "71e31105bb92a2d15747a0d0a042dbfd"}}, "coordinator_ieee": "932ca934d9d05d12", "pan_id": "9bb0", "extended_pan_id": "0d499199aecd3c35", "nwk_update_id": 18, "security_level": 5, "channel": 15, "channel_mask": [15, 20, 25], "network_key": { "key": "9a79d69adaec45c6f2efebafdaa307b6", "sequence_number": 108, "frame_counter": 39009277, }, "devices": [ { "nwk_address": "2c59", "ieee_address": "a31af68e199523be", "link_key": { "key": "ca02e8bb757c94f89339d39cb3cda7be", "tx_counter": 2597245184, "rx_counter": 824424412, }, # "is_child": True, # Implicitly a child device }, { "nwk_address": None, "ieee_address": "690c0752aad77d71", "link_key": { "key": "857c05003e761af9689a49416a605c76", "tx_counter": 3792973670, "rx_counter": 1083290572, }, "is_child": False, }, { "nwk_address": None, "ieee_address": "a402a0dc17d817df", "is_child": True, }, { "nwk_address": "1ca0", "ieee_address": "c6df28f96033db03", "is_child": True, }, { "nwk_address": "16b5", "ieee_address": "7abf38a95921a07a", "is_child": False, }, { "nwk_address": "bfb9", "ieee_address": "1055fe6724ea96d3", "is_child": False, }, { "nwk_address": "1af6", "ieee_address": "9a0e1050001b1a5f", "is_child": False, }, { "nwk_address": "abc", # missing `0` prefix "ieee_address": "aabbccdd11223344", "is_child": False, }, ], } @pytest.fixture def zigate_backup_json(): return { "backup_time": "2022-07-20T17:58:16.694438+00:00", "network_info": { "extended_pan_id": "9d:ff:72:2d:19:2c:d1:01", "pan_id": "D08A", "nwk_update_id": 0, # missing "nwk_manager_id": "0000", "channel": 15, "channel_mask": [15], "security_level": 5, "network_key": { # missing "key": "ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff:ff", "tx_counter": 0, "rx_counter": 0, "seq": 0, "partner_ieee": "ff:ff:ff:ff:ff:ff:ff:ff", }, "tc_link_key": { # missing "key": "5a:69:67:42:65:65:41:6c:6c:69:61:6e:63:65:30:39", "tx_counter": 0, "rx_counter": 0, "seq": 0, "partner_ieee": "00:15:8d:00:06:a3:fd:fe", }, "key_table": [], "children": [], "nwk_addresses": {}, "stack_specific": {}, "metadata": {"zigate": {"version": "3.21"}}, "source": "zigpy-zigate@0.9.0", }, "node_info": { "nwk": "0000", "ieee": "00:15:8d:00:06:a3:fd:fe", "logical_type": "coordinator", }, } def test_state_backup_as_dict(backup): obj = json.loads(json.dumps(backup.as_dict())) restored_backup = type(backup).from_dict(obj) assert backup == restored_backup def test_state_backup_as_open_coordinator(backup): obj = json.loads(json.dumps(backup.as_open_coordinator_json())) backup2 = zigpy.backups.NetworkBackup.from_open_coordinator_json(obj) assert backup == backup2 def test_z2m_backup_parsing(z2m_backup_json, backup): backup.network_info.metadata = None backup.network_info.source = None backup.node_info.manufacturer = None backup.node_info.model = None backup.node_info.version = None backup.network_info.tc_link_key.tx_counter = 0 for key in backup.network_info.key_table: key.seq = 0 backup2 = zigpy.backups.NetworkBackup.from_open_coordinator_json(z2m_backup_json) backup2.network_info.metadata = None backup2.network_info.source = None # Key order may be different backup.network_info.key_table.sort(key=lambda k: k.key) backup2.network_info.key_table.sort(key=lambda k: k.key) assert backup == backup2 def test_from_dict_automatic(z2m_backup_json): backup1 = zigpy.backups.NetworkBackup.from_open_coordinator_json(z2m_backup_json) backup2 = zigpy.backups.NetworkBackup.from_dict(z2m_backup_json) assert backup1 == backup2 def test_from_dict_failure(): with pytest.raises(ValueError): zigpy.backups.NetworkBackup.from_dict({"some": "json"}) def test_backup_compatibility(backup_factory): backup1 = backup_factory() assert backup1.is_compatible_with(backup1) # Incompatible due to different coordinator IEEE backup2 = backup_factory() backup2.node_info.ieee = t.EUI64.convert("AA:AA:AA:AA:AA:AA:AA:AA") assert not backup2.supersedes(backup1) assert not backup1.supersedes(backup2) assert not backup1.is_compatible_with(backup2) # NWK frame counter must always be greater backup3 = backup_factory() backup3.network_info.network_key.tx_counter -= 1 assert backup3.is_compatible_with(backup1) assert not backup3.supersedes(backup1) backup4 = backup_factory() backup4.network_info.network_key.tx_counter += 1 assert backup4.is_compatible_with(backup1) assert backup4.supersedes(backup1) async def test_backup_completeness(backup, zigate_backup_json): assert backup.is_complete() zigate_backup = zigpy.backups.NetworkBackup.from_dict(zigate_backup_json) assert not zigate_backup.is_complete() backups = zigpy.backups.BackupManager(None) with pytest.raises(ValueError): await backups.restore_backup(zigate_backup) async def test_add_backup(backup_factory): backups = zigpy.backups.BackupManager(None) # First backup backup1 = backup_factory() backups.add_backup(backup1) assert backups.backups == [backup1] # Adding the same backup twice will do nothing backups.add_backup(backup1) assert backups.backups == [backup1] # Adding an identical backup that is newer replaces the old one backup2 = backup_factory() backup2.backup_time += timedelta(hours=1) backups.add_backup(backup2) assert backups.backups == [backup2] # An even more recent one with a rolled back frame counter is appended backup3 = backup_factory() backup3.backup_time += timedelta(hours=2) backup3.network_info.network_key.tx_counter -= 1000 backups.add_backup(backup3) assert backups.backups == [backup2, backup3] # A final one replacing them both is added backup4 = backup_factory() backup4.backup_time += timedelta(hours=3) backup4.network_info.network_key.tx_counter += 1000 backups.add_backup(backup4) assert backups.backups == [backup4] # An incompatible backup will be added to the list. Nothing will be replaced. backup5 = backup_factory() backup5.network_info.pan_id += 1 backups.add_backup(backup5) assert backups.backups == [backup4, backup5] async def test_restore_backup_create_new(app, backup): backups = zigpy.backups.BackupManager(app) backups.create_backup = AsyncMock() await backups.restore_backup(backup) app.write_network_info.assert_called_once() backups.create_backup.assert_called_once() app.write_network_info.reset_mock() backups.create_backup.reset_mock() await backups.restore_backup(backup, create_new=False) app.write_network_info.assert_called_once() backups.create_backup.assert_not_called() # Won't be called zigpy-0.80.1/tests/test_config.py000066400000000000000000000140421501451476000167660ustar00rootroot00000000000000"""Test configuration.""" import pathlib import warnings import pytest import voluptuous as vol import zigpy.config import zigpy.config.validators @pytest.mark.parametrize( ("value", "result"), [ (False, False), (True, True), ("1", True), ("yes", True), ("YeS", True), ("on", True), ("oN", True), ("enable", True), ("enablE", True), (0, False), ("no", False), ("nO", False), ("off", False), ("ofF", False), ("disable", False), ("disablE", False), ], ) def test_config_validation_bool(value, result): """Test boolean config validation.""" assert zigpy.config.validators.cv_boolean(value) is result schema = vol.Schema({vol.Required("value"): zigpy.config.validators.cv_boolean}) validated = schema({"value": value}) assert validated["value"] is result @pytest.mark.parametrize("value", ["invalid", "not a bool", "something"]) def test_config_validation_bool_invalid(value): """Test boolean config validation.""" with pytest.raises(vol.Invalid): zigpy.config.validators.cv_boolean(value) def test_config_validation_key_not_16_list(): """Validate key fails.""" with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([0x00]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([0x00 for i in range(15)]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([0x00 for i in range(17)]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key(None) zigpy.config.validators.cv_key([0x00 for i in range(16)]) def test_config_validation_key_not_a_byte(): """Validate key fails.""" with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([-1 for i in range(16)]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([256 for i in range(16)]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([0] * 15 + [256]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([0] * 15 + [-1]) with pytest.raises(vol.Invalid): zigpy.config.validators.cv_key([0] * 15 + ["x1"]) zigpy.config.validators.cv_key([0xFF for i in range(16)]) def test_config_validation_key_success(): """Validate key success.""" key = zigpy.config.validators.cv_key(zigpy.config.CONF_NWK_TC_LINK_KEY_DEFAULT) assert key.serialize() == b"ZigBeeAlliance09" @pytest.mark.parametrize( ("value", "result"), [ (0x1234, 0x1234), ("0x1234", 0x1234), (1234, 1234), ("1234", 1234), ("001234", 1234), ("0e1234", vol.Invalid), ("1234abcd", vol.Invalid), ("0xabGG", vol.Invalid), (None, vol.Invalid), ], ) def test_config_validation_hex_number(value, result): """Test hex number config validation.""" if isinstance(result, int): assert zigpy.config.validators.cv_hex(value) == result else: with pytest.raises(vol.Invalid): zigpy.config.validators.cv_hex(value) @pytest.mark.parametrize( ("value", "result"), [ (1, vol.Invalid), (11, 11), (0x11, 17), ("26", 26), (27, vol.Invalid), ("27", vol.Invalid), ], ) def test_schema_network_channel(value, result): """Test network schema for channel.""" config = {zigpy.config.CONF_NWK_CHANNEL: value} if isinstance(result, int): config = zigpy.config.SCHEMA_NETWORK(config) assert config[zigpy.config.CONF_NWK_CHANNEL] == result else: with pytest.raises(vol.Invalid): zigpy.config.SCHEMA_NETWORK(config) def test_schema_network_pan_id(): """Test Extended Pan-id.""" config = zigpy.config.SCHEMA_NETWORK({}) assert ( config[zigpy.config.CONF_NWK_EXTENDED_PAN_ID] == zigpy.config.CONF_NWK_EXTENDED_PAN_ID_DEFAULT ) config = zigpy.config.SCHEMA_NETWORK( {zigpy.config.CONF_NWK_EXTENDED_PAN_ID: "00:11:22:33:44:55:66:77"} ) assert ( config[zigpy.config.CONF_NWK_EXTENDED_PAN_ID].serialize() == b"\x77\x66\x55\x44\x33\x22\x11\x00" ) def test_schema_network_short_pan_id(): """Test Pan-id.""" config = zigpy.config.SCHEMA_NETWORK({}) assert config[zigpy.config.CONF_NWK_PAN_ID] is None config = zigpy.config.SCHEMA_NETWORK({zigpy.config.CONF_NWK_PAN_ID: 0x1234}) assert config[zigpy.config.CONF_NWK_PAN_ID].serialize() == b"\x34\x12" def test_deprecated(): """Test key deprecation.""" schema = vol.Schema( { vol.Optional("value"): vol.All( zigpy.config.validators.cv_hex, zigpy.config.validators.cv_deprecated("Test message"), ) } ) with pytest.warns(DeprecationWarning, match="Test message"): assert schema({"value": 123}) == {"value": 123} # No warnings are raised with warnings.catch_warnings(): warnings.simplefilter("error") assert schema({}) == {} def test_cv_json_file(tmp_path: pathlib.Path) -> None: """Test `cv_json_file` validator.""" path = tmp_path / "file.json" # Does not exist with pytest.raises(vol.Invalid): zigpy.config.validators.cv_json_file(str(path)) # Not a file path.mkdir() with pytest.raises(vol.Invalid): zigpy.config.validators.cv_json_file(str(path)) path.rmdir() # File exists path.write_text("{}") assert zigpy.config.validators.cv_json_file(str(path)) == path def test_cv_folder(tmp_path: pathlib.Path) -> None: """Test `cv_folder` validator.""" folder_path = tmp_path / "folder" file_path = tmp_path / "not_folder" # Does not exist with pytest.raises(vol.Invalid): zigpy.config.validators.cv_folder(str(folder_path)) # Not a folder file_path.write_text("") with pytest.raises(vol.Invalid): zigpy.config.validators.cv_folder(str(file_path)) # Folder exists folder_path.mkdir() assert zigpy.config.validators.cv_folder(str(folder_path)) == folder_path zigpy-0.80.1/tests/test_datastructures.py000066400000000000000000000307021501451476000205770ustar00rootroot00000000000000import asyncio from unittest.mock import Mock, patch import pytest from zigpy import datastructures async def test_dynamic_bounded_semaphore_simple_locking(): """Test simple, serial locking/unlocking.""" sem = datastructures.PriorityDynamicBoundedSemaphore() assert "unlocked" not in repr(sem) and "locked" in repr(sem) assert sem.value == 0 assert sem.max_value == 0 assert sem.locked() # Invalid max value with pytest.raises(ValueError): sem.max_value = -1 assert sem.value == 0 assert sem.max_value == 0 assert sem.locked() # Max value is now specified sem.max_value = 1 assert not sem.locked() assert sem.max_value == 1 assert sem.value == 1 assert "unlocked" in repr(sem) # Semaphore can now be acquired async with sem: assert sem.value == 0 assert sem.locked() assert not sem.locked() assert sem.max_value == 1 assert sem.value == 1 await sem.acquire() assert sem.value == 0 assert sem.locked() sem.release() assert not sem.locked() assert sem.max_value == 1 assert sem.value == 1 with pytest.raises(ValueError): sem.release() async def test_dynamic_bounded_semaphore_multiple_locking(): """Test multiple locking/unlocking.""" sem = datastructures.PriorityDynamicBoundedSemaphore(5) assert sem.value == 5 assert not sem.locked() async with sem: assert sem.value == 4 assert not sem.locked() async with sem, sem, sem: assert sem.value == 1 assert not sem.locked() async with sem: assert sem.locked() assert sem.value == 0 assert not sem.locked() assert sem.value == 1 assert sem.value == 4 assert not sem.locked() assert sem.value == 5 assert not sem.locked() async def test_dynamic_bounded_semaphore_hanging_bug(): """Test semaphore hanging bug.""" sem = datastructures.PriorityDynamicBoundedSemaphore(1) async def c1(): async with sem: await asyncio.sleep(0) t2.cancel() async def c2(): async with sem: pytest.fail("Should never get here") t1 = asyncio.create_task(c1()) t2 = asyncio.create_task(c2()) r1, r2 = await asyncio.gather(t1, t2, return_exceptions=True) assert r1 is None assert isinstance(r2, asyncio.CancelledError) assert not sem.locked() async with sem: assert True def test_dynamic_bounded_semaphore_multiple_event_loops(): """Test semaphore detecting multiple loops.""" async def test_semaphore(sem): async with sem: await asyncio.sleep(0.1) async def make_semaphore(): sem = datastructures.PriorityDynamicBoundedSemaphore(1) # The loop reference is lazily created so we need to actually lock the semaphore await asyncio.gather(test_semaphore(sem), test_semaphore(sem)) return sem loop1 = asyncio.new_event_loop() sem = loop1.run_until_complete(make_semaphore()) async def inner(): await asyncio.gather(test_semaphore(sem), test_semaphore(sem)) loop2 = asyncio.new_event_loop() with pytest.raises(RuntimeError): loop2.run_until_complete(inner()) async def test_dynamic_bounded_semaphore_runtime_limit_increase(): """Test changing the max_value at runtime.""" sem = datastructures.PriorityDynamicBoundedSemaphore(2) def set_limit(n): sem.max_value = n asyncio.get_running_loop().call_later(0.1, set_limit, 3) async with sem: # Play with the value, testing edge cases sem.max_value = 100 assert sem.value == 99 assert not sem.locked() sem.max_value = 2 assert sem.value == 1 assert not sem.locked() sem.max_value = 1 assert sem.value == 0 assert sem.locked() # Setting it to `0` seems undefined but we keep track of locks so it works sem.max_value = 0 assert sem.value == -1 assert sem.locked() sem.max_value = 2 assert sem.value == 1 assert not sem.locked() async with sem: assert sem.locked() assert sem.value == 0 assert sem.max_value == 2 async with sem: # We're now locked until the limit is increased pass assert not sem.locked() assert sem.value == 1 assert sem.max_value == 3 assert sem.value == 2 assert sem.max_value == 3 assert sem.value == 3 assert sem.max_value == 3 async def test_dynamic_bounded_semaphore_errors(): """Test semaphore handling errors and cancellation.""" sem = datastructures.PriorityDynamicBoundedSemaphore(1) def set_limit(n): sem.max_value = n async def acquire(): async with sem: await asyncio.sleep(60) # The first acquire call will succeed acquire1 = asyncio.create_task(acquire()) # The remaining two will stall acquire2 = asyncio.create_task(acquire()) acquire3 = asyncio.create_task(acquire()) await asyncio.sleep(0.1) # Cancel the first one, which holds the lock acquire1.cancel() # But also cancel the second one, which was waiting acquire2.cancel() with pytest.raises(asyncio.CancelledError): await acquire1 with pytest.raises(asyncio.CancelledError): await acquire2 await asyncio.sleep(0.1) # The third one will have succeeded assert sem.locked() assert sem.value == 0 assert sem.max_value == 1 acquire3.cancel() with pytest.raises(asyncio.CancelledError): await acquire3 assert not sem.locked() assert sem.value == 1 assert sem.max_value == 1 async def test_dynamic_bounded_semaphore_cancellation(): """Test semaphore handling errors and cancellation.""" sem = datastructures.PriorityDynamicBoundedSemaphore(2) async def acquire(): async with sem: await asyncio.sleep(0.2) tasks = [] # First two lock up the semaphore but succeed tasks.append(asyncio.create_task(acquire())) tasks.append(asyncio.create_task(acquire())) # Next two get in line, will be cancelled tasks.append(asyncio.create_task(acquire())) tasks.append(asyncio.create_task(acquire())) await asyncio.sleep(0) exc = RuntimeError("Uh oh :(") sem.cancel_waiting(exc) # Last one makes it through tasks.append(asyncio.create_task(acquire())) assert (await asyncio.gather(*tasks, return_exceptions=True)) == [ None, None, exc, exc, None, ] assert not sem.locked() async def test_priority_lock(): """Test priority lock.""" lock = datastructures.PriorityLock() with pytest.raises(ValueError): lock.max_value = 2 assert lock.max_value == 1 # Default priority of 0 async with lock: pass # Overridden priority of 100 async with lock(priority=100): pass run_order = [] async def test_priority(priority: int, item: str): assert lock.locked() async with lock(priority=priority): run_order.append(item) # Lock first async with lock: assert lock.locked() names = { "1: first": 1, "5: first": 5, "1: second": 1, "1: third": 1, "5: second": 5, "-5: only": -5, "1: fourth": 1, "2: only": 2, } tasks = { name: asyncio.create_task(test_priority(priority + 0, name + "")) for name, priority in names.items() } await asyncio.sleep(0) tasks["1: second"].cancel() await asyncio.sleep(0) await asyncio.gather(*tasks.values(), return_exceptions=True) assert run_order == [ "5: first", "5: second", "2: only", "1: first", # "1: second", "1: third", "1: fourth", "-5: only", ] async def test_reschedulable_timeout(): callback = Mock() timeout = datastructures.ReschedulableTimeout(callback) timeout.reschedule(0.1) assert len(callback.mock_calls) == 0 await asyncio.sleep(0.09) assert len(callback.mock_calls) == 0 await asyncio.sleep(0.02) assert len(callback.mock_calls) == 1 async def test_reschedulable_timeout_reschedule(): callback = Mock() timeout = datastructures.ReschedulableTimeout(callback) timeout.reschedule(0.1) timeout.reschedule(0.2) await asyncio.sleep(0.19) assert len(callback.mock_calls) == 0 await asyncio.sleep(0.02) assert len(callback.mock_calls) == 1 async def test_reschedulable_timeout_cancel(): callback = Mock() timeout = datastructures.ReschedulableTimeout(callback) timeout.reschedule(0.1) assert len(callback.mock_calls) == 0 await asyncio.sleep(0.09) timeout.cancel() await asyncio.sleep(0.02) assert len(callback.mock_calls) == 0 async def test_debouncer(): """Test debouncer.""" debouncer = datastructures.Debouncer() debouncer.clean() assert repr(debouncer) == "" obj1 = object() assert not debouncer.is_filtered(obj1) assert not debouncer.filter(obj1, expire_in=0.1) assert debouncer.is_filtered(obj1) assert debouncer.filter(obj1, expire_in=1) assert debouncer.filter(obj1, expire_in=0.1) assert debouncer.filter(obj1, expire_in=1) assert debouncer.is_filtered(obj1) assert repr(debouncer) == "" obj2 = object() assert not debouncer.is_filtered(obj2) assert not debouncer.filter(obj2, expire_in=0.2) assert debouncer.filter(obj1, expire_in=1) assert debouncer.filter(obj2, expire_in=1) assert debouncer.filter(obj1, expire_in=1) assert debouncer.filter(obj2, expire_in=1) assert debouncer.is_filtered(obj1) assert debouncer.is_filtered(obj2) assert repr(debouncer) == "" await asyncio.sleep(0.1) assert not debouncer.is_filtered(obj1) assert debouncer.is_filtered(obj2) assert repr(debouncer) == "" await asyncio.sleep(0.1) assert not debouncer.is_filtered(obj1) assert not debouncer.is_filtered(obj2) assert repr(debouncer) == "" async def test_debouncer_low_resolution_clock(): """Test debouncer with a low resolution clock.""" loop = asyncio.get_running_loop() now = loop.time() # Make sure we can debounce on a low resolution clock with patch.object(loop, "time", return_value=now): debouncer = datastructures.Debouncer() obj1 = object() debouncer.filter(obj1, expire_in=0.1) assert debouncer.is_filtered(obj1) obj2 = object() debouncer.filter(obj2, expire_in=0.1) assert debouncer.is_filtered(obj2) # The two objects cannot be compared with pytest.raises(TypeError): obj1 < obj2 # noqa: B015 async def test_debouncer_cleaning_bug(): """Test debouncer bug when using heapq improperly.""" debouncer = datastructures.Debouncer() obj1 = object() obj2 = object() obj3 = object() # Filter obj1 with an expiration of 0.3 seconds debouncer.filter(obj1, expire_in=0.3) # Slight delay to ensure different expiration times await asyncio.sleep(0.05) # Filter obj2 with an expiration of 0.1 seconds debouncer.filter(obj2, expire_in=0.1) # Another slight delay await asyncio.sleep(0.05) # Filter obj3 with an expiration of 0.2 seconds debouncer.filter(obj3, expire_in=0.2) assert debouncer.is_filtered(obj1) assert debouncer.is_filtered(obj2) assert debouncer.is_filtered(obj3) # Wait until after obj2 should have expired await asyncio.sleep(0.11) # Total elapsed time ~0.21 seconds from start # Clean up expired items debouncer.clean() # obj2 should have expired, but due to the bug, it might still be filtered assert not debouncer.is_filtered(obj2) # obj1 and obj3 should still be filtered assert debouncer.is_filtered(obj1) assert debouncer.is_filtered(obj3) # Wait until after obj1 and obj3 should have expired await asyncio.sleep(0.1) # Total elapsed time ~0.31 seconds from start # Clean up expired items debouncer.clean() # Now all objects should have expired assert not debouncer.is_filtered(obj1) assert not debouncer.is_filtered(obj3) # The queue should be empty assert len(debouncer._queue) == 0 zigpy-0.80.1/tests/test_device.py000066400000000000000000001570231501451476000167670ustar00rootroot00000000000000import asyncio from datetime import datetime, timezone import logging from unittest.mock import call import pytest from zigpy import device, endpoint import zigpy.application import zigpy.exceptions from zigpy.ota import OtaImagesResult import zigpy.ota.image from zigpy.profiles import zha import zigpy.state import zigpy.types as t import zigpy.util from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Basic, Ota from zigpy.zdo import types as zdo_t from .async_mock import AsyncMock, MagicMock, int_sentinel, patch, sentinel @pytest.fixture def dev(monkeypatch, app_mock): monkeypatch.setattr(device, "APS_REPLY_TIMEOUT_EXTENDED", 0.1) ieee = t.EUI64(map(t.uint8_t, [0, 1, 2, 3, 4, 5, 6, 7])) dev = device.Device(app_mock, ieee, 65535) node_desc = zdo_t.NodeDescriptor(1, 1, 1, 4, 5, 6, 7, 8) with patch.object( dev.zdo, "Node_Desc_req", new=AsyncMock(return_value=(0, 0xFFFF, node_desc)) ): yield dev async def test_initialize(monkeypatch, dev): async def mockrequest(*args, **kwargs): return [0, None, [0, 1, 2, 3, 4]] async def mockepinit(self, *args, **kwargs): self.status = endpoint.Status.ZDO_INIT self.add_input_cluster(Basic.cluster_id) async def mock_ep_get_model_info(self): if self.endpoint_id == 1: return None, None elif self.endpoint_id == 2: return "Model", None elif self.endpoint_id == 3: return None, "Manufacturer" else: return "Model2", "Manufacturer2" monkeypatch.setattr(endpoint.Endpoint, "initialize", mockepinit) monkeypatch.setattr(endpoint.Endpoint, "get_model_info", mock_ep_get_model_info) dev.zdo.Active_EP_req = mockrequest await dev.initialize() assert dev.endpoints[0] is dev.zdo assert 1 in dev.endpoints assert 2 in dev.endpoints assert 3 in dev.endpoints assert 4 in dev.endpoints assert dev._application.device_initialized.call_count == 1 assert dev.is_initialized # First one for each is chosen assert dev.model == "Model" assert dev.manufacturer == "Manufacturer" dev.schedule_initialize() assert dev._application.device_initialized.call_count == 2 await dev.initialize() assert dev._application.device_initialized.call_count == 3 async def test_initialize_fail(dev): async def mockrequest(nwk, tries=None, delay=None): return [1, dev.nwk, []] dev.zdo.Active_EP_req = mockrequest await dev.initialize() assert not dev.is_initialized assert not dev.has_non_zdo_endpoints @patch("zigpy.device.Device.get_node_descriptor", AsyncMock()) async def test_initialize_ep_failed(monkeypatch, dev): async def mockrequest(req, nwk, tries=None, delay=None): return [0, None, [1, 2]] async def mockepinit(self): raise AttributeError monkeypatch.setattr(endpoint.Endpoint, "initialize", mockepinit) dev.zdo.request = mockrequest await dev.initialize() assert not dev.is_initialized assert dev.application.listener_event.call_count == 1 assert dev.application.listener_event.call_args[0][0] == "device_init_failure" async def test_request(dev): seq = int_sentinel.tsn async def mock_req(*args, **kwargs): dev._pending[seq].result.set_result(sentinel.result) dev.application.send_packet = AsyncMock(side_effect=mock_req) r = await dev.request(1, 2, 3, 3, seq, b"") assert r is sentinel.result assert dev._application.send_packet.call_count == 1 async def test_request_without_reply(dev): seq = int_sentinel.tsn dev._pending.new = MagicMock() dev.application.send_packet = AsyncMock() r = await dev.request(1, 2, 3, 3, seq, b"", expect_reply=False) assert r is None assert dev._application.send_packet.call_count == 1 assert len(dev._pending.new.mock_calls) == 0 async def test_request_tsn_error(dev): seq = int_sentinel.tsn dev._pending.new = MagicMock(side_effect=zigpy.exceptions.ControllerException()) dev.application.request = MagicMock() dev.application.send_packet = AsyncMock() # We don't leave a dangling coroutine on error with pytest.raises(zigpy.exceptions.ControllerException): await dev.request(1, 2, 3, 3, seq, b"") assert dev._application.send_packet.call_count == 0 assert dev._application.request.call_count == 0 assert len(dev._pending.new.mock_calls) == 1 async def test_failed_request(dev): assert dev.last_seen is None dev._application.send_packet = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Uh oh") ) with pytest.raises(zigpy.exceptions.DeliveryError): await dev.request(1, 2, 3, 4, 5, b"") assert dev.last_seen is None def test_skip_configuration(dev): assert dev.skip_configuration is False dev.skip_configuration = True assert dev.skip_configuration is True def test_radio_details(dev): dev.radio_details(1, 2) assert dev.lqi == 1 assert dev.rssi == 2 dev.radio_details(lqi=3) assert dev.lqi == 3 assert dev.rssi == 2 dev.radio_details(rssi=4) assert dev.lqi == 3 assert dev.rssi == 4 async def test_handle_message_read_report_conf(dev): ep = dev.add_endpoint(3) ep.add_input_cluster(0x702) tsn = 0x56 req_mock = MagicMock() dev._pending[tsn] = req_mock # Read Report Configuration Success rsp = dev.packet_received( t.ZigbeePacket( profile_id=0x104, cluster_id=0x702, src_ep=3, dst_ep=3, data=t.SerializableBytes( b"\x18\x56\x09\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06" ), # message dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), ) ) # Returns decoded msg when response is not pending, None otherwise assert rsp is None assert req_mock.result.set_result.call_count == 1 cfg_sup1 = req_mock.result.set_result.call_args[0][0].attribute_configs[0] assert isinstance(cfg_sup1, zigpy.zcl.foundation.AttributeReportingConfigWithStatus) assert cfg_sup1.status == zigpy.zcl.foundation.Status.SUCCESS assert cfg_sup1.config.direction == 0 assert cfg_sup1.config.attrid == 0 assert cfg_sup1.config.datatype == 0x25 assert cfg_sup1.config.min_interval == 30 assert cfg_sup1.config.max_interval == 900 assert cfg_sup1.config.reportable_change == 0x060504030201 # Unsupported attributes tsn2 = 0x5B req_mock2 = MagicMock() dev._pending[tsn2] = req_mock2 rsp2 = dev.packet_received( t.ZigbeePacket( profile_id=0x104, cluster_id=0x702, src_ep=3, dst_ep=3, data=t.SerializableBytes( b"\x18\x5b\x09\x86\x00\x00\x00\x86\x00\x12\x00\x86\x00\x00\x04" ), # message 3x("Unsupported attribute" response) dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), ) ) # Returns decoded msg when response is not pending, None otherwise assert rsp2 is None cfg_unsup1, cfg_unsup2, cfg_unsup3 = req_mock2.result.set_result.call_args[0][ 0 ].attribute_configs assert ( cfg_unsup1.status == cfg_unsup2.status == cfg_unsup3.status == zigpy.zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE ) assert cfg_unsup1.config.direction == 0x00 and cfg_unsup1.config.attrid == 0x0000 assert cfg_unsup2.config.direction == 0x00 and cfg_unsup2.config.attrid == 0x0012 assert cfg_unsup3.config.direction == 0x00 and cfg_unsup3.config.attrid == 0x0400 # One supported, one unsupported tsn3 = 0x5C req_mock3 = MagicMock() dev._pending[tsn3] = req_mock3 rsp3 = dev.packet_received( t.ZigbeePacket( profile_id=0x104, cluster_id=0x702, src_ep=3, dst_ep=3, data=t.SerializableBytes( b"\x18\x5c\x09\x86\x00\x00\x00\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06" ), dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), ) ) assert rsp3 is None cfg_unsup4, cfg_sup2 = req_mock3.result.set_result.call_args[0][0].attribute_configs assert cfg_unsup4.status == zigpy.zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE assert cfg_sup2.status == zigpy.zcl.foundation.Status.SUCCESS assert cfg_sup2.serialize() == cfg_sup1.serialize() async def test_handle_message_deserialize_error(dev): ep = dev.add_endpoint(3) ep.deserialize = MagicMock(side_effect=ValueError) ep.handle_message = MagicMock() dev.packet_received( t.ZigbeePacket( profile_id=99, cluster_id=98, src_ep=3, dst_ep=3, data=t.SerializableBytes(b"abcd"), dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), ) ) assert ep.handle_message.call_count == 0 def test_endpoint_getitem(dev): ep = dev.add_endpoint(3) assert dev[3] is ep with pytest.raises(KeyError): dev[1] async def test_broadcast(app_mock): app_mock.state.node_info.ieee = t.EUI64.convert("08:09:0A:0B:0C:0D:0E:0F") (profile, cluster, src_ep, dst_ep, data) = ( zha.PROFILE_ID, 1, 2, 3, b"\x02\x01\x00", ) await device.broadcast(app_mock, profile, cluster, src_ep, dst_ep, 0, 0, 123, data) assert app_mock.send_packet.call_count == 1 packet = app_mock.send_packet.mock_calls[0].args[0] assert packet.profile_id == profile assert packet.cluster_id == cluster assert packet.src_ep == src_ep assert packet.dst_ep == dst_ep assert packet.data.serialize() == data async def _get_node_descriptor(dev, zdo_success=True, request_success=True): async def mockrequest(nwk, tries=None, delay=None, **kwargs): if not request_success: raise asyncio.TimeoutError status = 0 if zdo_success else 1 return [status, nwk, zdo_t.NodeDescriptor.deserialize(b"abcdefghijklm")[0]] dev.zdo.Node_Desc_req = MagicMock(side_effect=mockrequest) return await dev.get_node_descriptor() async def test_get_node_descriptor(dev): nd = await _get_node_descriptor(dev, zdo_success=True, request_success=True) assert nd is not None assert isinstance(nd, zdo_t.NodeDescriptor) assert dev.zdo.Node_Desc_req.call_count == 1 async def test_get_node_descriptor_no_reply(dev): with pytest.raises(asyncio.TimeoutError): await _get_node_descriptor(dev, zdo_success=True, request_success=False) assert dev.zdo.Node_Desc_req.call_count == 1 async def test_get_node_descriptor_fail(dev): with pytest.raises(zigpy.exceptions.InvalidResponse): await _get_node_descriptor(dev, zdo_success=False, request_success=True) assert dev.zdo.Node_Desc_req.call_count == 1 async def test_add_to_group(dev, monkeypatch): grp_id, grp_name = 0x1234, "test group 0x1234" epmock = MagicMock(spec_set=endpoint.Endpoint) monkeypatch.setattr(endpoint, "Endpoint", MagicMock(return_value=epmock)) epmock.add_to_group = AsyncMock() dev.add_endpoint(3) dev.add_endpoint(4) await dev.add_to_group(grp_id, grp_name) assert epmock.add_to_group.call_count == 2 assert epmock.add_to_group.call_args[0][0] == grp_id assert epmock.add_to_group.call_args[0][1] == grp_name async def test_remove_from_group(dev, monkeypatch): grp_id = 0x1234 epmock = MagicMock(spec_set=endpoint.Endpoint) monkeypatch.setattr(endpoint, "Endpoint", MagicMock(return_value=epmock)) epmock.remove_from_group = AsyncMock() dev.add_endpoint(3) dev.add_endpoint(4) await dev.remove_from_group(grp_id) assert epmock.remove_from_group.call_count == 2 assert epmock.remove_from_group.call_args[0][0] == grp_id async def test_schedule_group_membership(dev, caplog): """Test preempting group membership scan.""" p1 = patch.object(dev, "group_membership_scan", new=AsyncMock()) caplog.set_level(logging.DEBUG) with p1 as scan_mock: dev.schedule_group_membership_scan() await asyncio.sleep(0) assert scan_mock.call_count == 1 assert scan_mock.await_count == 1 assert not [r for r in caplog.records if r.name != "asyncio"] scan_mock.reset_mock() dev.schedule_group_membership_scan() dev.schedule_group_membership_scan() await asyncio.sleep(0) assert scan_mock.await_count == 1 assert "Cancelling old group rescan" in caplog.text async def test_group_membership_scan(dev): ep = dev.add_endpoint(1) ep.status = endpoint.Status.ZDO_INIT with patch.object(ep, "group_membership_scan", new=AsyncMock()): await dev.group_membership_scan() assert ep.group_membership_scan.await_count == 1 def test_device_manufacture_id_override(dev): """Test manufacturer id override.""" assert dev.manufacturer_id is None assert dev.manufacturer_id_override is None dev.node_desc = zdo_t.NodeDescriptor(1, 64, 142, 4153, 82, 255, 0, 255, 0) assert dev.manufacturer_id == 4153 dev.manufacturer_id_override = 2345 assert dev.manufacturer_id == 2345 dev.node_desc = None assert dev.manufacturer_id == 2345 def test_device_name(dev): """Test device name property.""" assert dev.nwk == 0xFFFF assert dev.name == "0xFFFF" def test_device_last_seen(dev, monkeypatch): """Test the device last_seen property handles updates and broadcasts events.""" monkeypatch.setattr(dev, "listener_event", MagicMock()) assert dev.last_seen is None dev.last_seen = 0 epoch = datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc) assert dev.last_seen == epoch.timestamp() dev.listener_event.assert_called_once_with("device_last_seen_updated", epoch) dev.listener_event.reset_mock() now = datetime.now(timezone.utc) dev.last_seen = now dev.listener_event.assert_called_once_with("device_last_seen_updated", now) async def test_ignore_unknown_endpoint(dev, caplog): """Test that unknown endpoints are ignored.""" dev.add_endpoint(1) with caplog.at_level(logging.DEBUG): dev.packet_received( t.ZigbeePacket( profile_id=260, cluster_id=1, src_ep=2, dst_ep=3, data=t.SerializableBytes(b"data"), src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=dev.nwk, ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=0x0000, ), ) ) assert "Ignoring message on unknown endpoint" in caplog.text async def test_update_device_firmware_no_ota_cluster(dev): """Test that device firmware updates fails: no ota cluster.""" with pytest.raises(ValueError, match="Device has no OTA cluster"): await dev.update_firmware(sentinel.firmware_image, sentinel.progress_callback) dev.add_endpoint(1) dev.endpoints[1].output_clusters = MagicMock(side_effect=KeyError) with pytest.raises(ValueError, match="Device has no OTA cluster"): await dev.update_firmware(sentinel.firmware_image, sentinel.progress_callback) async def test_update_device_firmware_already_in_progress(dev, caplog): """Test that device firmware updates no ops when update is in progress.""" dev.ota_in_progress = True await dev.update_firmware(sentinel.firmware_image, sentinel.progress_callback) assert "OTA already in progress" in caplog.text @patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1) @patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01) @patch( "zigpy.device.OTA_RETRY_DECORATOR", zigpy.util.retryable_request(tries=1, delay=0.01), ) async def test_update_device_firmware(monkeypatch, dev, caplog): """Test that device firmware updates execute the expected calls.""" ep = dev.add_endpoint(1) cluster = zigpy.zcl.Cluster.from_id(ep, Ota.cluster_id, is_server=False) ep.add_output_cluster(Ota.cluster_id, cluster) async def mockrequest(nwk, tries=None, delay=None): return [0, None, [0, 1, 2, 3, 4]] async def mockepinit(self, *args, **kwargs): self.status = endpoint.Status.ZDO_INIT self.add_input_cluster(Basic.cluster_id) async def mock_ep_get_model_info(self): if self.endpoint_id == 1: return "Model2", "Manufacturer2" monkeypatch.setattr(endpoint.Endpoint, "initialize", mockepinit) monkeypatch.setattr(endpoint.Endpoint, "get_model_info", mock_ep_get_model_info) dev.zdo.Active_EP_req = mockrequest await dev.initialize() fw_image = zigpy.ota.OtaImageWithMetadata( metadata=zigpy.ota.providers.BaseOtaImageMetadata( file_version=0x12345678, manufacturer_id=0x1234, image_type=0x90, ), firmware=zigpy.ota.image.OTAImage( header=zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=0x12345678, image_type=0x90, manufacturer_id=0x1234, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 8, ), subelements=[zigpy.ota.image.SubElement(tag_id=0x0000, data=b"fw_image")], ), ) fw_image_force = fw_image.replace( firmware=fw_image.firmware.replace( header=fw_image.firmware.header.replace( file_version=0xFFFFFFFF - 1, ) ) ) dev.application.ota.get_ota_images = MagicMock( return_value=OtaImagesResult(upgrades=(), downgrades=()) ) dev.update_firmware = MagicMock(wraps=dev.update_firmware) def make_packet(cmd_name: str, **kwargs): req_hdr, req_cmd = cluster._create_request( general=False, command_id=cluster.commands_by_name[cmd_name].id, schema=cluster.commands_by_name[cmd_name].schema, disable_default_response=False, direction=foundation.Direction.Client_to_Server, args=(), kwargs=kwargs, ) return t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=req_hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) async def send_packet(packet: t.ZigbeePacket): if dev.update_firmware.mock_calls[-1].kwargs.get("force", False): active_fw_image = fw_image_force else: active_fw_image = fw_image if packet.cluster_id == Ota.cluster_id: hdr, cmd = cluster.deserialize(packet.data.serialize()) if isinstance(cmd, Ota.ImageNotifyCommand): dev.application.packet_received( make_packet( "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, current_file_version=active_fw_image.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance( cmd, Ota.ClientCommandDefs.query_next_image_response.schema ): assert cmd.status == foundation.Status.SUCCESS assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert cmd.file_version == active_fw_image.firmware.header.file_version assert cmd.image_size == active_fw_image.firmware.header.image_size dev.application.packet_received( make_packet( "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, file_version=active_fw_image.firmware.header.file_version, file_offset=0, maximum_data_size=40, request_node_addr=dev.ieee, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.image_block_response.schema): if cmd.file_offset == 0: assert cmd.status == foundation.Status.SUCCESS assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert ( cmd.file_version == active_fw_image.firmware.header.file_version ) assert cmd.file_offset == 0 assert cmd.image_data == active_fw_image.firmware.serialize()[0:40] dev.application.packet_received( make_packet( "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, file_version=active_fw_image.firmware.header.file_version, file_offset=40, maximum_data_size=40, request_node_addr=dev.ieee, ) ) elif cmd.file_offset == 40: assert cmd.status == foundation.Status.SUCCESS assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert ( cmd.file_version == active_fw_image.firmware.header.file_version ) assert cmd.file_offset == 40 assert cmd.image_data == active_fw_image.firmware.serialize()[40:70] dev.application.packet_received( make_packet( "upgrade_end", status=foundation.Status.SUCCESS, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, file_version=active_fw_image.firmware.header.file_version, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.upgrade_end_response.schema): assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert cmd.file_version == active_fw_image.firmware.header.file_version assert cmd.current_time == 0 assert cmd.upgrade_time == 0 elif isinstance( cmd, foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].schema, ): assert cmd.attribute_ids == [Ota.AttributeDefs.current_file_version.id] req_hdr, req_cmd = cluster._create_request( general=True, command_id=foundation.GeneralCommand.Read_Attributes_rsp, schema=foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes_rsp ].schema, tsn=hdr.tsn, disable_default_response=True, direction=foundation.Direction.Server_to_Client, args=(), kwargs={ "status_records": [ foundation.ReadAttributeRecord( attrid=Ota.AttributeDefs.current_file_version.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.uint32, value=active_fw_image.firmware.header.file_version, ), ) ] }, ) dev.application.packet_received( t.ZigbeePacket( src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=dev.nwk ), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes( req_hdr.serialize() + req_cmd.serialize() ), lqi=255, rssi=-30, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback = MagicMock() result = await dev.update_firmware(fw_image, progress_callback) assert ( dev.endpoints[1] .out_clusters[Ota.cluster_id] ._attr_cache[Ota.AttributeDefs.current_file_version.id] == 0x12345678 ) assert dev.application.send_packet.await_count == 6 assert progress_callback.call_count == 2 assert progress_callback.call_args_list[0] == call(40, 70, 57.142857142857146) assert progress_callback.call_args_list[1] == call(70, 70, 100.0) assert result == foundation.Status.SUCCESS progress_callback.reset_mock() dev.application.send_packet.reset_mock() result = await dev.update_firmware( fw_image, progress_callback=progress_callback, force=True ) assert dev.application.send_packet.await_count == 6 assert progress_callback.call_count == 2 assert progress_callback.call_args_list[0] == call(40, 70, 57.142857142857146) assert progress_callback.call_args_list[1] == call(70, 70, 100.0) assert result == foundation.Status.SUCCESS # _image_query_req exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() image_notify = cluster.image_notify cluster.image_notify = AsyncMock(side_effect=zigpy.exceptions.DeliveryError("Foo")) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert dev.application.send_packet.await_count == 0 assert progress_callback.call_count == 0 assert "OTA image_notify handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.image_notify = image_notify caplog.clear() # _image_query_req exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() query_next_image_response = cluster.query_next_image_response cluster.query_next_image_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert dev.application.send_packet.await_count == 1 # just image notify assert progress_callback.call_count == 0 assert "OTA query_next_image handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.query_next_image_response = query_next_image_response caplog.clear() # _image_block_req exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() image_block_response = cluster.image_block_response cluster.image_block_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert ( dev.application.send_packet.await_count == 2 ) # just image notify + query next image assert progress_callback.call_count == 0 assert "OTA image_block handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.image_block_response = image_block_response caplog.clear() # _upgrade_end exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() upgrade_end_response = cluster.upgrade_end_response cluster.upgrade_end_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert ( dev.application.send_packet.await_count == 4 ) # just image notify, qne, and 2 img blocks assert progress_callback.call_count == 2 assert "OTA upgrade_end handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.upgrade_end_response = upgrade_end_response caplog.clear() async def send_packet(packet: t.ZigbeePacket): if packet.cluster_id == Ota.cluster_id: hdr, cmd = cluster.deserialize(packet.data.serialize()) if isinstance(cmd, Ota.ImageNotifyCommand): dev.application.packet_received( make_packet( "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=fw_image.firmware.header.manufacturer_id, image_type=fw_image.firmware.header.image_type, current_file_version=fw_image.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance( cmd, Ota.ClientCommandDefs.query_next_image_response.schema ): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == fw_image.firmware.header.manufacturer_id assert cmd.image_type == fw_image.firmware.header.image_type assert cmd.file_version == fw_image.firmware.header.file_version assert cmd.image_size == fw_image.firmware.header.image_size dev.application.packet_received( make_packet( "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=fw_image.firmware.header.manufacturer_id, image_type=fw_image.firmware.header.image_type, file_version=fw_image.firmware.header.file_version, file_offset=300, maximum_data_size=40, request_node_addr=dev.ieee, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback.reset_mock() image_block_response = cluster.image_block_response cluster.image_block_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert ( dev.application.send_packet.await_count == 2 ) # just image notify, qne, img block response fails assert progress_callback.call_count == 0 assert "OTA image_block handler[MALFORMED_COMMAND] exception" in caplog.text assert result == foundation.Status.MALFORMED_COMMAND cluster.image_block_response = image_block_response @patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1) @patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01) @patch( "zigpy.device.OTA_RETRY_DECORATOR", zigpy.util.retryable_request(tries=1, delay=0.01), ) async def test_update_legrand_device_firmware(monkeypatch, dev, caplog): """Legrand device (manufacturer_code == 4129) firmware update expects the "image_block" command "maximum_data_size" to be complied with.""" ep = dev.add_endpoint(1) cluster = zigpy.zcl.Cluster.from_id(ep, Ota.cluster_id, is_server=False) ep.add_output_cluster(Ota.cluster_id, cluster) async def mockrequest(nwk, tries=None, delay=None): return [0, None, [0, 1, 2, 3, 4]] async def mockepinit(self, *args, **kwargs): self.status = endpoint.Status.ZDO_INIT self.add_input_cluster(Basic.cluster_id) async def mock_ep_get_model_info(self): if self.endpoint_id == 1: return "SomeModel", "Legrand" monkeypatch.setattr(endpoint.Endpoint, "initialize", mockepinit) monkeypatch.setattr(endpoint.Endpoint, "get_model_info", mock_ep_get_model_info) dev.zdo.Active_EP_req = mockrequest await dev.initialize() fw_image = zigpy.ota.OtaImageWithMetadata( metadata=zigpy.ota.providers.BaseOtaImageMetadata( file_version=0x12345678, manufacturer_id=4129, image_type=0x90, ), firmware=zigpy.ota.image.OTAImage( header=zigpy.ota.image.OTAImageHeader( upgrade_file_id=zigpy.ota.image.OTAImageHeader.MAGIC_VALUE, file_version=0x12345678, image_type=0x90, manufacturer_id=4129, header_version=256, header_length=56, field_control=0, stack_version=2, header_string="This is a test header!", image_size=56 + 2 + 4 + 8, ), subelements=[zigpy.ota.image.SubElement(tag_id=0x0000, data=b"fw_image")], ), ) fw_image_force = fw_image.replace( firmware=fw_image.firmware.replace( header=fw_image.firmware.header.replace( file_version=0xFFFFFFFF - 1, ) ) ) dev.application.ota.get_ota_images = MagicMock( return_value=OtaImagesResult(upgrades=(), downgrades=()) ) dev.update_firmware = MagicMock(wraps=dev.update_firmware) def make_packet(cmd_name: str, **kwargs): req_hdr, req_cmd = cluster._create_request( general=False, command_id=cluster.commands_by_name[cmd_name].id, schema=cluster.commands_by_name[cmd_name].schema, disable_default_response=False, direction=foundation.Direction.Client_to_Server, args=(), kwargs=kwargs, ) return t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=req_hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes(req_hdr.serialize() + req_cmd.serialize()), lqi=255, rssi=-30, ) async def send_packet(packet: t.ZigbeePacket): if dev.update_firmware.mock_calls[-1].kwargs.get("force", False): active_fw_image = fw_image_force else: active_fw_image = fw_image if packet.cluster_id == Ota.cluster_id: hdr, cmd = cluster.deserialize(packet.data.serialize()) if isinstance(cmd, Ota.ImageNotifyCommand): dev.application.packet_received( make_packet( "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, current_file_version=active_fw_image.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance( cmd, Ota.ClientCommandDefs.query_next_image_response.schema ): assert cmd.status == foundation.Status.SUCCESS assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert cmd.file_version == active_fw_image.firmware.header.file_version assert cmd.image_size == active_fw_image.firmware.header.image_size dev.application.packet_received( make_packet( "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, file_version=active_fw_image.firmware.header.file_version, file_offset=0, maximum_data_size=64, request_node_addr=dev.ieee, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.image_block_response.schema): if cmd.file_offset == 0: assert cmd.status == foundation.Status.SUCCESS assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert ( cmd.file_version == active_fw_image.firmware.header.file_version ) assert cmd.file_offset == 0 assert cmd.image_data == active_fw_image.firmware.serialize()[0:64] dev.application.packet_received( make_packet( "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, file_version=active_fw_image.firmware.header.file_version, file_offset=64, maximum_data_size=64, request_node_addr=dev.ieee, ) ) elif cmd.file_offset == 64: assert cmd.status == foundation.Status.SUCCESS assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert ( cmd.file_version == active_fw_image.firmware.header.file_version ) assert cmd.file_offset == 64 assert cmd.image_data == active_fw_image.firmware.serialize()[64:70] dev.application.packet_received( make_packet( "upgrade_end", status=foundation.Status.SUCCESS, manufacturer_code=active_fw_image.firmware.header.manufacturer_id, image_type=active_fw_image.firmware.header.image_type, file_version=active_fw_image.firmware.header.file_version, ) ) elif isinstance(cmd, Ota.ClientCommandDefs.upgrade_end_response.schema): assert ( cmd.manufacturer_code == active_fw_image.firmware.header.manufacturer_id ) assert cmd.image_type == active_fw_image.firmware.header.image_type assert cmd.file_version == active_fw_image.firmware.header.file_version assert cmd.current_time == 0 assert cmd.upgrade_time == 0 elif isinstance( cmd, foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].schema, ): assert cmd.attribute_ids == [Ota.AttributeDefs.current_file_version.id] req_hdr, req_cmd = cluster._create_request( general=True, command_id=foundation.GeneralCommand.Read_Attributes_rsp, schema=foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes_rsp ].schema, tsn=hdr.tsn, disable_default_response=True, direction=foundation.Direction.Server_to_Client, args=(), kwargs={ "status_records": [ foundation.ReadAttributeRecord( attrid=Ota.AttributeDefs.current_file_version.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.uint32, value=active_fw_image.firmware.header.file_version, ), ) ] }, ) dev.application.packet_received( t.ZigbeePacket( src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=dev.nwk ), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, tsn=hdr.tsn, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes( req_hdr.serialize() + req_cmd.serialize() ), lqi=255, rssi=-30, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback = MagicMock() result = await dev.update_firmware(fw_image, progress_callback) assert ( dev.endpoints[1] .out_clusters[Ota.cluster_id] ._attr_cache[Ota.AttributeDefs.current_file_version.id] == 0x12345678 ) assert dev.application.send_packet.await_count == 6 assert progress_callback.call_count == 2 assert progress_callback.call_args_list[0] == call(64, 70, 91.42857142857143) assert progress_callback.call_args_list[1] == call(70, 70, 100.0) assert result == foundation.Status.SUCCESS progress_callback.reset_mock() dev.application.send_packet.reset_mock() result = await dev.update_firmware( fw_image, progress_callback=progress_callback, force=True ) assert dev.application.send_packet.await_count == 6 assert progress_callback.call_count == 2 assert progress_callback.call_args_list[0] == call(64, 70, 91.42857142857143) assert progress_callback.call_args_list[1] == call(70, 70, 100.0) assert result == foundation.Status.SUCCESS # _image_query_req exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() image_notify = cluster.image_notify cluster.image_notify = AsyncMock(side_effect=zigpy.exceptions.DeliveryError("Foo")) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert dev.application.send_packet.await_count == 0 assert progress_callback.call_count == 0 assert "OTA image_notify handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.image_notify = image_notify caplog.clear() # _image_query_req exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() query_next_image_response = cluster.query_next_image_response cluster.query_next_image_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert dev.application.send_packet.await_count == 1 # just image notify assert progress_callback.call_count == 0 assert "OTA query_next_image handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.query_next_image_response = query_next_image_response caplog.clear() # _image_block_req exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() image_block_response = cluster.image_block_response cluster.image_block_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert ( dev.application.send_packet.await_count == 2 ) # just image notify + query next image assert progress_callback.call_count == 0 assert "OTA image_block handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.image_block_response = image_block_response caplog.clear() # _upgrade_end exception test dev.application.send_packet.reset_mock() progress_callback.reset_mock() upgrade_end_response = cluster.upgrade_end_response cluster.upgrade_end_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert ( dev.application.send_packet.await_count == 4 ) # just image notify, qne, and 2 img blocks assert progress_callback.call_count == 2 assert "OTA upgrade_end handler exception" in caplog.text assert result != foundation.Status.SUCCESS cluster.upgrade_end_response = upgrade_end_response caplog.clear() async def send_packet(packet: t.ZigbeePacket): if packet.cluster_id == Ota.cluster_id: hdr, cmd = cluster.deserialize(packet.data.serialize()) if isinstance(cmd, Ota.ImageNotifyCommand): dev.application.packet_received( make_packet( "query_next_image", field_control=Ota.QueryNextImageCommand.FieldControl.HardwareVersion, manufacturer_code=fw_image.firmware.header.manufacturer_id, image_type=fw_image.firmware.header.image_type, current_file_version=fw_image.firmware.header.file_version - 10, hardware_version=1, ) ) elif isinstance( cmd, Ota.ClientCommandDefs.query_next_image_response.schema ): assert cmd.status == foundation.Status.SUCCESS assert cmd.manufacturer_code == fw_image.firmware.header.manufacturer_id assert cmd.image_type == fw_image.firmware.header.image_type assert cmd.file_version == fw_image.firmware.header.file_version assert cmd.image_size == fw_image.firmware.header.image_size dev.application.packet_received( make_packet( "image_block", field_control=Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, manufacturer_code=fw_image.firmware.header.manufacturer_id, image_type=fw_image.firmware.header.image_type, file_version=fw_image.firmware.header.file_version, file_offset=300, maximum_data_size=64, request_node_addr=dev.ieee, ) ) dev.application.send_packet = AsyncMock(side_effect=send_packet) progress_callback.reset_mock() image_block_response = cluster.image_block_response cluster.image_block_response = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("Foo") ) result = await dev.update_firmware(fw_image, progress_callback=progress_callback) assert ( dev.application.send_packet.await_count == 2 ) # just image notify, qne, img block response fails assert progress_callback.call_count == 0 assert "OTA image_block handler[MALFORMED_COMMAND] exception" in caplog.text assert result == foundation.Status.MALFORMED_COMMAND cluster.image_block_response = image_block_response @pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_deserialize_backwards_compat(dev): """Test that deserialization uses the method if it is overloaded.""" dev._packet_debouncer.filter = MagicMock(return_value=False) packet = t.ZigbeePacket( profile_id=260, cluster_id=Basic.cluster_id, src_ep=1, dst_ep=1, data=t.SerializableBytes( b"\x18\x56\x09\x00\x00\x00\x00\x25\x1e\x00\x84\x03\x01\x02\x03\x04\x05\x06" ), src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=dev.nwk, ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=0x0000, ), ) ep = dev.add_endpoint(1) ep.add_input_cluster(Basic.cluster_id) dev.packet_received(packet) # Replace the method dev.deserialize = MagicMock(side_effect=dev.deserialize) dev.packet_received(packet) assert dev.deserialize.call_count == 1 async def test_request_exception_propagation(dev): """Test that exceptions are propagated to the caller.""" tsn = 0x12 ep = dev.add_endpoint(1) ep.add_input_cluster(Basic.cluster_id) ep.deserialize = MagicMock(side_effect=RuntimeError()) dev.get_sequence = MagicMock(return_value=tsn) asyncio.get_running_loop().call_soon( dev.packet_received, t.ZigbeePacket( profile_id=260, cluster_id=Basic.cluster_id, src_ep=1, dst_ep=1, data=t.SerializableBytes( foundation.ZCLHeader( frame_control=foundation.FrameControl( frame_type=foundation.FrameType.CLUSTER_COMMAND, is_manufacturer_specific=False, direction=foundation.Direction.Server_to_Client, disable_default_response=True, reserved=0, ), tsn=tsn, command_id=foundation.GeneralCommand.Default_Response, manufacturer=None, ).serialize() + ( foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Default_Response ] .schema( command_id=Basic.ServerCommandDefs.reset_fact_default.id, status=foundation.Status.SUCCESS, ) .serialize() ) ), src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=dev.nwk, ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=0x0000, ), ), ) with pytest.raises(zigpy.exceptions.ParsingError) as exc: await ep.basic.reset_fact_default() assert type(exc.value.__cause__) is RuntimeError async def test_debouncing(dev): """Test that request debouncing filters out duplicate packets.""" ep = dev.add_endpoint(1) cluster = ep.add_input_cluster(0xEF00) packet = t.ZigbeePacket( src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), src_ep=1, dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=202, profile_id=260, cluster_id=cluster.cluster_id, data=t.SerializableBytes(b"\t6\x02\x00\x89m\x02\x00\x04\x00\x00\x00\x00"), tx_options=t.TransmitOptions.NONE, radius=0, non_member_radius=0, lqi=148, rssi=-63, ) packet_received = MagicMock() with dev.application.callback_for_response( src=dev, filters=[lambda hdr, cmd: True], callback=packet_received, ): for i in range(10): new_packet = packet.replace( timestamp=None, tsn=packet.tsn + i, lqi=packet.lqi + i, rssi=packet.rssi + i, ) dev.packet_received(new_packet) assert len(packet_received.mock_calls) == 1 async def test_device_concurrency(dev: device.Device) -> None: """Test that the device can handle multiple requests concurrently.""" ep = dev.add_endpoint(1) ep.add_input_cluster(Basic.cluster_id) async def delayed_receive(*args, **kwargs) -> None: await asyncio.sleep(0.1) dev._application.request = AsyncMock(side_effect=delayed_receive) await asyncio.gather( # First low priority request makes it through, since the slot is free dev.request( profile=0x0401, cluster=Basic.cluster_id, src_ep=1, dst_ep=1, sequence=dev.get_sequence(), data=b"test low 1!", priority=t.PacketPriority.LOW, expect_reply=False, ), # Second one (and all subsequent requests) are enqueued dev.request( profile=0x0401, cluster=Basic.cluster_id, src_ep=1, dst_ep=1, sequence=dev.get_sequence(), data=b"test low 2!", priority=t.PacketPriority.LOW, expect_reply=False, ), dev.request( profile=0x0401, cluster=Basic.cluster_id, src_ep=1, dst_ep=1, sequence=dev.get_sequence(), data=b"test normal!", expect_reply=False, ), dev.request( profile=0x0401, cluster=Basic.cluster_id, src_ep=1, dst_ep=1, sequence=dev.get_sequence(), data=b"test high!", priority=999, expect_reply=False, ), dev.request( profile=0x0401, cluster=Basic.cluster_id, src_ep=1, dst_ep=1, sequence=dev.get_sequence(), data=b"test high!", priority=t.PacketPriority.HIGH, expect_reply=False, ), ) assert len(dev._application.request.mock_calls) == 5 assert [c.kwargs["priority"] for c in dev._application.request.mock_calls] == [ t.PacketPriority.LOW, # First one that made it through 999, # Super high t.PacketPriority.HIGH, t.PacketPriority.NORMAL, t.PacketPriority.LOW, ] zigpy-0.80.1/tests/test_endpoint.py000066400000000000000000000426231501451476000173470ustar00rootroot00000000000000import asyncio from unittest.mock import AsyncMock, MagicMock, call, patch, sentinel import pytest from zigpy import endpoint, group, zcl import zigpy.device import zigpy.exceptions import zigpy.types as t from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand, Status as ZCLStatus from zigpy.zdo import types @pytest.fixture def ep(): dev = MagicMock() dev.request = AsyncMock() dev.reply = AsyncMock() return endpoint.Endpoint(dev, 1) async def _test_initialize(ep, profile): async def mockrequest(*args, **kwargs): sd = types.SimpleDescriptor() sd.endpoint = 1 sd.profile = profile sd.device_type = 0xFF sd.input_clusters = [5] sd.output_clusters = [6] return [0, None, sd] ep._device.zdo.Simple_Desc_req = mockrequest await ep.initialize() assert ep.status > endpoint.Status.NEW assert 5 in ep.in_clusters assert 6 in ep.out_clusters async def test_inactive_initialize(ep): async def mockrequest(*args, **kwargs): sd = types.SimpleDescriptor() sd.endpoint = 2 return [131, None, sd] ep._device.zdo.Simple_Desc_req = mockrequest await ep.initialize() assert ep.status == endpoint.Status.ENDPOINT_INACTIVE async def test_initialize_zha(ep): return await _test_initialize(ep, 260) async def test_initialize_zll(ep): return await _test_initialize(ep, 49246) async def test_initialize_other(ep): return await _test_initialize(ep, 0x1234) async def test_initialize_fail(ep): async def mockrequest(*args, **kwargs): return [1, None, None] ep._device.zdo.Simple_Desc_req = mockrequest # The request succeeds but the response is invalid with pytest.raises(zigpy.exceptions.InvalidResponse): await ep.initialize() assert ep.status == endpoint.Status.NEW async def test_reinitialize(ep): await _test_initialize(ep, 260) assert ep.profile_id == 260 ep.profile_id = 10 await _test_initialize(ep, 260) assert ep.profile_id == 10 def test_add_input_cluster(ep): ep.add_input_cluster(0) assert 0 in ep.in_clusters assert ep.in_clusters[0].is_server is True assert ep.in_clusters[0].is_client is False def test_add_custom_input_cluster(ep): mock_cluster = MagicMock() ep.add_input_cluster(0, mock_cluster) assert 0 in ep.in_clusters assert ep.in_clusters[0] is mock_cluster def test_add_output_cluster(ep): ep.add_output_cluster(0) assert 0 in ep.out_clusters assert ep.out_clusters[0].is_server is False assert ep.out_clusters[0].is_client is True def test_add_custom_output_cluster(ep): mock_cluster = MagicMock() ep.add_output_cluster(0, mock_cluster) assert 0 in ep.out_clusters assert ep.out_clusters[0] is mock_cluster def test_multiple_add_input_cluster(ep): ep.add_input_cluster(0) assert ep.in_clusters[0].cluster_id == 0 ep.in_clusters[0].cluster_id = 1 assert ep.in_clusters[0].cluster_id == 1 ep.add_input_cluster(0) assert ep.in_clusters[0].cluster_id == 1 def test_multiple_add_output_cluster(ep): ep.add_output_cluster(0) assert ep.out_clusters[0].cluster_id == 0 ep.out_clusters[0].cluster_id = 1 assert ep.out_clusters[0].cluster_id == 1 ep.add_output_cluster(0) assert ep.out_clusters[0].cluster_id == 1 def test_handle_message(ep): c = ep.add_input_cluster(0) c.handle_message = MagicMock() ep.handle_message(sentinel.profile, 0, sentinel.hdr, sentinel.data) c.handle_message.assert_called_once_with( sentinel.hdr, sentinel.data, dst_addressing=None ) def test_handle_message_output(ep): c = ep.add_output_cluster(0) c.handle_message = MagicMock() ep.handle_message(sentinel.profile, 0, sentinel.hdr, sentinel.data) c.handle_message.assert_called_once_with( sentinel.hdr, sentinel.data, dst_addressing=None ) def test_handle_request_unknown(ep): hdr = MagicMock() hdr.command_id = sentinel.command_id ep.handle_message(sentinel.profile, 99, hdr, sentinel.args) def test_cluster_attr(ep): with pytest.raises(AttributeError): ep.basic # noqa: B018 ep.add_input_cluster(0) assert ep.basic is not None async def test_request(ep): ep.profile_id = 260 await ep.request(7, 8, b"") assert ep._device.request.call_count == 1 assert ep._device.request.await_count == 1 async def test_request_change_profileid(ep): ep.profile_id = 49246 await ep.request(7, 9, b"") ep.profile_id = 49246 await ep.request(0x1000, 10, b"") ep.profile_id = 260 await ep.request(0x1000, 11, b"") assert ep._device.request.call_count == 3 assert ep._device.request.await_count == 3 async def test_reply(ep): ep.profile_id = 260 await ep.reply(7, 8, b"") assert ep._device.reply.call_count == 1 async def test_reply_change_profile_id(ep): ep.profile_id = 49246 await ep.reply(cluster=0x1000, sequence=8, data=b"", command_id=0x3F) assert ep._device.reply.mock_calls == [ call( profile=49246, cluster=0x1000, src_ep=1, dst_ep=1, sequence=8, data=b"", timeout=5, expect_reply=False, use_ieee=False, ask_for_ack=None, priority=t.PacketPriority.NORMAL, ) ] ep._device.reply.reset_mock() await ep.reply(cluster=0x1000, sequence=8, data=b"", command_id=0x40) assert ep._device.reply.mock_calls == [ call( profile=0x0104, cluster=0x1000, src_ep=1, dst_ep=1, sequence=8, data=b"", timeout=5, expect_reply=False, use_ieee=False, ask_for_ack=None, priority=t.PacketPriority.NORMAL, ) ] ep._device.reply.reset_mock() ep.profile_id = 0xBEEF await ep.reply(cluster=0x1000, sequence=8, data=b"", command_id=0x40) assert ep._device.reply.mock_calls == [ call( profile=0xBEEF, cluster=0x1000, src_ep=1, dst_ep=1, sequence=8, data=b"", timeout=5, expect_reply=False, use_ieee=False, ask_for_ack=None, priority=t.PacketPriority.NORMAL, ) ] def _mk_rar(attrid, value, status=0): r = zcl.foundation.ReadAttributeRecord() r.attrid = attrid r.status = status r.value = zcl.foundation.TypeValue() r.value.value = value return r def _get_model_info(ep, attributes={}): clus = ep.add_input_cluster(0) assert 0 in ep.in_clusters assert ep.in_clusters[0] is clus async def mockrequest( foundation, command, schema, args, manufacturer=None, **kwargs ): assert foundation is True assert command == 0 result = [] for attr_id, value in zip(args, attributes[tuple(args)]): if isinstance(value, BaseException): raise value elif value is None: rar = _mk_rar(attr_id, None, status=1) else: raw_attr_value = t.uint8_t(len(value)).serialize() + value rar = _mk_rar(attr_id, t.CharacterString.deserialize(raw_attr_value)[0]) result.append(rar) return [result] clus.request = mockrequest return ep.get_model_info() async def test_get_model_info(ep): mod, man = await _get_model_info( ep, attributes={ (0x0004, 0x0005): (b"Mock Manufacturer", b"Mock Model"), }, ) assert man == "Mock Manufacturer" assert mod == "Mock Model" async def test_init_endpoint_info_none(ep): mod, man = await _get_model_info( ep, attributes={ (0x0004, 0x0005): (None, None), (0x0004,): (None,), (0x0005,): (None,), }, ) assert man is None assert mod is None async def test_get_model_info_missing_basic_cluster(ep): assert zcl.clusters.general.Basic.cluster_id not in ep.in_clusters model, manuf = await ep.get_model_info() assert model is None assert manuf is None async def test_init_endpoint_info_null_padded_manuf(ep): mod, man = await _get_model_info( ep, attributes={ (0x0004, 0x0005): ( b"Mock Manufacturer\x00\x04\\\x00\\\x00\x00\x00\x00\x00\x07", b"Mock Model", ), }, ) assert man == "Mock Manufacturer" assert mod == "Mock Model" async def test_init_endpoint_info_null_padded_model(ep): mod, man = await _get_model_info( ep, attributes={ (0x0004, 0x0005): ( b"Mock Manufacturer", b"Mock Model\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", ), }, ) assert man == "Mock Manufacturer" assert mod == "Mock Model" async def test_init_endpoint_info_null_padded_manuf_model(ep): mod, man = await _get_model_info( ep, attributes={ (0x0004, 0x0005): ( b"Mock Manufacturer\x00\x04\\\x00\\\x00\x00\x00\x00\x00\x07", b"Mock Model\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", ), }, ) assert man == "Mock Manufacturer" assert mod == "Mock Model" async def test_get_model_info_delivery_error(ep): with pytest.raises(zigpy.exceptions.ZigbeeException): await _get_model_info( ep, attributes={ (0x0004, 0x0005): ( zigpy.exceptions.ZigbeeException(), zigpy.exceptions.ZigbeeException(), ) }, ) async def test_get_model_info_timeout(ep): with pytest.raises(asyncio.TimeoutError): await _get_model_info( ep, attributes={ (0x0004, 0x0005): (asyncio.TimeoutError(), asyncio.TimeoutError()), (0x0004,): (asyncio.TimeoutError(),), (0x0005,): (asyncio.TimeoutError(),), }, ) async def test_get_model_info_double_read_timeout(ep): mod, man = await _get_model_info( ep, attributes={ # The double read fails (0x0004, 0x0005): (asyncio.TimeoutError(), asyncio.TimeoutError()), # But individually the attributes can be read (0x0004,): (b"Mock Manufacturer",), (0x0005,): (b"Mock Model",), }, ) assert man == "Mock Manufacturer" assert mod == "Mock Model" def _group_add_mock(ep, status=ZCLStatus.SUCCESS, no_groups_cluster=False): async def mock_req(*args, **kwargs): return [status, sentinel.group_id] if not no_groups_cluster: ep.add_input_cluster(4) ep.request = MagicMock(side_effect=mock_req) ep.device.application.groups = MagicMock(spec_set=group.Groups) return ep @pytest.mark.parametrize("status", [ZCLStatus.SUCCESS, ZCLStatus.DUPLICATE_EXISTS]) async def test_add_to_group(ep, status): ep = _group_add_mock(ep, status=status) grp_id, grp_name = 0x1234, "Group 0x1234**" res = await ep.add_to_group(grp_id, grp_name) assert res == status assert ep.request.call_count == 1 groups = ep.device.application.groups assert groups.add_group.call_count == 1 assert groups.remove_group.call_count == 0 assert groups.add_group.call_args[0][0] == grp_id assert groups.add_group.call_args[0][1] == grp_name async def test_add_to_group_no_groups(ep): ep = _group_add_mock(ep, no_groups_cluster=True) grp_id, grp_name = 0x1234, "Group 0x1234**" res = await ep.add_to_group(grp_id, grp_name) assert res != ZCLStatus.SUCCESS assert ep.request.call_count == 0 groups = ep.device.application.groups assert groups.add_group.call_count == 0 assert groups.remove_group.call_count == 0 @pytest.mark.parametrize( "status", (s for s in ZCLStatus if s not in (ZCLStatus.SUCCESS, ZCLStatus.DUPLICATE_EXISTS)), ) async def test_add_to_group_fail(ep, status): ep = _group_add_mock(ep, status=status) grp_id, grp_name = 0x1234, "Group 0x1234**" res = await ep.add_to_group(grp_id, grp_name) assert res != ZCLStatus.SUCCESS assert ep.request.call_count == 1 groups = ep.device.application.groups assert groups.add_group.call_count == 0 assert groups.remove_group.call_count == 0 def _group_remove_mock(ep, success=True, no_groups_cluster=False, not_member=False): async def mock_req(*args, **kwargs): if success: return [ZCLStatus.SUCCESS, sentinel.group_id] return [ZCLStatus.DUPLICATE_EXISTS, sentinel.group_id] if not no_groups_cluster: ep.add_input_cluster(4) ep.request = MagicMock(side_effect=mock_req) ep.device.application.groups = MagicMock(spec_set=group.Groups) grp = MagicMock(spec_set=group.Group) ep.device.application.groups.__contains__.return_value = not not_member ep.device.application.groups.__getitem__.return_value = grp return ep, grp async def test_remove_from_group(ep): grp_id = 0x1234 ep, grp_mock = _group_remove_mock(ep) res = await ep.remove_from_group(grp_id) assert res == ZCLStatus.SUCCESS assert ep.request.call_count == 1 groups = ep.device.application.groups assert groups.add_group.call_count == 0 assert groups.remove_group.call_count == 0 assert groups.__getitem__.call_args[0][0] == grp_id assert grp_mock.add_member.call_count == 0 assert grp_mock.remove_member.call_count == 1 assert grp_mock.remove_member.call_args[0][0] == ep async def test_remove_from_group_no_groups_cluster(ep): grp_id = 0x1234 ep, grp_mock = _group_remove_mock(ep, no_groups_cluster=True) res = await ep.remove_from_group(grp_id) assert res != ZCLStatus.SUCCESS assert ep.request.call_count == 0 groups = ep.device.application.groups assert groups.add_group.call_count == 0 assert groups.remove_group.call_count == 0 assert grp_mock.add_member.call_count == 0 assert grp_mock.remove_member.call_count == 0 async def test_remove_from_group_fail(ep): grp_id = 0x1234 ep, grp_mock = _group_remove_mock(ep, success=False) res = await ep.remove_from_group(grp_id) assert res != ZCLStatus.SUCCESS assert ep.request.call_count == 1 groups = ep.device.application.groups assert groups.add_group.call_count == 0 assert groups.remove_group.call_count == 0 assert grp_mock.add_member.call_count == 0 assert grp_mock.remove_member.call_count == 0 def test_ep_manufacturer(ep): ep.device.manufacturer = sentinel.device_manufacturer assert ep.manufacturer is sentinel.device_manufacturer ep.manufacturer = sentinel.ep_manufacturer assert ep.manufacturer is sentinel.ep_manufacturer def test_ep_model(ep): ep.device.model = sentinel.device_model assert ep.model is sentinel.device_model ep.model = sentinel.ep_model assert ep.model is sentinel.ep_model async def test_group_membership_scan(ep): """Test group membership scan.""" ep.device.application.groups.update_group_membership = MagicMock() await ep.group_membership_scan() assert ep.device.application.groups.update_group_membership.call_count == 0 assert ep.device.request.call_count == 0 ep.add_input_cluster(4) ep.device.request.return_value = [0, [1, 3, 7]] await ep.group_membership_scan() assert ep.device.application.groups.update_group_membership.call_count == 1 assert ep.device.application.groups.update_group_membership.call_args[0][1] == { 1, 3, 7, } assert ep.device.request.call_count == 1 async def test_group_membership_scan_fail(ep): """Test group membership scan failure.""" ep.device.application.groups.update_group_membership = MagicMock() ep.add_input_cluster(4) ep.device.request.side_effect = asyncio.TimeoutError await ep.group_membership_scan() assert ep.device.application.groups.update_group_membership.call_count == 0 assert ep.device.request.call_count == 1 async def test_group_membership_scan_fail_default_response(ep, caplog): """Test group membership scan failure because group commands are unsupported.""" ep.device.application.groups.update_group_membership = MagicMock() ep.add_input_cluster(4) ep.device.request.side_effect = asyncio.TimeoutError with patch.object(ep.groups, "get_membership", new=AsyncMock()) as get_membership: get_membership.return_value = GENERAL_COMMANDS[ GeneralCommand.Default_Response ].schema(command_id=2, status=ZCLStatus.UNSUP_CLUSTER_COMMAND) await ep.group_membership_scan() assert "Device does not support group commands" in caplog.text assert ep.device.application.groups.update_group_membership.call_count == 0 def test_endpoint_manufacturer_id(ep): """Test manufacturer id.""" ep.device.manufacturer_id = sentinel.manufacturer_id assert ep.manufacturer_id is sentinel.manufacturer_id def test_endpoint_repr(ep): ep.status = endpoint.Status.ZDO_INIT # All standard ep.add_input_cluster(0x0001) ep.add_input_cluster(0x0002) ep.add_output_cluster(0x0006) ep.add_output_cluster(0x0008) # Spec-violating but still happens (https://github.com/zigpy/zigpy/issues/758) ep.add_input_cluster(0xEF00) assert "ZDO_INIT" in repr(ep) assert "power:0x0001" in repr(ep) assert "device_temperature:0x0002" in repr(ep) assert "on_off:0x0006" in repr(ep) assert "level:0x0008" in repr(ep) assert "0xEF00" in repr(ep) zigpy-0.80.1/tests/test_group.py000066400000000000000000000257211501451476000166630ustar00rootroot00000000000000import pytest import zigpy.device import zigpy.endpoint import zigpy.group import zigpy.types as t import zigpy.zcl from .async_mock import AsyncMock, MagicMock, call, sentinel FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" @pytest.fixture def endpoint(app_mock): ieee = t.EUI64(map(t.uint8_t, [0, 1, 2, 3, 4, 5, 6, 7])) dev = zigpy.device.Device(app_mock, ieee, 65535) return zigpy.endpoint.Endpoint(dev, 3) @pytest.fixture def groups(app_mock): groups = zigpy.group.Groups(app_mock) groups.listener_event = MagicMock() groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) return groups @pytest.fixture def group(): groups_mock = MagicMock(spec_set=zigpy.group.Groups) groups_mock.application.mrequest = AsyncMock() return zigpy.group.Group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, groups_mock) @pytest.fixture def group_endpoint(group): group.request = AsyncMock() return zigpy.group.GroupEndpoint(group) def test_add_group(groups, monkeypatch): monkeypatch.setattr( zigpy.group, "Group", MagicMock(spec_set=zigpy.group.Group, return_value=sentinel.group), ) grp_id, grp_name = 0x1234, "Group Name for 0x1234 group." assert grp_id not in groups ret = groups.add_group(grp_id, grp_name) assert groups.listener_event.call_count == 1 assert ret is sentinel.group groups.listener_event.reset_mock() ret = groups.add_group(grp_id, grp_name) assert groups.listener_event.call_count == 0 assert ret is sentinel.group def test_add_group_no_evt(groups, monkeypatch): monkeypatch.setattr( zigpy.group, "Group", MagicMock(spec_set=zigpy.group.Group, return_value=sentinel.group), ) grp_id, grp_name = 0x1234, "Group Name for 0x1234 group." assert grp_id not in groups ret = groups.add_group(grp_id, grp_name, suppress_event=True) assert groups.listener_event.call_count == 0 assert ret is sentinel.group groups.listener_event.reset_mock() ret = groups.add_group(grp_id, grp_name) assert groups.listener_event.call_count == 0 assert ret is sentinel.group def test_pop_group_id(groups, endpoint): group = groups[FIXTURE_GRP_ID] group.add_member(endpoint) group.remove_member = MagicMock(side_effect=group.remove_member) groups.listener_event.reset_mock() assert FIXTURE_GRP_ID in groups grp = groups.pop(FIXTURE_GRP_ID) assert isinstance(grp, zigpy.group.Group) assert FIXTURE_GRP_ID not in groups assert groups.listener_event.call_count == 2 assert group.remove_member.call_count == 1 assert group.remove_member.call_args[0][0] is endpoint with pytest.raises(KeyError): groups.pop(FIXTURE_GRP_ID) def test_pop_group(groups, endpoint): assert FIXTURE_GRP_ID in groups group = groups[FIXTURE_GRP_ID] group.add_member(endpoint) group.remove_member = MagicMock(side_effect=group.remove_member) groups.listener_event.reset_mock() grp = groups.pop(group) assert isinstance(grp, zigpy.group.Group) assert FIXTURE_GRP_ID not in groups assert groups.listener_event.call_count == 2 assert group.remove_member.call_count == 1 assert group.remove_member.call_args[0][0] is endpoint with pytest.raises(KeyError): groups.pop(grp) def test_group_add_member(group, endpoint): listener = MagicMock() group.add_listener(listener) assert endpoint.unique_id not in group.members assert FIXTURE_GRP_ID not in endpoint.member_of group.add_member(endpoint) assert endpoint.unique_id in group.members assert FIXTURE_GRP_ID in endpoint.member_of assert listener.member_added.call_count == 1 assert listener.member_removed.call_count == 0 listener.reset_mock() group.add_member(endpoint) assert listener.member_added.call_count == 0 assert listener.member_removed.call_count == 0 group.__repr__() assert group.name == FIXTURE_GRP_NAME with pytest.raises(ValueError): group.add_member(endpoint.endpoint_id) def test_group_add_member_no_evt(group, endpoint): listener = MagicMock() group.add_listener(listener) assert endpoint.unique_id not in group group.add_member(endpoint, suppress_event=True) assert endpoint.unique_id in group assert FIXTURE_GRP_ID in endpoint.member_of assert listener.member_added.call_count == 0 assert listener.member_removed.call_count == 0 def test_noname_group(): group = zigpy.group.Group(FIXTURE_GRP_ID) assert group.name.startswith("No name group ") def test_group_remove_member(group, endpoint): listener = MagicMock() group.add_listener(listener) group.add_member(endpoint, suppress_event=True) assert endpoint.unique_id in group assert FIXTURE_GRP_ID in endpoint.member_of group.remove_member(endpoint) assert endpoint.unique_id not in group assert FIXTURE_GRP_ID not in endpoint.member_of assert listener.member_added.call_count == 0 assert listener.member_removed.call_count == 1 def test_group_magic_methods(group, endpoint): group.add_member(endpoint, suppress_event=True) assert endpoint.unique_id in group.members assert endpoint.unique_id in group assert group[endpoint.unique_id] is endpoint def test_groups_properties(groups: zigpy.group.Groups): """Test groups properties.""" assert groups.application is not None def test_group_properties(group: zigpy.group.Group): """Test group properties.""" assert group.application is not None assert group.groups is not None assert isinstance(group.endpoint, zigpy.group.GroupEndpoint) def test_group_cluster_from_cluster_id(): """Group cluster by cluster id.""" cls = zigpy.group.GroupCluster.from_id(MagicMock(), 6) assert isinstance(cls, zigpy.zcl.Cluster) with pytest.raises(KeyError): zigpy.group.GroupCluster.from_id(MagicMock(), 0xFFFF) def test_group_cluster_from_cluster_name(): """Group cluster by cluster name.""" cls = zigpy.group.GroupCluster.from_attr(MagicMock(), "on_off") assert isinstance(cls, zigpy.zcl.Cluster) with pytest.raises(AttributeError): zigpy.group.GroupCluster.from_attr(MagicMock(), "no_such_cluster") async def test_group_ep_request(group_endpoint): on_off = zigpy.group.GroupCluster.from_attr(group_endpoint, "on_off") await on_off.on() assert group_endpoint.device.request.mock_calls == [ call( 260, # profile 0x0006, # cluster 1, # sequence b"\x01\x01\x01", # data ) ] def test_group_ep_reply(group_endpoint): group_endpoint.request = MagicMock() group_endpoint.reply( sentinel.cluster, sentinel.seq, sentinel.data, sentinel.extra_arg, extra_kwarg=sentinel.extra_kwarg, ) assert group_endpoint.request.call_count == 1 assert group_endpoint.request.call_args[0][0] is sentinel.cluster assert group_endpoint.request.call_args[0][1] is sentinel.seq assert group_endpoint.request.call_args[0][2] is sentinel.data assert group_endpoint.request.call_args[0][3] is sentinel.extra_arg assert group_endpoint.request.call_args[1]["extra_kwarg"] is sentinel.extra_kwarg def test_group_ep_by_cluster_id(group_endpoint, monkeypatch): clusters = {} group_endpoint._clusters = MagicMock(return_value=clusters) group_endpoint._clusters.__getitem__.side_effect = clusters.__getitem__ group_endpoint._clusters.__setitem__.side_effect = clusters.__setitem__ group_cluster_mock = MagicMock() group_cluster_mock.from_id.return_value = sentinel.group_cluster monkeypatch.setattr(zigpy.group, "GroupCluster", group_cluster_mock) assert len(clusters) == 0 cluster = group_endpoint[6] assert cluster is sentinel.group_cluster assert group_cluster_mock.from_id.call_count == 1 assert len(clusters) == 1 cluster = group_endpoint[6] assert cluster is sentinel.group_cluster assert group_cluster_mock.from_id.call_count == 1 def test_group_ep_by_cluster_attr(group_endpoint, monkeypatch): cluster_by_attr = {} group_endpoint._cluster_by_attr = MagicMock(return_value=cluster_by_attr) group_endpoint._cluster_by_attr.__getitem__.side_effect = ( cluster_by_attr.__getitem__ ) group_endpoint._cluster_by_attr.__setitem__.side_effect = ( cluster_by_attr.__setitem__ ) group_cluster_mock = MagicMock() group_cluster_mock.from_attr.return_value = sentinel.group_cluster monkeypatch.setattr(zigpy.group, "GroupCluster", group_cluster_mock) assert len(cluster_by_attr) == 0 cluster = group_endpoint.on_off assert cluster is sentinel.group_cluster assert group_cluster_mock.from_attr.call_count == 1 assert len(cluster_by_attr) == 1 cluster = group_endpoint.on_off assert cluster is sentinel.group_cluster assert group_cluster_mock.from_attr.call_count == 1 async def test_group_request(group): group.application.send_packet = AsyncMock() data = b"\x01\x02\x03\x04\x05" res = await group.request( sentinel.profile, sentinel.cluster, sentinel.sequence, data, ) assert group.application.send_packet.call_count == 1 packet = group.application.send_packet.mock_calls[0].args[0] assert packet.dst == t.AddrModeAddress( addr_mode=t.AddrMode.Group, address=group.group_id ) assert packet.profile_id is sentinel.profile assert packet.cluster_id is sentinel.cluster assert packet.tsn is sentinel.sequence assert packet.data.serialize() == data assert res.status is zigpy.zcl.foundation.Status.SUCCESS assert res.command_id == data[2] def test_update_group_membership_remove_member(groups, endpoint): """New device is not member of the old groups.""" groups[FIXTURE_GRP_ID].add_member(endpoint) assert endpoint.unique_id in groups[FIXTURE_GRP_ID] groups.update_group_membership(endpoint, set()) assert endpoint.unique_id not in groups[FIXTURE_GRP_ID] def test_update_group_membership_remove_add(groups, endpoint): """New device is not member of the old group, but member of new one.""" groups[FIXTURE_GRP_ID].add_member(endpoint) assert endpoint.unique_id in groups[FIXTURE_GRP_ID] new_group_id = 0x1234 assert new_group_id not in groups groups.update_group_membership(endpoint, {new_group_id}) assert endpoint.unique_id not in groups[FIXTURE_GRP_ID] assert new_group_id in groups assert endpoint.unique_id in groups[new_group_id] def test_update_group_membership_add_existing(groups, endpoint): """New device is member of new and existing groups.""" groups[FIXTURE_GRP_ID].add_member(endpoint) assert endpoint.unique_id in groups[FIXTURE_GRP_ID] new_group_id = 0x1234 groups.add_group(new_group_id) assert new_group_id in groups groups.update_group_membership(endpoint, {new_group_id, FIXTURE_GRP_ID}) assert endpoint.unique_id in groups[FIXTURE_GRP_ID] assert new_group_id in groups assert endpoint.unique_id in groups[new_group_id] zigpy-0.80.1/tests/test_listeners.py000066400000000000000000000132611501451476000175330ustar00rootroot00000000000000import asyncio import logging from unittest import mock import pytest from zigpy import listeners from zigpy.zcl import foundation import zigpy.zcl.clusters.general import zigpy.zdo.types as zdo_t def make_hdr(cmd, **kwargs): return foundation.ZCLHeader.cluster(tsn=0x12, command_id=cmd.command.id, **kwargs) query_next_image = zigpy.zcl.clusters.general.Ota.commands_by_name[ "query_next_image" ].schema on = zigpy.zcl.clusters.general.OnOff.commands_by_name["on"].schema off = zigpy.zcl.clusters.general.OnOff.commands_by_name["off"].schema toggle = zigpy.zcl.clusters.general.OnOff.commands_by_name["toggle"].schema async def test_future_listener(): listener = listeners.FutureListener( matchers=[ query_next_image(manufacturer_code=0x1234), on(), lambda hdr, cmd: hdr.command_id == 0x02, ], future=asyncio.get_running_loop().create_future(), ) assert not listener.resolve(make_hdr(off()), off()) assert not listener.resolve( make_hdr(query_next_image()), query_next_image( field_control=0, manufacturer_code=0x5678, # wrong `manufacturer_code` image_type=0x0000, current_file_version=0x00000000, ), ) # Only `on()` matches assert listener.resolve(make_hdr(on()), on()) assert listener.future.result() == (make_hdr(on()), on()) # Subsequent matches will not work assert not listener.resolve(make_hdr(on()), on()) # Reset the future object.__setattr__(listener, "future", asyncio.get_running_loop().create_future()) valid_query = query_next_image( field_control=0, manufacturer_code=0x1234, # correct `manufacturer_code` image_type=0x0000, current_file_version=0x00000000, ) assert listener.resolve(make_hdr(valid_query), valid_query) assert listener.future.result() == (make_hdr(valid_query), valid_query) # Reset the future object.__setattr__(listener, "future", asyncio.get_running_loop().create_future()) # Function matcher works assert listener.resolve(make_hdr(toggle()), toggle()) assert listener.future.result() == (make_hdr(toggle()), toggle()) async def test_future_listener_cancellation(): listener = listeners.FutureListener( matchers=[], future=asyncio.get_running_loop().create_future(), ) assert listener.cancel() assert listener.cancel() assert listener.cancel() with pytest.raises(asyncio.CancelledError): await listener.future async def test_callback_listener(): listener = listeners.CallbackListener( matchers=[ query_next_image(manufacturer_code=0x1234), on(), ], callback=mock.Mock(), ) assert not listener.resolve(make_hdr(off()), off()) assert not listener.resolve( make_hdr(query_next_image()), query_next_image( field_control=0, manufacturer_code=0x5678, # wrong `manufacturer_code` image_type=0x0000, current_file_version=0x00000000, ), ) # Only `on()` matches assert listener.resolve(make_hdr(on()), on()) assert listener.callback.mock_calls == [mock.call(make_hdr(on()), on())] # Subsequent matches still work assert not listener.cancel() # cancellation is not supported assert listener.resolve(make_hdr(on()), on()) assert listener.callback.mock_calls == [ mock.call(make_hdr(on()), on()), mock.call(make_hdr(on()), on()), ] async def test_callback_listener_error(caplog): listener = listeners.CallbackListener( matchers=[ on(), ], callback=mock.Mock(side_effect=RuntimeError("Uh oh")), ) with caplog.at_level(logging.WARNING): assert listener.resolve(make_hdr(on()), on()) assert "Caught an exception while executing callback" in caplog.text assert "RuntimeError: Uh oh" in caplog.text async def test_listener_callback_matches(): listener = listeners.CallbackListener( matchers=[lambda hdr, command: True], callback=mock.Mock(), ) assert listener.resolve(make_hdr(off()), off()) assert listener.callback.mock_calls == [mock.call(make_hdr(off()), off())] async def test_listener_callback_no_matches(): listener = listeners.CallbackListener( matchers=[lambda hdr, command: False], callback=mock.Mock(), ) assert not listener.resolve(make_hdr(off()), off()) assert listener.callback.mock_calls == [] async def test_listener_callback_invalid_matcher(caplog): listener = listeners.CallbackListener( matchers=[object()], callback=mock.Mock(), ) with caplog.at_level(logging.WARNING): assert not listener.resolve(make_hdr(off()), off()) assert listener.callback.mock_calls == [] assert f"Matcher {listener.matchers[0]!r} and command" in caplog.text async def test_listener_callback_invalid_call(caplog): listener = listeners.CallbackListener( matchers=[on()], callback=mock.Mock(), ) with caplog.at_level(logging.WARNING): assert not listener.resolve(make_hdr(on()), b"data") assert listener.callback.mock_calls == [] assert f"Matcher {listener.matchers[0]!r} and command" in caplog.text async def test_listener_callback_zdo(caplog): listener = listeners.CallbackListener( matchers=[ query_next_image(manufacturer_code=0x1234), ], callback=mock.Mock(), ) zdo_hdr = zdo_t.ZDOHeader(command_id=zdo_t.ZDOCmd.NWK_addr_req, tsn=0x01) zdo_cmd = [0x0000] with caplog.at_level(logging.WARNING): assert not listener.resolve(zdo_hdr, zdo_cmd) assert caplog.text == "" zigpy-0.80.1/tests/test_quirks.py000066400000000000000000001161271501451476000170460ustar00rootroot00000000000000import asyncio import importlib.util import itertools import pathlib import pkgutil import sys from typing import Final import pytest from zigpy import zcl from zigpy.const import ( SIG_ENDPOINTS, SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE, SIG_MANUFACTURER, SIG_MODEL, SIG_MODELS_INFO, SIG_SKIP_CONFIG, ) import zigpy.device import zigpy.endpoint import zigpy.quirks from zigpy.quirks.registry import DeviceRegistry import zigpy.types as t from .async_mock import AsyncMock, MagicMock, patch, sentinel ALLOWED_SIGNATURE = { SIG_EP_PROFILE, SIG_EP_TYPE, SIG_MANUFACTURER, SIG_MODEL, SIG_EP_INPUT, SIG_EP_OUTPUT, } ALLOWED_REPLACEMENT = {SIG_ENDPOINTS} def test_registry(): class TestDevice(zigpy.quirks.CustomDevice): signature = {SIG_MODEL: "model"} assert TestDevice in zigpy.quirks._DEVICE_REGISTRY assert zigpy.quirks._DEVICE_REGISTRY.remove(TestDevice) is None # :-/ assert TestDevice not in zigpy.quirks._DEVICE_REGISTRY @pytest.fixture def real_device(app_mock): ieee = sentinel.ieee nwk = 0x2233 real_device = zigpy.device.Device(app_mock, ieee, nwk) real_device.add_endpoint(1) real_device[1].profile_id = 255 real_device[1].device_type = 255 real_device.model = "model" real_device.manufacturer = "manufacturer" real_device[1].add_input_cluster(3) real_device[1].add_output_cluster(6) return real_device @pytest.fixture def real_device_2(app_mock): ieee = sentinel.ieee_2 nwk = 0x3344 real_device = zigpy.device.Device(app_mock, ieee, nwk) real_device.add_endpoint(1) real_device[1].profile_id = 255 real_device[1].device_type = 255 real_device.model = "model" real_device.manufacturer = "A different manufacturer" real_device[1].add_input_cluster(3) real_device[1].add_output_cluster(6) return real_device def _dev_reg(device): registry = DeviceRegistry() registry.add_to_registry(device) return registry def test_get_device_new_sig(real_device): class TestDevice: signature = {} def __init__(*args, **kwargs): pass def get_signature(self): pass registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_ENDPOINTS] = {1: {SIG_EP_PROFILE: 1}} registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_PROFILE] = 255 TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_TYPE] = 1 registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_TYPE] = 255 TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_INPUT] = [1] registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_INPUT] = [3] TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_OUTPUT] = [1] registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_ENDPOINTS][1][SIG_EP_OUTPUT] = [6] TestDevice.signature[SIG_MODEL] = "x" registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_MODEL] = "model" TestDevice.signature[SIG_MANUFACTURER] = "x" registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_MANUFACTURER] = "manufacturer" registry = _dev_reg(TestDevice) assert isinstance(registry.get_device(real_device), TestDevice) TestDevice.signature[SIG_ENDPOINTS][2] = {SIG_EP_PROFILE: 2} registry = _dev_reg(TestDevice) assert registry.get_device(real_device) is real_device assert zigpy.quirks.get_device(real_device, registry) is real_device def test_model_manuf_device_sig(real_device): class TestDevice: signature = {} def __init__(*args, **kwargs): pass def get_signature(self): pass registry = DeviceRegistry() registry.add_to_registry(TestDevice) assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_ENDPOINTS] = { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } } TestDevice.signature[SIG_MODEL] = "x" assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_MODEL] = "model" TestDevice.signature[SIG_MANUFACTURER] = "x" assert registry.get_device(real_device) is real_device TestDevice.signature[SIG_MANUFACTURER] = "manufacturer" assert isinstance(registry.get_device(real_device), TestDevice) def test_custom_devices(): def _check_range(cluster): for left, right in zcl.Cluster._registry_range: if left <= cluster <= right: return True return False # Validate that all CustomDevices look sane reg = zigpy.quirks._DEVICE_REGISTRY.registry_v1 candidates = list( itertools.chain(*itertools.chain(*[m.values() for m in reg.values()])) ) for device in candidates: # enforce new style of signature assert SIG_ENDPOINTS in device.signature numeric = [eid for eid in device.signature if isinstance(eid, int)] assert not numeric # Check that the signature data is OK signature = device.signature[SIG_ENDPOINTS] for profile_id, profile_data in signature.items(): assert isinstance(profile_id, int) assert set(profile_data.keys()) - ALLOWED_SIGNATURE == set() # Check that the replacement data is OK assert set(device.replacement.keys()) - ALLOWED_REPLACEMENT == set() for epid, epdata in device.replacement.get(SIG_ENDPOINTS, {}).items(): assert (epid in signature) or ( "profile" in epdata and SIG_EP_TYPE in epdata ) if "profile" in epdata: profile = epdata["profile"] assert isinstance(profile, int) and 0 <= profile <= 0xFFFF if SIG_EP_TYPE in epdata: device_type = epdata[SIG_EP_TYPE] assert isinstance(device_type, int) and 0 <= device_type <= 0xFFFF all_clusters = epdata.get(SIG_EP_INPUT, []) + epdata.get(SIG_EP_OUTPUT, []) for cluster in all_clusters: assert ( (isinstance(cluster, int) and cluster in zcl.Cluster._registry) or (isinstance(cluster, int) and _check_range(cluster)) or issubclass(cluster, zcl.Cluster) ) def test_custom_device(app_mock): class Device(zigpy.quirks.CustomDevice): signature = {} class MyEndpoint: def __init__(self, device, endpoint_id, *args, **kwargs): assert args == (sentinel.custom_endpoint_arg, replaces) class MyCluster(zigpy.quirks.CustomCluster): cluster_id = 0x8888 replacement = { SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: sentinel.profile_id, SIG_EP_INPUT: [0x0000, MyCluster], SIG_EP_OUTPUT: [0x0001, MyCluster], }, 2: (MyEndpoint, sentinel.custom_endpoint_arg), }, SIG_MODEL: "Mock Model", SIG_MANUFACTURER: "Mock Manufacturer", } class Device2(zigpy.quirks.CustomDevice): signature = {} class MyEndpoint: def __init__(self, device, endpoint_id, *args, **kwargs): assert args == (sentinel.custom_endpoint_arg, replaces) class MyCluster(zigpy.quirks.CustomCluster): cluster_id = 0x8888 replacement = { SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: sentinel.profile_id, SIG_EP_INPUT: [0x0000, MyCluster], SIG_EP_OUTPUT: [0x0001, MyCluster], }, 2: (MyEndpoint, sentinel.custom_endpoint_arg), }, SIG_MODEL: "Mock Model", SIG_MANUFACTURER: "Mock Manufacturer", SIG_SKIP_CONFIG: True, } assert 0x8888 not in zcl.Cluster._registry replaces = MagicMock() replaces[1].device_type = sentinel.device_type test_device = Device(app_mock, None, 0x4455, replaces) test_device2 = Device2(app_mock, None, 0x4455, replaces) assert test_device2.skip_configuration is True assert test_device.manufacturer == "Mock Manufacturer" assert test_device.model == "Mock Model" assert test_device.skip_configuration is False assert test_device[1].profile_id == sentinel.profile_id assert test_device[1].device_type == sentinel.device_type assert 0x0000 in test_device[1].in_clusters assert 0x8888 in test_device[1].in_clusters assert isinstance(test_device[1].in_clusters[0x8888], Device.MyCluster) assert 0x0001 in test_device[1].out_clusters assert 0x8888 in test_device[1].out_clusters assert isinstance(test_device[1].out_clusters[0x8888], Device.MyCluster) assert isinstance(test_device[2], Device.MyEndpoint) test_device.add_endpoint(3) assert isinstance(test_device[3], zigpy.endpoint.Endpoint) assert zigpy.quirks._DEVICE_REGISTRY.remove(Device) is None # :-/ assert Device not in zigpy.quirks._DEVICE_REGISTRY def test_custom_cluster_idx(): class TestClusterIdx(zigpy.quirks.CustomCluster): cluster_id = 0x1234 class AttributeDefs(zcl.foundation.BaseAttributeDefs): first_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0000, type=t.uint8_t ) second_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x00FF, type=t.enum8 ) class ServerCommandDefs(zcl.foundation.BaseCommandDefs): server_cmd_0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={"param1": t.uint8_t, "param2": t.uint8_t}, direction=False, ) server_cmd_2: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={"param1": t.uint8_t, "param2": t.uint8_t}, direction=False, ) class ClientCommandDefs(zcl.foundation.BaseCommandDefs): client_cmd_0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={"param1": t.uint8_t}, direction=True ) client_cmd_1: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={"param1": t.uint8_t}, direction=True ) assert hasattr(TestClusterIdx, "attributes_by_name") attr_idx_len = len(TestClusterIdx.attributes_by_name) attrs_len = len(TestClusterIdx.attributes) assert attr_idx_len == attrs_len for attr_name, attr in TestClusterIdx.attributes_by_name.items(): assert TestClusterIdx.attributes[attr.id].name == attr_name async def test_read_attributes_uncached(): class TestCluster(zigpy.quirks.CustomCluster): cluster_id = 0x1234 _CONSTANT_ATTRIBUTES = {0x0001: 5} class AttributeDefs(zcl.foundation.BaseAttributeDefs): first_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0000, type=t.uint8_t ) second_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0001, type=t.uint8_t ) third_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0002, type=t.uint8_t ) fouth_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0003, type=t.enum8 ) class ServerCommandDefs(zcl.foundation.BaseCommandDefs): server_cmd_0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={"param1": t.uint8_t, "param2": t.uint8_t}, direction=False, ) server_cmd_2: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={"param1": t.uint8_t, "param2": t.uint8_t}, direction=False, ) class ClientCommandDefs(zcl.foundation.BaseCommandDefs): client_cmd_0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={"param1": t.uint8_t}, direction=True ) client_cmd_1: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={"param1": t.uint8_t}, direction=True ) class TestCluster2(zigpy.quirks.CustomCluster): cluster_id = 0x1235 class AttributeDefs(zcl.foundation.BaseAttributeDefs): first_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0000, type=t.uint8_t ) epmock = MagicMock() epmock._device.get_sequence.return_value = 123 epmock.device.get_sequence.return_value = 123 cluster = TestCluster(epmock, True) cluster2 = TestCluster2(epmock, True) async def mockrequest( foundation, command, schema, args, manufacturer=None, **kwargs ): assert foundation is True assert command == 0x00 rar0 = _mk_rar(0x0000, 99) rar99 = _mk_rar(0x0002, None, 1) rar199 = _mk_rar(0x0003, 199) return [[rar0, rar99, rar199]] # Unknown attribute read passes through with pytest.raises(KeyError): cluster.get("unknown_attribute", 123) assert "unknown_attribute" not in cluster._attr_cache # Constant attribute can be read with `get` assert cluster.get("second_attribute") == 5 assert "second_attribute" not in cluster._attr_cache # test no constants cluster.request = mockrequest success, failure = await cluster.read_attributes([0, 2, 3]) assert success[0x0000] == 99 assert failure[0x0002] == 1 assert success[0x0003] == 199 assert cluster.get(0x0003) == 199 # test mixed response with constant success, failure = await cluster.read_attributes([0, 1, 2, 3]) assert success[0x0000] == 99 assert success[0x0001] == 5 assert failure[0x0002] == 1 assert success[0x0003] == 199 # test just constant attr success, failure = await cluster.read_attributes([1]) assert success[1] == 5 # test just constant attr cluster2.request = mockrequest success, failure = await cluster2.read_attributes([0, 2, 3]) assert success[0x0000] == 99 assert failure[0x0002] == 1 assert success[0x0003] == 199 async def test_read_attributes_default_response(): class TestCluster(zigpy.quirks.CustomCluster): cluster_id = 0x1234 _CONSTANT_ATTRIBUTES = {0x0001: 5} class AttributeDefs(zcl.foundation.BaseAttributeDefs): first_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0000, type=t.uint8_t ) second_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0001, type=t.uint8_t ) third_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0002, type=t.uint8_t ) fouth_attribute: Final = zcl.foundation.ZCLAttributeDef( id=0x0003, type=t.enum8 ) class ServerCommandDefs(zcl.foundation.BaseCommandDefs): server_cmd_0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={"param1": t.uint8_t, "param2": t.uint8_t}, direction=False, ) server_cmd_2: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={"param1": t.uint8_t, "param2": t.uint8_t}, direction=False, ) class ClientCommandDefs(zcl.foundation.BaseCommandDefs): client_cmd_0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={"param1": t.uint8_t}, direction=True ) client_cmd_1: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={"param1": t.uint8_t}, direction=True ) epmock = MagicMock() epmock._device.get_sequence.return_value = 123 epmock.device.get_sequence.return_value = 123 cluster = TestCluster(epmock, True) async def mockrequest( foundation, command, schema, args, manufacturer=None, **kwargs ): assert foundation is True assert command == 0 return [0xC1] cluster.request = mockrequest # test constants with errors success, failure = await cluster.read_attributes([0, 1, 2, 3], allow_cache=False) assert success == {1: 5} assert failure == {0: 0xC1, 2: 0xC1, 3: 0xC1} def _mk_rar(attrid, value, status=0): r = zcl.foundation.ReadAttributeRecord() r.attrid = attrid r.status = status r.value = zcl.foundation.TypeValue() r.value.value = value return r class ManufacturerSpecificCluster(zigpy.quirks.CustomCluster): cluster_id = 0x2222 ep_attribute = "just_a_cluster" class AttributeDefs(zcl.foundation.BaseAttributeDefs): attr0: Final = zcl.foundation.ZCLAttributeDef(id=0x0000, type=t.uint8_t) attr1: Final = zcl.foundation.ZCLAttributeDef( id=0x0001, type=t.uint16_t, is_manufacturer_specific=True ) class ServerCommandDefs(zcl.foundation.BaseCommandDefs): server_cmd0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={}, direction=False ) server_cmd1: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={}, direction=False, is_manufacturer_specific=True ) class ClientCommandDefs(zcl.foundation.BaseCommandDefs): client_cmd0: Final = zcl.foundation.ZCLCommandDef( id=0x00, schema={}, direction=False ) client_cmd1: Final = zcl.foundation.ZCLCommandDef( id=0x01, schema={}, direction=False, is_manufacturer_specific=True ) @pytest.fixture def manuf_cluster(): """Return a manufacturer specific cluster fixture.""" ep = MagicMock() ep.manufacturer_id = sentinel.manufacturer_id return ManufacturerSpecificCluster.from_id(ep, 0x2222) @pytest.fixture def manuf_cluster2(): """Return a manufacturer specific cluster fixture.""" class ManufCluster2(ManufacturerSpecificCluster): ep_attribute = "just_a_manufacturer_specific_cluster" cluster_id = 0xFC00 ep = MagicMock() ep.manufacturer_id = sentinel.manufacturer_id2 cluster = ManufCluster2(ep) cluster.cluster_id = 0xFC00 return cluster @pytest.mark.parametrize( ("cmd_name", "manufacturer"), [ ("client_cmd0", None), ("client_cmd1", sentinel.manufacturer_id), ], ) async def test_client_cmd_vendor_specific_by_name( manuf_cluster, manuf_cluster2, cmd_name, manufacturer ): """Test manufacturer specific client commands.""" with patch.object(manuf_cluster, "reply", AsyncMock()) as cmd_mock: await getattr(manuf_cluster, cmd_name)() await asyncio.sleep(0.01) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1][SIG_MANUFACTURER] is manufacturer with patch.object(manuf_cluster2, "reply", AsyncMock()) as cmd_mock: await getattr(manuf_cluster2, cmd_name)() await asyncio.sleep(0.01) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1][SIG_MANUFACTURER] is sentinel.manufacturer_id2 @pytest.mark.parametrize( ("cmd_name", "manufacturer"), [ ("server_cmd0", None), ("server_cmd1", sentinel.manufacturer_id), ], ) async def test_srv_cmd_vendor_specific_by_name( manuf_cluster, manuf_cluster2, cmd_name, manufacturer ): """Test manufacturer specific server commands.""" with patch.object(manuf_cluster, "request", AsyncMock()) as cmd_mock: await getattr(manuf_cluster, cmd_name)() await asyncio.sleep(0.01) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is manufacturer with patch.object(manuf_cluster2, "request", AsyncMock()) as cmd_mock: await getattr(manuf_cluster2, cmd_name)() await asyncio.sleep(0.01) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.manufacturer_id2 @pytest.mark.parametrize( ("attr_name", "manufacturer"), [ ("attr0", None), ("attr1", sentinel.manufacturer_id), ], ) async def test_read_attr_manufacture_specific( manuf_cluster, manuf_cluster2, attr_name, manufacturer ): """Test manufacturer specific read_attributes command.""" with patch.object(zcl.Cluster, "_read_attributes", AsyncMock()) as cmd_mock: await manuf_cluster.read_attributes([attr_name]) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is manufacturer cmd_mock.reset_mock() await manuf_cluster.read_attributes( [attr_name], manufacturer=sentinel.another_id ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id with patch.object(zcl.Cluster, "_read_attributes", AsyncMock()) as cmd_mock: await manuf_cluster2.read_attributes([attr_name]) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.manufacturer_id2 cmd_mock.reset_mock() await manuf_cluster2.read_attributes( [attr_name], manufacturer=sentinel.another_id ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id @pytest.mark.parametrize( ("attr_name", "manufacturer"), [ ("attr0", None), ("attr1", sentinel.manufacturer_id), ], ) async def test_write_attr_manufacture_specific( manuf_cluster, manuf_cluster2, attr_name, manufacturer ): """Test manufacturer specific write_attributes command.""" with patch.object(zcl.Cluster, "_write_attributes", AsyncMock()) as cmd_mock: await manuf_cluster.write_attributes({attr_name: 0x12}) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is manufacturer cmd_mock.reset_mock() await manuf_cluster.write_attributes( {attr_name: 0x12}, manufacturer=sentinel.another_id ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id with patch.object(zcl.Cluster, "_write_attributes", AsyncMock()) as cmd_mock: await manuf_cluster2.write_attributes({attr_name: 0x12}) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.manufacturer_id2 cmd_mock.reset_mock() await manuf_cluster2.write_attributes( {attr_name: 0x12}, manufacturer=sentinel.another_id ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id @pytest.mark.parametrize( ("attr_name", "manufacturer"), [ ("attr0", None), ("attr1", sentinel.manufacturer_id), ], ) async def test_write_attr_undivided_manufacture_specific( manuf_cluster, manuf_cluster2, attr_name, manufacturer ): """Test manufacturer specific write_attributes_undivided command.""" with patch.object( zcl.Cluster, "_write_attributes_undivided", AsyncMock() ) as cmd_mock: await manuf_cluster.write_attributes_undivided({attr_name: 0x12}) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is manufacturer cmd_mock.reset_mock() await manuf_cluster.write_attributes_undivided( {attr_name: 0x12}, manufacturer=sentinel.another_id ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id with patch.object( zcl.Cluster, "_write_attributes_undivided", AsyncMock() ) as cmd_mock: await manuf_cluster2.write_attributes_undivided({attr_name: 0x12}) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.manufacturer_id2 cmd_mock.reset_mock() await manuf_cluster2.write_attributes_undivided( {attr_name: 0x12}, manufacturer=sentinel.another_id ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id @pytest.mark.parametrize( ("attr_name", "manufacturer"), [ ("attr0", None), ("attr1", sentinel.manufacturer_id), ], ) async def test_configure_reporting_manufacture_specific( manuf_cluster, manuf_cluster2, attr_name, manufacturer ): """Test manufacturer specific configure_reporting command.""" with patch.object(zcl.Cluster, "_configure_reporting", AsyncMock()) as cmd_mock: await manuf_cluster.configure_reporting( attr_name, min_interval=1, max_interval=1, reportable_change=1 ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is manufacturer cmd_mock.reset_mock() await manuf_cluster.configure_reporting( attr_name, min_interval=1, max_interval=1, reportable_change=1, manufacturer=sentinel.another_id, ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id with patch.object(zcl.Cluster, "_configure_reporting", AsyncMock()) as cmd_mock: await manuf_cluster2.configure_reporting( attr_name, min_interval=1, max_interval=1, reportable_change=1 ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.manufacturer_id2 cmd_mock.reset_mock() await manuf_cluster2.configure_reporting( attr_name, min_interval=1, max_interval=1, reportable_change=1, manufacturer=sentinel.another_id, ) assert cmd_mock.call_count == 1 assert cmd_mock.call_args[1]["manufacturer"] is sentinel.another_id def test_different_manuf_same_model(real_device, real_device_2): """Test quirk matching for same model, but different manufacturers.""" class TestDevice_1(zigpy.quirks.CustomDevice): signature = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } def get_signature(self): pass class TestDevice_2(zigpy.quirks.CustomDevice): signature = { SIG_MODELS_INFO: (("A different manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } def get_signature(self): pass registry = DeviceRegistry() registry.add_to_registry(TestDevice_1) assert isinstance(registry.get_device(real_device), TestDevice_1) assert registry.get_device(real_device_2) is real_device_2 registry.add_to_registry(TestDevice_2) assert isinstance(registry.get_device(real_device_2), TestDevice_2) assert not zigpy.quirks.get_quirk_list("manufacturer", "no such model") assert not zigpy.quirks.get_quirk_list("manufacturer", "no such model", registry) assert not zigpy.quirks.get_quirk_list("A different manufacturer", "no such model") assert not zigpy.quirks.get_quirk_list( "A different manufacturer", "no such model", registry ) assert not zigpy.quirks.get_quirk_list("no such manufacturer", "model") assert not zigpy.quirks.get_quirk_list("no such manufacturer", "model", registry) manuf1_list = zigpy.quirks.get_quirk_list("manufacturer", "model", registry) assert len(manuf1_list) == 1 assert manuf1_list[0] is TestDevice_1 manuf2_list = zigpy.quirks.get_quirk_list( "A different manufacturer", "model", registry ) assert len(manuf2_list) == 1 assert manuf2_list[0] is TestDevice_2 def test_quirk_match_order(real_device, real_device_2): """Test quirk matching order to allow user overrides via custom quirks.""" class BuiltInQuirk(zigpy.quirks.CustomDevice): signature = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } def get_signature(self): pass class CustomQuirk(BuiltInQuirk): pass registry = DeviceRegistry() registry.add_to_registry(BuiltInQuirk) # With only a single matching quirk there is no choice but to use the first one assert type(registry.get_device(real_device)) is BuiltInQuirk registry.add_to_registry(CustomQuirk) # A quirk registered later that also matches the device will be preferred assert type(registry.get_device(real_device)) is CustomQuirk def test_quirk_wildcard_manufacturer(real_device, real_device_2): """Test quirk matching with a wildcard (None) manufacturer.""" class BaseDev(zigpy.quirks.CustomDevice): def get_signature(self): pass class ModelsQuirk(BaseDev): signature = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } class ModelsQuirkNoMatch(BaseDev): # same model and manufacture, different endpoint signature signature = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 260, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } class ModelOnlyQuirk(BaseDev): # Wildcard Manufacturer signature = { SIG_MODEL: "model", SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } class ModelOnlyQuirkNoMatch(BaseDev): # Wildcard Manufacturer, none matching endpoint signature signature = { SIG_MODEL: "model", SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 260, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } registry = DeviceRegistry() for quirk in ModelsQuirk, ModelsQuirkNoMatch, ModelOnlyQuirk, ModelOnlyQuirkNoMatch: registry.add_to_registry(quirk) quirked = registry.get_device(real_device) assert isinstance(quirked, ModelsQuirk) quirked = registry.get_device(real_device_2) assert isinstance(quirked, ModelOnlyQuirk) real_device.manufacturer = ( "We are expected to match a manufacturer wildcard quirk now" ) quirked = registry.get_device(real_device) assert isinstance(quirked, ModelOnlyQuirk) real_device.model = "And now we should not match any quirk" quirked = registry.get_device(real_device) assert quirked is real_device async def test_manuf_id_disable(real_device): class TestCluster(ManufacturerSpecificCluster): cluster_id = 0xFF00 real_device.manufacturer_id_override = 0x1234 ep = real_device.endpoints[1] ep.add_input_cluster(TestCluster.cluster_id, TestCluster(ep)) assert isinstance(ep.just_a_cluster, TestCluster) assert ep.manufacturer_id == 0x1234 # The default behavior for a manufacturer-specific cluster, command, or attribute is # to include the manufacturer ID in the request with patch.object(ep, "request", AsyncMock()) as request_mock: request_mock.return_value = (zcl.foundation.Status.SUCCESS, "done") await ep.just_a_cluster.command( ep.just_a_cluster.commands_by_name["server_cmd0"].id, ) await ep.just_a_cluster.read_attributes(["attr0"]) await ep.just_a_cluster.write_attributes({"attr0": 1}) assert len(request_mock.mock_calls) == 3 for mock_call in request_mock.mock_calls: data = mock_call.kwargs["data"] hdr, _ = zcl.foundation.ZCLHeader.deserialize(data) assert hdr.manufacturer == 0x1234 # But it can be disabled by passing NO_MANUFACTURER_ID with patch.object(ep, "request", AsyncMock()) as request_mock: request_mock.return_value = (zcl.foundation.Status.SUCCESS, "done") await ep.just_a_cluster.command( ep.just_a_cluster.commands_by_name["server_cmd0"].id, manufacturer=zcl.foundation.ZCLHeader.NO_MANUFACTURER_ID, ) await ep.just_a_cluster.read_attributes( ["attr0"], manufacturer=zcl.foundation.ZCLHeader.NO_MANUFACTURER_ID ) await ep.just_a_cluster.write_attributes( {"attr0": 1}, manufacturer=zcl.foundation.ZCLHeader.NO_MANUFACTURER_ID ) assert len(request_mock.mock_calls) == 3 for mock_call in request_mock.mock_calls: data = mock_call.kwargs["data"] hdr, _ = zcl.foundation.ZCLHeader.deserialize(data) assert hdr.manufacturer is None async def test_cluster_manufacturer_id_override(real_device): class TestCluster(ManufacturerSpecificCluster): cluster_id = 0xFF00 manufacturer_id_override = 0xABCD real_device.manufacturer_id_override = 0x1234 ep = real_device.endpoints[1] ep.add_input_cluster(TestCluster.cluster_id, TestCluster(ep)) assert isinstance(ep.just_a_cluster, TestCluster) assert ep.manufacturer_id == 0x1234 with patch.object(ep, "request", AsyncMock()) as request_mock: await ep.just_a_cluster.read_attributes(["attr0"]) # We prefer the cluster-level override data = request_mock.mock_calls[0].kwargs["data"] hdr, _ = zcl.foundation.ZCLHeader.deserialize(data) assert hdr.manufacturer == 0xABCD async def test_request_with_kwargs(real_device): class CustomLevel(zigpy.quirks.CustomCluster, zcl.clusters.general.LevelControl): pass class TestQuirk(zigpy.quirks.CustomDevice): signature = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } replacement = { SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3, CustomLevel], SIG_EP_OUTPUT: [6], } }, } registry = DeviceRegistry() registry.add_to_registry(TestQuirk) quirked = registry.get_device(real_device) assert isinstance(quirked, TestQuirk) ep = quirked.endpoints[1] with patch.object(ep, "request", AsyncMock()) as request_mock: ep.device.get_sequence = MagicMock(return_value=1) await ep.level.move_to_level(0x00, 123) await ep.level.move_to_level(0x00, transition_time=123) await ep.level.move_to_level(level=0x00, transition_time=123) assert len(request_mock.mock_calls) == 3 assert all(c == request_mock.mock_calls[0] for c in request_mock.mock_calls) def test_purge_custom_quirks(tmp_path: pathlib.Path, app_mock) -> None: def load_quirks(): for importer, modname, _ in pkgutil.walk_packages(path=[str(tmp_path)]): spec = importer.find_spec(modname) module = importlib.util.module_from_spec(spec) sys.modules[modname] = module spec.loader.exec_module(module) (tmp_path / "quirk1.py").write_text(""" import zigpy.quirks from zigpy.zcl.clusters.general import LevelControl from zigpy.const import ( SIG_ENDPOINTS, SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE, SIG_MODELS_INFO, ) class CustomLevel1(zigpy.quirks.CustomCluster, LevelControl): pass class TestQuirk1(zigpy.quirks.CustomDevice): signature = { SIG_MODELS_INFO: (("manufacturer1", "model1"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } replacement = { SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3, CustomLevel1], SIG_EP_OUTPUT: [6], } }, }""") (tmp_path / "quirk2.py").write_text(""" import zigpy.quirks from zigpy.quirks.v2 import QuirkBuilder from zigpy.zcl import ClusterType from zigpy.zcl.clusters.general import LevelControl class CustomLevel2(zigpy.quirks.CustomCluster, LevelControl): pass QuirkBuilder("manufacturer2", "model2").adds( cluster=CustomLevel2, cluster_type=ClusterType.Server, endpoint_id=1, ).add_to_registry() """) dev1 = zigpy.device.Device( app_mock, t.EUI64.convert("11:11:11:11:11:11:11:11"), 0x1234 ) dev1.add_endpoint(1) dev1[1].profile_id = 255 dev1[1].device_type = 255 dev1.model = "model1" dev1.manufacturer = "manufacturer1" dev1[1].add_input_cluster(3) dev1[1].add_output_cluster(6) dev2 = zigpy.device.Device( app_mock, t.EUI64.convert("22:22:22:22:22:22:22:22"), 0x5678 ) dev2.add_endpoint(1) dev2[1].profile_id = 255 dev2[1].device_type = 255 dev2.model = "model2" dev2.manufacturer = "manufacturer2" dev2[1].add_input_cluster(3) dev2[1].add_output_cluster(6) registry = zigpy.quirks.DEVICE_REGISTRY assert not registry.registry_v1.get("manufacturer1", {}).get("model1", []) assert not registry.registry_v2.get(("manufacturer2", "model2"), set()) load_quirks() assert registry.registry_v1.get("manufacturer1", {}).get("model1", []) assert registry.registry_v2.get(("manufacturer2", "model2"), set()) assert type(registry.get_device(dev1)).__name__ == "TestQuirk1" assert registry.get_device(dev2).quirk_metadata.quirk_file.name == "quirk2.py" # Only quirks from the passed directory are purged so this is a no-op registry.purge_custom_quirks(tmp_path / "some_other_dir") assert registry.registry_v1.get("manufacturer1", {}).get("model1", []) assert registry.registry_v2.get(("manufacturer2", "model2"), set()) # Now we really remove them registry.purge_custom_quirks(tmp_path) assert not registry.registry_v1.get("manufacturer1", {}).get("model1", []) assert not registry.registry_v2.get(("manufacturer2", "model2"), set()) assert registry.get_device(dev1) is dev1 assert registry.get_device(dev2) is dev2 zigpy-0.80.1/tests/test_quirks_registry.py000066400000000000000000000113711501451476000207710ustar00rootroot00000000000000from collections import deque from unittest import mock import pytest from zigpy.const import SIG_MODELS_INFO from zigpy.quirks.registry import DeviceRegistry class FakeDevice: def __init__(self): self.signature = {} @pytest.fixture def fake_dev(): return FakeDevice() def test_add_to_registry_new_sig(fake_dev): fake_dev.signature = { 1: {}, 2: {}, 3: { "manufacturer": mock.sentinel.legacy_manufacturer, "model": mock.sentinel.legacy_model, }, "endpoints": { 1: { "manufacturer": mock.sentinel.manufacturer, "model": mock.sentinel.model, } }, "manufacturer": mock.sentinel.dev_manufacturer, "model": mock.sentinel.dev_model, } reg = DeviceRegistry() reg.add_to_registry(fake_dev) assert reg._registry_v1[mock.sentinel.dev_manufacturer][ mock.sentinel.dev_model ] == deque([fake_dev]) def test_add_to_registry_models_info(fake_dev): fake_dev.signature = { 1: {}, 2: {}, 3: { "manufacturer": mock.sentinel.legacy_manufacturer, "model": mock.sentinel.legacy_model, }, "endpoints": { 1: { "manufacturer": mock.sentinel.manufacturer, "model": mock.sentinel.model, } }, SIG_MODELS_INFO: [ (mock.sentinel.manuf_1, mock.sentinel.model_1), (mock.sentinel.manuf_2, mock.sentinel.model_2), ], } reg = DeviceRegistry() reg.add_to_registry(fake_dev) assert reg._registry_v1[mock.sentinel.manuf_1][mock.sentinel.model_1] == deque( [fake_dev] ) assert reg._registry_v1[mock.sentinel.manuf_2][mock.sentinel.model_2] == deque( [fake_dev] ) def test_remove_new_sig(fake_dev): fake_dev.signature = { 1: {}, 2: {}, 3: { "manufacturer": mock.sentinel.legacy_manufacturer, "model": mock.sentinel.legacy_model, }, "endpoints": { 1: { "manufacturer": mock.sentinel.manufacturer, "model": mock.sentinel.model, } }, "manufacturer": mock.sentinel.dev_manufacturer, "model": mock.sentinel.dev_model, } reg = DeviceRegistry() quirk_list = mock.MagicMock() model_dict = mock.MagicMock(spec_set=dict) model_dict.__getitem__.return_value = quirk_list manuf_dict = mock.MagicMock() manuf_dict.__getitem__.return_value = model_dict reg._registry_v1 = manuf_dict reg.remove(fake_dev) assert manuf_dict.__getitem__.call_count == 1 assert manuf_dict.__getitem__.call_args[0][0] is mock.sentinel.dev_manufacturer assert model_dict.__getitem__.call_count == 1 assert model_dict.__getitem__.call_args[0][0] is mock.sentinel.dev_model assert quirk_list.insert.call_count == 0 assert quirk_list.remove.call_count == 1 assert quirk_list.remove.call_args[0][0] is fake_dev def test_remove_models_info(fake_dev): fake_dev.signature = { 1: {}, 2: {}, 3: { "manufacturer": mock.sentinel.legacy_manufacturer, "model": mock.sentinel.legacy_model, }, "endpoints": { 1: { "manufacturer": mock.sentinel.manufacturer, "model": mock.sentinel.model, } }, SIG_MODELS_INFO: [ (mock.sentinel.manuf_1, mock.sentinel.model_1), (mock.sentinel.manuf_2, mock.sentinel.model_2), ], } reg = DeviceRegistry() quirk_list = mock.MagicMock() model_dict = mock.MagicMock(spec_set=dict) model_dict.__getitem__.return_value = quirk_list manuf_dict = mock.MagicMock() manuf_dict.__getitem__.return_value = model_dict reg._registry_v1 = manuf_dict reg.remove(fake_dev) assert manuf_dict.__getitem__.call_count == 2 assert manuf_dict.__getitem__.call_args_list[0][0][0] is mock.sentinel.manuf_1 assert manuf_dict.__getitem__.call_args_list[1][0][0] is mock.sentinel.manuf_2 assert model_dict.__getitem__.call_count == 2 assert model_dict.__getitem__.call_args_list[0][0][0] is mock.sentinel.model_1 assert model_dict.__getitem__.call_args_list[1][0][0] is mock.sentinel.model_2 assert quirk_list.insert.call_count == 0 assert quirk_list.remove.call_count == 2 assert quirk_list.remove.call_args_list[0][0][0] is fake_dev assert quirk_list.remove.call_args_list[1][0][0] is fake_dev @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_property_accessors(): reg = DeviceRegistry() assert reg.registry is reg._registry_v1 assert reg.registry_v1 is reg._registry_v1 assert reg.registry_v2 is reg._registry_v2 zigpy-0.80.1/tests/test_quirks_v2.py000066400000000000000000001316501501451476000174530ustar00rootroot00000000000000"""Tests for the quirks v2 module.""" import pathlib from typing import Final from unittest.mock import AsyncMock import pytest from zigpy.const import ( SIG_ENDPOINTS, SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE, SIG_MODELS_INFO, ) from zigpy.device import Device from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice, signature_matches from zigpy.quirks.registry import DeviceRegistry from zigpy.quirks.v2 import ( BinarySensorMetadata, CustomDeviceV2, DeviceAlertLevel, DeviceAlertMetadata, EntityMetadata, EntityPlatform, EntityType, NumberMetadata, PreventDefaultEntityCreationMetadata, QuirkBuilder, SwitchMetadata, WriteAttributeButtonMetadata, ZCLCommandButtonMetadata, ZCLSensorMetadata, add_to_registry_v2, ) from zigpy.quirks.v2.homeassistant import UnitOfTime import zigpy.types as t from zigpy.zcl import ClusterType from zigpy.zcl.clusters.general import ( Alarms, Basic, Groups, Identify, LevelControl, OnOff, Ota, PowerConfiguration, Scenes, ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.lightlink import LightLink from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef, ZCLCommandDef from zigpy.zdo.types import LogicalType, NodeDescriptor from .async_mock import sentinel @pytest.fixture(name="device_mock") def real_device(app_mock) -> Device: """Device fixture with a single endpoint.""" ieee = sentinel.ieee nwk = 0x2233 device = Device(app_mock, ieee, nwk) device.add_endpoint(1) device[1].profile_id = 255 device[1].device_type = 255 device.model = "model" device.manufacturer = "manufacturer" device[1].add_input_cluster(3) device[1].add_output_cluster(6) return device async def test_quirks_v2(device_mock): """Test adding a v2 quirk to the registry and getting back a quirked device.""" registry = DeviceRegistry() signature = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 255, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], SIG_EP_OUTPUT: [6], } }, } class TestCustomCluster(CustomCluster, Basic): """Custom cluster for testing quirks v2.""" class AttributeDefs(BaseAttributeDefs): # pylint: disable=too-few-public-methods """Attribute definitions for the custom cluster.""" # pylint: disable=disallowed-name foo: Final = ZCLAttributeDef(id=0x0000, type=t.uint8_t) # pylint: disable=disallowed-name bar: Final = ZCLAttributeDef(id=0x0000, type=t.uint8_t) # pylint: disable=disallowed-name, invalid-name report: Final = ZCLAttributeDef(id=0x0000, type=t.uint8_t) entry = ( # Quirk builder creation line, this comment is read by this unit test QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .filter(signature_matches(signature)) .adds( TestCustomCluster, constant_attributes={TestCustomCluster.AttributeDefs.foo: 3}, ) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) # coverage for overridden __eq__ method assert entry.adds_metadata[0] != entry.adds_metadata[1] assert entry.adds_metadata[0] != entry quirked = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) assert quirked in registry # this would need to be updated if the line number of the call to QuirkBuilder # changes in this test in the future assert str(quirked.quirk_metadata.quirk_file).endswith( "zigpy/tests/test_quirks_v2.py" ) # To avoid having to rewrite this test every time quirks change, we read the current # file to find the line number quirk_builder_line = next( index for index, line in enumerate(pathlib.Path(__file__).read_text().splitlines()) if "# Quirk builder creation line" in line ) assert quirked.quirk_metadata.quirk_file_line == quirk_builder_line + 2 ep = quirked.endpoints[1] assert ep.basic is not None assert isinstance(ep.basic, Basic) assert isinstance(ep.basic, TestCustomCluster) # pylint: disable=protected-access assert ep.basic._CONSTANT_ATTRIBUTES[TestCustomCluster.AttributeDefs.foo.id] == 3 assert ep.on_off is not None assert isinstance(ep.on_off, OnOff) additional_entities = quirked.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ] assert len(additional_entities) == 1 assert additional_entities[0].endpoint_id == 1 assert additional_entities[0].cluster_id == OnOff.cluster_id assert additional_entities[0].cluster_type == ClusterType.Server assert ( additional_entities[0].attribute_name == OnOff.AttributeDefs.start_up_on_off.name ) assert additional_entities[0].enum == OnOff.StartUpOnOff assert additional_entities[0].entity_type == EntityType.CONFIG registry.remove(quirked) assert quirked not in registry async def test_quirks_v2_model_manufacturer(device_mock): """Test the potential exceptions when model and manufacturer are set up incorrectly.""" registry = DeviceRegistry() with pytest.raises( ValueError, match="manufacturer and model must be provided together or completely omitted.", ): ( QuirkBuilder(device_mock.manufacturer, model=None, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, ) .add_to_registry() ) with pytest.raises( ValueError, match="manufacturer and model must be provided together or completely omitted.", ): ( QuirkBuilder(manufacturer=None, model=device_mock.model, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, ) .add_to_registry() ) with pytest.raises( ValueError, match="At least one manufacturer and model must be specified for a v2 quirk.", ): ( QuirkBuilder(registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) async def test_quirks_v2_quirk_builder_cloning(device_mock): """Test the quirk builder clone functionality.""" registry = DeviceRegistry() base = ( QuirkBuilder(registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .applies_to("foo", "bar") ) cloned = base.clone() base.add_to_registry() ( cloned.adds(PowerConfiguration.cluster_id) .applies_to(device_mock.manufacturer, device_mock.model) .add_to_registry() ) quirked = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) assert ( quirked.endpoints[1].in_clusters.get(PowerConfiguration.cluster_id) is not None ) async def test_quirks_v2_signature_match(device_mock): """Test the signature_matches filter.""" registry = DeviceRegistry() signature_no_match = { SIG_MODELS_INFO: (("manufacturer", "model"),), SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: 260, SIG_EP_TYPE: 255, SIG_EP_INPUT: [3], } }, } ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .filter(signature_matches(signature_no_match)) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) quirked = registry.get_device(device_mock) assert not isinstance(quirked, CustomDeviceV2) async def test_quirks_v2_multiple_matches_not_raises(device_mock): """Test that adding multiple quirks v2 entries for the same device doesn't raise. When the quirk is EXACTLY the same the semantics of sets prevents us from having multiple quirks in the registry. """ registry = DeviceRegistry() entry1 = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) entry2 = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) assert entry1 == entry2 assert entry1 != registry assert isinstance(registry.get_device(device_mock), CustomDeviceV2) async def test_quirks_v2_with_custom_device_class(device_mock): """Test adding a quirk with a custom device class to the registry.""" registry = DeviceRegistry() class CustomTestDevice(CustomDeviceV2): """Custom test device for testing quirks v2.""" ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .device_class(CustomTestDevice) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) assert isinstance(registry.get_device(device_mock), CustomTestDevice) async def test_quirks_v2_with_node_descriptor(device_mock): """Test adding a quirk with an overridden node descriptor to the registry.""" registry = DeviceRegistry() node_descriptor = NodeDescriptor( logical_type=LogicalType.Router, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=NodeDescriptor.FrequencyBand.Freq2400MHz, mac_capability_flags=NodeDescriptor.MACCapabilityFlags.AllocateAddress, manufacturer_code=4174, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=0, maximum_outgoing_transfer_size=82, descriptor_capability_field=NodeDescriptor.DescriptorCapability.NONE, ) assert device_mock.node_desc != node_descriptor ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .node_descriptor(node_descriptor) .add_to_registry() ) quirked: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) assert quirked.node_desc == node_descriptor async def test_quirks_v2_replace_occurrences(device_mock): """Test adding a quirk that replaces all occurrences of a cluster.""" registry = DeviceRegistry() device_mock[1].add_output_cluster(Identify.cluster_id) device_mock.add_endpoint(2) device_mock[2].profile_id = 255 device_mock[2].device_type = 255 device_mock[2].add_input_cluster(Identify.cluster_id) device_mock.add_endpoint(3) device_mock[3].profile_id = 255 device_mock[3].device_type = 255 device_mock[3].add_output_cluster(Identify.cluster_id) class CustomIdentifyCluster(CustomCluster, Identify): """Custom identify cluster for testing quirks v2.""" ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .replace_cluster_occurrences(CustomIdentifyCluster) .add_to_registry() ) quirked: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) assert isinstance( quirked.endpoints[1].in_clusters[Identify.cluster_id], CustomIdentifyCluster ) assert isinstance( quirked.endpoints[1].out_clusters[Identify.cluster_id], CustomIdentifyCluster ) assert isinstance( quirked.endpoints[2].in_clusters[Identify.cluster_id], CustomIdentifyCluster ) assert isinstance( quirked.endpoints[3].out_clusters[Identify.cluster_id], CustomIdentifyCluster ) async def test_quirks_v2_skip_configuration(device_mock): """Test adding a quirk that skips configuration to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .skip_configuration() .add_to_registry() ) quirked: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) assert quirked.skip_configuration is True async def test_quirks_v2_removes(device_mock): """Test adding a quirk that removes a cluster to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .removes(Identify.cluster_id) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(Identify.cluster_id) is None async def test_quirks_v2_endpoints(device_mock): """Test adding a quirk that modifies endpoints to the registry.""" registry = DeviceRegistry() device_mock[1].add_output_cluster(Identify.cluster_id) device_mock.add_endpoint(2) device_mock[2].profile_id = 255 device_mock[2].device_type = 255 device_mock[2].add_input_cluster(Identify.cluster_id) device_mock[2].add_output_cluster(OnOff.cluster_id) device_mock.add_endpoint(3) device_mock[3].profile_id = 255 device_mock[3].device_type = 255 device_mock[3].add_input_cluster(Identify.cluster_id) device_mock[3].add_output_cluster(OnOff.cluster_id) ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds_endpoint(1, profile_id=260, device_type=260) # 1 not modified .removes_endpoint(2) .replaces_endpoint(3, profile_id=260, device_type=260) .adds_endpoint(4) .adds(OnOff.cluster_id, endpoint_id=4) .replaces_endpoint(5) .add_to_registry() ) quirked: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) # verify endpoint 1 was not modified, as it already existed before assert 1 in quirked.endpoints assert quirked.endpoints[1].profile_id == 255 assert quirked.endpoints[1].device_type == 255 # verify endpoint 2 was removed assert 2 not in quirked.endpoints # verify endpoint 3 profile id and device type were replaced assert 3 in quirked.endpoints assert quirked.endpoints[3].profile_id == 260 assert quirked.endpoints[3].device_type == 260 # verify original clusters still exist on endpoint 3 where id and type were replaced assert quirked.endpoints[3].in_clusters.get(Identify.cluster_id) is not None assert quirked.endpoints[3].out_clusters.get(OnOff.cluster_id) is not None # verify endpoint 4 was added with default profile id and device type using adds assert 4 in quirked.endpoints assert quirked.endpoints[4].profile_id == 260 assert quirked.endpoints[4].device_type == 255 # verify cluster was added to endpoint 4 assert quirked.endpoints[4].in_clusters.get(OnOff.cluster_id) is not None # verify endpoint 5 was added with default profile id and device type using replaces assert 5 in quirked.endpoints assert quirked.endpoints[5].profile_id == 260 assert quirked.endpoints[5].device_type == 255 async def test_quirks_v2_processing_order(device_mock): """Test quirks v2 metadata processing order.""" registry = DeviceRegistry() device_mock.add_endpoint(2) device_mock[2].add_input_cluster(Identify.cluster_id) device_mock[2].add_output_cluster(OnOff.cluster_id) device_mock.add_endpoint(3) device_mock[3].add_input_cluster(Identify.cluster_id) device_mock[3].add_output_cluster(OnOff.cluster_id) class TestCustomIdentifyCluster(CustomCluster, Identify): """Custom identify cluster for testing quirks v2.""" # the order of operations in the quirk builder below barely matters, # but is laid out in a way that generally follows the expected execution order ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .removes_endpoint(2) # wipes device reported clusters from endpoint 2 .adds_endpoint(2) # adds a new "blank" endpoint 2 with no clusters .removes(Identify.cluster_id, endpoint_id=3) # test removing cluster .adds(TestCustomIdentifyCluster, endpoint_id=3) # then "replacing" it by adds .adds(LevelControl.cluster_id, endpoint_id=2) # adds one custom cluster to ep 2 .add_to_registry() ) quirked: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked, CustomDeviceV2) # verify endpoint 2 was removed and a new one added with device clusters removed assert 2 in quirked.endpoints assert quirked.endpoints[2].in_clusters.get(Identify.cluster_id) is None assert quirked.endpoints[2].out_clusters.get(OnOff.cluster_id) is None # verify endpoint 2 cluster added by quirk is present though assert quirked.endpoints[2].in_clusters.get(LevelControl.cluster_id) is not None # verify endpoint 3 cluster was replaced by alternatively using removes and adds # instead of just using replaces directly assert 3 in quirked.endpoints assert isinstance( quirked.endpoints[3].in_clusters[Identify.cluster_id], TestCustomIdentifyCluster ) async def test_quirks_v2_apply_custom_configuration(device_mock): """Test adding a quirk custom configuration to the registry.""" registry = DeviceRegistry() class CustomOnOffCluster(CustomCluster, OnOff): """Custom on off cluster for testing quirks v2.""" ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(CustomOnOffCluster) .adds(CustomOnOffCluster, cluster_type=ClusterType.Client) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) # pylint: disable=line-too-long quirked_cluster: CustomOnOffCluster = quirked_device.endpoints[1].in_clusters[ CustomOnOffCluster.cluster_id ] assert isinstance(quirked_cluster, CustomOnOffCluster) # verify server cluster type was set when adding assert quirked_cluster.cluster_type == ClusterType.Server quirked_cluster.apply_custom_configuration = AsyncMock() quirked_client_cluster: CustomOnOffCluster = quirked_device.endpoints[ 1 ].out_clusters[CustomOnOffCluster.cluster_id] assert isinstance(quirked_client_cluster, CustomOnOffCluster) # verify client cluster type was set when adding assert quirked_client_cluster.cluster_type == ClusterType.Client quirked_client_cluster.apply_custom_configuration = AsyncMock() await quirked_device.apply_custom_configuration() assert quirked_cluster.apply_custom_configuration.await_count == 1 assert quirked_client_cluster.apply_custom_configuration.await_count == 1 async def test_quirks_v2_sensor(device_mock): """Test adding a quirk that defines a sensor to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .sensor( OnOff.AttributeDefs.on_time.name, OnOff.cluster_id, translation_key="on_time", fallback_name="On time", suggested_display_precision=0, ) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(OnOff.cluster_id) is not None # pylint: disable=line-too-long sensor_metadata: EntityMetadata = quirked_device.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ][0] assert sensor_metadata.entity_type == EntityType.STANDARD assert sensor_metadata.entity_platform == EntityPlatform.SENSOR assert sensor_metadata.cluster_id == OnOff.cluster_id assert sensor_metadata.endpoint_id == 1 assert sensor_metadata.cluster_type == ClusterType.Server assert isinstance(sensor_metadata, ZCLSensorMetadata) assert sensor_metadata.attribute_name == OnOff.AttributeDefs.on_time.name assert sensor_metadata.divisor == 1 assert sensor_metadata.multiplier == 1 assert sensor_metadata.suggested_display_precision == 0 async def test_quirks_v2_sensor_validation_failure_no_translation_key(device_mock): """Test translation key and device class both not set causes exception.""" registry = DeviceRegistry() with pytest.raises(ValueError, match="must have a translation_key or device_class"): ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .sensor( OnOff.AttributeDefs.on_time.name, OnOff.cluster_id, fallback_name="On time", ) .add_to_registry() ) async def test_quirks_v2_switch(device_mock): """Test adding a quirk that defines a switch to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .switch( OnOff.AttributeDefs.on_time.name, OnOff.cluster_id, force_inverted=True, invert_attribute_name=OnOff.AttributeDefs.off_wait_time.name, translation_key="on_time", fallback_name="On time", ) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(OnOff.cluster_id) is not None switch_metadata: EntityMetadata = quirked_device.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ][0] assert switch_metadata.entity_type == EntityType.CONFIG assert switch_metadata.entity_platform == EntityPlatform.SWITCH assert switch_metadata.cluster_id == OnOff.cluster_id assert switch_metadata.endpoint_id == 1 assert switch_metadata.cluster_type == ClusterType.Server assert isinstance(switch_metadata, SwitchMetadata) assert switch_metadata.attribute_name == OnOff.AttributeDefs.on_time.name assert switch_metadata.force_inverted is True assert ( switch_metadata.invert_attribute_name == OnOff.AttributeDefs.off_wait_time.name ) async def test_quirks_v2_number(device_mock): """Test adding a quirk that defines a number to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .number( OnOff.AttributeDefs.on_time.name, OnOff.cluster_id, min_value=0, max_value=100, step=1, unit=UnitOfTime.SECONDS, translation_key="on_time", fallback_name="On time", ) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(OnOff.cluster_id) is not None # pylint: disable=line-too-long number_metadata: EntityMetadata = quirked_device.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ][0] assert number_metadata.entity_type == EntityType.CONFIG assert number_metadata.entity_platform == EntityPlatform.NUMBER assert number_metadata.cluster_id == OnOff.cluster_id assert number_metadata.endpoint_id == 1 assert number_metadata.cluster_type == ClusterType.Server assert isinstance(number_metadata, NumberMetadata) assert number_metadata.attribute_name == OnOff.AttributeDefs.on_time.name assert number_metadata.min == 0 assert number_metadata.max == 100 assert number_metadata.step == 1 assert number_metadata.unit == "s" assert number_metadata.mode is None assert number_metadata.multiplier is None async def test_quirks_v2_binary_sensor(device_mock): """Test adding a quirk that defines a binary sensor to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .binary_sensor( OnOff.AttributeDefs.on_off.name, OnOff.cluster_id, translation_key="on_off", fallback_name="On/off", ) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(OnOff.cluster_id) is not None # pylint: disable=line-too-long binary_sensor_metadata: EntityMetadata = quirked_device.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ][0] assert binary_sensor_metadata.entity_type == EntityType.DIAGNOSTIC assert binary_sensor_metadata.entity_platform == EntityPlatform.BINARY_SENSOR assert binary_sensor_metadata.cluster_id == OnOff.cluster_id assert binary_sensor_metadata.endpoint_id == 1 assert binary_sensor_metadata.cluster_type == ClusterType.Server assert isinstance(binary_sensor_metadata, BinarySensorMetadata) assert binary_sensor_metadata.attribute_name == OnOff.AttributeDefs.on_off.name async def test_quirks_v2_write_attribute_button(device_mock): """Test adding a quirk that defines a write attr button to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .write_attr_button( OnOff.AttributeDefs.on_time.name, 20, OnOff.cluster_id, translation_key="on_time", fallback_name="On time", ) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(OnOff.cluster_id) is not None # pylint: disable=line-too-long write_attribute_button: EntityMetadata = quirked_device.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ][0] assert write_attribute_button.entity_type == EntityType.CONFIG assert write_attribute_button.entity_platform == EntityPlatform.BUTTON assert write_attribute_button.cluster_id == OnOff.cluster_id assert write_attribute_button.endpoint_id == 1 assert write_attribute_button.cluster_type == ClusterType.Server assert isinstance(write_attribute_button, WriteAttributeButtonMetadata) assert write_attribute_button.attribute_name == OnOff.AttributeDefs.on_time.name assert write_attribute_button.attribute_value == 20 async def test_quirks_v2_command_button(device_mock): """Test adding a quirk that defines a command button to the registry.""" registry = DeviceRegistry() ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .command_button( OnOff.ServerCommandDefs.on_with_timed_off.name, OnOff.cluster_id, command_kwargs={"on_off_control": OnOff.OnOffControl.Accept_Only_When_On}, translation_key="on_with_timed_off", fallback_name="On with timed off", ) .command_button( OnOff.ServerCommandDefs.on_with_timed_off.name, OnOff.cluster_id, command_kwargs={ "on_off_control_foo": OnOff.OnOffControl.Accept_Only_When_On }, translation_key="on_with_timed_off", fallback_name="On with timed off", ) .command_button( OnOff.ServerCommandDefs.on_with_timed_off.name, OnOff.cluster_id, translation_key="on_with_timed_off", fallback_name="On with timed off", ) .add_to_registry() ) quirked_device: CustomDeviceV2 = registry.get_device(device_mock) assert isinstance(quirked_device, CustomDeviceV2) assert quirked_device.endpoints[1].in_clusters.get(OnOff.cluster_id) is not None button: EntityMetadata = quirked_device.exposes_metadata[ (1, OnOff.cluster_id, ClusterType.Server) ][0] assert button.entity_type == EntityType.CONFIG assert button.entity_platform == EntityPlatform.BUTTON assert button.cluster_id == OnOff.cluster_id assert button.endpoint_id == 1 assert button.cluster_type == ClusterType.Server assert isinstance(button, ZCLCommandButtonMetadata) assert button.command_name == OnOff.ServerCommandDefs.on_with_timed_off.name assert len(button.kwargs) == 1 assert button.kwargs["on_off_control"] == OnOff.OnOffControl.Accept_Only_When_On # coverage for overridden eq method assert ( button != quirked_device.exposes_metadata[(1, OnOff.cluster_id, ClusterType.Server)][1] ) assert button != quirked_device button = quirked_device.exposes_metadata[(1, OnOff.cluster_id, ClusterType.Server)][ 2 ] assert button.kwargs == {} assert button.args == () async def test_quirks_v2_also_applies_to(device_mock): """Test adding the same quirk for multiple manufacturers and models.""" registry = DeviceRegistry() class CustomTestDevice(CustomDeviceV2): """Custom test device for testing quirks v2.""" ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .also_applies_to("manufacturer2", "model2") .also_applies_to("manufacturer3", "model3") .device_class(CustomTestDevice) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) assert isinstance(registry.get_device(device_mock), CustomTestDevice) device_mock.manufacturer = "manufacturer2" device_mock.model = "model2" assert isinstance(registry.get_device(device_mock), CustomTestDevice) device_mock.manufacturer = "manufacturer3" device_mock.model = "model3" assert isinstance(registry.get_device(device_mock), CustomTestDevice) async def test_quirks_v2_with_custom_device_class_raises(device_mock): """Test adding a quirk with a custom device class to the registry raises if the class is not a subclass of CustomDeviceV2. """ registry = DeviceRegistry() class CustomTestDevice(CustomDevice): """Custom test device for testing quirks v2.""" with pytest.raises( AssertionError, match="is not a subclass of CustomDeviceV2", ): ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .device_class(CustomTestDevice) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, ) .add_to_registry() ) async def test_quirks_v2_matches_v1(app_mock): """Test that quirks v2 entries are equivalent to quirks v1.""" registry = DeviceRegistry() class PowerConfig1CRCluster(CustomCluster, PowerConfiguration): """Updating power attributes: 1 CR2032.""" _CONSTANT_ATTRIBUTES = { PowerConfiguration.AttributeDefs.battery_size.id: 10, PowerConfiguration.AttributeDefs.battery_quantity.id: 1, PowerConfiguration.AttributeDefs.battery_rated_voltage.id: 30, } class ScenesCluster(CustomCluster, Scenes): """Ikea Scenes cluster.""" server_commands = Scenes.server_commands.copy() server_commands.update( { 0x0007: ZCLCommandDef( "press", {"param1": t.int16s, "param2": t.int8s, "param3": t.int8s}, False, is_manufacturer_specific=True, ), 0x0008: ZCLCommandDef( "hold", {"param1": t.int16s, "param2": t.int8s}, False, is_manufacturer_specific=True, ), 0x0009: ZCLCommandDef( "release", { "param1": t.int16s, }, False, is_manufacturer_specific=True, ), } ) # pylint: disable=invalid-name SHORT_PRESS = "remote_button_short_press" TURN_ON = "turn_on" COMMAND = "command" COMMAND_RELEASE = "release" COMMAND_TOGGLE = "toggle" CLUSTER_ID = "cluster_id" ENDPOINT_ID = "endpoint_id" PARAMS = "params" LONG_PRESS = "remote_button_long_press" triggers = { (SHORT_PRESS, TURN_ON): { COMMAND: COMMAND_TOGGLE, CLUSTER_ID: 6, ENDPOINT_ID: 1, }, (LONG_PRESS, TURN_ON): { COMMAND: COMMAND_RELEASE, CLUSTER_ID: 5, ENDPOINT_ID: 1, PARAMS: {"param1": 0}, }, } class IkeaTradfriRemote3(CustomDevice): """Custom device representing variation of IKEA five button remote.""" signature = { # SIG_MODELS_INFO: [("IKEA of Sweden", "TRADFRI remote control")], SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, SIG_EP_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER, SIG_EP_INPUT: [ Basic.cluster_id, PowerConfiguration.cluster_id, Identify.cluster_id, Alarms.cluster_id, Diagnostic.cluster_id, LightLink.cluster_id, ], SIG_EP_OUTPUT: [ Identify.cluster_id, Groups.cluster_id, Scenes.cluster_id, OnOff.cluster_id, LevelControl.cluster_id, Ota.cluster_id, LightLink.cluster_id, ], } }, } replacement = { SIG_ENDPOINTS: { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, SIG_EP_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER, SIG_EP_INPUT: [ Basic.cluster_id, PowerConfig1CRCluster, Identify.cluster_id, Alarms.cluster_id, LightLink.cluster_id, ], SIG_EP_OUTPUT: [ Identify.cluster_id, Groups.cluster_id, ScenesCluster, OnOff.cluster_id, LevelControl.cluster_id, Ota.cluster_id, LightLink.cluster_id, ], } } } device_automation_triggers = triggers ieee = sentinel.ieee nwk = 0x2233 ikea_device = Device(app_mock, ieee, nwk) ikea_device.add_endpoint(1) ikea_device[1].profile_id = zha.PROFILE_ID ikea_device[1].device_type = zha.DeviceType.COLOR_SCENE_CONTROLLER ikea_device.model = "TRADFRI remote control" ikea_device.manufacturer = "IKEA of Sweden" ikea_device[1].add_input_cluster(Basic.cluster_id) ikea_device[1].add_input_cluster(PowerConfiguration.cluster_id) ikea_device[1].add_input_cluster(Identify.cluster_id) ikea_device[1].add_input_cluster(Alarms.cluster_id) ikea_device[1].add_input_cluster(Diagnostic.cluster_id) ikea_device[1].add_input_cluster(LightLink.cluster_id) ikea_device[1].add_output_cluster(Identify.cluster_id) ikea_device[1].add_output_cluster(Groups.cluster_id) ikea_device[1].add_output_cluster(Scenes.cluster_id) ikea_device[1].add_output_cluster(OnOff.cluster_id) ikea_device[1].add_output_cluster(LevelControl.cluster_id) ikea_device[1].add_output_cluster(Ota.cluster_id) ikea_device[1].add_output_cluster(LightLink.cluster_id) registry.add_to_registry(IkeaTradfriRemote3) quirked = registry.get_device(ikea_device) assert isinstance(quirked, IkeaTradfriRemote3) registry = DeviceRegistry() ( QuirkBuilder(ikea_device.manufacturer, ikea_device.model, registry=registry) .replaces(PowerConfig1CRCluster) .replaces(ScenesCluster, cluster_type=ClusterType.Client) .device_automation_triggers(triggers) .add_to_registry() ) quirked_v2 = registry.get_device(ikea_device) assert isinstance(quirked_v2, CustomDeviceV2) assert len(quirked_v2.endpoints[1].in_clusters) == 6 assert len(quirked_v2.endpoints[1].out_clusters) == 7 assert isinstance( quirked_v2.endpoints[1].in_clusters[PowerConfig1CRCluster.cluster_id], PowerConfig1CRCluster, ) assert isinstance( quirked_v2.endpoints[1].out_clusters[ScenesCluster.cluster_id], ScenesCluster ) for cluster_id, cluster in quirked.endpoints[1].in_clusters.items(): assert isinstance( quirked_v2.endpoints[1].in_clusters[cluster_id], type(cluster) ) for cluster_id, cluster in quirked.endpoints[1].out_clusters.items(): assert isinstance( quirked_v2.endpoints[1].out_clusters[cluster_id], type(cluster) ) assert quirked.device_automation_triggers == quirked_v2.device_automation_triggers async def test_quirks_v2_add_to_registry_v2_logs_error(caplog): """Test adding a quirk with old API logs.""" registry = DeviceRegistry() ( add_to_registry_v2("foo", "bar", registry=registry) .adds(OnOff.cluster_id) .binary_sensor( OnOff.AttributeDefs.on_off.name, OnOff.cluster_id, translation_key="on_off", fallback_name="On/off", ) .add_to_registry() ) assert ( "add_to_registry_v2 is deprecated and will be removed in a future release" in caplog.text ) async def test_quirks_v2_friendly_name(device_mock: Device) -> None: registry = DeviceRegistry() entry = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .friendly_name(model="Real Model Name", manufacturer="Real Manufacturer") .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) assert entry.friendly_name is not None assert entry.friendly_name.model == "Real Model Name" assert entry.friendly_name.manufacturer == "Real Manufacturer" async def test_quirks_v2_no_friendly_name(device_mock: Device) -> None: registry = DeviceRegistry() entry = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(Basic.cluster_id) .adds(OnOff.cluster_id) .enum( OnOff.AttributeDefs.start_up_on_off.name, OnOff.StartUpOnOff, OnOff.cluster_id, translation_key="start_up_on_off", fallback_name="Start up on/off", ) .add_to_registry() ) assert entry.friendly_name is None async def test_quirks_v2_device_alerts(device_mock: Device) -> None: registry = DeviceRegistry() entry = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .device_alert(level="warning", message="This device has routing problems.") .device_alert( level="error", message="This device irreparably crashes the mesh." ) .add_to_registry() ) assert entry.device_alerts == ( DeviceAlertMetadata( level=DeviceAlertLevel.WARNING, message="This device has routing problems.", ), DeviceAlertMetadata( level=DeviceAlertLevel.ERROR, message="This device irreparably crashes the mesh.", ), ) async def test_quirks_v2_disable_entity_creation(device_mock: Device) -> None: registry = DeviceRegistry() def filter_func(entity) -> bool: return True entry = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .prevent_default_entity_creation(endpoint_id=1, unique_id_suffix="something") .prevent_default_entity_creation(endpoint_id=1, cluster_id=OnOff.cluster_id) .prevent_default_entity_creation( endpoint_id=1, cluster_id=OnOff.cluster_id, cluster_type=ClusterType.Client ) .prevent_default_entity_creation(function=filter_func) .add_to_registry() ) assert entry.disabled_default_entities == ( PreventDefaultEntityCreationMetadata( endpoint_id=1, cluster_id=None, cluster_type=None, unique_id_suffix="something", function=None, ), PreventDefaultEntityCreationMetadata( endpoint_id=1, cluster_id=OnOff.cluster_id, cluster_type=ClusterType.Server, # by default unique_id_suffix=None, function=None, ), PreventDefaultEntityCreationMetadata( endpoint_id=1, cluster_id=OnOff.cluster_id, cluster_type=ClusterType.Client, unique_id_suffix=None, function=None, ), PreventDefaultEntityCreationMetadata( endpoint_id=None, cluster_id=None, cluster_type=None, unique_id_suffix=None, function=filter_func, ), ) async def test_quirks_v2_primary_entity(device_mock: Device) -> None: registry = DeviceRegistry() builder = ( QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry) .adds(OnOff.cluster_id) .switch( OnOff.AttributeDefs.on_time.name, OnOff.cluster_id, force_inverted=True, invert_attribute_name=OnOff.AttributeDefs.off_wait_time.name, translation_key="on_time", fallback_name="On time", primary=True, ) ) with pytest.raises(ValueError): # Having a second primary entity is not allowed builder.sensor( OnOff.AttributeDefs.on_time.name, OnOff.cluster_id, translation_key="on_time", fallback_name="On time", primary=True, ) entry = builder.add_to_registry() assert len(entry.entity_metadata) == 1 assert entry.entity_metadata[0].primary is True zigpy-0.80.1/tests/test_serial.py000066400000000000000000000126451501451476000170070ustar00rootroot00000000000000from __future__ import annotations import asyncio import fcntl import pathlib from unittest.mock import AsyncMock, Mock, call, patch import pytest import zigpy.serial from zigpy.typing import UNDEFINED, UndefinedType # fmt: off @pytest.mark.parametrize(("url", "flow_control", "xonxoff", "rtscts", "expected_kwargs"), [ # `flow_control` on its own ("/dev/ttyUSB1", "hardware", UNDEFINED, UNDEFINED, {"xonxoff": False, "rtscts": True}), ("/dev/ttyUSB1", "software", UNDEFINED, UNDEFINED, {"xonxoff": True, "rtscts": False}), ("/dev/ttyUSB1", None, UNDEFINED, UNDEFINED, {"xonxoff": False, "rtscts": False}), # `flow_control` overrides `xonxoff` and `rtscts` ("/dev/ttyUSB1", "hardware", True, False, {"xonxoff": False, "rtscts": True}), ("/dev/ttyUSB1", "software", False, True, {"xonxoff": True, "rtscts": False}), ("/dev/ttyUSB1", None, True, False, {"xonxoff": False, "rtscts": False}), # `flow_control` defaults to undefined so `xonxoff` and `rtscts` are used ("/dev/ttyUSB1", UNDEFINED, True, False, {"xonxoff": True, "rtscts": False}), ("/dev/ttyUSB1", UNDEFINED, False, True, {"xonxoff": False, "rtscts": True}), ("/dev/ttyUSB1", UNDEFINED, True, True, {"xonxoff": True, "rtscts": True}), # The defaults are used when `flow_control`, `xonxoff`, and `rtscts` are all undefined ("/dev/ttyUSB1", UNDEFINED, UNDEFINED, UNDEFINED, {"xonxoff": False, "rtscts": False}), ]) # fmt: on async def test_serial_normal( url: str, flow_control: str | UndefinedType, xonxoff: bool | UndefinedType, rtscts: bool | UndefinedType, expected_kwargs: dict[str, bool], ) -> None: loop = asyncio.get_running_loop() protocol_factory = Mock() kwargs = {"url": url} if flow_control is not UNDEFINED: kwargs["flow_control"] = flow_control if xonxoff is not UNDEFINED: kwargs["xonxoff"] = xonxoff if rtscts is not UNDEFINED: kwargs["rtscts"] = rtscts with patch( "zigpy.serial.pyserial_asyncio.create_serial_connection", AsyncMock( return_value=(AsyncMock(), AsyncMock()) ), ) as mock_create_serial_connection: await zigpy.serial.create_serial_connection(loop, protocol_factory, **kwargs) mock_calls = mock_create_serial_connection.mock_calls assert len(mock_calls) == 1 assert mock_calls[0].kwargs["url"] == "/dev/ttyUSB1" assert mock_calls[0].kwargs["baudrate"] == 115200 for kwarg in expected_kwargs: assert mock_calls[0].kwargs[kwarg] == expected_kwargs[kwarg] async def test_serial_socket() -> None: loop = asyncio.get_running_loop() protocol_factory = Mock() with patch.object( loop, "create_connection", AsyncMock( return_value=(AsyncMock(), AsyncMock()) ), ): await zigpy.serial.create_serial_connection( loop, protocol_factory, "socket://1.2.3.4:5678" ) await zigpy.serial.create_serial_connection( loop, protocol_factory, "socket://1.2.3.4" ) assert len(loop.create_connection.mock_calls) == 2 assert loop.create_connection.mock_calls[0].kwargs["host"] == "1.2.3.4" assert loop.create_connection.mock_calls[0].kwargs["port"] == 5678 assert loop.create_connection.mock_calls[1].kwargs["host"] == "1.2.3.4" assert loop.create_connection.mock_calls[1].kwargs["port"] == 6638 async def test_pyserial_error_remapping(tmp_path: pathlib.Path) -> None: loop = asyncio.get_running_loop() protocol_factory = Mock() # FileNotFoundError missing_port = tmp_path / "missing" assert not missing_port.exists() with pytest.raises(FileNotFoundError): await zigpy.serial.create_serial_connection( loop, protocol_factory, url=missing_port ) # PermissionError denied_port = tmp_path / "denied" denied_port.touch() denied_port.chmod(0o000) with pytest.raises(PermissionError): await zigpy.serial.create_serial_connection( loop, protocol_factory, url=denied_port ) # IsADirectoryError a_folder = tmp_path / "a_folder" a_folder.mkdir() with pytest.raises(IsADirectoryError): await zigpy.serial.create_serial_connection( loop, protocol_factory, url=a_folder ) # Locked locked_port = tmp_path / "locked" with locked_port.open("w") as f: # Lock the file fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB) with pytest.raises( PermissionError, match="The serial port is locked by another application" ): await zigpy.serial.create_serial_connection( loop, protocol_factory, url=locked_port ) async def test_serial_protocol() -> None: class SampleSerialProtocol(zigpy.serial.SerialProtocol): pass loop = asyncio.get_running_loop() protocol = SampleSerialProtocol() transport = Mock() loop.call_soon(protocol.connection_made, transport) # Connect await protocol.wait_until_connected() # Receive some data protocol.data_received(b"Hello") protocol.data_received(b" ") protocol.data_received(b"world") assert protocol._buffer == b"Hello world" # Close the transport asyncio.get_event_loop().call_soon(protocol.connection_lost, None) await protocol.disconnect() assert transport.close.mock_calls == [call()] zigpy-0.80.1/tests/test_struct.py000066400000000000000000000632601501451476000170530ustar00rootroot00000000000000from __future__ import annotations import enum from unittest import mock import pytest import zigpy.types as t from zigpy.zcl.foundation import Status import zigpy.zdo.types as zdo_t @pytest.fixture def expose_global(): """`typing.get_type_hints` does not work for types defined within functions""" objects = [] def inner(obj): assert obj.__name__ not in globals() globals()[obj.__name__] = obj objects.append(obj) return obj yield inner for obj in objects: del globals()[obj.__name__] def test_enum_fields(): class EnumNamed(t.enum8): NAME1 = 0x01 NAME2 = 0x10 assert EnumNamed("0x01") == EnumNamed.NAME1 assert EnumNamed("1") == EnumNamed.NAME1 assert EnumNamed("0x10") == EnumNamed.NAME2 assert EnumNamed("16") == EnumNamed.NAME2 assert EnumNamed("NAME1") == EnumNamed.NAME1 assert EnumNamed("NAME2") == EnumNamed.NAME2 assert EnumNamed("EnumNamed.NAME1") == EnumNamed.NAME1 assert EnumNamed("EnumNamed.NAME2") == EnumNamed.NAME2 def test_struct_fields(): class TestStruct(t.Struct): a: t.uint8_t b: t.uint16_t assert TestStruct.fields.a.name == "a" assert TestStruct.fields.a.type == t.uint8_t assert TestStruct.fields.b.name == "b" assert TestStruct.fields.b.type == t.uint16_t def test_struct_subclass_creation(): # In-class constants are allowed class TestStruct3(t.Struct): CONSTANT1: int = 123 CONSTANT2 = 1234 _private1: int = 456 _private2 = 4567 _PRIVATE_CONST = mock.sentinel.priv_const class Test: pass assert not TestStruct3.fields assert TestStruct3.CONSTANT1 == 123 assert TestStruct3.CONSTANT2 == 1234 assert TestStruct3._private1 == 456 assert TestStruct3._private2 == 4567 assert TestStruct3._PRIVATE_CONST is mock.sentinel.priv_const assert TestStruct3()._PRIVATE_CONST is mock.sentinel.priv_const assert TestStruct3.Test # type: ignore[truthy-function] assert TestStruct3().Test assert "Test" not in TestStruct3().as_dict() # Still valid class TestStruct4(t.Struct): pass # Annotations with values are not fields class TestStruct5(t.Struct): a: t.uint8_t = 2 # not a field b: t.uint16_t # is a field inst6 = TestStruct5(123) assert "a" not in inst6.as_dict() assert "b" in inst6.as_dict() # unless they are a StructField class TestStruct6(t.Struct): a: t.uint8_t = t.StructField() assert "a" in TestStruct6(2).as_dict() def test_struct_construction(): class TestStruct(t.Struct): a: t.uint8_t b: t.LVBytes s1 = TestStruct(a=1) s1.b = b"foo" s2 = TestStruct(a=1, b=b"foo") assert s1 == s2 assert s1.a == s2.a assert s1.replace(b=b"foo") == s2.replace(b=b"foo") assert s1.serialize() == s2.serialize() == b"\x01\x03foo" assert TestStruct(s1) == s1 # You cannot use the copy constructor with other keyword arguments with pytest.raises(ValueError): TestStruct(s1, b=b"foo") # Types are coerced on construction so you cannot pass bad values with pytest.raises(ValueError): TestStruct(a=object()) # You can still assign bad values but serialization will fail s1.serialize() s1.b = object() with pytest.raises(ValueError): s1.serialize() def test_nested_structs(expose_global): class OuterStruct(t.Struct): class InnerStruct(t.Struct): b: t.uint8_t c: t.uint8_t a: t.uint8_t inner: None = t.StructField(type=InnerStruct) d: t.uint8_t assert len(OuterStruct.fields) == 3 assert OuterStruct.fields.a.type is t.uint8_t assert OuterStruct.fields.inner.type is OuterStruct.InnerStruct assert len(OuterStruct.fields.inner.type.fields) == 2 assert OuterStruct.fields.d.type is t.uint8_t s, remaining = OuterStruct.deserialize(b"\x00\x01\x02\x03" + b"asd") assert remaining == b"asd" assert s.a == 0 assert s.inner.b == 1 assert s.inner.c == 2 assert s.d == 3 def test_nested_structs2(expose_global): class OuterStruct(t.Struct): class InnerStruct(t.Struct): b: t.uint8_t c: t.uint8_t a: t.uint8_t inner: None = t.StructField(type=InnerStruct) d: t.uint8_t assert len(OuterStruct.fields) == 3 assert OuterStruct.fields[0].type is t.uint8_t assert OuterStruct.fields[1].type is OuterStruct.InnerStruct assert len(OuterStruct.fields[1].type.fields) == 2 assert OuterStruct.fields[2].type is t.uint8_t s, remaining = OuterStruct.deserialize(b"\x00\x01\x02\x03" + b"asd") assert remaining == b"asd" assert s.a == 0 assert s.inner.b == 1 assert s.inner.c == 2 assert s.d == 3 def test_struct_init(): class TestStruct(t.Struct): a: t.uint8_t b: t.uint16_t c: t.CharacterString ts = TestStruct(a=1, b=0x0100, c="TestStruct") assert repr(ts) assert isinstance(ts.a, t.uint8_t) assert isinstance(ts.b, t.uint16_t) assert isinstance(ts.c, t.CharacterString) assert ts.a == 1 assert ts.b == 0x100 assert ts.c == "TestStruct" ts2, remaining = TestStruct.deserialize(b"\x01\x00\x01\x0aTestStruct") assert not remaining assert ts == ts2 assert ts.serialize() == ts2.serialize() ts3 = ts2.replace(b=0x0100) assert ts3 == ts2 assert ts3.serialize() == ts2.serialize() ts4 = ts2.replace(b=0x0101) assert ts4 != ts2 assert ts4.serialize() != ts2.serialize() def test_struct_string_is_none(): class TestStruct(t.Struct): a: t.CharacterString # str(None) == "None", which is bad with pytest.raises(ValueError): TestStruct(a=None).serialize() def test_struct_field_dependencies(): class TestStruct(t.Struct): foo: t.uint8_t status: Status bar: t.uint8_t = t.StructField(requires=lambda s: s.status == Status.SUCCESS) baz: t.uint8_t # Status is FAILURE so bar is not defined TestStruct(foo=1, status=Status.FAILURE, baz=2) ts1, remaining = TestStruct.deserialize( b"\x01" + Status.SUCCESS.serialize() + b"\x02\x03" ) assert not remaining assert ts1 == TestStruct(foo=1, status=Status.SUCCESS, bar=2, baz=3) ts2, remaining = TestStruct.deserialize( b"\x01" + Status.FAILURE.serialize() + b"\x02\x03" ) assert remaining == b"\x03" assert ts2 == TestStruct(foo=1, status=Status.FAILURE, bar=None, baz=2) def test_struct_field_invalid_dependencies(): class TestStruct(t.Struct): status: t.uint8_t value: t.uint8_t = t.StructField(requires=lambda s: s.status == 0x00) # Value will be ignored during serialization even though it has been assigned ts1 = TestStruct(status=0x01, value=0x02) assert ts1.serialize() == b"\x01" assert len(ts1.assigned_fields()) == 1 # Value wasn't provided but it is required ts2 = TestStruct(status=0x00, value=None) assert len(ts1.assigned_fields()) == 1 with pytest.raises(ValueError): ts2.serialize() # Value is not optional but doesn't need to be passed due to dependencies ts3 = TestStruct(status=0x01) assert ts3.serialize() == b"\x01" assert len(ts3.assigned_fields()) == 1 def test_struct_multiple_requires(expose_global): @expose_global class StrictStatus(t.enum8): SUCCESS = 0x00 FAILURE = 0x01 # Missing members cause a parsing failure _missing_ = enum.Enum._missing_ class TestStruct(t.Struct): foo: t.uint8_t status1: StrictStatus value1: t.uint8_t = t.StructField( requires=lambda s: s.status1 == StrictStatus.SUCCESS ) status2: StrictStatus value2: t.uint8_t = t.StructField( requires=lambda s: s.status2 == StrictStatus.SUCCESS ) # status1: success, status2: success ts0, remaining = TestStruct.deserialize( b"\x00" + StrictStatus.SUCCESS.serialize() + b"\x01" + StrictStatus.SUCCESS.serialize() + b"\x02" ) assert not remaining assert ts0 == TestStruct( foo=0, status1=StrictStatus.SUCCESS, value1=1, status2=StrictStatus.SUCCESS, value2=2, ) # status1: failure, status2: success ts1, remaining = TestStruct.deserialize( b"\x00" + StrictStatus.FAILURE.serialize() + StrictStatus.SUCCESS.serialize() + b"\x02" ) assert not remaining assert ts1 == TestStruct( foo=0, status1=StrictStatus.FAILURE, status2=StrictStatus.SUCCESS, value2=2 ) # status1: success, status2: failure, trailing ts2, remaining = TestStruct.deserialize( b"\x00" + StrictStatus.SUCCESS.serialize() + b"\x01" + StrictStatus.FAILURE.serialize() + b"\x02" ) assert remaining == b"\x02" assert ts2 == TestStruct( foo=0, status1=StrictStatus.SUCCESS, value1=1, status2=StrictStatus.FAILURE ) # status1: failure, status2: failure ts3, remaining = TestStruct.deserialize( b"\x00" + StrictStatus.FAILURE.serialize() + StrictStatus.FAILURE.serialize() ) assert not remaining assert ts3 == TestStruct( foo=0, status1=StrictStatus.FAILURE, status2=StrictStatus.FAILURE ) with pytest.raises(ValueError): # status1: failure TestStruct.deserialize(b"\x00" + StrictStatus.FAILURE.serialize()) with pytest.raises(ValueError): # status1: failure, invalid trailing TestStruct.deserialize(b"\x00" + StrictStatus.FAILURE.serialize() + b"\xff") def test_struct_equality(): class TestStruct1(t.Struct): foo: t.uint8_t class TestStruct2(t.Struct): foo: t.uint8_t assert TestStruct1() != TestStruct2() assert TestStruct1(foo=1) != TestStruct2(foo=1) assert TestStruct1() == TestStruct1() assert TestStruct1(foo=1) == TestStruct1(foo=1) @pytest.mark.parametrize( "data", [ b"\x00", b"\x00\x00", b"\x01", b"\x01\x00", b"\x01\x02\x03", b"", b"\x00\x00\x00\x00", ], ) def test_struct_subclass_extension(data): class TestStruct(t.Struct): foo: t.uint8_t class TestStructSubclass(TestStruct): bar: t.uint8_t = t.StructField(requires=lambda s: s.foo == 0x01) class TestCombinedStruct(t.Struct): foo: t.uint8_t bar: t.uint8_t = t.StructField(requires=lambda s: s.foo == 0x01) assert len(TestStructSubclass.fields) == 2 assert len(TestCombinedStruct.fields) == 2 error1 = None error2 = None try: ts1, remaining1 = TestStructSubclass.deserialize(data) except Exception as e: # noqa: BLE001 error1 = e try: ts2, remaining2 = TestCombinedStruct.deserialize(data) except Exception as e: # noqa: BLE001 error2 = e assert (error1 and error2) or (not error1 and not error2) if error1 or error2: assert repr(error1) == repr(error2) else: assert ts1.as_dict() == ts2.as_dict() assert remaining1 == remaining2 def test_optional_struct_special_case(): class TestStruct(t.Struct): foo: t.uint8_t OptionalTestStruct = t.Optional(TestStruct) assert OptionalTestStruct.deserialize(b"") == (None, b"") assert OptionalTestStruct.deserialize(b"\x00") == ( OptionalTestStruct(foo=0x00), b"", ) def test_conflicting_types(): class GoodStruct(t.Struct): foo: t.uint8_t = t.StructField(type=t.uint8_t) with pytest.raises(TypeError): class BadStruct(t.Struct): foo: t.uint8_t = t.StructField(type=t.uint16_t) def test_uppercase_field(): class Neighbor(t.Struct): """Neighbor Descriptor""" PanId: t.EUI64 IEEEAddr: t.EUI64 NWKAddr: t.NWK NeighborType: t.uint8_t PermitJoining: t.uint8_t Depth: t.uint8_t LQI: t.uint8_t # this should not be a constant assert len(Neighbor.fields) == 7 assert Neighbor.fields[6].name == "LQI" assert Neighbor.fields[6].type == t.uint8_t def test_non_annotated_field(): with pytest.raises(TypeError): class TestStruct1(t.Struct): field1: t.uint8_t # Python does not provide any simple way to get the order of both defined # class attributes and annotations. This is bad. field2 = t.StructField(type=t.uint16_t) field3: t.uint32_t class TestStruct2(t.Struct): field1: t.uint8_t field2: None = t.StructField(type=t.uint16_t) field3: t.uint32_t assert len(TestStruct2.fields) == 3 assert TestStruct2.fields[0] == t.StructField(name="field1", type=t.uint8_t) assert TestStruct2.fields[1] == t.StructField(name="field2", type=t.uint16_t) assert TestStruct2.fields[2] == t.StructField(name="field3", type=t.uint32_t) def test_allowed_non_fields(): class Other: def bar(self): return "bar" def foo2_(_): return "foo2" class TestStruct(t.Struct): @property def prop(self): return "prop" @prop.setter def prop(self, value): return foo1 = lambda _: "foo1" # noqa: E731 foo2 = foo2_ bar = Other.bar field: t.uint8_t CONSTANT1: t.uint8_t = "CONSTANT1" CONSTANT2 = "CONSTANT2" assert len(TestStruct.fields) == 1 assert TestStruct.CONSTANT1 == "CONSTANT1" assert TestStruct.CONSTANT2 == "CONSTANT2" assert TestStruct().prop == "prop" assert TestStruct().foo1() == "foo1" assert TestStruct().foo2() == "foo2" assert TestStruct().bar() == "bar" instance = TestStruct() instance.prop = None assert instance.prop == "prop" def test_as_dict_empty_fields(): class TestStruct(t.Struct): foo: t.uint8_t bar: t.uint8_t = t.StructField(requires=lambda s: s.foo == 0x01) assert TestStruct(foo=1, bar=2).as_dict() == {"foo": 1, "bar": 2} assert TestStruct(foo=0, bar=2).as_dict() == {"foo": 0, "bar": 2} assert TestStruct(foo=0).as_dict() == {"foo": 0, "bar": None} # Same thing as above but assigned as attributes ts1 = TestStruct() ts1.foo = 1 ts1.bar = 2 assert ts1.as_dict() == {"foo": 1, "bar": 2} ts2 = TestStruct() ts2.foo = 0 ts2.bar = 2 assert ts2.as_dict() == {"foo": 0, "bar": 2} ts3 = TestStruct() ts3.foo = 0 assert ts3.as_dict() == {"foo": 0, "bar": None} def test_no_types(): with pytest.raises(TypeError): class TestBadStruct(t.Struct): field: None = t.StructField() def test_repr(): class TestStruct(t.Struct): foo: t.uint8_t assert repr(TestStruct(foo=1)) == "TestStruct(foo=1)" assert repr(TestStruct(foo=None)) == "TestStruct()" # Invalid values still work ts = TestStruct() ts.foo = 1j assert repr(ts) == "TestStruct(foo=1j)" def test_repr_properties(): class TestStruct(t.Struct): foo: t.uint8_t bar: t.uint8_t @property def baz(self): if self.bar is None: return None return t.Bool((self.bar & 0xF0) >> 4) assert repr(TestStruct(foo=1)) == "TestStruct(foo=1)" assert ( repr(TestStruct(foo=1, bar=16)) == "TestStruct(foo=1, bar=16, *baz=)" ) assert repr(TestStruct()) == "TestStruct()" def test_bitstruct_simple(): class BitStruct1(t.Struct): foo: t.uint4_t bar: t.uint4_t s = BitStruct1(foo=0b1100, bar=0b1010) assert s.serialize() == bytes([0b1010_1100]) s2, remaining = BitStruct1.deserialize(b"\x01\x02") assert remaining == b"\x02" assert s2.foo == 0b0001 assert s2.bar == 0b0000 def test_bitstruct_nesting(expose_global): @expose_global class InnerBitStruct(t.Struct): baz1: t.uint1_t baz2: t.uint3_t baz3: t.uint1_t baz4: t.uint3_t class OuterStruct(t.Struct): foo: t.LVBytes bar: InnerBitStruct asd: t.uint8_t inner = InnerBitStruct(baz1=0b1, baz2=0b010, baz3=0b0, baz4=0b111) assert inner.serialize() == bytes([0b111_0_010_1]) assert InnerBitStruct.deserialize(inner.serialize() + b"asd") == (inner, b"asd") s = OuterStruct(foo=b"asd", bar=inner, asd=0xFF) assert s.serialize() == b"\x03asd" + bytes([0b111_0_010_1]) + b"\xff" s2, remaining = OuterStruct.deserialize(s.serialize() + b"test") assert remaining == b"test" assert s == s2 def test_bitstruct_misaligned(): class TestStruct(t.Struct): foo: t.uint1_t bar: t.uint8_t # Even though this field is byte-serializable, it is misaligned baz: t.uint7_t s = TestStruct(foo=0b1, bar=0b10101010, baz=0b1110111) assert s.serialize() == bytes([0b1110111_1, 0b0101010_1]) s2, remaining = TestStruct.deserialize(s.serialize() + b"asd") assert s == s2 with pytest.raises(ValueError): TestStruct.deserialize(b"\xff") def test_non_byte_sized_struct(): class TestStruct(t.Struct): foo: t.uint1_t bar: t.uint8_t s = TestStruct(foo=1, bar=2) with pytest.raises(ValueError): s.serialize() with pytest.raises(ValueError): TestStruct.deserialize(b"\x00\x00\x00\x00") def test_non_aligned_struct_non_integer_types(): class TestStruct(t.Struct): foo: t.uint1_t bar: t.data8 foo: t.uint7_t s = TestStruct(foo=1, bar=[2]) with pytest.raises(ValueError): s.serialize() with pytest.raises(ValueError): TestStruct.deserialize(b"\x00\x00\x00\x00") def test_bitstruct_complex(): data = ( b"\x11\x00\xff\xee\xdd\xcc\xbb\xaa\x08\x07\x06" b"\x05\x04\x03\x02\x01\x00\x00\x24\x02\x00\x7c" ) neighbor, rest = zdo_t.Neighbor.deserialize(data + b"asd") assert rest == b"asd" neighbor2 = zdo_t.Neighbor( extended_pan_id=t.ExtendedPanId.convert("aa:bb:cc:dd:ee:ff:00:11"), ieee=t.EUI64.convert("01:02:03:04:05:06:07:08"), nwk=0x0000, device_type=zdo_t.Neighbor.DeviceType.Coordinator, rx_on_when_idle=zdo_t.Neighbor.RxOnWhenIdle.On, relationship=zdo_t.Neighbor.RelationShip.Sibling, reserved1=0b0, permit_joining=zdo_t.Neighbor.PermitJoins.Unknown, reserved2=0b000000, depth=0, lqi=124, ) assert neighbor == neighbor2 assert neighbor2.serialize() == data def test_int_struct(): class NonIntegralStruct(t.Struct): foo: t.uint8_t with pytest.raises(TypeError): int(NonIntegralStruct(123)) # Integer structs must inherit from IntStruct with pytest.raises(TypeError): class BadIntegralStruct(t.Struct, t.uint8_t): foo: t.uint8_t # Integer structs must inherit from an integer type with pytest.raises(TypeError): class BadIntegralStruct2(t.IntStruct): foo: t.uint8_t class IntegralStruct(t.IntStruct, t.uint32_t): foo: t.uint8_t bar: t.uint16_t baz: t.uint7_t asd: t.uint1_t class IntegralStruct2(IntegralStruct): pass assert ( IntegralStruct(0b0_1110001_1100110011001100_10101010) == IntegralStruct( foo=0b10101010, bar=0b1100110011001100, baz=0b1110001, asd=0b0, ) == 0b0_1110001_1100110011001100_10101010 ) assert ( IntegralStruct2(0b0_1110001_1100110011001100_10101010) == IntegralStruct2( foo=0b10101010, bar=0b1100110011001100, baz=0b1110001, asd=0b0, ) == 0b0_1110001_1100110011001100_10101010 ) with pytest.raises(ValueError): # One extra bit IntegralStruct(0b1_0_1110001_1100110011001100_10101010) assert issubclass(IntegralStruct, t.uint32_t) assert issubclass(IntegralStruct, int) assert isinstance(IntegralStruct(1909247146), t.uint32_t) assert isinstance(IntegralStruct(1909247146), int) assert IntegralStruct(1909247146) == IntegralStruct(IntegralStruct(1909247146)) # We do not accept anything but kwargs with pytest.raises(TypeError): assert IntegralStruct(1909247146, bar=0, baz=0, asd=0) # Or multiple positional arguments with pytest.raises(TypeError): assert IntegralStruct(1909247146, 0) def test_struct_optional(): class TestStruct(t.Struct): foo: t.uint8_t bar: t.uint16_t baz: t.uint8_t = t.StructField(requires=lambda s: s.bar == 2, optional=True) s1 = TestStruct(foo=1, bar=2, baz=3) assert s1.serialize() == b"\x01\x02\x00\x03" assert TestStruct.deserialize(s1.serialize() + b"asd") == (s1, b"asd") assert s1.replace(baz=None).serialize() == b"\x01\x02\x00" assert s1.replace(bar=4).serialize() == b"\x01\x04\x00" assert TestStruct.deserialize(b"\x01\x03\x00\x04") == ( TestStruct(foo=1, bar=3), b"\x04", ) def test_struct_field_repr(): class TestStruct(t.Struct): foo: t.uint8_t = t.StructField(repr=lambda v: v + 1) bar: t.uint16_t = t.StructField(repr=lambda v: "bar") baz: t.CharacterString = t.StructField(repr=lambda v: "baz") s1 = TestStruct(foo=1, bar=2, baz="asd") assert repr(s1) == "TestStruct(foo=2, bar=bar, baz=baz)" def test_skip_missing(): class TestStruct(t.Struct): foo: t.uint8_t bar: t.uint16_t assert TestStruct(foo=1).as_dict() == {"foo": 1, "bar": None} assert TestStruct(foo=1).as_dict(skip_missing=True) == {"foo": 1} assert TestStruct(foo=1).as_tuple() == (1, None) assert TestStruct(foo=1).as_tuple(skip_missing=True) == (1,) def test_from_dict(expose_global): @expose_global class InnerStruct(t.Struct): field1: t.uint8_t field2: t.CharacterString class TestStruct(t.Struct): foo: t.uint8_t bar: InnerStruct baz: t.CharacterString s = TestStruct(foo=1, bar=InnerStruct(field1=2, field2="field2"), baz="field3") assert s == TestStruct.from_dict(s.as_dict(recursive=True)) def test_matching(expose_global): @expose_global class InnerStruct(t.Struct): field1: t.uint8_t field2: t.CharacterString class TestStruct(t.Struct): foo: t.uint8_t bar: InnerStruct baz: t.CharacterString assert TestStruct().matches(TestStruct()) assert not TestStruct().matches(InnerStruct()) assert TestStruct(foo=1).matches(TestStruct(foo=1)) assert not TestStruct(foo=1).matches(TestStruct(foo=2)) assert TestStruct(foo=1).matches(TestStruct()) s = TestStruct(foo=1, bar=InnerStruct(field1=2, field2="asd"), baz="foo") assert s.matches(s) assert s.matches(TestStruct()) assert s.matches(TestStruct(bar=InnerStruct())) assert s.matches(TestStruct(bar=InnerStruct(field1=2, field2="asd"))) assert not s.matches(TestStruct(bar=InnerStruct(field1=3))) def test_int_comparison(expose_global): @expose_global class FirmwarePlatform(t.enum8): Conbee = 0x05 Conbee_II = 0x07 Conbee_III = 0x09 class FirmwareVersion(t.IntStruct, t.uint32_t): reserved: t.uint8_t platform: FirmwarePlatform minor: t.uint8_t major: t.uint8_t fw_ver = FirmwareVersion(0x264F0900) assert fw_ver == FirmwareVersion( reserved=0, platform=FirmwarePlatform.Conbee_III, minor=79, major=38 ) assert fw_ver == 0x264F0900 assert int(fw_ver) == 0x264F0900 assert "0x264F0900" in str(fw_ver) assert int(fw_ver) <= fw_ver assert fw_ver <= int(fw_ver) assert int(fw_ver) - 1 < fw_ver assert fw_ver < int(fw_ver) + 1 assert int(fw_ver) >= fw_ver assert fw_ver >= int(fw_ver) assert int(fw_ver) + 1 > fw_ver assert fw_ver > int(fw_ver) - 1 assert (fw_ver & 0b0010101) == (int(fw_ver) & 0b0010101) assert (fw_ver | 0b0010101) == (int(fw_ver) | 0b0010101) assert (fw_ver >> 3) == (int(fw_ver) >> 3) assert (fw_ver << 3) == (int(fw_ver) << 3) assert bool(fw_ver & 0) is False assert bool(fw_ver & 0xFFFF) is True assert hash(fw_ver) == hash(int(fw_ver)) def test_int_comparison_non_int(expose_global): @expose_global class FirmwarePlatform(t.enum8): Conbee = 0x05 Conbee_II = 0x07 Conbee_III = 0x09 # This isn't an integer class FirmwareVersion(t.Struct): reserved: t.uint8_t platform: FirmwarePlatform minor: t.uint8_t major: t.uint8_t fw_ver = FirmwareVersion( reserved=0, platform=FirmwarePlatform.Conbee_III, minor=79, major=38 ) with pytest.raises(TypeError): fw_ver < 0 # noqa: B015 with pytest.raises(TypeError): fw_ver <= 0 # noqa: B015 with pytest.raises(TypeError): fw_ver > 0 # noqa: B015 with pytest.raises(TypeError): fw_ver >= 0 # noqa: B015 def test_frozen_struct(): class OuterStruct(t.Struct): class InnerStruct(t.Struct): b: t.uint8_t c: t.uint8_t a: t.uint8_t inner: None = t.StructField(type=InnerStruct) d: t.uint8_t e: t.uint16_t struct = OuterStruct(a=1, inner=OuterStruct.InnerStruct(b=2, c=3), d=4) frozen = struct.freeze() assert "frozen" not in repr(struct) assert "frozen" in repr(frozen) with pytest.raises(TypeError, match="Unhashable type"): hash(struct) # Setting attributes has no effect assert frozen.a == 1 assert frozen.inner.b == 2 with pytest.raises(AttributeError): frozen.a = 2 with pytest.raises(AttributeError): frozen.inner.b = 5 assert frozen.a == 1 assert frozen.inner.b == 2 assert {frozen: 2}[frozen] == 2 assert {frozen, frozen} == {frozen} assert frozen == frozen.replace(a=1) assert {frozen, frozen, frozen.replace(a=1), frozen.replace(a=2)} == { frozen, frozen.replace(a=2), } zigpy-0.80.1/tests/test_topology.py000066400000000000000000000311161501451476000173760ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib from unittest import mock import pytest from tests.conftest import App, make_ieee, make_neighbor, make_route import zigpy.config as conf import zigpy.device import zigpy.endpoint import zigpy.profiles import zigpy.topology import zigpy.types as t import zigpy.zdo.types as zdo_t @pytest.fixture(autouse=True) def remove_request_delay(): with mock.patch("zigpy.topology.REQUEST_DELAY", new=(0, 0)): yield @pytest.fixture def topology(make_initialized_device): app = App( { conf.CONF_DEVICE: {conf.CONF_DEVICE_PATH: "/dev/null"}, conf.CONF_TOPO_SKIP_COORDINATOR: True, } ) coordinator = make_initialized_device(app) coordinator.nwk = 0x0000 app.state.node_info.nwk = coordinator.nwk app.state.node_info.ieee = coordinator.ieee app.state.node_info.logical_type = zdo_t.LogicalType.Coordinator return zigpy.topology.Topology(app) @contextlib.contextmanager def patch_device_tables( device: zigpy.device.Device, neighbors: list | BaseException | zdo_t.Status, routes: list | BaseException | zdo_t.Status, ): def mgmt_lqi_req(StartIndex: t.uint8_t): status = zdo_t.Status.SUCCESS entries = 0 start_index = 0 table: list[zdo_t.Neighbor] = [] if isinstance(neighbors, zdo_t.Status): status = neighbors elif isinstance(neighbors, BaseException): raise neighbors else: entries = len(neighbors) start_index = StartIndex table = neighbors[StartIndex : StartIndex + 3] return list( { "Status": status, "Neighbors": zdo_t.Neighbors( Entries=entries, StartIndex=start_index, NeighborTableList=table, ), }.values() ) def mgmt_rtg_req(StartIndex: t.uint8_t): status = zdo_t.Status.SUCCESS entries = 0 start_index = 0 table: list[zdo_t.Route] = [] if isinstance(routes, zdo_t.Status): status = routes elif isinstance(routes, BaseException): raise routes else: entries = len(routes) start_index = StartIndex table = routes[StartIndex : StartIndex + 3] return list( { "Status": status, "Routes": zdo_t.Routes( Entries=entries, StartIndex=start_index, RoutingTableList=table, ), }.values() ) lqi_req_patch = mock.patch.object( device.zdo, "Mgmt_Lqi_req", mock.AsyncMock(side_effect=mgmt_lqi_req, spec_set=device.zdo.Mgmt_Lqi_req), ) rtg_req_patch = mock.patch.object( device.zdo, "Mgmt_Rtg_req", mock.AsyncMock(side_effect=mgmt_rtg_req, spec_set=device.zdo.Mgmt_Rtg_req), ) with lqi_req_patch, rtg_req_patch: yield async def test_scan_no_devices(topology) -> None: await topology.scan() assert not topology.neighbors assert not topology.routes @pytest.mark.parametrize( ("neighbors", "routes"), [ ([], asyncio.TimeoutError()), ([], []), (asyncio.TimeoutError(), asyncio.TimeoutError()), ], ) async def test_scan_failures( topology, make_initialized_device, neighbors, routes ) -> None: dev = make_initialized_device(topology._app) with patch_device_tables(dev, neighbors=neighbors, routes=routes): await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 1 if not neighbors else 3 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 1 if not routes else 3 assert not topology.neighbors[dev.ieee] assert not topology.routes[dev.ieee] async def test_neighbors_not_supported(topology, make_initialized_device) -> None: dev = make_initialized_device(topology._app) with patch_device_tables(dev, neighbors=zdo_t.Status.NOT_SUPPORTED, routes=[]): await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 1 await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 2 async def test_routes_not_supported(topology, make_initialized_device) -> None: dev = make_initialized_device(topology._app) with patch_device_tables(dev, neighbors=[], routes=zdo_t.Status.NOT_SUPPORTED): await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 1 await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 2 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 1 async def test_routes_and_neighbors_not_supported( topology, make_initialized_device ) -> None: dev = make_initialized_device(topology._app) with patch_device_tables( dev, neighbors=zdo_t.Status.NOT_SUPPORTED, routes=zdo_t.Status.NOT_SUPPORTED ): await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 1 await topology.scan() assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 1 async def test_scan_end_device(topology, make_initialized_device) -> None: dev = make_initialized_device(topology._app) dev.node_desc.logical_type = zdo_t.LogicalType.EndDevice with patch_device_tables(dev, neighbors=[], routes=[]): await topology.scan() # The device will not be scanned because it is not a router assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 0 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 0 async def test_scan_explicit_device(topology, make_initialized_device) -> None: dev1 = make_initialized_device(topology._app) dev2 = make_initialized_device(topology._app) with patch_device_tables(dev1, neighbors=[], routes=[]): with patch_device_tables(dev2, neighbors=[], routes=[]): await topology.scan(devices=[dev2]) # Only the second device was scanned assert len(dev1.zdo.Mgmt_Lqi_req.mock_calls) == 0 assert len(dev1.zdo.Mgmt_Rtg_req.mock_calls) == 0 assert len(dev2.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(dev2.zdo.Mgmt_Rtg_req.mock_calls) == 1 async def test_scan_router_many(topology, make_initialized_device) -> None: dev = make_initialized_device(topology._app) with patch_device_tables( dev, neighbors=[ make_neighbor(ieee=make_ieee(2 + i), nwk=0x1234 + i) for i in range(100) ], routes=[ make_route(dest_nwk=0x1234 + i, next_hop=0x1234 + i) for i in range(100) ], ): await topology.scan() # We only permit three scans per request assert len(dev.zdo.Mgmt_Lqi_req.mock_calls) == 34 assert len(dev.zdo.Mgmt_Rtg_req.mock_calls) == 34 assert topology.neighbors[dev.ieee] == [ make_neighbor(ieee=make_ieee(2 + i), nwk=0x1234 + i) for i in range(100) ] assert topology.routes[dev.ieee] == [ make_route(dest_nwk=0x1234 + i, next_hop=0x1234 + i) for i in range(100) ] async def test_scan_skip_coordinator(topology, make_initialized_device) -> None: coordinator = topology._app._device assert coordinator.nwk == 0x0000 with patch_device_tables(coordinator, neighbors=[], routes=[]): await topology.scan() assert len(coordinator.zdo.Mgmt_Lqi_req.mock_calls) == 0 assert len(coordinator.zdo.Mgmt_Rtg_req.mock_calls) == 0 assert not topology.neighbors[coordinator.ieee] assert not topology.routes[coordinator.ieee] async def test_scan_coordinator(topology) -> None: app = topology._app app.config[conf.CONF_TOPO_SKIP_COORDINATOR] = False coordinator = app._device coordinator.node_desc.logical_type = zdo_t.LogicalType.Coordinator assert coordinator.nwk == 0x0000 with patch_device_tables( coordinator, neighbors=[ make_neighbor(ieee=make_ieee(2), nwk=0x1234), ], routes=[ make_route(dest_nwk=0x1234, next_hop=0x1234), ], ): await topology.scan() assert len(coordinator.zdo.Mgmt_Lqi_req.mock_calls) == 1 assert len(coordinator.zdo.Mgmt_Rtg_req.mock_calls) == 1 assert topology.neighbors[coordinator.ieee] == [ make_neighbor(ieee=make_ieee(2), nwk=0x1234) ] assert topology.routes[coordinator.ieee] == [ make_route(dest_nwk=0x1234, next_hop=0x1234) ] @mock.patch("zigpy.application.ControllerApplication._discover_unknown_device") async def test_discover_new_devices( discover_unknown_device, topology, make_initialized_device ) -> None: dev1 = make_initialized_device(topology._app) dev2 = make_initialized_device(topology._app) await topology._find_unknown_devices( neighbors={ dev1.ieee: [ # Existing devices make_neighbor(ieee=dev1.ieee, nwk=dev1.nwk), make_neighbor(ieee=dev2.ieee, nwk=dev2.nwk), # Unknown device make_neighbor( ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), nwk=0xFF00 ), ], dev2.ieee: [], }, routes={ dev1.ieee: [ # Existing devices make_route(dest_nwk=dev1.nwk, next_hop=dev1.nwk), make_route(dest_nwk=dev2.nwk, next_hop=dev2.nwk), # Via existing devices make_route(dest_nwk=0xFF01, next_hop=dev2.nwk), make_route(dest_nwk=dev2.nwk, next_hop=0xFF02), # Inactive route make_route( dest_nwk=0xFF03, next_hop=0xFF04, status=zdo_t.RouteStatus.Inactive ), ], dev2.ieee: [], }, ) assert len(discover_unknown_device.mock_calls) == 3 assert mock.call(0xFF00) in discover_unknown_device.mock_calls assert mock.call(0xFF01) in discover_unknown_device.mock_calls assert mock.call(0xFF02) in discover_unknown_device.mock_calls @mock.patch("zigpy.topology.Topology._scan") async def test_scan_start_concurrent(mock_scan, topology): concurrency = 0 max_concurrency = 0 async def _scan(_): nonlocal concurrency nonlocal max_concurrency concurrency += 1 max_concurrency = max(concurrency, max_concurrency) try: await asyncio.sleep(0.01) finally: concurrency -= 1 max_concurrency = max(concurrency, max_concurrency) mock_scan.side_effect = _scan topology.start_periodic_scans(0.1) topology.start_periodic_scans(0.1) topology.start_periodic_scans(0.1) topology.start_periodic_scans(0.1) topology.start_periodic_scans(0.1) scan1 = asyncio.create_task(topology.scan()) scan2 = asyncio.create_task(topology.scan()) await asyncio.sleep(0.01) with pytest.raises(asyncio.CancelledError): await scan1 await scan2 # Wait for a "scan" to finish await asyncio.sleep(0.15) await topology._scan_task topology.stop_periodic_scans() # Only a single one was actually running assert max_concurrency == 1 topology.stop_periodic_scans() await asyncio.sleep(0) # All of the tasks have been stopped assert topology._scan_task.done() assert topology._scan_loop_task.done() @mock.patch("zigpy.topology.Topology.scan", side_effect=RuntimeError()) async def test_periodic_scan_failure(mock_scan, topology): topology.start_periodic_scans(0.01) await asyncio.sleep(0.1) topology.stop_periodic_scans() async def test_periodic_scan_priority(topology): async def _scan(_): await asyncio.sleep(0.5) with mock.patch.object(topology, "_scan", side_effect=_scan) as mock_scan: scan_task = asyncio.create_task(topology.scan()) await asyncio.sleep(0.1) # Start a periodic scan. It won't have time to run yet, the old scan is running topology.start_periodic_scans(0.05) # Wait for the original scan to finish await scan_task # Start another scan, interrupting the periodic scan await asyncio.sleep(0.15) await topology.scan() # Now we can cancel the periodic scan topology.stop_periodic_scans() await asyncio.sleep(0) # Our two manual scans succeeded and the periodic one was attempted assert len(mock_scan.mock_calls) == 3 zigpy-0.80.1/tests/test_types.py000066400000000000000000000542641501451476000166770ustar00rootroot00000000000000import itertools import math import struct import pytest import zigpy.types as t def test_abstract_ints(): assert issubclass(t.uint8_t, t.uint_t) assert not issubclass(t.uint8_t, t.int_t) assert t.int_t._signed is True assert t.uint_t._signed is False assert t.int_t._byteorder == "little" assert t.int_t_be._byteorder == "big" with pytest.raises(TypeError): t.int_t(0) with pytest.raises(TypeError): t.FixedIntType(0) def test_int_out_of_bounds(): assert t.uint8_t._size == 1 assert t.uint8_t._bits == 8 t.uint8_t(0) with pytest.raises(ValueError): # Normally this would throw an OverflowError. We re-raise it as a ValueError. t.uint8_t(-1) with pytest.raises(ValueError): t.uint8_t(0xFF + 1) def test_int_too_short(): with pytest.raises(ValueError): t.uint8_t.deserialize(b"") with pytest.raises(ValueError): t.uint16_t.deserialize(b"\x00") def test_fractional_ints_corner(): assert t.uint1_t._size is None assert t.uint1_t._bits == 1 assert t.uint1_t.min_value == 0 assert t.uint1_t.max_value == 1 assert t.uint1_t(0) == 0 assert t.uint1_t(1) == 1 with pytest.raises(ValueError): t.uint1_t(-1) with pytest.raises(ValueError): t.uint1_t(2) n = t.uint1_t(0b1) with pytest.raises(TypeError): n.serialize() assert t.uint1_t(0).bits() == [0] assert t.uint1_t(1).bits() == [1] assert t.uint1_t.from_bits([1, 1]) == (1, [1]) assert t.uint1_t.from_bits([0, 1]) == (1, [0]) def test_fractional_ints_larger(): assert t.uint7_t._size is None assert t.uint7_t._bits == 7 assert t.uint7_t.min_value == 0 assert t.uint7_t.max_value == 2**7 - 1 assert t.uint7_t(0) == 0 assert t.uint7_t(1) == 1 assert t.uint7_t(0b1111111) == 0b1111111 with pytest.raises(ValueError): t.uint7_t(-1) with pytest.raises(ValueError): t.uint7_t(0b1111111 + 1) n = t.uint7_t(0b1111111) with pytest.raises(TypeError): n.serialize() assert t.uint7_t(0).bits() == [0, 0, 0, 0, 0, 0, 0] assert t.uint7_t(1).bits() == [0, 0, 0, 0, 0, 0, 1] assert t.uint7_t(0b1011111).bits() == [1, 0, 1, 1, 1, 1, 1] assert t.uint7_t.from_bits([1, 0, 1, 1, 1, 1, 0, 1, 1, 1]) == (0b1110111, [1, 0, 1]) with pytest.raises(ValueError): assert t.uint7_t.from_bits([1] * 6) def test_ints_signed(): class int7s(t.int_t, bits=7): pass assert int7s._size is None assert int7s._bits == 7 assert int7s(0) == 0 assert int7s(1) == 1 assert int7s(-1) == -1 assert int7s(2**6 - 1) == 2**6 - 1 assert int7s(-(2**6)) == -(2**6) with pytest.raises(ValueError): int7s(2**6) with pytest.raises(ValueError): int7s(-(2**6) - 1) n = int7s(2**6 - 1) with pytest.raises(TypeError): n.serialize() assert int7s(0).bits() == [0, 0, 0, 0, 0, 0, 0] assert int7s(1).bits() == [0, 0, 0, 0, 0, 0, 1] assert int7s(-1).bits() == [1, 1, 1, 1, 1, 1, 1] assert int7s(2**6 - 1).bits() == [0, 1, 1, 1, 1, 1, 1] assert int7s.from_bits([1, 0, 1, 0, 1, 1, 0, 1, 1, 1]) == (0b0110111, [1, 0, 1]) with pytest.raises(TypeError): int7s.deserialize(b"\xff") t.int8s.deserialize(b"\xff") n = t.int8s(-126) bits = [1, 0] + t.Bits.deserialize(n.serialize())[0] assert t.int8s.from_bits(bits) == (n, [1, 0]) def test_bigendian_ints(): assert t.uint32_t_be(0x12345678).serialize() == b"\x12\x34\x56\x78" assert t.uint32_t_be.deserialize(b"\x12\x34\x56\x78") == (0x12345678, b"") assert t.int32s_be(0x12345678).serialize() == b"\x12\x34\x56\x78" assert t.int32s_be(-1).serialize() == b"\xff\xff\xff\xff" assert t.int32s_be.deserialize(b"\xfe\xdc\xba\x98") == (-0x01234568, b"") assert ( t.uint32_t_be(0x12345678).serialize()[::-1] == t.uint32_t(0x12345678).serialize() ) def test_bits(): assert t.Bits() == [] assert t.Bits([1] + [0] * 15).serialize() == b"\x80\x00" assert t.Bits.deserialize(b"\x80\x00") == ([1] + [0] * 15, b"") bits = t.Bits([0] * 7) with pytest.raises(ValueError): assert bits.serialize() def compare_with_nan(v1, v2): if not math.isnan(v1) ^ math.isnan(v2): return True return v1 == v2 @pytest.mark.parametrize( "value", [ 1.25, 0, -1.25, float("nan"), float("+inf"), float("-inf"), # Max value held by Half 65504, -65504, ], ) def test_floats(value): extra = b"ab12!" for data_type in (t.Half, t.Single, t.Double): value2, remaining = data_type.deserialize(data_type(value).serialize() + extra) assert remaining == extra # nan != nan so make sure they're both nan or the same value assert compare_with_nan(value, value2) assert len(data_type(value).serialize()) == data_type._size @pytest.mark.parametrize( ("value", "only_double"), [ (2, False), (1.25, False), (0, False), (-1.25, False), (-2, False), (float("nan"), False), (float("+inf"), False), (float("-inf"), False), (struct.unpack(">f", bytes.fromhex("7f7f ffff"))[0], False), (struct.unpack(">f", bytes.fromhex("3f7f ffff"))[0], False), (struct.unpack(">d", bytes.fromhex("7f7f ffff ffff ffff"))[0], True), (struct.unpack(">d", bytes.fromhex("3f7f ffff ffff ffff"))[0], True), ], ) def test_single_and_double_with_struct(value, only_double): # Float and double must match the behavior of the built-in struct module if not only_double: assert t.Single(value).serialize() == struct.pack("" assert f"0x{TestEnum.Member:02X}" == "0x00" def test_bitmap(): """Test bitmaps.""" class TestBitmap(t.bitmap16): CH_1 = 0x0010 CH_2 = 0x0020 CH_3 = 0x0040 CH_4 = 0x0080 ALL = 0x00F0 extra = b"extra data\xaa\55" data = b"\xf0\x00" r, rest = TestBitmap.deserialize(data + extra) assert rest == extra assert r is TestBitmap.ALL assert r.name == "ALL" assert r.value == 0x00F0 assert r.serialize() == data data = b"\x60\x00" r, rest = TestBitmap.deserialize(data + extra) assert rest == extra assert TestBitmap.CH_1 not in r assert TestBitmap.CH_2 in r assert TestBitmap.CH_3 in r assert TestBitmap.CH_4 not in r assert TestBitmap.ALL not in r assert r.value == 0x0060 assert r.serialize() == data def test_bitmap_undef(): """Test bitmaps with some undefined flags.""" class TestBitmap(t.bitmap16): CH_1 = 0x0010 CH_2 = 0x0020 CH_3 = 0x0040 CH_4 = 0x0080 ALL = 0x00F0 extra = b"extra data\xaa\55" data = b"\x60\x0f" r, rest = TestBitmap.deserialize(data + extra) assert rest == extra assert TestBitmap.CH_1 not in r assert TestBitmap.CH_2 in r assert TestBitmap.CH_3 in r assert TestBitmap.CH_4 not in r assert TestBitmap.ALL not in r assert r.value == 0x0F60 assert r.serialize() == data def test_bitmap_instance_types(): class TestBitmap(t.bitmap16): CH_1 = 0x0010 CH_2 = 0x0020 CH_3 = 0x0040 CH_4 = 0x0080 ALL = 0x00F0 assert TestBitmap._member_type_ is t.uint16_t assert type(TestBitmap.ALL.value) is t.uint16_t assert isinstance(TestBitmap.ALL, t.uint16_t) assert issubclass(TestBitmap, t.uint16_t) assert isinstance(TestBitmap(0xFF00), t.uint16_t) assert isinstance(TestBitmap(0xFF00), TestBitmap) def test_nwk_convert(): assert t.NWK.convert(str(t.NWK(0x1234))[2:]) == t.NWK(0x1234) assert str(t.NWK(0x0012))[2:] == "0012" assert str(t.NWK(0x1200))[2:] == "1200" def test_serializable_bytes(): obj = t.SerializableBytes(b"test") assert obj == obj # noqa: PLR0124 assert obj == t.SerializableBytes(b"test") assert t.SerializableBytes(obj) == obj assert obj != b"test" assert obj.serialize() == b"test" assert "test" in repr([obj]) with pytest.raises(TypeError): obj + b"test" with pytest.raises(ValueError): t.SerializableBytes("test") with pytest.raises(ValueError): t.SerializableBytes([1, 2, 3]) zigpy-0.80.1/tests/test_zcl.py000066400000000000000000001226451501451476000163220ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Any from unittest import mock from unittest.mock import AsyncMock, MagicMock, patch, sentinel import pytest from zigpy import zcl import zigpy.device import zigpy.endpoint import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Ota DEFAULT_TSN = 123 @pytest.fixture def endpoint(): ep = zigpy.endpoint.Endpoint(MagicMock(), 1) ep.add_input_cluster(0) ep.add_input_cluster(3) return ep def test_deserialize_general(endpoint): hdr, args = endpoint.deserialize(0, b"\x00\x01\x00") assert hdr.tsn == 1 assert hdr.command_id == 0 assert hdr.direction == foundation.Direction.Client_to_Server def test_deserialize_general_unknown(endpoint): hdr, args = endpoint.deserialize(0, b"\x00\x01\xff") assert hdr.tsn == 1 assert hdr.frame_control.is_general is True assert hdr.frame_control.is_cluster is False assert hdr.command_id == 255 assert hdr.direction == foundation.Direction.Client_to_Server def test_deserialize_cluster(endpoint): hdr, args = endpoint.deserialize(0, b"\x01\x01\x00xxx") assert hdr.tsn == 1 assert hdr.frame_control.is_general is False assert hdr.frame_control.is_cluster is True assert hdr.command_id == 0 assert hdr.direction == foundation.Direction.Client_to_Server def test_deserialize_cluster_client(endpoint): hdr, args = endpoint.deserialize(3, b"\x09\x01\x00AB") assert hdr.tsn == 1 assert hdr.frame_control.is_general is False assert hdr.frame_control.is_cluster is True assert hdr.command_id == 0 assert list(args) == [0x4241] assert hdr.direction == foundation.Direction.Server_to_Client def test_deserialize_cluster_unknown(endpoint): with pytest.raises(KeyError): endpoint.deserialize(0xFF00, b"\x05\x00\x00\x01\x00") def test_deserialize_cluster_command_unknown(endpoint): hdr, args = endpoint.deserialize(0, b"\x01\x01\xff") assert hdr.tsn == 1 assert hdr.command_id == 255 assert hdr.direction == foundation.Direction.Client_to_Server def test_unknown_cluster(): c = zcl.Cluster.from_id(None, 999) assert isinstance(c, zcl.Cluster) assert c.cluster_id == 999 def test_manufacturer_specific_cluster(): import zigpy.zcl.clusters.manufacturer_specific as ms c = zcl.Cluster.from_id(None, 0xFC00) assert isinstance(c, ms.ManufacturerSpecificCluster) assert hasattr(c, "cluster_id") c = zcl.Cluster.from_id(None, 0xFFFF) assert isinstance(c, ms.ManufacturerSpecificCluster) assert hasattr(c, "cluster_id") @pytest.fixture def cluster_by_id(): def _cluster(cluster_id=0): epmock = MagicMock() epmock._device.get_sequence.return_value = DEFAULT_TSN epmock.device.get_sequence.return_value = DEFAULT_TSN epmock.device.zdo.bind = AsyncMock() epmock.device.zdo.unbind = AsyncMock() epmock.request = AsyncMock() epmock.reply = AsyncMock() return zcl.Cluster.from_id(epmock, cluster_id) return _cluster @pytest.fixture def cluster(cluster_by_id): return cluster_by_id(0) @pytest.fixture def client_cluster(): epmock = AsyncMock() epmock.device.get_sequence = MagicMock(return_value=DEFAULT_TSN) return Ota(epmock) async def test_request_general(cluster): await cluster.request( general=True, command_id=foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].id, schema=foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].schema, attribute_ids=[], ) assert cluster._endpoint.request.call_count == 1 async def test_request_manufacturer(cluster): command = foundation.ZCLCommandDef( name="test_command", id=0x00, schema={"param1": t.uint8_t} ).with_compiled_schema() await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, ) assert cluster._endpoint.request.call_count == 1 org_size = len(cluster._endpoint.request.mock_calls[0].kwargs["data"]) await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, manufacturer=1, ) assert cluster._endpoint.request.call_count == 2 assert org_size + 2 == len(cluster._endpoint.request.mock_calls[1].kwargs["data"]) async def test_request_optional(cluster): command = foundation.ZCLCommandDef( name="test_command", id=0x00, schema={ "param1": t.uint8_t, "param2": t.uint16_t, "param3?": t.uint16_t, "param4?": t.uint8_t, }, ).with_compiled_schema() cluster.endpoint.request = AsyncMock() with pytest.raises(ValueError): await cluster.request( general=True, command_id=command.id, schema=command.schema, ) assert cluster._endpoint.request.call_count == 0 cluster._endpoint.request.reset_mock() with pytest.raises(ValueError): await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, ) assert cluster._endpoint.request.call_count == 0 cluster._endpoint.request.reset_mock() await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, param2=2, ) assert cluster._endpoint.request.call_count == 1 cluster._endpoint.request.reset_mock() await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, param2=2, param3=3, ) assert cluster._endpoint.request.call_count == 1 cluster._endpoint.request.reset_mock() await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, param2=2, param3=3, param4=4, ) assert cluster._endpoint.request.call_count == 1 cluster._endpoint.request.reset_mock() with pytest.raises(TypeError): await cluster.request( general=True, command_id=command.id, schema=command.schema, param1=1, param2=2, param3=3, param4=4, param5=5, ) assert cluster._endpoint.request.call_count == 0 cluster._endpoint.request.reset_mock() async def test_reply_general(cluster): command = foundation.ZCLCommandDef( name="test_command", id=0x00, schema={} ).with_compiled_schema() await cluster.reply(general=False, command_id=command.id, schema=command.schema) assert cluster._endpoint.reply.call_count == 1 async def test_reply_manufacturer(cluster): command = foundation.ZCLCommandDef( name="test_command", id=0x00, schema={ "param1": t.uint8_t, }, ).with_compiled_schema() await cluster.reply( general=False, command_id=command.id, schema=command.schema, param1=1 ) assert cluster._endpoint.reply.call_count == 1 org_size = len(cluster._endpoint.reply.mock_calls[0].kwargs["data"]) await cluster.reply( general=False, command_id=command.id, schema=command.schema, param1=1, manufacturer=1, ) assert cluster._endpoint.reply.call_count == 2 assert org_size + 2 == len(cluster._endpoint.reply.mock_calls[1].kwargs["data"]) def test_attribute_report(cluster): attr = zcl.foundation.Attribute() attr.attrid = 4 attr.value = zcl.foundation.TypeValue() attr.value.value = "manufacturer" hdr = MagicMock(auto_spec=foundation.ZCLHeader) hdr.command_id = foundation.GeneralCommand.Report_Attributes hdr.frame_control.is_general = True hdr.frame_control.is_cluster = False cmd = foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Report_Attributes ].schema([attr]) cluster.handle_message(hdr, cmd) assert cluster._attr_cache[4] == "manufacturer" attr.attrid = 0x89AB cluster.handle_message(hdr, cmd) assert cluster._attr_cache[attr.attrid] == "manufacturer" def test_handle_request_unknown(cluster): hdr = MagicMock(auto_spec=foundation.ZCLHeader) hdr.command_id = 0x42 hdr.frame_control.is_general = True hdr.frame_control.is_cluster = False cluster.listener_event = MagicMock() cluster._update_attribute = MagicMock() cluster.handle_cluster_general_request = MagicMock() cluster.handle_cluster_request = MagicMock() cluster.handle_message(hdr, sentinel.args) assert cluster.listener_event.call_count == 1 assert cluster.listener_event.call_args[0][0] == "general_command" assert cluster._update_attribute.call_count == 0 assert cluster.handle_cluster_general_request.call_count == 1 assert cluster.handle_cluster_request.call_count == 0 def test_handle_cluster_request(cluster): hdr = MagicMock(auto_spec=foundation.ZCLHeader) hdr.command_id = 0x42 hdr.frame_control.is_general = False hdr.frame_control.is_cluster = True cluster.listener_event = MagicMock() cluster._update_attribute = MagicMock() cluster.handle_cluster_general_request = MagicMock() cluster.handle_cluster_request = MagicMock() cluster.handle_message(hdr, sentinel.args) assert cluster.listener_event.call_count == 1 assert cluster.listener_event.call_args[0][0] == "cluster_command" assert cluster._update_attribute.call_count == 0 assert cluster.handle_cluster_general_request.call_count == 0 assert cluster.handle_cluster_request.call_count == 1 def _mk_rar(attrid, value, status=0): r = zcl.foundation.ReadAttributeRecord() r.attrid = attrid r.status = status r.value = zcl.foundation.TypeValue() r.value.value = value return r async def test_read_attributes_uncached(cluster): async def mockrequest( is_general_req, command, schema, args, manufacturer=None, **kwargs ): assert is_general_req is True assert command == 0 rar0 = _mk_rar(0, 99) rar4 = _mk_rar(4, "Manufacturer") rar99 = _mk_rar(99, None, 1) rar199 = _mk_rar(199, 199) rar16 = _mk_rar(0x0010, None, zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE) return [[rar0, rar4, rar99, rar199, rar16]] cluster.request = mockrequest success, failure = await cluster.read_attributes([0, "manufacturer", 99, 199, 16]) assert success[0] == 99 assert success["manufacturer"] == "Manufacturer" assert failure[99] == 1 assert {99, 0x0010} == failure.keys() assert success[199] == 199 assert cluster.unsupported_attributes == {0x0010, "location_desc"} async def test_read_attributes_cached(cluster): cluster.request = MagicMock() cluster._attr_cache[0] = 99 cluster._attr_cache[4] = "Manufacturer" cluster.unsupported_attributes.add(0x0010) success, failure = await cluster.read_attributes( [0, "manufacturer", 0x0010], allow_cache=True ) assert cluster.request.call_count == 0 assert success[0] == 99 assert success["manufacturer"] == "Manufacturer" assert failure == {0x0010: zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE} async def test_read_attributes_mixed_cached(cluster): """Reading cached and uncached attributes.""" cluster.request = AsyncMock(return_value=[[_mk_rar(5, "Model")]]) cluster._attr_cache[0] = 99 cluster._attr_cache[4] = "Manufacturer" cluster.unsupported_attributes.add(0x0010) success, failure = await cluster.read_attributes( [0, "manufacturer", "model", 0x0010], allow_cache=True ) assert success[0] == 99 assert success["manufacturer"] == "Manufacturer" assert success["model"] == "Model" assert cluster.request.await_count == 1 assert cluster.request.call_args[0][3] == [0x0005] assert failure == {0x0010: zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE} async def test_read_attributes_default_response(cluster): async def mockrequest( foundation, command, schema, args, manufacturer=None, **kwargs ): assert foundation is True assert command == 0 return [0xC1] cluster.request = mockrequest success, failure = await cluster.read_attributes([0, 5, 23], allow_cache=False) assert success == {} assert failure == {0: 0xC1, 5: 0xC1, 23: 0xC1} async def test_item_access_attributes(cluster): cluster._attr_cache[5] = sentinel.model assert cluster["model"] == sentinel.model assert cluster[5] == sentinel.model assert cluster.get("model") == sentinel.model assert cluster.get(5) == sentinel.model assert cluster.get("model", sentinel.default) == sentinel.model assert cluster.get(5, sentinel.default) == sentinel.model with pytest.raises(KeyError): cluster[4] assert cluster.get(4) is None assert cluster.get("manufacturer") is None assert cluster.get(4, sentinel.default) is sentinel.default assert cluster.get("manufacturer", sentinel.default) is sentinel.default with pytest.raises(KeyError): cluster["manufacturer"] with pytest.raises(KeyError): # wrong attr name cluster["some_non_existent_attr"] with pytest.raises(ValueError): # wrong key type cluster[None] with pytest.raises(ValueError): # wrong key type cluster.get(None) # Test access to cached attribute via wrong attr name with pytest.raises(KeyError): cluster.get("no_such_attribute") async def test_item_set_attributes(cluster): with patch.object(cluster, "write_attributes") as write_mock: cluster["model"] = sentinel.model await asyncio.sleep(0) assert write_mock.await_count == 1 assert write_mock.call_args[0][0] == {"model": sentinel.model} with pytest.raises(ValueError): cluster[None] = sentinel.manufacturer async def test_write_attributes(cluster): with patch.object(cluster, "_write_attributes", new=AsyncMock()): await cluster.write_attributes({0: 5, "app_version": 4}) assert cluster._write_attributes.call_count == 1 async def test_write_wrong_attribute(cluster): with patch.object(cluster, "_write_attributes", new=AsyncMock()): await cluster.write_attributes({0xFF: 5}) assert cluster._write_attributes.call_count == 1 async def test_write_unknown_attribute(cluster): with patch.object(cluster, "_write_attributes", new=AsyncMock()): with pytest.raises(KeyError): # Using an invalid attribute name, the call should fail await cluster.write_attributes({"dummy_attribute": 5}) assert cluster._write_attributes.call_count == 0 async def test_write_attributes_wrong_type(cluster): with patch.object(cluster, "_write_attributes", new=AsyncMock()): with pytest.raises(ValueError): await cluster.write_attributes({18: 0x2222}) assert cluster._write_attributes.call_count == 0 async def test_write_attributes_raw(cluster): with patch.object(cluster, "_write_attributes", new=AsyncMock()): # write_attributes_raw does not check the attributes, # send to unknown attribute in cluster, the write should be effective await cluster.write_attributes_raw({0: 5, 0x3000: 5}) assert cluster._write_attributes.call_count == 1 @pytest.mark.parametrize( ("cluster_id", "attr", "value", "serialized"), [ (0, "zcl_version", 0xAA, b"\x00\x00\x20\xaa"), (0, "model", "model x", b"\x05\x00\x42\x07model x"), (0, "device_enabled", True, b"\x12\x00\x10\x01"), (0, "alarm_mask", 0x55, b"\x13\x00\x18\x55"), (0x0202, "fan_mode", 0xDE, b"\x00\x00\x30\xde"), ], ) async def test_write_attribute_types( cluster_id: int, attr: str, value: Any, serialized: bytes, cluster_by_id ): cluster = cluster_by_id(cluster_id) with patch.object(cluster.endpoint, "request", new=AsyncMock()): await cluster.write_attributes({attr: value}) assert cluster._endpoint.reply.call_count == 0 assert cluster._endpoint.request.call_count == 1 assert cluster.endpoint.request.mock_calls[0].kwargs["data"][3:] == serialized @pytest.mark.parametrize( "status", [foundation.Status.SUCCESS, foundation.Status.UNSUPPORTED_ATTRIBUTE] ) async def test_write_attributes_cache_default_response(cluster, status): write_mock = AsyncMock( return_value=[foundation.GeneralCommand.Write_Attributes, status] ) with patch.object(cluster, "_write_attributes", write_mock): attributes = {4: "manufacturer", 5: "model", 12: 12} await cluster.write_attributes(attributes) assert cluster._write_attributes.call_count == 1 for attr_id in attributes: assert attr_id not in cluster._attr_cache @pytest.mark.parametrize( ("attributes", "result"), [ ({4: "manufacturer"}, b"\x00"), ({4: "manufacturer", 5: "model"}, b"\x00"), ({4: "manufacturer", 5: "model", 3: 12}, b"\x00"), ({4: "manufacturer", 5: "model"}, b"\x00\x00"), ({4: "manufacturer", 5: "model", 3: 12}, b"\x00\x00\x00"), ], ) async def test_write_attributes_cache_success(cluster, attributes, result): listener = MagicMock() cluster.add_listener(listener) rsp_type = t.List[foundation.WriteAttributesStatusRecord] write_mock = AsyncMock(return_value=[rsp_type.deserialize(result)[0]]) with patch.object(cluster, "_write_attributes", write_mock): await cluster.write_attributes(attributes) assert cluster._write_attributes.call_count == 1 for attr_id in attributes: assert cluster._attr_cache[attr_id] == attributes[attr_id] listener.attribute_updated.assert_any_call( attr_id, attributes[attr_id], mock.ANY ) @pytest.mark.parametrize( ("attributes", "result", "failed"), [ ({4: "manufacturer"}, b"\x86\x04\x00", [4]), ({4: "manufacturer", 5: "model"}, b"\x86\x05\x00", [5]), ({4: "manufacturer", 5: "model"}, b"\x86\x04\x00\x86\x05\x00", [4, 5]), ( {4: "manufacturer", 5: "model", 3: 12}, b"\x86\x05\x00", [5], ), ( {4: "manufacturer", 5: "model", 3: 12}, b"\x86\x05\x00\x01\x03\x00", [5, 3], ), ( {4: "manufacturer", 5: "model", 3: 12}, b"\x02\x04\x00\x86\x05\x00\x01\x03\x00", [4, 5, 3], ), ], ) async def test_write_attributes_cache_failure(cluster, attributes, result, failed): listener = MagicMock() cluster.add_listener(listener) rsp_type = foundation.WriteAttributesResponse write_mock = AsyncMock(return_value=[rsp_type.deserialize(result)[0]]) with patch.object(cluster, "_write_attributes", write_mock): await cluster.write_attributes(attributes) assert cluster._write_attributes.call_count == 1 for attr_id in attributes: if attr_id in failed: assert attr_id not in cluster._attr_cache # Failed writes do not propagate with pytest.raises(AssertionError): listener.attribute_updated.assert_any_call( attr_id, attributes[attr_id] ) else: assert cluster._attr_cache[attr_id] == attributes[attr_id] listener.attribute_updated.assert_any_call( attr_id, attributes[attr_id], mock.ANY ) async def test_bind(cluster): result = await cluster.bind() cluster._endpoint.device.zdo.bind.assert_called_with(cluster=cluster) assert cluster._endpoint.device.zdo.bind.call_count == 1 assert result is cluster._endpoint.device.zdo.bind.return_value async def test_unbind(cluster): result = await cluster.unbind() cluster._endpoint.device.zdo.unbind.assert_called_with(cluster=cluster) assert cluster._endpoint.device.zdo.unbind.call_count == 1 assert result is cluster._endpoint.device.zdo.unbind.return_value async def test_configure_reporting(cluster): await cluster.configure_reporting(0, 10, 20, 1) async def test_configure_reporting_named(cluster): await cluster.configure_reporting("zcl_version", 10, 20, 1) assert cluster._endpoint.request.call_count == 1 async def test_configure_reporting_wrong_named(cluster): with pytest.raises(ValueError): await cluster.configure_reporting("wrong_attr_name", 10, 20, 1) assert cluster._endpoint.request.call_count == 0 async def test_configure_reporting_wrong_attrid(cluster): with pytest.raises(ValueError): await cluster.configure_reporting(0xABCD, 10, 20, 1) assert cluster._endpoint.request.call_count == 0 async def test_configure_reporting_manuf(): ep = MagicMock() cluster = zcl.Cluster.from_id(ep, 6) cluster.request = AsyncMock(name="request") await cluster.configure_reporting(0, 10, 20, 1) cluster.request.assert_called_with( True, 0x06, mock.ANY, mock.ANY, expect_reply=True, manufacturer=None, tsn=mock.ANY, ) cluster.request.reset_mock() manufacturer_id = 0xFCFC await cluster.configure_reporting(0, 10, 20, 1, manufacturer=manufacturer_id) cluster.request.assert_called_with( True, 0x06, mock.ANY, mock.ANY, expect_reply=True, manufacturer=manufacturer_id, tsn=mock.ANY, ) assert cluster.request.call_count == 1 @pytest.mark.parametrize( ("cluster_id", "attr", "data_type"), [ (0, "zcl_version", 0x20), (0, "model", 0x42), (0, "device_enabled", 0x10), (0, "alarm_mask", 0x18), (0x0202, "fan_mode", 0x30), ], ) async def test_configure_reporting_types(cluster_id, attr, data_type, cluster_by_id): cluster = cluster_by_id(cluster_id) await cluster.configure_reporting(attr, 0x1234, 0x2345, 0xAA) assert cluster._endpoint.reply.call_count == 0 assert cluster._endpoint.request.call_count == 1 assert cluster.endpoint.request.mock_calls[0].kwargs["data"][6] == data_type async def test_command(cluster): await cluster.command(0x00) assert cluster._endpoint.request.call_count == 1 assert cluster._endpoint.request.mock_calls[0].kwargs["sequence"] == DEFAULT_TSN async def test_command_override_tsn(cluster): await cluster.command(0x00, tsn=22) assert cluster._endpoint.request.call_count == 1 assert cluster._endpoint.request.mock_calls[0].kwargs["sequence"] == 22 async def test_command_attr(cluster): await cluster.reset_fact_default() assert cluster._endpoint.request.call_count == 1 async def test_client_command_attr(client_cluster): await client_cluster.query_specific_file_response(status=foundation.Status.SUCCESS) assert client_cluster._endpoint.reply.call_count == 1 async def test_command_invalid_attr(cluster): with pytest.raises(AttributeError): await cluster.no_such_command() async def test_invalid_arguments_cluster_command(cluster): with pytest.raises(TypeError): await cluster.command(0x00, 1) async def test_invalid_arguments_cluster_client_command(client_cluster): with pytest.raises(ValueError): await client_cluster.client_command( command_id=Ota.ClientCommandDefs.upgrade_end_response.id, manufacturer_code=0, image_type=0, # Missing: file_version, current_time, upgrade_time ) def test_name(cluster): assert cluster.name == "Basic" def test_commands(cluster): assert cluster.commands == [cluster.ServerCommandDefs.reset_fact_default] def test_general_command(cluster): cluster.request = MagicMock() cluster.reply = MagicMock() cmd_id = 0x0C cluster.general_command(cmd_id, sentinel.start, sentinel.items, manufacturer=0x4567) assert cluster.reply.call_count == 0 assert cluster.request.call_count == 1 cluster.request.assert_called_with( True, cmd_id, mock.ANY, sentinel.start, sentinel.items, expect_reply=True, manufacturer=0x4567, tsn=mock.ANY, ) def test_general_command_reply(cluster): cluster.request = MagicMock() cluster.reply = MagicMock() cmd_id = 0x0D cluster.general_command(cmd_id, True, [], manufacturer=0x4567) assert cluster.request.call_count == 0 assert cluster.reply.call_count == 1 cluster.reply.assert_called_with( True, cmd_id, mock.ANY, True, [], manufacturer=0x4567, tsn=None ) cluster.request.reset_mock() cluster.reply.reset_mock() cluster.general_command(cmd_id, True, [], manufacturer=0x4567, tsn=sentinel.tsn) assert cluster.request.call_count == 0 assert cluster.reply.call_count == 1 cluster.reply.assert_called_with( True, cmd_id, mock.ANY, True, [], manufacturer=0x4567, tsn=sentinel.tsn ) def test_handle_cluster_request_handler(cluster): hdr = foundation.ZCLHeader.cluster(123, 0x00) cluster.handle_cluster_request(hdr, [sentinel.arg1, sentinel.arg2]) async def test_handle_cluster_general_request_disable_default_rsp(endpoint): hdr, values = endpoint.deserialize( 0, b"\x18\xcd\x0a\x01\xff\x42\x25\x01\x21\x95\x0b\x04\x21\xa8\x43\x05\x21\x36\x00" b"\x06\x24\x02\x00\x05\x00\x00\x64\x29\xf8\x07\x65\x21\xd9\x0e\x66\x2b\x84\x87" b"\x01\x00\x0a\x21\x00\x00", ) cluster = endpoint.in_clusters[0] p1 = patch.object(cluster, "_update_attribute") p2 = patch.object(cluster, "general_command") with p1 as attr_lst_mock, p2 as general_cmd_mock: cluster.handle_cluster_general_request(hdr, values) await asyncio.sleep(0) assert attr_lst_mock.call_count > 0 assert general_cmd_mock.call_count == 0 with p1 as attr_lst_mock, p2 as general_cmd_mock: hdr.frame_control = hdr.frame_control.replace(disable_default_response=False) cluster.handle_cluster_general_request(hdr, values) await asyncio.sleep(0) assert attr_lst_mock.call_count > 0 assert general_cmd_mock.call_count == 1 assert general_cmd_mock.call_args[1]["tsn"] == hdr.tsn async def test_handle_cluster_general_request_not_attr_report(cluster): hdr = foundation.ZCLHeader.general(1, foundation.GeneralCommand.Write_Attributes) p1 = patch.object(cluster, "_update_attribute") p2 = patch.object(cluster, "create_catching_task") with p1 as attr_lst_mock, p2 as response_mock: cluster.handle_cluster_general_request(hdr, [1, 2, 3]) await asyncio.sleep(0) assert attr_lst_mock.call_count == 0 assert response_mock.call_count == 0 async def test_write_attributes_undivided(cluster): with patch.object(cluster, "request", new=AsyncMock()): i = cluster.write_attributes_undivided({0: 5, "app_version": 4}) await i assert cluster.request.call_count == 1 async def test_configure_reporting_multiple(cluster): await cluster.configure_reporting( attribute=3, min_interval=5, max_interval=15, reportable_change=20, manufacturer=0x2345, ) await cluster.configure_reporting_multiple( attributes={3: (5, 15, 20)}, manufacturer=0x2345 ) assert cluster.endpoint.request.call_count == 2 assert ( cluster.endpoint.request.mock_calls[0].kwargs["data"] == cluster.endpoint.request.mock_calls[2].kwargs["data"] ) async def test_configure_reporting_multiple_def_rsp(cluster): """Configure reporting returned a default response. May happen.""" cluster.endpoint.request.return_value = ( zcl.foundation.GeneralCommand.Configure_Reporting, zcl.foundation.Status.UNSUP_GENERAL_COMMAND, ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 1 assert cluster.unsupported_attributes == set() def _mk_cfg_rsp(responses: dict[int, zcl.foundation.Status]): """A helper to create a configure response record.""" cfg_response = zcl.foundation.ConfigureReportingResponse() for attrid, status in responses.items(): cfg_response.append( zcl.foundation.ConfigureReportingResponseRecord( status, zcl.foundation.ReportingDirection.ReceiveReports, attrid ) ) return [cfg_response] async def test_configure_reporting_multiple_single_success(cluster): """Configure reporting returned a single success response.""" cluster.endpoint.request.return_value = _mk_cfg_rsp( {0: zcl.foundation.Status.SUCCESS} ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 1 assert cluster.unsupported_attributes == set() async def test_configure_reporting_multiple_single_fail(cluster): """Configure reporting returned a single failure response.""" cluster.endpoint.request.return_value = _mk_cfg_rsp( {3: zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE} ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 1 assert cluster.unsupported_attributes == {"hw_version", 3} cluster.endpoint.request.return_value = _mk_cfg_rsp( {3: zcl.foundation.Status.SUCCESS} ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 2 assert cluster.unsupported_attributes == set() async def test_configure_reporting_multiple_single_unreportable(cluster): """Configure reporting returned a single failure response for unreportable attribute.""" cluster.endpoint.request.return_value = _mk_cfg_rsp( {4: zcl.foundation.Status.UNREPORTABLE_ATTRIBUTE} ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 1 assert cluster.unsupported_attributes == set() async def test_configure_reporting_multiple_both_unsupp(cluster): """Configure reporting returned unsupported attributes for both.""" cluster.endpoint.request.return_value = _mk_cfg_rsp( { 3: zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE, 4: zcl.foundation.Status.UNSUPPORTED_ATTRIBUTE, } ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 1 assert cluster.unsupported_attributes == {"hw_version", 3, "manufacturer", 4} cluster.endpoint.request.return_value = _mk_cfg_rsp( { 3: zcl.foundation.Status.SUCCESS, 4: zcl.foundation.Status.SUCCESS, } ) await cluster.configure_reporting_multiple( {3: (5, 15, 20), 4: (6, 16, 26)}, manufacturer=0x2345 ) assert cluster.endpoint.request.await_count == 2 assert cluster.unsupported_attributes == set() def test_unsupported_attr_add(cluster): """Test adding unsupported attributes.""" assert "manufacturer" not in cluster.unsupported_attributes assert 4 not in cluster.unsupported_attributes assert "model" not in cluster.unsupported_attributes assert 5 not in cluster.unsupported_attributes cluster.add_unsupported_attribute(4) assert "manufacturer" in cluster.unsupported_attributes assert 4 in cluster.unsupported_attributes cluster.add_unsupported_attribute("model") assert "model" in cluster.unsupported_attributes assert 5 in cluster.unsupported_attributes def test_unsupported_attr_add_no_reverse_attr_name(cluster): """Test adding unsupported attributes without corresponding reverse attr name.""" assert "no_such_attr" not in cluster.unsupported_attributes assert 0xDEED not in cluster.unsupported_attributes cluster.add_unsupported_attribute("no_such_attr") cluster.add_unsupported_attribute("no_such_attr") assert "no_such_attr" in cluster.unsupported_attributes cluster.add_unsupported_attribute(0xDEED) assert 0xDEED in cluster.unsupported_attributes def test_unsupported_attr_remove(cluster): """Test removing unsupported attributes.""" assert "manufacturer" not in cluster.unsupported_attributes assert 4 not in cluster.unsupported_attributes assert "model" not in cluster.unsupported_attributes assert 5 not in cluster.unsupported_attributes cluster.add_unsupported_attribute(4) assert "manufacturer" in cluster.unsupported_attributes assert 4 in cluster.unsupported_attributes cluster.add_unsupported_attribute("model") assert "model" in cluster.unsupported_attributes assert 5 in cluster.unsupported_attributes cluster.remove_unsupported_attribute(4) assert "manufacturer" not in cluster.unsupported_attributes assert 4 not in cluster.unsupported_attributes cluster.remove_unsupported_attribute("model") assert "model" not in cluster.unsupported_attributes assert 5 not in cluster.unsupported_attributes def test_unsupported_attr_remove_no_reverse_attr_name(cluster): """Test removing unsupported attributes without corresponding reverse attr name.""" assert "no_such_attr" not in cluster.unsupported_attributes assert 0xDEED not in cluster.unsupported_attributes cluster.add_unsupported_attribute("no_such_attr") assert "no_such_attr" in cluster.unsupported_attributes cluster.add_unsupported_attribute(0xDEED) assert 0xDEED in cluster.unsupported_attributes cluster.remove_unsupported_attribute("no_such_attr") assert "no_such_attr" not in cluster.unsupported_attributes cluster.remove_unsupported_attribute(0xDEED) assert 0xDEED not in cluster.unsupported_attributes def test_zcl_command_duplicate_name_prevention(): assert 0x1234 not in zcl.clusters.CLUSTERS_BY_ID with pytest.raises(TypeError): class TestCluster(zcl.Cluster): cluster_id = 0x1234 ep_attribute = "test_cluster" server_commands = { 0x00: foundation.ZCLCommandDef( name="command1", schema={}, direction=False ), 0x01: foundation.ZCLCommandDef( name="command1", schema={}, direction=False ), } def test_zcl_attridx_deprecation(cluster): with pytest.deprecated_call(): cluster.attridx # noqa: B018 with pytest.deprecated_call(): assert cluster.attridx is cluster.attributes_by_name def test_zcl_response_type_tuple_like(): req = ( zcl.clusters.general.OnOff(None) .commands_by_name["on_with_timed_off"] .schema( on_off_control=0, on_time=1, off_wait_time=2, ) ) on_off_control, on_time, off_wait_time = req assert req.on_off_control == on_off_control == req[0] == 0 assert req.on_time == on_time == req[1] == 1 assert req.off_wait_time == off_wait_time == req[2] == 2 assert req == (0, 1, 2) assert req == req # noqa: PLR0124 assert req == req.replace() async def test_zcl_request_direction(): """Test that the request header's `direction` field is properly set.""" dev = MagicMock() ep = zigpy.endpoint.Endpoint(dev, 1) ep._device.get_sequence.return_value = DEFAULT_TSN ep.device.get_sequence.return_value = DEFAULT_TSN ep.request = AsyncMock() ep.add_input_cluster(zcl.clusters.general.OnOff.cluster_id) ep.add_input_cluster(zcl.clusters.lighting.Color.cluster_id) ep.add_output_cluster(zcl.clusters.general.OnOff.cluster_id) # Input cluster await ep.in_clusters[zcl.clusters.general.OnOff.cluster_id].on() hdr1, _ = foundation.ZCLHeader.deserialize(ep.request.mock_calls[0].kwargs["data"]) assert hdr1.direction == foundation.Direction.Client_to_Server ep.request.reset_mock() # Output cluster await ep.out_clusters[zcl.clusters.general.OnOff.cluster_id].on() hdr2, _ = foundation.ZCLHeader.deserialize(ep.request.mock_calls[0].kwargs["data"]) assert hdr2.direction == foundation.Direction.Server_to_Client # Color cluster that also uses `direction` as a kwarg await ep.light_color.move_to_hue( hue=0, direction=zcl.clusters.lighting.Color.Direction.Shortest_distance, transition_time=10, ) async def test_zcl_reply_direction(app_mock): """Test that the reply header's `direction` field is properly set.""" dev = zigpy.device.Device( application=app_mock, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), nwk=0x1234, ) dev._send_sequence = DEFAULT_TSN ep = dev.add_endpoint(1) ep.add_input_cluster(zcl.clusters.general.OnOff.cluster_id) hdr = foundation.ZCLHeader( frame_control=foundation.FrameControl( frame_type=foundation.FrameType.GLOBAL_COMMAND, is_manufacturer_specific=0, direction=foundation.Direction.Server_to_Client, disable_default_response=0, reserved=0, ), tsn=87, command_id=foundation.GeneralCommand.Report_Attributes, ) attr = zcl.foundation.Attribute() attr.attrid = zcl.clusters.general.OnOff.AttributeDefs.on_off.id attr.value = zcl.foundation.TypeValue() attr.value.value = t.Bool.true cmd = foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Report_Attributes ].schema([attr]) ep.handle_message( profile=260, cluster=zcl.clusters.general.OnOff.cluster_id, hdr=hdr, args=cmd, ) await asyncio.sleep(0.1) packet = app_mock.send_packet.mock_calls[0].args[0] assert packet.cluster_id == zcl.clusters.general.OnOff.cluster_id # The direction is correct packet_hdr, _ = foundation.ZCLHeader.deserialize(packet.data.serialize()) assert packet_hdr.direction == foundation.Direction.Client_to_Server async def test_zcl_cluster_definition_backwards_compatibility(): class TestCluster(zcl.Cluster): cluster_id = 0xABCD ep_attribute = "test_cluster" attributes = { 0x1234: ("attribute", t.uint8_t), 0x1235: ("attribute2", t.uint32_t, True), } server_commands = { 0x00: ("server_command", (t.uint8_t,), True), } client_commands = { 0x01: ("client_command", (t.uint8_t, t.uint16_t), False), } assert TestCluster.cluster_id == 0xABCD assert TestCluster.AttributeDefs.attribute.id == 0x1234 assert TestCluster.AttributeDefs.attribute.type == t.uint8_t assert TestCluster.AttributeDefs.attribute.is_manufacturer_specific is False assert TestCluster.AttributeDefs.attribute2.id == 0x1235 assert TestCluster.AttributeDefs.attribute2.type == t.uint32_t assert TestCluster.AttributeDefs.attribute2.is_manufacturer_specific is True assert TestCluster.ServerCommandDefs.server_command.id == 0x00 assert len(TestCluster.ServerCommandDefs.server_command.schema.fields) == 1 assert ( TestCluster.ServerCommandDefs.server_command.schema.fields.param1.type == t.uint8_t ) assert TestCluster.ClientCommandDefs.client_command.id == 0x01 assert len(TestCluster.ClientCommandDefs.client_command.schema.fields) == 2 assert ( TestCluster.ClientCommandDefs.client_command.schema.fields.param1.type == t.uint8_t ) assert ( TestCluster.ClientCommandDefs.client_command.schema.fields.param2.type == t.uint16_t ) async def test_zcl_cluster_definition_invalid_name(): # This is fine class TestCluster(zcl.Cluster): cluster_id = 0xABCD ep_attribute = "test_cluster" class AttributeDefs(zcl.BaseAttributeDefs): upgrade_server_id = foundation.ZCLAttributeDef( name="upgrade_server_id", id=0x0000, type=t.EUI64, access="r", mandatory=True, ) class ServerCommandDefs(zcl.BaseCommandDefs): upgrade_end = foundation.ZCLCommandDef( name="upgrade_end", id=0x06, schema={ "status": foundation.Status, "manufacturer_code": t.uint16_t, "image_type": t.uint16_t, "file_version": t.uint32_t, }, direction=foundation.Direction.Client_to_Server, ) # This is not with pytest.raises(TypeError): class TestCluster(zcl.Cluster): cluster_id = 0xABCD ep_attribute = "test_cluster" class AttributeDefs(zcl.BaseAttributeDefs): upgrade_server_id = foundation.ZCLAttributeDef( name="some_other_name", id=0x0000, type=t.EUI64, access="r", mandatory=True, ) # Nor is this with pytest.raises(TypeError): class TestCluster(zcl.Cluster): cluster_id = 0xABCD ep_attribute = "test_cluster" class ServerCommandDefs(zcl.BaseCommandDefs): upgrade_end = foundation.ZCLCommandDef( name="some_other_name", id=0x06, schema={ "status": foundation.Status, "manufacturer_code": t.uint16_t, "image_type": t.uint16_t, "file_version": t.uint32_t, }, direction=foundation.Direction.Client_to_Server, ) zigpy-0.80.1/tests/test_zcl_clusters.py000066400000000000000000000524321501451476000202420ustar00rootroot00000000000000from __future__ import annotations import asyncio from datetime import datetime, timezone import re from typing import Any from zoneinfo import ZoneInfo import pytest from zigpy import device, types, zcl import zigpy.endpoint from zigpy.ota import OtaImagesResult from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Basic, Ota, Time import zigpy.zcl.clusters.security as sec from zigpy.zdo import types as zdo_t from .async_mock import AsyncMock, MagicMock, call, patch, sentinel IMAGE_SIZE = 0x2345 IMAGE_OFFSET = 0x2000 def test_registry(): for cluster_id, cluster in zcl.Cluster._registry.items(): assert 0 <= getattr(cluster, "cluster_id", -1) <= 65535 assert cluster_id == cluster.cluster_id assert issubclass(cluster, zcl.Cluster) def test_attributes(): for cluster in zcl.Cluster._registry.values(): for attrid, attr in cluster.attributes.items(): assert 0 <= attrid <= 0xFFFF assert isinstance(attr, zcl.foundation.ZCLAttributeDef) assert attr.id == attrid assert attr.name assert attr.type assert callable(attr.type.deserialize) assert callable(attr.type.serialize) def _test_commands(cmdattr): for cluster in zcl.Cluster._registry.values(): for cmdid, cmdspec in getattr(cluster, cmdattr).items(): assert 0 <= cmdid <= 0xFF assert cmdspec.id == cmdid assert isinstance(cmdspec, zcl.foundation.ZCLCommandDef) assert issubclass(cmdspec.schema, types.Struct) for field in cmdspec.schema.fields: assert callable(field.type.deserialize) assert callable(field.type.serialize) def test_server_commands(): _test_commands("server_commands") def test_client_commands(): _test_commands("client_commands") def test_ep_attributes(): seen = set() for cluster in zcl.Cluster._registry.values(): assert isinstance(cluster.ep_attribute, str) assert re.match(r"^[a-z_][a-z0-9_]*$", cluster.ep_attribute) assert cluster.ep_attribute not in seen seen.add(cluster.ep_attribute) ep = zigpy.endpoint.Endpoint(None, 1) assert not hasattr(ep, cluster.ep_attribute) async def read_attributes(cluster, attribute_ids: list[int]) -> dict[int, Any]: schema = foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes ].schema hdr, _ = cluster._create_request( general=True, command_id=foundation.GeneralCommand.Read_Attributes, schema=schema, disable_default_response=False, direction=foundation.Direction.Client_to_Server, args=(), kwargs={"attribute_ids": attribute_ids}, ) command = schema(attribute_ids=attribute_ids) with patch.object(cluster, "reply") as reply_mock: cluster.handle_message(hdr, command) call = reply_mock.mock_calls[0] return call.args[2](call.args[3]) async def test_basic_cluster(): ep = MagicMock() ep.reply = AsyncMock() cluster = Basic(ep) rsp = await read_attributes( cluster, [ Basic.AttributeDefs.zcl_version.id, Basic.AttributeDefs.power_source.id, Basic.AttributeDefs.serial_number.id, ], ) assert rsp.status_records[0] == foundation.ReadAttributeRecord( attrid=Basic.AttributeDefs.zcl_version.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.uint8, value=8, ), ) assert rsp.status_records[1] == foundation.ReadAttributeRecord( attrid=Basic.AttributeDefs.power_source.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.enum8, value=Basic.PowerSource.DC_Source, ), ) assert rsp.status_records[2] == foundation.ReadAttributeRecord( attrid=Basic.AttributeDefs.serial_number.id, status=foundation.Status.UNSUPPORTED_ATTRIBUTE, ) async def test_time_cluster(): ep = MagicMock() ep.reply = AsyncMock() cluster = Time(ep) Read_Attributes_rsp = foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Read_Attributes_rsp ].schema # Datetime objects need to be subclassed to be patched so we may as well implement # the patches directly class PatchedDatetime(datetime): _fake_now = datetime( 2000, 1, 2, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") ) def astimezone(self): return self.replace(tzinfo=self._fake_now.tzinfo) @classmethod def now(cls, tzinfo=None): if tzinfo is None: return cls( cls._fake_now.year, cls._fake_now.month, cls._fake_now.day, cls._fake_now.hour, cls._fake_now.minute, cls._fake_now.second, ) else: assert tzinfo is timezone.utc return ( cls( cls._fake_now.year, cls._fake_now.month, cls._fake_now.day, cls._fake_now.hour, cls._fake_now.minute, cls._fake_now.second, tzinfo=tzinfo, ) - cls._fake_now.utcoffset() ) with patch("zigpy.zcl.clusters.general.datetime", PatchedDatetime): # Supported attributes rsp1 = await read_attributes( cluster, [ Time.AttributeDefs.time.id, Time.AttributeDefs.time_status.id, Time.AttributeDefs.time_zone.id, Time.AttributeDefs.local_time.id, ], ) assert rsp1.status_records[0] == foundation.ReadAttributeRecord( attrid=Time.AttributeDefs.time.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.UTC, # One day from the epoch, plus time zone offset value=24 * 60 * 60 + 8 * 60 * 60, ), ) assert rsp1.status_records[1] == foundation.ReadAttributeRecord( attrid=Time.AttributeDefs.time_status.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.map8, value=( Time.TimeStatus.Master | Time.TimeStatus.Synchronized | Time.TimeStatus.Master_for_Zone_and_DST ), ), ) assert rsp1.status_records[2] == foundation.ReadAttributeRecord( attrid=Time.AttributeDefs.time_zone.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.int32, # Time zone offset value=-(8 * 60 * 60), ), ) assert rsp1.status_records[3] == foundation.ReadAttributeRecord( attrid=Time.AttributeDefs.local_time.id, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=foundation.DataTypeId.uint32, # One day from the epoch, as on the clock value=24 * 60 * 60, ), ) # Unsupported rsp2 = await read_attributes(cluster, [0xABCD]) assert rsp2 == Read_Attributes_rsp( status_records=[ foundation.ReadAttributeRecord( attrid=0xABCD, status=foundation.Status.UNSUPPORTED_ATTRIBUTE, ) ] ) @pytest.fixture def dev(monkeypatch, app_mock): monkeypatch.setattr(device, "APS_REPLY_TIMEOUT_EXTENDED", 0.1) ieee = types.EUI64(map(types.uint8_t, [0, 1, 2, 3, 4, 5, 6, 7])) dev = device.Device(app_mock, ieee, 65535) node_desc = zdo_t.NodeDescriptor(1, 1, 1, 4, 5, 6, 7, 8) with patch.object( dev.zdo, "Node_Desc_req", new=AsyncMock(return_value=(0, 0xFFFF, node_desc)) ): yield dev @pytest.fixture def ota_cluster(dev): ep = dev.add_endpoint(1) cluster = zcl.Cluster._registry[0x0019](ep) with ( patch.object(cluster, "reply", AsyncMock()), patch.object(cluster, "request", AsyncMock()), ): yield cluster async def test_ota_handle_cluster_req_wrapper(ota_cluster, caplog): ota_cluster._handle_query_next_image = AsyncMock() hdr = zigpy.zcl.foundation.ZCLHeader.cluster(123, 0x01) ota_cluster.handle_cluster_request(hdr, [sentinel.args]) assert ota_cluster._handle_query_next_image.call_count == 1 assert ota_cluster._handle_query_next_image.mock_calls[0].args == ( hdr, [sentinel.args], ) ota_cluster._handle_query_next_image.reset_mock() # This command isn't currently handled hdr.command_id = 0x08 ota_cluster.handle_cluster_request(hdr, [sentinel.just_args]) assert ota_cluster._handle_query_next_image.call_count == 0 async def test_ota_handle_query_next_image(ota_cluster): dev = ota_cluster.endpoint.device ota_cluster.query_next_image_response = AsyncMock() dev.ota_in_progress = False listener = MagicMock() dev.add_listener(listener) # TODO: get rid of `sentinel` and mock the actual command hdr = zigpy.zcl.foundation.ZCLHeader.cluster( tsn=0x12, command_id=Ota.ServerCommandDefs.query_next_image.id ) cmd = MagicMock() # No image is available dev.application.ota.get_ota_images = AsyncMock( return_value=OtaImagesResult(upgrades=(), downgrades=()) ) ota_cluster.handle_cluster_request(hdr, cmd) await asyncio.sleep(0) assert ota_cluster.query_next_image_response.mock_calls == [ call(zcl.foundation.Status.NO_IMAGE_AVAILABLE, tsn=hdr.tsn) ] assert listener.device_ota_image_query_result.mock_calls == [ call(OtaImagesResult(upgrades=(), downgrades=()), cmd) ] ota_cluster.query_next_image_response.reset_mock() listener.device_ota_image_query_result.reset_mock() # Now one is available img = MagicMock() dev.application.ota.get_ota_images = AsyncMock( return_value=OtaImagesResult(upgrades=(img,), downgrades=()) ) ota_cluster.handle_cluster_request(hdr, cmd) await asyncio.sleep(0) assert ota_cluster.query_next_image_response.mock_calls == [ call(zcl.foundation.Status.NO_IMAGE_AVAILABLE, tsn=hdr.tsn) ] assert listener.device_ota_image_query_result.mock_calls == [ call(OtaImagesResult(upgrades=(img,), downgrades=()), cmd) ] async def test_ota_handle_image_block_req(ota_cluster): dev = ota_cluster.endpoint.device ota_cluster.image_block_response = AsyncMock() dev.ota_in_progress = False hdr = zigpy.zcl.foundation.ZCLHeader.cluster( tsn=0x12, command_id=Ota.ServerCommandDefs.image_block.id ) cmd = MagicMock() # Stop the upgrade, none is in progress ota_cluster.handle_cluster_request(hdr, cmd) await asyncio.sleep(0) assert ota_cluster.image_block_response.mock_calls == [ call(zcl.foundation.Status.ABORT, tsn=hdr.tsn) ] ota_cluster.image_block_response.reset_mock() # If we flip the progress flag, send nothing dev.ota_in_progress = True ota_cluster.handle_cluster_request(hdr, cmd) await asyncio.sleep(0) assert ota_cluster.image_block_response.mock_calls == [] def test_ias_zone_type(): extra = b"\xaa\x55" zone, rest = sec.IasZone.ZoneType.deserialize(b"\x0d\x00" + extra) assert rest == extra assert zone is sec.IasZone.ZoneType.Motion_Sensor zone, rest = sec.IasZone.ZoneType.deserialize(b"\x81\x81" + extra) assert rest == extra assert zone.name.startswith("manufacturer_specific") assert zone.value == 0x8181 def test_ias_ace_audible_notification(): extra = b"\xaa\x55" notification_type, rest = sec.IasAce.AudibleNotification.deserialize( b"\x00" + extra ) assert rest == extra assert notification_type is sec.IasAce.AudibleNotification.Mute notification_type, rest = sec.IasAce.AudibleNotification.deserialize( b"\x81" + extra ) assert rest == extra assert notification_type.name.startswith("manufacturer_specific") assert notification_type.value == 0x81 def test_basic_cluster_power_source(): extra = b"The rest of the owl\xaa\x55" pwr_src, rest = zcl.clusters.general.Basic.PowerSource.deserialize(b"\x81" + extra) assert rest == extra assert pwr_src == zcl.clusters.general.Basic.PowerSource.Mains_single_phase assert pwr_src == 0x01 assert pwr_src.value == 0x01 assert pwr_src.battery_backup @pytest.mark.parametrize( ("raw", "mode", "name"), [ (0x00, 0, "Stop"), (0x01, 0, "Stop"), (0x02, 0, "Stop"), (0x03, 0, "Stop"), (0x30, 3, "Emergency"), (0x31, 3, "Emergency"), (0x32, 3, "Emergency"), (0x33, 3, "Emergency"), ], ) def test_security_iaswd_warning_mode(raw, mode, name): """Test warning command class of IasWD cluster.""" def _test(warning, data): assert warning.serialize() == data assert warning == raw assert warning.mode == mode assert warning.mode.name == name warning.mode = mode assert warning.serialize() == data assert warning.mode == mode data = types.uint8_t(raw).serialize() _test(sec.IasWd.Warning(raw), data) extra = b"The rest of the owl\xaa\x55" warn, rest = sec.IasWd.Warning.deserialize(data + extra) assert rest == extra _test(warn, data) repr(warn) def test_security_iaswd_warning_mode_2(): """Test warning command class of IasWD cluster.""" def _test(data, raw, mode, name): warning, _ = sec.IasWd.Warning.deserialize(data) assert warning.serialize() == data assert warning == raw assert warning.mode == mode assert warning.mode.name == name warning.mode = mode assert warning.serialize() == data assert warning.mode == mode for mode in sec.IasWd.Warning.WarningMode: for other in range(16): raw = mode << 4 | other data = types.uint8_t(raw).serialize() _test(data, raw, mode.value, mode.name) def test_security_iaswd_warning_strobe(): """Test strobe of warning command class of IasWD cluster.""" for strobe in sec.IasWd.Warning.Strobe: for mode in range(16): for siren in range(4): raw = mode << 4 | siren raw |= strobe.value << 2 data = types.uint8_t(raw).serialize() warning, _ = sec.IasWd.Warning.deserialize(data) assert warning.serialize() == data assert warning == raw assert warning.strobe == strobe.value assert warning.strobe.name == strobe.name warning.strobe = strobe assert warning.serialize() == data assert warning.strobe == strobe.value def test_security_iaswd_warning_siren(): """Test siren of warning command class of IasWD cluster.""" for siren in sec.IasWd.Warning.SirenLevel: for mode in range(16): for strobe in range(4): raw = mode << 4 | (strobe << 2) raw |= siren.value data = types.uint8_t(raw).serialize() warning, _ = sec.IasWd.Warning.deserialize(data) assert warning.serialize() == data assert warning == raw assert warning.level == siren.value assert warning.level.name == siren.name warning.level = siren assert warning.serialize() == data assert warning.level == siren.value @pytest.mark.parametrize( ("raw", "mode", "name"), [ (0x00, 0, "Armed"), (0x01, 0, "Armed"), (0x02, 0, "Armed"), (0x03, 0, "Armed"), (0x10, 1, "Disarmed"), (0x11, 1, "Disarmed"), (0x12, 1, "Disarmed"), (0x13, 1, "Disarmed"), ], ) def test_security_iaswd_squawk_mode(raw, mode, name): """Test squawk command class of IasWD cluster.""" def _test(squawk, data): assert squawk.serialize() == data assert squawk == raw assert squawk.mode == mode assert squawk.mode.name == name squawk.mode = mode assert squawk.serialize() == data assert squawk.mode == mode data = types.uint8_t(raw).serialize() _test(sec.IasWd.Squawk(raw), data) extra = b"The rest of the owl\xaa\x55" squawk, rest = sec.IasWd.Squawk.deserialize(data + extra) assert rest == extra _test(squawk, data) repr(squawk) def test_security_iaswd_squawk_strobe(): """Test strobe of squawk command class of IasWD cluster.""" for strobe in sec.IasWd.Squawk.Strobe: for mode in range(16): for level in range(4): raw = mode << 4 | level raw |= strobe.value << 3 data = types.uint8_t(raw).serialize() squawk, _ = sec.IasWd.Squawk.deserialize(data) assert squawk.serialize() == data assert squawk == raw assert squawk.strobe == strobe.value assert squawk.strobe == strobe assert squawk.strobe.name == strobe.name squawk.strobe = strobe assert squawk.serialize() == data assert squawk.strobe == strobe def test_security_iaswd_squawk_level(): """Test level of squawk command class of IasWD cluster.""" for level in sec.IasWd.Squawk.SquawkLevel: for other in range(64): raw = other << 2 | level.value data = types.uint8_t(raw).serialize() squawk, _ = sec.IasWd.Squawk.deserialize(data) assert squawk.serialize() == data assert squawk == raw assert squawk.level == level.value assert squawk.level == level assert squawk.level.name == level.name squawk.level = level assert squawk.serialize() == data assert squawk.level == level def test_hvac_thermostat_system_type(): """Test system_type class.""" hvac = zcl.clusters.hvac sys_type = hvac.Thermostat.SystemType(0x00) assert sys_type.cooling_system_stage == hvac.CoolingSystemStage.Cool_Stage_1 assert sys_type.heating_system_stage == hvac.HeatingSystemStage.Heat_Stage_1 assert sys_type.heating_fuel_source == hvac.HeatingFuelSource.Electric assert sys_type.heating_system_type == hvac.HeatingSystemType.Conventional sys_type = hvac.Thermostat.SystemType(0x35) assert sys_type.cooling_system_stage == hvac.CoolingSystemStage.Cool_Stage_2 assert sys_type.heating_system_stage == hvac.HeatingSystemStage.Heat_Stage_2 assert sys_type.heating_fuel_source == hvac.HeatingFuelSource.Gas assert sys_type.heating_system_type == hvac.HeatingSystemType.Heat_Pump @patch("zigpy.zcl.Cluster.send_default_rsp") async def test_ias_zone(send_rsp_mock): """Test sending default response on zone status notification.""" ep = MagicMock() ep.reply = AsyncMock() t = zcl.Cluster._registry[sec.IasZone.cluster_id](ep, is_server=False) # suppress default response hdr, args = t.deserialize(b"\tK\x00&\x00\x00\x00\x00\x00") hdr.frame_control = hdr.frame_control.replace(disable_default_response=True) t.handle_message(hdr, args) assert send_rsp_mock.call_count == 0 # this should generate a default response hdr.frame_control = hdr.frame_control.replace(disable_default_response=False) t.handle_message(hdr, args) assert send_rsp_mock.call_count == 0 t = zcl.Cluster._registry[sec.IasZone.cluster_id](ep, is_server=True) # suppress default response hdr, args = t.deserialize(b"\tK\x00&\x00\x00\x00\x00\x00") hdr.frame_control = hdr.frame_control.replace(disable_default_response=True) t.handle_message(hdr, args) assert send_rsp_mock.call_count == 0 # this should generate a default response hdr.frame_control = hdr.frame_control.replace(disable_default_response=False) t.handle_message(hdr, args) assert send_rsp_mock.call_count == 1 def test_ota_image_block_field_control(): """Test OTA image_block with field control deserializes properly.""" data = bytes.fromhex("01d403020b101d01001f000100000000400000") ep = MagicMock() cluster = zcl.clusters.general.Ota(ep) hdr, response = cluster.deserialize(data) assert hdr.serialize() + response.serialize() == data image_block = cluster.commands_by_name["image_block"].schema assert response == image_block( field_control=image_block.FieldControl.MinimumBlockPeriod, manufacturer_code=4107, image_type=285, file_version=0x01001F00, file_offset=0, maximum_data_size=64, minimum_block_period=0, ) assert response.request_node_addr is None def test_general_analog_in_application_type(): """Test AnalogInput General Cluster, Application Type Attribute.""" app_type = zcl.clusters.general_const.ApplicationType( 0x00_01_0007 ) # Group 0x00, Type 0x01, Application 0x0007 assert app_type.group == 0x00 assert ( app_type.type == zcl.clusters.general_const.AnalogInputType.Relative_Humidity_Percent ) assert ( app_type.index == zcl.clusters.general_const.RelativeHumidityPercent.Space_Humidity ) zigpy-0.80.1/tests/test_zcl_foundation.py000066400000000000000000000627411501451476000205500ustar00rootroot00000000000000import logging import pytest import zigpy.types as t from zigpy.zcl import foundation def test_typevalue(): tv = foundation.TypeValue() tv.type = 0x20 tv.value = t.uint8_t(99) ser = tv.serialize() r = repr(tv) assert r.startswith("TypeValue(") and r.endswith(")") assert "type=uint8_t" in r assert "value=99" in r tv2, data = foundation.TypeValue.deserialize(ser) assert data == b"" assert tv2.type == tv.type assert tv2.value == tv.value tv3 = foundation.TypeValue(tv2) assert tv3.type == tv.type assert tv3.value == tv.value assert tv3 == tv2 tv4 = foundation.TypeValue() tv4.type = 0x42 tv4.value = t.CharacterString("test") assert "CharacterString" in str(tv4) assert "'test'" in str(tv4) tv5 = foundation.TypeValue() tv5.type = 0x42 tv5.value = t.CharacterString("test") assert tv5 == tv5 # noqa: PLR0124 assert tv5 == tv4 assert tv5 != tv3 def test_read_attribute_record(): orig = b"\x00\x00\x00\x20\x99" rar, data = foundation.ReadAttributeRecord.deserialize(orig) assert data == b"" assert rar.status == 0 assert isinstance(rar.value, foundation.TypeValue) assert isinstance(rar.value.value, t.uint8_t) assert rar.value.value == 0x99 r = repr(rar) assert len(r) > 5 assert repr(foundation.Status.SUCCESS) in r ser = rar.serialize() assert ser == orig def test_attribute_reporting_config_0(): arc = foundation.AttributeReportingConfig() arc.direction = foundation.ReportingDirection.SendReports arc.attrid = 99 arc.datatype = 0x20 arc.min_interval = 10 arc.max_interval = 20 arc.reportable_change = 30 ser = arc.serialize() arc2, data = foundation.AttributeReportingConfig.deserialize(ser) assert data == b"" assert arc2.direction == arc.direction assert arc2.attrid == arc.attrid assert arc2.datatype == arc.datatype assert arc2.min_interval == arc.min_interval assert arc2.max_interval == arc.max_interval assert arc.reportable_change == arc.reportable_change assert repr(arc) assert repr(arc) == repr(arc2) def test_attribute_reporting_config_1(): arc = foundation.AttributeReportingConfig() arc.direction = 1 arc.attrid = 99 arc.timeout = 0x7E ser = arc.serialize() arc2, data = foundation.AttributeReportingConfig.deserialize(ser) assert data == b"" assert arc2.direction == arc.direction assert arc2.timeout == arc.timeout assert repr(arc) def test_attribute_reporting_config_only_dir_and_attrid(): arc = foundation.AttributeReportingConfig() arc.direction = foundation.ReportingDirection.SendReports arc.attrid = 99 ser = arc.serialize(_only_dir_and_attrid=True) arc2, data = foundation.AttributeReportingConfig.deserialize( ser, _only_dir_and_attrid=True ) assert data == b"" assert arc2.direction == arc.direction assert arc2.attrid == arc.attrid assert repr(arc) assert repr(arc) == repr(arc2) def test_attribute_reporting_config_bad_datatype(caplog): arc = foundation.AttributeReportingConfig() arc.direction = foundation.ReportingDirection.SendReports arc.attrid = 99 arc.datatype = 0xFE # unknown arc.min_interval = 10 arc.max_interval = 20 arc.reportable_change = 30 with caplog.at_level(logging.WARNING): arc.serialize() assert "Unknown ZCL type" in caplog.text arc2 = foundation.AttributeReportingConfig() arc2.direction = foundation.ReportingDirection.SendReports arc2.attrid = 99 arc2.datatype = 0xFE # unknown arc2.min_interval = 10 arc2.max_interval = 20 # Missing the reportable change, since it can't be set assert arc.serialize() == arc2.serialize() caplog.clear() with caplog.at_level(logging.WARNING): arc3, data = foundation.AttributeReportingConfig.deserialize(arc.serialize()) assert "Unknown ZCL type" in caplog.text assert arc3.serialize() == arc.serialize() def test_write_attribute_status_record(): attr_id = b"\x01\x00" extra = b"12da-" res, d = foundation.WriteAttributesStatusRecord.deserialize( b"\x00" + attr_id + extra ) assert res.status == foundation.Status.SUCCESS assert res.attrid is None assert d == attr_id + extra r = repr(res) assert r.startswith(foundation.WriteAttributesStatusRecord.__name__) assert "status" in r assert "attrid" not in r res, d = foundation.WriteAttributesStatusRecord.deserialize( b"\x87" + attr_id + extra ) assert res.status == foundation.Status.INVALID_VALUE assert res.attrid == 0x0001 assert d == extra r = repr(res) assert "status" in r assert "attrid" in r rec = foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS, 0xAABB) assert rec.serialize() == b"\x00" rec.status = foundation.Status.UNSUPPORTED_ATTRIBUTE assert rec.serialize()[0:1] == foundation.Status.UNSUPPORTED_ATTRIBUTE.serialize() assert rec.serialize()[1:] == b"\xbb\xaa" def test_configure_reporting_response_serialization(): # success status only res, d = foundation.ConfigureReportingResponseRecord.deserialize(b"\x00") assert res.status == foundation.Status.SUCCESS assert res.direction is None assert res.attrid is None assert d == b"" # success + direction and attr id direction_attr_id = b"\x00\x01\x10" extra = b"12da-" res, d = foundation.ConfigureReportingResponseRecord.deserialize( b"\x00" + direction_attr_id + extra ) assert res.status == foundation.Status.SUCCESS assert res.direction is foundation.ReportingDirection.SendReports assert res.attrid == 0x1001 assert d == extra r = repr(res) assert r.startswith(foundation.ConfigureReportingResponseRecord.__name__ + "(") assert "status" in r assert "direction" not in r assert "attrid" not in r # failure record deserialization res, d = foundation.ConfigureReportingResponseRecord.deserialize( b"\x8c" + direction_attr_id + extra ) assert res.status == foundation.Status.UNREPORTABLE_ATTRIBUTE assert res.direction is not None assert res.attrid == 0x1001 assert d == extra r = repr(res) assert "status" in r assert "direction" in r assert "attrid" in r # successful record serializes only Status rec = foundation.ConfigureReportingResponseRecord( foundation.Status.SUCCESS, 0x00, 0xAABB ) assert rec.serialize() == b"\x00" rec.status = foundation.Status.UNREPORTABLE_ATTRIBUTE assert rec.serialize()[0:1] == foundation.Status.UNREPORTABLE_ATTRIBUTE.serialize() assert rec.serialize()[1:] == b"\x00\xbb\xaa" def test_status_undef(): data = b"\xff" extra = b"extra" status, rest = foundation.Status.deserialize(data + extra) assert rest == extra assert status == 0xFF assert status.value == 0xFF assert status.name == "undefined_0xff" assert isinstance(status, foundation.Status) def test_frame_control(): """Test FrameControl frame_type.""" extra = b"abcd\xaa\x55" frc, rest = foundation.FrameControl.deserialize(b"\x00" + extra) assert rest == extra assert frc.frame_type == foundation.FrameType.GLOBAL_COMMAND frc, rest = foundation.FrameControl.deserialize(b"\x01" + extra) assert rest == extra assert frc.frame_type == foundation.FrameType.CLUSTER_COMMAND r = repr(frc) assert isinstance(r, str) def test_frame_control_general(): frc = foundation.FrameControl.general( direction=foundation.Direction.Client_to_Server ) assert frc.is_cluster is False assert frc.is_general is True data = frc.serialize() assert data == b"\x00" assert not frc.is_manufacturer_specific frc = frc.replace(is_manufacturer_specific=False) assert frc.serialize() == b"\x00" frc = frc.replace(is_manufacturer_specific=True) assert frc.serialize() == b"\x04" frc = foundation.FrameControl.general( direction=foundation.Direction.Client_to_Server ) assert frc.direction == foundation.Direction.Client_to_Server assert frc.serialize() == b"\x00" frc = frc.replace(direction=foundation.Direction.Server_to_Client) assert frc.serialize() == b"\x08" assert ( foundation.FrameControl.general( direction=foundation.Direction.Server_to_Client ).serialize() == b"\x18" ) frc = foundation.FrameControl.general( direction=foundation.Direction.Client_to_Server ) assert not frc.disable_default_response assert frc.serialize() == b"\x00" frc = frc.replace(disable_default_response=False) assert frc.serialize() == b"\x00" frc = frc.replace(disable_default_response=True) assert frc.serialize() == b"\x10" def test_frame_control_cluster(): frc = foundation.FrameControl.cluster( direction=foundation.Direction.Client_to_Server ) assert frc.is_cluster is True assert frc.is_general is False data = frc.serialize() assert data == b"\x01" assert not frc.is_manufacturer_specific frc = frc.replace(is_manufacturer_specific=False) assert frc.serialize() == b"\x01" frc = frc.replace(is_manufacturer_specific=True) assert frc.serialize() == b"\x05" frc = foundation.FrameControl.cluster( direction=foundation.Direction.Client_to_Server ) assert frc.direction == foundation.Direction.Client_to_Server assert frc.serialize() == b"\x01" frc = frc.replace(direction=foundation.Direction.Client_to_Server) assert frc.serialize() == b"\x01" frc = frc.replace(direction=foundation.Direction.Server_to_Client) assert frc.serialize() == b"\x09" assert ( foundation.FrameControl.cluster( direction=foundation.Direction.Server_to_Client ).serialize() == b"\x19" ) frc = foundation.FrameControl.cluster( direction=foundation.Direction.Client_to_Server ) assert not frc.disable_default_response assert frc.serialize() == b"\x01" frc = frc.replace(disable_default_response=False) assert frc.serialize() == b"\x01" frc = frc.replace(disable_default_response=True) assert frc.serialize() == b"\x11" def test_frame_header(): """Test frame header deserialization.""" data = b"\x1c_\x11\xc0\n" extra = b"\xaa\xaa\x55\x55" hdr, rest = foundation.ZCLHeader.deserialize(data + extra) assert rest == extra assert hdr.command_id == 0x0A assert hdr.direction == foundation.Direction.Server_to_Client assert hdr.manufacturer == 0x115F assert hdr.tsn == 0xC0 assert hdr.serialize() == data # check no manufacturer hdr.frame_control = hdr.frame_control.replace(is_manufacturer_specific=False) assert hdr.serialize() == b"\x18\xc0\n" r = repr(hdr) assert isinstance(r, str) def test_frame_header_general(): """Test frame header general command.""" (tsn, cmd_id, manufacturer) = (0x11, 0x15, 0x3344) hdr = foundation.ZCLHeader.general(tsn, cmd_id, manufacturer) assert hdr.frame_control.frame_type == foundation.FrameType.GLOBAL_COMMAND assert hdr.command_id == cmd_id assert hdr.tsn == tsn assert hdr.manufacturer == manufacturer assert hdr.frame_control.is_manufacturer_specific hdr.manufacturer = None assert hdr.manufacturer is None assert not hdr.frame_control.is_manufacturer_specific def test_frame_header_cluster(): """Test frame header cluster command.""" (tsn, cmd_id, manufacturer) = (0x11, 0x16, 0x3344) hdr = foundation.ZCLHeader.cluster( tsn=tsn, command_id=cmd_id, manufacturer=manufacturer ) assert hdr.frame_control.frame_type == foundation.FrameType.CLUSTER_COMMAND assert hdr.command_id == cmd_id assert hdr.tsn == tsn assert hdr.manufacturer == manufacturer assert hdr.frame_control.is_manufacturer_specific hdr.manufacturer = None assert hdr.manufacturer is None assert not hdr.frame_control.is_manufacturer_specific def test_frame_header_disable_manufacturer_id(): """Test frame header manufacturer ID can be disabled with NO_MANUFACTURER_ID.""" hdr = foundation.ZCLHeader.cluster(tsn=123, command_id=0x12, manufacturer=None) assert hdr.manufacturer is None hdr.manufacturer = 0x1234 assert hdr.manufacturer == 0x1234 hdr.manufacturer = foundation.ZCLHeader.NO_MANUFACTURER_ID assert hdr.manufacturer is None hdr2 = foundation.ZCLHeader.cluster( tsn=123, command_id=0x12, manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID ) assert hdr2.manufacturer is None def test_attribute_report(): a = foundation.AttributeReportingConfig() a.direction = 0x01 a.attrid = 0xAA55 a.timeout = 900 b = foundation.AttributeReportingConfig(a) assert a.attrid == b.attrid assert a.direction == b.direction assert a.timeout == b.timeout def test_pytype_to_datatype_derived_enums(): """Test pytype_to_datatype_id lookup for derived enums.""" class e_1(t.enum8): pass class e_2(t.enum8): pass class e_3(t.enum16): pass enum8_id = foundation.DataType.from_python_type(t.enum8) enum16_id = foundation.DataType.from_python_type(t.enum16) assert foundation.DataType.from_python_type(e_1) == enum8_id assert foundation.DataType.from_python_type(e_2) == enum8_id assert foundation.DataType.from_python_type(e_3) == enum16_id assert foundation.DataType.from_python_type(e_2) == enum8_id assert foundation.DataType.from_python_type(e_3) == enum16_id def test_pytype_to_datatype_derived_bitmaps(): """Test pytype_to_datatype_id lookup for derived enums.""" class b_1(t.bitmap8): pass class b_2(t.bitmap8): pass class b_3(t.bitmap16): pass bitmap8_id = foundation.DataType.from_python_type(t.bitmap8) bitmap16_id = foundation.DataType.from_python_type(t.bitmap16) assert foundation.DataType.from_python_type(b_1) == bitmap8_id assert foundation.DataType.from_python_type(b_2) == bitmap8_id assert foundation.DataType.from_python_type(b_3) == bitmap16_id assert foundation.DataType.from_python_type(b_2) == bitmap8_id assert foundation.DataType.from_python_type(b_3) == bitmap16_id def test_ptype_to_datatype_lvlist(): """Test pytype for Structure.""" data = b"L\x06\x00\x10\x00!\xce\x0b!\xa8\x01$\x00\x00\x00\x00\x00!\xbdJ ]" extra = b"\xaa\x55extra\x00" result, rest = foundation.TypeValue.deserialize(data + extra) assert rest == extra assert ( foundation.DataType.from_python_type(result.value.__class__) == foundation.DataType.struct ) assert ( foundation.DataType.from_python_type(foundation.ZCLStructure) == foundation.DataType.struct ) class _Similar(t.LVList, item_type=foundation.TypeValue, length_type=t.uint16_t): pass assert foundation.DataType.from_python_type(_Similar) == foundation.DataType.unk def test_ptype_to_datatype_notype(): """Test pytype for NoData.""" class ZigpyUnknown: pass assert foundation.DataType.from_python_type(ZigpyUnknown) == foundation.DataType.unk def test_write_attrs_response_deserialize(): """Test deserialization.""" data = b"\x00" extra = b"\xaa\x55" r, rest = foundation.WriteAttributesResponse.deserialize(data + extra) assert len(r) == 1 assert r[0].status == foundation.Status.SUCCESS assert rest == extra data = b"\x86\x34\x12\x87\x35\x12" r, rest = foundation.WriteAttributesResponse.deserialize(data + extra) assert len(r) == 2 assert rest == extra assert r[0].status == foundation.Status.UNSUPPORTED_ATTRIBUTE assert r[0].attrid == 0x1234 assert r[1].status == foundation.Status.INVALID_VALUE assert r[1].attrid == 0x1235 @pytest.mark.parametrize( ("attributes", "data"), [ ({4: 0, 5: 0, 3: 0}, b"\x00"), ({4: 0, 5: 0, 3: 0x86}, b"\x86\x03\x00"), ({4: 0x87, 5: 0, 3: 0x86}, b"\x87\x04\x00\x86\x03\x00"), ({4: 0x87, 5: 0x86, 3: 0x86}, b"\x87\x04\x00\x86\x05\x00\x86\x03\x00"), ], ) def test_write_attrs_response_serialize(attributes, data): """Test WriteAttributes Response serialization.""" r = foundation.WriteAttributesResponse() for attr_id, status in attributes.items(): rec = foundation.WriteAttributesStatusRecord() rec.status = status rec.attrid = attr_id r.append(rec) assert r.serialize() == data def test_configure_reporting_response_deserialize(): """Test deserialization.""" data = b"\x00" r, rest = foundation.ConfigureReportingResponse.deserialize(data) assert len(r) == 1 assert r[0].status == foundation.Status.SUCCESS assert r[0].direction is None assert r[0].attrid is None assert rest == b"" data = b"\x00" extra = b"\x01\xaa\x55" r, rest = foundation.ConfigureReportingResponse.deserialize(data + extra) assert len(r) == 1 assert r[0].status == foundation.Status.SUCCESS assert r[0].direction == foundation.ReportingDirection.ReceiveReports assert r[0].attrid == 0x55AA assert rest == b"" data = b"\x86\x01\x34\x12\x87\x01\x35\x12" r, rest = foundation.ConfigureReportingResponse.deserialize(data) assert len(r) == 2 assert rest == b"" assert r[0].status == foundation.Status.UNSUPPORTED_ATTRIBUTE assert r[0].attrid == 0x1234 assert r[1].status == foundation.Status.INVALID_VALUE assert r[1].attrid == 0x1235 with pytest.raises(ValueError): foundation.ConfigureReportingResponse.deserialize(data + extra) def test_configure_reporting_response_serialize_empty(): r = foundation.ConfigureReportingResponse() # An empty configure reporting response doesn't make sense with pytest.raises(ValueError): r.serialize() @pytest.mark.parametrize( ("attributes", "data"), [ ({4: 0, 5: 0, 3: 0}, b"\x00"), ({4: 0, 5: 0, 3: 0x86}, b"\x86\x01\x03\x00"), ({4: 0x87, 5: 0, 3: 0x86}, b"\x87\x01\x04\x00\x86\x01\x03\x00"), ( {4: 0x87, 5: 0x86, 3: 0x86}, b"\x87\x01\x04\x00\x86\x01\x05\x00\x86\x01\x03\x00", ), ], ) def test_configure_reporting_response_serialize(attributes, data): """Test ConfigureReporting Response serialization.""" r = foundation.ConfigureReportingResponse() for attr_id, status in attributes.items(): rec = foundation.ConfigureReportingResponseRecord() rec.status = status rec.direction = 0x01 rec.attrid = attr_id r.append(rec) assert r.serialize() == data def test_status_enum(): """Test Status enums chaining.""" status_names = [e.name for e in foundation.Status] aps_names = [e.name for e in t.APSStatus] nwk_names = [e.name for e in t.NWKStatus] mac_names = [e.name for e in t.MACStatus] status = foundation.Status(0x98) assert status.name in status_names assert status.name not in aps_names assert status.name not in nwk_names assert status.name not in mac_names status = foundation.Status(0xAE) assert status.name not in status_names assert status.name in aps_names assert status.name not in nwk_names assert status.name not in mac_names status = foundation.Status(0xD0) assert status.name not in status_names assert status.name not in aps_names assert status.name in nwk_names assert status.name not in mac_names status = foundation.Status(0xE9) assert status.name not in status_names assert status.name not in aps_names assert status.name not in nwk_names assert status.name in mac_names status = foundation.Status(0xFF) assert status.name not in status_names assert status.name not in aps_names assert status.name not in nwk_names assert status.name not in mac_names assert status.name == "undefined_0xff" def test_schema(): """Test schema parameter parsing""" bad_s = foundation.ZCLCommandDef( id=0x12, name="test", schema={ "uh oh": t.uint16_t, }, direction=foundation.Direction.Client_to_Server, ) with pytest.raises(ValueError): bad_s.with_compiled_schema() s = foundation.ZCLCommandDef( id=0x12, name="test", schema={ "foo": t.uint8_t, "bar?": t.uint16_t, "baz?": t.uint8_t, }, direction=foundation.Direction.Client_to_Server, ) s = s.with_compiled_schema() str(s) assert s.schema.foo.type is t.uint8_t assert not s.schema.foo.optional assert s.schema.bar.type is t.uint16_t assert s.schema.bar.optional assert s.schema.baz.type is t.uint8_t assert s.schema.baz.optional assert "test" in str(s) and "direction=" assert singleton == singleton # noqa: PLR0124 obj = {} obj[singleton] = 5 assert obj[singleton] == 5 @pytest.mark.parametrize( ("input_relays", "expected_relays"), [ ([0x0000, 0x0000, 0x0001, 0x0001, 0x0002], [0x0001, 0x0002]), ([0x0001, 0x0002], [0x0001, 0x0002]), ([], []), ([0x0000], []), ], ) def test_relay_filtering(input_relays: list[int], expected_relays: list[int]): assert util.filter_relays(input_relays) == expected_relays async def test_combine_concurrent_calls(): class TestFuncs: def __init__(self): self.slow_calls = 0 self.slow_error_calls = 0 async def slow(self, n=None): await asyncio.sleep(0.1) self.slow_calls += 1 return (self.slow_calls, n) async def slow_error(self, n=None): await asyncio.sleep(0.1) self.slow_error_calls += 1 raise RuntimeError combined_slow = util.combine_concurrent_calls(slow) combined_slow_error = util.combine_concurrent_calls(slow_error) f = TestFuncs() assert f.slow_calls == 0 await f.slow() assert f.slow_calls == 1 await f.combined_slow() assert f.slow_calls == 2 results = await asyncio.gather(*[f.combined_slow() for _ in range(5)]) assert results == [(3, None)] * 5 assert f.slow_calls == 3 results = await asyncio.gather(*[f.combined_slow() for _ in range(5)]) assert results == [(4, None)] * 5 assert f.slow_calls == 4 # Unique keyword arguments results = await asyncio.gather(*[f.combined_slow(n=i) for i in range(5)]) assert results == [(5 + i, 0 + i) for i in range(5)] assert f.slow_calls == 9 # Non-unique keyword arguments results = await asyncio.gather(*[f.combined_slow(i // 2) for i in range(5)]) assert results == [(10, 0), (10, 0), (11, 1), (11, 1), (12, 2)] assert f.slow_calls == 12 # Mixed keyword and non-keyword results = await asyncio.gather( f.combined_slow(0), f.combined_slow(n=0), f.combined_slow(1), f.combined_slow(n=1), f.combined_slow(n=1), ) assert results == [(13, 0), (13, 0), (14, 1), (14, 1), (14, 1)] assert f.slow_calls == 14 assert f.slow_error_calls == 0 with pytest.raises(RuntimeError): await f.slow_error() assert f.slow_error_calls == 1 for coro in asyncio.as_completed([f.combined_slow_error() for _ in range(5)]): with pytest.raises(RuntimeError): await coro assert f.slow_error_calls == 2 @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_deprecated(): @util.deprecated("This function is deprecated") def foo(): return 1 with pytest.deprecated_call(): foo() class Bar: pass obj = util.deprecated_attrs({"foo": Bar}) assert obj("foo") == Bar with pytest.raises(AttributeError): obj("baz") async def test_async_iterate_in_chunks() -> None: def iterator(n: int) -> typing.Generator[int, None, None]: for i in range(n): time.sleep(0.1) yield i chunks = [c async for c in util.async_iterate_in_chunks(iterator(10), chunk_size=3)] assert chunks == [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] zigpy-0.80.1/zigpy/000077500000000000000000000000001501451476000141075ustar00rootroot00000000000000zigpy-0.80.1/zigpy/__init__.py000066400000000000000000000000001501451476000162060ustar00rootroot00000000000000zigpy-0.80.1/zigpy/appdb.py000066400000000000000000001430741501451476000155600ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib from datetime import datetime, timedelta, timezone import json import logging import re import types from typing import Any import aiosqlite import zigpy.appdb_schemas import zigpy.backups import zigpy.device import zigpy.endpoint import zigpy.exceptions import zigpy.group import zigpy.profiles import zigpy.quirks import zigpy.state import zigpy.types as t import zigpy.typing import zigpy.util from zigpy.zcl import ClusterType from zigpy.zcl.clusters.general import Basic from zigpy.zdo import types as zdo_t LOGGER = logging.getLogger(__name__) DB_VERSION = 13 DB_V = f"_v{DB_VERSION}" MIN_SQLITE_VERSION = (3, 24, 0) UNIX_EPOCH = datetime.fromtimestamp(0, tz=timezone.utc) DB_V_REGEX = re.compile(r"(?:_v\d+)?$") MIN_UPDATE_DELTA = timedelta(seconds=30).total_seconds() def _import_compatible_sqlite3(min_version: tuple[int, int, int]) -> types.ModuleType: """Loads an SQLite module with a library version matching the provided constraint.""" import sqlite3 try: import pysqlite3 except ImportError: pysqlite3 = None for module in [sqlite3, pysqlite3]: if module is None: continue LOGGER.debug("SQLite version for %s: %s", module, module.sqlite_version) if module.sqlite_version_info >= min_version: return module min_ver = ".".join(map(str, min_version)) raise RuntimeError( f"zigpy requires SQLite {min_ver} or newer. If your distribution does not" f" provide a more recent release, install pysqlite3 with" f" `pip install pysqlite3-binary`" ) sqlite3 = _import_compatible_sqlite3(min_version=MIN_SQLITE_VERSION) def _register_sqlite_adapters(): def adapt_ieee(eui64): return str(eui64) sqlite3.register_adapter(t.EUI64, adapt_ieee) sqlite3.register_adapter(t.ExtendedPanId, adapt_ieee) def convert_ieee(s): return t.EUI64.convert(s.decode()) sqlite3.register_converter("ieee", convert_ieee) def aiosqlite_connect( database: str, iter_chunk_size: int = 64, **kwargs ) -> aiosqlite.Connection: """Copy of the the `aiosqlite.connect` function that connects using either the built-in `sqlite3` module or the imported `pysqlite3` module. """ return aiosqlite.Connection( connector=lambda: sqlite3.connect(str(database), **kwargs), iter_chunk_size=iter_chunk_size, ) def decode_str_attribute(value: str | bytes) -> str: if isinstance(value, str): return value return value.split(b"\x00", 1)[0].decode("utf-8") class PersistingListener(zigpy.util.CatchingTaskMixin): def __init__( self, connection: aiosqlite.Connection, application: zigpy.typing.ControllerApplicationType, ) -> None: _register_sqlite_adapters() self._db = connection self._application = application self._callback_handlers: asyncio.Queue = asyncio.Queue() self.running = False self._worker_task = asyncio.create_task(self._worker()) async def initialize_tables(self) -> None: async with self.execute("PRAGMA integrity_check") as cursor: rows = await cursor.fetchall() status = "\n".join(row[0] for row in rows) if status != "ok": LOGGER.error( "Zigbee database is corrupted, integrity check failed!\n%s", status ) async with self.execute("PRAGMA foreign_key_check") as cursor: rows = await cursor.fetchall() if rows: LOGGER.error( "Zigbee database is corrupted, foreign key check failed!\n%s", rows ) # Truncate the SQLite journal file instead of deleting it after transactions await self._set_isolation_level(None) await self.execute("PRAGMA journal_mode = WAL") await self.execute("PRAGMA synchronous = normal") await self.execute("PRAGMA temp_store = memory") await self._set_isolation_level("DEFERRED") await self.execute("PRAGMA foreign_keys = ON") await self._run_migrations() @classmethod async def new( cls, database_file: str, app: zigpy.typing.ControllerApplicationType ) -> PersistingListener: """Create an instance of persisting listener.""" sqlite_conn = await aiosqlite_connect( database_file, detect_types=sqlite3.PARSE_DECLTYPES, isolation_level="DEFERRED", # The default is "", an alias for "DEFERRED" ) listener = cls(sqlite_conn, app) try: await listener.initialize_tables() except Exception: # noqa: BLE001 await listener.shutdown() raise listener.running = True return listener async def _worker(self) -> None: """Process request in the received order.""" while True: cb_name, args = await self._callback_handlers.get() handler = getattr(self, cb_name) assert handler try: await handler(*args) except sqlite3.Error as exc: LOGGER.debug( "Error handling '%s' event with %s params: %s", cb_name, args, str(exc), ) except Exception as ex: # noqa: BLE001 LOGGER.error( "Unexpected error while processing %s(%s): %s", cb_name, args, ex ) self._callback_handlers.task_done() async def shutdown(self) -> None: """Shutdown connection.""" self.running = False await self._callback_handlers.join() if not self._worker_task.done(): self._worker_task.cancel() # Delete the journal on shutdown await self._set_isolation_level(None) await self.execute("PRAGMA wal_checkpoint;") await self._set_isolation_level("DEFERRED") await self._db.close() # FIXME: aiosqlite's thread won't always be closed immediately await asyncio.get_running_loop().run_in_executor(None, self._db.join) def enqueue(self, cb_name: str, *args) -> None: """Enqueue an async callback handler action.""" if not self.running: LOGGER.debug("Discarding %s event", cb_name) return self._callback_handlers.put_nowait((cb_name, args)) async def _set_isolation_level(self, level: str | None): """Set the SQLite statement isolation level in a thread-safe way.""" await self._db._execute(lambda: setattr(self._db, "isolation_level", level)) def execute(self, *args, **kwargs): return self._db.execute(*args, **kwargs) async def executescript(self, sql): """Naive replacement for `sqlite3.Cursor.executescript` that does not execute a `COMMIT` before running the script. This extra `COMMIT` breaks transactions that run scripts. """ # XXX: This will break if you use a semicolon anywhere but at the end of a line for statement in sql.split(";"): await self.execute(statement) def device_joined(self, device: zigpy.typing.DeviceType) -> None: self.enqueue("_update_device_nwk", device.ieee, device.nwk) async def _update_device_nwk(self, ieee: t.EUI64, nwk: t.NWK) -> None: await self.execute(f"UPDATE devices{DB_V} SET nwk=? WHERE ieee=?", (nwk, ieee)) await self._db.commit() def device_initialized(self, device: zigpy.typing.DeviceType) -> None: pass def device_left(self, device: zigpy.typing.DeviceType) -> None: pass def device_last_seen_updated( self, device: zigpy.typing.DeviceType, last_seen: datetime ) -> None: """Device last_seen time is updated.""" self.enqueue("_save_device_last_seen", device.ieee, last_seen) async def _save_device_last_seen(self, ieee: t.EUI64, last_seen: datetime) -> None: q = f"""UPDATE devices{DB_V} SET last_seen=:ts WHERE ieee=:ieee AND :ts - last_seen > :min_update_delta""" await self.execute( q, { "ts": last_seen.timestamp(), "ieee": ieee, "min_update_delta": MIN_UPDATE_DELTA, }, ) await self._db.commit() def device_relays_updated( self, device: zigpy.typing.DeviceType, relays: t.Relays | None ) -> None: """Device relay list is updated.""" self.enqueue("_save_device_relays", device.ieee, relays) async def _save_device_relays(self, ieee: t.EUI64, relays: t.Relays | None) -> None: if relays is None: await self.execute(f"DELETE FROM relays{DB_V} WHERE ieee = ?", (ieee,)) else: q = f"""INSERT INTO relays{DB_V} VALUES (:ieee, :relays) ON CONFLICT (ieee) DO UPDATE SET relays=excluded.relays WHERE relays != :relays""" await self.execute(q, {"ieee": ieee, "relays": relays.serialize()}) await self._db.commit() def attribute_updated( self, cluster: zigpy.typing.ClusterType, attrid: int, value: Any, timestamp: datetime, ) -> None: self.enqueue( "_save_attribute", cluster.endpoint.device.ieee, cluster.endpoint.endpoint_id, cluster.cluster_type, cluster.cluster_id, attrid, value, timestamp, ) def attribute_cleared(self, cluster: zigpy.typing.ClusterType, attrid: int) -> None: self.enqueue( "_clear_attribute", cluster.endpoint.device.ieee, cluster.endpoint.endpoint_id, cluster.cluster_type, cluster.cluster_id, attrid, ) def unsupported_attribute_added( self, cluster: zigpy.typing.ClusterType, attrid: int ) -> None: self.enqueue( "_unsupported_attribute_added", cluster.endpoint.device.ieee, cluster.endpoint.endpoint_id, cluster.cluster_type, cluster.cluster_id, attrid, ) async def _unsupported_attribute_added( self, ieee: t.EUI64, endpoint_id: int, cluster_type: ClusterType, cluster_id: int, attrid: int, ) -> None: q = f"""INSERT INTO unsupported_attributes{DB_V} VALUES (?, ?, ?, ?, ?) ON CONFLICT (ieee, endpoint_id, cluster_type, cluster_id, attr_id) DO NOTHING""" await self.execute(q, (ieee, endpoint_id, cluster_type, cluster_id, attrid)) await self._db.commit() def unsupported_attribute_removed( self, cluster: zigpy.typing.ClusterType, attrid: int ) -> None: self.enqueue( "_unsupported_attribute_removed", cluster.endpoint.device.ieee, cluster.endpoint.endpoint_id, cluster.cluster_type, cluster.cluster_id, attrid, ) async def _unsupported_attribute_removed( self, ieee: t.EUI64, endpoint_id: int, cluster_type: ClusterType, cluster_id: int, attrid: int, ) -> None: q = f"""DELETE FROM unsupported_attributes{DB_V} WHERE ieee = ? AND endpoint_id = ? AND cluster_type = ? AND cluster_id = ? AND attr_id = ?""" await self.execute(q, (ieee, endpoint_id, cluster_type, cluster_id, attrid)) await self._db.commit() def neighbors_updated(self, ieee: t.EUI64, neighbors: list[zdo_t.Neighbor]) -> None: """Neighbor update from Mgmt_Lqi_req.""" self.enqueue("_neighbors_updated", ieee, neighbors) async def _neighbors_updated( self, ieee: t.EUI64, neighbors: list[zdo_t.Neighbor] ) -> None: await self.execute(f"DELETE FROM neighbors{DB_V} WHERE device_ieee = ?", [ieee]) rows = [(ieee, *neighbor.as_tuple()) for neighbor in neighbors] await self._db.executemany( f"INSERT INTO neighbors{DB_V} VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", rows ) await self._db.commit() def routes_updated(self, ieee: t.EUI64, routes: list[zdo_t.Route]) -> None: """Route update from Mgmt_Rtg_req.""" self.enqueue("_routes_updated", ieee, routes) async def _routes_updated(self, ieee: t.EUI64, routes: list[zdo_t.Route]) -> None: await self.execute(f"DELETE FROM routes{DB_V} WHERE device_ieee = ?", [ieee]) rows = [(ieee, *route.as_tuple()) for route in routes] await self._db.executemany( f"INSERT INTO routes{DB_V} VALUES (?,?,?,?,?,?,?,?)", rows ) await self._db.commit() def group_added(self, group: zigpy.group.Group) -> None: """Group is added.""" self.enqueue("_group_added", group) async def _group_added(self, group: zigpy.group.Group) -> None: q = f"""INSERT INTO groups{DB_V} VALUES (?, ?) ON CONFLICT (group_id) DO UPDATE SET name=excluded.name""" await self.execute(q, (group.group_id, group.name)) await self._db.commit() def group_member_added( self, group: zigpy.group.Group, ep: zigpy.typing.EndpointType ) -> None: """Called when a group member is added.""" self.enqueue("_group_member_added", group, ep) async def _group_member_added( self, group: zigpy.group.Group, ep: zigpy.typing.EndpointType ) -> None: q = f"""INSERT INTO group_members{DB_V} VALUES (?, ?, ?) ON CONFLICT DO NOTHING""" await self.execute(q, (group.group_id, *ep.unique_id)) await self._db.commit() def group_member_removed( self, group: zigpy.group.Group, ep: zigpy.typing.EndpointType ) -> None: """Called when a group member is removed.""" self.enqueue("_group_member_removed", group, ep) async def _group_member_removed( self, group: zigpy.group.Group, ep: zigpy.typing.EndpointType ) -> None: q = f"""DELETE FROM group_members{DB_V} WHERE group_id=? AND ieee=? AND endpoint_id=?""" await self.execute(q, (group.group_id, *ep.unique_id)) await self._db.commit() def group_removed(self, group: zigpy.group.Group) -> None: """Called when a group is removed.""" self.enqueue("_group_removed", group) async def _group_removed(self, group: zigpy.group.Group) -> None: q = f"DELETE FROM groups{DB_V} WHERE group_id=?" await self.execute(q, (group.group_id,)) await self._db.commit() def device_removed(self, device: zigpy.typing.DeviceType) -> None: self.enqueue("_remove_device", device) async def _remove_device(self, device: zigpy.typing.DeviceType) -> None: await self.execute(f"DELETE FROM devices{DB_V} WHERE ieee = ?", (device.ieee,)) await self._db.commit() def raw_device_initialized(self, device: zigpy.typing.DeviceType) -> None: self.enqueue("_save_device", device) async def _save_device(self, device: zigpy.typing.DeviceType) -> None: q = f"""INSERT INTO devices{DB_V} (ieee, nwk, status, last_seen) VALUES (?, ?, ?, ?) ON CONFLICT (ieee) DO UPDATE SET nwk=excluded.nwk, status=excluded.status, last_seen=excluded.last_seen""" await self.execute( q, ( device.ieee, device.nwk, device.status, (device._last_seen or UNIX_EPOCH).timestamp(), ), ) if device.node_desc is not None: await self._save_node_descriptor(device) if isinstance(device, zigpy.quirks.BaseCustomDevice): await self._db.commit() return await self._save_endpoints(device) for ep in device.non_zdo_endpoints: await self._save_clusters(ep) await self._save_attribute_cache(ep) await self._save_unsupported_attributes(ep) await self._db.commit() async def _save_endpoints(self, device: zigpy.typing.DeviceType) -> None: rows = [ ( device.ieee, ep.endpoint_id, ep.profile_id, ep.device_type, ep.status, ) for ep in device.non_zdo_endpoints ] q = f"""INSERT INTO endpoints{DB_V} VALUES (?, ?, ?, ?, ?) ON CONFLICT (ieee, endpoint_id) DO UPDATE SET profile_id=excluded.profile_id, device_type=excluded.device_type, status=excluded.status""" await self._db.executemany(q, rows) async def _save_node_descriptor(self, device: zigpy.typing.DeviceType) -> None: q = f"""INSERT INTO node_descriptors{DB_V} VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (ieee) DO UPDATE SET logical_type=excluded.logical_type, complex_descriptor_available=excluded.complex_descriptor_available, user_descriptor_available=excluded.user_descriptor_available, reserved=excluded.reserved, aps_flags=excluded.aps_flags, frequency_band=excluded.frequency_band, mac_capability_flags=excluded.mac_capability_flags, manufacturer_code=excluded.manufacturer_code, maximum_buffer_size=excluded.maximum_buffer_size, maximum_incoming_transfer_size=excluded.maximum_incoming_transfer_size, server_mask=excluded.server_mask, maximum_outgoing_transfer_size=excluded.maximum_outgoing_transfer_size, descriptor_capability_field=excluded.descriptor_capability_field""" await self.execute(q, (device.ieee, *device.node_desc.as_tuple())) async def _save_clusters(self, endpoint: zigpy.typing.EndpointType) -> None: clusters = [ ( endpoint.device.ieee, endpoint.endpoint_id, cluster.cluster_type, cluster.cluster_id, ) for cluster in endpoint.clusters ] q = f"""INSERT INTO clusters{DB_V} VALUES (?, ?, ?, ?) ON CONFLICT (ieee, endpoint_id, cluster_type, cluster_id) DO NOTHING""" await self._db.executemany(q, clusters) async def _save_attribute_cache(self, ep: zigpy.typing.EndpointType) -> None: clusters = [ ( ep.device.ieee, ep.endpoint_id, cluster.cluster_type, cluster.cluster_id, attrid, value, cluster._attr_last_updated.get(attrid, UNIX_EPOCH).timestamp(), ) for cluster in ep.clusters for attrid, value in cluster._attr_cache.items() ] q = f"""INSERT INTO attributes_cache{DB_V} VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (ieee, endpoint_id, cluster_type, cluster_id, attr_id) DO UPDATE SET value=excluded.value, last_updated=excluded.last_updated""" await self._db.executemany(q, clusters) async def _save_unsupported_attributes(self, ep: zigpy.typing.EndpointType) -> None: clusters = [ ( ep.device.ieee, ep.endpoint_id, cluster.cluster_type, cluster.cluster_id, attr, ) for cluster in ep.clusters for attr in cluster.unsupported_attributes if isinstance(attr, int) ] q = f"""INSERT INTO unsupported_attributes{DB_V} VALUES (?, ?, ?, ?, ?) ON CONFLICT (ieee, endpoint_id, cluster_type, cluster_id, attr_id) DO NOTHING""" await self._db.executemany(q, clusters) async def _save_attribute( self, ieee: t.EUI64, endpoint_id: int, cluster_type: ClusterType, cluster_id: int, attrid: int, value: Any, timestamp: datetime, ) -> None: q = f""" INSERT INTO attributes_cache{DB_V} VALUES (:ieee, :endpoint_id, :cluster_type, :cluster_id, :attr_id, :value, :timestamp) ON CONFLICT (ieee, endpoint_id, cluster_type, cluster_id, attr_id) DO UPDATE SET value=excluded.value, last_updated=excluded.last_updated WHERE value != excluded.value OR :timestamp - last_updated > :min_update_delta """ await self.execute( q, { "ieee": ieee, "endpoint_id": endpoint_id, "cluster_type": cluster_type, "cluster_id": cluster_id, "attr_id": attrid, "value": value, "timestamp": timestamp.timestamp(), "min_update_delta": MIN_UPDATE_DELTA, }, ) await self._db.commit() async def _clear_attribute( self, ieee: t.EUI64, endpoint_id: int, cluster_type: ClusterType, cluster_id: int, attrid: int, ) -> None: q = f""" DELETE FROM attributes_cache{DB_V} WHERE ieee = :ieee AND endpoint_id = :endpoint_id AND cluster_type = :cluster_type AND cluster_id = :cluster_id AND attr_id = :attr_id """ await self.execute( q, { "ieee": ieee, "endpoint_id": endpoint_id, "cluster_type": cluster_type, "cluster_id": cluster_id, "attr_id": attrid, }, ) await self._db.commit() def network_backup_created(self, backup: zigpy.backups.NetworkBackup) -> None: self.enqueue("_network_backup_created", json.dumps(backup.as_dict())) async def _network_backup_created(self, backup_json: str) -> None: q = f"""INSERT INTO network_backups{DB_V} VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET backup_json=excluded.backup_json""" await self.execute(q, (None, backup_json)) await self._db.commit() def network_backup_removed(self, backup: zigpy.backups.NetworkBackup) -> None: self.enqueue("_network_backup_removed", backup.backup_time) async def _network_backup_removed(self, backup_time: datetime) -> None: q = f"""DELETE FROM network_backups{DB_V} WHERE json_extract(backup_json, '$.backup_time')=?""" await self.execute(q, (backup_time.isoformat(),)) await self._db.commit() async def load(self) -> None: LOGGER.debug("Loading application state") await self._load_devices() await self._load_node_descriptors() await self._load_endpoints() await self._load_clusters() # Quirks require the manufacturer and model name to be populated await self._load_attributes( f""" cluster_type={ClusterType.Server} AND cluster_id={Basic.cluster_id} AND ( attr_id={Basic.AttributeDefs.manufacturer.id} OR attr_id={Basic.AttributeDefs.model.id} ) """ ) for device in self._application.devices.values(): device = zigpy.quirks.get_device(device) self._application.devices[device.ieee] = device await self._load_attributes() await self._load_unsupported_attributes() await self._load_groups() await self._load_group_members() await self._load_relays() await self._load_neighbors() await self._load_routes() await self._load_network_backups() await self._register_device_listeners() async def _load_attributes(self, filter: str | None = None) -> None: if filter: query = f"SELECT * FROM attributes_cache{DB_V} WHERE {filter}" else: query = f"SELECT * FROM attributes_cache{DB_V}" async with self.execute(query) as cursor: async for ( ieee, endpoint_id, cluster_type, cluster_id, attr_id, value, last_updated, ) in cursor: dev = self._application.get_device(ieee) # Some quirks create endpoints and clusters that do not exist if endpoint_id not in dev.endpoints: continue ep = dev.endpoints[endpoint_id] clusters = ( ep.in_clusters if cluster_type == ClusterType.Server else ep.out_clusters ) if cluster_id not in clusters: continue clusters[cluster_id]._attr_cache[attr_id] = value clusters[cluster_id]._attr_last_updated[attr_id] = ( datetime.fromtimestamp(last_updated, timezone.utc) ) LOGGER.debug( "[0x%04x:%s:0x%04x] Attribute id: %s value: %s", dev.nwk, endpoint_id, cluster_id, attr_id, value, ) # Populate the device's manufacturer and model attributes if ( cluster_id == Basic.cluster_id and attr_id == Basic.AttributeDefs.manufacturer.id ): dev.manufacturer = decode_str_attribute(value) elif ( cluster_id == Basic.cluster_id and attr_id == Basic.AttributeDefs.model.id ): dev.model = decode_str_attribute(value) async def _load_unsupported_attributes(self) -> None: """Load unsuppoted attributes.""" async with self.execute( f"SELECT * FROM unsupported_attributes{DB_V}" ) as cursor: async for ieee, endpoint_id, cluster_type, cluster_id, attr_id in cursor: dev = self._application.get_device(ieee) try: ep = dev.endpoints[endpoint_id] except KeyError: continue clusters = ( ep.in_clusters if cluster_type == ClusterType.Server else ep.out_clusters ) try: cluster = clusters[cluster_id] except KeyError: continue cluster.add_unsupported_attribute(attr_id, inhibit_events=True) async def _load_devices(self) -> None: async with self.execute(f"SELECT * FROM devices{DB_V}") as cursor: async for ieee, nwk, status, last_seen in cursor: dev = self._application.add_device(ieee, nwk) dev.status = zigpy.device.Status(status) if last_seen > 0: dev.last_seen = last_seen async def _load_node_descriptors(self) -> None: async with self.execute(f"SELECT * FROM node_descriptors{DB_V}") as cursor: async for ieee, *fields in cursor: dev = self._application.get_device(ieee) dev.node_desc = zdo_t.NodeDescriptor(*fields) assert dev.node_desc.is_valid async def _load_endpoints(self) -> None: async with self.execute(f"SELECT * FROM endpoints{DB_V}") as cursor: async for ieee, epid, profile_id, device_type, status in cursor: dev = self._application.get_device(ieee) ep = dev.add_endpoint(epid) ep.profile_id = profile_id ep.status = zigpy.endpoint.Status(status) if profile_id == zigpy.profiles.zha.PROFILE_ID: ep.device_type = zigpy.profiles.zha.DeviceType(device_type) elif profile_id == zigpy.profiles.zll.PROFILE_ID: ep.device_type = zigpy.profiles.zll.DeviceType(device_type) else: ep.device_type = device_type async def _load_clusters(self) -> None: async with self.execute(f"SELECT * FROM clusters{DB_V}") as cursor: async for ieee, endpoint_id, cluster_type, cluster_id in cursor: dev = self._application.get_device(ieee) ep = dev.endpoints[endpoint_id] if ClusterType(cluster_type) == ClusterType.Server: ep.add_input_cluster(cluster_id) else: ep.add_output_cluster(cluster_id) async def _load_groups(self) -> None: async with self.execute(f"SELECT * FROM groups{DB_V}") as cursor: async for group_id, name in cursor: self._application.groups.add_group(group_id, name, suppress_event=True) async def _load_group_members(self) -> None: async with self.execute(f"SELECT * FROM group_members{DB_V}") as cursor: async for group_id, ieee, ep_id in cursor: dev = self._application.get_device(ieee) group = self._application.groups[group_id] group.add_member(dev.endpoints[ep_id], suppress_event=True) async def _load_relays(self) -> None: async with self.execute(f"SELECT * FROM relays{DB_V}") as cursor: async for ieee, value in cursor: dev = self._application.get_device(ieee) relays, _ = t.Relays.deserialize(value) dev.relays = zigpy.util.filter_relays(relays) async def _load_neighbors(self) -> None: async with self.execute(f"SELECT * FROM neighbors{DB_V}") as cursor: async for ieee, *fields in cursor: neighbor = zdo_t.Neighbor(*fields) self._application.topology.neighbors[ieee].append(neighbor) async def _load_routes(self) -> None: async with self.execute(f"SELECT * FROM routes{DB_V}") as cursor: async for ieee, *fields in cursor: route = zdo_t.Route(*fields) self._application.topology.routes[ieee].append(route) async def _load_network_backups(self) -> None: self._application.backups.backups.clear() async with self.execute( f"SELECT * FROM network_backups{DB_V} ORDER BY id" ) as cursor: backups = [] async for _id, backup_json in cursor: backup = zigpy.backups.NetworkBackup.from_dict(json.loads(backup_json)) backups.append(backup) backups.sort(key=lambda b: b.backup_time) for backup in backups: self._application.backups.add_backup(backup, suppress_event=True) async def _register_device_listeners(self) -> None: for dev in self._application.devices.values(): dev.add_context_listener(self) @contextlib.asynccontextmanager async def _transaction(self): await self.execute("BEGIN TRANSACTION") try: yield except Exception: # noqa: BLE001 await self.execute("ROLLBACK") raise else: await self.execute("COMMIT") async def _get_table_versions(self) -> dict[str, int]: tables = {} async with self.execute( "SELECT name FROM sqlite_master WHERE type='table'" ) as cursor: async for (name,) in cursor: # Ignore tables internal to SQLite if name.startswith("sqlite_"): continue # The regex will always return a match match = DB_V_REGEX.search(name) assert match is not None tables[name] = int(match.group(0)[2:] or "0") return tables async def _table_exists(self, name: str) -> bool: return name in (await self._get_table_versions()) async def _run_migrations(self) -> bool: """Migrates the database to the newest schema, returning True if migrations ran.""" tables = await self._get_table_versions() tables_version = max(tables.values(), default=0) async with self.execute("PRAGMA user_version") as cursor: (db_version,) = await cursor.fetchone() LOGGER.debug( "Current database version is v%s (table version v%s)", db_version, tables_version, ) # Table version suffixes were introduced in v4. If the table version suffix does # not match `user_version`, either zigpy was downgraded to a *really* old # version (July 2021), or it's corrupt. Running migrations could delete existing # table data, and since we cannot guarantee the schema is intact, fail early. if tables_version >= 4 and tables_version != db_version: raise zigpy.exceptions.CorruptDatabase( f"The `zigbee.db` database version ({db_version}) does not match its" f" max table version ({tables_version}). The database is inconsistent.", ) if db_version == 0 and not tables: # If this is a brand new database, just load the current schema await self.executescript(zigpy.appdb_schemas.SCHEMAS[DB_VERSION]) return False elif db_version > DB_VERSION: LOGGER.error( "This zigpy release uses database schema v%s but the database is v%s." " Downgrading zigpy is *not* recommended and may result in data loss." " Use at your own risk.", DB_VERSION, db_version, ) return False # All migrations must succeed. If any fail, the database is not touched. async with self._transaction(): for migration, to_db_version in [ (self._migrate_to_v4, 4), (self._migrate_to_v5, 5), (self._migrate_to_v6, 6), (self._migrate_to_v7, 7), (self._migrate_to_v8, 8), (self._migrate_to_v9, 9), (self._migrate_to_v10, 10), (self._migrate_to_v11, 11), (self._migrate_to_v12, 12), (self._migrate_to_v13, 13), ]: if db_version >= min(to_db_version, DB_VERSION): continue LOGGER.info( "Migrating database from v%d to v%d", db_version, to_db_version ) await self.executescript(zigpy.appdb_schemas.SCHEMAS[to_db_version]) await migration() db_version = to_db_version return True async def _migrate_tables( self, table_map: dict[str, str], *, errors: str = "raise" ): """Copy rows from one set of tables into another.""" # Extract the "old" table version suffix tables = await self._get_table_versions() old_table_name = list(table_map.keys())[0] old_version = tables[old_table_name] # Check which tables would not be migrated old_tables = [t for t, v in tables.items() if v == old_version] unmigrated_old_tables = [t for t in old_tables if t not in table_map] if unmigrated_old_tables: raise RuntimeError( f"The following tables were not migrated: {unmigrated_old_tables}" ) # Insertion order matters for foreign key constraints but any rows that fail # to insert due to constraint violations can be discarded for old_table, new_table in table_map.items(): # Ignore tables without a migration if new_table is None: continue async with self.execute(f"SELECT * FROM {old_table}") as cursor: async for row in cursor: placeholders = ",".join("?" * len(row)) try: await self.execute( f"INSERT INTO {new_table} VALUES ({placeholders})", row ) except sqlite3.IntegrityError as e: if errors == "raise": raise elif errors == "warn": LOGGER.warning( "Failed to migrate row %s%s: %s", old_table, row, e ) elif errors == "ignore": pass else: raise ValueError( f"Invalid value for `errors`: {errors!r}" ) from e async def _migrate_to_v4(self): """Schema v4 expanded the node descriptor and neighbor table columns""" # The `node_descriptors` table was added in v1 if await self._table_exists("node_descriptors"): async with self.execute("SELECT * FROM node_descriptors") as cur: async for dev_ieee, value in cur: node_desc, rest = zdo_t.NodeDescriptor.deserialize(value) assert not rest await self.execute( "INSERT INTO node_descriptors_v4" " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (dev_ieee, *node_desc.as_tuple()), ) # The `neighbors` table was added in v3 but the version number was not # incremented. It may not exist. if await self._table_exists("neighbors"): async with self.execute("SELECT * FROM neighbors") as cur: async for dev_ieee, epid, ieee, nwk, packed, prm, depth, lqi in cur: neighbor = zdo_t.Neighbor( extended_pan_id=epid, ieee=ieee, nwk=nwk, permit_joining=prm, depth=depth, lqi=lqi, reserved2=0b000000, **zdo_t.Neighbor._parse_packed(packed), ) await self.execute( "INSERT INTO neighbors_v4 VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", (dev_ieee, *neighbor.as_tuple()), ) async def _migrate_to_v5(self): """Schema v5 introduced global table version suffixes and removed stale rows""" await self._migrate_tables( { "devices": "devices_v5", "endpoints": "endpoints_v5", "clusters": "in_clusters_v5", "output_clusters": "out_clusters_v5", "groups": "groups_v5", "group_members": "group_members_v5", "relays": "relays_v5", "attributes": "attributes_cache_v5", # These were migrated in v4 "neighbors_v4": "neighbors_v5", "node_descriptors_v4": "node_descriptors_v5", # Explicitly specify which tables will not be migrated "neighbors": None, "node_descriptors": None, }, errors="warn", ) async def _migrate_to_v6(self): """Schema v6 relaxed the `attribute_cache` table schema to ignore endpoints""" await self._migrate_tables( { "devices_v5": "devices_v6", "endpoints_v5": "endpoints_v6", "in_clusters_v5": "in_clusters_v6", "out_clusters_v5": "out_clusters_v6", "groups_v5": "groups_v6", "group_members_v5": "group_members_v6", "relays_v5": "relays_v6", "attributes_cache_v5": "attributes_cache_v6", "neighbors_v5": "neighbors_v6", "node_descriptors_v5": "node_descriptors_v6", } ) # See if we can migrate any `attributes_cache` rows skipped by the v5 migration if await self._table_exists("attributes"): async with self.execute("SELECT count(*) FROM attributes") as cur: (num_attrs_v4,) = await cur.fetchone() async with self.execute("SELECT count(*) FROM attributes_cache_v6") as cur: (num_attrs_v6,) = await cur.fetchone() if num_attrs_v6 < num_attrs_v4: LOGGER.warning( "Migrating up to %d rows skipped by v5 migration", num_attrs_v4 - num_attrs_v6, ) await self._migrate_tables( { "attributes": "attributes_cache_v6", "devices": None, "endpoints": None, "clusters": None, "neighbors": None, "node_descriptors": None, "output_clusters": None, "groups": None, "group_members": None, "relays": None, }, errors="ignore", ) async def _migrate_to_v7(self): """Schema v7 added the `unsupported_attributes` table.""" await self._migrate_tables( { "devices_v6": "devices_v7", "endpoints_v6": "endpoints_v7", "in_clusters_v6": "in_clusters_v7", "out_clusters_v6": "out_clusters_v7", "groups_v6": "groups_v7", "group_members_v6": "group_members_v7", "relays_v6": "relays_v7", "attributes_cache_v6": "attributes_cache_v7", "neighbors_v6": "neighbors_v7", "node_descriptors_v6": "node_descriptors_v7", } ) async def _migrate_to_v8(self): """Schema v8 added the `devices_v8.last_seen` column.""" async with self.execute("SELECT * FROM devices_v7") as cursor: async for ieee, nwk, status in cursor: # Set the default `last_seen` to the unix epoch await self.execute( "INSERT INTO devices_v8 VALUES (?, ?, ?, ?)", (ieee, nwk, status, 0), ) # Copy the devices table first, it should have no conflicts await self._migrate_tables( { "endpoints_v7": "endpoints_v8", "in_clusters_v7": "in_clusters_v8", "out_clusters_v7": "out_clusters_v8", "groups_v7": "groups_v8", "group_members_v7": "group_members_v8", "relays_v7": "relays_v8", "attributes_cache_v7": "attributes_cache_v8", "neighbors_v7": "neighbors_v8", "node_descriptors_v7": "node_descriptors_v8", "unsupported_attributes_v7": "unsupported_attributes_v8", "devices_v7": None, } ) async def _migrate_to_v9(self): """Schema v9 changed the data type of the `devices_v8.last_seen` column.""" await self.execute( """INSERT INTO devices_v9 (ieee, nwk, status, last_seen) SELECT ieee, nwk, status, last_seen / 1000.0 FROM devices_v8""" ) await self._migrate_tables( { "endpoints_v8": "endpoints_v9", "in_clusters_v8": "in_clusters_v9", "out_clusters_v8": "out_clusters_v9", "groups_v8": "groups_v9", "group_members_v8": "group_members_v9", "relays_v8": "relays_v9", "attributes_cache_v8": "attributes_cache_v9", "neighbors_v8": "neighbors_v9", "node_descriptors_v8": "node_descriptors_v9", "unsupported_attributes_v8": "unsupported_attributes_v9", "devices_v8": None, } ) async def _migrate_to_v10(self): """Schema v10 added a new `network_backups_v10` table.""" await self._migrate_tables( { "devices_v9": "devices_v10", "endpoints_v9": "endpoints_v10", "in_clusters_v9": "in_clusters_v10", "out_clusters_v9": "out_clusters_v10", "groups_v9": "groups_v10", "group_members_v9": "group_members_v10", "relays_v9": "relays_v10", "attributes_cache_v9": "attributes_cache_v10", "neighbors_v9": "neighbors_v10", "node_descriptors_v9": "node_descriptors_v10", "unsupported_attributes_v9": "unsupported_attributes_v10", } ) async def _migrate_to_v11(self): """Schema v11 added a new `routes_v11` table.""" await self._migrate_tables( { "devices_v10": "devices_v11", "endpoints_v10": "endpoints_v11", "in_clusters_v10": "in_clusters_v11", "out_clusters_v10": "out_clusters_v11", "groups_v10": "groups_v11", "group_members_v10": "group_members_v11", "relays_v10": "relays_v11", "attributes_cache_v10": "attributes_cache_v11", "neighbors_v10": "neighbors_v11", "node_descriptors_v10": "node_descriptors_v11", "unsupported_attributes_v10": "unsupported_attributes_v11", "network_backups_v10": "network_backups_v11", } ) async def _migrate_to_v12(self): """Schema v12 added a `timestamp` column to attribute updates.""" await self._migrate_tables( { "devices_v11": "devices_v12", "endpoints_v11": "endpoints_v12", "in_clusters_v11": "in_clusters_v12", "neighbors_v11": "neighbors_v12", "routes_v11": "routes_v12", "node_descriptors_v11": "node_descriptors_v12", "out_clusters_v11": "out_clusters_v12", "groups_v11": "groups_v12", "group_members_v11": "group_members_v12", "relays_v11": "relays_v12", "unsupported_attributes_v11": "unsupported_attributes_v12", "network_backups_v11": "network_backups_v12", "attributes_cache_v11": None, } ) async with self.execute("SELECT * FROM attributes_cache_v11") as cursor: async for ieee, endpoint_id, cluster_id, attrid, value in cursor: # Set the default `last_updated` to the unix epoch await self.execute( "INSERT INTO attributes_cache_v12 VALUES (?, ?, ?, ?, ?, ?)", (ieee, endpoint_id, cluster_id, attrid, value, 0), ) async def _migrate_to_v13(self): """Schema v13 combines both cluster types and caching for all attributes.""" await self._migrate_tables( { "devices_v12": "devices_v13", "endpoints_v12": "endpoints_v13", "neighbors_v12": "neighbors_v13", "routes_v12": "routes_v13", "node_descriptors_v12": "node_descriptors_v13", "groups_v12": "groups_v13", "group_members_v12": "group_members_v13", "relays_v12": "relays_v13", "network_backups_v12": "network_backups_v13", "in_clusters_v12": None, "out_clusters_v12": None, "unsupported_attributes_v12": None, "attributes_cache_v12": None, } ) async with self.execute("SELECT * FROM in_clusters_v12") as cursor: async for ieee, endpoint_id, cluster_id in cursor: await self.execute( "INSERT INTO clusters_v13 VALUES (?, ?, ?, ?)", (ieee, endpoint_id, ClusterType.Server, cluster_id), ) async with self.execute("SELECT * FROM out_clusters_v12") as cursor: async for ieee, endpoint_id, cluster_id in cursor: await self.execute( "INSERT INTO clusters_v13 VALUES (?, ?, ?, ?)", (ieee, endpoint_id, ClusterType.Client, cluster_id), ) async with self.execute("SELECT * FROM unsupported_attributes_v12") as cursor: async for ieee, endpoint_id, cluster_id, attrid in cursor: await self.execute( "INSERT INTO unsupported_attributes_v13 VALUES (?, ?, ?, ?, ?)", (ieee, endpoint_id, ClusterType.Server, cluster_id, attrid), ) async with self.execute("SELECT * FROM attributes_cache_v12") as cursor: async for ( ieee, endpoint_id, cluster_id, attrid, value, last_updated, ) in cursor: await self.execute( "INSERT INTO attributes_cache_v13 VALUES (?, ?, ?, ?, ?, ?, ?)", ( ieee, endpoint_id, ClusterType.Server, cluster_id, attrid, value, last_updated, ), ) zigpy-0.80.1/zigpy/appdb_schemas/000077500000000000000000000000001501451476000167005ustar00rootroot00000000000000zigpy-0.80.1/zigpy/appdb_schemas/__init__.py000066400000000000000000000004431501451476000210120ustar00rootroot00000000000000from __future__ import annotations import importlib.resources # Map each schema version to its SQL SCHEMAS = {} for file in importlib.resources.files(__name__).glob("schema_v*.sql"): n = int(file.name.replace("schema_v", "").replace(".sql", ""), 10) SCHEMAS[n] = file.read_text() zigpy-0.80.1/zigpy/appdb_schemas/schema_v0.sql000066400000000000000000000015341501451476000212710ustar00rootroot00000000000000PRAGMA user_version = 0; CREATE TABLE IF NOT EXISTS devices (ieee ieee, nwk, status); CREATE TABLE IF NOT EXISTS endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status); CREATE TABLE IF NOT EXISTS clusters (ieee ieee, endpoint_id, cluster); CREATE TABLE IF NOT EXISTS output_clusters (ieee ieee, endpoint_id, cluster); CREATE TABLE IF NOT EXISTS attributes (ieee ieee, endpoint_id, cluster, attrid, value); CREATE UNIQUE INDEX IF NOT EXISTS ieee_idx ON devices(ieee); CREATE UNIQUE INDEX IF NOT EXISTS endpoint_idx ON endpoints(ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS cluster_idx ON clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); zigpy-0.80.1/zigpy/appdb_schemas/schema_v1.sql000066400000000000000000000032551501451476000212740ustar00rootroot00000000000000PRAGMA user_version = 1; CREATE TABLE IF NOT EXISTS devices (ieee ieee, nwk, status); CREATE TABLE IF NOT EXISTS endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status); CREATE TABLE IF NOT EXISTS clusters (ieee ieee, endpoint_id, cluster); CREATE TABLE IF NOT EXISTS node_descriptors (ieee ieee, value, FOREIGN KEY(ieee) REFERENCES devices(ieee)); CREATE TABLE IF NOT EXISTS output_clusters (ieee ieee, endpoint_id, cluster); CREATE TABLE IF NOT EXISTS attributes (ieee ieee, endpoint_id, cluster, attrid, value); CREATE TABLE IF NOT EXISTS groups (group_id, name); CREATE TABLE IF NOT EXISTS group_members (group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id), FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id)); CREATE TABLE IF NOT EXISTS relays (ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE UNIQUE INDEX IF NOT EXISTS ieee_idx ON devices(ieee); CREATE UNIQUE INDEX IF NOT EXISTS endpoint_idx ON endpoints(ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS cluster_idx ON clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS node_descriptors_idx ON node_descriptors(ieee); CREATE UNIQUE INDEX IF NOT EXISTS output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX IF NOT EXISTS group_idx ON groups(group_id); CREATE UNIQUE INDEX IF NOT EXISTS group_members_idx ON group_members(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS relays_idx ON relays(ieee); zigpy-0.80.1/zigpy/appdb_schemas/schema_v10.sql000066400000000000000000000121061501451476000213470ustar00rootroot00000000000000PRAGMA user_version = 10; -- devices DROP TABLE IF EXISTS devices_v10; CREATE TABLE devices_v10 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen REAL NOT NULL ); CREATE UNIQUE INDEX devices_idx_v10 ON devices_v10(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v10; CREATE TABLE endpoints_v10 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v10(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v10 ON endpoints_v10(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v10; CREATE TABLE in_clusters_v10 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v10(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v10 ON in_clusters_v10(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v10; CREATE TABLE neighbors_v10 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v10(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v10 ON neighbors_v10(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v10; CREATE TABLE node_descriptors_v10 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v10(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v10 ON node_descriptors_v10(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v10; CREATE TABLE out_clusters_v10 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v10(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v10 ON out_clusters_v10(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v10; CREATE TABLE attributes_cache_v10 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v10(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v10 ON attributes_cache_v10(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v10; CREATE TABLE groups_v10 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v10 ON groups_v10(group_id); -- group members DROP TABLE IF EXISTS group_members_v10; CREATE TABLE group_members_v10 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v10(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v10(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v10 ON group_members_v10(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v10; CREATE TABLE relays_v10 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v10(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v10 ON relays_v10(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v10; CREATE TABLE unsupported_attributes_v10 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v10(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v10(ieee, endpoint_id, cluster) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v10 ON unsupported_attributes_v10(ieee, endpoint_id, cluster, attrid); -- network backups DROP TABLE IF EXISTS network_backups_v10; CREATE TABLE network_backups_v10 ( id INTEGER PRIMARY KEY AUTOINCREMENT, backup_json TEXT NOT NULL ); zigpy-0.80.1/zigpy/appdb_schemas/schema_v11.sql000066400000000000000000000127411501451476000213550ustar00rootroot00000000000000PRAGMA user_version = 11; -- devices DROP TABLE IF EXISTS devices_v11; CREATE TABLE devices_v11 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen REAL NOT NULL ); CREATE UNIQUE INDEX devices_idx_v11 ON devices_v11(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v11; CREATE TABLE endpoints_v11 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v11(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v11 ON endpoints_v11(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v11; CREATE TABLE in_clusters_v11 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v11(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v11 ON in_clusters_v11(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v11; CREATE TABLE neighbors_v11 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v11(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v11 ON neighbors_v11(device_ieee); -- routes DROP TABLE IF EXISTS routes_v11; CREATE TABLE routes_v11 ( device_ieee ieee NOT NULL, dst_nwk INTEGER NOT NULL, route_status INTEGER NOT NULL, memory_constrained INTEGER NOT NULL, many_to_one INTEGER NOT NULL, route_record_required INTEGER NOT NULL, reserved INTEGER NOT NULL, next_hop INTEGER NOT NULL ); CREATE INDEX routes_idx_v11 ON routes_v11(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v11; CREATE TABLE node_descriptors_v11 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v11(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v11 ON node_descriptors_v11(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v11; CREATE TABLE out_clusters_v11 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v11(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v11 ON out_clusters_v11(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v11; CREATE TABLE attributes_cache_v11 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v11(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v11 ON attributes_cache_v11(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v11; CREATE TABLE groups_v11 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v11 ON groups_v11(group_id); -- group members DROP TABLE IF EXISTS group_members_v11; CREATE TABLE group_members_v11 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v11(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v11(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v11 ON group_members_v11(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v11; CREATE TABLE relays_v11 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v11(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v11 ON relays_v11(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v11; CREATE TABLE unsupported_attributes_v11 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v11(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v11(ieee, endpoint_id, cluster) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v11 ON unsupported_attributes_v11(ieee, endpoint_id, cluster, attrid); -- network backups DROP TABLE IF EXISTS network_backups_v11; CREATE TABLE network_backups_v11 ( id INTEGER PRIMARY KEY AUTOINCREMENT, backup_json TEXT NOT NULL ); zigpy-0.80.1/zigpy/appdb_schemas/schema_v12.sql000066400000000000000000000130011501451476000213440ustar00rootroot00000000000000PRAGMA user_version = 12; -- devices DROP TABLE IF EXISTS devices_v12; CREATE TABLE devices_v12 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen REAL NOT NULL ); CREATE UNIQUE INDEX devices_idx_v12 ON devices_v12(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v12; CREATE TABLE endpoints_v12 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v12(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v12 ON endpoints_v12(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v12; CREATE TABLE in_clusters_v12 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v12(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v12 ON in_clusters_v12(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v12; CREATE TABLE neighbors_v12 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v12(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v12 ON neighbors_v12(device_ieee); -- routes DROP TABLE IF EXISTS routes_v12; CREATE TABLE routes_v12 ( device_ieee ieee NOT NULL, dst_nwk INTEGER NOT NULL, route_status INTEGER NOT NULL, memory_constrained INTEGER NOT NULL, many_to_one INTEGER NOT NULL, route_record_required INTEGER NOT NULL, reserved INTEGER NOT NULL, next_hop INTEGER NOT NULL ); CREATE INDEX routes_idx_v12 ON routes_v12(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v12; CREATE TABLE node_descriptors_v12 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v12(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v12 ON node_descriptors_v12(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v12; CREATE TABLE out_clusters_v12 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v12(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v12 ON out_clusters_v12(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v12; CREATE TABLE attributes_cache_v12 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, last_updated REAL NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v12(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v12 ON attributes_cache_v12(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v12; CREATE TABLE groups_v12 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v12 ON groups_v12(group_id); -- group members DROP TABLE IF EXISTS group_members_v12; CREATE TABLE group_members_v12 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v12(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v12(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v12 ON group_members_v12(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v12; CREATE TABLE relays_v12 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v12(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v12 ON relays_v12(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v12; CREATE TABLE unsupported_attributes_v12 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v12(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v12(ieee, endpoint_id, cluster) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v12 ON unsupported_attributes_v12(ieee, endpoint_id, cluster, attrid); -- network backups DROP TABLE IF EXISTS network_backups_v12; CREATE TABLE network_backups_v12 ( id INTEGER PRIMARY KEY AUTOINCREMENT, backup_json TEXT NOT NULL ); zigpy-0.80.1/zigpy/appdb_schemas/schema_v13.sql000066400000000000000000000124731501451476000213610ustar00rootroot00000000000000PRAGMA user_version = 13; -- devices DROP TABLE IF EXISTS devices_v13; CREATE TABLE devices_v13 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen REAL NOT NULL ); CREATE UNIQUE INDEX devices_idx_v13 ON devices_v13(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v13; CREATE TABLE endpoints_v13 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v13(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v13 ON endpoints_v13(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS clusters_v13; CREATE TABLE clusters_v13 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster_type INTEGER NOT NULL, cluster_id INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v13(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX clusters_idx_v13 ON clusters_v13(ieee, endpoint_id, cluster_type, cluster_id); -- attributes DROP TABLE IF EXISTS attributes_cache_v13; CREATE TABLE attributes_cache_v13 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster_type INTEGER NOT NULL, cluster_id INTEGER NOT NULL, attr_id INTEGER NOT NULL, value BLOB NOT NULL, last_updated REAL NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v13(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_cache_idx_v13 ON attributes_cache_v13(ieee, endpoint_id, cluster_type, cluster_id, attr_id); -- neighbors DROP TABLE IF EXISTS neighbors_v13; CREATE TABLE neighbors_v13 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v13(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v13 ON neighbors_v13(device_ieee); -- routes DROP TABLE IF EXISTS routes_v13; CREATE TABLE routes_v13 ( device_ieee ieee NOT NULL, dst_nwk INTEGER NOT NULL, route_status INTEGER NOT NULL, memory_constrained INTEGER NOT NULL, many_to_one INTEGER NOT NULL, route_record_required INTEGER NOT NULL, reserved INTEGER NOT NULL, next_hop INTEGER NOT NULL ); CREATE INDEX routes_idx_v13 ON routes_v13(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v13; CREATE TABLE node_descriptors_v13 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v13(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v13 ON node_descriptors_v13(ieee); -- groups DROP TABLE IF EXISTS groups_v13; CREATE TABLE groups_v13 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v13 ON groups_v13(group_id); -- group members DROP TABLE IF EXISTS group_members_v13; CREATE TABLE group_members_v13 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v13(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v13(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v13 ON group_members_v13(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v13; CREATE TABLE relays_v13 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v13(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v13 ON relays_v13(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v13; CREATE TABLE unsupported_attributes_v13 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster_type INTEGER NOT NULL, cluster_id INTEGER NOT NULL, attr_id INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v13(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster_type, cluster_id) REFERENCES clusters_v13(ieee, endpoint_id, cluster_type, cluster_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v13 ON unsupported_attributes_v13(ieee, endpoint_id, cluster_type, cluster_id, attr_id); -- network backups DROP TABLE IF EXISTS network_backups_v13; CREATE TABLE network_backups_v13 ( id INTEGER PRIMARY KEY AUTOINCREMENT, backup_json TEXT NOT NULL ); zigpy-0.80.1/zigpy/appdb_schemas/schema_v2.sql000066400000000000000000000040571501451476000212760ustar00rootroot00000000000000PRAGMA user_version = 2; CREATE TABLE IF NOT EXISTS devices (ieee ieee, nwk, status); CREATE TABLE IF NOT EXISTS endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS node_descriptors (ieee ieee, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS output_clusters (ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS attributes (ieee ieee, endpoint_id, cluster, attrid, value, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS groups (group_id, name); CREATE TABLE IF NOT EXISTS group_members (group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS relays (ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE UNIQUE INDEX IF NOT EXISTS ieee_idx ON devices(ieee); CREATE UNIQUE INDEX IF NOT EXISTS endpoint_idx ON endpoints(ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS cluster_idx ON clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS node_descriptors_idx ON node_descriptors(ieee); CREATE UNIQUE INDEX IF NOT EXISTS output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX IF NOT EXISTS group_idx ON groups(group_id); CREATE UNIQUE INDEX IF NOT EXISTS group_members_idx ON group_members(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS relays_idx ON relays(ieee); zigpy-0.80.1/zigpy/appdb_schemas/schema_v3.sql000066400000000000000000000040501501451476000212700ustar00rootroot00000000000000PRAGMA user_version = 3; CREATE TABLE IF NOT EXISTS devices (ieee ieee, nwk, status); CREATE TABLE IF NOT EXISTS endpoints (ieee ieee, endpoint_id, profile_id, device_type device_type, status); CREATE TABLE IF NOT EXISTS clusters (ieee ieee, endpoint_id, cluster); CREATE TABLE IF NOT EXISTS node_descriptors (ieee ieee, value, FOREIGN KEY(ieee) REFERENCES devices(ieee)); CREATE TABLE IF NOT EXISTS output_clusters (ieee ieee, endpoint_id, cluster); CREATE TABLE IF NOT EXISTS attributes (ieee ieee, endpoint_id, cluster, attrid, value); CREATE TABLE IF NOT EXISTS groups (group_id, name); CREATE TABLE IF NOT EXISTS group_members (group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id), FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id)); CREATE TABLE IF NOT EXISTS relays (ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE TABLE IF NOT EXISTS neighbors (device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL,ieee ieee NOT NULL, nwk INTEGER NOT NULL, struct INTEGER NOT NULL, permit_joining INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices(ieee) ON DELETE CASCADE); CREATE UNIQUE INDEX IF NOT EXISTS ieee_idx ON devices(ieee); CREATE UNIQUE INDEX IF NOT EXISTS endpoint_idx ON endpoints(ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS cluster_idx ON clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS node_descriptors_idx ON node_descriptors(ieee); CREATE UNIQUE INDEX IF NOT EXISTS output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); CREATE UNIQUE INDEX IF NOT EXISTS attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); CREATE UNIQUE INDEX IF NOT EXISTS group_idx ON groups(group_id); CREATE UNIQUE INDEX IF NOT EXISTS group_members_idx ON group_members(group_id, ieee, endpoint_id); CREATE UNIQUE INDEX IF NOT EXISTS relays_idx ON relays(ieee); CREATE INDEX IF NOT EXISTS neighbors_idx ON neighbors(device_ieee); zigpy-0.80.1/zigpy/appdb_schemas/schema_v4.sql000066400000000000000000000067641501451476000213070ustar00rootroot00000000000000PRAGMA user_version = 4; -- devices CREATE TABLE IF NOT EXISTS devices ( ieee ieee, nwk, status ); CREATE UNIQUE INDEX IF NOT EXISTS ieee_idx ON devices(ieee); -- endpoints CREATE TABLE IF NOT EXISTS endpoints ( ieee ieee, endpoint_id, profile_id, device_type device_type, status, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS endpoint_idx ON endpoints(ieee, endpoint_id); -- clusters CREATE TABLE IF NOT EXISTS clusters ( ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS cluster_idx ON clusters(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v4; CREATE TABLE neighbors_v4 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL ); CREATE INDEX neighbors_idx_v4 ON neighbors_v4(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v4; CREATE TABLE node_descriptors_v4 ( ieee ieee, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v4 ON node_descriptors_v4(ieee); -- output clusters CREATE TABLE IF NOT EXISTS output_clusters ( ieee ieee, endpoint_id, cluster, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS output_cluster_idx ON output_clusters(ieee, endpoint_id, cluster); -- attributes CREATE TABLE IF NOT EXISTS attributes ( ieee ieee, endpoint_id, cluster, attrid, value, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS attribute_idx ON attributes(ieee, endpoint_id, cluster, attrid); -- groups CREATE TABLE IF NOT EXISTS groups ( group_id, name ); CREATE UNIQUE INDEX IF NOT EXISTS group_idx ON groups(group_id); -- group members CREATE TABLE IF NOT EXISTS group_members ( group_id, ieee ieee, endpoint_id, FOREIGN KEY(group_id) REFERENCES groups(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS group_members_idx ON group_members(group_id, ieee, endpoint_id); -- relays CREATE TABLE IF NOT EXISTS relays ( ieee ieee, relays, FOREIGN KEY(ieee) REFERENCES devices(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX IF NOT EXISTS relays_idx ON relays(ieee); zigpy-0.80.1/zigpy/appdb_schemas/schema_v5.sql000066400000000000000000000104221501451476000212720ustar00rootroot00000000000000PRAGMA user_version = 5; -- devices DROP TABLE IF EXISTS devices_v5; CREATE TABLE devices_v5 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL ); CREATE UNIQUE INDEX devices_idx_v5 ON devices_v5(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v5; CREATE TABLE endpoints_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v5 ON endpoints_v5(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v5; CREATE TABLE in_clusters_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v5 ON in_clusters_v5(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v5; CREATE TABLE neighbors_v5 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v5 ON neighbors_v5(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v5; CREATE TABLE node_descriptors_v5 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v5 ON node_descriptors_v5(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v5; CREATE TABLE out_clusters_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v5 ON out_clusters_v5(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v5; CREATE TABLE attributes_cache_v5 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters that won't be present in the DB but whose -- values still need to be cached FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v5 ON attributes_cache_v5(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v5; CREATE TABLE groups_v5 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v5 ON groups_v5(group_id); -- group members DROP TABLE IF EXISTS group_members_v5; CREATE TABLE group_members_v5 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v5(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v5(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v5 ON group_members_v5(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v5; CREATE TABLE relays_v5 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v5(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v5 ON relays_v5(ieee); zigpy-0.80.1/zigpy/appdb_schemas/schema_v6.sql000066400000000000000000000104031501451476000212720ustar00rootroot00000000000000PRAGMA user_version = 6; -- devices DROP TABLE IF EXISTS devices_v6; CREATE TABLE devices_v6 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL ); CREATE UNIQUE INDEX devices_idx_v6 ON devices_v6(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v6; CREATE TABLE endpoints_v6 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v6(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v6 ON endpoints_v6(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v6; CREATE TABLE in_clusters_v6 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v6(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v6 ON in_clusters_v6(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v6; CREATE TABLE neighbors_v6 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v6(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v6 ON neighbors_v6(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v6; CREATE TABLE node_descriptors_v6 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v6(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v6 ON node_descriptors_v6(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v6; CREATE TABLE out_clusters_v6 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v6(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v6 ON out_clusters_v6(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v6; CREATE TABLE attributes_cache_v6 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v6(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v6 ON attributes_cache_v6(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v6; CREATE TABLE groups_v6 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v6 ON groups_v6(group_id); -- group members DROP TABLE IF EXISTS group_members_v6; CREATE TABLE group_members_v6 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v6(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v6(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v6 ON group_members_v6(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v6; CREATE TABLE relays_v6 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v6(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v6 ON relays_v6(ieee);zigpy-0.80.1/zigpy/appdb_schemas/schema_v7.sql000066400000000000000000000115041501451476000212760ustar00rootroot00000000000000PRAGMA user_version = 7; -- devices DROP TABLE IF EXISTS devices_v7; CREATE TABLE devices_v7 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL ); CREATE UNIQUE INDEX devices_idx_v7 ON devices_v7(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v7; CREATE TABLE endpoints_v7 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v7(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v7 ON endpoints_v7(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v7; CREATE TABLE in_clusters_v7 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v7(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v7 ON in_clusters_v7(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v7; CREATE TABLE neighbors_v7 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v7(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v7 ON neighbors_v7(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v7; CREATE TABLE node_descriptors_v7 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v7(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v7 ON node_descriptors_v7(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v7; CREATE TABLE out_clusters_v7 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v7(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v7 ON out_clusters_v7(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v7; CREATE TABLE attributes_cache_v7 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v7(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v7 ON attributes_cache_v7(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v7; CREATE TABLE groups_v7 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v7 ON groups_v7(group_id); -- group members DROP TABLE IF EXISTS group_members_v7; CREATE TABLE group_members_v7 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v7(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v7(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v7 ON group_members_v7(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v7; CREATE TABLE relays_v7 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v7(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v7 ON relays_v7(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v7; CREATE TABLE unsupported_attributes_v7 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v7(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v7(ieee, endpoint_id, cluster) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v7 ON unsupported_attributes_v7(ieee, endpoint_id, cluster, attrid); zigpy-0.80.1/zigpy/appdb_schemas/schema_v8.sql000066400000000000000000000115531501451476000213030ustar00rootroot00000000000000PRAGMA user_version = 8; -- devices DROP TABLE IF EXISTS devices_v8; CREATE TABLE devices_v8 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen unix_timestamp NOT NULL ); CREATE UNIQUE INDEX devices_idx_v8 ON devices_v8(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v8; CREATE TABLE endpoints_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v8 ON endpoints_v8(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v8; CREATE TABLE in_clusters_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v8(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v8 ON in_clusters_v8(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v8; CREATE TABLE neighbors_v8 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v8 ON neighbors_v8(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v8; CREATE TABLE node_descriptors_v8 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v8 ON node_descriptors_v8(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v8; CREATE TABLE out_clusters_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v8(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v8 ON out_clusters_v8(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v8; CREATE TABLE attributes_cache_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v8 ON attributes_cache_v8(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v8; CREATE TABLE groups_v8 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v8 ON groups_v8(group_id); -- group members DROP TABLE IF EXISTS group_members_v8; CREATE TABLE group_members_v8 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v8(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v8(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v8 ON group_members_v8(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v8; CREATE TABLE relays_v8 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v8 ON relays_v8(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v8; CREATE TABLE unsupported_attributes_v8 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v8(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v8(ieee, endpoint_id, cluster) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v8 ON unsupported_attributes_v8(ieee, endpoint_id, cluster, attrid); zigpy-0.80.1/zigpy/appdb_schemas/schema_v9.sql000066400000000000000000000115411501451476000213010ustar00rootroot00000000000000PRAGMA user_version = 9; -- devices DROP TABLE IF EXISTS devices_v9; CREATE TABLE devices_v9 ( ieee ieee NOT NULL, nwk INTEGER NOT NULL, status INTEGER NOT NULL, last_seen REAL NOT NULL ); CREATE UNIQUE INDEX devices_idx_v9 ON devices_v9(ieee); -- endpoints DROP TABLE IF EXISTS endpoints_v9; CREATE TABLE endpoints_v9 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, profile_id INTEGER NOT NULL, device_type INTEGER NOT NULL, status INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v9(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX endpoint_idx_v9 ON endpoints_v9(ieee, endpoint_id); -- clusters DROP TABLE IF EXISTS in_clusters_v9; CREATE TABLE in_clusters_v9 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v9(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX in_clusters_idx_v9 ON in_clusters_v9(ieee, endpoint_id, cluster); -- neighbors DROP TABLE IF EXISTS neighbors_v9; CREATE TABLE neighbors_v9 ( device_ieee ieee NOT NULL, extended_pan_id ieee NOT NULL, ieee ieee NOT NULL, nwk INTEGER NOT NULL, device_type INTEGER NOT NULL, rx_on_when_idle INTEGER NOT NULL, relationship INTEGER NOT NULL, reserved1 INTEGER NOT NULL, permit_joining INTEGER NOT NULL, reserved2 INTEGER NOT NULL, depth INTEGER NOT NULL, lqi INTEGER NOT NULL, FOREIGN KEY(device_ieee) REFERENCES devices_v9(ieee) ON DELETE CASCADE ); CREATE INDEX neighbors_idx_v9 ON neighbors_v9(device_ieee); -- node descriptors DROP TABLE IF EXISTS node_descriptors_v9; CREATE TABLE node_descriptors_v9 ( ieee ieee NOT NULL, logical_type INTEGER NOT NULL, complex_descriptor_available INTEGER NOT NULL, user_descriptor_available INTEGER NOT NULL, reserved INTEGER NOT NULL, aps_flags INTEGER NOT NULL, frequency_band INTEGER NOT NULL, mac_capability_flags INTEGER NOT NULL, manufacturer_code INTEGER NOT NULL, maximum_buffer_size INTEGER NOT NULL, maximum_incoming_transfer_size INTEGER NOT NULL, server_mask INTEGER NOT NULL, maximum_outgoing_transfer_size INTEGER NOT NULL, descriptor_capability_field INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v9(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX node_descriptors_idx_v9 ON node_descriptors_v9(ieee); -- output clusters DROP TABLE IF EXISTS out_clusters_v9; CREATE TABLE out_clusters_v9 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v9(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX out_clusters_idx_v9 ON out_clusters_v9(ieee, endpoint_id, cluster); -- attributes DROP TABLE IF EXISTS attributes_cache_v9; CREATE TABLE attributes_cache_v9 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, value BLOB NOT NULL, -- Quirks can create "virtual" clusters and endpoints that won't be present in the -- DB but whose values still need to be cached FOREIGN KEY(ieee) REFERENCES devices_v9(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX attributes_idx_v9 ON attributes_cache_v9(ieee, endpoint_id, cluster, attrid); -- groups DROP TABLE IF EXISTS groups_v9; CREATE TABLE groups_v9 ( group_id INTEGER NOT NULL, name TEXT NOT NULL ); CREATE UNIQUE INDEX groups_idx_v9 ON groups_v9(group_id); -- group members DROP TABLE IF EXISTS group_members_v9; CREATE TABLE group_members_v9 ( group_id INTEGER NOT NULL, ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES groups_v9(group_id) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id) REFERENCES endpoints_v9(ieee, endpoint_id) ON DELETE CASCADE ); CREATE UNIQUE INDEX group_members_idx_v9 ON group_members_v9(group_id, ieee, endpoint_id); -- relays DROP TABLE IF EXISTS relays_v9; CREATE TABLE relays_v9 ( ieee ieee NOT NULL, relays BLOB NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v9(ieee) ON DELETE CASCADE ); CREATE UNIQUE INDEX relays_idx_v9 ON relays_v9(ieee); -- unsupported attributes DROP TABLE IF EXISTS unsupported_attributes_v9; CREATE TABLE unsupported_attributes_v9 ( ieee ieee NOT NULL, endpoint_id INTEGER NOT NULL, cluster INTEGER NOT NULL, attrid INTEGER NOT NULL, FOREIGN KEY(ieee) REFERENCES devices_v9(ieee) ON DELETE CASCADE, FOREIGN KEY(ieee, endpoint_id, cluster) REFERENCES in_clusters_v9(ieee, endpoint_id, cluster) ON DELETE CASCADE ); CREATE UNIQUE INDEX unsupported_attributes_idx_v9 ON unsupported_attributes_v9(ieee, endpoint_id, cluster, attrid); zigpy-0.80.1/zigpy/application.py000066400000000000000000001431431501451476000167720ustar00rootroot00000000000000from __future__ import annotations import abc import asyncio import collections from collections.abc import AsyncGenerator, Coroutine import contextlib from datetime import datetime, timezone import errno import logging import os import random import sys import time import typing from typing import Any, TypeVar import warnings if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout # pragma: no cover else: from asyncio import timeout as asyncio_timeout # pragma: no cover import zigpy.appdb import zigpy.backups import zigpy.config as conf from zigpy.const import INTERFERENCE_MESSAGE from zigpy.datastructures import PriorityDynamicBoundedSemaphore import zigpy.device import zigpy.endpoint import zigpy.exceptions import zigpy.group import zigpy.listeners import zigpy.ota import zigpy.profiles import zigpy.quirks import zigpy.state import zigpy.topology import zigpy.types as t import zigpy.typing import zigpy.util import zigpy.zcl import zigpy.zdo import zigpy.zdo.types as zdo_types DEFAULT_ENDPOINT_ID = 1 LOGGER = logging.getLogger(__name__) TRANSIENT_CONNECTION_ERRORS = { errno.ENETUNREACH, } ENERGY_SCAN_WARN_THRESHOLD = 0.75 * 255 _R = TypeVar("_R") CHANNEL_CHANGE_BROADCAST_DELAY_S = 1.0 CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S = 1.0 class ControllerApplication(zigpy.util.ListenableMixin, abc.ABC): SCHEMA = conf.CONFIG_SCHEMA _watchdog_period: int = 30 _probe_configs: list[dict[str, Any]] = [] def __init__(self, config: dict) -> None: self.devices: dict[t.EUI64, zigpy.device.Device] = {} self.state: zigpy.state.State = zigpy.state.State() self._listeners = {} self._config = self.SCHEMA(config) self._dblistener = None self._groups = zigpy.group.Groups(self) self._listeners = {} self._send_sequence = 0 self._tasks: set[asyncio.Future[Any]] = set() self._watchdog_task: asyncio.Task | None = None self._concurrent_requests_semaphore = PriorityDynamicBoundedSemaphore( self._config[conf.CONF_MAX_CONCURRENT_REQUESTS] ) self.ota = zigpy.ota.OTA(self._config[conf.CONF_OTA], self) self.backups: zigpy.backups.BackupManager = zigpy.backups.BackupManager(self) self.topology: zigpy.topology.Topology = zigpy.topology.Topology(self) self._req_listeners: collections.defaultdict[ zigpy.device.Device, collections.deque[zigpy.listeners.BaseRequestListener], ] = collections.defaultdict(lambda: collections.deque([])) def create_task( self, target: Coroutine[Any, Any, _R], name: str | None = None ) -> asyncio.Task[_R]: """Create a task and store a reference to it until the task completes. target: target to call. """ task = asyncio.get_running_loop().create_task(target, name=name) self._tasks.add(task) task.add_done_callback(self._tasks.remove) return task async def _load_db(self) -> None: """Restore save state.""" database_file = self.config[conf.CONF_DATABASE] if not database_file: return self._dblistener = await zigpy.appdb.PersistingListener.new(database_file, self) await self._dblistener.load() self._add_db_listeners() def _add_db_listeners(self): if self._dblistener is None: return self.add_listener(self._dblistener) self.groups.add_listener(self._dblistener) self.backups.add_listener(self._dblistener) self.topology.add_listener(self._dblistener) def _remove_db_listeners(self): if self._dblistener is None: return self.topology.remove_listener(self._dblistener) self.backups.remove_listener(self._dblistener) self.groups.remove_listener(self._dblistener) self.remove_listener(self._dblistener) async def initialize(self, *, auto_form: bool = False) -> None: """Starts the network on a connected radio, optionally forming one with random settings if necessary. """ # Make sure the first thing we do is feed the watchdog if self.config[conf.CONF_WATCHDOG_ENABLED]: await self.watchdog_feed() self._watchdog_task = asyncio.create_task(self._watchdog_loop()) last_backup = self.backups.most_recent_backup() try: await self.load_network_info(load_devices=False) except zigpy.exceptions.NetworkNotFormed: LOGGER.info("Network is not formed") if not auto_form: raise if last_backup is None: # Form a new network if we have no backup await self.form_network() else: # Otherwise, restore the most recent backup LOGGER.info("Restoring the most recent network backup") await self.backups.restore_backup(last_backup) LOGGER.debug("Network info: %s", self.state.network_info) LOGGER.debug("Node info: %s", self.state.node_info) new_state = self.backups.from_network_state() if ( self.config[conf.CONF_NWK_VALIDATE_SETTINGS] and last_backup is not None and not new_state.is_compatible_with(last_backup) ): raise zigpy.exceptions.NetworkSettingsInconsistent( f"Radio network settings are not compatible with most recent backup!\n" f"Current settings: {new_state!r}\n" f"Last backup: {last_backup!r}", old_state=last_backup, new_state=new_state, ) await self.start_network() self._persist_coordinator_model_strings_in_db() # Some radios erroneously permit joins on startup try: await self.permit(0) except zigpy.exceptions.DeliveryError as e: if e.status != t.MACStatus.MAC_CHANNEL_ACCESS_FAILURE: raise # Some radios (like the Conbee) can fail to deliver the startup broadcast # due to interference LOGGER.warning("Failed to send startup broadcast: %s", e) LOGGER.warning(INTERFERENCE_MESSAGE) if self.config[conf.CONF_NWK_BACKUP_ENABLED]: self.backups.start_periodic_backups( # Config specifies the period in minutes, not seconds period=(60 * self.config[conf.CONF_NWK_BACKUP_PERIOD]) ) if self.config[conf.CONF_TOPO_SCAN_ENABLED]: # Config specifies the period in minutes, not seconds self.topology.start_periodic_scans( period=(60 * self.config[zigpy.config.CONF_TOPO_SCAN_PERIOD]) ) if ( self.config[conf.CONF_OTA][conf.CONF_OTA_ENABLED] and self.config[conf.CONF_OTA][conf.CONF_OTA_BROADCAST_ENABLED] ): self.ota.start_periodic_broadcasts( initial_delay=self._config[conf.CONF_OTA][ conf.CONF_OTA_BROADCAST_INITIAL_DELAY ], interval=self._config[conf.CONF_OTA][conf.CONF_OTA_BROADCAST_INTERVAL], ) async def startup(self, *, auto_form: bool = False) -> None: """Starts a network, optionally forming one with random settings if necessary.""" try: await self.connect() await self.initialize(auto_form=auto_form) except Exception as e: # noqa: BLE001 await self.shutdown(db=False) if isinstance(e, ConnectionError) or ( isinstance(e, OSError) and e.errno in TRANSIENT_CONNECTION_ERRORS ): raise zigpy.exceptions.TransientConnectionError from e raise @classmethod async def new( cls, config: dict, auto_form: bool = False, start_radio: bool = True ) -> ControllerApplication: """Create new instance of application controller.""" app = cls(config) await app._load_db() if start_radio: await app.startup(auto_form=auto_form) return app async def energy_scan( self, channels: t.Channels, duration_exp: int, count: int ) -> dict[int, float]: """Runs an energy detection scan and returns the per-channel scan results.""" try: rsp = await self._device.zdo.Mgmt_NWK_Update_req( zigpy.zdo.types.NwkUpdate( ScanChannels=channels, ScanDuration=duration_exp, ScanCount=count, ) ) except (asyncio.TimeoutError, zigpy.exceptions.DeliveryError): LOGGER.warning("Coordinator does not support energy scanning") scanned_channels = channels energy_values = [0] * scanned_channels else: _, scanned_channels, _, _, energy_values = rsp return dict(zip(scanned_channels, energy_values)) async def _move_network_to_channel( self, new_channel: int, new_nwk_update_id: int ) -> None: """Broadcasts the channel migration update request.""" # Default implementation for radios that migrate via a loopback ZDO request await self._device.zdo.Mgmt_NWK_Update_req( zigpy.zdo.types.NwkUpdate( ScanChannels=zigpy.types.Channels.from_channel_list([new_channel]), ScanDuration=zigpy.zdo.types.NwkUpdate.CHANNEL_CHANGE_REQ, nwkUpdateId=new_nwk_update_id, ) ) async def move_network_to_channel( self, new_channel: int, *, num_broadcasts: int = 5 ) -> None: """Moves the network to a new channel.""" if self.state.network_info.channel == new_channel: return new_nwk_update_id = (self.state.network_info.nwk_update_id + 1) % 0xFF for attempt in range(num_broadcasts): LOGGER.info( "Broadcasting migration to channel %s (%s of %s)", new_channel, attempt + 1, num_broadcasts, ) await zigpy.zdo.broadcast( app=self, command=zigpy.zdo.types.ZDOCmd.Mgmt_NWK_Update_req, grpid=None, radius=30, # Explicitly set the maximum radius broadcast_address=zigpy.types.BroadcastAddress.ALL_DEVICES, NwkUpdate=zigpy.zdo.types.NwkUpdate( ScanChannels=zigpy.types.Channels.from_channel_list([new_channel]), ScanDuration=zigpy.zdo.types.NwkUpdate.CHANNEL_CHANGE_REQ, nwkUpdateId=new_nwk_update_id, ), ) await asyncio.sleep(CHANNEL_CHANGE_BROADCAST_DELAY_S) # Move the coordinator itself, if supported await self._move_network_to_channel( new_channel=new_channel, new_nwk_update_id=new_nwk_update_id ) # Wait for settings to update while self.state.network_info.channel != new_channel: LOGGER.info("Waiting for channel change to take effect") await self.load_network_info(load_devices=False) await asyncio.sleep(CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S) LOGGER.info("Successfully migrated to channel %d", new_channel) async def form_network(self, *, fast: bool = False) -> None: """Writes random network settings to the coordinator.""" # First, make the settings consistent and randomly generate missing values channel = self.config[conf.CONF_NWK][conf.CONF_NWK_CHANNEL] channels = self.config[conf.CONF_NWK][conf.CONF_NWK_CHANNELS] pan_id = self.config[conf.CONF_NWK][conf.CONF_NWK_PAN_ID] extended_pan_id = self.config[conf.CONF_NWK][conf.CONF_NWK_EXTENDED_PAN_ID] network_key = self.config[conf.CONF_NWK][conf.CONF_NWK_KEY] tc_address = self.config[conf.CONF_NWK][conf.CONF_NWK_TC_ADDRESS] stack_specific = {} if fast: # Indicate to the radio library that the network is ephemeral stack_specific["form_quickly"] = True if pan_id is None: pan_id = random.SystemRandom().randint(0x0001, 0xFFFE + 1) if channel is None and fast: # Don't run an energy scan if this is an ephemeral network channel = next(iter(channels)) elif channel is None and not fast: # We can't run an energy scan without a running network on most radios try: await self.start_network() except zigpy.exceptions.NetworkNotFormed: await self.form_network(fast=True) await self.start_network() channel_energy = await self.energy_scan( channels=t.Channels.ALL_CHANNELS, duration_exp=4, count=1 ) channel = zigpy.util.pick_optimal_channel(channel_energy, channels=channels) if extended_pan_id is None: # TODO: exclude `FF:FF:FF:FF:FF:FF:FF:FF` and possibly more reserved EPIDs extended_pan_id = t.ExtendedPanId(os.urandom(8)) if network_key is None: network_key = t.KeyData(os.urandom(16)) if tc_address is None: tc_address = t.EUI64.UNKNOWN network_info = zigpy.state.NetworkInfo( extended_pan_id=extended_pan_id, pan_id=pan_id, nwk_update_id=self.config[conf.CONF_NWK][conf.CONF_NWK_UPDATE_ID], nwk_manager_id=0x0000, channel=channel, channel_mask=t.Channels.from_channel_list([channel]), security_level=5, network_key=zigpy.state.Key( key=network_key, tx_counter=0, rx_counter=0, seq=self.config[conf.CONF_NWK][conf.CONF_NWK_KEY_SEQ], ), tc_link_key=zigpy.state.Key( key=self.config[conf.CONF_NWK][conf.CONF_NWK_TC_LINK_KEY], tx_counter=0, rx_counter=0, seq=0, partner_ieee=tc_address, ), children=[], key_table=[], nwk_addresses={}, stack_specific=stack_specific, ) node_info = zigpy.state.NodeInfo( nwk=0x0000, ieee=t.EUI64.UNKNOWN, # Use the device IEEE address logical_type=zdo_types.LogicalType.Coordinator, ) LOGGER.debug("Forming a new network") await self.backups.restore_backup( backup=zigpy.backups.NetworkBackup( network_info=network_info, node_info=node_info, ), counter_increment=0, allow_incomplete=True, create_new=(not fast), ) async def shutdown(self, *, db: bool = True) -> None: """Shutdown controller.""" if self._watchdog_task is not None: self._watchdog_task.cancel() self.ota.stop_periodic_broadcasts() self.backups.stop_periodic_backups() self.topology.stop_periodic_scans() try: await self.disconnect() except Exception: # noqa: BLE001 LOGGER.warning("Failed to disconnect from radio", exc_info=True) if db and self._dblistener: self._remove_db_listeners() try: await self._dblistener.shutdown() except Exception: # noqa: BLE001 LOGGER.warning("Failed to disconnect from database", exc_info=True) def add_device(self, ieee: t.EUI64, nwk: t.NWK) -> zigpy.device.Device: """Creates a zigpy `Device` object with the provided IEEE and NWK addresses.""" assert isinstance(ieee, t.EUI64) # TODO: Shut down existing device dev = zigpy.device.Device(self, ieee, nwk) self.devices[ieee] = dev return dev def device_initialized(self, device: zigpy.device.Device) -> None: """Used by a device to signal that it is initialized""" LOGGER.debug("Device is initialized %s", device) self.listener_event("raw_device_initialized", device) device = zigpy.quirks.get_device(device) self.devices[device.ieee] = device if self._dblistener is not None: device.add_context_listener(self._dblistener) self.listener_event("device_initialized", device) async def remove( self, ieee: t.EUI64, remove_children: bool = True, rejoin: bool = False ) -> None: """Try to remove a device from the network. :param ieee: address of the device to be removed """ assert isinstance(ieee, t.EUI64) dev = self.devices.get(ieee) if not dev: LOGGER.debug("Device not found for removal: %s", ieee) return dev.cancel_initialization() LOGGER.info("Removing device 0x%04x (%s)", dev.nwk, ieee) self.create_task( self._remove_device(dev, remove_children=remove_children, rejoin=rejoin), f"remove_device-nwk={dev.nwk!r}-ieee={ieee!r}", ) if dev.node_desc is not None and dev.node_desc.is_end_device: parents = [] for parent in self.devices.values(): for zdo_neighbor in self.topology.neighbors[parent.ieee]: try: neighbor = self.get_device(ieee=zdo_neighbor.ieee) except KeyError: continue if neighbor is dev: parents.append(parent) for parent in parents: LOGGER.debug( "Sending leave request for %s to %s parent", dev.ieee, parent.ieee ) opts = parent.zdo.LeaveOptions.RemoveChildren if rejoin: opts |= parent.zdo.LeaveOptions.Rejoin parent.zdo.create_catching_task( parent.zdo.Mgmt_Leave_req(dev.ieee, opts) ) self.listener_event("device_removed", dev) async def _remove_device( self, device: zigpy.device.Device, remove_children: bool = True, rejoin: bool = False, ) -> None: """Send a remove request then pop the device.""" try: async with asyncio_timeout( 30 if device.node_desc is not None and device.node_desc.is_end_device else 7 ): await device.zdo.leave(remove_children=remove_children, rejoin=rejoin) except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: LOGGER.debug("Sending 'zdo_leave_req' failed: %s", ex) self.devices.pop(device.ieee, None) def deserialize( self, sender: zigpy.device.Device, endpoint_id: t.uint8_t, cluster_id: t.uint16_t, data: bytes, ) -> tuple[Any, bytes]: return sender.deserialize(endpoint_id, cluster_id, data) def handle_join( self, nwk: t.NWK, ieee: t.EUI64, parent_nwk: t.NWK, *, handle_rejoin: bool = True, ) -> None: """Called when a device joins or announces itself on the network.""" ieee = t.EUI64(ieee) try: dev = self.get_device(ieee=ieee) except KeyError: dev = self.add_device(ieee, nwk) LOGGER.info("New device 0x%04x (%s) joined the network", nwk, ieee) new_join = True else: if handle_rejoin: LOGGER.info("Device 0x%04x (%s) joined the network", nwk, ieee) new_join = False if dev.nwk != nwk: LOGGER.debug("Device %s changed id (0x%04x => 0x%04x)", ieee, dev.nwk, nwk) dev.nwk = nwk new_join = True # Not all stacks send a ZDO command when a device joins so the last_seen should # be updated dev.last_seen = datetime.now(timezone.utc) # Cancel all pending requests for the device dev._concurrent_requests_semaphore.cancel_waiting( zigpy.exceptions.DeliveryError("Device has re-joined the network") ) if new_join: self.listener_event("device_joined", dev) dev.schedule_initialize() elif not dev.is_initialized: # Re-initialize partially-initialized devices but don't emit "device_joined" dev.schedule_initialize() elif handle_rejoin: # Rescan groups for devices that are not newly joining and initialized dev.schedule_group_membership_scan() def handle_leave(self, nwk: t.NWK, ieee: t.EUI64): """Called when a device has left the network.""" LOGGER.info("Device 0x%04x (%s) left the network", nwk, ieee) try: dev = self.get_device(ieee=ieee) except KeyError: return dev._concurrent_requests_semaphore.cancel_waiting( zigpy.exceptions.DeliveryError("Device has left the network") ) self.listener_event("device_left", dev) def handle_relays(self, nwk: t.NWK, relays: list[t.NWK]) -> None: """Called when a list of relaying devices is received.""" try: device = self.get_device(nwk=nwk) except KeyError: LOGGER.warning("Received relays from an unknown device: %s", nwk) self.create_task( self._discover_unknown_device(nwk), f"discover_unknown_device_from_relays-nwk={nwk!r}", ) else: device.relays = zigpy.util.filter_relays(relays) @classmethod async def probe(cls, device_config: dict[str, Any]) -> bool | dict[str, Any]: """Probes the device specified by `device_config` and returns valid device settings if the radio supports the device. If the device is not supported, `False` is returned. """ device_configs = [conf.SCHEMA_DEVICE(device_config)] for overrides in cls._probe_configs: new_config = conf.SCHEMA_DEVICE({**device_config, **overrides}) if new_config not in device_configs: device_configs.append(new_config) for config in device_configs: app = cls({conf.CONF_DEVICE: config}) try: await app.connect() except Exception: # noqa: BLE001 LOGGER.debug("Failed to probe with config %s", config, exc_info=True) else: return config finally: await app.disconnect() return False @abc.abstractmethod async def connect(self) -> None: """Connect to the radio hardware and verify that it is compatible with the library. This method should be stateless if the connection attempt fails. """ raise NotImplementedError # pragma: no cover async def watchdog_feed(self) -> None: """Reset the firmware watchdog timer.""" LOGGER.debug("Feeding watchdog") await self._watchdog_feed() async def _watchdog_feed(self) -> None: """Reset the firmware watchdog timer. Implemented by the radio library.""" async def _watchdog_loop(self) -> None: """Watchdog loop to periodically test if the stack is still running.""" LOGGER.debug("Starting watchdog loop") while True: await asyncio.sleep(self._watchdog_period) try: await self.watchdog_feed() except Exception as e: # noqa: BLE001 LOGGER.warning("Watchdog failure", exc_info=e) # Treat the watchdog failure as a disconnect self.connection_lost(e) break LOGGER.debug("Stopping watchdog loop") def connection_lost(self, exc: Exception) -> None: """Connection lost callback.""" LOGGER.debug("Connection to the radio has been lost: %r", exc) self.listener_event("connection_lost", exc) @abc.abstractmethod async def disconnect(self): """Disconnects from the radio hardware and shuts down the network.""" raise NotImplementedError # pragma: no cover @abc.abstractmethod async def start_network(self): """Starts a Zigbee network with settings currently stored in the radio hardware.""" raise NotImplementedError # pragma: no cover @abc.abstractmethod async def force_remove(self, dev: zigpy.device.Device): """Instructs the radio to remove a device with a lower-level leave command. Not all radios implement this. """ raise NotImplementedError # pragma: no cover @abc.abstractmethod async def add_endpoint(self, descriptor: zdo_types.SimpleDescriptor): """Registers a new endpoint on the controlled device. Not all radios will implement this. """ raise NotImplementedError # pragma: no cover async def register_endpoints(self) -> None: """Registers all necessary endpoints. The exact order in which this method is called depends on the radio module. """ await self.add_endpoint( zdo_types.SimpleDescriptor( endpoint=1, profile=zigpy.profiles.zha.PROFILE_ID, device_type=zigpy.profiles.zha.DeviceType.IAS_CONTROL, device_version=0b0000, input_clusters=[ zigpy.zcl.clusters.general.Basic.cluster_id, zigpy.zcl.clusters.general.OnOff.cluster_id, zigpy.zcl.clusters.general.Time.cluster_id, zigpy.zcl.clusters.general.Ota.cluster_id, zigpy.zcl.clusters.security.IasAce.cluster_id, ], output_clusters=[ zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, zigpy.zcl.clusters.general.PollControl.cluster_id, zigpy.zcl.clusters.security.IasZone.cluster_id, zigpy.zcl.clusters.security.IasWd.cluster_id, ], ) ) await self.add_endpoint( zdo_types.SimpleDescriptor( endpoint=2, profile=zigpy.profiles.zll.PROFILE_ID, device_type=zigpy.profiles.zll.DeviceType.CONTROLLER, device_version=0b0000, input_clusters=[zigpy.zcl.clusters.general.Basic.cluster_id], output_clusters=[], ) ) for endpoint in self.config[conf.CONF_ADDITIONAL_ENDPOINTS]: await self.add_endpoint(endpoint) @contextlib.asynccontextmanager async def _limit_concurrency(self, *, priority: int = t.PacketPriority.NORMAL): """Async context manager to limit global coordinator request concurrency.""" start_time = time.monotonic() was_locked = self._concurrent_requests_semaphore.locked() if was_locked: LOGGER.debug( "Max concurrency (%s) reached, delaying request (%s enqueued)", self._concurrent_requests_semaphore.max_value, self._concurrent_requests_semaphore.num_waiting, ) async with self._concurrent_requests_semaphore(priority=priority): if was_locked: LOGGER.debug( "Previously delayed request is now running, delayed by %0.2fs", time.monotonic() - start_time, ) yield @abc.abstractmethod async def send_packet(self, packet: t.ZigbeePacket) -> None: """Send a Zigbee packet using the appropriate addressing mode and provided options.""" raise NotImplementedError # pragma: no cover def build_source_route_to(self, dest: zigpy.device.Device) -> list[t.NWK] | None: """Compute a source route to the destination device.""" if dest.relays is None: return None # TODO: utilize topology scanner information return dest.relays[::-1] async def request( self, device: zigpy.device.Device, profile: t.uint16_t, cluster: t.uint16_t, src_ep: t.uint8_t, dst_ep: t.uint8_t, sequence: t.uint8_t, data: bytes, *, expect_reply: bool = True, use_ieee: bool = False, extended_timeout: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, ) -> tuple[zigpy.zcl.foundation.Status, str]: """Submit and send data out as an unicast transmission. :param device: destination device :param profile: Zigbee Profile ID to use for outgoing message :param cluster: cluster id where the message is being sent :param src_ep: source endpoint id :param dst_ep: destination endpoint id :param sequence: transaction sequence number of the message :param data: Zigbee message payload :param expect_reply: True if this is essentially a request :param use_ieee: use EUI64 for destination addressing :param extended_timeout: instruct the radio to use slower APS retries """ if use_ieee: src = t.AddrModeAddress( addr_mode=t.AddrMode.IEEE, address=self.state.node_info.ieee ) dst = t.AddrModeAddress(addr_mode=t.AddrMode.IEEE, address=device.ieee) else: src = t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=self.state.node_info.nwk ) dst = t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk) if self.config[conf.CONF_SOURCE_ROUTING]: source_route = self.build_source_route_to(dest=device) else: source_route = None tx_options = t.TransmitOptions.NONE if ask_for_ack is not None: # Prefer `ask_for_ack` to `expect_reply` if ask_for_ack: tx_options |= t.TransmitOptions.ACK elif not expect_reply: tx_options |= t.TransmitOptions.ACK # Performing retries within zigpy allows us to reprioritize requests quickly # without locking up for ~30s when communicating with end devices max_attempts = self._config[conf.CONF_NWK_MAX_RETRIES] + 1 for attempt in range(max_attempts): if attempt > 0: tx_options |= t.TransmitOptions.FORCE_ROUTE_DISCOVERY try: await self.send_packet( t.ZigbeePacket( src=src, src_ep=src_ep, dst=dst, dst_ep=dst_ep, tsn=sequence, profile_id=profile, cluster_id=cluster, data=t.SerializableBytes(data), extended_timeout=extended_timeout, source_route=source_route, tx_options=tx_options, priority=priority, ) ) break except Exception: LOGGER.debug( "Failed to send packet, attempt %d of %d", attempt + 1, max_attempts, exc_info=True, ) if attempt >= max_attempts - 1: raise continue return (zigpy.zcl.foundation.Status.SUCCESS, "") async def mrequest( self, group_id: t.uint16_t, profile: t.uint8_t, cluster: t.uint16_t, src_ep: t.uint8_t, sequence: t.uint8_t, data: bytes, *, hops: int = 0, non_member_radius: int = 3, priority: int = t.PacketPriority.NORMAL, ): """Submit and send data out as a multicast transmission. :param group_id: destination multicast address :param profile: Zigbee Profile ID to use for outgoing message :param cluster: cluster id where the message is being sent :param src_ep: source endpoint id :param sequence: transaction sequence number of the message :param data: Zigbee message payload :param hops: the message will be delivered to all nodes within this number of hops of the sender. A value of zero is converted to MAX_HOPS :param non_member_radius: the number of hops that the message will be forwarded by devices that are not members of the group. A value of 7 or greater is treated as infinite """ await self.send_packet( t.ZigbeePacket( src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=self.state.node_info.nwk ), src_ep=src_ep, dst=t.AddrModeAddress(addr_mode=t.AddrMode.Group, address=group_id), tsn=sequence, profile_id=profile, cluster_id=cluster, data=t.SerializableBytes(data), tx_options=t.TransmitOptions.NONE, radius=hops, non_member_radius=non_member_radius, priority=priority, ) ) return (zigpy.zcl.foundation.Status.SUCCESS, "") async def broadcast( self, profile: t.uint16_t, cluster: t.uint16_t, src_ep: t.uint8_t, dst_ep: t.uint8_t, grpid: t.uint16_t, radius: int, sequence: t.uint8_t, data: bytes, broadcast_address: t.BroadcastAddress = t.BroadcastAddress.RX_ON_WHEN_IDLE, priority: int = t.PacketPriority.NORMAL, ) -> tuple[zigpy.zcl.foundation.Status, str]: """Submit and send data out as an unicast transmission. :param profile: Zigbee Profile ID to use for outgoing message :param cluster: cluster id where the message is being sent :param src_ep: source endpoint id :param dst_ep: destination endpoint id :param: grpid: group id to address the broadcast to :param radius: max radius of the broadcast :param sequence: transaction sequence number of the message :param data: zigbee message payload :param timeout: how long to wait for transmission ACK :param broadcast_address: broadcast address. """ await self.send_packet( t.ZigbeePacket( src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=self.state.node_info.nwk ), src_ep=src_ep, dst=t.AddrModeAddress( addr_mode=t.AddrMode.Broadcast, address=broadcast_address ), dst_ep=dst_ep, tsn=sequence, profile_id=profile, cluster_id=cluster, data=t.SerializableBytes(data), tx_options=t.TransmitOptions.NONE, radius=radius, priority=priority, ) ) return (zigpy.zcl.foundation.Status.SUCCESS, "") async def _discover_unknown_device(self, nwk: t.NWK) -> None: """Discover the IEEE address of a device with an unknown NWK.""" return await zigpy.zdo.broadcast( app=self, command=zdo_types.ZDOCmd.IEEE_addr_req, grpid=None, radius=0, NWKAddrOfInterest=nwk, RequestType=zdo_types.AddrRequestType.Single, StartIndex=0, ) def _maybe_parse_zdo(self, packet: t.ZigbeePacket) -> None: """Attempt to parse an incoming packet as ZDO, to extract useful notifications.""" # The current zigpy device may not exist if we receive a packet early try: zdo = self._device.zdo except KeyError: zdo = zigpy.zdo.ZDO(None) try: zdo_hdr, zdo_args = zdo.deserialize( cluster_id=packet.cluster_id, data=packet.data.serialize() ) except ValueError: LOGGER.debug("Could not parse ZDO message from packet") return # Interpret useful global ZDO responses and notifications if zdo_hdr.command_id == zdo_types.ZDOCmd.Device_annce: nwk, ieee, _ = zdo_args self.handle_join(nwk=nwk, ieee=ieee, parent_nwk=None) elif zdo_hdr.command_id in ( zdo_types.ZDOCmd.NWK_addr_rsp, zdo_types.ZDOCmd.IEEE_addr_rsp, ): status, ieee, nwk, _, _, _ = zdo_args if status == zdo_types.Status.SUCCESS: LOGGER.debug("Discovered IEEE address for NWK=%s: %s", nwk, ieee) self.handle_join( nwk=nwk, ieee=ieee, parent_nwk=None, handle_rejoin=False ) def packet_received(self, packet: t.ZigbeePacket) -> None: """Notify zigpy of a received Zigbee packet.""" LOGGER.debug("Received a packet: %r", packet) assert packet.src is not None assert packet.dst is not None # Peek into ZDO packets to handle possible ZDO notifications if zigpy.zdo.ZDO_ENDPOINT in (packet.src_ep, packet.dst_ep): self._maybe_parse_zdo(packet) try: device = self.get_device_with_address(packet.src) except KeyError: LOGGER.warning("Unknown device %r", packet.src) if packet.src.addr_mode == t.AddrMode.NWK: # Manually send a ZDO IEEE address request to discover the device self.create_task( self._discover_unknown_device(packet.src.address), f"discover_unknown_device_from_packet-nwk={packet.src.address!r}", ) return None self.listener_event( "handle_message", device, packet.profile_id, packet.cluster_id, packet.src_ep, packet.dst_ep, packet.data.serialize(), ) if device.is_initialized: return device.packet_received(packet) LOGGER.debug( "Received frame on uninitialized device %s" " from ep %s to ep %s, cluster %s: %r", device, packet.src_ep, packet.dst_ep, packet.cluster_id, packet.data, ) if ( packet.dst_ep == 0 or device.all_endpoints_init or ( device.has_non_zdo_endpoints and packet.cluster_id == zigpy.zcl.clusters.general.Basic.cluster_id ) ): # Allow the following responses: # - any ZDO # - ZCL if endpoints are initialized # - ZCL from Basic packet.cluster_id if endpoints are initializing if not device.initializing: device.schedule_initialize() return device.packet_received(packet) # Give quirks a chance to fast-initialize the device (at the moment only Xiaomi) zigpy.quirks.handle_message_from_uninitialized_sender( device, packet.profile_id, packet.cluster_id, packet.src_ep, packet.dst_ep, packet.data.serialize(), ) # Reload the device device object, in it was replaced by the quirk device = self.get_device(ieee=device.ieee) # If the quirk did not fast-initialize the device, start initialization if not device.initializing and not device.is_initialized: device.schedule_initialize() def handle_message( self, sender: zigpy.device.Device, profile: int, cluster: int, src_ep: int, dst_ep: int, message: bytes, *, dst_addressing: zigpy.typing.AddressingMode | None = None, ): """Deprecated compatibility function. Use `packet_received` instead.""" warnings.warn( "`handle_message` is deprecated, use `packet_received`", DeprecationWarning ) if dst_addressing is None: dst_addressing = t.AddrMode.NWK self.packet_received( t.ZigbeePacket( profile_id=profile, cluster_id=cluster, src_ep=src_ep, dst_ep=dst_ep, data=t.SerializableBytes(message), src=t.AddrModeAddress( addr_mode=dst_addressing, address={ t.AddrMode.NWK: sender.nwk, t.AddrMode.IEEE: sender.ieee, }[dst_addressing], ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=self.state.node_info.nwk, ), ) ) def get_device_with_address( self, address: t.AddrModeAddress ) -> zigpy.device.Device: """Gets a `Device` object using the provided address mode address.""" if address.addr_mode == t.AddrMode.NWK: return self.get_device(nwk=address.address) elif address.addr_mode == t.AddrMode.IEEE: return self.get_device(ieee=address.address) else: raise ValueError(f"Invalid address: {address!r}") @contextlib.contextmanager def callback_for_response( self, src: zigpy.device.Device | zigpy.listeners.ANY_DEVICE, filters: list[zigpy.listeners.MatcherType], callback: typing.Callable[ [ zigpy.zcl.foundation.ZCLHeader, zigpy.zcl.foundation.CommandSchema, ], typing.Any, ], ) -> typing.Any: """Context manager to create a callback that is passed Zigbee responses.""" listener = zigpy.listeners.CallbackListener( matchers=tuple(filters), callback=callback, ) self._req_listeners[src].append(listener) try: yield finally: self._req_listeners[src].remove(listener) @contextlib.contextmanager def wait_for_response( self, src: zigpy.device.Device | zigpy.listeners.ANY_DEVICE, filters: list[zigpy.listeners.MatcherType], ) -> typing.Any: """Context manager to wait for a Zigbee response.""" listener = zigpy.listeners.FutureListener( matchers=tuple(filters), future=asyncio.get_running_loop().create_future(), ) self._req_listeners[src].append(listener) try: yield listener.future finally: self._req_listeners[src].remove(listener) @abc.abstractmethod async def permit_ncp(self, time_s: int = 60) -> None: """Permit joining on NCP. Not all radios will require this method. """ raise NotImplementedError # pragma: no cover async def permit_with_key(self, node: t.EUI64, code: bytes, time_s: int = 60): """Permit a node to join with the provided install code bytes.""" warnings.warn( "`permit_with_key` is deprecated, use `permit_with_link_key`", DeprecationWarning, ) key = zigpy.util.convert_install_code(code) if key is None: raise ValueError(f"Invalid install code: {code!r}") await self.permit_with_link_key(node=node, link_key=key, time_s=time_s) @abc.abstractmethod async def permit_with_link_key( self, node: t.EUI64, link_key: t.KeyData, time_s: int = 60 ) -> None: """Permit a node to join with the provided link key.""" raise NotImplementedError # pragma: no cover @abc.abstractmethod async def write_network_info( self, *, network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo, ) -> None: """Writes network and node state to the radio hardware. Any information not supported by the radio should be logged as a warning. """ raise NotImplementedError # pragma: no cover @abc.abstractmethod async def load_network_info(self, *, load_devices: bool = False) -> None: """Loads network and node information from the radio hardware. :param load_devices: if `False`, supplementary network information that may take a while to load should be skipped. For example, device NWK addresses and link keys. """ raise NotImplementedError # pragma: no cover @abc.abstractmethod async def reset_network_info(self) -> None: """Leaves the current network.""" raise NotImplementedError # pragma: no cover async def network_scan( self, channels: t.Channels, duration_exp: int ) -> AsyncGenerator[t.NetworkBeacon, None]: """Scans for 802.15.4 networks with a specified duration exponent.""" async for network in self._network_scan( channels=channels, duration_exp=duration_exp ): yield network # @abc.abstractmethod async def _network_scan( self, channels: t.Channels, duration_exp: int ) -> AsyncGenerator[t.NetworkBeacon, None]: """Scans for 802.15.4 networks with a specified duration exponent.""" if False: yield # pragma: no cover async def packet_capture( self, channel: int ) -> AsyncGenerator[t.CapturedPacket, None]: """Packet capture on the specified channel.""" async for packet in self._packet_capture(channel=channel): yield packet # @abc.abstractmethod async def _packet_capture( self, channel: int ) -> AsyncGenerator[t.CapturedPacket, None]: """Packet capture on the specified channel, internal.""" if False: yield # pragma: no cover async def packet_capture_change_channel(self, channel: int) -> None: """Change the channel of an active packet capture.""" await self._packet_capture_change_channel(channel=channel) # @abc.abstractmethod async def _packet_capture_change_channel(self, channel: int) -> None: """Change the channel of an active packet capture, internal.""" async def permit(self, time_s: int = 60, node: t.EUI64 | str | None = None) -> None: """Permit joining on a specific node or all router nodes.""" assert 0 <= time_s <= 254 if node is not None: if not isinstance(node, t.EUI64): node = t.EUI64([t.uint8_t(p) for p in node]) if node != self.state.node_info.ieee: try: dev = self.get_device(ieee=node) r = await dev.zdo.permit(time_s) LOGGER.debug("Sent 'mgmt_permit_joining_req' to %s: %s", node, r) except KeyError: LOGGER.warning("Device '%s' not found", node) except zigpy.exceptions.DeliveryError as ex: LOGGER.warning("Couldn't open '%s' for joining: %s", node, ex) else: await self.permit_ncp(time_s) return await zigpy.zdo.broadcast( self, # app zdo_types.ZDOCmd.Mgmt_Permit_Joining_req, # command 0x0000, # grpid 0x00, # radius time_s, 0, broadcast_address=t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, ) await self.permit_ncp(time_s) def get_sequence(self) -> t.uint8_t: self._send_sequence = (self._send_sequence + 1) % 256 return self._send_sequence def get_device( self, ieee: t.EUI64 = None, nwk: t.NWK | int = None ) -> zigpy.device.Device: """Looks up a device in the `devices` dictionary based either on its NWK or IEEE address. """ if ieee is not None: return self.devices[ieee] # If there two coordinators are loaded from the database, we want the active one if nwk == self.state.node_info.nwk: return self.devices[self.state.node_info.ieee] # TODO: Make this not terrible # Unlike its IEEE address, a device's NWK address can change at runtime so this # is not as simple as building a second mapping for dev in self.devices.values(): if dev.nwk == nwk: return dev raise KeyError(f"Device not found: nwk={nwk!r}, ieee={ieee!r}") def get_endpoint_id(self, cluster_id: int, is_server_cluster: bool = False) -> int: """Returns coordinator endpoint id for specified cluster id.""" return DEFAULT_ENDPOINT_ID def get_dst_address(self, cluster: zigpy.zcl.Cluster) -> zdo_types.MultiAddress: """Helper to get a dst address for bind/unbind operations. Allows radios to provide correct information especially for radios which listen on specific endpoints only. :param cluster: cluster instance to be bound to coordinator :returns: returns a "destination address" """ dstaddr = zdo_types.MultiAddress() dstaddr.addrmode = 3 dstaddr.ieee = self.state.node_info.ieee dstaddr.endpoint = self.get_endpoint_id(cluster.cluster_id, cluster.is_server) return dstaddr @property def config(self) -> dict: """Return current configuration.""" return self._config @property def groups(self) -> zigpy.group.Groups: return self._groups @property def _device(self) -> zigpy.device.Device: """The device being controlled.""" return self.get_device(ieee=self.state.node_info.ieee) def _persist_coordinator_model_strings_in_db(self) -> None: cluster = self._device.endpoints[1].add_input_cluster( zigpy.zcl.clusters.general.Basic.cluster_id ) cluster.update_attribute( attrid=zigpy.zcl.clusters.general.Basic.AttributeDefs.model.id, value=self._device.model, ) cluster.update_attribute( attrid=zigpy.zcl.clusters.general.Basic.AttributeDefs.manufacturer.id, value=self._device.manufacturer, ) self.device_initialized(self._device) zigpy-0.80.1/zigpy/backports/000077500000000000000000000000001501451476000160775ustar00rootroot00000000000000zigpy-0.80.1/zigpy/backports/__init__.py000066400000000000000000000000541501451476000202070ustar00rootroot00000000000000"""Backports from newer Python versions.""" zigpy-0.80.1/zigpy/backports/enum.py000066400000000000000000000021241501451476000174140ustar00rootroot00000000000000"""Enum backports from standard lib.""" from __future__ import annotations from enum import Enum from typing import Any, TypeVar _StrEnumSelfT = TypeVar("_StrEnumSelfT", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" def __new__( cls: type[_StrEnumSelfT], value: str, *args: Any, **kwargs: Any ) -> _StrEnumSelfT: # noqa: PYI019 """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") return super().__new__(cls, value, *args, **kwargs) def __str__(self) -> str: """Return self.value.""" return self.value @staticmethod def _generate_next_value_( name: str, start: int, count: int, last_values: list[Any] ) -> Any: """Make `auto()` explicitly unsupported. We may revisit this when it's very clear that Python 3.11's `StrEnum.auto()` behavior will no longer change. """ raise TypeError("auto() is not supported by this implementation") zigpy-0.80.1/zigpy/backups.py000066400000000000000000000405351501451476000161200ustar00rootroot00000000000000"""Classes to interact with zigpy network backups, including JSON serialization.""" from __future__ import annotations import asyncio import copy import dataclasses from datetime import datetime, timezone import logging from typing import TYPE_CHECKING, Any import zigpy.config as conf import zigpy.state import zigpy.types as t from zigpy.util import ListenableMixin if TYPE_CHECKING: import zigpy.application LOGGER = logging.getLogger(__name__) BACKUP_FORMAT_VERSION = 1 @dataclasses.dataclass class NetworkBackup(t.BaseDataclassMixin): version: int = dataclasses.field(default=BACKUP_FORMAT_VERSION) backup_time: datetime = dataclasses.field( default_factory=lambda: datetime.now(timezone.utc) ) network_info: zigpy.state.NetworkInfo = dataclasses.field( default_factory=zigpy.state.NetworkInfo ) node_info: zigpy.state.NodeInfo = dataclasses.field( default_factory=zigpy.state.NodeInfo ) def is_compatible_with(self, backup: NetworkBackup) -> bool: """Two backups are compatible if, ignoring frame counters, the same external device will be able to join either network. """ return ( self.node_info.nwk == backup.node_info.nwk and self.node_info.logical_type == backup.node_info.logical_type and self.node_info.ieee == backup.node_info.ieee and self.network_info.extended_pan_id == backup.network_info.extended_pan_id and self.network_info.pan_id == backup.network_info.pan_id and self.network_info.nwk_update_id == backup.network_info.nwk_update_id and self.network_info.nwk_manager_id == backup.network_info.nwk_manager_id and self.network_info.channel == backup.network_info.channel and self.network_info.security_level == backup.network_info.security_level and self.network_info.tc_link_key.key == backup.network_info.tc_link_key.key and self.network_info.network_key.key == backup.network_info.network_key.key ) def supersedes(self, backup: NetworkBackup) -> bool: """Checks if this network backup is more recent than another backup.""" return ( self.is_compatible_with(backup) and ( self.network_info.network_key.tx_counter > backup.network_info.network_key.tx_counter ) and self.network_info.nwk_update_id >= backup.network_info.nwk_update_id ) def is_complete(self) -> bool: """Checks if this backup captures enough network state to recreate the network.""" return ( self.node_info.ieee != t.EUI64.UNKNOWN # noqa: PLR1714 and self.network_info.extended_pan_id != t.EUI64.UNKNOWN and self.network_info.pan_id not in (0x0000, 0xFFFF) and self.network_info.channel in range(11, 26 + 1) and self.network_info.network_key.key != t.KeyData.UNKNOWN ) def as_dict(self) -> dict[str, Any]: return { "version": self.version, "backup_time": self.backup_time.isoformat(), "network_info": self.network_info.as_dict(), "node_info": self.node_info.as_dict(), } @classmethod def from_dict(cls, obj: dict[str, Any]) -> NetworkBackup: if "metadata" in obj: return cls.from_open_coordinator_json(obj) elif "network_info" in obj: version = obj.get("version", 0) # Version 1 introduced the `model`, `manufacturer`, and `version` fields if version == 0: obj = copy.deepcopy(obj) obj["node_info"]["model"] = None obj["node_info"]["manufacturer"] = None obj["node_info"]["version"] = None version = 1 assert version == BACKUP_FORMAT_VERSION return cls( version=BACKUP_FORMAT_VERSION, backup_time=datetime.fromisoformat(obj["backup_time"]), network_info=zigpy.state.NetworkInfo.from_dict(obj["network_info"]), node_info=zigpy.state.NodeInfo.from_dict(obj["node_info"]), ) else: raise ValueError(f"Invalid network backup object: {obj!r}") def as_open_coordinator_json(self) -> dict[str, Any]: return _network_backup_to_open_coordinator_backup(self) @classmethod def from_open_coordinator_json(cls, obj: dict[str, Any]) -> NetworkBackup: return _open_coordinator_backup_to_network_backup(obj) class BackupManager(ListenableMixin): def __init__(self, app: zigpy.application.ControllerApplication): super().__init__() self.app: zigpy.application.ControllerApplication = app self.backups: list[NetworkBackup] = [] self._backup_task: asyncio.Task | None = None def most_recent_backup(self) -> NetworkBackup | None: """Most recent network backup""" return self.backups[-1] if self.backups else None def from_network_state(self) -> NetworkBackup: """Create a backup object from the current network's state.""" return NetworkBackup( network_info=self.app.state.network_info, node_info=self.app.state.node_info, ) async def create_backup(self, *, load_devices: bool = False) -> NetworkBackup: await self.app.load_network_info(load_devices=load_devices) backup = self.from_network_state() self.add_backup(backup) return backup async def restore_backup( self, backup: NetworkBackup, *, counter_increment: int = 10000, allow_incomplete: bool = False, create_new: bool = True, ) -> None: LOGGER.debug("Restoring backup %s", backup) if not backup.is_complete() and not allow_incomplete: raise ValueError("Backup is incomplete, it is not possible to restore") key = backup.network_info.network_key new_backup = NetworkBackup( network_info=backup.network_info.replace( network_key=key.replace(tx_counter=key.tx_counter + counter_increment) ), node_info=backup.node_info, ) await self.app.write_network_info( network_info=new_backup.network_info, node_info=new_backup.node_info, ) if create_new: await self.create_backup() def add_backup( self, backup: NetworkBackup, *, suppress_event: bool = False ) -> None: """Adds a new backup to the database, superseding older ones if necessary.""" LOGGER.debug("Adding a new backup %s", backup) if not backup.is_complete(): LOGGER.debug("Backup is incomplete, ignoring") return # Only delete the most recent backup if the frame counter doesn't roll back. # 1. Old Conbee backups replace one another: the FC never increments # 2. EZSP -> old Conbee: create bad backup for Conbee # 3. Old Conbee -> EZSP: replace Conbee backup, its FC is always zero for old_backup in self.backups[:]: if backup.is_compatible_with(old_backup) and ( backup.network_info.network_key.tx_counter >= old_backup.network_info.network_key.tx_counter ): if not suppress_event: self.listener_event("network_backup_removed", old_backup) self.backups.remove(old_backup) if not suppress_event: self.listener_event("network_backup_created", backup) self.backups.append(backup) def start_periodic_backups(self, period: float) -> None: self.stop_periodic_backups() self._backup_task = asyncio.create_task(self._backup_loop(period)) def stop_periodic_backups(self): if self._backup_task is not None: self._backup_task.cancel() async def _backup_loop(self, period: float): while True: try: await self.create_backup() except Exception: # noqa: BLE001 LOGGER.warning("Failed to create a network backup", exc_info=True) LOGGER.debug("Waiting for %ss before backing up again", period) await asyncio.sleep(period) def __getitem__(self, key) -> NetworkBackup: return self.backups[key] def _network_backup_to_open_coordinator_backup(backup: NetworkBackup) -> dict[str, Any]: """Converts a `NetworkBackup` to an Open Coordinator Backup-compatible dictionary.""" node_info = backup.node_info network_info = backup.network_info devices = {} for ieee, nwk in network_info.nwk_addresses.items(): devices[ieee] = { "ieee_address": ieee.serialize()[::-1].hex(), "nwk_address": nwk.serialize()[::-1].hex(), "is_child": False, } for ieee in network_info.children: if ieee not in devices: devices[ieee] = { "ieee_address": ieee.serialize()[::-1].hex(), "nwk_address": None, "is_child": True, } else: devices[ieee]["is_child"] = True for key in network_info.key_table: if key.partner_ieee not in devices: devices[key.partner_ieee] = { "ieee_address": key.partner_ieee.serialize()[::-1].hex(), "nwk_address": None, "is_child": False, } devices[key.partner_ieee]["link_key"] = { "key": key.key.serialize().hex(), "tx_counter": key.tx_counter, "rx_counter": key.rx_counter, } return { "metadata": { "version": 1, "format": "zigpy/open-coordinator-backup", "source": network_info.source, "internal": { "creation_time": backup.backup_time.isoformat(), "node": { "ieee": node_info.ieee.serialize()[::-1].hex(), "nwk": node_info.nwk.serialize()[::-1].hex(), "type": zigpy.state.LOGICAL_TYPE_TO_JSON[node_info.logical_type], "model": node_info.model, "manufacturer": node_info.manufacturer, "version": node_info.version, }, "network": { "tc_link_key": { "key": network_info.tc_link_key.key.serialize().hex(), "frame_counter": network_info.tc_link_key.tx_counter, }, "tc_address": network_info.tc_link_key.partner_ieee.serialize()[ ::-1 ].hex(), "nwk_manager": network_info.nwk_manager_id.serialize()[::-1].hex(), }, "link_key_seqs": { key.partner_ieee.serialize()[::-1].hex(): key.seq for key in network_info.key_table }, **network_info.metadata, }, }, "stack_specific": network_info.stack_specific, "coordinator_ieee": node_info.ieee.serialize()[::-1].hex(), "pan_id": network_info.pan_id.serialize()[::-1].hex(), "extended_pan_id": network_info.extended_pan_id.serialize()[::-1].hex(), "nwk_update_id": network_info.nwk_update_id, "security_level": network_info.security_level, "channel": network_info.channel, "channel_mask": list(network_info.channel_mask), "network_key": { "key": network_info.network_key.key.serialize().hex(), "sequence_number": network_info.network_key.seq or 0, "frame_counter": network_info.network_key.tx_counter or 0, }, "devices": sorted(devices.values(), key=lambda d: d["ieee_address"]), } def _open_coordinator_backup_to_network_backup(obj: dict[str, Any]) -> NetworkBackup: """Creates a `NetworkBackup` from an Open Coordinator Backup dictionary.""" internal = obj["metadata"].get("internal", {}) node_info = zigpy.state.NodeInfo() node_meta = internal.get("node", {}) if "nwk" in node_meta: node_info.nwk, _ = t.NWK.deserialize(bytes.fromhex(node_meta["nwk"])[::-1]) else: node_info.nwk = t.NWK(0x0000) node_info.logical_type = zigpy.state.JSON_TO_LOGICAL_TYPE[ node_meta.get("type", "coordinator") ] # Should be identical to `metadata.internal.node.ieee` node_info.ieee, _ = t.EUI64.deserialize( bytes.fromhex(obj["coordinator_ieee"])[::-1] ) node_info.model = node_meta.get("model") node_info.manufacturer = node_meta.get("manufacturer") node_info.version = node_meta.get("version") network_info = zigpy.state.NetworkInfo() network_info.source = obj["metadata"]["source"] network_info.metadata = { k: v for k, v in internal.items() if k not in ("node", "network", "link_key_seqs", "creation_time") } network_info.pan_id, _ = t.NWK.deserialize(bytes.fromhex(obj["pan_id"])[::-1]) network_info.extended_pan_id, _ = t.EUI64.deserialize( bytes.fromhex(obj["extended_pan_id"])[::-1] ) network_info.nwk_update_id = obj["nwk_update_id"] network_meta = internal.get("network", {}) if "nwk_manager" in network_meta: network_info.nwk_manager_id, _ = t.NWK.deserialize( bytes.fromhex(network_meta["nwk_manager"]) ) else: network_info.nwk_manager_id = t.NWK(0x0000) network_info.channel = obj["channel"] network_info.channel_mask = t.Channels.from_channel_list(obj["channel_mask"]) network_info.security_level = obj["security_level"] if obj.get("stack_specific"): network_info.stack_specific = obj.get("stack_specific") network_info.tc_link_key = zigpy.state.Key() if "tc_link_key" in network_meta: network_info.tc_link_key.key, _ = t.KeyData.deserialize( bytes.fromhex(network_meta["tc_link_key"]["key"]) ) network_info.tc_link_key.tx_counter = network_meta["tc_link_key"].get( "frame_counter", 0 ) network_info.tc_link_key.partner_ieee, _ = t.EUI64.deserialize( bytes.fromhex(network_meta["tc_address"])[::-1] ) else: network_info.tc_link_key.key = conf.CONF_NWK_TC_LINK_KEY_DEFAULT network_info.tc_link_key.partner_ieee = node_info.ieee network_info.network_key = zigpy.state.Key() network_info.network_key.key, _ = t.KeyData.deserialize( bytes.fromhex(obj["network_key"]["key"]) ) network_info.network_key.tx_counter = obj["network_key"]["frame_counter"] network_info.network_key.seq = obj["network_key"]["sequence_number"] network_info.children = [] network_info.nwk_addresses = {} for device in obj["devices"]: if device["nwk_address"] is not None: # zfill(4) is used because Z2M backups include 0x0ABC as `abc`, not `0abc` nwk, _ = t.NWK.deserialize( bytes.fromhex(device["nwk_address"].zfill(4))[::-1] ) else: nwk = None ieee, _ = t.EUI64.deserialize(bytes.fromhex(device["ieee_address"])[::-1]) # The `is_child` key is currently optional if device.get("is_child", True): network_info.children.append(ieee) if nwk is not None: network_info.nwk_addresses[ieee] = nwk if "link_key" in device: key = zigpy.state.Key() key.key, _ = t.KeyData.deserialize(bytes.fromhex(device["link_key"]["key"])) key.tx_counter = device["link_key"]["tx_counter"] key.rx_counter = device["link_key"]["rx_counter"] key.partner_ieee = ieee try: key.seq = obj["metadata"]["internal"]["link_key_seqs"][ device["ieee_address"] ] except KeyError: key.seq = 0 network_info.key_table.append(key) # XXX: Devices that are not children, have no NWK address, and have no link key # are effectively ignored, since there is no place to write them if "date" in internal: # Z2M format creation_time = internal["date"].replace("Z", "+00:00") else: # Zigpy format creation_time = internal.get("creation_time", "1970-01-01T00:00:00+00:00") return NetworkBackup( version=BACKUP_FORMAT_VERSION, backup_time=datetime.fromisoformat(creation_time), network_info=network_info, node_info=node_info, ) zigpy-0.80.1/zigpy/config/000077500000000000000000000000001501451476000153545ustar00rootroot00000000000000zigpy-0.80.1/zigpy/config/__init__.py000066400000000000000000000331631501451476000174730ustar00rootroot00000000000000"""Config schemas and validation.""" from __future__ import annotations import voluptuous as vol from zigpy.config.defaults import ( CONF_DEVICE_BAUDRATE_DEFAULT, CONF_DEVICE_FLOW_CONTROL_DEFAULT, CONF_MAX_CONCURRENT_REQUESTS_DEFAULT, CONF_NWK_BACKUP_ENABLED_DEFAULT, CONF_NWK_BACKUP_PERIOD_DEFAULT, CONF_NWK_CHANNEL_DEFAULT, CONF_NWK_CHANNELS_DEFAULT, CONF_NWK_EXTENDED_PAN_ID_DEFAULT, CONF_NWK_KEY_DEFAULT, CONF_NWK_KEY_SEQ_DEFAULT, CONF_NWK_MAX_RETRIES_DEFAULT, CONF_NWK_PAN_ID_DEFAULT, CONF_NWK_TC_ADDRESS_DEFAULT, CONF_NWK_TC_LINK_KEY_DEFAULT, CONF_NWK_UPDATE_ID_DEFAULT, CONF_NWK_VALIDATE_SETTINGS_DEFAULT, CONF_OTA_BROADCAST_ENABLED_DEFAULT, CONF_OTA_BROADCAST_INITIAL_DELAY_DEFAULT, CONF_OTA_BROADCAST_INTERVAL_DEFAULT, CONF_OTA_DISABLE_DEFAULT_PROVIDERS_DEFAULT, CONF_OTA_ENABLED_DEFAULT, CONF_OTA_EXTRA_PROVIDERS_DEFAULT, CONF_OTA_PROVIDERS_DEFAULT, CONF_SOURCE_ROUTING_DEFAULT, CONF_TOPO_SCAN_ENABLED_DEFAULT, CONF_TOPO_SCAN_PERIOD_DEFAULT, CONF_TOPO_SKIP_COORDINATOR_DEFAULT, CONF_WATCHDOG_ENABLED_DEFAULT, ) from zigpy.config.validators import ( cv_boolean, cv_deprecated, cv_folder, cv_hex, cv_json_file, cv_key, cv_ota_provider, cv_ota_provider_name, cv_simple_descriptor, ) import zigpy.types as t CONF_ADDITIONAL_ENDPOINTS = "additional_endpoints" CONF_DATABASE = "database_path" CONF_DEVICE = "device" CONF_DEVICE_PATH = "path" CONF_DEVICE_BAUDRATE = "baudrate" CONF_DEVICE_FLOW_CONTROL = "flow_control" CONF_MAX_CONCURRENT_REQUESTS = "max_concurrent_requests" CONF_NWK = "network" CONF_NWK_CHANNEL = "channel" CONF_NWK_CHANNELS = "channels" CONF_NWK_EXTENDED_PAN_ID = "extended_pan_id" CONF_NWK_PAN_ID = "pan_id" CONF_NWK_KEY = "key" CONF_NWK_KEY_SEQ = "key_sequence_number" CONF_NWK_MAX_RETRIES = "max_retries" CONF_NWK_TC_ADDRESS = "tc_address" CONF_NWK_TC_LINK_KEY = "tc_link_key" CONF_NWK_UPDATE_ID = "update_id" CONF_NWK_BACKUP_ENABLED = "backup_enabled" CONF_NWK_BACKUP_PERIOD = "backup_period" CONF_NWK_VALIDATE_SETTINGS = "validate_network_settings" CONF_OTA = "ota" CONF_OTA_PROVIDERS = "providers" CONF_OTA_ENABLED = "enabled" CONF_OTA_EXTRA_PROVIDERS = "extra_providers" CONF_OTA_DISABLE_DEFAULT_PROVIDERS = "disable_default_providers" CONF_OTA_PROVIDER_TYPE = "type" CONF_OTA_PROVIDER_URL = "url" CONF_OTA_PROVIDER_PATH = "path" CONF_OTA_PROVIDER_INDEX_FILE = "index_file" CONF_OTA_PROVIDER_OVERRIDE_PREVIOUS = "override_previous" CONF_OTA_PROVIDER_WARNING = "warning" CONF_OTA_BROADCAST_ENABLED = "broadcast_enabled" CONF_OTA_BROADCAST_INITIAL_DELAY = "broadcast_initial_delay" CONF_OTA_BROADCAST_INTERVAL = "broadcast_interval" CONF_OTA_PROVIDER_MANUF_IDS = "manufacturer_ids" CONF_SOURCE_ROUTING = "source_routing" CONF_STARTUP_ENERGY_SCAN = ( "startup_energy_scan" # Unused, kept to avoid breaking imports in dependencies ) CONF_TOPO_SCAN_PERIOD = "topology_scan_period" CONF_TOPO_SCAN_ENABLED = "topology_scan_enabled" CONF_TOPO_SKIP_COORDINATOR = "topology_scan_skip_coordinator" CONF_WATCHDOG_ENABLED = "watchdog_enabled" CONF_OTA_ALLOW_ADVANCED_DIR_STRING = ( "I understand I can *destroy* my devices by enabling OTA updates from files." " Some OTA updates can be mistakenly applied to the wrong device, breaking it." " I am consciously using this at my own risk." ) # Deprecated keys CONF_OTA_ADVANCED_DIR = "advanced_ota_dir" CONF_OTA_ALLOW_ADVANCED_DIR = "allow_advanced_ota_dir" CONF_OTA_DIR = "otau_dir" CONF_OTA_IKEA = "ikea_provider" CONF_OTA_IKEA_URL = "ikea_update_url" CONF_OTA_INOVELLI = "inovelli_provider" CONF_OTA_LEDVANCE = "ledvance_provider" CONF_OTA_SALUS = "salus_provider" CONF_OTA_SONOFF = "sonoff_provider" CONF_OTA_SONOFF_URL = "sonoff_update_url" CONF_OTA_THIRDREALITY = "thirdreality_provider" CONF_OTA_REMOTE_PROVIDERS = "remote_providers" CONF_OTA_Z2M_LOCAL_INDEX = "z2m_local_index" CONF_OTA_Z2M_REMOTE_INDEX = "z2m_remote_index" SCHEMA_DEVICE = vol.Schema( { vol.Required(CONF_DEVICE_PATH): str, vol.Optional(CONF_DEVICE_BAUDRATE, default=CONF_DEVICE_BAUDRATE_DEFAULT): int, vol.Optional( CONF_DEVICE_FLOW_CONTROL, default=CONF_DEVICE_FLOW_CONTROL_DEFAULT ): vol.In(["hardware", "software", None]), } ) SCHEMA_NETWORK = vol.Schema( { vol.Optional(CONF_NWK_CHANNEL, default=CONF_NWK_CHANNEL_DEFAULT): vol.Any( None, vol.All(cv_hex, vol.Range(min=11, max=26)) ), vol.Optional(CONF_NWK_CHANNELS, default=CONF_NWK_CHANNELS_DEFAULT): vol.Any( t.Channels, vol.All(list, t.Channels.from_channel_list) ), vol.Optional( CONF_NWK_EXTENDED_PAN_ID, default=CONF_NWK_EXTENDED_PAN_ID_DEFAULT ): vol.Any(None, t.ExtendedPanId, t.ExtendedPanId.convert), vol.Optional(CONF_NWK_KEY, default=CONF_NWK_KEY_DEFAULT): vol.Any(None, cv_key), vol.Optional(CONF_NWK_KEY_SEQ, default=CONF_NWK_KEY_SEQ_DEFAULT): vol.Range( min=0, max=255 ), vol.Optional(CONF_NWK_PAN_ID, default=CONF_NWK_PAN_ID_DEFAULT): vol.Any( None, t.PanId, vol.All(cv_hex, vol.Coerce(t.PanId)) ), vol.Optional(CONF_NWK_TC_ADDRESS, default=CONF_NWK_TC_ADDRESS_DEFAULT): vol.Any( None, t.EUI64, t.EUI64.convert ), vol.Optional( CONF_NWK_TC_LINK_KEY, default=CONF_NWK_TC_LINK_KEY_DEFAULT ): cv_key, vol.Optional(CONF_NWK_UPDATE_ID, default=CONF_NWK_UPDATE_ID_DEFAULT): vol.All( cv_hex, vol.Range(min=0, max=255) ), } ) SCHEMA_OTA_PROVIDER_BASE = vol.Schema( { vol.Required(CONF_OTA_PROVIDER_TYPE): cv_ota_provider_name, vol.Optional(CONF_OTA_PROVIDER_OVERRIDE_PREVIOUS, default=False): bool, vol.Optional(CONF_OTA_PROVIDER_MANUF_IDS, default=None): vol.Any( None, [cv_hex] ), } ) SCHEMA_OTA_PROVIDER_URL = SCHEMA_OTA_PROVIDER_BASE.extend( {vol.Optional(CONF_OTA_PROVIDER_URL): vol.Url()} ) SCHEMA_OTA_PROVIDER_URL_REQUIRED = SCHEMA_OTA_PROVIDER_BASE.extend( {vol.Required(CONF_OTA_PROVIDER_URL): vol.Url()} ) SCHEMA_OTA_PROVIDER_JSON_INDEX = SCHEMA_OTA_PROVIDER_BASE.extend( {vol.Required(CONF_OTA_PROVIDER_INDEX_FILE): cv_json_file} ) SCHEMA_OTA_PROVIDER_FOLDER = SCHEMA_OTA_PROVIDER_BASE.extend( { vol.Required(CONF_OTA_PROVIDER_PATH): cv_folder, vol.Required(CONF_OTA_PROVIDER_WARNING): vol.Equal( CONF_OTA_ALLOW_ADVANCED_DIR_STRING ), } ) # Deprecated SCHEMA_OTA_PROVIDER_REMOTE = vol.Schema( { vol.Required(CONF_OTA_PROVIDER_URL): str, vol.Optional(CONF_OTA_PROVIDER_MANUF_IDS, default=[]): [cv_hex], } ) SCHEMA_OTA_BASE = { vol.Optional(CONF_OTA_ENABLED, default=CONF_OTA_ENABLED_DEFAULT): cv_boolean, vol.Optional( CONF_OTA_BROADCAST_ENABLED, default=CONF_OTA_BROADCAST_ENABLED_DEFAULT ): cv_boolean, vol.Optional( CONF_OTA_BROADCAST_INITIAL_DELAY, default=CONF_OTA_BROADCAST_INITIAL_DELAY_DEFAULT, ): vol.All(vol.Coerce(float), vol.Range(min=0)), vol.Optional( CONF_OTA_BROADCAST_INTERVAL, default=CONF_OTA_BROADCAST_INTERVAL_DEFAULT ): vol.All(vol.Coerce(float), vol.Range(min=0)), vol.Optional(CONF_OTA_PROVIDERS, default=CONF_OTA_PROVIDERS_DEFAULT): [ cv_ota_provider ], vol.Optional( CONF_OTA_DISABLE_DEFAULT_PROVIDERS, default=CONF_OTA_DISABLE_DEFAULT_PROVIDERS_DEFAULT, ): [cv_ota_provider_name], vol.Optional(CONF_OTA_EXTRA_PROVIDERS, default=CONF_OTA_EXTRA_PROVIDERS_DEFAULT): [ cv_ota_provider ], } SCHEMA_OTA_DEPRECATED = { # Deprecated OTA providers vol.Optional(CONF_OTA_IKEA): vol.All( cv_deprecated( "The `ikea_provider` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'ikea'}]`" ), vol.Any( cv_boolean, vol.Url(), ), ), vol.Optional(CONF_OTA_INOVELLI): vol.All( cv_deprecated( "The `inovelli_provider` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'inovelli'}]`" ), vol.Any( cv_boolean, vol.Url(), ), ), vol.Optional(CONF_OTA_LEDVANCE): vol.All( cv_deprecated( "The `ledvance_provider` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'ledvance'}]`" ), vol.Any( cv_boolean, vol.Url(), ), ), vol.Optional(CONF_OTA_SALUS): vol.All( cv_deprecated( "The `salus_provider` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'salus'}]`" ), vol.Any( cv_boolean, vol.Url(), ), ), vol.Optional(CONF_OTA_SONOFF): vol.All( cv_deprecated( "The `sonoff_provider` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'sonoff'}]`" ), vol.Any( cv_boolean, vol.Url(), ), ), vol.Optional(CONF_OTA_THIRDREALITY): vol.All( cv_deprecated( "The `thirdreality_provider` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'thirdreality'}]`" ), vol.Any( cv_boolean, vol.Url(), ), ), # Z2M OTA providers vol.Optional(CONF_OTA_Z2M_LOCAL_INDEX): vol.All( cv_deprecated( "The `z2m_local_index` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'z2m_local'," " 'index_file': '/path/to/index.json'}]`" ), cv_json_file, ), vol.Optional(CONF_OTA_Z2M_REMOTE_INDEX): vol.All( cv_deprecated( "The `z2m_index` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'z2m'}]" ), vol.Any( cv_boolean, vol.Url(), ), ), # Advanced OTA config. You *do not* need to use this unless you're testing a new # OTA firmware that has no known metadata. vol.Optional(CONF_OTA_ADVANCED_DIR): vol.All( cv_deprecated( "The `advanced_ota_dir` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'advanced'," " 'warning': 'I understand ...'}]" ), cv_folder, ), # Unused keys vol.Optional(CONF_OTA_ALLOW_ADVANCED_DIR): vol.All( cv_deprecated( "The `allow_advanced_ota_dir` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'advanced'," " 'warning': 'I understand ...'}]" ), vol.Equal(CONF_OTA_ALLOW_ADVANCED_DIR_STRING), ), vol.Optional(CONF_OTA_REMOTE_PROVIDERS): vol.All( cv_deprecated( "The `remote_providers` key is deprecated, migrate your configuration" " to the `extra_providers` list instead: `extra_providers: [{'type': 'remote'," " 'url': 'https://example.com'}]`" ), [SCHEMA_OTA_PROVIDER_REMOTE], ), vol.Optional(CONF_OTA_SONOFF_URL): vol.All( cv_deprecated("The `sonoff_update_url` key has been removed") ), vol.Optional(CONF_OTA_DIR): vol.All( cv_deprecated( "`otau_dir` has been removed, use the `z2m` or `zigpy` providers instead" ) ), vol.Optional(CONF_OTA_IKEA_URL): vol.All( cv_deprecated("The `ikea_update_url` key has been removed") ), } SCHEMA_OTA = vol.Schema( {**SCHEMA_OTA_BASE, **SCHEMA_OTA_DEPRECATED}, extra=vol.ALLOW_EXTRA ) ZIGPY_SCHEMA = vol.Schema( { vol.Optional(CONF_DATABASE, default=None): vol.Any(None, str), vol.Optional(CONF_NWK, default={}): SCHEMA_NETWORK, vol.Optional(CONF_OTA, default={}): SCHEMA_OTA, vol.Optional( CONF_TOPO_SCAN_PERIOD, default=CONF_TOPO_SCAN_PERIOD_DEFAULT ): vol.All(int, vol.Range(min=20)), vol.Optional( CONF_TOPO_SCAN_ENABLED, default=CONF_TOPO_SCAN_ENABLED_DEFAULT ): cv_boolean, vol.Optional( CONF_TOPO_SKIP_COORDINATOR, default=CONF_TOPO_SKIP_COORDINATOR_DEFAULT ): cv_boolean, vol.Optional( CONF_NWK_BACKUP_ENABLED, default=CONF_NWK_BACKUP_ENABLED_DEFAULT ): cv_boolean, vol.Optional( CONF_NWK_BACKUP_PERIOD, default=CONF_NWK_BACKUP_PERIOD_DEFAULT ): vol.All(cv_hex, vol.Range(min=1)), vol.Optional( CONF_NWK_VALIDATE_SETTINGS, default=CONF_NWK_VALIDATE_SETTINGS_DEFAULT ): cv_boolean, vol.Optional( CONF_NWK_MAX_RETRIES, default=CONF_NWK_MAX_RETRIES_DEFAULT ): vol.All(int, vol.Range(min=0)), vol.Optional(CONF_ADDITIONAL_ENDPOINTS, default=[]): [cv_simple_descriptor], vol.Optional( CONF_MAX_CONCURRENT_REQUESTS, default=CONF_MAX_CONCURRENT_REQUESTS_DEFAULT ): vol.All(int, vol.Range(min=0)), vol.Optional(CONF_SOURCE_ROUTING, default=CONF_SOURCE_ROUTING_DEFAULT): ( cv_boolean ), vol.Optional( CONF_WATCHDOG_ENABLED, default=CONF_WATCHDOG_ENABLED_DEFAULT ): cv_boolean, }, extra=vol.ALLOW_EXTRA, ) CONFIG_SCHEMA = ZIGPY_SCHEMA.extend( {vol.Required(CONF_DEVICE): SCHEMA_DEVICE}, extra=vol.ALLOW_EXTRA ) zigpy-0.80.1/zigpy/config/defaults.py000066400000000000000000000031171501451476000175370ustar00rootroot00000000000000from __future__ import annotations import typing import zigpy.types as t if typing.TYPE_CHECKING: from zigpy.config import CONF_OTA_PROVIDER_TYPE CONF_OTA_PROVIDER_TYPE = "type" CONF_DEVICE_BAUDRATE_DEFAULT = 115200 CONF_DEVICE_FLOW_CONTROL_DEFAULT = None CONF_MAX_CONCURRENT_REQUESTS_DEFAULT = 8 CONF_NWK_BACKUP_ENABLED_DEFAULT = True CONF_NWK_BACKUP_PERIOD_DEFAULT = 24 * 60 # 24 hours CONF_NWK_CHANNEL_DEFAULT = None CONF_NWK_CHANNELS_DEFAULT = [11, 15, 20, 25] CONF_NWK_EXTENDED_PAN_ID_DEFAULT = None CONF_NWK_PAN_ID_DEFAULT = None CONF_NWK_KEY_DEFAULT = None CONF_NWK_KEY_SEQ_DEFAULT = 0x00 CONF_NWK_MAX_RETRIES_DEFAULT = 2 CONF_NWK_TC_ADDRESS_DEFAULT = None CONF_NWK_TC_LINK_KEY_DEFAULT = t.KeyData(b"ZigBeeAlliance09") CONF_NWK_UPDATE_ID_DEFAULT = 0x00 CONF_NWK_VALIDATE_SETTINGS_DEFAULT = False CONF_OTA_ENABLED_DEFAULT = True CONF_OTA_DISABLE_DEFAULT_PROVIDERS_DEFAULT: list[str] = [] CONF_OTA_BROADCAST_ENABLED_DEFAULT = True CONF_OTA_BROADCAST_INITIAL_DELAY_DEFAULT = 3.9 * 60 * 60 # 3.9 hours CONF_OTA_BROADCAST_INTERVAL_DEFAULT = 3.9 * 60 * 60 # 3.9 hours CONF_OTA_PROVIDERS_DEFAULT = [ { CONF_OTA_PROVIDER_TYPE: "ledvance", }, { CONF_OTA_PROVIDER_TYPE: "sonoff", }, { CONF_OTA_PROVIDER_TYPE: "inovelli", }, { CONF_OTA_PROVIDER_TYPE: "thirdreality", }, ] CONF_OTA_EXTRA_PROVIDERS_DEFAULT: list[dict[str, typing.Any]] = [] CONF_SOURCE_ROUTING_DEFAULT = False CONF_TOPO_SCAN_PERIOD_DEFAULT = 4 * 60 # 4 hours CONF_TOPO_SCAN_ENABLED_DEFAULT = True CONF_TOPO_SKIP_COORDINATOR_DEFAULT = False CONF_WATCHDOG_ENABLED_DEFAULT = True zigpy-0.80.1/zigpy/config/validators.py000066400000000000000000000067601501451476000201070ustar00rootroot00000000000000from __future__ import annotations import logging import pathlib import typing import warnings import voluptuous as vol import zigpy.config import zigpy.types as t import zigpy.zdo.types as zdo_t if typing.TYPE_CHECKING: import zigpy.ota.providers _LOGGER = logging.getLogger(__name__) def cv_boolean(value: bool | int | str) -> bool: """Validate and coerce a boolean value.""" if isinstance(value, bool): return value if isinstance(value, str): value = value.lower().strip() if value in ("1", "true", "yes", "on", "enable"): return True if value in ("0", "false", "no", "off", "disable"): return False elif isinstance(value, int): return bool(value) raise vol.Invalid(f"invalid boolean '{value}' value") def cv_hex(value: int | str) -> int: """Convert string with possible hex number into int.""" if isinstance(value, int): return value if not isinstance(value, str): raise vol.Invalid(f"{value} is not a valid hex number") try: if value.startswith("0x"): value = int(value, base=16) else: value = int(value) except ValueError as err: raise vol.Invalid(f"Could not convert '{value}' to number") from err return value def cv_key(key: list[int]) -> t.KeyData: """Validate a key.""" if not isinstance(key, list) or not all(isinstance(v, int) for v in key): raise vol.Invalid("key must be a list of integers") if len(key) != 16: raise vol.Invalid("key length must be 16") if not all(0 <= e <= 255 for e in key): raise vol.Invalid("Key bytes must be within (0..255) range") return t.KeyData(key) def cv_simple_descriptor(obj: dict[str, typing.Any]) -> zdo_t.SimpleDescriptor: """Validates a ZDO simple descriptor.""" if not isinstance(obj, dict): raise vol.Invalid("Not a dictionary") descriptor = zdo_t.SimpleDescriptor(**obj) if not descriptor.is_valid: raise vol.Invalid(f"Invalid simple descriptor {descriptor!r}") return descriptor def cv_deprecated(message: str) -> typing.Callable[[typing.Any], typing.Any]: """Factory function for creating a deprecation warning validator.""" def wrapper(obj: typing.Any) -> typing.Any: _LOGGER.warning(message) warnings.warn(message, DeprecationWarning, stacklevel=2) return obj return wrapper def cv_json_file(value: str) -> pathlib.Path: """Validate a JSON file.""" path = pathlib.Path(value) if not path.is_file(): raise vol.Invalid(f"{value} is not a JSON file") return path def cv_folder(value: str) -> pathlib.Path: """Validate a folder path.""" path = pathlib.Path(value) if not path.is_dir(): raise vol.Invalid(f"{value} is not a directory") return path def cv_ota_provider_name(name: str | None) -> type[zigpy.ota.providers.BaseOtaProvider]: """Validate OTA provider name.""" import zigpy.ota.providers if name not in zigpy.ota.providers.OTA_PROVIDER_TYPES: raise vol.Invalid(f"Unknown OTA provider: {name!r}") return zigpy.ota.providers.OTA_PROVIDER_TYPES[name] def cv_ota_provider(obj: dict) -> zigpy.ota.providers.BaseOtaProvider: """Validate OTA provider.""" provider_type = obj.get(zigpy.config.CONF_OTA_PROVIDER_TYPE) provider_cls = cv_ota_provider_name(provider_type) kwargs = provider_cls.VOL_SCHEMA(obj) kwargs.pop(zigpy.config.CONF_OTA_PROVIDER_TYPE) return provider_cls(**kwargs) zigpy-0.80.1/zigpy/const.py000066400000000000000000000012721501451476000156110ustar00rootroot00000000000000"""Zigpy Constants.""" from __future__ import annotations SIG_ENDPOINTS = "endpoints" SIG_EP_INPUT = "input_clusters" SIG_EP_OUTPUT = "output_clusters" SIG_EP_PROFILE = "profile_id" SIG_EP_TYPE = "device_type" SIG_MANUFACTURER = "manufacturer" SIG_MODEL = "model" SIG_MODELS_INFO = "models_info" SIG_NODE_DESC = "node_desc" SIG_SKIP_CONFIG = "skip_configuration" INTERFERENCE_MESSAGE = ( "If you are having problems joining new devices, are missing sensor" " updates, or have issues keeping devices joined, ensure your" " coordinator is away from interference sources such as USB 3.0" " devices, SSDs, WiFi routers, etc." ) APS_REPLY_TIMEOUT = 5 APS_REPLY_TIMEOUT_EXTENDED = 28 zigpy-0.80.1/zigpy/datastructures.py000066400000000000000000000235031501451476000175410ustar00rootroot00000000000000"""Primitive data structures.""" from __future__ import annotations import asyncio import bisect import contextlib import functools import types import typing class WrappedContextManager: def __init__( self, context_manager: contextlib.AbstractAsyncContextManager, on_enter: typing.Callable[[], typing.Awaitable[None]], ) -> None: self.on_enter = on_enter self.context_manager = context_manager async def __aenter__(self) -> None: await self.on_enter() return self.context_manager async def __aexit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, traceback: types.TracebackType | None, ) -> None: await self.context_manager.__aexit__(exc_type, exc, traceback) class PriorityDynamicBoundedSemaphore: """`asyncio.BoundedSemaphore` with public interface to change the max value.""" def __init__(self, value: int = 0) -> None: self._value: int = value self._max_value: int = value self._comparison_counter: int = 0 self._waiters: list[tuple[int, int, asyncio.Future]] = [] self._loop: asyncio.BaseEventLoop | None = None def _get_loop(self) -> asyncio.BaseEventLoop: loop = asyncio.get_running_loop() if self._loop is None: self._loop = loop if loop is not self._loop: raise RuntimeError(f"{self!r} is bound to a different event loop") return loop def _wake_up_next(self) -> bool: """Wake up the first waiter that isn't done.""" if not self._waiters: return False for _, _, fut in self._waiters: if not fut.done(): self._value -= 1 fut.set_result(True) # `fut` is now `done()` and not `cancelled()`. return True return False def cancel_waiting(self, exc: BaseException) -> None: """Cancel all waiters with the given exception.""" for _, _, fut in self._waiters: if not fut.done(): fut.set_exception(exc) @property def value(self) -> int: return self._value @property def max_value(self) -> int: return self._max_value @max_value.setter def max_value(self, new_value: int) -> None: """Update the semaphore's max value.""" if new_value < 0: raise ValueError(f"Semaphore value must be >= 0: {new_value!r}") delta = new_value - self._max_value self._value += delta self._max_value += delta # Wake up any pending waiters for _ in range(max(0, delta)): if not self._wake_up_next(): break @property def num_waiting(self) -> int: return len(self._waiters) def locked(self) -> bool: """Returns True if semaphore cannot be acquired immediately.""" # Due to state, or FIFO rules (must allow others to run first). return self._value <= 0 or (any(not w.cancelled() for _, _, w in self._waiters)) async def acquire(self, priority: int = 0) -> typing.Literal[True]: """Acquire a semaphore. If the internal counter is larger than zero on entry, decrement it by one and return True immediately. If it is zero on entry, block, waiting until some other task has called release() to make it larger than 0, and then return True. """ if not self.locked(): # Maintain FIFO, wait for others to start even if _value > 0. self._value -= 1 return True # To ensure that our objects don't have to be themselves comparable, we # maintain a global count and increment it on every insert. This way, # the tuple `(-priority, count, item)` will never have to compare `item`. self._comparison_counter += 1 fut = self._get_loop().create_future() obj = (-priority, self._comparison_counter, fut) bisect.insort_right(self._waiters, obj) try: try: await fut finally: self._waiters.remove(obj) except asyncio.CancelledError: # Currently the only exception designed be able to occur here. if fut.done() and not fut.cancelled(): # Our Future was successfully set to True via _wake_up_next(), # but we are not about to successfully acquire(). Therefore we # must undo the bookkeeping already done and attempt to wake # up someone else. self._value += 1 raise finally: # New waiters may have arrived but had to wait due to FIFO. # Wake up as many as are allowed. while self._value > 0: if not self._wake_up_next(): break # There was no-one to wake up. return True def release(self) -> None: """Release a semaphore, incrementing the internal counter by one. When it was zero on entry and another task is waiting for it to become larger than zero again, wake up that task. """ if self._value >= self._max_value: raise ValueError("Semaphore released too many times") self._value += 1 self._wake_up_next() def __call__(self, priority: int = 0) -> WrappedContextManager: """Allows specifying the priority by calling the context manager. This allows both `async with sem:` and `async with sem(priority=5):`. """ return WrappedContextManager( context_manager=self, on_enter=lambda: self.acquire(priority), ) async def __aenter__(self) -> None: await self.acquire() async def __aexit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, traceback: types.TracebackType | None, ) -> None: self.release() def __repr__(self) -> str: if self.locked(): extra = f"locked, max value:{self._max_value}, waiters:{len(self._waiters)}" else: extra = f"unlocked, value:{self._value}, max value:{self._max_value}" return f"<{self.__class__.__name__} [{extra}]>" class PriorityLock(PriorityDynamicBoundedSemaphore): def __init__(self): super().__init__(value=1) @PriorityDynamicBoundedSemaphore.max_value.setter def max_value(self, new_value: int) -> None: """Update the locks's max value.""" raise ValueError("Max value of lock cannot be updated") # Backwards compatibility DynamicBoundedSemaphore = PriorityDynamicBoundedSemaphore class ReschedulableTimeout: """Timeout object made to be efficiently rescheduled continuously.""" def __init__(self, callback: typing.Callable[[], None]) -> None: self._timer: asyncio.TimerHandle | None = None self._callback = callback self._when: float = 0 @functools.cached_property def _loop(self) -> asyncio.AbstractEventLoop: return asyncio.get_running_loop() def _timeout_trigger(self) -> None: now = self._loop.time() # If we triggered early, reschedule if self._when > now: self._reschedule() return self._timer = None self._callback() def _reschedule(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = self._loop.call_at(self._when, self._timeout_trigger) def reschedule(self, delay: float) -> None: self._when = self._loop.time() + delay # If the current timer will expire too late (or isn't running), reschedule if self._timer is None or self._timer.when() > self._when: self._reschedule() def cancel(self) -> None: if self._timer is not None: self._timer.cancel() self._timer = None class Debouncer: """Generic debouncer supporting per-invocation expiration.""" def __init__(self): self._times: dict[typing.Any, float] = {} self._queue: list[tuple[float, int, typing.Any]] = [] self._last_time: int = 0 self._dedup_counter: int = 0 @functools.cached_property def _loop(self) -> asyncio.BaseEventLoop: return asyncio.get_running_loop() def clean(self, now: float | None = None) -> None: """Clean up stale timers.""" if now is None: now = self._loop.time() # We store the negative expiration time to ensure we can pop expiring objects while self._queue and -self._queue[-1][0] < now: _, _, obj = self._queue.pop() self._times.pop(obj) def is_filtered(self, obj: typing.Any, now: float | None = None) -> bool: """Check if an object will be filtered.""" if now is None: now = self._loop.time() # Clean up stale timers self.clean(now) # If an object still exists after cleaning, it won't be expired return obj in self._times def filter(self, obj: typing.Any, expire_in: float) -> bool: """Check if an object should be filtered. If not, store it.""" now = self._loop.time() # For platforms with low-resolution clocks, we need to make sure that `obj` will # never be compared by `heapq`! if now > self._last_time: self._last_time = now self._dedup_counter = 0 self._dedup_counter += 1 # If the object is filtered, do nothing if self.is_filtered(obj, now=now): return True # Otherwise, queue it self._times[obj] = now + expire_in bisect.insort_right(self._queue, (-(now + expire_in), self._dedup_counter, obj)) return False def __repr__(self) -> str: """String representation of the debouncer.""" return f"<{self.__class__.__name__} [tracked:{len(self._queue)}]>" zigpy-0.80.1/zigpy/device.py000066400000000000000000000600311501451476000157200ustar00rootroot00000000000000from __future__ import annotations import asyncio import contextlib from datetime import datetime, timezone import enum import itertools import logging import sys import time import typing import warnings from zigpy.ota.manager import find_ota_cluster, update_firmware from zigpy.zcl.clusters.general import Ota if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout # pragma: no cover else: from asyncio import timeout as asyncio_timeout # pragma: no cover from zigpy import zdo from zigpy.const import ( APS_REPLY_TIMEOUT, APS_REPLY_TIMEOUT_EXTENDED, SIG_ENDPOINTS, SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE, SIG_MANUFACTURER, SIG_MODEL, SIG_NODE_DESC, ) import zigpy.datastructures import zigpy.endpoint import zigpy.exceptions import zigpy.listeners import zigpy.types as t from zigpy.typing import AddressingMode import zigpy.util from zigpy.zcl import foundation import zigpy.zdo.types as zdo_t if typing.TYPE_CHECKING: from zigpy.application import ControllerApplication from zigpy.ota.providers import OtaImageWithMetadata LOGGER = logging.getLogger(__name__) PACKET_DEBOUNCE_WINDOW = 10 MAX_DEVICE_CONCURRENCY = 1 AFTER_OTA_ATTR_READ_DELAY = 10 OTA_RETRY_DECORATOR = zigpy.util.retryable_request( tries=4, delay=AFTER_OTA_ATTR_READ_DELAY ) class Status(enum.IntEnum): """The status of a Device. Maintained for backwards compatibility.""" # No initialization done NEW = 0 # ZDO endpoint discovery done ZDO_INIT = 1 # Endpoints initialized ENDPOINTS_INIT = 2 class Device(zigpy.util.LocalLogMixin, zigpy.util.ListenableMixin): """A device on the network""" manufacturer_id_override = None def __init__(self, application: ControllerApplication, ieee: t.EUI64, nwk: t.NWK): self._application: ControllerApplication = application self._ieee: t.EUI64 = ieee self.nwk: t.NWK = t.NWK(nwk) self.zdo: zdo.ZDO = zdo.ZDO(self) self.endpoints: dict[int, zdo.ZDO | zigpy.endpoint.Endpoint] = {0: self.zdo} self.lqi: int | None = None self.rssi: int | None = None self.ota_in_progress: bool = False self._last_seen: datetime | None = None self._initialize_task: asyncio.Task | None = None self._group_scan_task: asyncio.Task | None = None self._listeners = {} self._manufacturer: str | None = None self._model: str | None = None self.node_desc: zdo_t.NodeDescriptor | None = None self._pending: zigpy.util.Requests[t.uint8_t] = zigpy.util.Requests() self._relays: t.Relays | None = None self._skip_configuration: bool = False self._send_sequence: int = 0 self._packet_debouncer = zigpy.datastructures.Debouncer() self._concurrent_requests_semaphore = ( zigpy.datastructures.PriorityDynamicBoundedSemaphore(MAX_DEVICE_CONCURRENCY) ) # Retained for backwards compatibility, will be removed in a future release self.status = Status.NEW @contextlib.asynccontextmanager async def _limit_concurrency(self, *, priority: int = 0): """Async context manager to limit device request concurrency.""" start_time = time.monotonic() was_locked = self._concurrent_requests_semaphore.locked() if was_locked: LOGGER.debug( "Device concurrency (%s) reached, delaying device request (%s enqueued)", self._concurrent_requests_semaphore.max_value, self._concurrent_requests_semaphore.num_waiting, ) async with self._concurrent_requests_semaphore(priority=priority): if was_locked: LOGGER.debug( "Previously delayed device request is now running, delayed by %0.2fs", time.monotonic() - start_time, ) yield def get_sequence(self) -> t.uint8_t: self._send_sequence = (self._send_sequence + 1) % 256 return self._send_sequence @property def name(self) -> str: return f"0x{self.nwk:04X}" def update_last_seen(self) -> None: """Update the `last_seen` attribute to the current time and emit an event.""" warnings.warn( "Calling `update_last_seen` directly is deprecated", DeprecationWarning ) self.last_seen = datetime.now(timezone.utc) @property def last_seen(self) -> float | None: return self._last_seen.timestamp() if self._last_seen is not None else None @last_seen.setter def last_seen(self, value: datetime | float): if isinstance(value, (int, float)): value = datetime.fromtimestamp(value, timezone.utc) self._last_seen = value self.listener_event("device_last_seen_updated", self._last_seen) @property def non_zdo_endpoints(self) -> list[zigpy.endpoint.Endpoint]: return [ ep for epid, ep in self.endpoints.items() if not (isinstance(ep, zdo.ZDO)) ] @property def has_non_zdo_endpoints(self) -> bool: return bool(self.non_zdo_endpoints) @property def all_endpoints_init(self) -> bool: return self.has_non_zdo_endpoints and all( ep.status != zigpy.endpoint.Status.NEW for ep in self.non_zdo_endpoints ) @property def is_initialized(self) -> bool: return self.node_desc is not None and self.all_endpoints_init def schedule_group_membership_scan(self) -> asyncio.Task: """Rescan device group's membership.""" if self._group_scan_task and not self._group_scan_task.done(): self.debug("Cancelling old group rescan") self._group_scan_task.cancel() self._group_scan_task = asyncio.create_task(self.group_membership_scan()) return self._group_scan_task async def group_membership_scan(self) -> None: """Sync up group membership.""" for ep in self.non_zdo_endpoints: await ep.group_membership_scan() @property def initializing(self) -> bool: """Return True if device is being initialized.""" return self._initialize_task is not None and not self._initialize_task.done() def cancel_initialization(self) -> None: """Cancel initialization call.""" if self.initializing: self.debug("Canceling old initialize call") self._initialize_task.cancel() # type:ignore[union-attr] def schedule_initialize(self) -> asyncio.Task | None: # Already-initialized devices don't need to be re-initialized if self.is_initialized: self.debug("Skipping initialization, device is fully initialized") self._application.device_initialized(self) return None self.debug("Scheduling initialization") self.cancel_initialization() self._initialize_task = asyncio.create_task(self.initialize()) return self._initialize_task async def get_node_descriptor(self) -> zdo_t.NodeDescriptor: self.info("Requesting 'Node Descriptor'") status, _, node_desc = await self.zdo.Node_Desc_req( self.nwk, priority=t.PacketPriority.HIGH, ) if status != zdo_t.Status.SUCCESS: raise zigpy.exceptions.InvalidResponse( f"Requesting Node Descriptor failed: {status}" ) self.node_desc = node_desc self.info("Got Node Descriptor: %s", node_desc) return node_desc async def initialize(self) -> None: try: await self._initialize() except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException): self.application.listener_event("device_init_failure", self) except Exception: # noqa: BLE001 LOGGER.warning( "Device %r failed to initialize due to unexpected error", self, exc_info=True, ) self.application.listener_event("device_init_failure", self) @zigpy.util.retryable_request(tries=5, delay=0.5) async def _initialize(self) -> None: """Attempts multiple times to discover all basic information about a device: namely its node descriptor, all endpoints and clusters, and the model and manufacturer attributes from any Basic cluster exposing those attributes. """ # Some devices are improperly initialized and are missing a node descriptor if self.node_desc is None: await self.get_node_descriptor() # Devices should have endpoints other than ZDO if self.has_non_zdo_endpoints: self.info("Already have endpoints: %s", self.endpoints) else: self.info("Discovering endpoints") status, _, endpoints = await self.zdo.Active_EP_req( self.nwk, priority=t.PacketPriority.HIGH ) if status != zdo_t.Status.SUCCESS: raise zigpy.exceptions.InvalidResponse( f"Endpoint request failed: {status}" ) self.info("Discovered endpoints: %s", endpoints) for endpoint_id in endpoints: if endpoint_id != 0: self.add_endpoint(endpoint_id) self.status = Status.ZDO_INIT # Initialize all of the discovered endpoints if self.all_endpoints_init: self.info( "All endpoints are already initialized: %s", self.non_zdo_endpoints ) else: self.info("Initializing endpoints %s", self.non_zdo_endpoints) for ep in self.non_zdo_endpoints: await ep.initialize() # Query model info if self.model is not None and self.manufacturer is not None: self.info("Already have model and manufacturer info") else: for ep in self.non_zdo_endpoints: if self.model is None or self.manufacturer is None: model, manufacturer = await ep.get_model_info() self.info( "Read model %r and manufacturer %r from %s", model, manufacturer, ep, ) if model is not None: self.model = model if manufacturer is not None: self.manufacturer = manufacturer self.status = Status.ENDPOINTS_INIT self.info("Discovered basic device information for %s", self) # Signal to the application that the device is ready self._application.device_initialized(self) def add_endpoint(self, endpoint_id) -> zigpy.endpoint.Endpoint: ep = zigpy.endpoint.Endpoint(self, endpoint_id) self.endpoints[endpoint_id] = ep return ep async def add_to_group(self, grp_id: int, name: str | None = None) -> None: for ep in self.non_zdo_endpoints: await ep.add_to_group(grp_id, name) async def remove_from_group(self, grp_id: int) -> None: for ep in self.non_zdo_endpoints: await ep.remove_from_group(grp_id) async def request( self, profile, cluster, src_ep, dst_ep, sequence, data, expect_reply=True, timeout=APS_REPLY_TIMEOUT, use_ieee=False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, ): extended_timeout = False if self.node_desc is None or self.node_desc.is_end_device: self.debug("Extending timeout for 0x%02x request", sequence) timeout = APS_REPLY_TIMEOUT_EXTENDED extended_timeout = True # Use a lambda so we don't leave the coroutine unawaited in case of an exception send_request = lambda: self._application.request( # noqa: E731 device=self, profile=profile, cluster=cluster, src_ep=src_ep, dst_ep=dst_ep, sequence=sequence, data=data, expect_reply=expect_reply, use_ieee=use_ieee, extended_timeout=extended_timeout, ask_for_ack=ask_for_ack, priority=priority, ) async with self._limit_concurrency(priority=priority): if not expect_reply: await send_request() return None # Only create a pending request if we are expecting a reply with self._pending.new(sequence) as req: await send_request() async with asyncio_timeout(timeout): return await req.result def handle_message( self, profile: int, cluster: int, src_ep: int, dst_ep: int, message: bytes, *, dst_addressing: AddressingMode | None = None, ): """Deprecated compatibility function. Use `packet_received` instead.""" warnings.warn( "`handle_message` is deprecated, use `packet_received`", DeprecationWarning ) if dst_addressing is None: dst_addressing = t.AddrMode.NWK self.packet_received( t.ZigbeePacket( profile_id=profile, cluster_id=cluster, src_ep=src_ep, dst_ep=dst_ep, data=t.SerializableBytes(message), dst=t.AddrModeAddress( addr_mode=dst_addressing, address={ t.AddrMode.NWK: self.nwk, t.AddrMode.IEEE: self.ieee, }[dst_addressing], ), ) ) def deserialize(self, endpoint_id, cluster_id, data): """Deprecated compatibility function.""" warnings.warn( "`deserialize` is deprecated, avoid rewriting packet structures this way", DeprecationWarning, ) return self.endpoints[endpoint_id].deserialize(cluster_id, data) def packet_received(self, packet: t.ZigbeePacket) -> None: # Set radio details that can be read from any type of packet self.last_seen = packet.timestamp if packet.lqi is not None: self.lqi = packet.lqi if packet.rssi is not None: self.rssi = packet.rssi if self._packet_debouncer.filter( # Be conservative with deduplication obj=packet.replace(timestamp=None, tsn=None, lqi=None, rssi=None), expire_in=PACKET_DEBOUNCE_WINDOW, ): self.debug("Filtering duplicate packet") return # Filter out packets that refer to unknown endpoints or clusters if packet.src_ep not in self.endpoints: self.debug( "Ignoring message on unknown endpoint %s (expected one of %s)", packet.src_ep, self.endpoints, ) return endpoint = self.endpoints[packet.src_ep] # Ignore packets that do not match the endpoint's clusters. # TODO: this isn't actually necessary, we can parse most packets by cluster ID. if ( packet.dst_ep != zdo.ZDO_ENDPOINT and packet.cluster_id not in endpoint.in_clusters and packet.cluster_id not in endpoint.out_clusters ): self.debug( "Ignoring message on unknown cluster %s for endpoint %s", packet.cluster_id, endpoint, ) return # Parse the ZCL/ZDO header first. This should never fail. data = packet.data.serialize() if packet.dst_ep == zdo.ZDO_ENDPOINT: hdr, _ = zdo_t.ZDOHeader.deserialize(packet.cluster_id, data) else: hdr, _ = foundation.ZCLHeader.deserialize(data) try: if ( type(self).deserialize is not Device.deserialize or getattr(self.deserialize, "__func__", None) is not Device.deserialize ): # XXX: support for custom deserialization will be removed hdr, args = self.deserialize(packet.src_ep, packet.cluster_id, data) else: # Next, parse the ZCL/ZDO payload # FIXME: ZCL deserialization mutates the header! hdr, args = endpoint.deserialize(packet.cluster_id, data) except Exception as exc: # noqa: BLE001 error = zigpy.exceptions.ParsingError() error.__cause__ = exc self.debug("Failed to parse packet %r", packet, exc_info=error) else: error = None # Resolve the future if this is a response to a request if hdr.tsn in self._pending and ( hdr.direction == foundation.Direction.Server_to_Client if isinstance(hdr, foundation.ZCLHeader) else hdr.is_reply ): future = self._pending[hdr.tsn] try: if error is not None: future.result.set_exception(error) else: future.result.set_result(args) except asyncio.InvalidStateError: self.debug( ( "Invalid state on future for 0x%02x seq " "-- probably duplicate response" ), hdr.tsn, ) return if error is not None: return # Pass the request off to a listener, if one is registered for listener in itertools.chain( self._application._req_listeners[zigpy.listeners.ANY_DEVICE], self._application._req_listeners[self], ): # Resolve only until the first future listener if listener.resolve(hdr, args) and isinstance( listener, zigpy.listeners.FutureListener ): break # Finally, pass it off to the endpoint message handler. This will be removed. endpoint.handle_message( packet.profile_id, packet.cluster_id, hdr, args, dst_addressing=packet.dst.addr_mode if packet.dst is not None else None, ) async def reply( self, profile, cluster, src_ep, dst_ep, sequence, data, timeout=APS_REPLY_TIMEOUT, expect_reply: bool = False, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, ): return await self.request( profile=profile, cluster=cluster, src_ep=src_ep, dst_ep=dst_ep, sequence=sequence, data=data, expect_reply=expect_reply, timeout=timeout, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) async def update_firmware( self, image: OtaImageWithMetadata, progress_callback: callable | None = None, force: bool = False, ) -> foundation.Status: """Update device firmware.""" if self.ota_in_progress: self.debug("OTA already in progress") return None self.ota_in_progress = True try: result = await update_firmware( device=self, image=image, progress_callback=progress_callback, force=force, ) except Exception as exc: # noqa: BLE001 self.debug("OTA failed!", exc_info=exc) raise finally: self.ota_in_progress = False if result != foundation.Status.SUCCESS: return result # Clear the current file version when the update succeeds ota = find_ota_cluster(self) ota.update_attribute(Ota.AttributeDefs.current_file_version.id, None) await asyncio.sleep(AFTER_OTA_ATTR_READ_DELAY) await OTA_RETRY_DECORATOR(ota.read_attributes)( [Ota.AttributeDefs.current_file_version.name] ) return result def radio_details(self, lqi=None, rssi=None) -> None: if lqi is not None: self.lqi = lqi if rssi is not None: self.rssi = rssi def log(self, lvl, msg, *args, **kwargs) -> None: msg = "[0x%04x] " + msg args = (self.nwk, *args) LOGGER.log(lvl, msg, *args, **kwargs) @property def application(self) -> ControllerApplication: return self._application @property def ieee(self) -> t.EUI64: return self._ieee @property def manufacturer(self) -> str | None: return self._manufacturer @manufacturer.setter def manufacturer(self, value) -> None: if isinstance(value, str): self._manufacturer = value @property def manufacturer_id(self) -> int | None: """Return manufacturer id.""" if self.manufacturer_id_override: return self.manufacturer_id_override elif self.node_desc is not None: return self.node_desc.manufacturer_code else: return None @property def model(self) -> str | None: return self._model @model.setter def model(self, value) -> None: if isinstance(value, str): self._model = value @property def skip_configuration(self) -> bool: return self._skip_configuration @skip_configuration.setter def skip_configuration(self, should_skip_configuration) -> None: if isinstance(should_skip_configuration, bool): self._skip_configuration = should_skip_configuration else: self._skip_configuration = False @property def relays(self) -> t.Relays | None: """Relay list.""" return self._relays @relays.setter def relays(self, relays: t.Relays | None) -> None: if relays is None: pass elif not isinstance(relays, t.Relays): relays = t.Relays(relays) self._relays = relays self.listener_event("device_relays_updated", relays) def __getitem__(self, key): return self.endpoints[key] def get_signature(self) -> dict[str, typing.Any]: # return the device signature by providing essential device information # - Model Identifier ( Attribute 0x0005 of Basic Cluster 0x0000 ) # - Manufacturer Name ( Attribute 0x0004 of Basic Cluster 0x0000 ) # - Endpoint list # - Profile Id, Device Id, Cluster Out, Cluster In signature: dict[str, typing.Any] = {} if self._manufacturer is not None: signature[SIG_MANUFACTURER] = self.manufacturer if self._model is not None: signature[SIG_MODEL] = self._model if self.node_desc is not None: signature[SIG_NODE_DESC] = self.node_desc.as_dict() for endpoint_id, endpoint in self.endpoints.items(): if endpoint_id == 0: # ZDO continue signature.setdefault(SIG_ENDPOINTS, {}) in_clusters = list(endpoint.in_clusters) out_clusters = list(endpoint.out_clusters) signature[SIG_ENDPOINTS][endpoint_id] = { SIG_EP_PROFILE: endpoint.profile_id, SIG_EP_TYPE: endpoint.device_type, SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: out_clusters, } return signature def __repr__(self) -> str: return ( f"<" f"{type(self).__name__}" f" model={self.model!r}" f" manuf={self.manufacturer!r}" f" nwk={t.NWK(self.nwk)}" f" ieee={self.ieee}" f" is_initialized={self.is_initialized}" f">" ) async def broadcast( app, profile, cluster, src_ep, dst_ep, grpid, radius, sequence, data, broadcast_address=t.BroadcastAddress.RX_ON_WHEN_IDLE, ): return await app.broadcast( profile, cluster, src_ep, dst_ep, grpid, radius, sequence, data, broadcast_address=broadcast_address, ) zigpy-0.80.1/zigpy/endpoint.py000066400000000000000000000314511501451476000163050ustar00rootroot00000000000000from __future__ import annotations import asyncio import enum import logging from typing import Any from zigpy.const import APS_REPLY_TIMEOUT import zigpy.exceptions import zigpy.profiles import zigpy.types as t from zigpy.typing import AddressingMode, DeviceType import zigpy.util import zigpy.zcl from zigpy.zcl.foundation import ( GENERAL_COMMANDS, CommandSchema, GeneralCommand, Status as ZCLStatus, ZCLHeader, ) from zigpy.zdo.types import Status as ZDOStatus LOGGER = logging.getLogger(__name__) class Status(enum.IntEnum): """The status of an Endpoint""" # No initialization is done NEW = 0 # Endpoint information (device type, clusters, etc) init done ZDO_INIT = 1 # Endpoint Inactive ENDPOINT_INACTIVE = 3 class Endpoint(zigpy.util.LocalLogMixin, zigpy.util.ListenableMixin): """An endpoint on a device on the network""" def __init__(self, device: DeviceType, endpoint_id: int) -> None: self._device: DeviceType = device self._endpoint_id: int = endpoint_id self._listeners: dict = {} self.status: Status = Status.NEW self.profile_id: int | None = None self.device_type: zigpy.profiles.zha.DeviceType | None = None self.in_clusters: dict = {} self.out_clusters: dict = {} self._cluster_attr: dict = {} self._member_of: dict = {} self._manufacturer: str | None = None self._model: str | None = None async def initialize(self) -> None: self.info("Discovering endpoint information") if self.profile_id is not None or self.status == Status.ENDPOINT_INACTIVE: self.info("Endpoint descriptor already queried") else: status, _, sd = await self._device.zdo.Simple_Desc_req( self._device.nwk, self._endpoint_id, priority=t.PacketPriority.HIGH ) if status == ZDOStatus.NOT_ACTIVE: # These endpoints are essentially junk but this lets the device join self.status = Status.ENDPOINT_INACTIVE return elif status != ZDOStatus.SUCCESS: raise zigpy.exceptions.InvalidResponse( "Failed to retrieve service descriptor: %s", status ) self.info("Discovered endpoint information: %s", sd) self.profile_id = sd.profile self.device_type = sd.device_type if self.profile_id == zigpy.profiles.zha.PROFILE_ID: self.device_type = zigpy.profiles.zha.DeviceType(self.device_type) elif self.profile_id == zigpy.profiles.zll.PROFILE_ID: self.device_type = zigpy.profiles.zll.DeviceType(self.device_type) for cluster in sd.input_clusters: self.add_input_cluster(cluster) for cluster in sd.output_clusters: self.add_output_cluster(cluster) self.status = Status.ZDO_INIT @property def clusters(self) -> list[zigpy.zcl.Cluster]: """Return all clusters on this endpoint.""" return [*self.in_clusters.values(), *self.out_clusters.values()] def add_input_cluster( self, cluster_id: int, cluster: zigpy.zcl.Cluster | None = None ) -> zigpy.zcl.Cluster: """Adds an endpoint's input cluster (a server cluster supported by the device) """ if cluster is None: if cluster_id in self.in_clusters: return self.in_clusters[cluster_id] cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=True) self.in_clusters[cluster_id] = cluster if cluster.ep_attribute is not None: self._cluster_attr[cluster.ep_attribute] = cluster if self._device.application._dblistener is not None: listener = zigpy.zcl.ClusterPersistingListener( self._device.application._dblistener, cluster ) cluster.add_listener(listener) return cluster def add_output_cluster( self, cluster_id: int, cluster: zigpy.zcl.Cluster | None = None ) -> zigpy.zcl.Cluster: """Adds an endpoint's output cluster (a client cluster supported by the device) """ if cluster is None: if cluster_id in self.out_clusters: return self.out_clusters[cluster_id] cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=False) self.out_clusters[cluster_id] = cluster if self._device.application._dblistener is not None: listener = zigpy.zcl.ClusterPersistingListener( self._device.application._dblistener, cluster ) cluster.add_listener(listener) return cluster async def add_to_group(self, grp_id: int, name: str | None = None) -> ZCLStatus: try: res = await self.groups.add(grp_id, name) except AttributeError: self.debug("Cannot add 0x%04x group, no groups cluster", grp_id) return ZCLStatus.FAILURE if res[0] not in (ZCLStatus.SUCCESS, ZCLStatus.DUPLICATE_EXISTS): self.debug("Couldn't add to 0x%04x group: %s", grp_id, res[0]) return res[0] group = self.device.application.groups.add_group(grp_id, name) group.add_member(self) return res[0] async def remove_from_group(self, grp_id: int) -> ZCLStatus: try: res = await self.groups.remove(grp_id) except AttributeError: self.debug("Cannot remove 0x%04x group, no groups cluster", grp_id) return ZCLStatus.FAILURE if res[0] not in (ZCLStatus.SUCCESS, ZCLStatus.NOT_FOUND): self.debug("Couldn't remove to 0x%04x group: %s", grp_id, res[0]) return res[0] if grp_id in self.device.application.groups: self.device.application.groups[grp_id].remove_member(self) return res[0] async def group_membership_scan(self) -> None: """Sync up group membership.""" try: res = await self.groups.get_membership(groups=[]) except AttributeError: return except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException): self.debug("Failed to sync-up group membership") return if isinstance(res, GENERAL_COMMANDS[GeneralCommand.Default_Response].schema): self.debug("Device does not support group commands: %s", res) return groups = set(res[1]) self.device.application.groups.update_group_membership(self, groups) async def get_model_info(self) -> tuple[str | None, str | None]: if zigpy.zcl.clusters.general.Basic.cluster_id not in self.in_clusters: return None, None # Some devices can't handle multiple attributes in the same read request for names in (["manufacturer", "model"], ["manufacturer"], ["model"]): try: success, failure = await self.basic.read_attributes( names, allow_cache=True, priority=t.PacketPriority.HIGH ) except asyncio.TimeoutError: # Only swallow the `TimeoutError` on the double attribute read if len(names) == 2: continue raise if "model" in success: self._model = success["model"] if "manufacturer" in success: self._manufacturer = success["manufacturer"] return self._model, self._manufacturer def deserialize( self, cluster_id: t.ClusterId, data: bytes ) -> tuple[ZCLHeader, CommandSchema]: """Deserialize data for ZCL""" if cluster_id not in self.in_clusters and cluster_id not in self.out_clusters: raise KeyError(f"No cluster ID 0x{cluster_id:04x} on {self.unique_id}") cluster = self.in_clusters.get(cluster_id, self.out_clusters.get(cluster_id)) return cluster.deserialize(data) def handle_message( self, profile: int, cluster: int, hdr: ZCLHeader, args: list, *, dst_addressing: AddressingMode | None = None, ) -> None: if cluster in self.in_clusters: handler = self.in_clusters[cluster].handle_message elif cluster in self.out_clusters: handler = self.out_clusters[cluster].handle_message else: self.debug("Message on unknown cluster 0x%04x", cluster) self.listener_event("unknown_cluster_message", hdr.command_id, args) return handler(hdr, args, dst_addressing=dst_addressing) async def request( self, cluster: t.ClusterId, sequence: t.uint8_t, data: bytes, command_id: GeneralCommand | t.uint8_t = 0x00, timeout=APS_REPLY_TIMEOUT, expect_reply: bool = True, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, ): if self.profile_id == zigpy.profiles.zll.PROFILE_ID and not ( cluster == zigpy.zcl.clusters.lightlink.LightLink.cluster_id and command_id < 0x40 ): profile_id = zigpy.profiles.zha.PROFILE_ID else: profile_id = self.profile_id return await self.device.request( profile=profile_id, cluster=cluster, src_ep=self._endpoint_id, dst_ep=self._endpoint_id, sequence=sequence, data=data, timeout=timeout, expect_reply=expect_reply, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) async def reply( self, cluster: t.ClusterId, sequence: t.uint8_t, data: bytes, command_id: GeneralCommand | t.uint8_t = 0x00, timeout=APS_REPLY_TIMEOUT, expect_reply: bool = False, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, ) -> None: if self.profile_id == zigpy.profiles.zll.PROFILE_ID and not ( cluster == zigpy.zcl.clusters.lightlink.LightLink.cluster_id and command_id < 0x40 ): profile_id = zigpy.profiles.zha.PROFILE_ID else: profile_id = self.profile_id return await self.device.reply( profile=profile_id, cluster=cluster, src_ep=self._endpoint_id, dst_ep=self._endpoint_id, sequence=sequence, data=data, timeout=timeout, expect_reply=expect_reply, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) def log(self, lvl: int, msg: str, *args: Any, **kwargs: Any) -> None: msg = "[0x%04x:%s] " + msg args = (self._device.nwk, self._endpoint_id, *args) LOGGER.log(lvl, msg, *args, **kwargs) @property def device(self) -> DeviceType: return self._device @property def endpoint_id(self) -> int: return self._endpoint_id @property def manufacturer(self) -> str: if self._manufacturer is not None: return self._manufacturer return self.device.manufacturer @manufacturer.setter def manufacturer(self, value) -> None: self.warning( "Overriding manufacturer from quirks is not supported and " "will be removed in the next zigpy version" ) self._manufacturer = value @property def manufacturer_id(self) -> int | None: """Return device's manufacturer id code.""" return self.device.manufacturer_id @property def member_of(self) -> dict: return self._member_of @property def model(self) -> str: if self._model is not None: return self._model return self.device.model @model.setter def model(self, value) -> None: self.warning( "Overriding model from quirks is not supported and " "will be removed in the next version" ) self._model = value @property def unique_id(self) -> tuple[t.EUI64, int]: return self.device.ieee, self.endpoint_id def __getattr__(self, name: str) -> zigpy.zcl.Cluster: try: return self._cluster_attr[name] except KeyError as exc: raise AttributeError from exc def __repr__(self) -> str: def cluster_repr(clusters): return ", ".join( [f"{c.ep_attribute}:0x{c.cluster_id:04X}" for c in clusters] ) return ( f"<{type(self).__name__}" f" id={self.endpoint_id}" f" in=[{cluster_repr(self.in_clusters.values())}]" f" out=[{cluster_repr(self.out_clusters.values())}]" f" status={self.status!r}" f">" ) zigpy-0.80.1/zigpy/exceptions.py000066400000000000000000000036401501451476000166450ustar00rootroot00000000000000from __future__ import annotations import typing if typing.TYPE_CHECKING: import zigpy.backups class ZigbeeException(Exception): """Base exception class""" class ParsingError(ZigbeeException): """Failed to parse a frame""" class ControllerException(ZigbeeException): """Application controller failed in some way.""" class APIException(ZigbeeException): """Radio API failed in some way.""" class DeliveryError(ZigbeeException): """Message delivery failed in some way""" def __init__(self, message: str, status: int | None = None): super().__init__(message) self.status = status class SendError(DeliveryError): """Message could not be enqueued.""" class InvalidResponse(ZigbeeException): """A ZDO or ZCL response has an unsuccessful status code""" class RadioException(Exception): """Base exception class for radio exceptions""" class TransientConnectionError(RadioException): """Connection to the radio failed but will likely succeed in the near future""" class NetworkNotFormed(RadioException): """A network cannot be started because the radio has no stored network info""" class FormationFailure(RadioException): """Network settings could not be written to the radio""" class NetworkSettingsInconsistent(ZigbeeException): """Loaded network settings are different from what is in the database""" def __init__( self, message: str, new_state: zigpy.backups.NetworkBackup, old_state: zigpy.backups.NetworkBackup, ) -> None: super().__init__(message) self.new_state = new_state self.old_state = old_state class CorruptDatabase(ZigbeeException): """The SQLite database is corrupt or otherwise inconsistent""" class QuirksException(Exception): """Base exception class""" class MultipleQuirksMatchException(QuirksException): """Thrown when multiple v2 quirks match a device""" zigpy-0.80.1/zigpy/group.py000066400000000000000000000212041501451476000156140ustar00rootroot00000000000000from __future__ import annotations import logging from typing import TYPE_CHECKING, Any from zigpy import types as t from zigpy.endpoint import Endpoint import zigpy.profiles.zha as zha_profile from zigpy.util import ListenableMixin, LocalLogMixin import zigpy.zcl from zigpy.zcl import foundation if TYPE_CHECKING: from zigpy.application import ControllerApplication LOGGER = logging.getLogger(__name__) class Group(ListenableMixin, dict): def __init__( self, group_id: int, name: str | None = None, groups: Groups | None = None, *args: Any, **kwargs: Any, ): super().__init__(*args, **kwargs) self._groups: Groups = groups self._group_id: t.Group = t.Group(group_id) self._name: str = name self._endpoint: GroupEndpoint = GroupEndpoint(self) self._send_sequence = 0 if groups is not None: self.add_listener(groups) def get_sequence(self) -> t.uint8_t: self._send_sequence = (self._send_sequence + 1) % 256 return self._send_sequence def add_member(self, ep: Endpoint, suppress_event: bool = False) -> Group: if not isinstance(ep, Endpoint): raise ValueError(f"{ep} is not {Endpoint.__class__.__name__} class") # noqa: TRY004 if ep.unique_id in self: return self[ep.unique_id] self[ep.unique_id] = ep ep.member_of[self.group_id] = self if not suppress_event: self.listener_event("member_added", self, ep) return self def remove_member(self, ep: Endpoint, suppress_event: bool = False) -> Group: self.pop(ep.unique_id, None) ep.member_of.pop(self.group_id, None) if not suppress_event: self.listener_event("member_removed", self, ep) return self async def request(self, profile, cluster, sequence, data, *args, **kwargs): """Send multicast request.""" await self.application.send_packet( t.ZigbeePacket( src_ep=self.application.get_endpoint_id( cluster, is_server_cluster=False ), dst=t.AddrModeAddress( addr_mode=t.AddrMode.Group, address=self.group_id ), tsn=sequence, profile_id=profile, cluster_id=cluster, data=t.SerializableBytes(data), radius=0, non_member_radius=3, ) ) return foundation.GENERAL_COMMANDS[ foundation.GeneralCommand.Default_Response ].schema( status=foundation.Status.SUCCESS, command_id=data[2], ) def __repr__(self) -> str: return f"<{self.__class__.__name__} group_id={self.group_id} name='{self.name}' members={super().__repr__()}>" @property def application(self) -> ControllerApplication: """Expose application to FakeEndpoint/GroupCluster.""" return self.groups.application @property def groups(self) -> Groups: return self._groups @property def group_id(self) -> t.Group: return self._group_id @property def members(self) -> Group: return self @property def name(self) -> str: if self._name is None: return f"No name group {self.group_id}" return self._name @property def endpoint(self) -> GroupEndpoint: return self._endpoint class Groups(ListenableMixin, dict): def __init__(self, app: ControllerApplication, *args: Any, **kwargs: Any): self._application: ControllerApplication = app self._listeners: dict = {} super().__init__(*args, **kwargs) def add_group( self, group_id: int, name: str | None = None, suppress_event: bool = False ) -> Group: if group_id in self: return self[group_id] LOGGER.debug("Adding group: %s, %s", group_id, name) group = Group(group_id, name, self) self[group_id] = group if not suppress_event: self.listener_event("group_added", group) return group def member_added(self, group: Group, ep: Endpoint) -> None: self.listener_event("group_member_added", group, ep) def member_removed(self, group: Group, ep: Endpoint) -> None: self.listener_event("group_member_removed", group, ep) def pop(self, item, *args: Any) -> Group | None: if isinstance(item, Group): group = super().pop(item.group_id, *args) if isinstance(group, Group): for member in (*group.values(),): group.remove_member(member) self.listener_event("group_removed", group) return group group = super().pop(item, *args) if isinstance(group, Group): for member in (*group.values(),): group.remove_member(member) self.listener_event("group_removed", group) return group remove_group = pop def update_group_membership(self, ep: Endpoint, groups: set[int]) -> None: """Sync up device group membership.""" old_groups = { group.group_id for group in self.values() if ep.unique_id in group.members } for grp_id in old_groups - groups: self[grp_id].remove_member(ep) for grp_id in groups - old_groups: group = self.add_group(grp_id) group.add_member(ep) @property def application(self) -> ControllerApplication: """Return application controller.""" return self._application class GroupCluster(zigpy.zcl.Cluster): """Virtual cluster for group requests.""" @classmethod def from_id( cls, group_endpoint: GroupEndpoint, cluster_id: int, is_server=True ) -> zigpy.zcl.Cluster: """Instantiate from ZCL cluster by cluster id.""" if is_server is not True: raise ValueError("Only server clusters are supported for group requests") if cluster_id in cls._registry: return cls._registry[cluster_id](group_endpoint, is_server=True) group_endpoint.debug( "0x%04x cluster id is not supported for group requests", cluster_id ) raise KeyError(f"Unsupported 0x{cluster_id:04x} cluster id for groups") @classmethod def from_attr( cls, group_endpoint: GroupEndpoint, ep_name: str ) -> zigpy.zcl.Cluster: """Instantiate by Cluster name.""" for cluster in cls._registry.values(): if cluster.ep_attribute == ep_name: return cluster(group_endpoint, is_server=True) raise AttributeError(f"Unsupported {ep_name} group cluster") class GroupEndpoint(LocalLogMixin): """Group request handlers. wrapper for virtual clusters. """ def __init__(self, group: Group): """Instantiate GroupRequest.""" self._group: Group = group self._clusters: dict = {} self._cluster_by_attr: dict = {} @property def endpoint_id(self) -> None: return None @property def clusters(self) -> dict: """Group clusters. most of the times, group requests are addressed from client -> server clusters. """ return self._clusters @property def device(self) -> Group: """Group is our fake zigpy device""" return self._group def request(self, cluster, sequence, data, *args, **kwargs): """Send multicast request.""" return self.device.request(zha_profile.PROFILE_ID, cluster, sequence, data) def reply(self, cluster, sequence, data, *args, **kwargs): """Send multicast reply. do we really need this one :shrug: """ return self.request(cluster, sequence, data, *args, **kwargs) def log(self, lvl: int, msg: str, *args: Any, **kwargs: Any) -> None: msg = "[0x%04x] " + msg args = (self._group.group_id, *args) LOGGER.log(lvl, msg, *args, **kwargs) def __getitem__(self, item: int): """Return or instantiate a group cluster.""" try: return self.clusters[item] except KeyError: self.debug("trying to create new group %s cluster id", item) cluster = GroupCluster.from_id(self, item) self.clusters[item] = cluster return cluster def __getattr__(self, name: str): """Return or instantiate a group cluster by cluster name.""" try: return self._cluster_by_attr[name] except KeyError: self.debug("trying to create a new group '%s' cluster", name) cluster = GroupCluster.from_attr(self, name) self._cluster_by_attr[name] = cluster return cluster zigpy-0.80.1/zigpy/listeners.py000066400000000000000000000075611501451476000165020ustar00rootroot00000000000000from __future__ import annotations import asyncio import dataclasses import inspect import logging import typing from zigpy.util import Singleton from zigpy.zcl import foundation import zigpy.zdo.types as zdo_t LOGGER = logging.getLogger(__name__) ANY_DEVICE = Singleton("ANY_DEVICE") @dataclasses.dataclass(frozen=True) class BaseRequestListener: matchers: tuple[MatcherType] def resolve( self, hdr: foundation.ZCLHeader | zdo_t.ZDOHeader, command: foundation.CommandSchema, ) -> bool: """Attempts to resolve the listener with a given response. Can be called with any command as an argument, including ones we don't match. """ for matcher in self.matchers: match = None is_matcher_cmd = isinstance(matcher, foundation.CommandSchema) if is_matcher_cmd and isinstance(command, foundation.CommandSchema): match = command.matches(matcher) elif is_matcher_cmd and isinstance(hdr, zdo_t.ZDOHeader): # FIXME: ZDO does not use command schemas and cannot be matched pass elif callable(matcher): match = matcher(hdr, command) else: LOGGER.warning( "Matcher %r and command %r %r are incompatible", matcher, hdr, command, ) if match: return self._resolve(hdr, command) return False def _resolve( self, hdr: foundation.ZCLHeader | zdo_t.ZDOHeader, command: foundation.CommandSchema, ) -> bool: """Implemented by subclasses to handle matched commands. Return value indicates whether or not the listener has actually resolved, which can sometimes be unavoidable. """ raise NotImplementedError # pragma: no cover def cancel(self): """Implement by subclasses to cancel the listener. Return value indicates whether or not the listener is cancelable. """ raise NotImplementedError # pragma: no cover @dataclasses.dataclass(frozen=True) class FutureListener(BaseRequestListener): future: asyncio.Future def _resolve( self, hdr: foundation.ZCLHeader | zdo_t.ZDOHeader, command: foundation.CommandSchema, ) -> bool: if self.future.done(): return False self.future.set_result((hdr, command)) return True def cancel(self): self.future.cancel() return True @dataclasses.dataclass(frozen=True) class CallbackListener(BaseRequestListener): callback: typing.Callable[ [foundation.ZCLHeader | zdo_t.ZDOHeader, foundation.CommandSchema], typing.Any ] _tasks: set[asyncio.Task] = dataclasses.field(default_factory=set) def _resolve( self, hdr: foundation.ZCLHeader | zdo_t.ZDOHeader, command: foundation.CommandSchema, ) -> bool: try: potential_awaitable = self.callback(hdr, command) if inspect.isawaitable(potential_awaitable): task: asyncio.Task = asyncio.get_running_loop().create_task( potential_awaitable, name="CallbackListener" ) self._tasks.add(task) task.add_done_callback(self._tasks.remove) except Exception: # noqa: BLE001 LOGGER.warning( "Caught an exception while executing callback", exc_info=True ) # Callbacks are always resolved return True def cancel(self): # You can't cancel a callback return False MatcherFuncType = typing.Callable[ [ typing.Union[foundation.ZCLHeader, zdo_t.ZDOHeader], foundation.CommandSchema, ], bool, ] MatcherType = typing.Union[MatcherFuncType, foundation.CommandSchema] zigpy-0.80.1/zigpy/ota/000077500000000000000000000000001501451476000146725ustar00rootroot00000000000000zigpy-0.80.1/zigpy/ota/OTA_URLs.md000066400000000000000000000170131501451476000165460ustar00rootroot00000000000000# Zigbee OTA source provider sources for these and others Collection of external Zigbee OTA firmware images from official and unofficial OTA provider sources. ### Inovelli OTA Firmware provider Manufacturer ID = 4655 Inovelli Zigbee OTA firmware images for zigpy are made publicly available by Inovelli (first-party) at the following URLs: https://files.inovelli.com/firmware/firmware-zha.json https://files.inovelli.com/firmware ### Sonoff OTA Firmware provider Manufacturer ID = 4742 Sonoff Zigbee OTA firmware images are made publicly available by Sonoff (first-party) at the following URLs: https://zigbee-ota.sonoff.tech/releases/upgrade.json ### Koenkk zigbee-OTA repository Koenkk zigbee-OTA repository host third-party OTA firmware images and external URLs for many third-party Zigbee OTA firmware images. https://github.com/Koenkk/zigbee-OTA/tree/master/images https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json ### Dresden Elektronik Manufacturer ID = 4405 Dresden Elektronik Zigbee OTA firmware images are made publicly available by Dresden Elektronik (first-party) at the following URLs: https://deconz.dresden-elektronik.de/otau/ Dresden Elektronik also provide third-party OTA firmware images and external URLs for many third-party Zigbee OTA firmware images here: https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions Dresden Elektronik themselvers implement updates of third-party Zigbee firmware images via their deCONZ STD OTAU plugin: https://github.com/dresden-elektronik/deconz-ota-plugin ### EUROTRONICS EUROTRONICS Zigbee OTA firmware images are made publicly available by EUROTRONIC Technology (first-party) at the following URL: https://github.com/EUROTRONIC-Technology/Spirit-ZigBee/releases/download/ ### IKEA Trådfri Manufacturer ID = 4476 IKEA Trådfri Zigbee OTA firmware images are made publicly available by IKEA (first-party) at the following URLs: * https://fw.ota.homesmart.ikea.com/DIRIGERA/version_info.json * http://fw.ota.homesmart.ikea.net/feed/version_info.json Release changelogs https://ww8.ikea.com/ikeahomesmart/releasenotes/releasenotes.html ### LEDVANCE/Sylvania and OSRAM Lightify Manufacturer ID = 4364 LEDVANCE/Sylvania and OSRAM Lightify Zigbee OTA firmware images are made publicly available by LEDVANCE (first-party) at the following URL: https://update.ledvance.com/firmware-overview https://api.update.ledvance.com/v1/zigbee/firmwares/download https://consumer.sylvania.com/our-products/smart/sylvania-smart-zigbee-products-menu/index.jsp ### Legrand/Netatmo Manufacturer ID = 4129 Legrand/Netatmo Zigbee OTA firmware images are made publicly available by Legrand (first-party) at the following URL: https://developer.legrand.com/documentation/operating-manual/ https://developer.legrand.com/documentation/firmwares-download/ ### LiXee LiXee Zigbee OTA firmware images are made publicly available by Fairecasoimeme / ZiGate (first-party) at the following URL: https://github.com/fairecasoimeme/Zlinky_TIC/releases ### Sengled Manufacturer ID = 4448 Sengled Zigbee OTA firmware images are made publicly available by Sengled (first-party) at the following URLs but does now seem to allow listing: http://us-fm.cloud.sengled.com:8000/sengled/zigbee/firmware/ Note that Sengled do not seem to provide their firmware for use with other Zigbee gateways than the Sengled Smart Hub. The communication between their hub/gateway/bridge appliance and the server hosting the firmware files is encrypted, so we cannot directly get listing of all the files available. To find the URL for firmware files, you need to sniff the traffic from the Hue bridge to the Internet, as it downloads the files, (since the bridge will only download firmware files for connected devices with outdated firmware sniffing traffic is not repeatable once the device has been updated). The official URLs for Philips Hue (Signify) Zigbee OTA firmware images are therefore documented by community and third-parties such as Koenkk and Dresden Elektronik: https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions#sengled ### Philips Hue (Signify) Manufacturer ID = 4107 Philips Hue OTA firmware images are available for different Hue devices for several official sources that do not all use the same APIs: https://firmware.meethue.com/v1/checkUpdate https://firmware.meethue.com/storage/ http://fds.dc1.philips.com/firmware/ Philips Hue (Signify) Zigbee OTA firmware images direct URLs are available by Koenkk zigbee-OTA repository (third-party) at following URL: https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json Note that Philips/Signify do not provide their firmware for use with other Zigbee gateways than the Philips Hue bridge. The communication between their hub/gateway/bridge appliance and the server hosting the firmware files is encrypted, so we cannot directly get listing of all the files available. To find the URL for firmware files, you need to sniff the traffic from the Hue bridge to the Internet, as it downloads the files, (since the bridge will only download firmware files for connected devices with outdated firmware sniffing traffic is not repeatable once the device has been updated). The official URLs for Philips Hue (Signify) Zigbee OTA firmware images are therefore documented by community and third-parties such as Koenkk and Dresden Elektronik: https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/OTA-Image-Types---Firmware-versions#philips-hue https://github.com/dresden-elektronik/deconz-ota-plugin/blob/master/README.md#hue-firmware ### Lutron Manufacturer ID = 4420 Lutron Zigbee OTA firmware images for Lutron Aurora Smart Dimmer Z3-1BRL-WH-L0 is made publicly available by Philips (first-party as ODM) at the following URL: http://fds.dc1.philips.com/firmware/ZGB_1144_0000/3040/Superman_v3_04_Release_3040.ota ### Ubisys Manufacturer ID = 4338 Ubisys Zigbee OTA firmware images are made publicly available by Ubisys (first-party) at the following URLs: https://www.ubisys.de/en/support/firmware/ https://www.ubisys.de/wp-content/uploads/ ### Third Reality (3reality) Manufacturer IDs = 4659, 4877 ThirdReality (3reality) Zigbee OTA firmware images are made publicly available by Third Reality, Inc. (first-party) at the following URL: https://tr-zha.s3.amazonaws.com/firmware.json ### Danfoss Manufacturer ID = 4678 Danfoss Zigbee OTA firmware images for Danfoss Ally devices are made publicly available by Danfoss (first-party) at the following URL: https://files.danfoss.com/download/Heating/Ally/Danfoss%20Ally More information about updateting Danfoss Ally smart heating products available at: https://www.danfoss.com/en/products/dhs/smart-heating/smart-heating/danfoss-ally/danfoss-ally-support/#tab-approvals ### Busch-Jaeger Manufacturer ID = 4398 The ZLL switches from Busch-Jaeger does have upgradable firmware but unfortunately they do not publish the OTOU image files directly via an public OTA provider server. However the firmware can be download and extracted from an Windows Upgrade Tool provided by Busch-Jaeger with the following steps: - Download the Upgrade Tool from https://www.busch-jaeger.de/bje/software/Zigbee_Software/BJE_ZLL_Update_Tool_Setup_V1_2_0_Windows_Version.exe - Extract the contents of the *.exe file with 7zip (7z x BJE_ZLL_Update_Tool_Setup_V1_2_0_Windows_Version.exe). - Navigate to the device/ folder and get the firmware images. zigpy-0.80.1/zigpy/ota/__init__.py000066400000000000000000000466161501451476000170200ustar00rootroot00000000000000"""OTA support for Zigbee devices.""" from __future__ import annotations import asyncio from collections import defaultdict import contextlib import dataclasses import logging import sys import typing from zigpy.config import ( CONF_OTA_ADVANCED_DIR, CONF_OTA_ALLOW_ADVANCED_DIR, CONF_OTA_DISABLE_DEFAULT_PROVIDERS, CONF_OTA_ENABLED, CONF_OTA_EXTRA_PROVIDERS, CONF_OTA_IKEA, CONF_OTA_INOVELLI, CONF_OTA_LEDVANCE, CONF_OTA_PROVIDER_MANUF_IDS, CONF_OTA_PROVIDER_URL, CONF_OTA_PROVIDERS, CONF_OTA_REMOTE_PROVIDERS, CONF_OTA_SALUS, CONF_OTA_SONOFF, CONF_OTA_THIRDREALITY, CONF_OTA_Z2M_LOCAL_INDEX, CONF_OTA_Z2M_REMOTE_INDEX, ) from zigpy.ota.image import BaseOTAImage import zigpy.ota.providers import zigpy.types as t import zigpy.util from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Ota if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout # pragma: no cover else: from asyncio import timeout as asyncio_timeout # pragma: no cover if typing.TYPE_CHECKING: import zigpy.application query_next_image = Ota.ServerCommandDefs.query_next_image.schema _LOGGER = logging.getLogger(__name__) OTA_FETCH_TIMEOUT = 20 MAX_DEVICES_CHECKING_IN_PER_BROADCAST = 15 @dataclasses.dataclass(frozen=True) class OtaImagesResult(t.BaseDataclassMixin): upgrades: tuple[zigpy.ota.providers.BaseOtaImageMetadata] downgrades: tuple[zigpy.ota.providers.BaseOtaImageMetadata] @dataclasses.dataclass(frozen=True) class OtaImageWithMetadata(t.BaseDataclassMixin): metadata: zigpy.ota.providers.BaseOtaImageMetadata firmware: BaseOTAImage | None @property def version(self) -> int: return self.metadata.file_version @property def _min_hardware_version(self) -> int | None: if self.metadata.min_hardware_version is not None: return self.metadata.min_hardware_version elif ( self.firmware is not None and self.firmware.header.minimum_hardware_version is not None ): return self.firmware.header.minimum_hardware_version else: return None @property def _max_hardware_version(self) -> int | None: if self.metadata.max_hardware_version is not None: return self.metadata.max_hardware_version elif ( self.firmware is not None and self.firmware.header.maximum_hardware_version is not None ): return self.firmware.header.maximum_hardware_version else: return None @property def _manufacturer_id(self) -> int | None: if self.metadata.manufacturer_id is not None: return self.metadata.manufacturer_id elif self.firmware is not None: return self.firmware.header.manufacturer_id else: return None @property def _image_type(self) -> int | None: if self.metadata.image_type is not None: return self.metadata.image_type elif self.firmware is not None: return self.firmware.header.image_type else: return None @property def specificity(self) -> int: """Return a numerical representation of the metadata specificity. Higher specificity is preferred to lower when picking a final OTA image. """ total = 0 if self.metadata.manufacturer_names: total += 1000 if self.metadata.model_names: total += 1000 if self._image_type is not None: total += 100 if self._manufacturer_id is not None: total += 100 if self.metadata.min_current_file_version is not None: total += 10 if self.metadata.max_current_file_version is not None: total += 10 if self._min_hardware_version is not None: total += 1 if self._max_hardware_version is not None: total += 1 # Boost the specificity if self.metadata.specificity is not None: total += self.metadata.specificity return total def check_compatibility( self, device: zigpy.device.Device, query_cmd: query_next_image, ) -> bool: """Check if an OTA image and its metadata is compatible with a device.""" if ( self._manufacturer_id is not None and self._manufacturer_id != query_cmd.manufacturer_code ): return False if self._image_type is not None and self._image_type != query_cmd.image_type: return False if self.metadata.model_names and device.model not in self.metadata.model_names: return False if ( self.metadata.manufacturer_names and device.manufacturer not in self.metadata.manufacturer_names ): return False if self._min_hardware_version is not None and ( query_cmd.hardware_version is None or query_cmd.hardware_version < self._min_hardware_version ): return False if self._max_hardware_version is not None and ( query_cmd.hardware_version is None or query_cmd.hardware_version > self._max_hardware_version ): return False return True def check_version(self, current_file_version: int) -> bool: """Check if the image is a newer version than the device's current version.""" if self.version <= current_file_version: return False if ( self.metadata.min_current_file_version is not None and current_file_version < self.metadata.min_current_file_version ): return False if ( self.metadata.max_current_file_version is not None and current_file_version > self.metadata.max_current_file_version ): return False return True async def fetch(self) -> OtaImageWithMetadata: firmware = await self.metadata.fetch() return self.replace( metadata=self.metadata, firmware=firmware, ) class OTA: """OTA Manager.""" def __init__( self, config: dict[str, typing.Any], application: zigpy.application.ControllerApplication, ) -> None: self._config = config self._application = application self._providers: list[zigpy.ota.providers.BaseOtaProvider] = [] self._image_cache: dict[ zigpy.ota.providers.BaseOtaImageMetadata, OtaImageWithMetadata ] = {} self._broadcast_loop_task = None if config[CONF_OTA_ENABLED]: self._register_providers(self._config) async def broadcast_loop(self, initial_delay: float, interval: float) -> None: """Periodically broadcast an image notification to get devices to check in.""" await asyncio.sleep(initial_delay) while True: _LOGGER.debug("Broadcasting OTA notification") try: await self.broadcast_notify() except Exception: # noqa: BLE001 _LOGGER.debug("OTA broadcast failed", exc_info=True) await asyncio.sleep(interval) def start_periodic_broadcasts(self, initial_delay: float, interval: float) -> None: """Start the periodic OTA broadcasts.""" self._broadcast_loop_task = asyncio.create_task( self.broadcast_loop( initial_delay=initial_delay, interval=interval, ) ) def stop_periodic_broadcasts(self) -> None: """Stop the periodic OTA broadcasts.""" if self._broadcast_loop_task is not None: self._broadcast_loop_task.cancel() self._broadcast_loop_task = None def _register_providers(self, config: dict[str, typing.Any]) -> None: # Config gets a little complicated when you mix deprecated config and the new # providers config. We treat every option as an "intent" and merge configs in # the end. with_providers: list[zigpy.ota.providers.BaseOtaProvider] = [ *config[CONF_OTA_PROVIDERS], *config[CONF_OTA_EXTRA_PROVIDERS], ] without_providers: set[type[zigpy.ota.providers.BaseOtaProvider]] = set( config[CONF_OTA_DISABLE_DEFAULT_PROVIDERS] ) - {type(p) for p in config[CONF_OTA_EXTRA_PROVIDERS]} def register_deprecated_provider( enabled: bool | str | None, provider: type[zigpy.ota.providers.BaseOtaProvider], config: dict[str, typing.Any] | None = None, ) -> None: if isinstance(enabled, str) and not config: config = {"url": enabled} enabled = True if not config: config = {} if enabled is True: with_providers.append(provider(**config)) with contextlib.suppress(KeyError): without_providers.remove(provider) elif enabled is False: without_providers.add(provider) else: pass register_deprecated_provider( enabled=config.get(CONF_OTA_IKEA), provider=zigpy.ota.providers.Tradfri, ) register_deprecated_provider( enabled=config.get(CONF_OTA_INOVELLI), provider=zigpy.ota.providers.Inovelli, ) register_deprecated_provider( enabled=config.get(CONF_OTA_LEDVANCE), provider=zigpy.ota.providers.Ledvance, ) register_deprecated_provider( enabled=config.get(CONF_OTA_SALUS), provider=zigpy.ota.providers.Salus, ) register_deprecated_provider( enabled=config.get(CONF_OTA_SONOFF), provider=zigpy.ota.providers.Sonoff, ) register_deprecated_provider( enabled=config.get(CONF_OTA_THIRDREALITY), provider=zigpy.ota.providers.ThirdReality, ) register_deprecated_provider( enabled=config.get(CONF_OTA_Z2M_REMOTE_INDEX), provider=zigpy.ota.providers.RemoteZ2MProvider, ) register_deprecated_provider( enabled=config.get(CONF_OTA_ALLOW_ADVANCED_DIR), provider=zigpy.ota.providers.AdvancedFileProvider, config={"path": config.get(CONF_OTA_ADVANCED_DIR)}, ) register_deprecated_provider( enabled=None if config.get(CONF_OTA_Z2M_LOCAL_INDEX) is None else True, provider=zigpy.ota.providers.LocalZ2MProvider, config={"index_file": config.get(CONF_OTA_Z2M_LOCAL_INDEX)}, ) for provider_config in config.get(CONF_OTA_REMOTE_PROVIDERS, []): register_deprecated_provider( enabled=True, provider=zigpy.ota.providers.RemoteZigpyProvider, config={ "url": provider_config[CONF_OTA_PROVIDER_URL], "manufacturer_ids": provider_config[CONF_OTA_PROVIDER_MANUF_IDS], }, ) replaced_providers: list[zigpy.ota.providers.BaseOtaProvider] = [] for provider in with_providers: if type(provider) in without_providers: continue if provider.override_previous: replaced_providers = [ p for p in replaced_providers if type(p) is not type(provider) ] replaced_providers.append(provider) for provider in replaced_providers: self.register_provider(provider) def register_provider(self, provider: zigpy.ota.providers.BaseOtaProvider) -> None: """Register a new OTA provider.""" _LOGGER.debug("Registering new OTA provider: %s", provider) self._providers.append(provider) @zigpy.util.combine_concurrent_calls async def _load_provider_index( self, provider: zigpy.ota.providers.BaseOtaProvider ) -> list[zigpy.ota.providers.BaseOtaImageMetadata]: """Load the index of a provider.""" async with asyncio_timeout(OTA_FETCH_TIMEOUT): return await provider.load_index() @zigpy.util.combine_concurrent_calls async def _fetch_image( self, image: OtaImageWithMetadata ) -> list[OtaImageWithMetadata]: """Load the index of a provider.""" async with asyncio_timeout(OTA_FETCH_TIMEOUT): return await image.fetch() async def get_ota_images( self, device: zigpy.device.Device, query_cmd: query_next_image, ) -> OtaImagesResult: """Get OTA images compatible with the device.""" # Only consider providers that are compatible with the device compatible_providers = [ p for p in self._providers if p.compatible_with_device(device) ] # Load the index of every provider for provider in compatible_providers: try: index = await self._load_provider_index(provider) except Exception as exc: # noqa: BLE001 _LOGGER.debug("Failed to load provider %s", provider, exc_info=exc) continue if index is None: _LOGGER.debug( "Provider %s was recently contacted, using cached response", provider, ) continue _LOGGER.debug("Loaded %d images from provider: %s", len(index), provider) # Cache its images. If the concurrent call's result was shared, the first # caller will cache these images for meta in index: if meta not in self._image_cache: self._image_cache[meta] = OtaImageWithMetadata( metadata=meta, firmware=None ) # Find all superficially compatible images. Note that if an image's contents # are unknown and its metadata does not describe hardware compatibility, we will # still download in the next step to double check, in case the file itself does. candidates = sorted( [ img for img in self._image_cache.values() if img.check_compatibility(device, query_cmd) ], key=lambda img: img.version, ) upgrades = { img.metadata: img for img in candidates if img.check_version(query_cmd.current_file_version) } downgrades = { img.metadata: img for img in candidates if img.metadata not in upgrades } # Only download upgrade images, downgrades are used just to indicate the latest # version undownloaded_images = [img for img in upgrades.values() if img.firmware is None] # Fetch all the candidates that are missing from the cache results = await asyncio.gather( *(self._fetch_image(img) for img in undownloaded_images), return_exceptions=True, ) for img, result in zip(undownloaded_images, results): if isinstance(result, BaseException): _LOGGER.debug( "Failed to download image, ignoring: %s", img, exc_info=result ) upgrades.pop(img.metadata) continue # `img` is the metadata without downloaded firmware. `result` is the same # image with downloaded firmware. img = result # Cache the image if it isn't already cached if self._image_cache[img.metadata].firmware is None: _LOGGER.debug("Caching image %s", img) self._image_cache[img.metadata] = img if not img.check_compatibility(device, query_cmd): # Ignore images that become incompatible once downloaded del upgrades[img.metadata] else: upgrades[img.metadata] = img # As a final pass, identify images with identical versions and specificity but # differing contents upgrade_collisions: defaultdict[defaultdict[list]] = defaultdict( lambda: defaultdict(list) ) for img in upgrades.values(): assert img.firmware is not None upgrade_collisions[img.version, img.specificity][ img.firmware.serialize() ].append(img) for (version, specificity), buckets in upgrade_collisions.items(): if len(buckets) < 2: continue bad_images = [] for bucket in buckets.values(): bad_images.extend(bucket) _LOGGER.warning( "Multiple unique OTA images for version %08X with specificity %d exist." " It is not possible to tell which image is correct so all %d of the" " colliding images will be ignored.", version, specificity, len(bad_images), ) _LOGGER.debug("Colliding images: %s", bad_images) for img in bad_images: upgrades.pop(img.metadata) return OtaImagesResult( upgrades=tuple( sorted( upgrades.values(), key=lambda img: (img.version, img.specificity), reverse=True, ) ), downgrades=tuple( sorted( downgrades.values(), key=lambda img: (img.version, img.specificity), reverse=True, ) ), ) async def broadcast_notify( self, broadcast_address: t.BroadcastAddress = t.BroadcastAddress.ALL_DEVICES, jitter: int | None = None, ) -> None: tsn = self._application.get_sequence() command = Ota.ClientCommandDefs.image_notify # To avoid flooding huge networks, set the jitter such that we will probably # have a fixed number of devices checking in at once. All devices should # eventually check in, just not every time. if jitter is None: num_devices = len(self._application.devices) jitter = 100 * min( max(0, MAX_DEVICES_CHECKING_IN_PER_BROADCAST / max(1, num_devices)), 1 ) hdr, request = Ota._create_request( self=None, general=False, command_id=command.id, schema=command.schema, tsn=tsn, disable_default_response=True, direction=foundation.Direction.Server_to_Client, args=(), kwargs={ "payload_type": Ota.ImageNotifyCommand.PayloadType.QueryJitter, "query_jitter": jitter, }, ) # Broadcast await self._application.send_packet( t.ZigbeePacket( src=t.AddrModeAddress( addr_mode=t.AddrMode.NWK, address=self._application.state.node_info.nwk, ), src_ep=1, dst=t.AddrModeAddress( addr_mode=t.AddrMode.Broadcast, address=broadcast_address, ), dst_ep=0xFF, tsn=tsn, profile_id=zigpy.profiles.zha.PROFILE_ID, cluster_id=Ota.cluster_id, data=t.SerializableBytes(hdr.serialize() + request.serialize()), tx_options=t.TransmitOptions.NONE, radius=30, ) ) zigpy-0.80.1/zigpy/ota/image.py000066400000000000000000000227601501451476000163350ustar00rootroot00000000000000"""OTA Firmware handling.""" from __future__ import annotations import hashlib import logging import attr from typing_extensions import Self import zigpy.types as t LOGGER = logging.getLogger(__name__) class HWVersion(t.uint16_t): @property def version(self): return self >> 8 @property def revision(self): return self & 0x00FF def __repr__(self): return f"<{self.__class__.__name__} version={self.version} revision={self.revision}>" class HeaderString(bytes): _size = 32 def __new__(cls, value: str | bytes): if isinstance(value, str): value = value.encode("utf-8").ljust(cls._size, b"\x00") if len(value) != cls._size: raise ValueError(f"HeaderString must be exactly {cls._size} bytes long") return super().__new__(cls, value) @classmethod def deserialize(cls, data: bytes) -> tuple[HeaderString, bytes]: if len(data) < cls._size: raise ValueError(f"Data is too short. Should be at least {cls._size}") raw = data[: cls._size] return cls(raw), data[cls._size :] def serialize(self) -> bytes: return self def __str__(self) -> str: return repr(self) def __repr__(self) -> str: try: text = repr(self.rstrip(b"\x00").decode("utf-8")) except UnicodeDecodeError: text = f"{len(self)}:{self.hex()}" return f"<{text}>" class FieldControl(t.bitmap16): SECURITY_CREDENTIAL_VERSION_PRESENT = 0b001 DEVICE_SPECIFIC_FILE_PRESENT = 0b010 HARDWARE_VERSIONS_PRESENT = 0b100 class OTAImageHeader(t.Struct): MAGIC_VALUE = 0x0BEEF11E OTA_HEADER = MAGIC_VALUE.to_bytes(4, "little") upgrade_file_id: t.uint32_t header_version: t.uint16_t header_length: t.uint16_t field_control: FieldControl manufacturer_id: t.uint16_t image_type: t.uint16_t file_version: t.uint32_t stack_version: t.uint16_t header_string: HeaderString image_size: t.uint32_t security_credential_version: t.uint8_t = t.StructField( requires=lambda s: s.field_control is not None and FieldControl.SECURITY_CREDENTIAL_VERSION_PRESENT in s.field_control ) upgrade_file_destination: t.EUI64 = t.StructField( requires=lambda s: s.field_control is not None and FieldControl.DEVICE_SPECIFIC_FILE_PRESENT in s.field_control ) minimum_hardware_version: HWVersion = t.StructField( requires=lambda s: s.field_control is not None and FieldControl.HARDWARE_VERSIONS_PRESENT in s.field_control ) maximum_hardware_version: HWVersion = t.StructField( requires=lambda s: s.field_control is not None and FieldControl.HARDWARE_VERSIONS_PRESENT in s.field_control ) @property def security_credential_version_present(self) -> bool: if self.field_control is None: return None return bool( self.field_control & FieldControl.SECURITY_CREDENTIAL_VERSION_PRESENT ) @property def device_specific_file(self) -> bool: if self.field_control is None: return None return bool(self.field_control & FieldControl.DEVICE_SPECIFIC_FILE_PRESENT) @property def hardware_versions_present(self) -> bool: if self.field_control is None: return None return bool(self.field_control & FieldControl.HARDWARE_VERSIONS_PRESENT) @classmethod def deserialize(cls, data: bytes) -> tuple[OTAImageHeader, bytes]: hdr, data = super().deserialize(data) if hdr.upgrade_file_id != cls.MAGIC_VALUE: raise ValueError( f"Wrong magic number for OTA Image: {hdr.upgrade_file_id!r}" ) return hdr, data class ElementTagId(t.enum16): UPGRADE_IMAGE = 0x0000 ECDSA_SIGNATURE_CRYPTO_SUITE_1 = 0x0001 ECDSA_SIGNING_CERTIFICATE_CRYPTO_SUITE_1 = 0x0002 IMAGE_INTEGRITY_CODE = 0x0003 PICTURE_DATA = 0x0004 ECDSA_SIGNATURE_CRYPTO_SUITE_2 = 0x0005 ECDSA_SIGNING_CERTIFICATE_CRYPTO_SUITE_2 = 0x0006 class LVBytes32(t.LVBytes): _prefix_length = 4 class SubElement(t.Struct): tag_id: ElementTagId data: LVBytes32 def __repr__(self) -> str: if len(self.data) > 32: data = self.data[:25].hex() + "..." + self.data[-7:].hex() else: data = self.data.hex() return ( f"<{self.__class__.__name__}(tag_id={self.tag_id!r}," f" data=[{len(self.data)}:{data}])>" ) class BaseOTAImage: """Base OTA image container type. Not all images are valid Zigbee OTA images but are nonetheless accepted by devices. Only requirement is that the image contains a valid OTAImageHeader property and can be serialized/deserialized. """ header: OTAImageHeader @classmethod def deserialize(cls, data) -> tuple[BaseOTAImage, bytes]: raise NotImplementedError # pragma: no cover def serialize(self): raise NotImplementedError # pragma: no cover class OTAImage(t.Struct, BaseOTAImage): """Zigbee OTA image according to 11.4 of the ZCL specification.""" header: OTAImageHeader subelements: t.List[SubElement] @classmethod def deserialize(cls, data: bytes) -> tuple[OTAImage, bytes]: hdr, data = OTAImageHeader.deserialize(data) elements_len = hdr.image_size - hdr.header_length if elements_len > len(data): raise ValueError( f"Data is too short for {cls}: expected at least {hdr.image_size} -" f" {hdr.header_length} = {elements_len} bytes, got {len(data)}" ) image = cls(header=hdr, subelements=[]) element_data, data = data[:elements_len], data[elements_len:] while element_data: element, element_data = SubElement.deserialize(element_data) image.subelements.append(element) return image, data def serialize(self) -> bytes: res = super().serialize() if self.header.image_size != len(res): raise ValueError( f"Image size in header ({self.header.image_size} bytes)" f" does not match actual image size ({len(res)} bytes)" ) return res @attr.s class HueSBLOTAImage(BaseOTAImage): """Unique OTA image format for certain Hue devices. Starts with a valid header but does not contain any valid subelements beyond that point. """ SUBELEMENTS_MAGIC = b"\x2a\x00\x01" header = attr.ib(default=None) data = attr.ib(default=None) def serialize(self) -> bytes: return self.header.serialize() + self.data @classmethod def deserialize(cls, data: bytes) -> tuple[Self, bytes]: header, remaining_data = OTAImageHeader.deserialize(data) firmware = remaining_data[: header.image_size - len(header.serialize())] if len(data) < header.image_size: raise ValueError( f"Data is too short to contain image: {len(data)} < {header.image_size}" ) if not firmware.startswith(cls.SUBELEMENTS_MAGIC): raise ValueError( f"Firmware does not start with expected magic bytes: {firmware[:10]!r}" ) if header.manufacturer_id != 4107: raise ValueError( f"Only Hue images are expected. Got: {header.manufacturer_id}" ) return cls(header=header, data=firmware), data[header.image_size :] def parse_ota_image(data: bytes) -> tuple[BaseOTAImage, bytes]: """Attempts to extract any known OTA image type from data. Does not validate firmware.""" if len(data) > 4 and int.from_bytes(data[0:4], "little") + 21 == len(data): # Legrand OTA images are prefixed with their unwrapped size and include a 1 + 16 # byte suffix return OTAImage.deserialize(data[4:-17]) elif ( len(data) > 152 # Avoid the SHA512 hash until we're pretty sure this is a Third Reality image and int.from_bytes(data[68:72], "little") + 64 == len(data) and data.startswith(hashlib.sha512(data[64:]).digest()) ): # Third Reality OTA images contain a 152 byte header with multiple SHA512 hashes # and the image length return OTAImage.deserialize(data[152:]) elif data.startswith(b"NGIS"): # IKEA container needs to be unwrapped if len(data) <= 24: raise ValueError( f"Data too short to contain IKEA container header: {len(data)}" ) offset = int.from_bytes(data[16:20], "little") size = int.from_bytes(data[20:24], "little") if len(data) <= offset + size: raise ValueError(f"Data too short to be IKEA container: {len(data)}") wrapped_data = data[offset : offset + size] image, rest = OTAImage.deserialize(wrapped_data) if rest: LOGGER.warning( "Fixing IKEA OTA image with trailing data (%s bytes)", size - image.header.image_size, ) image.header.image_size += len(rest) # No other structure has been observed assert len(image.subelements) == 1 assert image.subelements[0].tag_id == ElementTagId.UPGRADE_IMAGE image.subelements[0].data += rest rest = b"" return image, rest try: # Hue sbl-ota images start with a Zigbee OTA header but contain no valid # subelements after that. Try it first. return HueSBLOTAImage.deserialize(data) except ValueError: return OTAImage.deserialize(data) zigpy-0.80.1/zigpy/ota/json_schemas.py000066400000000000000000000300541501451476000177220ustar00rootroot00000000000000TRADFRI_SCHEMA = { "type": "array", "items": { "oneOf": [ { "type": "object", "properties": { "fw_image_type": {"type": "integer"}, "fw_type": {"type": "integer"}, "fw_sha3_256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, "fw_binary_url": {"type": "string", "format": "uri"}, }, "required": [ "fw_image_type", "fw_type", "fw_sha3_256", "fw_binary_url", ], }, { "type": "object", "properties": { # For the gateway firmware, ignore the rest of the fields "fw_type": {"type": "integer", "const": 3}, }, "required": [ "fw_type", ], }, # Old IKEA format (new gateway) { "type": "object", "properties": { "fw_binary_url": {"type": "string", "format": "uri"}, "fw_filesize": {"type": "integer"}, "fw_hotfix_version": {"type": "integer"}, "fw_major_version": {"type": "integer"}, "fw_minor_version": {"type": "integer"}, "fw_req_hotfix_version": {"type": "integer"}, "fw_req_major_version": {"type": "integer"}, "fw_req_minor_version": {"type": "integer"}, "fw_type": {"const": 0}, "fw_update_prio": {"type": "integer"}, "fw_weblink_relnote": {"type": "string", "format": "uri"}, }, "required": [ "fw_binary_url", "fw_filesize", "fw_hotfix_version", "fw_major_version", "fw_minor_version", "fw_req_hotfix_version", "fw_req_major_version", "fw_req_minor_version", "fw_type", "fw_update_prio", "fw_weblink_relnote", ], }, # Old IKEA format (device) { "type": "object", "properties": { "fw_binary_url": {"type": "string", "format": "uri"}, "fw_file_version_LSB": {"type": "integer"}, "fw_file_version_MSB": {"type": "integer"}, "fw_filesize": {"type": "integer"}, "fw_image_type": {"type": "integer"}, "fw_manufacturer_id": {"type": "integer"}, "fw_type": {"const": 2}, }, "required": [ "fw_binary_url", "fw_file_version_LSB", "fw_file_version_MSB", "fw_filesize", "fw_image_type", "fw_manufacturer_id", "fw_type", ], }, # Old IKEA format (old gateway) { "type": "object", "properties": { "fw_binary_url": {"type": "string", "format": "uri"}, "fw_build_version": {"type": "integer"}, "fw_file_version_LSB": {"type": "integer"}, "fw_file_version_MSB": {"type": "integer"}, "fw_filesize": {"type": "integer"}, "fw_hotfix_version": {"type": "integer"}, "fw_image_type": {"type": "integer"}, "fw_major_version": {"type": "integer"}, "fw_manufacturer_id": {"type": "integer"}, "fw_minor_version": {"type": "integer"}, "fw_type": {"const": 1}, }, "required": [ "fw_binary_url", "fw_build_version", "fw_file_version_LSB", "fw_file_version_MSB", "fw_filesize", "fw_hotfix_version", "fw_image_type", "fw_major_version", "fw_manufacturer_id", "fw_minor_version", "fw_type", ], }, ] }, } LEDVANCE_SCHEMA = { "type": "object", "properties": { "firmwares": { "type": "array", "items": { "type": "object", "properties": { "blob": {"type": ["null", "string"]}, "identity": { "type": "object", "properties": { "company": {"type": "integer"}, "product": {"type": "integer"}, "version": { "type": "object", "properties": { "major": {"type": "integer"}, "minor": {"type": "integer"}, "build": {"type": "integer"}, "revision": {"type": "integer"}, }, "required": ["major", "minor", "build", "revision"], }, }, "required": ["company", "product", "version"], }, "releaseNotes": {"type": "string"}, "shA256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}, "name": {"type": "string"}, "productName": {"type": "string"}, "fullName": {"type": "string"}, "extension": {"type": "string"}, "released": {"type": "string", "format": "date-time"}, "salesRegion": {"type": ["string", "null"]}, "length": {"type": "integer"}, }, "required": [ "blob", "identity", "releaseNotes", "shA256", "name", "productName", "fullName", "extension", "released", "salesRegion", "length", ], }, } }, "required": ["firmwares"], } SONOFF_SCHEMA = { "type": "array", "items": { "type": "object", "properties": { "fw_binary_url": {"type": "string", "format": "uri"}, "fw_file_version": {"type": "integer"}, "fw_filesize": {"type": "integer"}, "fw_image_type": {"type": "integer"}, "fw_manufacturer_id": {"type": "integer"}, "model_id": {"type": "string"}, }, "required": [ "fw_binary_url", "fw_file_version", "fw_filesize", "fw_image_type", "fw_manufacturer_id", "model_id", ], }, } INOVELLI_SCHEMA = { "type": "object", "patternProperties": { "^[A-Z0-9_-]+$": { "type": "array", "items": { "type": "object", "properties": { "version": { "type": "string", "pattern": "^(?:[0-9A-F]{8}|[0-9]+)$", }, "channel": {"type": "string"}, "firmware": {"type": "string", "format": "uri"}, "manufacturer_id": {"type": "integer"}, "image_type": {"type": "integer"}, }, "required": [ "version", "channel", "firmware", "manufacturer_id", "image_type", ], }, } }, } THIRD_REALITY_SCHEMA = { "type": "object", "properties": { "versions": { "type": "array", "items": { "type": "object", "properties": { "modelId": {"type": "string"}, "url": {"type": "string", "format": "uri"}, "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$", }, "imageType": {"type": "integer"}, "manufacturerId": {"type": "integer"}, "fileVersion": {"type": "integer"}, }, "required": [ "modelId", "url", "version", "imageType", "manufacturerId", "fileVersion", ], }, } }, "required": ["versions"], } REMOTE_PROVIDER_SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "firmwares": { "type": "array", "items": { "type": "object", "properties": { "binary_url": {"type": "string", "format": "uri"}, "path": {"type": "string"}, "file_version": {"type": "integer"}, "file_size": {"type": "integer"}, "image_type": {"type": "integer"}, "manufacturer_names": { "type": "array", "items": {"type": "string"}, }, "model_names": {"type": "array", "items": {"type": "string"}}, "manufacturer_id": {"type": "integer"}, "changelog": {"type": "string"}, "release_notes": {"type": "string"}, "checksum": { "type": "string", "pattern": "^sha3-256:[a-f0-9]{64}$", }, "min_hardware_version": {"type": "integer"}, "max_hardware_version": {"type": "integer"}, "min_current_file_version": {"type": "integer"}, "max_current_file_version": {"type": "integer"}, "specificity": {"type": "integer"}, }, "required": [ # "binary_url", # "path", "file_version", "file_size", "image_type", # "manufacturer_names", # "model_names", "manufacturer_id", # "changelog", "checksum", # "min_hardware_version", # "max_hardware_version", # "min_current_file_version", # "max_current_file_version", # "release_notes", # "specificity", ], }, } }, "required": ["firmwares"], } Z2M_SCHEMA = { "type": "array", "items": { "type": "object", "properties": { "fileVersion": {"type": "integer"}, "fileSize": {"type": "integer"}, "manufacturerCode": {"type": "integer"}, "imageType": {"type": "integer"}, "sha512": {"type": "string", "pattern": "^[a-f0-9]{128}$"}, "url": {"type": "string", "format": "uri"}, "path": {"type": "string"}, "minFileVersion": {"type": "integer"}, "maxFileVersion": {"type": "integer"}, "manufacturerName": {"type": "array", "items": {"type": "string"}}, "modelId": {"type": "string"}, }, "required": [ "fileVersion", "fileSize", "manufacturerCode", "imageType", "sha512", "url", ], }, } zigpy-0.80.1/zigpy/ota/manager.py000066400000000000000000000271571501451476000166720ustar00rootroot00000000000000"""OTA manager for Zigpy. initial implementation from: https://github.com/zigpy/zigpy/pull/1102""" from __future__ import annotations import asyncio import contextlib from typing import TYPE_CHECKING import zigpy.datastructures from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Ota if TYPE_CHECKING: from typing_extensions import Self from zigpy.device import Device from zigpy.ota.providers import OtaImageWithMetadata # Devices often ask for bigger blocks than radios can send MAXIMUM_IMAGE_BLOCK_SIZE = 40 MAX_TIME_WITHOUT_PROGRESS = 30 def find_ota_cluster(device: Device) -> Ota: """Finds the first OTA cluster available on the device.""" for ep in device.non_zdo_endpoints: if Ota.cluster_id in ep.out_clusters: return ep.out_clusters[Ota.cluster_id] raise ValueError("Device has no OTA cluster") class OTAManager: """Class to manage OTA updates for a device.""" def __init__( self, device: Device, image: OtaImageWithMetadata, progress_callback=None, force: bool = False, ) -> None: self.device = device self.ota_cluster = find_ota_cluster(device) self.image = image self._image_data = image.firmware.serialize() self.progress_callback = progress_callback self.force = force self._upgrade_end_future = asyncio.get_running_loop().create_future() self._stall_timer = zigpy.datastructures.ReschedulableTimeout( self._stall_callback ) self.stack = contextlib.ExitStack() def __enter__(self) -> Self: self.stack.enter_context( self.device._application.callback_for_response( src=self.device, filters=[ Ota.ServerCommandDefs.query_next_image.schema(), ], callback=self._image_query_req, ) ) self.stack.enter_context( self.device._application.callback_for_response( src=self.device, filters=[ Ota.ServerCommandDefs.image_block.schema(), ], callback=self._image_block_req, ) ) self.stack.enter_context( self.device._application.callback_for_response( src=self.device, filters=[ Ota.ServerCommandDefs.image_page.schema(), ], callback=self._image_page_req, ) ) self.stack.enter_context( self.device._application.callback_for_response( src=self.device, filters=[ Ota.ServerCommandDefs.upgrade_end.schema(), ], callback=self._upgrade_end, ) ) return self def __exit__(self, *exc_details) -> None: self.stack.close() def _stall_callback(self) -> None: """Handle the stall timer expiring.""" self._finish(foundation.Status.TIMEOUT) def _finish(self, status: foundation.Status) -> None: """Finish the OTA process.""" self._stall_timer.cancel() if not self._upgrade_end_future.done(): self._upgrade_end_future.set_result(status) async def _image_query_req( self, hdr: foundation.ZCLHeader, command: Ota.QueryNextImageCommand ) -> None: """Handle image query request.""" # If we try to send a device an old image (e.g. cache issue), don't bother if not self.force and ( not self.image.check_compatibility(self.device, command) or not self.image.check_version(command.current_file_version) ): status = foundation.Status.NO_IMAGE_AVAILABLE else: status = foundation.Status.SUCCESS try: await self.ota_cluster.query_next_image_response( status=status, manufacturer_code=self.image.firmware.header.manufacturer_id, image_type=self.image.firmware.header.image_type, file_version=self.image.firmware.header.file_version, image_size=self.image.firmware.header.image_size, tsn=hdr.tsn, ) except Exception as ex: # noqa: BLE001 self.device.debug("OTA query_next_image handler exception", exc_info=ex) status = foundation.Status.FAILURE if status != foundation.Status.SUCCESS: self._finish(status) async def _finish_malformed_image_block_response(self, handler: str, tsn: int): """Create an image block response failure.""" try: await self.ota_cluster.image_block_response( status=foundation.Status.MALFORMED_COMMAND, tsn=tsn ) except Exception as ex: # noqa: BLE001 self.device.debug( "OTA %s handler[MALFORMED_COMMAND] exception", handler, exc_info=ex ) self._finish(foundation.Status.MALFORMED_COMMAND) async def _image_block_req( self, hdr: foundation.ZCLHeader, command: Ota.ImageBlockCommand ) -> None: """Handle image block request.""" if command.manufacturer_code == 4129: # Legrand devices (manufacturer_code == 4129) require up to 64 bytes. default_image_block_size = 255 else: default_image_block_size = MAXIMUM_IMAGE_BLOCK_SIZE block = self._image_data[ command.file_offset : command.file_offset + min(default_image_block_size, command.maximum_data_size) ] if not block: await self._finish_malformed_image_block_response( "image_block", tsn=hdr.tsn ) return try: await self.ota_cluster.image_block_response( status=foundation.Status.SUCCESS, manufacturer_code=self.image.firmware.header.manufacturer_id, image_type=self.image.firmware.header.image_type, file_version=self.image.firmware.header.file_version, file_offset=command.file_offset, image_data=block, tsn=hdr.tsn, ) self._stall_timer.reschedule(MAX_TIME_WITHOUT_PROGRESS) # Image block requests can sometimes succeed after the device aborts the # update. We should not allow the progress callback to be called. if ( self.progress_callback is not None and not self._upgrade_end_future.done() ): self.progress_callback( command.file_offset + len(block), len(self._image_data) ) except Exception as ex: # noqa: BLE001 self.device.debug("OTA image_block handler exception", exc_info=ex) async def _image_page_req( self, hdr: foundation.ZCLHeader, command: Ota.ImagePageCommand ) -> None: """Handle image page request.""" offset = command.file_offset bytes_remaining = min( command.page_size, len(self._image_data) - command.file_offset ) if bytes_remaining <= 0: await self._finish_malformed_image_block_response( "image_page_req", tsn=hdr.tsn, ) return while bytes_remaining > 0: block_size = min( MAXIMUM_IMAGE_BLOCK_SIZE, command.maximum_data_size, bytes_remaining, ) block = self._image_data[offset : offset + block_size] offset += block_size bytes_remaining -= block_size try: # Once we have a way to send requests without waiting for replies, # this can be converted to just `self.ota_cluster.image_block_response` await self.ota_cluster.request( general=False, command_id=Ota.ClientCommandDefs.image_block_response.id, schema=Ota.ClientCommandDefs.image_block_response.schema, expect_reply=False, # kwargs status=foundation.Status.SUCCESS, manufacturer_code=self.image.firmware.header.manufacturer_id, image_type=self.image.firmware.header.image_type, file_version=self.image.firmware.header.file_version, file_offset=offset - block_size, image_data=block, ) self._stall_timer.reschedule(MAX_TIME_WITHOUT_PROGRESS) if ( self.progress_callback is not None and not self._upgrade_end_future.done() ): self.progress_callback( offset - block_size + len(block), len(self._image_data) ) except Exception as ex: # noqa: BLE001 self.device.debug("OTA image_page handler exception", exc_info=ex) return # Delay according to what the device asks await asyncio.sleep(command.response_spacing / 1000) async def _upgrade_end( self, hdr: foundation.ZCLHeader, command: foundation.CommandSchema ) -> None: """Handle upgrade end request.""" try: await self.ota_cluster.upgrade_end_response( manufacturer_code=self.image.firmware.header.manufacturer_id, image_type=self.image.firmware.header.image_type, file_version=self.image.firmware.header.file_version, current_time=0x00000000, upgrade_time=0x00000000, tsn=hdr.tsn, ) self._finish(command.status) except Exception as ex: # noqa: BLE001 self.device.debug("OTA upgrade_end handler exception", exc_info=ex) self._finish(foundation.Status.FAILURE) async def notify(self) -> None: """Notify device of new image.""" try: await self.ota_cluster.image_notify( payload_type=( self.ota_cluster.ImageNotifyCommand.PayloadType.QueryJitter ), query_jitter=100, ) except Exception as ex: # noqa: BLE001 self.device.debug("OTA image_notify handler exception", exc_info=ex) self._finish(foundation.Status.FAILURE) else: self._stall_timer.reschedule(MAX_TIME_WITHOUT_PROGRESS) async def wait(self) -> foundation.Status: """Wait for upgrade end response.""" return await self._upgrade_end_future async def update_firmware( device: Device, image: OtaImageWithMetadata, progress_callback: callable | None = None, force: bool = False, ) -> foundation.Status: """Update the firmware on a Zigbee device.""" if force: # Force it to send the image even if it's the same version image = image.replace( metadata=image.metadata.replace(file_version=0xFFFFFFFF - 1), firmware=image.firmware.replace( header=image.firmware.header.replace(file_version=0xFFFFFFFF - 1) ), ) def progress(current: int, total: int): progress = (100 * current) / total device.info( "OTA upgrade progress: (%d / %d): %0.4f%%", current, total, progress, ) if progress_callback is not None: progress_callback(current, total, progress) with OTAManager(device, image, progress_callback=progress, force=force) as ota: await ota.notify() return await ota.wait() zigpy-0.80.1/zigpy/ota/providers.py000066400000000000000000000626131501451476000172710ustar00rootroot00000000000000"""OTA Firmware providers.""" from __future__ import annotations import asyncio import dataclasses import datetime import hashlib import json import logging import pathlib import re import ssl import typing import urllib.parse import aiohttp import attrs import jsonschema import voluptuous as vol import zigpy.config from zigpy.ota import json_schemas from zigpy.ota.image import BaseOTAImage, parse_ota_image import zigpy.types as t import zigpy.util LOGGER = logging.getLogger(__name__) OTA_PROVIDER_TYPES: dict[str, type[BaseOtaProvider]] = {} def register_provider(provider: type[BaseOtaProvider]) -> type[BaseOtaProvider]: """Register a new OTA provider.""" OTA_PROVIDER_TYPES[provider.NAME] = provider return provider @attrs.define(frozen=True, kw_only=True) class BaseOtaImageMetadata(t.BaseDataclassMixin): file_version: int manufacturer_id: int | None = None image_type: int | None = None checksum: str | None = None file_size: int | None = None manufacturer_names: tuple[str] = () model_names: tuple[str] = () changelog: str | None = None release_notes: str | None = None min_hardware_version: int | None = None max_hardware_version: int | None = None min_current_file_version: int | None = None max_current_file_version: int | None = None specificity: int | None = None source: str = "Unknown" async def _fetch(self) -> bytes: raise NotImplementedError async def _validate(self, data: bytes) -> None: if self.file_size is not None and len(data) != self.file_size: raise ValueError( f"Image size is invalid: expected {self.file_size} bytes," f" got {len(data)} bytes" ) if self.checksum is not None: algorithm, checksum = self.checksum.split(":") hasher = hashlib.new(algorithm) await asyncio.get_running_loop().run_in_executor(None, hasher.update, data) if hasher.hexdigest() != checksum: raise ValueError( f"Image checksum is invalid: expected {checksum}," f" got {hasher.hexdigest()}" ) async def fetch(self) -> BaseOTAImage: data = await self._fetch() await self._validate(data) image, _ = parse_ota_image(data) return image @attrs.define(frozen=True, kw_only=True) class RemoteOtaImageMetadata(BaseOtaImageMetadata): url: str # If a provider uses a self-signed certificate, it can override this ssl_ctx: ssl.SSLContext | None = None async def _fetch(self) -> bytes: async with aiohttp.ClientSession(raise_for_status=True) as req: async with req.get(self.url, ssl=self.ssl_ctx) as rsp: return await rsp.read() @attrs.define(frozen=True, kw_only=True) class LocalOtaImageMetadata(BaseOtaImageMetadata): path: pathlib.Path async def _fetch(self) -> bytes: loop = asyncio.get_running_loop() return await loop.run_in_executor(None, self.path.read_bytes) @attrs.define(frozen=True, kw_only=True) class IkeaRemoteOtaImageMetadata(RemoteOtaImageMetadata): ssl_ctx = dataclasses.field(default_factory=lambda: Tradfri.SSL_CTX) async def _fetch(self) -> bytes: async with aiohttp.ClientSession(raise_for_status=True) as req: # Use IKEA's self-signed certificate async with req.get(self.url, ssl=Tradfri.SSL_CTX) as rsp: return await rsp.read() @attrs.define(frozen=True, kw_only=True) class SignedIkeaRemoteOtaImageMetadata(IkeaRemoteOtaImageMetadata): ssl_ctx = dataclasses.field(default_factory=lambda: Tradfri.SSL_CTX) async def _validate(self, data: bytes) -> None: ota_offset = int.from_bytes(data[16:20], "little") ota_size = int.from_bytes(data[20:24], "little") block_size = int.from_bytes(data[32:36], "little") num_block_hashes = int.from_bytes(data[36:40], "little") if ( not data.startswith(b"NGIS") or self.file_size != ota_size or 40 + 32 * num_block_hashes != ota_offset or block_size * num_block_hashes < ota_size ): raise ValueError(f"Invalid signed container: {data[:16]!r}") loop = asyncio.get_running_loop() for block_num in range(num_block_hashes): offset = ota_offset + block_size * block_num size = block_size - max(0, offset + block_size - (ota_offset + ota_size)) block = data[offset : offset + size] expected_checksum = data[40 + 32 * block_num : 40 + 32 * (block_num + 1)] hasher = await loop.run_in_executor(None, hashlib.sha256, block) if hasher.digest() != expected_checksum: raise ValueError(f"Block {block_num} has invalid checksum") class BaseOtaProvider: NAME: str MANUFACTURER_IDS: tuple[int] = () DEFAULT_URL: str | None = None VOL_SCHEMA: vol.Schema JSON_SCHEMA: dict | None = None INDEX_EXPIRATION_TIME = datetime.timedelta(hours=24) def __init__( self, url: str | typing.Literal[True] | None = None, manufacturer_ids: list[int] | None = None, *, override_previous: bool = False, ) -> None: self.url = self.DEFAULT_URL if url in (True, None) else url self._index_last_updated = datetime.datetime.fromtimestamp( 0, tz=datetime.timezone.utc ) if manufacturer_ids is not None: self.manufacturer_ids = tuple(manufacturer_ids) else: self.manufacturer_ids = tuple(self.MANUFACTURER_IDS) self.override_previous = override_previous def compatible_with_device(self, device: zigpy.device.Device) -> bool: if not self.manufacturer_ids: return True return device.manufacturer_id in self.manufacturer_ids async def load_index(self) -> list[BaseOtaImageMetadata] | None: now = datetime.datetime.now(datetime.timezone.utc) # Don't hammer the OTA indexes too frequently if now - self._index_last_updated < self.INDEX_EXPIRATION_TIME: return None try: async with aiohttp.ClientSession( headers={"accept": "application/json"}, raise_for_status=True, ) as session: return [meta async for meta in self._load_index(session)] finally: self._index_last_updated = now async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: if typing.TYPE_CHECKING: yield raise NotImplementedError def __eq__(self, other: object) -> bool: if not isinstance(other, self.__class__): return NotImplemented return self.url == other.url and self.manufacturer_ids == other.manufacturer_ids def __hash__(self) -> int: return hash((self.url, self.manufacturer_ids)) def __repr__(self) -> str: return f"{self.__class__.__name__}(url={self.url!r}, manufacturer_ids={self.manufacturer_ids!r})" @register_provider class Tradfri(BaseOtaProvider): NAME = "ikea" MANUFACTURER_IDS = (4476,) DEFAULT_URL = "https://fw.ota.homesmart.ikea.com/DIRIGERA/version_info.json" VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL JSON_SCHEMA = json_schemas.TRADFRI_SCHEMA # `openssl s_client -connect fw.ota.homesmart.ikea.com:443 -showcerts` SSL_CTX: ssl.SSLContext = ssl.create_default_context() SSL_CTX.load_verify_locations( cadata="""\ -----BEGIN CERTIFICATE----- MIICGDCCAZ+gAwIBAgIUdfH0KDnENv/dEcxH8iVqGGGDqrowCgYIKoZIzj0EAwMw SzELMAkGA1UEBhMCU0UxGjAYBgNVBAoMEUlLRUEgb2YgU3dlZGVuIEFCMSAwHgYD VQQDDBdJS0VBIEhvbWUgc21hcnQgUm9vdCBDQTAgFw0yMTA1MjYxOTAxMDlaGA8y MDcxMDUxNDE5MDEwOFowSzELMAkGA1UEBhMCU0UxGjAYBgNVBAoMEUlLRUEgb2Yg U3dlZGVuIEFCMSAwHgYDVQQDDBdJS0VBIEhvbWUgc21hcnQgUm9vdCBDQTB2MBAG ByqGSM49AgEGBSuBBAAiA2IABIDRUvKGFMUu2zIhTdgfrfNcPULwMlc0TGSrDLBA oTr0SMMV4044CRZQbl81N4qiuHGhFzCnXapZogkiVuFu7ZqSslsFuELFjc6ZxBjk Kmud+pQM6QQdsKTE/cS06dA+P6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E FgQUcdlEnfX0MyZA4zAdY6CLOye9wfwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49 BAMDA2cAMGQCMG6mFIeB2GCFch3r0Gre4xRH+f5pn/bwLr9yGKywpeWvnUPsQ1KW ckMLyxbeNPXdQQIwQc2YZDq/Mz0mOkoheTUWiZxK2a5bk0Uz1XuGshXmQvEg5TGy 2kVHW/Mz9/xwpy4u -----END CERTIFICATE-----""" ) async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get(self.url, ssl=self.SSL_CTX) as rsp: # IKEA does not always respond with an appropriate Content-Type but the # response is always JSON fw_lst = await rsp.json(content_type=None) jsonschema.validate(fw_lst, self.JSON_SCHEMA) for fw in fw_lst: # Skip the gateway image if "fw_image_type" not in fw: continue if "fw_sha3_256" in fw: # New style IKEA file_version_match = re.match(r".*_v(?P\d+)_.*", fw["fw_binary_url"]) if file_version_match is None: LOGGER.warning("Could not parse IKEA OTA JSON: %r", fw) continue image = IkeaRemoteOtaImageMetadata( file_version=int(file_version_match.group("v"), 10), manufacturer_id=self.MANUFACTURER_IDS[0], image_type=fw["fw_image_type"], checksum="sha3-256:" + fw["fw_sha3_256"], url=fw["fw_binary_url"], source="IKEA (DIRIGERA)", ) else: # Old style IKEA if fw["fw_type"] != 2: continue image = SignedIkeaRemoteOtaImageMetadata( file_version=( (fw["fw_file_version_MSB"] << 16) | (fw["fw_file_version_LSB"] << 0) ), manufacturer_id=fw["fw_manufacturer_id"], image_type=fw["fw_image_type"], # The file size is of the contained image, not the container! file_size=fw["fw_filesize"], url=fw["fw_binary_url"].replace("http://", "https://", 1), source="IKEA (TRÅDFRI)", ) # Bricking update: https://github.com/zigpy/zigpy/issues/1428 if image.image_type in (8704, 8710): continue yield image @register_provider class Ledvance(BaseOtaProvider): NAME = "ledvance" # This isn't static but no more than these two have ever existed MANUFACTURER_IDS = (4489, 4364) DEFAULT_URL = "https://api.update.ledvance.com/v1/zigbee/firmwares" JSON_SCHEMA = json_schemas.LEDVANCE_SCHEMA VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get(self.url) as rsp: fw_lst = await rsp.json() jsonschema.validate(fw_lst, self.JSON_SCHEMA) for fw in fw_lst["firmwares"]: identity = fw["identity"] version = identity["version"] yield RemoteOtaImageMetadata( file_version=int(fw["fullName"].split("/")[1], 16), manufacturer_id=identity["company"], image_type=identity["product"], checksum="sha256:" + fw["shA256"], file_size=fw["length"], model_names=(fw["productName"],), url=( "https://api.update.ledvance.com/v1/zigbee/firmwares/download?" + urllib.parse.urlencode( { "Company": identity["company"], "Product": identity["product"], "Version": ( f"{version['major']}.{version['minor']}" f".{version['build']}.{version['revision']}" ), } ) ), release_notes=fw["releaseNotes"], source="Ledvance", ) # stub provider to keep existing configurations working @register_provider class Salus(BaseOtaProvider): NAME = "salus" MANUFACTURER_IDS = (4216, 43981) VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: if False: yield # pragma: no cover @register_provider class Sonoff(BaseOtaProvider): NAME = "sonoff" MANUFACTURER_IDS = (4742,) JSON_SCHEMA = json_schemas.SONOFF_SCHEMA VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get( "https://zigbee-ota.sonoff.tech/releases/upgrade.json" ) as rsp: fw_lst = await rsp.json() jsonschema.validate(fw_lst, self.JSON_SCHEMA) for fw in fw_lst: yield RemoteOtaImageMetadata( file_version=fw["fw_file_version"], manufacturer_id=fw["fw_manufacturer_id"], image_type=fw["fw_image_type"], file_size=fw["fw_filesize"], url=fw["fw_binary_url"], model_names=(fw["model_id"],), source="Sonoff", ) @register_provider class Inovelli(BaseOtaProvider): NAME = "inovelli" MANUFACTURER_IDS = (4655,) JSON_SCHEMA = json_schemas.INOVELLI_SCHEMA VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get( "https://files.inovelli.com/firmware/firmware-zha-v2.json" ) as rsp: fw_lst = await rsp.json() jsonschema.validate(fw_lst, self.JSON_SCHEMA) for model, firmwares in fw_lst.items(): for fw in firmwares: version = int(fw["version"], 16) if version > 0x0000000B: # Only the first firmware was in hex, all others are decimal version = int(fw["version"]) yield RemoteOtaImageMetadata( file_version=version, manufacturer_id=fw["manufacturer_id"], image_type=fw["image_type"], model_names=(model,), url=fw["firmware"], source="Inovelli", ) @register_provider class ThirdReality(BaseOtaProvider): NAME = "thirdreality" MANUFACTURER_IDS = (4659, 4877, 5127) JSON_SCHEMA = json_schemas.THIRD_REALITY_SCHEMA VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get("https://tr-zha.s3.amazonaws.com/firmware.json") as rsp: fw_lst = await rsp.json() jsonschema.validate(fw_lst, self.JSON_SCHEMA) for fw in fw_lst["versions"]: yield RemoteOtaImageMetadata( file_version=fw["fileVersion"], manufacturer_id=fw["manufacturerId"], model_names=(fw["modelId"],), image_type=fw["imageType"], url=fw["url"], source="ThirdReality", ) class BaseZigpyProvider(BaseOtaProvider): JSON_SCHEMA = json_schemas.REMOTE_PROVIDER_SCHEMA @classmethod def _load_zigpy_index(cls, index: dict, *, index_root: pathlib.Path | None = None): jsonschema.validate(index, cls.JSON_SCHEMA) for fw in index["firmwares"]: shared_kwargs = { "file_version": fw["file_version"], "manufacturer_id": fw["manufacturer_id"], "image_type": fw["image_type"], "manufacturer_names": tuple(fw.get("manufacturer_names", [])), "model_names": tuple(fw.get("model_names", [])), "checksum": fw["checksum"], "file_size": fw["file_size"], "min_hardware_version": fw.get("min_hardware_version"), "max_hardware_version": fw.get("max_hardware_version"), "min_current_file_version": fw.get("min_current_file_version"), "max_current_file_version": fw.get("max_current_file_version"), "changelog": fw.get("changelog"), "release_notes": fw.get("release_notes"), "specificity": fw.get("specificity"), "source": "", # Set in a subclass } if "path" in fw and index_root is not None: yield LocalOtaImageMetadata( **shared_kwargs, path=index_root / fw["path"] ) else: yield RemoteOtaImageMetadata(**shared_kwargs, url=fw["binary_url"]) @register_provider class LocalZigpyProvider(BaseZigpyProvider): NAME = "zigpy_local" VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_JSON_INDEX def __init__(self, index_file: pathlib.Path, **kwargs): super().__init__(url=None, **kwargs) self.index_file = index_file async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: index_text = await asyncio.get_running_loop().run_in_executor( None, self.index_file.read_text ) index = json.loads(index_text) for img in self._load_zigpy_index(index, index_root=self.index_file.parent): yield img.replace(source=f"Local zigpy provider ({self.index_file})") def __eq__(self, other: object) -> bool: if ( not isinstance(other, self.__class__) or super().__eq__(other) is NotImplemented ): return NotImplemented return super().__eq__(other) and self.index_file == other.index_file def __hash__(self) -> int: return hash((self.index_file, self.manufacturer_ids)) def __repr__(self) -> str: return f"{self.__class__.__name__}(index_file={self.index_file!r}, manufacturer_ids={self.manufacturer_ids!r})" @register_provider class RemoteZigpyProvider(BaseZigpyProvider): NAME = "zigpy_remote" VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL_REQUIRED async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get(self.url) as rsp: fw_lst = await rsp.json(content_type=None) jsonschema.validate(fw_lst, self.JSON_SCHEMA) for img in self._load_zigpy_index(fw_lst): yield img.replace(source=f"Remote zigpy provider ({self.url})") class BaseZ2MProvider(BaseOtaProvider): JSON_SCHEMA = json_schemas.Z2M_SCHEMA @classmethod def _load_z2m_index( cls, index: dict, *, index_root: pathlib.Path | None = None, ssl_ctx: ssl.SSLContext | None = None, ) -> typing.Iterator[LocalOtaImageMetadata | RemoteOtaImageMetadata]: jsonschema.validate(index, cls.JSON_SCHEMA) for fw in index: shared_kwargs = { "file_version": fw["fileVersion"], "manufacturer_id": fw["manufacturerCode"], "image_type": fw["imageType"], "checksum": "sha512:" + fw["sha512"], "file_size": fw["fileSize"], "manufacturer_names": tuple(fw.get("manufacturerName", [])), "model_names": tuple([fw["modelId"]] if "modelId" in fw else []), "min_current_file_version": fw.get("minFileVersion"), "max_current_file_version": fw.get("maxFileVersion"), "min_hardware_version": fw.get("hardwareVersionMin"), "max_hardware_version": fw.get("hardwareVersionMax"), "changelog": fw.get("releaseNotes"), # Changelog is short "source": "", # Set in a subclass } if "path" in fw and index_root is not None: yield LocalOtaImageMetadata( **shared_kwargs, path=index_root / fw["path"] ) else: yield RemoteOtaImageMetadata( **shared_kwargs, url=fw["url"], ssl_ctx=ssl_ctx ) @register_provider class LocalZ2MProvider(BaseZ2MProvider): NAME = "z2m_local" VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_JSON_INDEX def __init__(self, index_file: pathlib.Path, **kwargs): super().__init__(**kwargs) self.index_file = index_file async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: index_text = await asyncio.get_running_loop().run_in_executor( None, self.index_file.read_text ) index = json.loads(index_text) for img in self._load_z2m_index(index, index_root=self.index_file.parent): yield img.replace(source=f"Local Z2M provider ({self.index_file})") def __eq__(self, other: object) -> bool: if ( not isinstance(other, self.__class__) or super().__eq__(other) is NotImplemented ): return NotImplemented return super().__eq__(other) and self.index_file == other.index_file def __hash__(self) -> int: return hash((self.index_file, self.manufacturer_ids)) def __repr__(self) -> str: return f"{self.__class__.__name__}(index_file={self.index_file!r}, manufacturer_ids={self.manufacturer_ids!r})" @register_provider class RemoteZ2MProvider(BaseZ2MProvider): NAME = "z2m" DEFAULT_URL = ( "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/index.json" ) VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_URL # `openssl s_client -connect otau.meethue.com:443 -showcerts` SSL_CTX = ssl.create_default_context() SSL_CTX.load_verify_locations( cadata="""\ -----BEGIN CERTIFICATE----- MIIBwDCCAWagAwIBAgIJAJtrMkoTxs+WMAoGCCqGSM49BAMCMDIxCzAJBgNVBAYT Ak5MMRQwEgYDVQQKDAtQaGlsaXBzIEh1ZTENMAsGA1UEAwwEcm9vdDAgFw0xNjA4 MjUwNzU5NDNaGA8yMDY4MDEwNTA3NTk0M1owMjELMAkGA1UEBhMCTkwxFDASBgNV BAoMC1BoaWxpcHMgSHVlMQ0wCwYDVQQDDARyb290MFkwEwYHKoZIzj0CAQYIKoZI zj0DAQcDQgAEENC1JOl6BxJrwCb+YK655zlM57VKFSi5OHDsmlCaF/EfTGGgU08/ JUtkCyMlHUUoYBZyzCBKXqRKkrT512evEKNjMGEwHQYDVR0OBBYEFAlkFYACVzir qTr++cWia8AKH/fOMB8GA1UdIwQYMBaAFAlkFYACVzirqTr++cWia8AKH/fOMA8G A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0gAMEUC IQDcGfyXaUl5hjr5YE8m2piXhMcDzHTNbO1RvGgz4r9IswIgFTTw/R85KyfIiW+E clwJRVSsq8EApeFREenCkRM0EIk= -----END CERTIFICATE-----""" ) async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: async with session.get(self.url) as rsp: fw_lst = await rsp.json(content_type=None) for img in self._load_z2m_index(fw_lst, ssl_ctx=self.SSL_CTX): yield img.replace(source=f"Remote Z2M provider ({self.url})") @register_provider class AdvancedFileProvider(BaseOtaProvider): NAME = "advanced" VOL_SCHEMA = zigpy.config.SCHEMA_OTA_PROVIDER_FOLDER def __init__(self, path: pathlib.Path, **kwargs): # The `vol` schema passes through the `warning` key, which is unused kwargs.pop("warning", None) super().__init__(url=None, **kwargs) self.path = path async def _load_index( self, session: aiohttp.ClientSession ) -> typing.AsyncIterator[BaseOtaImageMetadata]: loop = asyncio.get_running_loop() paths = await loop.run_in_executor(None, self.path.rglob, "*") async for chunk in zigpy.util.async_iterate_in_chunks(paths, chunk_size=100): for path in chunk: if not path.is_file(): continue data = await loop.run_in_executor(None, path.read_bytes) try: image, _ = parse_ota_image(data) except Exception as exc: # noqa: BLE001 LOGGER.debug("Failed to parse image %s: %r", path, exc) continue # This protects against images being swapped out in the local filesystem hasher = await loop.run_in_executor(None, hashlib.sha1, data) yield LocalOtaImageMetadata( path=path, file_version=image.header.file_version, manufacturer_id=image.header.manufacturer_id, image_type=image.header.image_type, checksum="sha1:" + hasher.hexdigest(), file_size=len(data), min_hardware_version=image.header.minimum_hardware_version, max_hardware_version=image.header.maximum_hardware_version, source=f"Advanced file provider ({self.path})", ) def __eq__(self, other: object) -> bool: if ( not isinstance(other, self.__class__) or super().__eq__(other) is NotImplemented ): return NotImplemented return super().__eq__(other) and self.path == other.path def __hash__(self) -> int: return hash((self.path, self.manufacturer_ids)) def __repr__(self) -> str: return f"{self.__class__.__name__}(path={self.path!r}, manufacturer_ids={self.manufacturer_ids!r})" zigpy-0.80.1/zigpy/ota/validators.py000066400000000000000000000102321501451476000174120ustar00rootroot00000000000000from __future__ import annotations import enum import logging import typing import zlib from zigpy.ota.image import BaseOTAImage, ElementTagId, OTAImage VALID_SILABS_CRC = 0x2144DF1C # CRC32(anything | CRC32(anything)) == CRC32(0x00000000) LOGGER = logging.getLogger(__name__) class ValidationResult(enum.Enum): INVALID = 0 VALID = 1 UNKNOWN = 2 class ValidationError(Exception): pass def parse_silabs_ebl(data: bytes) -> typing.Iterable[tuple[bytes, bytes]]: """Parses a Silicon Labs EBL firmware image.""" if len(data) % 64 != 0: raise ValidationError( f"Image size ({len(data)}) must be a multiple of 64 bytes" ) orig_data = data while True: if len(data) < 4: raise ValidationError( "Image is truncated: not long enough to contain a valid tag" ) tag = data[:2] length = int.from_bytes(data[2:4], "big") value = data[4 : 4 + length] if len(value) < length: raise ValidationError("Image is truncated: tag value is cut off") data = data[4 + length :] yield tag, value # EBL end tag if tag != b"\xfc\x04": continue # At this point the EBL should contain nothing but padding if data.strip(b"\xff"): raise ValidationError("Image padding contains invalid bytes") unpadded_image = orig_data[: -len(data)] if data else orig_data computed_crc = zlib.crc32(unpadded_image) if computed_crc != VALID_SILABS_CRC: raise ValidationError( f"Image CRC-32 is invalid:" f" expected 0x{VALID_SILABS_CRC:08X}, got 0x{computed_crc:08X}" ) break # pragma: no cover def parse_silabs_gbl(data: bytes) -> typing.Iterable[tuple[bytes, bytes]]: """Parses a Silicon Labs GBL firmware image.""" orig_data = data while True: if len(data) < 8: raise ValidationError( "Image is truncated: not long enough to contain a valid tag" ) tag = data[:4] length = int.from_bytes(data[4:8], "little") value = data[8 : 8 + length] if len(value) < length: raise ValidationError("Image is truncated: tag value is cut off") data = data[8 + length :] yield tag, value # GBL end tag if tag != b"\xfc\x04\x04\xfc": continue # GBL images aren't expected to contain padding but some are (i.e. Hue) unpadded_image = orig_data[: -len(data)] if data else orig_data computed_crc = zlib.crc32(unpadded_image) if computed_crc != VALID_SILABS_CRC: raise ValidationError( f"Image CRC-32 is invalid:" f" expected 0x{VALID_SILABS_CRC:08X}, got 0x{computed_crc:08X}" ) break # pragma: no cover def validate_firmware(data: bytes) -> ValidationResult: """Validates a firmware image.""" parser = None if data.startswith(b"\xeb\x17\xa6\x03"): parser = parse_silabs_gbl elif data.startswith(b"\x00\x00\x00\x8c"): parser = parse_silabs_ebl else: return ValidationResult.UNKNOWN tuple(parser(data)) return ValidationResult.VALID def validate_ota_image(image: BaseOTAImage) -> ValidationResult: """Validates a Zigbee OTA image's embedded firmwares and indicates if an image is valid, invalid, or of an unknown type. """ if not isinstance(image, OTAImage): return ValidationResult.UNKNOWN results = [ validate_firmware(subelement.data) for subelement in image.subelements if subelement.tag_id == ElementTagId.UPGRADE_IMAGE ] if not results or any(r == ValidationResult.UNKNOWN for r in results): return ValidationResult.UNKNOWN return ValidationResult.VALID def check_invalid(image: BaseOTAImage) -> bool: """Checks if an image is invalid or not. Unknown image types are considered valid.""" try: validate_ota_image(image) except ValidationError as e: LOGGER.warning("Image %s is invalid: %s", image.header, e) return True else: return False zigpy-0.80.1/zigpy/profiles/000077500000000000000000000000001501451476000157325ustar00rootroot00000000000000zigpy-0.80.1/zigpy/profiles/__init__.py000066400000000000000000000002141501451476000200400ustar00rootroot00000000000000from __future__ import annotations from . import zgp, zha, zll PROFILES = {zha.PROFILE_ID: zha, zll.PROFILE_ID: zll, zgp.PROFILE_ID: zgp} zigpy-0.80.1/zigpy/profiles/zgp.py000066400000000000000000000011321501451476000171010ustar00rootroot00000000000000from __future__ import annotations import zigpy.types as t PROFILE_ID = 41440 class DeviceType(t.enum16): PROXY = 0x0060 PROXY_BASIC = 0x0061 TARGET_PLUS = 0x0062 TARGET = 0x0063 COMM_TOOL = 0x0064 COMBO = 0x0065 COMBO_BASIC = 0x0066 CLUSTERS = { DeviceType.PROXY: ([0x0021], [0x0021]), DeviceType.PROXY_BASIC: ([], [0x0021]), DeviceType.TARGET_PLUS: ([0x0021], [0x0021]), DeviceType.TARGET: ([0x0021], [0x0021]), DeviceType.COMM_TOOL: ([0x0021], []), DeviceType.COMBO: ([0x0021], [0x0021]), DeviceType.COMBO_BASIC: ([0x0021], [0x0021]), } zigpy-0.80.1/zigpy/profiles/zha.py000066400000000000000000000067441501451476000171010ustar00rootroot00000000000000from __future__ import annotations import zigpy.types as t PROFILE_ID = 260 class DeviceType(t.enum16): # Generic ON_OFF_SWITCH = 0x0000 LEVEL_CONTROL_SWITCH = 0x0001 ON_OFF_OUTPUT = 0x0002 LEVEL_CONTROLLABLE_OUTPUT = 0x0003 SCENE_SELECTOR = 0x0004 CONFIGURATION_TOOL = 0x0005 REMOTE_CONTROL = 0x0006 COMBINED_INTERFACE = 0x0007 RANGE_EXTENDER = 0x0008 MAIN_POWER_OUTLET = 0x0009 DOOR_LOCK = 0x000A DOOR_LOCK_CONTROLLER = 0x000B SIMPLE_SENSOR = 0x000C CONSUMPTION_AWARENESS_DEVICE = 0x000D HOME_GATEWAY = 0x0050 SMART_PLUG = 0x0051 WHITE_GOODS = 0x0052 METER_INTERFACE = 0x0053 # Lighting ON_OFF_LIGHT = 0x0100 DIMMABLE_LIGHT = 0x0101 COLOR_DIMMABLE_LIGHT = 0x0102 ON_OFF_LIGHT_SWITCH = 0x0103 DIMMER_SWITCH = 0x0104 COLOR_DIMMER_SWITCH = 0x0105 LIGHT_SENSOR = 0x0106 OCCUPANCY_SENSOR = 0x0107 # ZLO device types ON_OFF_BALLAST = 0x0108 DIMMABLE_BALLAST = 0x0109 ON_OFF_PLUG_IN_UNIT = 0x010A DIMMABLE_PLUG_IN_UNIT = 0x010B COLOR_TEMPERATURE_LIGHT = 0x010C EXTENDED_COLOR_LIGHT = 0x010D LIGHT_LEVEL_SENSOR = 0x010E # Closure SHADE = 0x0200 SHADE_CONTROLLER = 0x0201 WINDOW_COVERING_DEVICE = 0x0202 WINDOW_COVERING_CONTROLLER = 0x0203 # HVAC HEATING_COOLING_UNIT = 0x0300 THERMOSTAT = 0x0301 TEMPERATURE_SENSOR = 0x0302 PUMP = 0x0303 PUMP_CONTROLLER = 0x0304 PRESSURE_SENSOR = 0x0305 FLOW_SENSOR = 0x0306 MINI_SPLIT_AC = 0x0307 # Intruder Alarm Systems IAS_CONTROL = 0x0400 # IAS Control and Indicating Equipment IAS_ANCILLARY_CONTROL = 0x0401 # IAS Ancillary Control Equipment IAS_ZONE = 0x0402 IAS_WARNING_DEVICE = 0x0403 # ZLO device types, continued COLOR_CONTROLLER = 0x0800 COLOR_SCENE_CONTROLLER = 0x0810 NON_COLOR_CONTROLLER = 0x0820 NON_COLOR_SCENE_CONTROLLER = 0x0830 CONTROL_BRIDGE = 0x0840 ON_OFF_SENSOR = 0x0850 CLUSTERS = { # Generic DeviceType.ON_OFF_SWITCH: ([0x0007], [0x0004, 0x0005, 0x0006]), DeviceType.LEVEL_CONTROL_SWITCH: ([0x0007], [0x0004, 0x0005, 0x0006, 0x0008]), DeviceType.ON_OFF_OUTPUT: ([0x0004, 0x0005, 0x0006], []), DeviceType.LEVEL_CONTROLLABLE_OUTPUT: ([0x0004, 0x0005, 0x0006, 0x0008], []), DeviceType.SCENE_SELECTOR: ([], [0x0004, 0x0005]), DeviceType.REMOTE_CONTROL: ([], [0x0004, 0x0005, 0x0006, 0x0008]), DeviceType.MAIN_POWER_OUTLET: ([0x0004, 0x0005, 0x0006], []), DeviceType.SMART_PLUG: ([0x0004, 0x0005, 0x0006], []), # Lighting DeviceType.ON_OFF_LIGHT: ([0x0004, 0x0005, 0x0006, 0x0008], []), DeviceType.DIMMABLE_LIGHT: ([0x0004, 0x0005, 0x0006, 0x0008], []), DeviceType.COLOR_DIMMABLE_LIGHT: ([0x0004, 0x0005, 0x0006, 0x0008, 0x0300], []), DeviceType.ON_OFF_LIGHT_SWITCH: ([0x0007], [0x0004, 0x0005, 0x0006]), DeviceType.DIMMER_SWITCH: ([0x0007], [0x0004, 0x0005, 0x0006, 0x0008]), DeviceType.COLOR_DIMMER_SWITCH: ( [0x0007], [0x0004, 0x0005, 0x0006, 0x0008, 0x0300], ), DeviceType.LIGHT_SENSOR: ([0x0400], []), DeviceType.OCCUPANCY_SENSOR: ([0x0406], []), DeviceType.COLOR_TEMPERATURE_LIGHT: ( [0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0300], [], ), DeviceType.EXTENDED_COLOR_LIGHT: ( [0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0300], [], ), # Closures DeviceType.WINDOW_COVERING_DEVICE: ([0x0004, 0x0005, 0x0102], []), # HVAC DeviceType.THERMOSTAT: ([0x0201, 0x0204], [0x0200, 0x0202, 0x0203]), } zigpy-0.80.1/zigpy/profiles/zll.py000066400000000000000000000031131501451476000171030ustar00rootroot00000000000000from __future__ import annotations import zigpy.types as t PROFILE_ID = 49246 class DeviceType(t.enum16): ON_OFF_LIGHT = 0x0000 ON_OFF_PLUGIN_UNIT = 0x0010 DIMMABLE_LIGHT = 0x0100 DIMMABLE_PLUGIN_UNIT = 0x0110 COLOR_LIGHT = 0x0200 EXTENDED_COLOR_LIGHT = 0x0210 COLOR_TEMPERATURE_LIGHT = 0x0220 COLOR_CONTROLLER = 0x0800 COLOR_SCENE_CONTROLLER = 0x0810 CONTROLLER = 0x0820 SCENE_CONTROLLER = 0x0830 CONTROL_BRIDGE = 0x0840 ON_OFF_SENSOR = 0x0850 CLUSTERS = { DeviceType.ON_OFF_LIGHT: ([0x0004, 0x0005, 0x0006, 0x0008, 0x1000], []), DeviceType.ON_OFF_PLUGIN_UNIT: ([0x0004, 0x0005, 0x0006, 0x0008, 0x1000], []), DeviceType.DIMMABLE_LIGHT: ([0x0004, 0x0005, 0x0006, 0x0008, 0x1000], []), DeviceType.DIMMABLE_PLUGIN_UNIT: ([0x0004, 0x0005, 0x0006, 0x0008, 0x1000], []), DeviceType.COLOR_LIGHT: ([0x0004, 0x0005, 0x0006, 0x0008, 0x0300, 0x1000], []), DeviceType.EXTENDED_COLOR_LIGHT: ( [0x0004, 0x0005, 0x0006, 0x0008, 0x0300, 0x1000], [], ), DeviceType.COLOR_TEMPERATURE_LIGHT: ( [0x0004, 0x0005, 0x0006, 0x0008, 0x0300, 0x1000], [], ), DeviceType.COLOR_CONTROLLER: ([], [0x0004, 0x0006, 0x0008, 0x0300]), DeviceType.COLOR_SCENE_CONTROLLER: ([], [0x0004, 0x0005, 0x0006, 0x0008, 0x0300]), DeviceType.CONTROLLER: ([], [0x0004, 0x0006, 0x0008]), DeviceType.SCENE_CONTROLLER: ([], [0x0004, 0x0005, 0x0006, 0x0008]), DeviceType.CONTROL_BRIDGE: ([], [0x0004, 0x0005, 0x0006, 0x0008, 0x0300]), DeviceType.ON_OFF_SENSOR: ([], [0x0004, 0x0005, 0x0006, 0x0008, 0x0300]), } zigpy-0.80.1/zigpy/quirks/000077500000000000000000000000001501451476000154255ustar00rootroot00000000000000zigpy-0.80.1/zigpy/quirks/__init__.py000066400000000000000000000415431501451476000175450ustar00rootroot00000000000000"""Zigpy quirks module.""" from __future__ import annotations import logging import typing from zigpy.const import ( # noqa: F401 SIG_ENDPOINTS, SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE, SIG_MANUFACTURER, SIG_MODEL, SIG_MODELS_INFO, SIG_NODE_DESC, SIG_SKIP_CONFIG, ) import zigpy.device import zigpy.endpoint from zigpy.quirks.registry import DeviceRegistry import zigpy.types as t from zigpy.types.basic import uint16_t import zigpy.zcl from zigpy.zcl import foundation from zigpy.zdo import ZDO if typing.TYPE_CHECKING: from zigpy.application import ControllerApplication _LOGGER = logging.getLogger(__name__) DEVICE_REGISTRY = _DEVICE_REGISTRY = DeviceRegistry() _uninitialized_device_message_handlers = [] def get_device( device: zigpy.device.Device, registry: DeviceRegistry | None = None ) -> zigpy.device.Device: """Get a CustomDevice object, if one is available""" if registry is None: return _DEVICE_REGISTRY.get_device(device) return registry.get_device(device) def get_quirk_list( manufacturer: str, model: str, registry: DeviceRegistry | None = None ): """Get the Quirk list for a given manufacturer and model.""" if registry is None: return _DEVICE_REGISTRY.registry_v1[manufacturer][model] return registry.registry_v1[manufacturer][model] def register_uninitialized_device_message_handler(handler: typing.Callable) -> None: """Register an handler for messages received by uninitialized devices. each handler is passed same parameters as zigpy.application.ControllerApplication.handle_message """ if handler not in _uninitialized_device_message_handlers: _uninitialized_device_message_handlers.append(handler) class BaseCustomDevice(zigpy.device.Device): """Base class for custom devices.""" _copy_cluster_attr_cache = False replacement: dict[str, typing.Any] = {} def __init__( self, application: ControllerApplication, ieee: t.EUI64, nwk: t.NWK, replaces: zigpy.device.Device, ) -> None: super().__init__(application, ieee, nwk) def set_device_attr(attr): if attr in self.replacement: setattr(self, attr, self.replacement[attr]) else: setattr(self, attr, getattr(replaces, attr)) for attr in ("lqi", "rssi", "last_seen", "relays"): setattr(self, attr, getattr(replaces, attr)) set_device_attr("status") set_device_attr(SIG_NODE_DESC) set_device_attr(SIG_MANUFACTURER) set_device_attr(SIG_MODEL) set_device_attr(SIG_SKIP_CONFIG) for endpoint_id in self.replacement.get(SIG_ENDPOINTS, {}): self.add_endpoint(endpoint_id, replace_device=replaces) def add_endpoint( self, endpoint_id: int, replace_device: zigpy.device.Device | None = None ) -> zigpy.endpoint.Endpoint: if endpoint_id not in self.replacement.get(SIG_ENDPOINTS, {}): return super().add_endpoint(endpoint_id) endpoints = self.replacement[SIG_ENDPOINTS] if isinstance(endpoints[endpoint_id], tuple): custom_ep_type = endpoints[endpoint_id][0] replacement_data = endpoints[endpoint_id][1] else: custom_ep_type = CustomEndpoint replacement_data = endpoints[endpoint_id] ep = custom_ep_type(self, endpoint_id, replacement_data, replace_device) self.endpoints[endpoint_id] = ep return ep async def apply_custom_configuration(self, *args, **kwargs): """Hook for applications to instruct instances to apply custom configuration.""" for endpoint in self.endpoints.values(): if isinstance(endpoint, ZDO): continue for cluster in endpoint.in_clusters.values(): if ( isinstance(cluster, CustomCluster) and cluster.apply_custom_configuration != CustomCluster.apply_custom_configuration ): await cluster.apply_custom_configuration(*args, **kwargs) for cluster in endpoint.out_clusters.values(): if ( isinstance(cluster, CustomCluster) and cluster.apply_custom_configuration != CustomCluster.apply_custom_configuration ): await cluster.apply_custom_configuration(*args, **kwargs) class CustomDevice(BaseCustomDevice): """Implementation of a quirks v1 custom device.""" signature = None def __init_subclass__(cls) -> None: if getattr(cls, "signature", None) is not None: _DEVICE_REGISTRY.add_to_registry(cls) class CustomEndpoint(zigpy.endpoint.Endpoint): """Custom endpoint implementation for quirks.""" def __init__( self, device: BaseCustomDevice, endpoint_id: int, replacement_data: dict[str, typing.Any], replace_device: zigpy.device.Device, ) -> None: super().__init__(device, endpoint_id) def set_device_attr(attr): if attr in replacement_data: setattr(self, attr, replacement_data[attr]) else: setattr(self, attr, getattr(replace_device[endpoint_id], attr)) set_device_attr(SIG_EP_PROFILE) set_device_attr(SIG_EP_TYPE) self.status = zigpy.endpoint.Status.ZDO_INIT for c in replacement_data.get(SIG_EP_INPUT, []): if isinstance(c, int): cluster = None cluster_id = c else: cluster = c(self, is_server=True) cluster_id = cluster.cluster_id cluster = self.add_input_cluster(cluster_id, cluster) if self.device._copy_cluster_attr_cache: if ( endpoint_id in replace_device.endpoints and cluster_id in replace_device.endpoints[endpoint_id].in_clusters ): cluster._attr_cache = ( replace_device[endpoint_id] .in_clusters[cluster_id] ._attr_cache.copy() ) for c in replacement_data.get(SIG_EP_OUTPUT, []): if isinstance(c, int): cluster = None cluster_id = c else: cluster = c(self, is_server=False) cluster_id = cluster.cluster_id cluster = self.add_output_cluster(cluster_id, cluster) if self.device._copy_cluster_attr_cache: if ( endpoint_id in replace_device.endpoints and cluster_id in replace_device.endpoints[endpoint_id].out_clusters ): cluster._attr_cache = ( replace_device[endpoint_id] .out_clusters[cluster_id] ._attr_cache.copy() ) class CustomCluster(zigpy.zcl.Cluster): """Custom cluster implementation for quirks.""" _skip_registry = True _CONSTANT_ATTRIBUTES: dict[int, typing.Any] | None = None manufacturer_id_override: t.uint16_t | None = None @property def _is_manuf_specific(self) -> bool: """Return True if cluster_id is within manufacturer specific range.""" return 0xFC00 <= self.cluster_id <= 0xFFFF def _has_manuf_attr(self, attrs_to_process: typing.Iterable | list | dict) -> bool: """Return True if contains a manufacturer specific attribute.""" if self._is_manuf_specific: return True for attr_id in attrs_to_process: if ( attr_id in self.attributes and self.attributes[attr_id].is_manufacturer_specific ): return True return False @property def _manufacturer_id(self) -> int | None: """Return manufacturer id, accounting for local overrides.""" return ( self.manufacturer_id_override if self.manufacturer_id_override is not None else self.endpoint.manufacturer_id ) async def command( self, command_id: foundation.GeneralCommand | int | t.uint8_t, *args, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, tsn: int | t.uint8_t | None = None, **kwargs: typing.Any, ) -> typing.Coroutine: command = self.server_commands[command_id] if manufacturer is None and ( self._is_manuf_specific or command.is_manufacturer_specific ): manufacturer = self._manufacturer_id return await self.request( False, command.id, command.schema, *args, manufacturer=manufacturer, expect_reply=expect_reply, tsn=tsn, **kwargs, ) async def client_command( self, command_id: foundation.GeneralCommand | int | t.uint8_t, *args, manufacturer: int | t.uint16_t | None = None, tsn: int | t.uint8_t | None = None, **kwargs: typing.Any, ): command = self.client_commands[command_id] if manufacturer is None and ( self._is_manuf_specific or command.is_manufacturer_specific ): manufacturer = self._manufacturer_id return await self.reply( False, command.id, command.schema, *args, manufacturer=manufacturer, tsn=tsn, **kwargs, ) async def read_attributes_raw( self, attributes: list[uint16_t], manufacturer: uint16_t | None = None, **kwargs ): if not self._CONSTANT_ATTRIBUTES: return await super().read_attributes_raw( attributes, manufacturer=manufacturer, **kwargs ) succeeded = [ foundation.ReadAttributeRecord( attrid=attr, status=foundation.Status.SUCCESS, value=foundation.TypeValue( type=None, value=self._CONSTANT_ATTRIBUTES[attr], ), ) for attr in attributes if attr in self._CONSTANT_ATTRIBUTES ] attrs_to_read = [ attr for attr in attributes if attr not in self._CONSTANT_ATTRIBUTES ] if not attrs_to_read: return [succeeded] results = await super().read_attributes_raw( attrs_to_read, manufacturer=manufacturer, **kwargs ) if not isinstance(results[0], list): for attrid in attrs_to_read: succeeded.append( # noqa: PERF401 foundation.ReadAttributeRecord( attrid, results[0], foundation.TypeValue(), ) ) else: succeeded.extend(results[0]) return [succeeded] async def _configure_reporting( # type:ignore[override] self, config_records: list[foundation.AttributeReportingConfig], *args, manufacturer: int | t.uint16_t | None = None, **kwargs, ): """Configure reporting ZCL foundation command.""" if manufacturer is None and self._has_manuf_attr( [a.attrid for a in config_records] ): manufacturer = self._manufacturer_id return await super()._configure_reporting( config_records, *args, manufacturer=manufacturer, **kwargs, ) async def _read_attributes( # type:ignore[override] self, attribute_ids: list[t.uint16_t], *args, manufacturer: int | t.uint16_t | None = None, **kwargs, ): """Read attributes ZCL foundation command.""" if manufacturer is None and self._has_manuf_attr(attribute_ids): manufacturer = self._manufacturer_id return await super()._read_attributes( attribute_ids, *args, manufacturer=manufacturer, **kwargs ) async def _write_attributes( # type:ignore[override] self, attributes: list[foundation.Attribute], *args, manufacturer: int | t.uint16_t | None = None, **kwargs, ): """Write attribute ZCL foundation command.""" if manufacturer is None and self._has_manuf_attr( [a.attrid for a in attributes] ): manufacturer = self._manufacturer_id return await super()._write_attributes( attributes, *args, manufacturer=manufacturer, **kwargs ) async def _write_attributes_undivided( # type:ignore[override] self, attributes: list[foundation.Attribute], *args, manufacturer: int | t.uint16_t | None = None, **kwargs, ): """Write attribute undivided ZCL foundation command.""" if manufacturer is None and self._has_manuf_attr( [a.attrid for a in attributes] ): manufacturer = self._manufacturer_id return await super()._write_attributes_undivided( attributes, *args, manufacturer=manufacturer, **kwargs ) def get(self, key: int | str, default: typing.Any | None = None) -> typing.Any: """Get cached attribute.""" try: attr_def = self.find_attribute(key) except KeyError: return super().get(key, default) # Ensure we check the constant attributes dictionary first, since their values # will not be in the attribute cache but can be read immediately. if ( self._CONSTANT_ATTRIBUTES is not None and attr_def.id in self._CONSTANT_ATTRIBUTES ): return self._CONSTANT_ATTRIBUTES[attr_def.id] return super().get(key, default) async def apply_custom_configuration(self, *args, **kwargs): """Hook for applications to instruct instances to apply custom configuration.""" FilterType = typing.Callable[ [zigpy.device.Device], bool, ] def signature_matches( signature: dict[str, typing.Any], ) -> FilterType: """Return True if device matches signature.""" def _match(a: dict | typing.Iterable, b: dict | typing.Iterable) -> bool: return set(a) == set(b) def _filter(device: zigpy.device.Device) -> bool: """Return True if device matches signature.""" if device.model != signature.get(SIG_MODEL, device.model): _LOGGER.debug("Fail, because device model mismatch: '%s'", device.model) return False if device.manufacturer != signature.get(SIG_MANUFACTURER, device.manufacturer): _LOGGER.debug( "Fail, because device manufacturer mismatch: '%s'", device.manufacturer, ) return False dev_ep = set(device.endpoints) - {0} sig = signature.get(SIG_ENDPOINTS) if sig is None: return False if not _match(sig, dev_ep): _LOGGER.debug( "Fail because endpoint list mismatch: %s %s", set(sig.keys()), dev_ep, ) return False if not all( device[eid].profile_id == sig[eid].get(SIG_EP_PROFILE, device[eid].profile_id) for eid in sig ): _LOGGER.debug("Fail because profile_id mismatch on at least one endpoint") return False if not all( device[eid].device_type == sig[eid].get(SIG_EP_TYPE, device[eid].device_type) for eid in sig ): _LOGGER.debug("Fail because device_type mismatch on at least one endpoint") return False if not all( _match(device[eid].in_clusters, ep.get(SIG_EP_INPUT, [])) for eid, ep in sig.items() ): _LOGGER.debug( "Fail because input cluster mismatch on at least one endpoint" ) return False if not all( _match(device[eid].out_clusters, ep.get(SIG_EP_OUTPUT, [])) for eid, ep in sig.items() ): _LOGGER.debug( "Fail because output cluster mismatch on at least one endpoint" ) return False _LOGGER.debug( "Device matches filter signature - device ieee[%s]: filter signature[%s]", device.ieee, signature, ) return True return _filter def handle_message_from_uninitialized_sender( sender: zigpy.device.Device, profile: int, cluster: int, src_ep: int, dst_ep: int, message: bytes, ) -> None: """Processes message from an uninitialized sender.""" for handler in _uninitialized_device_message_handlers: if handler(sender, profile, cluster, src_ep, dst_ep, message): break zigpy-0.80.1/zigpy/quirks/registry.py000066400000000000000000000150241501451476000176510ustar00rootroot00000000000000"""Zigpy quirks registry.""" from __future__ import annotations from collections import defaultdict, deque import inspect import itertools import logging import pathlib from typing import TYPE_CHECKING from zigpy.const import SIG_MANUFACTURER, SIG_MODEL, SIG_MODELS_INFO import zigpy.quirks from zigpy.typing import CustomDeviceType, DeviceType from zigpy.util import deprecated if TYPE_CHECKING: from zigpy.quirks import CustomDevice from zigpy.quirks.v2 import QuirksV2RegistryEntry _LOGGER = logging.getLogger(__name__) class DeviceRegistry: """Device registry for Zigpy quirks.""" def __init__(self, *args, **kwargs) -> None: """Initialize the registry.""" self._registry_v1: dict[str | None, dict[str | None, deque[CustomDevice]]] = ( defaultdict(lambda: defaultdict(deque)) ) self._registry_v2: dict[tuple[str, str], deque[QuirksV2RegistryEntry]] = ( defaultdict(deque) ) def purge_custom_quirks(self, custom_quirks_root: pathlib.Path) -> None: # If zhaquirks aren't being used, we can't tell if a quirk is custom or not for model_registry in self._registry_v1.values(): for quirks in model_registry.values(): to_remove = [] for quirk in quirks: module = inspect.getmodule(quirk) assert module is not None # All quirks should have modules quirk_module = pathlib.Path(module.__file__) if quirk_module.is_relative_to(custom_quirks_root): to_remove.append(quirk) for quirk in to_remove: _LOGGER.debug("Removing stale custom v1 quirk: %s", quirk) quirks.remove(quirk) for registry in self._registry_v2.values(): to_remove = [] for entry in registry: if entry.quirk_file.is_relative_to(custom_quirks_root): to_remove.append(entry) for entry in to_remove: _LOGGER.debug("Removing stale custom v2 quirk: %s", entry) registry.remove(entry) def add_to_registry(self, custom_device: CustomDeviceType) -> None: """Add a device to the registry""" models_info = custom_device.signature.get(SIG_MODELS_INFO) if models_info: for manuf, model in models_info: if custom_device not in self.registry_v1[manuf][model]: self.registry_v1[manuf][model].appendleft(custom_device) else: manufacturer = custom_device.signature.get(SIG_MANUFACTURER) model = custom_device.signature.get(SIG_MODEL) if custom_device not in self.registry_v1[manufacturer][model]: self.registry_v1[manufacturer][model].appendleft(custom_device) def add_to_registry_v2( self, manufacturer: str, model: str, entry: QuirksV2RegistryEntry ) -> None: """Add an entry to the registry.""" self._registry_v2[(manufacturer, model)].appendleft(entry) def remove(self, custom_device: CustomDeviceType) -> None: """Remove a device from the registry""" if hasattr(custom_device, "quirk_metadata"): key = (custom_device.manufacturer, custom_device.model) self._registry_v2[key].remove(custom_device.quirk_metadata) return models_info = custom_device.signature.get(SIG_MODELS_INFO) if models_info: for manuf, model in models_info: self.registry_v1[manuf][model].remove(custom_device) else: manufacturer = custom_device.signature.get(SIG_MANUFACTURER) model = custom_device.signature.get(SIG_MODEL) self.registry_v1[manufacturer][model].remove(custom_device) def get_device(self, device: DeviceType) -> CustomDeviceType | DeviceType: """Get a CustomDevice object, if one is available""" if isinstance(device, zigpy.quirks.BaseCustomDevice): return device _LOGGER.debug( "Checking quirks for %s %s (%s)", device.manufacturer, device.model, device.ieee, ) # Try v2 quirks first key = (device.manufacturer, device.model) if key in self._registry_v2: for entry in self._registry_v2[key]: if entry.matches_device(device): return entry.create_device(device) # Then, fall back to v1 quirks for candidate in itertools.chain( self.registry_v1[device.manufacturer][device.model], self.registry_v1[device.manufacturer][None], self.registry_v1[None][device.model], self.registry_v1[None][None], ): matcher = zigpy.quirks.signature_matches(candidate.signature) _LOGGER.debug("Considering %s", candidate) if not matcher(device): continue _LOGGER.debug( "Found custom device replacement for %s: %s", device.ieee, candidate ) return candidate(device._application, device.ieee, device.nwk, device) # If none match, return the original device return device @property @deprecated("The `registry` property is deprecated, use `registry_v1` instead.") def registry(self) -> dict[str | None, dict[str | None, deque[CustomDevice]]]: """Return the v1 registry.""" return self._registry_v1 @property def registry_v1(self) -> dict[str | None, dict[str | None, deque[CustomDevice]]]: """Return the v1 registry.""" return self._registry_v1 @property def registry_v2(self) -> dict[tuple[str, str], deque[QuirksV2RegistryEntry]]: """Return the v2 registry.""" return self._registry_v2 def __contains__(self, device: CustomDeviceType) -> bool: """Check if a device is in the registry.""" if hasattr(device, "quirk_metadata"): manufacturer, model = device.manufacturer, device.model return device.quirk_metadata in self._registry_v2[(manufacturer, model)] manufacturer, model = device.signature.get( SIG_MODELS_INFO, [ ( device.signature.get(SIG_MANUFACTURER), device.signature.get(SIG_MODEL), ) ], )[0] return device in itertools.chain( self.registry_v1[manufacturer][model], self.registry_v1[manufacturer][None], self.registry_v1[None][None], ) zigpy-0.80.1/zigpy/quirks/v2/000077500000000000000000000000001501451476000157545ustar00rootroot00000000000000zigpy-0.80.1/zigpy/quirks/v2/__init__.py000066400000000000000000001306611501451476000200740ustar00rootroot00000000000000"""Quirks v2 module.""" from __future__ import annotations import collections from copy import deepcopy import dataclasses from enum import Enum import inspect import logging import pathlib from types import FrameType from typing import TYPE_CHECKING, Any, Callable import attrs from frozendict import deepfreeze, frozendict from zigpy.const import ( SIG_ENDPOINTS, SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE, SIG_NODE_DESC, SIG_SKIP_CONFIG, ) import zigpy.profiles.zha from zigpy.quirks import _DEVICE_REGISTRY, BaseCustomDevice, CustomCluster, FilterType from zigpy.quirks.registry import DeviceRegistry from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass import zigpy.types as t from zigpy.zcl import ClusterType from zigpy.zdo import ZDO from zigpy.zdo.types import NodeDescriptor if TYPE_CHECKING: from zigpy.application import ControllerApplication from zigpy.device import Device from zigpy.endpoint import Endpoint from zigpy.zcl import Cluster from zigpy.zcl.foundation import ZCLAttributeDef _LOGGER = logging.getLogger(__name__) UNBUILT_QUIRK_BUILDERS: list[QuirkBuilder] = [] # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments # pylint: disable=too-few-public-methods @dataclasses.dataclass(frozen=True) class ReportingConfig: """Reporting config for an entity attribute.""" min_interval: int max_interval: int reportable_change: int class CustomDeviceV2(BaseCustomDevice): """Implementation of a quirks v2 custom device.""" _copy_cluster_attr_cache = True def __init__( self, application: ControllerApplication, ieee: t.EUI64, nwk: t.NWK, replaces: Device, quirk_metadata: QuirksV2RegistryEntry, ) -> None: self.quirk_metadata: QuirksV2RegistryEntry = quirk_metadata # this is done to simplify extending from CustomDevice self._replacement_from_replaces(replaces) super().__init__(application, ieee, nwk, replaces) # we no longer need this after calling super().__init__ self.replacement = {} self._exposes_metadata: dict[ # (endpoint_id, cluster_id, cluster_type) tuple[int, int, ClusterType], list[EntityMetadata], ] = collections.defaultdict(list) # endpoints need to be modified before clusters for remove_endpoint_meta in quirk_metadata.removes_endpoint_metadata: remove_endpoint_meta(self) for add_endpoint_meta in quirk_metadata.adds_endpoint_metadata: add_endpoint_meta(self) for replace_endpoint_meta in quirk_metadata.replaces_endpoint_metadata: replace_endpoint_meta(self) for remove_meta in quirk_metadata.removes_metadata: remove_meta(self) for add_meta in quirk_metadata.adds_metadata: add_meta(self) for replace_meta in quirk_metadata.replaces_metadata: replace_meta(self) for ( replace_occurrences_meta ) in quirk_metadata.replaces_cluster_occurrences_metadata: replace_occurrences_meta(self) for entity_meta in quirk_metadata.entity_metadata: entity_meta(self) if quirk_metadata.device_automation_triggers_metadata: self.device_automation_triggers = ( quirk_metadata.device_automation_triggers_metadata ) def _replacement_from_replaces(self, replaces: Device) -> None: """Set replacement data from replaces device.""" self.replacement = { SIG_ENDPOINTS: { key: { SIG_EP_PROFILE: endpoint.profile_id, SIG_EP_TYPE: endpoint.device_type, SIG_EP_INPUT: [ cluster.cluster_id for cluster in endpoint.in_clusters.values() ], SIG_EP_OUTPUT: [ cluster.cluster_id for cluster in endpoint.out_clusters.values() ], } for key, endpoint in replaces.endpoints.items() if not isinstance(endpoint, ZDO) } } self.replacement[SIG_SKIP_CONFIG] = ( self.quirk_metadata.skip_device_configuration ) if self.quirk_metadata.device_node_descriptor: self.replacement[SIG_NODE_DESC] = self.quirk_metadata.device_node_descriptor @property def exposes_metadata( self, ) -> dict[ tuple[int, int, ClusterType], list[EntityMetadata], ]: """Return EntityMetadata for exposed entities. The key is a tuple of (endpoint_id, cluster_id, cluster_type). The value is a list of EntityMetadata instances. """ return self._exposes_metadata @attrs.define(frozen=True, kw_only=True, repr=True) class AddsMetadata: """Adds metadata for adding a cluster to a device.""" cluster: int | type[Cluster | CustomCluster] = attrs.field() endpoint_id: int = attrs.field(default=1) cluster_type: ClusterType = attrs.field(default=ClusterType.Server) constant_attributes: frozendict[ZCLAttributeDef, Any] = attrs.field( factory=frozendict, converter=deepfreeze ) def __call__(self, device: CustomDeviceV2) -> None: """Process the add.""" endpoint: Endpoint = device.endpoints[self.endpoint_id] if is_server_cluster := self.cluster_type == ClusterType.Server: add_cluster = endpoint.add_input_cluster else: add_cluster = endpoint.add_output_cluster if isinstance(self.cluster, int): cluster = None cluster_id = self.cluster else: cluster = self.cluster(endpoint, is_server=is_server_cluster) cluster_id = cluster.cluster_id cluster = add_cluster(cluster_id, cluster) if self.constant_attributes: cluster._CONSTANT_ATTRIBUTES = { attribute.id: value for attribute, value in self.constant_attributes.items() } @attrs.define(frozen=True, kw_only=True, repr=True) class RemovesMetadata: """Removes metadata for removing a cluster from a device.""" cluster_id: int = attrs.field() endpoint_id: int = attrs.field(default=1) cluster_type: ClusterType = attrs.field(default=ClusterType.Server) def __call__(self, device: CustomDeviceV2) -> None: """Process the remove.""" endpoint = device.endpoints[self.endpoint_id] if self.cluster_type == ClusterType.Server: endpoint.in_clusters.pop(self.cluster_id, None) else: endpoint.out_clusters.pop(self.cluster_id, None) @attrs.define(frozen=True, kw_only=True, repr=True) class ReplacesMetadata: """Replaces metadata for replacing a cluster on a device.""" remove: RemovesMetadata = attrs.field() add: AddsMetadata = attrs.field() def __call__(self, device: CustomDeviceV2) -> None: """Process the replace.""" self.remove(device) self.add(device) @attrs.define(frozen=True, kw_only=True, repr=True) class ReplaceClusterOccurrencesMetadata: """Replaces metadata for replacing all occurrences of a cluster on a device.""" cluster_types: tuple[ClusterType] = attrs.field() cluster: type[Cluster | CustomCluster] = attrs.field() def __call__(self, device: CustomDeviceV2) -> None: """Process the replace.""" for endpoint in device.endpoints.values(): if isinstance(endpoint, ZDO): continue if ( ClusterType.Server in self.cluster_types and self.cluster.cluster_id in endpoint.in_clusters ): endpoint.in_clusters.pop(self.cluster.cluster_id) endpoint.add_input_cluster( self.cluster.cluster_id, self.cluster(endpoint) ) if ( ClusterType.Client in self.cluster_types and self.cluster.cluster_id in endpoint.out_clusters ): endpoint.out_clusters.pop(self.cluster.cluster_id) endpoint.add_output_cluster( self.cluster.cluster_id, self.cluster(endpoint, is_server=False) ) @attrs.define(frozen=True, kw_only=True, repr=True) class AddsEndpointMetadata: """Adds metadata for adding an endpoint to a device.""" endpoint_id: int = attrs.field() profile_id: int = attrs.field() device_type: int = attrs.field() def __call__(self, device: CustomDeviceV2) -> None: """Process the add.""" if self.endpoint_id not in device.endpoints: ep = device.add_endpoint(self.endpoint_id) ep.profile_id = self.profile_id ep.device_type = self.device_type @attrs.define(frozen=True, kw_only=True, repr=True) class RemovesEndpointMetadata: """Removes metadata for removing an endpoint from a device.""" endpoint_id: int = attrs.field() def __call__(self, device: CustomDeviceV2) -> None: """Process the remove.""" device.endpoints.pop(self.endpoint_id, None) @attrs.define(frozen=True, kw_only=True, repr=True) class ReplacesEndpointMetadata: """Replaces metadata for replacing an endpoint on a device.""" endpoint_id: int = attrs.field() profile_id: int = attrs.field() device_type: int = attrs.field() def __call__(self, device: CustomDeviceV2) -> None: """Process the replace.""" if self.endpoint_id in device.endpoints: ep: Endpoint = device.endpoints[self.endpoint_id] else: ep = device.add_endpoint(self.endpoint_id) ep.profile_id = self.profile_id ep.device_type = self.device_type @attrs.define(frozen=True, kw_only=True, repr=True) class EntityMetadata: """Metadata for an exposed entity.""" entity_platform: EntityPlatform = attrs.field() entity_type: EntityType = attrs.field() cluster_id: int = attrs.field() endpoint_id: int = attrs.field(default=1) cluster_type: ClusterType = attrs.field(default=ClusterType.Server) initially_disabled: bool = attrs.field(default=False) attribute_initialized_from_cache: bool = attrs.field(default=True) unique_id_suffix: str | None = attrs.field(default=None) translation_key: str | None = attrs.field(default=None) fallback_name: str = attrs.field(validator=attrs.validators.instance_of(str)) primary: bool | None = attrs.field(default=None) def __attrs_post_init__(self) -> None: """Validate the entity metadata.""" self._validate() def __call__(self, device: CustomDeviceV2) -> None: """Add the entity metadata to the quirks v2 device.""" self._validate() device.exposes_metadata[ (self.endpoint_id, self.cluster_id, self.cluster_type) ].append(self) def _validate(self) -> None: """Validate the entity metadata.""" has_device_class: bool = getattr(self, "device_class", None) is not None if self.translation_key is None and not has_device_class: raise ValueError( f"EntityMetadata must have a translation_key or device_class: {self}" ) @attrs.define(frozen=True, kw_only=True, repr=True) class ZCLEnumMetadata(EntityMetadata): """Metadata for exposed ZCL enum based entity.""" enum: type[Enum] = attrs.field() attribute_name: str = attrs.field() reporting_config: ReportingConfig | None = attrs.field(default=None) @attrs.define(frozen=True, kw_only=True, repr=True) class ZCLSensorMetadata(EntityMetadata): """Metadata for exposed ZCL attribute based sensor entity.""" attribute_name: str | None = attrs.field(default=None) attribute_converter: Callable[[Any], Any] | None = attrs.field(default=None) reporting_config: ReportingConfig | None = attrs.field(default=None) divisor: int | None = attrs.field(default=None) multiplier: int | None = attrs.field(default=None) suggested_display_precision: int | None = attrs.field(default=None) unit: str | None = attrs.field(default=None) device_class: SensorDeviceClass | None = attrs.field(default=None) state_class: SensorStateClass | None = attrs.field(default=None) @attrs.define(frozen=True, kw_only=True, repr=True) class SwitchMetadata(EntityMetadata): """Metadata for exposed switch entity.""" attribute_name: str = attrs.field() reporting_config: ReportingConfig | None = attrs.field(default=None) force_inverted: bool = attrs.field(default=False) invert_attribute_name: str | None = attrs.field(default=None) off_value: int = attrs.field(default=0) on_value: int = attrs.field(default=1) @attrs.define(frozen=True, kw_only=True, repr=True) class NumberMetadata(EntityMetadata): """Metadata for exposed number entity.""" attribute_name: str = attrs.field() reporting_config: ReportingConfig | None = attrs.field(default=None) min: float | None = attrs.field(default=None) max: float | None = attrs.field(default=None) step: float | None = attrs.field(default=None) unit: str | None = attrs.field(default=None) mode: str | None = attrs.field(default=None) multiplier: float | None = attrs.field(default=None) device_class: NumberDeviceClass | None = attrs.field(default=None) @attrs.define(frozen=True, kw_only=True, repr=True) class BinarySensorMetadata(EntityMetadata): """Metadata for exposed binary sensor entity.""" attribute_name: str = attrs.field() attribute_converter: Callable[[Any], Any] | None = attrs.field(default=None) reporting_config: ReportingConfig | None = attrs.field(default=None) device_class: BinarySensorDeviceClass | None = attrs.field(default=None) @attrs.define(frozen=True, kw_only=True, repr=True) class WriteAttributeButtonMetadata(EntityMetadata): """Metadata for exposed button entity that writes an attribute when pressed.""" attribute_name: str = attrs.field() attribute_value: int = attrs.field() @attrs.define(frozen=True, kw_only=True, repr=True) class ZCLCommandButtonMetadata(EntityMetadata): """Metadata for exposed button entity that executes a ZCL command when pressed.""" command_name: str = attrs.field() args: tuple = attrs.field(default=tuple) kwargs: frozendict[str, Any] = attrs.field(default=frozendict, converter=frozendict) @attrs.define(frozen=True, kw_only=True, repr=True) class ManufacturerModelMetadata: """Metadata for manufacturers and models to apply this quirk to.""" manufacturer: str = attrs.field(default=None) model: str = attrs.field(default=None) @attrs.define(frozen=True, kw_only=True, repr=True) class FriendlyNameMetadata: """Metadata to rename a device.""" model: str = attrs.field() manufacturer: str = attrs.field() class DeviceAlertLevel(Enum): """Device alert level.""" INFO = "info" WARNING = "warning" ERROR = "error" @attrs.define(frozen=True, kw_only=True, repr=True) class DeviceAlertMetadata: """Metadata for device-specific alerts.""" level: DeviceAlertLevel = attrs.field(converter=DeviceAlertLevel) message: str = attrs.field() @attrs.define(frozen=True, kw_only=True, repr=True) class PreventDefaultEntityCreationMetadata: """Metadata to prevent the default creation of an entity.""" endpoint_id: int | None = attrs.field() cluster_id: int | None = attrs.field() cluster_type: ClusterType | None = attrs.field() unique_id_suffix: str | None = attrs.field() function: Callable[[Any], bool] | None = attrs.field() @attrs.define(frozen=True, kw_only=True, repr=True) class QuirksV2RegistryEntry: """Quirks V2 registry entry.""" quirk_file: str = attrs.field(default=None, eq=False) quirk_file_line: int = attrs.field(default=None, eq=False) manufacturer_model_metadata: tuple[ManufacturerModelMetadata] = attrs.field( factory=tuple ) friendly_name: FriendlyNameMetadata | None = attrs.field(default=None) device_alerts: tuple[DeviceAlertMetadata] = attrs.field(factory=tuple) disabled_default_entities: tuple[PreventDefaultEntityCreationMetadata] = ( attrs.field(factory=tuple) ) filters: tuple[FilterType] = attrs.field(factory=tuple) custom_device_class: type[CustomDeviceV2] | None = attrs.field(default=None) device_node_descriptor: NodeDescriptor | None = attrs.field(default=None) skip_device_configuration: bool = attrs.field(default=False) adds_metadata: tuple[AddsMetadata] = attrs.field(factory=tuple) removes_metadata: tuple[RemovesMetadata] = attrs.field(factory=tuple) replaces_metadata: tuple[ReplacesMetadata] = attrs.field(factory=tuple) replaces_cluster_occurrences_metadata: tuple[ReplaceClusterOccurrencesMetadata] = ( attrs.field(factory=tuple) ) adds_endpoint_metadata: tuple[AddsEndpointMetadata] = attrs.field(factory=tuple) removes_endpoint_metadata: tuple[RemovesEndpointMetadata] = attrs.field( factory=tuple ) replaces_endpoint_metadata: tuple[ReplacesEndpointMetadata] = attrs.field( factory=tuple ) entity_metadata: tuple[ ZCLEnumMetadata | SwitchMetadata | NumberMetadata | BinarySensorMetadata | WriteAttributeButtonMetadata | ZCLCommandButtonMetadata ] = attrs.field(factory=tuple) device_automation_triggers_metadata: frozendict[ tuple[str, str], frozendict[str, str] ] = attrs.field(factory=frozendict, converter=deepfreeze) def matches_device(self, device: Device) -> bool: """Determine if this quirk should be applied to the passed in device.""" return all(_filter(device) for _filter in self.filters) def create_device(self, device: Device) -> CustomDeviceV2: """Create the quirked device.""" if self.custom_device_class: return self.custom_device_class( device.application, device.ieee, device.nwk, device, self ) return CustomDeviceV2(device.application, device.ieee, device.nwk, device, self) class QuirkBuilder: """Quirks V2 registry entry.""" def __init__( self, manufacturer: str | None = None, model: str | None = None, registry: DeviceRegistry = _DEVICE_REGISTRY, ) -> None: """Initialize the quirk builder.""" if manufacturer and not model or model and not manufacturer: raise ValueError( "manufacturer and model must be provided together or completely omitted." ) self.registry: DeviceRegistry = registry self.manufacturer_model_metadata: list[ManufacturerModelMetadata] = [] self.friendly_name_metadata: FriendlyNameMetadata | None = None self.device_alerts: list[DeviceAlertMetadata] = [] self.disabled_default_entities: list[PreventDefaultEntityCreationMetadata] = [] self.filters: list[FilterType] = [] self.custom_device_class: type[CustomDeviceV2] | None = None self.device_node_descriptor: NodeDescriptor | None = None self.skip_device_configuration: bool = False self.adds_metadata: list[AddsMetadata] = [] self.removes_metadata: list[RemovesMetadata] = [] self.replaces_metadata: list[ReplacesMetadata] = [] self.replaces_cluster_occurrences_metadata: list[ ReplaceClusterOccurrencesMetadata ] = [] self.adds_endpoint_metadata: list[AddsEndpointMetadata] = [] self.removes_endpoint_metadata: list[RemovesEndpointMetadata] = [] self.replaces_endpoint_metadata: list[ReplacesEndpointMetadata] = [] self.entity_metadata: list[ ZCLEnumMetadata | ZCLSensorMetadata | SwitchMetadata | NumberMetadata | BinarySensorMetadata | WriteAttributeButtonMetadata | ZCLCommandButtonMetadata ] = [] self.device_automation_triggers_metadata: dict[ tuple[str, str], dict[str, str] ] = {} current_frame: FrameType = inspect.currentframe() caller: FrameType = current_frame.f_back self.quirk_file = pathlib.Path(caller.f_code.co_filename) self.quirk_file_line = caller.f_lineno if manufacturer and model: self.applies_to(manufacturer, model) UNBUILT_QUIRK_BUILDERS.append(self) def _add_entity_metadata(self, entity_metadata: EntityMetadata) -> QuirkBuilder: """Register new entity metadata and validate config.""" if entity_metadata.primary and any( entity.primary for entity in self.entity_metadata ): raise ValueError("Only one primary entity can be defined per device") self.entity_metadata.append(entity_metadata) return self def applies_to(self, manufacturer: str, model: str) -> QuirkBuilder: """Register this quirks v2 entry for the specified manufacturer and model.""" self.manufacturer_model_metadata.append( ManufacturerModelMetadata(manufacturer=manufacturer, model=model) ) return self # backward compatibility also_applies_to = applies_to def filter(self, filter_function: FilterType) -> QuirkBuilder: """Add a filter and returns self. The filter function should take a single argument, a zigpy.device.Device instance, and return a boolean if the condition the filter is testing passes. Ex: def some_filter(device: zigpy.device.Device) -> bool: """ self.filters.append(filter_function) return self def device_class(self, custom_device_class: type[CustomDeviceV2]) -> QuirkBuilder: """Set the custom device class to be used in this quirk and returns self. The custom device class must be a subclass of CustomDeviceV2. """ assert issubclass( custom_device_class, CustomDeviceV2 ), f"{custom_device_class} is not a subclass of CustomDeviceV2" self.custom_device_class = custom_device_class return self def node_descriptor(self, node_descriptor: NodeDescriptor) -> QuirkBuilder: """Set the node descriptor and returns self. The node descriptor must be a NodeDescriptor instance and it will be used to replace the node descriptor of the device when the quirk is applied. """ self.device_node_descriptor = node_descriptor.freeze() return self def skip_configuration(self, skip_configuration: bool = True) -> QuirkBuilder: """Set the skip_configuration and returns self. If skip_configuration is True, reporting configuration will not be applied to any cluster on this device. """ self.skip_device_configuration = skip_configuration return self def adds( self, cluster: int | type[Cluster | CustomCluster], cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, constant_attributes: dict[ZCLAttributeDef, Any] | None = None, ) -> QuirkBuilder: """Add an AddsMetadata entry and returns self. This method allows adding a cluster to a device when the quirk is applied. If cluster is an int, it will be used as the cluster_id. If cluster is a subclass of Cluster or CustomCluster, it will be used to create a new cluster instance. If constant_attributes is provided, it should be a dictionary of ZCLAttributeDef instances and their values. These attributes will be added to the cluster when the quirk is applied and the values will be constant. """ add = AddsMetadata( endpoint_id=endpoint_id, cluster=cluster, cluster_type=cluster_type, constant_attributes=constant_attributes or {}, ) self.adds_metadata.append(add) return self def removes( self, cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, ) -> QuirkBuilder: """Add a RemovesMetadata entry and returns self. This method allows removing a cluster from a device when the quirk is applied. """ remove = RemovesMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, ) self.removes_metadata.append(remove) return self def replaces( self, replacement_cluster_class: type[Cluster | CustomCluster], cluster_id: int | None = None, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, ) -> QuirkBuilder: """Add a ReplacesMetadata entry and returns self. This method allows replacing a cluster on a device when the quirk is applied. replacement_cluster_class should be a subclass of Cluster or CustomCluster and will be used to create a new cluster instance to replace the existing cluster. If cluster_id is provided, it will be used as the cluster_id for the cluster to be removed. If cluster_id is not provided, the cluster_id of the replacement cluster will be used. """ remove = RemovesMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id if cluster_id is not None else replacement_cluster_class.cluster_id, cluster_type=cluster_type, ) add = AddsMetadata( endpoint_id=endpoint_id, cluster=replacement_cluster_class, cluster_type=cluster_type, ) replace = ReplacesMetadata(remove=remove, add=add) self.replaces_metadata.append(replace) return self def replace_cluster_occurrences( self, replacement_cluster_class: type[Cluster | CustomCluster], replace_server_instances: bool = True, replace_client_instances: bool = True, ) -> QuirkBuilder: """Add a ReplaceClusterOccurrencesMetadata entry and returns self. This method allows replacing a cluster on a device across all endpoints for the specified cluster types when the quirk is applied. replacement_cluster_class should be a subclass of Cluster or CustomCluster and will be used to create a new cluster instance to replace the existing cluster. replace_server_instances and replace_client_instances control the cluster types that will be replaced. If replace_server_instances is True, all server instances of the cluster will be replaced. If replace_client_instances is True, all client instances of the cluster will be replaced. """ types = [] if replace_server_instances: types.append(ClusterType.Server) if replace_client_instances: types.append(ClusterType.Client) self.replaces_cluster_occurrences_metadata.append( ReplaceClusterOccurrencesMetadata( cluster_types=tuple(types), cluster=replacement_cluster_class, ) ) return self def adds_endpoint( self, endpoint_id: int, profile_id: int = zigpy.profiles.zha.PROFILE_ID, device_type: int = 0xFF, ) -> QuirkBuilder: """Add an AddsEndpointMetadata entry and return self.""" add = AddsEndpointMetadata( endpoint_id=endpoint_id, profile_id=profile_id, device_type=device_type ) self.adds_endpoint_metadata.append(add) return self def removes_endpoint(self, endpoint_id: int) -> QuirkBuilder: """Add a RemovesEndpointMetadata entry and return self.""" remove = RemovesEndpointMetadata(endpoint_id=endpoint_id) self.removes_endpoint_metadata.append(remove) return self def replaces_endpoint( self, endpoint_id: int, profile_id: int = zigpy.profiles.zha.PROFILE_ID, device_type: int = 0xFF, ) -> QuirkBuilder: """Add a ReplacesEndpointMetadata entry and return self.""" replace = ReplacesEndpointMetadata( endpoint_id=endpoint_id, profile_id=profile_id, device_type=device_type ) self.replaces_endpoint_metadata.append(replace) return self def enum( self, attribute_name: str, enum_class: type[Enum], cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, entity_platform: EntityPlatform = EntityPlatform.SELECT, entity_type: EntityType = EntityType.CONFIG, initially_disabled: bool = False, attribute_initialized_from_cache: bool = True, reporting_config: ReportingConfig | None = None, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing ZCLEnumMetadata and return self. This method allows exposing an enum based entity in Home Assistant. """ self._add_entity_metadata( ZCLEnumMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=entity_platform, entity_type=entity_type, initially_disabled=initially_disabled, attribute_initialized_from_cache=attribute_initialized_from_cache, reporting_config=reporting_config, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, enum=enum_class, attribute_name=attribute_name, primary=primary, ) ) return self def sensor( self, attribute_name: str, cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, divisor: int = 1, multiplier: int = 1, suggested_display_precision: int = 1, entity_type: EntityType = EntityType.STANDARD, device_class: SensorDeviceClass | None = None, state_class: SensorStateClass | None = None, unit: str | None = None, initially_disabled: bool = False, attribute_initialized_from_cache: bool = True, attribute_converter: Callable[[Any], Any] | None = None, reporting_config: ReportingConfig | None = None, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing ZCLSensorMetadata and return self. This method allows exposing a sensor entity in Home Assistant. """ self._add_entity_metadata( ZCLSensorMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=EntityPlatform.SENSOR, entity_type=entity_type, initially_disabled=initially_disabled, attribute_initialized_from_cache=attribute_initialized_from_cache, reporting_config=reporting_config, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, attribute_name=attribute_name, attribute_converter=attribute_converter, divisor=divisor, multiplier=multiplier, suggested_display_precision=suggested_display_precision, unit=unit, device_class=device_class, state_class=state_class, primary=primary, ) ) return self def switch( self, attribute_name: str, cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, force_inverted: bool = False, invert_attribute_name: str | None = None, off_value: int = 0, on_value: int = 1, entity_platform=EntityPlatform.SWITCH, entity_type: EntityType = EntityType.CONFIG, initially_disabled: bool = False, attribute_initialized_from_cache: bool = True, reporting_config: ReportingConfig | None = None, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing SwitchMetadata and return self. This method allows exposing a switch entity in Home Assistant. """ self._add_entity_metadata( SwitchMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=entity_platform, entity_type=entity_type, initially_disabled=initially_disabled, attribute_initialized_from_cache=attribute_initialized_from_cache, reporting_config=reporting_config, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, attribute_name=attribute_name, force_inverted=force_inverted, invert_attribute_name=invert_attribute_name, off_value=off_value, on_value=on_value, primary=primary, ) ) return self def number( self, attribute_name: str, cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, min_value: float | None = None, max_value: float | None = None, step: float | None = None, unit: str | None = None, mode: str | None = None, multiplier: float | None = None, entity_type: EntityType = EntityType.CONFIG, device_class: NumberDeviceClass | None = None, initially_disabled: bool = False, attribute_initialized_from_cache: bool = True, reporting_config: ReportingConfig | None = None, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing NumberMetadata and return self. This method allows exposing a number entity in Home Assistant. """ self._add_entity_metadata( NumberMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=EntityPlatform.NUMBER, entity_type=entity_type, initially_disabled=initially_disabled, attribute_initialized_from_cache=attribute_initialized_from_cache, reporting_config=reporting_config, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, attribute_name=attribute_name, min=min_value, max=max_value, step=step, unit=unit, mode=mode, multiplier=multiplier, device_class=device_class, primary=primary, ) ) return self def binary_sensor( self, attribute_name: str, cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, entity_type: EntityType = EntityType.DIAGNOSTIC, device_class: BinarySensorDeviceClass | None = None, initially_disabled: bool = False, attribute_initialized_from_cache: bool = True, attribute_converter: Callable[[Any], Any] | None = None, reporting_config: ReportingConfig | None = None, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing BinarySensorMetadata and return self. This method allows exposing a binary sensor entity in Home Assistant. """ self._add_entity_metadata( BinarySensorMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=EntityPlatform.BINARY_SENSOR, entity_type=entity_type, initially_disabled=initially_disabled, attribute_initialized_from_cache=attribute_initialized_from_cache, reporting_config=reporting_config, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, attribute_name=attribute_name, attribute_converter=attribute_converter, device_class=device_class, primary=primary, ) ) return self def write_attr_button( self, attribute_name: str, attribute_value: int, cluster_id: int, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, entity_type: EntityType = EntityType.CONFIG, initially_disabled: bool = False, attribute_initialized_from_cache: bool = True, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing WriteAttributeButtonMetadata and return self. This method allows exposing a button entity in Home Assistant that writes a value to an attribute when pressed. """ self._add_entity_metadata( WriteAttributeButtonMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=EntityPlatform.BUTTON, entity_type=entity_type, initially_disabled=initially_disabled, attribute_initialized_from_cache=attribute_initialized_from_cache, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, attribute_name=attribute_name, attribute_value=attribute_value, primary=primary, ) ) return self def command_button( self, command_name: str, cluster_id: int, command_args: tuple | None = None, command_kwargs: dict[str, Any] | None = None, cluster_type: ClusterType = ClusterType.Server, endpoint_id: int = 1, entity_type: EntityType = EntityType.CONFIG, initially_disabled: bool = False, unique_id_suffix: str | None = None, translation_key: str | None = None, fallback_name: str | None = None, primary: bool | None = None, ) -> QuirkBuilder: """Add an EntityMetadata containing ZCLCommandButtonMetadata and return self. This method allows exposing a button entity in Home Assistant that executes a ZCL command when pressed. """ self._add_entity_metadata( ZCLCommandButtonMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, entity_platform=EntityPlatform.BUTTON, entity_type=entity_type, initially_disabled=initially_disabled, unique_id_suffix=unique_id_suffix, translation_key=translation_key, fallback_name=fallback_name, command_name=command_name, args=command_args if command_args is not None else (), kwargs=command_kwargs if command_kwargs is not None else frozendict(), primary=primary, ) ) return self def device_automation_triggers( self, device_automation_triggers: dict[tuple[str, str], dict[str, str]] ) -> QuirkBuilder: """Add device automation triggers and returns self.""" self.device_automation_triggers_metadata.update(device_automation_triggers) return self def friendly_name(self, *, model: str, manufacturer: str) -> QuirkBuilder: """Renames the device.""" self.friendly_name_metadata = FriendlyNameMetadata( model=model, manufacturer=manufacturer ) return self def device_alert(self, *, level: DeviceAlertLevel, message: str) -> QuirkBuilder: """Adds a device alert.""" self.device_alerts.append(DeviceAlertMetadata(level=level, message=message)) return self def prevent_default_entity_creation( self, *, endpoint_id: int | None = None, cluster_id: int | None = None, cluster_type: ClusterType | None = None, unique_id_suffix: str | None = None, function: Callable[[Any], bool] | None = None, ) -> QuirkBuilder: """Do not create default entities.""" if cluster_id is not None and cluster_type is None: cluster_type = ClusterType.Server self.disabled_default_entities.append( PreventDefaultEntityCreationMetadata( endpoint_id=endpoint_id, cluster_id=cluster_id, cluster_type=cluster_type, unique_id_suffix=unique_id_suffix, function=function, ), ) return self def add_to_registry(self) -> QuirksV2RegistryEntry: """Build the quirks v2 registry entry.""" if not self.manufacturer_model_metadata: raise ValueError( "At least one manufacturer and model must be specified for a v2 quirk." ) quirk: QuirksV2RegistryEntry = QuirksV2RegistryEntry( manufacturer_model_metadata=tuple(self.manufacturer_model_metadata), friendly_name=self.friendly_name_metadata, device_alerts=tuple(self.device_alerts), disabled_default_entities=tuple(self.disabled_default_entities), quirk_file=self.quirk_file, quirk_file_line=self.quirk_file_line, filters=tuple(self.filters), custom_device_class=self.custom_device_class, device_node_descriptor=self.device_node_descriptor, skip_device_configuration=self.skip_device_configuration, adds_metadata=tuple(self.adds_metadata), removes_metadata=tuple(self.removes_metadata), replaces_metadata=tuple(self.replaces_metadata), replaces_cluster_occurrences_metadata=tuple( self.replaces_cluster_occurrences_metadata ), adds_endpoint_metadata=tuple(self.adds_endpoint_metadata), removes_endpoint_metadata=tuple(self.removes_endpoint_metadata), replaces_endpoint_metadata=tuple(self.replaces_endpoint_metadata), entity_metadata=tuple(self.entity_metadata), device_automation_triggers_metadata=self.device_automation_triggers_metadata, ) for manufacturer_model in self.manufacturer_model_metadata: self.registry.add_to_registry_v2( manufacturer_model.manufacturer, manufacturer_model.model, quirk ) if self in UNBUILT_QUIRK_BUILDERS: UNBUILT_QUIRK_BUILDERS.remove(self) return quirk def clone(self, omit_man_model_data=True) -> QuirkBuilder: """Clone this QuirkBuilder potentially omitting manufacturer and model data.""" new_builder = deepcopy(self) new_builder.registry = self.registry if omit_man_model_data: new_builder.manufacturer_model_metadata = [] return new_builder def add_to_registry_v2( manufacturer: str, model: str, registry: DeviceRegistry = _DEVICE_REGISTRY ) -> QuirkBuilder: """Add an entry to the registry.""" _LOGGER.error( "add_to_registry_v2 is deprecated and will be removed in a future release. " "Please QuirkBuilder() instead and ensure you call add_to_registry()." ) return QuirkBuilder(manufacturer, model, registry=registry) zigpy-0.80.1/zigpy/quirks/v2/homeassistant/000077500000000000000000000000001501451476000206365ustar00rootroot00000000000000zigpy-0.80.1/zigpy/quirks/v2/homeassistant/__init__.py000066400000000000000000000146041501451476000227540ustar00rootroot00000000000000"""Homeassistant specific quirks v2 things.""" from typing import Final from zigpy.backports.enum import StrEnum class EntityType(StrEnum): """Entity type.""" CONFIG = "config" DIAGNOSTIC = "diagnostic" STANDARD = "standard" class EntityPlatform(StrEnum): """Entity platform.""" BINARY_SENSOR = "binary_sensor" BUTTON = "button" NUMBER = "number" SENSOR = "sensor" SELECT = "select" SWITCH = "switch" class UnitOfApparentPower(StrEnum): """Apparent power units.""" VOLT_AMPERE = "VA" # Power units class UnitOfPower(StrEnum): """Power units.""" WATT = "W" KILO_WATT = "kW" BTU_PER_HOUR = "BTU/h" # Reactive power units POWER_VOLT_AMPERE_REACTIVE: Final = "var" # Energy units class UnitOfEnergy(StrEnum): """Energy units.""" GIGA_JOULE = "GJ" KILO_WATT_HOUR = "kWh" MEGA_JOULE = "MJ" MEGA_WATT_HOUR = "MWh" WATT_HOUR = "Wh" # Electric_current units class UnitOfElectricCurrent(StrEnum): """Electric current units.""" MILLIAMPERE = "mA" AMPERE = "A" # Electric_potential units class UnitOfElectricPotential(StrEnum): """Electric potential units.""" MILLIVOLT = "mV" VOLT = "V" # Degree units DEGREE: Final = "°" # Currency units CURRENCY_EURO: Final = "€" CURRENCY_DOLLAR: Final = "$" CURRENCY_CENT: Final = "¢" # Temperature units class UnitOfTemperature(StrEnum): """Temperature units.""" CELSIUS = "°C" FAHRENHEIT = "°F" KELVIN = "K" # Time units class UnitOfTime(StrEnum): """Time units.""" MICROSECONDS = "μs" MILLISECONDS = "ms" SECONDS = "s" MINUTES = "min" HOURS = "h" DAYS = "d" WEEKS = "w" MONTHS = "m" YEARS = "y" # Length units class UnitOfLength(StrEnum): """Length units.""" MILLIMETERS = "mm" CENTIMETERS = "cm" METERS = "m" KILOMETERS = "km" INCHES = "in" FEET = "ft" YARDS = "yd" MILES = "mi" # Frequency units class UnitOfFrequency(StrEnum): """Frequency units.""" HERTZ = "Hz" KILOHERTZ = "kHz" MEGAHERTZ = "MHz" GIGAHERTZ = "GHz" # Pressure units class UnitOfPressure(StrEnum): """Pressure units.""" PA = "Pa" HPA = "hPa" KPA = "kPa" BAR = "bar" CBAR = "cbar" MBAR = "mbar" MMHG = "mmHg" INHG = "inHg" PSI = "psi" # Sound pressure units class UnitOfSoundPressure(StrEnum): """Sound pressure units.""" DECIBEL = "dB" WEIGHTED_DECIBEL_A = "dBA" # Volume units class UnitOfVolume(StrEnum): """Volume units.""" CUBIC_FEET = "ft³" CENTUM_CUBIC_FEET = "CCF" CUBIC_METERS = "m³" LITERS = "L" MILLILITERS = "mL" GALLONS = "gal" """Assumed to be US gallons in conversion utilities. British/Imperial gallons are not yet supported""" FLUID_OUNCES = "fl. oz." """Assumed to be US fluid ounces in conversion utilities. British/Imperial fluid ounces are not yet supported""" # Volume Flow Rate units class UnitOfVolumeFlowRate(StrEnum): """Volume flow rate units.""" CUBIC_METERS_PER_HOUR = "m³/h" CUBIC_FEET_PER_MINUTE = "ft³/min" LITERS_PER_MINUTE = "L/min" GALLONS_PER_MINUTE = "gal/min" # Area units AREA_SQUARE_METERS: Final = "m²" # Mass units class UnitOfMass(StrEnum): """Mass units.""" GRAMS = "g" KILOGRAMS = "kg" MILLIGRAMS = "mg" MICROGRAMS = "µg" OUNCES = "oz" POUNDS = "lb" STONES = "st" # Conductivity units class UnitOfConductivity(StrEnum): """Conductivity units.""" SIEMENS_PER_CM = "S/cm" MICROSIEMENS_PER_CM = "µS/cm" MILLISIEMENS_PER_CM = "mS/cm" # Light units LIGHT_LUX: Final = "lx" # UV Index units UV_INDEX: Final = "UV index" # Percentage units PERCENTAGE: Final = "%" # Rotational speed units REVOLUTIONS_PER_MINUTE: Final = "rpm" # Irradiance units class UnitOfIrradiance(StrEnum): """Irradiance units.""" WATTS_PER_SQUARE_METER = "W/m²" BTUS_PER_HOUR_SQUARE_FOOT = "BTU/(h⋅ft²)" class UnitOfVolumetricFlux(StrEnum): """Volumetric flux, commonly used for precipitation intensity. The derivation of these units is a volume of rain amassing in a container with constant cross section in a given time """ INCHES_PER_DAY = "in/d" """Derived from in³/(in²⋅d)""" INCHES_PER_HOUR = "in/h" """Derived from in³/(in²⋅h)""" MILLIMETERS_PER_DAY = "mm/d" """Derived from mm³/(mm²⋅d)""" MILLIMETERS_PER_HOUR = "mm/h" """Derived from mm³/(mm²⋅h)""" class UnitOfPrecipitationDepth(StrEnum): """Precipitation depth. The derivation of these units is a volume of rain amassing in a container with constant cross section """ INCHES = "in" """Derived from in³/in²""" MILLIMETERS = "mm" """Derived from mm³/mm²""" CENTIMETERS = "cm" """Derived from cm³/cm²""" # Concentration units CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" # Speed units class UnitOfSpeed(StrEnum): """Speed units.""" FEET_PER_SECOND = "ft/s" METERS_PER_SECOND = "m/s" KILOMETERS_PER_HOUR = "km/h" KNOTS = "kn" MILES_PER_HOUR = "mph" # Signal_strength units SIGNAL_STRENGTH_DECIBELS: Final = "dB" SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" # Data units class UnitOfInformation(StrEnum): """Information units.""" BITS = "bit" KILOBITS = "kbit" MEGABITS = "Mbit" GIGABITS = "Gbit" BYTES = "B" KILOBYTES = "kB" MEGABYTES = "MB" GIGABYTES = "GB" TERABYTES = "TB" PETABYTES = "PB" EXABYTES = "EB" ZETTABYTES = "ZB" YOTTABYTES = "YB" KIBIBYTES = "KiB" MEBIBYTES = "MiB" GIBIBYTES = "GiB" TEBIBYTES = "TiB" PEBIBYTES = "PiB" EXBIBYTES = "EiB" ZEBIBYTES = "ZiB" YOBIBYTES = "YiB" # Data_rate units class UnitOfDataRate(StrEnum): """Data rate units.""" BITS_PER_SECOND = "bit/s" KILOBITS_PER_SECOND = "kbit/s" MEGABITS_PER_SECOND = "Mbit/s" GIGABITS_PER_SECOND = "Gbit/s" BYTES_PER_SECOND = "B/s" KILOBYTES_PER_SECOND = "kB/s" MEGABYTES_PER_SECOND = "MB/s" GIGABYTES_PER_SECOND = "GB/s" KIBIBYTES_PER_SECOND = "KiB/s" MEBIBYTES_PER_SECOND = "MiB/s" GIBIBYTES_PER_SECOND = "GiB/s" zigpy-0.80.1/zigpy/quirks/v2/homeassistant/binary_sensor.py000066400000000000000000000042651501451476000240740ustar00rootroot00000000000000"""Homeassistant sensor platform quirks v2 supporting items.""" from enum import Enum class BinarySensorDeviceClass(Enum): """Device class for binary sensors.""" # On means low, Off means normal BATTERY = "battery" # On means charging, Off means not charging BATTERY_CHARGING = "battery_charging" # On means carbon monoxide detected, Off means no carbon monoxide (clear) CO = "carbon_monoxide" # On means cold, Off means normal COLD = "cold" # On means connected, Off means disconnected CONNECTIVITY = "connectivity" # On means open, Off means closed DOOR = "door" # On means open, Off means closed GARAGE_DOOR = "garage_door" # On means gas detected, Off means no gas (clear) GAS = "gas" # On means hot, Off means normal HEAT = "heat" # On means light detected, Off means no light LIGHT = "light" # On means open (unlocked), Off means closed (locked) LOCK = "lock" # On means wet, Off means dry MOISTURE = "moisture" # On means motion detected, Off means no motion (clear) MOTION = "motion" # On means moving, Off means not moving (stopped) MOVING = "moving" # On means occupied, Off means not occupied (clear) OCCUPANCY = "occupancy" # On means open, Off means closed OPENING = "opening" # On means plugged in, Off means unplugged PLUG = "plug" # On means power detected, Off means no power POWER = "power" # On means home, Off means away PRESENCE = "presence" # On means problem detected, Off means no problem (OK) PROBLEM = "problem" # On means running, Off means not running RUNNING = "running" # On means unsafe, Off means safe SAFETY = "safety" # On means smoke detected, Off means no smoke (clear) SMOKE = "smoke" # On means sound detected, Off means no sound (clear) SOUND = "sound" # On means tampering detected, Off means no tampering (clear) TAMPER = "tamper" # On means update available, Off means up-to-date UPDATE = "update" # On means vibration detected, Off means no vibration VIBRATION = "vibration" # On means open, Off means closed WINDOW = "window" zigpy-0.80.1/zigpy/quirks/v2/homeassistant/number.py000066400000000000000000000157311501451476000225070ustar00rootroot00000000000000"""Homeassistant number platform quirks v2 supporting items.""" from enum import Enum class NumberDeviceClass(Enum): """Device class for numbers.""" # NumberDeviceClass should be aligned with SensorDeviceClass ACCELERATION = "acceleration" """Acceleration. Unit of measurement: `G`, `m/s²` """ APPARENT_POWER = "apparent_power" """Apparent power. Unit of measurement: `VA` """ AQI = "aqi" """Air Quality Index. Unit of measurement: `None` """ ATMOSPHERIC_PRESSURE = "atmospheric_pressure" """Atmospheric pressure. Unit of measurement: `UnitOfPressure` units """ BATTERY = "battery" """Percentage of battery that is left. Unit of measurement: `%` """ CO = "carbon_monoxide" """Carbon Monoxide gas concentration. Unit of measurement: `ppm` (parts per million) """ CO2 = "carbon_dioxide" """Carbon Dioxide gas concentration. Unit of measurement: `ppm` (parts per million) """ CURRENT = "current" """Current. Unit of measurement: `A`, `mA` """ DATA_RATE = "data_rate" """Data rate. Unit of measurement: UnitOfDataRate """ DATA_SIZE = "data_size" """Data size. Unit of measurement: UnitOfInformation """ DISTANCE = "distance" """Generic distance. Unit of measurement: `LENGTH_*` units - SI /metric: `mm`, `cm`, `m`, `km` - USCS / imperial: `in`, `ft`, `yd`, `mi` """ DURATION = "duration" """Fixed duration. Unit of measurement: `d`, `h`, `min`, `s`, `ms` """ ENERGY = "energy" """Energy. Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` """ ENERGY_STORAGE = "energy_storage" """Stored energy. Use this device class for sensors measuring stored energy, for example the amount of electric energy currently stored in a battery or the capacity of a battery. Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` """ FREQUENCY = "frequency" """Frequency. Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` """ GAS = "gas" """Gas. Unit of measurement: - SI / metric: `m³` - USCS / imperial: `ft³`, `CCF` """ HUMIDITY = "humidity" """Relative humidity. Unit of measurement: `%` """ ILLUMINANCE = "illuminance" """Illuminance. Unit of measurement: `lx` """ IRRADIANCE = "irradiance" """Irradiance. Unit of measurement: - SI / metric: `W/m²` - USCS / imperial: `BTU/(h⋅ft²)` """ MOISTURE = "moisture" """Moisture. Unit of measurement: `%` """ MONETARY = "monetary" """Amount of money. Unit of measurement: ISO4217 currency code See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes """ NITROGEN_DIOXIDE = "nitrogen_dioxide" """Amount of NO2. Unit of measurement: `µg/m³` """ NITROGEN_MONOXIDE = "nitrogen_monoxide" """Amount of NO. Unit of measurement: `µg/m³` """ NITROUS_OXIDE = "nitrous_oxide" """Amount of N2O. Unit of measurement: `µg/m³` """ OZONE = "ozone" """Amount of O3. Unit of measurement: `µg/m³` """ PH = "ph" """Potential hydrogen (acidity/alkalinity). Unit of measurement: Unitless """ PM1 = "pm1" """Particulate matter <= 1 μm. Unit of measurement: `µg/m³` """ PM10 = "pm10" """Particulate matter <= 10 μm. Unit of measurement: `µg/m³` """ PM25 = "pm25" """Particulate matter <= 2.5 μm. Unit of measurement: `µg/m³` """ POWER_FACTOR = "power_factor" """Power factor. Unit of measurement: `%`, `None` """ POWER = "power" """Power. Unit of measurement: `W`, `kW` """ PRECIPITATION = "precipitation" """Accumulated precipitation. Unit of measurement: UnitOfPrecipitationDepth - SI / metric: `cm`, `mm` - USCS / imperial: `in` """ PRECIPITATION_INTENSITY = "precipitation_intensity" """Precipitation intensity. Unit of measurement: UnitOfVolumetricFlux - SI /metric: `mm/d`, `mm/h` - USCS / imperial: `in/d`, `in/h` """ PRESSURE = "pressure" """Pressure. Unit of measurement: - `mbar`, `cbar`, `bar` - `Pa`, `hPa`, `kPa` - `inHg` - `psi` """ REACTIVE_POWER = "reactive_power" """Reactive power. Unit of measurement: `var` """ SIGNAL_STRENGTH = "signal_strength" """Signal strength. Unit of measurement: `dB`, `dBm` """ SOUND_PRESSURE = "sound_pressure" """Sound pressure. Unit of measurement: `dB`, `dBA` """ SPEED = "speed" """Generic speed. Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - Nautical: `kn` """ SULPHUR_DIOXIDE = "sulphur_dioxide" """Amount of SO2. Unit of measurement: `µg/m³` """ TEMPERATURE = "temperature" """Temperature. Unit of measurement: `°C`, `°F`, `K` """ VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. Unit of measurement: `µg/m³` """ VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" """Ratio of VOC. Unit of measurement: `ppm`, `ppb` """ VOLTAGE = "voltage" """Voltage. Unit of measurement: `V`, `mV` """ VOLUME = "volume" """Generic volume. Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ VOLUME_STORAGE = "volume_storage" """Generic stored volume. Use this device class for sensors measuring stored volume, for example the amount of fuel in a fuel tank. Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ VOLUME_FLOW_RATE = "volume_flow_rate" """Generic flow rate Unit of measurement: UnitOfVolumeFlowRate - SI / metric: `m³/h`, `L/min` - USCS / imperial: `ft³/min`, `gal/min` """ WATER = "water" """Water. Unit of measurement: - SI / metric: `m³`, `L` - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ WEIGHT = "weight" """Generic weight, represents a measurement of an object's mass. Weight is used instead of mass to fit with every day language. Unit of measurement: `MASS_*` units - SI / metric: `µg`, `mg`, `g`, `kg` - USCS / imperial: `oz`, `lb` """ WIND_SPEED = "wind_speed" """Wind speed. Unit of measurement: `SPEED_*` units - SI /metric: `m/s`, `km/h` - USCS / imperial: `ft/s`, `mph` - Nautical: `kn` """ zigpy-0.80.1/zigpy/quirks/v2/homeassistant/sensor.py000066400000000000000000000201071501451476000225210ustar00rootroot00000000000000"""Homeassistant sensor platform quirks v2 supporting items.""" from enum import Enum class SensorDeviceClass(Enum): """Device class for sensors.""" # Non-numerical device classes DATE = "date" """Date. Unit of measurement: `None` ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 """ ENUM = "enum" """Enumeration. Provides a fixed list of options the state of the sensor can be in. Unit of measurement: `None` """ TIMESTAMP = "timestamp" """Timestamp. Unit of measurement: `None` ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 """ # Numerical device classes, these should be aligned with NumberDeviceClass ACCELERATION = "acceleration" """Acceleration. Unit of measurement: `G`, `m/s²` """ APPARENT_POWER = "apparent_power" """Apparent power. Unit of measurement: `VA` """ AQI = "aqi" """Air Quality Index. Unit of measurement: `None` """ ATMOSPHERIC_PRESSURE = "atmospheric_pressure" """Atmospheric pressure. Unit of measurement: `UnitOfPressure` units """ BATTERY = "battery" """Percentage of battery that is left. Unit of measurement: `%` """ CO = "carbon_monoxide" """Carbon Monoxide gas concentration. Unit of measurement: `ppm` (parts per million) """ CO2 = "carbon_dioxide" """Carbon Dioxide gas concentration. Unit of measurement: `ppm` (parts per million) """ CONDUCTIVITY = "conductivity" """Conductivity. Unit of measurement: 'S/cm', 'µS/cm', 'mS/cm' """ CURRENT = "current" """Current. Unit of measurement: `A`, `mA` """ DATA_RATE = "data_rate" """Data rate. Unit of measurement: UnitOfDataRate """ DATA_SIZE = "data_size" """Data size. Unit of measurement: UnitOfInformation """ DISTANCE = "distance" """Generic distance. Unit of measurement: `LENGTH_*` units - SI /metric: `mm`, `cm`, `m`, `km` - USCS / imperial: `in`, `ft`, `yd`, `mi` """ DURATION = "duration" """Fixed duration. Unit of measurement: `d`, `h`, `min`, `s`, `ms` """ ENERGY = "energy" """Energy. Use this device class for sensors measuring energy consumption, for example electric energy consumption. Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` """ ENERGY_STORAGE = "energy_storage" """Stored energy. Use this device class for sensors measuring stored energy, for example the amount of electric energy currently stored in a battery or the capacity of a battery. Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` """ FREQUENCY = "frequency" """Frequency. Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` """ GAS = "gas" """Gas. Unit of measurement: - SI / metric: `m³` - USCS / imperial: `ft³`, `CCF` """ HUMIDITY = "humidity" """Relative humidity. Unit of measurement: `%` """ ILLUMINANCE = "illuminance" """Illuminance. Unit of measurement: `lx` """ IRRADIANCE = "irradiance" """Irradiance. Unit of measurement: - SI / metric: `W/m²` - USCS / imperial: `BTU/(h⋅ft²)` """ MOISTURE = "moisture" """Moisture. Unit of measurement: `%` """ MONETARY = "monetary" """Amount of money. Unit of measurement: ISO4217 currency code See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes """ NITROGEN_DIOXIDE = "nitrogen_dioxide" """Amount of NO2. Unit of measurement: `µg/m³` """ NITROGEN_MONOXIDE = "nitrogen_monoxide" """Amount of NO. Unit of measurement: `µg/m³` """ NITROUS_OXIDE = "nitrous_oxide" """Amount of N2O. Unit of measurement: `µg/m³` """ OZONE = "ozone" """Amount of O3. Unit of measurement: `µg/m³` """ PH = "ph" """Potential hydrogen (acidity/alkalinity). Unit of measurement: Unitless """ PM1 = "pm1" """Particulate matter <= 1 μm. Unit of measurement: `µg/m³` """ PM10 = "pm10" """Particulate matter <= 10 μm. Unit of measurement: `µg/m³` """ PM25 = "pm25" """Particulate matter <= 2.5 μm. Unit of measurement: `µg/m³` """ POWER_FACTOR = "power_factor" """Power factor. Unit of measurement: `%`, `None` """ POWER = "power" """Power. Unit of measurement: `W`, `kW` """ PRECIPITATION = "precipitation" """Accumulated precipitation. Unit of measurement: UnitOfPrecipitationDepth - SI / metric: `cm`, `mm` - USCS / imperial: `in` """ PRECIPITATION_INTENSITY = "precipitation_intensity" """Precipitation intensity. Unit of measurement: UnitOfVolumetricFlux - SI /metric: `mm/d`, `mm/h` - USCS / imperial: `in/d`, `in/h` """ PRESSURE = "pressure" """Pressure. Unit of measurement: - `mbar`, `cbar`, `bar` - `Pa`, `hPa`, `kPa` - `inHg` - `psi` """ REACTIVE_POWER = "reactive_power" """Reactive power. Unit of measurement: `var` """ SIGNAL_STRENGTH = "signal_strength" """Signal strength. Unit of measurement: `dB`, `dBm` """ SOUND_PRESSURE = "sound_pressure" """Sound pressure. Unit of measurement: `dB`, `dBA` """ SPEED = "speed" """Generic speed. Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - Nautical: `kn` """ SULPHUR_DIOXIDE = "sulphur_dioxide" """Amount of SO2. Unit of measurement: `µg/m³` """ TEMPERATURE = "temperature" """Temperature. Unit of measurement: `°C`, `°F`, `K` """ VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" """Amount of VOC. Unit of measurement: `µg/m³` """ VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" """Ratio of VOC. Unit of measurement: `ppm`, `ppb` """ VOLTAGE = "voltage" """Voltage. Unit of measurement: `V`, `mV` """ VOLUME = "volume" """Generic volume. Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ VOLUME_STORAGE = "volume_storage" """Generic stored volume. Use this device class for sensors measuring stored volume, for example the amount of fuel in a fuel tank. Unit of measurement: `VOLUME_*` units - SI / metric: `mL`, `L`, `m³` - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ VOLUME_FLOW_RATE = "volume_flow_rate" """Generic flow rate Unit of measurement: UnitOfVolumeFlowRate - SI / metric: `m³/h`, `L/min` - USCS / imperial: `ft³/min`, `gal/min` """ WATER = "water" """Water. Unit of measurement: - SI / metric: `m³`, `L` - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in USCS/imperial units are currently assumed to be US volumes) """ WEIGHT = "weight" """Generic weight, represents a measurement of an object's mass. Weight is used instead of mass to fit with every day language. Unit of measurement: `MASS_*` units - SI / metric: `µg`, `mg`, `g`, `kg` - USCS / imperial: `oz`, `lb` """ WIND_SPEED = "wind_speed" """Wind speed. Unit of measurement: `SPEED_*` units - SI /metric: `m/s`, `km/h` - USCS / imperial: `ft/s`, `mph` - Nautical: `kn` """ class SensorStateClass(Enum): """State class for sensors.""" MEASUREMENT = "measurement" """The state represents a measurement in present time.""" TOTAL = "total" """The state represents a total amount. For example: net energy consumption""" TOTAL_INCREASING = "total_increasing" """The state represents a monotonically increasing total. For example: an amount of consumed gas""" zigpy-0.80.1/zigpy/serial.py000066400000000000000000000110041501451476000157340ustar00rootroot00000000000000from __future__ import annotations import asyncio import logging import pathlib import sys import typing from typing import Literal import urllib.parse if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout # pragma: no cover else: from asyncio import timeout as asyncio_timeout # pragma: no cover import serial as pyserial from zigpy.typing import UNDEFINED, UndefinedType LOGGER = logging.getLogger(__name__) DEFAULT_SOCKET_PORT = 6638 SOCKET_CONNECT_TIMEOUT = 5 try: import serial_asyncio_fast as pyserial_asyncio LOGGER.info("Using pyserial-asyncio-fast in place of pyserial-asyncio") except ImportError: import serial_asyncio as pyserial_asyncio class SerialProtocol(asyncio.Protocol): """Base class for packet-parsing serial protocol implementations.""" def __init__(self) -> None: self._buffer = bytearray() self._transport: pyserial_asyncio.SerialTransport | None = None self._connected_event = asyncio.Event() self._disconnected_event = asyncio.Event() self._disconnected_event.set() async def wait_until_connected(self) -> None: """Wait for the protocol's transport to be connected.""" await self._connected_event.wait() def connection_made(self, transport: pyserial_asyncio.SerialTransport) -> None: LOGGER.debug("Connection made: %s", transport) self._transport = transport self._disconnected_event.clear() self._connected_event.set() def connection_lost(self, exc: BaseException | None) -> None: LOGGER.debug("Connection lost: %r", exc) self._connected_event.clear() self._disconnected_event.set() self._transport = None def data_received(self, data: bytes) -> None: self._buffer += data def close(self) -> None: self._buffer.clear() if self._transport is not None: self._transport.close() async def wait_until_closed(self) -> None: LOGGER.debug("Waiting for serial port to close") await self._disconnected_event.wait() async def disconnect(self) -> None: self.close() await self.wait_until_closed() async def create_serial_connection( loop: asyncio.BaseEventLoop, protocol_factory: typing.Callable[[], asyncio.Protocol], url: pathlib.Path | str, *, baudrate: int = 115200, # We default to 115200 instead of 9600 exclusive: bool | None = True, xonxoff: bool | UndefinedType = UNDEFINED, rtscts: bool | UndefinedType = UNDEFINED, flow_control: Literal["hardware", "software", None] | UndefinedType = UNDEFINED, **kwargs: typing.Any, ) -> tuple[asyncio.Transport, asyncio.Protocol]: """Wrapper around pyserial-asyncio that transparently substitutes a normal TCP transport and protocol when a `socket` connection URI is provided. """ if flow_control is not UNDEFINED: xonxoff = flow_control == "software" rtscts = flow_control == "hardware" if xonxoff is UNDEFINED: xonxoff = False if rtscts is UNDEFINED: rtscts = False LOGGER.debug( "Opening a serial connection to %r (baudrate=%s, xonxoff=%s, rtscts=%s)", url, baudrate, xonxoff, rtscts, ) url = str(url) parsed_url = urllib.parse.urlparse(url) if parsed_url.scheme in ("socket", "tcp"): async with asyncio_timeout(SOCKET_CONNECT_TIMEOUT): transport, protocol = await loop.create_connection( protocol_factory=protocol_factory, host=parsed_url.hostname, port=parsed_url.port or DEFAULT_SOCKET_PORT, ) else: try: try: transport, protocol = await pyserial_asyncio.create_serial_connection( loop, protocol_factory, url=url, baudrate=baudrate, exclusive=exclusive, xonxoff=xonxoff, rtscts=rtscts, **kwargs, ) except pyserial.SerialException as exc: # Unwrap unnecessarily wrapped PySerial exceptions if exc.__context__ is not None: raise exc.__context__ from None raise except BlockingIOError as exc: # Re-raise a more useful exception raise PermissionError( "The serial port is locked by another application" ) from exc return transport, protocol zigpy-0.80.1/zigpy/state.py000066400000000000000000000270241501451476000156060ustar00rootroot00000000000000"""Classes to implement status of the application controller.""" from __future__ import annotations from collections.abc import Iterable, Iterator import dataclasses from dataclasses import InitVar import functools from typing import Any import zigpy.config as conf import zigpy.types as t import zigpy.util import zigpy.zdo.types as zdo_t LOGICAL_TYPE_TO_JSON = { zdo_t.LogicalType.Coordinator: "coordinator", zdo_t.LogicalType.Router: "router", zdo_t.LogicalType.EndDevice: "end_device", } JSON_TO_LOGICAL_TYPE = {v: k for k, v in LOGICAL_TYPE_TO_JSON.items()} @dataclasses.dataclass class Key(t.BaseDataclassMixin): """APS/TC Link key.""" key: t.KeyData = dataclasses.field(default_factory=lambda: t.KeyData.UNKNOWN) tx_counter: t.uint32_t = 0 rx_counter: t.uint32_t = 0 seq: t.uint8_t = 0 partner_ieee: t.EUI64 = dataclasses.field(default_factory=lambda: t.EUI64.UNKNOWN) def as_dict(self) -> dict[str, Any]: return { "key": str(t.KeyData(self.key)), "tx_counter": self.tx_counter, "rx_counter": self.rx_counter, "seq": self.seq, "partner_ieee": str(self.partner_ieee), } @classmethod def from_dict(cls, obj: dict[str, Any]) -> Key: return cls( key=t.KeyData.convert(obj["key"]), tx_counter=obj["tx_counter"], rx_counter=obj["rx_counter"], seq=obj["seq"], partner_ieee=t.EUI64.convert(obj["partner_ieee"]), ) @dataclasses.dataclass class NodeInfo(t.BaseDataclassMixin): """Controller Application network Node information.""" nwk: t.NWK = t.NWK(0xFFFE) ieee: t.EUI64 = dataclasses.field(default_factory=lambda: t.EUI64.UNKNOWN) logical_type: zdo_t.LogicalType = zdo_t.LogicalType.EndDevice # Device information model: str | None = None manufacturer: str | None = None version: str | None = None def as_dict(self) -> dict[str, Any]: return { "nwk": str(self.nwk)[2:], "ieee": str(self.ieee), "logical_type": LOGICAL_TYPE_TO_JSON[self.logical_type], "model": self.model, "manufacturer": self.manufacturer, "version": self.version, } @classmethod def from_dict(cls, obj: dict[str, Any]) -> NodeInfo: return cls( nwk=t.NWK.convert(obj["nwk"]), ieee=t.EUI64.convert(obj["ieee"]), logical_type=JSON_TO_LOGICAL_TYPE[obj["logical_type"]], model=obj["model"], manufacturer=obj["manufacturer"], version=obj["version"], ) @dataclasses.dataclass class NetworkInfo(t.BaseDataclassMixin): """Network information.""" extended_pan_id: t.ExtendedPanId = dataclasses.field( default_factory=lambda: t.ExtendedPanId.UNKNOWN ) pan_id: t.PanId = t.PanId(0xFFFE) nwk_update_id: t.uint8_t = t.uint8_t(0x00) nwk_manager_id: t.NWK = t.NWK(0x0000) channel: t.uint8_t = 0 channel_mask: t.Channels = t.Channels.NO_CHANNELS security_level: t.uint8_t = 0 network_key: Key = dataclasses.field(default_factory=Key) tc_link_key: Key = dataclasses.field( default_factory=lambda: Key( key=conf.CONF_NWK_TC_LINK_KEY_DEFAULT, tx_counter=0, rx_counter=0, seq=0, partner_ieee=t.EUI64.UNKNOWN, ) ) key_table: list[Key] = dataclasses.field(default_factory=list) children: list[t.EUI64] = dataclasses.field(default_factory=list) # If exposed by the stack, NWK addresses of other connected devices on the network nwk_addresses: dict[t.EUI64, t.NWK] = dataclasses.field(default_factory=dict) # dict to keep track of stack-specific network information. # Z-Stack, for example, has a TCLK_SEED that should be backed up. stack_specific: dict[str, Any] = dataclasses.field(default_factory=dict) # Internal metadata not directly used for network restoration metadata: dict[str, Any] = dataclasses.field(default_factory=dict) # Package generating the network information source: str | None = None def as_dict(self) -> dict[str, Any]: return { "extended_pan_id": str(self.extended_pan_id), "pan_id": str(t.PanId(self.pan_id))[2:], "nwk_update_id": self.nwk_update_id, "nwk_manager_id": str(t.NWK(self.nwk_manager_id))[2:], "channel": self.channel, "channel_mask": list(self.channel_mask), "security_level": self.security_level, "network_key": self.network_key.as_dict(), "tc_link_key": self.tc_link_key.as_dict(), "key_table": [key.as_dict() for key in self.key_table], "children": sorted(str(ieee) for ieee in self.children), "nwk_addresses": { str(ieee): str(t.NWK(nwk))[2:] for ieee, nwk in sorted(self.nwk_addresses.items()) }, "stack_specific": self.stack_specific, "metadata": self.metadata, "source": self.source, } @classmethod def from_dict(cls, obj: dict[str, Any]) -> NetworkInfo: return cls( extended_pan_id=t.ExtendedPanId.convert(obj["extended_pan_id"]), pan_id=t.PanId.convert(obj["pan_id"]), nwk_update_id=obj["nwk_update_id"], nwk_manager_id=t.NWK.convert(obj["nwk_manager_id"]), channel=obj["channel"], channel_mask=t.Channels.from_channel_list(obj["channel_mask"]), security_level=obj["security_level"], network_key=Key.from_dict(obj["network_key"]), tc_link_key=Key.from_dict(obj["tc_link_key"]), key_table=sorted( (Key.from_dict(o) for o in obj["key_table"]), key=lambda k: k.partner_ieee, ), children=[t.EUI64.convert(ieee) for ieee in obj["children"]], nwk_addresses={ t.EUI64.convert(ieee): t.NWK.convert(nwk) for ieee, nwk in obj["nwk_addresses"].items() }, stack_specific=obj["stack_specific"], metadata=obj["metadata"], source=obj["source"], ) @dataclasses.dataclass class Counter(t.BaseDataclassMixin): """Ever increasing Counter.""" name: str initial_value: InitVar[int] = 0 _raw_value: int = dataclasses.field(init=False, default=0) reset_count: int = dataclasses.field(init=False, default=0) _last_reset_value: int = dataclasses.field(init=False, default=0) def __eq__(self, other) -> bool: """Compare two counters.""" if isinstance(other, self.__class__): return self.value == other.value return self.value == other def __int__(self) -> int: """Return int of the current value.""" return self.value def __post_init__(self, initial_value: int) -> None: """Initialize instance.""" self._raw_value = initial_value def __str__(self) -> str: """String representation.""" return f"{self.name} = {self.value}" @property def value(self) -> int: """Current value of the counter.""" return self._last_reset_value + self._raw_value def update(self, new_value: int) -> None: """Update counter value.""" if new_value == self._raw_value: return diff = new_value - self._raw_value if diff < 0: # Roll over or reset self.reset_and_update(new_value) return self._raw_value = new_value def increment(self, increment: int = 1) -> None: """Increment current value by increment.""" assert increment >= 0 self._raw_value += increment def reset_and_update(self, value: int) -> None: """Clear (rollover event) and optionally update.""" self._last_reset_value = self.value self._raw_value = value self.reset_count += 1 reset = functools.partialmethod(reset_and_update, 0) class CounterGroup(dict): """Named collection of related counters.""" def __init__( self, collection_name: str | None = None, ) -> None: """Initialize instance.""" self._name: str | None = collection_name super().__init__() def counters(self) -> Iterable[Counter]: """Return an iterable of the counters""" return (counter for counter in self.values() if isinstance(counter, Counter)) def groups(self) -> Iterable[CounterGroup]: """Return an iterable of the counter groups""" return (group for group in self.values() if isinstance(group, CounterGroup)) def tags(self) -> Iterable[int | str]: """Return an iterable if tags""" return (group.name for group in self.groups()) def __missing__(self, counter_id: Any) -> Counter: """Default counter factory.""" counter = Counter(counter_id) self[counter_id] = counter return counter def __repr__(self) -> str: """Representation magic method.""" counters = ( f"{counter.__class__.__name__}('{counter.name}', {int(counter)})" for counter in self.counters() ) counters = ", ".join(counters) return f"{self.__class__.__name__}('{self.name}', {{{counters}}})" def __str__(self) -> str: """String magic method.""" counters = [str(counter) for counter in self.counters()] return f"{self.name}: [{', '.join(counters)}]" @property def name(self) -> str: """Return counter collection name.""" return self._name if self._name is not None else "No Name" def increment(self, name: int | str, *tags: int | str) -> None: """Create and Update all counters recursively.""" if tags: tag, *rest = tags self.setdefault(tag, CounterGroup(tag)) self[tag][name].increment() self[tag].increment(name, *rest) return def reset(self) -> None: """Clear and rollover counters.""" for counter in self.values(): counter.reset() class CounterGroups(dict): """A collection of unrelated counter groups in a dict.""" def __iter__(self) -> Iterator[CounterGroup]: """Return an iterable of the counters""" return iter(self.values()) def __missing__(self, counter_group_name: Any) -> CounterGroup: """Default counter factory.""" counter_group = CounterGroup(counter_group_name) super().__setitem__(counter_group_name, counter_group) return counter_group @dataclasses.dataclass class State: node_info: NodeInfo = dataclasses.field(default_factory=NodeInfo) network_info: NetworkInfo = dataclasses.field(default_factory=NetworkInfo) counters: CounterGroups = dataclasses.field(init=False, default=None) broadcast_counters: CounterGroups = dataclasses.field(init=False, default=None) device_counters: CounterGroups = dataclasses.field(init=False, default=None) group_counters: CounterGroups = dataclasses.field(init=False, default=None) def __post_init__(self) -> None: """Initialize default counters.""" for col_name in ("", "broadcast_", "device_", "group_"): setattr(self, f"{col_name}counters", CounterGroups()) @property @zigpy.util.deprecated("`network_information` has been renamed to `network_info`") def network_information(self) -> NetworkInfo: return self.network_info @property @zigpy.util.deprecated("`node_information` has been renamed to `node_info`") def node_information(self) -> NodeInfo: return self.node_info zigpy-0.80.1/zigpy/topology.py000066400000000000000000000205371501451476000163440ustar00rootroot00000000000000"""Topology builder.""" from __future__ import annotations import asyncio import collections import itertools import logging import random import typing import zigpy.config import zigpy.device import zigpy.types as t import zigpy.util import zigpy.zdo.types as zdo_t LOGGER = logging.getLogger(__name__) REQUEST_DELAY = (1.0, 1.5) if typing.TYPE_CHECKING: import zigpy.application RETRY_SLOW = zigpy.util.retryable_request(tries=3, delay=1) class ScanNotSupported(Exception): pass INVALID_NEIGHBOR_IEEES = { t.EUI64.convert("00:00:00:00:00:00:00:00"), t.EUI64.convert("ff:ff:ff:ff:ff:ff:ff:ff"), } class Topology(zigpy.util.ListenableMixin): """Topology scanner.""" def __init__(self, app: zigpy.application.ControllerApplication) -> None: """Instantiate.""" self._app: zigpy.application.ControllerApplication = app self._listeners: dict = {} self._scan_task: asyncio.Task | None = None self._scan_loop_task: asyncio.Task | None = None # Keep track of devices that do not support scanning self._neighbors_unsupported: set[t.EUI64] = set() self._routes_unsupported: set[t.EUI64] = set() self.neighbors: dict[t.EUI64, list[zdo_t.Neighbor]] = collections.defaultdict( list ) self.routes: dict[t.EUI64, list[zdo_t.Route]] = collections.defaultdict(list) def start_periodic_scans(self, period: float) -> None: self.stop_periodic_scans() self._scan_loop_task = asyncio.create_task(self._scan_loop(period)) def stop_periodic_scans(self) -> None: if self._scan_loop_task is not None: self._scan_loop_task.cancel() async def _scan_loop(self, period: float) -> None: """Delay scan by creating a task.""" while True: await asyncio.sleep(period) # Don't run a scheduled scan if a scan is already running if self._scan_task is not None and not self._scan_task.done(): continue LOGGER.debug("Starting scheduled neighbor scan") try: await self.scan() except asyncio.CancelledError: # We explicitly catch a cancellation here to ensure the scan loop will # not be interrupted if a manual scan is initiated LOGGER.debug("Topology scan cancelled") except (Exception, asyncio.CancelledError): LOGGER.debug("Topology scan failed", exc_info=True) async def scan( self, devices: typing.Iterable[zigpy.device.Device] | None = None ) -> None: """Preempt Topology scan and reschedule.""" if self._scan_task and not self._scan_task.done(): LOGGER.debug("Cancelling old scanning task") self._scan_task.cancel() self._scan_task = asyncio.create_task(self._scan(devices)) await self._scan_task async def _scan_table( self, scan_request: typing.Callable, entries_attr: str ) -> list[typing.Any]: """Scan a device table by sending ZDO requests.""" index = 0 table = [] while True: status, rsp = await RETRY_SLOW(scan_request)(index) if status != zdo_t.Status.SUCCESS: raise ScanNotSupported entries = getattr(rsp, entries_attr) table.extend(entries) index += len(entries) # We intentionally sleep after every request, even the last one, to simplify # delay logic when scanning many devices in quick succession await asyncio.sleep(random.uniform(*REQUEST_DELAY)) if index >= rsp.Entries or not entries: break return table async def _scan_neighbors( self, device: zigpy.device.Device ) -> list[zdo_t.Neighbor]: if device.ieee in self._neighbors_unsupported: return [] LOGGER.debug("Scanning neighbors of %s", device) try: table = await self._scan_table(device.zdo.Mgmt_Lqi_req, "NeighborTableList") except ScanNotSupported: table = [] self._neighbors_unsupported.add(device.ieee) return [n for n in table if n.ieee not in INVALID_NEIGHBOR_IEEES] async def _scan_routes(self, device: zigpy.device.Device) -> list[zdo_t.Route]: if device.ieee in self._routes_unsupported: return [] LOGGER.debug("Scanning routing table of %s", device) try: table = await self._scan_table(device.zdo.Mgmt_Rtg_req, "RoutingTableList") except ScanNotSupported: table = [] self._routes_unsupported.add(device.ieee) return table async def _scan( self, devices: typing.Iterable[zigpy.device.Device] | None = None ) -> None: """Scan topology.""" if devices is None: # We iterate over a copy of the devices as opposed to the live dictionary devices = list(self._app.devices.values()) for index, device in enumerate(devices): LOGGER.debug( "Scanning topology (%d/%d) of %s", index + 1, len(devices), device ) # Ignore devices that aren't routers if device.node_desc is None or not ( device.node_desc.is_router or device.node_desc.is_coordinator ): continue # Ignore devices that do not support scanning tables if ( device.ieee in self._neighbors_unsupported and device.ieee in self._routes_unsupported ): continue # Some coordinators have issues when performing loopback scans if ( self._app.config[zigpy.config.CONF_TOPO_SKIP_COORDINATOR] and device is self._app._device ): continue try: self.neighbors[device.ieee] = await self._scan_neighbors(device) except Exception as e: # noqa: BLE001 LOGGER.debug("Failed to scan neighbors of %s", device, exc_info=e) else: LOGGER.info( "Scanned neighbors of %s: %s", device, self.neighbors[device.ieee] ) self.listener_event( "neighbors_updated", device.ieee, self.neighbors[device.ieee] ) try: # Filter out inactive routes routes = await self._scan_routes(device) self.routes[device.ieee] = [ route for route in routes if route.RouteStatus != zdo_t.RouteStatus.Inactive ] except Exception as e: # noqa: BLE001 LOGGER.debug("Failed to scan routes of %s", device, exc_info=e) else: LOGGER.info( "Scanned routes of %s: %s", device, self.routes[device.ieee] ) self.listener_event("routes_updated", device.ieee, self.routes[device.ieee]) LOGGER.debug("Finished scanning neighbors for all devices") await self._find_unknown_devices(neighbors=self.neighbors, routes=self.routes) async def _find_unknown_devices( self, *, neighbors: dict[t.EUI64, list[zdo_t.Neighbor]], routes: dict[t.EUI64, list[zdo_t.Route]], ) -> None: """Discover unknown devices discovered during topology scanning""" # Build a list of unknown devices from the topology scan unknown_nwks = set() for neighbor in itertools.chain.from_iterable(neighbors.values()): try: self._app.get_device(nwk=neighbor.nwk) except KeyError: unknown_nwks.add(neighbor.nwk) for route in itertools.chain.from_iterable(routes.values()): # Ignore inactive or pending routes if route.RouteStatus != zdo_t.RouteStatus.Active: continue for nwk in (route.DstNWK, route.NextHop): try: self._app.get_device(nwk=nwk) except KeyError: unknown_nwks.add(nwk) # Try to discover any unknown devices for nwk in unknown_nwks: LOGGER.debug("Found unknown device nwk=%s", nwk) await self._app._discover_unknown_device(nwk) await asyncio.sleep(random.uniform(*REQUEST_DELAY)) zigpy-0.80.1/zigpy/types/000077500000000000000000000000001501451476000152535ustar00rootroot00000000000000zigpy-0.80.1/zigpy/types/__init__.py000066400000000000000000000006551501451476000173720ustar00rootroot00000000000000from __future__ import annotations from .basic import * # noqa: F401,F403 from .named import * # noqa: F401,F403 from .struct import * # noqa: F401,F403 def deserialize(data, schema): result = [] for type_ in schema: value, data = type_.deserialize(data) result.append(value) return result, data def serialize(data, schema): return b"".join(t(v).serialize() for t, v in zip(schema, data)) zigpy-0.80.1/zigpy/types/basic.py000066400000000000000000000652251501451476000167200ustar00rootroot00000000000000from __future__ import annotations import enum import inspect import struct import sys import typing from typing_extensions import Self CALLABLE_T = typing.TypeVar("CALLABLE_T", bound=typing.Callable) T = typing.TypeVar("T") class Bits(list): @classmethod def from_bitfields(cls, fields): instance = cls() # Little endian, so [11, 1000, 00] will be packed as 00_1000_11 for field in fields[::-1]: instance.extend(field.bits()) return instance def serialize(self) -> bytes: if len(self) % 8 != 0: raise ValueError(f"Cannot serialize {len(self)} bits into bytes: {self}") serialized_bytes = [] for index in range(0, len(self), 8): byte = 0x00 for bit in self[index : index + 8]: byte <<= 1 byte |= bit serialized_bytes.append(byte) return bytes(serialized_bytes) @classmethod def deserialize(cls, data) -> tuple[Bits, bytes]: bits: list[int] = [] for byte in data: bits.extend((byte >> i) & 1 for i in range(7, -1, -1)) return cls(bits), b"" class SerializableBytes: """A container object for raw bytes that enforces `serialize()` will be called.""" def __init__(self, value: bytes = b"") -> None: if isinstance(value, SerializableBytes): value = value.value elif not isinstance(value, (bytes, bytearray)): raise ValueError(f"Object is not bytes: {value!r}") # noqa: TRY004 self.value: bytes | bytearray = value def __eq__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented return self.value == other.value def serialize(self) -> bytes: return self.value def __repr__(self) -> str: return f"Serialized[{self.value!r}]" def __hash__(self) -> int: return hash(self.value) NOT_SET = object() class FixedIntType(int): _signed = None _bits = None _size = None # Only for backwards compatibility, not set for smaller ints _byteorder = None min_value: int max_value: int def __new__(cls, *args, **kwargs): if cls._signed is None or cls._bits is None: raise TypeError(f"{cls} is abstract and cannot be created") n = super().__new__(cls, *args, **kwargs) # We use `n + 0` to convert `n` into an integer without calling `int()` if not cls.min_value <= n + 0 <= cls.max_value: raise ValueError( f"{int(n)} is not an {'un' if not cls._signed else ''}signed" f" {cls._bits} bit integer" ) return n def _hex_repr(self): assert self._bits % 4 == 0 return f"0x{{:0{self._bits // 4}X}}".format(int(self)) def _bin_repr(self): return f"0b{{:0{self._bits}b}}".format(int(self)) def __init_subclass__( cls, signed=NOT_SET, bits=NOT_SET, repr=NOT_SET, byteorder=NOT_SET ) -> None: super().__init_subclass__() if signed is not NOT_SET: cls._signed = signed if bits is not NOT_SET: cls._bits = bits if bits % 8 == 0: cls._size = bits // 8 else: cls._size = None if cls._bits is not None and cls._signed is not None: if cls._signed: cls.min_value = -(2 ** (cls._bits - 1)) cls.max_value = 2 ** (cls._bits - 1) - 1 else: cls.min_value = 0 cls.max_value = 2**cls._bits - 1 if repr == "hex": assert cls._bits % 4 == 0 cls.__str__ = cls.__repr__ = cls._hex_repr elif repr == "bin": cls.__str__ = cls.__repr__ = cls._bin_repr elif not repr: cls.__str__ = super().__str__ cls.__repr__ = super().__repr__ elif repr is not NOT_SET: raise ValueError(f"Invalid repr value {repr!r}. Must be either hex or bin") if byteorder is not NOT_SET: cls._byteorder = byteorder elif cls._byteorder is None: cls._byteorder = "little" if sys.version_info < (3, 10): # XXX: The enum module uses the first class with __new__ in its __dict__ # as the member type. We have to ensure this is true for # every subclass. # Fixed with https://github.com/python/cpython/pull/26658 if "__new__" not in cls.__dict__: cls.__new__ = cls.__new__ # XXX: The enum module sabotages pickling using the same logic. if "__reduce_ex__" not in cls.__dict__: cls.__reduce_ex__ = cls.__reduce_ex__ def bits(self) -> Bits: return Bits([(self >> n) & 0b1 for n in range(self._bits - 1, -1, -1)]) @classmethod def from_bits(cls, bits: Bits) -> tuple[FixedIntType, Bits]: if len(bits) < cls._bits: raise ValueError(f"Not enough bits to decode {cls}: {bits}") n = 0 for bit in bits[-cls._bits :]: n <<= 1 n |= bit & 1 if cls._signed and n >= 2 ** (cls._bits - 1): n -= 2**cls._bits return cls(n), bits[: -cls._bits] def serialize(self) -> bytes: if self._bits % 8 != 0: raise TypeError(f"Integer type with {self._bits} bits is not byte aligned") return self.to_bytes(self._bits // 8, self._byteorder, signed=self._signed) @classmethod def deserialize(cls, data: bytes) -> tuple[FixedIntType, bytes]: if cls._bits % 8 != 0: raise TypeError(f"Integer type with {cls._bits} bits is not byte aligned") byte_size = cls._bits // 8 if len(data) < byte_size: raise ValueError(f"Data is too short to contain {byte_size} bytes") r = cls.from_bytes(data[:byte_size], cls._byteorder, signed=cls._signed) data = data[byte_size:] return r, data class uint_t(FixedIntType, signed=False): pass class int_t(FixedIntType, signed=True): pass class int8s(int_t, bits=8): pass class int16s(int_t, bits=16): pass class int24s(int_t, bits=24): pass class int32s(int_t, bits=32): pass class int40s(int_t, bits=40): pass class int48s(int_t, bits=48): pass class int56s(int_t, bits=56): pass class int64s(int_t, bits=64): pass class uint1_t(uint_t, bits=1): pass class uint2_t(uint_t, bits=2): pass class uint3_t(uint_t, bits=3): pass class uint4_t(uint_t, bits=4): pass class uint5_t(uint_t, bits=5): pass class uint6_t(uint_t, bits=6): pass class uint7_t(uint_t, bits=7): pass class uint8_t(uint_t, bits=8): pass class uint16_t(uint_t, bits=16): pass class uint24_t(uint_t, bits=24): pass class uint32_t(uint_t, bits=32): pass class uint40_t(uint_t, bits=40): pass class uint48_t(uint_t, bits=48): pass class uint56_t(uint_t, bits=56): pass class uint64_t(uint_t, bits=64): pass class uint_t_be(FixedIntType, signed=False, byteorder="big"): pass class int_t_be(FixedIntType, signed=True, byteorder="big"): pass class int16s_be(int_t_be, bits=16): pass class int24s_be(int_t_be, bits=24): pass class int32s_be(int_t_be, bits=32): pass class int40s_be(int_t_be, bits=40): pass class int48s_be(int_t_be, bits=48): pass class int56s_be(int_t_be, bits=56): pass class int64s_be(int_t_be, bits=64): pass class uint16_t_be(uint_t_be, bits=16): pass class uint24_t_be(uint_t_be, bits=24): pass class uint32_t_be(uint_t_be, bits=32): pass class uint40_t_be(uint_t_be, bits=40): pass class uint48_t_be(uint_t_be, bits=48): pass class uint56_t_be(uint_t_be, bits=56): pass class uint64_t_be(uint_t_be, bits=64): pass class AlwaysCreateEnumType(enum.EnumMeta): """Enum metaclass that skips the functional creation API.""" def __call__(self, value, names=None, *values) -> type[enum.Enum]: # type: ignore[override] # noqa: N804 """Custom implementation of Enum.__new__. From https://github.com/python/cpython/blob/v3.11.5/Lib/enum.py#L1091-L1140 """ # all enum instances are actually created during class construction # without calling this method; this method is called by the metaclass' # __call__ (i.e. Color(3) ), and by pickle if type(value) is self: # For lookups like Color(Color.RED) return value # by-value search for a matching enum member # see if it's in the reverse mapping (for hashable values) try: return self._value2member_map_[value] except KeyError: # Not found, no need to do long O(n) search pass except TypeError: # not there, now do long search -- O(n) behavior for member in self._member_map_.values(): if member._value_ == value: return member # still not found -- try _missing_ hook try: exc = None result = self._missing_(value) except Exception as e: # noqa: BLE001 exc = e result = None try: if isinstance(result, self) or ( enum.Flag is not None and issubclass(self, enum.Flag) and self._boundary_ is enum.EJECT and isinstance(result, int) ): return result else: ve_exc = ValueError(f"{value!r} is not a valid {self.__qualname__}") if result is None and exc is None: raise ve_exc elif exc is None: exc = TypeError( f"error in {self.__name__}._missing_: returned {result!r} instead of None or a valid member" ) if not isinstance(exc, ValueError): exc.__context__ = ve_exc raise exc finally: # ensure all variables that could hold an exception are destroyed exc = None ve_exc = None class _IntEnumMeta(AlwaysCreateEnumType): def __call__(self, value, names=None, *args, **kwargs): # noqa: N804 if isinstance(value, str): if value.startswith("0x"): value = int(value, base=16) elif value.isnumeric(): value = int(value) elif value.startswith(self.__name__ + "."): value = self[value[len(self.__name__) + 1 :]].value else: value = self[value].value return super().__call__(value, names, *args, **kwargs) def bitmap_factory(int_type: CALLABLE_T) -> CALLABLE_T: """Mixins are broken by Python 3.8.6 so we must dynamically create the enum with the appropriate methods but with only one non-Enum parent class. """ if sys.version_info >= (3, 11): class _NewEnum( int_type, enum.ReprEnum, enum.Flag, boundary=enum.KEEP, metaclass=AlwaysCreateEnumType, ): pass else: class _NewEnum(int_type, enum.Flag): # Rebind classmethods to our own class _missing_ = classmethod(enum.IntFlag._missing_.__func__) _create_pseudo_member_ = classmethod( # type: ignore[var-annotated] enum.IntFlag._create_pseudo_member_.__func__ ) __or__ = enum.IntFlag.__or__ __and__ = enum.IntFlag.__and__ __xor__ = enum.IntFlag.__xor__ __ror__ = enum.IntFlag.__ror__ __rand__ = enum.IntFlag.__rand__ __rxor__ = enum.IntFlag.__rxor__ __invert__ = enum.IntFlag.__invert__ return _NewEnum def enum_factory(int_type: CALLABLE_T, undefined: str = "undefined") -> CALLABLE_T: """Enum factory.""" class _NewEnum(int_type, enum.Enum, metaclass=_IntEnumMeta): @classmethod def _missing_(cls, value): new = cls._member_type_.__new__(cls, value) if cls._bits % 8 == 0: name = f"{undefined}_{new._hex_repr().lower()}" else: name = f"{undefined}_{new._bin_repr()}" new._name_ = name.format(value) new._value_ = value return new def __format__(self, format_spec: str) -> str: if format_spec: # Allow formatting the integer enum value return self._member_type_.__format__(self, format_spec) else: # Otherwise, format it as its string representation return object.__format__(repr(self), format_spec) return _NewEnum class enum1(enum_factory(uint1_t)): # noqa: N801 pass class enum2(enum_factory(uint2_t)): # noqa: N801 pass class enum3(enum_factory(uint3_t)): # noqa: N801 pass class enum4(enum_factory(uint4_t)): # noqa: N801 pass class enum5(enum_factory(uint5_t)): # noqa: N801 pass class enum6(enum_factory(uint6_t)): # noqa: N801 pass class enum7(enum_factory(uint7_t)): # noqa: N801 pass class enum8(enum_factory(uint8_t)): # noqa: N801 pass class enum16(enum_factory(uint16_t)): # noqa: N801 pass class enum32(enum_factory(uint32_t)): # noqa: N801 pass class enum16_be(enum_factory(uint16_t_be)): # noqa: N801 pass class enum32_be(enum_factory(uint32_t_be)): # noqa: N801 pass class bitmap2(bitmap_factory(uint2_t)): pass class bitmap3(bitmap_factory(uint3_t)): pass class bitmap4(bitmap_factory(uint4_t)): pass class bitmap5(bitmap_factory(uint5_t)): pass class bitmap6(bitmap_factory(uint6_t)): pass class bitmap7(bitmap_factory(uint7_t)): pass class bitmap8(bitmap_factory(uint8_t)): pass class bitmap16(bitmap_factory(uint16_t)): pass class bitmap24(bitmap_factory(uint24_t)): pass class bitmap32(bitmap_factory(uint32_t)): pass class bitmap40(bitmap_factory(uint40_t)): pass class bitmap48(bitmap_factory(uint48_t)): pass class bitmap56(bitmap_factory(uint56_t)): pass class bitmap64(bitmap_factory(uint64_t)): pass class bitmap16_be(bitmap_factory(uint16_t_be)): pass class bitmap24_be(bitmap_factory(uint24_t_be)): pass class bitmap32_be(bitmap_factory(uint32_t_be)): pass class bitmap40_be(bitmap_factory(uint40_t_be)): pass class bitmap48_be(bitmap_factory(uint48_t_be)): pass class bitmap56_be(bitmap_factory(uint56_t_be)): pass class bitmap64_be(bitmap_factory(uint64_t_be)): pass class BaseFloat(float): _exponent_bits = None _fraction_bits = None _size = None def __init_subclass__(cls, exponent_bits, fraction_bits): size_bits = 1 + exponent_bits + fraction_bits assert size_bits % 8 == 0 cls._exponent_bits = exponent_bits cls._fraction_bits = fraction_bits cls._size = size_bits // 8 @staticmethod def _convert_format(*, src: BaseFloat, dst: BaseFloat, n: int) -> int: """Converts an integer representing a float from one format into another. Note: 1. Format is assumed to be little endian: 0b[sign bit] [exponent] [fraction] 2. Truncates/extends the exponent, preserving the special cases of all 1's and all 0's. 3. Truncates/extends the fractional bits from the right, allowing lossless conversion to a "bigger" representation. """ src_sign = n >> (src._exponent_bits + src._fraction_bits) src_frac = n & ((1 << src._fraction_bits) - 1) src_biased_exp = (n >> src._fraction_bits) & ((1 << src._exponent_bits) - 1) src_exp = src_biased_exp - 2 ** (src._exponent_bits - 1) if src_biased_exp == (1 << src._exponent_bits) - 1: dst_biased_exp = 2**dst._exponent_bits - 1 elif src_biased_exp == 0: dst_biased_exp = 0 else: dst_min_exp = 2 - 2 ** (dst._exponent_bits - 1) # Can't be all zeroes dst_max_exp = 2 ** (dst._exponent_bits - 1) - 2 # Can't be all ones dst_exp = min(max(dst_min_exp, src_exp), dst_max_exp) dst_biased_exp = dst_exp + 2 ** (dst._exponent_bits - 1) # We add/remove LSBs if src._fraction_bits > dst._fraction_bits: dst_frac = src_frac >> (src._fraction_bits - dst._fraction_bits) else: dst_frac = src_frac << (dst._fraction_bits - src._fraction_bits) return ( src_sign << (dst._exponent_bits + dst._fraction_bits) | dst_biased_exp << (dst._fraction_bits) | dst_frac ) def serialize(self) -> bytes: return self._convert_format( src=Double, dst=self, n=int.from_bytes(struct.pack(" tuple[BaseFloat, bytes]: if len(data) < cls._size: raise ValueError(f"Data is too short to contain {cls._size} bytes") double_bytes = cls._convert_format( src=cls, dst=Double, n=int.from_bytes(data[: cls._size], "little") ).to_bytes(Double._size, "little") return cls(struct.unpack("= pow(256, self._prefix_length) - 1: raise ValueError("OctetString is too long") return len(self).to_bytes(self._prefix_length, "little", signed=False) + self @classmethod def deserialize(cls, data): if len(data) < cls._prefix_length: raise ValueError("Data is too short") num_bytes = int.from_bytes(data[: cls._prefix_length], "little") if len(data) < cls._prefix_length + num_bytes: raise ValueError("Data is too short") s = data[cls._prefix_length : cls._prefix_length + num_bytes] return cls(s), data[cls._prefix_length + num_bytes :] def LimitedLVBytes(max_len): # noqa: N802 class LimitedLVBytes(LVBytes): _max_len = max_len def serialize(self): if len(self) > self._max_len: raise ValueError(f"LVBytes is too long (>{self._max_len})") return super().serialize() return LimitedLVBytes class LVBytesSize2(LVBytes): def serialize(self): if len(self) != 2: raise ValueError("LVBytes must be of size 2") return super().serialize() @classmethod def deserialize(cls, data): d, r = super().deserialize(data) if len(d) != 2: raise ValueError("LVBytes must be of size 2") return d, r class LongOctetString(LVBytes): _prefix_length = 2 class KwargTypeMeta(type): # So things like `LVList[NWK, t.uint8_t]` are singletons _anonymous_classes = {} # type:ignore[var-annotated] def __new__(cls, name, bases, namespaces, **kwargs): cls_kwarg_attrs = namespaces.get("_getitem_kwargs", {}) def __init_subclass__(cls, **kwargs): filtered_kwargs = kwargs.copy() for key in kwargs: if key in cls_kwarg_attrs: setattr(cls, f"_{key}", filtered_kwargs.pop(key)) super().__init_subclass__(**filtered_kwargs) if "__init_subclass__" not in namespaces: namespaces["__init_subclass__"] = __init_subclass__ return type.__new__(cls, name, bases, namespaces, **kwargs) def __getitem__(cls, key): # Make sure Foo[a] is the same as Foo[a,] if not isinstance(key, tuple): key = (key,) signature = inspect.Signature( parameters=[ inspect.Parameter( name=k, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, default=v if v is not None else inspect.Parameter.empty, ) for k, v in cls._getitem_kwargs.items() ] ) bound = signature.bind(*key) bound.apply_defaults() # Default types need to work, which is why we need to create the key down here expanded_key = tuple(bound.arguments.values()) if (cls, expanded_key) in cls._anonymous_classes: return cls._anonymous_classes[cls, expanded_key] class AnonSubclass(cls, **bound.arguments): pass AnonSubclass.__name__ = AnonSubclass.__qualname__ = f"Anonymous{cls.__name__}" cls._anonymous_classes[cls, expanded_key] = AnonSubclass return AnonSubclass def __subclasscheck__(cls, subclass): if type(subclass) is not KwargTypeMeta: return False # Named subclasses are handled normally if not cls.__name__.startswith("Anonymous"): return super().__subclasscheck__(subclass) # Anonymous subclasses must be identical if subclass.__name__.startswith("Anonymous"): return cls is subclass # A named class is a "subclass" of an anonymous subclass only if its ancestors # are all the same if subclass.__mro__[-len(cls.__mro__) + 1 :] != cls.__mro__[1:]: return False # They must also have the same class kwargs for key in cls._getitem_kwargs: key = f"_{key}" if getattr(cls, key) != getattr(subclass, key): return False return True def __instancecheck__(cls, subclass): # We rely on __subclasscheck__ to do the work if issubclass(type(subclass), cls): return True return super().__instancecheck__(subclass) class List(list, metaclass=KwargTypeMeta): _item_type = None _getitem_kwargs = {"item_type": None} def serialize(self) -> bytes: assert self._item_type is not None return b"".join([self._item_type(i).serialize() for i in self]) @classmethod def deserialize(cls: type[T], data: bytes) -> tuple[T, bytes]: assert cls._item_type is not None lst = cls() while data: item, data = cls._item_type.deserialize(data) lst.append(item) return lst, data class LVList(list, metaclass=KwargTypeMeta): _item_type = None _length_type = uint8_t _getitem_kwargs = {"item_type": None, "length_type": uint8_t} def serialize(self) -> bytes: assert self._item_type is not None return self._length_type(len(self)).serialize() + b"".join( [self._item_type(i).serialize() for i in self] ) @classmethod def deserialize(cls: type[T], data: bytes) -> tuple[T, bytes]: assert cls._item_type is not None length, data = cls._length_type.deserialize(data) r = cls() for _i in range(length): item, data = cls._item_type.deserialize(data) r.append(item) return r, data class FixedList(list, metaclass=KwargTypeMeta): _item_type = None _length = None _getitem_kwargs = {"item_type": None, "length": None} def serialize(self) -> bytes: assert self._length is not None if len(self) != self._length: raise ValueError( f"Invalid length for {self!r}: expected {self._length}, got {len(self)}" ) return b"".join([self._item_type(i).serialize() for i in self]) @classmethod def deserialize(cls: type[T], data: bytes) -> tuple[T, bytes]: assert cls._item_type is not None r = cls() for _i in range(cls._length): item, data = cls._item_type.deserialize(data) r.append(item) return r, data class CharacterString(str): __slots__ = ("invalid", "raw") _prefix_length = 1 _invalid_length = (1 << (8 * _prefix_length)) - 1 def __new__(cls, value: str = "", *, invalid: bool = False) -> Self: instance = super().__new__(cls, value) instance.invalid = invalid instance.raw = value return instance def serialize(self) -> bytes: if len(self) >= pow(256, self._prefix_length) - 1: raise ValueError("String is too long") if self.invalid: return self._invalid_length.to_bytes(self._prefix_length, "little") return len(self).to_bytes( self._prefix_length, "little", signed=False ) + self.encode("utf8") @classmethod def deserialize(cls: type[T], data: bytes) -> tuple[T, bytes]: if len(data) < cls._prefix_length: raise ValueError("Data is too short") length = int.from_bytes(data[: cls._prefix_length], "little") if length == cls._invalid_length: return cls("", invalid=True), data[cls._prefix_length :] # type:ignore[call-arg] if len(data) < cls._prefix_length + length: raise ValueError("Data is too short") raw = data[cls._prefix_length : cls._prefix_length + length] text = raw.split(b"\x00")[0].decode("utf8", errors="replace") # FIXME: figure out how to get this working: `T` is not behaving as expected in # the classmethod when it is not bound. r = cls(text) # type:ignore[call-arg] r.raw = raw return r, data[cls._prefix_length + length :] class LongCharacterString(CharacterString): _prefix_length = 2 def LimitedCharString(max_len): # noqa: N802 class LimitedCharString(CharacterString): _max_len = max_len def serialize(self) -> bytes: if len(self) > self._max_len: raise ValueError(f"String is too long (>{self._max_len})") return super().serialize() return LimitedCharString def Optional(optional_item_type): class Optional(optional_item_type): optional = True @classmethod def deserialize(cls, data): try: return super().deserialize(data) except ValueError: return None, b"" return Optional class data8(FixedList, item_type=uint8_t, length=1): """General data, Discrete, 8 bit.""" class data16(FixedList, item_type=uint8_t, length=2): """General data, Discrete, 16 bit.""" class data24(FixedList, item_type=uint8_t, length=3): """General data, Discrete, 24 bit.""" class data32(FixedList, item_type=uint8_t, length=4): """General data, Discrete, 32 bit.""" class data40(FixedList, item_type=uint8_t, length=5): """General data, Discrete, 40 bit.""" class data48(FixedList, item_type=uint8_t, length=6): """General data, Discrete, 48 bit.""" class data56(FixedList, item_type=uint8_t, length=7): """General data, Discrete, 56 bit.""" class data64(FixedList, item_type=uint8_t, length=8): """General data, Discrete, 64 bit.""" zigpy-0.80.1/zigpy/types/named.py000066400000000000000000000510531501451476000167150ustar00rootroot00000000000000from __future__ import annotations import dataclasses from datetime import datetime, timezone import enum import typing import attrs from . import basic from .struct import Struct if typing.TYPE_CHECKING: from typing_extensions import Self class BaseDataclassMixin: def replace(self, **kwargs: typing.Any) -> Self: if dataclasses.is_dataclass(self): assert not isinstance(self, type) # `is_dataclass` works on types as well return dataclasses.replace(self, **kwargs) else: return attrs.evolve(self, **kwargs) def _hex_string_to_bytes(hex_string: str) -> bytes: """Parses a hex string with optional colon delimiters and whitespace into bytes.""" # Strips out whitespace and colons cleaned = "".join(hex_string.replace(":", "").split()).upper() return bytes.fromhex(cleaned) class BroadcastAddress(basic.enum16): ALL_DEVICES = 0xFFFF RESERVED_FFFE = 0xFFFE RX_ON_WHEN_IDLE = 0xFFFD ALL_ROUTERS_AND_COORDINATOR = 0xFFFC LOW_POWER_ROUTER = 0xFFFB RESERVED_FFFA = 0xFFFA RESERVED_FFF9 = 0xFFF9 RESERVED_FFF8 = 0xFFF8 class EUI64(basic.FixedList, item_type=basic.uint8_t, length=8): # EUI 64-bit ID (an IEEE address). def __repr__(self) -> str: return ":".join(f"{i:02x}" for i in self[::-1]) def __hash__(self) -> int: # type: ignore[override] return hash(repr(self)) @classmethod def convert(cls, ieee: str) -> EUI64: if ieee is None: return None ieee = [basic.uint8_t(p) for p in _hex_string_to_bytes(ieee)[::-1]] assert len(ieee) == cls._length return cls(ieee) EUI64.UNKNOWN = EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF") class KeyData(basic.FixedList, item_type=basic.uint8_t, length=16): def __repr__(self) -> str: return ":".join(f"{i:02x}" for i in self) @classmethod def convert(cls, key: str) -> KeyData: key = [basic.uint8_t(p) for p in _hex_string_to_bytes(key)] assert len(key) == cls._length return cls(key) KeyData.UNKNOWN = KeyData.convert("FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF") class Bool(basic.enum8): false = 0 true = 1 class AttributeId(basic.uint16_t, repr="hex"): pass class BACNetOid(basic.uint32_t): pass class Channels(basic.bitmap32): """Zigbee Channels.""" NO_CHANNELS = 0x00000000 ALL_CHANNELS = 0x07FFF800 CHANNEL_11 = 0x00000800 CHANNEL_12 = 0x00001000 CHANNEL_13 = 0x00002000 CHANNEL_14 = 0x00004000 CHANNEL_15 = 0x00008000 CHANNEL_16 = 0x00010000 CHANNEL_17 = 0x00020000 CHANNEL_18 = 0x00040000 CHANNEL_19 = 0x00080000 CHANNEL_20 = 0x00100000 CHANNEL_21 = 0x00200000 CHANNEL_22 = 0x00400000 CHANNEL_23 = 0x00800000 CHANNEL_24 = 0x01000000 CHANNEL_25 = 0x02000000 CHANNEL_26 = 0x04000000 @classmethod def from_channel_list(cls: Channels, channels: typing.Iterable[int]) -> Channels: mask = cls.NO_CHANNELS for channel in channels: if not 11 <= channel <= 26: raise ValueError( f"Invalid channel number {channel}. Must be between 11 and 26." ) mask |= cls[f"CHANNEL_{channel}"] return mask def __iter__(self): cls = type(self) channels = [c for c in range(11, 26 + 1) if self & cls[f"CHANNEL_{c}"]] if self != cls.from_channel_list(channels): raise ValueError(f"Channels bitmap has unexpected members: {self}") return iter(channels) class ClusterId(basic.uint16_t): pass class Date(Struct): years_since_1900: basic.uint8_t month: basic.uint8_t day: basic.uint8_t day_of_week: basic.uint8_t @property def year(self): if self.years_since_1900 is None: return None return 1900 + self.years_since_1900 @year.setter def year(self, years): assert 1900 <= years <= 2155 self.years_since_1900 = years - 1900 class NWK(basic.uint16_t, repr="hex"): @classmethod def convert(cls, data: str) -> NWK: assert 4 * len(data) == cls._bits return cls.deserialize(bytes.fromhex(data)[::-1])[0] class PanId(NWK): pass class ExtendedPanId(EUI64): pass class Group(basic.uint16_t, repr="hex"): pass class NoData: @classmethod def deserialize(cls, data): return cls(), data def serialize(self): return b"" class TimeOfDay(Struct): hours: basic.uint8_t minutes: basic.uint8_t seconds: basic.uint8_t hundredths: basic.uint8_t class _Time(basic.uint32_t): pass class UTCTime(_Time): pass class StandardTime(_Time): """Adjusted for TimeZone but not for daylight saving.""" class LocalTime(_Time): """Standard time adjusted for daylight saving.""" class Relays(basic.LVList, item_type=NWK, length_type=basic.uint8_t): """Relay list for static routing.""" class APSStatus(basic.enum8): # A request has been executed successfully APS_SUCCESS = 0x00 # A transmit request failed since the ASDU is too large and fragmentation # is not supported APS_ASDU_TOO_LONG = 0xA0 # A received fragmented frame could not be defragmented at the current time APS_DEFRAG_DEFERRED = 0xA1 # A received fragmented frame could not be defragmented since the device # does not support fragmentation APS_DEFRAG_UNSUPPORTED = 0xA2 # A parameter value was out of range APS_ILLEGAL_REQUEST = 0xA3 # An APSME-UNBIND.request failed due to the requested binding link not # existing in the binding table APS_INVALID_BINDING = 0xA4 # An APSME-REMOVE-GROUP.request has been issued with a group identifier # that does not appear in the group table APS_INVALID_GROUP = 0xA5 # A parameter value was invalid or out of range APS_INVALID_PARAMETER = 0xA6 # An APSDE-DATA.request requesting acknowledged transmission failed due to # no acknowledgement being received APS_NO_ACK = 0xA7 # An APSDE-DATA.request with a destination addressing mode set to 0x00 # failed due to there being no devices bound to this device APS_NO_BOUND_DEVICE = 0xA8 # An APSDE-DATA.request with a destination addressing mode set to 0x03 # failed due to no corresponding short address found in the address map # table APS_NO_SHORT_ADDRESS = 0xA9 # An APSDE-DATA.request with a destination addressing mode set to 0x00 # failed due to a binding table not being supported on the device APS_NOT_SUPPORTED = 0xAA # An ASDU was received that was secured using a link key APS_SECURED_LINK_KEY = 0xAB # An ASDU was received that was secured using a network key APS_SECURED_NWK_KEY = 0xAC # An APSDE-DATA.request requesting security has resulted in an error # during the corresponding security processing APS_SECURITY_FAIL = 0xAD # An APSME-BIND.request or APSME.ADDGROUP.request issued when the binding # or group tables, respectively, were full APS_TABLE_FULL = 0xAE # An ASDU was received without any security APS_UNSECURED = 0xAF # An APSME-GET.request or APSMESET.request has been issued with an unknown # attribute identifier APS_UNSUPPORTED_ATTRIBUTE = 0xB0 @classmethod def _missing_(cls, value): chained = NWKStatus(value) status = cls._member_type_.__new__(cls, chained.value) status._name_ = chained.name status._value_ = value return status class MACStatus(basic.enum8): # Operation was successful MAC_SUCCESS = 0x00 # Association Status field MAC_PAN_AT_CAPACITY = 0x01 MAC_PAN_ACCESS_DENIED = 0x02 # The frame counter purportedly applied by the originator of the received # frame is invalid MAC_COUNTER_ERROR = 0xDB # The key purportedly applied by the originator of the received frame is # not allowed to be used with that frame type according to the key usage # policy of the recipient MAC_IMPROPER_KEY_TYPE = 0xDC # The security level purportedly applied # by the originator of the # received frame does not meet the minimum security level # required/expected by the recipient for that frame type MAC_IMPROPER_SECURITY_LEVEL = 0xDD # The received frame was purportedly secured using security based on IEEE # Std 802.15.4-2003, and such security is not supported by this standard MAC_UNSUPPORTED_LEGACY = 0xDE # The security purportedly applied by the originator of the received frame # is not supported MAC_UNSUPPORTED_SECURITY = 0xDF # The beacon was lost following a synchronization request MAC_BEACON_LOSS = 0xE0 # A transmission could not take place due to activity on the channel, i.e. # the CSMA-CA mechanism has failed MAC_CHANNEL_ACCESS_FAILURE = 0xE1 # The GTS request has been denied by the PAN coordinator MAC_DENIED = 0xE2 # The attempt to disable the transceiver has failed MAC_DISABLE_TRX_FAILURE = 0xE3 # Cryptographic processing of the received secured frame failed MAC_SECURITY_ERROR = 0xE4 # Either a frame resulting from processing has a length that is greater # than aMaxPHYPacketSize or a requested transaction is too large to fit in # the CAP or GTS MAC_FRAME_TOO_LONG = 0xE5 # The requested GTS transmission failed because the specified GTS either # did not have a transmit GTS direction or was not defined MAC_INVALID_GTS = 0xE6 # A request to purge an MSDU from the transaction queue was made using an # MSDU handle that was not found in the transaction table MAC_INVALID_HANDLE = 0xE7 # A parameter in the primitive is either not supported or is out of the # valid range MAC_INVALID_PARAMETER = 0xE8 # No acknowledgment was received after macMaxFrameRetries MAC_NO_ACK = 0xE9 # A scan operation failed to find any network beacons MAC_NO_BEACON = 0xEA # No response data was available following a request MAC_NO_DATA = 0xEB # The operation failed because a 16-bit short address was not allocated MAC_NO_SHORT_ADDRESS = 0xEC # A receiver enable request was unsuccessful because it could not be # completed within the CAP. @note The enumeration description is not used # in this standard, and it is included only to meet the backwards # compatibility requirements for IEEE Std 802.15.4-2003 MAC_OUT_OF_CAP = 0xED # A PAN identifier conflict has been detected and communicated to the PAN # coordinator MAC_PAN_ID_CONFLICT = 0xEE # A coordinator realignment command has been received MAC_REALIGNMENT = 0xEF # The transaction has expired and its information was discarded MAC_TRANSACTION_EXPIRED = 0xF0 # There is no capacity to store the transaction MAC_TRANSACTION_OVERFLOW = 0xF1 # The transceiver was in the transmitter enabled state when the receiver # was requested to be enabled. @note The enumeration description is not # used in this standard, and it is included only to meet the backwards # compatibility requirements for IEEE Std 802.15.4-2003 MAC_TX_ACTIVE = 0xF2 # The key purportedly used by the originator of the received frame is not # available or, if available, the originating device is not known or is # blacklisted with that particular key MAC_UNAVAILABLE_KEY = 0xF3 # A SET/GET request was issued with the identifier of a PIB attribute that # is not supported MAC_UNSUPPORTED_ATTRIBUTE = 0xF4 # A request to send data was unsuccessful because neither the source # address parameters nor the destination address parameters were present MAC_INVALID_ADDRESS = 0xF5 # A receiver enable request was unsuccessful because it specified a number # of symbols that was longer than the beacon interval MAC_ON_TIME_TOO_LONG = 0xF6 # A receiver enable request was unsuccessful because it could not be # completed within the current superframe and was not permitted to be # deferred until the next superframe MAC_PAST_TIME = 0xF7 # The device was instructed to start sending beacons based on the timing # of the beacon transmissions of its coordinator, but the device is not # currently tracking the beacon of its coordinator MAC_TRACKING_OFF = 0xF8 # An attempt to write to a MAC PIB attribute that is in a table failed # because the specified table index was out of range MAC_INVALID_INDEX = 0xF9 # A scan operation terminated prematurely because the number of PAN # descriptors stored reached an implementation specified maximum MAC_LIMIT_REACHED = 0xFA # A SET/GET request was issued with the identifier of an attribute that is # read only MAC_READ_ONLY = 0xFB # A request to perform a scan operation failed because the MLME was in the # process of performing a previously initiated scan operation MAC_SCAN_IN_PROGRESS = 0xFC # The device was instructed to start sending beacons based on the timing # of the beacon transmissions of its coordinator, but the instructed start # time overlapped the transmission time of the beacon of its coordinator MAC_SUPERFRAME_OVERLAP = 0xFD class NWKStatus(basic.enum8): # A request has been executed successfully NWK_SUCCESS = 0x00 # An invalid or out-of-range parameter has been passed to a primitive from # the next higher layer NWK_INVALID_PARAMETER = 0xC1 # The next higher layer has issued a request that is invalid or cannot be # executed given the current state of the NWK layer NWK_INVALID_REQUEST = 0xC2 # An NLME-JOIN.request has been disallowed NWK_NOT_PERMITTED = 0xC3 # An NLME-NETWORK-FORMATION.request has failed to start a network NWK_STARTUP_FAILURE = 0xC4 # A device with the address supplied to the NLMEDIRECT-JOIN.request is # already present in the neighbor table of the device on which the # NLMEDIRECT-JOIN.request was issued NWK_ALREADY_PRESENT = 0xC5 # Used to indicate that an NLME-SYNC.request has failed at the MAC layer NWK_SYNC_FAILURE = 0xC6 # An NLME-JOIN-DIRECTLY.request has failed because there is no more room # in the neighbor table NWK_NEIGHBOR_TABLE_FULL = 0xC7 # An NLME-LEAVE.request has failed because the device addressed in the # parameter list is not in the neighbor table of the issuing device NWK_UNKNOWN_DEVICE = 0xC8 # An NLME-GET.request or NLME-SET.request has been issued with an unknown # attribute identifier NWK_UNSUPPORTED_ATTRIBUTE = 0xC9 # An NLME-JOIN.request has been issued in an environment where no networks # are detectable NWK_NO_NETWORKS = 0xCA NWK_RESERVED_0xCB = 0xCB # Security processing has been attempted on an outgoing frame, and has # failed because the frame counter has reached its maximum value NWK_NWK_MAX_FRM_COUNTER = 0xCC # Security processing has been attempted on an outgoing frame, and has # failed because no key was available with which to process it NWK_NO_KEY = 0xCD # Security processing has been attempted on an outgoing frame, and has # failed because the security engine produced erroneous output NWK_BAD_CCM_OUTPUT = 0xCE NWK_RESERVED_0xCF = 0xCF # An attempt to discover a route has failed due to a reason other than a # lack of routing capacity NWK_ROUTE_DISCOVERY_FAILED = 0xD0 # An NLDE-DATA.request has failed due to a routing failure on the sending # device or an NLMEROUTE-DISCOVERY.request has failed due to the cause # cited in the accompanying NetworkStatusCode NWK_ROUTE_ERROR = 0xD1 # An attempt to send a broadcast frame or member mode multicast has failed # due to the fact that there is no room in the BTT NWK_BT_TABLE_FULL = 0xD2 # An NLDE-DATA.request has failed due to insufficient buffering available. # A non-member mode multicast frame was discarded pending route discovery NWK_FRAME_NOT_BUFFERED = 0xD3 @classmethod def _missing_(cls, value): chained = MACStatus(value) status = cls._member_type_.__new__(cls, chained.value) status._name_ = chained.name status._value_ = value return status class AddrMode(basic.enum8): """Addressing mode.""" Group = 0x01 NWK = 0x02 IEEE = 0x03 Broadcast = 0x0F class Addressing: """Deprecated, only present for backwards compatibility.""" Group = AddrMode NWK = AddrMode IEEE = AddrMode Broadcast = AddrMode @dataclasses.dataclass class AddrModeAddress(BaseDataclassMixin): """Address mode and address.""" addr_mode: AddrMode address: NWK | Group | EUI64 | BroadcastAddress | None def __post_init__(self) -> None: if self.addr_mode is not None and self.address is not None: self.address = { AddrMode.Group: Group, AddrMode.NWK: NWK, AddrMode.IEEE: EUI64, AddrMode.Broadcast: BroadcastAddress, }[self.addr_mode](self.address) def __hash__(self) -> int: return hash((self.addr_mode, self.address)) class TransmitOptions(enum.Flag): NONE = 0 ACK = 1 << 0 APS_Encryption = 1 << 1 FORCE_ROUTE_DISCOVERY = 1 << 2 class PacketPriority(enum.IntEnum): """Packet priority""" CRITICAL = 2 HIGH = 1 NORMAL = 0 LOW = -1 @dataclasses.dataclass class ZigbeePacket(BaseDataclassMixin): """Container for the information in an incoming or outgoing ZDO or ZCL packet. The radio library is expected to fill this object in with all received data and pass it to zigpy for every type of packet. """ timestamp: datetime = dataclasses.field( compare=False, default_factory=lambda: datetime.now(timezone.utc) ) # Higher priority will try to be sent before lower priority: int = dataclasses.field(default=0) # Set to `None` when the packet is outgoing src: AddrModeAddress | None = dataclasses.field(default=None) src_ep: basic.uint8_t | None = dataclasses.field(default=None) # Set to `None` when the packet is incoming dst: AddrModeAddress | None = dataclasses.field(default=None) dst_ep: basic.uint8_t | None = dataclasses.field(default=None) # If the radio supports it, a source route for the packet source_route: list[NWK] | None = dataclasses.field(default=None) extended_timeout: bool = dataclasses.field(default=False) tsn: basic.uint8_t = dataclasses.field(default=0x00) profile_id: basic.uint16_t = dataclasses.field(default=0x0000) cluster_id: basic.uint16_t = dataclasses.field(default=0x0000) # Any serializable object data: basic.SerializableBytes = dataclasses.field( default_factory=basic.SerializableBytes ) # Options for outgoing packets tx_options: TransmitOptions = dataclasses.field(default=TransmitOptions.NONE) radius: basic.uint8_t = dataclasses.field(default=0) non_member_radius: basic.uint8_t = dataclasses.field(default=0) # Options for incoming packets lqi: basic.uint8_t | None = dataclasses.field(default=None) rssi: basic.int8s | None = dataclasses.field(default=None) def __hash__(self) -> int: return hash( ( self.timestamp, self.src, self.src_ep, self.dst, self.dst_ep, self.source_route, self.extended_timeout, self.tsn, self.profile_id, self.cluster_id, self.data, self.tx_options, self.radius, self.non_member_radius, self.lqi, self.rssi, self.priority, ) ) @dataclasses.dataclass(frozen=True) class NetworkBeacon(BaseDataclassMixin): pan_id: PanId extended_pan_id: EUI64 channel: basic.uint8_t permit_joining: bool stack_profile: basic.uint8_t nwk_update_id: basic.uint8_t lqi: basic.uint8_t # Migrate to kwarg-only once we drop 3.9 src: NWK | None = None rssi: basic.int8s | None = None depth: basic.uint8_t | None = None router_capacity: bool | None = None device_capacity: bool | None = None protocol_version: basic.uint8_t | None = None @dataclasses.dataclass(frozen=True) class CapturedPacket(BaseDataclassMixin): timestamp: datetime rssi: float lqi: basic.uint8_t channel: basic.uint8_t data: bytes def compute_fcs(self) -> bytes: crc = 0x0000 for c in self.data: q = (crc ^ c) & 15 # Do low-order 4 bits crc = (crc // 16) ^ (q * 0x1081) q = (crc ^ (c // 16)) & 15 # And high 4 bits crc = (crc // 16) ^ (q * 0x1081) return crc.to_bytes(2, "little") zigpy-0.80.1/zigpy/types/struct.py000066400000000000000000000425061501451476000171600ustar00rootroot00000000000000from __future__ import annotations import dataclasses import inspect import typing from typing_extensions import Self import zigpy.types as t NoneType = type(None) # To make sure mypy is aware that `IntStruct` is technically a mixin, we need to # convince it that it really is an integer at runtime if typing.TYPE_CHECKING: IntMixin = int else: IntMixin = object class ListSubclass(list): # So we can call `setattr()` on it pass class EmptyObject: # So we can call `setattr()` on it pass @dataclasses.dataclass(frozen=True) class StructField: name: str | None = None type: type = None requires: typing.Callable[[Struct], bool] | None = dataclasses.field( default=None, repr=False ) optional: bool | None = False repr: typing.Callable[[typing.Any], str] | None = dataclasses.field( default=repr, repr=False ) def replace(self, **kwargs) -> StructField: return dataclasses.replace(self, **kwargs) def _convert_type(self, value): if value is None or isinstance(value, self.type): return value try: return self.type(value) except Exception as e: # noqa: BLE001 raise ValueError( f"Failed to convert {self.name}={value!r} from type" f" {type(value)} to {self.type}" ) from e class Struct: @classmethod def _real_cls(cls) -> type: # The "Optional" subclass is dynamically created and breaks types. # We have to use a little introspection to find our real class. return next(c for c in cls.__mro__ if c.__name__ != "Optional") def __init_subclass__(cls) -> None: super().__init_subclass__() # We generate fields up here to fail early and cache it cls.fields = cls._real_cls()._get_fields() cls._signature = inspect.Signature( parameters=[ inspect.Parameter( name=f.name, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None, annotation=f.type, ) for f in cls.fields ] ) # Check to see if the Struct is also an integer if next( ( c for c in cls.__mro__[1:] if issubclass(c, t.FixedIntType) and not issubclass(c, Struct) ), None, ) is not None and not issubclass(cls, IntStruct): raise TypeError("Integer structs must be subclasses of `IntStruct`") cls._hash = -1 cls._frozen = False def __new__(cls: type[Self], *args, **kwargs) -> Self: cls = cls._real_cls() # noqa: PLW0642 if len(args) == 1 and isinstance(args[0], cls): # Like a copy constructor if kwargs: raise ValueError(f"Cannot use copy constructor with kwargs: {kwargs!r}") kwargs = args[0].as_dict() args = () # Pretend our signature is `__new__(cls, p1: t1, p2: t2, ...)` bound = cls._signature.bind(*args, **kwargs) bound.apply_defaults() instance = super().__new__(cls) # Set each attributes on the instance for name, value in bound.arguments.items(): field = getattr(cls.fields, name) setattr(instance, name, field._convert_type(value)) return instance @classmethod def _get_fields(cls) -> list[StructField]: fields = ListSubclass() # We need both to throw type errors in case a field is not annotated annotations = typing.get_type_hints(cls._real_cls()) # Make sure every `StructField` is annotated for name in vars(cls._real_cls()): value = getattr(cls, name) if isinstance(value, StructField) and name not in annotations: raise TypeError( f"Field {name!r}={value} must have some annotation." f" Use `None` if it is specified in the `StructField`." ) # XXX: Python doesn't provide a simple way to get all defined attributes *and* # order them with respect to annotation-only fields. # Every struct field must be annotated. for name, annotation in annotations.items(): field = getattr(cls, name, StructField()) if not isinstance(field, StructField): continue field = field.replace(name=name) # An annotation of `None` means to use the field's type if annotation is not NoneType: if field.type is not None and field.type != annotation: raise TypeError( f"Field {name!r} type annotation conflicts with provided type:" f" {annotation} != {field.type}" ) field = field.replace(type=annotation) elif field.type is None: raise TypeError(f"Field {name!r} has no type") fields.append(field) setattr(fields, field.name, field) return fields def assigned_fields(self, *, strict=False) -> list[tuple[StructField, typing.Any]]: assigned_fields = ListSubclass() for field in self.fields: value = getattr(self, field.name) # Ignore fields that aren't required if field.requires is not None and not field.requires(self): continue # Missing fields cause an error if strict if value is None and not field.optional: if strict: raise ValueError( f"Value for field {field.name!r} is required: {self!r}" ) else: # Python bug, the following `continue` is never covered continue # pragma: no cover assigned_fields.append((field, value)) setattr(assigned_fields, field.name, (field, value)) return assigned_fields @classmethod def from_dict(cls: type[Self], obj: dict[str, typing.Any]) -> Self: instance = cls() for key, value in obj.items(): field = getattr(cls.fields, key) if issubclass(field.type, Struct): setattr(instance, field.name, field.type.from_dict(value)) else: setattr(instance, field.name, value) return instance def as_dict( self, *, skip_missing: bool = False, recursive: bool = False ) -> dict[str, typing.Any]: d = {} for f in self.fields: value = getattr(self, f.name) if value is None and skip_missing: continue elif recursive and isinstance(value, Struct): d[f.name] = value.as_dict( skip_missing=skip_missing, recursive=recursive ) else: d[f.name] = value return d def as_tuple(self, *, skip_missing: bool = False) -> tuple: return tuple(self.as_dict(skip_missing=skip_missing).values()) def serialize(self) -> bytes: chunks = [] bit_offset = 0 bitfields = [] for field, value in self.assigned_fields(strict=True): if value is None and field.optional: continue value = field._convert_type(value) # All integral types are compacted into one chunk, unless they start and end # on a byte boundary. if issubclass(field.type, t.FixedIntType) and not ( value._bits % 8 == 0 and bit_offset % 8 == 0 ): bit_offset += value._bits bitfields.append(value) # Serialize the current segment of bitfields once we reach a boundary if bit_offset % 8 == 0: chunks.append(t.Bits.from_bitfields(bitfields).serialize()) bitfields = [] continue elif bitfields: raise ValueError( f"Segment of bitfields did not terminate on a byte boundary: " f" {bitfields}" ) chunks.append(value.serialize()) if bitfields: raise ValueError( f"Trailing segment of bitfields did not terminate on a byte boundary: " f" {bitfields}" ) return b"".join(chunks) @staticmethod def _deserialize_internal( fields: list[StructField], data: bytes ) -> tuple[dict[str, typing.Any], bytes]: bit_length = 0 bitfields = [] result = {} # We need to create a temporary instance to call the field's `requires` method, # which expects a struct-like object temp_instance = EmptyObject() for field in fields: setattr(temp_instance, field.name, None) for field in fields: if (field.requires is not None and not field.requires(temp_instance)) or ( not data and field.optional ): continue if issubclass(field.type, t.FixedIntType) and not ( field.type._bits % 8 == 0 and bit_length % 8 == 0 ): bit_length += field.type._bits bitfields.append(field) if bit_length % 8 == 0: if len(data) < bit_length // 8: raise ValueError(f"Data is too short to contain {bitfields}") bits, _ = t.Bits.deserialize(data[: bit_length // 8]) data = data[bit_length // 8 :] for f in bitfields: value, bits = f.type.from_bits(bits) result[f.name] = value setattr(temp_instance, f.name, value) assert not bits bit_length = 0 bitfields = [] continue elif bitfields: raise ValueError( f"Segment of bitfields did not terminate on a byte boundary: " f" {bitfields}" ) value, data = field.type.deserialize(data) result[field.name] = value setattr(temp_instance, field.name, value) if bitfields: raise ValueError( f"Trailing segment of bitfields did not terminate on a byte boundary: " f" {bitfields}" ) return result, data @classmethod def deserialize(cls: type[Self], data: bytes) -> tuple[Self, bytes]: fields, data = cls._deserialize_internal(cls.fields, data) return cls(**fields), data def replace(self, **kwargs: dict[str, typing.Any]) -> Struct: d = self.as_dict().copy() d.update(kwargs) instance = type(self)(**d) if self._frozen: instance = instance.freeze() return instance def __eq__(self, other: object) -> bool: if not isinstance(self, type(other)) and not isinstance(other, type(self)): return NotImplemented return self.as_dict() == other.as_dict() def _repr_extra_parts(self) -> list[str]: extra_parts = [] if self._frozen: extra_parts.append("frozen") return extra_parts def __repr__(self) -> str: fields = [] # Assigned fields are displayed as `field=value` for f, v in self.assigned_fields(): fields.append(f"{f.name}={f.repr(v)}") cls = type(self) # Properties are displayed as `*prop=value` for attr in dir(cls): cls_attr = getattr(cls, attr) if not isinstance(cls_attr, property) or hasattr(Struct, attr): continue value = getattr(self, attr) if value is not None: fields.append(f"*{attr}={value!r}") extra_parts = self._repr_extra_parts() if extra_parts: extra = f"<{', '.join(extra_parts)}>" else: extra = "" return f"{type(self).__name__}{extra}({', '.join(fields)})" @property def is_valid(self) -> bool: try: self.serialize() except ValueError: return False else: return True def matches(self, other: Struct) -> bool: if not isinstance(self, type(other)) and not isinstance(other, type(self)): return False for field in self.fields: actual = getattr(self, field.name) expected = getattr(other, field.name) if expected is None: continue if isinstance(expected, Struct): if not actual.matches(expected): return False elif actual != expected: return False return True def __setattr__(self, name: str, value: typing.Any) -> None: if self._frozen: raise AttributeError("Frozen structs are immutable, use `replace` instead") return super().__setattr__(name, value) def __hash__(self) -> int: if self._frozen: return self._hash # XXX: This implementation is incorrect only for a single case: # `isinstance(struct, collections.abc.Hashable)` always returns True raise TypeError(f"Unhashable type: {type(self)}") def freeze(self) -> Self: """Freeze a Struct instance, making it hashable and immutable.""" if self._frozen: return self kwargs = {} for f in self.fields: value = getattr(self, f.name) if isinstance(value, Struct): value = value.freeze() kwargs[f.name] = value cls = self._real_cls() instance = cls(**kwargs) instance._hash = hash((cls, tuple(kwargs.items()))) instance._frozen = True return instance class IntStruct(Struct, IntMixin): def __init_subclass__(cls) -> None: super().__init_subclass__() try: cls._int_type: type[t.FixedIntType] = next( c for c in cls.__mro__[1:] if issubclass(c, t.FixedIntType) and not issubclass(c, Struct) ) except StopIteration: raise TypeError("Integer structs must be an integer subclasses") from None def __new__( cls: type[Self], *args, _underlying_int: int | None = None, **kwargs ) -> Self: # Integers are immutable in Python so we need to know, at creation time, what # the integer value of this object will be. This means that these structs *must* # also be immutable. cls = cls._real_cls() # noqa: PLW0642 underlying_int = _underlying_int if len(args) > 1: raise TypeError(f"{cls} takes no positional arguments") # Like a copy constructor if len(args) == 1: if not isinstance(args[0], int) or kwargs: raise TypeError( f"{cls} can only be constructed from an integer" f" or with just keyword arguments" ) underlying_int = args[0] data = cls._int_type(underlying_int).serialize() kwargs, _ = cls._deserialize_internal(cls.fields, data) args = () if underlying_int is None: # To compute the underlying integer, we create a temp instance and serialize temp_instance = super(Struct, cls).__new__(cls, 0) # Set the correct attributes on the instance so we can serialize bound = cls._signature.bind(*args, **kwargs) bound.apply_defaults() for name, value in bound.arguments.items(): field = getattr(cls.fields, name) setattr(temp_instance, name, value) # Finally, serialize underlying_int, rest = cls._int_type.deserialize(temp_instance.serialize()) assert not rest # Pretend we were called with the correct kwargs args = () kwargs = temp_instance.as_dict() bound = cls._signature.bind(*args, **kwargs) bound.apply_defaults() instance = super(Struct, cls).__new__(cls, underlying_int) # Set attributes on the final instance for name, value in bound.arguments.items(): field = getattr(cls.fields, name) setattr(instance, name, field._convert_type(value)) # Freeze it instance._frozen = True return instance __hash__ = int.__hash__ def _repr_extra_parts(self) -> list[str]: # We override this method to omit the unnecessary `` return [f"{self._int_type(int(self))._hex_repr()}"] def __eq__(self, other: object) -> bool: if not isinstance(other, int): raise NotImplementedError return int(self) == int(other) @classmethod def deserialize(cls: type[Self], data: bytes) -> tuple[Self, bytes]: fields, remaining = cls._deserialize_internal(cls.fields, data) underlying_int, _ = cls._int_type.deserialize( data[: len(data) - len(remaining)] ) # We overload deserialization to avoid an unnecessary serialize-deserialize # during `cls.__new__` to compute the underlying integer, since we have all the # data here already return cls(_underlying_int=underlying_int, **fields), remaining zigpy-0.80.1/zigpy/typing.py000066400000000000000000000025051501451476000157750ustar00rootroot00000000000000"""Typing helpers for Zigpy.""" from __future__ import annotations import enum from typing import TYPE_CHECKING, Any, Union ConfigType = dict[str, Any] # pylint: disable=invalid-name ClusterType = "Cluster" ControllerApplicationType = "ControllerApplication" CustomClusterType = "CustomCluster" CustomDeviceType = "CustomDevice" CustomEndpointType = "CustomEndpoint" DeviceType = "Device" EndpointType = "Endpoint" ZDOType = "ZDO" AddressingMode = "AddressingMode" class UndefinedType(enum.Enum): """Singleton type for use with not set sentinel values.""" _singleton = 0 UNDEFINED = UndefinedType._singleton # noqa: SLF001 if TYPE_CHECKING: import zigpy.application import zigpy.device import zigpy.endpoint import zigpy.quirks import zigpy.types import zigpy.zcl import zigpy.zdo ClusterType = zigpy.zcl.Cluster ControllerApplicationType = zigpy.application.ControllerApplication CustomClusterType = zigpy.quirks.CustomCluster CustomDeviceType = zigpy.quirks.BaseCustomDevice CustomEndpointType = zigpy.quirks.CustomEndpoint DeviceType = zigpy.device.Device EndpointType = zigpy.endpoint.Endpoint ZDOType = zigpy.zdo.ZDO AddressingMode = Union[ zigpy.types.Addressing.Group, zigpy.types.Addressing.IEEE, zigpy.types.Addressing.NWK, ] zigpy-0.80.1/zigpy/util.py000066400000000000000000000375001501451476000154430ustar00rootroot00000000000000from __future__ import annotations import abc import asyncio import functools import inspect import itertools import logging import traceback import types import typing import warnings from crccheck.crc import CrcX25 from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import ECB from typing_extensions import Self from zigpy.datastructures import DynamicBoundedSemaphore # noqa: F401 from zigpy.exceptions import ControllerException, ZigbeeException import zigpy.types as t LOGGER = logging.getLogger(__name__) _T = typing.TypeVar("_T") class ListenableMixin: def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._listeners: dict[int, tuple[typing.Callable, bool]] = {} def _add_listener(self, listener: typing.Any, include_context: bool) -> int: id_ = id(listener) while id_ in self._listeners: id_ += 1 self._listeners[id_] = (listener, include_context) return id_ def add_listener(self, listener: typing.Any) -> int: return self._add_listener(listener, include_context=False) def add_context_listener(self, listener: CatchingTaskMixin) -> int: return self._add_listener(listener, include_context=True) def remove_listener(self, listener: typing.Any) -> None: for id_, (attached_listener, _) in self._listeners.items(): if attached_listener is listener: del self._listeners[id_] break def listener_event(self, method_name: str, *args) -> list[typing.Any | None]: result = [] for listener, include_context in tuple(self._listeners.values()): method = getattr(listener, method_name, None) if method is None: continue try: if include_context: result.append(method(self, *args)) else: result.append(method(*args)) except Exception as e: # noqa: BLE001 LOGGER.debug( "Error calling listener %r with args %r", method, args, exc_info=e ) return result async def async_event(self, method_name: str, *args) -> list[typing.Any]: tasks = [] for listener, include_context in tuple(self._listeners.values()): method = getattr(listener, method_name, None) if method is None: continue if include_context: tasks.append(method(self, *args)) else: tasks.append(method(*args)) results = [] for result in await asyncio.gather(*tasks, return_exceptions=True): if isinstance(result, Exception): LOGGER.debug( "Error calling listener %r with args %r", method, args, exc_info=result, ) else: results.append(result) return results class LocalLogMixin: @abc.abstractmethod def log(self, lvl: int, msg: str, *args, **kwargs): # pragma: no cover pass def _log(self, lvl: int, msg: str, *args, **kwargs) -> None: return self.log(lvl, msg, *args, stacklevel=4, **kwargs) def exception(self, msg, *args, **kwargs): return self._log(logging.ERROR, msg, *args, **kwargs) def debug(self, msg: str, *args, **kwargs) -> None: return self._log(logging.DEBUG, msg, *args, **kwargs) def info(self, msg: str, *args, **kwargs) -> None: return self._log(logging.INFO, msg, *args, **kwargs) def warning(self, msg: str, *args, **kwargs) -> None: return self._log(logging.WARNING, msg, *args, **kwargs) def error(self, msg, *args, **kwargs): return self._log(logging.ERROR, msg, *args, **kwargs) async def retry( func: typing.Callable[[], typing.Awaitable[typing.Any]], retry_exceptions: typing.Iterable[BaseException], tries: int = 3, delay: float = 0.1, ) -> typing.Any: """Retry a function in case of exception Only exceptions in `retry_exceptions` will be retried. """ while True: LOGGER.debug("Tries remaining: %s", tries) try: return await func() except retry_exceptions: if tries <= 1: raise tries -= 1 await asyncio.sleep(delay) def retryable( retry_exceptions: typing.Iterable[BaseException], tries: int = 1, delay: float = 0.1 ) -> typing.Callable: """Return a decorator which makes a function able to be retried. Only exceptions in `retry_exceptions` will be retried. """ def decorator(func: typing.Callable) -> typing.Callable: nonlocal tries, delay @functools.wraps(func) def wrapper(*args, **kwargs): if tries <= 1: return func(*args, **kwargs) return retry( functools.partial(func, *args, **kwargs), retry_exceptions, tries=tries, delay=delay, ) return wrapper return decorator retryable_request = functools.partial( retryable, (ZigbeeException, asyncio.TimeoutError) ) def aes_mmo_hash_update(length: int, result: bytes, data: bytes) -> tuple[int, bytes]: block_size = AES.block_size // 8 while len(data) >= block_size: block = bytes(data[:block_size]) # Encrypt aes = Cipher(AES(bytes(result)), ECB()).encryptor() result = bytearray(aes.update(block) + aes.finalize()) # XOR plaintext into ciphertext for i in range(block_size): result[i] ^= block[i] data = data[block_size:] length += block_size return (length, result) def aes_mmo_hash(data: bytes) -> t.KeyData: block_size = AES.block_size // 8 result_len = 0 remaining_length = 0 length = len(data) result = bytearray([0] * block_size) temp = bytearray([0] * block_size) if data and length > 0: remaining_length = length & (block_size - 1) if length >= block_size: # Mask out the lower byte since hash update will hash # everything except the last piece, if the last piece # is less than 16 bytes. hashed_length = length & ~(block_size - 1) (result_len, result) = aes_mmo_hash_update(result_len, result, data) data = data[hashed_length:] for i in range(remaining_length): temp[i] = data[i] # Per the spec, Concatenate a 1 bit followed by all zero bits # (previous memset() on temp[] set the rest of the bits to zero) temp[remaining_length] = 0x80 result_len += remaining_length # If appending the bit string will push us beyond the 16-byte boundary # we must hash that block and append another 16-byte block. if (block_size - remaining_length) < 3: (result_len, result) = aes_mmo_hash_update(result_len, result, temp) # Since this extra data is due to the concatenation, # we remove that length. We want the length of data only # and not the padding. result_len -= block_size temp = bytearray([0] * block_size) bit_size = result_len * 8 temp[block_size - 2] = (bit_size >> 8) & 0xFF temp[block_size - 1] = (bit_size) & 0xFF (result_len, result) = aes_mmo_hash_update(result_len, result, temp) return t.KeyData(result) def convert_install_code(code: bytes) -> t.KeyData: if len(code) not in (8, 10, 14, 18): return None real_crc = bytes(code[-2:]) crc = CrcX25() crc.process(code[:-2]) if real_crc != crc.finalbytes(byteorder="little"): return None return aes_mmo_hash(code) T = typing.TypeVar("T") class Request(typing.Generic[T]): """Request context manager.""" def __init__(self, pending: dict, sequence: T) -> None: """Init context manager for requests.""" self._pending = pending self._result: asyncio.Future = asyncio.Future() self._sequence = sequence @property def result(self) -> asyncio.Future: return self._result @property def sequence(self) -> T: """Request sequence.""" return self._sequence def __enter__(self) -> Self: """Return context manager.""" self._pending[self.sequence] = self return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_traceback: types.TracebackType | None, ) -> bool: """Clean up pending on exit.""" if not self.result.done(): self.result.cancel() self._pending.pop(self.sequence) return not exc_type class Requests(dict, typing.Generic[T]): def new(self, sequence: T) -> Self[T]: """Wrap new request into a context manager.""" if sequence in self: LOGGER.debug("Duplicate %s TSN: pending %s", sequence, self) raise ControllerException(f"Duplicate TSN: {sequence}") return Request(self, sequence) class CatchingTaskMixin(LocalLogMixin): """Allow creating tasks suppressing exceptions.""" _tasks: set[asyncio.Future[typing.Any]] = set() def create_catching_task( self, target: typing.Coroutine, exceptions: type[Exception] | tuple | None = None, name: str | None = None, ) -> None: """Create a task.""" task = asyncio.get_running_loop().create_task( self.catching_coro(target, exceptions), name=name ) self._tasks.add(task) task.add_done_callback(self._tasks.remove) async def catching_coro( self, target: typing.Coroutine, exceptions: type[Exception] | tuple | None = None, ) -> typing.Any: """Wrap a target coro and catch specified exceptions.""" if exceptions is None: exceptions = (asyncio.TimeoutError, ZigbeeException) try: return await target except exceptions: pass except (Exception, asyncio.CancelledError): # pylint: disable=broad-except # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) self.exception("%s", exc_msg) return None def deprecated(message: str) -> typing.Callable[[typing.Callable], typing.Callable]: """Decorator that emits a DeprecationWarning when the function or property is accessed.""" def decorator(function: typing.Callable) -> typing.Callable: @functools.wraps(function) def replacement(*args, **kwargs): warnings.warn( f"{function.__name__} is deprecated: {message}", DeprecationWarning ) return function(*args, **kwargs) return replacement return decorator def deprecated_attrs( mapping: dict[str, typing.Any], ) -> typing.Callable[[str], typing.Any]: """Create a module-level `__getattr__` function that remaps deprecated objects.""" def __getattr__(name: str) -> typing.Any: if name not in mapping: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") replacement = mapping[name] warnings.warn( ( f"`{__name__}.{name}` has been renamed to" f" `{__name__}.{replacement.__name__}`" ), DeprecationWarning, ) return replacement return __getattr__ def pick_optimal_channel( channel_energy: dict[int, float], channels: t.Channels = t.Channels.from_channel_list([11, 15, 20, 25]), *, kernel: list[float] = (0.1, 0.5, 1.0, 0.5, 0.1), channel_penalty: dict[int, float] = { 11: 2.0, # ZLL but WiFi interferes 12: 3.0, 13: 3.0, 14: 3.0, 15: 1.0, # ZLL 16: 3.0, 17: 3.0, 18: 3.0, 19: 3.0, 20: 1.0, # ZLL 21: 3.0, 22: 3.0, 23: 3.0, 24: 3.0, 25: 1.0, # ZLL 26: 2.0, # Not ZLL but WiFi can interfere in some regions }, ) -> int: """Scans all channels and picks the best one from the given mask.""" assert len(kernel) % 2 == 1 kernel_width = (len(kernel) - 1) // 2 # Scan all channels even if we're restricted to picking among a few, since # nearby channels will affect our decision assert set(channel_energy.keys()) == set(t.Channels.ALL_CHANNELS) # type: ignore[call-overload] # We don't know energies above channel 26 or below 11. Assume the scan results # just continue indefinitely with the last-seen value. ext_energies = ( [channel_energy[11]] * kernel_width + [channel_energy[c] for c in t.Channels.ALL_CHANNELS] + [channel_energy[26]] * kernel_width ) # Incorporate the energies of nearby channels into our calculation by performing # a discrete convolution with the provided kernel. convolution = ext_energies[:] for i in range(len(ext_energies)): for j in range(-kernel_width, kernel_width + 1): if 0 <= i + j < len(convolution): convolution[i + j] += ext_energies[i] * kernel[kernel_width + j] # Crop off the extended bounds, leaving us with an array of the original size convolution = convolution[kernel_width:-kernel_width] # Incorporate a penalty to avoid specific channels unless absolutely necessary. # Adding `1` ensures the score is positive and the channel penalty gets applied even # when the reported LQI is zero. scores = { c: (1 + convolution[c - 11]) * channel_penalty.get(c, 1.0) for c in t.Channels.ALL_CHANNELS } optimal_channel = min(channels, key=lambda c: scores[c]) LOGGER.info("Optimal channel is %s", optimal_channel) LOGGER.debug("Channel scores: %s", scores) return optimal_channel class Singleton: """Singleton class.""" def __init__(self, name: str) -> None: self.name = name def __repr__(self) -> str: return f"" def __hash__(self) -> int: return hash(self.name) def filter_relays(relays: list[int]) -> list[int]: """Filter out invalid relays.""" filtered_relays = [] # BUG: relays sometimes include 0x0000 or duplicate entries for relay in relays: if relay != 0x0000 and relay not in filtered_relays: filtered_relays.append(relay) return filtered_relays def combine_concurrent_calls( function: typing.Callable[ ..., typing.Coroutine[typing.Any, typing.Any, typing.Any] ], ) -> typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, typing.Any]]: """Decorator that allows concurrent calls to expensive coroutines to share a result.""" tasks: dict[tuple, asyncio.Task] = {} signature = inspect.signature(function) @functools.wraps(function) async def replacement(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: bound = signature.bind(*args, **kwargs) bound.apply_defaults() # XXX: all args and kwargs are assumed to be hashable key = tuple(bound.arguments.items()) if key in tasks: return await tasks[key] tasks[key] = asyncio.create_task(function(*args, **kwargs)) try: return await tasks[key] finally: assert tasks[key].done() del tasks[key] return replacement async def async_iterate_in_chunks( iterable: typing.Iterable[_T], chunk_size: int ) -> typing.AsyncGenerator[list[_T], None]: """Safely iterate over a synchronous iterable in chunks.""" loop = asyncio.get_running_loop() iterator = iter(iterable) while True: chunk = await loop.run_in_executor( None, list, itertools.islice(iterator, chunk_size) ) if not chunk: break yield chunk zigpy-0.80.1/zigpy/zcl/000077500000000000000000000000001501451476000146775ustar00rootroot00000000000000zigpy-0.80.1/zigpy/zcl/__init__.py000066400000000000000000001107001501451476000170070ustar00rootroot00000000000000from __future__ import annotations import collections from collections.abc import Iterable, Sequence from datetime import datetime, timezone import enum import functools import itertools import logging import types from typing import TYPE_CHECKING, Any import warnings from zigpy import util from zigpy.const import APS_REPLY_TIMEOUT import zigpy.types as t from zigpy.typing import AddressingMode, EndpointType from zigpy.zcl import foundation from zigpy.zcl.foundation import BaseAttributeDefs, BaseCommandDefs if TYPE_CHECKING: from zigpy.appdb import PersistingListener from zigpy.endpoint import Endpoint LOGGER = logging.getLogger(__name__) def convert_list_schema( schema: Sequence[type], command_id: int, direction: foundation.Direction ) -> type[t.Struct]: schema_dict = {} for i, param_type in enumerate(schema, start=1): name = f"param{i}" real_type = next(c for c in param_type.__mro__ if c.__name__ != "Optional") if real_type is not param_type: name += "?" schema_dict[name] = real_type temp = foundation.ZCLCommandDef( schema=schema_dict, direction=direction, id=command_id, name="schema", ) return temp.with_compiled_schema().schema class ClusterType(enum.IntEnum): Server = 0 Client = 1 class Cluster(util.ListenableMixin, util.CatchingTaskMixin): """A cluster on an endpoint""" class AttributeDefs(BaseAttributeDefs): pass class ServerCommandDefs(BaseCommandDefs): pass class ClientCommandDefs(BaseCommandDefs): pass # Custom clusters for quirks subclass Cluster but should not be stored in any global # registries, since they're device-specific and collide with existing clusters. _skip_registry: bool = False # Most clusters are identified by a single cluster ID cluster_id: t.uint16_t = None # Clusters are accessible by name from their endpoint as an attribute ep_attribute: str = None # Manufacturer specific clusters exist between 0xFC00 and 0xFFFF. This exists solely # to remove the need to create 1024 "ManufacturerSpecificCluster" instances. cluster_id_range: tuple[t.uint16_t, t.uint16_t] = None # Deprecated: clusters contain attributes and both client and server commands attributes: dict[int, foundation.ZCLAttributeDef] = {} client_commands: dict[int, foundation.ZCLCommandDef] = {} server_commands: dict[int, foundation.ZCLCommandDef] = {} attributes_by_name: dict[str, foundation.ZCLAttributeDef] = {} commands_by_name: dict[str, foundation.ZCLCommandDef] = {} # Internal caches and indices _registry: dict = {} _registry_range: dict = {} def __init_subclass__(cls) -> None: if cls.cluster_id is not None: cls.cluster_id = t.ClusterId(cls.cluster_id) # Compile the old command definitions for commands in [cls.server_commands, cls.client_commands]: for command_id, command in list(commands.items()): if isinstance(command, tuple): # Backwards compatibility with old command tuples name, schema, direction = command command = foundation.ZCLCommandDef( id=command_id, name=name, schema=convert_list_schema(schema, command_id, direction), direction=direction, ) command = command.replace(id=command_id).with_compiled_schema() commands[command.id] = command # Compile the old attribute definitions for attr_id, attr in list(cls.attributes.items()): if isinstance(attr, tuple): if len(attr) == 2: attr_name, attr_type = attr attr_manuf_specific = False else: attr_name, attr_type, attr_manuf_specific = attr attr = foundation.ZCLAttributeDef( id=attr_id, name=attr_name, type=attr_type, is_manufacturer_specific=attr_manuf_specific, ) else: attr = attr.replace(id=attr_id) cls.attributes[attr.id] = attr.replace(id=attr_id) # Create new definitions from the old-style definitions if cls.attributes and "AttributeDefs" not in cls.__dict__: cls.AttributeDefs = types.new_class( name="AttributeDefs", bases=(BaseAttributeDefs,), ) for attr in cls.attributes.values(): setattr(cls.AttributeDefs, attr.name, attr) if cls.server_commands and "ServerCommandDefs" not in cls.__dict__: cls.ServerCommandDefs = types.new_class( name="ServerCommandDefs", bases=(BaseCommandDefs,), ) for command in cls.server_commands.values(): setattr(cls.ServerCommandDefs, command.name, command) if cls.client_commands and "ClientCommandDefs" not in cls.__dict__: cls.ClientCommandDefs = types.new_class( name="ClientCommandDefs", bases=(BaseCommandDefs,), ) for command in cls.client_commands.values(): setattr(cls.ClientCommandDefs, command.name, command) # Check the old definitions for duplicates for old_defs in [cls.attributes, cls.server_commands, cls.client_commands]: counts = collections.Counter(d.name for d in old_defs.values()) if len(counts) != sum(counts.values()): duplicates = [n for n, c in counts.items() if c > 1] raise TypeError(f"Duplicate definitions exist for {duplicates}") # Populate the `name` attribute of every definition for defs in (cls.ServerCommandDefs, cls.ClientCommandDefs, cls.AttributeDefs): for name in dir(defs): definition = getattr(defs, name) if isinstance( definition, (foundation.ZCLCommandDef, foundation.ZCLAttributeDef), ): if definition.name is None: object.__setattr__(definition, "name", name) elif definition.name != name: raise TypeError( f"Definition name {definition.name!r} does not match" f" attribute name {name!r}" ) # Compile the schemas for defs in (cls.ServerCommandDefs, cls.ClientCommandDefs): for name in dir(defs): definition = getattr(defs, name) if isinstance(definition, foundation.ZCLCommandDef): setattr(defs, definition.name, definition.with_compiled_schema()) # Recreate the old structures using the new-style definitions cls.attributes = {attr.id: attr for attr in cls.AttributeDefs} cls.client_commands = {cmd.id: cmd for cmd in cls.ClientCommandDefs} cls.server_commands = {cmd.id: cmd for cmd in cls.ServerCommandDefs} cls.attributes_by_name = {attr.name: attr for attr in cls.AttributeDefs} all_cmds: Iterable[foundation.ZCLCommandDef] = itertools.chain( cls.ClientCommandDefs, cls.ServerCommandDefs ) cls.commands_by_name = {cmd.name: cmd for cmd in all_cmds} if cls._skip_registry: return if cls.cluster_id is not None: cls._registry[cls.cluster_id] = cls if cls.cluster_id_range is not None: cls._registry_range[cls.cluster_id_range] = cls def __init__(self, endpoint: EndpointType, is_server: bool = True) -> None: self._endpoint: EndpointType = endpoint self._attr_cache: dict[int, Any] = {} self._attr_last_updated: dict[int, datetime] = {} self.unsupported_attributes: set[int | str] = set() self._listeners = {} self._type: ClusterType = ( ClusterType.Server if is_server else ClusterType.Client ) @property def attridx(self): warnings.warn( "`attridx` has been replaced by `attributes_by_name`", DeprecationWarning ) return self.attributes_by_name def find_attribute(self, name_or_id: int | str) -> foundation.ZCLAttributeDef: if isinstance(name_or_id, str): return self.attributes_by_name[name_or_id] elif isinstance(name_or_id, int): return self.attributes[name_or_id] else: raise ValueError( # noqa: TRY004 f"Attribute must be either a string or an integer," f" not {name_or_id!r} ({type(name_or_id)!r}" ) @classmethod def from_id( cls, endpoint: EndpointType, cluster_id: int, is_server: bool = True ) -> Cluster: cluster_id = t.ClusterId(cluster_id) if cluster_id in cls._registry: return cls._registry[cluster_id](endpoint, is_server) for (start, end), cluster in cls._registry_range.items(): if start <= cluster_id <= end: cluster = cluster(endpoint, is_server) cluster.cluster_id = cluster_id return cluster LOGGER.debug("Unknown cluster 0x%04X", cluster_id) cluster = cls(endpoint, is_server) cluster.cluster_id = cluster_id return cluster def deserialize(self, data: bytes) -> tuple[foundation.ZCLHeader, ...]: self.debug("Received ZCL frame: %r", data) hdr, data = foundation.ZCLHeader.deserialize(data) self.debug("Decoded ZCL frame header: %r", hdr) if hdr.frame_control.frame_type == foundation.FrameType.CLUSTER_COMMAND: # Cluster command if hdr.direction == foundation.Direction.Server_to_Client: commands = self.client_commands else: commands = self.server_commands if hdr.command_id not in commands: self.debug("Unknown cluster command %s %s", hdr.command_id, data) return hdr, data command = commands[hdr.command_id] else: # General command if hdr.command_id not in foundation.GENERAL_COMMANDS: self.debug("Unknown foundation command %s %s", hdr.command_id, data) return hdr, data command = foundation.GENERAL_COMMANDS[hdr.command_id] hdr.frame_control = hdr.frame_control.replace(direction=command.direction) response, data = command.schema.deserialize(data) self.debug("Decoded ZCL frame: %s:%r", type(self).__name__, response) if data: self.debug("Data remains after deserializing ZCL frame: %r", data) return hdr, response def _create_request( self, *, general: bool, command_id: foundation.GeneralCommand | int, schema: type[t.Struct], manufacturer: int | None = None, tsn: int | None = None, disable_default_response: bool, direction: foundation.Direction, # Schema args and kwargs args: tuple[Any, ...], kwargs: Any, ) -> tuple[foundation.ZCLHeader, bytes]: request = schema(*args, **kwargs) # type:ignore[operator] request.serialize() # Throw an error before generating a new TSN if tsn is None: tsn = self._endpoint.device.get_sequence() frame_control = foundation.FrameControl( frame_type=( foundation.FrameType.GLOBAL_COMMAND if general else foundation.FrameType.CLUSTER_COMMAND ), is_manufacturer_specific=(manufacturer is not None), direction=direction, disable_default_response=disable_default_response, reserved=0b000, ) hdr = foundation.ZCLHeader( frame_control=frame_control, manufacturer=manufacturer, tsn=tsn, command_id=command_id, ) return hdr, request async def request( self, general: bool, command_id: foundation.GeneralCommand | int | t.uint8_t, schema: type[t.Struct], *args, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, tsn: int | t.uint8_t | None = None, timeout=APS_REPLY_TIMEOUT, **kwargs, ): hdr, request = self._create_request( general=general, command_id=command_id, schema=schema, manufacturer=manufacturer, tsn=tsn, disable_default_response=self.is_client, direction=( foundation.Direction.Server_to_Client if self.is_client else foundation.Direction.Client_to_Server ), args=args, kwargs=kwargs, ) self.debug("Sending request header: %r", hdr) self.debug("Sending request: %r", request) data = hdr.serialize() + request.serialize() return await self._endpoint.request( cluster=self.cluster_id, sequence=hdr.tsn, data=data, command_id=hdr.command_id, timeout=timeout, expect_reply=expect_reply, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) async def reply( self, general: bool, command_id: foundation.GeneralCommand | int | t.uint8_t, schema: type[t.Struct], *args, manufacturer: int | t.uint16_t | None = None, tsn: int | t.uint8_t | None = None, timeout=APS_REPLY_TIMEOUT, expect_reply: bool = False, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, **kwargs, ) -> None: hdr, request = self._create_request( general=general, command_id=command_id, schema=schema, manufacturer=manufacturer, tsn=tsn, disable_default_response=True, direction=( foundation.Direction.Server_to_Client if self.is_client else foundation.Direction.Client_to_Server ), args=args, kwargs=kwargs, ) self.debug("Sending reply header: %r", hdr) self.debug("Sending reply: %r", request) data = hdr.serialize() + request.serialize() return await self._endpoint.reply( cluster=self.cluster_id, sequence=hdr.tsn, data=data, command_id=hdr.command_id, timeout=timeout, expect_reply=expect_reply, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) def handle_message( self, hdr: foundation.ZCLHeader, args: list[Any], *, dst_addressing: AddressingMode | None = None, ) -> None: self.debug( "Received command 0x%02X (TSN %d): %s", hdr.command_id, hdr.tsn, args ) if hdr.frame_control.is_cluster: self.handle_cluster_request(hdr, args, dst_addressing=dst_addressing) self.listener_event("cluster_command", hdr.tsn, hdr.command_id, args) return self.listener_event("general_command", hdr, args) self.handle_cluster_general_request(hdr, args, dst_addressing=dst_addressing) def handle_cluster_request( self, hdr: foundation.ZCLHeader, args: list[Any], *, dst_addressing: AddressingMode | None = None, ): self.debug( "No explicit handler for cluster command 0x%02x: %s", hdr.command_id, args, ) def handle_cluster_general_request( self, hdr: foundation.ZCLHeader, args: list, *, dst_addressing: AddressingMode | None = None, ) -> None: if hdr.command_id == foundation.GeneralCommand.Report_Attributes: values = [] for a in args.attribute_reports: if a.attrid in self.attributes: values.append(f"{self.attributes[a.attrid].name}={a.value.value!r}") else: values.append(f"0x{a.attrid:04X}={a.value.value!r}") self.debug("Attribute report received: %s", ", ".join(values)) for attr in args.attribute_reports: try: value = self.attributes[attr.attrid].type(attr.value.value) except KeyError: value = attr.value.value except ValueError: self.debug( "Couldn't normalize %a attribute with %s value", attr.attrid, attr.value.value, exc_info=True, ) value = attr.value.value self._update_attribute(attr.attrid, value) if not hdr.frame_control.disable_default_response: self.send_default_rsp( hdr, foundation.Status.SUCCESS, ) if hdr.command_id == foundation.GeneralCommand.Read_Attributes: records = [] for attrid in args.attribute_ids: record = foundation.ReadAttributeRecord(attrid=attrid) records.append(record) try: attr_def = self.find_attribute(attrid) except KeyError: record.status = foundation.Status.UNSUPPORTED_ATTRIBUTE continue attr_read_func = getattr( self, f"handle_read_attribute_{attr_def.name}", None ) if attr_read_func is None: record.status = foundation.Status.UNSUPPORTED_ATTRIBUTE continue record.status = foundation.Status.SUCCESS record.value = foundation.TypeValue( type=attr_def.zcl_type, value=attr_read_func(), ) self.create_catching_task(self.read_attributes_rsp(records, tsn=hdr.tsn)) def read_attributes_raw(self, attributes, manufacturer=None, **kwargs): attributes = [t.uint16_t(a) for a in attributes] return self._read_attributes(attributes, manufacturer=manufacturer, **kwargs) async def read_attributes( self, attributes: list[int | str], allow_cache: bool = False, only_cache: bool = False, manufacturer: int | t.uint16_t | None = None, **kwargs, ) -> Any: success, failure = {}, {} attribute_ids: list[int] = [] orig_attributes: dict[int, int | str] = {} for attribute in attributes: if isinstance(attribute, str): attrid = self.attributes_by_name[attribute].id else: # Allow reading attributes that aren't defined attrid = attribute attribute_ids.append(attrid) orig_attributes[attrid] = attribute to_read = [] if allow_cache or only_cache: for idx, attribute in enumerate(attribute_ids): if attribute in self._attr_cache: success[attributes[idx]] = self._attr_cache[attribute] elif attribute in self.unsupported_attributes: failure[attributes[idx]] = foundation.Status.UNSUPPORTED_ATTRIBUTE else: to_read.append(attribute) else: to_read = attribute_ids if not to_read or only_cache: return success, failure result = await self.read_attributes_raw( to_read, manufacturer=manufacturer, **kwargs ) if not isinstance(result[0], list): for attrid in to_read: orig_attribute = orig_attributes[attrid] failure[orig_attribute] = result[0] # Assume default response else: for record in result[0]: orig_attribute = orig_attributes[record.attrid] if record.status == foundation.Status.SUCCESS: try: value = self.attributes[record.attrid].type(record.value.value) except KeyError: value = record.value.value except ValueError: value = record.value.value self.debug( "Couldn't normalize %a attribute with %s value", record.attrid, value, exc_info=True, ) self._update_attribute(record.attrid, value) success[orig_attribute] = value self.remove_unsupported_attribute(record.attrid) else: if record.status == foundation.Status.UNSUPPORTED_ATTRIBUTE: self.add_unsupported_attribute(record.attrid) failure[orig_attribute] = record.status return success, failure def _write_attr_records( self, attributes: dict[str | int, Any] ) -> list[foundation.Attribute]: args = [] for attrid, value in attributes.items(): try: attr_def = self.find_attribute(attrid) except KeyError: self.error("%s is not a valid attribute id", attrid) # Throw an error if it's an unknown attribute name, without an ID if isinstance(attrid, str): raise continue attr = foundation.Attribute(attr_def.id, foundation.TypeValue()) attr.value.type = attr_def.zcl_type try: attr.value.value = attr_def.type(value) except ValueError as e: if isinstance(attrid, int): attrid = f"0x{attrid:04X}" raise ValueError( f"Failed to convert attribute {attrid} from {value!r}" f" ({type(value)}) to type {attr_def.type}" ) from e else: args.append(attr) return args async def write_attributes( self, attributes: dict[str | int, Any], manufacturer: int | None = None, **kwargs, ) -> list: """Write attributes to device with internal 'attributes' validation""" attrs = self._write_attr_records(attributes) return await self.write_attributes_raw(attrs, manufacturer, **kwargs) async def write_attributes_raw( self, attrs: list[foundation.Attribute], manufacturer: int | None = None, **kwargs, ) -> list: """Write attributes to device without internal 'attributes' validation""" result = await self._write_attributes( attrs, manufacturer=manufacturer, **kwargs ) if not isinstance(result[0], list): return result records = result[0] if len(records) == 1 and records[0].status == foundation.Status.SUCCESS: for attr_rec in attrs: self._update_attribute(attr_rec.attrid, attr_rec.value.value) else: failed = [rec.attrid for rec in records] for attr_rec in attrs: if attr_rec.attrid not in failed: self._update_attribute(attr_rec.attrid, attr_rec.value.value) return result def write_attributes_undivided( self, attributes: dict[str | int, Any], manufacturer: int | None = None ) -> list: """Either all or none of the attributes are written by the device.""" args = self._write_attr_records(attributes) return self._write_attributes_undivided(args, manufacturer=manufacturer) async def bind(self): return await self._endpoint.device.zdo.bind(cluster=self) async def unbind(self): return await self._endpoint.device.zdo.unbind(cluster=self) def _attr_reporting_rec( self, attribute: int | str, min_interval: int, max_interval: int, reportable_change: int = 1, direction: int = 0x00, ) -> foundation.AttributeReportingConfig: try: attr_def = self.find_attribute(attribute) except KeyError as exc: raise ValueError( f"Unknown attribute {attribute!r} of {self} cluster" ) from exc cfg = foundation.AttributeReportingConfig() cfg.direction = direction cfg.attrid = attr_def.id cfg.datatype = foundation.DataType.from_python_type(attr_def.type).type_id cfg.min_interval = min_interval cfg.max_interval = max_interval cfg.reportable_change = reportable_change return cfg async def configure_reporting( self, attribute: int | str, min_interval: int, max_interval: int, reportable_change: int, manufacturer: int | None = None, ) -> list[foundation.ConfigureReportingResponseRecord]: """Configure attribute reporting for a single attribute.""" return await self.configure_reporting_multiple( {attribute: (min_interval, max_interval, reportable_change)}, manufacturer=manufacturer, ) async def configure_reporting_multiple( self, attributes: dict[int | str, tuple[int, int, int]], manufacturer: int | None = None, ) -> list[foundation.ConfigureReportingResponseRecord]: """Configure attribute reporting for multiple attributes in the same request. :param attributes: dict of attributes to configure attribute reporting. Key is either int or str for attribute id or attribute name. Value is a tuple of: - minimum reporting interval - maximum reporting interval - reportable change :param manufacturer: optional manufacturer id to use with the command """ cfg = [ self._attr_reporting_rec(attr, rep[0], rep[1], rep[2]) for attr, rep in attributes.items() ] res = await self._configure_reporting(cfg, manufacturer=manufacturer) # Parse configure reporting result for unsupported attributes records = res[0] if ( isinstance(records, list) and not ( len(records) == 1 and records[0].status == foundation.Status.SUCCESS ) and len(records) >= 0 ): failed = [ r.attrid for r in records if r.status == foundation.Status.UNSUPPORTED_ATTRIBUTE ] for attr in failed: self.add_unsupported_attribute(attr) success = [ r.attrid for r in records if r.status == foundation.Status.SUCCESS ] for attr in success: self.remove_unsupported_attribute(attr) elif isinstance(records, list) and ( len(records) == 1 and records[0].status == foundation.Status.SUCCESS ): # we get a single success when all are supported for attr in attributes: self.remove_unsupported_attribute(attr) return res def command( self, command_id: foundation.GeneralCommand | int | t.uint8_t, *args, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, tsn: int | t.uint8_t | None = None, **kwargs, ): command = self.server_commands[command_id] return self.request( False, command_id, command.schema, *args, manufacturer=manufacturer, expect_reply=expect_reply, tsn=tsn, **kwargs, ) def client_command( self, command_id: foundation.GeneralCommand | int | t.uint8_t, *args, manufacturer: int | t.uint16_t | None = None, tsn: int | t.uint8_t | None = None, **kwargs, ): command = self.client_commands[command_id] return self.reply( False, command_id, command.schema, *args, manufacturer=manufacturer, tsn=tsn, **kwargs, ) @property def cluster_type(self) -> ClusterType: """Return the type of this cluster.""" return self._type @property def is_client(self) -> bool: """Return True if this is a client cluster.""" return self._type == ClusterType.Client @property def is_server(self) -> bool: """Return True if this is a server cluster.""" return self._type == ClusterType.Server @property def name(self) -> str: return self.__class__.__name__ @property def endpoint(self) -> Endpoint: return self._endpoint @property def commands(self): return list(self.ServerCommandDefs) def update_attribute(self, attrid: int | t.uint16_t, value: Any) -> None: """Update specified attribute with specified value""" self._update_attribute(attrid, value) def _update_attribute(self, attrid: int | t.uint16_t, value: Any) -> None: if value is None: if attrid not in self._attr_cache: return self._attr_cache.pop(attrid) self._attr_last_updated.pop(attrid) self.listener_event("attribute_cleared", attrid) else: now = datetime.now(timezone.utc) self._attr_cache[attrid] = value self._attr_last_updated[attrid] = now self.listener_event("attribute_updated", attrid, value, now) def log(self, lvl: int, msg: str, *args, **kwargs) -> None: msg = "[%s:%s:0x%04x] " + msg args = ( self._endpoint.device.name, self._endpoint.endpoint_id, self.cluster_id, *args, ) return LOGGER.log(lvl, msg, *args, **kwargs) def __getattr__(self, name: str) -> functools.partial: try: cmd = getattr(self.ClientCommandDefs, name) except AttributeError: pass else: return functools.partial(self.client_command, cmd.id) try: cmd = getattr(self.ServerCommandDefs, name) except AttributeError: pass else: return functools.partial(self.command, cmd.id) raise AttributeError(f"No such command name: {name}") def get(self, key: int | str, default: Any | None = None) -> Any: """Get cached attribute.""" attr_def = self.find_attribute(key) return self._attr_cache.get(attr_def.id, default) def __getitem__(self, key: int | str) -> Any: """Return cached value of the attr.""" return self._attr_cache[self.find_attribute(key).id] def __setitem__(self, key: int | str, value: Any) -> None: """Set cached value through attribute write.""" if not isinstance(key, (int, str)): raise ValueError("attr_name or attr_id are accepted only") # noqa: TRY004 self.create_catching_task(self.write_attributes({key: value})) def general_command( self, command_id: foundation.GeneralCommand | int | t.uint8_t, *args, manufacturer: int | t.uint16_t | None = None, expect_reply: bool = True, tsn: int | t.uint8_t | None = None, **kwargs, ): command = foundation.GENERAL_COMMANDS[command_id] if command.direction == foundation.Direction.Server_to_Client: # should reply be retryable? return self.reply( True, command.id, command.schema, *args, manufacturer=manufacturer, tsn=tsn, **kwargs, ) return self.request( True, command.id, command.schema, *args, manufacturer=manufacturer, expect_reply=expect_reply, tsn=tsn, **kwargs, ) _configure_reporting = functools.partialmethod( general_command, foundation.GeneralCommand.Configure_Reporting ) _read_attributes = functools.partialmethod( general_command, foundation.GeneralCommand.Read_Attributes ) read_attributes_rsp = functools.partialmethod( general_command, foundation.GeneralCommand.Read_Attributes_rsp ) _write_attributes = functools.partialmethod( general_command, foundation.GeneralCommand.Write_Attributes ) _write_attributes_undivided = functools.partialmethod( general_command, foundation.GeneralCommand.Write_Attributes_Undivided ) discover_attributes = functools.partialmethod( general_command, foundation.GeneralCommand.Discover_Attributes ) discover_attributes_extended = functools.partialmethod( general_command, foundation.GeneralCommand.Discover_Attribute_Extended ) discover_commands_received = functools.partialmethod( general_command, foundation.GeneralCommand.Discover_Commands_Received ) discover_commands_generated = functools.partialmethod( general_command, foundation.GeneralCommand.Discover_Commands_Generated ) def send_default_rsp( self, hdr: foundation.ZCLHeader, status: foundation.Status = foundation.Status.SUCCESS, ) -> None: """Send default response unconditionally.""" self.create_catching_task( self.general_command( foundation.GeneralCommand.Default_Response, hdr.command_id, status, tsn=hdr.tsn, priority=t.PacketPriority.LOW, ) ) def add_unsupported_attribute( self, attr: int | str, inhibit_events: bool = False ) -> None: """Adds unsupported attribute.""" if attr in self.unsupported_attributes: return self.unsupported_attributes.add(attr) if isinstance(attr, int) and not inhibit_events: self.listener_event("unsupported_attribute_added", attr) try: attrdef = self.find_attribute(attr) except KeyError: pass else: if isinstance(attr, int): self.add_unsupported_attribute(attrdef.name, inhibit_events) else: self.add_unsupported_attribute(attrdef.id, inhibit_events) def remove_unsupported_attribute( self, attr: int | str, inhibit_events: bool = False ) -> None: """Removes an unsupported attribute.""" if attr not in self.unsupported_attributes: return self.unsupported_attributes.remove(attr) if isinstance(attr, int) and not inhibit_events: self.listener_event("unsupported_attribute_removed", attr) try: attrdef = self.find_attribute(attr) except KeyError: pass else: if isinstance(attr, int): self.remove_unsupported_attribute(attrdef.name, inhibit_events) else: self.remove_unsupported_attribute(attrdef.id, inhibit_events) class ClusterPersistingListener: def __init__(self, applistener: PersistingListener, cluster: Cluster) -> None: self._applistener = applistener self._cluster = cluster def attribute_updated( self, attrid: int | t.uint16_t, value: Any, timestamp: datetime ) -> None: self._applistener.attribute_updated(self._cluster, attrid, value, timestamp) def attribute_cleared(self, attrid: int | t.uint16_t) -> None: self._applistener.attribute_cleared(self._cluster, attrid) def cluster_command(self, *args, **kwargs) -> None: pass def general_command(self, *args, **kwargs) -> None: pass def unsupported_attribute_added(self, attrid: int) -> None: """An unsupported attribute was added.""" self._applistener.unsupported_attribute_added(self._cluster, attrid) def unsupported_attribute_removed(self, attrid: int) -> None: """Remove an unsupported attribute.""" self._applistener.unsupported_attribute_removed(self._cluster, attrid) # Import to populate the registry from . import clusters # noqa: F401, E402, isort:skip zigpy-0.80.1/zigpy/zcl/clusters/000077500000000000000000000000001501451476000165435ustar00rootroot00000000000000zigpy-0.80.1/zigpy/zcl/clusters/__init__.py000066400000000000000000000021111501451476000206470ustar00rootroot00000000000000from __future__ import annotations import inspect from .. import Cluster from . import ( closures, general, general_const as general_const, # noqa: PLC0414 homeautomation, hvac, lighting, lightlink, manufacturer_specific, measurement, protocol, security, smartenergy, ) CLUSTERS_BY_ID: dict[int, Cluster] = {} CLUSTERS_BY_NAME: dict[str, Cluster] = {} for cls in ( closures, general, homeautomation, hvac, lighting, lightlink, manufacturer_specific, measurement, protocol, security, smartenergy, ): for name in dir(cls): obj = getattr(cls, name) # Object must be a concrete Cluster subclass if ( not inspect.isclass(obj) or not issubclass(obj, Cluster) or obj.cluster_id is None ): continue assert CLUSTERS_BY_ID.get(obj.cluster_id, obj) is obj assert CLUSTERS_BY_NAME.get(obj.ep_attribute, obj) is obj CLUSTERS_BY_ID[obj.cluster_id] = obj CLUSTERS_BY_NAME[obj.ep_attribute] = obj zigpy-0.80.1/zigpy/zcl/clusters/closures.py000066400000000000000000001000451501451476000207540ustar00rootroot00000000000000"""Closures Functional Domain""" from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster, foundation from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, Direction, ZCLAttributeDef, ZCLCommandDef, ) class ShadeStatus(t.bitmap8): Operational = 0b00000001 Adjusting = 0b00000010 Opening = 0b00000100 Motor_forward_is_opening = 0b00001000 class ShadeMode(t.enum8): Normal = 0x00 Configure = 0x00 Unknown = 0xFF class Shade(Cluster): """Attributes and commands for configuring a shade""" ShadeStatus: Final = ShadeStatus ShadeMode: Final = ShadeMode cluster_id: Final[t.uint16_t] = 0x0100 name: Final = "Shade Configuration" ep_attribute: Final = "shade" class AttributeDefs(BaseAttributeDefs): # Shade Information physical_closed_limit: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="r" ) motor_step_size: Final = ZCLAttributeDef(id=0x0001, type=t.uint8_t, access="r") status: Final = ZCLAttributeDef( id=0x0002, type=ShadeStatus, access="rw", mandatory=True ) # Shade Settings closed_limit: Final = ZCLAttributeDef( id=0x0010, type=t.uint16_t, access="rw", mandatory=True ) mode: Final = ZCLAttributeDef( id=0x0012, type=ShadeMode, access="rw", mandatory=True ) class LockState(t.enum8): Not_fully_locked = 0x00 Locked = 0x01 Unlocked = 0x02 Undefined = 0xFF class LockType(t.enum8): Dead_bolt = 0x00 Magnetic = 0x01 Other = 0x02 Mortise = 0x03 Rim = 0x04 Latch_bolt = 0x05 Cylindrical_lock = 0x06 Tubular_lock = 0x07 Interconnected_lock = 0x08 Dead_latch = 0x09 Door_furniture = 0x0A class DoorState(t.enum8): Open = 0x00 Closed = 0x01 Error_jammed = 0x02 Error_forced_open = 0x03 Error_unspecified = 0x04 Undefined = 0xFF class OperatingMode(t.enum8): Normal = 0x00 Vacation = 0x01 Privacy = 0x02 No_RF_Lock_Unlock = 0x03 Passage = 0x04 class SupportedOperatingModes(t.bitmap16): Normal = 0x0001 Vacation = 0x0002 Privacy = 0x0004 No_RF = 0x0008 Passage = 0x0010 class DefaultConfigurationRegister(t.bitmap16): Enable_Local_Programming = 0x0001 Keypad_Interface_default_access = 0x0002 RF_Interface_default_access = 0x0004 Sound_Volume_non_zero = 0x0020 Auto_Relock_time_non_zero = 0x0040 Led_settings_non_zero = 0x0080 class ZigbeeSecurityLevel(t.enum8): Network_Security = 0x00 APS_Security = 0x01 class AlarmMask(t.bitmap16): Deadbolt_Jammed = 0x0001 Lock_Reset_to_Factory_Defaults = 0x0002 Reserved = 0x0004 RF_Module_Power_Cycled = 0x0008 Tamper_Alarm_wrong_code_entry_limit = 0x0010 Tamper_Alarm_front_escutcheon_removed = 0x0020 Forced_Door_Open_under_Door_Lockec_Condition = 0x0040 class KeypadOperationEventMask(t.bitmap16): Manufacturer_specific = 0x0001 Lock_source_keypad = 0x0002 Unlock_source_keypad = 0x0004 Lock_source_keypad_error_invalid_code = 0x0008 Lock_source_keypad_error_invalid_schedule = 0x0010 Unlock_source_keypad_error_invalid_code = 0x0020 Unlock_source_keypad_error_invalid_schedule = 0x0040 Non_Access_User_Operation = 0x0080 class RFOperationEventMask(t.bitmap16): Manufacturer_specific = 0x0001 Lock_source_RF = 0x0002 Unlock_source_RF = 0x0004 Lock_source_RF_error_invalid_code = 0x0008 Lock_source_RF_error_invalid_schedule = 0x0010 Unlock_source_RF_error_invalid_code = 0x0020 Unlock_source_RF_error_invalid_schedule = 0x0040 class ManualOperatitonEventMask(t.bitmap16): Manufacturer_specific = 0x0001 Thumbturn_Lock = 0x0002 Thumbturn_Unlock = 0x0004 One_touch_lock = 0x0008 Key_Lock = 0x0010 Key_Unlock = 0x0020 Auto_lock = 0x0040 Schedule_Lock = 0x0080 Schedule_Unlock = 0x0100 Manual_Lock_key_or_thumbturn = 0x0200 Manual_Unlock_key_or_thumbturn = 0x0400 class RFIDOperationEventMask(t.bitmap16): Manufacturer_specific = 0x0001 Lock_source_RFID = 0x0002 Unlock_source_RFID = 0x0004 Lock_source_RFID_error_invalid_RFID_ID = 0x0008 Lock_source_RFID_error_invalid_schedule = 0x0010 Unlock_source_RFID_error_invalid_RFID_ID = 0x0020 Unlock_source_RFID_error_invalid_schedule = 0x0040 class KeypadProgrammingEventMask(t.bitmap16): Manufacturer_Specific = 0x0001 Master_code_changed = 0x0002 PIN_added = 0x0004 PIN_deleted = 0x0008 PIN_changed = 0x0010 class RFProgrammingEventMask(t.bitmap16): Manufacturer_Specific = 0x0001 PIN_added = 0x0004 PIN_deleted = 0x0008 PIN_changed = 0x0010 RFID_code_added = 0x0020 RFID_code_deleted = 0x0040 class RFIDProgrammingEventMask(t.bitmap16): Manufacturer_Specific = 0x0001 RFID_code_added = 0x0020 RFID_code_deleted = 0x0040 class OperationEventSource(t.enum8): Keypad = 0x00 RF = 0x01 Manual = 0x02 RFID = 0x03 Indeterminate = 0xFF class OperationEvent(t.enum8): UnknownOrMfgSpecific = 0x00 Lock = 0x01 Unlock = 0x02 LockFailureInvalidPINorID = 0x03 LockFailureInvalidSchedule = 0x04 UnlockFailureInvalidPINorID = 0x05 UnlockFailureInvalidSchedule = 0x06 OnTouchLock = 0x07 KeyLock = 0x08 KeyUnlock = 0x09 AutoLock = 0x0A ScheduleLock = 0x0B ScheduleUnlock = 0x0C Manual_Lock = 0x0D Manual_Unlock = 0x0E Non_Access_User_Operational_Event = 0x0F class ProgrammingEvent(t.enum8): UnknownOrMfgSpecific = 0x00 MasterCodeChanged = 0x01 PINCodeAdded = 0x02 PINCodeDeleted = 0x03 PINCodeChanges = 0x04 RFIDCodeAdded = 0x05 RFIDCodeDeleted = 0x06 class UserStatus(t.enum8): Available = 0x00 Enabled = 0x01 Disabled = 0x03 Not_Supported = 0xFF class UserType(t.enum8): Unrestricted = 0x00 Year_Day_Schedule_User = 0x01 Week_Day_Schedule_User = 0x02 Master_User = 0x03 Non_Access_User = 0x04 Not_Supported = 0xFF class DayMask(t.bitmap8): Sun = 0x01 Mon = 0x02 Tue = 0x04 Wed = 0x08 Thu = 0x10 Fri = 0x20 Sat = 0x40 class EventType(t.enum8): Operation = 0x00 Programming = 0x01 Alarm = 0x02 class DoorLock(Cluster): """The door lock cluster provides an interface to a generic way to secure a door.""" LockState: Final = LockState LockType: Final = LockType DoorState: Final = DoorState OperatingMode: Final = OperatingMode SupportedOperatingModes: Final = SupportedOperatingModes DefaultConfigurationRegister: Final = DefaultConfigurationRegister ZigbeeSecurityLevel: Final = ZigbeeSecurityLevel AlarmMask: Final = AlarmMask KeypadOperationEventMask: Final = KeypadOperationEventMask RFOperationEventMask: Final = RFOperationEventMask ManualOperatitonEventMask: Final = ManualOperatitonEventMask RFIDOperationEventMask: Final = RFIDOperationEventMask KeypadProgrammingEventMask: Final = KeypadProgrammingEventMask RFProgrammingEventMask: Final = RFProgrammingEventMask RFIDProgrammingEventMask: Final = RFIDProgrammingEventMask OperationEventSource: Final = OperationEventSource OperationEvent: Final = OperationEvent ProgrammingEvent: Final = ProgrammingEvent UserStatus: Final = UserStatus UserType: Final = UserType DayMask: Final = DayMask EventType: Final = EventType cluster_id: Final[t.uint16_t] = 0x0101 name: Final = "Door Lock" ep_attribute: Final = "door_lock" class AttributeDefs(BaseAttributeDefs): lock_state: Final = ZCLAttributeDef( id=0x0000, type=LockState, access="rp", mandatory=True ) lock_type: Final = ZCLAttributeDef( id=0x0001, type=LockType, access="r", mandatory=True ) actuator_enabled: Final = ZCLAttributeDef( id=0x0002, type=t.Bool, access="r", mandatory=True ) door_state: Final = ZCLAttributeDef(id=0x0003, type=DoorState, access="rp") door_open_events: Final = ZCLAttributeDef( id=0x0004, type=t.uint32_t, access="rw" ) door_closed_events: Final = ZCLAttributeDef( id=0x0005, type=t.uint32_t, access="rw" ) open_period: Final = ZCLAttributeDef(id=0x0006, type=t.uint16_t, access="rw") num_of_lock_records_supported: Final = ZCLAttributeDef( id=0x0010, type=t.uint16_t, access="r" ) num_of_total_users_supported: Final = ZCLAttributeDef( id=0x0011, type=t.uint16_t, access="r" ) num_of_pin_users_supported: Final = ZCLAttributeDef( id=0x0012, type=t.uint16_t, access="r" ) num_of_rfid_users_supported: Final = ZCLAttributeDef( id=0x0013, type=t.uint16_t, access="r" ) num_of_week_day_schedules_supported_per_user: Final = ZCLAttributeDef( id=0x0014, type=t.uint8_t, access="r" ) num_of_year_day_schedules_supported_per_user: Final = ZCLAttributeDef( id=0x0015, type=t.uint8_t, access="r" ) num_of_holiday_scheduleds_supported: Final = ZCLAttributeDef( id=0x0016, type=t.uint8_t, access="r" ) max_pin_len: Final = ZCLAttributeDef(id=0x0017, type=t.uint8_t, access="r") min_pin_len: Final = ZCLAttributeDef(id=0x0018, type=t.uint8_t, access="r") max_rfid_len: Final = ZCLAttributeDef(id=0x0019, type=t.uint8_t, access="r") min_rfid_len: Final = ZCLAttributeDef(id=0x001A, type=t.uint8_t, access="r") enable_logging: Final = ZCLAttributeDef(id=0x0020, type=t.Bool, access="r*wp") language: Final = ZCLAttributeDef( id=0x0021, type=t.LimitedCharString(3), access="r*wp" ) led_settings: Final = ZCLAttributeDef(id=0x0022, type=t.uint8_t, access="r*wp") auto_relock_time: Final = ZCLAttributeDef( id=0x0023, type=t.uint32_t, access="r*wp" ) sound_volume: Final = ZCLAttributeDef(id=0x0024, type=t.uint8_t, access="r*wp") operating_mode: Final = ZCLAttributeDef( id=0x0025, type=OperatingMode, access="r*wp" ) supported_operating_modes: Final = ZCLAttributeDef( id=0x0026, type=SupportedOperatingModes, access="r" ) default_configuration_register: Final = ZCLAttributeDef( id=0x0027, type=DefaultConfigurationRegister, access="rp", ) enable_local_programming: Final = ZCLAttributeDef( id=0x0028, type=t.Bool, access="r*wp" ) enable_one_touch_locking: Final = ZCLAttributeDef( id=0x0029, type=t.Bool, access="rwp" ) enable_inside_status_led: Final = ZCLAttributeDef( id=0x002A, type=t.Bool, access="rwp" ) enable_privacy_mode_button: Final = ZCLAttributeDef( id=0x002B, type=t.Bool, access="rwp" ) wrong_code_entry_limit: Final = ZCLAttributeDef( id=0x0030, type=t.uint8_t, access="r*wp" ) user_code_temporary_disable_time: Final = ZCLAttributeDef( id=0x0031, type=t.uint8_t, access="r*wp" ) send_pin_ota: Final = ZCLAttributeDef(id=0x0032, type=t.Bool, access="r*wp") require_pin_for_rf_operation: Final = ZCLAttributeDef( id=0x0033, type=t.Bool, access="r*wp" ) zigbee_security_level: Final = ZCLAttributeDef( id=0x0034, type=ZigbeeSecurityLevel, access="rp" ) alarm_mask: Final = ZCLAttributeDef(id=0x0040, type=AlarmMask, access="rwp") keypad_operation_event_mask: Final = ZCLAttributeDef( id=0x0041, type=KeypadOperationEventMask, access="rwp" ) rf_operation_event_mask: Final = ZCLAttributeDef( id=0x0042, type=RFOperationEventMask, access="rwp" ) manual_operation_event_mask: Final = ZCLAttributeDef( id=0x0043, type=ManualOperatitonEventMask, access="rwp" ) rfid_operation_event_mask: Final = ZCLAttributeDef( id=0x0044, type=RFIDOperationEventMask, access="rwp" ) keypad_programming_event_mask: Final = ZCLAttributeDef( id=0x0045, type=KeypadProgrammingEventMask, access="rwp", ) rf_programming_event_mask: Final = ZCLAttributeDef( id=0x0046, type=RFProgrammingEventMask, access="rwp" ) rfid_programming_event_mask: Final = ZCLAttributeDef( id=0x0047, type=RFIDProgrammingEventMask, access="rwp" ) class ServerCommandDefs(BaseCommandDefs): lock_door: Final = ZCLCommandDef( id=0x00, schema={"pin_code?": t.CharacterString}, direction=Direction.Client_to_Server, ) unlock_door: Final = ZCLCommandDef( id=0x01, schema={"pin_code?": t.CharacterString}, direction=Direction.Client_to_Server, ) toggle_door: Final = ZCLCommandDef( id=0x02, schema={"pin_code?": t.CharacterString}, direction=Direction.Client_to_Server, ) unlock_with_timeout: Final = ZCLCommandDef( id=0x03, schema={"timeout": t.uint16_t, "pin_code?": t.CharacterString}, direction=Direction.Client_to_Server, ) get_log_record: Final = ZCLCommandDef( id=0x04, schema={"log_index": t.uint16_t}, direction=Direction.Client_to_Server, ) set_pin_code: Final = ZCLCommandDef( id=0x05, schema={ "user_id": t.uint16_t, "user_status": UserStatus, "user_type": UserType, "pin_code": t.CharacterString, }, direction=Direction.Client_to_Server, ) get_pin_code: Final = ZCLCommandDef( id=0x06, schema={"user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) clear_pin_code: Final = ZCLCommandDef( id=0x07, schema={"user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) clear_all_pin_codes: Final = ZCLCommandDef( id=0x08, schema={}, direction=Direction.Client_to_Server ) set_user_status: Final = ZCLCommandDef( id=0x09, schema={"user_id": t.uint16_t, "user_status": UserStatus}, direction=Direction.Client_to_Server, ) get_user_status: Final = ZCLCommandDef( id=0x0A, schema={"user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) set_week_day_schedule: Final = ZCLCommandDef( id=0x0B, schema={ "schedule_id": t.uint8_t, "user_id": t.uint16_t, "days_mask": DayMask, "start_hour": t.uint8_t, "start_minute": t.uint8_t, "end_hour": t.uint8_t, "end_minute": t.uint8_t, }, direction=Direction.Client_to_Server, ) get_week_day_schedule: Final = ZCLCommandDef( id=0x0C, schema={"schedule_id": t.uint8_t, "user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) clear_week_day_schedule: Final = ZCLCommandDef( id=0x0D, schema={"schedule_id": t.uint8_t, "user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) set_year_day_schedule: Final = ZCLCommandDef( id=0x0E, schema={ "schedule_id": t.uint8_t, "user_id": t.uint16_t, "local_start_time": t.LocalTime, "local_end_time": t.LocalTime, }, direction=Direction.Client_to_Server, ) get_year_day_schedule: Final = ZCLCommandDef( id=0x0F, schema={"schedule_id": t.uint8_t, "user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) clear_year_day_schedule: Final = ZCLCommandDef( id=0x10, schema={"schedule_id": t.uint8_t, "user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) set_holiday_schedule: Final = ZCLCommandDef( id=0x11, schema={ "holiday_schedule_id": t.uint8_t, "local_start_time": t.LocalTime, "local_end_time": t.LocalTime, "operating_mode_during_holiday": OperatingMode, }, direction=Direction.Client_to_Server, ) get_holiday_schedule: Final = ZCLCommandDef( id=0x12, schema={"holiday_schedule_id": t.uint8_t}, direction=Direction.Client_to_Server, ) clear_holiday_schedule: Final = ZCLCommandDef( id=0x13, schema={"holiday_schedule_id": t.uint8_t}, direction=Direction.Client_to_Server, ) set_user_type: Final = ZCLCommandDef( id=0x14, schema={"user_id": t.uint16_t, "user_type": UserType}, direction=Direction.Client_to_Server, ) get_user_type: Final = ZCLCommandDef( id=0x15, schema={"user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) set_rfid_code: Final = ZCLCommandDef( id=0x16, schema={ "user_id": t.uint16_t, "user_status": UserStatus, "user_type": UserType, "rfid_code": t.CharacterString, }, direction=Direction.Client_to_Server, ) get_rfid_code: Final = ZCLCommandDef( id=0x17, schema={"user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) clear_rfid_code: Final = ZCLCommandDef( id=0x18, schema={"user_id": t.uint16_t}, direction=Direction.Client_to_Server, ) clear_all_rfid_codes: Final = ZCLCommandDef( id=0x19, schema={}, direction=Direction.Client_to_Server ) class ClientCommandDefs(BaseCommandDefs): lock_door_response: Final = ZCLCommandDef( id=0x00, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) unlock_door_response: Final = ZCLCommandDef( id=0x01, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) toggle_door_response: Final = ZCLCommandDef( id=0x02, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) unlock_with_timeout_response: Final = ZCLCommandDef( id=0x03, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_log_record_response: Final = ZCLCommandDef( id=0x04, schema={ "log_entry_id": t.uint16_t, "timestamp": t.uint32_t, "event_type": EventType, "source": OperationEventSource, "event_id_or_alarm_code": t.uint8_t, "user_id": t.uint16_t, "pin?": t.CharacterString, }, direction=Direction.Server_to_Client, ) set_pin_code_response: Final = ZCLCommandDef( id=0x05, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_pin_code_response: Final = ZCLCommandDef( id=0x06, schema={ "user_id": t.uint16_t, "user_status": UserStatus, "user_type": UserType, "code": t.CharacterString, }, direction=Direction.Server_to_Client, ) clear_pin_code_response: Final = ZCLCommandDef( id=0x07, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) clear_all_pin_codes_response: Final = ZCLCommandDef( id=0x08, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) set_user_status_response: Final = ZCLCommandDef( id=0x09, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_user_status_response: Final = ZCLCommandDef( id=0x0A, schema={"user_id": t.uint16_t, "user_status": UserStatus}, direction=Direction.Server_to_Client, ) set_week_day_schedule_response: Final = ZCLCommandDef( id=0x0B, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_week_day_schedule_response: Final = ZCLCommandDef( id=0x0C, schema={ "schedule_id": t.uint8_t, "user_id": t.uint16_t, "status": foundation.Status, "days_mask?": t.uint8_t, "start_hour?": t.uint8_t, "start_minute?": t.uint8_t, "end_hour?": t.uint8_t, "end_minute?": t.uint8_t, }, direction=Direction.Server_to_Client, ) clear_week_day_schedule_response: Final = ZCLCommandDef( id=0x0D, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) set_year_day_schedule_response: Final = ZCLCommandDef( id=0x0E, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_year_day_schedule_response: Final = ZCLCommandDef( id=0x0F, schema={ "schedule_id": t.uint8_t, "user_id": t.uint16_t, "status": foundation.Status, "local_start_time?": t.LocalTime, "local_end_time?": t.LocalTime, }, direction=Direction.Server_to_Client, ) clear_year_day_schedule_response: Final = ZCLCommandDef( id=0x10, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) set_holiday_schedule_response: Final = ZCLCommandDef( id=0x11, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_holiday_schedule_response: Final = ZCLCommandDef( id=0x12, schema={ "holiday_schedule_id": t.uint8_t, "status": foundation.Status, "local_start_time?": t.LocalTime, "local_end_time?": t.LocalTime, "operating_mode_during_holiday?": t.uint8_t, }, direction=Direction.Server_to_Client, ) clear_holiday_schedule_response: Final = ZCLCommandDef( id=0x13, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) set_user_type_response: Final = ZCLCommandDef( id=0x14, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_user_type_response: Final = ZCLCommandDef( id=0x15, schema={"user_id": t.uint16_t, "user_type": UserType}, direction=Direction.Server_to_Client, ) set_rfid_code_response: Final = ZCLCommandDef( id=0x16, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) get_rfid_code_response: Final = ZCLCommandDef( id=0x17, schema={ "user_id": t.uint16_t, "user_status": UserStatus, "user_type": UserType, "rfid_code": t.CharacterString, }, direction=Direction.Server_to_Client, ) clear_rfid_code_response: Final = ZCLCommandDef( id=0x18, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) clear_all_rfid_codes_response: Final = ZCLCommandDef( id=0x19, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) operation_event_notification: Final = ZCLCommandDef( id=0x20, schema={ "operation_event_source": OperationEventSource, "operation_event_code": OperationEvent, "user_id": t.uint16_t, "pin": t.CharacterString, "local_time": t.LocalTime, "data?": t.CharacterString, }, direction=Direction.Server_to_Client, ) programming_event_notification: Final = ZCLCommandDef( id=0x21, schema={ "program_event_source": OperationEventSource, "program_event_code": ProgrammingEvent, "user_id": t.uint16_t, "pin": t.CharacterString, "user_type": UserType, "user_status": UserStatus, "local_time": t.LocalTime, "data?": t.CharacterString, }, direction=Direction.Server_to_Client, ) class WindowCoveringType(t.enum8): Rollershade = 0x00 Rollershade_two_motors = 0x01 Rollershade_exterior = 0x02 Rollershade_exterior_two_motors = 0x03 Drapery = 0x04 Awning = 0x05 Shutter = 0x06 Tilt_blind_tilt_only = 0x07 Tilt_blind_tilt_and_lift = 0x08 Projector_screen = 0x09 class ConfigStatus(t.bitmap8): Operational = 0b00000001 Online = 0b00000010 Open_up_commands_reversed = 0b00000100 Closed_loop_lift_control = 0b00001000 Closed_loop_tilt_control = 0b00010000 Encoder_controlled_lift = 0b00100000 Encoder_controlled_tilt = 0b01000000 class WindowCoveringMode(t.bitmap8): Motor_direction_reversed = 0b00000001 Run_in_calibration_mode = 0b00000010 Motor_in_maintenance_mode = 0b00000100 LEDs_display_feedback = 0b00001000 class WindowCovering(Cluster): WindowCoveringType: Final = WindowCoveringType ConfigStatus: Final = ConfigStatus WindowCoveringMode: Final = WindowCoveringMode cluster_id: Final[t.uint16_t] = 0x0102 name: Final = "Window Covering" ep_attribute: Final = "window_covering" class AttributeDefs(BaseAttributeDefs): # Window Covering Information window_covering_type: Final = ZCLAttributeDef( id=0x0000, type=WindowCoveringType, access="r", mandatory=True ) physical_closed_limit_lift: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r" ) physical_closed_limit_tilt: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r" ) current_position_lift: Final = ZCLAttributeDef( id=0x0003, type=t.uint16_t, access="r" ) current_position_tilt: Final = ZCLAttributeDef( id=0x0004, type=t.uint16_t, access="r" ) number_of_actuations_lift: Final = ZCLAttributeDef( id=0x0005, type=t.uint16_t, access="r" ) number_of_actuations_tilt: Final = ZCLAttributeDef( id=0x0006, type=t.uint16_t, access="r" ) config_status: Final = ZCLAttributeDef( id=0x0007, type=ConfigStatus, access="r", mandatory=True ) # All subsequent attributes are mandatory if their control types are enabled current_position_lift_percentage: Final = ZCLAttributeDef( id=0x0008, type=t.uint8_t, access="rps" ) current_position_tilt_percentage: Final = ZCLAttributeDef( id=0x0009, type=t.uint8_t, access="rps" ) # Window Covering Settings installed_open_limit_lift: Final = ZCLAttributeDef( id=0x0010, type=t.uint16_t, access="r" ) installed_closed_limit_lift: Final = ZCLAttributeDef( id=0x0011, type=t.uint16_t, access="r" ) installed_open_limit_tilt: Final = ZCLAttributeDef( id=0x0012, type=t.uint16_t, access="r" ) installed_closed_limit_tilt: Final = ZCLAttributeDef( id=0x0013, type=t.uint16_t, access="r" ) velocity_lift: Final = ZCLAttributeDef(id=0x0014, type=t.uint16_t, access="rw") acceleration_time_lift: Final = ZCLAttributeDef( id=0x0015, type=t.uint16_t, access="rw" ) deceleration_time_lift: Final = ZCLAttributeDef( id=0x0016, type=t.uint16_t, access="rw" ) window_covering_mode: Final = ZCLAttributeDef( id=0x0017, type=WindowCoveringMode, access="rw", mandatory=True ) intermediate_setpoints_lift: Final = ZCLAttributeDef( id=0x0018, type=t.LVBytes, access="rw" ) intermediate_setpoints_tilt: Final = ZCLAttributeDef( id=0x0019, type=t.LVBytes, access="rw" ) class ServerCommandDefs(BaseCommandDefs): up_open: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Client_to_Server ) down_close: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) stop: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) go_to_lift_value: Final = ZCLCommandDef( id=0x04, schema={"lift_value": t.uint16_t}, direction=Direction.Client_to_Server, ) go_to_lift_percentage: Final = ZCLCommandDef( id=0x05, schema={"percentage_lift_value": t.uint8_t}, direction=Direction.Client_to_Server, ) go_to_tilt_value: Final = ZCLCommandDef( id=0x07, schema={"tilt_value": t.uint16_t}, direction=Direction.Client_to_Server, ) go_to_tilt_percentage: Final = ZCLCommandDef( id=0x08, schema={"percentage_tilt_value": t.uint8_t}, direction=Direction.Client_to_Server, ) class MovingState(t.enum8): Stopped = 0x00 Closing = 0x01 Opening = 0x02 class SafetyStatus(t.bitmap16): Remote_Lockout = 0b00000000_00000001 Tamper_Detected = 0b00000000_00000010 Failed_Communication = 0b00000000_00000100 Position_Failure = 0b00000000_00001000 class Capabilities(t.bitmap8): Partial_Barrier = 0b00000001 class BarrierControl(Cluster): cluster_id: Final = 0x0103 name: Final = "Barrier Control" ep_attribute: Final = "barrier_control" class AttributeDefs(BaseAttributeDefs): moving_state: Final = ZCLAttributeDef( id=0x0001, type=MovingState, access="rp", mandatory=True ) safety_status: Final = ZCLAttributeDef( id=0x0002, type=SafetyStatus, access="rp", mandatory=True ) capabilities: Final = ZCLAttributeDef( id=0x0003, type=Capabilities, access="r", mandatory=True ) open_events: Final = ZCLAttributeDef(id=0x0004, type=t.uint16_t, access="rw") close_events: Final = ZCLAttributeDef(id=0x0005, type=t.uint16_t, access="rw") command_open_events: Final = ZCLAttributeDef( id=0x0006, type=t.uint16_t, access="rw" ) command_close_events: Final = ZCLAttributeDef( id=0x0007, type=t.uint16_t, access="rw" ) open_period: Final = ZCLAttributeDef(id=0x0008, type=t.uint16_t, access="rw") close_period: Final = ZCLAttributeDef(id=0x0009, type=t.uint16_t, access="rw") barrier_position: Final = ZCLAttributeDef( id=0x000A, type=t.uint8_t, access="rps", mandatory=True ) class ServerCommandDefs(BaseCommandDefs): go_to_percent: Final = ZCLCommandDef( id=0x00, schema={"percent_open": t.uint8_t}, direction=Direction.Client_to_Server, ) stop: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) class ClientCommandDefs(BaseCommandDefs): pass zigpy-0.80.1/zigpy/zcl/clusters/general.py000066400000000000000000002614011501451476000205360ustar00rootroot00000000000000"""General Functional Domain""" from __future__ import annotations from datetime import datetime, timezone from typing import Any, Final import zigpy.types as t from zigpy.typing import AddressingMode from zigpy.zcl import Cluster, foundation from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, Direction, ZCLAttributeDef, ZCLCommandDef, ) ZIGBEE_EPOCH = datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc) class PowerSource(t.enum8): """Power source enum.""" Unknown = 0x00 Mains_single_phase = 0x01 Mains_three_phase = 0x02 Battery = 0x03 DC_Source = 0x04 Emergency_Mains_Always_On = 0x05 Emergency_Mains_Transfer_Switch = 0x06 def __init__(self, *args, **kwargs): self.battery_backup = False @classmethod def deserialize(cls, data: bytes) -> tuple[bytes, bytes]: val, data = t.uint8_t.deserialize(data) r = cls(val & 0x7F) r.battery_backup = bool(val & 0x80) return r, data class PhysicalEnvironment(t.enum8): Unspecified_environment = 0x00 # Mirror Capacity Available: for 0x0109 Profile Id only; use 0x71 moving forward # Atrium: defined for legacy devices with non-0x0109 Profile Id; use 0x70 moving # forward # Note: This value is deprecated for Profile Id 0x0104. The value 0x01 is # maintained for historical purposes and SHOULD only be used for backwards # compatibility with devices developed before this specification. The 0x01 # value MUST be interpreted using the Profile Id of the endpoint upon # which it is implemented. For endpoints with the Smart Energy Profile Id # (0x0109) the value 0x01 has a meaning of Mirror. For endpoints with any # other profile identifier, the value 0x01 has a meaning of Atrium. Mirror_or_atrium_legacy = 0x01 Bar = 0x02 Courtyard = 0x03 Bathroom = 0x04 Bedroom = 0x05 Billiard_Room = 0x06 Utility_Room = 0x07 Cellar = 0x08 Storage_Closet = 0x09 Theater = 0x0A Office = 0x0B Deck = 0x0C Den = 0x0D Dining_Room = 0x0E Electrical_Room = 0x0F Elevator = 0x10 Entry = 0x11 Family_Room = 0x12 Main_Floor = 0x13 Upstairs = 0x14 Downstairs = 0x15 Basement = 0x16 Gallery = 0x17 Game_Room = 0x18 Garage = 0x19 Gym = 0x1A Hallway = 0x1B House = 0x1C Kitchen = 0x1D Laundry_Room = 0x1E Library = 0x1F Master_Bedroom = 0x20 Mud_Room_small_room_for_coats_and_boots = 0x21 Nursery = 0x22 Pantry = 0x23 Office_2 = 0x24 Outside = 0x25 Pool = 0x26 Porch = 0x27 Sewing_Room = 0x28 Sitting_Room = 0x29 Stairway = 0x2A Yard = 0x2B Attic = 0x2C Hot_Tub = 0x2D Living_Room = 0x2E Sauna = 0x2F Workshop = 0x30 Guest_Bedroom = 0x31 Guest_Bath = 0x32 Back_Yard = 0x34 Front_Yard = 0x35 Patio = 0x36 Driveway = 0x37 Sun_Room = 0x38 Grand_Room = 0x39 Spa = 0x3A Whirlpool = 0x3B Shed = 0x3C Equipment_Storage = 0x3D Craft_Room = 0x3E Fountain = 0x3F Pond = 0x40 Reception_Room = 0x41 Breakfast_Room = 0x42 Nook = 0x43 Garden = 0x44 Balcony = 0x45 Panic_Room = 0x46 Terrace = 0x47 Roof = 0x48 Toilet = 0x49 Toilet_Main = 0x4A Outside_Toilet = 0x4B Shower_room = 0x4C Study = 0x4D Front_Garden = 0x4E Back_Garden = 0x4F Kettle = 0x50 Television = 0x51 Stove = 0x52 Microwave = 0x53 Toaster = 0x54 Vacuum = 0x55 Appliance = 0x56 Front_Door = 0x57 Back_Door = 0x58 Fridge_Door = 0x59 Medication_Cabinet_Door = 0x60 Wardrobe_Door = 0x61 Front_Cupboard_Door = 0x62 Other_Door = 0x63 Waiting_Room = 0x64 Triage_Room = 0x65 Doctors_Office = 0x66 Patients_Private_Room = 0x67 Consultation_Room = 0x68 Nurse_Station = 0x69 Ward = 0x6A Corridor = 0x6B Operating_Theatre = 0x6C Dental_Surgery_Room = 0x6D Medical_Imaging_Room = 0x6E Decontamination_Room = 0x6F Atrium = 0x70 Mirror = 0x71 Unknown_environment = 0xFF class AlarmMask(t.bitmap8): General_hardware_fault = 0x01 General_software_fault = 0x02 class DisableLocalConfig(t.bitmap8): Reset = 0x01 Device_Configuration = 0x02 class GenericDeviceClass(t.enum8): Lighting = 0x00 class GenericLightingDeviceType(t.enum8): Incandescent = 0x00 Spotlight_Halogen = 0x01 Halogen_bulb = 0x02 CFL = 0x03 Linear_Fluorescent = 0x04 LED_bulb = 0x05 Spotlight_LED = 0x06 LED_strip = 0x07 LED_tube = 0x08 Generic_indoor_luminaire = 0x09 Generic_outdoor_luminaire = 0x0A Pendant_luminaire = 0x0B Floor_standing_luminaire = 0x0C Generic_Controller = 0xE0 Wall_Switch = 0xE1 Portable_remote_controller = 0xE2 Motion_sensor = 0xE3 # 0xe4 to 0xef Reserved Generic_actuator = 0xF0 Wall_socket = 0xF1 Gateway_Bridge = 0xF2 Plug_in_unit = 0xF3 Retrofit_actuator = 0xF4 Unspecified = 0xFF class Basic(Cluster): """Attributes for determining basic information about a device, setting user device information such as location, and enabling a device. """ PowerSource: Final = PowerSource PhysicalEnvironment: Final = PhysicalEnvironment AlarmMask: Final = AlarmMask DisableLocalConfig: Final = DisableLocalConfig GenericDeviceClass: Final = GenericDeviceClass GenericLightingDeviceType: Final = GenericLightingDeviceType cluster_id: Final[t.uint16_t] = 0x0000 ep_attribute: Final = "basic" class AttributeDefs(BaseAttributeDefs): # Basic Device Information zcl_version: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="r", mandatory=True ) app_version: Final = ZCLAttributeDef(id=0x0001, type=t.uint8_t, access="r") stack_version: Final = ZCLAttributeDef(id=0x0002, type=t.uint8_t, access="r") hw_version: Final = ZCLAttributeDef(id=0x0003, type=t.uint8_t, access="r") manufacturer: Final = ZCLAttributeDef( id=0x0004, type=t.LimitedCharString(32), access="r" ) model: Final = ZCLAttributeDef( id=0x0005, type=t.LimitedCharString(32), access="r" ) date_code: Final = ZCLAttributeDef( id=0x0006, type=t.LimitedCharString(16), access="r" ) power_source: Final = ZCLAttributeDef( id=0x0007, type=PowerSource, access="r", mandatory=True ) generic_device_class: Final = ZCLAttributeDef( id=0x0008, type=GenericDeviceClass, access="r" ) # Lighting is the only non-reserved device type generic_device_type: Final = ZCLAttributeDef( id=0x0009, type=GenericLightingDeviceType, access="r" ) product_code: Final = ZCLAttributeDef(id=0x000A, type=t.LVBytes, access="r") product_url: Final = ZCLAttributeDef( id=0x000B, type=t.CharacterString, access="r" ) manufacturer_version_details: Final = ZCLAttributeDef( id=0x000C, type=t.CharacterString, access="r" ) serial_number: Final = ZCLAttributeDef( id=0x000D, type=t.CharacterString, access="r" ) product_label: Final = ZCLAttributeDef( id=0x000E, type=t.CharacterString, access="r" ) # Basic Device Settings location_desc: Final = ZCLAttributeDef( id=0x0010, type=t.LimitedCharString(16), access="rw" ) physical_env: Final = ZCLAttributeDef( id=0x0011, type=PhysicalEnvironment, access="rw" ) device_enabled: Final = ZCLAttributeDef(id=0x0012, type=t.Bool, access="rw") alarm_mask: Final = ZCLAttributeDef(id=0x0013, type=AlarmMask, access="rw") disable_local_config: Final = ZCLAttributeDef( id=0x0014, type=DisableLocalConfig, access="rw" ) sw_build_id: Final = ZCLAttributeDef( id=0x4000, type=t.CharacterString, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): reset_fact_default: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Client_to_Server ) def handle_read_attribute_zcl_version(self) -> t.uint8_t: return t.uint8_t(8) def handle_read_attribute_power_source(self) -> PowerSource: return PowerSource.DC_Source class MainsAlarmMask(t.bitmap8): Voltage_Too_Low = 0b00000001 Voltage_Too_High = 0b00000010 Power_Supply_Unavailable = 0b00000100 class BatterySize(t.enum8): No_battery = 0x00 Built_in = 0x01 Other = 0x02 AA = 0x03 AAA = 0x04 C = 0x05 D = 0x06 CR2 = 0x07 CR123A = 0x08 Unknown = 0xFF class PowerConfiguration(Cluster): """Attributes for determining more detailed information about a device’s power source(s), and for configuring under/over voltage alarms. """ MainsAlarmMask: Final = MainsAlarmMask BatterySize: Final = BatterySize cluster_id: Final[t.uint16_t] = 0x0001 name: Final = "Power Configuration" ep_attribute: Final = "power" class AttributeDefs(BaseAttributeDefs): # Mains Information mains_voltage: Final = ZCLAttributeDef(id=0x0000, type=t.uint16_t, access="r") mains_frequency: Final = ZCLAttributeDef(id=0x0001, type=t.uint8_t, access="r") # Mains Settings mains_alarm_mask: Final = ZCLAttributeDef( id=0x0010, type=MainsAlarmMask, access="rw" ) mains_volt_min_thres: Final = ZCLAttributeDef( id=0x0011, type=t.uint16_t, access="rw" ) mains_volt_max_thres: Final = ZCLAttributeDef( id=0x0012, type=t.uint16_t, access="rw" ) mains_voltage_dwell_trip_point: Final = ZCLAttributeDef( id=0x0013, type=t.uint16_t, access="rw" ) # Battery Information battery_voltage: Final = ZCLAttributeDef(id=0x0020, type=t.uint8_t, access="r") battery_percentage_remaining: Final = ZCLAttributeDef( id=0x0021, type=t.uint8_t, access="rp" ) # Battery Settings battery_manufacturer: Final = ZCLAttributeDef( id=0x0030, type=t.LimitedCharString(16), access="rw" ) battery_size: Final = ZCLAttributeDef(id=0x0031, type=BatterySize, access="rw") battery_a_hr_rating: Final = ZCLAttributeDef( id=0x0032, type=t.uint16_t, access="rw" ) # measured in units of 10mAHr battery_quantity: Final = ZCLAttributeDef( id=0x0033, type=t.uint8_t, access="rw" ) battery_rated_voltage: Final = ZCLAttributeDef( id=0x0034, type=t.uint8_t, access="rw" ) # measured in units of 100mV battery_alarm_mask: Final = ZCLAttributeDef( id=0x0035, type=t.bitmap8, access="rw" ) battery_volt_min_thres: Final = ZCLAttributeDef( id=0x0036, type=t.uint8_t, access="rw" ) battery_volt_thres1: Final = ZCLAttributeDef( id=0x0037, type=t.uint16_t, access="r*w" ) battery_volt_thres2: Final = ZCLAttributeDef( id=0x0038, type=t.uint16_t, access="r*w" ) battery_volt_thres3: Final = ZCLAttributeDef( id=0x0039, type=t.uint16_t, access="r*w" ) battery_percent_min_thres: Final = ZCLAttributeDef( id=0x003A, type=t.uint8_t, access="r*w" ) battery_percent_thres1: Final = ZCLAttributeDef( id=0x003B, type=t.uint8_t, access="r*w" ) battery_percent_thres2: Final = ZCLAttributeDef( id=0x003C, type=t.uint8_t, access="r*w" ) battery_percent_thres3: Final = ZCLAttributeDef( id=0x003D, type=t.uint8_t, access="r*w" ) battery_alarm_state: Final = ZCLAttributeDef( id=0x003E, type=t.bitmap32, access="rp" ) # Battery 2 Information battery_2_voltage: Final = ZCLAttributeDef( id=0x0040, type=t.uint8_t, access="r" ) battery_2_percentage_remaining: Final = ZCLAttributeDef( id=0x0041, type=t.uint8_t, access="rp" ) # Battery 2 Settings battery_2_manufacturer: Final = ZCLAttributeDef( id=0x0050, type=t.CharacterString, access="rw" ) battery_2_size: Final = ZCLAttributeDef( id=0x0051, type=BatterySize, access="rw" ) battery_2_a_hr_rating: Final = ZCLAttributeDef( id=0x0052, type=t.uint16_t, access="rw" ) battery_2_quantity: Final = ZCLAttributeDef( id=0x0053, type=t.uint8_t, access="rw" ) battery_2_rated_voltage: Final = ZCLAttributeDef( id=0x0054, type=t.uint8_t, access="rw" ) battery_2_alarm_mask: Final = ZCLAttributeDef( id=0x0055, type=t.bitmap8, access="rw" ) battery_2_volt_min_thres: Final = ZCLAttributeDef( id=0x0056, type=t.uint8_t, access="rw" ) battery_2_volt_thres1: Final = ZCLAttributeDef( id=0x0057, type=t.uint16_t, access="r*w" ) battery_2_volt_thres2: Final = ZCLAttributeDef( id=0x0058, type=t.uint16_t, access="r*w" ) battery_2_volt_thres3: Final = ZCLAttributeDef( id=0x0059, type=t.uint16_t, access="r*w" ) battery_2_percent_min_thres: Final = ZCLAttributeDef( id=0x005A, type=t.uint8_t, access="r*w" ) battery_2_percent_thres1: Final = ZCLAttributeDef( id=0x005B, type=t.uint8_t, access="r*w" ) battery_2_percent_thres2: Final = ZCLAttributeDef( id=0x005C, type=t.uint8_t, access="r*w" ) battery_2_percent_thres3: Final = ZCLAttributeDef( id=0x005D, type=t.uint8_t, access="r*w" ) battery_2_alarm_state: Final = ZCLAttributeDef( id=0x005E, type=t.bitmap32, access="rp" ) # Battery 3 Information battery_3_voltage: Final = ZCLAttributeDef( id=0x0060, type=t.uint8_t, access="r" ) battery_3_percentage_remaining: Final = ZCLAttributeDef( id=0x0061, type=t.uint8_t, access="rp" ) # Battery 3 Settings battery_3_manufacturer: Final = ZCLAttributeDef( id=0x0070, type=t.CharacterString, access="rw" ) battery_3_size: Final = ZCLAttributeDef( id=0x0071, type=BatterySize, access="rw" ) battery_3_a_hr_rating: Final = ZCLAttributeDef( id=0x0072, type=t.uint16_t, access="rw" ) battery_3_quantity: Final = ZCLAttributeDef( id=0x0073, type=t.uint8_t, access="rw" ) battery_3_rated_voltage: Final = ZCLAttributeDef( id=0x0074, type=t.uint8_t, access="rw" ) battery_3_alarm_mask: Final = ZCLAttributeDef( id=0x0075, type=t.bitmap8, access="rw" ) battery_3_volt_min_thres: Final = ZCLAttributeDef( id=0x0076, type=t.uint8_t, access="rw" ) battery_3_volt_thres1: Final = ZCLAttributeDef( id=0x0077, type=t.uint16_t, access="r*w" ) battery_3_volt_thres2: Final = ZCLAttributeDef( id=0x0078, type=t.uint16_t, access="r*w" ) battery_3_volt_thres3: Final = ZCLAttributeDef( id=0x0079, type=t.uint16_t, access="r*w" ) battery_3_percent_min_thres: Final = ZCLAttributeDef( id=0x007A, type=t.uint8_t, access="r*w" ) battery_3_percent_thres1: Final = ZCLAttributeDef( id=0x007B, type=t.uint8_t, access="r*w" ) battery_3_percent_thres2: Final = ZCLAttributeDef( id=0x007C, type=t.uint8_t, access="r*w" ) battery_3_percent_thres3: Final = ZCLAttributeDef( id=0x007D, type=t.uint8_t, access="r*w" ) battery_3_alarm_state: Final = ZCLAttributeDef( id=0x007E, type=t.bitmap32, access="rp" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class DeviceTempAlarmMask(t.bitmap8): Temp_too_low = 0b00000001 Temp_too_high = 0b00000010 class DeviceTemperature(Cluster): """Attributes for determining information about a device’s internal temperature, and for configuring under/over temperature alarms. """ DeviceTempAlarmMask: Final = DeviceTempAlarmMask cluster_id: Final[t.uint16_t] = 0x0002 name: Final = "Device Temperature" ep_attribute: Final = "device_temperature" class AttributeDefs(BaseAttributeDefs): # Device Temperature Information current_temperature: Final = ZCLAttributeDef( id=0x0000, type=t.int16s, access="r", mandatory=True ) min_temp_experienced: Final = ZCLAttributeDef( id=0x0001, type=t.int16s, access="r" ) max_temp_experienced: Final = ZCLAttributeDef( id=0x0002, type=t.int16s, access="r" ) over_temp_total_dwell: Final = ZCLAttributeDef( id=0x0003, type=t.uint16_t, access="r" ) # Device Temperature Settings dev_temp_alarm_mask: Final = ZCLAttributeDef( id=0x0010, type=DeviceTempAlarmMask, access="rw" ) low_temp_thres: Final = ZCLAttributeDef(id=0x0011, type=t.int16s, access="rw") high_temp_thres: Final = ZCLAttributeDef(id=0x0012, type=t.int16s, access="rw") low_temp_dwell_trip_point: Final = ZCLAttributeDef( id=0x0013, type=t.uint24_t, access="rw" ) high_temp_dwell_trip_point: Final = ZCLAttributeDef( id=0x0014, type=t.uint24_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class EffectIdentifier(t.enum8): Blink = 0x00 Breathe = 0x01 Okay = 0x02 Channel_change = 0x03 Finish_effect = 0xFE Stop_effect = 0xFF class EffectVariant(t.enum8): Default = 0x00 class Identify(Cluster): """Attributes and commands for putting a device into Identification mode (e.g. flashing a light) """ EffectIdentifier: Final = EffectIdentifier EffectVariant: Final = EffectVariant cluster_id: Final[t.uint16_t] = 0x0003 ep_attribute: Final = "identify" class AttributeDefs(BaseAttributeDefs): identify_time: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rw", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): identify: Final = ZCLCommandDef( id=0x00, schema={"identify_time": t.uint16_t}, direction=Direction.Client_to_Server, ) identify_query: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) # 0x02: ("ezmode_invoke", (t.bitmap8,), False), # 0x03: ("update_commission_state", (t.bitmap8,), False), trigger_effect: Final = ZCLCommandDef( id=0x40, schema={"effect_id": EffectIdentifier, "effect_variant": EffectVariant}, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): identify_query_response: Final = ZCLCommandDef( id=0x00, schema={"timeout": t.uint16_t}, direction=Direction.Server_to_Client, ) class NameSupport(t.bitmap8): Supported = 0b10000000 class Groups(Cluster): """Attributes and commands for group configuration and manipulation. """ NameSupport: Final = NameSupport cluster_id: Final[t.uint16_t] = 0x0004 ep_attribute: Final = "groups" class AttributeDefs(BaseAttributeDefs): name_support: Final = ZCLAttributeDef( id=0x0000, type=NameSupport, access="r", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): add: Final = ZCLCommandDef( id=0x00, schema={"group_id": t.Group, "group_name": t.LimitedCharString(16)}, direction=Direction.Client_to_Server, ) view: Final = ZCLCommandDef( id=0x01, schema={"group_id": t.Group}, direction=Direction.Client_to_Server ) get_membership: Final = ZCLCommandDef( id=0x02, schema={"groups": t.LVList[t.Group]}, direction=Direction.Client_to_Server, ) remove: Final = ZCLCommandDef( id=0x03, schema={"group_id": t.Group}, direction=Direction.Client_to_Server ) remove_all: Final = ZCLCommandDef( id=0x04, schema={}, direction=Direction.Client_to_Server ) add_if_identifying: Final = ZCLCommandDef( id=0x05, schema={"group_id": t.Group, "group_name": t.LimitedCharString(16)}, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): add_response: Final = ZCLCommandDef( id=0x00, schema={"status": foundation.Status, "group_id": t.Group}, direction=Direction.Server_to_Client, ) view_response: Final = ZCLCommandDef( id=0x01, schema={ "status": foundation.Status, "group_id": t.Group, "group_name": t.LimitedCharString(16), }, direction=Direction.Server_to_Client, ) get_membership_response: Final = ZCLCommandDef( id=0x02, schema={"capacity": t.uint8_t, "groups": t.LVList[t.Group]}, direction=Direction.Server_to_Client, ) remove_response: Final = ZCLCommandDef( id=0x03, schema={"status": foundation.Status, "group_id": t.Group}, direction=Direction.Server_to_Client, ) class Scenes(Cluster): """Attributes and commands for scene configuration and manipulation. """ NameSupport: Final = NameSupport cluster_id: Final[t.uint16_t] = 0x0005 ep_attribute: Final = "scenes" class AttributeDefs(BaseAttributeDefs): # Scene Management Information count: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="r", mandatory=True ) current_scene: Final = ZCLAttributeDef( id=0x0001, type=t.uint8_t, access="r", mandatory=True ) current_group: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) scene_valid: Final = ZCLAttributeDef( id=0x0003, type=t.Bool, access="r", mandatory=True ) name_support: Final = ZCLAttributeDef( id=0x0004, type=NameSupport, access="r", mandatory=True ) last_configured_by: Final = ZCLAttributeDef(id=0x0005, type=t.EUI64, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): add: Final = ZCLCommandDef( id=0x00, schema={ "group_id": t.Group, "scene_id": t.uint8_t, "transition_time": t.uint16_t, "scene_name": t.LimitedCharString(16), }, direction=Direction.Client_to_Server, ) # TODO: + extension field sets view: Final = ZCLCommandDef( id=0x01, schema={"group_id": t.Group, "scene_id": t.uint8_t}, direction=Direction.Client_to_Server, ) remove: Final = ZCLCommandDef( id=0x02, schema={"group_id": t.Group, "scene_id": t.uint8_t}, direction=Direction.Client_to_Server, ) remove_all: Final = ZCLCommandDef( id=0x03, schema={"group_id": t.Group}, direction=Direction.Client_to_Server ) store: Final = ZCLCommandDef( id=0x04, schema={"group_id": t.Group, "scene_id": t.uint8_t}, direction=Direction.Client_to_Server, ) recall: Final = ZCLCommandDef( id=0x05, schema={ "group_id": t.Group, "scene_id": t.uint8_t, "transition_time?": t.uint16_t, }, direction=Direction.Client_to_Server, ) get_scene_membership: Final = ZCLCommandDef( id=0x06, schema={"group_id": t.Group}, direction=Direction.Client_to_Server ) enhanced_add: Final = ZCLCommandDef( id=0x40, schema={ "group_id": t.Group, "scene_id": t.uint8_t, "transition_time": t.uint16_t, "scene_name": t.LimitedCharString(16), }, direction=Direction.Client_to_Server, ) enhanced_view: Final = ZCLCommandDef( id=0x41, schema={"group_id": t.Group, "scene_id": t.uint8_t}, direction=Direction.Client_to_Server, ) copy: Final = ZCLCommandDef( id=0x42, schema={ "mode": t.uint8_t, "group_id_from": t.uint16_t, "scene_id_from": t.uint8_t, "group_id_to": t.uint16_t, "scene_id_to": t.uint8_t, }, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): add_scene_response: Final = ZCLCommandDef( id=0x00, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, }, direction=Direction.Server_to_Client, ) view_response: Final = ZCLCommandDef( id=0x01, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, "transition_time?": t.uint16_t, "scene_name?": t.LimitedCharString(16), }, direction=Direction.Server_to_Client, ) # TODO: + extension field sets remove_scene_response: Final = ZCLCommandDef( id=0x02, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, }, direction=Direction.Server_to_Client, ) remove_all_scenes_response: Final = ZCLCommandDef( id=0x03, schema={"status": foundation.Status, "group_id": t.Group}, direction=Direction.Server_to_Client, ) store_scene_response: Final = ZCLCommandDef( id=0x04, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, }, direction=Direction.Server_to_Client, ) get_scene_membership_response: Final = ZCLCommandDef( id=0x06, schema={ "status": foundation.Status, "capacity": t.uint8_t, "group_id": t.Group, "scenes?": t.LVList[t.uint8_t], }, direction=Direction.Server_to_Client, ) enhanced_add_response: Final = ZCLCommandDef( id=0x40, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, }, direction=Direction.Server_to_Client, ) enhanced_view_response: Final = ZCLCommandDef( id=0x41, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, "transition_time?": t.uint16_t, "scene_name?": t.LimitedCharString(16), }, direction=Direction.Server_to_Client, ) # TODO: + extension field sets copy_response: Final = ZCLCommandDef( id=0x42, schema={ "status": foundation.Status, "group_id": t.Group, "scene_id": t.uint8_t, }, direction=Direction.Server_to_Client, ) class StartUpOnOff(t.enum8): Off = 0x00 On = 0x01 Toggle = 0x02 PreviousValue = 0xFF class OffEffectIdentifier(t.enum8): Delayed_All_Off = 0x00 Dying_Light = 0x01 class OnOffControl(t.bitmap8): Accept_Only_When_On = 0b00000001 class OnOff(Cluster): """Attributes and commands for switching devices between ‘On’ and ‘Off’ states. """ StartUpOnOff: Final = StartUpOnOff OffEffectIdentifier: Final = OffEffectIdentifier OnOffControl: Final = OnOffControl DELAYED_ALL_OFF_FADE_TO_OFF = 0x00 DELAYED_ALL_OFF_NO_FADE = 0x01 DELAYED_ALL_OFF_DIM_THEN_FADE_TO_OFF = 0x02 DYING_LIGHT_DIM_UP_THEN_FADE_TO_OFF = 0x00 cluster_id: Final[t.uint16_t] = 0x0006 name: Final = "On/Off" ep_attribute: Final = "on_off" class AttributeDefs(BaseAttributeDefs): on_off: Final = ZCLAttributeDef( id=0x0000, type=t.Bool, access="rps", mandatory=True ) global_scene_control: Final = ZCLAttributeDef( id=0x4000, type=t.Bool, access="r" ) on_time: Final = ZCLAttributeDef(id=0x4001, type=t.uint16_t, access="rw") off_wait_time: Final = ZCLAttributeDef(id=0x4002, type=t.uint16_t, access="rw") start_up_on_off: Final = ZCLAttributeDef( id=0x4003, type=StartUpOnOff, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): off: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Client_to_Server ) on: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) toggle: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) off_with_effect: Final = ZCLCommandDef( id=0x40, schema={"effect_id": OffEffectIdentifier, "effect_variant": t.uint8_t}, direction=Direction.Client_to_Server, ) on_with_recall_global_scene: Final = ZCLCommandDef( id=0x41, schema={}, direction=Direction.Client_to_Server ) on_with_timed_off: Final = ZCLCommandDef( id=0x42, schema={ "on_off_control": OnOffControl, "on_time": t.uint16_t, "off_wait_time": t.uint16_t, }, direction=Direction.Client_to_Server, ) class SwitchType(t.enum8): Toggle = 0x00 Momentary = 0x01 Multifunction = 0x02 class SwitchActions(t.enum8): OnOff = 0x00 OffOn = 0x01 ToggleToggle = 0x02 class OnOffConfiguration(Cluster): """Attributes and commands for configuring On/Off switching devices""" SwitchType: Final = SwitchType SwitchActions: Final = SwitchActions cluster_id: Final[t.uint16_t] = 0x0007 name: Final = "On/Off Switch Configuration" ep_attribute: Final = "on_off_config" class AttributeDefs(BaseAttributeDefs): switch_type: Final = ZCLAttributeDef( id=0x0000, type=SwitchType, access="r", mandatory=True ) switch_actions: Final = ZCLAttributeDef( id=0x0010, type=SwitchActions, access="rw", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class MoveMode(t.enum8): Up = 0x00 Down = 0x01 class StepMode(t.enum8): Up = 0x00 Down = 0x01 class OptionsMask(t.bitmap8): Execute_if_off_present = 0b00000001 Couple_color_temp_to_level_present = 0b00000010 class Options(t.bitmap8): Execute_if_off = 0b00000001 Couple_color_temp_to_level = 0b00000010 class LevelControl(Cluster): """Attributes and commands for controlling devices that can be set to a level between fully ‘On’ and fully ‘Off’. """ MoveMode: Final = MoveMode StepMode: Final = StepMode Options: Final = Options OptionsMask: Final = OptionsMask cluster_id: Final[t.uint16_t] = 0x0008 name: Final = "Level control" ep_attribute: Final = "level" class AttributeDefs(BaseAttributeDefs): current_level: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="rps", mandatory=True ) remaining_time: Final = ZCLAttributeDef(id=0x0001, type=t.uint16_t, access="r") min_level: Final = ZCLAttributeDef(id=0x0002, type=t.uint8_t, access="r") max_level: Final = ZCLAttributeDef(id=0x0003, type=t.uint8_t, access="r") current_frequency: Final = ZCLAttributeDef( id=0x0004, type=t.uint16_t, access="rps" ) min_frequency: Final = ZCLAttributeDef(id=0x0005, type=t.uint16_t, access="r") max_frequency: Final = ZCLAttributeDef(id=0x0006, type=t.uint16_t, access="r") options: Final = ZCLAttributeDef(id=0x000F, type=t.bitmap8, access="rw") on_off_transition_time: Final = ZCLAttributeDef( id=0x0010, type=t.uint16_t, access="rw" ) on_level: Final = ZCLAttributeDef(id=0x0011, type=t.uint8_t, access="rw") on_transition_time: Final = ZCLAttributeDef( id=0x0012, type=t.uint16_t, access="rw" ) off_transition_time: Final = ZCLAttributeDef( id=0x0013, type=t.uint16_t, access="rw" ) default_move_rate: Final = ZCLAttributeDef( id=0x0014, type=t.uint8_t, access="rw" ) start_up_current_level: Final = ZCLAttributeDef( id=0x4000, type=t.uint8_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): move_to_level: Final = ZCLCommandDef( id=0x00, schema={ "level": t.uint8_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=Direction.Client_to_Server, ) move: Final = ZCLCommandDef( id=0x01, schema={ "move_mode": MoveMode, "rate": t.uint8_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=Direction.Client_to_Server, ) step: Final = ZCLCommandDef( id=0x02, schema={ "step_mode": StepMode, "step_size": t.uint8_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=Direction.Client_to_Server, ) stop: Final = ZCLCommandDef( id=0x03, schema={ "options_mask?": OptionsMask, "options_override?": Options, }, direction=Direction.Client_to_Server, ) move_to_level_with_on_off: Final = ZCLCommandDef( id=0x04, schema={"level": t.uint8_t, "transition_time": t.uint16_t}, direction=Direction.Client_to_Server, ) move_with_on_off: Final = ZCLCommandDef( id=0x05, schema={"move_mode": MoveMode, "rate": t.uint8_t}, direction=Direction.Client_to_Server, ) step_with_on_off: Final = ZCLCommandDef( id=0x06, schema={ "step_mode": StepMode, "step_size": t.uint8_t, "transition_time": t.uint16_t, }, direction=Direction.Client_to_Server, ) stop_with_on_off: Final = ZCLCommandDef( id=0x07, schema={}, direction=Direction.Client_to_Server ) move_to_closest_frequency: Final = ZCLCommandDef( id=0x08, schema={"frequency": t.uint16_t}, direction=Direction.Client_to_Server, ) class Alarms(Cluster): """Attributes and commands for sending notifications and configuring alarm functionality. """ cluster_id: Final[t.uint16_t] = 0x0009 ep_attribute: Final = "alarms" class AttributeDefs(BaseAttributeDefs): alarm_count: Final = ZCLAttributeDef(id=0x0000, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): reset_alarm: Final = ZCLCommandDef( id=0x00, schema={"alarm_code": t.uint8_t, "cluster_id": t.uint16_t}, direction=Direction.Client_to_Server, ) reset_all_alarms: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) get_alarm: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) reset_alarm_log: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Client_to_Server ) # 0x04: ("publish_event_log", {}, False), class ClientCommandDefs(BaseCommandDefs): alarm: Final = ZCLCommandDef( id=0x00, schema={"alarm_code": t.uint8_t, "cluster_id": t.uint16_t}, direction=Direction.Client_to_Server, ) get_alarm_response: Final = ZCLCommandDef( id=0x01, schema={ "status": foundation.Status, "alarm_code?": t.uint8_t, "cluster_id?": t.uint16_t, "timestamp?": t.uint32_t, }, direction=Direction.Server_to_Client, ) # 0x02: ("get_event_log", {}, False), class TimeStatus(t.bitmap8): Master = 0b00000001 Synchronized = 0b00000010 Master_for_Zone_and_DST = 0b00000100 Superseding = 0b00001000 class Time(Cluster): """Attributes and commands that provide a basic interface to a real-time clock. """ TimeStatus: Final = TimeStatus cluster_id: Final[t.uint16_t] = 0x000A ep_attribute: Final = "time" class AttributeDefs(BaseAttributeDefs): time: Final = ZCLAttributeDef( id=0x0000, type=t.UTCTime, access="r*w", mandatory=True ) time_status: Final = ZCLAttributeDef( id=0x0001, type=TimeStatus, access="r*w", mandatory=True ) time_zone: Final = ZCLAttributeDef(id=0x0002, type=t.int32s, access="rw") dst_start: Final = ZCLAttributeDef(id=0x0003, type=t.uint32_t, access="rw") dst_end: Final = ZCLAttributeDef(id=0x0004, type=t.uint32_t, access="rw") dst_shift: Final = ZCLAttributeDef(id=0x0005, type=t.int32s, access="rw") standard_time: Final = ZCLAttributeDef( id=0x0006, type=t.StandardTime, access="r" ) local_time: Final = ZCLAttributeDef(id=0x0007, type=t.LocalTime, access="r") last_set_time: Final = ZCLAttributeDef(id=0x0008, type=t.UTCTime, access="r") valid_until_time: Final = ZCLAttributeDef( id=0x0009, type=t.UTCTime, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR def handle_read_attribute_time(self) -> t.UTCTime: now = datetime.now(timezone.utc) return t.UTCTime((now - ZIGBEE_EPOCH).total_seconds()) def handle_read_attribute_time_status(self) -> TimeStatus: return ( TimeStatus.Master | TimeStatus.Synchronized | TimeStatus.Master_for_Zone_and_DST ) def handle_read_attribute_time_zone(self) -> t.int32s: tz_offset = datetime.now().astimezone().utcoffset() assert tz_offset is not None return t.int32s(tz_offset.total_seconds()) def handle_read_attribute_local_time(self) -> t.LocalTime: now = datetime.now(timezone.utc) tz_offset = datetime.now().astimezone().utcoffset() assert tz_offset is not None return t.LocalTime((now + tz_offset - ZIGBEE_EPOCH).total_seconds()) class LocationMethod(t.enum8): Lateration = 0x00 Signposting = 0x01 RF_fingerprinting = 0x02 Out_of_band = 0x03 Centralized = 0x04 class NeighborInfo(t.Struct): neighbor: t.EUI64 x: t.int16s y: t.int16s z: t.int16s rssi: t.int8s num_measurements: t.uint8_t class RSSILocation(Cluster): """Attributes and commands that provide a means for exchanging location information and channel parameters among devices. """ LocationMethod: Final = LocationMethod NeighborInfo: Final = NeighborInfo cluster_id: Final[t.uint16_t] = 0x000B ep_attribute: Final = "rssi_location" class AttributeDefs(BaseAttributeDefs): # Location Information type: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="rw", mandatory=True ) method: Final = ZCLAttributeDef( id=0x0001, type=LocationMethod, access="rw", mandatory=True ) age: Final = ZCLAttributeDef(id=0x0002, type=t.uint16_t, access="r") quality_measure: Final = ZCLAttributeDef(id=0x0003, type=t.uint8_t, access="r") num_of_devices: Final = ZCLAttributeDef(id=0x0004, type=t.uint8_t, access="r") # Location Settings coordinate1: Final = ZCLAttributeDef( id=0x0010, type=t.int16s, access="rw", mandatory=True ) coordinate2: Final = ZCLAttributeDef( id=0x0011, type=t.int16s, access="rw", mandatory=True ) coordinate3: Final = ZCLAttributeDef(id=0x0012, type=t.int16s, access="rw") power: Final = ZCLAttributeDef( id=0x0013, type=t.int16s, access="rw", mandatory=True ) path_loss_exponent: Final = ZCLAttributeDef( id=0x0014, type=t.uint16_t, access="rw", mandatory=True ) reporting_period: Final = ZCLAttributeDef( id=0x0015, type=t.uint16_t, access="rw" ) calculation_period: Final = ZCLAttributeDef( id=0x0016, type=t.uint16_t, access="rw" ) number_rssi_measurements: Final = ZCLAttributeDef( id=0x0017, type=t.uint8_t, access="rw", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): set_absolute_location: Final = ZCLCommandDef( id=0x00, schema={ "coordinate1": t.int16s, "coordinate2": t.int16s, "coordinate3": t.int16s, "power": t.int16s, "path_loss_exponent": t.uint16_t, }, direction=Direction.Client_to_Server, ) set_dev_config: Final = ZCLCommandDef( id=0x01, schema={ "power": t.int16s, "path_loss_exponent": t.uint16_t, "calculation_period": t.uint16_t, "num_rssi_measurements": t.uint8_t, "reporting_period": t.uint16_t, }, direction=Direction.Client_to_Server, ) get_dev_config: Final = ZCLCommandDef( id=0x02, schema={"target_addr": t.EUI64}, direction=Direction.Client_to_Server, ) get_location_data: Final = ZCLCommandDef( id=0x03, schema={ "packed": t.bitmap8, "num_responses": t.uint8_t, "target_addr": t.EUI64, }, direction=Direction.Client_to_Server, ) rssi_response: Final = ZCLCommandDef( id=0x04, schema={ "replying_device": t.EUI64, "x": t.int16s, "y": t.int16s, "z": t.int16s, "rssi": t.int8s, "num_rssi_measurements": t.uint8_t, }, direction=Direction.Server_to_Client, ) send_pings: Final = ZCLCommandDef( id=0x05, schema={ "target_addr": t.EUI64, "num_rssi_measurements": t.uint8_t, "calculation_period": t.uint16_t, }, direction=Direction.Client_to_Server, ) anchor_node_announce: Final = ZCLCommandDef( id=0x06, schema={ "anchor_node_ieee_addr": t.EUI64, "x": t.int16s, "y": t.int16s, "z": t.int16s, }, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): dev_config_response: Final = ZCLCommandDef( id=0x00, schema={ "status": foundation.Status, "power?": t.int16s, "path_loss_exponent?": t.uint16_t, "calculation_period?": t.uint16_t, "num_rssi_measurements?": t.uint8_t, "reporting_period?": t.uint16_t, }, direction=Direction.Server_to_Client, ) location_data_response: Final = ZCLCommandDef( id=0x01, schema={ "status": foundation.Status, "location_type?": t.uint8_t, "coordinate1?": t.int16s, "coordinate2?": t.int16s, "coordinate3?": t.int16s, "power?": t.uint16_t, "path_loss_exponent?": t.uint8_t, "location_method?": t.uint8_t, "quality_measure?": t.uint8_t, "location_age?": t.uint16_t, }, direction=Direction.Server_to_Client, ) location_data_notification: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) compact_location_data_notification: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Client_to_Server ) rssi_ping: Final = ZCLCommandDef( id=0x04, schema={"location_type": t.uint8_t}, direction=Direction.Client_to_Server, ) rssi_req: Final = ZCLCommandDef( id=0x05, schema={}, direction=Direction.Client_to_Server ) report_rssi_measurements: Final = ZCLCommandDef( id=0x06, schema={ "measuring_device": t.EUI64, "neighbors": t.LVList[NeighborInfo], }, direction=Direction.Client_to_Server, ) request_own_location: Final = ZCLCommandDef( id=0x07, schema={"ieee_of_blind_node": t.EUI64}, direction=Direction.Client_to_Server, ) class Reliability(t.enum8): No_fault_detected = 0 No_sensor = 1 Over_range = 2 Under_range = 3 Open_loop = 4 Shorted_loop = 5 No_output = 6 Unreliable_other = 7 Process_error = 8 Multi_state_fault = 9 Configuration_error = 10 class AnalogInput(Cluster): Reliability: Final = Reliability cluster_id: Final[t.uint16_t] = 0x000C ep_attribute: Final = "analog_input" class AttributeDefs(BaseAttributeDefs): description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) max_present_value: Final = ZCLAttributeDef( id=0x0041, type=t.Single, access="r*w" ) min_present_value: Final = ZCLAttributeDef( id=0x0045, type=t.Single, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.Single, access="rwp", mandatory=True ) reliability: Final = ZCLAttributeDef(id=0x0067, type=Reliability, access="r*w") resolution: Final = ZCLAttributeDef(id=0x006A, type=t.Single, access="r*w") status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="rp", mandatory=True ) engineering_units: Final = ZCLAttributeDef( id=0x0075, type=t.enum16, access="r*w" ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class AnalogOutput(Cluster): cluster_id: Final[t.uint16_t] = 0x000D ep_attribute: Final = "analog_output" class AttributeDefs(BaseAttributeDefs): description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) max_present_value: Final = ZCLAttributeDef( id=0x0041, type=t.Single, access="r*w" ) min_present_value: Final = ZCLAttributeDef( id=0x0045, type=t.Single, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.Single, access="rwp", mandatory=True ) # 0x0057: ZCLAttributeDef('priority_array', type=TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") relinquish_default: Final = ZCLAttributeDef( id=0x0068, type=t.Single, access="r*w" ) resolution: Final = ZCLAttributeDef(id=0x006A, type=t.Single, access="r*w") status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="rp", mandatory=True ) engineering_units: Final = ZCLAttributeDef( id=0x0075, type=t.enum16, access="r*w" ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class AnalogValue(Cluster): cluster_id: Final[t.uint16_t] = 0x000E ep_attribute: Final = "analog_value" class AttributeDefs(BaseAttributeDefs): description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.Single, access="rw", mandatory=True ) # 0x0057: ('priority_array', TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") relinquish_default: Final = ZCLAttributeDef( id=0x0068, type=t.Single, access="r*w" ) status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) engineering_units: Final = ZCLAttributeDef( id=0x0075, type=t.enum16, access="r*w" ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class BinaryInput(Cluster): cluster_id: Final[t.uint16_t] = 0x000F name: Final = "Binary Input (Basic)" ep_attribute: Final = "binary_input" class AttributeDefs(BaseAttributeDefs): active_text: Final = ZCLAttributeDef( id=0x0004, type=t.CharacterString, access="r*w" ) description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) inactive_text: Final = ZCLAttributeDef( id=0x002E, type=t.CharacterString, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) polarity: Final = ZCLAttributeDef(id=0x0054, type=t.enum8, access="r") present_value: Final = ZCLAttributeDef( id=0x0055, type=t.Bool, access="r*w", mandatory=True ) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class BinaryOutput(Cluster): cluster_id: Final[t.uint16_t] = 0x0010 ep_attribute: Final = "binary_output" class AttributeDefs(BaseAttributeDefs): active_text: Final = ZCLAttributeDef( id=0x0004, type=t.CharacterString, access="r*w" ) description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) inactive_text: Final = ZCLAttributeDef( id=0x002E, type=t.CharacterString, access="r*w" ) minimum_off_time: Final = ZCLAttributeDef( id=0x0042, type=t.uint32_t, access="r*w" ) minimum_on_time: Final = ZCLAttributeDef( id=0x0043, type=t.uint32_t, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) polarity: Final = ZCLAttributeDef(id=0x0054, type=t.enum8, access="r") present_value: Final = ZCLAttributeDef( id=0x0055, type=t.Bool, access="r*w", mandatory=True ) # 0x0057: ('priority_array', TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") relinquish_default: Final = ZCLAttributeDef( id=0x0068, type=t.Bool, access="r*w" ) status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class BinaryValue(Cluster): cluster_id: Final[t.uint16_t] = 0x0011 ep_attribute: Final = "binary_value" class AttributeDefs(BaseAttributeDefs): active_text: Final = ZCLAttributeDef( id=0x0004, type=t.CharacterString, access="r*w" ) description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) inactive_text: Final = ZCLAttributeDef( id=0x002E, type=t.CharacterString, access="r*w" ) minimum_off_time: Final = ZCLAttributeDef( id=0x0042, type=t.uint32_t, access="r*w" ) minimum_on_time: Final = ZCLAttributeDef( id=0x0043, type=t.uint32_t, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.Single, access="r*w", mandatory=True ) # 0x0057: ZCLAttributeDef('priority_array', type=TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") relinquish_default: Final = ZCLAttributeDef( id=0x0068, type=t.Single, access="r*w" ) status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class MultistateInput(Cluster): cluster_id: Final[t.uint16_t] = 0x0012 ep_attribute: Final = "multistate_input" class AttributeDefs(BaseAttributeDefs): state_text: Final = ZCLAttributeDef( id=0x000E, type=t.LVList[t.CharacterString, t.uint16_t], access="r*w" ) description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) number_of_states: Final = ZCLAttributeDef( id=0x004A, type=t.uint16_t, access="r*w" ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.uint16_t, access="r*w", mandatory=True ) # 0x0057: ('priority_array', TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class MultistateOutput(Cluster): cluster_id: Final[t.uint16_t] = 0x0013 ep_attribute: Final = "multistate_output" class AttributeDefs(BaseAttributeDefs): state_text: Final = ZCLAttributeDef( id=0x000E, type=t.LVList[t.CharacterString, t.uint16_t], access="r*w" ) description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) number_of_states: Final = ZCLAttributeDef( id=0x004A, type=t.uint16_t, access="r*w", mandatory=True ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.uint16_t, access="r*w", mandatory=True ) # 0x0057: ZCLAttributeDef('priority_array', type=TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") relinquish_default: Final = ZCLAttributeDef( id=0x0068, type=t.uint16_t, access="r*w" ) status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class MultistateValue(Cluster): cluster_id: Final[t.uint16_t] = 0x0014 ep_attribute: Final = "multistate_value" class AttributeDefs(BaseAttributeDefs): state_text: Final = ZCLAttributeDef( id=0x000E, type=t.LVList[t.CharacterString, t.uint16_t], access="r*w" ) description: Final = ZCLAttributeDef( id=0x001C, type=t.CharacterString, access="r*w" ) number_of_states: Final = ZCLAttributeDef( id=0x004A, type=t.uint16_t, access="r*w", mandatory=True ) out_of_service: Final = ZCLAttributeDef( id=0x0051, type=t.Bool, access="r*w", mandatory=True ) present_value: Final = ZCLAttributeDef( id=0x0055, type=t.uint16_t, access="r*w", mandatory=True ) # 0x0057: ZCLAttributeDef('priority_array', type=TODO.array), # Array of 16 structures of (boolean, # single precision) reliability: Final = ZCLAttributeDef(id=0x0067, type=t.enum8, access="r*w") relinquish_default: Final = ZCLAttributeDef( id=0x0068, type=t.uint16_t, access="r*w" ) status_flags: Final = ZCLAttributeDef( id=0x006F, type=t.bitmap8, access="r", mandatory=True ) application_type: Final = ZCLAttributeDef( id=0x0100, type=t.uint32_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class StartupControl(t.enum8): Part_of_network = 0x00 Form_network = 0x01 Rejoin_network = 0x02 Start_from_scratch = 0x03 class NetworkKeyType(t.enum8): Standard = 0x01 class Commissioning(Cluster): """Attributes and commands for commissioning and managing a Zigbee device. """ StartupControl: Final = StartupControl NetworkKeyType: Final = NetworkKeyType cluster_id: Final[t.uint16_t] = 0x0015 ep_attribute: Final = "commissioning" class AttributeDefs(BaseAttributeDefs): # Startup Parameters short_address: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rw", mandatory=True ) extended_pan_id: Final = ZCLAttributeDef( id=0x0001, type=t.EUI64, access="rw", mandatory=True ) pan_id: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="rw", mandatory=True ) channel_mask: Final = ZCLAttributeDef( id=0x0003, type=t.Channels, access="rw", mandatory=True ) protocol_version: Final = ZCLAttributeDef( id=0x0004, type=t.uint8_t, access="rw", mandatory=True ) stack_profile: Final = ZCLAttributeDef( id=0x0005, type=t.uint8_t, access="rw", mandatory=True ) startup_control: Final = ZCLAttributeDef( id=0x0006, type=StartupControl, access="rw", mandatory=True ) trust_center_address: Final = ZCLAttributeDef( id=0x0010, type=t.EUI64, access="rw", mandatory=True ) trust_center_master_key: Final = ZCLAttributeDef( id=0x0011, type=t.KeyData, access="rw" ) network_key: Final = ZCLAttributeDef( id=0x0012, type=t.KeyData, access="rw", mandatory=True ) use_insecure_join: Final = ZCLAttributeDef( id=0x0013, type=t.Bool, access="rw", mandatory=True ) preconfigured_link_key: Final = ZCLAttributeDef( id=0x0014, type=t.KeyData, access="rw", mandatory=True ) network_key_seq_num: Final = ZCLAttributeDef( id=0x0015, type=t.uint8_t, access="rw", mandatory=True ) network_key_type: Final = ZCLAttributeDef( id=0x0016, type=NetworkKeyType, access="rw", mandatory=True ) network_manager_address: Final = ZCLAttributeDef( id=0x0017, type=t.uint16_t, access="rw", mandatory=True ) # Join Parameters scan_attempts: Final = ZCLAttributeDef(id=0x0020, type=t.uint8_t, access="rw") time_between_scans: Final = ZCLAttributeDef( id=0x0021, type=t.uint16_t, access="rw" ) rejoin_interval: Final = ZCLAttributeDef( id=0x0022, type=t.uint16_t, access="rw" ) max_rejoin_interval: Final = ZCLAttributeDef( id=0x0023, type=t.uint16_t, access="rw" ) # End Device Parameters indirect_poll_rate: Final = ZCLAttributeDef( id=0x0030, type=t.uint16_t, access="rw" ) parent_retry_threshold: Final = ZCLAttributeDef( id=0x0031, type=t.uint8_t, access="r" ) # Concentrator Parameters concentrator_flag: Final = ZCLAttributeDef(id=0x0040, type=t.Bool, access="rw") concentrator_radius: Final = ZCLAttributeDef( id=0x0041, type=t.uint8_t, access="rw" ) concentrator_discovery_time: Final = ZCLAttributeDef( id=0x0042, type=t.uint8_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): restart_device: Final = ZCLCommandDef( id=0x00, schema={"options": t.bitmap8, "delay": t.uint8_t, "jitter": t.uint8_t}, direction=Direction.Client_to_Server, ) save_startup_parameters: Final = ZCLCommandDef( id=0x01, schema={"options": t.bitmap8, "index": t.uint8_t}, direction=Direction.Client_to_Server, ) restore_startup_parameters: Final = ZCLCommandDef( id=0x02, schema={"options": t.bitmap8, "index": t.uint8_t}, direction=Direction.Client_to_Server, ) reset_startup_parameters: Final = ZCLCommandDef( id=0x03, schema={"options": t.bitmap8, "index": t.uint8_t}, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): restart_device_response: Final = ZCLCommandDef( id=0x00, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) save_startup_params_response: Final = ZCLCommandDef( id=0x01, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) restore_startup_params_response: Final = ZCLCommandDef( id=0x02, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) reset_startup_params_response: Final = ZCLCommandDef( id=0x03, schema={"status": foundation.Status}, direction=Direction.Server_to_Client, ) class Partition(Cluster): cluster_id: Final[t.uint16_t] = 0x0016 ep_attribute: Final = "partition" class AttributeDefs(BaseAttributeDefs): maximum_incoming_transfer_size: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="r", mandatory=True, ) maximum_outgoing_transfer_size: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True, ) partitioned_frame_size: Final = ZCLAttributeDef( id=0x0002, type=t.uint8_t, access="rw", mandatory=True ) large_frame_size: Final = ZCLAttributeDef( id=0x0003, type=t.uint16_t, access="rw", mandatory=True ) number_of_ack_frame: Final = ZCLAttributeDef( id=0x0004, type=t.uint8_t, access="rw", mandatory=True ) nack_timeout: Final = ZCLAttributeDef( id=0x0005, type=t.uint16_t, access="r", mandatory=True ) interframe_delay: Final = ZCLAttributeDef( id=0x0006, type=t.uint8_t, access="rw", mandatory=True ) number_of_send_retries: Final = ZCLAttributeDef( id=0x0007, type=t.uint8_t, access="r", mandatory=True ) sender_timeout: Final = ZCLAttributeDef( id=0x0008, type=t.uint16_t, access="r", mandatory=True ) receiver_timeout: Final = ZCLAttributeDef( id=0x0009, type=t.uint16_t, access="r", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ImageUpgradeStatus(t.enum8): Normal = 0x00 Download_in_progress = 0x01 Download_complete = 0x02 Waiting_to_upgrade = 0x03 Count_down = 0x04 Wait_for_more = 0x05 Waiting_to_Upgrade_via_External_Event = 0x06 class UpgradeActivationPolicy(t.enum8): OTA_server_allowed = 0x00 Out_of_band_allowed = 0x01 class UpgradeTimeoutPolicy(t.enum8): Apply_after_timeout = 0x00 Do_not_apply_after_timeout = 0x01 class ImageNotifyPayloadType(t.enum8): QueryJitter = 0x00 QueryJitter_ManufacturerCode = 0x01 QueryJitter_ManufacturerCode_ImageType = 0x02 QueryJitter_ManufacturerCode_ImageType_NewFileVersion = 0x03 class ImageNotifyCommand(foundation.CommandSchema): PayloadType = ImageNotifyPayloadType payload_type: ImageNotifyPayloadType query_jitter: t.uint8_t manufacturer_code: t.uint16_t = t.StructField( requires=( lambda s: s.payload_type >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode ) ) image_type: t.uint16_t = t.StructField( requires=( lambda s: s.payload_type >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode_ImageType ) ) new_file_version: t.uint32_t = t.StructField( requires=( lambda s: s.payload_type >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode_ImageType_NewFileVersion ) ) class QueryNextImageCommandFieldControl(t.bitmap8): HardwareVersion = 0b00000001 class QueryNextImageCommand(foundation.CommandSchema): FieldControl = QueryNextImageCommandFieldControl field_control: QueryNextImageCommandFieldControl manufacturer_code: t.uint16_t image_type: t.uint16_t current_file_version: t.uint32_t hardware_version: t.uint16_t = t.StructField( requires=( lambda s: s.field_control & QueryNextImageCommandFieldControl.HardwareVersion ) ) class ImageBlockCommandFieldControl(t.bitmap8): RequestNodeAddr = 0b00000001 MinimumBlockPeriod = 0b00000010 class ImageBlockCommand(foundation.CommandSchema): FieldControl = ImageBlockCommandFieldControl field_control: ImageBlockCommandFieldControl manufacturer_code: t.uint16_t image_type: t.uint16_t file_version: t.uint32_t file_offset: t.uint32_t maximum_data_size: t.uint8_t request_node_addr: t.EUI64 = t.StructField( requires=( lambda s: s.field_control & ImageBlockCommandFieldControl.RequestNodeAddr ) ) minimum_block_period: t.uint16_t = t.StructField( requires=( lambda s: s.field_control & ImageBlockCommandFieldControl.MinimumBlockPeriod ) ) class ImagePageCommandFieldControl(t.bitmap8): RequestNodeAddr = 0b00000001 class ImagePageCommand(foundation.CommandSchema): field_control: ImagePageCommandFieldControl manufacturer_code: t.uint16_t image_type: t.uint16_t file_version: t.uint32_t file_offset: t.uint32_t maximum_data_size: t.uint8_t page_size: t.uint16_t response_spacing: t.uint16_t request_node_addr: t.EUI64 = t.StructField( requires=lambda s: ( s.field_control & ImagePageCommandFieldControl.RequestNodeAddr ) ) class ImageBlockResponseCommand(foundation.CommandSchema): # All responses contain at least a status status: foundation.Status # Payload with `SUCCESS` status manufacturer_code: t.uint16_t = t.StructField( requires=lambda s: s.status == foundation.Status.SUCCESS ) image_type: t.uint16_t = t.StructField( requires=lambda s: s.status == foundation.Status.SUCCESS ) file_version: t.uint32_t = t.StructField( requires=lambda s: s.status == foundation.Status.SUCCESS ) file_offset: t.uint32_t = t.StructField( requires=lambda s: s.status == foundation.Status.SUCCESS ) image_data: t.LVBytes = t.StructField( requires=lambda s: s.status == foundation.Status.SUCCESS ) # Payload with `WAIT_FOR_DATA` status current_time: t.UTCTime = t.StructField( requires=lambda s: s.status == foundation.Status.WAIT_FOR_DATA ) request_time: t.UTCTime = t.StructField( requires=lambda s: s.status == foundation.Status.WAIT_FOR_DATA ) minimum_block_period: t.uint16_t = t.StructField( requires=lambda s: s.status == foundation.Status.WAIT_FOR_DATA ) class Ota(Cluster): ImageUpgradeStatus: Final = ImageUpgradeStatus UpgradeActivationPolicy: Final = UpgradeActivationPolicy UpgradeTimeoutPolicy: Final = UpgradeTimeoutPolicy ImageNotifyCommand: Final = ImageNotifyCommand QueryNextImageCommand: Final = QueryNextImageCommand ImageBlockCommand: Final = ImageBlockCommand ImagePageCommand: Final = ImagePageCommand ImageBlockResponseCommand: Final = ImageBlockResponseCommand cluster_id: Final[t.uint16_t] = 0x0019 ep_attribute: Final = "ota" class AttributeDefs(BaseAttributeDefs): upgrade_server_id: Final = ZCLAttributeDef( id=0x0000, type=t.EUI64, access="r", mandatory=True ) file_offset: Final = ZCLAttributeDef(id=0x0001, type=t.uint32_t, access="r") current_file_version: Final = ZCLAttributeDef( id=0x0002, type=t.uint32_t, access="r" ) current_zigbee_stack_version: Final = ZCLAttributeDef( id=0x0003, type=t.uint16_t, access="r" ) downloaded_file_version: Final = ZCLAttributeDef( id=0x0004, type=t.uint32_t, access="r" ) downloaded_zigbee_stack_version: Final = ZCLAttributeDef( id=0x0005, type=t.uint16_t, access="r" ) image_upgrade_status: Final = ZCLAttributeDef( id=0x0006, type=ImageUpgradeStatus, access="r", mandatory=True ) manufacturer_id: Final = ZCLAttributeDef(id=0x0007, type=t.uint16_t, access="r") image_type_id: Final = ZCLAttributeDef(id=0x0008, type=t.uint16_t, access="r") minimum_block_req_delay: Final = ZCLAttributeDef( id=0x0009, type=t.uint16_t, access="r" ) image_stamp: Final = ZCLAttributeDef(id=0x000A, type=t.uint32_t, access="r") upgrade_activation_policy: Final = ZCLAttributeDef( id=0x000B, type=UpgradeActivationPolicy, access="r" ) upgrade_timeout_policy: Final = ZCLAttributeDef( id=0x000C, type=UpgradeTimeoutPolicy, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): query_next_image: Final = ZCLCommandDef( id=0x01, schema=QueryNextImageCommand, direction=Direction.Client_to_Server ) image_block: Final = ZCLCommandDef( id=0x03, schema=ImageBlockCommand, direction=Direction.Client_to_Server ) image_page: Final = ZCLCommandDef( id=0x04, schema=ImagePageCommand, direction=Direction.Client_to_Server ) upgrade_end: Final = ZCLCommandDef( id=0x06, schema={ "status": foundation.Status, "manufacturer_code": t.uint16_t, "image_type": t.uint16_t, "file_version": t.uint32_t, }, direction=Direction.Client_to_Server, ) query_specific_file: Final = ZCLCommandDef( id=0x08, schema={ "request_node_addr": t.EUI64, "manufacturer_code": t.uint16_t, "image_type": t.uint16_t, "file_version": t.uint32_t, "current_zigbee_stack_version": t.uint16_t, }, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): image_notify: Final = ZCLCommandDef( id=0x00, schema=ImageNotifyCommand, direction=Direction.Client_to_Server ) query_next_image_response: Final = ZCLCommandDef( id=0x02, schema={ "status": foundation.Status, "manufacturer_code?": t.uint16_t, "image_type?": t.uint16_t, "file_version?": t.uint32_t, "image_size?": t.uint32_t, }, direction=Direction.Server_to_Client, ) image_block_response: Final = ZCLCommandDef( id=0x05, schema=ImageBlockResponseCommand, direction=Direction.Server_to_Client, ) upgrade_end_response: Final = ZCLCommandDef( id=0x07, schema={ "manufacturer_code": t.uint16_t, "image_type": t.uint16_t, "file_version": t.uint32_t, "current_time": t.UTCTime, "upgrade_time": t.UTCTime, }, direction=Direction.Server_to_Client, ) query_specific_file_response: Final = ZCLCommandDef( id=0x09, schema={ "status": foundation.Status, "manufacturer_code?": t.uint16_t, "image_type?": t.uint16_t, "file_version?": t.uint32_t, "image_size?": t.uint32_t, }, direction=Direction.Server_to_Client, ) def handle_cluster_request( self, hdr: foundation.ZCLHeader, args: list[Any], *, dst_addressing: AddressingMode | None = None, ): # We don't want the cluster to do anything here because it would interfere with # the OTA manager device = self.endpoint.device if device.ota_in_progress: return if ( hdr.direction == foundation.Direction.Client_to_Server and hdr.command_id == self.ServerCommandDefs.query_next_image.id ): self.create_catching_task( self._handle_query_next_image(hdr, args), ) elif ( hdr.direction == foundation.Direction.Client_to_Server and hdr.command_id == self.ServerCommandDefs.image_block.id ): self.create_catching_task( self._handle_image_block_req(hdr, args), ) async def _handle_query_next_image(self, hdr, cmd): # Always send no image available response so that the device stops asking await self.query_next_image_response( foundation.Status.NO_IMAGE_AVAILABLE, tsn=hdr.tsn ) device = self.endpoint.device images_result = await device.application.ota.get_ota_images(device, cmd) device.listener_event( "device_ota_image_query_result", images_result, cmd, ) async def _handle_image_block_req(self, hdr, cmd): # Abort any running firmware update (i.e. the integration is reloaded midway) await self.image_block_response(foundation.Status.ABORT, tsn=hdr.tsn) class ScheduleRecord(t.Struct): phase_id: t.uint8_t scheduled_time: t.uint16_t class PowerProfilePhase(t.Struct): energy_phase_id: t.uint8_t macro_phase_id: t.uint8_t expected_duration: t.uint16_t peak_power: t.uint16_t energy: t.uint16_t class PowerProfileType(t.Struct): power_profile_id: t.uint8_t energy_phase_id: t.uint8_t power_profile_remote_control: t.Bool power_profile_state: t.uint8_t class PowerProfile(Cluster): ScheduleRecord: Final = ScheduleRecord PowerProfilePhase: Final = PowerProfilePhase PowerProfile: Final = PowerProfileType cluster_id: Final[t.uint16_t] = 0x001A ep_attribute: Final = "power_profile" class AttributeDefs(BaseAttributeDefs): total_profile_num: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="r", mandatory=True ) multiple_scheduling: Final = ZCLAttributeDef( id=0x0001, type=t.Bool, access="r", mandatory=True ) energy_formatting: Final = ZCLAttributeDef( id=0x0002, type=t.bitmap8, access="r", mandatory=True ) energy_remote: Final = ZCLAttributeDef( id=0x0003, type=t.Bool, access="r", mandatory=True ) schedule_mode: Final = ZCLAttributeDef( id=0x0004, type=t.bitmap8, access="rwp", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): power_profile_request: Final = ZCLCommandDef( id=0x00, schema={"power_profile_id": t.uint8_t}, direction=Direction.Client_to_Server, ) power_profile_state_request: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) get_power_profile_price_response: Final = ZCLCommandDef( id=0x02, schema={ "power_profile_id": t.uint8_t, "currency": t.uint16_t, "price": t.uint32_t, "price_trailing_digit": t.uint8_t, }, direction=Direction.Server_to_Client, ) get_overall_schedule_price_response: Final = ZCLCommandDef( id=0x03, schema={ "currency": t.uint16_t, "price": t.uint32_t, "price_trailing_digit": t.uint8_t, }, direction=Direction.Server_to_Client, ) energy_phases_schedule_notification: Final = ZCLCommandDef( id=0x04, schema={ "power_profile_id": t.uint8_t, "scheduled_phases": t.LVList[ScheduleRecord], }, direction=Direction.Client_to_Server, ) energy_phases_schedule_response: Final = ZCLCommandDef( id=0x05, schema={ "power_profile_id": t.uint8_t, "scheduled_phases": t.LVList[ScheduleRecord], }, direction=Direction.Server_to_Client, ) power_profile_schedule_constraints_request: Final = ZCLCommandDef( id=0x06, schema={"power_profile_id": t.uint8_t}, direction=Direction.Client_to_Server, ) energy_phases_schedule_state_request: Final = ZCLCommandDef( id=0x07, schema={"power_profile_id": t.uint8_t}, direction=Direction.Client_to_Server, ) get_power_profile_price_extended_response: Final = ZCLCommandDef( id=0x08, schema={ "power_profile_id": t.uint8_t, "currency": t.uint16_t, "price": t.uint32_t, "price_trailing_digit": t.uint8_t, }, direction=Direction.Server_to_Client, ) class ClientCommandDefs(BaseCommandDefs): power_profile_notification: Final = ZCLCommandDef( id=0x00, schema={ "total_profile_num": t.uint8_t, "power_profile_id": t.uint8_t, "transfer_phases": t.LVList[PowerProfilePhase], }, direction=Direction.Client_to_Server, ) power_profile_response: Final = ZCLCommandDef( id=0x01, schema={ "total_profile_num": t.uint8_t, "power_profile_id": t.uint8_t, "transfer_phases": t.LVList[PowerProfilePhase], }, direction=Direction.Server_to_Client, ) power_profile_state_response: Final = ZCLCommandDef( id=0x02, schema={"power_profiles": t.LVList[PowerProfileType]}, direction=Direction.Server_to_Client, ) get_power_profile_price: Final = ZCLCommandDef( id=0x03, schema={"power_profile_id": t.uint8_t}, direction=Direction.Client_to_Server, ) power_profile_state_notification: Final = ZCLCommandDef( id=0x04, schema={"power_profiles": t.LVList[PowerProfileType]}, direction=Direction.Client_to_Server, ) get_overall_schedule_price: Final = ZCLCommandDef( id=0x05, schema={}, direction=Direction.Client_to_Server ) energy_phases_schedule_request: Final = ZCLCommandDef( id=0x06, schema={"power_profile_id": t.uint8_t}, direction=Direction.Client_to_Server, ) energy_phases_schedule_state_response: Final = ZCLCommandDef( id=0x07, schema={ "power_profile_id": t.uint8_t, "num_scheduled_energy_phases": t.uint8_t, }, direction=Direction.Server_to_Client, ) energy_phases_schedule_state_notification: Final = ZCLCommandDef( id=0x08, schema={ "power_profile_id": t.uint8_t, "num_scheduled_energy_phases": t.uint8_t, }, direction=Direction.Client_to_Server, ) power_profile_schedule_constraints_notification: Final = ZCLCommandDef( id=0x09, schema={ "power_profile_id": t.uint8_t, "start_after": t.uint16_t, "stop_before": t.uint16_t, }, direction=Direction.Client_to_Server, ) power_profile_schedule_constraints_response: Final = ZCLCommandDef( id=0x0A, schema={ "power_profile_id": t.uint8_t, "start_after": t.uint16_t, "stop_before": t.uint16_t, }, direction=Direction.Server_to_Client, ) get_power_profile_price_extended: Final = ZCLCommandDef( id=0x0B, schema={ "options": t.bitmap8, "power_profile_id": t.uint8_t, "power_profile_start_time?": t.uint16_t, }, direction=Direction.Client_to_Server, ) class ApplianceControl(Cluster): cluster_id: Final[t.uint16_t] = 0x001B ep_attribute: Final = "appliance_control" class AttributeDefs(BaseAttributeDefs): start_time: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) finish_time: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="rp", mandatory=True ) remaining_time: Final = ZCLAttributeDef(id=0x0002, type=t.uint16_t, access="rp") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class PollControl(Cluster): cluster_id: Final[t.uint16_t] = 0x0020 name: Final = "Poll Control" ep_attribute: Final = "poll_control" class AttributeDefs(BaseAttributeDefs): checkin_interval: Final = ZCLAttributeDef( id=0x0000, type=t.uint32_t, access="rw", mandatory=True ) long_poll_interval: Final = ZCLAttributeDef( id=0x0001, type=t.uint32_t, access="r", mandatory=True ) short_poll_interval: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) fast_poll_timeout: Final = ZCLAttributeDef( id=0x0003, type=t.uint16_t, access="rw", mandatory=True ) checkin_interval_min: Final = ZCLAttributeDef( id=0x0004, type=t.uint32_t, access="r" ) long_poll_interval_min: Final = ZCLAttributeDef( id=0x0005, type=t.uint32_t, access="r" ) fast_poll_timeout_max: Final = ZCLAttributeDef( id=0x0006, type=t.uint16_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): checkin_response: Final = ZCLCommandDef( id=0x00, schema={"start_fast_polling": t.Bool, "fast_poll_timeout": t.uint16_t}, direction=Direction.Server_to_Client, ) fast_poll_stop: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) set_long_poll_interval: Final = ZCLCommandDef( id=0x02, schema={"new_long_poll_interval": t.uint32_t}, direction=Direction.Client_to_Server, ) set_short_poll_interval: Final = ZCLCommandDef( id=0x03, schema={"new_short_poll_interval": t.uint16_t}, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): checkin: Final = ZCLCommandDef( id=0x0000, schema={}, direction=Direction.Client_to_Server ) class GreenPowerProxy(Cluster): cluster_id: Final[t.uint16_t] = 0x0021 ep_attribute: Final = "green_power" class KeepAlive(Cluster): """Keep Alive cluster definition.""" cluster_id: Final[t.uint16_t] = 0x0025 ep_attribute: Final = "keep_alive" class AttributeDefs(BaseAttributeDefs): """Keep Alive cluster attributes.""" tc_keep_alive_base: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="r", mandatory=True ) tc_keep_alive_jitter: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) zigpy-0.80.1/zigpy/zcl/clusters/general_const.py000066400000000000000000000211271501451476000217430ustar00rootroot00000000000000"""Constants Related to General Clusters""" from __future__ import annotations import zigpy.types as t class AnalogInputType(t.enum8): Temp_Degrees_C = 0x00 Relative_Humidity_Percent = 0x01 Pressure_Pascal = 0x02 Flow_Liters_Per_Sec = 0x03 Percentage = 0x04 Parts_Per_Million = 0x05 Rotational_Speed_RPM = 0x06 Current_Amps = 0x07 Frequency_Hz = 0x08 Power_Watts = 0x09 Power_Kilo_Watts = 0x0A Energy_Kilo_Watt_Hours = 0x0B Count = 0x0C Enthalpy_KJoules_Per_Kg = 0x0D Time_Seconds = 0x0E class TempDegreesC(t.enum16): Two_Pipe_Entering_Water_Temperature = 0x0000 Two_Pipe_Leaving_Water_Temperature = 0x0001 Boiler_Entering_Temperature = 0x0002 Boiler_Leaving_Temperature = 0x0003 Chiller_Chilled_Water_Entering_Temp = 0x0004 Chiller_Chilled_Water_Leaving_Temp = 0x0005 Chiller_Condenser_Water_Entering_Temp = 0x0006 Chiller_Condenser_Water_Leaving_Temp = 0x0007 Cold_Deck_Temperature = 0x0008 Cooling_Coil_Discharge_Temperature = 0x0009 Cooling_Entering_Water_Temperature = 0x000A Cooling_Leaving_Water_Temperature = 0x000B Condenser_Water_Return_Temperature = 0x000C Condenser_Water_Supply_Temperature = 0x000D Decouple_Loop_Temperature = 0x000E Building_Load = 0x000F Decouple_Loop_Temperature_2 = 0x0010 Dew_Point_Temperature = 0x0011 Discharge_Air_Temperature = 0x0012 Discharge_Temperature = 0x0013 Exhaust_Air_Temperature_After_Heat_Recovery = 0x0014 Exhaust_Air_Temperature = 0x0015 Glycol_Temperature = 0x0016 Heat_Recovery_Air_Temperature = 0x0017 Hot_Deck_Temperature = 0x0018 Heat_Exchanger_Bypass_Temp = 0x0019 Heat_Exchanger_Entering_Temp = 0x001A Heat_Exchanger_Leaving_Temp = 0x001B Mechanical_Room_Temperature = 0x001C Mixed_Air_Temperature = 0x001D Mixed_Air_Temperature_2 = 0x001E Outdoor_Air_Dewpoint_Temp = 0x001F Outdoor_Air_Temperature = 0x0020 Preheat_Air_Temperature = 0x0021 Preheat_Entering_Water_Temperature = 0x0022 Preheat_Leaving_Water_Temperature = 0x0023 Primary_Chilled_Water_Return_Temp = 0x0024 Primary_Chilled_Water_Supply_Temp = 0x0025 Primary_Hot_Water_Return_Temp = 0x0026 Primary_Hot_Water_Supply_Temp = 0x0027 Reheat_Coil_Discharge_Temperature = 0x0028 Reheat_Entering_Water_Temperature = 0x0029 Reheat_Leaving_Water_Temperature = 0x002A Return_Air_Temperature = 0x002B Secondary_Chilled_Water_Return_Temp = 0x002C Secondary_Chilled_Water_Supply_Temp = 0x002D Secondary_HW_Return_Temp = 0x002E Secondary_HW_Supply_Temp = 0x002F Sideloop_Reset_Temperature = 0x0030 Sideloop_Temperature_Setpoint = 0x0031 Sideloop_Temperature = 0x0032 Source_Temperature = 0x0033 Supply_Air_Temperature = 0x0034 Supply_Low_Limit_Temperature = 0x0035 Tower_Basin_Temp = 0x0036 Two_Pipe_Leaving_Water_Temp = 0x0037 Reserved = 0x0038 Zone_Dewpoint_Temperature = 0x0039 Zone_Sensor_Setpoint = 0x003A Zone_Sensor_Setpoint_Offset = 0x003B Zone_Temperature = 0x003C # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class RelativeHumidityPercent(t.enum16): Discharge_Humidity = 0x0000 Exhaust_Humidity = 0x0001 Hot_Deck_Humidity = 0x0002 Mixed_Air_Humidity = 0x0003 Outdoor_Air_Humidity = 0x0004 Return_Humidity = 0x0005 Sideloop_Humidity = 0x0006 Space_Humidity = 0x0007 Zone_Humidity = 0x0008 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class PressurePascal(t.enum16): Boiler_Pump_Differential_Pressure = 0x0000 Building_Static_Pressure = 0x0001 Cold_Deck_Differential_Pressure_Sensor = 0x0002 Chilled_Water_Building_Differential_Pressure = 0x0003 Cold_Deck_Differential_Pressure = 0x0004 Cold_Deck_Static_Pressure = 0x0005 Condenser_Water_Pump_Differential_Pressure = 0x0006 Discharge_Differential_Pressure = 0x0007 Discharge_Static_Pressure_1 = 0x0008 Discharge_Static_Pressure_2 = 0x0009 Exhaust_Air_Differential_Pressure = 0x000A Exhaust_Air_Static_Pressure = 0x000B Exhaust_Differential_Pressure = 0x000C Exhaust_Differential_Pressure_2 = 0x000D Hot_Deck_Differential_Pressure = 0x000E Hot_Deck_Differential_Pressure_2 = 0x000F Hot_Deck_Static_Pressure = 0x0010 Hot_Water_Bldg_Diff_Pressure = 0x0011 Heat_Exchanger_Steam_Pressure = 0x0012 Minimum_Outdoor_Air_Differential_Pressure = 0x0013 Outdoor_Air_Differential_Pressure = 0x0014 Primary_Chilled_Water_Pump_Differential_Pressure = 0x0015 Primary_Hot_Water_Pump_Differential_Pressure = 0x0016 Relief_Differential_Pressure = 0x0017 Return_Air_Static_Pressure = 0x0018 Return_Differential_Pressure = 0x0019 Secondary_Chilled_Water_Pump_Differential_Pressure = 0x001A Secondary_Hot_Water_Pump_Differential_Pressure = 0x001B Sideloop_Pressure = 0x001C Steam_Pressure = 0x001D Supply_Differential_Pressure_Sensor = 0x001E # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class FlowLitersPerSec(t.enum16): Chilled_Water_Flow = 0x0000 Chiller_Chilled_Water_Flow = 0x0001 Chiller_Condenser_Water_Flow = 0x0002 Cold_Deck_Flow = 0x0003 Decouple_Loop_Flow = 0x0004 Discharge_Flow = 0x0005 Exhaust_Fan_Flow = 0x0006 Exhaust_Flow = 0x0007 Fan_Flow = 0x0008 Hot_Deck_Flow = 0x0009 Hot_Water_Flow = 0x000A Minimum_Outdoor_Air_Fan_Flow = 0x000B Minimum_Outdoor_Air_Flow = 0x000C Outdoor_Air_Flow = 0x000D Primary_Chilled_Water_Flow = 0x000E Relief_Fan_Flow = 0x000F Relief_Flow = 0x0010 Return_Fan_Flow = 0x0011 Return_Flow = 0x0012 Secondary_Chilled_Water_Flow = 0x0013 Supply_Fan_Flow = 0x0014 Tower_Fan_Flow = 0x0015 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class Percentage(t.enum16): Chiller_Percent_Full_Load_Amperage = 0x000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class PartsPerMillion(t.enum16): Return_Carbon_Dioxide = 0x0000 Zone_Carbon_Dioxide = 0x0001 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class RotationalSpeedRPM(t.enum16): Exhaust_Fan_Remote_Speed = 0x0000 Heat_Recovery_Wheel_Remote_Speed = 0x0001 Min_Outdoor_Air_Fan_Remote_Speed = 0x0002 Relief_Fan_Remote_Speed = 0x0003 Return_Fan_Remote_Speed = 0x0004 Supply_Fan_Remote_Speed = 0x0005 Variable_Speed_Drive_Motor_Speed = 0x0006 Variable_Speed_Drive_Speed_Setpoint = 0x0007 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class CurrentAmps(t.enum16): Chiller_Amps = 0x0000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class FrequencyHz(t.enum16): Variable_Speed_Drive_Output_Frequency = 0x0000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class PowerWatts(t.enum16): Power_Consumption = 0x0000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class PowerKiloWatts(t.enum16): Absolute_Power = 0x0000 Power_Consumption = 0x0001 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class EnergyKiloWattHours(t.enum16): Variable_Speed_Drive_Kilowatt_Hours = 0x0000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class Count(t.enum16): Count = 0x0000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class EnthalpyKJoulesPerKg(t.enum16): Outdoor_Air_Enthalpy = 0x0000 Return_Air_Enthalpy = 0x0001 Space_Enthalpy = 0x0002 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF class TimeSeconds(t.enum16): Relative_Time = 0x0000 # 0x0200 through 0xFFFE are vendor defined Other = 0xFFFF ANALOG_INPUT_TYPES = { AnalogInputType.Temp_Degrees_C: TempDegreesC, AnalogInputType.Relative_Humidity_Percent: RelativeHumidityPercent, AnalogInputType.Pressure_Pascal: PressurePascal, AnalogInputType.Flow_Liters_Per_Sec: FlowLitersPerSec, AnalogInputType.Percentage: Percentage, AnalogInputType.Parts_Per_Million: PartsPerMillion, AnalogInputType.Rotational_Speed_RPM: RotationalSpeedRPM, AnalogInputType.Current_Amps: CurrentAmps, AnalogInputType.Frequency_Hz: FrequencyHz, AnalogInputType.Power_Watts: PowerWatts, AnalogInputType.Power_Kilo_Watts: PowerKiloWatts, AnalogInputType.Energy_Kilo_Watt_Hours: EnergyKiloWattHours, AnalogInputType.Count: Count, AnalogInputType.Enthalpy_KJoules_Per_Kg: EnthalpyKJoulesPerKg, AnalogInputType.Time_Seconds: TimeSeconds, } class ApplicationType(t.IntStruct, t.uint32_t): # Index = Bits 0 to 15 index: t.uint16_t # Type = Bits 16 to 23 type: AnalogInputType # Group = Bits 24 to 31 group: t.uint8_t zigpy-0.80.1/zigpy/zcl/clusters/homeautomation.py000066400000000000000000000631011501451476000221470ustar00rootroot00000000000000from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster, foundation from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, ZCLAttributeDef, ZCLCommandDef, ) class ApplianceIdentification(Cluster): cluster_id: Final[t.uint16_t] = 0x0B00 name: Final = "Appliance Identification" ep_attribute: Final = "appliance_id" class AttributeDefs(BaseAttributeDefs): basic_identification: Final = ZCLAttributeDef( id=0x0000, type=t.uint56_t, access="r", mandatory=True ) company_name: Final = ZCLAttributeDef( id=0x0010, type=t.LimitedCharString(16), access="r" ) company_id: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t, access="r") brand_name: Final = ZCLAttributeDef( id=0x0012, type=t.LimitedCharString(16), access="r" ) brand_id: Final = ZCLAttributeDef(id=0x0013, type=t.uint16_t, access="r") model: Final = ZCLAttributeDef(id=0x0014, type=t.LimitedLVBytes(16), access="r") part_number: Final = ZCLAttributeDef( id=0x0015, type=t.LimitedLVBytes(16), access="r" ) product_revision: Final = ZCLAttributeDef( id=0x0016, type=t.LimitedLVBytes(6), access="r" ) software_revision: Final = ZCLAttributeDef( id=0x0017, type=t.LimitedLVBytes(6), access="r" ) product_type_name: Final = ZCLAttributeDef( id=0x0018, type=t.LVBytesSize2, access="r" ) product_type_id: Final = ZCLAttributeDef(id=0x0019, type=t.uint16_t, access="r") ceced_specification_version: Final = ZCLAttributeDef( id=0x001A, type=t.uint8_t, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class MeterIdentification(Cluster): cluster_id: Final[t.uint16_t] = 0x0B01 name: Final = "Meter Identification" ep_attribute: Final = "meter_id" class AttributeDefs(BaseAttributeDefs): company_name: Final = ZCLAttributeDef( id=0x0000, type=t.LimitedCharString(16), access="r", mandatory=True ) meter_type_id: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) data_quality_id: Final = ZCLAttributeDef( id=0x0004, type=t.uint16_t, access="r", mandatory=True ) customer_name: Final = ZCLAttributeDef( id=0x0005, type=t.LimitedCharString(16), access="rw" ) model: Final = ZCLAttributeDef(id=0x0006, type=t.LimitedLVBytes(16), access="r") part_number: Final = ZCLAttributeDef( id=0x0007, type=t.LimitedLVBytes(16), access="r" ) product_revision: Final = ZCLAttributeDef( id=0x0008, type=t.LimitedLVBytes(6), access="r" ) software_revision: Final = ZCLAttributeDef( id=0x000A, type=t.LimitedLVBytes(6), access="r" ) utility_name: Final = ZCLAttributeDef( id=0x000B, type=t.LimitedCharString(16), access="r" ) pod: Final = ZCLAttributeDef( id=0x000C, type=t.LimitedCharString(16), access="r", mandatory=True ) available_power: Final = ZCLAttributeDef( id=0x000D, type=t.int24s, access="r", mandatory=True ) power_threshold: Final = ZCLAttributeDef( id=0x000E, type=t.int24s, access="r", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ApplianceEventAlerts(Cluster): cluster_id: Final[t.uint16_t] = 0x0B02 name: Final = "Appliance Event Alerts" ep_attribute: Final = "appliance_event" class AttributeDefs(BaseAttributeDefs): cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): get_alerts: Final = ZCLCommandDef(id=0x00, schema={}, direction=False) class ClientCommandDefs(BaseCommandDefs): get_alerts_response: Final = ZCLCommandDef(id=0x00, schema={}, direction=True) alerts_notification: Final = ZCLCommandDef(id=0x01, schema={}, direction=False) event_notification: Final = ZCLCommandDef(id=0x02, schema={}, direction=False) class ApplianceStatistics(Cluster): cluster_id: Final[t.uint16_t] = 0x0B03 name: Final = "Appliance Statistics" ep_attribute: Final = "appliance_stats" class AttributeDefs(BaseAttributeDefs): log_max_size: Final = ZCLAttributeDef( id=0x0000, type=t.uint32_t, access="r", mandatory=True ) log_queue_max_size: Final = ZCLAttributeDef( id=0x0001, type=t.uint8_t, access="r", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): log: Final = ZCLCommandDef(id=0x00, schema={}, direction=False) log_queue: Final = ZCLCommandDef(id=0x01, schema={}, direction=False) class ClientCommandDefs(BaseCommandDefs): log_notification: Final = ZCLCommandDef(id=0x00, schema={}, direction=False) log_response: Final = ZCLCommandDef(id=0x01, schema={}, direction=True) log_queue_response: Final = ZCLCommandDef(id=0x02, schema={}, direction=True) statistics_available: Final = ZCLCommandDef(id=0x03, schema={}, direction=False) class MeasurementType(t.bitmap32): Active_measurement_AC = 1 << 0 Reactive_measurement_AC = 1 << 1 Apparent_measurement_AC = 1 << 2 Phase_A_measurement = 1 << 3 Phase_B_measurement = 1 << 4 Phase_C_measurement = 1 << 5 DC_measurement = 1 << 6 Harmonics_measurement = 1 << 7 Power_quality_measurement = 1 << 8 class DCOverloadAlarmMark(t.bitmap8): Voltage_Overload = 1 << 0 Current_Overload = 1 << 1 class ACAlarmsMask(t.bitmap16): Voltage_Overload = 1 << 0 Current_Overload = 1 << 1 Active_Power_Overload = 1 << 2 Reactive_Power_Overload = 1 << 3 Average_RMS_Over_Voltage = 1 << 4 Average_RMS_Under_Voltage = 1 << 5 RMS_Extreme_Over_Voltage = 1 << 6 RMS_Extreme_Under_Voltage = 1 << 7 RMS_Voltage_Sag = 1 << 8 RMS_Voltage_Swell = 1 << 9 class ElectricalMeasurement(Cluster): cluster_id: Final[t.uint16_t] = 0x0B04 name: Final = "Electrical Measurement" ep_attribute: Final = "electrical_measurement" MeasurementType: Final = MeasurementType DCOverloadAlarmMark: Final = DCOverloadAlarmMark ACAlarmsMask: Final = ACAlarmsMask class AttributeDefs(BaseAttributeDefs): # Basic Information measurement_type: Final = ZCLAttributeDef( id=0x0000, type=MeasurementType, access="r", mandatory=True ) # DC Measurement dc_voltage: Final = ZCLAttributeDef(id=0x0100, type=t.int16s, access="rp") dc_voltage_min: Final = ZCLAttributeDef(id=0x0101, type=t.int16s, access="r") dc_voltage_max: Final = ZCLAttributeDef(id=0x0102, type=t.int16s, access="r") dc_current: Final = ZCLAttributeDef(id=0x0103, type=t.int16s, access="rp") dc_current_min: Final = ZCLAttributeDef(id=0x0104, type=t.int16s, access="r") dc_current_max: Final = ZCLAttributeDef(id=0x0105, type=t.int16s, access="r") dc_power: Final = ZCLAttributeDef(id=0x0106, type=t.int16s, access="rp") dc_power_min: Final = ZCLAttributeDef(id=0x0107, type=t.int16s, access="r") dc_power_max: Final = ZCLAttributeDef(id=0x0108, type=t.int16s, access="r") # DC Formatting dc_voltage_multiplier: Final = ZCLAttributeDef( id=0x0200, type=t.uint16_t, access="rp" ) dc_voltage_divisor: Final = ZCLAttributeDef( id=0x0201, type=t.uint16_t, access="rp" ) dc_current_multiplier: Final = ZCLAttributeDef( id=0x0202, type=t.uint16_t, access="rp" ) dc_current_divisor: Final = ZCLAttributeDef( id=0x0203, type=t.uint16_t, access="rp" ) dc_power_multiplier: Final = ZCLAttributeDef( id=0x0204, type=t.uint16_t, access="rp" ) dc_power_divisor: Final = ZCLAttributeDef( id=0x0205, type=t.uint16_t, access="rp" ) # AC (Non-phase Specific) Measurements ac_frequency: Final = ZCLAttributeDef(id=0x0300, type=t.uint16_t, access="rp") ac_frequency_min: Final = ZCLAttributeDef( id=0x0301, type=t.uint16_t, access="r" ) ac_frequency_max: Final = ZCLAttributeDef( id=0x0302, type=t.uint16_t, access="r" ) neutral_current: Final = ZCLAttributeDef( id=0x0303, type=t.uint16_t, access="rp" ) total_active_power: Final = ZCLAttributeDef( id=0x0304, type=t.int32s, access="rp" ) total_reactive_power: Final = ZCLAttributeDef( id=0x0305, type=t.int32s, access="rp" ) total_apparent_power: Final = ZCLAttributeDef( id=0x0306, type=t.uint32_t, access="rp" ) meas1st_harmonic_current: Final = ZCLAttributeDef( id=0x0307, type=t.int16s, access="rp" ) meas3rd_harmonic_current: Final = ZCLAttributeDef( id=0x0308, type=t.int16s, access="rp" ) meas5th_harmonic_current: Final = ZCLAttributeDef( id=0x0309, type=t.int16s, access="rp" ) meas7th_harmonic_current: Final = ZCLAttributeDef( id=0x030A, type=t.int16s, access="rp" ) meas9th_harmonic_current: Final = ZCLAttributeDef( id=0x030B, type=t.int16s, access="rp" ) meas11th_harmonic_current: Final = ZCLAttributeDef( id=0x030C, type=t.int16s, access="rp" ) meas_phase1st_harmonic_current: Final = ZCLAttributeDef( id=0x030D, type=t.int16s, access="rp" ) meas_phase3rd_harmonic_current: Final = ZCLAttributeDef( id=0x030E, type=t.int16s, access="rp" ) meas_phase5th_harmonic_current: Final = ZCLAttributeDef( id=0x030F, type=t.int16s, access="rp" ) meas_phase7th_harmonic_current: Final = ZCLAttributeDef( id=0x0310, type=t.int16s, access="rp" ) meas_phase9th_harmonic_current: Final = ZCLAttributeDef( id=0x0311, type=t.int16s, access="rp" ) meas_phase11th_harmonic_current: Final = ZCLAttributeDef( id=0x0312, type=t.int16s, access="rp" ) # AC (Non-phase specific) Formatting ac_frequency_multiplier: Final = ZCLAttributeDef( id=0x0400, type=t.uint16_t, access="rp" ) ac_frequency_divisor: Final = ZCLAttributeDef( id=0x0401, type=t.uint16_t, access="rp" ) power_multiplier: Final = ZCLAttributeDef( id=0x0402, type=t.uint32_t, access="rp" ) power_divisor: Final = ZCLAttributeDef(id=0x0403, type=t.uint32_t, access="rp") harmonic_current_multiplier: Final = ZCLAttributeDef( id=0x0404, type=t.int8s, access="rp" ) phase_harmonic_current_multiplier: Final = ZCLAttributeDef( id=0x0405, type=t.int8s, access="rp" ) # AC (Single Phase or Phase A) Measurements instantaneous_voltage: Final = ZCLAttributeDef( id=0x0500, type=t.int16s, access="rp" ) instantaneous_line_current: Final = ZCLAttributeDef( id=0x0501, type=t.uint16_t, access="rp" ) instantaneous_active_current: Final = ZCLAttributeDef( id=0x0502, type=t.int16s, access="rp" ) instantaneous_reactive_current: Final = ZCLAttributeDef( id=0x0503, type=t.int16s, access="rp" ) instantaneous_power: Final = ZCLAttributeDef( id=0x0504, type=t.int16s, access="rp" ) rms_voltage: Final = ZCLAttributeDef(id=0x0505, type=t.uint16_t, access="rp") rms_voltage_min: Final = ZCLAttributeDef(id=0x0506, type=t.uint16_t, access="r") rms_voltage_max: Final = ZCLAttributeDef(id=0x0507, type=t.uint16_t, access="r") rms_current: Final = ZCLAttributeDef(id=0x0508, type=t.uint16_t, access="rp") rms_current_min: Final = ZCLAttributeDef(id=0x0509, type=t.uint16_t, access="r") rms_current_max: Final = ZCLAttributeDef(id=0x050A, type=t.uint16_t, access="r") active_power: Final = ZCLAttributeDef(id=0x050B, type=t.int16s, access="rp") active_power_min: Final = ZCLAttributeDef(id=0x050C, type=t.int16s, access="r") active_power_max: Final = ZCLAttributeDef(id=0x050D, type=t.int16s, access="r") reactive_power: Final = ZCLAttributeDef(id=0x050E, type=t.int16s, access="rp") apparent_power: Final = ZCLAttributeDef(id=0x050F, type=t.uint16_t, access="rp") power_factor: Final = ZCLAttributeDef(id=0x0510, type=t.int8s, access="r") average_rms_voltage_meas_period: Final = ZCLAttributeDef( id=0x0511, type=t.uint16_t, access="rw" ) average_rms_over_voltage_counter: Final = ZCLAttributeDef( id=0x0512, type=t.uint16_t, access="rw" ) average_rms_under_voltage_counter: Final = ZCLAttributeDef( id=0x0513, type=t.uint16_t, access="rw" ) rms_extreme_over_voltage_period: Final = ZCLAttributeDef( id=0x0514, type=t.uint16_t, access="rw" ) rms_extreme_under_voltage_period: Final = ZCLAttributeDef( id=0x0515, type=t.uint16_t, access="rw" ) rms_voltage_sag_period: Final = ZCLAttributeDef( id=0x0516, type=t.uint16_t, access="rw" ) rms_voltage_swell_period: Final = ZCLAttributeDef( id=0x0517, type=t.uint16_t, access="rw" ) # AC Formatting ac_voltage_multiplier: Final = ZCLAttributeDef( id=0x0600, type=t.uint16_t, access="rp" ) ac_voltage_divisor: Final = ZCLAttributeDef( id=0x0601, type=t.uint16_t, access="rp" ) ac_current_multiplier: Final = ZCLAttributeDef( id=0x0602, type=t.uint16_t, access="rp" ) ac_current_divisor: Final = ZCLAttributeDef( id=0x0603, type=t.uint16_t, access="rp" ) ac_power_multiplier: Final = ZCLAttributeDef( id=0x0604, type=t.uint16_t, access="rp" ) ac_power_divisor: Final = ZCLAttributeDef( id=0x0605, type=t.uint16_t, access="rp" ) # DC Manufacturer Threshold Alarms dc_overload_alarms_mask: Final = ZCLAttributeDef( id=0x0700, type=DCOverloadAlarmMark, access="rp" ) dc_voltage_overload: Final = ZCLAttributeDef( id=0x0701, type=t.int16s, access="rp" ) dc_current_overload: Final = ZCLAttributeDef( id=0x0702, type=t.int16s, access="rp" ) # AC Manufacturer Threshold Alarms ac_alarms_mask: Final = ZCLAttributeDef( id=0x0800, type=ACAlarmsMask, access="rw" ) ac_voltage_overload: Final = ZCLAttributeDef( id=0x0801, type=t.int16s, access="r" ) ac_current_overload: Final = ZCLAttributeDef( id=0x0802, type=t.int16s, access="r" ) ac_active_power_overload: Final = ZCLAttributeDef( id=0x0803, type=t.int16s, access="r" ) ac_reactive_power_overload: Final = ZCLAttributeDef( id=0x0804, type=t.int16s, access="r" ) average_rms_over_voltage: Final = ZCLAttributeDef( id=0x0805, type=t.int16s, access="r" ) average_rms_under_voltage: Final = ZCLAttributeDef( id=0x0806, type=t.int16s, access="r" ) rms_extreme_over_voltage: Final = ZCLAttributeDef( id=0x0807, type=t.int16s, access="rw" ) rms_extreme_under_voltage: Final = ZCLAttributeDef( id=0x0808, type=t.int16s, access="rw" ) rms_voltage_sag: Final = ZCLAttributeDef(id=0x0809, type=t.int16s, access="rw") rms_voltage_swell: Final = ZCLAttributeDef( id=0x080A, type=t.int16s, access="rw" ) # AC Phase B Measurements line_current_ph_b: Final = ZCLAttributeDef( id=0x0901, type=t.uint16_t, access="rp" ) active_current_ph_b: Final = ZCLAttributeDef( id=0x0902, type=t.int16s, access="rp" ) reactive_current_ph_b: Final = ZCLAttributeDef( id=0x0903, type=t.int16s, access="rp" ) rms_voltage_ph_b: Final = ZCLAttributeDef( id=0x0905, type=t.uint16_t, access="rp" ) rms_voltage_min_ph_b: Final = ZCLAttributeDef( id=0x0906, type=t.uint16_t, access="r" ) rms_voltage_max_ph_b: Final = ZCLAttributeDef( id=0x0907, type=t.uint16_t, access="r" ) rms_current_ph_b: Final = ZCLAttributeDef( id=0x0908, type=t.uint16_t, access="rp" ) rms_current_min_ph_b: Final = ZCLAttributeDef( id=0x0909, type=t.uint16_t, access="r" ) rms_current_max_ph_b: Final = ZCLAttributeDef( id=0x090A, type=t.uint16_t, access="r" ) active_power_ph_b: Final = ZCLAttributeDef( id=0x090B, type=t.int16s, access="rp" ) active_power_min_ph_b: Final = ZCLAttributeDef( id=0x090C, type=t.int16s, access="r" ) active_power_max_ph_b: Final = ZCLAttributeDef( id=0x090D, type=t.int16s, access="r" ) reactive_power_ph_b: Final = ZCLAttributeDef( id=0x090E, type=t.int16s, access="rp" ) apparent_power_ph_b: Final = ZCLAttributeDef( id=0x090F, type=t.uint16_t, access="rp" ) power_factor_ph_b: Final = ZCLAttributeDef(id=0x0910, type=t.int8s, access="r") average_rms_voltage_measure_period_ph_b: Final = ZCLAttributeDef( id=0x0911, type=t.uint16_t, access="rw" ) average_rms_over_voltage_counter_ph_b: Final = ZCLAttributeDef( id=0x0912, type=t.uint16_t, access="rw" ) average_under_voltage_counter_ph_b: Final = ZCLAttributeDef( id=0x0913, type=t.uint16_t, access="rw" ) rms_extreme_over_voltage_period_ph_b: Final = ZCLAttributeDef( id=0x0914, type=t.uint16_t, access="rw" ) rms_extreme_under_voltage_period_ph_b: Final = ZCLAttributeDef( id=0x0915, type=t.uint16_t, access="rw" ) rms_voltage_sag_period_ph_b: Final = ZCLAttributeDef( id=0x0916, type=t.uint16_t, access="rw" ) rms_voltage_swell_period_ph_b: Final = ZCLAttributeDef( id=0x0917, type=t.uint16_t, access="rw" ) # AC Phase C Measurements line_current_ph_c: Final = ZCLAttributeDef( id=0x0A01, type=t.uint16_t, access="rp" ) active_current_ph_c: Final = ZCLAttributeDef( id=0x0A02, type=t.int16s, access="rp" ) reactive_current_ph_c: Final = ZCLAttributeDef( id=0x0A03, type=t.int16s, access="rp" ) rms_voltage_ph_c: Final = ZCLAttributeDef( id=0x0A05, type=t.uint16_t, access="rp" ) rms_voltage_min_ph_c: Final = ZCLAttributeDef( id=0x0A06, type=t.uint16_t, access="r" ) rms_voltage_max_ph_c: Final = ZCLAttributeDef( id=0x0A07, type=t.uint16_t, access="r" ) rms_current_ph_c: Final = ZCLAttributeDef( id=0x0A08, type=t.uint16_t, access="rp" ) rms_current_min_ph_c: Final = ZCLAttributeDef( id=0x0A09, type=t.uint16_t, access="r" ) rms_current_max_ph_c: Final = ZCLAttributeDef( id=0x0A0A, type=t.uint16_t, access="r" ) active_power_ph_c: Final = ZCLAttributeDef( id=0x0A0B, type=t.int16s, access="rp" ) active_power_min_ph_c: Final = ZCLAttributeDef( id=0x0A0C, type=t.int16s, access="r" ) active_power_max_ph_c: Final = ZCLAttributeDef( id=0x0A0D, type=t.int16s, access="r" ) reactive_power_ph_c: Final = ZCLAttributeDef( id=0x0A0E, type=t.int16s, access="rp" ) apparent_power_ph_c: Final = ZCLAttributeDef( id=0x0A0F, type=t.uint16_t, access="rp" ) power_factor_ph_c: Final = ZCLAttributeDef(id=0x0A10, type=t.int8s, access="r") average_rms_voltage_meas_period_ph_c: Final = ZCLAttributeDef( id=0x0A11, type=t.uint16_t, access="rw" ) average_rms_over_voltage_counter_ph_c: Final = ZCLAttributeDef( id=0x0A12, type=t.uint16_t, access="rw" ) average_under_voltage_counter_ph_c: Final = ZCLAttributeDef( id=0x0A13, type=t.uint16_t, access="rw" ) rms_extreme_over_voltage_period_ph_c: Final = ZCLAttributeDef( id=0x0A14, type=t.uint16_t, access="rw" ) rms_extreme_under_voltage_period_ph_c: Final = ZCLAttributeDef( id=0x0A15, type=t.uint16_t, access="rw" ) rms_voltage_sag_period_ph_c: Final = ZCLAttributeDef( id=0x0A16, type=t.uint16_t, access="rw" ) rms_voltage_swell_period_ph_c: Final = ZCLAttributeDef( id=0x0A17, type=t.uint16_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): get_profile_info: Final = ZCLCommandDef(id=0x00, schema={}, direction=False) get_measurement_profile: Final = ZCLCommandDef( id=0x01, schema={}, direction=False ) class ClientCommandDefs(BaseCommandDefs): get_profile_info_response: Final = ZCLCommandDef( id=0x00, schema={}, direction=True ) get_measurement_profile_response: Final = ZCLCommandDef( id=0x01, schema={}, direction=True ) class Diagnostic(Cluster): cluster_id: Final[t.uint16_t] = 0x0B05 ep_attribute: Final = "diagnostic" class AttributeDefs(BaseAttributeDefs): # Hardware Information number_of_resets: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="r" ) persistent_memory_writes: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r" ) # Stack/Network Information mac_rx_bcast: Final = ZCLAttributeDef(id=0x0100, type=t.uint32_t, access="r") mac_tx_bcast: Final = ZCLAttributeDef(id=0x0101, type=t.uint32_t, access="r") mac_rx_ucast: Final = ZCLAttributeDef(id=0x0102, type=t.uint32_t, access="r") mac_tx_ucast: Final = ZCLAttributeDef(id=0x0103, type=t.uint32_t, access="r") mac_tx_ucast_retry: Final = ZCLAttributeDef( id=0x0104, type=t.uint16_t, access="r" ) mac_tx_ucast_fail: Final = ZCLAttributeDef( id=0x0105, type=t.uint16_t, access="r" ) aps_rx_bcast: Final = ZCLAttributeDef(id=0x0106, type=t.uint16_t, access="r") aps_tx_bcast: Final = ZCLAttributeDef(id=0x0107, type=t.uint16_t, access="r") aps_rx_ucast: Final = ZCLAttributeDef(id=0x0108, type=t.uint16_t, access="r") aps_tx_ucast_success: Final = ZCLAttributeDef( id=0x0109, type=t.uint16_t, access="r" ) aps_tx_ucast_retry: Final = ZCLAttributeDef( id=0x010A, type=t.uint16_t, access="r" ) aps_tx_ucast_fail: Final = ZCLAttributeDef( id=0x010B, type=t.uint16_t, access="r" ) route_disc_initiated: Final = ZCLAttributeDef( id=0x010C, type=t.uint16_t, access="r" ) neighbor_added: Final = ZCLAttributeDef(id=0x010D, type=t.uint16_t, access="r") neighbor_removed: Final = ZCLAttributeDef( id=0x010E, type=t.uint16_t, access="r" ) neighbor_stale: Final = ZCLAttributeDef(id=0x010F, type=t.uint16_t, access="r") join_indication: Final = ZCLAttributeDef(id=0x0110, type=t.uint16_t, access="r") child_moved: Final = ZCLAttributeDef(id=0x0111, type=t.uint16_t, access="r") nwk_fc_failure: Final = ZCLAttributeDef(id=0x0112, type=t.uint16_t, access="r") aps_fc_failure: Final = ZCLAttributeDef(id=0x0113, type=t.uint16_t, access="r") aps_unauthorized_key: Final = ZCLAttributeDef( id=0x0114, type=t.uint16_t, access="r" ) nwk_decrypt_failures: Final = ZCLAttributeDef( id=0x0115, type=t.uint16_t, access="r" ) aps_decrypt_failures: Final = ZCLAttributeDef( id=0x0116, type=t.uint16_t, access="r" ) packet_buffer_allocate_failures: Final = ZCLAttributeDef( id=0x0117, type=t.uint16_t, access="r" ) relayed_ucast: Final = ZCLAttributeDef(id=0x0118, type=t.uint16_t, access="r") phy_to_mac_queue_limit_reached: Final = ZCLAttributeDef( id=0x0119, type=t.uint16_t, access="r" ) packet_validate_drop_count: Final = ZCLAttributeDef( id=0x011A, type=t.uint16_t, access="r" ) average_mac_retry_per_aps_message_sent: Final = ZCLAttributeDef( id=0x011B, type=t.uint16_t, access="r" ) last_message_lqi: Final = ZCLAttributeDef(id=0x011C, type=t.uint8_t, access="r") last_message_rssi: Final = ZCLAttributeDef(id=0x011D, type=t.int8s, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR zigpy-0.80.1/zigpy/zcl/clusters/hvac.py000066400000000000000000000516521501451476000200470ustar00rootroot00000000000000"""HVAC Functional Domain""" from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, Direction, ZCLAttributeDef, ZCLCommandDef, ) class PumpAlarmMask(t.bitmap16): Supply_voltage_too_low = 0x0001 Supply_voltage_too_high = 0x0002 Power_missing_phase = 0x0004 System_pressure_too_low = 0x0008 System_pressure_too_high = 0x0010 Dry_running = 0x0020 Motor_temperature_too_high = 0x0040 Pump_motor_has_fatal_failure = 0x0080 Electronic_temperature_too_high = 0x0100 Pump_blocked = 0x0200 Sensor_failure = 0x0400 Electronic_non_fatal_failure = 0x0800 Electronic_fatal_failure = 0x1000 General_fault = 0x2000 class ControlMode(t.enum8): Constant_speed = 0x00 Constant_pressure = 0x01 Proportional_pressure = 0x02 Constant_flow = 0x03 Constant_temperature = 0x05 Automatic = 0x07 class OperationMode(t.enum8): Normal = 0x00 Minimum = 0x01 Maximum = 0x02 Local = 0x03 class PumpStatus(t.bitmap16): Device_fault = 0x0001 Supply_fault = 0x0002 Speed_low = 0x0004 Speed_high = 0x0008 Local_override = 0x0010 Running = 0x0020 Remote_Pressure = 0x0040 Remote_Flow = 0x0080 Remote_Temperature = 0x0100 class Pump(Cluster): """An interface for configuring and controlling pumps.""" AlarmMask: Final = PumpAlarmMask ControlMode: Final = ControlMode OperationMode: Final = OperationMode PumpStatus: Final = PumpStatus cluster_id: Final[t.uint16_t] = 0x0200 name: Final = "Pump Configuration and Control" ep_attribute: Final = "pump" class AttributeDefs(BaseAttributeDefs): # Pump Information max_pressure: Final = ZCLAttributeDef( id=0x0000, type=t.int16s, access="r", mandatory=True ) max_speed: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_flow: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) min_const_pressure: Final = ZCLAttributeDef( id=0x0003, type=t.int16s, access="r" ) max_const_pressure: Final = ZCLAttributeDef( id=0x0004, type=t.int16s, access="r" ) min_comp_pressure: Final = ZCLAttributeDef(id=0x0005, type=t.int16s, access="r") max_comp_pressure: Final = ZCLAttributeDef(id=0x0006, type=t.int16s, access="r") min_const_speed: Final = ZCLAttributeDef(id=0x0007, type=t.uint16_t, access="r") max_const_speed: Final = ZCLAttributeDef(id=0x0008, type=t.uint16_t, access="r") min_const_flow: Final = ZCLAttributeDef(id=0x0009, type=t.uint16_t, access="r") max_const_flow: Final = ZCLAttributeDef(id=0x000A, type=t.uint16_t, access="r") min_const_temp: Final = ZCLAttributeDef(id=0x000B, type=t.int16s, access="r") max_const_temp: Final = ZCLAttributeDef(id=0x000C, type=t.int16s, access="r") # Pump Dynamic Information pump_status: Final = ZCLAttributeDef(id=0x0010, type=PumpStatus, access="rp") effective_operation_mode: Final = ZCLAttributeDef( id=0x0011, type=OperationMode, access="r", mandatory=True ) effective_control_mode: Final = ZCLAttributeDef( id=0x0012, type=ControlMode, access="r", mandatory=True ) capacity: Final = ZCLAttributeDef( id=0x0013, type=t.int16s, access="rp", mandatory=True ) speed: Final = ZCLAttributeDef(id=0x0014, type=t.uint16_t, access="r") lifetime_running_hours: Final = ZCLAttributeDef( id=0x0015, type=t.uint24_t, access="rw" ) power: Final = ZCLAttributeDef(id=0x0016, type=t.uint24_t, access="rw") lifetime_energy_consumed: Final = ZCLAttributeDef( id=0x0017, type=t.uint32_t, access="r" ) # Pump Settings operation_mode: Final = ZCLAttributeDef( id=0x0020, type=OperationMode, access="rw", mandatory=True ) control_mode: Final = ZCLAttributeDef(id=0x0021, type=ControlMode, access="rw") alarm_mask: Final = ZCLAttributeDef(id=0x0022, type=PumpAlarmMask, access="r") class CoolingSystemStage(t.enum8): Cool_Stage_1 = 0x00 Cool_Stage_2 = 0x01 Cool_Stage_3 = 0x02 Reserved = 0x03 class HeatingSystemStage(t.enum8): Heat_Stage_1 = 0x00 Heat_Stage_2 = 0x01 Heat_Stage_3 = 0x02 Reserved = 0x03 class HeatingSystemType(t.enum8): Conventional = 0x00 Heat_Pump = 0x01 class HeatingFuelSource(t.enum8): Electric = 0x00 Gas = 0x01 class ACCapacityFormat(t.enum8): BTUh = 0x00 class ACCompressorType(t.enum8): Reserved = 0x00 T1_max_working_43C = 0x01 T2_max_working_35C = 0x02 T3_max_working_52C = 0x03 class ACType(t.enum8): Reserved = 0x00 Cooling_fixed_speed = 0x01 Heat_Pump_fixed_speed = 0x02 Cooling_Inverter = 0x03 Heat_Pump_Inverter = 0x04 class ACRefrigerantType(t.enum8): Reserved = 0x00 R22 = 0x01 R410a = 0x02 R407c = 0x03 class ACErrorCode(t.bitmap32): No_Errors = 0x00000000 Commpressor_Failure = 0x00000001 Room_Temperature_Sensor_Failure = 0x00000002 Outdoor_Temperature_Sensor_Failure = 0x00000004 Indoor_Coil_Temperature_Sensor_Failure = 0x00000008 Fan_Failure = 0x00000010 class ACLouverPosition(t.enum8): Closed = 0x01 Open = 0x02 Qurter_Open = 0x03 Half_Open = 0x04 Three_Quarters_Open = 0x05 class AlarmMask(t.bitmap8): No_Alarms = 0x00 Initialization_failure = 0x01 Hardware_failure = 0x02 Self_calibration_failure = 0x04 class ControlSequenceOfOperation(t.enum8): Cooling_Only = 0x00 Cooling_With_Reheat = 0x01 Heating_Only = 0x02 Heating_With_Reheat = 0x03 Cooling_and_Heating = 0x04 Cooling_and_Heating_with_Reheat = 0x05 class SeqDayOfWeek(t.bitmap8): Sunday = 0x01 Monday = 0x02 Tuesday = 0x04 Wednesday = 0x08 Thursday = 0x10 Friday = 0x20 Saturday = 0x40 Away = 0x80 class SeqMode(t.bitmap8): Heat = 0x01 Cool = 0x02 class Occupancy(t.bitmap8): Unoccupied = 0x00 Occupied = 0x01 class ProgrammingOperationMode(t.bitmap8): Simple = 0x00 Schedule_programming_mode = 0x01 Auto_recovery_mode = 0x02 Economy_mode = 0x04 class RemoteSensing(t.bitmap8): all_local = 0x00 local_temperature_sensed_remotely = 0x01 outdoor_temperature_sensed_remotely = 0x02 occupancy_sensed_remotely = 0x04 class SetpointChangeSource(t.enum8): Manual = 0x00 Schedule = 0x01 External = 0x02 class SetpointMode(t.enum8): Heat = 0x00 Cool = 0x01 Both = 0x02 class StartOfWeek(t.enum8): Sunday = 0x00 Monday = 0x01 Tuesday = 0x02 Wednesday = 0x03 Thursday = 0x04 Friday = 0x05 Saturday = 0x06 class SystemMode(t.enum8): Off = 0x00 Auto = 0x01 Cool = 0x03 Heat = 0x04 Emergency_Heating = 0x05 Pre_cooling = 0x06 Fan_only = 0x07 Dry = 0x08 Sleep = 0x09 class SystemType(t.bitmap8): Heat_and_or_Cool_Stage_1 = 0x00 Cool_Stage_1 = 0x01 Cool_Stage_2 = 0x02 Heat_Stage_1 = 0x04 Heat_Stage_2 = 0x08 Heat_Pump = 0x10 Gas = 0x20 @property def cooling_system_stage(self) -> CoolingSystemStage: return CoolingSystemStage(self & 0x03) @property def heating_system_stage(self) -> HeatingSystemStage: return HeatingSystemStage((self >> 2) & 0x03) @property def heating_system_type(self) -> HeatingSystemType: return HeatingSystemType((self >> 4) & 0x01) @property def heating_fuel_source(self) -> HeatingFuelSource: return HeatingFuelSource((self >> 5) & 0x01) class TemperatureSetpointHold(t.enum8): Setpoint_Hold_Off = 0x00 Setpoint_Hold_On = 0x01 class RunningMode(t.enum8): Off = 0x00 Cool = 0x03 Heat = 0x04 class RunningState(t.bitmap16): Idle = 0x0000 Heat_State_On = 0x0001 Cool_State_On = 0x0002 Fan_State_On = 0x0004 Heat_2nd_Stage_On = 0x0008 Cool_2nd_Stage_On = 0x0010 Fan_2nd_Stage_On = 0x0020 Fan_3rd_Stage_On = 0x0040 class Thermostat(Cluster): """An interface for configuring and controlling the functionality of a thermostat. """ ACCapacityFormat: Final = ACCapacityFormat ACErrorCode: Final = ACErrorCode ACLouverPosition: Final = ACLouverPosition AlarmMask: Final = AlarmMask ControlSequenceOfOperation: Final = ControlSequenceOfOperation SeqDayOfWeek: Final = SeqDayOfWeek SeqMode: Final = SeqMode Occupancy: Final = Occupancy ProgrammingOperationMode: Final = ProgrammingOperationMode RemoteSensing: Final = RemoteSensing SetpointChangeSource: Final = SetpointChangeSource SetpointMode: Final = SetpointMode StartOfWeek: Final = StartOfWeek SystemMode: Final = SystemMode SystemType: Final = SystemType TemperatureSetpointHold: Final = TemperatureSetpointHold RunningMode: Final = RunningMode RunningState: Final = RunningState cluster_id: Final[t.uint16_t] = 0x0201 ep_attribute: Final = "thermostat" class AttributeDefs(BaseAttributeDefs): # Thermostat Information local_temperature: Final = ZCLAttributeDef( id=0x0000, type=t.int16s, access="rp", mandatory=True ) outdoor_temperature: Final = ZCLAttributeDef( id=0x0001, type=t.int16s, access="r" ) occupancy: Final = ZCLAttributeDef(id=0x0002, type=Occupancy, access="r") abs_min_heat_setpoint_limit: Final = ZCLAttributeDef( id=0x0003, type=t.int16s, access="r" ) abs_max_heat_setpoint_limit: Final = ZCLAttributeDef( id=0x0004, type=t.int16s, access="r" ) abs_min_cool_setpoint_limit: Final = ZCLAttributeDef( id=0x0005, type=t.int16s, access="r" ) abs_max_cool_setpoint_limit: Final = ZCLAttributeDef( id=0x0006, type=t.int16s, access="r" ) pi_cooling_demand: Final = ZCLAttributeDef( id=0x0007, type=t.uint8_t, access="rp" ) pi_heating_demand: Final = ZCLAttributeDef( id=0x0008, type=t.uint8_t, access="rp" ) system_type_config: Final = ZCLAttributeDef( id=0x0009, type=SystemType, access="r*w" ) # Thermostat Settings local_temperature_calibration: Final = ZCLAttributeDef( id=0x0010, type=t.int8s, access="rw" ) # At least one of these two attribute sets will be available occupied_cooling_setpoint: Final = ZCLAttributeDef( id=0x0011, type=t.int16s, access="rws" ) occupied_heating_setpoint: Final = ZCLAttributeDef( id=0x0012, type=t.int16s, access="rws" ) unoccupied_cooling_setpoint: Final = ZCLAttributeDef( id=0x0013, type=t.int16s, access="rw" ) unoccupied_heating_setpoint: Final = ZCLAttributeDef( id=0x0014, type=t.int16s, access="rw" ) min_heat_setpoint_limit: Final = ZCLAttributeDef( id=0x0015, type=t.int16s, access="rw" ) max_heat_setpoint_limit: Final = ZCLAttributeDef( id=0x0016, type=t.int16s, access="rw" ) min_cool_setpoint_limit: Final = ZCLAttributeDef( id=0x0017, type=t.int16s, access="rw" ) max_cool_setpoint_limit: Final = ZCLAttributeDef( id=0x0018, type=t.int16s, access="rw" ) min_setpoint_dead_band: Final = ZCLAttributeDef( id=0x0019, type=t.int8s, access="r*w" ) remote_sensing: Final = ZCLAttributeDef( id=0x001A, type=RemoteSensing, access="rw" ) ctrl_sequence_of_oper: Final = ZCLAttributeDef( id=0x001B, type=ControlSequenceOfOperation, access="rw", mandatory=True, ) system_mode: Final = ZCLAttributeDef( id=0x001C, type=SystemMode, access="rws", mandatory=True ) alarm_mask: Final = ZCLAttributeDef(id=0x001D, type=AlarmMask, access="r") running_mode: Final = ZCLAttributeDef(id=0x001E, type=RunningMode, access="r") # Schedule start_of_week: Final = ZCLAttributeDef(id=0x0020, type=StartOfWeek, access="r") number_of_weekly_transitions: Final = ZCLAttributeDef( id=0x0021, type=t.uint8_t, access="r" ) number_of_daily_transitions: Final = ZCLAttributeDef( id=0x0022, type=t.uint8_t, access="r" ) temp_setpoint_hold: Final = ZCLAttributeDef( id=0x0023, type=TemperatureSetpointHold, access="rw" ) temp_setpoint_hold_duration: Final = ZCLAttributeDef( id=0x0024, type=t.uint16_t, access="rw" ) programing_oper_mode: Final = ZCLAttributeDef( id=0x0025, type=ProgrammingOperationMode, access="rwp" ) # HVAC Relay running_state: Final = ZCLAttributeDef(id=0x0029, type=RunningState, access="r") # Thermostat Setpoint Change Tracking setpoint_change_source: Final = ZCLAttributeDef( id=0x0030, type=SetpointChangeSource, access="r" ) setpoint_change_amount: Final = ZCLAttributeDef( id=0x0031, type=t.int16s, access="r" ) setpoint_change_source_timestamp: Final = ZCLAttributeDef( id=0x0032, type=t.UTCTime, access="r" ) occupied_setback: Final = ZCLAttributeDef( id=0x0034, type=t.uint8_t, access="rw" ) occupied_setback_min: Final = ZCLAttributeDef( id=0x0035, type=t.uint8_t, access="r" ) occupied_setback_max: Final = ZCLAttributeDef( id=0x0036, type=t.uint8_t, access="r" ) unoccupied_setback: Final = ZCLAttributeDef( id=0x0037, type=t.uint8_t, access="rw" ) unoccupied_setback_min: Final = ZCLAttributeDef( id=0x0038, type=t.uint8_t, access="r" ) unoccupied_setback_max: Final = ZCLAttributeDef( id=0x0039, type=t.uint8_t, access="r" ) emergency_heat_delta: Final = ZCLAttributeDef( id=0x003A, type=t.uint8_t, access="rw" ) # AC Information ac_type: Final = ZCLAttributeDef(id=0x0040, type=ACType, access="rw") ac_capacity: Final = ZCLAttributeDef(id=0x0041, type=t.uint16_t, access="rw") ac_refrigerant_type: Final = ZCLAttributeDef( id=0x0042, type=ACRefrigerantType, access="rw" ) ac_compressor_type: Final = ZCLAttributeDef( id=0x0043, type=ACCompressorType, access="rw" ) ac_error_code: Final = ZCLAttributeDef(id=0x0044, type=ACErrorCode, access="rw") ac_louver_position: Final = ZCLAttributeDef( id=0x0045, type=ACLouverPosition, access="rw" ) ac_coil_temperature: Final = ZCLAttributeDef( id=0x0046, type=t.int16s, access="r" ) ac_capacity_format: Final = ZCLAttributeDef( id=0x0047, type=ACCapacityFormat, access="rw" ) class ServerCommandDefs(BaseCommandDefs): setpoint_raise_lower: Final = ZCLCommandDef( id=0x00, schema={"mode": SetpointMode, "amount": t.int8s}, direction=Direction.Client_to_Server, ) set_weekly_schedule: Final = ZCLCommandDef( id=0x01, schema={ "num_transitions_for_sequence": t.enum8, "day_of_week_for_sequence": SeqDayOfWeek, "mode_for_sequence": SeqMode, "values": t.List[t.int16s], }, direction=Direction.Client_to_Server, ) get_weekly_schedule: Final = ZCLCommandDef( id=0x02, schema={"days_to_return": SeqDayOfWeek, "mode_to_return": SeqMode}, direction=Direction.Client_to_Server, ) clear_weekly_schedule: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Client_to_Server ) get_relay_status_log: Final = ZCLCommandDef( id=0x04, schema={}, direction=Direction.Client_to_Server ) class ClientCommandDefs(BaseCommandDefs): get_weekly_schedule_response: Final = ZCLCommandDef( id=0x00, schema={ "num_transitions_for_sequence": t.enum8, "day_of_week_for_sequence": SeqDayOfWeek, "mode_for_sequence": SeqMode, "values": t.List[t.int16s], }, direction=Direction.Server_to_Client, ) get_relay_status_log_response: Final = ZCLCommandDef( id=0x01, schema={ "time_of_day": t.uint16_t, "relay_status": t.bitmap8, "local_temperature": t.int16s, "humidity_in_percentage": t.uint8_t, "set_point": t.int16s, "unread_entries": t.uint16_t, }, direction=Direction.Server_to_Client, ) class FanMode(t.enum8): Off = 0x00 Low = 0x01 Medium = 0x02 High = 0x03 On = 0x04 Auto = 0x05 Smart = 0x06 class FanModeSequence(t.enum8): Low_Med_High = 0x00 Low_High = 0x01 Low_Med_High_Auto = 0x02 Low_High_Auto = 0x03 On_Auto = 0x04 class Fan(Cluster): """An interface for controlling a fan in a heating / cooling system. """ FanMode: Final = FanMode FanModeSequence: Final = FanModeSequence cluster_id: Final[t.uint16_t] = 0x0202 name: Final = "Fan Control" ep_attribute: Final = "fan" class AttributeDefs(BaseAttributeDefs): fan_mode: Final = ZCLAttributeDef(id=0x0000, type=FanMode, access="") fan_mode_sequence: Final = ZCLAttributeDef( id=0x0001, type=FanModeSequence, access="" ) class RelativeHumidityMode(t.enum8): RH_measured_locally = 0x00 RH_measured_remotely = 0x01 class DehumidificationLockout(t.enum8): Dehumidification_not_allowed = 0x00 Dehumidification_is_allowed = 0x01 class RelativeHumidityDisplay(t.enum8): RH_not_displayed = 0x00 RH_is_displayed = 0x01 class Dehumidification(Cluster): """An interface for controlling dehumidification.""" RelativeHumidityMode: Final = RelativeHumidityMode DehumidificationLockout: Final = DehumidificationLockout RelativeHumidityDisplay: Final = RelativeHumidityDisplay cluster_id: Final[t.uint16_t] = 0x0203 ep_attribute: Final = "dehumidification" class AttributeDefs(BaseAttributeDefs): # Dehumidification Information relative_humidity: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="r" ) dehumidification_cooling: Final = ZCLAttributeDef( id=0x0001, type=t.uint8_t, access="rp", mandatory=True ) # Dehumidification Settings rh_dehumidification_setpoint: Final = ZCLAttributeDef( id=0x0010, type=t.uint8_t, access="rw", mandatory=True ) relative_humidity_mode: Final = ZCLAttributeDef( id=0x0011, type=RelativeHumidityMode, access="rw" ) dehumidification_lockout: Final = ZCLAttributeDef( id=0x0012, type=DehumidificationLockout, access="rw" ) dehumidification_hysteresis: Final = ZCLAttributeDef( id=0x0013, type=t.uint8_t, access="rw", mandatory=True ) dehumidification_max_cool: Final = ZCLAttributeDef( id=0x0014, type=t.uint8_t, access="rw", mandatory=True ) relative_humidity_display: Final = ZCLAttributeDef( id=0x0015, type=RelativeHumidityDisplay, access="rw" ) class TemperatureDisplayMode(t.enum8): Metric = 0x00 Imperial = 0x01 class KeypadLockout(t.enum8): No_lockout = 0x00 Level_1_lockout = 0x01 Level_2_lockout = 0x02 Level_3_lockout = 0x03 Level_4_lockout = 0x04 Level_5_lockout = 0x05 class ScheduleProgrammingVisibility(t.enum8): Enabled = 0x00 Disabled = 0x02 class UserInterface(Cluster): """An interface for configuring the user interface of a thermostat (which may be remote from the thermostat). """ TemperatureDisplayMode: Final = TemperatureDisplayMode KeypadLockout: Final = KeypadLockout ScheduleProgrammingVisibility: Final = ScheduleProgrammingVisibility cluster_id: Final[t.uint16_t] = 0x0204 name: Final = "Thermostat User Interface Configuration" ep_attribute: Final = "thermostat_ui" class AttributeDefs(BaseAttributeDefs): temperature_display_mode: Final = ZCLAttributeDef( id=0x0000, type=TemperatureDisplayMode, access="rw", mandatory=True, ) keypad_lockout: Final = ZCLAttributeDef( id=0x0001, type=KeypadLockout, access="rw", mandatory=True ) schedule_programming_visibility: Final = ZCLAttributeDef( id=0x0002, type=ScheduleProgrammingVisibility, access="rw", ) zigpy-0.80.1/zigpy/zcl/clusters/lighting.py000066400000000000000000000431601501451476000207260ustar00rootroot00000000000000"""Lighting Functional Domain""" from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster, foundation from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, Direction as CommandDirection, ZCLAttributeDef, ZCLCommandDef, ) class ColorMode(t.enum8): Hue_and_saturation = 0x00 X_and_Y = 0x01 Color_temperature = 0x02 class EnhancedColorMode(t.enum8): Hue_and_saturation = 0x00 X_and_Y = 0x01 Color_temperature = 0x02 Enhanced_hue_and_saturation = 0x03 class ColorCapabilities(t.bitmap16): Hue_and_saturation = 0b00000000_00000001 Enhanced_hue = 0b00000000_00000010 Color_loop = 0b00000000_00000100 XY_attributes = 0b00000000_00001000 Color_temperature = 0b00000000_00010000 class Direction(t.enum8): Shortest_distance = 0x00 Longest_distance = 0x01 Up = 0x02 Down = 0x03 class MoveMode(t.enum8): Stop = 0x00 Up = 0x01 Down = 0x03 class StepMode(t.enum8): Up = 0x01 Down = 0x03 class ColorLoopUpdateFlags(t.bitmap8): Action = 0b0000_0001 Direction = 0b0000_0010 Time = 0b0000_0100 Start_Hue = 0b0000_1000 class ColorLoopAction(t.enum8): Deactivate = 0x00 Activate_from_color_loop_hue = 0x01 Activate_from_current_hue = 0x02 class ColorLoopDirection(t.enum8): Decrement = 0x00 Increment = 0x01 class DriftCompensation(t.enum8): NONE = 0x00 Other_or_unknown = 0x01 Temperature_monitoring = 0x02 Luminance_monitoring = 0x03 Color_monitoring = 0x03 class OptionsMask(t.bitmap8): Execute_if_off_present = 0b00000001 class Options(t.bitmap8): Execute_if_off = 0b00000001 class Color(Cluster): """Attributes and commands for controlling the color properties of a color-capable light """ ColorMode: Final = ColorMode EnhancedColorMode: Final = EnhancedColorMode ColorCapabilities: Final = ColorCapabilities Direction: Final = Direction MoveMode: Final = MoveMode StepMode: Final = StepMode ColorLoopUpdateFlags: Final = ColorLoopUpdateFlags ColorLoopAction: Final = ColorLoopAction ColorLoopDirection: Final = ColorLoopDirection DriftCompensation: Final = DriftCompensation Options: Final = Options OptionsMask: Final = OptionsMask cluster_id: Final[t.uint16_t] = 0x0300 name: Final = "Color Control" ep_attribute: Final = "light_color" class AttributeDefs(BaseAttributeDefs): current_hue: Final = ZCLAttributeDef(id=0x0000, type=t.uint8_t, access="rp") current_saturation: Final = ZCLAttributeDef( id=0x0001, type=t.uint8_t, access="rps" ) remaining_time: Final = ZCLAttributeDef(id=0x0002, type=t.uint16_t, access="r") current_x: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="rps") current_y: Final = ZCLAttributeDef(id=0x0004, type=t.uint16_t, access="rps") drift_compensation: Final = ZCLAttributeDef( id=0x0005, type=DriftCompensation, access="r" ) compensation_text: Final = ZCLAttributeDef( id=0x0006, type=t.CharacterString, access="r" ) color_temperature: Final = ZCLAttributeDef( id=0x0007, type=t.uint16_t, access="rps" ) color_mode: Final = ZCLAttributeDef( id=0x0008, type=ColorMode, access="r", mandatory=True ) options: Final = ZCLAttributeDef( id=0x000F, type=Options, access="rw", mandatory=True ) # Defined Primaries Information num_primaries: Final = ZCLAttributeDef(id=0x0010, type=t.uint8_t, access="r") primary1_x: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t, access="r") primary1_y: Final = ZCLAttributeDef(id=0x0012, type=t.uint16_t, access="r") primary1_intensity: Final = ZCLAttributeDef( id=0x0013, type=t.uint8_t, access="r" ) primary2_x: Final = ZCLAttributeDef(id=0x0015, type=t.uint16_t, access="r") primary2_y: Final = ZCLAttributeDef(id=0x0016, type=t.uint16_t, access="r") primary2_intensity: Final = ZCLAttributeDef( id=0x0017, type=t.uint8_t, access="r" ) primary3_x: Final = ZCLAttributeDef(id=0x0019, type=t.uint16_t, access="r") primary3_y: Final = ZCLAttributeDef(id=0x001A, type=t.uint16_t, access="r") primary3_intensity: Final = ZCLAttributeDef( id=0x001B, type=t.uint8_t, access="r" ) # Additional Defined Primaries Information primary4_x: Final = ZCLAttributeDef(id=0x0020, type=t.uint16_t, access="r") primary4_y: Final = ZCLAttributeDef(id=0x0021, type=t.uint16_t, access="r") primary4_intensity: Final = ZCLAttributeDef( id=0x0022, type=t.uint8_t, access="r" ) primary5_x: Final = ZCLAttributeDef(id=0x0024, type=t.uint16_t, access="r") primary5_y: Final = ZCLAttributeDef(id=0x0025, type=t.uint16_t, access="r") primary5_intensity: Final = ZCLAttributeDef( id=0x0026, type=t.uint8_t, access="r" ) primary6_x: Final = ZCLAttributeDef(id=0x0028, type=t.uint16_t, access="r") primary6_y: Final = ZCLAttributeDef(id=0x0029, type=t.uint16_t, access="r") primary6_intensity: Final = ZCLAttributeDef( id=0x002A, type=t.uint8_t, access="r" ) # Defined Color Point Settings white_point_x: Final = ZCLAttributeDef(id=0x0030, type=t.uint16_t, access="r") white_point_y: Final = ZCLAttributeDef(id=0x0031, type=t.uint16_t, access="r") color_point_r_x: Final = ZCLAttributeDef(id=0x0032, type=t.uint16_t, access="r") color_point_r_y: Final = ZCLAttributeDef(id=0x0033, type=t.uint16_t, access="r") color_point_r_intensity: Final = ZCLAttributeDef( id=0x0034, type=t.uint8_t, access="r" ) color_point_g_x: Final = ZCLAttributeDef(id=0x0036, type=t.uint16_t, access="r") color_point_g_y: Final = ZCLAttributeDef(id=0x0037, type=t.uint16_t, access="r") color_point_g_intensity: Final = ZCLAttributeDef( id=0x0038, type=t.uint8_t, access="r" ) color_point_b_x: Final = ZCLAttributeDef(id=0x003A, type=t.uint16_t, access="r") color_point_b_y: Final = ZCLAttributeDef(id=0x003B, type=t.uint16_t, access="r") color_point_b_intensity: Final = ZCLAttributeDef( id=0x003C, type=t.uint8_t, access="r" ) # ... enhanced_current_hue: Final = ZCLAttributeDef( id=0x4000, type=t.uint16_t, access="rs" ) enhanced_color_mode: Final = ZCLAttributeDef( id=0x4001, type=EnhancedColorMode, access="r", mandatory=True ) color_loop_active: Final = ZCLAttributeDef( id=0x4002, type=t.uint8_t, access="rs" ) color_loop_direction: Final = ZCLAttributeDef( id=0x4003, type=t.uint8_t, access="rs" ) color_loop_time: Final = ZCLAttributeDef( id=0x4004, type=t.uint16_t, access="rs" ) color_loop_start_enhanced_hue: Final = ZCLAttributeDef( id=0x4005, type=t.uint16_t, access="r" ) color_loop_stored_enhanced_hue: Final = ZCLAttributeDef( id=0x4006, type=t.uint16_t, access="r" ) color_capabilities: Final = ZCLAttributeDef( id=0x400A, type=ColorCapabilities, access="r", mandatory=True ) color_temp_physical_min: Final = ZCLAttributeDef( id=0x400B, type=t.uint16_t, access="r" ) color_temp_physical_max: Final = ZCLAttributeDef( id=0x400C, type=t.uint16_t, access="r" ) couple_color_temp_to_level_min: Final = ZCLAttributeDef( id=0x400D, type=t.uint16_t, access="r" ) start_up_color_temperature: Final = ZCLAttributeDef( id=0x4010, type=t.uint16_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): move_to_hue: Final = ZCLCommandDef( id=0x00, schema={ "hue": t.uint8_t, "direction": Direction, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_hue: Final = ZCLCommandDef( id=0x01, schema={ "move_mode": MoveMode, "rate": t.uint8_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) step_hue: Final = ZCLCommandDef( id=0x02, schema={ "step_mode": StepMode, "step_size": t.uint8_t, "transition_time": t.uint8_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_to_saturation: Final = ZCLCommandDef( id=0x03, schema={ "saturation": t.uint8_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_saturation: Final = ZCLCommandDef( id=0x04, schema={ "move_mode": MoveMode, "rate": t.uint8_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) step_saturation: Final = ZCLCommandDef( id=0x05, schema={ "step_mode": StepMode, "step_size": t.uint8_t, "transition_time": t.uint8_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_to_hue_and_saturation: Final = ZCLCommandDef( id=0x06, schema={ "hue": t.uint8_t, "saturation": t.uint8_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_to_color: Final = ZCLCommandDef( id=0x07, schema={ "color_x": t.uint16_t, "color_y": t.uint16_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_color: Final = ZCLCommandDef( id=0x08, schema={ "rate_x": t.uint16_t, "rate_y": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) step_color: Final = ZCLCommandDef( id=0x09, schema={ "step_x": t.uint16_t, "step_y": t.uint16_t, "duration": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_to_color_temp: Final = ZCLCommandDef( id=0x0A, schema={ "color_temp_mireds": t.uint16_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) enhanced_move_to_hue: Final = ZCLCommandDef( id=0x40, schema={ "enhanced_hue": t.uint16_t, "direction": Direction, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) enhanced_move_hue: Final = ZCLCommandDef( id=0x41, schema={ "move_mode": MoveMode, "rate": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) enhanced_step_hue: Final = ZCLCommandDef( id=0x42, schema={ "step_mode": StepMode, "step_size": t.uint16_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) enhanced_move_to_hue_and_saturation: Final = ZCLCommandDef( id=0x43, schema={ "enhanced_hue": t.uint16_t, "saturation": t.uint8_t, "transition_time": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) color_loop_set: Final = ZCLCommandDef( id=0x44, schema={ "update_flags": ColorLoopUpdateFlags, "action": ColorLoopAction, "direction": ColorLoopDirection, "time": t.uint16_t, "start_hue": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) stop_move_step: Final = ZCLCommandDef( id=0x47, schema={ "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) move_color_temp: Final = ZCLCommandDef( id=0x4B, schema={ "move_mode": MoveMode, "rate": t.uint16_t, "color_temp_min_mireds": t.uint16_t, "color_temp_max_mireds": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) step_color_temp: Final = ZCLCommandDef( id=0x4C, schema={ "step_mode": StepMode, "step_size": t.uint16_t, "transition_time": t.uint16_t, "color_temp_min_mireds": t.uint16_t, "color_temp_max_mireds": t.uint16_t, "options_mask?": OptionsMask, "options_override?": Options, }, direction=CommandDirection.Client_to_Server, ) class BallastStatus(t.bitmap8): Non_operational = 0b00000001 Lamp_failure = 0b00000010 class LampAlarmMode(t.bitmap8): Lamp_burn_hours = 0b00000001 class Ballast(Cluster): """Attributes and commands for configuring a lighting ballast """ BallastStatus: Final = BallastStatus LampAlarmMode: Final = LampAlarmMode cluster_id: Final[t.uint16_t] = 0x0301 ep_attribute: Final = "light_ballast" class AttributeDefs(BaseAttributeDefs): physical_min_level: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, access="r", mandatory=True ) physical_max_level: Final = ZCLAttributeDef( id=0x0001, type=t.uint8_t, access="r", mandatory=True ) ballast_status: Final = ZCLAttributeDef( id=0x0002, type=BallastStatus, access="r" ) # Ballast Settings min_level: Final = ZCLAttributeDef( id=0x0010, type=t.uint8_t, access="rw", mandatory=True ) max_level: Final = ZCLAttributeDef( id=0x0011, type=t.uint8_t, access="rw", mandatory=True ) power_on_level: Final = ZCLAttributeDef(id=0x0012, type=t.uint8_t, access="rw") power_on_fade_time: Final = ZCLAttributeDef( id=0x0013, type=t.uint16_t, access="rw" ) intrinsic_ballast_factor: Final = ZCLAttributeDef( id=0x0014, type=t.uint8_t, access="rw" ) ballast_factor_adjustment: Final = ZCLAttributeDef( id=0x0015, type=t.uint8_t, access="rw" ) # Lamp Information lamp_quantity: Final = ZCLAttributeDef(id=0x0020, type=t.uint8_t, access="r") # Lamp Settings lamp_type: Final = ZCLAttributeDef( id=0x0030, type=t.LimitedCharString(16), access="rw" ) lamp_manufacturer: Final = ZCLAttributeDef( id=0x0031, type=t.LimitedCharString(16), access="rw" ) lamp_rated_hours: Final = ZCLAttributeDef( id=0x0032, type=t.uint24_t, access="rw" ) lamp_burn_hours: Final = ZCLAttributeDef( id=0x0033, type=t.uint24_t, access="rw" ) lamp_alarm_mode: Final = ZCLAttributeDef( id=0x0034, type=LampAlarmMode, access="rw" ) lamp_burn_hours_trip_point: Final = ZCLAttributeDef( id=0x0035, type=t.uint24_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR zigpy-0.80.1/zigpy/zcl/clusters/lightlink.py000066400000000000000000000231461501451476000211100ustar00rootroot00000000000000from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster from zigpy.zcl.foundation import BaseCommandDefs, Direction, ZCLCommandDef class LogicalType(t.enum2): Coordinator = 0b00 Router = 0b01 EndDevice = 0b10 class ZigbeeInformation(t.Struct): logical_type: LogicalType rx_on_when_idle: t.uint1_t reserved: t.uint5_t class ScanRequestInformation(t.Struct): # whether the device is factory new factory_new: t.uint1_t # whether the device is capable of assigning addresses address_assignment: t.uint1_t reserved1: t.uint2_t # indicate the device is capable of initiating a link (i.e., it supports the # touchlink commissioning cluster at the client side) or 0 otherwise (i.e., it does # not support the touchlink commissioning cluster at the client side). touchlink_initiator: t.uint1_t undefined: t.uint1_t reserved2: t.uint1_t # If the ZLL profile is implemented, this bit shall be set to 0. In all other case # (Profile Interop / Zigbee 3.0), this bit shall be set to 1 profile_interop: t.uint1_t class ScanResponseInformation(t.Struct): factory_new: t.uint1_t address_assignment: t.uint1_t reserved1: t.uint2_t touchlink_initiator: t.uint1_t touchlink_priority_request: t.uint1_t reserved2: t.uint1_t profile_interop: t.uint1_t class DeviceInfoRecord(t.Struct): ieee: t.EUI64 endpoint_id: t.uint8_t profile_id: t.uint8_t device_id: t.uint16_t version: t.uint8_t group_id_count: t.uint8_t sort: t.uint8_t class Status(t.enum8): Success = 0x00 Failure = 0x01 class GroupInfoRecord(t.Struct): group_id: t.Group group_type: t.uint8_t class EndpointInfoRecord(t.Struct): nwk_addr: t.NWK endpoint_id: t.uint8_t profile_id: t.uint16_t device_id: t.uint16_t version: t.uint8_t class LightLink(Cluster): cluster_id: Final[t.uint16_t] = 0x1000 ep_attribute: Final = "lightlink" class ServerCommandDefs(BaseCommandDefs): scan: Final = ZCLCommandDef( id=0x00, schema={ "inter_pan_transaction_id": t.uint32_t, "zigbee_information": ZigbeeInformation, "touchlink_information": ScanRequestInformation, }, direction=Direction.Client_to_Server, ) device_info: Final = ZCLCommandDef( id=0x02, schema={"inter_pan_transaction_id": t.uint32_t, "start_index": t.uint8_t}, direction=Direction.Client_to_Server, ) identify: Final = ZCLCommandDef( id=0x06, schema={ "inter_pan_transaction_id": t.uint32_t, "identify_duration": t.uint16_t, }, direction=Direction.Client_to_Server, ) reset_to_factory_new: Final = ZCLCommandDef( id=0x07, schema={"inter_pan_transaction_id": t.uint32_t}, direction=Direction.Client_to_Server, ) network_start: Final = ZCLCommandDef( id=0x10, schema={ "inter_pan_transaction_id": t.uint32_t, "epid": t.EUI64, "key_index": t.uint8_t, "encrypted_network_key": t.KeyData, "logical_channel": t.uint8_t, "pan_id": t.PanId, "nwk_addr": t.NWK, "group_identifiers_begin": t.Group, "group_identifiers_end": t.Group, "free_network_addr_range_begin": t.NWK, "free_network_addr_range_end": t.NWK, "free_group_id_range_begin": t.Group, "free_group_id_range_end": t.Group, "initiator_ieee": t.EUI64, "initiator_nwk": t.NWK, }, direction=Direction.Client_to_Server, ) network_join_router: Final = ZCLCommandDef( id=0x12, schema={ "inter_pan_transaction_id": t.uint32_t, "epid": t.EUI64, "key_index": t.uint8_t, "encrypted_network_key": t.KeyData, "nwk_update_id": t.uint8_t, "logical_channel": t.uint8_t, "pan_id": t.PanId, "nwk_addr": t.NWK, "group_identifiers_begin": t.Group, "group_identifiers_end": t.Group, "free_network_addr_range_begin": t.NWK, "free_network_addr_range_end": t.NWK, "free_group_id_range_begin": t.Group, "free_group_id_range_end": t.Group, }, direction=Direction.Client_to_Server, ) network_join_end_device: Final = ZCLCommandDef( id=0x14, schema={ "inter_pan_transaction_id": t.uint32_t, "epid": t.EUI64, "key_index": t.uint8_t, "encrypted_network_key": t.KeyData, "nwk_update_id": t.uint8_t, "logical_channel": t.uint8_t, "pan_id": t.PanId, "nwk_addr": t.NWK, "group_identifiers_begin": t.Group, "group_identifiers_end": t.Group, "free_network_addr_range_begin": t.NWK, "free_network_addr_range_end": t.NWK, "free_group_id_range_begin": t.Group, "free_group_id_range_end": t.Group, }, direction=Direction.Client_to_Server, ) network_update: Final = ZCLCommandDef( id=0x16, schema={ "inter_pan_transaction_id": t.uint32_t, "epid": t.EUI64, "nwk_update_id": t.uint8_t, "logical_channel": t.uint8_t, "pan_id": t.PanId, "nwk_addr": t.NWK, }, direction=Direction.Client_to_Server, ) # Utility get_group_identifiers: Final = ZCLCommandDef( id=0x41, schema={ "start_index": t.uint8_t, }, direction=Direction.Client_to_Server, ) get_endpoint_list: Final = ZCLCommandDef( id=0x42, schema={ "start_index": t.uint8_t, }, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): scan_rsp: Final = ZCLCommandDef( id=0x01, schema={ "inter_pan_transaction_id": t.uint32_t, "rssi_correction": t.uint8_t, "zigbee_info": ZigbeeInformation, "touchlink_info": ScanResponseInformation, "key_bitmask": t.uint16_t, "response_id": t.uint32_t, "epid": t.EUI64, "nwk_update_id": t.uint8_t, "logical_channel": t.uint8_t, "pan_id": t.PanId, "nwk_addr": t.NWK, "num_sub_devices": t.uint8_t, "total_group_ids": t.uint8_t, "endpoint_id?": t.uint8_t, "profile_id?": t.uint16_t, "device_id?": t.uint16_t, "version?": t.uint8_t, "group_id_count?": t.uint8_t, }, direction=Direction.Server_to_Client, ) device_info_rsp: Final = ZCLCommandDef( id=0x03, schema={ "inter_pan_transaction_id": t.uint32_t, "num_sub_devices": t.uint8_t, "start_index": t.uint8_t, "device_info_records": t.LVList[DeviceInfoRecord], }, direction=Direction.Server_to_Client, ) network_start_rsp: Final = ZCLCommandDef( id=0x11, schema={ "inter_pan_transaction_id": t.uint32_t, "status": Status, "epid": t.EUI64, "nwk_update_id": t.uint8_t, "logical_channel": t.uint8_t, "pan_id": t.PanId, }, direction=Direction.Server_to_Client, ) network_join_router_rsp: Final = ZCLCommandDef( id=0x13, schema={ "inter_pan_transaction_id": t.uint32_t, "status": Status, }, direction=Direction.Server_to_Client, ) network_join_end_device_rsp: Final = ZCLCommandDef( id=0x15, schema={ "inter_pan_transaction_id": t.uint32_t, "status": Status, }, direction=Direction.Server_to_Client, ) # Utility endpoint_info: Final = ZCLCommandDef( id=0x40, schema={ "ieee_addr": t.EUI64, "nwk_addr": t.NWK, "endpoint_id": t.uint8_t, "profile_id": t.uint16_t, "device_id": t.uint16_t, "version": t.uint8_t, }, direction=Direction.Server_to_Client, ) get_group_identifiers_rsp: Final = ZCLCommandDef( id=0x41, schema={ "total": t.uint8_t, "start_index": t.uint8_t, "group_info_records": t.LVList[GroupInfoRecord], }, direction=Direction.Server_to_Client, ) get_endpoint_list_rsp: Final = ZCLCommandDef( id=0x42, schema={ "total": t.uint8_t, "start_index": t.uint8_t, "endpoint_info_records": t.LVList[EndpointInfoRecord], }, direction=Direction.Server_to_Client, ) zigpy-0.80.1/zigpy/zcl/clusters/manufacturer_specific.py000066400000000000000000000004161501451476000234570ustar00rootroot00000000000000from __future__ import annotations from typing import Final from zigpy.zcl import Cluster class ManufacturerSpecificCluster(Cluster): cluster_id_range = (0xFC00, 0xFFFF) ep_attribute: Final = "manufacturer_specific" name: Final = "Manufacturer Specific" zigpy-0.80.1/zigpy/zcl/clusters/measurement.py000066400000000000000000000502411501451476000214440ustar00rootroot00000000000000"""Measurement & Sensing Functional Domain""" from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster, foundation from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef class LightSensorType(t.enum8): Photodiode = 0x00 CMOS = 0x01 Unknown = 0xFF class IlluminanceMeasurement(Cluster): LightSensorType: Final = LightSensorType cluster_id: Final[t.uint16_t] = 0x0400 name: Final = "Illuminance Measurement" ep_attribute: Final = "illuminance" class AttributeDefs(BaseAttributeDefs): measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") light_sensor_type: Final = ZCLAttributeDef( id=0x0004, type=LightSensorType, access="r" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class LevelStatus(t.enum8): Illuminance_On_Target = 0x00 Illuminance_Below_Target = 0x01 Illuminance_Above_Target = 0x02 class IlluminanceLevelSensing(Cluster): LevelStatus: Final = LevelStatus LightSensorType: Final = LightSensorType cluster_id: Final[t.uint16_t] = 0x0401 name: Final = "Illuminance Level Sensing" ep_attribute: Final = "illuminance_level" class AttributeDefs(BaseAttributeDefs): level_status: Final = ZCLAttributeDef( id=0x0000, type=LevelStatus, access="r", mandatory=True ) light_sensor_type: Final = ZCLAttributeDef( id=0x0001, type=LightSensorType, access="r" ) # Illuminance Level Sensing Settings illuminance_target_level: Final = ZCLAttributeDef( id=0x0010, type=t.uint16_t, access="rw", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class TemperatureMeasurement(Cluster): cluster_id: Final[t.uint16_t] = 0x0402 name: Final = "Temperature Measurement" ep_attribute: Final = "temperature" class AttributeDefs(BaseAttributeDefs): # Temperature Measurement Information measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.int16s, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.int16s, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.int16s, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") # 0x0010: ('min_percent_change', UNKNOWN), # 0x0011: ('min_absolute_change', UNKNOWN), cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class PressureMeasurement(Cluster): cluster_id: Final[t.uint16_t] = 0x0403 name: Final = "Pressure Measurement" ep_attribute: Final = "pressure" class AttributeDefs(BaseAttributeDefs): # Pressure Measurement Information measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.int16s, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.int16s, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.int16s, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") # Extended attribute set scaled_value: Final = ZCLAttributeDef(id=0x0010, type=t.int16s, access="r") min_scaled_value: Final = ZCLAttributeDef(id=0x0011, type=t.int16s, access="r") max_scaled_value: Final = ZCLAttributeDef(id=0x0012, type=t.int16s, access="r") scaled_tolerance: Final = ZCLAttributeDef( id=0x0013, type=t.uint16_t, access="r" ) scale: Final = ZCLAttributeDef(id=0x0014, type=t.int8s, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class FlowMeasurement(Cluster): cluster_id: Final[t.uint16_t] = 0x0404 name: Final = "Flow Measurement" ep_attribute: Final = "flow" class AttributeDefs(BaseAttributeDefs): measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class RelativeHumidity(Cluster): cluster_id: Final[t.uint16_t] = 0x0405 name: Final = "Relative Humidity Measurement" ep_attribute: Final = "humidity" class AttributeDefs(BaseAttributeDefs): measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class Occupancy(t.bitmap8): Unoccupied = 0b00000000 Occupied = 0b00000001 class OccupancySensorType(t.enum8): PIR = 0x00 Ultrasonic = 0x01 PIR_and_Ultrasonic = 0x02 Physical_Contact = 0x03 class OccupancySensorTypeBitmap(t.bitmap8): PIR = 0b00000001 Ultrasonic = 0b00000010 Physical_Contact = 0b00000100 class OccupancySensing(Cluster): Occupancy: Final = Occupancy OccupancySensorType: Final = OccupancySensorType OccupancySensorTypeBitmap: Final = OccupancySensorTypeBitmap cluster_id: Final[t.uint16_t] = 0x0406 name: Final = "Occupancy Sensing" ep_attribute: Final = "occupancy" class AttributeDefs(BaseAttributeDefs): # Occupancy Sensor Information occupancy: Final = ZCLAttributeDef( id=0x0000, type=Occupancy, access="rp", mandatory=True ) occupancy_sensor_type_bitmap: Final = ZCLAttributeDef( id=0x0001, type=t.bitmap8, access="r", mandatory=True ) # PIR Configuration pir_o_to_u_delay: Final = ZCLAttributeDef( id=0x0010, type=t.uint16_t, access="rw" ) pir_u_to_o_delay: Final = ZCLAttributeDef( id=0x0011, type=t.uint16_t, access="rw" ) pir_u_to_o_threshold: Final = ZCLAttributeDef( id=0x0012, type=t.uint8_t, access="rw" ) # Ultrasonic Configuration ultrasonic_o_to_u_delay: Final = ZCLAttributeDef( id=0x0020, type=t.uint16_t, access="rw" ) ultrasonic_u_to_o_delay: Final = ZCLAttributeDef( id=0x0021, type=t.uint16_t, access="rw" ) ultrasonic_u_to_o_threshold: Final = ZCLAttributeDef( id=0x0022, type=t.uint8_t, access="rw" ) # Physical Contact Configuration physical_contact_o_to_u_delay: Final = ZCLAttributeDef( id=0x0030, type=t.uint16_t, access="rw" ) physical_contact_u_to_o_delay: Final = ZCLAttributeDef( id=0x0031, type=t.uint16_t, access="rw" ) physical_contact_u_to_o_threshold: Final = ZCLAttributeDef( id=0x0032, type=t.uint8_t, access="rw" ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class LeafWetness(Cluster): cluster_id: Final[t.uint16_t] = 0x0407 name: Final = "Leaf Wetness Measurement" ep_attribute: Final = "leaf_wetness" class AttributeDefs(BaseAttributeDefs): # Leaf Wetness Measurement Information measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class SoilMoisture(Cluster): cluster_id: Final[t.uint16_t] = 0x0408 name: Final = "Soil Moisture Measurement" ep_attribute: Final = "soil_moisture" class AttributeDefs(BaseAttributeDefs): # Soil Moisture Measurement Information measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class PH(Cluster): cluster_id: Final[t.uint16_t] = 0x0409 name: Final = "pH Measurement" ep_attribute: Final = "ph" class AttributeDefs(BaseAttributeDefs): # pH Measurement Information measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ElectricalConductivity(Cluster): cluster_id: Final[t.uint16_t] = 0x040A name: Final = "Electrical Conductivity" ep_attribute: Final = "electrical_conductivity" class AttributeDefs(BaseAttributeDefs): measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class WindSpeed(Cluster): cluster_id: Final[t.uint16_t] = 0x040B name: Final = "Wind Speed Measurement" ep_attribute: Final = "wind_speed" class AttributeDefs(BaseAttributeDefs): # Wind Speed Measurement Information measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rp", mandatory=True ) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.uint16_t, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.uint16_t, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.uint16_t, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class _ConcentrationMixin: """Mixin for the common attributes of the concentration measurement clusters""" class AttributeDefs(BaseAttributeDefs): measured_value: Final = ZCLAttributeDef( id=0x0000, type=t.Single, access="rp", mandatory=True ) # fraction of 1 (one) min_measured_value: Final = ZCLAttributeDef( id=0x0001, type=t.Single, access="r", mandatory=True ) max_measured_value: Final = ZCLAttributeDef( id=0x0002, type=t.Single, access="r", mandatory=True ) tolerance: Final = ZCLAttributeDef(id=0x0003, type=t.Single, access="r") cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class CarbonMonoxideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x040C name: Final = "Carbon Monoxide (CO) Concentration" ep_attribute: Final = "carbon_monoxide_concentration" class CarbonDioxideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x040D name: Final = "Carbon Dioxide (CO₂) Concentration" ep_attribute: Final = "carbon_dioxide_concentration" class EthyleneConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x040E name: Final = "Ethylene (CH₂) Concentration" ep_attribute: Final = "ethylene_concentration" class EthyleneOxideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x040F name: Final = "Ethylene Oxide (C₂H₄O) Concentration" ep_attribute: Final = "ethylene_oxide_concentration" class HydrogenConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0410 name: Final = "Hydrogen (H) Concentration" ep_attribute: Final = "hydrogen_concentration" class HydrogenSulfideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0411 name: Final = "Hydrogen Sulfide (H₂S) Concentration" ep_attribute: Final = "hydrogen_sulfide_concentration" class NitricOxideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0412 name: Final = "Nitric Oxide (NO) Concentration" ep_attribute: Final = "nitric_oxide_concentration" class NitrogenDioxideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0413 name: Final = "Nitrogen Dioxide (NO₂) Concentration" ep_attribute: Final = "nitrogen_dioxide_concentration" class OxygenConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0414 name: Final = "Oxygen (O₂) Concentration" ep_attribute: Final = "oxygen_concentration" class OzoneConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0415 name: Final = "Ozone (O₃) Concentration" ep_attribute: Final = "ozone_concentration" class SulfurDioxideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0416 name: Final = "Sulfur Dioxide (SO₂) Concentration" ep_attribute: Final = "sulfur_dioxide_concentration" class DissolvedOxygenConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0417 name: Final = "Dissolved Oxygen (DO) Concentration" ep_attribute: Final = "dissolved_oxygen_concentration" class BromateConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0418 name: Final = "Bromate Concentration" ep_attribute: Final = "bromate_concentration" class ChloraminesConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0419 name: Final = "Chloramines Concentration" ep_attribute: Final = "chloramines_concentration" class ChlorineConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x041A name: Final = "Chlorine Concentration" ep_attribute: Final = "chlorine_concentration" class FecalColiformAndEColiFraction(_ConcentrationMixin, Cluster): """Percent of positive samples""" cluster_id: Final[t.uint16_t] = 0x041B name: Final = "Fecal coliform & E. Coli Fraction" ep_attribute: Final = "fecal_coliform_and_e_coli_fraction" class FluorideConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = ( 0x041C # XXX: spec repeats 0x041B but this seems like a mistake ) name: Final = "Fluoride Concentration" ep_attribute: Final = "fluoride_concentration" class HaloaceticAcidsConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x041D name: Final = "Haloacetic Acids Concentration" ep_attribute: Final = "haloacetic_acids_concentration" class TotalTrihalomethanesConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x041E name: Final = "Total Trihalomethanes Concentration" ep_attribute: Final = "total_trihalomethanes_concentration" class TotalColiformBacteriaFraction(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x041F name: Final = "Total Coliform Bacteria Fraction" ep_attribute: Final = "total_coliform_bacteria_fraction" # XXX: is this a concentration? What are the units? class Turbidity(_ConcentrationMixin, Cluster): """Cloudiness of particles in water where an average person would notice a 5 or higher""" cluster_id: Final[t.uint16_t] = 0x0420 name: Final = "Turbidity" ep_attribute: Final = "turbidity" class CopperConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0421 name: Final = "Copper Concentration" ep_attribute: Final = "copper_concentration" class LeadConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0422 name: Final = "Lead Concentration" ep_attribute: Final = "lead_concentration" class ManganeseConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0423 name: Final = "Manganese Concentration" ep_attribute: Final = "manganese_concentration" class SulfateConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0424 name: Final = "Sulfate Concentration" ep_attribute: Final = "sulfate_concentration" class BromodichloromethaneConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0425 name: Final = "Bromodichloromethane Concentration" ep_attribute: Final = "bromodichloromethane_concentration" class BromoformConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0426 name: Final = "Bromoform Concentration" ep_attribute: Final = "bromoform_concentration" class ChlorodibromomethaneConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0427 name: Final = "Chlorodibromomethane Concentration" ep_attribute: Final = "chlorodibromomethane_concentration" class ChloroformConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0428 name: Final = "Chloroform Concentration" ep_attribute: Final = "chloroform_concentration" class SodiumConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x0429 name: Final = "Sodium Concentration" ep_attribute: Final = "sodium_concentration" # XXX: is this a concentration? What are the units? class PM25(_ConcentrationMixin, Cluster): """Particulate Matter 2.5 microns or less""" cluster_id: Final[t.uint16_t] = 0x042A name: Final = "PM2.5" ep_attribute: Final = "pm25" class FormaldehydeConcentration(_ConcentrationMixin, Cluster): cluster_id: Final[t.uint16_t] = 0x042B name: Final = "Formaldehyde Concentration" ep_attribute: Final = "formaldehyde_concentration" zigpy-0.80.1/zigpy/zcl/clusters/protocol.py000066400000000000000000000413641501451476000207660ustar00rootroot00000000000000"""Protocol Interfaces Functional Domain""" from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, Direction, ZCLAttributeDef, ZCLCommandDef, ) class DateTime(t.Struct): date: t.uint32_t time: t.uint32_t class GenericTunnel(Cluster): cluster_id: Final[t.uint16_t] = 0x0600 ep_attribute: Final = "generic_tunnel" class AttributeDefs(BaseAttributeDefs): max_income_trans_size: Final = ZCLAttributeDef(id=0x0001, type=t.uint16_t) max_outgo_trans_size: Final = ZCLAttributeDef(id=0x0002, type=t.uint16_t) protocol_addr: Final = ZCLAttributeDef(id=0x0003, type=t.LVBytes) class ServerCommandDefs(BaseCommandDefs): match_protocol_addr: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Client_to_Server ) class ClientCommandDefs(BaseCommandDefs): match_protocol_addr_response: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Server_to_Client ) advertise_protocol_address: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) class BacnetProtocolTunnel(Cluster): cluster_id: Final[t.uint16_t] = 0x0601 ep_attribute: Final = "bacnet_tunnel" class ServerCommandDefs(BaseCommandDefs): transfer_npdu: Final = ZCLCommandDef( id=0x00, schema={"npdu": t.LVBytes}, direction=Direction.Client_to_Server ) class AnalogInputRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x0602 ep_attribute: Final = "bacnet_regular_analog_input" class AttributeDefs(BaseAttributeDefs): cov_increment: Final = ZCLAttributeDef(id=0x0016, type=t.Single) device_type: Final = ZCLAttributeDef(id=0x001F, type=t.CharacterString) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) update_interval: Final = ZCLAttributeDef(id=0x0076, type=t.uint8_t) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class AnalogInputExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x0603 ep_attribute: Final = "bacnet_extended_analog_input" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) deadband: Final = ZCLAttributeDef(id=0x0019, type=t.Single) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) high_limit: Final = ZCLAttributeDef(id=0x002D, type=t.Single) limit_enable: Final = ZCLAttributeDef(id=0x0034, type=t.bitmap8) low_limit: Final = ZCLAttributeDef(id=0x003B, type=t.Single) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # event_time_stamps: Final = ZCLAttributeDef(id=0x0082, type=t.Array[3, t.uint32_t]) # integer, time of day, or structure of (date, time of day)) class ServerCommandDefs(BaseCommandDefs): transfer_apdu: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Client_to_Server ) connect_req: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) disconnect_req: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) connect_status_noti: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Client_to_Server ) class AnalogOutputRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x0604 ep_attribute: Final = "bacnet_regular_analog_output" class AttributeDefs(BaseAttributeDefs): cov_increment: Final = ZCLAttributeDef(id=0x0016, type=t.Single) device_type: Final = ZCLAttributeDef(id=0x001F, type=t.CharacterString) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) update_interval: Final = ZCLAttributeDef(id=0x0076, type=t.uint8_t) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class AnalogOutputExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x0605 ep_attribute: Final = "bacnet_extended_analog_output" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) deadband: Final = ZCLAttributeDef(id=0x0019, type=t.Single) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) high_limit: Final = ZCLAttributeDef(id=0x002D, type=t.Single) limit_enable: Final = ZCLAttributeDef(id=0x0034, type=t.bitmap8) low_limit: Final = ZCLAttributeDef(id=0x003B, type=t.Single) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # event_time_stamps: Final = ZCLAttributeDef(id=0x0082, type=t.Array[3, t.uint32_t]) # integer, time of day, or structure of (date, time of day)) class AnalogValueRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x0606 ep_attribute: Final = "bacnet_regular_analog_value" class AttributeDefs(BaseAttributeDefs): cov_increment: Final = ZCLAttributeDef(id=0x0016, type=t.Single) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class AnalogValueExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x0607 ep_attribute: Final = "bacnet_extended_analog_value" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) deadband: Final = ZCLAttributeDef(id=0x0019, type=t.Single) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) high_limit: Final = ZCLAttributeDef(id=0x002D, type=t.Single) limit_enable: Final = ZCLAttributeDef(id=0x0034, type=t.bitmap8) low_limit: Final = ZCLAttributeDef(id=0x003B, type=t.Single) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) class BinaryInputRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x0608 ep_attribute: Final = "bacnet_regular_binary_input" class AttributeDefs(BaseAttributeDefs): change_of_state_count: Final = ZCLAttributeDef(id=0x000F, type=t.uint32_t) change_of_state_time: Final = ZCLAttributeDef(id=0x0010, type=DateTime) device_type: Final = ZCLAttributeDef(id=0x001F, type=t.CharacterString) elapsed_active_time: Final = ZCLAttributeDef(id=0x0021, type=t.uint32_t) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) time_of_at_reset: Final = ZCLAttributeDef(id=0x0072, type=DateTime) time_of_sc_reset: Final = ZCLAttributeDef(id=0x0073, type=DateTime) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class BinaryInputExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x0609 ep_attribute: Final = "bacnet_extended_binary_input" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) alarm_value: Final = ZCLAttributeDef(id=0x0006, type=t.Bool) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # 0x0082: ZCLAttributeDef('event_time_stamps', type=TODO.array), # Array[3] of (16-bit unsigned # integer, time of day, or structure of (date, time of day)) class BinaryOutputRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x060A ep_attribute: Final = "bacnet_regular_binary_output" class AttributeDefs(BaseAttributeDefs): change_of_state_count: Final = ZCLAttributeDef(id=0x000F, type=t.uint32_t) change_of_state_time: Final = ZCLAttributeDef(id=0x0010, type=DateTime) device_type: Final = ZCLAttributeDef(id=0x001F, type=t.CharacterString) elapsed_active_time: Final = ZCLAttributeDef(id=0x0021, type=t.uint32_t) feed_back_value: Final = ZCLAttributeDef(id=0x0028, type=t.enum8) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) time_of_at_reset: Final = ZCLAttributeDef(id=0x0072, type=DateTime) time_of_sc_reset: Final = ZCLAttributeDef(id=0x0073, type=DateTime) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class BinaryOutputExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x060B ep_attribute: Final = "bacnet_extended_binary_output" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # 0x0082: ZCLAttributeDef('event_time_stamps', type=TODO.array), # Array[3] of (16-bit unsigned # integer, time of day, or structure of (date, time of day)) class BinaryValueRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x060C ep_attribute: Final = "bacnet_regular_binary_value" class AttributeDefs(BaseAttributeDefs): change_of_state_count: Final = ZCLAttributeDef(id=0x000F, type=t.uint32_t) change_of_state_time: Final = ZCLAttributeDef(id=0x0010, type=DateTime) elapsed_active_time: Final = ZCLAttributeDef(id=0x0021, type=t.uint32_t) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) time_of_at_reset: Final = ZCLAttributeDef(id=0x0072, type=DateTime) time_of_sc_reset: Final = ZCLAttributeDef(id=0x0073, type=DateTime) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class BinaryValueExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x060D ep_attribute: Final = "bacnet_extended_binary_value" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) alarm_value: Final = ZCLAttributeDef(id=0x0006, type=t.Bool) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # 0x0082: ZCLAttributeDef('event_time_stamps', type=TODO.array), # Array[3] of (16-bit unsigned # integer, time of day, or structure of (date, time of day)) class MultistateInputRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x060E ep_attribute: Final = "bacnet_regular_multistate_input" class AttributeDefs(BaseAttributeDefs): device_type: Final = ZCLAttributeDef(id=0x001F, type=t.CharacterString) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class MultistateInputExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x060F ep_attribute: Final = "bacnet_extended_multistate_input" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) alarm_value: Final = ZCLAttributeDef(id=0x0006, type=t.uint16_t) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) fault_values: Final = ZCLAttributeDef(id=0x0025, type=t.uint16_t) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # 0x0082: ZCLAttributeDef('event_time_stamps', type=TODO.array), # Array[3] of (16-bit unsigned # integer, time of day, or structure of (date, time of day)) class MultistateOutputRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x0610 ep_attribute: Final = "bacnet_regular_multistate_output" class AttributeDefs(BaseAttributeDefs): device_type: Final = ZCLAttributeDef(id=0x001F, type=t.CharacterString) feed_back_value: Final = ZCLAttributeDef(id=0x0028, type=t.enum8) object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class MultistateOutputExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x0611 ep_attribute: Final = "bacnet_extended_multistate_output" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # 0x0082: ZCLAttributeDef('event_time_stamps', type=TODO.array), # Array[3] of (16-bit unsigned # integer, time of day, or structure of (date, time of day)) class MultistateValueRegular(Cluster): cluster_id: Final[t.uint16_t] = 0x0612 ep_attribute: Final = "bacnet_regular_multistate_value" class AttributeDefs(BaseAttributeDefs): object_id: Final = ZCLAttributeDef(id=0x004B, type=t.FixedList[4, t.uint8_t]) object_name: Final = ZCLAttributeDef(id=0x004D, type=t.CharacterString) object_type: Final = ZCLAttributeDef(id=0x004F, type=t.enum16) profile_name: Final = ZCLAttributeDef(id=0x00A8, type=t.CharacterString) class MultistateValueExtended(Cluster): cluster_id: Final[t.uint16_t] = 0x0613 ep_attribute: Final = "bacnet_extended_multistate_value" class AttributeDefs(BaseAttributeDefs): acked_transitions: Final = ZCLAttributeDef(id=0x0000, type=t.bitmap8) alarm_value: Final = ZCLAttributeDef(id=0x0006, type=t.uint16_t) notification_class: Final = ZCLAttributeDef(id=0x0011, type=t.uint16_t) event_enable: Final = ZCLAttributeDef(id=0x0023, type=t.bitmap8) event_state: Final = ZCLAttributeDef(id=0x0024, type=t.enum8) fault_values: Final = ZCLAttributeDef(id=0x0025, type=t.uint16_t) notify_type: Final = ZCLAttributeDef(id=0x0048, type=t.enum8) time_delay: Final = ZCLAttributeDef(id=0x0071, type=t.uint8_t) # 0x0082: ZCLAttributeDef('event_time_stamps', type=TODO.array), # Array[3] of (16-bit unsigned # integer, time of day, or structure of (date, time of day)) zigpy-0.80.1/zigpy/zcl/clusters/security.py000066400000000000000000000371001501451476000207650ustar00rootroot00000000000000"""Security and Safety Functional Domain""" from __future__ import annotations from typing import Any, Final import zigpy.types as t from zigpy.typing import AddressingMode from zigpy.zcl import Cluster, foundation from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, Direction, ZCLAttributeDef, ZCLCommandDef, ) class ZoneState(t.enum8): Not_enrolled = 0x00 Enrolled = 0x01 class ZoneType(t.enum_factory(t.uint16_t, "manufacturer_specific")): """Zone type enum.""" Standard_CIE = 0x0000 Motion_Sensor = 0x000D Contact_Switch = 0x0015 Fire_Sensor = 0x0028 Water_Sensor = 0x002A Carbon_Monoxide_Sensor = 0x002B Personal_Emergency_Device = 0x002C Vibration_Movement_Sensor = 0x002D Remote_Control = 0x010F Key_Fob = 0x0115 Key_Pad = 0x021D Standard_Warning_Device = 0x0225 Glass_Break_Sensor = 0x0226 Security_Repeater = 0x0229 Invalid_Zone_Type = 0xFFFF class ZoneStatus(t.bitmap16): """ZoneStatus attribute.""" Alarm_1 = 0x0001 Alarm_2 = 0x0002 Tamper = 0x0004 Battery = 0x0008 Supervision_reports = 0x0010 Restore_reports = 0x0020 Trouble = 0x0040 AC_mains = 0x0080 Test = 0x0100 Battery_Defect = 0x0200 class EnrollResponse(t.enum8): """Enroll response code.""" Success = 0x00 Not_supported = 0x01 No_enroll_permit = 0x02 Too_many_zones = 0x03 class IasZone(Cluster): """The IAS Zone cluster defines an interface to the functionality of an IAS security zone device. IAS Zone supports up to two alarm types per zone, low battery reports and supervision of the IAS network. """ ZoneState: Final = ZoneState ZoneType: Final = ZoneType ZoneStatus: Final = ZoneStatus EnrollResponse: Final = EnrollResponse cluster_id: Final[t.uint16_t] = 0x0500 name: Final = "IAS Zone" ep_attribute: Final = "ias_zone" class AttributeDefs(BaseAttributeDefs): # Zone Information zone_state: Final = ZCLAttributeDef( id=0x0000, type=ZoneState, access="r", mandatory=True ) zone_type: Final = ZCLAttributeDef( id=0x0001, type=ZoneType, access="r", mandatory=True ) zone_status: Final = ZCLAttributeDef( id=0x0002, type=ZoneStatus, access="r", mandatory=True ) # Zone Settings cie_addr: Final = ZCLAttributeDef( id=0x0010, type=t.EUI64, access="rw", mandatory=True ) zone_id: Final = ZCLAttributeDef( id=0x0011, type=t.uint8_t, access="r", mandatory=True ) # Both attributes will be supported/unsupported num_zone_sensitivity_levels_supported: Final = ZCLAttributeDef( id=0x0012, type=t.uint8_t, access="r" ) current_zone_sensitivity_level: Final = ZCLAttributeDef( id=0x0013, type=t.uint8_t, access="rw" ) class ServerCommandDefs(BaseCommandDefs): enroll_response: Final = ZCLCommandDef( id=0x00, schema={"enroll_response_code": EnrollResponse, "zone_id": t.uint8_t}, direction=Direction.Server_to_Client, ) init_normal_op_mode: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) init_test_mode: Final = ZCLCommandDef( id=0x02, schema={ "test_mode_duration": t.uint8_t, "current_zone_sensitivity_level": t.uint8_t, }, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): status_change_notification: Final = ZCLCommandDef( id=0x00, schema={ "zone_status": ZoneStatus, "extended_status": t.bitmap8, "zone_id": t.uint8_t, "delay": t.uint16_t, }, direction=Direction.Client_to_Server, ) enroll: Final = ZCLCommandDef( id=0x01, schema={"zone_type": ZoneType, "manufacturer_code": t.uint16_t}, direction=Direction.Client_to_Server, ) def handle_cluster_request( self, hdr: foundation.ZCLHeader, args: list[Any], *, dst_addressing: AddressingMode | None = None, ): if ( hdr.command_id == self.commands_by_name["enroll_response"].id and self.is_server and not hdr.frame_control.disable_default_response ): hdr.frame_control = hdr.frame_control.replace( direction=Direction.Client_to_Server ) # this is a client -> server cmd self.send_default_rsp(hdr, foundation.Status.SUCCESS) class AlarmStatus(t.enum8): """IAS ACE alarm status enum.""" No_Alarm = 0x00 Burglar = 0x01 Fire = 0x02 Emergency = 0x03 Police_Panic = 0x04 Fire_Panic = 0x05 Emergency_Panic = 0x06 class ArmMode(t.enum8): """IAS ACE arm mode enum.""" Disarm = 0x00 Arm_Day_Home_Only = 0x01 Arm_Night_Sleep_Only = 0x02 Arm_All_Zones = 0x03 class ArmNotification(t.enum8): """IAS ACE arm notification enum.""" All_Zones_Disarmed = 0x00 Only_Day_Home_Zones_Armed = 0x01 Only_Night_Sleep_Zones_Armed = 0x02 All_Zones_Armed = 0x03 Invalid_Arm_Disarm_Code = 0x04 Not_Ready_To_Arm = 0x05 Already_Disarmed = 0x06 class AudibleNotification(t.enum_factory(t.uint8_t, "manufacturer_specific")): """IAS ACE audible notification enum.""" Mute = 0x00 Default_Sound = 0x01 class BypassResponse(t.enum8): """Bypass result.""" Zone_bypassed = 0x00 Zone_not_bypassed = 0x01 Not_allowed = 0x02 Invalid_Zone_ID = 0x03 Unknown_Zone_ID = 0x04 Invalid_Code = 0x05 class PanelStatus(t.enum8): """IAS ACE panel status enum.""" Panel_Disarmed = 0x00 Armed_Stay = 0x01 Armed_Night = 0x02 Armed_Away = 0x03 Exit_Delay = 0x04 Entry_Delay = 0x05 Not_Ready_To_Arm = 0x06 In_Alarm = 0x07 Arming_Stay = 0x08 Arming_Night = 0x09 Arming_Away = 0x0A class ZoneStatusRsp(t.Struct): """Zone status response.""" zone_id: t.uint8_t zone_status: IasZone.ZoneStatus class IasAce(Cluster): """IAS Ancillary Control Equipment cluster.""" AlarmStatus: Final = AlarmStatus ArmMode: Final = ArmMode ArmNotification: Final = ArmNotification AudibleNotification: Final = AudibleNotification BypassResponse: Final = BypassResponse PanelStatus: Final = PanelStatus ZoneType: Final = IasZone.ZoneType ZoneStatus: Final = IasZone.ZoneStatus ZoneStatusRsp: Final = ZoneStatusRsp cluster_id: Final[t.uint16_t] = 0x0501 name: Final = "IAS Ancillary Control Equipment" ep_attribute: Final = "ias_ace" class AttributeDefs(BaseAttributeDefs): cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): arm: Final = ZCLCommandDef( id=0x00, schema={ "arm_mode": ArmMode, "arm_disarm_code": t.CharacterString, "zone_id": t.uint8_t, }, direction=Direction.Client_to_Server, ) bypass: Final = ZCLCommandDef( id=0x01, schema={ "zones_ids": t.LVList[t.uint8_t], "arm_disarm_code": t.CharacterString, }, direction=Direction.Client_to_Server, ) emergency: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) fire: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Client_to_Server ) panic: Final = ZCLCommandDef( id=0x04, schema={}, direction=Direction.Client_to_Server ) get_zone_id_map: Final = ZCLCommandDef( id=0x05, schema={}, direction=Direction.Client_to_Server ) get_zone_info: Final = ZCLCommandDef( id=0x06, schema={"zone_id": t.uint8_t}, direction=Direction.Client_to_Server ) get_panel_status: Final = ZCLCommandDef( id=0x07, schema={}, direction=Direction.Client_to_Server ) get_bypassed_zone_list: Final = ZCLCommandDef( id=0x08, schema={}, direction=Direction.Client_to_Server ) get_zone_status: Final = ZCLCommandDef( id=0x09, schema={ "starting_zone_id": t.uint8_t, "max_num_zone_ids": t.uint8_t, "zone_status_mask_flag": t.Bool, "zone_status_mask": ZoneStatus, }, direction=Direction.Client_to_Server, ) class ClientCommandDefs(BaseCommandDefs): arm_response: Final = ZCLCommandDef( id=0x00, schema={"arm_notification": ArmNotification}, direction=Direction.Server_to_Client, ) get_zone_id_map_response: Final = ZCLCommandDef( id=0x01, schema={"zone_id_map_sections": t.List[t.bitmap16]}, direction=Direction.Server_to_Client, ) get_zone_info_response: Final = ZCLCommandDef( id=0x02, schema={ "zone_id": t.uint8_t, "zone_type": ZoneType, "ieee": t.EUI64, "zone_label": t.CharacterString, }, direction=Direction.Server_to_Client, ) zone_status_changed: Final = ZCLCommandDef( id=0x03, schema={ "zone_id": t.uint8_t, "zone_status": ZoneStatus, "audible_notification": AudibleNotification, "zone_label": t.CharacterString, }, direction=Direction.Client_to_Server, ) panel_status_changed: Final = ZCLCommandDef( id=0x04, schema={ "panel_status": PanelStatus, "seconds_remaining": t.uint8_t, "audible_notification": AudibleNotification, "alarm_status": AlarmStatus, }, direction=Direction.Client_to_Server, ) panel_status_response: Final = ZCLCommandDef( id=0x05, schema={ "panel_status": PanelStatus, "seconds_remaining": t.uint8_t, "audible_notification": AudibleNotification, "alarm_status": AlarmStatus, }, direction=Direction.Server_to_Client, ) set_bypassed_zone_list: Final = ZCLCommandDef( id=0x06, schema={"zone_ids": t.LVList[t.uint8_t]}, direction=Direction.Client_to_Server, ) bypass_response: Final = ZCLCommandDef( id=0x07, schema={"bypass_results": t.LVList[BypassResponse]}, direction=Direction.Server_to_Client, ) get_zone_status_response: Final = ZCLCommandDef( id=0x08, schema={ "zone_status_complete": t.Bool, "zone_statuses": t.LVList[ZoneStatusRsp], }, direction=Direction.Server_to_Client, ) class Strobe(t.enum8): No_strobe = 0x00 Strobe = 0x01 class _SquawkOrWarningCommand: def __init__(self, value: int = 0) -> None: self.value = t.uint8_t(value) @classmethod def deserialize(cls, data: bytes) -> tuple[_SquawkOrWarningCommand, bytes]: val, data = t.uint8_t.deserialize(data) return cls(val), data def serialize(self) -> bytes: return t.uint8_t(self.value).serialize() def __repr__(self) -> str: return ( f"<{self.__class__.__name__}.mode={self.mode.name} " f"strobe={self.strobe.name} level={self.level.name}: " f"{self.value}>" ) def __eq__(self, other): """Compare to int.""" return self.value == other class StrobeLevel(t.enum8): Low_level_strobe = 0x00 Medium_level_strobe = 0x01 High_level_strobe = 0x02 Very_high_level_strobe = 0x03 class WarningType(_SquawkOrWarningCommand): Strobe = Strobe class SirenLevel(t.enum8): Low_level_sound = 0x00 Medium_level_sound = 0x01 High_level_sound = 0x02 Very_high_level_sound = 0x03 class WarningMode(t.enum8): Stop = 0x00 Burglar = 0x01 Fire = 0x02 Emergency = 0x03 Police_Panic = 0x04 Fire_Panic = 0x05 Emergency_Panic = 0x06 @property def mode(self) -> WarningMode: return self.WarningMode((self.value >> 4) & 0x0F) @mode.setter def mode(self, mode: WarningMode) -> None: self.value = (self.value & 0xF) | (mode << 4) @property def strobe(self) -> Strobe: return self.Strobe((self.value >> 2) & 0x01) @strobe.setter def strobe(self, strobe: Strobe) -> None: self.value = (self.value & 0xF7) | ( (strobe & 0x01) << 2 # type:ignore[operator] ) @property def level(self) -> SirenLevel: return self.SirenLevel(self.value & 0x03) @level.setter def level(self, level: SirenLevel) -> None: self.value = (self.value & 0xFC) | (level & 0x03) class Squawk(_SquawkOrWarningCommand): Strobe = Strobe class SquawkLevel(t.enum8): Low_level_sound = 0x00 Medium_level_sound = 0x01 High_level_sound = 0x02 Very_high_level_sound = 0x03 class SquawkMode(t.enum8): Armed = 0x00 Disarmed = 0x01 @property def mode(self) -> SquawkMode: return self.SquawkMode((self.value >> 4) & 0x0F) @mode.setter def mode(self, mode: SquawkMode) -> None: self.value = (self.value & 0xF) | ((mode & 0x0F) << 4) @property def strobe(self) -> Strobe: return self.Strobe((self.value >> 3) & 0x01) @strobe.setter def strobe(self, strobe: Strobe) -> None: self.value = (self.value & 0xF7) | (strobe << 3) # type:ignore[operator] @property def level(self) -> SquawkLevel: return self.SquawkLevel(self.value & 0x03) @level.setter def level(self, level: SquawkLevel) -> None: self.value = (self.value & 0xFC) | (level & 0x03) class IasWd(Cluster): """The IAS WD cluster provides an interface to the functionality of any Warning Device equipment of the IAS system. Using this cluster, a Zigbee enabled CIE device can access a Zigbee enabled IAS WD device and issue alarm warning indications (siren, strobe lighting, etc.) when a system alarm condition is detected """ StrobeLevel: Final = StrobeLevel Warning: Final = WarningType Squawk: Final = Squawk cluster_id: Final[t.uint16_t] = 0x0502 name: Final = "IAS Warning Device" ep_attribute: Final = "ias_wd" class AttributeDefs(BaseAttributeDefs): max_duration: Final = ZCLAttributeDef( id=0x0000, type=t.uint16_t, access="rw", mandatory=True ) cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR reporting_status: Final = foundation.ZCL_REPORTING_STATUS_ATTR class ServerCommandDefs(BaseCommandDefs): start_warning: Final = ZCLCommandDef( id=0x00, schema={ "warning": WarningType, "warning_duration": t.uint16_t, "strobe_duty_cycle": t.uint8_t, "stobe_level": StrobeLevel, }, direction=Direction.Client_to_Server, ) squawk: Final = ZCLCommandDef( id=0x01, schema={"squawk": Squawk}, direction=Direction.Client_to_Server ) zigpy-0.80.1/zigpy/zcl/clusters/smartenergy.py000066400000000000000000000633071501451476000214660ustar00rootroot00000000000000from __future__ import annotations from typing import Final import zigpy.types as t from zigpy.zcl import Cluster from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, DataType, Direction, ZCLAttributeDef, ZCLCommandDef, ) class Price(Cluster): cluster_id: Final[t.uint16_t] = 0x0700 ep_attribute: Final = "smartenergy_price" class Drlc(Cluster): cluster_id: Final[t.uint16_t] = 0x0701 ep_attribute: Final = "smartenergy_drlc" class RegisteredTier(t.enum8): No_Tier = 0x00 Tier_1 = 0x01 Tier_2 = 0x02 Tier_3 = 0x03 Tier_4 = 0x04 Tier_5 = 0x05 Tier_6 = 0x06 Tier_7 = 0x07 Tier_8 = 0x08 Tier_9 = 0x09 Tier_10 = 0x0A Tier_11 = 0x0B Tier_12 = 0x0C Tier_13 = 0x0D Tier_14 = 0x0E Extended_Tier = 0x0F class MeteringDeviceType(t.enum8): """Metering device type.""" Electric_Metering = 0 Gas_Metering = 1 Water_Metering = 2 Thermal_Metering = 3 # Deprecated Pressure_Metering = 4 Heat_Metering = 5 Cooling_Metering = 6 EUMD_for_metering_electric_vehicle_charging = 7 PV_Generation_Metering = 8 Wind_Turbine_Generation_Metering = 9 Water_Turbine_Generation_Metering = 10 Micro_Generation_Metering = 11 Solar_Hot_Water_Generation_Metering = 12 Electric_Metering_Element_Phase_1 = 13 Electric_Metering_Element_Phase_2 = 14 Electric_Metering_Element_Phase_3 = 15 # 127 + above enum values Mirrored_Electric_Metering = 127 Mirrored_Gas_Metering = 128 Mirrored_Water_Metering = 129 Mirrored_Thermal_Metering = 130 # Deprecated Mirrored_Pressure_Metering = 131 Mirrored_Heat_Metering = 132 Mirrored_Cooling_Metering = 133 Mirrored_EUMD_for_metering_electric_vehicle_charging = 134 Mirrored_PV_Generation_Metering = 135 Mirrored_Wind_Turbine_Generation_Metering = 136 Mirrored_Water_Turbine_Generation_Metering = 137 Mirrored_Micro_Generation_Metering = 138 Mirrored_Solar_Hot_Water_Generation_Metering = 139 Mirrored_Electric_Metering_Element_Phase_1 = 140 Mirrored_Electric_Metering_Element_Phase_2 = 141 Mirrored_Electric_Metering_Element_Phase_3 = 142 class MeteringUnitofMeasure(t.enum8): """Metering unit of measure.""" Kwh_and_Kwh_binary = 0x00 Cubic_Meter_and_Cubic_Meter_per_Hour_binary = 0x01 Cubic_Feet_and_Cubic_Feet_per_Hour_binary = 0x02 Ccf_and_Ccf_per_Hour_binary = 0x03 US_Gallons_and_US_Gallons_per_Hour_binary = 0x04 Imperial_Gallons_and_Imperial_Gallons_per_Hour_binary = 0x05 BTU_and_BTU_per_Hour_binary = 0x06 Liters_and_Liters_per_Hour_binary = 0x07 KPA_gauge_binary = 0x08 KPA_absolute_binary = 0x09 MCF_and_MCF_per_Hour_binary = 0x0A Unitless_binary = 0x0B Mega_Joule_and_Mega_Joule_per_second_binary = 0x0C Kvar_and_Kvarh_binary = 0x0D Kwh_and_Kwh_bcd = 0x80 Cubic_Meter_and_Cubic_Meter_per_Hour_bcd = 0x81 Cubic_Feet_and_Cubic_Feet_per_Hour_bcd = 0x82 Ccf_and_Ccf_per_Hour_bcd = 0x83 US_Gallons_and_US_Gallons_per_Hour_bcd = 0x84 Imperial_Gallons_and_Imperial_Gallons_per_Hour_bcd = 0x85 BTU_and_BTU_per_Hour_bcd = 0x86 Liters_and_Liters_per_Hour_bcd = 0x87 KPA_gauge_bcd = 0x88 KPA_absolute_bcd = 0x89 MCF_and_MCF_per_Hour_bcd = 0x8A Unitless_bcd = 0x8B Mega_Joule_and_Mega_Joule_per_second_bcd = 0x8C Kvar_and_Kvarh_bcd = 0x8D class NumberFormatting(t.IntStruct, t.uint8_t): """Number formatting.""" num_digits_right_of_decimal: t.uint3_t num_digits_left_of_decimal: t.uint4_t suppress_leading_zeros: t.uint1_t class MeteringStatus(t.bitmap8): """Metering status.""" Check_Meter = 0b00000001 Low_Battery = 0b00000010 Tamper_Detect = 0b00000100 Power_Failure = 0b00001000 Power_Quality = 0b00010000 Leak_Detect = 0b00100000 Service_Disconnect_Open = 0b01000000 Reserved = 0b10000000 class Metering(Cluster): RegisteredTier: Final = RegisteredTier MeteringDeviceType: Final = MeteringDeviceType MeteringUnitofMeasure: Final = MeteringUnitofMeasure NumberFormatting: Final = NumberFormatting cluster_id: Final[t.uint16_t] = 0x0702 ep_attribute: Final = "smartenergy_metering" class AttributeDefs(BaseAttributeDefs): current_summ_delivered: Final = ZCLAttributeDef( id=0x0000, type=t.uint48_t, access="r" ) current_summ_received: Final = ZCLAttributeDef( id=0x0001, type=t.uint48_t, access="r" ) current_max_demand_delivered: Final = ZCLAttributeDef( id=0x0002, type=t.uint48_t, access="r" ) current_max_demand_received: Final = ZCLAttributeDef( id=0x0003, type=t.uint48_t, access="r" ) dft_summ: Final = ZCLAttributeDef(id=0x0004, type=t.uint48_t, access="r") daily_freeze_time: Final = ZCLAttributeDef( id=0x0005, type=t.uint16_t, access="r" ) power_factor: Final = ZCLAttributeDef(id=0x0006, type=t.int8s, access="r") reading_snapshot_time: Final = ZCLAttributeDef( id=0x0007, type=t.UTCTime, access="r" ) current_max_demand_delivered_time: Final = ZCLAttributeDef( id=0x0008, type=t.UTCTime, access="r" ) current_max_demand_received_time: Final = ZCLAttributeDef( id=0x0009, type=t.UTCTime, access="r" ) default_update_period: Final = ZCLAttributeDef( id=0x000A, type=t.uint8_t, access="r" ) fast_poll_update_period: Final = ZCLAttributeDef( id=0x000B, type=t.uint8_t, access="r" ) current_block_period_consumption_delivered: Final = ZCLAttributeDef( id=0x000C, type=t.uint48_t, access="r" ) daily_consumption_target: Final = ZCLAttributeDef( id=0x000D, type=t.uint24_t, access="r" ) current_block: Final = ZCLAttributeDef(id=0x000E, type=t.enum8, access="r") profile_interval_period: Final = ZCLAttributeDef( id=0x000F, type=t.enum8, access="r" ) # 0x0010: ('interval_read_reporting_period', UNKNOWN), # Deprecated preset_reading_time: Final = ZCLAttributeDef( id=0x0011, type=t.uint16_t, access="r" ) volume_per_report: Final = ZCLAttributeDef( id=0x0012, type=t.uint16_t, access="r" ) flow_restriction: Final = ZCLAttributeDef(id=0x0013, type=t.uint8_t, access="r") supply_status: Final = ZCLAttributeDef(id=0x0014, type=t.enum8, access="r") current_in_energy_carrier_summ: Final = ZCLAttributeDef( id=0x0015, type=t.uint48_t, access="r" ) current_out_energy_carrier_summ: Final = ZCLAttributeDef( id=0x0016, type=t.uint48_t, access="r" ) inlet_temperature: Final = ZCLAttributeDef(id=0x0017, type=t.int24s, access="r") outlet_temperature: Final = ZCLAttributeDef( id=0x0018, type=t.int24s, access="r" ) control_temperature: Final = ZCLAttributeDef( id=0x0019, type=t.int24s, access="r" ) current_in_energy_carrier_demand: Final = ZCLAttributeDef( id=0x001A, type=t.int24s, access="r" ) current_out_energy_carrier_demand: Final = ZCLAttributeDef( id=0x001B, type=t.int24s, access="r" ) current_block_period_consumption_received: Final = ZCLAttributeDef( id=0x001D, type=t.uint48_t, access="r" ) current_block_received: Final = ZCLAttributeDef( id=0x001E, type=t.uint48_t, access="r" ) dft_summation_received: Final = ZCLAttributeDef( id=0x001F, type=t.uint48_t, access="r" ) active_register_tier_delivered: Final = ZCLAttributeDef( id=0x0020, type=RegisteredTier, access="r" ) active_register_tier_received: Final = ZCLAttributeDef( id=0x0021, type=RegisteredTier, access="r" ) last_block_switch_time: Final = ZCLAttributeDef( id=0x0022, type=t.UTCTime, access="r" ) # 0x0100: ('change_reporting_profile', UNKNOWN), current_tier1_summ_delivered: Final = ZCLAttributeDef( id=0x0100, type=t.uint48_t, access="r" ) current_tier1_summ_received: Final = ZCLAttributeDef( id=0x0101, type=t.uint48_t, access="r" ) current_tier2_summ_delivered: Final = ZCLAttributeDef( id=0x0102, type=t.uint48_t, access="r" ) current_tier2_summ_received: Final = ZCLAttributeDef( id=0x0103, type=t.uint48_t, access="r" ) current_tier3_summ_delivered: Final = ZCLAttributeDef( id=0x0104, type=t.uint48_t, access="r" ) current_tier3_summ_received: Final = ZCLAttributeDef( id=0x0105, type=t.uint48_t, access="r" ) current_tier4_summ_delivered: Final = ZCLAttributeDef( id=0x0106, type=t.uint48_t, access="r" ) current_tier4_summ_received: Final = ZCLAttributeDef( id=0x0107, type=t.uint48_t, access="r" ) current_tier5_summ_delivered: Final = ZCLAttributeDef( id=0x0108, type=t.uint48_t, access="r" ) current_tier5_summ_received: Final = ZCLAttributeDef( id=0x0109, type=t.uint48_t, access="r" ) current_tier6_summ_delivered: Final = ZCLAttributeDef( id=0x010A, type=t.uint48_t, access="r" ) current_tier6_summ_received: Final = ZCLAttributeDef( id=0x010B, type=t.uint48_t, access="r" ) current_tier7_summ_delivered: Final = ZCLAttributeDef( id=0x010C, type=t.uint48_t, access="r" ) current_tier7_summ_received: Final = ZCLAttributeDef( id=0x010D, type=t.uint48_t, access="r" ) current_tier8_summ_delivered: Final = ZCLAttributeDef( id=0x010E, type=t.uint48_t, access="r" ) current_tier8_summ_received: Final = ZCLAttributeDef( id=0x010F, type=t.uint48_t, access="r" ) current_tier9_summ_delivered: Final = ZCLAttributeDef( id=0x0110, type=t.uint48_t, access="r" ) current_tier9_summ_received: Final = ZCLAttributeDef( id=0x0111, type=t.uint48_t, access="r" ) current_tier10_summ_delivered: Final = ZCLAttributeDef( id=0x0112, type=t.uint48_t, access="r" ) current_tier10_summ_received: Final = ZCLAttributeDef( id=0x0113, type=t.uint48_t, access="r" ) current_tier11_summ_delivered: Final = ZCLAttributeDef( id=0x0114, type=t.uint48_t, access="r" ) current_tier11_summ_received: Final = ZCLAttributeDef( id=0x0115, type=t.uint48_t, access="r" ) current_tier12_summ_delivered: Final = ZCLAttributeDef( id=0x0116, type=t.uint48_t, access="r" ) current_tier12_summ_received: Final = ZCLAttributeDef( id=0x0117, type=t.uint48_t, access="r" ) current_tier13_summ_delivered: Final = ZCLAttributeDef( id=0x0118, type=t.uint48_t, access="r" ) current_tier13_summ_received: Final = ZCLAttributeDef( id=0x0119, type=t.uint48_t, access="r" ) current_tier14_summ_delivered: Final = ZCLAttributeDef( id=0x011A, type=t.uint48_t, access="r" ) current_tier14_summ_received: Final = ZCLAttributeDef( id=0x011B, type=t.uint48_t, access="r" ) current_tier15_summ_delivered: Final = ZCLAttributeDef( id=0x011C, type=t.uint48_t, access="r" ) current_tier15_summ_received: Final = ZCLAttributeDef( id=0x011D, type=t.uint48_t, access="r" ) status: Final = ZCLAttributeDef(id=0x0200, type=MeteringStatus, access="r") remaining_battery_life: Final = ZCLAttributeDef( id=0x0201, type=t.uint8_t, access="r" ) hours_in_operation: Final = ZCLAttributeDef( id=0x0202, type=t.uint24_t, access="r" ) hours_in_fault: Final = ZCLAttributeDef(id=0x0203, type=t.uint24_t, access="r") extended_status: Final = ZCLAttributeDef(id=0x0204, type=t.bitmap64, access="r") remaining_battery_life_days: Final = ZCLAttributeDef( id=0x0205, type=t.uint16_t, access="r" ) current_meter_id: Final = ZCLAttributeDef(id=0x0206, type=t.LVBytes, access="r") iambient_consumption_indicator: Final = ZCLAttributeDef( id=0x0207, type=t.enum8, access="r" ) unit_of_measure: Final = ZCLAttributeDef( id=0x0300, type=MeteringUnitofMeasure, access="r" ) multiplier: Final = ZCLAttributeDef(id=0x0301, type=t.uint24_t, access="r") divisor: Final = ZCLAttributeDef(id=0x0302, type=t.uint24_t, access="r") # This attribute shall be used against the following attributes: # • CurrentSummationDelivered # • CurrentSummationReceived # • SummationDeliveredPerReport # • TOU Information attributes # • DFTSummation # • Block Information attributes summation_formatting: Final = ZCLAttributeDef( id=0x0303, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) # This attribute shall be used against the following attributes: # • CurrentMaxDemandDelivered # • CurrentMaxDemandReceived # • InstantaneousDemand demand_formatting: Final = ZCLAttributeDef( id=0x0304, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) # This attribute shall be used against the following attributes: # • CurrentDayConsumptionDelivered # • CurrentDayConsumptionReceived # • PreviousDayConsumptionDelivered # • PreviousDayConsumptionReceived # • CurrentPartialProfileIntervalValue # • Intervals # • DailyConsumptionTarget # • CurrentDayConsumptionDelivered # • CurrentDayConsumptionReceived # • PreviousDayNConsumptionDelivered # • PreviousDayNConsumptionReceived # • CurrentWeekConsumptionDelivered # • CurrentWeekConsumptionReceived # • PreviousWeekNConsumptionDelivered # • PreviousWeekNConsumptionReceived # • CurrentMonthConsumptionDelivered # • CurrentMonthConsumptionReceived # • PreviousMonthNConsumptionDelivered # • PreviousMonthNConsumptionReceived historical_consumption_formatting: Final = ZCLAttributeDef( id=0x0305, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) metering_device_type: Final = ZCLAttributeDef( id=0x0306, type=MeteringDeviceType, # Note that these values represent an Enumeration, and not an 8-bit bitmap # as indicated in the attribute description. For backwards compatibility # reasons, the data type has not been changed, though the data itself should # be treated like an enum zcl_type=DataType.map8, access="r", ) site_id: Final = ZCLAttributeDef( id=0x0307, type=t.LimitedLVBytes(32), access="r" ) meter_serial_number: Final = ZCLAttributeDef( id=0x0308, type=t.LimitedLVBytes(24), access="r" ) energy_carrier_unit_of_measure: Final = ZCLAttributeDef( id=0x0309, type=MeteringUnitofMeasure, access="r" ) energy_carrier_summation_formatting: Final = ZCLAttributeDef( id=0x030A, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) energy_carrier_demand_formatting: Final = ZCLAttributeDef( id=0x030B, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) temperature_unit_of_measure: Final = ZCLAttributeDef( id=0x030C, type=MeteringUnitofMeasure, access="r" ) # This attribute shall be used in relation with the following attributes: # • InletTemperature # • OutletTemperature # • ControlTemperature temperature_formatting: Final = ZCLAttributeDef( id=0x030D, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) module_serial_number: Final = ZCLAttributeDef( id=0x030E, type=t.LimitedLVBytes(24), access="r" ) operating_tariff_label_delivered: Final = ZCLAttributeDef( id=0x030F, type=t.LimitedLVBytes(24), access="r" ) operating_tariff_label_received: Final = ZCLAttributeDef( id=0x0310, type=t.LimitedLVBytes(24), access="r" ) customer_id_number: Final = ZCLAttributeDef( id=0x0311, type=t.LimitedLVBytes(24), access="r" ) alternative_unit_of_measure: Final = ZCLAttributeDef( id=0x0312, type=MeteringUnitofMeasure, access="r" ) # This attribute shall be used against the following attribute: # • AlternativeInstantaneousDemand alternative_demand_formatting: Final = ZCLAttributeDef( id=0x0313, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) # This attribute shall be used against the following attributes: # • CurrentDayAlternativeConsumptionDelivered # • CurrentDayAlternativeConsumptionReceived # • PreviousDayAlternativeConsumptionDelivered # • PreviousDayAlternativeConsumptionReceived # • CurrentAlternativePartialProfileIntervalValue # • PreviousDayNAlternativeConsumptionDelivered # • PreviousDayNAlternativeConsumptionReceived # • CurrentWeekAlternativeConsumptionDelivered # • CurrentWeekAlternativeConsumptionReceived # • PreviousWeekNAlternativeConsumptionDelivered # • PreviousWeekNAlternativeConsumptionReceived # • CurrentMonthAlternativeConsumptionDelivered # • CurrentMonthAlternativeConsumptionReceived # • PreviousMonthNAlternativeConsumptionDelivered # • PreviousMonthNAlternativeConsumptionReceived alternative_consumption_formatting: Final = ZCLAttributeDef( id=0x0314, zcl_type=DataType.map8, type=NumberFormatting, access="r" ) instantaneous_demand: Final = ZCLAttributeDef( id=0x0400, type=t.int24s, access="r" ) currentday_consumption_delivered: Final = ZCLAttributeDef( id=0x0401, type=t.uint24_t, access="r" ) currentday_consumption_received: Final = ZCLAttributeDef( id=0x0402, type=t.uint24_t, access="r" ) previousday_consumption_delivered: Final = ZCLAttributeDef( id=0x0403, type=t.uint24_t, access="r" ) previousday_consumption_received: Final = ZCLAttributeDef( id=0x0404, type=t.uint24_t, access="r" ) cur_part_profile_int_start_time_delivered: Final = ZCLAttributeDef( id=0x0405, type=t.uint32_t, access="r" ) cur_part_profile_int_start_time_received: Final = ZCLAttributeDef( id=0x0406, type=t.uint32_t, access="r" ) cur_part_profile_int_value_delivered: Final = ZCLAttributeDef( id=0x0407, type=t.uint24_t, access="r" ) cur_part_profile_int_value_received: Final = ZCLAttributeDef( id=0x0408, type=t.uint24_t, access="r" ) current_day_max_pressure: Final = ZCLAttributeDef( id=0x0409, type=t.uint48_t, access="r" ) current_day_min_pressure: Final = ZCLAttributeDef( id=0x040A, type=t.uint48_t, access="r" ) previous_day_max_pressure: Final = ZCLAttributeDef( id=0x040B, type=t.uint48_t, access="r" ) previous_day_min_pressure: Final = ZCLAttributeDef( id=0x040C, type=t.uint48_t, access="r" ) current_day_max_demand: Final = ZCLAttributeDef( id=0x040D, type=t.int24s, access="r" ) previous_day_max_demand: Final = ZCLAttributeDef( id=0x040E, type=t.int24s, access="r" ) current_month_max_demand: Final = ZCLAttributeDef( id=0x040F, type=t.int24s, access="r" ) current_year_max_demand: Final = ZCLAttributeDef( id=0x0410, type=t.int24s, access="r" ) currentday_max_energy_carr_demand: Final = ZCLAttributeDef( id=0x0411, type=t.int24s, access="r" ) previousday_max_energy_carr_demand: Final = ZCLAttributeDef( id=0x0412, type=t.int24s, access="r" ) cur_month_max_energy_carr_demand: Final = ZCLAttributeDef( id=0x0413, type=t.int24s, access="r" ) cur_month_min_energy_carr_demand: Final = ZCLAttributeDef( id=0x0414, type=t.int24s, access="r" ) cur_year_max_energy_carr_demand: Final = ZCLAttributeDef( id=0x0415, type=t.int24s, access="r" ) cur_year_min_energy_carr_demand: Final = ZCLAttributeDef( id=0x0416, type=t.int24s, access="r" ) max_number_of_periods_delivered: Final = ZCLAttributeDef( id=0x0500, type=t.uint8_t, access="r" ) current_demand_delivered: Final = ZCLAttributeDef( id=0x0600, type=t.uint24_t, access="r" ) demand_limit: Final = ZCLAttributeDef(id=0x0601, type=t.uint24_t, access="r") demand_integration_period: Final = ZCLAttributeDef( id=0x0602, type=t.uint8_t, access="r" ) number_of_demand_subintervals: Final = ZCLAttributeDef( id=0x0603, type=t.uint8_t, access="r" ) demand_limit_arm_duration: Final = ZCLAttributeDef( id=0x0604, type=t.uint16_t, access="r" ) generic_alarm_mask: Final = ZCLAttributeDef( id=0x0800, type=t.bitmap16, access="r" ) electricity_alarm_mask: Final = ZCLAttributeDef( id=0x0801, type=t.bitmap32, access="r" ) gen_flow_pressure_alarm_mask: Final = ZCLAttributeDef( id=0x0802, type=t.bitmap16, access="r" ) water_specific_alarm_mask: Final = ZCLAttributeDef( id=0x0803, type=t.bitmap16, access="r" ) heat_cool_specific_alarm_mask: Final = ZCLAttributeDef( id=0x0804, type=t.bitmap16, access="r" ) gas_specific_alarm_mask: Final = ZCLAttributeDef( id=0x0805, type=t.bitmap16, access="r" ) extended_generic_alarm_mask: Final = ZCLAttributeDef( id=0x0806, type=t.bitmap48, access="r" ) manufacture_alarm_mask: Final = ZCLAttributeDef( id=0x0807, type=t.bitmap16, access="r" ) bill_to_date: Final = ZCLAttributeDef(id=0x0A00, type=t.uint32_t, access="r") bill_to_date_time_stamp: Final = ZCLAttributeDef( id=0x0A01, type=t.uint32_t, access="r" ) projected_bill: Final = ZCLAttributeDef(id=0x0A02, type=t.uint32_t, access="r") projected_bill_time_stamp: Final = ZCLAttributeDef( id=0x0A03, type=t.uint32_t, access="r" ) class ServerCommandDefs(BaseCommandDefs): get_profile: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Client_to_Server ) req_mirror: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Client_to_Server ) mirror_rem: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Client_to_Server ) req_fast_poll_mode: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Client_to_Server ) get_snapshot: Final = ZCLCommandDef( id=0x04, schema={}, direction=Direction.Client_to_Server ) take_snapshot: Final = ZCLCommandDef( id=0x05, schema={}, direction=Direction.Client_to_Server ) mirror_report_attr_response: Final = ZCLCommandDef( id=0x06, schema={}, direction=Direction.Server_to_Client ) class ClientCommandDefs(BaseCommandDefs): get_profile_response: Final = ZCLCommandDef( id=0x00, schema={}, direction=Direction.Server_to_Client ) req_mirror_response: Final = ZCLCommandDef( id=0x01, schema={}, direction=Direction.Server_to_Client ) mirror_rem_response: Final = ZCLCommandDef( id=0x02, schema={}, direction=Direction.Server_to_Client ) req_fast_poll_mode_response: Final = ZCLCommandDef( id=0x03, schema={}, direction=Direction.Server_to_Client ) get_snapshot_response: Final = ZCLCommandDef( id=0x04, schema={}, direction=Direction.Server_to_Client ) class Messaging(Cluster): cluster_id: Final[t.uint16_t] = 0x0703 ep_attribute: Final = "smartenergy_messaging" class Tunneling(Cluster): cluster_id: Final[t.uint16_t] = 0x0704 ep_attribute: Final = "smartenergy_tunneling" class Prepayment(Cluster): cluster_id: Final[t.uint16_t] = 0x0705 ep_attribute: Final = "smartenergy_prepayment" class EnergyManagement(Cluster): cluster_id: Final[t.uint16_t] = 0x0706 ep_attribute: Final = "smartenergy_energy_management" class Calendar(Cluster): cluster_id: Final[t.uint16_t] = 0x0707 ep_attribute: Final = "smartenergy_calendar" class DeviceManagement(Cluster): cluster_id: Final[t.uint16_t] = 0x0708 ep_attribute: Final = "smartenergy_device_management" class Events(Cluster): cluster_id: Final[t.uint16_t] = 0x0709 ep_attribute: Final = "smartenergy_events" class MduPairing(Cluster): cluster_id: Final[t.uint16_t] = 0x070A ep_attribute: Final = "smartenergy_mdu_pairing" class KeyEstablishment(Cluster): cluster_id: Final[t.uint16_t] = 0x0800 ep_attribute: Final = "smartenergy_key_establishment" zigpy-0.80.1/zigpy/zcl/foundation.py000066400000000000000000001224771501451476000174340ustar00rootroot00000000000000from __future__ import annotations import dataclasses import enum import functools import keyword import logging import typing from typing_extensions import Self import zigpy.types as t _LOGGER = logging.getLogger(__name__) def _hex_uint16_repr(v: int) -> str: return t.uint16_t(v)._hex_repr() def ensure_valid_name(name: str | None) -> None: """Ensures that the name of an attribute or command is valid.""" if name is not None and not name.isidentifier(): raise ValueError(f"{name!r} is not a valid identifier name.") class Status(t.enum8): SUCCESS = 0x00 # Operation was successful. FAILURE = 0x01 # Operation was not successful NOT_AUTHORIZED = 0x7E # The sender of the command does not have RESERVED_FIELD_NOT_ZERO = 0x7F # A reserved field/subfield/bit contains a MALFORMED_COMMAND = 0x80 # The command appears to contain the wrong UNSUP_CLUSTER_COMMAND = 0x81 # The specified cluster command is not UNSUP_GENERAL_COMMAND = 0x82 # The specified general ZCL command is not UNSUP_MANUF_CLUSTER_COMMAND = 0x83 # A manufacturer specific unicast, UNSUP_MANUF_GENERAL_COMMAND = 0x84 # A manufacturer specific unicast, ZCL INVALID_FIELD = 0x85 # At least one field of the command contains an UNSUPPORTED_ATTRIBUTE = 0x86 # The specified attribute does not exist on INVALID_VALUE = 0x87 # Out of range error, or set to a reserved value. READ_ONLY = 0x88 # Attempt to write a read only attribute. INSUFFICIENT_SPACE = 0x89 # An operation (e.g. an attempt to create an DUPLICATE_EXISTS = 0x8A # An attempt to create an entry in a table failed NOT_FOUND = 0x8B # The requested information (e.g. table entry) UNREPORTABLE_ATTRIBUTE = 0x8C # Periodic reports cannot be issued for this INVALID_DATA_TYPE = 0x8D # The data type given for an attribute is INVALID_SELECTOR = 0x8E # The selector for an attribute is incorrect. WRITE_ONLY = 0x8F # A request has been made to read an attribute INCONSISTENT_STARTUP_STATE = 0x90 # Setting the requested values would put DEFINED_OUT_OF_BAND = 0x91 # An attempt has been made to write an INCONSISTENT = ( 0x92 # The supplied values (e.g., contents of table cells) are inconsistent ) ACTION_DENIED = 0x93 # The credentials presented by the device sending the TIMEOUT = 0x94 # The exchange was aborted due to excessive response time ABORT = 0x95 # Failed case when a client or a server decides to abort the upgrade process INVALID_IMAGE = 0x96 # Invalid OTA upgrade image (ex. failed signature WAIT_FOR_DATA = 0x97 # Server does not have data block available yet NO_IMAGE_AVAILABLE = 0x98 # No OTA upgrade image available for a particular client REQUIRE_MORE_IMAGE = 0x99 # The client still requires more OTA upgrade image NOTIFICATION_PENDING = 0x9A # The command has been received and is being processed HARDWARE_FAILURE = 0xC0 # An operation was unsuccessful due to a SOFTWARE_FAILURE = 0xC1 # An operation was unsuccessful due to a CALIBRATION_ERROR = 0xC2 # An error occurred during calibration UNSUPPORTED_CLUSTER = 0xC3 # The cluster is not supported @classmethod def _missing_(cls, value): chained = t.APSStatus(value) status = cls._member_type_.__new__(cls, chained.value) status._name_ = chained.name status._value_ = value return status class DataClass(enum.Enum): Null = 0 Analog = 1 Discrete = 2 Composite = 3 # TODO: Backwards compatibility, remove later Null = DataClass.Null Analog = DataClass.Analog Discrete = DataClass.Discrete Composite = DataClass.Composite class Unknown(t.NoData): pass @dataclasses.dataclass() class TypeValue: type: t.uint8_t = dataclasses.field(default=None) value: typing.Any = dataclasses.field(default=None) def __init__(self, type: t.uint8_t | None = None, value: typing.Any = None) -> None: # "Copy constructor" if type is not None and value is None and isinstance(type, self.__class__): other = type type = other.type # noqa: A001 value = other.value self.type = type self.value = value def serialize(self) -> bytes: return self.type.to_bytes(1, "little") + self.value.serialize() @classmethod def deserialize(cls, data: bytes) -> tuple[TypeValue, bytes]: data_type, data = t.uint8_t.deserialize(data) python_type = DataType.from_type_id(data_type).python_type value, data = python_type.deserialize(data) return cls(type=data_type, value=value), data def __repr__(self) -> str: return ( f"{type(self).__name__}(" f"type={type(self.value).__name__}, value={self.value!r}" f")" ) class TypedCollection(TypeValue): @classmethod def deserialize(cls, data): data_type, data = t.uint8_t.deserialize(data) python_type = DataType.from_type_id(data_type).python_type values, data = t.LVList[python_type, t.uint16_t].deserialize(data) return cls(type=data_type, value=values), data class Array(TypedCollection): pass class Bag(TypedCollection): pass class Set(TypedCollection): pass # ToDo: Make this a real set? class ZCLStructure(t.LVList, item_type=TypeValue, length_type=t.uint16_t): """ZCL Structure data type.""" class DataTypeId(t.enum8): unk = 0xFF nodata = 0x00 data8 = 0x08 data16 = 0x09 data24 = 0x0A data32 = 0x0B data40 = 0x0C data48 = 0x0D data56 = 0x0E data64 = 0x0F bool_ = 0x10 map8 = 0x18 map16 = 0x19 map24 = 0x1A map32 = 0x1B map40 = 0x1C map48 = 0x1D map56 = 0x1E map64 = 0x1F uint8 = 0x20 uint16 = 0x21 uint24 = 0x22 uint32 = 0x23 uint40 = 0x24 uint48 = 0x25 uint56 = 0x26 uint64 = 0x27 int8 = 0x28 int16 = 0x29 int24 = 0x2A int32 = 0x2B int40 = 0x2C int48 = 0x2D int56 = 0x2E int64 = 0x2F enum8 = 0x30 enum16 = 0x31 semi = 0x38 single = 0x39 double = 0x3A octstr = 0x41 string = 0x42 octstr16 = 0x43 string16 = 0x44 array = 0x48 struct = 0x4C set = 0x50 bag = 0x51 ToD = 0xE0 date = 0xE1 UTC = 0xE2 clusterId = 0xE8 # noqa: N815 attribId = 0xE9 # noqa: N815 bacOID = 0xEA # noqa: N815 EUI64 = 0xF0 key128 = 0xF1 @dataclasses.dataclass(frozen=True) class DataTypeInfo: type_id: DataTypeId python_type: type type_class: DataClass description: str non_value: typing.Any | None class DataType(DataTypeInfo, enum.Enum): unk = ( DataTypeId.unk, Unknown, DataClass.Null, "Unknown", None, ) nodata = ( DataTypeId.nodata, t.NoData, DataClass.Null, "No data", None, ) data8 = ( DataTypeId.data8, t.data8, DataClass.Discrete, "General", None, ) data16 = ( DataTypeId.data16, t.data16, DataClass.Discrete, "General", None, ) data24 = ( DataTypeId.data24, t.data24, DataClass.Discrete, "General", None, ) data32 = ( DataTypeId.data32, t.data32, DataClass.Discrete, "General", None, ) data40 = ( DataTypeId.data40, t.data40, DataClass.Discrete, "General", None, ) data48 = ( DataTypeId.data48, t.data48, DataClass.Discrete, "General", None, ) data56 = ( DataTypeId.data56, t.data56, DataClass.Discrete, "General", None, ) data64 = ( DataTypeId.data64, t.data64, DataClass.Discrete, "General", None, ) bool_ = ( DataTypeId.bool_, t.Bool, DataClass.Discrete, "Boolean", t.Bool(0xFF), ) map8 = ( DataTypeId.map8, t.bitmap8, DataClass.Discrete, "Bitmap", None, ) map16 = ( DataTypeId.map16, t.bitmap16, DataClass.Discrete, "Bitmap", None, ) map24 = ( DataTypeId.map24, t.bitmap24, DataClass.Discrete, "Bitmap", None, ) map32 = ( DataTypeId.map32, t.bitmap32, DataClass.Discrete, "Bitmap", None, ) map40 = ( DataTypeId.map40, t.bitmap40, DataClass.Discrete, "Bitmap", None, ) map48 = ( DataTypeId.map48, t.bitmap48, DataClass.Discrete, "Bitmap", None, ) map56 = ( DataTypeId.map56, t.bitmap56, DataClass.Discrete, "Bitmap", None, ) map64 = ( DataTypeId.map64, t.bitmap64, DataClass.Discrete, "Bitmap", None, ) uint8 = ( DataTypeId.uint8, t.uint8_t, DataClass.Analog, "Unsigned 8-bit integer", t.uint8_t(0xFF), ) uint16 = ( DataTypeId.uint16, t.uint16_t, DataClass.Analog, "Unsigned 16-bit integer", t.uint16_t(0xFFFF), ) uint24 = ( DataTypeId.uint24, t.uint24_t, DataClass.Analog, "Unsigned 24-bit integer", t.uint24_t(0xFFFFFF), ) uint32 = ( DataTypeId.uint32, t.uint32_t, DataClass.Analog, "Unsigned 32-bit integer", t.uint32_t(0xFFFFFFFF), ) uint40 = ( DataTypeId.uint40, t.uint40_t, DataClass.Analog, "Unsigned 40-bit integer", t.uint40_t(0xFFFFFFFFFF), ) uint48 = ( DataTypeId.uint48, t.uint48_t, DataClass.Analog, "Unsigned 48-bit integer", t.uint48_t(0xFFFFFFFFFFFF), ) uint56 = ( DataTypeId.uint56, t.uint56_t, DataClass.Analog, "Unsigned 56-bit integer", t.uint56_t(0xFFFFFFFFFFFFFF), ) uint64 = ( DataTypeId.uint64, t.uint64_t, DataClass.Analog, "Unsigned 64-bit integer", t.uint64_t(0xFFFFFFFFFFFFFF), ) int8 = ( DataTypeId.int8, t.int8s, DataClass.Analog, "Signed 8-bit integer", t.int8s(-0x80), ) int16 = ( DataTypeId.int16, t.int16s, DataClass.Analog, "Signed 16-bit integer", t.int16s(-0x8000), ) int24 = ( DataTypeId.int24, t.int24s, DataClass.Analog, "Signed 24-bit integer", t.int24s(-0x800000), ) int32 = ( DataTypeId.int32, t.int32s, DataClass.Analog, "Signed 32-bit integer", t.int32s(-0x80000000), ) int40 = ( DataTypeId.int40, t.int40s, DataClass.Analog, "Signed 40-bit integer", t.int40s(-0x8000000000), ) int48 = ( DataTypeId.int48, t.int48s, DataClass.Analog, "Signed 48-bit integer", t.int48s(-0x800000000000), ) int56 = ( DataTypeId.int56, t.int56s, DataClass.Analog, "Signed 56-bit integer", t.int56s(-0x80000000000000), ) int64 = ( DataTypeId.int64, t.int64s, DataClass.Analog, "Signed 64-bit integer", t.int64s(-0x80000000000000), ) enum8 = ( DataTypeId.enum8, t.enum8, DataClass.Discrete, "8-bit enumeration", t.enum8(0xFF), ) enum16 = ( DataTypeId.enum16, t.enum16, DataClass.Discrete, "16-bit enumeration", t.enum16(0xFF), ) semi = ( DataTypeId.semi, t.Half, DataClass.Analog, "Semi-precision", t.Half(float("nan")), ) single = ( DataTypeId.single, t.Single, DataClass.Analog, "Single precision", t.Single(float("nan")), ) double = ( DataTypeId.double, t.Double, DataClass.Analog, "Double precision", t.Double(float("nan")), ) octstr = ( DataTypeId.octstr, t.LVBytes, DataClass.Discrete, "Octet string", None, ) string = ( DataTypeId.string, t.CharacterString, DataClass.Discrete, "Character string", None, ) octstr16 = ( DataTypeId.octstr16, t.LongOctetString, DataClass.Discrete, "Long octet string", None, ) string16 = ( DataTypeId.string16, t.LongCharacterString, DataClass.Discrete, "Long character string", None, ) array = ( DataTypeId.array, Array, DataClass.Discrete, "Array", None, ) struct = ( DataTypeId.struct, ZCLStructure, DataClass.Discrete, "Structure", None, ) set = ( DataTypeId.set, Set, DataClass.Discrete, "Set", None, ) bag = ( DataTypeId.bag, Bag, DataClass.Discrete, "Bag", None, ) ToD = ( DataTypeId.ToD, t.TimeOfDay, DataClass.Analog, "Time of day", t.TimeOfDay(hours=0xFF, minutes=0xFF, seconds=0xFF, hundredths=0xFF), ) date = ( DataTypeId.date, t.Date, DataClass.Analog, "Date", t.Date(years_since_1900=0xFF, month=0xFF, day=0xFF, day_of_week=0xFF), ) UTC = ( DataTypeId.UTC, t.UTCTime, DataClass.Analog, "UTCTime", t.UTCTime(0xFFFFFFFF), ) clusterId = ( # noqa: N815 DataTypeId.clusterId, t.ClusterId, DataClass.Discrete, "Cluster ID", t.ClusterId(0xFFFF), ) attribId = ( # noqa: N815 DataTypeId.attribId, t.AttributeId, DataClass.Discrete, "Attribute ID", t.AttributeId(0xFFFF), ) bacOID = ( # noqa: N815 DataTypeId.bacOID, t.BACNetOid, DataClass.Discrete, "BACNet OID", t.BACNetOid(0xFFFFFFFF), ) EUI64 = ( DataTypeId.EUI64, t.EUI64, DataClass.Discrete, "IEEE address", t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"), ) key128 = ( DataTypeId.key128, t.KeyData, DataClass.Discrete, "128-bit security key", t.KeyData.convert("FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF"), ) @classmethod @functools.cache def _python_type_index(cls: type[Self]) -> dict[type, Self]: # noqa: N805 return {d.python_type: d for d in cls} @classmethod def from_python_type(cls: type[Self], python_type: type) -> Self: """Return Zigbee Datatype ID for a give python type.""" python_type_index = cls._python_type_index() # We return the most specific parent class for parent_cls in python_type.__mro__: if parent_cls in python_type_index: return python_type_index[parent_cls] return cls.unk @classmethod @functools.cache def _data_type_index(cls: type[Self]) -> dict[type, Self]: # noqa: N805 return {d.type_id: d for d in cls} @classmethod def from_type_id(cls: type[Self], type_id: DataTypeId) -> Self: return cls._data_type_index()[type_id] @dataclasses.dataclass() class ReadAttributeRecord: """Read Attribute Record.""" attrid: t.uint16_t status: Status value: TypeValue | Array | Bag | Set | None def __init__( self, attrid: t.uint16_t | Self = t.uint16_t(0x0000), status: Status = Status.SUCCESS, value: TypeValue | Array | Bag | Set | None = None, ) -> None: if isinstance(attrid, self.__class__): # "Copy constructor" self.attrid = attrid.attrid self.status = attrid.status self.value = attrid.value return self.attrid = t.uint16_t(attrid) self.status = Status(status) self.value = value @classmethod def deserialize(cls, data: bytes) -> tuple[Self, bytes]: attrid, data = t.uint16_t.deserialize(data) status, data = Status.deserialize(data) value = None if status == Status.SUCCESS: type_id, data = DataTypeId.deserialize(data) # Arrays, Sets, and Bags are treated differently if type_id in (DataTypeId.array, DataTypeId.set, DataTypeId.bag): value, data = DataType.from_type_id(type_id).python_type.deserialize( data ) else: value, data = TypeValue.deserialize(type_id.serialize() + data) return cls(attrid=attrid, status=status, value=value), data def serialize(self) -> bytes: data = self.attrid.serialize() data += self.status.serialize() if self.status == Status.SUCCESS: assert self.value is not None if isinstance(self.value, (Array, Set, Bag)): data += ( DataType.from_python_type(type(self.value)).type_id.serialize() + self.value.serialize() ) else: data += self.value.serialize() return data class Attribute(t.Struct): attrid: t.uint16_t = t.StructField(repr=_hex_uint16_repr) value: TypeValue class WriteAttributesStatusRecord(t.Struct): status: Status attrid: t.uint16_t = t.StructField( requires=lambda s: s.status != Status.SUCCESS, repr=_hex_uint16_repr ) class WriteAttributesResponse(list): """Write Attributes response list. Response to Write Attributes request should contain only success status, in case when all attributes were successfully written or list of status + attr_id records for all failed writes. """ @classmethod def deserialize(cls, data: bytes) -> tuple[WriteAttributesResponse, bytes]: record, data = WriteAttributesStatusRecord.deserialize(data) r = cls([record]) if record.status == Status.SUCCESS: return r, data while len(data) >= 3: record, data = WriteAttributesStatusRecord.deserialize(data) r.append(record) return r, data def serialize(self): failed = [record for record in self if record.status != Status.SUCCESS] if failed: return b"".join( [WriteAttributesStatusRecord(i).serialize() for i in failed] ) return Status.SUCCESS.serialize() class ReportingDirection(t.enum8): SendReports = 0x00 ReceiveReports = 0x01 class AttributeReportingStatus(t.enum8): Pending = 0x00 Attribute_Reporting_Complete = 0x01 class AttributeReportingConfig: def __init__(self, other: AttributeReportingConfig | None = None) -> None: if isinstance(other, self.__class__): self.direction: ReportingDirection = other.direction self.attrid: t.uint16_t = other.attrid if self.direction == ReportingDirection.ReceiveReports: self.timeout: int = other.timeout return self.datatype: DataTypeId = other.datatype self.min_interval: int = other.min_interval self.max_interval: int = other.max_interval self.reportable_change: int = other.reportable_change def serialize(self, *, _only_dir_and_attrid: bool = False) -> bytes: r = ReportingDirection(self.direction).serialize() r += t.uint16_t(self.attrid).serialize() if _only_dir_and_attrid: return r if self.direction == ReportingDirection.ReceiveReports: r += t.uint16_t(self.timeout).serialize() else: r += t.uint8_t(self.datatype).serialize() r += t.uint16_t(self.min_interval).serialize() r += t.uint16_t(self.max_interval).serialize() try: data_type = DataType.from_type_id(self.datatype) except KeyError: _LOGGER.warning( "Unknown ZCL type %d, not setting reportable change", self.datatype ) else: if data_type.type_class is Analog: r += data_type.python_type(self.reportable_change).serialize() return r @classmethod def deserialize( cls, data, *, _only_dir_and_attrid: bool = False ) -> tuple[AttributeReportingConfig, bytes]: self = cls() self.direction, data = ReportingDirection.deserialize(data) self.attrid, data = t.uint16_t.deserialize(data) # The report is only a direction and attribute if _only_dir_and_attrid: return self, data if self.direction == ReportingDirection.ReceiveReports: # Requesting things to be received by me self.timeout, data = t.uint16_t.deserialize(data) else: # Notifying that I will report things to you self.datatype, data = t.uint8_t.deserialize(data) self.min_interval, data = t.uint16_t.deserialize(data) self.max_interval, data = t.uint16_t.deserialize(data) try: data_type = DataType.from_type_id(self.datatype) except KeyError: _LOGGER.warning( "Unknown ZCL type %d, cannot read reportable change", self.datatype ) else: if data_type.type_class is Analog: self.reportable_change, data = data_type.python_type.deserialize( data ) return self, data def __repr__(self) -> str: r = f"{self.__class__.__name__}(" r += f"direction={self.direction}" r += f", attrid=0x{self.attrid:04X}" if self.direction == ReportingDirection.ReceiveReports: r += f", timeout={self.timeout}" elif hasattr(self, "datatype"): r += f", datatype={self.datatype}" r += f", min_interval={self.min_interval}" r += f", max_interval={self.max_interval}" if self.reportable_change is not None: r += f", reportable_change={self.reportable_change}" r += ")" return r class AttributeReportingConfigWithStatus(t.Struct): status: Status config: AttributeReportingConfig @classmethod def deserialize( cls, data: bytes ) -> tuple[AttributeReportingConfigWithStatus, bytes]: status, data = Status.deserialize(data) # FIXME: The reporting configuration will not include anything other than the # direction and the attribute ID when the status is not successful. This # information isn't a part of the attribute reporting config structure so we # have to pass it in externally. config, data = AttributeReportingConfig.deserialize( data, _only_dir_and_attrid=(status != Status.SUCCESS) ) return cls(status=status, config=config), data def serialize(self) -> bytes: return self.status.serialize() + self.config.serialize( _only_dir_and_attrid=(self.status != Status.SUCCESS) ) class ConfigureReportingResponseRecord(t.Struct): status: Status direction: ReportingDirection attrid: t.uint16_t = t.StructField(repr=_hex_uint16_repr) @classmethod def deserialize(cls, data: bytes) -> tuple[ConfigureReportingResponseRecord, bytes]: r = cls() r.status, data = Status.deserialize(data) if r.status == Status.SUCCESS: r.direction, data = t.Optional(t.uint8_t).deserialize(data) if r.direction is not None: r.direction = ReportingDirection(r.direction) r.attrid, data = t.Optional(t.uint16_t).deserialize(data) return r, data r.direction, data = ReportingDirection.deserialize(data) r.attrid, data = t.uint16_t.deserialize(data) return r, data def serialize(self): r = Status(self.status).serialize() if self.status != Status.SUCCESS: r += ReportingDirection(self.direction).serialize() r += t.uint16_t(self.attrid).serialize() return r def __repr__(self) -> str: r = f"{self.__class__.__name__}(status={self.status}" if self.status != Status.SUCCESS: r += f", direction={self.direction}, attrid={self.attrid}" r += ")" return r class ConfigureReportingResponse(t.List[ConfigureReportingResponseRecord]): # In the case of successful configuration of all attributes, only a single # attribute status record SHALL be included in the command, with the status # field set to SUCCESS and the direction and attribute identifier fields omitted def serialize(self): if not self: raise ValueError("Cannot serialize empty list") failed = [record for record in self if record.status != Status.SUCCESS] if not failed: return ConfigureReportingResponseRecord(status=Status.SUCCESS).serialize() # Note that attribute status records are not included for successfully # configured attributes, in order to save bandwidth. return b"".join( [ConfigureReportingResponseRecord(r).serialize() for r in failed] ) class ReadReportingConfigRecord(t.Struct): direction: t.uint8_t attrid: t.uint16_t class DiscoverAttributesResponseRecord(t.Struct): attrid: t.uint16_t datatype: t.uint8_t class AttributeAccessControl(t.bitmap8): READ = 0x01 WRITE = 0x02 REPORT = 0x04 class DiscoverAttributesExtendedResponseRecord(t.Struct): attrid: t.uint16_t datatype: t.uint8_t acl: AttributeAccessControl class FrameType(t.enum2): """ZCL Frame Type.""" GLOBAL_COMMAND = 0b00 CLUSTER_COMMAND = 0b01 RESERVED_2 = 0b10 RESERVED_3 = 0b11 class Direction(t.enum1): """ZCL frame control direction.""" Client_to_Server = 0 Server_to_Client = 1 @classmethod def _from_is_reply(cls, is_reply: bool) -> Direction: return cls.Server_to_Client if is_reply else cls.Client_to_Server class FrameControl(t.IntStruct, t.uint8_t): """The frame control field contains information defining the command type and other control flags. """ frame_type: FrameType is_manufacturer_specific: t.uint1_t direction: Direction disable_default_response: t.uint1_t reserved: t.uint3_t @classmethod def cluster( cls, direction: Direction = Direction.Client_to_Server, is_manufacturer_specific: bool = False, ): return cls( frame_type=FrameType.CLUSTER_COMMAND, is_manufacturer_specific=is_manufacturer_specific, direction=direction, disable_default_response=(direction == Direction.Server_to_Client), reserved=0b000, ) @classmethod def general( cls, direction: Direction = Direction.Client_to_Server, is_manufacturer_specific: bool = False, ): return cls( frame_type=FrameType.GLOBAL_COMMAND, is_manufacturer_specific=is_manufacturer_specific, direction=direction, disable_default_response=(direction == Direction.Server_to_Client), reserved=0b000, ) @property def is_cluster(self) -> bool: """Return True if command is a local cluster specific command.""" return bool(self.frame_type == FrameType.CLUSTER_COMMAND) @property def is_general(self) -> bool: """Return True if command is a global ZCL command.""" return bool(self.frame_type == FrameType.GLOBAL_COMMAND) class ZCLHeader(t.Struct): NO_MANUFACTURER_ID = -1 # type: typing.Literal frame_control: FrameControl manufacturer: t.uint16_t = t.StructField( requires=lambda hdr: hdr.frame_control.is_manufacturer_specific ) tsn: t.uint8_t command_id: t.uint8_t def __new__( cls: type[Self], frame_control: FrameControl | None = None, manufacturer: t.uint16_t | None = None, tsn: int | t.uint8_t | None = None, command_id: int | GeneralCommand | None = None, ) -> Self: # Allow "auto manufacturer ID" to be disabled in higher layers if manufacturer is cls.NO_MANUFACTURER_ID: manufacturer = None if frame_control is not None and manufacturer is not None: frame_control = frame_control.replace(is_manufacturer_specific=True) return super().__new__(cls, frame_control, manufacturer, tsn, command_id) @property def direction(self) -> bool: """Return direction of Frame Control.""" return self.frame_control.direction def __setattr__( self, name: str, value: t.uint16_t | FrameControl | t.uint8_t | GeneralCommand | None, ) -> None: if name == "manufacturer" and value is self.NO_MANUFACTURER_ID: value = None super().__setattr__(name, value) if name == "manufacturer" and self.frame_control is not None: self.frame_control = self.frame_control.replace( is_manufacturer_specific=value is not None ) @classmethod def general( cls, tsn: int | t.uint8_t, command_id: int | t.uint8_t, manufacturer: int | t.uint16_t | None = None, direction: Direction = Direction.Client_to_Server, ) -> ZCLHeader: return cls( frame_control=FrameControl.general( direction=direction, is_manufacturer_specific=(manufacturer is not None), ), manufacturer=manufacturer, tsn=tsn, command_id=command_id, ) @classmethod def cluster( cls, tsn: int | t.uint8_t, command_id: int | t.uint8_t, manufacturer: int | t.uint16_t | None = None, direction: Direction = Direction.Client_to_Server, ) -> ZCLHeader: return cls( frame_control=FrameControl.cluster( direction=direction, is_manufacturer_specific=(manufacturer is not None), ), manufacturer=manufacturer, tsn=tsn, command_id=command_id, ) @dataclasses.dataclass(frozen=True) class ZCLCommandDef(t.BaseDataclassMixin): id: t.uint8_t = None schema: CommandSchema = None direction: Direction = None is_manufacturer_specific: bool = None # set later name: str = None def __post_init__(self) -> None: # Backwards compatibility with positional syntax where the name was first if isinstance(self.id, str): object.__setattr__(self, "name", self.id) object.__setattr__(self, "id", None) ensure_valid_name(self.name) if isinstance(self.direction, bool): object.__setattr__( self, "direction", Direction._from_is_reply(self.direction) ) def with_compiled_schema(self) -> ZCLCommandDef: """Return a copy of the ZCL command definition object with its dictionary command schema converted into a `CommandSchema` subclass. """ if isinstance(self.schema, tuple): raise ValueError( # noqa: TRY004 f"Tuple schemas are deprecated: {self.schema!r}. Use a dictionary or a" f" Struct subclass." ) elif not isinstance(self.schema, dict): # If the schema is already a struct, do nothing self.schema.command = self return self assert self.id is not None assert self.name is not None cls_attrs = { "__annotations__": {}, "command": self, } for name, param_type in self.schema.items(): plain_name = name.rstrip("?") # Make sure parameters with names like "foo bar" and "class" can't exist if not plain_name.isidentifier() or keyword.iskeyword(plain_name): raise ValueError( f"Schema parameter {name} must be a valid Python identifier" ) cls_attrs["__annotations__"][plain_name] = "None" cls_attrs[plain_name] = t.StructField( type=param_type, optional=name.endswith("?"), ) schema = type(self.name, (CommandSchema,), cls_attrs) return self.replace(schema=schema) def __repr__(self) -> str: return ( f"{self.__class__.__name__}(" f"id=0x{self.id:02X}, " f"name={self.name!r}, " f"direction={self.direction}, " f"schema={self.schema}, " f"is_manufacturer_specific={self.is_manufacturer_specific}" f")" ) class CommandSchema(t.Struct, tuple): # noqa: SLOT001 """Struct subclass that behaves more like a tuple.""" command: ZCLCommandDef = None def __iter__(self): return iter(self.as_tuple()) def __getitem__( self, item: slice | typing.SupportsIndex ) -> typing.Any | tuple[typing.Any, ...]: return self.as_tuple()[item] def __len__(self) -> int: return len(self.as_tuple()) def __eq__(self, other) -> bool: if isinstance(other, tuple) and not isinstance(other, type(self)): return self.as_tuple() == other return super().__eq__(other) class ZCLAttributeAccess(enum.Flag): NONE = 0 Read = 1 Write = 2 Write_Optional = 4 Report = 8 Scene = 16 _names: dict[ZCLAttributeAccess, str] @classmethod @functools.lru_cache(None) def from_str(cls: ZCLAttributeAccess, value: str) -> ZCLAttributeAccess: orig_value = value access = cls.NONE while value: for mode, prefix in cls._names.items(): if value.startswith(prefix): value = value[len(prefix) :] access |= mode break else: raise ValueError(f"Invalid access mode: {orig_value!r}") return cls(access) ZCLAttributeAccess._names = { ZCLAttributeAccess.Write_Optional: "*w", ZCLAttributeAccess.Write: "w", ZCLAttributeAccess.Read: "r", ZCLAttributeAccess.Report: "p", ZCLAttributeAccess.Scene: "s", } @dataclasses.dataclass(frozen=True) class ZCLAttributeDef(t.BaseDataclassMixin): id: t.uint16_t = None type: type = None zcl_type: DataTypeId = None access: ZCLAttributeAccess = ( ZCLAttributeAccess.Read | ZCLAttributeAccess.Write | ZCLAttributeAccess.Report ) mandatory: bool = False is_manufacturer_specific: bool = False # The name will be specified later name: str = None def __post_init__(self) -> None: # Backwards compatibility with positional syntax where the name was first if isinstance(self.id, str): object.__setattr__(self, "name", self.id) object.__setattr__(self, "id", None) if self.id is not None and not isinstance(self.id, t.uint16_t): object.__setattr__(self, "id", t.uint16_t(self.id)) if isinstance(self.access, str): object.__setattr__(self, "access", ZCLAttributeAccess.from_str(self.access)) if self.zcl_type is None: object.__setattr__( self, "zcl_type", DataType.from_python_type(self.type).type_id ) ensure_valid_name(self.name) def __repr__(self) -> str: return ( f"{self.__class__.__name__}(" f"id=0x{self.id:04X}, " f"name={self.name!r}, " f"type={self.type}, " f"zcl_type={self.zcl_type}, " f"access={self.access!r}, " f"mandatory={self.mandatory!r}, " f"is_manufacturer_specific={self.is_manufacturer_specific}" f")" ) class IterableMemberMeta(type): def __iter__(cls) -> typing.Iterator[typing.Any]: for name in dir(cls): if not name.startswith("_"): yield getattr(cls, name) class BaseCommandDefs(metaclass=IterableMemberMeta): pass class BaseAttributeDefs(metaclass=IterableMemberMeta): pass class GeneralCommand(t.enum8): """ZCL Foundation General Command IDs.""" Read_Attributes = 0x00 Read_Attributes_rsp = 0x01 Write_Attributes = 0x02 Write_Attributes_Undivided = 0x03 Write_Attributes_rsp = 0x04 Write_Attributes_No_Response = 0x05 Configure_Reporting = 0x06 Configure_Reporting_rsp = 0x07 Read_Reporting_Configuration = 0x08 Read_Reporting_Configuration_rsp = 0x09 Report_Attributes = 0x0A Default_Response = 0x0B Discover_Attributes = 0x0C Discover_Attributes_rsp = 0x0D # Read_Attributes_Structured = 0x0e # Write_Attributes_Structured = 0x0f # Write_Attributes_Structured_rsp = 0x10 Discover_Commands_Received = 0x11 Discover_Commands_Received_rsp = 0x12 Discover_Commands_Generated = 0x13 Discover_Commands_Generated_rsp = 0x14 Discover_Attribute_Extended = 0x15 Discover_Attribute_Extended_rsp = 0x16 GENERAL_COMMANDS = COMMANDS = { GeneralCommand.Read_Attributes: ZCLCommandDef( schema={"attribute_ids": t.List[t.uint16_t]}, direction=Direction.Client_to_Server, ), GeneralCommand.Read_Attributes_rsp: ZCLCommandDef( schema={"status_records": t.List[ReadAttributeRecord]}, direction=Direction.Server_to_Client, ), GeneralCommand.Write_Attributes: ZCLCommandDef( schema={"attributes": t.List[Attribute]}, direction=Direction.Client_to_Server ), GeneralCommand.Write_Attributes_Undivided: ZCLCommandDef( schema={"attributes": t.List[Attribute]}, direction=Direction.Client_to_Server ), GeneralCommand.Write_Attributes_rsp: ZCLCommandDef( schema={"status_records": WriteAttributesResponse}, direction=Direction.Server_to_Client, ), GeneralCommand.Write_Attributes_No_Response: ZCLCommandDef( schema={"attributes": t.List[Attribute]}, direction=Direction.Client_to_Server ), GeneralCommand.Configure_Reporting: ZCLCommandDef( schema={"config_records": t.List[AttributeReportingConfig]}, direction=Direction.Client_to_Server, ), GeneralCommand.Configure_Reporting_rsp: ZCLCommandDef( schema={"status_records": ConfigureReportingResponse}, direction=Direction.Server_to_Client, ), GeneralCommand.Read_Reporting_Configuration: ZCLCommandDef( schema={"attribute_records": t.List[ReadReportingConfigRecord]}, direction=Direction.Client_to_Server, ), GeneralCommand.Read_Reporting_Configuration_rsp: ZCLCommandDef( schema={"attribute_configs": t.List[AttributeReportingConfigWithStatus]}, direction=Direction.Server_to_Client, ), GeneralCommand.Report_Attributes: ZCLCommandDef( schema={"attribute_reports": t.List[Attribute]}, direction=Direction.Client_to_Server, ), GeneralCommand.Default_Response: ZCLCommandDef( schema={"command_id": t.uint8_t, "status": Status}, direction=Direction.Server_to_Client, ), GeneralCommand.Discover_Attributes: ZCLCommandDef( schema={"start_attribute_id": t.uint16_t, "max_attribute_ids": t.uint8_t}, direction=Direction.Client_to_Server, ), GeneralCommand.Discover_Attributes_rsp: ZCLCommandDef( schema={ "discovery_complete": t.Bool, "attribute_info": t.List[DiscoverAttributesResponseRecord], }, direction=Direction.Server_to_Client, ), # Command.Read_Attributes_Structured: ZCLCommandDef(schema=(, ), direction=Direction.Client_to_Server), # Command.Write_Attributes_Structured: ZCLCommandDef(schema=(, ), direction=Direction.Client_to_Server), # Command.Write_Attributes_Structured_rsp: ZCLCommandDef(schema=(, ), direction=Direction.Server_to_Client), GeneralCommand.Discover_Commands_Received: ZCLCommandDef( schema={"start_command_id": t.uint8_t, "max_command_ids": t.uint8_t}, direction=Direction.Client_to_Server, ), GeneralCommand.Discover_Commands_Received_rsp: ZCLCommandDef( schema={"discovery_complete": t.Bool, "command_ids": t.List[t.uint8_t]}, direction=Direction.Server_to_Client, ), GeneralCommand.Discover_Commands_Generated: ZCLCommandDef( schema={"start_command_id": t.uint8_t, "max_command_ids": t.uint8_t}, direction=Direction.Client_to_Server, ), GeneralCommand.Discover_Commands_Generated_rsp: ZCLCommandDef( schema={"discovery_complete": t.Bool, "command_ids": t.List[t.uint8_t]}, direction=Direction.Server_to_Client, ), GeneralCommand.Discover_Attribute_Extended: ZCLCommandDef( schema={"start_attribute_id": t.uint16_t, "max_attribute_ids": t.uint8_t}, direction=Direction.Client_to_Server, ), GeneralCommand.Discover_Attribute_Extended_rsp: ZCLCommandDef( schema={ "discovery_complete": t.Bool, "extended_attr_info": t.List[DiscoverAttributesExtendedResponseRecord], }, direction=Direction.Server_to_Client, ), } for command_id, command_def in list(GENERAL_COMMANDS.items()): GENERAL_COMMANDS[command_id] = command_def.replace( id=command_id, name=command_id.name ).with_compiled_schema() ZCL_CLUSTER_REVISION_ATTR = ZCLAttributeDef( id=0xFFFD, type=t.uint16_t, access="r", mandatory=True ) ZCL_REPORTING_STATUS_ATTR = ZCLAttributeDef( id=0xFFFE, type=AttributeReportingStatus, access="r" ) zigpy-0.80.1/zigpy/zdo/000077500000000000000000000000001501451476000147035ustar00rootroot00000000000000zigpy-0.80.1/zigpy/zdo/__init__.py000066400000000000000000000220151501451476000170140ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Coroutine import functools import logging from zigpy.const import APS_REPLY_TIMEOUT import zigpy.profiles import zigpy.types as t from zigpy.typing import AddressingMode import zigpy.util from . import types LOGGER = logging.getLogger(__name__) ZDO_ENDPOINT = 0 class ZDO(zigpy.util.CatchingTaskMixin, zigpy.util.ListenableMixin): """The ZDO endpoint of a device""" class LeaveOptions(t.bitmap8): """ZDO Mgmt_Leave_req Options.""" NONE = 0 RemoveChildren = 1 << 6 Rejoin = 1 << 7 def __init__(self, device): self._device = device self._listeners = {} def _serialize(self, command, *args, **kwargs): keys, schema = types.CLUSTERS[command] # TODO: expose this in a future PR assert not kwargs return t.serialize(args, schema) def deserialize(self, cluster_id, data): if cluster_id not in types.CLUSTERS: raise ValueError(f"Invalid ZDO cluster ID: 0x{cluster_id:04X}") _, param_types = types.CLUSTERS[cluster_id] hdr, data = types.ZDOHeader.deserialize(cluster_id, data) args, data = t.deserialize(data, param_types) if data: # TODO: Seems sane to check, but what should we do? self.warning("Data remains after deserializing ZDO frame: %r", data) return hdr, args async def request( self, command, *args, timeout=APS_REPLY_TIMEOUT, expect_reply: bool = True, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, **kwargs, ): data = self._serialize(command, *args, **kwargs) tsn = self.device.get_sequence() return await self._device.request( profile=0x0000, cluster=command, src_ep=ZDO_ENDPOINT, dst_ep=ZDO_ENDPOINT, sequence=tsn, data=t.uint8_t(tsn).serialize() + data, timeout=timeout, expect_reply=expect_reply, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) async def reply( self, command, *args, tsn: int | t.uint8_t | None = None, timeout=APS_REPLY_TIMEOUT, expect_reply: bool = False, use_ieee: bool = False, ask_for_ack: bool | None = None, priority: int = t.PacketPriority.NORMAL, **kwargs, ): data = self._serialize(command, *args, **kwargs) if tsn is None: tsn = self.device.get_sequence() return await self._device.reply( profile=0x0000, cluster=command, src_ep=ZDO_ENDPOINT, dst_ep=ZDO_ENDPOINT, sequence=tsn, data=t.uint8_t(tsn).serialize() + data, timeout=timeout, expect_reply=expect_reply, use_ieee=use_ieee, ask_for_ack=ask_for_ack, priority=priority, ) def handle_message( self, profile: int, cluster: int, hdr: types.ZDOHeader, args: list, *, dst_addressing: AddressingMode | None = None, ) -> None: self.debug("ZDO request %s: %s", hdr.command_id, args) handler = getattr(self, f"handle_{hdr.command_id.name.lower()}", None) if handler is not None: handler(hdr, *args, dst_addressing=dst_addressing) else: self.debug("No handler for ZDO request:%s(%s)", hdr.command_id, args) self.listener_event( f"zdo_{hdr.command_id.name.lower()}", self._device, dst_addressing, hdr, args, ) def handle_nwk_addr_req( self, hdr: types.ZDOHeader, ieee: t.EUI64, request_type: int, start_index: int | None = None, dst_addressing: AddressingMode | None = None, ): """Handle ZDO NWK Address request.""" app = self._device.application if ieee == app.state.node_info.ieee: self.create_catching_task( self.NWK_addr_rsp( 0, app.state.node_info.ieee, app.state.node_info.nwk, 0, 0, [], tsn=hdr.tsn, priority=t.PacketPriority.LOW, ) ) def handle_ieee_addr_req( self, hdr: types.ZDOHeader, nwk: t.NWK, request_type: int, start_index: int | None = None, dst_addressing: AddressingMode | None = None, ): """Handle ZDO IEEE Address request.""" app = self._device.application if nwk in ( t.BroadcastAddress.ALL_DEVICES, t.BroadcastAddress.RX_ON_WHEN_IDLE, t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, app.state.node_info.nwk, ): self.create_catching_task( self.IEEE_addr_rsp( 0, app.state.node_info.ieee, app.state.node_info.nwk, 0, 0, [], tsn=hdr.tsn, priority=t.PacketPriority.LOW, ) ) def handle_device_annce( self, hdr: types.ZDOHeader, nwk: t.NWK, ieee: t.EUI64, capability: int, dst_addressing: AddressingMode | None = None, ): """Handle ZDO device announcement request.""" self.listener_event("device_announce", self._device) def handle_mgmt_permit_joining_req( self, hdr: types.ZDOHeader, permit_duration: int, tc_significance: int, dst_addressing: AddressingMode | None = None, ): """Handle ZDO permit joining request.""" self.listener_event("permit_duration", permit_duration) def handle_match_desc_req( self, hdr: types.ZDOHeader, addr: t.NWK, profile: int, in_clusters: list, out_cluster: list, dst_addressing: AddressingMode | None = None, ): """Handle ZDO Match_desc_req request.""" local_addr = self._device.application.state.node_info.nwk if profile != zigpy.profiles.zha.PROFILE_ID: self.create_catching_task( self.Match_Desc_rsp( 0, local_addr, [], tsn=hdr.tsn, priority=t.PacketPriority.HIGH, ) ) return self.create_catching_task( self.Match_Desc_rsp( 0, local_addr, [t.uint8_t(1)], tsn=hdr.tsn, priority=t.PacketPriority.HIGH, ) ) async def bind(self, cluster): return await self.Bind_req( self._device.ieee, cluster.endpoint.endpoint_id, cluster.cluster_id, self.device.application.get_dst_address(cluster), ) async def unbind(self, cluster): return await self.Unbind_req( self._device.ieee, cluster.endpoint.endpoint_id, cluster.cluster_id, self.device.application.get_dst_address(cluster), ) def leave(self, remove_children: bool = True, rejoin: bool = False) -> Coroutine: opts = self.LeaveOptions.NONE if remove_children: opts |= self.LeaveOptions.RemoveChildren if rejoin: opts |= self.LeaveOptions.Rejoin return self.Mgmt_Leave_req(self._device.ieee, opts) def permit(self, duration=60, tc_significance=0): return self.Mgmt_Permit_Joining_req(duration, tc_significance) def log(self, lvl, msg, *args, **kwargs): msg = "[0x%04x:zdo] " + msg args = (self._device.nwk, *args) return LOGGER.log(lvl, msg, *args, **kwargs) @property def device(self): return self._device def __getattr__(self, name): try: command = types.ZDOCmd[name] except KeyError as exc: raise AttributeError(f"No such '{name}' ZDO command") from exc if command & 0x8000: return functools.partial(self.reply, command) return functools.partial(self.request, command) def broadcast( app, command, grpid, radius, *args, broadcast_address=t.BroadcastAddress.RX_ON_WHEN_IDLE, **kwargs, ): params, param_types = types.CLUSTERS[command] named_args = dict(zip(params, args)) named_args.update(kwargs) assert set(named_args.keys()) & set(params) sequence = app.get_sequence() data = bytes([sequence]) + t.serialize(named_args.values(), param_types) return zigpy.device.broadcast( app, 0, command, 0, 0, grpid, radius, sequence, data, broadcast_address=broadcast_address, ) zigpy-0.80.1/zigpy/zdo/types.py000066400000000000000000000572441501451476000164350ustar00rootroot00000000000000from __future__ import annotations import typing import zigpy.types as t class _PowerDescriptorEnums: class CurrentPowerMode(t.enum4): RxOnSyncedWithNodeDesc = 0b0000 RxOnPeriodically = 0b0001 RxOnWhenStimulated = 0b0010 class PowerSources(t.bitmap4): MainsPower = 0b0001 RechargeableBattery = 0b0010 DisposableBattery = 0b0100 Reserved = 0b1000 class PowerSourceLevel(t.enum4): Critical = 0b0000 Percent33 = 0b0100 Percent66 = 0b1000 Percent100 = 0b1100 class PowerDescriptor(t.Struct): CurrentPowerMode = _PowerDescriptorEnums.CurrentPowerMode PowerSources = _PowerDescriptorEnums.PowerSources PowerSourceLevel = _PowerDescriptorEnums.PowerSourceLevel current_power_mode: _PowerDescriptorEnums.CurrentPowerMode available_power_sources: _PowerDescriptorEnums.PowerSources current_power_source: _PowerDescriptorEnums.PowerSources current_power_source_level: _PowerDescriptorEnums.PowerSourceLevel class SimpleDescriptor(t.Struct): endpoint: t.uint8_t profile: t.uint16_t device_type: t.uint16_t device_version: t.uint8_t input_clusters: t.LVList[t.uint16_t] output_clusters: t.LVList[t.uint16_t] class SizePrefixedSimpleDescriptor(SimpleDescriptor): def serialize(self): data = super().serialize() return len(data).to_bytes(1, "little") + data @classmethod def deserialize(cls, data): if not data or data[0] == 0: return None, data[1:] return super().deserialize(data[1:]) class LogicalType(t.enum3): Coordinator = 0b000 Router = 0b001 EndDevice = 0b010 class _NodeDescriptorEnums: class MACCapabilityFlags(t.bitmap8): NONE = 0 AlternatePanCoordinator = 0b00000001 FullFunctionDevice = 0b00000010 MainsPowered = 0b00000100 RxOnWhenIdle = 0b00001000 SecurityCapable = 0b01000000 AllocateAddress = 0b10000000 class FrequencyBand(t.bitmap5): Freq868MHz = 0b00001 Freq902MHz = 0b00100 Freq2400MHz = 0b01000 class DescriptorCapability(t.bitmap8): NONE = 0 ExtendedActiveEndpointListAvailable = 0b00000001 ExtendedSimpleDescriptorListAvailable = 0b00000010 class NodeDescriptor(t.Struct): FrequencyBand = _NodeDescriptorEnums.FrequencyBand MACCapabilityFlags = _NodeDescriptorEnums.MACCapabilityFlags DescriptorCapability = _NodeDescriptorEnums.DescriptorCapability logical_type: LogicalType complex_descriptor_available: t.uint1_t user_descriptor_available: t.uint1_t reserved: t.uint3_t aps_flags: t.uint3_t frequency_band: _NodeDescriptorEnums.FrequencyBand mac_capability_flags: _NodeDescriptorEnums.MACCapabilityFlags manufacturer_code: t.uint16_t maximum_buffer_size: t.uint8_t maximum_incoming_transfer_size: t.uint16_t server_mask: t.uint16_t maximum_outgoing_transfer_size: t.uint16_t descriptor_capability_field: _NodeDescriptorEnums.DescriptorCapability def __new__(cls, *args, **kwargs): # Old style constructor if len(args) == 9 or "byte1" in kwargs or "byte2" in kwargs: return cls._old_constructor(*args, **kwargs) return super().__new__(cls, *args, **kwargs) @classmethod def _old_constructor( cls: NodeDescriptor, byte1: t.uint8_t = None, byte2: t.uint8_t = None, mac_capability_flags: MACCapabilityFlags = None, manufacturer_code: t.uint16_t = None, maximum_buffer_size: t.uint8_t = None, maximum_incoming_transfer_size: t.uint16_t = None, server_mask: t.uint16_t = None, maximum_outgoing_transfer_size: t.uint16_t = None, descriptor_capability_field: t.uint8_t = None, ) -> NodeDescriptor: logical_type = None complex_descriptor_available = None user_descriptor_available = None reserved = None if byte1 is not None: bits, _ = t.Bits.deserialize(bytes([byte1])) logical_type, bits = LogicalType.from_bits(bits) complex_descriptor_available, bits = t.uint1_t.from_bits(bits) user_descriptor_available, bits = t.uint1_t.from_bits(bits) reserved, bits = t.uint3_t.from_bits(bits) assert not bits aps_flags = None frequency_band = None if byte2 is not None: bits, _ = t.Bits.deserialize(bytes([byte2])) aps_flags, bits = t.uint3_t.from_bits(bits) frequency_band, bits = cls.FrequencyBand.from_bits(bits) assert not bits return cls( # type:ignore[operator] logical_type=logical_type, complex_descriptor_available=complex_descriptor_available, user_descriptor_available=user_descriptor_available, reserved=reserved, aps_flags=aps_flags, frequency_band=frequency_band, mac_capability_flags=mac_capability_flags, manufacturer_code=manufacturer_code, maximum_buffer_size=maximum_buffer_size, maximum_incoming_transfer_size=maximum_incoming_transfer_size, server_mask=server_mask, maximum_outgoing_transfer_size=maximum_outgoing_transfer_size, descriptor_capability_field=descriptor_capability_field, ) @property def is_end_device(self) -> bool | None: if self.logical_type is None: return None return self.logical_type == LogicalType.EndDevice @property def is_router(self) -> bool | None: if self.logical_type is None: return None return self.logical_type == LogicalType.Router @property def is_coordinator(self) -> bool | None: if self.logical_type is None: return None return self.logical_type == LogicalType.Coordinator @property def is_alternate_pan_coordinator(self) -> bool | None: if self.mac_capability_flags is None: return None return bool( self.mac_capability_flags & self.MACCapabilityFlags.AlternatePanCoordinator ) @property def is_full_function_device(self) -> bool | None: if self.mac_capability_flags is None: return None return bool( self.mac_capability_flags & self.MACCapabilityFlags.FullFunctionDevice ) @property def is_mains_powered(self) -> bool | None: if self.mac_capability_flags is None: return None return bool(self.mac_capability_flags & self.MACCapabilityFlags.MainsPowered) @property def is_receiver_on_when_idle(self) -> bool | None: if self.mac_capability_flags is None: return None return bool(self.mac_capability_flags & self.MACCapabilityFlags.RxOnWhenIdle) @property def is_security_capable(self) -> bool | None: if self.mac_capability_flags is None: return None return bool(self.mac_capability_flags & self.MACCapabilityFlags.SecurityCapable) @property def allocate_address(self) -> bool | None: if self.mac_capability_flags is None: return None return bool(self.mac_capability_flags & self.MACCapabilityFlags.AllocateAddress) class MultiAddress(t.Struct): """Used for binds, represents an IEEE+endpoint or NWK address""" addrmode: t.uint8_t nwk: t.uint16_t = t.StructField(requires=lambda s: s.addrmode == 0x01) ieee: t.EUI64 = t.StructField(requires=lambda s: s.addrmode == 0x03) endpoint: t.uint8_t = t.StructField(requires=lambda s: s.addrmode == 0x03) @classmethod def deserialize(cls, data): r, data = super().deserialize(data) if r.addrmode not in (0x01, 0x03): raise ValueError("Invalid MultiAddress - unknown address mode") return r, data def serialize(self): if self.addrmode not in (0x01, 0x03): raise ValueError("Invalid MultiAddress - unknown address mode") return super().serialize() class _NeighborEnums: class DeviceType(t.enum2): Coordinator = 0x0 Router = 0x1 EndDevice = 0x2 Unknown = 0x3 class RxOnWhenIdle(t.enum2): Off = 0x0 On = 0x1 Unknown = 0x2 class Relationship(t.enum3): Parent = 0x0 Child = 0x1 Sibling = 0x2 NoneOfTheAbove = 0x3 PreviousChild = 0x4 class PermitJoins(t.enum2): NotAccepting = 0x0 Accepting = 0x1 Unknown = 0x2 class Neighbor(t.Struct): """Neighbor Descriptor""" PermitJoins = _NeighborEnums.PermitJoins DeviceType = _NeighborEnums.DeviceType RxOnWhenIdle = _NeighborEnums.RxOnWhenIdle Relationship = _NeighborEnums.Relationship # Backwards-compatible alternate spelling RelationShip = Relationship extended_pan_id: t.ExtendedPanId ieee: t.EUI64 nwk: t.NWK device_type: _NeighborEnums.DeviceType rx_on_when_idle: _NeighborEnums.RxOnWhenIdle relationship: _NeighborEnums.Relationship reserved1: t.uint1_t permit_joining: _NeighborEnums.PermitJoins reserved2: t.uint6_t depth: t.uint8_t lqi: t.uint8_t @classmethod def _parse_packed(cls, packed: t.uint8_t) -> dict[str, typing.Any]: data = 18 * b"\x00" + t.uint16_t(packed).serialize() + 3 * b"\x00" tmp_neighbor, _ = cls.deserialize(data) return { "device_type": tmp_neighbor.device_type, "rx_on_when_idle": tmp_neighbor.rx_on_when_idle, "relationship": tmp_neighbor.relationship, "reserved1": tmp_neighbor.reserved1, } class Neighbors(t.Struct): """Mgmt_Lqi_rsp""" Entries: t.uint8_t StartIndex: t.uint8_t NeighborTableList: t.LVList[Neighbor] class RouteStatus(t.enum3): """Route descriptor route status.""" Active = 0x00 Discovery_Underway = 0x01 Discovery_Failed = 0x02 Inactive = 0x03 Validation_Underway = 0x04 Reserved_5 = 0x05 Reserved_6 = 0x06 Reserved_7 = 0x07 class Route(t.Struct): """Route Descriptor""" DstNWK: t.NWK RouteStatus: RouteStatus # Whether the device is a memory constrained concentrator. MemoryConstrained: t.uint1_t # The destination is a concentrator that issued a many-to-one request. ManyToOne: t.uint1_t # A route record command frame should be sent to the destination prior to the next # data packet. RouteRecordRequired: t.uint1_t Reserved: t.uint2_t NextHop: t.NWK class Routes(t.Struct): Entries: t.uint8_t StartIndex: t.uint8_t RoutingTableList: t.LVList[Route] CHANNEL_CHANGE_REQ = 0xFE CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ = 0xFF class NwkUpdate(t.Struct): CHANNEL_CHANGE_REQ = CHANNEL_CHANGE_REQ CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ = CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ ScanChannels: t.Channels ScanDuration: t.uint8_t ScanCount: t.uint8_t = t.StructField(requires=lambda s: s.ScanDuration <= 0x05) nwkUpdateId: t.uint8_t = t.StructField( # noqa: N815 requires=lambda s: s.ScanDuration in (CHANNEL_CHANGE_REQ, CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ) ) nwkManagerAddr: t.NWK = t.StructField( # noqa: N815 requires=lambda s: s.ScanDuration == CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ ) class Binding(t.Struct): SrcAddress: t.EUI64 SrcEndpoint: t.uint8_t ClusterId: t.uint16_t DstAddress: MultiAddress class AddrRequestType(t.enum8): Single = 0x00 Extended = 0x01 class Status(t.enum8): # The requested operation or transmission was completed successfully. SUCCESS = 0x00 # The supplied request type was invalid. INV_REQUESTTYPE = 0x80 # The requested device did not exist on a device following a child # descriptor request to a parent. DEVICE_NOT_FOUND = 0x81 # The supplied endpoint was equal to = 0x00 or between 0xf1 and 0xff. INVALID_EP = 0x82 # The requested endpoint is not described by a simple descriptor. NOT_ACTIVE = 0x83 # The requested optional feature is not supported on the target device. NOT_SUPPORTED = 0x84 # A timeout has occurred with the requested operation. TIMEOUT = 0x85 # The end device bind request was unsuccessful due to a failure to match # any suitable clusters. NO_MATCH = 0x86 # The unbind request was unsuccessful due to the coordinator or source # device not having an entry in its binding table to unbind. NO_ENTRY = 0x88 # A child descriptor was not available following a discovery request to a # parent. NO_DESCRIPTOR = 0x89 # The device does not have storage space to support the requested # operation. INSUFFICIENT_SPACE = 0x8A # The device is not in the proper state to support the requested operation. NOT_PERMITTED = 0x8B # The device does not have table space to support the operation. TABLE_FULL = 0x8C # The permissions configuration table on the target indicates that the # request is not authorized from this device. NOT_AUTHORIZED = 0x8D @classmethod def _missing_(cls, value): chained = t.APSStatus(value) status = cls._member_type_.__new__(cls, chained.value) status._name_ = chained.name status._value_ = value return status NWK = ("NWKAddr", t.NWK) NWKI = ("NWKAddrOfInterest", t.NWK) IEEE = ("IEEEAddr", t.EUI64) STATUS = ("Status", Status) class _CommandID(t.uint16_t, repr="hex"): pass class ZDOCmd(t.enum_factory(_CommandID)): # Device and Service Discovery Server Requests NWK_addr_req = 0x0000 IEEE_addr_req = 0x0001 Node_Desc_req = 0x0002 Power_Desc_req = 0x0003 Simple_Desc_req = 0x0004 Active_EP_req = 0x0005 Match_Desc_req = 0x0006 Complex_Desc_req = 0x0010 User_Desc_req = 0x0011 Discovery_Cache_req = 0x0012 Device_annce = 0x0013 User_Desc_set = 0x0014 System_Server_Discovery_req = 0x0015 Discovery_store_req = 0x0016 Node_Desc_store_req = 0x0017 Active_EP_store_req = 0x0019 Simple_Desc_store_req = 0x001A Remove_node_cache_req = 0x001B Find_node_cache_req = 0x001C Extended_Simple_Desc_req = 0x001D Extended_Active_EP_req = 0x001E Parent_annce = 0x001F # Bind Management Server Services Responses End_Device_Bind_req = 0x0020 Bind_req = 0x0021 Unbind_req = 0x0022 # Network Management Server Services Requests # ... TODO optional stuff ... Mgmt_Lqi_req = 0x0031 Mgmt_Rtg_req = 0x0032 Mgmt_Bind_req = 0x0033 # ... TODO optional stuff ... Mgmt_Leave_req = 0x0034 Mgmt_Permit_Joining_req = 0x0036 Mgmt_NWK_Update_req = 0x0038 # ... TODO optional stuff ... # Responses # Device and Service Discovery Server Responses NWK_addr_rsp = 0x8000 IEEE_addr_rsp = 0x8001 Node_Desc_rsp = 0x8002 Power_Desc_rsp = 0x8003 Simple_Desc_rsp = 0x8004 Active_EP_rsp = 0x8005 Match_Desc_rsp = 0x8006 Complex_Desc_rsp = 0x8010 User_Desc_rsp = 0x8011 Discovery_Cache_rsp = 0x8012 User_Desc_conf = 0x8014 System_Server_Discovery_rsp = 0x8015 Discovery_Store_rsp = 0x8016 Node_Desc_store_rsp = 0x8017 Power_Desc_store_rsp = 0x8018 Active_EP_store_rsp = 0x8019 Simple_Desc_store_rsp = 0x801A Remove_node_cache_rsp = 0x801B Find_node_cache_rsp = 0x801C Extended_Simple_Desc_rsp = 0x801D Extended_Active_EP_rsp = 0x801E Parent_annce_rsp = 0x801F # Bind Management Server Services Responses End_Device_Bind_rsp = 0x8020 Bind_rsp = 0x8021 Unbind_rsp = 0x8022 # ... TODO optional stuff ... # Network Management Server Services Responses Mgmt_Lqi_rsp = 0x8031 Mgmt_Rtg_rsp = 0x8032 Mgmt_Bind_rsp = 0x8033 # ... TODO optional stuff ... Mgmt_Leave_rsp = 0x8034 Mgmt_Permit_Joining_rsp = 0x8036 # ... TODO optional stuff ... Mgmt_NWK_Update_rsp = 0x8038 CLUSTERS = { # Device and Service Discovery Server Requests ZDOCmd.NWK_addr_req: ( IEEE, ("RequestType", AddrRequestType), ("StartIndex", t.uint8_t), ), ZDOCmd.IEEE_addr_req: ( NWKI, ("RequestType", AddrRequestType), ("StartIndex", t.uint8_t), ), ZDOCmd.Node_Desc_req: (NWKI,), ZDOCmd.Power_Desc_req: (NWKI,), ZDOCmd.Simple_Desc_req: (NWKI, ("EndPoint", t.uint8_t)), ZDOCmd.Active_EP_req: (NWKI,), ZDOCmd.Match_Desc_req: ( NWKI, ("ProfileID", t.uint16_t), ("InClusterList", t.LVList[t.uint16_t]), ("OutClusterList", t.LVList[t.uint16_t]), ), # ZDO.Complex_Desc_req: (NWKI, ), ZDOCmd.User_Desc_req: (NWKI,), ZDOCmd.Discovery_Cache_req: (NWK, IEEE), ZDOCmd.Device_annce: (NWK, IEEE, ("Capability", t.uint8_t)), ZDOCmd.User_Desc_set: ( NWKI, ("UserDescriptor", t.FixedList[16, t.uint8_t]), ), # Really a string ZDOCmd.System_Server_Discovery_req: (("ServerMask", t.uint16_t),), ZDOCmd.Discovery_store_req: ( NWK, IEEE, ("NodeDescSize", t.uint8_t), ("PowerDescSize", t.uint8_t), ("ActiveEPSize", t.uint8_t), ("SimpleDescSizeList", t.LVList[t.uint8_t]), ), ZDOCmd.Node_Desc_store_req: (NWK, IEEE, ("NodeDescriptor", NodeDescriptor)), ZDOCmd.Active_EP_store_req: (NWK, IEEE, ("ActiveEPList", t.LVList[t.uint8_t])), ZDOCmd.Simple_Desc_store_req: ( NWK, IEEE, ("SimpleDescriptor", SizePrefixedSimpleDescriptor), ), ZDOCmd.Remove_node_cache_req: (NWK, IEEE), ZDOCmd.Find_node_cache_req: (NWK, IEEE), ZDOCmd.Extended_Simple_Desc_req: ( NWKI, ("EndPoint", t.uint8_t), ("StartIndex", t.uint8_t), ), ZDOCmd.Extended_Active_EP_req: (NWKI, ("StartIndex", t.uint8_t)), ZDOCmd.Parent_annce: (("Children", t.LVList[t.EUI64]),), # Bind Management Server Services Responses ZDOCmd.End_Device_Bind_req: ( ("BindingTarget", t.uint16_t), ("SrcAddress", t.EUI64), ("SrcEndpoint", t.uint8_t), ("ProfileID", t.uint8_t), ("InClusterList", t.LVList[t.uint8_t]), ("OutClusterList", t.LVList[t.uint8_t]), ), ZDOCmd.Bind_req: ( ("SrcAddress", t.EUI64), ("SrcEndpoint", t.uint8_t), ("ClusterID", t.uint16_t), ("DstAddress", MultiAddress), ), ZDOCmd.Unbind_req: ( ("SrcAddress", t.EUI64), ("SrcEndpoint", t.uint8_t), ("ClusterID", t.uint16_t), ("DstAddress", MultiAddress), ), # Network Management Server Services Requests # ... TODO optional stuff ... ZDOCmd.Mgmt_Lqi_req: (("StartIndex", t.uint8_t),), ZDOCmd.Mgmt_Rtg_req: (("StartIndex", t.uint8_t),), ZDOCmd.Mgmt_Bind_req: (("StartIndex", t.uint8_t),), # ... TODO optional stuff ... ZDOCmd.Mgmt_Leave_req: (("DeviceAddress", t.EUI64), ("Options", t.bitmap8)), ZDOCmd.Mgmt_Permit_Joining_req: ( ("PermitDuration", t.uint8_t), ("TC_Significant", t.Bool), ), ZDOCmd.Mgmt_NWK_Update_req: (("NwkUpdate", NwkUpdate),), # ... TODO optional stuff ... # Responses # Device and Service Discovery Server Responses ZDOCmd.NWK_addr_rsp: ( STATUS, IEEE, NWK, ("NumAssocDev", t.Optional(t.uint8_t)), ("StartIndex", t.Optional(t.uint8_t)), ("NWKAddressAssocDevList", t.Optional(t.List[t.NWK])), ), ZDOCmd.IEEE_addr_rsp: ( STATUS, IEEE, NWK, ("NumAssocDev", t.Optional(t.uint8_t)), ("StartIndex", t.Optional(t.uint8_t)), ("NWKAddrAssocDevList", t.Optional(t.List[t.NWK])), ), ZDOCmd.Node_Desc_rsp: ( STATUS, NWKI, ("NodeDescriptor", t.Optional(NodeDescriptor)), ), ZDOCmd.Power_Desc_rsp: ( STATUS, NWKI, ("PowerDescriptor", t.Optional(PowerDescriptor)), ), ZDOCmd.Simple_Desc_rsp: ( STATUS, NWKI, ("SimpleDescriptor", t.Optional(SizePrefixedSimpleDescriptor)), ), ZDOCmd.Active_EP_rsp: (STATUS, NWKI, ("ActiveEPList", t.LVList[t.uint8_t])), ZDOCmd.Match_Desc_rsp: (STATUS, NWKI, ("MatchList", t.LVList[t.uint8_t])), # ZDO.Complex_Desc_rsp: ( # STATUS, # NWKI, # ('Length', t.uint8_t), # ('ComplexDescriptor', t.Optional(ComplexDescriptor)), # ), ZDOCmd.User_Desc_rsp: ( STATUS, NWKI, ("Length", t.uint8_t), ("UserDescriptor", t.Optional(t.FixedList[16, t.uint8_t])), ), ZDOCmd.Discovery_Cache_rsp: (STATUS,), ZDOCmd.User_Desc_conf: (STATUS, NWKI), ZDOCmd.System_Server_Discovery_rsp: (STATUS, ("ServerMask", t.uint16_t)), ZDOCmd.Discovery_Store_rsp: (STATUS,), ZDOCmd.Node_Desc_store_rsp: (STATUS,), ZDOCmd.Power_Desc_store_rsp: (STATUS, IEEE, ("PowerDescriptor", PowerDescriptor)), ZDOCmd.Active_EP_store_rsp: (STATUS,), ZDOCmd.Simple_Desc_store_rsp: (STATUS,), ZDOCmd.Remove_node_cache_rsp: (STATUS,), ZDOCmd.Find_node_cache_rsp: (("CacheNWKAddr", t.EUI64), NWK, IEEE), ZDOCmd.Extended_Simple_Desc_rsp: ( STATUS, NWK, ("Endpoint", t.uint8_t), ("AppInputClusterCount", t.uint8_t), ("AppOutputClusterCount", t.uint8_t), ("StartIndex", t.uint8_t), ("AppClusterList", t.Optional(t.List[t.uint16_t])), ), ZDOCmd.Extended_Active_EP_rsp: ( STATUS, NWKI, ("ActiveEPCount", t.uint8_t), ("StartIndex", t.uint8_t), ("ActiveEPList", t.List[t.uint8_t]), ), ZDOCmd.Parent_annce_rsp: (STATUS, ("Children", t.LVList[t.EUI64])), # Bind Management Server Services Responses ZDOCmd.End_Device_Bind_rsp: (STATUS,), ZDOCmd.Bind_rsp: (STATUS,), ZDOCmd.Unbind_rsp: (STATUS,), # ... TODO optional stuff ... # Network Management Server Services Responses ZDOCmd.Mgmt_Lqi_rsp: (STATUS, ("Neighbors", t.Optional(Neighbors))), ZDOCmd.Mgmt_Rtg_rsp: (STATUS, ("Routes", t.Optional(Routes))), ZDOCmd.Mgmt_Bind_rsp: ( STATUS, ("BindingTableEntries", t.uint8_t), ("StartIndex", t.uint8_t), ("BindingTableList", t.LVList[Binding]), ), # ... TODO optional stuff ... ZDOCmd.Mgmt_Leave_rsp: (STATUS,), ZDOCmd.Mgmt_Permit_Joining_rsp: (STATUS,), ZDOCmd.Mgmt_NWK_Update_rsp: ( STATUS, ("ScannedChannels", t.Channels), ("TotalTransmissions", t.uint16_t), ("TransmissionFailures", t.uint16_t), ("EnergyValues", t.LVList[t.uint8_t]), ), # ... TODO optional stuff ... } # Rewrite to (name, param_names, param_types) for command_id, schema in CLUSTERS.items(): param_names = [p[0] for p in schema] param_types = [p[1] for p in schema] CLUSTERS[command_id] = (param_names, param_types) class ZDOHeader: """Just a wrapper representing ZDO header, similar to ZCL header.""" def __init__(self, command_id: t.uint16_t = 0x0000, tsn: t.uint8_t = 0) -> None: self._command_id = ZDOCmd(command_id) self._tsn = t.uint8_t(tsn) @property def command_id(self) -> ZDOCmd: """Return ZDO command.""" return self._command_id @command_id.setter def command_id(self, value: t.uint16_t) -> None: """Command ID setter.""" self._command_id = ZDOCmd(value) @property def is_reply(self) -> bool: """Return True if this is a reply.""" return bool(self._command_id & 0x8000) @property def tsn(self) -> t.uint8_t: """Return transaction seq number.""" return self._tsn @tsn.setter def tsn(self, value: t.uint8_t) -> None: """Set TSN.""" self._tsn = t.uint8_t(value) @classmethod def deserialize( cls, command_id: t.uint16_t, data: bytes ) -> tuple[ZDOHeader, bytes]: """Deserialize data.""" tsn, data = t.uint8_t.deserialize(data) return cls(command_id, tsn), data def serialize(self) -> bytes: """Serialize header.""" return self.tsn.serialize()