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
[](https://github.com/zigpy/zigpy/workflows/CI/badge.svg?branch=dev)
[](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.
[](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�
��m�M
�
3��
�
� 3���N�=�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_clustersCREATE 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_descriptorsCREATE 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)�9�AtableendpointsendpointsCREATE 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
���qP0{[�!;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:ff�H;ec:1b:bd:ff:fe:37:72:7e�G ;ec:1b:bd:ff:fe:33:a0:04�S;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:1e�q;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&rM���o
�
� ��r �N)
����\
&6�"�; 60:a4:23:ff:fe:02:36:a0
#�; 80:4b:50:ff:fe:41:58:f3���a#�; 80:4b:50:ff:fe:41:59:63���a"�; 80:4b:50:ff:fe:41:59:63
#�; 80:4b:50:ff:fe:41:67:d4���a#@; 68:0a:e2:ff:fe:70:00:69���a"?; 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:59���a"{; 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:30���a"/; 60:a4:23:ff:fe:02:32:30
#J; 60:a4:23:ff:fe:02:34:91���a"I; 60:a4:23:ff:fe:02:34:91
#z; 60:a4:23:ff:fe:02:30:39���a"y; 60:a4:23:ff:fe:02:30:39
#u; 60:a4:23:ff:fe:02:54:1e���a"t; 60:a4:23:ff:fe:02:54:1e
#\; 60:a4:23:ff:fe:02:2f:42���a"[; 60:a4:23:ff:fe:02:2f:42
#; 60:a4:23:ff:fe:02:2f:4d���a"; 60:a4:23:ff:fe:02:2f:4d
#j; 60:a4:23:ff:fe:02:51:70���a"i; 60:a4:23:ff:fe:02:51:70
#n; 60:a4:23:ff:fe:02:2f:96���a"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:b4���a"; 60:a4:23:ff:fe:02:3b:b4
#r; ec:1b:bd:ff:fe:94:18:a4���f"q; ec:1b:bd:ff:fe:94:18:a4�^ #; 60:a4:23:ff:fe:02:36:9c���a"; 60:a4:23:ff:fe:02:36:9c
#f; 60:a4:23:ff:fe:02:38:60���a"e; 60:a4:23:ff:fe:02:38:60
#p; 60:a4:23:ff:fe:02:38:af���a"o; 60:a4:23:ff:fe:02:38:af
##�; 60:a4:23:ff:fe:02:36:a0���a#^; 60:a4:23:ff:fe:02:36:24���a"]; 60:a4:23:ff:fe:02:36:24
#V; 60:a4:23:ff:fe:02:36:93���a"U; 60:a4:23:ff:fe:02:36:93
#`; 60:a4:23:ff:fe:02:39:7b���a"_; 60:a4:23:ff:fe:02:39:7b
"; cc:cc:cc:ff:fe:a5:f2:83��
i6 Cd*G
�
x
Z
<�u��
K
,�����L-�jX9 C
��v����
�
�
�W8
� � 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:30�0;60:a4:23:ff:fe:02:32:30/;60:a4:23:ff:fe:02:34:91�J;60:a4:23:ff:fe:02:34:91I;60:a4:23:ff:fe:02:30:39�z;60:a4:23:ff:fe:02:30:39y;60:a4:23:ff:fe:02:54:1e�u;60:a4:23:ff:fe:02:54:1et;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:70�j;60:a4:23:ff:fe:02:51:70i;60:a4:23:ff:fe:02:2f:96�n;60:a4:23:ff:fe:02:2f:96m
�;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:a4�r; 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:60�f;60:a4:23:ff:fe:02:38:60e;60:a4:23:ff:fe:02:38:af�p;60:a4:23:ff:fe:02:38:afo;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:93�V;60:a4:23:ff:fe:02:36:93U;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(�
�+���������������/��@��1��4��h0��5��!��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
pz��[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@�ͫR�A,�
�
�
�
�
Ey��A %��!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���������dG) � � � a C $ ����jK+����pQ1����vW7���Y9
�
�
�
�
l
L
�
�
�
�999999999999999999999���\;68:0a:e2:ff:fe:70:00:69�![;68:0a:e2:ff:fe:70:00:69
Z;68:0a:e2:ff:fe:70:00:69Y;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:ffxUi;80:4b:50:ff:fe:41:58:f3�!h;80:4b:50:ff:fe:41:58:f3g;80:4b:50:ff:fe:41:59:63�!f;80:4b:50:ff:fe:41:59:63e;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:30c;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:69Z ;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:f3h ;80:4b:50:ff:fe:41:59:63�!g;80:4b:50:ff:fe:41:59:63f ;80:4b:50:ff:fe:41:67:d4�!e;80:4b:50:ff:fe:41:67:d4d ; 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:91b�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$�
T������eT#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:599;60:a4:23:ff:fe:02:3b:b48;80:4b:50:ff:fe:41:58:f37;60:a4:23:ff:fe:02:36:9c6;80:4b:50:ff:fe:41:59:635;80:4b:50:ff:fe:41:67:d44;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:04�3;80:4b:50:ff:fe:41:58:f32;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!�
f��fF&
�
�
�
���
&
F
F;60:a4:23:ff:fe:02:38:59:;60:a4:23:ff:fe:02:3b:b49;80:4b:50:ff:fe:41:58:f38;60:a4:23:ff:fe:02:36:9c7;80:4b:50:ff:fe:41:59:636;80:4b:50:ff:fe:41:67:d45;80:4b:50:ff:fe:41:59:634;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:f33;80:4b:50:ff:fe:41:67:d42;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:d4���w;60:a4:23:ff:fe:02:36:a0��;ec:1b:bd:ff:fe:94:18:a4K��E;60:a4:23:ff:fe:02:36:24�!��;80:4b:50:ff:fe:41:59:63�G��;60:a4:23:ff:fe:02:51:70K��;80:4b:50:ff:fe:41:58:f3���G���D;ec:1b:bd:ff:fe:33:a0:04q����;00:15:8d:00:05:1e:13:46��K��;68:0a:e2:ff:fe:70:00:69�����s;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:af��L;00:15:8d:00:05:4a:73:c3T�K��.;60:a4:23:ff:fe:02:39:7b��a;00:15:8d:00:05:1e:0e:32��
�\z���>�L.���jj
�
�
�
�\
�jj���Z
e9
e
e
G
-
Q79��<��
�j�j;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#.
�;09���u|V1sY2�
�����N�c �
r
LX-
�
�
�
`
9
���sM(���8���sHL%���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 400lm<�B; Iec:1b:bd:ff:fe:37:72:7eTRADFRI bulb E14 W op/ch 400lm#�2;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@�(�;60:a4:23:ff:fe:02:36:9cGL-B-008P� $�~;60:a4:23:ff:fe:02:2f:96@�#�:;60:a4:23:ff:fe:02:3b:b4@
$�9;60:a4:23:ff:fe:02:3b:b4@�$�8;60:a4:23:ff:fe:02:3b:b4@�(�7;60:a4:23:ff:fe:02:3b:b4GL-B-008P�$�;60:a4:23:ff:fe:02:2f:4d@�(�
;60:a4:23:ff:fe:02:2f:4dGL-S-007P'� ;60:a4:23:ff:fe:02:2f:4dGLEDOPTO#�;60:a4:23:ff:fe:02:2f:96@
#t;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@���$�;60:a4:23:ff:fe:02:2f:4d@�#d;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@�(a;60:a4:23:ff:fe:02:38:60GL-B-007Pc#�;60:a4:23:ff:fe:02:2f:4d@
([;60:a4:23:ff:fe:02:38:afGL-B-007P(�;60:a4:23:ff:fe:02:2f:42GL-S-007P'�;60:a4:23:ff:fe:02:2f:42GLEDOPTO
*$�;60:a4:23:ff:fe:02:2f:42@�
�@
�#�!;60:a4:23:ff:fe:02:2f:42@
$� ;60:a4:23:ff:fe:02:2f:42@��F$�[;60:a4:23:ff:fe:02:51:70@�$�Z;60:a4:23:ff:fe:02:51:70@�(�R;60:a4:23:ff:fe:02:51:70GL-S-007P'�Q;60:a4:23:ff:fe:02:51:70GLEDOPTO-�"�{; 00:15:8d:00:05:1e:0e:32LUMI#�^;60:a4:23:ff:fe:02:32:30@
$�];60:a4:23:ff:fe:02:32:30@�$�\;60:a4:23:ff:fe:02:32:30@�(�P;60:a4:23:ff:fe:02:32:30GL-B-001P#�q;60:a4:23:ff:fe:02:54:1e@
$�p;60:a4:23:ff:fe:02:54:1e@�$�o;60:a4:23:ff:fe:02:54:1e@�(�a;60:a4:23:ff:fe:02:54:1eGL-S-007P'�`;60:a4:23:ff:fe:02:54:1eGLEDOPTO#�\;60:a4:23:ff:fe:02:51:70@
$�};60:a4:23:ff:fe:02:2f:96@�(�y;60:a4:23:ff:fe:02:2f:96GL-S-007P[(�/;60:a4:23:ff:fe:02:38:59GL-B-007P'�.;60:a4:23:ff:fe:02:38:59GLEDOPTO"�&; 00:15:8d:00:05:1e:13:46'_!�%; 00:15:8d:00:05:1e:13:46�*�"; %00:15:8d:00:05:1e:13:46lumi.weather"�!; 00:15:8d:00:05:1e:13:46LUMI �; 00:15:8d:00:05:4a:73:c3
7"� ; 00:15:8d:00:05:4a:73:c3LUMI"�; 00:15:8d:00:05:1e:0e:32'Y!�; 00:15:8d:00:05:1e:0e:32�
�@�����`=���tM ��b����W0 ���T)
�
�
�
x
*�
��|Y7����eB
� � � � _ < ���nC :������yQ*����Z'���uJ����#�P;60:a4��q;60:a4:23:ffn#��/;60:a4:23:ff:fe:02:39:7b@
$��.;60:a4:23:ff:fe:02:39:7b@�$��-;60:a4:23:ff:fe:02:39:7b@� ��g;58:8e:81:ff:fe:15:e3:ff��d; 58:8e:81:ff:fe:15:e3:ff��]; ec:1b:bd:ff:fe:33:a0:04 ��l;58:8e:81:ff:fe:15:e3:ff-��; +58:8e:81:ff:fe:15:e3:ffVG-ZG9001K8-DIM)��; #58:8e:81:ff:fe:15:e3:ffVaris Group#��;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:39GL-S-007P'��;60:a4:23:ff:fe:02:30:39GLEDOPTO��; 00:15:8d:00:05:1e:0e:323��|; 00:15:8d:00:05:1e:0e:321
��K; 00:15:8d:00:05:1e:13:463��H; 00:15:8d:00:05:1e:13:461
���; 00:15:8d:00:05:4a:73:c33��; 00:15:8d:00:05:4a:73:c31 #��Z;60:a4:23:ff:fe:02:36:9c@
���;60:a4:23:ff:fe:02:39:7b'��;60:a4:23:ff:fe:02:39:7bGLEDOPTO(��;60:a4:23:ff:fe:02:39:7bGL-B-008P'��
;60:a4:23:ff:fe:02:32:30GLEDOPTO#��%;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:91GLEDOPTO(��;60:a4:23:ff:fe:02:34:91GL-B-001P ��-; 60:a4:23:ff:fe:02:3b:b4 ��,;60:a4:23:ff:fe:02:3b:b4��,��i; )ec:1b:bd:ff:fe:33:a0:04IKEA of Sweden��; 68:0a:e2:ff:fe:70:00:69��
;60:a4:23:ff:fe:02:32:30��;60:a4:23:ff:fe:02:38:59��x;60:a4:23:ff:fe:02:54:1e��u;60:a4:23:ff:fe:02:30:39��i;60:a4:23:ff:fe:02:2f:42��c;60:a4:23:ff:fe:02:2f:4d��Z;60:a4:23:ff:fe:02:51:70��M; ec:1b:bd:ff:fe:94:18:a4��F;60:a4:23:ff:fe:02:3b:b4��<;60:a4:23:ff:fe:02:38:60��;;60:a4:23:ff:fe:02:36:9c��.;60:a4:23:ff:fe:02:38:af
M#��a; ec:1b:bd:ff:fe:37:72:7e��*;60:a4:23:ff:fe:02:2f:96+,��A; )ec:1b:bd:ff:fe:37:72:7eIKEA of Sweden'��6;60:a4:23:ff:fe:02:36:9cGLEDOPTO'��&;60:a4:23:ff:fe:02:38:afGLEDOPTO'��";60:a4:23:ff:fe:02:2f:96GLEDOPTO'��!;60:a4:23:ff:fe:02:3b:b4GLEDOPTO-��; +ec:1b:bd:ff:fe:94:18:a4Varis Group FZC'��;60:a4:23:ff:fe:02:38:60GLEDOPTO/��;-68:0a:e2:ff:fe:70:00:69_TZ3000_00mk2xzy"��v; 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@�*��c; %ec:1b:bd:ff:fe:94:18:a4CCT Lighting*��a; %00:15:8d:00:05:1e:0e:32lumi.weather%��};68:0a:e2:ff:fe:70:00:69TS011F ��q;58:8e:81:ff:fe:15:e3:ff
g�V���
�w3����bA
�<
�
�
t
Q
.
������^����mK)
�
�
�
\
9
0 � � � � i F "����SiD����pM)���xpK&���vS/����vQ,���{X4����{U/
!; 00:15:8d:00:05:4a:73:c3�h�""; 58:8e:81:ff:fe:15:e3:ff!!N$;60:a4:23:ff:fe:02:2f:42�S$;60:a4:23:ff:fe:02:2f:4d%l$;60:a4:23:ff:fe:02:2f:96$f %!; 00:15:8d:00:05:4a:73:c3 %&�$;60:a4:23:ff:fe:02:30:39%E$;60:a4:23:ff:fe:02:30:39�X$;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 n�";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@$j$;60:a4:23:ff:fe:02:2f:96%F$;60:a4:23:ff:fe:02:2f:96%@$;60:a4:23:ff:fe:02:2f:96%?#; 60:a4:23:ff:fe:02:2f:96%P#;60:a4:23:ff:fe:02:2f:96%O";60:a4:23:ff:fe:02:2f:96%A";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 4"$;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@%p$;60:a4:23:ff:fe:02:2f:4d%m$;60:a4:23:ff:fe:02:2f:4d%o$;60:a4:23:ff:fe:02:2f:4d%n#; 60:a4:23:ff:fe:02:2f:4d4#;60:a4:23:ff:fe:02:2f:4d3";60:a4:23:ff:fe:02:2f:4d%k";60:a4:23:ff:fe:02:2f:4d��";60:a4:23:ff:fe:02:2f:4d n�!;60:a4:23:ff:fe:02:2f:4d
!;60:a4:23:ff:fe:02:2f:4d $;60:a4:23:ff:fe:02:2f:42@ $;60:a4:23:ff:fe:02:2f:42@$;60:a4:23:ff:fe:02:2f:42@
!%;60:a4:23:ff:fe:02:2f:42@�W$;60:a4:23:ff:fe:02:2f:42�T$;60:a4:23:ff:fe:02:2f:42�Z$;60:a4:23:ff:fe:02:2f:42�Y#; 60:a4:23:ff:fe:02:2f:42%[#;60:a4:23:ff:fe:02:2f:42%Z";60:a4:23:ff:fe:02:2f:42%C";60:a4:23:ff:fe:02:2f:42�";60:a4:23:ff:fe:02:2f:42 n�!;60:a4:23:ff:fe:02:2f:42!;60:a4:23:ff:fe:02:2f:42";58:8e:81:ff:fe:15:e3:ff"q";58:8e:81:ff:fe:15:e3:ff"l"; 58:8e:81:ff:fe:15:e3:ff!�";58:8e:81:ff:fe:15:e3:ff"g!; 58:8e:81:ff:fe:15:e3:ff3�0!; 58:8e:81:ff:fe:15:e3:ff1�/!; 58:8e:81:ff:fe:15:e3:ff!#�!; 58:8e:81:ff:fe:15:e3:ff #�!; 58:8e:81:ff:fe:15:e3:ff"d!; 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�B"; 00:15:8d:00:05:4a:73:c3�A!; 00:15:8d:00:05:4a:73:c33&�!; 00:15:8d:00:05:4a:73:c31&�!; 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: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:463&�!; 00:15:8d:00:05:1e:13:461&�!; 00:15:8d:00:05:1e:0e:32 %U!; 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%X"; 00:15:8d:00:05:1e:0e:32�"; 00:15:8d:00:05:1e:0e:32"; 00:15:8d:00:05:1e:0e:32%Y"; 00:15:8d:00:05:1e:0e:32%W!; 00:15:8d:00:05:1e:0e:323'�!; 00:15:8d:00:05:1e:0e:321'� ; 00:15:8d:00:05:1e:0e:32�!; 00:15:8d:00:05:1e:0e:32!%V#; 00:15:8d:00:05:1e:0e:32�%T!; 00:15:8d:00:05:1e:0e:32Ha ; 00:15:8d:00:05:1e:0e:32�
�d��[5����sO*���pK&
�
�
�
w
S
/
���uO)���wT0
�
�
�
x
R
,
� � � z W 3 ���{U/ ���{X5���~Y3
���{X5����[6���|Z7����%;60:a4:23:ff:fe:02:36:24@
�^K!;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: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 o!;60:a4:23:ff:fe:02:38:59/!;60:a4:23:ff:fe:02:38:59.%;60:a4:23:ff:fe:02:36:a0@�i%;60:a4:23:ff:fe:02:36:a0@�h%;60:a4:23:ff:fe:02:36:a0@
�j%;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�";60:a4:23:ff:fe:02:36:a0�";60:a4:23:ff:fe:02:36:a0�g";60:a4:23:ff:fe:02:36:a0�e";60:a4:23:ff:fe:02:36:a0�f%;60:a4:23:ff:fe:02:36:9c@�Y%;60:a4:23:ff:fe:02:36:9c@�X%;60:a4:23:ff:fe:02:36:9c@
�Z%;60:a4:23:ff:fe:02:36:9c@%u$;60:a4:23:ff:fe:02:36:9c%t$;60:a4:23:ff:fe:02:36:9c��$;60:a4:23:ff:fe:02:36:9c�O$;60:a4:23:ff:fe:02:36:9c�N#; 60:a4:23:ff:fe:02:36:9c�#;60:a4:23:ff:fe:02:36:9c�";60:a4:23:ff:fe:02:36:9c%�";60:a4:23:ff:fe:02:36:9c%�";60:a4:23:ff:fe:02:36:9c n�!;60:a4:23:ff:fe:02:36:9c�";60:a4:23:ff:fe:02:36:9c 46%;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�$;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�";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: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�$;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�H";60:a4:23:ff:fe:02:36:24�F";60:a4:23:ff:fe:02:36:24�G%;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@$2$;60:a4:23:ff:fe:02:34:91$0$;60:a4:23:ff:fe:02:34:91%Q$;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%G";60:a4:23:ff:fe:02:34:91�q";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: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 o
��R��H
�
�
@��6��*
�
x
� m ���������������������cbd)
T��v ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3�%>T��u ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4�%HT��t ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%ST��r ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%>T�U�� ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6558:8e:81:ff:fe:15:e3:ff�H�V�� ;;;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ݢ%MU��z ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%CV��y ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b��%�U��w ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4��%+U��v ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%4U��u ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e��%6U��t ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%iT��s ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59}�%5T��r ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%>T��q ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04d{%6T��p ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39Q�%-U��o ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M��U��n ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0Mx%�T��m ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%*U��l ;;;60:a4:23:ff:fe:02:36:937f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%�Q��k ;;;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
����sS2����mL+
�
�
�K
�
g
G
'
� � � � d C " ��)o�|[:��{Z9����rQ0����iH'����`?����wX8����vV5����m!;60:a4:23:ff:fe:02:2f:96�!;60:a4:23:ff:fe:02:2f:42w�;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:9c8 ;60:a4:23:ff:fe:02:36:9c7;60:a4:23:ff:fe:02:36:9c6;60:a4:23:ff:fe:02:36:9c5;60:a4:23:ff:fe:02:36:9c4;60:a4:23:ff:fe:02:36:9c3;60:a4:23:ff:fe:02:36:9c2;60:a4:23:ff:fe:02:36:9c1!;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:93Z ;60:a4:23:ff:fe:02:36:93Y;60:a4:23:ff:fe:02:36:93X!;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:24z ;60:a4:23:ff:fe:02:36:24y;60:a4:23:ff:fe:02:36:24x!;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:4dz;60:a4:23:ff:fe:02:2f:4dy;60:a4:23:ff:fe:02:2f:4dx;60:a4:23:ff:fe:02:2f:4dw;60:a4:23:ff:fe:02:2f:4dv!;60:a4:23:ff:fe:02:2f:42v ;60:a4:23:ff:fe:02:2f:42u ;60:a4:23:ff:fe:02:2f:42t ;60:a4:23:ff:fe:02:2f:42s ;60:a4:23:ff:fe:02:2f:42r ;60:a4:23:ff:fe:02:2f:42q;60:a4:23:ff:fe:02:2f:42p!;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
)
����bA ����{[;
�
�
�
�
y
X
7
� � � � p O .
����gF%���xW6���pO.
������`?���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:b4J ;60:a4:23:ff:fe:02:3b:b4I;60:a4:23:ff:fe:02:3b:b4H;60:a4:23:ff:fe:02:3b:b4G;60:a4:23:ff:fe:02:3b:b4F;60:a4:23:ff:fe:02:3b:b4E;60:a4:23:ff:fe:02:3b:b4D;60:a4:23:ff:fe:02:3b:b4C!;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 1����gH(W8����x����_>
�
�
�
�
_
A
{[;������||||||||||||||||||||||||||||
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{
{��������������������
�
����C; 00:15:8d:00:05:1e:13:46�B; 00:15:8d:00:05:4a:73:c3�A; 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:b4I;60:a4:23:ff:fe:02:3b:b4H;60:a4:23:ff:fe:02:3b:b4G;60:a4:23:ff:fe:02:3b:b4F;60:a4:23:ff:fe:02:3b:b4E;60:a4:23:ff:fe:02:3b:b4D;60:a4:23:ff:fe:02:3b:b4C;60:a4:23:ff:fe:02:3b:b4ux;60:a4:23:ff:fe:02:2f:4dw;60:a4:23:ff:fe:02:2f:4dv;60:a4:23:ff:fe:02:2f:4d
*�8;60:a4:23:ff:fe:02:36:9c7;60:a4:23:ff:fe:02:36:9c6;60:a4:23:ff:fe:02:36:9c5;60:a4:23:ff:fe:02:36:9c4;60:a4:23:ff:fe:02:36:9c3;60:a4:23:ff:fe:02:36:9c2;60:a4:23:ff:fe:02:36:9c1;60:a4:23:ff:fe:02:36:9cR�H; 00:15:8d:00:05:1e:13:46�G; 00:15:8d:00:05:1e:13:46�F; 00:15:8d:00:05:1e:13:46�E; 00:15:8d:00:05:1e:13:46���D; 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:4dz;60:a4:23:ff:fe:02:2f:4dy;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:32�9; 00:15:8d:00:05:1e:0e:32�8; 00:15:8d:00:05:1e:0e:32���7; 00:15:8d:00:05:1e:0e:32�6; 00:15:8d:00:05:1e:0e:32�5;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:ff�z; 58:8e:81:ff:fe:15:e3:ff�y;58:8e:81:ff:fe:15:e3:ff�x;58:8e:81:ff:fe:15:e3:ff�w;58:8e:81:ff:fe:15:e3:ff�v; 58:8e:81:ff:fe:15:e3:ff�u;58:8e:81:ff:fe:15:e3:ff�t; 58:8e:81:ff:fe:15:e3:ff�s; 58:8e:81:ff:fe:15:e3:ff�r; 58:8e:81:ff:fe:15:e3:ff�q; 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
_
�
w���kF! ����xT0U���
� < �
����$�e����iE!
2 `
:M(G
^
�
O
� �
���O+�l@� �
�����o�s���5 �\7
�
�zL)��r�Z�* �����^:���� !#; 60:a4:23:ff:fe:02:38:60l#;60:a4:23:ff:fe:02:38:60k";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:60a�
";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:1ea!;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:70R!;60:a4:23:ff:fe:02:51:70Q$;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���_� ���3���nP,
?�
�
�!
�
�
�
n�� � `
�
y\4�UB��b:��
J�� ��
�����lH���p(���������������������b����[33333��������nh(�&�&& ��$; 60:a4:23:ff:fe:02:39:7b ��#;60:a4:23:ff:fe:02:39:7b&F4��h; 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:a0GL-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:24GLEDOPTO(��F;60:a4:23:ff:fe:02:36:24GL-B-008P(��n;60:a4:23:ff:fe:02:36:93GL-B-008P '��f;60:a4:23:ff:fe:02:36:a0GLEDOPTO ��; 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:93GLEDOPTO&�#��j;60:a4:23:ff:fe:02:36:a0@
��p;60:a4:23:ff:fe:02:36:93t ��;; 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:63GL-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:f3GLEDOPTO$��#;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:f3GL-B-001P��0; 58:8e:81:ff:fe:15:e3:ff3 ��/; 58:8e:81:ff:fe:15:e3:ff1�
9 ��[;60:a4:23:ff:fe:02:38:59A�� ; 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:f4���v; cc:cc:cc:ff:fe:29:27:01��u; 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:91p ��; 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 �Y�M��G��>��4��*�xxxxxxxxxx"�ffff��K@
�
�
�U��8 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4��%.U��7 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%MU��6 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%`U��5 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04�S%-T��4 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%0T��3 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%?U��2 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M�%�U��1 ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G�%�T��0 ;;;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:1e�q%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:04�S%�
E
"���u�sN(3��qK�'���qK��������L&jY�/
��
�
�
�
h&uS/}[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:a4�c!; 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:ab�Q$;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
,��M��B
�
�
6��/�~%
�
r
� f �Z�R��I��>��2�&�s�h�Q��d ;;;60:a4:23:ff:fe:02:38:af7f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$cV��c ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30��%�U��b ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%kU��a ;;;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:91���U��^ ;;;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:1e�q%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��%IU��Z ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%;V��Y ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93�%%�U��X ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4��%,U��W ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%aU��V ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%?U��U ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04�S%;T��T ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%5U��S ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M�%�T��R ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%LT��Q ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%5R��P ;;;60:a4:23:ff:fe:02:36:a07f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$�V��O ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:32:30��%�U��N ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%@U��M ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69�%?V��L ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91��%�U��K ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%?V��J ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b��%�U��I ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%=V��H ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93�%%�U��G ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%IU��F ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%IT��E ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%BU��D ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G�%�T��C ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%PR��B ;;;60:a4:23:ff:fe:02:36:247f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83$�U��A ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6500:15:8d:00:05:1e:0e:32��U��@ ;;;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:1e�q%8U��: ;;;60:a4:23:ff:fe:02:39:7b7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%5V��9 ;;;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
�
ajL����
�
` �
�
�
���
�%�����uN�iB��7G$����p`9��sLwP�+*������
;%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 E��T; V00:15:8d:00:05:1e:0e:32�!�!�!@$d)ke!\f+=�
!��#��o;60:a4:23:ff:fe:02:2f:4dj�$��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:69B"��}; ec:1b:bd:ff:fe:94:18:a4�#��d;80:4b:50:ff:fe:41:67:d4j�$��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:afc#��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:42L��q;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:59j�$��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:f3j�#��E;60:a4:23:ff:fe:02:30:39�
��k;60:a4:23:ff:fe:02:2f:4dL#��K;80:4b:50:ff:fe:41:59:63j�$��J;80:4b:50:ff:fe:41:59:63��
��A;60:a4:23:ff:fe:02:2f:96L#��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:7e���z; 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:b4j�$��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:96j�"��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:9cA��%; 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:39b ��=; 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
-d�Q��F
�
�
<��2��*
�
w
� k �_�R��K��C��:��.�z �l�dT��m ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%T��l ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4�% T��k ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%dV��j ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42�_%�V��i ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%�V��h ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%�V��g ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%�V��f ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%�U��e ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93�%%(V��d ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%�U��c ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%kU��b ;;;ec:1b:bd:ff:fe:33:a0:047f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%�T��a ;;;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$V��Z ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%�U��Y ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42�_%MV��X ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69�%�V��W ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%�U��V ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%U��U ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b��%FV��T ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%�U��S ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%TU��R ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93�%%FV��Q ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%�V��P ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%�U��O ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04�S%sT��N ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%wT��M ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M�%AT��L ;;;60:a4:23:ff:fe:02:38:597f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G�%*����tV����� 4 �
�
�����vX�f�
�
�
l
N
0
������������������2�2�X;60:a4:23:ff:fe:02:51:70�T;60:a4:23:ff:fe:02:3b:b4�1;60:a4:23:ff:fe:02:3b:b4�0;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:70�W;60:a4:23:ff:fe:02:51:70�V;60:a4:23:ff:fe:02:51:70�U;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:70�X;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:70�Z;60:a4:23:ff:fe:02:51:70�Y;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:b4�5;60:a4:23:ff:fe:02:3b:b4�4;60:a4:23:ff:fe:02:3b:b4�3;60:a4:23:ff:fe:02:3b:b4�2;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:b4�6;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:b4�8;60:a4:23:ff:fe:02:3b:b4�7;60:a4:23:ff:fe:02:51:70�_;60:a4:23:ff:fe:02:3b:b4�9;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:70�c;60:a4:23:ff:fe:02:51:70�b;60:a4:23:ff:fe:02:51:70�a;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:70�h;60:a4:23:ff:fe:02:51:70�g;60:a4:23:ff:fe:02:51:70�f;60:a4:23:ff:fe:02:51:70�e;60:a4:23:ff:fe:02:51:70�d;68:0a:e2:ff:fe:70:00:69�32%+,
c����~^>
�
�
�
�
}
]
=
����~_@!����eF'
�
�
�
�
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:04�p; ec:1b:bd:ff:fe:33:a0:04�o; ec:1b:bd:ff:fe:33:a0:04�n; ec:1b:bd:ff:fe:33:a0:04�m; ec:1b:bd:ff:fe:33:a0:04�l; ec:1b:bd:ff:fe:33:a0:04�k; ec:1b:bd:ff:fe:33:a0:04�j; 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:7e�X; ec:1b:bd:ff:fe:37:72:7e�W; ec:1b:bd:ff:fe:37:72:7e�V; ec:1b:bd:ff:fe:37:72:7e�U; ec:1b:bd:ff:fe:37:72:7e�T; ec:1b:bd:ff:fe:37:72:7e�S; ec:1b:bd:ff:fe:37:72:7e�R; ec:1b:bd:ff:fe:37:72:7e�Q; ec:1b:bd:ff:fe:94:18:a4�P; ec:1b:bd:ff:fe:94:18:a4�O; ec:1b:bd:ff:fe:94:18:a4�N; ec:1b:bd:ff:fe:94:18:a4�M; ec:1b:bd:ff:fe:94:18:a4�L; ec:1b:bd:ff:fe:94:18:a4�K; ec:1b:bd:ff:fe:94:18:a4�J; ec:1b:bd:ff:fe:94:18:a4�I; ec:1b:bd:ff:fe:94:18:a4�H;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
)��M��B
�
�
7��,�|$
�
s
� f
�[�O��F��=��/�}#�p�U��C ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%|U��B ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69�%kV��A ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%�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:63���U��> ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%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:04�S%�T��9 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%vU��8 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%�T��7 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%\T��6 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%sU��5 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4�%�T��4 ;;;80:4b:50:ff:fe:41:58:f37f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%\V��3 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%�U��2 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%jU��1 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%pU��0 ;;;80:4b:50:ff:fe:41:59:637f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%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&����r�6���������jL.X:���8
�
�
�
�
~
`
B
$
� � � � p R 4 �����b�����fH*����vP2�n���n�;80:4b:50:ff:fe:41:58:f3�C;80:4b:50:ff:fe:41:58:f3�B;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:f3�A;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�9;80:4b:50:ff:fe:41:58:f3�8;80:4b:50:ff:fe:41:58:f3�7;80:4b:50:ff:fe:41:58:f3�6;80:4b:50:ff:fe:41:58:f3�5;80:4b:50:ff:fe:41:58:f3�4;80:4b:50:ff:fe:41:59:63�3;80:4b:50:ff:fe:41:59:63�2;80:4b:50:ff:fe:41:59:63�1;80:4b:50:ff:fe:41:59:63�0;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
,��P��C
�
�
7��+�w
�
m
� c
�V��I��<��1�&�u�h�Z�V��< ;;;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:1e�q%�V��: ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%�V��9 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%�V��8 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4��%�V��7 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%�V��6 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04�S%�U��5 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%�U��4 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%�T��3 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:24M�%OU��2 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%�U��1 ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3�%�U��0 ;;;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:1e�q%�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:1e�q%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:04�S%�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
,��P��E
�
�
8��-�z!
�
n
� f
�\�O��B��6��(�z"�p�e�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�%�T��9 ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%8Q��8 ;;;60:a4:23:ff:fe:02:34:917f:4f:6a:3c:05:70:f6:65cc:cc:cc:ff:fe:a5:f2:83[U��7 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%\V��6 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42�_%�V��5 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69�%�V��4 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%�V��3 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%�U��2 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b��%BU��1 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%tV��0 ;;;60:a4:23:ff:fe:02:30:397f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%�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:04�S%�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:7e�G%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:04�S%�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
,��O��G
�
�
;��.�{"
�
n
� b
�Y�N��E��7��*�v�o�f
�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:1e�q%�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:7e�G%�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:04�S%�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:c3u6�U��{ ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%sV��z ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42�_%�U��y ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6568:0a:e2:ff:fe:70:00:69�%kV��x ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%�V��w ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%�U��v ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:39:7b��%DU��u ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%pV��t ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:37:72:7e�G%�U��s ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:93�%%AV��r ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:94:18:a4��%�V��q ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%�U��p ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%kV��o ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04�S%�U��n ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%�T��m ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%vT��l ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:58:f3�%RT��k ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:60K%mT��j ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:67:d4�%oU��i ;;;60:a4:23:ff:fe:02:2f:4d7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:30:39%�
,��P��G
�
�
<��/�{!
�
n
� e
�]�T��G��:��,�y�l�f
�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:69��U��= ;;;60:a4:23:ff:fe:02:3b:b47f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:34:91��%G
-e�P��G
�
�
:��/�|$
�
s
� k �c
�W��L��A��5��,�|$�q�eU�� ;;;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:04�S%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:1e�q%]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:7e�G%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:04�S%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%wU��z ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:af��%V��y ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:42�_%�U��x ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:51:70Ջ%{U��w ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:54:1e�q%GU��v ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6580:4b:50:ff:fe:41:59:63��%V��u ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:3b:b4��%�U��t ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:9c�%TV��s ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:65ec:1b:bd:ff:fe:33:a0:04�S%�U��r ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:96m�%�T��q ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:38:59XM%&T��p ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:36:a0G�%T��o ;;;ec:1b:bd:ff:fe:37:72:7e7f:4f:6a:3c:05:70:f6:6560:a4:23:ff:fe:02:2f:4d=%kT��n ;;;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
��m��7
�
c�r�w
'!�3indexattribute_idxattributesCREATE 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:ec�O,;84:18:26:00:00:d9:86:e7�A+;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:ad�y;00:15:8d:00:02:36:84:85}5;00:15:8d:00:02:36:91:2f�?;00:15:8d:00:02:04:54:40�5;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/
���Yu�A
��=
)��
�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
���
@���pL(��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:7a���a ; 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
'
����qR3����~`@"
�
�
�
n
N
1
�
�+
����uV7 � � � x Y : 1�X;84:18:26:00:00:d9:86:e7W;84:18:26:00:00:01:30:50�Y; 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:b1��3; 00:15:8d:00:02:c3:af:b12; 00:15:8d:00:02:c3:af:b11; 00:15:8d:00:02:c3:95:8a��0; 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
�
} � b��dC
�
��Y:���
�
�
n
O
.
����t4T�%����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:b1��4; 00:15:8d:00:02:c3:af:b13; 00:15:8d:00:02:c3:af:b12 ; 00:15:8d:00:02:c3:95:8a��1; 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
+
���bA����mG%
�
�
w
V
� � � O ����oM*��{H$���}[$����\1����[8��pL)��f>���p����gg"��-;84:18:26:00:00:04:a7:c9�N��v; 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.aq13��n; 600:15:8d:00:02:36:91:2flumi.sensor_wleak.aq1��+; 00:15:8d:00:02:b5:f7:cd4��5; 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:801�c; 100:15:8d:00:02:b5:f7:cdbattery_voltage_mV�1�
; 100:15:8d:00:02:36:84:85battery_voltage_mV�1�s; 100:15:8d:00:02:36:91:2fbattery_voltage_mV�1�H; 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.aq2�h; 00:15:8d:00:02:04:54:40�T; 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:adMRVL�q; 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:85LUMI�Z; 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:404�z; 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 L���oK'��|O��
G
���xr@���oG
���wS-���sK��w �P �
�
l�!����pM***************�����nI_;�U�/���!��6;84:18:26:00:00:02:b7:13''��O; 00:15:8d:00:02:b5:f7:cd���K"��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:far9��9;@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:33��I; 84:18:26:00:00:02:44:33"��F;84:18:26:00:00:02:44:33���5; 84:18:26:00:00:02:44:33#��4;84:18:26:00:00:02:44:33r9��3;@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:13���0; 84:18:26:00:00:02:b7:13!��B;84:18:26:00:00:02:b7:13��3; 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:c9�9��+;@84:18:26:00:00:04:a7:c9LIGHTIFY A19 Tunable White$��*;84:18:26:00:00:04:a7:c9OSRAMN��v; 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:c9���j; 00:15:8d:00:02:c3:af:b1
���; 00:0d:6f:00:0d:2e:8d:e9>��.; 00:15:8d:00:02:36:84:85Cy��a; 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:e91��N; 84:18:26:00:00:00:d0:fa��I; 00:12:4b:00:19:36:95:c1��G; 84:18:26:00:00:04:a7:c9��F; 00:0d:6f:00:0c:a7:42:a6[��y; 00:0d:6f:00:0d:2e:8d:72>4��k; 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
"����zX6����iG%
�
�
�
z
X
6
� � � � f D !����wU3����`=����vS��3����h6��kI'����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:40�U ; 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:a6U�!; 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
sMN��g�p�a?O"-��`>�����D���#������
��
�
}
[
9
���{X4���~];
�
�
�
�
b
A
�)�� S 1 �p�Maaaaaaaaaa5���~[8����d@����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:85�T��!#; 00:15:8d:00:02:36:91:2f�U�!; 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!R�7E"; 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:8a�U/"; 00:15:8d:00:02:c3:af:b1T�!; 00:15:8d:00:02:c3:af:b1 T�#; 00:15:8d:00:02:c3:af:b1�T�!; 00:15:8d:00:02:36:91:2f
Z!; 00:15:8d:00:02:04:a0:62 R�1; 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:62�R�!; 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:b5�R�!; 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!R�1; 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:71U�S#; 00:15:8d:00:02:b8:bb:71��v!; 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:cd�O$; 00:15:8d:00:02:b5:f7:cd�*$; 00:15:8d:00:02:b5:f7:cd�N#; 00:15:8d:00:02:b5:f7:cdU�M#; 00:15:8d:00:02:b5:f7:cd�j"; 00:15:8d:00:02:b5:f7:cd��1; 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:b5�E$!; 00:15:8d:00:02:36:91:2f
U�
yW����gG'�������X�w8���2qQ % ����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:40��J; 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:72�d ;00:0d:6f:00:0d:2e:8d:72c;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:a6�G; 00:0d:6f:00:0c:a7:42:a6F; 00:0d:6f:00:0c:a7:42:a6E; 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
����vV8����{\=
�
�
�
�!���|Z8����qP.���bA ����M,����dAyX7����m
^
=
� � � � sxW6���c@ 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:ad�z; 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:b1��k; 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:8a��g; 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:b5��3; 00:15:8d:00:02:b5:2d:b54�
}����kL-����tV8
�
�
�����nP1����}^@!
�
�
�
�
m
O
0
� � � � y \ > ����gJ,����y[>N1����z\=�����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:b1��j; 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:8a��f; 00:15:8d:00:02:c3:95:8ae; 00:15:8d:00:02:c3:95:8ad;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:72Y; 00:0d:6f:00:0d:2e:8d:72z; 94:10:3e:f6:bf:42:8a:ad�y; 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:40��I; 00:15:8d:00:02:04:54:40H; 00:15:8d:00:02:04:54:40G; 00:0d:6f:00:0c:a7:42:a6�F; 00:0d:6f:00:0c:a7:42:a6E; 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: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:b5��2; 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
����wW7��
�
x
X
8
� � � � u U 5 ����q��wYqqqqq�T;84:18:26:00:00:d9:86:e7��S;84:18:26:00:00:d9:86:e7�R;84:18:26:00:00:d9:86:e7�Q;84:18:26:00:00:d9:86:e7�P;84:18:26:00:00:d9:86:e7�O;84:18:26:00:00:d9:86:e7�N;84:18:26:00:00:d9:86:e7�M;84:18:26:00:00:d9:86:e7�L;84:18:26:00:00:d9:86:e7�K;84:18:26:00:00:01:30:50��J;84:18:26:00:00:01:30:50�I;84:18:26:00:00:01:30:50�H;84:18:26:00:00:01:30:50�G;84:18:26:00:00:01:30:50�F;84:18:26:00:00:01:30:50�E;84:18:26:00:00:01:30:50�D;84:18:26:00:00:01:30:50�C;84:18:26:00:00:01:30:50��X; 00:15:8d:00:01:eb:71:ec�W; 00:15:8d:00:01:eb:71:ec�V; 00:15:8d:00:01:eb:71:ec�U; 00:15:8d:00:01:eb:71:ec�9;84:18:26:00:00:00:d1:df��8;84:18:26:00:00:00:d1:df�7;84:18:26:00:00:00:d1:df�6;84:18:26:00:00:00:d1:df�5;84:18:26:00:00:00:d1:df�4;84:18:26:00:00:00:d1:df�3;84:18:26:00:00:00:d1:df�2;84:18:26:00:00:00:d1:df�1;84:18:26:00:00:00:d1:df�0;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
���xU+���y
�
���Z3���c<�����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:fa���7; 84:18:26:00:00:00:d0:fa$��M;84:18:26:00:00:04:a7:c9%!��!; 00:0d:6f:00:0b:12:4b:62���K;00:12:4b:00:19:36:95:c1"��N; 00:0d:6f:00:0c:a7:42:a6A��*; 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:c9���r;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:50���u; 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:e7���M; 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:e7r9��a;@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:ad���b; 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�!�(!�!�$
!JTd��1; 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!@[�
=p����j; 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:e9��b; 00:0d:6f:00:0b:12:4b:621l!��j; 00:15:8d:00:02:b5:2d:b5���i; 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�!�(!�!$
!��d��P; 00:15:8d:00:02:b5:2d:b5 ��; 00:15:8d:00:02:04:54:40!�=��; F00:15:8d:00:02:04:54:40�!�(!�!�$
!�:d��C; 00:15:8d:00:02:04:a0:62
FB���sN(���yU0
[8���`<�
�
�
�
~
[
8
����_9����fB
�
�
�
�
j
G
$
� � � q L &���h�B ���~%;84:18:26:00:00:d9:86:e7P�";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:50P�";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:c9U�$;84:18:26:00:00:04:a7:c9
�#;84:18:26:00:00:04:a7:c9�-#;84:18:26:00:00:04:a7:c9�0#;84:18:26:00:00:04:a7:c9�W#;84:18:26:00:00:04:a7:c9�2"; 84:18:26:00:00:04:a7:c9�3";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:dfU�$;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:faP�$;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:faK�zigpy-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()