pax_global_header00006660000000000000000000000064150072017540014513gustar00rootroot0000000000000052 comment=37241f146044457554b1c870c2ca39c53cda2034 zigpy-deconz-0.25.0/000077500000000000000000000000001500720175400142215ustar00rootroot00000000000000zigpy-deconz-0.25.0/.coveragerc000066400000000000000000000000341500720175400163370ustar00rootroot00000000000000[run] source = zigpy_deconz zigpy-deconz-0.25.0/.github/000077500000000000000000000000001500720175400155615ustar00rootroot00000000000000zigpy-deconz-0.25.0/.github/workflows/000077500000000000000000000000001500720175400176165ustar00rootroot00000000000000zigpy-deconz-0.25.0/.github/workflows/ci.yml000066400000000000000000000005601500720175400207350ustar00rootroot00000000000000name: CI # yamllint disable-line rule:truthy on: push: pull_request: ~ jobs: shared-ci: uses: zigpy/workflows/.github/workflows/ci.yml@main with: CODE_FOLDER: zigpy_deconz CACHE_VERSION: 2 PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit MINIMUM_COVERAGE_PERCENTAGE: 97 secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} zigpy-deconz-0.25.0/.github/workflows/matchers/000077500000000000000000000000001500720175400214245ustar00rootroot00000000000000zigpy-deconz-0.25.0/.github/workflows/matchers/codespell.json000066400000000000000000000004001500720175400242630ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "codespell", "severity": "warning", "pattern": [ { "regexp": "^(.+):(\\d+):\\s(.+)$", "file": 1, "line": 2, "message": 3 } ] } ] } zigpy-deconz-0.25.0/.github/workflows/matchers/flake8.json000066400000000000000000000011011500720175400234620ustar00rootroot00000000000000{ "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-deconz-0.25.0/.github/workflows/matchers/python.json000066400000000000000000000005201500720175400236350ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "python", "pattern": [ { "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", "file": 1, "line": 2 }, { "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", "message": 2 } ] } ] } zigpy-deconz-0.25.0/.github/workflows/publish-to-pypi.yml000066400000000000000000000003621500720175400234070ustar00rootroot00000000000000name: 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-deconz-0.25.0/.gitignore000066400000000000000000000015101500720175400162060ustar00rootroot00000000000000# 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 zigpy-deconz-0.25.0/.pre-commit-config.yaml000066400000000000000000000017561500720175400205130ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/PyCQA/autoflake rev: v2.0.2 hooks: - id: autoflake - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black args: - --quiet - --safe - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - pydocstyle==5.1.1 - Flake8-pyproject==1.2.3 - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: - id: isort - 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/charliermarsh/ruff-pre-commit rev: v0.0.261 hooks: - id: ruff args: - --fixzigpy-deconz-0.25.0/COPYING000066400000000000000000000012241500720175400152530ustar00rootroot00000000000000zigpy-deconz Copyright (C) 2018 Daniel Schmidt 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-deconz-0.25.0/Contributors.md000066400000000000000000000002171500720175400172400ustar00rootroot00000000000000# Contributors - [Alexei Chetroi] (https://github.com/Adminiuga) - [damarco] (https://github.com/damarco) - [Hedda] (https://github.com/Hedda) zigpy-deconz-0.25.0/LICENSE000066400000000000000000001045131500720175400152320ustar00rootroot00000000000000 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-deconz-0.25.0/README.md000066400000000000000000000051271500720175400155050ustar00rootroot00000000000000# zigpy-deconz [![Build Status](https://travis-ci.org/zigpy/zigpy-deconz.svg?branch=master)](https://travis-ci.org/zigpy/zigpy-deconz) [![Coverage](https://coveralls.io/repos/github/zigpy/zigpy-deconz/badge.svg?branch=master)](https://coveralls.io/github/zigpy/zigpy-deconz?branch=master) [zigpy-deconz](https://github.com/zigpy/zigpy-deconz) is a Python 3 implementation for the [Zigpy](https://github.com/zigpy/) project to implement [deCONZ](https://www.dresden-elektronik.de/funktechnik/products/software/pc/deconz/) based [Zigbee](https://www.zigbee.org) radio devices. The goal of this project to add native support for the Dresden-Elektronik/Phoscon deCONZ based ZigBee modules in Home Assistant via [zigpy](https://github.com/zigpy/). This library uses the deCONZ serial protocol for communicating with [ConBee](https://phoscon.de/en/conbee), [ConBee II (ConBee 2)](https://phoscon.de/en/conbee2), [ConBee III (ConBee 3)](https://phoscon.de/en/conbee3), [RaspBee](https://phoscon.de/en/raspbee), and [RaspBee II (RaspBee 2)](https://phoscon.de/en/raspbee2) adapters from [Dresden-Elektronik](https://github.com/dresden-elektronik/)/[Phoscon](https://phoscon.de). # Releases via PyPI Tagged versions are also released via PyPI - https://pypi.org/project/zigpy-deconz/ - https://pypi.org/project/zigpy-deconz/#history - https://pypi.org/project/zigpy-deconz/#files # External documentation and reference Note! Latest official documentation for the deCONZ serial protocol can currently be obtained by following link in Dresden-Elektronik GitHub repository here: - https://github.com/dresden-elektronik/deconz-serial-protocol - https://github.com/dresden-elektronik/deconz-serial-protocol/issues/2 For reference, here is a list of unrelated projects that also use the same deCONZ serial protocol for other implementations: - https://github.com/Equidamoid/pyconz/commits/master - https://github.com/mozilla-iot/deconz-api - https://github.com/adetante/deconz-sp - https://github.com/frederic34/plugin-nodeBee # How to contribute If you are looking to make a contribution 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 Some developers might also be interested in receiving donations in the form of hardware such as Zigbee modules or devices, and even if such donations are most often donated with no strings attached it could in many cases help the developers motivation and indirect improve the development of this project. zigpy-deconz-0.25.0/pyproject.toml000066400000000000000000000036711500720175400171440ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0.0", "wheel", "setuptools-git-versioning<2"] build-backend = "setuptools.build_meta" [project] name = "zigpy-deconz" dynamic = ["version"] description = "A library which communicates with Deconz radios for zigpy" urls = {repository = "https://github.com/zigpy/zigpy-deconz"} authors = [ {name = "Daniel Schmidt", email = "schmidt.d@aon.at"} ] readme = "README.md" license = {text = "GPL-3.0"} requires-python = ">=3.8" dependencies = [ "voluptuous", "zigpy>=0.80.0", 'async-timeout; python_version<"3.11"', ] [tool.setuptools.packages.find] exclude = ["tests", "tests.*"] [project.optional-dependencies] testing = [ "pytest>=7.1.2", "pytest-asyncio>=0.19.0", "pytest-timeout>=2.1.0", "pytest-mock>=3.8.2", "pytest-cov>=3.0.0", ] [tool.setuptools-git-versioning] enabled = true [tool.isort] profile = "black" # will group `import x` and `from x import` of the same module. force_sort_within_sections = true known_first_party = ["zigpy_deconz", "tests"] forced_separate = "tests" combine_as_imports = true [tool.mypy] ignore_errors = true [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" [tool.flake8] exclude = [".venv", ".git", ".tox", "docs", "venv", "bin", "lib", "deps", "build"] # To work with Black max-line-length = 88 # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # E501: line too long # D202 No blank lines allowed after function docstring ignore = [ "W503", "E203", "E501", "D202", "D103", "D102", "D101", # TODO: remove these once docstrings are added ] per-file-ignores = ["tests/*:F811,F401,F403"] [tool.codespell] ignore-words-list = ["IntStruct"] skip = ["./.*", "tests/*", "pyproject.toml"] quiet-level = 2 [tool.coverage.report] exclude_also = [ "raise AssertionError", "raise NotImplementedError", "if (typing\\.)?TYPE_CHECKING:", "@(abc\\.)?abstractmethod", ] zigpy-deconz-0.25.0/requirements_test.txt000066400000000000000000000003421500720175400205430ustar00rootroot00000000000000# Test dependencies. asynctest isort black flake8 codecov colorlog codespell mypy==1.2.0 pre-commit pylint pytest-cov pytest-sugar pytest-timeout pytest-asyncio>=0.17 pytest>=7.1.3 zigpy>=0.54.1 ruff==0.0.261 Flake8-pyprojectzigpy-deconz-0.25.0/setup.py000066400000000000000000000001521500720175400157310ustar00rootroot00000000000000"""Setup stub for legacy builds.""" import setuptools if __name__ == "__main__": setuptools.setup() zigpy-deconz-0.25.0/tests/000077500000000000000000000000001500720175400153635ustar00rootroot00000000000000zigpy-deconz-0.25.0/tests/__init__.py000066400000000000000000000000251500720175400174710ustar00rootroot00000000000000"""Tests modules.""" zigpy-deconz-0.25.0/tests/async_mock.py000066400000000000000000000003351500720175400200640ustar00rootroot00000000000000"""Mock utilities that are async aware.""" import sys if sys.version_info[:2] < (3, 8): from asynctest.mock import * # noqa AsyncMock = CoroutineMock # noqa: F405 else: from unittest.mock import * # noqa zigpy-deconz-0.25.0/tests/test_api.py000066400000000000000000001076111500720175400175530ustar00rootroot00000000000000"""Test api module.""" import asyncio import collections import inspect import logging import sys import pytest import zigpy.config import zigpy.types as zigpy_t if sys.version_info[:2] < (3, 11): from async_timeout import timeout as asyncio_timeout else: from asyncio import timeout as asyncio_timeout from zigpy_deconz import api as deconz_api, types as t, uart import zigpy_deconz.exception import zigpy_deconz.zigbee.application from .async_mock import AsyncMock, MagicMock, call, patch DEVICE_CONFIG = {zigpy.config.CONF_DEVICE_PATH: "/dev/null"} @pytest.fixture async def gateway(): return uart.Gateway(api=None) @pytest.fixture async def api(gateway, mock_command_rsp): loop = asyncio.get_running_loop() async def mock_connect(config, api): transport = MagicMock() transport.close = MagicMock( side_effect=lambda: loop.call_soon(gateway.connection_lost, None) ) gateway._api = api gateway.connection_made(transport) return gateway with patch("zigpy_deconz.uart.connect", side_effect=mock_connect): controller = MagicMock( spec_set=zigpy_deconz.zigbee.application.ControllerApplication ) api = deconz_api.Deconz( controller, {zigpy.config.CONF_DEVICE_PATH: "/dev/null"} ) mock_command_rsp( command_id=deconz_api.CommandId.device_state, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(8), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(0), }, ) mock_command_rsp( command_id=deconz_api.CommandId.read_parameter, params={ "parameter_id": deconz_api.NetworkParameter.protocol_version, "parameter": t.Bytes(b""), }, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(10), "payload_length": t.uint16_t(3), "parameter_id": deconz_api.NetworkParameter.protocol_version, "parameter": t.Bytes(t.uint16_t(270).serialize()), }, ) mock_command_rsp( command_id=deconz_api.CommandId.version, params={"reserved": t.uint8_t(0)}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "version": deconz_api.FirmwareVersion(645400320), }, ) yield api @pytest.fixture async def mock_command_rsp(gateway): def inner(command_id, params, rsp, *, rsp_command=None, replace=False): if ( getattr(getattr(gateway.send, "side_effect", None), "_handlers", None) is None ): def receiver(data): command, _ = deconz_api.Command.deserialize(data) tx_schema, _ = deconz_api.COMMAND_SCHEMAS[command.command_id] schema = {} for k, v in tx_schema.items(): if v in (deconz_api.FRAME_LENGTH, deconz_api.PAYLOAD_LENGTH): v = t.uint16_t elif not inspect.isclass(v): v = type(v) schema[k] = v kwargs, rest = t.deserialize_dict(command.payload, schema) for params, rsp_command, mock in receiver._handlers[command.command_id]: if rsp_command is None: rsp_command = command.command_id if all(kwargs[k] == v for k, v in params.items()): _, rx_schema = deconz_api.COMMAND_SCHEMAS[rsp_command] ret = mock(**kwargs) asyncio.get_running_loop().call_soon( gateway._api.data_received, deconz_api.Command( command_id=rsp_command, seq=command.seq, payload=t.serialize_dict(ret, rx_schema), ).serialize(), ) receiver._handlers = collections.defaultdict(list) gateway.send = MagicMock(side_effect=receiver) if replace: gateway.send.side_effect._handlers[command_id].clear() mock = MagicMock(return_value=rsp) gateway.send.side_effect._handlers[command_id].append( (params, rsp_command, mock) ) return mock return inner def send_network_state( api, network_state: deconz_api.NetworkState2 = deconz_api.NetworkState2.CONNECTED, device_state: deconz_api.DeviceStateFlags = ( deconz_api.DeviceStateFlags.APSDE_DATA_CONFIRM ), ): _, rx_schema = deconz_api.COMMAND_SCHEMAS[deconz_api.CommandId.device_state_changed] data = deconz_api.Command( command_id=deconz_api.CommandId.device_state_changed, seq=api._seq, payload=t.serialize_dict( { "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(7), "device_state": deconz_api.DeviceState( network_state=network_state, device_state=device_state, ), "reserved": t.uint8_t(0), }, rx_schema, ), ).serialize() asyncio.get_running_loop().call_later(0.01, api.data_received, data) async def test_connect(api, mock_command_rsp): await api.connect() async def test_connect_failure(api, mock_command_rsp): transport = None def mock_version(*args, **kwargs): nonlocal transport transport = api._uart._transport raise asyncio.TimeoutError() with patch.object(api, "version", side_effect=mock_version): # We connect but fail to probe with pytest.raises(asyncio.TimeoutError): await api.connect() assert api._uart is None assert len(transport.close.mock_calls) == 1 async def test_close(api): await api.connect() uart = api._uart uart.disconnect = AsyncMock() await api.disconnect() assert api._uart is None assert uart.disconnect.call_count == 1 def test_commands(): for cmd, (tx_schema, rx_schema) in deconz_api.COMMAND_SCHEMAS.items(): assert isinstance(cmd, deconz_api.CommandId) assert isinstance(tx_schema, dict) or tx_schema is None assert isinstance(rx_schema, dict) async def test_command(api): await api.connect() addr = t.DeconzAddress() addr.address_mode = t.AddressMode.NWK addr.address = t.NWK(0x0000) params = { "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(61), "payload_length": t.uint16_t(54), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "dst_addr": addr, "dst_ep": t.uint8_t(0), "src_addr": addr, "src_ep": t.uint8_t(0), "profile_id": t.uint16_t(0), "cluster_id": t.uint16_t(32772), "asdu": t.LongOctetString( b"\x0f\x00\x00\x00\x1a\x01\x04\x01\x00\x04\x00\x05\x00\x00\x06" b"\x00\n\x00\x19\x00\x01\x05\x04\x01\x00 \x00\x00\x05\x02\x05" ), "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(175), "lqi": t.uint8_t(69), "reserved3": t.uint8_t(189), "reserved4": t.uint8_t(82), "reserved5": t.uint8_t(0), "reserved6": t.uint8_t(0), "rssi": t.int8s(27), } data = deconz_api.Command( command_id=deconz_api.CommandId.aps_data_indication, seq=api._seq, payload=t.serialize_dict( params, deconz_api.COMMAND_SCHEMAS[deconz_api.CommandId.aps_data_indication][1], ), ).serialize() asyncio.get_running_loop().call_later(0.01, api.data_received, data) rsp = await api._command( cmd=deconz_api.CommandId.aps_data_indication, flags=t.DataIndicationFlags.Include_Both_NWK_And_IEEE, ) assert rsp == params async def test_command_lock(api, mock_command_rsp): await api.connect() for i in range(4): mock_command_rsp( command_id=deconz_api.CommandId.version, params={"reserved": t.uint8_t(i)}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "version": deconz_api.FirmwareVersion(i), }, replace=(i == 0), ) async with api._command_lock: tasks = [ asyncio.create_task( api._command(cmd=deconz_api.CommandId.version, reserved=0) ), asyncio.create_task( api._command(cmd=deconz_api.CommandId.version, reserved=1) ), asyncio.create_task( api._command(cmd=deconz_api.CommandId.version, reserved=2) ), asyncio.create_task( api._command(cmd=deconz_api.CommandId.version, reserved=3) ), ] await asyncio.sleep(0.1) assert not any(t.done() for t in tasks) responses = await asyncio.gather(*tasks) for index, rsp in enumerate(responses): assert rsp["version"] == index async def test_command_timeout(api): await api.connect() with patch.object(deconz_api, "COMMAND_TIMEOUT", 0.1): with pytest.raises(asyncio.TimeoutError): await api._command( cmd=deconz_api.CommandId.change_network_state, network_state=deconz_api.NetworkState.OFFLINE, ) async def test_command_not_connected(api): api._uart = None with pytest.raises(deconz_api.CommandError): await api._command(cmd=deconz_api.CommandId.version, reserved=0) async def test_data_received(api, mock_command_rsp): await api.connect() src_addr = t.DeconzAddress() src_addr.address_mode = t.AddressMode.NWK src_addr.address = t.NWK(0xE695) dst_addr = t.DeconzAddress() dst_addr.address_mode = t.AddressMode.NWK dst_addr.address = t.NWK(0x0000) mock_command_rsp( command_id=deconz_api.CommandId.aps_data_indication, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(80), "payload_length": t.uint16_t(73), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "dst_addr": dst_addr, "dst_ep": t.uint8_t(1), "src_addr": src_addr, "src_ep": t.uint8_t(1), "profile_id": t.uint16_t(260), "cluster_id": t.uint16_t(0x0000), "asdu": t.LongOctetString( b"\x18\x1b\x01\x04\x00\x00B\x0eIKEA of Sweden" b"\x05\x00\x00B\x17TRADFRI wireless dimmer" ), "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(175), "lqi": t.uint8_t(255), "reserved3": t.uint8_t(142), "reserved4": t.uint8_t(98), "reserved5": t.uint8_t(0), "reserved6": t.uint8_t(0), "rssi": t.int8s(-49), }, ) # Unsolicited device_state_changed api.data_received(bytes.fromhex("0e2f000700ae00")) await asyncio.sleep(0.1) api._app.packet_received.assert_called_once_with( zigpy_t.ZigbeePacket( src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xE695), src_ep=1, dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=0x0000, data=zigpy_t.SerializableBytes( b"\x18\x1b\x01\x04\x00\x00B\x0eIKEA of Sweden" b"\x05\x00\x00B\x17TRADFRI wireless dimmer" ), tx_options=zigpy_t.TransmitOptions.NONE, radius=0, non_member_radius=0, lqi=255, rssi=-49, ) ) async def test_read_parameter(api, mock_command_rsp): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.read_parameter, params={ "parameter_id": deconz_api.NetworkParameter.nwk_update_id, "parameter": t.Bytes(b""), }, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "payload_length": t.uint16_t(2), "parameter_id": deconz_api.NetworkParameter.nwk_update_id, "parameter": t.Bytes(b"\x00"), }, ) mock_command_rsp( command_id=deconz_api.CommandId.read_parameter, params={ "parameter_id": deconz_api.NetworkParameter.network_key, "parameter": t.Bytes(b"\x00"), }, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(25), "payload_length": t.uint16_t(18), "parameter_id": deconz_api.NetworkParameter.network_key, "parameter": t.Bytes(b"\x00M\x07p\xb6\x0b|\x90\xad\\\x07\x8a8\xa9M\xf6["), }, ) rsp = await api.read_parameter(deconz_api.NetworkParameter.nwk_update_id) assert rsp == 0x00 rsp = await api.read_parameter(deconz_api.NetworkParameter.network_key, 0) assert rsp == deconz_api.IndexedKey( index=0, key=deconz_api.KeyData.convert( "4d:07:70:b6:0b:7c:90:ad:5c:07:8a:38:a9:4d:f6:5b" ), ) async def test_write_parameter(api, mock_command_rsp): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.write_parameter, params={ "parameter_id": deconz_api.NetworkParameter.watchdog_ttl, "parameter": t.uint32_t(600).serialize(), }, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(8), "payload_length": t.uint16_t(1), "parameter_id": deconz_api.NetworkParameter.watchdog_ttl, }, ) await api.write_parameter(deconz_api.NetworkParameter.watchdog_ttl, 600) async def test_write_parameter_failure(api, mock_command_rsp): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.write_parameter, params={ "parameter_id": deconz_api.NetworkParameter.watchdog_ttl, "parameter": t.uint32_t(600).serialize(), }, rsp={ "status": deconz_api.Status.INVALID_VALUE, "frame_length": t.uint16_t(8), "payload_length": t.uint16_t(1), "parameter_id": deconz_api.NetworkParameter.watchdog_ttl, }, ) with pytest.raises(deconz_api.CommandError): await api.write_parameter(deconz_api.NetworkParameter.watchdog_ttl, 600) @pytest.mark.parametrize( "protocol_ver, firmware_ver", [ (0x010A, 0x123405DD), (0x010B, 0x123405DD), (0x010A, 0x123407DD), (0x010B, 0x123407DD), ], ) async def test_version(protocol_ver, firmware_ver, api, mock_command_rsp): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.read_parameter, params={ "parameter_id": deconz_api.NetworkParameter.protocol_version, "parameter": t.Bytes(b""), }, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(10), "payload_length": t.uint16_t(3), "parameter_id": deconz_api.NetworkParameter.protocol_version, "parameter": t.Bytes(t.uint16_t(protocol_ver).serialize()), }, replace=True, ) mock_command_rsp( command_id=deconz_api.CommandId.version, params={"reserved": t.uint8_t(0)}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "version": deconz_api.FirmwareVersion(firmware_ver), }, replace=True, ) r = await api.version() assert r == firmware_ver assert api.protocol_version == protocol_ver assert api.firmware_version == firmware_ver @pytest.mark.parametrize( "data, network_state", ((0x00, "OFFLINE"), (0x01, "JOINING"), (0x02, "CONNECTED"), (0x03, "LEAVING")), ) def test_device_state_network_state(data, network_state): """Test device state flag.""" extra = b"the rest of the data\xaa\x55" for other_fields in (0x04, 0x08, 0x0C, 0x10, 0x24, 0x28, 0x30, 0x2C): new_data = t.uint8_t(data | other_fields).serialize() state, rest = deconz_api.DeviceState.deserialize(new_data + extra) assert rest == extra assert state.network_state == deconz_api.NetworkState[network_state] assert state.serialize() == new_data @pytest.mark.parametrize( "value, name", ( (0x00, "SUCCESS"), (0xA0, "APS_ASDU_TOO_LONG"), (0x01, "MAC_PAN_AT_CAPACITY"), (0xC9, "NWK_UNSUPPORTED_ATTRIBUTE"), (0xFE, "undefined_0xfe"), ), ) def test_tx_status(value, name): """Test tx status undefined values.""" i = deconz_api.TXStatus(value) assert i == value assert i.value == value assert i.name == name extra = b"\xaa\55" data = t.uint8_t(value).serialize() status, rest = deconz_api.TXStatus.deserialize(data + extra) assert rest == extra assert isinstance(status, deconz_api.TXStatus) assert status == value assert status.value == value assert status.name == name @pytest.mark.parametrize("relays", (None, [], [0x1234, 0x5678])) async def test_aps_data_request_relays(relays, api, mock_command_rsp): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.aps_data_request, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "payload_length": t.uint16_t(2), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "request_id": t.uint8_t(0x00), }, ) await api.aps_data_request( req_id=0x00, dst_addr_ep=t.DeconzAddressEndpoint.deserialize(b"\x02\xaa\x55\x01")[0], profile=0x0104, cluster=0x0007, src_ep=0x01, aps_payload=b"aps payload", relays=relays, ) with pytest.raises(ValueError) as exc: await api.aps_data_request( req_id=0x00, dst_addr_ep=t.DeconzAddressEndpoint.deserialize(b"\x02\xaa\x55\x01")[0], profile=0x0104, cluster=0x0007, src_ep=None, # This is not possible aps_payload=b"aps payload", ) assert "has non-trailing optional argument" in str(exc.value) async def test_aps_data_request_retries_failure(api, mock_command_rsp): await api.connect() mock_rsp = mock_command_rsp( command_id=deconz_api.CommandId.aps_data_request, params={}, rsp={ "status": deconz_api.Status.FAILURE, "frame_length": t.uint16_t(9), "payload_length": t.uint16_t(2), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "request_id": t.uint8_t(0x00), }, ) with pytest.raises(deconz_api.CommandError): await api.aps_data_request( req_id=0x00, dst_addr_ep=t.DeconzAddressEndpoint.deserialize(b"\x02\xaa\x55\x01")[0], profile=0x0104, cluster=0x0007, src_ep=1, aps_payload=b"aps payload", ) assert len(mock_rsp.mock_calls) == 1 async def test_aps_data_request_locking(caplog, api, mock_command_rsp): await api.connect() # No free slots send_network_state(api, device_state=deconz_api.DeviceStateFlags.APSDE_DATA_CONFIRM) await asyncio.sleep(0.1) mock_rsp = mock_command_rsp( command_id=deconz_api.CommandId.aps_data_request, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "payload_length": t.uint16_t(2), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "request_id": t.uint8_t(0x00), }, ) with caplog.at_level(logging.DEBUG): send = asyncio.create_task( api.aps_data_request( req_id=0x00, dst_addr_ep=t.DeconzAddressEndpoint.deserialize(b"\x02\xaa\x55\x01")[0], profile=0x0104, cluster=0x0007, src_ep=1, aps_payload=b"aps payload", ) ) await asyncio.sleep(0.1) assert "Waiting for free slots to become available" in caplog.text assert len(mock_rsp.mock_calls) == 0 send_network_state( api, device_state=deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE, ) await send assert len(mock_rsp.mock_calls) == 1 async def test_connection_lost(api): await api.connect() app = api._app = MagicMock() err = RuntimeError() api.connection_lost(err) app.connection_lost.assert_called_once_with(err) async def test_unknown_command(api, caplog): await api.connect() assert 0xFF not in deconz_api.COMMAND_SCHEMAS with caplog.at_level(logging.WARNING): api.data_received(b"\xFF\xAA\xBB") assert ( "Unknown command received: Command(command_id=," " seq=170, payload=b'\\xbb')" ) in caplog.text async def test_bad_command_parsing(api, caplog): await api.connect() assert 0xFF not in deconz_api.COMMAND_SCHEMAS with caplog.at_level(logging.DEBUG): api.data_received( bytes.fromhex( "172c002f0028002e02000000020000000000" "028011000300000010400f3511472b004000" # "2b000000af45838600001b" # truncated ) ) assert ( "Failed to parse command Command(command_id=" "" ) in caplog.text caplog.clear() with caplog.at_level(logging.DEBUG): api.data_received(bytes.fromhex("0d03000d0000077826") + b"TEST") assert ( "Unparsed data remains after frame" in caplog.text and "b'TEST'" in caplog.text ) async def test_bad_response_status(api, mock_command_rsp): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.write_parameter, params={ "parameter_id": deconz_api.NetworkParameter.nwk_update_id, "parameter": t.uint8_t(123).serialize(), }, rsp={ "status": deconz_api.Status.FAILURE, "frame_length": t.uint16_t(8), "payload_length": t.uint16_t(1), "parameter_id": deconz_api.NetworkParameter.nwk_update_id, }, ) with pytest.raises(deconz_api.CommandError) as exc: await api.write_parameter(deconz_api.NetworkParameter.nwk_update_id, 123) assert isinstance(exc.value, deconz_api.CommandError) assert exc.value.status == deconz_api.Status.FAILURE async def test_data_poller(api, mock_command_rsp): await api.connect() dst_addr_ep = t.DeconzAddressEndpoint() dst_addr_ep.address_mode = t.AddressMode.NWK dst_addr_ep.address = t.NWK(0x0000) dst_addr_ep.endpoint = t.uint8_t(0) src_addr = t.DeconzAddress() src_addr.address_mode = t.AddressMode.NWK src_addr.address = t.NWK(0xE695) dst_addr = t.DeconzAddress() dst_addr.address_mode = t.AddressMode.NWK dst_addr.address = t.NWK(0x0000) mock_command_rsp( command_id=deconz_api.CommandId.aps_data_confirm, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(19), "payload_length": t.uint16_t(12), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( # Include a data indication flag to trigger a poll deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE | deconz_api.DeviceStateFlags.APSDE_DATA_INDICATION ), ), "request_id": t.uint8_t(16), "dst_addr": dst_addr_ep, "src_ep": t.uint8_t(0), "confirm_status": deconz_api.TXStatus.SUCCESS, "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(0), "reserved3": t.uint8_t(0), "reserved4": t.uint8_t(0), }, ) mock_command_rsp( command_id=deconz_api.CommandId.aps_data_indication, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(80), "payload_length": t.uint16_t(73), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ), "dst_addr": dst_addr, "dst_ep": t.uint8_t(1), "src_addr": src_addr, "src_ep": t.uint8_t(1), "profile_id": t.uint16_t(260), "cluster_id": t.uint16_t(0x0000), "asdu": t.LongOctetString( b"\x18\x1b\x01\x04\x00\x00B\x0eIKEA of Sweden" b"\x05\x00\x00B\x17TRADFRI wireless dimmer" ), "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(175), "lqi": t.uint8_t(255), "reserved3": t.uint8_t(142), "reserved4": t.uint8_t(98), "reserved5": t.uint8_t(0), "reserved6": t.uint8_t(0), "rssi": t.int8s(-49), }, ) # Take us offline for a moment send_network_state(api, network_state=deconz_api.NetworkState2.OFFLINE) await asyncio.sleep(0.1) # Bring us back online with just a data confirmation to kick things off send_network_state( api, network_state=deconz_api.NetworkState2.CONNECTED, device_state=deconz_api.DeviceStateFlags.APSDE_DATA_CONFIRM, ) await asyncio.sleep(0.1) # Both callbacks have been called api._app.handle_tx_confirm.assert_called_once_with(16, deconz_api.TXStatus.SUCCESS) assert len(api._app.packet_received.mock_calls) == 1 # The task is cancelled on close task = api._data_poller_task await api.disconnect() assert api._data_poller_task is None assert task.done() async def test_get_device_state(api, mock_command_rsp): await api.connect() device_state = deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE ), ) mock_command_rsp( command_id=deconz_api.CommandId.device_state, params={}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(8), "device_state": device_state, "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(0), }, ) assert (await api.get_device_state()) == device_state async def test_change_network_state(api, mock_command_rsp): api._command = AsyncMock() await api.change_network_state(new_state=deconz_api.NetworkState.OFFLINE) assert api._command.mock_calls == [ call( deconz_api.CommandId.change_network_state, network_state=deconz_api.NetworkState.OFFLINE, ) ] async def test_add_neighbour(api, mock_command_rsp): api._command = AsyncMock() await api.add_neighbour( nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), mac_capability_flags=0x12, ) assert api._command.mock_calls == [ call( deconz_api.CommandId.update_neighbor, action=deconz_api.UpdateNeighborAction.ADD, nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), mac_capability_flags=0x12, ) ] async def test_add_neighbour_conbee3_success(api): api._command = AsyncMock(wraps=api._command) api._uart = AsyncMock() # Simulate a good but invalid response from the Conbee III asyncio.get_running_loop().call_later( 0.001, lambda: api.data_received( b"\x1d" + bytes([api._seq - 1]) + b"\x00\x06\x00\x01" ), ) await api.add_neighbour( nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), mac_capability_flags=0x12, ) assert api._command.mock_calls == [ call( deconz_api.CommandId.update_neighbor, action=deconz_api.UpdateNeighborAction.ADD, nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), mac_capability_flags=0x12, ) ] async def test_add_neighbour_conbee3_failure(api): api._command = AsyncMock(wraps=api._command) api._uart = AsyncMock() # Simulate a bad response from the Conbee III asyncio.get_running_loop().call_later( 0.001, lambda: api.data_received( b"\x1d" + bytes([api._seq - 1]) + b"\x01\x06\x00\x01" ), ) with pytest.raises(deconz_api.CommandError): await api.add_neighbour( nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), mac_capability_flags=0x12, ) assert api._command.mock_calls == [ call( deconz_api.CommandId.update_neighbor, action=deconz_api.UpdateNeighborAction.ADD, nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), mac_capability_flags=0x12, ) ] async def test_cb3_device_state_callback_bug(api, mock_command_rsp): mock_command_rsp( command_id=deconz_api.CommandId.version, params={"reserved": t.uint8_t(0)}, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "version": deconz_api.FirmwareVersion(0x26450900), }, replace=True, ) await api.connect() device_state = deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=deconz_api.DeviceStateFlags.APSDE_DATA_CONFIRM, ) assert api._device_state != device_state _, rx_schema = deconz_api.COMMAND_SCHEMAS[deconz_api.CommandId.device_state] api.data_received( deconz_api.Command( command_id=deconz_api.CommandId.device_state, seq=api._seq, payload=t.serialize_dict( { "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(8), "device_state": device_state, "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(0), }, rx_schema, ), ).serialize() ) await asyncio.sleep(0.01) assert api._device_state == device_state async def test_firmware_responding_with_wrong_type_with_correct_seq( api, mock_command_rsp, caplog ): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.aps_data_confirm, params={}, # Completely different response rsp_command=deconz_api.CommandId.version, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "version": deconz_api.FirmwareVersion(0x26450900), }, ) with caplog.at_level(logging.DEBUG): with pytest.raises(asyncio.TimeoutError): # We wait beyond 500ms to make sure it triggers async with asyncio_timeout(0.6): await api.send_command(deconz_api.CommandId.aps_data_confirm) assert ( "Firmware responded incorrectly (Response is mismatched! Sent" " , received ), retrying" ) in caplog.text async def test_firmware_responding_with_wrong_type_with_correct_seq_eventual_response( api, mock_command_rsp, caplog ): await api.connect() mock_command_rsp( command_id=deconz_api.CommandId.aps_data_confirm, params={}, # Completely different response rsp_command=deconz_api.CommandId.version, rsp={ "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(9), "version": deconz_api.FirmwareVersion(0x26450900), }, ) with caplog.at_level(logging.DEBUG): _, rx_schema = deconz_api.COMMAND_SCHEMAS[deconz_api.CommandId.aps_data_confirm] asyncio.get_running_loop().call_later( 0.1, api.data_received, deconz_api.Command( command_id=deconz_api.CommandId.aps_data_confirm, seq=api._seq, payload=t.serialize_dict( { "status": deconz_api.Status.SUCCESS, "frame_length": t.uint16_t(19), "payload_length": t.uint16_t(12), "device_state": deconz_api.DeviceState( network_state=deconz_api.NetworkState2.CONNECTED, device_state=( deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE | deconz_api.DeviceStateFlags.APSDE_DATA_INDICATION ), ), "request_id": t.uint8_t(16), "dst_addr": t.DeconzAddressEndpoint.deserialize( b"\x02\xaa\x55\x01" )[0], "src_ep": t.uint8_t(0), "confirm_status": deconz_api.TXStatus.SUCCESS, "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(0), "reserved3": t.uint8_t(0), "reserved4": t.uint8_t(0), }, rx_schema, ), ).serialize(), ) async with asyncio_timeout(0.2): rsp = await api.send_command(deconz_api.CommandId.aps_data_confirm) assert rsp["request_id"] == 16 assert ( "Firmware responded incorrectly (Response is mismatched! Sent" " , received ), retrying" ) not in caplog.text def test_get_command_priority(api): assert ( api._get_command_priority( deconz_api.Command(command_id=deconz_api.CommandId.write_parameter) ) > api._get_command_priority( deconz_api.Command(command_id=deconz_api.CommandId.update_neighbor) ) > api._get_command_priority( deconz_api.Command(command_id=deconz_api.CommandId.aps_data_request) ) ) zigpy-deconz-0.25.0/tests/test_application.py000066400000000000000000000462241500720175400213070ustar00rootroot00000000000000"""Test application module.""" import asyncio import logging from unittest import mock import pytest import zigpy.application import zigpy.config import zigpy.device from zigpy.types import EUI64, Channels, KeyData import zigpy.zdo.types as zdo_t from zigpy_deconz import types as t import zigpy_deconz.api as deconz_api import zigpy_deconz.exception import zigpy_deconz.zigbee.application as application from .async_mock import AsyncMock, MagicMock, patch, sentinel ZIGPY_NWK_CONFIG = { zigpy.config.CONF_NWK: { zigpy.config.CONF_NWK_PAN_ID: 0x4567, zigpy.config.CONF_NWK_EXTENDED_PAN_ID: "11:22:33:44:55:66:77:88", zigpy.config.CONF_NWK_UPDATE_ID: 22, zigpy.config.CONF_NWK_KEY: [0xAA] * 16, }, } @pytest.fixture def device_path(): return "/dev/null" @pytest.fixture def api(): """Return API fixture.""" api = MagicMock(spec_set=zigpy_deconz.api.Deconz(None, None)) api.get_device_state = AsyncMock( return_value=deconz_api.DeviceState(deconz_api.NetworkState.CONNECTED) ) api.write_parameter = AsyncMock() api.firmware_version = deconz_api.FirmwareVersion(0x26580700) # So the protocol version is effectively infinite api._protocol_version.__ge__.return_value = True api._protocol_version.__lt__.return_value = False api.protocol_version.__ge__.return_value = True api.protocol_version.__lt__.return_value = False return api @pytest.fixture def app(device_path, api): config = { **ZIGPY_NWK_CONFIG, zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: device_path}, } app = application.ControllerApplication(config) api.change_network_state = AsyncMock() device_state = MagicMock() device_state.network_state.__eq__.return_value = True api.get_device_state = AsyncMock(return_value=device_state) p1 = patch.object(app, "_api", api) p2 = patch.object(app, "_delayed_neighbour_scan") p3 = patch.object(app, "_change_network_state", wraps=app._change_network_state) with p1, p2, p3: yield app @pytest.fixture def ieee(): return EUI64.deserialize(b"\x00\x01\x02\x03\x04\x05\x06\x07")[0] @pytest.fixture def nwk(): return t.uint16_t(0x0100) @pytest.fixture def addr_ieee(ieee): addr = t.DeconzAddress() addr.address_mode = t.AddressMode.IEEE addr.address = ieee return addr @pytest.fixture def addr_nwk(nwk): addr = t.DeconzAddress() addr.address_mode = t.AddressMode.NWK addr.address = nwk return addr @pytest.fixture def addr_nwk_and_ieee(nwk, ieee): addr = t.DeconzAddress() addr.address_mode = t.AddressMode.NWK_AND_IEEE addr.address = nwk addr.ieee = ieee return addr @patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_POLL_TIME", 0.001) @pytest.mark.parametrize( "proto_ver, target_state, returned_state", [ (0x0107, deconz_api.NetworkState.CONNECTED, deconz_api.NetworkState.CONNECTED), (0x0106, deconz_api.NetworkState.CONNECTED, deconz_api.NetworkState.CONNECTED), (0x0107, deconz_api.NetworkState.OFFLINE, deconz_api.NetworkState.CONNECTED), (0x0107, deconz_api.NetworkState.CONNECTED, deconz_api.NetworkState.OFFLINE), ], ) async def test_start_network(app, proto_ver, target_state, returned_state): app.load_network_info = AsyncMock() app.restore_neighbours = AsyncMock() app.add_endpoint = AsyncMock() app._api.get_device_state = AsyncMock( return_value=deconz_api.DeviceState( device_state=deconz_api.DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE, network_state=returned_state, ) ) app._api._protocol_version = proto_ver app._api.protocol_version = proto_ver if ( target_state == deconz_api.NetworkState.CONNECTED and returned_state != deconz_api.NetworkState.CONNECTED ): with pytest.raises(zigpy.exceptions.FormationFailure): await app.start_network() return with patch.object(application.DeconzDevice, "initialize", AsyncMock()): await app.start_network() assert app.load_network_info.await_count == 1 assert app._change_network_state.await_count == 1 assert ( app._change_network_state.await_args_list[0][0][0] == deconz_api.NetworkState.CONNECTED ) if proto_ver >= application.PROTO_VER_NEIGBOURS: assert app.restore_neighbours.await_count == 1 else: assert app.restore_neighbours.await_count == 0 async def test_permit(app, nwk): app._api.write_parameter = AsyncMock() time_s = 30 await app.permit_ncp(time_s) assert app._api.write_parameter.call_count == 1 assert app._api.write_parameter.call_args_list[0][0][1] == time_s async def test_connect(app): def new_api(*args): api = MagicMock() api.connect = AsyncMock() return api with patch.object(application, "Deconz", new=new_api): app._api = None await app.connect() assert app._api is not None assert app._api.connect.await_count == 1 async def test_connect_failure(app): with patch.object(application, "Deconz") as api_mock: api = api_mock.return_value = MagicMock() api.connect = AsyncMock(side_effect=RuntimeError("Broken")) api.disconnect = AsyncMock() app._api = None with pytest.raises(RuntimeError): await app.connect() assert app._api is None api.connect.assert_called_once() api.disconnect.assert_called_once() async def test_disconnect(app): api_disconnect = app._api.disconnect = AsyncMock() await app.disconnect() assert app._api is None assert api_disconnect.call_count == 1 async def test_disconnect_no_api(app): app._api = None await app.disconnect() async def test_disconnect_close_error(app): app._api.write_parameter = MagicMock( side_effect=zigpy_deconz.exception.CommandError("Error", status=1, command=None) ) await app.disconnect() async def test_permit_with_link_key(app): app._api.write_parameter = AsyncMock() app.permit = AsyncMock() await app.permit_with_link_key( node=t.EUI64.convert("00:11:22:33:44:55:66:77"), link_key=KeyData.convert("aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd"), ) assert app._api.write_parameter.mock_calls == [ mock.call( deconz_api.NetworkParameter.link_key, deconz_api.LinkKey( ieee=t.EUI64.convert("00:11:22:33:44:55:66:77"), key=KeyData.convert("aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd"), ), ) ] assert app.permit.mock_calls == [mock.call(mock.ANY)] async def test_deconz_dev_add_to_group(app, nwk, device_path): group = MagicMock() app._groups = MagicMock() app._groups.add_group.return_value = group deconz = application.DeconzDevice("Conbee II", app, sentinel.ieee, nwk) deconz.endpoints = { 0: sentinel.zdo, 1: sentinel.ep1, 2: sentinel.ep2, } await deconz.add_to_group(sentinel.grp_id, sentinel.grp_name) assert group.add_member.call_count == 2 assert app.groups.add_group.call_count == 1 assert app.groups.add_group.call_args[0][0] is sentinel.grp_id assert app.groups.add_group.call_args[0][1] is sentinel.grp_name async def test_deconz_dev_remove_from_group(app, nwk, device_path): group = MagicMock() app.groups[sentinel.grp_id] = group deconz = application.DeconzDevice("Conbee II", app, sentinel.ieee, nwk) deconz.endpoints = { 0: sentinel.zdo, 1: sentinel.ep1, 2: sentinel.ep2, } await deconz.remove_from_group(sentinel.grp_id) assert group.remove_member.call_count == 2 def test_deconz_props(nwk, device_path): deconz = application.DeconzDevice("Conbee II", app, sentinel.ieee, nwk) assert deconz.manufacturer is not None assert deconz.model is not None async def test_deconz_new(app, nwk, device_path, monkeypatch): mock_init = AsyncMock() monkeypatch.setattr(zigpy.device.Device, "_initialize", mock_init) deconz = await application.DeconzDevice.new(app, sentinel.ieee, nwk, "Conbee II") assert isinstance(deconz, application.DeconzDevice) assert mock_init.call_count == 1 mock_init.reset_mock() mock_dev = MagicMock() mock_dev.endpoints = { 0: MagicMock(), 1: MagicMock(), 22: MagicMock(), } app.devices[sentinel.ieee] = mock_dev deconz = await application.DeconzDevice.new(app, sentinel.ieee, nwk, "Conbee II") assert isinstance(deconz, application.DeconzDevice) assert mock_init.call_count == 0 def test_tx_confirm_success(app): tsn = 123 req = app._pending[tsn] = MagicMock() app.handle_tx_confirm(tsn, sentinel.status) assert req.result.set_result.call_count == 1 assert req.result.set_result.call_args[0][0] is sentinel.status def test_tx_confirm_dup(app, caplog): caplog.set_level(logging.DEBUG) tsn = 123 req = app._pending[tsn] = MagicMock() req.result.set_result.side_effect = asyncio.InvalidStateError app.handle_tx_confirm(tsn, sentinel.status) assert req.result.set_result.call_count == 1 assert req.result.set_result.call_args[0][0] is sentinel.status assert any(r.levelname == "DEBUG" for r in caplog.records) assert "probably duplicate response" in caplog.text def test_tx_confirm_unexpcted(app, caplog): app.handle_tx_confirm(123, 0x00) assert any(r.levelname == "WARNING" for r in caplog.records) assert "Unexpected transmit confirm for request id" in caplog.text async def test_reset_watchdog(app): """Test watchdog.""" app._api.protocol_version = application.PROTO_VER_WATCHDOG app._api.get_device_state = AsyncMock() app._api.write_parameter = AsyncMock() await app._watchdog_feed() assert len(app._api.get_device_state.mock_calls) == 0 assert len(app._api.write_parameter.mock_calls) == 1 app._api.protocol_version = application.PROTO_VER_WATCHDOG - 1 app._api.get_device_state.reset_mock() app._api.write_parameter.reset_mock() await app._watchdog_feed() assert len(app._api.get_device_state.mock_calls) == 1 assert len(app._api.write_parameter.mock_calls) == 0 async def test_force_remove(app): """Test forcibly removing a device.""" await app.force_remove(sentinel.device) async def test_restore_neighbours(app, caplog): """Test neighbour restoration.""" # FFD, Rx on when idle device_1 = app.add_device(nwk=0x0001, ieee=EUI64.convert("00:00:00:00:00:00:00:01")) device_1.node_desc = zdo_t.NodeDescriptor(1, 64, 142, 0xBEEF, 82, 82, 0, 82, 0) # RFD, Rx on when idle device_2 = app.add_device(nwk=0x0002, ieee=EUI64.convert("00:00:00:00:00:00:00:02")) device_2.node_desc = zdo_t.NodeDescriptor(1, 64, 142, 0xBEEF, 82, 82, 0, 82, 0) device_3 = app.add_device(nwk=0x0003, ieee=EUI64.convert("00:00:00:00:00:00:00:03")) device_3.node_desc = None # RFD, Rx off when idle device_5 = app.add_device(nwk=0x0005, ieee=EUI64.convert("00:00:00:00:00:00:00:05")) device_5.node_desc = zdo_t.NodeDescriptor(2, 64, 128, 0xBEEF, 82, 82, 0, 82, 0) # RFD, Rx off when idle (duplicate) device_6 = app.add_device(nwk=0x0005, ieee=EUI64.convert("00:00:00:00:00:00:00:06")) device_6.node_desc = zdo_t.NodeDescriptor(2, 64, 128, 0xBEEF, 82, 82, 0, 82, 0) coord = MagicMock() coord.ieee = EUI64.convert("aa:aa:aa:aa:aa:aa:aa:aa") app.devices[coord.ieee] = coord app.state.node_info.ieee = coord.ieee app.topology.neighbors[coord.ieee] = [ zdo_t.Neighbor(ieee=device_1.ieee), zdo_t.Neighbor(ieee=device_2.ieee), zdo_t.Neighbor(ieee=device_3.ieee), zdo_t.Neighbor(ieee=EUI64.convert("00:00:00:00:00:00:00:04")), zdo_t.Neighbor(ieee=device_5.ieee), zdo_t.Neighbor(ieee=device_6.ieee), ] max_neighbors = 1 def mock_add_neighbour(nwk, ieee, mac_capability_flags): nonlocal max_neighbors max_neighbors -= 1 if max_neighbors < 0: raise zigpy_deconz.exception.CommandError( "Failure", status=deconz_api.Status.FAILURE, command=None, ) p = patch.object(app, "_api", spec_set=zigpy_deconz.api.Deconz(None, None)) with p as api_mock: err = zigpy_deconz.exception.CommandError( "Failure", status=deconz_api.Status.FAILURE, command=None ) api_mock.add_neighbour = AsyncMock(side_effect=[None, err, err, err]) with caplog.at_level(logging.DEBUG): await app.restore_neighbours() assert caplog.text.count("Failed to add device to neighbor table") == 1 assert api_mock.add_neighbour.call_count == 2 assert api_mock.add_neighbour.await_count == 2 @patch("zigpy_deconz.zigbee.application.DELAY_NEIGHBOUR_SCAN_S", 0) async def test_delayed_scan(): """Delayed scan.""" coord = MagicMock() config = { zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "usb0"}, zigpy.config.CONF_DATABASE: "tmp", } app = application.ControllerApplication(config) with patch.object(app, "get_device", return_value=coord): with patch.object(app, "topology", AsyncMock()): await app._delayed_neighbour_scan() app.topology.scan.assert_called_once_with(devices=[coord]) @patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_POLL_TIME", 0.001) async def test_change_network_state(app): app._api.get_device_state = AsyncMock( side_effect=[ deconz_api.DeviceState(deconz_api.NetworkState.OFFLINE), deconz_api.DeviceState(deconz_api.NetworkState.JOINING), deconz_api.DeviceState(deconz_api.NetworkState.CONNECTED), ] ) app._api._protocol_version = application.PROTO_VER_WATCHDOG app._api.protocol_version = application.PROTO_VER_WATCHDOG await app._change_network_state(deconz_api.NetworkState.CONNECTED, timeout=0.01) ENDPOINT = zdo_t.SimpleDescriptor( endpoint=None, profile=1, device_type=2, device_version=3, input_clusters=[4], output_clusters=[5], ) @pytest.mark.parametrize( "descriptor, slots, target_slot", [ (ENDPOINT.replace(endpoint=1), {0: ENDPOINT.replace(endpoint=2)}, 0), # Prefer the endpoint with the same ID ( ENDPOINT.replace(endpoint=1), { 0: ENDPOINT.replace(endpoint=2, profile=1234), 1: ENDPOINT.replace(endpoint=1, profile=1234), }, 1, ), ], ) async def test_add_endpoint(app, descriptor, slots, target_slot): async def read_param(param_id, index): assert param_id == deconz_api.NetworkParameter.configure_endpoint if index not in slots: raise zigpy_deconz.exception.CommandError( "Unsupported", status=deconz_api.Status.UNSUPPORTED, command=None, ) else: return deconz_api.IndexedEndpoint(index=index, descriptor=slots[index]) app._api.read_parameter = AsyncMock(side_effect=read_param) app._api.write_parameter = AsyncMock() await app.add_endpoint(descriptor) app._api.write_parameter.assert_called_once_with( deconz_api.NetworkParameter.configure_endpoint, deconz_api.IndexedEndpoint(index=target_slot, descriptor=descriptor), ) async def test_add_endpoint_no_free_space(app): async def read_param(param_id, index): assert param_id == deconz_api.NetworkParameter.configure_endpoint assert index in (0x00, 0x01) raise zigpy_deconz.exception.CommandError( "Unsupported", status=deconz_api.Status.UNSUPPORTED, command=None, ) app._api.read_parameter = AsyncMock(side_effect=read_param) app._api.write_parameter = AsyncMock() app._written_endpoints.add(0x00) app._written_endpoints.add(0x01) with pytest.raises(ValueError): await app.add_endpoint(ENDPOINT.replace(endpoint=1)) app._api.write_parameter.assert_not_called() async def test_add_endpoint_no_unnecessary_writes(app): async def read_param(param_id, index): assert param_id == deconz_api.NetworkParameter.configure_endpoint if index > 0x01: raise zigpy_deconz.exception.CommandError( "Unsupported", status=deconz_api.Status.UNSUPPORTED, command=None, ) return deconz_api.IndexedEndpoint( index=index, descriptor=ENDPOINT.replace(endpoint=1) ) app._api.read_parameter = AsyncMock(side_effect=read_param) app._api.write_parameter = AsyncMock() await app.add_endpoint(ENDPOINT.replace(endpoint=1)) app._api.write_parameter.assert_not_called() # Writing another endpoint will cause a write await app.add_endpoint(ENDPOINT.replace(endpoint=2)) app._api.write_parameter.assert_called_once_with( deconz_api.NetworkParameter.configure_endpoint, deconz_api.IndexedEndpoint(index=1, descriptor=ENDPOINT.replace(endpoint=2)), ) async def test_reset_network_info(app): app.form_network = AsyncMock() await app.reset_network_info() app.form_network.assert_called_once() async def test_energy_scan_conbee_2(app): with mock.patch.object( zigpy.application.ControllerApplication, "energy_scan", return_value={c: c for c in Channels.ALL_CHANNELS}, ): results = await app.energy_scan( channels=Channels.ALL_CHANNELS, duration_exp=0, count=1 ) assert results == {c: c * 3 for c in Channels.ALL_CHANNELS} async def test_energy_scan_conbee_3(app): app._api.firmware_version = deconz_api.FirmwareVersion(0x26580900) type(app)._device = AsyncMock() app._device.zdo.Mgmt_NWK_Update_req = AsyncMock( side_effect=zigpy.exceptions.DeliveryError("error") ) with pytest.raises(zigpy.exceptions.DeliveryError): await app.energy_scan(channels=Channels.ALL_CHANNELS, duration_exp=0, count=1) app._device.zdo.Mgmt_NWK_Update_req = AsyncMock( side_effect=[ asyncio.TimeoutError(), list( { "Status": zdo_t.Status.SUCCESS, "ScannedChannels": Channels.ALL_CHANNELS, "TotalTransmissions": 0, "TransmissionFailures": 0, "EnergyValues": [i for i in range(11, 26 + 1)], }.values() ), ] ) results = await app.energy_scan( channels=Channels.ALL_CHANNELS, duration_exp=0, count=1 ) assert results == {c: c for c in Channels.ALL_CHANNELS} async def test_channel_migration(app): app._api.write_parameter = AsyncMock() app._change_network_state = AsyncMock() await app._move_network_to_channel(new_channel=26, new_nwk_update_id=0x12) assert app._api.write_parameter.mock_calls == [ mock.call( deconz_api.NetworkParameter.channel_mask, Channels.from_channel_list([26]) ), mock.call(deconz_api.NetworkParameter.nwk_update_id, 0x12), ] assert app._change_network_state.mock_calls == [ mock.call(deconz_api.NetworkState.OFFLINE), mock.call(deconz_api.NetworkState.CONNECTED), ] zigpy-deconz-0.25.0/tests/test_exception.py000066400000000000000000000005501500720175400207720ustar00rootroot00000000000000"""Test exceptions.""" from unittest import mock import zigpy_deconz.exception def test_command_error(): ex = zigpy_deconz.exception.CommandError( mock.sentinel.message, status=mock.sentinel.status, command=mock.sentinel.command, ) assert ex.status is mock.sentinel.status assert ex.command is mock.sentinel.command zigpy-deconz-0.25.0/tests/test_network_state.py000066400000000000000000000237421500720175400216750ustar00rootroot00000000000000"""Test `load_network_info` and `write_network_info` methods.""" import importlib.metadata import pytest from zigpy.exceptions import ControllerException, NetworkNotFormed import zigpy.state as app_state import zigpy.types as t import zigpy.zdo.types as zdo_t import zigpy_deconz import zigpy_deconz.api import zigpy_deconz.exception import zigpy_deconz.zigbee.application as application from tests.async_mock import AsyncMock, patch from tests.test_application import api, app, device_path # noqa: F401 def merge_objects(obj: object, update: dict) -> None: for key, value in update.items(): if "." not in key: setattr(obj, key, value) else: subkey, rest = key.split(".", 1) merge_objects(getattr(obj, subkey), {rest: value}) @pytest.fixture def node_info(): return 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, manufacturer="dresden elektronik", model="Conbee II", version="0x26580700", ) @pytest.fixture def network_info(node_info): return 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=node_info.ieee, tx_counter=8712428, ), key_table=[], children=[], nwk_addresses={}, stack_specific={}, source=f"zigpy-deconz@{importlib.metadata.version('zigpy-deconz')}", metadata={"deconz": {"version": "0x26580700"}}, ) @patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001) @patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001) @pytest.mark.parametrize( "channel_mask, channel, security_level, fw_supports_fc, logical_type", [ ( t.Channels.from_channel_list([15]), 15, 0, True, zdo_t.LogicalType.Coordinator, ), ( t.Channels.from_channel_list([15]), 15, 0, False, zdo_t.LogicalType.Coordinator, ), ( t.Channels.from_channel_list([15, 20]), 15, 5, True, zdo_t.LogicalType.Coordinator, ), ( t.Channels.from_channel_list([15, 20, 25]), None, 5, True, zdo_t.LogicalType.Router, ), (None, 15, 5, True, zdo_t.LogicalType.Coordinator), ], ) async def test_write_network_info( app, # noqa: F811 network_info, node_info, channel_mask, channel, security_level, fw_supports_fc, logical_type, ): """Test that network info is correctly written.""" params = {} async def write_parameter(param, *args): if ( not fw_supports_fc and param == zigpy_deconz.api.NetworkParameter.nwk_frame_counter ): raise zigpy_deconz.exception.CommandError( "Command is unsupported", status=zigpy_deconz.api.Status.UNSUPPORTED, command=None, ) params[param.name] = args app._change_network_state = AsyncMock() app._api.write_parameter = AsyncMock(side_effect=write_parameter) network_info = network_info.replace( channel=channel, channel_mask=channel_mask, security_level=security_level, ) node_info = node_info.replace(logical_type=logical_type) if not fw_supports_fc: with pytest.raises( ControllerException, match=( "Please upgrade your adapter firmware. Firmware version 0x26580700 does" " not support writing the network key frame counter, which is required" " for migration to succeed." ), ): await app.write_network_info( network_info=network_info, node_info=node_info, ) return await app.write_network_info( network_info=network_info, node_info=node_info, ) params = { call[0][0].name: call[0][1:] for call in app._api.write_parameter.await_args_list } assert params["nwk_frame_counter"] == (network_info.network_key.tx_counter,) if node_info.logical_type == zdo_t.LogicalType.Coordinator: assert params["aps_designed_coordinator"] == (1,) else: assert params["aps_designed_coordinator"] == (0,) assert params["nwk_address"] == (node_info.nwk,) assert params["mac_address"] == (node_info.ieee,) if channel is not None: assert params["channel_mask"] == ( t.Channels.from_channel_list([network_info.channel]), ) elif channel_mask is not None: assert params["channel_mask"] == (network_info.channel_mask,) else: assert False assert params["use_predefined_nwk_panid"] == (True,) assert params["nwk_panid"] == (network_info.pan_id,) assert params["aps_extended_panid"] == (network_info.extended_pan_id,) assert params["nwk_update_id"] == (network_info.nwk_update_id,) assert params["network_key"] == ( zigpy_deconz.api.IndexedKey(index=0, key=network_info.network_key.key), ) assert params["trust_center_address"] == (node_info.ieee,) assert params["link_key"] == ( zigpy_deconz.api.LinkKey(ieee=node_info.ieee, key=network_info.tc_link_key.key), ) if security_level == 0: assert params["security_mode"] == (zigpy_deconz.api.SecurityMode.NO_SECURITY,) else: assert params["security_mode"] == (zigpy_deconz.api.SecurityMode.ONLY_TCLK,) @patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001) @patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001) @pytest.mark.parametrize( "error, param_overrides, nwk_state_changes, node_state_changes", [ (None, {}, {}, {}), ( None, {("aps_designed_coordinator",): 0x00}, {}, {"logical_type": zdo_t.LogicalType.Router}, ), ( None, { ("aps_extended_panid",): t.EUI64.convert("00:00:00:00:00:00:00:00"), ("nwk_extended_panid",): t.EUI64.convert("0D:49:91:99:AE:CD:3C:35"), }, {}, {}, ), (NetworkNotFormed, {("current_channel",): 0}, {}, {}), ( None, { ("nwk_frame_counter",): zigpy_deconz.exception.CommandError( "Some error", status=zigpy_deconz.api.Status.UNSUPPORTED, command=None, ) }, {"network_key.tx_counter": 0}, {}, ), ( None, {("security_mode",): zigpy_deconz.api.SecurityMode.NO_SECURITY}, {"security_level": 0}, {}, ), ( None, { ( "security_mode", ): zigpy_deconz.api.SecurityMode.PRECONFIGURED_NETWORK_KEY }, {"security_level": 5}, {}, ), ], ) async def test_load_network_info( app, # noqa: F811 network_info, node_info, error, param_overrides, nwk_state_changes, node_state_changes, ): """Test that network info is correctly read.""" params = { ("nwk_frame_counter",): network_info.network_key.tx_counter, ("aps_designed_coordinator",): 1, ("nwk_address",): node_info.nwk, ("mac_address",): node_info.ieee, ("current_channel",): network_info.channel, ("channel_mask",): t.Channels.from_channel_list([network_info.channel]), ("use_predefined_nwk_panid",): True, ("nwk_panid",): network_info.pan_id, ("aps_extended_panid",): network_info.extended_pan_id, ("nwk_update_id",): network_info.nwk_update_id, ("network_key", 0): zigpy_deconz.api.IndexedKey( index=0, key=network_info.network_key.key ), ("trust_center_address",): node_info.ieee, ("link_key", node_info.ieee): zigpy_deconz.api.LinkKey( ieee=node_info.ieee, key=network_info.tc_link_key.key ), ("security_mode",): zigpy_deconz.api.SecurityMode.ONLY_TCLK, ("protocol_version",): 0x010E, } params.update(param_overrides) async def read_param(param, *args): try: value = params[(param.name,) + args] except KeyError: raise zigpy_deconz.exception.CommandError( zigpy_deconz.api.Status.UNSUPPORTED, f"Unsupported: {param!r} {args!r}" ) if isinstance(value, Exception): raise value return value app._api.firmware_version = zigpy_deconz.api.FirmwareVersion(0x26580700) app._api.read_parameter = AsyncMock(side_effect=read_param) if error is not None: with pytest.raises(error): await app.load_network_info() return assert app.state.network_info != network_info assert app.state.node_info != node_info await app.load_network_info() # Almost all of the info matches network_info = network_info.replace( channel_mask=t.Channels.from_channel_list([network_info.channel]), network_key=network_info.network_key.replace(seq=0), tc_link_key=network_info.tc_link_key.replace(tx_counter=0), ) merge_objects(network_info, nwk_state_changes) assert app.state.network_info == network_info assert app.state.node_info == node_info.replace(**node_state_changes) zigpy-deconz-0.25.0/tests/test_send_receive.py000066400000000000000000000163041500720175400214330ustar00rootroot00000000000000"""Test sending and receiving Zigbee packets using the zigpy packet API.""" import asyncio import contextlib from unittest.mock import patch import pytest import zigpy.exceptions import zigpy.types as zigpy_t from zigpy_deconz.api import Status, TXStatus import zigpy_deconz.exception import zigpy_deconz.types as t from tests.test_application import api, app, device_path # noqa: F401 @contextlib.contextmanager def patch_data_request(app, *, fail_enqueue=False, fail_deliver=False): # noqa: F811 with patch.object(app._api, "aps_data_request") as mock_request: async def mock_send(req_id, *args, **kwargs): await asyncio.sleep(0) if fail_enqueue: raise zigpy_deconz.exception.CommandError( "Error", status=Status.FAILURE, command=None ) if fail_deliver: app.handle_tx_confirm( req_id, TXStatus(int(zigpy_t.APSStatus.APS_NO_ACK)) ) else: app.handle_tx_confirm(req_id, TXStatus.SUCCESS) mock_request.side_effect = mock_send yield mock_request @pytest.fixture def tx_packet(): return zigpy_t.ZigbeePacket( src=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x0000), src_ep=0x12, dst=zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0x1234), dst_ep=0x34, tsn=0x56, profile_id=0x7890, cluster_id=0xABCD, data=zigpy_t.SerializableBytes(b"some data"), tx_options=zigpy_t.TransmitOptions.ACK, radius=0, ) async def test_send_packet_nwk(app, tx_packet): # noqa: F811 with patch_data_request(app) as mock_req: await app.send_packet(tx_packet) assert len(mock_req.mock_calls) == 1 req = mock_req.mock_calls[0].kwargs assert req["dst_addr_ep"].address_mode == t.AddressMode.NWK assert req["dst_addr_ep"].address == tx_packet.dst.address assert req["dst_addr_ep"].endpoint == tx_packet.dst_ep assert req["profile"] == tx_packet.profile_id assert req["cluster"] == tx_packet.cluster_id assert req["aps_payload"] == tx_packet.data.serialize() assert req["tx_options"] == ( t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY | t.DeconzTransmitOptions.USE_APS_ACKS ) assert req["relays"] is None assert req["radius"] == 0 async def test_send_packet_nwk_no_ack(app, tx_packet): # noqa: F811 tx_packet.tx_options &= ~zigpy_t.TransmitOptions.ACK with patch_data_request(app) as mock_req: await app.send_packet(tx_packet) assert len(mock_req.mock_calls) == 1 req = mock_req.mock_calls[0].kwargs assert req["dst_addr_ep"].address_mode == t.AddressMode.NWK assert req["dst_addr_ep"].address == tx_packet.dst.address assert req["dst_addr_ep"].endpoint == tx_packet.dst_ep assert req["profile"] == tx_packet.profile_id assert req["cluster"] == tx_packet.cluster_id assert req["aps_payload"] == tx_packet.data.serialize() assert req["tx_options"] == t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY assert req["relays"] is None assert req["radius"] == 0 async def test_send_packet_source_route(app, tx_packet): # noqa: F811 tx_packet.source_route = [0xAABB, 0xCCDD] with patch_data_request(app) as mock_req: await app.send_packet(tx_packet) assert len(mock_req.mock_calls) == 1 req = mock_req.mock_calls[0].kwargs assert req["dst_addr_ep"].address_mode == t.AddressMode.NWK assert req["dst_addr_ep"].address == tx_packet.dst.address assert req["dst_addr_ep"].endpoint == tx_packet.dst_ep assert req["profile"] == tx_packet.profile_id assert req["cluster"] == tx_packet.cluster_id assert req["aps_payload"] == tx_packet.data.serialize() assert req["tx_options"] == ( t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY | t.DeconzTransmitOptions.USE_APS_ACKS ) assert req["relays"] == [0xAABB, 0xCCDD] assert req["radius"] == 0 async def test_send_packet_ieee(app, tx_packet): # noqa: F811 tx_packet.dst = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.IEEE, address=zigpy_t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), ) with patch_data_request(app) as mock_req: await app.send_packet(tx_packet) assert len(mock_req.mock_calls) == 1 req = mock_req.mock_calls[0].kwargs assert req["dst_addr_ep"].address_mode == t.AddressMode.IEEE assert req["dst_addr_ep"].address == tx_packet.dst.address assert req["dst_addr_ep"].endpoint == tx_packet.dst_ep assert req["profile"] == tx_packet.profile_id assert req["cluster"] == tx_packet.cluster_id assert req["aps_payload"] == tx_packet.data.serialize() assert req["tx_options"] == ( t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY | t.DeconzTransmitOptions.USE_APS_ACKS ) assert req["relays"] is None assert req["radius"] == 0 async def test_send_packet_group(app, tx_packet): # noqa: F811 tx_packet.dst = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.Group, address=0x1234 ) tx_packet.radius = 12 with patch_data_request(app) as mock_req: await app.send_packet(tx_packet) assert len(mock_req.mock_calls) == 1 req = mock_req.mock_calls[0].kwargs assert req["dst_addr_ep"].address_mode == t.AddressMode.GROUP assert req["dst_addr_ep"].address == tx_packet.dst.address assert req["dst_addr_ep"].endpoint == tx_packet.dst_ep assert req["profile"] == tx_packet.profile_id assert req["cluster"] == tx_packet.cluster_id assert req["aps_payload"] == tx_packet.data.serialize() assert req["tx_options"] == t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY assert req["relays"] is None assert req["radius"] == 12 async def test_send_packet_broadcast(app, tx_packet): # noqa: F811 tx_packet.dst = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.Broadcast, address=zigpy_t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, ) tx_packet.radius = 12 with patch_data_request(app) as mock_req: await app.send_packet(tx_packet) assert len(mock_req.mock_calls) == 1 req = mock_req.mock_calls[0].kwargs assert req["dst_addr_ep"].address_mode == t.AddressMode.NWK assert req["dst_addr_ep"].address == tx_packet.dst.address assert req["dst_addr_ep"].endpoint == tx_packet.dst_ep assert req["profile"] == tx_packet.profile_id assert req["cluster"] == tx_packet.cluster_id assert req["aps_payload"] == tx_packet.data.serialize() assert req["tx_options"] == t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY assert req["relays"] is None assert req["radius"] == 12 async def test_send_packet_enqueue_failure(app, tx_packet): # noqa: F811 with patch_data_request(app, fail_enqueue=True): # noqa: F811 with pytest.raises(zigpy.exceptions.DeliveryError) as e: await app.send_packet(tx_packet) assert "Failed to enqueue" in str(e) async def test_send_packet_deliver_failure(app, tx_packet): # noqa: F811 with patch_data_request(app, fail_deliver=True): # noqa: F811 with pytest.raises(zigpy.exceptions.DeliveryError) as e: await app.send_packet(tx_packet) assert "Failed to deliver" in str(e) zigpy-deconz-0.25.0/tests/test_types.py000066400000000000000000000150441500720175400201440ustar00rootroot00000000000000"""Tests for zigpy_deconz.types module.""" import pytest import zigpy.types as zigpy_t import zigpy_deconz.types as t def test_deconz_address_group(): data = b"\x01\x55\xaa" extra = b"the rest of the owl" addr, rest = t.DeconzAddress.deserialize(data + extra) assert rest == extra assert addr.address_mode == t.AddressMode.GROUP assert addr.address_mode == 1 assert addr.address == 0xAA55 assert addr.serialize() == data zigpy_addr = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.Group, address=0xAA55 ) assert addr.as_zigpy_type() == zigpy_addr converted_addr = t.DeconzAddress.from_zigpy_type(zigpy_addr) assert converted_addr == addr def test_deconz_address_nwk(): data = b"\x02\x55\xaa" extra = b"the rest of the owl" addr, rest = t.DeconzAddress.deserialize(data + extra) assert rest == extra assert addr.address_mode == t.AddressMode.NWK assert addr.address_mode == 2 assert addr.address == 0xAA55 assert addr.serialize() == data zigpy_addr = zigpy_t.AddrModeAddress(addr_mode=zigpy_t.AddrMode.NWK, address=0xAA55) assert addr.as_zigpy_type() == zigpy_addr converted_addr = t.DeconzAddress.from_zigpy_type(zigpy_addr) assert converted_addr == addr def test_deconz_address_nwk_broadcast(): data = b"\x02\xfc\xff" extra = b"the rest of the owl" addr, rest = t.DeconzAddress.deserialize(data + extra) assert rest == extra assert addr.address_mode == t.AddressMode.NWK assert addr.address_mode == 2 assert addr.address == 0xFFFC assert addr.serialize() == data zigpy_addr = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.Broadcast, address=zigpy_t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, ) assert addr.as_zigpy_type() == zigpy_addr converted_addr = t.DeconzAddress.from_zigpy_type(zigpy_addr) assert converted_addr == addr def test_deconz_address_ieee(): data = b"\x03\x55\xaa\xbb\xcc\xdd\xee\xef\xbe" extra = b"the rest of the owl" addr, rest = t.DeconzAddress.deserialize(data + extra) assert rest == extra assert addr.address_mode == t.AddressMode.IEEE assert addr.address_mode == 3 assert addr.address[0] == 0x55 assert addr.address[1] == 0xAA assert addr.address[2] == 0xBB assert addr.address[3] == 0xCC assert addr.address[4] == 0xDD assert addr.address[5] == 0xEE assert addr.address[6] == 0xEF assert addr.address[7] == 0xBE assert addr.serialize() == data zigpy_addr = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.IEEE, address=zigpy_t.EUI64.convert("BE:EF:EE:DD:CC:BB:AA:55"), ) assert addr.as_zigpy_type() == zigpy_addr converted_addr = t.DeconzAddress.from_zigpy_type(zigpy_addr) assert converted_addr == addr def test_deconz_address_nwk_and_ieee(): data = b"\x04\x55\xaa\x88\x99\xbb\xcc\xdd\xee\xef\xbe" extra = b"the rest of the owl" addr, rest = t.DeconzAddress.deserialize(data + extra) assert rest == extra assert addr.address_mode == t.AddressMode.NWK_AND_IEEE assert addr.address_mode == 4 assert addr.ieee[0] == 0x88 assert addr.ieee[1] == 0x99 assert addr.ieee[2] == 0xBB assert addr.ieee[3] == 0xCC assert addr.ieee[4] == 0xDD assert addr.ieee[5] == 0xEE assert addr.ieee[6] == 0xEF assert addr.ieee[7] == 0xBE assert addr.address == 0xAA55 assert addr.serialize() == data zigpy_addr = zigpy_t.AddrModeAddress( addr_mode=zigpy_t.AddrMode.IEEE, address=zigpy_t.EUI64.convert("BE:EF:EE:DD:CC:BB:99:88"), ) assert addr.as_zigpy_type() == zigpy_addr def test_bytes(): data = b"abcde\x00\xff" r, rest = t.Bytes.deserialize(data) assert rest == b"" assert r == data assert r.serialize() == data def test_addr_ep_nwk(): data = b"\x02\xaa\x55\xcc" extra = b"\x00extra data\xff" r, rest = t.DeconzAddressEndpoint.deserialize(data + extra) assert rest == extra assert r.address_mode == t.AddressMode.NWK assert r.address == 0x55AA assert r.endpoint == 0xCC def test_addr_ep_ieee(): data = b"\x0387654321\xcc" extra = b"\x00extra data\xff" r, rest = t.DeconzAddressEndpoint.deserialize(data + extra) assert rest == extra assert r.address_mode == t.AddressMode.IEEE assert repr(r.address) == "31:32:33:34:35:36:37:38" assert r.endpoint == 0xCC def test_deconz_addr_ep(): data = b"\x01\xaa\x55" extra = b"the rest of the owl" r, rest = t.DeconzAddressEndpoint.deserialize(data + extra) assert rest == extra assert r.address_mode == t.AddressMode.GROUP assert r.address == 0x55AA assert r.serialize() == data a = t.DeconzAddressEndpoint() a.address_mode = 1 a.address = 0x55AA assert a.serialize() == data data = b"\x02\xaa\x55\xcc" r, rest = t.DeconzAddressEndpoint.deserialize(data + extra) assert rest == extra assert r.address_mode == t.AddressMode.NWK assert r.address == 0x55AA assert r.endpoint == 0xCC assert r.serialize() == data a = t.DeconzAddressEndpoint() a.address_mode = 2 a.address = 0x55AA with pytest.raises(TypeError): a.serialize() a.endpoint = 0xCC assert a.serialize() == data data = b"\x03\x31\x32\x33\x34\x35\x36\x37\x38\xcc" r, rest = t.DeconzAddressEndpoint.deserialize(data + extra) assert rest == extra assert r.address_mode == t.AddressMode.IEEE assert r.address == [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38] assert r.endpoint == 0xCC assert r.serialize() == data a = t.DeconzAddressEndpoint() a.address_mode = 3 a.address = [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38] with pytest.raises(TypeError): a.serialize() a.endpoint = 0xCC assert a.serialize() == data def test_nwklist(): assert t.NWKList([]).serialize() == b"\x00" assert t.NWKList([0x1234]).serialize() == b"\x01" + t.NWK(0x1234).serialize() assert ( t.NWKList([0x1234, 0x5678]).serialize() == b"\x02" + t.NWK(0x1234).serialize() + t.NWK(0x5678).serialize() ) assert t.NWKList.deserialize(b"\x00abc") == (t.NWKList([]), b"abc") assert t.NWKList.deserialize(b"\x01\x34\x12abc") == (t.NWKList([0x1234]), b"abc") assert t.NWKList.deserialize(b"\x02\x34\x12\x78\x56abc") == ( t.NWKList([0x1234, 0x5678]), b"abc", ) def test_serialize_dict(): assert ( t.serialize_dict( {"foo": 1, "bar": 2, "baz": None}, {"foo": t.uint8_t, "bar": t.uint16_t, "baz": t.uint8_t}, ) == b"\x01\x02\x00" ) zigpy-deconz-0.25.0/tests/test_uart.py000066400000000000000000000075721500720175400177620ustar00rootroot00000000000000"""Tests for the uart module.""" import logging from unittest import mock import pytest from zigpy.config import ( CONF_DEVICE_BAUDRATE, CONF_DEVICE_FLOW_CONTROL, CONF_DEVICE_PATH, ) import zigpy.serial from zigpy_deconz import uart @pytest.fixture def gw(): gw = uart.Gateway(mock.MagicMock()) gw._transport = mock.MagicMock() return gw async def test_connect(monkeypatch): api = mock.MagicMock() async def mock_conn(loop, protocol_factory, **kwargs): protocol = protocol_factory() loop.call_soon(protocol.connection_made, None) return None, protocol monkeypatch.setattr(zigpy.serial, "create_serial_connection", mock_conn) await uart.connect( { CONF_DEVICE_PATH: "/dev/null", CONF_DEVICE_BAUDRATE: 115200, CONF_DEVICE_FLOW_CONTROL: None, }, api, ) def test_send(gw): data = b"test" gw.send(data) packet = b"" packet += b"\xC0" # END packet += b"test" # data packet += b"\x40\xFE" # checksum packet += b"\xC0" # END gw._transport.write.assert_called_once_with(packet) def test_close(gw): gw.close() assert gw._transport.close.call_count == 1 def test_data_received_chunk_frame(gw): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0" gw.data_received(data[:-4]) assert gw._api.data_received.call_count == 0 gw.data_received(data[-4:]) assert gw._api.data_received.call_count == 1 assert gw._api.data_received.call_args[0][0] == data[:-3] def test_data_received_full_frame(gw): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0" gw.data_received(data) assert gw._api.data_received.call_count == 1 assert gw._api.data_received.call_args[0][0] == data[:-3] def test_data_received_incomplete_frame(gw): data = b"~\x00\x00" gw.data_received(data) assert gw._api.data_received.call_count == 0 def test_data_received_runt_frame(gw): data = b"\x02\x44\xC0" gw.data_received(data) assert gw._api.data_received.call_count == 0 def test_data_received_extra(gw): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0\x00" gw.data_received(data) assert gw._api.data_received.call_count == 1 assert gw._api.data_received.call_args[0][0] == data[:-4] assert gw._buffer == b"\x00" def test_data_received_wrong_checksum(gw): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFE\xC0" gw.data_received(data) assert gw._api.data_received.call_count == 0 def test_data_received_error(gw, caplog): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02\x44\xFF\xC0" gw._api.data_received.side_effect = [RuntimeError("error")] with caplog.at_level(logging.ERROR): gw.data_received(data) assert "RuntimeError" in caplog.text and "handling the frame" in caplog.text assert gw._api.data_received.call_count == 1 assert gw._api.data_received.call_args[0][0] == data[:-3] def test_unescape(gw): data = b"\x00\xDB\xDC\x00\xDB\xDD\x00\x00\x00" data_unescaped = b"\x00\xC0\x00\xDB\x00\x00\x00" r = gw._unescape(data) assert r == data_unescaped def test_unescape_error(gw): data = b"\x00\xDB\xDC\x00\xDB\xDD\x00\x00\x00\xDB" r = gw._unescape(data) assert r is None def test_escape(gw): data = b"\x00\xC0\x00\xDB\x00\x00\x00" data_escaped = b"\x00\xDB\xDC\x00\xDB\xDD\x00\x00\x00" r = gw._escape(data) assert r == data_escaped def test_checksum(gw): data = b"\x07\x01\x00\x08\x00\xaa\x00\x02" checksum = b"\x44\xFF" r = gw._checksum(data) assert r == checksum def test_connection_lost_exc(gw): gw.connection_lost(mock.sentinel.exception) conn_lost = gw._api.connection_lost assert conn_lost.call_count == 1 assert conn_lost.call_args[0][0] is mock.sentinel.exception def test_connection_closed(gw): gw.connection_lost(None) assert gw._api.connection_lost.call_count == 1 zigpy-deconz-0.25.0/tests/test_utils.py000066400000000000000000000011451500720175400201350ustar00rootroot00000000000000"""Test utils module.""" import asyncio import logging from unittest.mock import AsyncMock from zigpy_deconz import utils async def test_restart_forever(caplog): mock = AsyncMock(side_effect=[None, RuntimeError(), RuntimeError(), None]) func = utils.restart_forever( mock, restart_delay=0.1, ) with caplog.at_level(logging.DEBUG): task = asyncio.create_task(func()) await asyncio.sleep(0.5) task.cancel() assert caplog.text.count("failed, restarting...") >= 2 assert caplog.text.count("RuntimeError") == 2 assert len(mock.mock_calls) >= 4 zigpy-deconz-0.25.0/zigpy_deconz/000077500000000000000000000000001500720175400167255ustar00rootroot00000000000000zigpy-deconz-0.25.0/zigpy_deconz/__init__.py000066400000000000000000000000421500720175400210320ustar00rootroot00000000000000"""Init file for zigpy_deconz.""" zigpy-deconz-0.25.0/zigpy_deconz/api.py000066400000000000000000000701741500720175400200610ustar00rootroot00000000000000"""deCONZ serial protocol API.""" from __future__ import annotations import asyncio import collections import itertools import logging import sys from typing import Any, Callable 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.datastructures import PriorityLock from zigpy.types import ( APSStatus, Bool, Channels, KeyData, SerializableBytes, Struct, ZigbeePacket, ) from zigpy.zdo.types import SimpleDescriptor from zigpy_deconz.exception import CommandError, MismatchedResponseError, ParsingError import zigpy_deconz.types as t import zigpy_deconz.uart from zigpy_deconz.utils import restart_forever LOGGER = logging.getLogger(__name__) MISMATCHED_RESPONSE_TIMEOUT = 0.5 COMMAND_TIMEOUT = 1.8 PROBE_TIMEOUT = 2 FRAME_LENGTH = object() PAYLOAD_LENGTH = object() class Status(t.enum8): SUCCESS = 0 FAILURE = 1 BUSY = 2 TIMEOUT = 3 UNSUPPORTED = 4 ERROR = 5 NO_NETWORK = 6 INVALID_VALUE = 7 class NetworkState2(t.enum2): OFFLINE = 0 JOINING = 1 CONNECTED = 2 LEAVING = 3 class DeviceStateFlags(t.bitmap6): APSDE_DATA_CONFIRM = 0b00001 APSDE_DATA_INDICATION = 0b000010 CONF_CHANGED = 0b000100 APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE = 0b0001000 class DeviceState(t.Struct): network_state: NetworkState2 device_state: DeviceStateFlags 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 class NetworkState(t.enum8): OFFLINE = 0 JOINING = 1 CONNECTED = 2 LEAVING = 3 class SecurityMode(t.enum8): NO_SECURITY = 0x00 PRECONFIGURED_NETWORK_KEY = 0x01 NETWORK_KEY_FROM_TC = 0x02 ONLY_TCLK = 0x03 class ZDPResponseHandling(t.bitmap16): NONE = 0x0000 NodeDescRsp = 0x0001 class CommandId(t.enum8): aps_data_confirm = 0x04 device_state = 0x07 change_network_state = 0x08 read_parameter = 0x0A write_parameter = 0x0B version = 0x0D device_state_changed = 0x0E aps_data_request = 0x12 aps_data_indication = 0x17 zigbee_green_power = 0x19 mac_poll = 0x1C update_neighbor = 0x1D mac_beacon_indication = 0x1F class TXStatus(t.enum8): SUCCESS = 0x00 @classmethod def _missing_(cls, value): chained = APSStatus(value) status = t.uint8_t.__new__(cls, chained.value) status._name_ = chained.name status._value_ = value return status class NetworkParameter(t.enum8): mac_address = 0x01 nwk_panid = 0x05 nwk_address = 0x07 nwk_extended_panid = 0x08 aps_designed_coordinator = 0x09 channel_mask = 0x0A aps_extended_panid = 0x0B trust_center_address = 0x0E security_mode = 0x10 configure_endpoint = 0x13 use_predefined_nwk_panid = 0x15 network_key = 0x18 link_key = 0x19 current_channel = 0x1C permit_join = 0x21 protocol_version = 0x22 nwk_update_id = 0x24 watchdog_ttl = 0x26 nwk_frame_counter = 0x27 app_zdp_response_handling = 0x28 class IndexedKey(Struct): index: t.uint8_t key: KeyData class LinkKey(Struct): ieee: t.EUI64 key: KeyData class IndexedEndpoint(Struct): index: t.uint8_t descriptor: SimpleDescriptor class UpdateNeighborAction(t.enum8): ADD = 0x01 NETWORK_PARAMETER_TYPES = { NetworkParameter.mac_address: (None, t.EUI64), NetworkParameter.nwk_panid: (None, t.PanId), NetworkParameter.nwk_address: (None, t.NWK), NetworkParameter.nwk_extended_panid: (None, t.ExtendedPanId), NetworkParameter.aps_designed_coordinator: (None, t.uint8_t), NetworkParameter.channel_mask: (None, Channels), NetworkParameter.aps_extended_panid: (None, t.ExtendedPanId), NetworkParameter.trust_center_address: (None, t.EUI64), NetworkParameter.security_mode: (None, t.uint8_t), NetworkParameter.configure_endpoint: (t.uint8_t, IndexedEndpoint), NetworkParameter.use_predefined_nwk_panid: (None, Bool), NetworkParameter.network_key: (t.uint8_t, IndexedKey), NetworkParameter.link_key: (t.EUI64, LinkKey), NetworkParameter.current_channel: (None, t.uint8_t), NetworkParameter.permit_join: (None, t.uint8_t), NetworkParameter.protocol_version: (None, t.uint16_t), NetworkParameter.nwk_update_id: (None, t.uint8_t), NetworkParameter.watchdog_ttl: (None, t.uint32_t), NetworkParameter.nwk_frame_counter: (None, t.uint32_t), NetworkParameter.app_zdp_response_handling: (None, ZDPResponseHandling), } class Command(Struct): command_id: CommandId seq: t.uint8_t payload: t.Bytes COMMAND_SCHEMAS = { CommandId.update_neighbor: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, "payload_length": PAYLOAD_LENGTH, "action": UpdateNeighborAction, "nwk": t.NWK, "ieee": t.EUI64, "mac_capability_flags": t.uint8_t, }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "action": UpdateNeighborAction, "nwk": t.NWK, "ieee": t.EUI64, "mac_capability_flags": t.uint8_t, }, ), CommandId.aps_data_confirm: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, "payload_length": PAYLOAD_LENGTH, }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "device_state": DeviceState, "request_id": t.uint8_t, "dst_addr": t.DeconzAddressEndpoint, "src_ep": t.uint8_t, "confirm_status": TXStatus, "reserved1": t.uint8_t, "reserved2": t.uint8_t, "reserved3": t.uint8_t, "reserved4": t.uint8_t, }, ), CommandId.aps_data_indication: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, "payload_length": PAYLOAD_LENGTH, "flags": t.DataIndicationFlags, }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "device_state": DeviceState, "dst_addr": t.DeconzAddress, "dst_ep": t.uint8_t, "src_addr": t.DeconzAddress, "src_ep": t.uint8_t, "profile_id": t.uint16_t, "cluster_id": t.uint16_t, "asdu": t.LongOctetString, "reserved1": t.uint8_t, "reserved2": t.uint8_t, "lqi": t.uint8_t, "reserved3": t.uint8_t, "reserved4": t.uint8_t, "reserved5": t.uint8_t, "reserved6": t.uint8_t, "rssi": t.int8s, }, ), CommandId.aps_data_request: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, "payload_length": PAYLOAD_LENGTH, "request_id": t.uint8_t, "flags": t.DeconzSendDataFlags, "dst": t.DeconzAddressEndpoint, "profile_id": t.uint16_t, "cluster_id": t.uint16_t, "src_ep": t.uint8_t, "asdu": t.LongOctetString, "tx_options": t.DeconzTransmitOptions, "radius": t.uint8_t, "relays": t.NWKList, # optional }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "device_state": DeviceState, "request_id": t.uint8_t, }, ), CommandId.change_network_state: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, # "payload_length": PAYLOAD_LENGTH, "network_state": NetworkState, }, { "status": Status, "frame_length": t.uint16_t, # "payload_length": t.uint16_t, "network_state": NetworkState, }, ), CommandId.device_state: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, # "payload_length": PAYLOAD_LENGTH, "reserved1": t.uint8_t(0), "reserved2": t.uint8_t(0), "reserved3": t.uint8_t(0), }, { "status": Status, "frame_length": t.uint16_t, # "payload_length": t.uint16_t, "device_state": DeviceState, "reserved1": t.uint8_t, "reserved2": t.uint8_t, }, ), CommandId.device_state_changed: ( None, { "status": Status, "frame_length": t.uint16_t, # "payload_length": t.uint16_t, "device_state": DeviceState, "reserved": t.uint8_t, }, ), CommandId.mac_poll: ( None, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "src_addr": t.DeconzAddress, "lqi": t.uint8_t, "rssi": t.int8s, "life_time": t.uint32_t, # Optional "device_timeout": t.uint32_t, # Optional }, ), CommandId.read_parameter: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, "payload_length": PAYLOAD_LENGTH, "parameter_id": NetworkParameter, "parameter": t.Bytes, }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "parameter_id": NetworkParameter, "parameter": t.Bytes, }, ), CommandId.version: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, # "payload_length": PAYLOAD_LENGTH, "reserved": t.uint32_t(0), }, { "status": Status, "frame_length": t.uint16_t, # "payload_length": t.uint16_t, "version": FirmwareVersion, }, ), CommandId.write_parameter: ( { "status": Status.SUCCESS, "frame_length": FRAME_LENGTH, "payload_length": PAYLOAD_LENGTH, "parameter_id": NetworkParameter, "parameter": t.Bytes, }, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "parameter_id": NetworkParameter, }, ), CommandId.zigbee_green_power: ( None, { "status": Status, "frame_length": t.uint16_t, "payload_length": t.uint16_t, "reserved": t.LongOctetString, }, ), } class Deconz: """deCONZ API class.""" def __init__(self, app: Callable, device_config: dict[str, Any]): """Init instance.""" self._app = app # [seq][cmd_id] = [fut1, fut2, ...] self._awaiting = collections.defaultdict(lambda: collections.defaultdict(list)) self._mismatched_response_timers: dict[int, asyncio.TimerHandle] = {} self._command_lock = PriorityLock() self._config = device_config self._device_state = DeviceState( network_state=NetworkState2.OFFLINE, device_state=( DeviceStateFlags.APSDE_DATA_CONFIRM | DeviceStateFlags.APSDE_DATA_INDICATION ), ) self._free_slots_available_event = asyncio.Event() self._free_slots_available_event.set() self._data_poller_event = asyncio.Event() self._data_poller_event.set() self._data_poller_task: asyncio.Task | None = None self._seq = 1 self._protocol_version = 0 self._firmware_version = FirmwareVersion(0) self._uart: zigpy_deconz.uart.Gateway | None = None @property def firmware_version(self) -> FirmwareVersion: """Return ConBee firmware version.""" return self._firmware_version @property def network_state(self) -> NetworkState: """Return current network state.""" return self._device_state.network_state @property def protocol_version(self) -> int: """Protocol Version.""" return self._protocol_version async def connect(self) -> None: assert self._uart is None self._uart = await zigpy_deconz.uart.connect(self._config, self) try: await self.version() device_state_rsp = await self.send_command(CommandId.device_state) except Exception: await self.disconnect() self._uart = None raise self._device_state = device_state_rsp["device_state"] self._data_poller_task = asyncio.create_task(self._data_poller()) def connection_lost(self, exc: Exception | None) -> None: """Lost serial connection.""" if self._app is not None: self._app.connection_lost(exc) async def disconnect(self): if self._data_poller_task is not None: self._data_poller_task.cancel() self._data_poller_task = None if self._uart is not None: await self._uart.disconnect() self._uart = None self._app = None def _get_command_priority(self, command: Command) -> int: return { # The watchdog is fed using `write_parameter` and `get_device_state` so they # must take priority CommandId.write_parameter: 999, CommandId.device_state: 999, # APS data requests are retried and can be deprioritized CommandId.aps_data_request: -1, }.get(command.command_id, 0) async def send_command(self, cmd, **kwargs) -> Any: while True: try: return await self._command(cmd, **kwargs) except MismatchedResponseError as exc: LOGGER.debug("Firmware responded incorrectly (%s), retrying", exc) async def _command(self, cmd, **kwargs): payload = [] tx_schema, _ = COMMAND_SCHEMAS[cmd] trailing_optional = False for name, param_type in tx_schema.items(): if isinstance(param_type, int): if name not in kwargs: # Default value value = param_type.serialize() else: value = type(param_type)(kwargs[name]).serialize() elif name in ("frame_length", "payload_length"): value = param_type elif kwargs.get(name) is None: trailing_optional = True value = None elif not isinstance(kwargs[name], param_type): value = param_type(kwargs[name]).serialize() else: value = kwargs[name].serialize() if value is None: continue if trailing_optional: raise ValueError( f"Command {cmd} with kwargs {kwargs}" f" has non-trailing optional argument" ) payload.append(value) if PAYLOAD_LENGTH in payload: payload = t.list_replace( lst=payload, old=PAYLOAD_LENGTH, new=t.uint16_t( sum(len(p) for p in payload[payload.index(PAYLOAD_LENGTH) + 1 :]) ).serialize(), ) if FRAME_LENGTH in payload: payload = t.list_replace( lst=payload, old=FRAME_LENGTH, new=t.uint16_t( 2 + sum(len(p) if p is not FRAME_LENGTH else 2 for p in payload) ).serialize(), ) command = Command( command_id=cmd, seq=None, payload=b"".join(payload), ) if self._uart is None: # connection was lost raise CommandError( "API is not running", status=Status.ERROR, command=command, ) async with self._command_lock(priority=self._get_command_priority(command)): seq = self._seq self._seq = (self._seq % 255) + 1 fut = asyncio.Future() self._awaiting[seq][cmd].append(fut) try: LOGGER.debug("Sending %s%s (seq=%s)", cmd, kwargs, seq) self._uart.send(command.replace(seq=seq).serialize()) async with asyncio_timeout(COMMAND_TIMEOUT): return await fut except asyncio.TimeoutError: LOGGER.debug("No response to '%s' command with seq %d", cmd, seq) raise finally: self._awaiting[seq][cmd].remove(fut) def data_received(self, data: bytes) -> None: command, _ = Command.deserialize(data) if command.command_id not in COMMAND_SCHEMAS: LOGGER.warning("Unknown command received: %s", command) return _, rx_schema = COMMAND_SCHEMAS[command.command_id] fut = None wrong_fut_cmd_id = None try: fut = self._awaiting[command.seq][command.command_id][0] except IndexError: # XXX: The firmware can sometimes respond with the wrong response. Find the # future associated with it so we can throw an appropriate error. for cmd_id, futs in self._awaiting[command.seq].items(): if futs: fut = futs[0] wrong_fut_cmd_id = cmd_id break try: params, rest = t.deserialize_dict(command.payload, rx_schema) except Exception: LOGGER.debug("Failed to parse command %s", command, exc_info=True) if fut is not None and not fut.done(): fut.set_exception( ParsingError( f"Failed to parse command: {command}", status=Status.ERROR, command=command, ) ) return if rest: LOGGER.debug("Unparsed data remains after frame: %s, %s", command, rest) assert params["frame_length"] == len(data) if "payload_length" in params: running_length = itertools.accumulate( len(v.serialize()) if v is not None else 0 for v in params.values() ) length_at_param = dict(zip(params.keys(), running_length)) assert ( len(data) - length_at_param["payload_length"] - 2 == params["payload_length"] ) LOGGER.debug( "Received command %s%s (seq %d)", command.command_id, params, command.seq ) status = params["status"] exc = None # Make sure to clear any pending mismatched response timers if command.seq in self._mismatched_response_timers: LOGGER.debug("Clearing existing mismatched response timer") self._mismatched_response_timers.pop(command.seq).cancel() if wrong_fut_cmd_id is not None: LOGGER.debug( "Mismatched response, triggering error in %0.2fs", MISMATCHED_RESPONSE_TIMEOUT, ) # The firmware *sometimes* responds with the correct response later self._mismatched_response_timers[ command.seq ] = asyncio.get_event_loop().call_later( MISMATCHED_RESPONSE_TIMEOUT, fut.set_exception, MismatchedResponseError( command.command_id, params, ( f"Response is mismatched! Sent {wrong_fut_cmd_id}," f" received {command.command_id}" ), ), ) # Make sure we do not resolve the future fut = None elif status != Status.SUCCESS: exc = CommandError( f"{command.command_id}, status: {status}", status=status, command=command, ) if fut is not None: try: if exc is None: fut.set_result(params) else: fut.set_exception(exc) except asyncio.InvalidStateError: LOGGER.debug( "Duplicate or delayed response for seq %s (awaiting %s)", command.seq, self._awaiting[command.seq], ) if exc is not None: return if handler := getattr(self, f"_handle_{command.command_id.name}", None): handler_params = { k: v for k, v in params.items() if k not in ("frame_length", "payload_length") } # Queue up the callback within the event loop asyncio.get_running_loop().call_soon(lambda: handler(**handler_params)) @restart_forever async def _data_poller(self): while True: await self._data_poller_event.wait() self._data_poller_event.clear() if self._device_state.network_state == NetworkState2.OFFLINE: continue # Poll data indication if ( DeviceStateFlags.APSDE_DATA_INDICATION in self._device_state.device_state ): # Old Conbee I firmware has an addressing bug for incoming multicasts if ( self.protocol_version >= 0x010B and self.firmware_version.platform == FirmwarePlatform.Conbee ): flags = t.DataIndicationFlags.Include_Both_NWK_And_IEEE else: flags = t.DataIndicationFlags.Always_Use_NWK_Source_Addr rsp = await self.send_command( CommandId.aps_data_indication, flags=flags ) self._handle_device_state_changed( status=rsp["status"], device_state=rsp["device_state"] ) self._app.packet_received( ZigbeePacket( src=rsp["src_addr"].as_zigpy_type(), src_ep=rsp["src_ep"], dst=rsp["dst_addr"].as_zigpy_type(), dst_ep=rsp["dst_ep"], tsn=None, profile_id=rsp["profile_id"], cluster_id=rsp["cluster_id"], data=SerializableBytes(rsp["asdu"]), lqi=rsp["lqi"], rssi=rsp["rssi"], ) ) # Poll data confirm if DeviceStateFlags.APSDE_DATA_CONFIRM in self._device_state.device_state: rsp = await self.send_command(CommandId.aps_data_confirm) self._app.handle_tx_confirm(rsp["request_id"], rsp["confirm_status"]) self._handle_device_state_changed( status=rsp["status"], device_state=rsp["device_state"] ) def _handle_device_state_changed( self, status: t.Status, device_state: DeviceState, reserved: t.uint8_t = 0, ) -> None: if device_state.network_state != self.network_state: LOGGER.debug( "Network device_state transition: %s -> %s", self.network_state.name, device_state.network_state.name, ) if ( DeviceStateFlags.APSDE_DATA_REQUEST_FREE_SLOTS_AVAILABLE in device_state.device_state ): self._free_slots_available_event.set() else: self._free_slots_available_event.clear() self._device_state = device_state self._data_poller_event.set() def _handle_device_state( self, status: t.Status, device_state: DeviceState, reserved1: t.uint8_t, reserved2: t.uint8_t, ) -> None: if ( self.firmware_version.platform == FirmwarePlatform.Conbee_III and self.firmware_version == 0x26450900 ): # Initial Conbee III firmware used the wrong command to notify of network # state changes self._handle_device_state_changed(status=status, device_state=device_state) async def version(self): self._protocol_version = await self.read_parameter( NetworkParameter.protocol_version ) version_rsp = await self.send_command(CommandId.version, reserved=0) self._firmware_version = version_rsp["version"] return self.firmware_version async def read_parameter( self, parameter_id: NetworkParameter, parameter: Any = None ) -> Any: read_param_type, write_param_type = NETWORK_PARAMETER_TYPES[parameter_id] if parameter is None: value = t.Bytes(b"") else: value = read_param_type(parameter).serialize() rsp = await self.send_command( CommandId.read_parameter, parameter_id=parameter_id, parameter=value, ) assert rsp["parameter_id"] == parameter_id result, _ = write_param_type.deserialize(rsp["parameter"]) LOGGER.debug("Read parameter %s(%s)=%r", parameter_id.name, parameter, result) return result async def write_parameter( self, parameter_id: NetworkParameter, parameter: Any ) -> None: read_param_type, write_param_type = NETWORK_PARAMETER_TYPES[parameter_id] await self.send_command( CommandId.write_parameter, parameter_id=parameter_id, parameter=write_param_type(parameter).serialize(), ) async def aps_data_request( self, req_id, dst_addr_ep, profile, cluster, src_ep, aps_payload, *, relays=None, tx_options=t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY, radius=0, ) -> None: flags = t.DeconzSendDataFlags.NONE # https://github.com/zigpy/zigpy-deconz/issues/180#issuecomment-1017932865 if relays is not None: # There is a max of 9 relays assert len(relays) <= 9 flags |= t.DeconzSendDataFlags.RELAYS if not self._free_slots_available_event.is_set(): LOGGER.debug("Waiting for free slots to become available") await self._free_slots_available_event.wait() rsp = await self.send_command( CommandId.aps_data_request, request_id=req_id, flags=flags, dst=dst_addr_ep, profile_id=profile, cluster_id=cluster, src_ep=src_ep, asdu=aps_payload, tx_options=tx_options, radius=radius, relays=relays, ) self._handle_device_state_changed( status=rsp["status"], device_state=rsp["device_state"] ) async def get_device_state(self) -> DeviceState: rsp = await self.send_command(CommandId.device_state) return rsp["device_state"] async def change_network_state(self, new_state: NetworkState) -> None: await self.send_command(CommandId.change_network_state, network_state=new_state) async def add_neighbour( self, nwk: t.NWK, ieee: t.EUI64, mac_capability_flags: t.uint8_t ) -> None: try: await self.send_command( CommandId.update_neighbor, action=UpdateNeighborAction.ADD, nwk=nwk, ieee=ieee, mac_capability_flags=mac_capability_flags, ) except ParsingError as exc: # Older Conbee III firmwares send back an invalid response status = Status(exc.command.payload[0]) if status != Status.SUCCESS: raise CommandError( f"{exc.command.command_id}, status: {status}", status=status, command=exc.command, ) from exc zigpy-deconz-0.25.0/zigpy_deconz/config.py000066400000000000000000000014021500720175400205410ustar00rootroot00000000000000"""Default configuration values.""" import voluptuous as vol from zigpy.config import ( # noqa: F401 pylint: disable=unused-import CONF_DEVICE, CONF_DEVICE_PATH, CONF_MAX_CONCURRENT_REQUESTS, CONF_NWK, CONF_NWK_CHANNEL, CONF_NWK_CHANNELS, CONF_NWK_EXTENDED_PAN_ID, CONF_NWK_KEY, CONF_NWK_PAN_ID, CONF_NWK_TC_ADDRESS, CONF_NWK_TC_LINK_KEY, CONF_NWK_UPDATE_ID, CONFIG_SCHEMA, SCHEMA_DEVICE, cv_boolean, ) CONF_DECONZ_CONFIG = "deconz_config" CONF_MAX_CONCURRENT_REQUESTS_DEFAULT = 8 CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { vol.Optional( CONF_MAX_CONCURRENT_REQUESTS, default=CONF_MAX_CONCURRENT_REQUESTS_DEFAULT ): CONFIG_SCHEMA.schema[CONF_MAX_CONCURRENT_REQUESTS], } ) zigpy-deconz-0.25.0/zigpy_deconz/exception.py000066400000000000000000000014531500720175400213000ustar00rootroot00000000000000"""Zigpy-deconz exceptions.""" from __future__ import annotations import typing from zigpy.exceptions import APIException if typing.TYPE_CHECKING: from zigpy_deconz.api import Command, CommandId, Status class CommandError(APIException): def __init__(self, *args, status: Status, command: Command, **kwargs): """Initialize instance.""" super().__init__(*args, **kwargs) self.command = command self.status = status class ParsingError(CommandError): pass class MismatchedResponseError(APIException): def __init__( self, command_id: CommandId, params: dict[str, typing.Any], *args, **kwargs ) -> None: """Initialize instance.""" super().__init__(*args, **kwargs) self.command_id = command_id self.params = params zigpy-deconz-0.25.0/zigpy_deconz/types.py000066400000000000000000000133501500720175400204450ustar00rootroot00000000000000"""Data types module.""" import zigpy.types as zigpy_t from zigpy.types import ( # noqa: F401 EUI64, NWK, ExtendedPanId, IntStruct, LongOctetString, LVBytes, LVList, PanId, Struct, bitmap3, bitmap5, bitmap6, bitmap8, bitmap16, enum2, enum3, enum8, int8s, uint8_t, uint16_t, uint32_t, uint64_t, ) def serialize_dict(data, schema): chunks = [] for key in schema: value = data[key] if value is None: break if not isinstance(value, schema[key]): value = schema[key](value) chunks.append(value.serialize()) return b"".join(chunks) def deserialize_dict(data, schema): result = {} for name, type_ in schema.items(): try: result[name], data = type_.deserialize(data) except ValueError: if data: raise result[name] = None return result, data def list_replace(lst: list, old: object, new: object) -> list: """Replace all occurrences of `old` with `new` in `lst`.""" return [new if x == old else x for x in lst] class Bytes(bytes): def serialize(self): return self @classmethod def deserialize(cls, data): return cls(data), b"" class AddressMode(enum8): # Address modes used in deconz protocol GROUP = 0x01 NWK = 0x02 IEEE = 0x03 NWK_AND_IEEE = 0x04 class DeconzSendDataFlags(bitmap8): NONE = 0x00 NODE_ID = 0x01 RELAYS = 0x02 class DeconzTransmitOptions(bitmap8): NONE = 0x00 SECURITY_ENABLED = 0x01 USE_NWK_KEY_SECURITY = 0x02 USE_APS_ACKS = 0x04 ALLOW_FRAGMENTATION = 0x08 class NWKList(LVList): _length_type = uint8_t _item_type = NWK ZIGPY_ADDR_MODE_MAPPING = { zigpy_t.AddrMode.NWK: AddressMode.NWK, zigpy_t.AddrMode.IEEE: AddressMode.IEEE, zigpy_t.AddrMode.Group: AddressMode.GROUP, zigpy_t.AddrMode.Broadcast: AddressMode.NWK, } ZIGPY_ADDR_TYPE_MAPPING = { zigpy_t.AddrMode.NWK: NWK, zigpy_t.AddrMode.IEEE: EUI64, zigpy_t.AddrMode.Group: NWK, zigpy_t.AddrMode.Broadcast: NWK, } ZIGPY_ADDR_MODE_REVERSE_MAPPING = { AddressMode.NWK: zigpy_t.AddrMode.NWK, AddressMode.IEEE: zigpy_t.AddrMode.IEEE, AddressMode.GROUP: zigpy_t.AddrMode.Group, AddressMode.NWK_AND_IEEE: zigpy_t.AddrMode.IEEE, } ZIGPY_ADDR_TYPE_REVERSE_MAPPING = { AddressMode.NWK: zigpy_t.NWK, AddressMode.IEEE: zigpy_t.EUI64, AddressMode.GROUP: zigpy_t.Group, AddressMode.NWK_AND_IEEE: zigpy_t.NWK, } class DeconzAddress(Struct): address_mode: AddressMode address: EUI64 ieee: EUI64 @classmethod def deserialize(cls, data): r = cls() mode, data = AddressMode.deserialize(data) r.address_mode = mode if mode in [AddressMode.GROUP, AddressMode.NWK, AddressMode.NWK_AND_IEEE]: r.address, data = NWK.deserialize(data) elif mode == AddressMode.IEEE: r.address, data = EUI64.deserialize(data) if mode == AddressMode.NWK_AND_IEEE: r.ieee, data = EUI64.deserialize(data) return r, data def serialize(self): r = self.address_mode.serialize() + self.address.serialize() if self.address_mode == AddressMode.NWK_AND_IEEE: r += self.ieee.serialize() return r def as_zigpy_type(self): addr_mode = ZIGPY_ADDR_MODE_REVERSE_MAPPING[self.address_mode] address = ZIGPY_ADDR_TYPE_REVERSE_MAPPING[self.address_mode](self.address) if self.address_mode == AddressMode.NWK and self.address > 0xFFF7: addr_mode = zigpy_t.AddrMode.Broadcast address = zigpy_t.BroadcastAddress(self.address) elif self.address_mode == AddressMode.NWK_AND_IEEE: address = zigpy_t.EUI64(self.ieee) return zigpy_t.AddrModeAddress( addr_mode=addr_mode, address=address, ) @classmethod def from_zigpy_type(cls, addr): instance = cls() instance.address_mode = ZIGPY_ADDR_MODE_MAPPING[addr.addr_mode] instance.address = ZIGPY_ADDR_TYPE_MAPPING[addr.addr_mode](addr.address) return instance class DeconzAddressEndpoint(Struct): address_mode: AddressMode address: EUI64 ieee: EUI64 endpoint: uint8_t @classmethod def deserialize(cls, data): r, data = DeconzAddress.deserialize.__func__(cls, data) if r.address_mode in ( AddressMode.NWK, AddressMode.IEEE, AddressMode.NWK_AND_IEEE, ): r.endpoint, data = uint8_t.deserialize(data) else: r.endpoint = None return r, data def serialize(self): r = uint8_t(self.address_mode).serialize() if self.address_mode in (AddressMode.NWK, AddressMode.NWK_AND_IEEE): r += NWK(self.address).serialize() elif self.address_mode == AddressMode.GROUP: r += NWK(self.address).serialize() if self.address_mode in (AddressMode.IEEE, AddressMode.NWK_AND_IEEE): r += EUI64(self.address).serialize() if self.address_mode in ( AddressMode.NWK, AddressMode.IEEE, AddressMode.NWK_AND_IEEE, ): r += uint8_t(self.endpoint).serialize() return r @classmethod def from_zigpy_type(cls, addr, endpoint): temp_addr = DeconzAddress.from_zigpy_type(addr) instance = cls() instance.address_mode = temp_addr.address_mode instance.address = temp_addr.address instance.endpoint = endpoint return instance class DataIndicationFlags(bitmap8): Always_Use_NWK_Source_Addr = 0b00000001 Last_Hop_In_Reserved_Bytes = 0b00000010 Include_Both_NWK_And_IEEE = 0b00000100 zigpy-deconz-0.25.0/zigpy_deconz/uart.py000066400000000000000000000072371500720175400202630ustar00rootroot00000000000000"""Uart module.""" from __future__ import annotations import asyncio import binascii import logging from typing import Any, Callable import zigpy.config import zigpy.serial LOGGER = logging.getLogger(__name__) class Gateway(zigpy.serial.SerialProtocol): END = b"\xC0" ESC = b"\xDB" ESC_END = b"\xDC" ESC_ESC = b"\xDD" def __init__(self, api): """Initialize instance of the UART gateway.""" super().__init__() self._api = api def connection_lost(self, exc: Exception | None) -> None: """Port was closed expectedly or unexpectedly.""" super().connection_lost(exc) if self._api is not None: self._api.connection_lost(exc) def close(self): super().close() self._api = None def send(self, data: bytes) -> None: """Send data, taking care of escaping and framing.""" checksum = bytes(self._checksum(data)) frame = self._escape(data + checksum) self._transport.write(self.END + frame + self.END) def data_received(self, data: bytes) -> None: """Handle data received from the uart.""" super().data_received(data) while self._buffer: end = self._buffer.find(self.END) if end < 0: return None frame = self._buffer[:end] self._buffer = self._buffer[(end + 1) :] frame = self._unescape(frame) if len(frame) < 4: continue checksum = frame[-2:] frame = frame[:-2] if self._checksum(frame) != checksum: LOGGER.warning( "Invalid checksum: 0x%s, data: 0x%s", binascii.hexlify(checksum).decode(), binascii.hexlify(frame).decode(), ) continue LOGGER.debug("Frame received: 0x%s", binascii.hexlify(frame).decode()) try: self._api.data_received(frame) except Exception as exc: LOGGER.error("Unexpected error handling the frame", exc_info=exc) def _unescape(self, data): ret = [] idx = 0 while idx < len(data): b = data[idx] if b == self.ESC[0]: idx += 1 if idx >= len(data): return None elif data[idx] == self.ESC_END[0]: b = self.END[0] elif data[idx] == self.ESC_ESC[0]: b = self.ESC[0] ret.append(b) idx += 1 return bytes(ret) def _escape(self, data): ret = [] for b in data: if b == self.END[0]: ret.append(self.ESC[0]) ret.append(self.ESC_END[0]) elif b == self.ESC[0]: ret.append(self.ESC[0]) ret.append(self.ESC_ESC[0]) else: ret.append(b) return bytes(ret) def _checksum(self, data): ret = [] s = ~(sum(data)) + 1 ret.append(s % 0x100) ret.append((s >> 8) % 0x100) return bytes(ret) async def connect(config: dict[str, Any], api: Callable) -> Gateway: protocol = Gateway(api) LOGGER.debug("Connecting to %s", config[zigpy.config.CONF_DEVICE_PATH]) _, protocol = await zigpy.serial.create_serial_connection( loop=asyncio.get_running_loop(), protocol_factory=lambda: protocol, url=config[zigpy.config.CONF_DEVICE_PATH], baudrate=config[zigpy.config.CONF_DEVICE_BAUDRATE], flow_control=config[zigpy.config.CONF_DEVICE_FLOW_CONTROL], ) await protocol.wait_until_connected() return protocol zigpy-deconz-0.25.0/zigpy_deconz/utils.py000066400000000000000000000011161500720175400204360ustar00rootroot00000000000000"""deCONZ serial protocol API.""" from __future__ import annotations import asyncio import functools import logging LOGGER = logging.getLogger(__name__) def restart_forever(func, *, restart_delay=1.0): @functools.wraps(func) async def replacement(*args, **kwargs): while True: try: await func(*args, **kwargs) except Exception: LOGGER.debug( "Endless task %s failed, restarting...", func, exc_info=True ) await asyncio.sleep(restart_delay) return replacement zigpy-deconz-0.25.0/zigpy_deconz/zigbee/000077500000000000000000000000001500720175400201725ustar00rootroot00000000000000zigpy-deconz-0.25.0/zigpy_deconz/zigbee/__init__.py000066400000000000000000000000541500720175400223020ustar00rootroot00000000000000"""ApplicationController implementation.""" zigpy-deconz-0.25.0/zigpy_deconz/zigbee/application.py000066400000000000000000000561241500720175400230570ustar00rootroot00000000000000"""ControllerApplication for deCONZ protocol based adapters.""" from __future__ import annotations import asyncio import importlib.metadata import logging import re import sys from typing import Any 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.application import zigpy.config import zigpy.device import zigpy.endpoint import zigpy.exceptions from zigpy.exceptions import FormationFailure, NetworkNotFormed import zigpy.state import zigpy.types import zigpy.util import zigpy.zdo.types as zdo_t import zigpy_deconz from zigpy_deconz import types as t from zigpy_deconz.api import ( Deconz, FirmwarePlatform, IndexedEndpoint, IndexedKey, LinkKey, NetworkParameter, NetworkState, SecurityMode, Status, TXStatus, ) from zigpy_deconz.config import CONFIG_SCHEMA import zigpy_deconz.exception LIB_VERSION = importlib.metadata.version("zigpy-deconz") LOGGER = logging.getLogger(__name__) CHANGE_NETWORK_POLL_TIME = 1 CHANGE_NETWORK_STATE_DELAY = 2 DELAY_NEIGHBOUR_SCAN_S = 1500 SEND_CONFIRM_TIMEOUT = 60 PROTO_VER_MANUAL_SOURCE_ROUTE = 0x010C PROTO_VER_WATCHDOG = 0x0108 PROTO_VER_NEIGBOURS = 0x0107 CONBEE_III_ENERGY_SCAN_ATTEMPTS = 5 class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA _probe_config_variants = [ {zigpy.config.CONF_DEVICE_BAUDRATE: 38400}, {zigpy.config.CONF_DEVICE_BAUDRATE: 115200}, ] _watchdog_period = 30 def __init__(self, config: dict[str, Any]): """Initialize instance.""" super().__init__(config=config) self._api = None self._pending = zigpy.util.Requests() self._delayed_neighbor_scan_task = None self._reconnect_task = None self._written_endpoints = set() async def _watchdog_feed(self): if self._api.protocol_version >= PROTO_VER_WATCHDOG and not ( self._api.firmware_version.platform == FirmwarePlatform.Conbee_III and self._api.firmware_version <= 0x26450900 ): await self._api.write_parameter( NetworkParameter.watchdog_ttl, int(2 * self._watchdog_period) ) else: await self._api.get_device_state() async def connect(self): api = Deconz(self, self._config[zigpy.config.CONF_DEVICE]) try: await api.connect() except Exception: await api.disconnect() raise self._api = api self._written_endpoints.clear() async def disconnect(self): if self._delayed_neighbor_scan_task is not None: self._delayed_neighbor_scan_task.cancel() self._delayed_neighbor_scan_task = None if self._api is not None: await self._api.disconnect() self._api = None async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s=60): await self._api.write_parameter( NetworkParameter.link_key, LinkKey(ieee=node, key=link_key), ) await self.permit(time_s) async def start_network(self): await self.register_endpoints() await self.load_network_info(load_devices=False) await self._change_network_state(NetworkState.CONNECTED) coordinator = await DeconzDevice.new( self, self.state.node_info.ieee, self.state.node_info.nwk, self.state.node_info.model, ) self.devices[self.state.node_info.ieee] = coordinator if self._api.protocol_version >= PROTO_VER_NEIGBOURS and not ( self._api.firmware_version.platform == FirmwarePlatform.Conbee_III and self._api.firmware_version < 0x264D0900 ): await self.restore_neighbours() self._delayed_neighbor_scan_task = asyncio.create_task( self._delayed_neighbour_scan() ) async def _change_network_state( self, target_state: NetworkState, *, timeout: int = 10 * CHANGE_NETWORK_POLL_TIME, ): async def change_loop(): while True: try: device_state = await self._api.get_device_state() except asyncio.TimeoutError: # 0x264B0900 and earlier can reset during device state changes # requiring a firmware reset, causing state polling to fail LOGGER.debug("Failed to poll device state") else: if NetworkState(device_state.network_state) == target_state: break await asyncio.sleep(CHANGE_NETWORK_POLL_TIME) await self._api.change_network_state(target_state) try: async with asyncio_timeout(timeout): await change_loop() except asyncio.TimeoutError: if target_state != NetworkState.CONNECTED: raise raise FormationFailure( "Network formation refused: there is likely too much RF interference." " Make sure your coordinator is on a USB 2.0 extension cable and" " away from any sources of interference, like USB 3.0 ports, SSDs," " 2.4GHz routers, motherboards, etc." ) async def reset_network_info(self): # TODO: There does not appear to be a way to factory reset a Conbee await self.form_network() async def write_network_info(self, *, network_info, node_info): try: await self._api.write_parameter( NetworkParameter.nwk_frame_counter, network_info.network_key.tx_counter ) except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED fw_version = f"{int(self._api.firmware_version):#010x}" raise zigpy.exceptions.ControllerException( f"Please upgrade your adapter firmware. Firmware version {fw_version}" f" does not support writing the network key frame counter, which is" f" required for migration to succeed." ) if node_info.logical_type == zdo_t.LogicalType.Coordinator: await self._api.write_parameter( NetworkParameter.aps_designed_coordinator, 1 ) else: await self._api.write_parameter( NetworkParameter.aps_designed_coordinator, 0 ) await self._api.write_parameter(NetworkParameter.nwk_address, node_info.nwk) if node_info.ieee != zigpy.types.EUI64.UNKNOWN: # TODO: is there a way to revert it back to the hardware default? Or is this # information lost when the parameter is overwritten? await self._api.write_parameter( NetworkParameter.mac_address, node_info.ieee ) node_ieee = node_info.ieee else: ieee = await self._api.read_parameter(NetworkParameter.mac_address) node_ieee = zigpy.types.EUI64(ieee) # There is no way to specify both a mask and the logical channel if network_info.channel is not None: channel_mask = zigpy.types.Channels.from_channel_list( [network_info.channel] ) if network_info.channel_mask and channel_mask != network_info.channel_mask: LOGGER.warning( "Channel mask %s will be replaced with current logical channel %s", network_info.channel_mask, channel_mask, ) else: channel_mask = network_info.channel_mask await self._api.write_parameter(NetworkParameter.channel_mask, channel_mask) await self._api.write_parameter(NetworkParameter.use_predefined_nwk_panid, True) await self._api.write_parameter(NetworkParameter.nwk_panid, network_info.pan_id) await self._api.write_parameter( NetworkParameter.aps_extended_panid, network_info.extended_pan_id ) await self._api.write_parameter( NetworkParameter.nwk_update_id, network_info.nwk_update_id ) await self._api.write_parameter( NetworkParameter.network_key, IndexedKey(index=0, key=network_info.network_key.key), ) if network_info.network_key.seq != 0: LOGGER.warning( "Non-zero network key sequence number is not supported: %s", network_info.network_key.seq, ) tc_link_key_partner_ieee = network_info.tc_link_key.partner_ieee if tc_link_key_partner_ieee == zigpy.types.EUI64.UNKNOWN: tc_link_key_partner_ieee = node_ieee await self._api.write_parameter( NetworkParameter.trust_center_address, tc_link_key_partner_ieee, ) await self._api.write_parameter( NetworkParameter.link_key, LinkKey( ieee=tc_link_key_partner_ieee, key=network_info.tc_link_key.key, ), ) if self._api.firmware_version.platform != FirmwarePlatform.Conbee_III: if network_info.security_level == 0x00: await self._api.write_parameter( NetworkParameter.security_mode, SecurityMode.NO_SECURITY ) else: await self._api.write_parameter( NetworkParameter.security_mode, SecurityMode.ONLY_TCLK ) # Note: Changed network configuration parameters become only affective after # sending a Leave Network Request followed by a Create or Join Network Request await self._change_network_state(NetworkState.OFFLINE) await asyncio.sleep(CHANGE_NETWORK_STATE_DELAY) await self._change_network_state(NetworkState.CONNECTED) async def load_network_info(self, *, load_devices=False): network_info = self.state.network_info node_info = self.state.node_info ieee = await self._api.read_parameter(NetworkParameter.mac_address) node_info.ieee = zigpy.types.EUI64(ieee) designed_coord = await self._api.read_parameter( NetworkParameter.aps_designed_coordinator ) if designed_coord == 0x01: node_info.logical_type = zdo_t.LogicalType.Coordinator else: node_info.logical_type = zdo_t.LogicalType.Router node_info.nwk = await self._api.read_parameter(NetworkParameter.nwk_address) node_info.manufacturer = "dresden elektronik" if re.match( r"/dev/tty(S|AMA|ACM)\d+", self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], ): node_info.model = "Raspbee" else: node_info.model = "Conbee" node_info.model += { FirmwarePlatform.Conbee: "", FirmwarePlatform.Conbee_II: " II", FirmwarePlatform.Conbee_III: " III", }[self._api.firmware_version.platform] node_info.version = f"{int(self._api.firmware_version):#010x}" network_info.source = f"zigpy-deconz@{LIB_VERSION}" network_info.metadata = { "deconz": { "version": node_info.version, } } network_info.pan_id = await self._api.read_parameter(NetworkParameter.nwk_panid) network_info.extended_pan_id = await self._api.read_parameter( NetworkParameter.aps_extended_panid ) if network_info.extended_pan_id == zigpy.types.EUI64.convert( "00:00:00:00:00:00:00:00" ): network_info.extended_pan_id = await self._api.read_parameter( NetworkParameter.nwk_extended_panid ) network_info.channel = await self._api.read_parameter( NetworkParameter.current_channel ) network_info.channel_mask = await self._api.read_parameter( NetworkParameter.channel_mask ) network_info.nwk_update_id = await self._api.read_parameter( NetworkParameter.nwk_update_id ) if network_info.channel == 0: raise NetworkNotFormed("Network channel is zero") indexed_key = await self._api.read_parameter(NetworkParameter.network_key, 0) network_info.network_key = zigpy.state.Key() network_info.network_key.key = indexed_key.key try: network_info.network_key.tx_counter = await self._api.read_parameter( NetworkParameter.nwk_frame_counter ) except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED network_info.tc_link_key = zigpy.state.Key() network_info.tc_link_key.partner_ieee = await self._api.read_parameter( NetworkParameter.trust_center_address ) link_key = await self._api.read_parameter( NetworkParameter.link_key, network_info.tc_link_key.partner_ieee, ) network_info.tc_link_key.key = link_key.key security_mode = await self._api.read_parameter(NetworkParameter.security_mode) if security_mode == SecurityMode.NO_SECURITY: network_info.security_level = 0x00 elif security_mode == SecurityMode.ONLY_TCLK: network_info.security_level = 0x05 else: LOGGER.warning("Unsupported security mode %r", security_mode) network_info.security_level = 0x05 async def force_remove(self, dev): """Forcibly remove device from NCP.""" async def energy_scan( self, channels: t.Channels.ALL_CHANNELS, duration_exp: int, count: int ) -> dict[int, float]: if self._api.firmware_version.platform in ( FirmwarePlatform.Conbee, FirmwarePlatform.Conbee_II, ): results = await super().energy_scan( channels=channels, duration_exp=duration_exp, count=count ) # The Conbee I/II seems to max out at an LQI of 85, which is exactly 255/3 return {c: v * 3 for c, v in results.items()} for i in range(CONBEE_III_ENERGY_SCAN_ATTEMPTS): # The Conbee III energy scan inherits the EmberZNet ZDO bug try: rsp = await self._device.zdo.Mgmt_NWK_Update_req( zigpy.zdo.types.NwkUpdate( ScanChannels=channels, ScanDuration=duration_exp, ScanCount=count, ) ) break except (asyncio.TimeoutError, zigpy.exceptions.DeliveryError): if i == CONBEE_III_ENERGY_SCAN_ATTEMPTS - 1: raise continue _, 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: """Move device to a new channel.""" channel_mask = zigpy.types.Channels.from_channel_list([new_channel]) await self._api.write_parameter(NetworkParameter.channel_mask, channel_mask) await self._api.write_parameter( NetworkParameter.nwk_update_id, new_nwk_update_id ) await self._change_network_state(NetworkState.OFFLINE) await asyncio.sleep(CHANGE_NETWORK_STATE_DELAY) await self._change_network_state(NetworkState.CONNECTED) async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: """Register an endpoint on the device, replacing any with conflicting IDs.""" endpoints = {} # Read and count the current endpoints. Some firmwares have three, others four. for index in range(255 + 1): try: current_descriptor = await self._api.read_parameter( NetworkParameter.configure_endpoint, index ) except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED break else: endpoints[index] = current_descriptor.descriptor LOGGER.debug("Got endpoint slots: %r", endpoints) # Don't write endpoints unnecessarily if descriptor in endpoints.values(): LOGGER.debug("Endpoint already registered, skipping") # Pretend we wrote it self._written_endpoints.add(list(endpoints.values()).index(descriptor)) return # Keep track of the best endpoint descriptor to replace target_index = None for index, current_descriptor in endpoints.items(): # Ignore ones we've already written if index in self._written_endpoints: continue target_index = index if current_descriptor.endpoint == descriptor.endpoint: # Prefer to replace the endpoint with the same ID break if target_index is None: raise ValueError(f"No available endpoint slots exist: {endpoints!r}") LOGGER.debug("Writing %s to slot %r", descriptor, target_index) await self._api.write_parameter( NetworkParameter.configure_endpoint, IndexedEndpoint(index=target_index, descriptor=descriptor), ) async def send_packet(self, packet): LOGGER.debug("Sending packet: %r", packet) tx_options = t.DeconzTransmitOptions.USE_NWK_KEY_SECURITY if ( zigpy.types.TransmitOptions.ACK in packet.tx_options and packet.dst.addr_mode in (zigpy.types.AddrMode.NWK, zigpy.types.AddrMode.IEEE) ): tx_options |= t.DeconzTransmitOptions.USE_APS_ACKS async with self._limit_concurrency(priority=packet.priority): req_id = self.get_sequence() with self._pending.new(req_id) as req: try: await self._api.aps_data_request( req_id=req_id, dst_addr_ep=t.DeconzAddressEndpoint.from_zigpy_type( packet.dst, packet.dst_ep or 0 ), profile=packet.profile_id, cluster=packet.cluster_id, src_ep=min(1, packet.src_ep), aps_payload=packet.data.serialize(), tx_options=tx_options, relays=packet.source_route, radius=packet.radius or 0, ) except zigpy_deconz.exception.CommandError as ex: raise zigpy.exceptions.DeliveryError( f"Failed to enqueue packet: {ex!r}", ex.status ) async with asyncio_timeout(SEND_CONFIRM_TIMEOUT): status = await req.result if status != TXStatus.SUCCESS: raise zigpy.exceptions.DeliveryError( f"Failed to deliver packet: {status!r}", status ) async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 await self._api.write_parameter(NetworkParameter.permit_join, time_s) def handle_tx_confirm(self, req_id, status): try: self._pending[req_id].result.set_result(status) return except KeyError: LOGGER.warning( "Unexpected transmit confirm for request id %s, Status: %s", req_id, status, ) except asyncio.InvalidStateError as exc: LOGGER.debug( "Invalid state on future - probably duplicate response: %s", exc ) async def restore_neighbours(self) -> None: """Restore children.""" coord = self.get_device(ieee=self.state.node_info.ieee) for neighbor in self.topology.neighbors[coord.ieee]: try: device = self.get_device(ieee=neighbor.ieee) except KeyError: continue descr = device.node_desc LOGGER.debug( "device: 0x%04x - %s %s, FFD=%s, Rx_on_when_idle=%s", device.nwk, device.manufacturer, device.model, descr.is_full_function_device if descr is not None else None, descr.is_receiver_on_when_idle if descr is not None else None, ) if ( descr is None or descr.is_full_function_device or descr.is_receiver_on_when_idle ): continue LOGGER.debug("Restoring %s as direct child", device) try: await self._api.add_neighbour( nwk=device.nwk, ieee=device.ieee, mac_capability_flags=descr.mac_capability_flags, ) except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.FAILURE LOGGER.debug("Failed to add device to neighbor table: %s", ex) async def _delayed_neighbour_scan(self) -> None: """Scan coordinator's neighbours.""" await asyncio.sleep(DELAY_NEIGHBOUR_SCAN_S) coord = self.get_device(ieee=self.state.node_info.ieee) await self.topology.scan(devices=[coord]) class DeconzDevice(zigpy.device.Device): """Zigpy Device representing Coordinator.""" def __init__(self, model: str, *args): """Initialize instance.""" super().__init__(*args) self._model = model async def add_to_group(self, grp_id: int, name: str = None) -> None: group = self.application.groups.add_group(grp_id, name) for epid in self.endpoints: if not epid: continue # skip ZDO group.add_member(self.endpoints[epid]) return [0] async def remove_from_group(self, grp_id: int) -> None: for epid in self.endpoints: if not epid: continue # skip ZDO self.application.groups[grp_id].remove_member(self.endpoints[epid]) return [0] @property def manufacturer(self): return "dresden elektronik" @property def model(self): return self._model @classmethod async def new(cls, application, ieee, nwk, model: str): """Create or replace zigpy device.""" dev = cls(model, application, ieee, nwk) if ieee in application.devices: from_dev = application.get_device(ieee=ieee) dev.status = from_dev.status dev.node_desc = from_dev.node_desc for ep_id, from_ep in from_dev.endpoints.items(): if not ep_id: continue # Skip ZDO ep = dev.add_endpoint(ep_id) ep.profile_id = from_ep.profile_id ep.device_type = from_ep.device_type ep.status = from_ep.status ep.in_clusters = from_ep.in_clusters ep.out_clusters = from_ep.out_clusters else: application.devices[ieee] = dev await dev.initialize() return dev