├── .gitattributes ├── .github ├── copilot-instructions.md └── workflows │ ├── build-docs.yml │ ├── codeql-analysis.yml │ ├── publish.yml │ └── test-on-push.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── codecov.yml ├── docs ├── Makefile ├── make.bat ├── modules.rst └── source │ ├── _static │ └── .gitkeep │ ├── api_introduction.rst │ ├── conf.py │ ├── gettingstarted.rst │ ├── homematicip.aio.rst │ ├── homematicip.base.rst │ ├── homematicip.rst │ ├── index.rst │ └── modules.rst ├── homematicip_demo ├── __init__.py ├── client.pem ├── fake_cloud_server.py ├── helper.py ├── json_data │ ├── home.json │ ├── security_journal.json │ └── unknown_types.json ├── server.key └── server.pem ├── pyproject.toml ├── readthedocs.yaml ├── requirements.txt ├── requirements_dev.txt ├── requirements_docs.txt ├── run.py ├── setup.cfg ├── setup.py ├── src └── homematicip │ ├── EventHook.py │ ├── HomeMaticIPObject.py │ ├── __init__.py │ ├── __main__.py │ ├── access_point_update_state.py │ ├── async_home.py │ ├── auth.py │ ├── base │ ├── __init__.py │ ├── channel_event.py │ ├── constants.py │ ├── enums.py │ ├── functionalChannels.py │ ├── helpers.py │ └── homematicip_object.py │ ├── class_maps.py │ ├── cli │ ├── hmip_cli.py │ └── hmip_generate_auth_token.py │ ├── client.py │ ├── connection │ ├── __init__.py │ ├── buckets.py │ ├── client_characteristics_builder.py │ ├── client_token_builder.py │ ├── connection_context.py │ ├── connection_factory.py │ ├── connection_url_resolver.py │ ├── rate_limited_rest_connection.py │ ├── rest_connection.py │ └── websocket_handler.py │ ├── device.py │ ├── exceptions │ ├── __init__.py │ └── connection_exceptions.py │ ├── functionalHomes.py │ ├── group.py │ ├── home.py │ ├── location.py │ ├── oauth_otk.py │ ├── rule.py │ ├── securityEvent.py │ └── weather.py ├── test.py ├── test_aio.py └── tests ├── conftest.py ├── connection ├── test_buckets.py ├── test_client_characteristics_builder.py ├── test_connection_context.py ├── test_connection_url_resolver.py ├── test_rate_limited_rest_connection.py └── test_rest_connection.py ├── fake_hmip_server.py ├── test_auth.py ├── test_config.py ├── test_devices.py ├── test_fake_cloud.py ├── test_functional_channels.py ├── test_groups.py ├── test_hmip_cli.py ├── test_home.py ├── test_misc.py └── test_websocket.py /.gitattributes: -------------------------------------------------------------------------------- 1 | homematicip/_version.py export-subst 2 | * text=auto -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | This library is primarily used to receive data from HomematicIP Cloud and forward it to HomeAssistant via events. 2 | For various devices, data can also be sent to the cloud to execute commands. 3 | 4 | The library is written in Python. 5 | 6 | To receive status updates from the HomematicIP Cloud, a websocket connection is used. 7 | Commands to the cloud are executed via REST calls. 8 | 9 | # Instructions 10 | - Use English text for comments and docstrings. 11 | - Use the PEP 8 style guide for Python code. 12 | - Use reStructuredText format for docstrings 13 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: "Build docs" 2 | 3 | on: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build_docs: 9 | # The type of runner that the job will run on 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.12" 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install -r requirements_docs.txt 21 | pip install -e . 22 | - name: Sphinx build 23 | run: | 24 | sphinx-build docs/source docs/build/html 25 | - name: Deploy to GitHub Pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | publish_branch: gh-pages 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: docs/build/html 31 | force_orphan: true 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | release: 17 | types: [published] 18 | # push: 19 | # branches: [ master ] 20 | # pull_request: 21 | # # The branches below must be a subset of the branches above 22 | # branches: [ master ] 23 | # schedule: 24 | # - cron: '24 0 * * 1' 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | language: [ 'python' ] 35 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 36 | # Learn more... 37 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish on release" 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 1 15 | matrix: 16 | python-version: ["3.12", "3.13"] 17 | steps: 18 | - name: Set Timezone 19 | uses: szenius/set-timezone@v1.1 20 | with: 21 | timezoneLinux: "Europe/Berlin" 22 | timezoneMacos: "Europe/Berlin" 23 | timezoneWindows: "Europe/Berlin" 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install -r requirements_dev.txt 33 | pip install -e . 34 | - name: Run tests and collect coverage 35 | run: pytest --cov tests --asyncio-mode=legacy 36 | # - name: Upload coverage to Codecov 37 | # uses: codecov/codecov-action@v3 38 | 39 | deploy: 40 | needs: test 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v3 44 | with: 45 | fetch-depth: 0 46 | - name: Set up Python 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: "3.x" 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install build 54 | - name: Build package 55 | run: python -m build 56 | - name: Publish package 57 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 58 | with: 59 | user: __token__ 60 | password: ${{ secrets.PYPI_API_TOKEN }} 61 | build_docs: 62 | # Publish docs after deployment 63 | needs: deploy 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: actions/checkout@v4 67 | - name: Set up Python 68 | uses: actions/setup-python@v4 69 | with: 70 | python-version: "3.12" 71 | - name: Install dependencies 72 | run: | 73 | python -m pip install --upgrade pip 74 | pip install -r requirements_docs.txt 75 | pip install -e . 76 | - name: Sphinx build 77 | run: | 78 | sphinx-build docs/source docs/build/html 79 | - name: Deploy to GitHub Pages 80 | uses: peaceiris/actions-gh-pages@v3 81 | with: 82 | publish_branch: gh-pages 83 | github_token: ${{ secrets.GITHUB_TOKEN }} 84 | publish_dir: docs/build/html 85 | force_orphan: true 86 | -------------------------------------------------------------------------------- /.github/workflows/test-on-push.yml: -------------------------------------------------------------------------------- 1 | name: "Test on push" 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.12", "3.13"] 14 | steps: 15 | - name: Set Timezone 16 | uses: szenius/set-timezone@v1.1 17 | with: 18 | timezoneLinux: "Europe/Berlin" 19 | timezoneMacos: "Europe/Berlin" 20 | timezoneWindows: "Europe/Berlin" 21 | - uses: actions/checkout@v3 22 | - name: Set up Python 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -r requirements_dev.txt 30 | pip install -e . 31 | - name: Run tests and collect coverage 32 | run: pytest --cov=./ tests --asyncio-mode=legacy 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v3 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | venv*/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | /config.py 92 | /.vs 93 | 94 | # PyCharm 95 | .idea 96 | 97 | /MANIFEST 98 | 99 | # CSV 100 | *.csv 101 | 102 | /Backup 103 | 104 | .DS_Store 105 | /config.ini 106 | /.pytest_cache 107 | /apk 108 | /TestResults 109 | /venv38 110 | 111 | #Vscode folder 112 | .vscode/ 113 | 114 | #own config file 115 | homematic.ini 116 | src/homematicip/_version.py 117 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at green@corona-bytes.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE.txt 2 | include homematicip/_version.py 3 | include homematicip_demo/json_data/*.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HomematicIP REST API 2 | 3 | A **Python 3** wrapper for the homematicIP REST API (Access Point Based) 4 | Since there is no official documentation about this API everything was 5 | done via reverse engineering. Use at your own risk. 6 | 7 | Any help from the community through e.g. pull requests would be highly appreciated. 8 | 9 | [![PyPI download month](https://img.shields.io/pypi/dm/homematicip.svg)](https://pypi.python.org/pypi/homematicip/) [![PyPI version fury.io](https://badge.fury.io/py/homematicip.svg)](https://pypi.python.org/pypi/homematicip/) [![Discord](https://img.shields.io/discord/537253254074073088.svg?logo=discord&style=plastic)](https://discord.gg/mZG2myJ) [![CircleCI](https://circleci.com/gh/hahn-th/homematicip-rest-api.svg?style=shield)](https://circleci.com/gh/hahn-th/homematicip-rest-api) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/homematicip) 10 | 11 | ## :ghost: Lot's of changes in version 2.0.0 12 | 13 | In version 2.0.0, there are many code and API changes. Almost all async devices have been removed. The corresponding functions are now in the formerly non-async classes. 14 | 15 | ## Get Help / Discord 16 | 17 | If you want to get in contact with me or need help with the library, you can get in touch with me via discord. There is a **[discord server](https://discord.gg/mZG2myJ)** and my discord tag is **agonist#6159** 18 | 19 | ## :book: New Documentation 20 | 21 | There is a new documentation site unter https://hahn-th.github.io/homematicip-rest-api which is still under construction. 22 | 23 | ## Support me 24 | 25 | :heart: If you want to say thank you or want to support me, you can do that via PayPal. 26 | [https://paypal.me/thomas08154711](https://paypal.me/thomas08154711?country.x=DE&locale.x=de_DE) 27 | 28 | ## Thanks 29 | 30 | Kudos and a big thank you to @coreGreenberet, who created this library. 31 | 32 | ## Installation 33 | 34 | To install the package, run: 35 | ```sh 36 | pip install -U homematicip 37 | ``` 38 | 39 | ### "Nightly" Builds 40 | 41 | Each push on the master branch will trigger a build. That way you can test the latest version of the library with your systems. 42 | ```sh 43 | pip install -U homematicip --pre 44 | ``` 45 | 46 | ## New devices and config dump 47 | 48 | If you missing a device which is not implemented yet, open an issue and append a dump of your configuration to it using https://gist.github.com. 49 | To create a dump use the CLI: 50 | ```sh 51 | hmip_cli --dump-configuration --anonymize 52 | ``` 53 | See [Usage](#usage) for more instructions. 54 | 55 | ## Usage 56 | 57 | ### Generate Token 58 | 59 | If you are about to connect to a **HomematicIP HCU1** you have to press the button on top of the device, before running the script. From now, you have 5 Minutes to complete the registration process. 60 | 61 | After that, run `hmip_generate_auth_token` (from the command line) to get an auth token for your access point. it will generate a “config.ini” in your current directory. 62 | 63 | ### Use the CLI 64 | 65 | You can send commands to homematicIP using the `hmip_cli` script. To get an overview, use -h or --help param. To address devices, use the argument -d in combination with the 24-digit ID (301400000000000000000000) from --list-devices. 66 | 67 | A few examples: 68 | 69 | - `hmip_cli --help` to get help 70 | - `hmip_cli --list-devices` to get a list of your devices. 71 | - `hmip_cli -d --toggle-garage-door` to toogle the garage door with HmIP-WGC. 72 | - `hmip_cli --list-events` to listen to events and changes in your homematicIP system 73 | - `hmip_cli -d --set-lock-state LOCKED --pin 1234` to lock a door with HmIP-DLD 74 | - `hmip_cli --dump-configuration --anonymize` to dump the current config and anonymize it. 75 | 76 | ## Implemented Stuff 77 | 78 | - [x] Generate authentication token 79 | - [x] Read current state of the Environment 80 | - [x] Weather 81 | - [x] Location 82 | - [x] Basic Informations( apversion, pinAssigned, timeZone, … ) 83 | - [x] Devices (partly) 84 | - [x] Client 85 | - [x] Groups 86 | 87 | ## Homematic IP Devices: 88 | 89 | - [x] ALPHA-IP-RBG (Alpha IP Wall Thermostat Display) 90 | - [x] ALPHA-IP-RBGa (ALpha IP Wall Thermostat Display analog) 91 | - [ ] ELV-SH-AI8 (Alarmline Interface 8x Inputs) \*powered by HmIP 92 | - [ ] ELV-SH-BS2 (Switch Actuator for brand switches 2x channels) \*powered by HmIP 93 | - [x] ELV-SH-CTV Tilt Vibration Sensor Compact 94 | - [ ] ELV-SH-GVI (Garden valve interface) \*powered by HmIP 95 | - [ ] ELV-SH-IRS8 (Infared Remote control - 8x channels) \*powered by HmIP 96 | - [ ] ELV-SH-SW1-BAT (2x Actuator Switch for 30V/1A with 2xAA Batteries) \*powered by HmIP 97 | - [ ] ELV-SH-WUA (Dimming Actuator, 0-10/1-10-V-Control inputs, 8A 230V) \*powered by HmIP 98 | - [ ] ELV-SH-WSC (2x Servo Controls, 5v - 12V) \*powered by HmIP 99 | - [x] HMIP-ASIR (Alarm Siren - indoor) 100 | - [x] HMIP-ASIR-B1 (Alarm Siren - indoor) _Silvercrest Edition_ 101 | - [x] HMIP-ASIR-2 (Alarm Siren - indoor) New Version 102 | - [x] HMIP-ASIR-O (Alarm Siren - outdoor) 103 | - [x] HMIP-BBL (Blind Actuator for brand switches) 104 | - [ ] HMIP-BBL-2 (Blind Actuator for brand switches) New Version 105 | - [x] HMIP-BDT (Dimming Actuator for brand switches) 106 | - [x] HMIP-BRC2 (Remote Control for brand switches – 2x channels) 107 | - [x] HMIP-BROLL (Shutter Actuator - brand-mount) 108 | - [ ] HMIP-BROLL-2 (Shutter Actuator - brand-mount) New Version 109 | - [x] HMIP-BSL (Switch Actuator for brand switches – with signal lamp) 110 | - [x] HMIP-BSM (Brand Switch and Meter Actuator) 111 | - [ ] HMIP-BSM-I (Brand Switch and Meter Actuator, International) 112 | - [x] HMIP-BWTH (Wall Thermostat Display with switching output – for brand switches, 230V) 113 | - [ ] HMIP-BWTH24 (Wall Thermostat Display with switching output – for brand switches, 24V) 114 | - [x] HMIP-DBB (Doorbell Push-Button) 115 | - [x] HMIP-DLD (Door Lock Drive) 116 | - [x] HMIP-DLS (Door Lock Sensor) 117 | - [x] HmIP-DRG-DALI (Dali Gateway - readonly at the moment) 118 | - [x] HMIP-DRBLI4 (Blind Actuator for DIN rail mount – 4 channels) 119 | - [x] HMIP-DRSI1 (Switch Actuator for DIN rail mount – 1x channel) 120 | - [x] HMIP-DRDI3 (Dimming Actuator Inbound 230V – 3x channels, 200W per channel) electrical DIN rail 121 | - [x] HMIP-DRSI4 (Switch Actuator for DIN rail mount – 4x channels) 122 | - [x] HMIP-DSD-PCB (Door Signal Dector PCB) 123 | - [x] HMIP-eTRV (Heating-Thermostat with Display) 124 | - [x] HMIP-eTRV-2 (Heating-Thermostat with Display) New Version 125 | - [ ] HMIP-eTRV-2 I9F (Heating-Thermostat with Display) New Version 126 | - [ ] HMIP-eTRV-2-UK (UK Version not tested, but it should work) 127 | - [x] HMIP-eTRV-B (Heating-Thermostat basic with Display) 128 | - [ ] HMIP-eTRV-B-2 (Heating-Thermostat basic with Display) New Version 129 | - [ ] HMIP-eTRV-B-2 R4M (Heating-Thermostat basic with Display) New Version 130 | - [ ] HMIP-eTRV-B-UK (UK Version not tested, but it should work) 131 | - [x] HMIP-eTRV-B1 (Heating-Thermostat basic with Display) _Silvercrest Edition_ 132 | - [x] HMIP-eTRV-C (Heating-Thermostat compact without display) 133 | - [x] HMIP-eTRV-C-2 (Heating-Thermostat compact without display) New Version 134 | - [ ] HmIP-eTRV-CL (Heating-thermostat compact with dispay) 135 | - [x] HMIP-eTRV-E (Heating-Thermostat Design Evo _New Generation_, white) 136 | - [ ] HMIP-eTRV-E-A (Heating-Thermostat Design Evo _New Generation_, anthracite) 137 | - [ ] HMIP-eTRV-E-S (Heating-Thermostat Design Evo _New Generation_, silver) 138 | - [x] HMIP-eTRV-3 (Heating-Thermostat pure, white) 139 | - [x] HMIP-eTRV-F (Heating-Thermostat flex, white) 140 | - [x] HMIP-FAL230-C6 (Floor Heating Actuator – 6x channels, 230V) 141 | - [x] HMIP-FAL230-C10 (Floor Heating Actuator – 10x channels, 230V) 142 | - [x] HMIP-FAL24-C6 (Floor Heating Actuator – 6x channels, 24V) 143 | - [x] HMIP-FAL24-C10 (Floor Heating Actuator – 10x channels, 24V) 144 | - [x] HMIP-FALMOT-C12 (Floor Heating Actuator – 12x channels, motorised) 145 | - [x] HMIP-FBL (Blind Actuator - flush-mount) 146 | - [x] HMIP-FCI1 (Contact Interface flush-mount – 1x channel) 147 | - [x] HMIP-FCI6 (Contact Interface flush-mount – 6x channels) 148 | - [x] HMIP-FDT (Dimming Actuator - flush-mount) 149 | - [x] HMIP-FROLL (Shutter Actuator - flush-mount) 150 | - [x] HMIP-FSI16 (Switch Actuator with Push-button Input 230V, 16A) 151 | - [x] HMIP-FSM (Switch Actuator and Meter 5A – flush-mount) 152 | - [x] HMIP-FSM16 (Switch Actuator and Meter 16A – flush-mount) 153 | - [ ] HMIP-FWI (Wiegand Interface) 154 | - [x] HMIP-HAP (Cloud Access Point) 155 | - [x] HMIP-HAP-B1 (Cloud Access Point) _Silvercrest Edition_ 156 | - [x] HMIP-HDM1 (Hunter Douglas & erfal window blinds 157 | - [ ] HMIP-HDRC (Hunter Douglas & erfal window blinds remote control) 158 | - [ ] HMIP-K-DRBLI4 (Blinds Actuator – 4x channels, 230V, 2,2A / 500W per channel) electrical DIN rail 159 | - [ ] HMIP-K-DRSI1 (Actuator Inbound 230V – 1x channel) electrical DIN rail 160 | - [ ] HMIP-K-DRDI3 (Dimming Actuator Inbound 230V – 3x channels, 200W per channel) electrical DIN rail 161 | - [ ] HMIP-K-DRSI4 (Switch Actuator – 4x channels, 16A per channel) electrical DIN rail 162 | - [x] HMIP-KRCA (Key Ring Remote Control & Alarm) 163 | - [x] HMIP-KRC4 (Key Ring Remote Control - 4x buttons) 164 | - [ ] HMIP-MIO16-PCB (Multi Analog/Digitial Interface - Switch Circuit Board) 165 | - [x] HMIP-MIOB (Multi IO Box for floor heating & cooling) 166 | - [x] HMIP-MOD-HO (Garage Door Module for Hörmann) 167 | - [x] HMIP-MOD-OC8 (Open Collector Module Receiver - 8x) 168 | - [x] HMIP-MOD-RC8 (Open Collector Module Sender - 8x) 169 | - [x] HMIP-MOD-TM (Garage Door Module for Novoferm and Tormatic door operators) 170 | - [ ] HMIP-MP3P (Combination Signalling Device MP3) 171 | - [ ] HMIP-P-DRG-DALI (DALI Lights Gateway) 172 | - [x] HMIP-PCBS (Switch Circuit Board - 1x channel) 173 | - [x] HMIP-PCBS2 (Switch Circuit Board - 2x channels) 174 | - [x] HMIP-PCBS-BAT (Switch Circuit Board with Battery - 1x channel) 175 | - [x] HMIP-PDT (Plugable Dimmer) 176 | - [ ] HMIP-PDT-UK (UK Version not tested, but it should work) 177 | - [x] HMIP-PMFS (Plugable Power Supply Monitoring) 178 | - [x] HMIP-PS (Plugable Switch) 179 | - [ ] HMIP-PS-2 (Plugable Switch) New Version 180 | - [x] HMIP-PSM (Plugable Switch Measuring, Type F - Standard for Homematic) 181 | - [ ] HMIP-PSM-2 (Plugable Switch Measuring, Type F - Standard for Homematic) New Version 182 | - [x] HMIP-PSM-CH (Plugable Switch Measuring, Type J) 183 | - [ ] HMIP-PSM-IT (Type L not tested, but it should work) 184 | - [ ] HMIP-PSM-PE (Type E not tested, but it should work) 185 | - [ ] HMIP-PSM-UK (Type G not tested, but it should work) 186 | - [x] HMIP-PSMCO (Schalt-Mess-Kabel – außen) 187 | - [x] HMIP-RC8 (Remote Control - 8x buttons) 188 | - [ ] HMIP-RCB1 (Remote Control - 1x button) 189 | - [x] HMIP-RGBW (RGB Led Controller - Readonly at the moment) 190 | - [x] HMIP-SAM (Acceleration Sensor) 191 | - [x] HMIP-SCI (Contact Interface Sensor) 192 | - [x] HMIP-SCTH230 (CO2, Temperature and Humidity Sensor 230V) 193 | - [ ] HMIP-SFD (Fine Dust Sensor) 194 | - [x] HMIP-SLO (Light Sensor - outdoor) 195 | - [x] HMIP-SMI (Motion Detector with Brightness Sensor - indoor) 196 | - [x] HMIP-SMI55 (Motion Detector with Brightness Sensor and Remote Control - 2x buttons) 197 | - [ ] HMIP-SMI55-2 (Motion Detector with Brightness Sensor and Remote Control - 2x buttons) New Version 198 | - [x] HMIP-SMO (Motion Detector with Brightness Sensor - outdoor) 199 | - [ ] HMIP-SMO-2 (Motion Detector with Brightness Sensor - outdoor) New Version 200 | - [x] HMIP-SMO-A (Motion Detector with Brightness Sensor - outdoor, anthracite) 201 | - [ ] HMIP-SMO-A-2 (Motion Detector with Brightness Sensor - outdoor, anthracite) New Version 202 | - [x] HmIP-SMO230-A 203 | - [x] HMIP-SPDR (Passage Sensor with Direction Recognition) 204 | - [x] HMIP-SPI (Presence Sensor - indoor) 205 | - [x] HMIP-SRH (Window Rotary Handle Sensor) 206 | - [x] HMIP-SRD (Rain Sensor) 207 | - [x] HMIP-STE2-PCB (Temperature Difference Sensors - 2x sensors) 208 | - [x] HMIP-STH (Temperature and Humidity Sensor without display - indoor) 209 | - [x] HMIP-STHD (Temperature and Humidity Sensor with display - indoor) 210 | - [x] HMIP-STHO (Temperature and Humidity Sensor - outdoor) 211 | - [x] HMIP-STHO-A (Temperature and Humidity Sensor – outdoor, anthracite) 212 | - [x] HMIP-STV (Inclination and vibration Sensor) 213 | - [x] HMIP-SWD (Water Sensor) 214 | - [x] HMIP-SWDM (Door / Window Contact - magnetic) 215 | - [ ] HMIP-SWDM-2 (Door / Window Contact - magnetic) New Version 216 | - [x] HMIP-SWDM-B2 (Door / Window Contact - magnetic) _Silvercrest Edition_ 217 | - [x] HMIP-SWDO (Shutter Contact Optical) 218 | - [ ] HMIP-SWDO-2 (Shutter Contact Optical) New Version 219 | - [x] HMIP-SWDO-I (Shutter Contact Optical Invisible) 220 | - [x] HMIP-SWDO-PL (Shutter Contact Optical Plus) 221 | - [ ] HMIP-SWDO-PL-2 (Shutter Contact Optical Plus) New Version 222 | - [x] HMIP-SWO-B (Weather Sensor - Basic) 223 | - [x] HMIP-SWO-PL (Weather Sensor – Plus) 224 | - [x] HMIP-SWO-PR (Weather Sensor – Pro) 225 | - [x] HMIP-SWSD (Smoke Detector) 226 | - [ ] HMIP-USBSM (USB Switching Measurement Actuator) 227 | - [x] HMIP-WGC (Garage Door Button) 228 | - [x] HMIP-WHS2 (Switch Actuator for heating systems – 2x channels) 229 | - [x] HMIP-WKP (Keypad) 230 | - [x] HMIP-WLAN-HAP (WLAN Access Point) 231 | - [x] HmIP-WLAN-HAP-B 232 | - [x] HMIP-WRC2 (Wall-mount Remote Control - 2x buttons) 233 | - [x] HMIP-WRC6 (Wall-mount Remote Control - 6x buttons) 234 | - [x] HMIP-WRCC2 (Wall-mount Remote Control – flat) 235 | - [ ] HMIP-WRCD (Wall-mount Remote Control - E-Paper-Status display) 236 | - [ ] HMIP-WRCR (Wall-mount Remote Control - Rotary) 237 | - [ ] HMIP-WT (Wall Mounted Thermostat without adjusting wheel) #probably only prototype for WTH-B and was not released 238 | - [x] HMIP-WTH (Wall Mounted Thermostat Pro with Display) 239 | - [ ] HMIP-WTH-1 (Wall Mounted Thermostat Pro with Display _Newest Version_ - successor of WTH-2 - really) 240 | - [x] HMIP-WTH-2 (Wall Mounted Thermostat Pro with Display) 241 | - [x] HMIP-WTH-B (Wall Mounted Thermostat basic without adjusting wheel) 242 | - [ ] HMIP-WTH-B-2 (Wall Mounted Thermostat basic without adjusting wheel) New Version 243 | - [ ] HMIP-WUA (Dimming Actuator, 0-10/1-10-V-Control inputs, 8A 230V) 244 | 245 | ## Homematic IP Wired Devices (no radio signal): 246 | 247 | - [x] HMIPW-DRAP (Homematic IP Wired Access Point) 248 | - [ ] HMIPW-BRC2 (Homematic IP Wired Remote Control for brand switches – 2x channels) 249 | - [x] HMIPW-DRBL4 (Homematic IP Wired Blinds Actuator – 4x channels) 250 | - [x] HMIPW-DRD3 (Homematic IP Wired Dimming Actuator – 3x channels) 251 | - [x] HMIPW-DRS4 (Homematic IP Wired Switch Actuator – 4x channels) 252 | - [ ] HMIPW-DRI16 (Homematic IP Wired Inbound module – 16x channels) 253 | - [x] HMIPW-DRI32 (Homematic IP Wired Inbound module – 32x channels) 254 | - [x] HMIPW-DRS8 (Homematic IP Wired Switch Actuator – 8x channels) 255 | - [ ] HMIPW-FAL24-C6 (Homematic IP Wired Floor Heating Actuator – 6x channels, 24V) 256 | - [ ] HMIPW-FAL24-C10 (Homematic IP Wired Floor Heating Actuator – 10x channels, 24V) 257 | - [ ] HMIPW-FAL230-C6 (Homematic IP Wired Floor Heating Actuator – 6x channels, 230V) 258 | - [ ] HMIPW-FAL230-C10 (Homematic IP Wired Floor Heating Actuator – 10x channels, 230V) 259 | - [x] HMIPW-FALMOT-C12 (Homematic IP Wired Floor Heating Actuator – 12x channels, motorised) 260 | - [x] HMIPW-FIO6 (Homematic IP Wired IO Module flush-mount – 6x channels) 261 | - [x] HMIPW-SCTHD (Homematic IP Wired CO2, Temperature and Humidity Sensor with Display) 262 | - [x] HMIPW-SMI55 (Homematic IP Wired Motion Detector with Brightness Sensor and Remote Control - 2x buttons) 263 | - [ ] HMIPW-SPI (Homematic IP Wired Presence Sensor - indoor) 264 | - [ ] HMIPW-STH (Homematic IP Wired Temperature and Humidity Sensor without display - indoor) 265 | - [ ] HMIPW-STHD (Homematic IP Wired Temperature and Humidity Sensor with display - indoor) 266 | - [ ] HMIPW-WGD (Homematic IP Wired Wall-mount Glas Display - black edition) 267 | - [ ] HMIPW-WGD-PL (Homematic IP Wired Wall-mount Glas Display Play - black edition) 268 | - [x] HMIPW-WRC2 (Homematic IP Wired Wall-mount Remote Control - 2x channels) 269 | - [x] HMIPW-WRC6 (Homematic IP Wired Wall-mount Remote Control - 6x channels) 270 | - [ ] HMIPW-WTH (Homematic IP Wired Wall Mounted Thermostat Pro with Display) 271 | 272 | ## Events 273 | 274 | It’s also possible to use push notifications based on a websocket connection: 275 | 276 | ```python 277 | # Example function to display incoming events. 278 | def print_events(event_list): 279 | for event in event_list: 280 | print("EventType: {} Data: {}".format(event["eventType"], event["data"])) 281 | 282 | 283 | # Initialise the API. 284 | config = homematicip.find_and_load_config_file() 285 | home = Home() 286 | home.set_auth_token(config.auth_token) 287 | home.init(config.access_point) 288 | 289 | # Add function to handle events and start the connection. 290 | home.onEvent += print_events 291 | home.enable_events() 292 | 293 | try: 294 | while True: 295 | time.sleep(1) 296 | except KeyboardInterrupt: 297 | print("Interrupt.") 298 | ``` 299 | 300 | ## Pathes for config.ini 301 | 302 | The scripts will look for a config.ini in 3 303 | different locations depending on your OS. Copy the file to one of these 304 | locations so that it will be accessible for the scripts. 305 | 306 | - General 307 | - current working directory 308 | - Windows 309 | - %APPDATA%\\homematicip-rest-api 310 | - %PROGRAMDATA%\\homematicip-rest-api 311 | - Linux 312 | - ~/.homematicip-rest-api/ 313 | - /etc/homematicip-rest-api/ 314 | - MAC OS 315 | - ~/Library/Preferences/homematicip-rest-api/ 316 | - /Library/Application Support/homematicip-rest-api/ 317 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "hmip_cli.py" 3 | - "hmip_generate_auth_token.py" 4 | - "versioneer.py" 5 | - "test.py" 6 | - "test_aio.py" 7 | - "setup.py" 8 | - "homematicip/_version.py" 9 | - "homematicip_cli_async.py" 10 | - "homematicip/base/constants.py" 11 | - "homematicip/HomeMaticIPObject.py" 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | src 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | homematicip 8 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahn-th/homematicip-rest-api/c745e0293bbf34ef757058a5f48cb88b8172a5ca/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/api_introduction.rst: -------------------------------------------------------------------------------- 1 | API Introduction 2 | **************** 3 | 4 | There are a few key classes for communication with the Rest API of HomematicIP. 5 | 6 | | **Home:** is the most important object as it has the "overview" of the installation 7 | | **Group:** a group of devices for a specific need. E.g. Heating group, security group, ... 8 | | **MetaGroup:** a collection of groups. In the HomematicIP App this is called a "Room" 9 | | **Device:** a hardware device e.g. shutter contact, heating thermostat, alarm siren, ... 10 | | **FunctionChannel:** a channel of a device. For example DoorLockChannel for DoorLockDrive or **DimmerChannel**. A device has multiple channels - depending on its functions. 11 | 12 | | For example: 13 | | The device HmIP-DLD is represented by the class **DoorLockDrive** (or AsyncDoorLockDrive). The device has multiple channels. 14 | | The base channel holds informations about the device and has the index 0. 15 | | The device has also a channel called **DoorLockChannel** which contains the functions "set_lock_state" and "async_set_lock_state". These are functions to set the lock state of that device. 16 | 17 | If you have dimmer with multiple I/Os, there are multiple channels. For each I/O a unique channel. -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import os 7 | import sys 8 | 9 | from importlib.metadata import version 10 | 11 | sys.path.insert(0, os.path.abspath("../src")) 12 | 13 | # -- Project information ----------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 15 | 16 | project = "HomematicIP-Rest-API" 17 | copyright = "2024, Thomas Hahn" 18 | author = "Thomas Hahn" 19 | release = version("homematicip") 20 | version = release 21 | # version = ".".join(release.split(".")[:3]) 22 | 23 | # -- General configuration --------------------------------------------------- 24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 25 | 26 | extensions = [ 27 | "sphinx.ext.autodoc", 28 | "sphinx.ext.viewcode", 29 | "sphinx.ext.napoleon", 30 | "myst_parser", 31 | ] 32 | 33 | templates_path = ["_templates"] 34 | exclude_patterns = [] 35 | 36 | # -- Options for HTML output ------------------------------------------------- 37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 38 | 39 | html_theme = "sphinx_rtd_theme" 40 | html_static_path = ["_static"] 41 | -------------------------------------------------------------------------------- /docs/source/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | *************** 3 | 4 | Installation 5 | ============ 6 | 7 | Just run **pip3 install -U homematicip** in the command line to get the package. 8 | This will install (and update) the library and all required packages 9 | 10 | Getting the AUTH-TOKEN 11 | ====================== 12 | Before you can start using the library you will need an auth-token. Otherwise the HMIP Cloud will not trust you. 13 | 14 | You will need: 15 | 16 | - Access to an active Access Point (it must glow blue) 17 | - the SGTIN of the Access Point 18 | - [optional] the PIN 19 | 20 | 21 | If you are about to connect to a **HomematicIP HCU1** you have to press the button on top of the device, before running the script. From now, you have 5 Minutes to complete the registration process. 22 | 23 | To get an auth-token you have to run the script `hmip_generate_auth_token` which is installed with the library. 24 | 25 | ```sh 26 | hmip_generate_auth_token 27 | ``` 28 | 29 | It will generate a **config.ini** in your current working directory. The scripts which are using this library are looking 30 | for this file to load the auth-token and SGTIN of the Access Point. You can either place it in the working directory when you are 31 | running the scripts or depending on your OS in different "global" folders: 32 | 33 | - General 34 | 35 | - current working directory 36 | 37 | - Windows 38 | 39 | - %APPDATA%\\homematicip-rest-api\ 40 | - %PROGRAMDATA%\\homematicip-rest-api\ 41 | 42 | - Linux 43 | 44 | - ~/.homematicip-rest-api/ 45 | - /etc/homematicip-rest-api/ 46 | 47 | - MAC OS 48 | 49 | - ~/Library/Preferences/homematicip-rest-api/ 50 | - /Library/Application Support/homematicip-rest-api/ 51 | 52 | Using the CLI 53 | ============= 54 | 55 | You can send commands to homematicIP using the `hmip_cli` script. To get an overview, use -h or --help param. To address devices, use the argument -d in combination with the 24-digit ID (301400000000000000000000) from --list-devices. 56 | 57 | Get Information about devices and groups 58 | ---------------------------------------- 59 | 60 | Commands are bound to the channel type. To get a list of all allowed actions for a device you can write `hmip_cli -d {deviceid} --print-allowed-commands` or `hmip_cli -d {deviceid} -ac`. 61 | 62 | To get infos for a device and its channels use the `--print-infos` argument in combination with -d for a device or -g for a group. 63 | 64 | Examples 65 | -------- 66 | 67 | A few examples: 68 | 69 | - `hmip_cli --help` to get help 70 | - `hmip_cli --list-devices` to get a list of your devices. 71 | - `hmip_cli -d --toggle-garage-door` to toogle the garage door with HmIP-WGC. 72 | - `hmip_cli --list-events` to listen to events and changes in your homematicIP system 73 | - `hmip_cli -d --set-lock-state LOCKED --pin 1234` to lock a door with HmIP-DLD 74 | - `hmip_cli --dump-configuration --anonymize` to dump the current config and anonymize it. 75 | -------------------------------------------------------------------------------- /docs/source/homematicip.aio.rst: -------------------------------------------------------------------------------- 1 | homematicip.aio package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | homematicip.aio.auth module 8 | --------------------------- 9 | 10 | .. automodule:: homematicip.aio.auth 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | homematicip.aio.class\_maps module 16 | ---------------------------------- 17 | 18 | .. automodule:: homematicip.aio.class_maps 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | homematicip.aio.connection module 24 | --------------------------------- 25 | 26 | .. automodule:: homematicip.aio.connection 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | homematicip.aio.device module 32 | ----------------------------- 33 | 34 | .. automodule:: homematicip.aio.device 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | homematicip.aio.group module 40 | ---------------------------- 41 | 42 | .. automodule:: homematicip.aio.group 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | homematicip.aio.home module 48 | --------------------------- 49 | 50 | .. automodule:: homematicip.aio.home 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | homematicip.aio.rule module 56 | --------------------------- 57 | 58 | .. automodule:: homematicip.aio.rule 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | homematicip.aio.securityEvent module 64 | ------------------------------------ 65 | 66 | .. automodule:: homematicip.aio.securityEvent 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | Module contents 72 | --------------- 73 | 74 | .. automodule:: homematicip.aio 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | -------------------------------------------------------------------------------- /docs/source/homematicip.base.rst: -------------------------------------------------------------------------------- 1 | homematicip.base package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | homematicip.base.HomeMaticIPObject module 8 | ----------------------------------------- 9 | 10 | .. automodule:: homematicip.base.HomeMaticIPObject 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | homematicip.base.base\_connection module 16 | ---------------------------------------- 17 | 18 | .. automodule:: homematicip.base.base_connection 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | homematicip.base.constants module 24 | --------------------------------- 25 | 26 | .. automodule:: homematicip.base.constants 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | homematicip.base.enums module 32 | ----------------------------- 33 | 34 | .. automodule:: homematicip.base.enums 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | homematicip.base.functionalChannels module 40 | ------------------------------------------ 41 | 42 | .. automodule:: homematicip.base.functionalChannels 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | homematicip.base.helpers module 48 | ------------------------------- 49 | 50 | .. automodule:: homematicip.base.helpers 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | Module contents 56 | --------------- 57 | 58 | .. automodule:: homematicip.base 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | -------------------------------------------------------------------------------- /docs/source/homematicip.rst: -------------------------------------------------------------------------------- 1 | homematicip package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | homematicip.aio 11 | homematicip.base 12 | 13 | Submodules 14 | ---------- 15 | 16 | homematicip.EventHook module 17 | ---------------------------- 18 | 19 | .. automodule:: homematicip.EventHook 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | homematicip.HomeMaticIPObject module 25 | ------------------------------------ 26 | 27 | .. automodule:: homematicip.HomeMaticIPObject 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | homematicip.access\_point\_update\_state module 33 | ----------------------------------------------- 34 | 35 | .. automodule:: homematicip.access_point_update_state 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | 40 | homematicip.auth module 41 | ----------------------- 42 | 43 | .. automodule:: homematicip.auth 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | homematicip.class\_maps module 49 | ------------------------------ 50 | 51 | .. automodule:: homematicip.class_maps 52 | :members: 53 | :undoc-members: 54 | :show-inheritance: 55 | 56 | homematicip.client module 57 | ------------------------- 58 | 59 | .. automodule:: homematicip.client 60 | :members: 61 | :undoc-members: 62 | :show-inheritance: 63 | 64 | homematicip.connection module 65 | ----------------------------- 66 | 67 | .. automodule:: homematicip.connection 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | 72 | homematicip.device module 73 | ------------------------- 74 | 75 | .. automodule:: homematicip.device 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | homematicip.functionalHomes module 81 | ---------------------------------- 82 | 83 | .. automodule:: homematicip.functionalHomes 84 | :members: 85 | :undoc-members: 86 | :show-inheritance: 87 | 88 | homematicip.group module 89 | ------------------------ 90 | 91 | .. automodule:: homematicip.group 92 | :members: 93 | :undoc-members: 94 | :show-inheritance: 95 | 96 | homematicip.home module 97 | ----------------------- 98 | 99 | .. automodule:: homematicip.home 100 | :members: 101 | :undoc-members: 102 | :show-inheritance: 103 | 104 | homematicip.location module 105 | --------------------------- 106 | 107 | .. automodule:: homematicip.location 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | 112 | homematicip.oauth\_otk module 113 | ----------------------------- 114 | 115 | .. automodule:: homematicip.oauth_otk 116 | :members: 117 | :undoc-members: 118 | :show-inheritance: 119 | 120 | homematicip.rule module 121 | ----------------------- 122 | 123 | .. automodule:: homematicip.rule 124 | :members: 125 | :undoc-members: 126 | :show-inheritance: 127 | 128 | homematicip.securityEvent module 129 | -------------------------------- 130 | 131 | .. automodule:: homematicip.securityEvent 132 | :members: 133 | :undoc-members: 134 | :show-inheritance: 135 | 136 | homematicip.weather module 137 | -------------------------- 138 | 139 | .. automodule:: homematicip.weather 140 | :members: 141 | :undoc-members: 142 | :show-inheritance: 143 | 144 | Module contents 145 | --------------- 146 | 147 | .. automodule:: homematicip 148 | :members: 149 | :undoc-members: 150 | :show-inheritance: 151 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. HomematicIP-Rest-API documentation master file, created by 2 | sphinx-quickstart on Fri Jan 12 11:14:32 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Homematic IP Rest API's documentation! 7 | ================================================= 8 | 9 | This documentation is for a **Python 3** wrapper for the homematicIP REST API (Access Point Based) 10 | Since there is no official documentation about this API everything was 11 | done via reverse engineering. Use at your own risk. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Getting started 16 | 17 | gettingstarted 18 | 19 | .. toctree:: 20 | :maxdepth: 4 21 | :caption: API Documentation 22 | 23 | api_introduction 24 | modules 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | homematicip 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | homematicip 8 | -------------------------------------------------------------------------------- /homematicip_demo/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for homematicip demo.""" 2 | -------------------------------------------------------------------------------- /homematicip_demo/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIByjCCAW+gAwIBAgIUQDAh8y47NcnO/WWc3/nkrU7IV9MwCgYIKoZIzj0EAwIw 3 | QDEXMBUGA1UECgwOdHJ1c3RtZSB2MS4yLjAxJTAjBgNVBAsMHFRlc3RpbmcgQ0Eg 4 | I1EtenFyeGh3Vl8zV01NWEYwIBcNMDAwMTAxMDAwMDAwWhgPMzAwMDAxMDEwMDAw 5 | MDBaMEAxFzAVBgNVBAoMDnRydXN0bWUgdjEuMi4wMSUwIwYDVQQLDBxUZXN0aW5n 6 | IENBICNRLXpxcnhod1ZfM1dNTVhGMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 7 | kKHIZlTL0OPtVRzmqskzQeDBChB/PH7T0hOAxZ21iPY5ZIU0g803huQinL/qE8T6 8 | EZR3JiZPsi0snTLEWfejf6NFMEMwHQYDVR0OBBYEFPRnOfQB4OXdgHNiBfjeZhEd 9 | wHT2MBIGA1UdEwEB/wQIMAYBAf8CAQkwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49 10 | BAMCA0kAMEYCIQCd3KQr2apEcvYBWrqrfVxyi5TPBrVVgEkVjrsoM8V7PgIhAM/t 11 | YBsqJyPLz8TXXnaAYbvkG/5fJ6y33nsFU2gUMKWD 12 | -----END CERTIFICATE----- 13 | -------------------------------------------------------------------------------- /homematicip_demo/helper.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | import warnings 4 | from functools import partial, partialmethod 5 | from pathlib import Path 6 | 7 | import requests 8 | 9 | 10 | def get_full_path(name): 11 | """Returns full path of incoming relative path. 12 | Relative path is relative to the script location. 13 | """ 14 | pth = Path(__file__).parent.joinpath(name) 15 | return pth 16 | 17 | 18 | def fake_home_download_configuration(): 19 | _full = get_full_path("json_data/home.json") 20 | with open(_full, encoding="UTF-8") as f: 21 | return json.load(f) 22 | 23 | 24 | @contextlib.contextmanager 25 | def no_ssl_verification(): 26 | old_request = requests.Session.request 27 | requests.Session.request = partialmethod(old_request, verify=False) 28 | 29 | warnings.filterwarnings("ignore", "Unverified HTTPS request") 30 | yield 31 | warnings.resetwarnings() 32 | 33 | requests.Session.request = old_request 34 | -------------------------------------------------------------------------------- /homematicip_demo/json_data/security_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "eventTimestamp": 1543777807110, 5 | "eventType": "ACTIVATION_CHANGED", 6 | "label": "Alarmanlage aus", 7 | "securityZoneValues": { 8 | "EXTERNAL": false, 9 | "INTERNAL": false 10 | } 11 | }, 12 | { 13 | "eventTimestamp": 1543777807110, 14 | "eventType": "ACTIVATION_CHANGED", 15 | "label": "Alarmanlage", 16 | "securityZoneValues": { 17 | "EXTERNAL": true, 18 | "INTERNAL": true 19 | } 20 | }, 21 | { 22 | "eventTimestamp": 1543777807110, 23 | "eventType": "ACCESS_POINT_DISCONNECTED", 24 | "label": "" 25 | }, 26 | { 27 | "eventTimestamp": 1543777807110, 28 | "eventType": "ACCESS_POINT_CONNECTED", 29 | "label": "" 30 | }, 31 | { 32 | "eventTimestamp": 1543777807110, 33 | "eventType": "SENSOR_EVENT", 34 | "label": "Fenster (Büro)" 35 | }, 36 | { 37 | "eventTimestamp": 1543777807110, 38 | "eventType": "SABOTAGE", 39 | "label": "Fenster (Schlafzimmer)" 40 | }, 41 | { 42 | "eventTimestamp": 1543777807110, 43 | "eventType": "MOISTURE_DETECTION_EVENT", 44 | "label": "Wassersensor (Badezimmer)" 45 | }, 46 | { 47 | "eventTimestamp": 1543777807110, 48 | "eventType": "FLUX_CAPACITOR_OVERLOAD_EVENT", 49 | "label": "Fluxkondensator" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /homematicip_demo/json_data/unknown_types.json: -------------------------------------------------------------------------------- 1 | { 2 | "clients": { 3 | "00000000-0000-0000-0000-000000000000": { 4 | "homeId": "00000000-0000-0000-0000-000000000001", 5 | "id": "00000000-0000-0000-0000-000000000000", 6 | "label": "TEST-Client", 7 | "clientType" : "APP" 8 | } 9 | }, 10 | "devices": { 11 | "3014F7110000000000000050": { 12 | "availableFirmwareVersion": "0.0.0", 13 | "connectionType": "HMIP_RF", 14 | "firmwareVersion": "1.0.2", 15 | "firmwareVersionInteger": "65538", 16 | "functionalChannels": { 17 | 18 | "0": { 19 | "configPending": false, 20 | "deviceId": "3014F7110000000000000050", 21 | "dutyCycle": false, 22 | "functionalChannelType": "DEVICE_UNKNOWN", 23 | "groupIndex": 0, 24 | "groups": [ 25 | ], 26 | "index": 0, 27 | "label": "", 28 | "lowBat": null, 29 | "routerModuleEnabled": false, 30 | "routerModuleSupported": false, 31 | "rssiDeviceValue": -78, 32 | "rssiPeerValue": -77, 33 | "unreach": false 34 | } 35 | }, 36 | "homeId": "00000000-0000-0000-0000-000000000001", 37 | "id": "3014F7110000000000000050", 38 | "label": "DUMMY_DEVICE", 39 | "lastStatusUpdate": 1530802738493, 40 | "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", 41 | "manufacturerCode": 1, 42 | "modelId": 353, 43 | "modelType": "HmIP-DUMMY", 44 | "oem": "eQ-3", 45 | "permanentlyReachable": true, 46 | "serializedGlobalTradeItemNumber": "3014F7110000000000000050", 47 | "type": "DUMMY_DEVICE", 48 | "updateState": "UP_TO_DATE" 49 | 50 | } 51 | }, 52 | "groups": { 53 | "00000000-0000-0000-0000-000000000020": { 54 | "channels": [], 55 | "configPending": false, 56 | "dutyCycle": false, 57 | "groups": [ ], 58 | "homeId": "00000000-0000-0000-0000-000000000001", 59 | "id": "00000000-0000-0000-0000-000000000020", 60 | "incorrectPositioned": null, 61 | "label": "DUMMY_GROUP", 62 | "lastStatusUpdate": 1524516556479, 63 | "lowBat": false, 64 | "metaGroupId": null, 65 | "sabotage": null, 66 | "type": "DUMMY_GROUP", 67 | "unreach": false 68 | } 69 | }, 70 | "home": { 71 | "accessPointUpdateStates": { 72 | "3014F711A000000000000000": { 73 | "accessPointUpdateState": "UP_TO_DATE", 74 | "successfulUpdateTimestamp": 0, 75 | "updateStateChangedTimestamp": 0 76 | } 77 | }, 78 | "apExchangeClientId": null, 79 | "apExchangeState": "NONE", 80 | "availableAPVersion": null, 81 | "carrierSense": null, 82 | "clients": [ 83 | "00000000-0000-0000-0000-000000000000" 84 | ], 85 | "connected": true, 86 | "currentAPVersion": "1.2.4", 87 | "deviceUpdateStrategy": "AUTOMATICALLY_IF_POSSIBLE", 88 | "dutyCycle": 8.0, 89 | "functionalHomes": { 90 | "DUMMY_FUNCTIONAL_HOME": { 91 | "absenceEndTime": null, 92 | "absenceType": "NOT_ABSENT", 93 | "active": true, 94 | "coolingEnabled": false, 95 | "ecoDuration": "PERMANENT", 96 | "ecoTemperature": 17.0, 97 | "floorHeatingSpecificGroups": { 98 | }, 99 | "functionalGroups": [ 100 | ], 101 | "optimumStartStopEnabled": false, 102 | "solution": "DUMMY_FUNCTIONAL_HOME" 103 | } 104 | }, 105 | "id": "00000000-0000-0000-0000-000000000001", 106 | "inboxGroup": "00000000-0000-0000-0000-000000000044", 107 | "lastReadyForUpdateTimestamp": 1522319489138, 108 | "location": { 109 | "city": "1010 Wien, Österreich", 110 | "latitude": "48.208088", 111 | "longitude": "16.358608" 112 | }, 113 | "metaGroups": [ 114 | ], 115 | "pinAssigned": false, 116 | "powerMeterCurrency": "EUR", 117 | "powerMeterUnitPrice": 0.0, 118 | "ruleGroups": [ 119 | ], 120 | "ruleMetaDatas": { 121 | }, 122 | "timeZoneId": "Europe/Vienna", 123 | "updateState": "UP_TO_DATE", 124 | "voiceControlSettings": { 125 | "allowedActiveSecurityZoneIds": [] 126 | }, 127 | "weather": { 128 | "humidity": 54, 129 | "maxTemperature": 16.6, 130 | "minTemperature": 16.6, 131 | "temperature": 16.6, 132 | "vaporAmount": 5.465858858389302, 133 | "weatherCondition": "LIGHT_CLOUDY", 134 | "weatherDayTime": "NIGHT", 135 | "windDirection": 294, 136 | "windSpeed": 8.568 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /homematicip_demo/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIK5/HnR9nKOUN6khHKmBi+cFBAuyNvXxKdNRbB5+HaDOoAoGCCqGSM49 3 | AwEHoUQDQgAEJRnuDisT95DvNlxQ51HowkWJRuDholQOG+axoZAmxmndcadKbyff 4 | ToQ2CUFJXIlRVXjIh9PMNdE1YrnEFMahFw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /homematicip_demo/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICRjCCAeugAwIBAgIUfbtenV+j6flJfAJo5rSbvTbxPHMwCgYIKoZIzj0EAwIw 3 | QDEXMBUGA1UECgwOdHJ1c3RtZSB2MS4yLjAxJTAjBgNVBAsMHFRlc3RpbmcgQ0Eg 4 | I1EtenFyeGh3Vl8zV01NWEYwIBcNMDAwMTAxMDAwMDAwWhgPMzAwMDAxMDEwMDAw 5 | MDBaMEIxFzAVBgNVBAoMDnRydXN0bWUgdjEuMi4wMScwJQYDVQQLDB5UZXN0aW5n 6 | IGNlcnQgI3Vwd1RCS2xXS1ZndkctbFUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC 7 | AAQlGe4OKxP3kO82XFDnUejCRYlG4OGiVA4b5rGhkCbGad1xp0pvJ99OhDYJQUlc 8 | iVFVeMiH08w10TViucQUxqEXo4G+MIG7MB0GA1UdDgQWBBR1tKNvUFqIzl/NYJiA 9 | RrF4gswtPzAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFPRnOfQB4OXdgHNiBfje 10 | ZhEdwHT2MC8GA1UdEQEB/wQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAA 11 | AAAAAAAAATAOBgNVHQ8BAf8EBAMCBaAwKgYDVR0lAQH/BCAwHgYIKwYBBQUHAwIG 12 | CCsGAQUFBwMBBggrBgEFBQcDAzAKBggqhkjOPQQDAgNJADBGAiEAyqU1lqcvBeHO 13 | xnASqRqpPNjarM9BitPvUfJwVhrDvLwCIQDy6mzQrRdsw79fuO4lzeEgpZqWW4aP 14 | OM+hFmEYXiBmIg== 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61", 4 | "setuptools-scm>=8.0"] 5 | 6 | [project] 7 | name = "homematicip" 8 | description = "An API for the homematicip cloud" 9 | readme = "README.md" 10 | dependencies = [ 11 | "requests>=2.32.0", 12 | "websockets>=13.1", 13 | "aiohttp>=3.10.11", 14 | "httpx>=0.27.2" 15 | ] 16 | requires-python = ">=3.12" 17 | authors = [ 18 | {name = "Thomas Hahn", email = "homematicip-rest-api@outlook.com"}, 19 | ] 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.12" 23 | ] 24 | keywords = ["homematicip cloud","homematicip"] 25 | dynamic = [ 26 | "version" 27 | ] 28 | license = {text = "GPL-3.0-or-later"} 29 | 30 | [project.urls] 31 | Homepage = "https://github.com/hahn-th/homematicip-rest-api" 32 | Repository = "https://github.com/hahn-th/homematicip-rest-api.git" 33 | Issues = "https://github.com/hahn-th/homematicip-rest-api/issues" 34 | Changelog = "https://github.com/hahn-th/homematicip-rest-api/blob/master/CHANGELOG.md" 35 | 36 | [tool.setuptools_scm] 37 | version_file = "src/homematicip/_version.py" 38 | version_scheme = "no-guess-dev" 39 | local_scheme = "no-local-version" 40 | 41 | [tool.pytest.ini_options] 42 | asyncio_mode = "auto" 43 | asyncio_default_fixture_loop_scope = "session" 44 | 45 | [project.scripts] 46 | hmip_cli = "homematicip.cli.hmip_cli:main" 47 | hmip_generate_auth_token = "homematicip.cli.hmip_generate_auth_token:main" 48 | -------------------------------------------------------------------------------- /readthedocs.yaml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.6 6 | setup_py_install: true -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.32.0 2 | aiohttp>=3.10.2 3 | httpx>=0.28.1 -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | requests>=2.32.0 2 | aiohttp>=3.10.11 3 | httpx>=0.28.1 4 | pytest>=7.1.2 5 | pytest-asyncio==0.24.0 6 | pytest-cov==3.0.0 7 | pytest-aiohttp 8 | pytest-rerunfailures==10.2 9 | pytest-mock==3.14.0 -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | myst-parser 3 | sphinx_rtd_theme -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import sys 4 | import time 5 | 6 | import homematicip 7 | from homematicip.async_home import AsyncHome 8 | 9 | def setup_config() -> homematicip.HmipConfig: 10 | """Initialize configuration.""" 11 | _config = homematicip.find_and_load_config_file() 12 | 13 | return _config 14 | 15 | 16 | async def get_home(config: homematicip.HmipConfig) -> AsyncHome: 17 | """Initialize home instance.""" 18 | home = AsyncHome() 19 | await home.init_async(config.access_point, config.auth_token) 20 | return home 21 | 22 | async def run_forever_task(home: AsyncHome): 23 | """Task to run forever.""" 24 | await home.enable_events(print_output) 25 | 26 | async def print_output(message): 27 | print(message) 28 | 29 | async def close_after_15_seconds(home: AsyncHome): 30 | for i in range(15): 31 | print(f"WebSocket is connected: {home.websocket_is_connected()}") 32 | print(f"Closing in {15-i} seconds") 33 | await asyncio.sleep(1) 34 | 35 | await home.disable_events_async() 36 | 37 | 38 | async def main(): 39 | config = setup_config() 40 | 41 | logging.basicConfig( 42 | level=logging.DEBUG, # Set the logging level 43 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 44 | handlers=[ 45 | logging.StreamHandler(sys.stdout) # Output to console 46 | ] 47 | ) 48 | 49 | if config is None: 50 | print("Could not find configuration file. Script will exit") 51 | sys.exit(-1) 52 | 53 | home = await get_home(config) 54 | 55 | try: 56 | # task_events = asyncio.create_task(home.enable_events(print_output)) 57 | # task = asyncio.create_task(close_after_15_seconds(home)) 58 | # 59 | # await asyncio.gather(task_events, task) 60 | await home.get_current_state_async() 61 | asyncio.create_task(home.enable_events()) 62 | 63 | for i in range(10): 64 | print(f"WebSocket is connected: {home.websocket_is_connected()}") 65 | print(f"Closing in {10-i} seconds") 66 | await asyncio.sleep(1) 67 | await home.disable_events_async() 68 | print(home.websocket_is_connected()) 69 | except KeyboardInterrupt: 70 | print("Client wird durch Benutzer beendet.") 71 | finally: 72 | await home.disable_events_async() 73 | print("WebSocket-Client beendet.") 74 | 75 | 76 | if __name__ == "__main__": 77 | asyncio.run(main()) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # [options] 2 | # packages = find: 3 | # package_dir = 4 | # =homematicip 5 | 6 | # [options.packages.find] 7 | # where = homematicip 8 | # include = * -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | if __name__ == "__main__": 4 | setup() 5 | # setup( 6 | # scripts=["bin/hmip_cli.py", "bin/hmip_generate_auth_token.py"], 7 | # ) 8 | -------------------------------------------------------------------------------- /src/homematicip/EventHook.py: -------------------------------------------------------------------------------- 1 | # by Michael Foord http://www.voidspace.org.uk/python/weblog/arch_d7_2007_02_03.shtml#e616 2 | 3 | 4 | class EventHook: 5 | def __init__(self): 6 | self.__handlers = [] 7 | 8 | def __iadd__(self, handler): 9 | self.__handlers.append(handler) 10 | return self 11 | 12 | def __isub__(self, handler): 13 | self.__handlers.remove(handler) 14 | return self 15 | 16 | def fire(self, *args, **keywargs): 17 | for handler in self.__handlers: 18 | handler(*args, **keywargs) 19 | -------------------------------------------------------------------------------- /src/homematicip/HomeMaticIPObject.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOGGER = logging.getLogger(__name__) 4 | 5 | LOGGER.warning( 6 | DeprecationWarning( 7 | "homematicip.HomeMaticIPObject is deprecated in favor of homematicip.base.HomeMaticIPObject" 8 | ) 9 | ) 10 | -------------------------------------------------------------------------------- /src/homematicip/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import configparser 3 | import os 4 | import platform 5 | from collections import namedtuple 6 | 7 | HmipConfig = namedtuple( 8 | "HmipConfig", ["auth_token", "access_point", "log_level", "log_file", "raw_config"] 9 | ) 10 | 11 | 12 | def find_and_load_config_file() -> HmipConfig: 13 | for f in get_config_file_locations(): 14 | try: 15 | return load_config_file(f) 16 | except FileNotFoundError: 17 | pass 18 | return None 19 | 20 | 21 | def load_config_file(config_file: str) -> HmipConfig: 22 | """Loads the config ini file. 23 | :raises a FileNotFoundError when the config file does not exist.""" 24 | expanded_config_file = os.path.expanduser(config_file) 25 | config = configparser.ConfigParser() 26 | with open(expanded_config_file, "r") as fl: 27 | config.read_file(fl) 28 | logging_filename = config.get("LOGGING", "FileName", fallback="hmip.log") 29 | if logging_filename == "None": 30 | logging_filename = None 31 | 32 | _hmip_config = HmipConfig( 33 | config["AUTH"]["AuthToken"], 34 | config["AUTH"]["AccessPoint"], 35 | int(config.get("LOGGING", "Level", fallback=30)), 36 | logging_filename, 37 | config._sections, 38 | ) 39 | return _hmip_config 40 | 41 | 42 | def get_config_file_locations() -> []: 43 | search_locations = ["./config.ini"] 44 | 45 | os_name = platform.system() 46 | 47 | if os_name == "Windows": 48 | appdata = os.getenv("appdata") 49 | programdata = os.getenv("programdata") 50 | search_locations.append( 51 | os.path.join(appdata, "homematicip-rest-api\\config.ini") 52 | ) 53 | search_locations.append( 54 | os.path.join(programdata, "homematicip-rest-api\\config.ini") 55 | ) 56 | elif os_name == "Linux": 57 | search_locations.append("~/.homematicip-rest-api/config.ini") 58 | search_locations.append("/etc/homematicip-rest-api/config.ini") 59 | elif os_name == "Darwin": # MAC 60 | # are these folders right? 61 | search_locations.append("~/Library/Preferences/homematicip-rest-api/config.ini") 62 | search_locations.append( 63 | "/Library/Application Support/homematicip-rest-api/config.ini" 64 | ) 65 | return search_locations 66 | -------------------------------------------------------------------------------- /src/homematicip/__main__.py: -------------------------------------------------------------------------------- 1 | """Default execution entry point if running the package via python -m.""" 2 | import asyncio 3 | import sys 4 | 5 | import homematicip.cli.hmip_cli 6 | 7 | 8 | def main(): 9 | """Run pypyr from script entry point.""" 10 | return asyncio.run(homematicip.cli.hmip_cli.main_async()) 11 | 12 | 13 | if __name__ == '__main__': 14 | sys.exit(main()) 15 | -------------------------------------------------------------------------------- /src/homematicip/access_point_update_state.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.enums import DeviceUpdateState 2 | from homematicip.base.homematicip_object import HomeMaticIPObject 3 | 4 | 5 | class AccessPointUpdateState(HomeMaticIPObject): 6 | def __init__(self, connection): 7 | super().__init__(connection) 8 | self.accessPointUpdateState = DeviceUpdateState.UP_TO_DATE 9 | self.successfulUpdateTimestamp = None 10 | self.updateStateChangedTimestamp = None 11 | 12 | def from_json(self, js): 13 | self.accessPointUpdateState = js["accessPointUpdateState"] 14 | self.successfulUpdateTimestamp = self.fromtimestamp( 15 | js["successfulUpdateTimestamp"] 16 | ) 17 | self.updateStateChangedTimestamp = self.fromtimestamp( 18 | js["updateStateChangedTimestamp"] 19 | ) 20 | -------------------------------------------------------------------------------- /src/homematicip/auth.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import logging 4 | import uuid 5 | from dataclasses import dataclass 6 | 7 | from homematicip.connection.rest_connection import RestResult, RestConnection 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | @dataclass 13 | class Auth: 14 | """This class generates the auth token for the homematic ip access point.""" 15 | 16 | client_id: str = str(uuid.uuid4()) 17 | header: dict = None 18 | accesspoint_id: str = None 19 | pin: str = None 20 | connection: RestConnection = None 21 | 22 | def __init__(self, connection: RestConnection, client_auth_token: str, accesspoint_id: str): 23 | """Initialize the auth object. 24 | @param connection: The connection object 25 | @param client_auth_token: The client auth token 26 | @param accesspoint_id: The access point id 27 | """ 28 | LOGGER.debug("Initialize new Auth") 29 | self.accesspoint_id = accesspoint_id 30 | self.connection = connection 31 | self.headers = { 32 | "content-type": "application/json", 33 | "accept": "application/json", 34 | "VERSION": "12", 35 | "CLIENTAUTH": client_auth_token, 36 | "ACCESSPOINT-ID": self.accesspoint_id, 37 | } 38 | 39 | def set_pin(self, pin: str): 40 | """Set the pin for the auth object. 41 | @param pin: The pin""" 42 | self.pin = pin 43 | 44 | async def connection_request(self, access_point: str, device_name="homematicip-python") -> RestResult: 45 | LOGGER.debug(f"Requesting connection for access point {access_point}") 46 | headers = self.headers.copy() 47 | if self.pin is not None: 48 | headers["PIN"] = self.pin 49 | 50 | data = { 51 | "deviceId": self.client_id, 52 | "deviceName": device_name, 53 | "sgtin": access_point 54 | } 55 | 56 | return await self.connection.async_post("auth/connectionRequest", data, headers) 57 | 58 | async def is_request_acknowledged(self) -> bool: 59 | LOGGER.debug("Checking if request is acknowledged") 60 | data = { 61 | "deviceId": self.client_id, 62 | "accessPointId": self.accesspoint_id 63 | } 64 | 65 | result = await self.connection.async_post("auth/isRequestAcknowledged", data, self.headers) 66 | 67 | LOGGER.debug(f"Request acknowledged result: {result}") 68 | return result.status == 200 69 | 70 | async def request_auth_token(self) -> str: 71 | """Request an auth token from the access point. 72 | @return: The auth token""" 73 | LOGGER.debug("Requesting auth token") 74 | data = {"deviceId": self.client_id} 75 | result = await self.connection.async_post("auth/requestAuthToken", data, self.headers) 76 | LOGGER.debug(f"Request auth token result: {result}") 77 | 78 | return result.json["authToken"] 79 | 80 | async def confirm_auth_token(self, auth_token: str) -> str: 81 | """Confirm the auth token and get the client id. 82 | @param auth_token: The auth token 83 | @return: The client id""" 84 | 85 | LOGGER.debug("Confirming auth token") 86 | data = {"deviceId": self.client_id, "authToken": auth_token} 87 | result = await self.connection.async_post("auth/confirmAuthToken", data, self.headers) 88 | LOGGER.debug(f"Confirm auth token result: {result}") 89 | 90 | return result.json["clientId"] 91 | 92 | # 93 | # class Auth(object): 94 | # def __init__(self, home: Home): 95 | # self.uuid = str(uuid.uuid4()) 96 | # self.headers = { 97 | # "content-type": "application/json", 98 | # "accept": "application/json", 99 | # "VERSION": "12", 100 | # "CLIENTAUTH": home._connection.clientauth_token, 101 | # } 102 | # self.url_rest = home._connection.urlREST 103 | # self.pin = None 104 | # 105 | # def connectionRequest( 106 | # self, access_point, devicename="homematicip-python" 107 | # ) -> requests.Response: 108 | # data = {"deviceId": self.uuid, "deviceName": devicename, "sgtin": access_point} 109 | # headers = self.headers 110 | # if self.pin != None: 111 | # headers["PIN"] = self.pin 112 | # response = requests.post( 113 | # "{}/hmip/auth/connectionRequest".format(self.url_rest), 114 | # json=data, 115 | # headers=headers, 116 | # ) 117 | # return response 118 | # 119 | # def isRequestAcknowledged(self): 120 | # data = {"deviceId": self.uuid} 121 | # response = requests.post( 122 | # "{}/hmip/auth/isRequestAcknowledged".format(self.url_rest), 123 | # json=data, 124 | # headers=self.headers, 125 | # ) 126 | # return response.status_code == 200 127 | # 128 | # def requestAuthToken(self): 129 | # data = {"deviceId": self.uuid} 130 | # response = requests.post( 131 | # "{}/hmip/auth/requestAuthToken".format(self.url_rest), 132 | # json=data, 133 | # headers=self.headers, 134 | # ) 135 | # return json.loads(response.text)["authToken"] 136 | # 137 | # def confirmAuthToken(self, authToken): 138 | # data = {"deviceId": self.uuid, "authToken": authToken} 139 | # response = requests.post( 140 | # "{}/hmip/auth/confirmAuthToken".format(self.url_rest), 141 | # json=data, 142 | # headers=self.headers, 143 | # ) 144 | # return json.loads(response.text)["clientId"] 145 | -------------------------------------------------------------------------------- /src/homematicip/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahn-th/homematicip-rest-api/c745e0293bbf34ef757058a5f48cb88b8172a5ca/src/homematicip/base/__init__.py -------------------------------------------------------------------------------- /src/homematicip/base/channel_event.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass() 5 | class ChannelEvent: 6 | """Class to hold a channel event.""" 7 | 8 | pushEventType: str | None = None 9 | deviceId: str | None = None 10 | channelIndex: int | None = None 11 | channelEventType: str | None = None 12 | functionalChannelIndex: int | None = None 13 | 14 | def from_json(self, data: dict) -> None: 15 | """Create a ChannelEvent from a JSON dictionary.""" 16 | self.pushEventType = data.get("pushEventType") 17 | self.deviceId = data.get("deviceId") 18 | self.channelIndex = data.get("channelIndex") 19 | self.channelEventType = data.get("channelEventType") 20 | self.functionalChannelIndex = data.get("functionalChannelIndex") 21 | 22 | if self.channelIndex is None: 23 | self.channelIndex = self.functionalChannelIndex 24 | 25 | # { 26 | # "pushEventType": "DEVICE_CHANNEL_EVENT", 27 | # "deviceId": "xxx", 28 | # "channelIndex": 1, 29 | # "channelEventType": "DOOR_BELL_SENSOR_EVENT", 30 | # } 31 | -------------------------------------------------------------------------------- /src/homematicip/base/constants.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | LOGGER = logging.getLogger(__name__) 4 | 5 | LOGGER.warning( 6 | "constants.py is deprecated. Please use the corresponding enums in enums.py" 7 | ) 8 | 9 | # DEVICES 10 | DEVICE = "DEVICE" 11 | FULL_FLUSH_SHUTTER = "FULL_FLUSH_SHUTTER" 12 | PLUGABLE_SWITCH = "PLUGABLE_SWITCH" 13 | KEY_REMOTE_CONTROL_ALARM = "KEY_REMOTE_CONTROL_ALARM" 14 | MOTION_DETECTOR_INDOOR = "MOTION_DETECTOR_INDOOR" 15 | ALARM_SIREN_INDOOR = "ALARM_SIREN_INDOOR" 16 | PUSH_BUTTON = "PUSH_BUTTON" 17 | TEMPERATURE_HUMIDITY_SENSOR_DISPLAY = "TEMPERATURE_HUMIDITY_SENSOR_DISPLAY" 18 | PLUGABLE_SWITCH_MEASURING = "PLUGABLE_SWITCH_MEASURING" 19 | FLOOR_TERMINAL_BLOCK_6 = "FLOOR_TERMINAL_BLOCK_6" 20 | SMOKE_DETECTOR = "SMOKE_DETECTOR" 21 | WALL_MOUNTED_THERMOSTAT_PRO = "WALL_MOUNTED_THERMOSTAT_PRO" 22 | SHUTTER_CONTACT = "SHUTTER_CONTACT" 23 | SHUTTER_CONTACT_INTERFACE = "SHUTTER_CONTACT_INTERFACE" 24 | HEATING_THERMOSTAT = "HEATING_THERMOSTAT" 25 | SHUTTER_CONTACT_INVISIBLE = "SHUTTER_CONTACT_INVISIBLE" 26 | BRAND_WALL_MOUNTED_THERMOSTAT = "BRAND_WALL_MOUNTED_THERMOSTAT" 27 | TEMPERATURE_HUMIDITY_SENSOR = "TEMPERATURE_HUMIDITY_SENSOR" 28 | BRAND_SHUTTER = "BRAND_SHUTTER" 29 | PRECENCE_DETECTOR_INDOOR = "PRECENCE_DETECTOR_INDOOR" 30 | PLUGGABLE_DIMMER = "PLUGGABLE_DIMMER" 31 | BRAND_DIMMER = "BRAND_DIMMER" 32 | BRAND_SWITCH_MEASURING = "BRAND_SWITCH_MEASURING" 33 | PRINTED_CIRCUIT_BOARD_SWITCH_BATTERY = "PRINTED_CIRCUIT_BOARD_SWITCH_BATTERY" 34 | ROOM_CONTROL_DEVICE = "ROOM_CONTROL_DEVICE" 35 | TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR = "TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR" 36 | WEATHER_SENSOR = "WEATHER_SENSOR" 37 | WEATHER_SENSOR_PRO = "WEATHER_SENSOR_PRO" 38 | ROTARY_HANDLE_SENSOR = "ROTARY_HANDLE_SENSOR" 39 | FULL_FLUSH_SWITCH_MEASURING = "FULL_FLUSH_SWITCH_MEASURING" 40 | MOTION_DETECTOR_PUSH_BUTTON = "MOTION_DETECTOR_PUSH_BUTTON" 41 | WATER_SENSOR = "WATER_SENSOR" 42 | SHUTTER_CONTACT_MAGNETIC = "SHUTTER_CONTACT_MAGNETIC" 43 | TORMATIC_MODULE = "TORMATIC_MODULE" 44 | 45 | # GROUPS 46 | EXTENDED_LINKED_SHUTTER = "EXTENDED_LINKED_SHUTTER" 47 | SHUTTER_WIND_PROTECTION_RULE = "SHUTTER_WIND_PROTECTION_RULE" 48 | LOCK_OUT_PROTECTION_RULE = "LOCK_OUT_PROTECTION_RULE" 49 | SMOKE_ALARM_DETECTION_RULE = "SMOKE_ALARM_DETECTION_RULE" 50 | OVER_HEAT_PROTECTION_RULE = "OVER_HEAT_PROTECTION_RULE" 51 | SWITCHING_PROFILE = "SWITCHING_PROFILE" 52 | HEATING_COOLING_DEMAND_PUMP = "HEATING_COOLING_DEMAND_PUMP" 53 | HEATING_COOLING_DEMAND_BOILER = "HEATING_COOLING_DEMAND_BOILER" 54 | HEATING_DEHUMIDIFIER = "HEATING_DEHUMIDIFIER" 55 | HEATING_EXTERNAL_CLOCK = "HEATING_EXTERNAL_CLOCK" 56 | HEATING_COOLING_DEMAND = "HEATING_COOLING_DEMAND" 57 | HEATING = "HEATING" 58 | SECURITY_ZONE = "SECURITY_ZONE" 59 | INBOX = "INBOX" 60 | HEATING_CHANGEOVER = "HEATING_CHANGEOVER" 61 | HEATING_TEMPERATURE_LIMITER = "HEATING_TEMPERATURE_LIMITER" 62 | HEATING_HUMIDITY_LIMITER = "HEATING_HUMIDITY_LIMITER" 63 | ALARM_SWITCHING = "ALARM_SWITCHING" 64 | LINKED_SWITCHING = "LINKED_SWITCHING" 65 | EXTENDED_LINKED_SWITCHING = "EXTENDED_LINKED_SWITCHING" 66 | SWITCHING = "SWITCHING" 67 | SECURITY = "SECURITY" 68 | ENVIRONMENT = "ENVIRONMENT" 69 | 70 | # Security Events 71 | SENSOR_EVENT = "SENSOR_EVENT" 72 | ACCESS_POINT_DISCONNECTED = "ACCESS_POINT_DISCONNECTED" 73 | ACCESS_POINT_CONNECTED = "ACCESS_POINT_CONNECTED" 74 | ACTIVATION_CHANGED = "ACTIVATION_CHANGED" 75 | SILENCE_CHANGED = "SILENCE_CHANGED" 76 | 77 | # Automation Rules 78 | SIMPLE_RULE = "SIMPLE" 79 | 80 | # Functional Homes 81 | INDOOR_CLIMATE = "INDOOR_CLIMATE" 82 | LIGHT_AND_SHADOW = "LIGHT_AND_SHADOW" 83 | SECURITY_AND_ALARM = "SECURITY_AND_ALARM" 84 | WEATHER_AND_ENVIRONMENT = "WEATHER_AND_ENVIRONMENT" 85 | -------------------------------------------------------------------------------- /src/homematicip/base/enums.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import logging 3 | 4 | from enum import Enum, auto 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class AutoNameEnum(str, Enum): 10 | """auto() will generate the name of the attribute as value""" 11 | 12 | def _generate_next_value_(name, start, count, last_values): 13 | return name 14 | 15 | def __str__(self): 16 | return self.value 17 | 18 | @classmethod 19 | def from_str(cls, text: str, default=None): 20 | """this function will create the enum object based on its string value 21 | 22 | Args: 23 | text(str): the string value of the enum 24 | default(AutoNameEnum): a default value if text could not be used 25 | Returns: 26 | the enum object or None if the text is None or the default value 27 | """ 28 | if text is None: 29 | return None 30 | try: 31 | return cls(text) 32 | except: 33 | logger.warning( 34 | "'%s' isn't a valid option for class '%s'", text, cls.__name__ 35 | ) 36 | return default 37 | 38 | 39 | class AcousticAlarmTiming(AutoNameEnum): 40 | PERMANENT = auto() 41 | THREE_MINUTES = auto() 42 | SIX_MINUTES = auto() 43 | ONCE_PER_MINUTE = auto() 44 | 45 | 46 | class WaterAlarmTrigger(AutoNameEnum): 47 | NO_ALARM = auto() 48 | MOISTURE_DETECTION = auto() 49 | WATER_DETECTION = auto() 50 | WATER_MOISTURE_DETECTION = auto() 51 | 52 | 53 | class AcousticAlarmSignal(AutoNameEnum): 54 | DISABLE_ACOUSTIC_SIGNAL = auto() 55 | FREQUENCY_RISING = auto() 56 | FREQUENCY_FALLING = auto() 57 | FREQUENCY_RISING_AND_FALLING = auto() 58 | FREQUENCY_ALTERNATING_LOW_HIGH = auto() 59 | FREQUENCY_ALTERNATING_LOW_MID_HIGH = auto() 60 | FREQUENCY_HIGHON_OFF = auto() 61 | FREQUENCY_HIGHON_LONGOFF = auto() 62 | FREQUENCY_LOWON_OFF_HIGHON_OFF = auto() 63 | FREQUENCY_LOWON_LONGOFF_HIGHON_LONGOFF = auto() 64 | LOW_BATTERY = auto() 65 | DISARMED = auto() 66 | INTERNALLY_ARMED = auto() 67 | EXTERNALLY_ARMED = auto() 68 | DELAYED_INTERNALLY_ARMED = auto() 69 | DELAYED_EXTERNALLY_ARMED = auto() 70 | EVENT = auto() 71 | ERROR = auto() 72 | 73 | 74 | class AlarmContactType(AutoNameEnum): 75 | PASSIVE_GLASS_BREAKAGE_DETECTOR = auto() 76 | WINDOW_DOOR_CONTACT = auto() 77 | 78 | 79 | class ClimateControlDisplay(AutoNameEnum): 80 | ACTUAL = auto() 81 | SETPOINT = auto() 82 | ACTUAL_HUMIDITY = auto() 83 | 84 | 85 | class WindowState(AutoNameEnum): 86 | OPEN = auto() 87 | CLOSED = auto() 88 | TILTED = auto() 89 | 90 | 91 | class ValveState(AutoNameEnum): 92 | STATE_NOT_AVAILABLE = auto() 93 | RUN_TO_START = auto() 94 | WAIT_FOR_ADAPTION = auto() 95 | ADAPTION_IN_PROGRESS = auto() 96 | ADAPTION_DONE = auto() 97 | TOO_TIGHT = auto() 98 | ADJUSTMENT_TOO_BIG = auto() 99 | ADJUSTMENT_TOO_SMALL = auto() 100 | ERROR_POSITION = auto() 101 | 102 | 103 | class HeatingValveType(AutoNameEnum): 104 | NORMALLY_CLOSE = auto() 105 | NORMALLY_OPEN = auto() 106 | 107 | 108 | class ContactType(AutoNameEnum): 109 | NORMALLY_CLOSE = auto() 110 | NORMALLY_OPEN = auto() 111 | 112 | 113 | class RGBColorState(AutoNameEnum): 114 | BLACK = auto() 115 | BLUE = auto() 116 | GREEN = auto() 117 | TURQUOISE = auto() 118 | RED = auto() 119 | PURPLE = auto() 120 | YELLOW = auto() 121 | WHITE = auto() 122 | 123 | 124 | class DeviceUpdateStrategy(AutoNameEnum): 125 | MANUALLY = auto() 126 | AUTOMATICALLY_IF_POSSIBLE = auto() 127 | 128 | 129 | class ApExchangeState(AutoNameEnum): 130 | NONE = auto() 131 | REQUESTED = auto() 132 | IN_PROGRESS = auto() 133 | DONE = auto() 134 | REJECTED = auto() 135 | 136 | 137 | class HomeUpdateState(AutoNameEnum): 138 | UP_TO_DATE = auto() 139 | UPDATE_AVAILABLE = auto() 140 | PERFORM_UPDATE_SENT = auto() 141 | PERFORMING_UPDATE = auto() 142 | 143 | 144 | class WeatherCondition(AutoNameEnum): 145 | CLEAR = auto() 146 | LIGHT_CLOUDY = auto() 147 | CLOUDY = auto() 148 | CLOUDY_WITH_RAIN = auto() 149 | CLOUDY_WITH_SNOW_RAIN = auto() 150 | HEAVILY_CLOUDY = auto() 151 | HEAVILY_CLOUDY_WITH_RAIN = auto() 152 | HEAVILY_CLOUDY_WITH_STRONG_RAIN = auto() 153 | HEAVILY_CLOUDY_WITH_SNOW = auto() 154 | HEAVILY_CLOUDY_WITH_SNOW_RAIN = auto() 155 | HEAVILY_CLOUDY_WITH_THUNDER = auto() 156 | HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER = auto() 157 | FOGGY = auto() 158 | STRONG_WIND = auto() 159 | UNKNOWN = auto() 160 | 161 | 162 | class WeatherDayTime(AutoNameEnum): 163 | DAY = auto() 164 | TWILIGHT = auto() 165 | NIGHT = auto() 166 | 167 | 168 | class ClimateControlMode(AutoNameEnum): 169 | AUTOMATIC = auto() 170 | MANUAL = auto() 171 | ECO = auto() 172 | 173 | 174 | class AbsenceType(AutoNameEnum): 175 | NOT_ABSENT = auto() 176 | PERIOD = auto() 177 | PERMANENT = auto() 178 | VACATION = auto() 179 | PARTY = auto() 180 | 181 | 182 | class EcoDuration(AutoNameEnum): 183 | ONE = auto() 184 | TWO = auto() 185 | FOUR = auto() 186 | SIX = auto() 187 | PERMANENT = auto() 188 | 189 | 190 | class SecurityZoneActivationMode(AutoNameEnum): 191 | ACTIVATION_WITH_DEVICE_IGNORELIST = auto() 192 | ACTIVATION_IF_ALL_IN_VALID_STATE = auto() 193 | 194 | 195 | class ClientType(AutoNameEnum): 196 | APP = auto() 197 | C2C = auto() 198 | SMART_WATCH = auto() 199 | 200 | 201 | class DeviceType(AutoNameEnum): 202 | DEVICE = auto() 203 | BASE_DEVICE = auto() 204 | EXTERNAL = auto() 205 | ACCELERATION_SENSOR = auto() 206 | ACCESS_POINT = auto() 207 | ALARM_SIREN_INDOOR = auto() 208 | ALARM_SIREN_OUTDOOR = auto() 209 | BLIND_MODULE = auto() 210 | BRAND_BLIND = auto() 211 | BRAND_DIMMER = auto() 212 | BRAND_PUSH_BUTTON = auto() 213 | BRAND_SHUTTER = auto() 214 | BRAND_SWITCH_2 = auto() 215 | BRAND_SWITCH_MEASURING = auto() 216 | BRAND_SWITCH_NOTIFICATION_LIGHT = auto() 217 | BRAND_WALL_MOUNTED_THERMOSTAT = auto() 218 | CARBON_DIOXIDE_SENSOR = auto() 219 | DALI_GATEWAY = auto() 220 | DIN_RAIL_BLIND_4 = auto() 221 | DIN_RAIL_SWITCH = auto() 222 | DIN_RAIL_SWITCH_4 = auto() 223 | DIN_RAIL_DIMMER_3 = auto() 224 | DOOR_BELL_BUTTON = auto() 225 | DOOR_BELL_CONTACT_INTERFACE = auto() 226 | DOOR_LOCK_DRIVE = auto() 227 | DOOR_LOCK_SENSOR = auto() 228 | ENERGY_SENSORS_INTERFACE = auto() 229 | FLOOR_TERMINAL_BLOCK_6 = auto() 230 | FLOOR_TERMINAL_BLOCK_10 = auto() 231 | FLOOR_TERMINAL_BLOCK_12 = auto() 232 | FULL_FLUSH_BLIND = auto() 233 | FULL_FLUSH_CONTACT_INTERFACE = auto() 234 | FULL_FLUSH_CONTACT_INTERFACE_6 = auto() 235 | FULL_FLUSH_DIMMER = auto() 236 | FULL_FLUSH_INPUT_SWITCH = auto() 237 | FULL_FLUSH_SHUTTER = auto() 238 | FULL_FLUSH_SWITCH_MEASURING = auto() 239 | HEATING_SWITCH_2 = auto() 240 | HEATING_THERMOSTAT = auto() 241 | HEATING_THERMOSTAT_COMPACT = auto() 242 | HEATING_THERMOSTAT_COMPACT_PLUS = auto() 243 | HEATING_THERMOSTAT_EVO = auto() 244 | HEATING_THERMOSTAT_THREE = auto() 245 | HEATING_THERMOSTAT_FLEX = auto() 246 | HOME_CONTROL_ACCESS_POINT = auto() 247 | HOERMANN_DRIVES_MODULE = auto() 248 | KEY_REMOTE_CONTROL_4 = auto() 249 | KEY_REMOTE_CONTROL_ALARM = auto() 250 | LIGHT_SENSOR = auto() 251 | MOTION_DETECTOR_INDOOR = auto() 252 | MOTION_DETECTOR_OUTDOOR = auto() 253 | MOTION_DETECTOR_PUSH_BUTTON = auto() 254 | MOTION_DETECTOR_SWITCH_OUTDOOR = auto() 255 | MULTI_IO_BOX = auto() 256 | OPEN_COLLECTOR_8_MODULE = auto() 257 | PASSAGE_DETECTOR = auto() 258 | PLUGGABLE_MAINS_FAILURE_SURVEILLANCE = auto() 259 | PLUGABLE_SWITCH = auto() 260 | PLUGABLE_SWITCH_MEASURING = auto() 261 | PLUGGABLE_DIMMER = auto() 262 | PRESENCE_DETECTOR_INDOOR = auto() 263 | PRINTED_CIRCUIT_BOARD_SWITCH_BATTERY = auto() 264 | PRINTED_CIRCUIT_BOARD_SWITCH_2 = auto() 265 | PUSH_BUTTON = auto() 266 | PUSH_BUTTON_6 = auto() 267 | PUSH_BUTTON_FLAT = auto() 268 | RAIN_SENSOR = auto() 269 | REMOTE_CONTROL_8 = auto() 270 | REMOTE_CONTROL_8_MODULE = auto() 271 | RGBW_DIMMER = auto() 272 | ROOM_CONTROL_DEVICE = auto() 273 | ROOM_CONTROL_DEVICE_ANALOG = auto() 274 | ROTARY_HANDLE_SENSOR = auto() 275 | SHUTTER_CONTACT = auto() 276 | SHUTTER_CONTACT_INTERFACE = auto() 277 | SHUTTER_CONTACT_INVISIBLE = auto() 278 | SHUTTER_CONTACT_MAGNETIC = auto() 279 | SHUTTER_CONTACT_OPTICAL_PLUS = auto() 280 | SMOKE_DETECTOR = auto() 281 | SWITCH_MEASURING_CABLE_OUTDOOR = auto() 282 | TEMPERATURE_HUMIDITY_SENSOR = auto() 283 | TEMPERATURE_HUMIDITY_SENSOR_COMPACT = auto() 284 | TEMPERATURE_HUMIDITY_SENSOR_DISPLAY = auto() 285 | TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR = auto() 286 | TEMPERATURE_SENSOR_2_EXTERNAL_DELTA = auto() 287 | TILT_VIBRATION_SENSOR = auto() 288 | TILT_VIBRATION_SENSOR_COMPACT = auto() 289 | TORMATIC_MODULE = auto() 290 | WALL_MOUNTED_KEY_PAD = auto() 291 | WALL_MOUNTED_THERMOSTAT_BASIC_HUMIDITY = auto() 292 | WALL_MOUNTED_THERMOSTAT_PRO = auto() 293 | WALL_MOUNTED_GARAGE_DOOR_CONTROLLER = auto() 294 | WALL_MOUNTED_UNIVERSAL_ACTUATOR = auto() 295 | WATER_SENSOR = auto() 296 | WEATHER_SENSOR = auto() 297 | WEATHER_SENSOR_PLUS = auto() 298 | WEATHER_SENSOR_PRO = auto() 299 | WIRED_BLIND_4 = auto() 300 | WIRED_DIMMER_3 = auto() 301 | WIRED_DIN_RAIL_ACCESS_POINT = auto() 302 | WIRED_FLOOR_TERMINAL_BLOCK_12 = auto() 303 | WIRED_INPUT_32 = auto() 304 | WIRED_INPUT_SWITCH_6 = auto() 305 | WIRED_MOTION_DETECTOR_PUSH_BUTTON = auto() 306 | WIRED_PRESENCE_DETECTOR_INDOOR = auto() 307 | WIRED_PUSH_BUTTON_2 = auto() 308 | WIRED_PUSH_BUTTON_6 = auto() 309 | WIRED_SWITCH_8 = auto() 310 | WIRED_SWITCH_4 = auto() 311 | WIRED_WALL_MOUNTED_THERMOSTAT = auto() 312 | WIRED_CARBON_TEMPERATURE_HUMIDITY_SENSOR_DISPLAY = auto() 313 | WIRELESS_ACCESS_POINT_BASIC = auto() 314 | 315 | 316 | class GroupType(AutoNameEnum): 317 | GROUP = auto() 318 | ACCESS_AUTHORIZATION_PROFILE = auto() 319 | ACCESS_CONTROL = auto() 320 | ALARM_SWITCHING = auto() 321 | AUTO_RELOCK_PROFILE = auto() 322 | ENERGY = auto() 323 | ENVIRONMENT = auto() 324 | EXTENDED_LINKED_GARAGE_DOOR = auto() 325 | EXTENDED_LINKED_SHUTTER = auto() 326 | EXTENDED_LINKED_SWITCHING = auto() 327 | HEATING = auto() 328 | HEATING_CHANGEOVER = auto() 329 | HEATING_COOLING_DEMAND = auto() 330 | HEATING_COOLING_DEMAND_BOILER = auto() 331 | HEATING_COOLING_DEMAND_PUMP = auto() 332 | HEATING_DEHUMIDIFIER = auto() 333 | HEATING_EXTERNAL_CLOCK = auto() 334 | HEATING_FAILURE_ALERT_RULE_GROUP = auto() 335 | HEATING_HUMIDITY_LIMITER = auto() 336 | HEATING_TEMPERATURE_LIMITER = auto() 337 | HOT_WATER = auto() 338 | HUMIDITY_WARNING_RULE_GROUP = auto() 339 | INBOX = auto() 340 | INDOOR_CLIMATE = auto() 341 | LINKED_SWITCHING = auto() 342 | LOCK_OUT_PROTECTION_RULE = auto() 343 | LOCK_PROFILE = auto() 344 | OVER_HEAT_PROTECTION_RULE = auto() 345 | SECURITY = auto() 346 | SECURITY_BACKUP_ALARM_SWITCHING = auto() 347 | SECURITY_ZONE = auto() 348 | SHUTTER_PROFILE = auto() 349 | SHUTTER_WIND_PROTECTION_RULE = auto() 350 | SMOKE_ALARM_DETECTION_RULE = auto() 351 | SWITCHING = auto() 352 | SWITCHING_PROFILE = auto() 353 | 354 | 355 | class SecurityEventType(AutoNameEnum): 356 | SENSOR_EVENT = auto() 357 | ACCESS_POINT_DISCONNECTED = auto() 358 | ACCESS_POINT_CONNECTED = auto() 359 | ACTIVATION_CHANGED = auto() 360 | SILENCE_CHANGED = auto() 361 | SABOTAGE = auto() 362 | MOISTURE_DETECTION_EVENT = auto() 363 | SMOKE_ALARM = auto() 364 | EXTERNAL_TRIGGERED = auto() 365 | OFFLINE_ALARM = auto() 366 | WATER_DETECTION_EVENT = auto() 367 | MAINS_FAILURE_EVENT = auto() 368 | OFFLINE_WATER_DETECTION_EVENT = auto() 369 | 370 | 371 | class AutomationRuleType(AutoNameEnum): 372 | SIMPLE = auto() 373 | 374 | 375 | class FunctionalHomeType(AutoNameEnum): 376 | ACCESS_CONTROL = auto() 377 | INDOOR_CLIMATE = auto() 378 | ENERGY = auto() 379 | LIGHT_AND_SHADOW = auto() 380 | SECURITY_AND_ALARM = auto() 381 | WEATHER_AND_ENVIRONMENT = auto() 382 | 383 | 384 | class EventType(AutoNameEnum): 385 | SECURITY_JOURNAL_CHANGED = auto() 386 | GROUP_ADDED = auto() 387 | GROUP_REMOVED = auto() 388 | DEVICE_REMOVED = auto() 389 | DEVICE_CHANGED = auto() 390 | DEVICE_ADDED = auto() 391 | DEVICE_CHANNEL_EVENT = auto() 392 | CLIENT_REMOVED = auto() 393 | CLIENT_CHANGED = auto() 394 | CLIENT_ADDED = auto() 395 | HOME_CHANGED = auto() 396 | GROUP_CHANGED = auto() 397 | 398 | 399 | class MotionDetectionSendInterval(AutoNameEnum): 400 | SECONDS_30 = auto() 401 | SECONDS_60 = auto() 402 | SECONDS_120 = auto() 403 | SECONDS_240 = auto() 404 | SECONDS_480 = auto() 405 | 406 | 407 | class SmokeDetectorAlarmType(AutoNameEnum): 408 | IDLE_OFF = auto() 409 | PRIMARY_ALARM = auto() 410 | INTRUSION_ALARM = auto() 411 | SECONDARY_ALARM = auto() 412 | 413 | 414 | class LiveUpdateState(AutoNameEnum): 415 | UP_TO_DATE = auto() 416 | UPDATE_AVAILABLE = auto() 417 | UPDATE_INCOMPLETE = auto() 418 | LIVE_UPDATE_NOT_SUPPORTED = auto() 419 | 420 | 421 | class OpticalAlarmSignal(AutoNameEnum): 422 | DISABLE_OPTICAL_SIGNAL = auto() 423 | BLINKING_ALTERNATELY_REPEATING = auto() 424 | BLINKING_BOTH_REPEATING = auto() 425 | DOUBLE_FLASHING_REPEATING = auto() 426 | FLASHING_BOTH_REPEATING = auto() 427 | CONFIRMATION_SIGNAL_0 = auto() 428 | CONFIRMATION_SIGNAL_1 = auto() 429 | CONFIRMATION_SIGNAL_2 = auto() 430 | 431 | 432 | class WindValueType(AutoNameEnum): 433 | CURRENT_VALUE = auto() 434 | MIN_VALUE = auto() 435 | MAX_VALUE = auto() 436 | AVERAGE_VALUE = auto() 437 | 438 | 439 | class FunctionalChannelType(AutoNameEnum): 440 | FUNCTIONAL_CHANNEL = auto() 441 | ACCELERATION_SENSOR_CHANNEL = auto() 442 | ACCESS_AUTHORIZATION_CHANNEL = auto() 443 | ACCESS_CONTROLLER_CHANNEL = auto() 444 | ACCESS_CONTROLLER_WIRED_CHANNEL = auto() 445 | ALARM_SIREN_CHANNEL = auto() 446 | ANALOG_OUTPUT_CHANNEL = auto() 447 | ANALOG_ROOM_CONTROL_CHANNEL = auto() 448 | BLIND_CHANNEL = auto() 449 | CHANGE_OVER_CHANNEL = auto() 450 | CARBON_DIOXIDE_SENSOR_CHANNEL = auto() 451 | CLIMATE_SENSOR_CHANNEL = auto() 452 | CODE_PROTECTED_PRIMARY_ACTION_CHANNEL = auto() 453 | CODE_PROTECTED_SECONDARY_ACTION_CHANNEL = auto() 454 | CONTACT_INTERFACE_CHANNEL = auto() 455 | DEHUMIDIFIER_DEMAND_CHANNEL = auto() 456 | DEVICE_BASE = auto() 457 | DEVICE_BASE_FLOOR_HEATING = auto() 458 | DEVICE_BLOCKING = auto() 459 | DEVICE_GLOBAL_PUMP_CONTROL = auto() 460 | DEVICE_INCORRECT_POSITIONED = auto() 461 | DEVICE_OPERATIONLOCK = auto() 462 | DEVICE_OPERATIONLOCK_WITH_SABOTAGE = auto() 463 | DEVICE_PERMANENT_FULL_RX = auto() 464 | DEVICE_RECHARGEABLE_WITH_SABOTAGE = auto() 465 | DEVICE_SABOTAGE = auto() 466 | DIMMER_CHANNEL = auto() 467 | DOOR_CHANNEL = auto() 468 | DOOR_LOCK_CHANNEL = auto() 469 | DOOR_LOCK_SENSOR_CHANNEL = auto() 470 | EXTERNAL_BASE_CHANNEL = auto() 471 | EXTERNAL_UNIVERSAL_LIGHT_CHANNEL = auto() 472 | ENERGY_SENSORS_INTERFACE_CHANNEL = auto() 473 | FLOOR_TERMINAL_BLOCK_CHANNEL = auto() 474 | FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL = auto() 475 | FLOOR_TERMINAL_BLOCK_MECHANIC_CHANNEL = auto() 476 | GENERIC_INPUT_CHANNEL = auto() 477 | HEAT_DEMAND_CHANNEL = auto() 478 | HEATING_THERMOSTAT_CHANNEL = auto() 479 | IMPULSE_OUTPUT_CHANNEL = auto() 480 | INTERNAL_SWITCH_CHANNEL = auto() 481 | LIGHT_SENSOR_CHANNEL = auto() 482 | MAINS_FAILURE_CHANNEL = auto() 483 | MOTION_DETECTION_CHANNEL = auto() 484 | MULTI_MODE_INPUT_BLIND_CHANNEL = auto() 485 | MULTI_MODE_INPUT_CHANNEL = auto() 486 | MULTI_MODE_INPUT_DIMMER_CHANNEL = auto() 487 | MULTI_MODE_INPUT_SWITCH_CHANNEL = auto() 488 | NOTIFICATION_LIGHT_CHANNEL = auto() 489 | OPTICAL_SIGNAL_CHANNEL = auto() 490 | OPTICAL_SIGNAL_GROUP_CHANNEL = auto() 491 | PASSAGE_DETECTOR_CHANNEL = auto() 492 | PRESENCE_DETECTION_CHANNEL = auto() 493 | RAIN_DETECTION_CHANNEL = auto() 494 | ROTARY_HANDLE_CHANNEL = auto() 495 | SHADING_CHANNEL = auto() 496 | SHUTTER_CHANNEL = auto() 497 | SHUTTER_CONTACT_CHANNEL = auto() 498 | SINGLE_KEY_CHANNEL = auto() 499 | SMOKE_DETECTOR_CHANNEL = auto() 500 | SWITCH_CHANNEL = auto() 501 | SWITCH_MEASURING_CHANNEL = auto() 502 | TEMPERATURE_SENSOR_2_EXTERNAL_DELTA_CHANNEL = auto() 503 | TILT_VIBRATION_SENSOR_CHANNEL = auto() 504 | UNIVERSAL_ACTUATOR_CHANNEL = auto() 505 | UNIVERSAL_LIGHT_CHANNEL = auto() 506 | UNIVERSAL_LIGHT_GROUP_CHANNEL = auto() 507 | WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL = auto() 508 | WALL_MOUNTED_THERMOSTAT_WITHOUT_DISPLAY_CHANNEL = auto() 509 | WALL_MOUNTED_THERMOSTAT_WITH_CARBON_CHANNEL = auto() 510 | WATER_SENSOR_CHANNEL = auto() 511 | WEATHER_SENSOR_CHANNEL = auto() 512 | WEATHER_SENSOR_PLUS_CHANNEL = auto() 513 | WEATHER_SENSOR_PRO_CHANNEL = auto() 514 | 515 | 516 | class ChannelEventTypes(AutoNameEnum): 517 | DOOR_BELL_SENSOR_EVENT = auto() 518 | 519 | 520 | class HeatingLoadType(AutoNameEnum): 521 | LOAD_BALANCING = auto() 522 | LOAD_COLLECTION = auto() 523 | 524 | 525 | class DeviceUpdateState(AutoNameEnum): 526 | UP_TO_DATE = auto() 527 | TRANSFERING_UPDATE = auto() 528 | UPDATE_AVAILABLE = auto() 529 | UPDATE_AUTHORIZED = auto() 530 | BACKGROUND_UPDATE_NOT_SUPPORTED = auto() 531 | 532 | 533 | class PassageDirection(AutoNameEnum): 534 | LEFT = auto() 535 | RIGHT = auto() 536 | 537 | 538 | class MultiModeInputMode(AutoNameEnum): 539 | KEY_BEHAVIOR = auto() 540 | SWITCH_BEHAVIOR = auto() 541 | BINARY_BEHAVIOR = auto() 542 | 543 | 544 | class BinaryBehaviorType(AutoNameEnum): 545 | NORMALLY_CLOSE = auto() 546 | NORMALLY_OPEN = auto() 547 | 548 | 549 | class HeatingFailureValidationType(AutoNameEnum): 550 | NO_HEATING_FAILURE = auto() 551 | HEATING_FAILURE_WARNING = auto() 552 | HEATING_FAILURE_ALARM = auto() 553 | 554 | 555 | class HumidityValidationType(AutoNameEnum): 556 | LESSER_LOWER_THRESHOLD = auto() 557 | GREATER_UPPER_THRESHOLD = auto() 558 | GREATER_LOWER_LESSER_UPPER_THRESHOLD = auto() 559 | 560 | 561 | class AccelerationSensorMode(AutoNameEnum): 562 | ANY_MOTION = auto() 563 | FLAT_DECT = auto() 564 | TILT = auto() 565 | 566 | 567 | class AccelerationSensorNeutralPosition(AutoNameEnum): 568 | HORIZONTAL = auto() 569 | VERTICAL = auto() 570 | 571 | 572 | class AccelerationSensorSensitivity(AutoNameEnum): 573 | SENSOR_RANGE_16G = auto() 574 | SENSOR_RANGE_8G = auto() 575 | SENSOR_RANGE_4G = auto() 576 | SENSOR_RANGE_2G = auto() 577 | SENSOR_RANGE_2G_PLUS_SENS = auto() 578 | SENSOR_RANGE_2G_2PLUS_SENSE = auto() 579 | 580 | 581 | class NotificationSoundType(AutoNameEnum): 582 | SOUND_NO_SOUND = auto() 583 | SOUND_SHORT = auto() 584 | SOUND_SHORT_SHORT = auto() 585 | SOUND_LONG = auto() 586 | 587 | 588 | class DoorState(AutoNameEnum): 589 | CLOSED = auto() 590 | OPEN = auto() 591 | VENTILATION_POSITION = auto() 592 | POSITION_UNKNOWN = auto() 593 | 594 | 595 | class DoorCommand(AutoNameEnum): 596 | OPEN = auto() 597 | STOP = auto() 598 | CLOSE = auto() 599 | PARTIAL_OPEN = auto() 600 | 601 | 602 | class ShadingStateType(AutoNameEnum): 603 | NOT_POSSIBLE = auto() 604 | NOT_EXISTENT = auto() 605 | POSITION_USED = auto() 606 | TILT_USED = auto() 607 | NOT_USED = auto() 608 | MIXED = auto() 609 | 610 | 611 | class GroupVisibility(AutoNameEnum): 612 | INVISIBLE_GROUP_AND_CONTROL = auto() 613 | INVISIBLE_CONTROL = auto() 614 | VISIBLE = auto() 615 | 616 | 617 | class ProfileMode(AutoNameEnum): 618 | AUTOMATIC = auto() 619 | MANUAL = auto() 620 | 621 | 622 | class AlarmSignalType(AutoNameEnum): 623 | NO_ALARM = auto() 624 | SILENT_ALARM = auto() 625 | FULL_ALARM = auto() 626 | 627 | 628 | class ConnectionType(AutoNameEnum): 629 | EXTERNAL = auto() 630 | HMIP_RF = auto() 631 | HMIP_WIRED = auto() 632 | HMIP_LAN = auto() 633 | HMIP_WLAN = auto() 634 | 635 | 636 | class DeviceArchetype(AutoNameEnum): 637 | EXTERNAL = auto() 638 | HMIP = auto() 639 | PLUGIN = auto() 640 | 641 | 642 | class DriveSpeed(AutoNameEnum): 643 | CREEP_SPEED = auto() 644 | SLOW_SPEED = auto() 645 | NOMINAL_SPEED = auto() 646 | OPTIONAL_SPEED = auto() 647 | 648 | 649 | class ShadingPackagePosition(AutoNameEnum): 650 | LEFT = auto() 651 | RIGHT = auto() 652 | CENTER = auto() 653 | SPLIT = auto() 654 | TOP = auto() 655 | BOTTOM = auto() 656 | TDBU = auto() 657 | NOT_USED = auto() 658 | 659 | 660 | class LockState(AutoNameEnum): 661 | OPEN = auto() 662 | UNLOCKED = auto() 663 | LOCKED = auto() 664 | NONE = auto() 665 | 666 | 667 | class MotorState(AutoNameEnum): 668 | STOPPED = auto() 669 | CLOSING = auto() 670 | OPENING = auto() 671 | 672 | 673 | class OpticalSignalBehaviour(AutoNameEnum): 674 | ON = auto() 675 | BLINKING_MIDDLE = auto() 676 | FLASH_MIDDLE = auto() 677 | BILLOW_MIDDLE = auto() 678 | OFF = auto() 679 | 680 | 681 | class CliActions(AutoNameEnum): 682 | SET_DIM_LEVEL = auto() 683 | SET_LOCK_STATE = auto() 684 | SET_SHUTTER_LEVEL = auto() 685 | SET_SHUTTER_STOP = auto() 686 | SET_SLATS_LEVEL = auto() 687 | TOGGLE_GARAGE_DOOR = auto() 688 | SET_SWITCH_STATE = auto() 689 | RESET_ENERGY_COUNTER = auto() 690 | SEND_DOOR_COMMAND = auto() 691 | -------------------------------------------------------------------------------- /src/homematicip/base/helpers.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import json 3 | import logging 4 | import re 5 | 6 | LOGGER = logging.getLogger(__name__) 7 | 8 | 9 | def get_functional_channel(channel_type, js): 10 | for channel in js["functionalChannels"].values(): 11 | if channel["functionalChannelType"] == channel_type: 12 | return channel 13 | return None 14 | 15 | 16 | def get_functional_channels(channel_type, js): 17 | result = [] 18 | for channel in js["functionalChannels"].values(): 19 | if channel["functionalChannelType"] == channel_type: 20 | result.append(channel) 21 | return result 22 | 23 | 24 | # from https://bugs.python.org/file43513/json_detect_encoding_3.patch 25 | def detect_encoding(b): 26 | bstartswith = b.startswith 27 | if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)): 28 | return "utf-32" 29 | if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)): 30 | return "utf-16" 31 | if bstartswith(codecs.BOM_UTF8): 32 | return "utf-8-sig" 33 | 34 | if len(b) >= 4: 35 | if not b[0]: 36 | # 00 00 -- -- - utf-32-be 37 | # 00 XX -- -- - utf-16-be 38 | return "utf-16-be" if b[1] else "utf-32-be" 39 | if not b[1]: 40 | # XX 00 00 00 - utf-32-le 41 | # XX 00 XX XX - utf-16-le 42 | return "utf-16-le" if b[2] or b[3] else "utf-32-le" 43 | elif len(b) == 2: 44 | if not b[0]: 45 | # 00 XX - utf-16-be 46 | return "utf-16-be" 47 | if not b[1]: 48 | # XX 00 - utf-16-le 49 | return "utf-16-le" 50 | # default 51 | return "utf-8" 52 | 53 | 54 | def bytes2str(b): 55 | if isinstance(b, (bytes, bytearray)): 56 | return b.decode(detect_encoding(b), "surrogatepass") 57 | if isinstance(b, str): 58 | return b 59 | raise TypeError( 60 | "the object must be str, bytes or bytearray, not {!r}".format( 61 | b.__class__.__name__ 62 | ) 63 | ) 64 | 65 | 66 | def handle_config(json_state: str, anonymize: bool) -> str: 67 | if "errorCode" in json_state: 68 | LOGGER.error( 69 | "Could not get the current configuration. Error: %s", 70 | json_state["errorCode"], 71 | ) 72 | return None 73 | else: 74 | c = json.dumps(json_state, indent=4, sort_keys=True) 75 | if anonymize: 76 | # generate dummy guids 77 | c = anonymizeConfig( 78 | c, 79 | "[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", 80 | "00000000-0000-0000-0000-{0:0>12}", 81 | ) 82 | # generate dummy SGTIN 83 | c = anonymizeConfig(c, '"[A-Z0-9]{24}"', '"3014F711{0:0>16}"', flags=0) 84 | # remove refresh Token 85 | c = anonymizeConfig(c, '"refreshToken": ?"[^"]+"', '"refreshToken": null') 86 | # location 87 | c = anonymizeConfig( 88 | c, '"city": ?"[^"]+"', '"city": "1010, Vienna, Austria"' 89 | ) 90 | c = anonymizeConfig(c, '"latitude": ?"[^"]+"', '"latitude": "48.208088"') 91 | c = anonymizeConfig(c, '"longitude": ?"[^"]+"', '"longitude": "16.358608"') 92 | 93 | return c 94 | 95 | 96 | def anonymizeConfig(config, pattern, format, flags=re.IGNORECASE): 97 | m = re.findall(pattern, config, flags=flags) 98 | if m is None: 99 | return config 100 | map = {} 101 | i = 0 102 | for s in m: 103 | if s in map.keys(): 104 | continue 105 | map[s] = format.format(i) 106 | i = i + 1 107 | 108 | for k, v in map.items(): 109 | config = config.replace(k, v) 110 | return config 111 | -------------------------------------------------------------------------------- /src/homematicip/base/homematicip_object.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from datetime import datetime 4 | 5 | from homematicip.base.enums import AutoNameEnum 6 | from homematicip.connection.rest_connection import RestConnection, RestResult 7 | 8 | LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | class HomeMaticIPObject: 12 | """This class represents a generic homematic ip object to make 13 | basic requests to the access point""" 14 | 15 | def __init__(self, connection): 16 | self._connection: RestConnection = connection 17 | #: List with remove handlers. 18 | self._on_remove = [] 19 | #: List with update handlers. 20 | self._on_update = [] 21 | 22 | #:the raw json data of the object 23 | self._rawJSONData = {} 24 | 25 | # list[str]:List of all attributes which were added via set_attr_from_dict 26 | self._dictAttributes = [] 27 | 28 | def on_remove(self, handler): 29 | """Adds an event handler to the remove method. Fires when a device 30 | is removed.""" 31 | self._on_remove.append(handler) 32 | 33 | def fire_remove_event(self, *args, **kwargs): 34 | """Trigger the method tied to _on_remove""" 35 | for _handler in self._on_remove: 36 | _handler(*args, **kwargs) 37 | 38 | def on_update(self, handler): 39 | """Adds an event handler to the update method. Fires when a device 40 | is updated.""" 41 | self._on_update.append(handler) 42 | 43 | def fire_update_event(self, *args, **kwargs): 44 | """Trigger the method tied to _on_update""" 45 | for _handler in self._on_update: 46 | _handler(*args, **kwargs) 47 | 48 | def remove_callback(self, handler): 49 | """Remove event handler.""" 50 | if handler in self._on_remove: 51 | self._on_remove.remove(handler) 52 | if handler in self._on_update: 53 | self._on_update.remove(handler) 54 | 55 | def _rest_call(self, path, body=None, custom_header: dict = None) -> RestResult: 56 | """Run a rest call non async. 57 | 58 | Args: 59 | path (str): the path to call without base url 60 | body (dict): the body to send 61 | custom_header (dict): the custom header to send. This will be merged with the default header 62 | """ 63 | loop = asyncio.get_event_loop() 64 | return loop.run_until_complete(self._connection.async_post(path, body, custom_header)) 65 | 66 | async def _rest_call_async(self, path, body=None, custom_header: dict = None): 67 | """Run a rest call async 68 | 69 | Args: 70 | path (str): the path to call without base url 71 | body (dict): the body to send 72 | custom_header (dict): the custom header to send. This will be merged with the default header 73 | """ 74 | return await self._connection.async_post(path, body, custom_header) 75 | 76 | def from_json(self, js) -> None: 77 | """this method will parse the homematicip object from a json object 78 | 79 | :param js: the json object to parse 80 | """ 81 | self._rawJSONData = js 82 | pass 83 | 84 | def fromtimestamp(self, timestamp): 85 | """internal helper function which will create a datetime object from a timestamp""" 86 | if timestamp is None or timestamp <= 0: 87 | return None 88 | return datetime.fromtimestamp(timestamp / 1000.0) 89 | 90 | def set_attr_from_dict( 91 | self, 92 | attr: str, 93 | dict, 94 | type: AutoNameEnum = None, 95 | dict_attr=None, 96 | addToStrOutput=True, 97 | ): 98 | """this method will add the value from dict to the given attr name 99 | 100 | Args: 101 | attr(str): the attribute which value should be changed 102 | dict(dict): the dictionary from which the value should be extracted 103 | type(AutoNameEnum): this will call type.from_str(value), if a type gets provided 104 | dict_attr: the name of the attribute in the dict. Set this to None(default) to use attr 105 | addToStrOutput(str): should the attribute be returned via __str__() 106 | """ 107 | if not dict_attr: 108 | dict_attr = attr 109 | 110 | if dict_attr not in dict: 111 | return None 112 | 113 | value = dict[dict_attr] 114 | if type: 115 | value = type.from_str(value) 116 | 117 | self.__dict__[attr] = value 118 | if addToStrOutput and attr not in self._dictAttributes: 119 | self._dictAttributes.append(attr) 120 | 121 | def str_from_attr_map(self) -> str: 122 | """this method will return a string with all key/values which were added via the set_attr_from_dict method""" 123 | return "".join([f"{x}({self.__dict__[x]}) " for x in self._dictAttributes])[:-1] 124 | 125 | def _run_non_async(self, method, *args, **kwargs): 126 | """Run an async method in a sync way""" 127 | loop = asyncio.get_event_loop() 128 | return loop.run_until_complete(method(*args, **kwargs)) 129 | -------------------------------------------------------------------------------- /src/homematicip/class_maps.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.functionalChannels import * 2 | from homematicip.device import * 3 | from homematicip.functionalHomes import * 4 | from homematicip.group import * 5 | from homematicip.rule import * 6 | from homematicip.securityEvent import * 7 | 8 | TYPE_CLASS_MAP = { 9 | DeviceType.DEVICE: Device, 10 | DeviceType.BASE_DEVICE: BaseDevice, 11 | DeviceType.EXTERNAL: ExternalDevice, 12 | DeviceType.ACCELERATION_SENSOR: AccelerationSensor, 13 | DeviceType.ACCESS_POINT: HomeControlUnit, 14 | DeviceType.ALARM_SIREN_INDOOR: AlarmSirenIndoor, 15 | DeviceType.ALARM_SIREN_OUTDOOR: AlarmSirenOutdoor, 16 | DeviceType.BLIND_MODULE: BlindModule, 17 | DeviceType.BRAND_BLIND: BrandBlind, 18 | DeviceType.BRAND_DIMMER: BrandDimmer, 19 | DeviceType.BRAND_PUSH_BUTTON: BrandPushButton, 20 | DeviceType.BRAND_SHUTTER: FullFlushShutter, 21 | DeviceType.BRAND_SWITCH_2: BrandSwitch2, 22 | DeviceType.BRAND_SWITCH_MEASURING: SwitchMeasuring, 23 | DeviceType.BRAND_SWITCH_NOTIFICATION_LIGHT: BrandSwitchNotificationLight, 24 | DeviceType.BRAND_WALL_MOUNTED_THERMOSTAT: WallMountedThermostatPro, 25 | DeviceType.CARBON_DIOXIDE_SENSOR: CarbonDioxideSensor, 26 | DeviceType.DALI_GATEWAY: DaliGateway, 27 | DeviceType.DIN_RAIL_BLIND_4: DinRailBlind4, 28 | DeviceType.DIN_RAIL_SWITCH: DinRailSwitch, 29 | DeviceType.DIN_RAIL_SWITCH_4: DinRailSwitch4, 30 | DeviceType.DIN_RAIL_DIMMER_3: DinRailDimmer3, 31 | DeviceType.DOOR_BELL_BUTTON: DoorBellButton, 32 | DeviceType.DOOR_BELL_CONTACT_INTERFACE: DoorBellContactInterface, 33 | DeviceType.DOOR_LOCK_DRIVE: DoorLockDrive, 34 | DeviceType.DOOR_LOCK_SENSOR: DoorLockSensor, 35 | DeviceType.ENERGY_SENSORS_INTERFACE: EnergySensorsInterface, 36 | DeviceType.FLOOR_TERMINAL_BLOCK_10: FloorTerminalBlock10, 37 | DeviceType.FLOOR_TERMINAL_BLOCK_12: FloorTerminalBlock12, 38 | DeviceType.FLOOR_TERMINAL_BLOCK_6: FloorTerminalBlock6, 39 | DeviceType.FULL_FLUSH_BLIND: FullFlushBlind, 40 | DeviceType.FULL_FLUSH_CONTACT_INTERFACE: FullFlushContactInterface, 41 | DeviceType.FULL_FLUSH_CONTACT_INTERFACE_6: FullFlushContactInterface6, 42 | DeviceType.FULL_FLUSH_DIMMER: FullFlushDimmer, 43 | DeviceType.FULL_FLUSH_INPUT_SWITCH: FullFlushInputSwitch, 44 | DeviceType.FULL_FLUSH_SHUTTER: FullFlushShutter, 45 | DeviceType.FULL_FLUSH_SWITCH_MEASURING: SwitchMeasuring, 46 | DeviceType.HEATING_SWITCH_2: HeatingSwitch2, 47 | DeviceType.HEATING_THERMOSTAT: HeatingThermostat, 48 | DeviceType.HEATING_THERMOSTAT_COMPACT: HeatingThermostatCompact, 49 | DeviceType.HEATING_THERMOSTAT_COMPACT_PLUS: HeatingThermostatCompact, 50 | DeviceType.HEATING_THERMOSTAT_EVO: HeatingThermostatEvo, 51 | DeviceType.HEATING_THERMOSTAT_THREE: HeatingThermostat, 52 | DeviceType.HEATING_THERMOSTAT_FLEX: HeatingThermostat, 53 | DeviceType.HOME_CONTROL_ACCESS_POINT: HomeControlAccessPoint, 54 | DeviceType.HOERMANN_DRIVES_MODULE: HoermannDrivesModule, 55 | DeviceType.KEY_REMOTE_CONTROL_4: KeyRemoteControl4, 56 | DeviceType.KEY_REMOTE_CONTROL_ALARM: KeyRemoteControlAlarm, 57 | DeviceType.LIGHT_SENSOR: LightSensor, 58 | DeviceType.MOTION_DETECTOR_INDOOR: MotionDetectorIndoor, 59 | DeviceType.MOTION_DETECTOR_OUTDOOR: MotionDetectorOutdoor, 60 | DeviceType.MOTION_DETECTOR_PUSH_BUTTON: MotionDetectorPushButton, 61 | DeviceType.MOTION_DETECTOR_SWITCH_OUTDOOR: MotionDetectorSwitchOutdoor, 62 | DeviceType.MULTI_IO_BOX: MultiIOBox, 63 | DeviceType.OPEN_COLLECTOR_8_MODULE: OpenCollector8Module, 64 | DeviceType.PASSAGE_DETECTOR: PassageDetector, 65 | DeviceType.PLUGABLE_SWITCH: PlugableSwitch, 66 | DeviceType.PLUGABLE_SWITCH_MEASURING: SwitchMeasuring, 67 | DeviceType.PLUGGABLE_DIMMER: PluggableDimmer, 68 | DeviceType.PLUGGABLE_MAINS_FAILURE_SURVEILLANCE: PluggableMainsFailureSurveillance, 69 | DeviceType.PRESENCE_DETECTOR_INDOOR: PresenceDetectorIndoor, 70 | DeviceType.PRINTED_CIRCUIT_BOARD_SWITCH_2: PrintedCircuitBoardSwitch2, 71 | DeviceType.PRINTED_CIRCUIT_BOARD_SWITCH_BATTERY: PrintedCircuitBoardSwitchBattery, 72 | DeviceType.PUSH_BUTTON: PushButton, 73 | DeviceType.PUSH_BUTTON_6: PushButton6, 74 | DeviceType.PUSH_BUTTON_FLAT: PushButtonFlat, 75 | DeviceType.RAIN_SENSOR: RainSensor, 76 | DeviceType.REMOTE_CONTROL_8: RemoteControl8, 77 | DeviceType.REMOTE_CONTROL_8_MODULE: RemoteControl8Module, 78 | DeviceType.RGBW_DIMMER: RgbwDimmer, 79 | DeviceType.ROOM_CONTROL_DEVICE: RoomControlDevice, 80 | DeviceType.ROOM_CONTROL_DEVICE_ANALOG: RoomControlDeviceAnalog, 81 | DeviceType.ROTARY_HANDLE_SENSOR: RotaryHandleSensor, 82 | DeviceType.SHUTTER_CONTACT: ShutterContact, 83 | DeviceType.SHUTTER_CONTACT_INTERFACE: ContactInterface, 84 | DeviceType.SHUTTER_CONTACT_INVISIBLE: ShutterContact, 85 | DeviceType.SHUTTER_CONTACT_MAGNETIC: ShutterContactMagnetic, 86 | DeviceType.SHUTTER_CONTACT_OPTICAL_PLUS: ShutterContactOpticalPlus, 87 | DeviceType.SMOKE_DETECTOR: SmokeDetector, 88 | DeviceType.SWITCH_MEASURING_CABLE_OUTDOOR: SwitchMeasuring, 89 | DeviceType.TEMPERATURE_HUMIDITY_SENSOR: TemperatureHumiditySensorWithoutDisplay, 90 | DeviceType.TEMPERATURE_HUMIDITY_SENSOR_COMPACT: TemperatureHumiditySensorOutdoor, 91 | DeviceType.TEMPERATURE_HUMIDITY_SENSOR_DISPLAY: TemperatureHumiditySensorDisplay, 92 | DeviceType.TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR: TemperatureHumiditySensorOutdoor, 93 | DeviceType.TEMPERATURE_SENSOR_2_EXTERNAL_DELTA: TemperatureDifferenceSensor2, 94 | DeviceType.TILT_VIBRATION_SENSOR: TiltVibrationSensor, 95 | DeviceType.TILT_VIBRATION_SENSOR_COMPACT: TiltVibrationSensor, 96 | DeviceType.TORMATIC_MODULE: GarageDoorModuleTormatic, 97 | DeviceType.WALL_MOUNTED_KEY_PAD: WallMountedKeyPad, 98 | DeviceType.WALL_MOUNTED_GARAGE_DOOR_CONTROLLER: WallMountedGarageDoorController, 99 | DeviceType.WALL_MOUNTED_THERMOSTAT_PRO: WallMountedThermostatPro, 100 | DeviceType.WALL_MOUNTED_THERMOSTAT_BASIC_HUMIDITY: WallMountedThermostatBasicHumidity, 101 | DeviceType.WATER_SENSOR: WaterSensor, 102 | DeviceType.WEATHER_SENSOR: WeatherSensor, 103 | DeviceType.WEATHER_SENSOR_PLUS: WeatherSensorPlus, 104 | DeviceType.WEATHER_SENSOR_PRO: WeatherSensorPro, 105 | DeviceType.WIRED_BLIND_4: WiredDinRailBlind4, 106 | DeviceType.WIRED_DIMMER_3: WiredDimmer3, 107 | DeviceType.WIRED_DIN_RAIL_ACCESS_POINT: WiredDinRailAccessPoint, 108 | DeviceType.WIRED_FLOOR_TERMINAL_BLOCK_12: WiredFloorTerminalBlock12, 109 | DeviceType.WIRED_INPUT_32: WiredInput32, 110 | DeviceType.WIRED_INPUT_SWITCH_6: WiredInputSwitch6, 111 | DeviceType.WIRED_MOTION_DETECTOR_PUSH_BUTTON: WiredMotionDetectorPushButton, 112 | DeviceType.WIRED_PRESENCE_DETECTOR_INDOOR: PresenceDetectorIndoor, 113 | DeviceType.WIRED_PUSH_BUTTON_2: WiredPushButton, 114 | DeviceType.WIRED_PUSH_BUTTON_6: WiredPushButton, 115 | DeviceType.WIRED_SWITCH_8: WiredSwitch8, 116 | DeviceType.WIRED_SWITCH_4: WiredSwitch4, 117 | DeviceType.WIRED_WALL_MOUNTED_THERMOSTAT: WallMountedThermostatPro, 118 | DeviceType.WIRED_CARBON_TEMPERATURE_HUMIDITY_SENSOR_DISPLAY: WiredCarbonTemperatureHumiditySensorDisplay, 119 | DeviceType.WIRELESS_ACCESS_POINT_BASIC: HomeControlUnit 120 | } 121 | 122 | TYPE_GROUP_MAP = { 123 | GroupType.GROUP: Group, 124 | GroupType.AUTO_RELOCK_PROFILE: Group, 125 | GroupType.ACCESS_AUTHORIZATION_PROFILE: AccessAuthorizationProfileGroup, 126 | GroupType.ACCESS_CONTROL: AccessControlGroup, 127 | GroupType.ALARM_SWITCHING: AlarmSwitchingGroup, 128 | GroupType.ENERGY: EnergyGroup, 129 | GroupType.ENVIRONMENT: EnvironmentGroup, 130 | GroupType.EXTENDED_LINKED_GARAGE_DOOR: ExtendedLinkedGarageDoorGroup, 131 | GroupType.EXTENDED_LINKED_SHUTTER: ExtendedLinkedShutterGroup, 132 | GroupType.EXTENDED_LINKED_SWITCHING: ExtendedLinkedSwitchingGroup, 133 | GroupType.HEATING_CHANGEOVER: HeatingChangeoverGroup, 134 | GroupType.HEATING_COOLING_DEMAND_BOILER: HeatingCoolingDemandBoilerGroup, 135 | GroupType.HEATING_COOLING_DEMAND_PUMP: HeatingCoolingDemandPumpGroup, 136 | GroupType.HEATING_COOLING_DEMAND: HeatingCoolingDemandGroup, 137 | GroupType.HEATING_DEHUMIDIFIER: HeatingDehumidifierGroup, 138 | GroupType.HEATING_EXTERNAL_CLOCK: HeatingExternalClockGroup, 139 | GroupType.HEATING_FAILURE_ALERT_RULE_GROUP: HeatingFailureAlertRuleGroup, 140 | GroupType.HEATING_HUMIDITY_LIMITER: HeatingHumidyLimiterGroup, 141 | GroupType.HEATING_TEMPERATURE_LIMITER: HeatingTemperatureLimiterGroup, 142 | GroupType.HEATING: HeatingGroup, 143 | GroupType.HOT_WATER: HotWaterGroup, 144 | GroupType.HUMIDITY_WARNING_RULE_GROUP: HumidityWarningRuleGroup, 145 | GroupType.INBOX: InboxGroup, 146 | GroupType.INDOOR_CLIMATE: IndoorClimateGroup, 147 | GroupType.LINKED_SWITCHING: LinkedSwitchingGroup, 148 | GroupType.LOCK_PROFILE: Group, 149 | GroupType.LOCK_OUT_PROTECTION_RULE: LockOutProtectionRule, 150 | GroupType.OVER_HEAT_PROTECTION_RULE: OverHeatProtectionRule, 151 | GroupType.SECURITY_BACKUP_ALARM_SWITCHING: AlarmSwitchingGroup, 152 | GroupType.SECURITY_ZONE: SecurityZoneGroup, 153 | GroupType.SECURITY: SecurityGroup, 154 | GroupType.SHUTTER_PROFILE: ShutterProfile, 155 | GroupType.SHUTTER_WIND_PROTECTION_RULE: ShutterWindProtectionRule, 156 | GroupType.SMOKE_ALARM_DETECTION_RULE: SmokeAlarmDetectionRule, 157 | GroupType.SWITCHING_PROFILE: SwitchingProfileGroup, 158 | GroupType.SWITCHING: SwitchingGroup, 159 | } 160 | 161 | TYPE_SECURITY_EVENT_MAP = { 162 | SecurityEventType.ACCESS_POINT_CONNECTED: AccessPointConnectedEvent, 163 | SecurityEventType.ACCESS_POINT_DISCONNECTED: AccessPointDisconnectedEvent, 164 | SecurityEventType.ACTIVATION_CHANGED: ActivationChangedEvent, 165 | SecurityEventType.EXTERNAL_TRIGGERED: ExternalTriggeredEvent, 166 | SecurityEventType.MAINS_FAILURE_EVENT: MainsFailureEvent, 167 | SecurityEventType.MOISTURE_DETECTION_EVENT: MoistureDetectionEvent, 168 | SecurityEventType.OFFLINE_ALARM: OfflineAlarmEvent, 169 | SecurityEventType.OFFLINE_WATER_DETECTION_EVENT: OfflineWaterDetectionEvent, 170 | SecurityEventType.SABOTAGE: SabotageEvent, 171 | SecurityEventType.SENSOR_EVENT: SensorEvent, 172 | SecurityEventType.SILENCE_CHANGED: SilenceChangedEvent, 173 | SecurityEventType.SMOKE_ALARM: SmokeAlarmEvent, 174 | SecurityEventType.WATER_DETECTION_EVENT: WaterDetectionEvent, 175 | } 176 | 177 | TYPE_RULE_MAP = {AutomationRuleType.SIMPLE: SimpleRule} 178 | 179 | TYPE_FUNCTIONALHOME_MAP = { 180 | FunctionalHomeType.INDOOR_CLIMATE: IndoorClimateHome, 181 | FunctionalHomeType.ENERGY: EnergyHome, 182 | FunctionalHomeType.LIGHT_AND_SHADOW: LightAndShadowHome, 183 | FunctionalHomeType.SECURITY_AND_ALARM: SecurityAndAlarmHome, 184 | FunctionalHomeType.WEATHER_AND_ENVIRONMENT: WeatherAndEnvironmentHome, 185 | FunctionalHomeType.ACCESS_CONTROL: AccessControlHome, 186 | } 187 | 188 | TYPE_FUNCTIONALCHANNEL_MAP = { 189 | FunctionalChannelType.FUNCTIONAL_CHANNEL: FunctionalChannel, 190 | FunctionalChannelType.ACCELERATION_SENSOR_CHANNEL: AccelerationSensorChannel, 191 | FunctionalChannelType.ACCESS_AUTHORIZATION_CHANNEL: AccessAuthorizationChannel, 192 | FunctionalChannelType.ACCESS_CONTROLLER_CHANNEL: AccessControllerChannel, 193 | FunctionalChannelType.ACCESS_CONTROLLER_WIRED_CHANNEL: AccessControllerWiredChannel, 194 | FunctionalChannelType.ALARM_SIREN_CHANNEL: AlarmSirenChannel, 195 | FunctionalChannelType.ANALOG_OUTPUT_CHANNEL: AnalogOutputChannel, 196 | FunctionalChannelType.ANALOG_ROOM_CONTROL_CHANNEL: AnalogRoomControlChannel, 197 | FunctionalChannelType.BLIND_CHANNEL: BlindChannel, 198 | FunctionalChannelType.CARBON_DIOXIDE_SENSOR_CHANNEL: CarbonDioxideSensorChannel, 199 | FunctionalChannelType.CHANGE_OVER_CHANNEL: ChangeOverChannel, 200 | FunctionalChannelType.CLIMATE_SENSOR_CHANNEL: ClimateSensorChannel, 201 | FunctionalChannelType.CODE_PROTECTED_PRIMARY_ACTION_CHANNEL: CodeProtectedPrimaryActionChannel, 202 | FunctionalChannelType.CODE_PROTECTED_SECONDARY_ACTION_CHANNEL: CodeProtectedSecondaryActionChannel, 203 | FunctionalChannelType.CONTACT_INTERFACE_CHANNEL: ContactInterfaceChannel, 204 | FunctionalChannelType.DEHUMIDIFIER_DEMAND_CHANNEL: DehumidifierDemandChannel, 205 | FunctionalChannelType.DEVICE_BASE_FLOOR_HEATING: DeviceBaseFloorHeatingChannel, 206 | FunctionalChannelType.DEVICE_BASE: DeviceBaseChannel, 207 | FunctionalChannelType.DEVICE_BLOCKING: DeviceBlockingChannel, 208 | FunctionalChannelType.DEVICE_GLOBAL_PUMP_CONTROL: DeviceGlobalPumpControlChannel, 209 | FunctionalChannelType.DEVICE_INCORRECT_POSITIONED: DeviceIncorrectPositionedChannel, 210 | FunctionalChannelType.DEVICE_OPERATIONLOCK: DeviceOperationLockChannel, 211 | FunctionalChannelType.DEVICE_OPERATIONLOCK_WITH_SABOTAGE: DeviceOperationLockChannelWithSabotage, 212 | FunctionalChannelType.DEVICE_PERMANENT_FULL_RX: DevicePermanentFullRxChannel, 213 | FunctionalChannelType.DEVICE_RECHARGEABLE_WITH_SABOTAGE: DeviceRechargeableWithSabotage, 214 | FunctionalChannelType.DEVICE_SABOTAGE: DeviceSabotageChannel, 215 | FunctionalChannelType.DIMMER_CHANNEL: DimmerChannel, 216 | FunctionalChannelType.DOOR_CHANNEL: DoorChannel, 217 | FunctionalChannelType.DOOR_LOCK_CHANNEL: DoorLockChannel, 218 | FunctionalChannelType.DOOR_LOCK_SENSOR_CHANNEL: DoorLockSensorChannel, 219 | FunctionalChannelType.EXTERNAL_BASE_CHANNEL: ExternalBaseChannel, 220 | FunctionalChannelType.EXTERNAL_UNIVERSAL_LIGHT_CHANNEL: ExternalUniversalLightChannel, 221 | FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL: EnergySensorInterfaceChannel, 222 | FunctionalChannelType.FLOOR_TERMINAL_BLOCK_CHANNEL: FloorTeminalBlockChannel, 223 | FunctionalChannelType.FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL: FloorTerminalBlockLocalPumpChannel, 224 | FunctionalChannelType.FLOOR_TERMINAL_BLOCK_MECHANIC_CHANNEL: FloorTerminalBlockMechanicChannel, 225 | FunctionalChannelType.GENERIC_INPUT_CHANNEL: GenericInputChannel, 226 | FunctionalChannelType.HEAT_DEMAND_CHANNEL: HeatDemandChannel, 227 | FunctionalChannelType.HEATING_THERMOSTAT_CHANNEL: HeatingThermostatChannel, 228 | FunctionalChannelType.IMPULSE_OUTPUT_CHANNEL: ImpulseOutputChannel, 229 | FunctionalChannelType.INTERNAL_SWITCH_CHANNEL: InternalSwitchChannel, 230 | FunctionalChannelType.LIGHT_SENSOR_CHANNEL: LightSensorChannel, 231 | FunctionalChannelType.MAINS_FAILURE_CHANNEL: MainsFailureChannel, 232 | FunctionalChannelType.MOTION_DETECTION_CHANNEL: MotionDetectionChannel, 233 | FunctionalChannelType.MULTI_MODE_INPUT_BLIND_CHANNEL: MultiModeInputBlindChannel, 234 | FunctionalChannelType.MULTI_MODE_INPUT_CHANNEL: MultiModeInputChannel, 235 | FunctionalChannelType.MULTI_MODE_INPUT_DIMMER_CHANNEL: MultiModeInputDimmerChannel, 236 | FunctionalChannelType.MULTI_MODE_INPUT_SWITCH_CHANNEL: MultiModeInputSwitchChannel, 237 | FunctionalChannelType.NOTIFICATION_LIGHT_CHANNEL: NotificationLightChannel, 238 | FunctionalChannelType.OPTICAL_SIGNAL_CHANNEL: OpticalSignalChannel, 239 | FunctionalChannelType.OPTICAL_SIGNAL_GROUP_CHANNEL: OpticalSignalGroupChannel, 240 | FunctionalChannelType.PASSAGE_DETECTOR_CHANNEL: PassageDetectorChannel, 241 | FunctionalChannelType.PRESENCE_DETECTION_CHANNEL: PresenceDetectionChannel, 242 | FunctionalChannelType.RAIN_DETECTION_CHANNEL: RainDetectionChannel, 243 | FunctionalChannelType.ROTARY_HANDLE_CHANNEL: RotaryHandleChannel, 244 | FunctionalChannelType.SHADING_CHANNEL: ShadingChannel, 245 | FunctionalChannelType.SHUTTER_CHANNEL: ShutterChannel, 246 | FunctionalChannelType.SHUTTER_CONTACT_CHANNEL: ShutterContactChannel, 247 | FunctionalChannelType.SINGLE_KEY_CHANNEL: SingleKeyChannel, 248 | FunctionalChannelType.SMOKE_DETECTOR_CHANNEL: SmokeDetectorChannel, 249 | FunctionalChannelType.SWITCH_CHANNEL: SwitchChannel, 250 | FunctionalChannelType.SWITCH_MEASURING_CHANNEL: SwitchMeasuringChannel, 251 | FunctionalChannelType.TEMPERATURE_SENSOR_2_EXTERNAL_DELTA_CHANNEL: TemperatureDifferenceSensor2Channel, 252 | FunctionalChannelType.TILT_VIBRATION_SENSOR_CHANNEL: TiltVibrationSensorChannel, 253 | FunctionalChannelType.UNIVERSAL_LIGHT_CHANNEL: UniversalLightChannel, 254 | FunctionalChannelType.UNIVERSAL_LIGHT_GROUP_CHANNEL: UniversalLightChannelGroup, 255 | FunctionalChannelType.WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL: WallMountedThermostatProChannel, 256 | FunctionalChannelType.WALL_MOUNTED_THERMOSTAT_WITHOUT_DISPLAY_CHANNEL: WallMountedThermostatWithoutDisplayChannel, 257 | FunctionalChannelType.WALL_MOUNTED_THERMOSTAT_WITH_CARBON_CHANNEL: WallMountedThermostatWithCarbonChannel, 258 | FunctionalChannelType.WATER_SENSOR_CHANNEL: WaterSensorChannel, 259 | FunctionalChannelType.WEATHER_SENSOR_CHANNEL: WeatherSensorChannel, 260 | FunctionalChannelType.WEATHER_SENSOR_PLUS_CHANNEL: WeatherSensorPlusChannel, 261 | FunctionalChannelType.WEATHER_SENSOR_PRO_CHANNEL: WeatherSensorProChannel, 262 | } 263 | 264 | FUNCTIONALCHANNEL_CLI_MAP = { 265 | FunctionalChannelType.DIMMER_CHANNEL: [ 266 | CliActions.SET_DIM_LEVEL, 267 | ], 268 | FunctionalChannelType.MULTI_MODE_INPUT_DIMMER_CHANNEL: [CliActions.SET_DIM_LEVEL], 269 | FunctionalChannelType.NOTIFICATION_LIGHT_CHANNEL: [CliActions.SET_DIM_LEVEL], 270 | FunctionalChannelType.DOOR_LOCK_CHANNEL: [CliActions.SET_LOCK_STATE], 271 | FunctionalChannelType.IMPULSE_OUTPUT_CHANNEL: [CliActions.TOGGLE_GARAGE_DOOR], 272 | FunctionalChannelType.DOOR_CHANNEL: [CliActions.SEND_DOOR_COMMAND], 273 | FunctionalChannelType.BLIND_CHANNEL: [ 274 | CliActions.SET_SHUTTER_LEVEL, 275 | CliActions.SET_SLATS_LEVEL, 276 | CliActions.SET_SHUTTER_STOP, 277 | ], 278 | FunctionalChannelType.MULTI_MODE_INPUT_BLIND_CHANNEL: [ 279 | CliActions.SET_SHUTTER_LEVEL, 280 | CliActions.SET_SLATS_LEVEL, 281 | CliActions.SET_SHUTTER_STOP, 282 | ], 283 | FunctionalChannelType.SHUTTER_CHANNEL: [ 284 | CliActions.SET_SHUTTER_LEVEL, 285 | CliActions.SET_SHUTTER_STOP, 286 | ], 287 | FunctionalChannelType.SHADING_CHANNEL: [CliActions.SET_SHUTTER_STOP], 288 | FunctionalChannelType.SWITCH_CHANNEL: [CliActions.SET_SWITCH_STATE], 289 | FunctionalChannelType.SWITCH_MEASURING_CHANNEL: [CliActions.SET_SWITCH_STATE, CliActions.RESET_ENERGY_COUNTER], 290 | FunctionalChannelType.MULTI_MODE_INPUT_SWITCH_CHANNEL: [ 291 | CliActions.SET_SWITCH_STATE 292 | ], 293 | } 294 | -------------------------------------------------------------------------------- /src/homematicip/cli/hmip_generate_auth_token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import asyncio 3 | import configparser 4 | import json 5 | import time 6 | from builtins import input 7 | 8 | import homematicip 9 | import homematicip.auth 10 | from homematicip.connection.connection_context import ConnectionContextBuilder 11 | from homematicip.connection.rest_connection import RestConnection 12 | 13 | 14 | async def run_auth(access_point: str = None, devicename: str = None, pin: str = None): 15 | print( 16 | "If you are about to connect to a HomematicIP HCU1 you have to press the button on top of the device, before you continue.") 17 | print("From now, you have 5 Minutes to complete the registration process.") 18 | input("Press Enter to continue...") 19 | 20 | while True: 21 | access_point = ( 22 | input("Please enter the accesspoint id (SGTIN): ").replace("-", "").upper() 23 | ) 24 | if len(access_point) != 24: 25 | print("Invalid access_point id") 26 | continue 27 | break 28 | 29 | context = await ConnectionContextBuilder.build_context_async(access_point) 30 | connection = RestConnection(context, log_status_exceptions=False) 31 | 32 | auth = homematicip.auth.Auth(connection, context.client_auth_token, access_point) 33 | 34 | devicename = input( 35 | "Please enter the client/devicename (leave blank to use default):" 36 | ) 37 | 38 | while True: 39 | pin = input("Please enter the PIN (leave Blank if there is none): ") 40 | 41 | if pin != "": 42 | auth.set_pin(pin) 43 | response = None 44 | if devicename == "": 45 | response = await auth.connection_request(access_point, devicename) 46 | else: 47 | response = await auth.connection_request(access_point) 48 | 49 | if response.status == 200: # ConnectionRequest was fine 50 | break 51 | 52 | errorCode = json.loads(response.text)["errorCode"] 53 | if errorCode == "INVALID_PIN": 54 | print("PIN IS INVALID!") 55 | elif errorCode == "ASSIGNMENT_LOCKED": 56 | print("LOCKED ! Press button on HCU to unlock.") 57 | time.sleep(5) 58 | else: 59 | print("Error: {}\nExiting".format(errorCode)) 60 | return 61 | 62 | print("Connection Request successful!") 63 | print("Please press the blue button on the access point") 64 | while not await auth.is_request_acknowledged(): 65 | print("Please press the blue button on the access point") 66 | time.sleep(2) 67 | 68 | auth_token = await auth.request_auth_token() 69 | clientId = await auth.confirm_auth_token(auth_token) 70 | 71 | print( 72 | "-----------------------------------------------------------------------------" 73 | ) 74 | print("Token successfully registered!") 75 | print( 76 | "AUTH_TOKEN:\t{}\nACCESS_POINT:\t{}\nClient ID:\t{}\nsaving configuration to ./config.ini".format( 77 | auth_token, access_point, clientId 78 | ) 79 | ) 80 | 81 | _config = configparser.ConfigParser() 82 | _config.add_section("AUTH") 83 | _config.add_section("LOGGING") 84 | _config["AUTH"] = {"AuthToken": auth_token, "AccessPoint": access_point} 85 | _config.set("LOGGING", "Level", "30") 86 | _config.set("LOGGING", "FileName", "None") 87 | with open("./config.ini", "w") as configfile: 88 | _config.write(configfile) 89 | 90 | 91 | def main(): 92 | asyncio.run(run_auth()) 93 | 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /src/homematicip/client.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.enums import ClientType 2 | from homematicip.base.homematicip_object import HomeMaticIPObject 3 | 4 | 5 | class Client(HomeMaticIPObject): 6 | """A client is an app which has access to the access point. 7 | e.g. smartphone, 3th party apps, google home, conrad connect 8 | """ 9 | 10 | def __init__(self, connection): 11 | super().__init__(connection) 12 | #:str: the unique id of the client 13 | self.id = "" 14 | #:str: a human understandable name of the client 15 | self.label = "" 16 | #:str: the home where the client belongs to 17 | self.homeId = "" 18 | #:str: the c2c service name 19 | self.c2cServiceIdentifier = "" 20 | #:ClientType: the type of this client 21 | self.clientType = ClientType.APP 22 | 23 | def from_json(self, js): 24 | super().from_json(js) 25 | self.id = js["id"] 26 | self.label = js["label"] 27 | self.homeId = js["homeId"] 28 | self.clientType = ClientType.from_str(js["clientType"]) 29 | if "c2cServiceIdentifier" in js: 30 | self.c2cServiceIdentifier = js["c2cServiceIdentifier"] 31 | 32 | def __str__(self): 33 | return "label({})".format(self.label) 34 | -------------------------------------------------------------------------------- /src/homematicip/connection/__init__.py: -------------------------------------------------------------------------------- 1 | # Connection constants 2 | ATTR_AUTH_TOKEN: str = "AUTHTOKEN" 3 | ATTR_CLIENT_AUTH: str = "CLIENTAUTH" 4 | ATTR_ACCESSPOINT_ID: str = "ACCESSPOINT-ID" 5 | 6 | THROTTLE_STATUS_CODE: int = 429 7 | 8 | # Initial rate limiter settings 9 | RATE_LIMITER_TOKENS: int = 10 # Number of tokens in the bucket 10 | RATE_LIMITER_FILL_RATE: int = 8 # Fill rate of the bucket in tokens per second 11 | -------------------------------------------------------------------------------- /src/homematicip/connection/buckets.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | 5 | class Buckets: 6 | """Class to manage the rate limiting of the HomematicIP Cloud API. 7 | The implementation is based on the token bucket algorithm.""" 8 | 9 | def __init__(self, tokens: int, fill_rate: int): 10 | """Initialize the Buckets with a token bucket algorithm. 11 | 12 | :param tokens: The number of tokens in the bucket. 13 | :param fill_rate: The fill rate of the bucket in tokens every x seconds.""" 14 | self.capacity: int = tokens 15 | self._tokens:int = tokens 16 | self.fill_rate: int = fill_rate 17 | self.timestamp: float = time.time() 18 | self.lock: asyncio.Lock = asyncio.Lock() 19 | 20 | async def take(self, tokens: int = 1) -> bool: 21 | """Get a single token from the bucket. Return True if successful, False otherwise. 22 | 23 | :param tokens: The number of tokens to take from the bucket. Default is 1. 24 | :return: True if successful, False otherwise. 25 | """ 26 | async with self.lock: 27 | if tokens <= await self.tokens(): 28 | self._tokens -= tokens 29 | return True 30 | return False 31 | 32 | async def wait_and_take(self, timeout: int = 120, tokens: int = 1) -> bool: 33 | """Wait until a token is available and then take it. Return True if successful, False otherwise. 34 | 35 | :param timeout: The maximum time to wait for a token in seconds. Default is 120 seconds. 36 | :param tokens: The number of tokens to take from the bucket. Default is 1. 37 | :return: True if successful, False otherwise. 38 | """ 39 | start_time = time.time() 40 | while True: 41 | if tokens <= await self.tokens(): 42 | self._tokens -= tokens 43 | return True 44 | 45 | if time.time() - start_time > timeout: 46 | raise asyncio.TimeoutError("Timeout while waiting for token.") 47 | 48 | await asyncio.sleep(1) # Wait for a second before checking again 49 | 50 | async def tokens(self): 51 | """Get the number of tokens in the bucket. Refill the bucket if necessary.""" 52 | if self._tokens < self.capacity: 53 | now = time.time() 54 | delta = int((now - self.timestamp) / self.fill_rate) 55 | if delta > 0: 56 | self._tokens = min(self.capacity, self._tokens + delta) 57 | self.timestamp = now 58 | return self._tokens 59 | -------------------------------------------------------------------------------- /src/homematicip/connection/client_characteristics_builder.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import platform 3 | 4 | 5 | class ClientCharacteristicsBuilder: 6 | 7 | @staticmethod 8 | def _get_lang() -> str: 9 | """Determines the language.""" 10 | def_locale = locale.getlocale() 11 | if def_locale is not None and def_locale[0] is not None: 12 | return def_locale[0] 13 | 14 | return "en_US" 15 | 16 | @classmethod 17 | def get(cls, access_point_id: str) -> dict: 18 | """Return client characteristics as dictionary 19 | :param access_point_id: The access point id 20 | :return: The client characteristics as dictionary""" 21 | 22 | return { 23 | "clientCharacteristics": { 24 | "apiVersion": "10", 25 | "applicationIdentifier": "homematicip-python", 26 | "applicationVersion": "1.0", 27 | "deviceManufacturer": "none", 28 | "deviceType": "Computer", 29 | "language": cls._get_lang(), 30 | "osType": platform.system(), 31 | "osVersion": platform.release(), 32 | }, 33 | "id": access_point_id, 34 | } 35 | -------------------------------------------------------------------------------- /src/homematicip/connection/client_token_builder.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | class ClientTokenBuilder: 5 | 6 | @staticmethod 7 | def build_client_token(accesspoint_id: str): 8 | return ( 9 | hashlib.sha512(str(accesspoint_id + "jiLpVitHvWnIGD1yo7MA").encode("utf-8")) 10 | .hexdigest() 11 | .upper() 12 | ) 13 | -------------------------------------------------------------------------------- /src/homematicip/connection/connection_context.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from ssl import SSLContext 3 | 4 | import httpx 5 | 6 | from homematicip.connection.client_characteristics_builder import ClientCharacteristicsBuilder 7 | from homematicip.connection.client_token_builder import ClientTokenBuilder 8 | from homematicip.connection.connection_url_resolver import ConnectionUrlResolver 9 | 10 | 11 | class ConnectionContextBuilder: 12 | 13 | @classmethod 14 | async def build_context_async(cls, accesspoint_id: str, 15 | lookup_url: str = "https://lookup.homematic.com:48335/getHost", 16 | auth_token: str | None = None, 17 | enforce_ssl: bool = True, 18 | httpx_client_session: httpx.AsyncClient | None = None, 19 | ssl_ctx=None): 20 | """ 21 | Create a new connection context and lookup urls 22 | 23 | :param accesspoint_id: Access point id 24 | :param lookup_url: Url to lookup the connection urls 25 | :param auth_token: The Auth Token if exists. If no one is provided None will be used 26 | :param enforce_ssl: Disable ssl verification by setting enforce_ssl to False 27 | :param httpx_client_session: The httpx client session if you want to use a custom one 28 | :param ssl_ctx: ssl context to use 29 | :return: a new ConnectionContext 30 | """ 31 | ctx = ConnectionContext() 32 | ctx.accesspoint_id = accesspoint_id 33 | ctx.client_auth_token = ClientTokenBuilder.build_client_token(accesspoint_id) 34 | ctx.ssl_ctx = ssl_ctx 35 | ctx.enforce_ssl = enforce_ssl 36 | 37 | cc = ClientCharacteristicsBuilder.get(accesspoint_id) 38 | ctx.rest_url, ctx.websocket_url = await ConnectionUrlResolver().lookup_urls_async(cc, lookup_url, enforce_ssl, 39 | ssl_ctx, httpx_client_session) 40 | 41 | if auth_token is not None: 42 | ctx.auth_token = auth_token 43 | 44 | return ctx 45 | 46 | @classmethod 47 | def build_context(cls, accesspoint_id: str, 48 | lookup_url: str = "https://lookup.homematic.com:48335/getHost", 49 | auth_token: str | None = None, 50 | enforce_ssl: bool = True, 51 | ssl_ctx: SSLContext | str | bool | None = None): 52 | """ 53 | Create a new connection context and lookup urls 54 | 55 | :param accesspoint_id: Access point id 56 | :param lookup_url: Url to lookup the connection urls 57 | :param auth_token: The Auth Token if exists. If no one is provided None will be used 58 | :param enforce_ssl: Disable ssl verification by setting enforce_ssl to False 59 | :param ssl_ctx: ssl context to use 60 | :return: a new ConnectionContext 61 | """ 62 | ctx = ConnectionContext() 63 | ctx.accesspoint_id = accesspoint_id 64 | ctx.client_auth_token = ClientTokenBuilder.build_client_token(accesspoint_id) 65 | ctx.ssl_ctx = ssl_ctx 66 | ctx.enforce_ssl = enforce_ssl 67 | 68 | cc = ClientCharacteristicsBuilder.get(accesspoint_id) 69 | ctx.rest_url, ctx.websocket_url = ConnectionUrlResolver().lookup_urls(cc, lookup_url, enforce_ssl, ssl_ctx) 70 | 71 | if auth_token is not None: 72 | ctx.auth_token = auth_token 73 | 74 | return ctx 75 | 76 | 77 | @dataclass 78 | class ConnectionContext: 79 | auth_token: str | None = None 80 | client_auth_token: str | None = None 81 | 82 | websocket_url: str = "ws://localhost:8765" 83 | rest_url: str | None = None 84 | accesspoint_id: str | None = None 85 | 86 | enforce_ssl: bool = True 87 | ssl_ctx: SSLContext | str | bool | None = None 88 | -------------------------------------------------------------------------------- /src/homematicip/connection/connection_factory.py: -------------------------------------------------------------------------------- 1 | from homematicip.connection.connection_context import ConnectionContext 2 | from homematicip.connection.rate_limited_rest_connection import RateLimitedRestConnection 3 | from homematicip.connection.rest_connection import RestConnection 4 | 5 | 6 | class ConnectionFactory: 7 | """factory class for creating connections""" 8 | 9 | @staticmethod 10 | def create_connection(context: ConnectionContext, use_rate_limited_connection: bool = True, 11 | httpx_client_session=None) -> RestConnection: 12 | """creates a connection object with the given context""" 13 | if use_rate_limited_connection: 14 | return RateLimitedRestConnection(context, httpx_client_session=httpx_client_session) 15 | return RestConnection(context, httpx_client_session=httpx_client_session) 16 | -------------------------------------------------------------------------------- /src/homematicip/connection/connection_url_resolver.py: -------------------------------------------------------------------------------- 1 | from ssl import SSLContext 2 | 3 | import httpx 4 | 5 | 6 | class ConnectionUrlResolver: 7 | """Lookup rest and websocket urls.""" 8 | 9 | @staticmethod 10 | async def lookup_urls_async( 11 | client_characteristics: dict, 12 | lookup_url: str, 13 | enforce_ssl: bool = True, 14 | ssl_context: SSLContext | None= None, 15 | httpx_client_session: httpx.AsyncClient | None = None, 16 | ) -> tuple[str, str]: 17 | """Lookup urls async. 18 | 19 | :param client_characteristics: The client characteristics 20 | :param lookup_url: The lookup url 21 | :param enforce_ssl: Disable ssl verification by setting enforce_ssl to False 22 | :param ssl_context: The ssl context 23 | :param httpx_client_session: The httpx client session if you want to use a custom one 24 | 25 | :return: The rest and websocket url as tuple 26 | """ 27 | verify = ConnectionUrlResolver._get_verify(enforce_ssl, ssl_context) 28 | 29 | if httpx_client_session is None: 30 | async with httpx.AsyncClient(verify=verify) as client: 31 | result = await client.post(lookup_url, json=client_characteristics) 32 | else: 33 | result = await httpx_client_session.post(lookup_url, json=client_characteristics) 34 | 35 | result.raise_for_status() 36 | 37 | js = result.json() 38 | 39 | rest_url = js["urlREST"] 40 | websocket_url = js["urlWebSocket"] 41 | 42 | return rest_url, websocket_url 43 | 44 | @staticmethod 45 | def lookup_urls( 46 | client_characteristics: dict, 47 | lookup_url: str, 48 | enforce_ssl: bool = True, 49 | ssl_context=None, 50 | ) -> tuple[str, str]: 51 | """Lookup urls. 52 | 53 | :param client_characteristics: The client characteristics 54 | :param lookup_url: The lookup url 55 | :param enforce_ssl: Disable ssl verification by setting enforce_ssl to False 56 | :param ssl_context: The ssl context 57 | 58 | :return: The rest and websocket url as tuple 59 | """ 60 | verify = ConnectionUrlResolver._get_verify(enforce_ssl, ssl_context) 61 | result = httpx.post(lookup_url, json=client_characteristics, verify=verify) 62 | result.raise_for_status() 63 | 64 | js = result.json() 65 | 66 | rest_url = js["urlREST"] 67 | websocket_url = js["urlWebSocket"] 68 | 69 | return rest_url, websocket_url 70 | 71 | @staticmethod 72 | def _get_verify(enforce_ssl: bool, ssl_context): 73 | if ssl_context is not None: 74 | return ssl_context 75 | if enforce_ssl: 76 | return enforce_ssl 77 | 78 | return True 79 | -------------------------------------------------------------------------------- /src/homematicip/connection/rate_limited_rest_connection.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | from homematicip.connection import RATE_LIMITER_FILL_RATE, RATE_LIMITER_TOKENS 4 | from homematicip.connection.buckets import Buckets 5 | from homematicip.connection.connection_context import ConnectionContext 6 | from homematicip.connection.rest_connection import RestConnection, RestResult 7 | 8 | 9 | class RateLimitedRestConnection(RestConnection): 10 | 11 | def __init__(self, 12 | context: ConnectionContext, 13 | tokens: int = RATE_LIMITER_TOKENS, 14 | fill_rate: int = RATE_LIMITER_FILL_RATE, 15 | httpx_client_session: httpx.AsyncClient | None = None): 16 | """Initialize the RateLimitedRestConnection with a token bucket algorithm. 17 | 18 | :param context: The connection context. 19 | :param tokens: The number of tokens in the bucket. Default is 10. 20 | :param fill_rate: The fill rate of the bucket in tokens per second. Default is 8. 21 | :param httpx_client_session: The httpx client session if you want to use a custom one. 22 | """ 23 | super().__init__(context, httpx_client_session=httpx_client_session) 24 | self._buckets = Buckets(tokens=tokens, fill_rate=fill_rate) 25 | 26 | async def async_post(self, url: str, data: dict | None = None, custom_header: dict | None = None) -> RestResult: 27 | """Post data to the HomematicIP Cloud API.""" 28 | await self._buckets.wait_and_take() 29 | return await super().async_post(url, data, custom_header) 30 | -------------------------------------------------------------------------------- /src/homematicip/connection/rest_connection.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from dataclasses import dataclass 4 | from ssl import SSLContext 5 | from typing import Optional 6 | 7 | import httpx 8 | 9 | from homematicip.connection import ATTR_AUTH_TOKEN, ATTR_CLIENT_AUTH, THROTTLE_STATUS_CODE, ATTR_ACCESSPOINT_ID 10 | from homematicip.connection.connection_context import ConnectionContext 11 | from homematicip.exceptions.connection_exceptions import HmipThrottlingError 12 | 13 | LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | @dataclass 17 | class RestResult: 18 | status: int = -1 19 | status_text: str = "" 20 | json: Optional[dict] = None 21 | exception: Optional[Exception] = None 22 | success: bool = False 23 | text: str = "" 24 | 25 | def __post_init__(self): 26 | self.status_text = httpx.codes.get_reason_phrase(self.status) 27 | if self.status_text == "": 28 | self.status_text = "No status code" 29 | 30 | self.success = 200 <= self.status < 300 31 | 32 | 33 | @dataclass 34 | class RestConnection: 35 | _context: ConnectionContext | None = None 36 | _headers: dict[str, str] = None 37 | _verify = None 38 | _log_status_exceptions = True 39 | _httpx_client_session: httpx.AsyncClient | None = None 40 | 41 | def __init__(self, context: ConnectionContext, httpx_client_session: httpx.AsyncClient | None = None, 42 | log_status_exceptions: bool = True): 43 | """Initialize the RestConnection object. 44 | 45 | @param context: The connection context 46 | @param httpx_client_session: The httpx client session if you want to use a custom one 47 | @param log_status_exceptions: If status exceptions should be logged 48 | """ 49 | LOGGER.debug("Initialize new RestConnection") 50 | self.update_connection_context(context) 51 | self._log_status_exceptions = log_status_exceptions 52 | self._httpx_client_session = httpx_client_session 53 | 54 | def update_connection_context(self, context: ConnectionContext) -> None: 55 | self._context: ConnectionContext = context 56 | self._headers: dict = self._get_header(context) 57 | self._verify: SSLContext | str | bool = self._get_verify(context.enforce_ssl, context.ssl_ctx) 58 | 59 | @staticmethod 60 | def _get_header(context: ConnectionContext) -> dict[str, str]: 61 | """Create a json header""" 62 | return { 63 | "content-type": "application/json", 64 | # "accept": "application/json", 65 | "VERSION": "12", 66 | ATTR_AUTH_TOKEN: context.auth_token, 67 | ATTR_CLIENT_AUTH: context.client_auth_token, 68 | ATTR_ACCESSPOINT_ID: context.accesspoint_id 69 | } 70 | 71 | def get_header(self) -> dict[str, str]: 72 | """If headers must be manipulated use this method to get the current headers.""" 73 | return self._headers 74 | 75 | async def async_post(self, url: str, data: dict | None = None, custom_header: dict | None = None) -> RestResult: 76 | """Send an async post request to cloud with json data. Returns a json result. 77 | @param url: The path of the url to send the request to 78 | @param data: The data to send as json 79 | @param custom_header: A custom header to send. Replaces the default header 80 | @return: The result as a RestResult object 81 | """ 82 | full_url = self._build_url(self._context.rest_url, url) 83 | try: 84 | header = self._headers 85 | if custom_header is not None: 86 | header = custom_header 87 | 88 | LOGGER.debug(f"Sending post request to url {full_url}. Data is: {data}") 89 | r = await self._execute_request_async(full_url, data, header) 90 | LOGGER.debug(f"Got response {r.status_code}.") 91 | 92 | if r.status_code == THROTTLE_STATUS_CODE: 93 | LOGGER.error("Got error 429 (Throttling active)") 94 | raise HmipThrottlingError 95 | 96 | r.raise_for_status() 97 | 98 | result = RestResult(status=r.status_code) 99 | try: 100 | result.json = r.json() 101 | except json.JSONDecodeError: 102 | pass 103 | 104 | return result 105 | except httpx.RequestError as exc: 106 | LOGGER.error(f"An error occurred while requesting {exc.request.url!r}.") 107 | return RestResult(status=-1, exception=exc) 108 | except httpx.HTTPStatusError as exc: 109 | if self._log_status_exceptions: 110 | LOGGER.error( 111 | f"Error response {exc.response.status_code} while requesting {exc.request.url!r} with data {data if data is not None else ""}." 112 | ) 113 | LOGGER.error(f"Response: {repr(exc.response)}") 114 | return RestResult(status=exc.response.status_code, exception=exc, text=exc.response.text) 115 | 116 | async def _execute_request_async(self, url: str, data: dict | None = None, header: dict | None = None): 117 | """Execute a request async. Uses the httpx client session if available. 118 | @param url: The path of the url to send the request to 119 | @param data: The data to send as json 120 | @param custom_header: A custom header to send. Replaces the default header 121 | @return: The result as a RestResult object 122 | """ 123 | if self._httpx_client_session is None: 124 | async with httpx.AsyncClient(verify=self._verify) as client: 125 | result = await client.post(url, json=data, headers=header) 126 | else: 127 | result = await self._httpx_client_session.post(url, json=data, headers=header) 128 | 129 | return result 130 | 131 | @staticmethod 132 | def _build_url(base_url: str, path: str) -> str: 133 | """Build full qualified url.""" 134 | return f"{base_url}/hmip/{path}" 135 | 136 | @staticmethod 137 | def _get_verify(enforce_ssl: bool, ssl_context) -> SSLContext | str | bool: 138 | if ssl_context is not None: 139 | return ssl_context 140 | if enforce_ssl: 141 | return enforce_ssl 142 | 143 | return True 144 | -------------------------------------------------------------------------------- /src/homematicip/connection/websocket_handler.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Callable, List 4 | 5 | import aiohttp 6 | 7 | from homematicip.connection import ATTR_AUTH_TOKEN, ATTR_CLIENT_AUTH, ATTR_ACCESSPOINT_ID 8 | from homematicip.connection.connection_context import ConnectionContext 9 | 10 | LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | class WebsocketHandler: 14 | """ 15 | Manages a WebSocket connection to Homematic IP and provides methods for starting, stopping, and processing messages. 16 | Supports automatic reconnect, adding handlers, and status queries. 17 | """ 18 | 19 | def __init__(self): 20 | self.url = None 21 | self._session = None 22 | self._ws = None 23 | self._stop_event = asyncio.Event() 24 | self._reconnect_task = None 25 | self._task_lock = asyncio.Lock() 26 | self._on_message_handlers: List[Callable] = [] 27 | self._on_connected_handler: List[Callable] = [] 28 | self._on_disconnected_handler: List[Callable] = [] 29 | 30 | def add_on_connected_handler(self, handler: Callable): 31 | """Adds a handler that is called when the connection is established.""" 32 | self._on_connected_handler.append(handler) 33 | 34 | def add_on_disconnected_handler(self, handler: Callable): 35 | """Adds a handler that is called when the connection is closed.""" 36 | self._on_disconnected_handler.append(handler) 37 | 38 | def add_on_message_handler(self, handler: Callable): 39 | """Adds a handler for incoming messages.""" 40 | self._on_message_handlers.append(handler) 41 | 42 | async def _call_handlers(self, handlers, *args): 43 | """Helper function to call handlers (sync and async).""" 44 | for handler in handlers: 45 | try: 46 | if asyncio.iscoroutinefunction(handler): 47 | await handler(*args) 48 | else: 49 | handler(*args) 50 | except Exception as e: 51 | handler_name = getattr(handler, "__name__", repr(handler)) 52 | LOGGER.error(f"Error in handler '{handler_name}': {e}", exc_info=True) 53 | 54 | async def _connect(self, context: ConnectionContext): 55 | backoff = 1 56 | max_backoff = 1800 57 | while not self._stop_event.is_set(): 58 | try: 59 | LOGGER.info(f"Connect to {context.websocket_url}") 60 | self._session = aiohttp.ClientSession() 61 | self._ws = await self._session.ws_connect( 62 | context.websocket_url, 63 | headers={ 64 | ATTR_AUTH_TOKEN: context.auth_token, 65 | ATTR_CLIENT_AUTH: context.client_auth_token, 66 | ATTR_ACCESSPOINT_ID: context.accesspoint_id 67 | }, 68 | ssl=getattr(context, 'ssl_ctx', True), 69 | heartbeat=30, 70 | timeout=aiohttp.ClientTimeout(total=30) 71 | ) 72 | LOGGER.info(f"WebSocket connection established to {context.websocket_url}.") 73 | await self._call_handlers(self._on_connected_handler) 74 | backoff = 1 75 | await self._listen() 76 | except Exception as e: 77 | LOGGER.warning(f"Websocket lost connection: {e}. Retry in {backoff:.1f}s.") 78 | await asyncio.sleep(backoff) 79 | backoff = min(backoff * 2, max_backoff) 80 | finally: 81 | await self._cleanup() 82 | 83 | async def _listen(self): 84 | async for msg in self._ws: 85 | if msg.type in (aiohttp.WSMsgType.TEXT, aiohttp.WSMsgType.BINARY): 86 | LOGGER.debug(f"Received message {msg.data}") 87 | await self._call_handlers(self._on_message_handlers, msg.data) 88 | elif msg.type == aiohttp.WSMsgType.ERROR: 89 | LOGGER.error(f"Error in websocket: {msg}") 90 | break 91 | 92 | async def _cleanup(self): 93 | if self._ws: 94 | if not self._ws.closed: 95 | await self._ws.close() 96 | self._ws = None 97 | if self._session: 98 | if not self._session.closed: 99 | await self._session.close() 100 | self._session = None 101 | 102 | async def start(self, context: ConnectionContext): 103 | async with self._task_lock: 104 | LOGGER.info("Start websocket client...") 105 | if self._reconnect_task and not self._reconnect_task.done(): 106 | LOGGER.info("Already connected.") 107 | return 108 | self._stop_event.clear() 109 | self._reconnect_task = asyncio.create_task(self._connect(context)) 110 | self._reconnect_task.add_done_callback(self._handle_task_result) 111 | LOGGER.info("Connect task started.") 112 | 113 | async def stop(self): 114 | LOGGER.info("Stop websocket client...") 115 | self._stop_event.set() 116 | async with self._task_lock: 117 | if self._reconnect_task: 118 | await self._reconnect_task 119 | self._reconnect_task = None 120 | await self._cleanup() 121 | LOGGER.info("[Stop] WebSocket client stopped.") 122 | 123 | def _handle_task_result(self, task: asyncio.Task): 124 | try: 125 | task.result() 126 | except asyncio.CancelledError: 127 | LOGGER.info("[Task] Reconnect task was cancelled.") 128 | except Exception as e: 129 | LOGGER.error(f"[Task] Error in reconnect task: {e}") 130 | 131 | def is_connected(self): 132 | """Returns True if the WebSocket connection is active.""" 133 | return self._ws is not None and not self._ws.closed 134 | -------------------------------------------------------------------------------- /src/homematicip/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahn-th/homematicip-rest-api/c745e0293bbf34ef757058a5f48cb88b8172a5ca/src/homematicip/exceptions/__init__.py -------------------------------------------------------------------------------- /src/homematicip/exceptions/connection_exceptions.py: -------------------------------------------------------------------------------- 1 | class HmipConnectionError(Exception): 2 | pass 3 | 4 | 5 | class HmipServerCloseError(HmipConnectionError): 6 | pass 7 | 8 | 9 | class HmipThrottlingError(HmipConnectionError): 10 | pass 11 | -------------------------------------------------------------------------------- /src/homematicip/functionalHomes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | from homematicip.base.enums import * 5 | from homematicip.base.homematicip_object import HomeMaticIPObject 6 | from homematicip.group import Group 7 | 8 | 9 | class FunctionalHome(HomeMaticIPObject): 10 | def __init__(self, connection): 11 | super().__init__(connection) 12 | 13 | self.functionalGroups = List[Group] 14 | self.solution = "" 15 | self.active = False 16 | 17 | def from_json(self, js, groups: List[Group]): 18 | super().from_json(js) 19 | 20 | self.solution = js["solution"] 21 | self.active = js["active"] 22 | 23 | self.functionalGroups = self.assignGroups(js["functionalGroups"], groups) 24 | 25 | def assignGroups(self, gids, groups: List[Group]): 26 | ret = [] 27 | for gid in gids: 28 | for g in groups: 29 | if g.id == gid: 30 | ret.append(g) 31 | return ret 32 | 33 | 34 | class IndoorClimateHome(FunctionalHome): 35 | def __init__(self, connection): 36 | super().__init__(connection) 37 | self.absenceEndTime = None 38 | self.absenceType = AbsenceType.NOT_ABSENT 39 | self.coolingEnabled = False 40 | self.ecoDuration = EcoDuration.PERMANENT 41 | self.ecoTemperature = 0.0 42 | self.optimumStartStopEnabled = False 43 | self.floorHeatingSpecificGroups = [] 44 | 45 | def from_json(self, js, groups: List[Group]): 46 | super().from_json(js, groups) 47 | if js["absenceEndTime"] is None: 48 | self.absenceEndTime = None 49 | else: 50 | # Why can't EQ-3 use the timestamp here like everywhere else -.- 51 | self.absenceEndTime = datetime.strptime( 52 | js["absenceEndTime"], "%Y_%m_%d %H:%M" 53 | ) 54 | self.absenceType = AbsenceType.from_str(js["absenceType"]) 55 | self.coolingEnabled = js["coolingEnabled"] 56 | self.ecoDuration = EcoDuration.from_str(js["ecoDuration"]) 57 | self.ecoTemperature = js["ecoTemperature"] 58 | self.optimumStartStopEnabled = js["optimumStartStopEnabled"] 59 | 60 | self.floorHeatingSpecificGroups = self.assignGroups( 61 | js["floorHeatingSpecificGroups"].values(), groups 62 | ) 63 | 64 | 65 | class EnergyHome(FunctionalHome): 66 | pass 67 | 68 | 69 | class WeatherAndEnvironmentHome(FunctionalHome): 70 | pass 71 | 72 | 73 | class LightAndShadowHome(FunctionalHome): 74 | def __init__(self, connection): 75 | super().__init__(connection) 76 | self.extendedLinkedShutterGroups = [] 77 | self.extendedLinkedSwitchingGroups = [] 78 | self.shutterProfileGroups = [] 79 | self.switchingProfileGroups = [] 80 | 81 | def from_json(self, js, groups: List[Group]): 82 | super().from_json(js, groups) 83 | 84 | self.extendedLinkedShutterGroups = self.assignGroups( 85 | js["extendedLinkedShutterGroups"], groups 86 | ) 87 | self.extendedLinkedSwitchingGroups = self.assignGroups( 88 | js["extendedLinkedSwitchingGroups"], groups 89 | ) 90 | self.shutterProfileGroups = self.assignGroups( 91 | js["shutterProfileGroups"], groups 92 | ) 93 | self.switchingProfileGroups = self.assignGroups( 94 | js["switchingProfileGroups"], groups 95 | ) 96 | 97 | 98 | class SecurityAndAlarmHome(FunctionalHome): 99 | def __init__(self, connection): 100 | super().__init__(connection) 101 | self.activationInProgress = False 102 | self.alarmActive = False 103 | self.alarmEventDeviceId = "" 104 | self.alarmEventTimestamp = None 105 | self.intrusionAlertThroughSmokeDetectors = False 106 | self.zoneActivationDelay = 0.0 107 | self.securityZoneActivationMode = ( 108 | SecurityZoneActivationMode.ACTIVATION_WITH_DEVICE_IGNORELIST 109 | ) 110 | 111 | self.securitySwitchingGroups = [] 112 | self.securityZones = [] 113 | 114 | def from_json(self, js, groups: List[Group]): 115 | super().from_json(js, groups) 116 | self.activationInProgress = js["activationInProgress"] 117 | self.alarmActive = js["alarmActive"] 118 | if js["alarmEventDeviceChannel"] != None: 119 | self.alarmEventDeviceId = js["alarmEventDeviceChannel"]["deviceId"] 120 | self.alarmEventTimestamp = self.fromtimestamp(js["alarmEventTimestamp"]) 121 | self.intrusionAlertThroughSmokeDetectors = js[ 122 | "intrusionAlertThroughSmokeDetectors" 123 | ] 124 | self.zoneActivationDelay = js["zoneActivationDelay"] 125 | self.securityZoneActivationMode = SecurityZoneActivationMode.from_str( 126 | js["securityZoneActivationMode"] 127 | ) 128 | 129 | self.securitySwitchingGroups = self.assignGroups( 130 | js["securitySwitchingGroups"].values(), groups 131 | ) 132 | self.securityZones = self.assignGroups(js["securityZones"].values(), groups) 133 | 134 | 135 | class AccessControlHome(FunctionalHome): 136 | def __init__(self, connection): 137 | super().__init__(connection) 138 | self.accessAuthorizationProfileGroups = [] 139 | self.lockProfileGroups = [] 140 | self.autoRelockProfileGroups = [] 141 | self.extendedLinkedGarageDoorGroups = [] 142 | self.extendedLinkedNotificationGroups = [] 143 | 144 | def from_json(self, js, groups: List[Group]): 145 | super().from_json(js, groups) 146 | self.accessAuthorizationProfileGroups = self.assignGroups( 147 | js["accessAuthorizationProfileGroups"], groups 148 | ) 149 | self.lockProfileGroups = self.assignGroups(js["lockProfileGroups"], groups) 150 | self.autoRelockProfileGroups = self.assignGroups( 151 | js["autoRelockProfileGroups"], groups 152 | ) 153 | self.extendedLinkedGarageDoorGroups = self.assignGroups( 154 | js["extendedLinkedGarageDoorGroups"], groups 155 | ) 156 | self.extendedLinkedNotificationGroups = self.assignGroups( 157 | js["extendedLinkedNotificationGroups"], groups 158 | ) 159 | -------------------------------------------------------------------------------- /src/homematicip/home.py: -------------------------------------------------------------------------------- 1 | from homematicip.async_home import AsyncHome 2 | from homematicip.group import * 3 | from homematicip.securityEvent import * 4 | 5 | LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | class Home(AsyncHome): 9 | """this class represents the 'Home' of the homematic ip""" 10 | 11 | def init(self, access_point_id, auth_token: str | None = None, lookup=True, use_rate_limiting=True): 12 | return self._run_non_async(self.init_async, access_point_id, auth_token, lookup, use_rate_limiting) 13 | 14 | def activate_absence_permanent(self): 15 | return self._run_non_async(self.activate_absence_permanent_async) 16 | 17 | def activate_absence_with_duration(self, duration: int): 18 | return self._run_non_async(self.activate_absence_with_duration_async, duration) 19 | 20 | def activate_absence_with_period(self, endtime: datetime): 21 | return self._run_non_async(self.activate_absence_with_period_async, endtime) 22 | 23 | def activate_vacation(self, endtime: datetime, temperature: float): 24 | return self._run_non_async(self.activate_vacation_async, endtime, temperature) 25 | 26 | def deactivate_absence(self): 27 | return self._run_non_async(self.deactivate_absence_async) 28 | 29 | def deactivate_vacation(self): 30 | return self._run_non_async(self.deactivate_vacation_async) 31 | 32 | def download_configuration(self) -> dict: 33 | return self._run_non_async(self.download_configuration_async) 34 | 35 | def get_current_state(self, clear_config: bool = False) -> dict: 36 | return self._run_non_async(self.get_current_state_async, clear_config) 37 | 38 | def get_OAuth_OTK(self): 39 | return self._run_non_async(self.get_OAuth_OTK_async) 40 | 41 | def get_security_journal(self): 42 | return self._run_non_async(self.get_security_journal_async) 43 | 44 | def set_cooling(self, cooling): 45 | return self._run_non_async(self.set_cooling_async, cooling) 46 | 47 | def set_intrusion_alert_through_smoke_detectors(self, activate: bool = True): 48 | return self._run_non_async(self.set_intrusion_alert_through_smoke_detectors_async, activate) 49 | 50 | def set_location(self, city, latitude, longitude): 51 | return self._run_non_async(self.set_location_async, city, latitude, longitude) 52 | 53 | def set_pin(self, newPin: str, oldPin: str = None) -> dict: 54 | return self._run_non_async(self.set_pin_async, newPin, oldPin) 55 | 56 | def set_powermeter_unit_price(self, price): 57 | return self._run_non_async(self.set_powermeter_unit_price_async, price) 58 | 59 | def set_security_zones_activation(self, internal=True, external=True): 60 | return self._run_non_async(self.set_security_zones_activation_async, internal, external) 61 | 62 | def set_silent_alarm(self, internal=True, external=True): 63 | return self._run_non_async(self.set_silent_alarm_async, internal, external) 64 | 65 | def set_timezone(self, timezone: str): 66 | return self._run_non_async(self.set_timezone_async, timezone) 67 | 68 | def set_zone_activation_delay(self, delay): 69 | return self._run_non_async(self.set_zone_activation_delay_async, delay) 70 | 71 | def start_inclusion(self, deviceId): 72 | return self._run_non_async(self.start_inclusion_async, deviceId) 73 | 74 | def set_zones_device_assignment(self, internal_devices, external_devices): 75 | return self._run_non_async(self.set_zones_device_assignment_async, internal_devices, external_devices) 76 | -------------------------------------------------------------------------------- /src/homematicip/location.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.homematicip_object import HomeMaticIPObject 2 | 3 | 4 | class Location(HomeMaticIPObject): 5 | """This class represents the possible location""" 6 | 7 | def __init__(self, connection): 8 | super().__init__(connection) 9 | #:str: the name of the city 10 | self.city = "London" 11 | #:float: the latitude of the location 12 | self.latitude = 51.509865 13 | #:float: the longitue of the location 14 | self.longitude = -0.118092 15 | 16 | def from_json(self, js): 17 | super().from_json(js) 18 | self.city = js["city"] 19 | self.latitude = js["latitude"] 20 | self.longitude = js["longitude"] 21 | 22 | def __str__(self): 23 | return "city({}) latitude({}) longitude({})".format( 24 | self.city, self.latitude, self.longitude 25 | ) 26 | -------------------------------------------------------------------------------- /src/homematicip/oauth_otk.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.homematicip_object import HomeMaticIPObject 2 | 3 | 4 | class OAuthOTK(HomeMaticIPObject): 5 | def __init__(self, connection): 6 | super().__init__(connection) 7 | self.authToken = None 8 | self.expirationTimestamp = None 9 | 10 | def from_json(self, js): 11 | super().from_json(js) 12 | self.authToken = js["authToken"] 13 | self.expirationTimestamp = self.fromtimestamp(js["expirationTimestamp"]) 14 | -------------------------------------------------------------------------------- /src/homematicip/rule.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.homematicip_object import HomeMaticIPObject 2 | 3 | 4 | class Rule(HomeMaticIPObject): 5 | """this class represents the automation rule """ 6 | 7 | def __init__(self, connection): 8 | super().__init__(connection) 9 | self.id = None 10 | self.homeId = None 11 | self.label = "" 12 | self.active = False 13 | self.ruleErrorCategories = [] 14 | self.ruleType = "" 15 | # these 3 fill be filled from subclasses 16 | self.errorRuleTriggerItems = [] 17 | self.errorRuleConditionItems = [] 18 | self.errorRuleActionItems = [] 19 | 20 | def from_json(self, js): 21 | super().from_json(js) 22 | self.id = js["id"] 23 | self.homeId = js["homeId"] 24 | self.label = js["label"] 25 | self.active = js["active"] 26 | self.ruleType = js["type"] 27 | 28 | self.devices = [] 29 | for errorCategory in js["ruleErrorCategories"]: 30 | pass # at the moment this was always empty 31 | 32 | def set_label(self, label): 33 | """ sets the label of the rule """ 34 | return self._run_non_async(self.set_label_async, label) 35 | 36 | async def set_label_async(self, label): 37 | """ sets the label of the rule """ 38 | data = {"ruleId": self.id, "label": label} 39 | return await self._rest_call_async("rule/setRuleLabel", data) 40 | 41 | def __str__(self): 42 | return "{} {} active({})".format(self.ruleType, self.label, self.active) 43 | 44 | 45 | class SimpleRule(Rule): 46 | """ This class represents a "Simple" automation rule """ 47 | 48 | def enable(self): 49 | """ enables the rule """ 50 | return self.set_rule_enabled_state(True) 51 | 52 | async def enable_async(self): 53 | """ enables the rule """ 54 | return await self.set_rule_enabled_state(True) 55 | 56 | def disable(self): 57 | """ disables the rule """ 58 | return self.set_rule_enabled_state(False) 59 | 60 | async def disable_async(self): 61 | """ disables the rule """ 62 | return await self.set_rule_enabled_state(False) 63 | 64 | def set_rule_enabled_state(self, enabled): 65 | """ enables/disables this rule""" 66 | return self._run_non_async(self.set_rule_enabled_state_async, enabled) 67 | 68 | async def set_rule_enabled_state_async(self, enabled): 69 | """ enables/disables this rule""" 70 | data = {"ruleId": self.id, "enabled": enabled} 71 | return await self._rest_call_async("rule/enableSimpleRule", data) 72 | 73 | def from_json(self, js): 74 | super().from_json(js) 75 | # self.get_simple_rule() 76 | 77 | def get_simple_rule(self): 78 | return self._run_non_async(self.get_simple_rule_async) 79 | 80 | async def get_simple_rule_async(self): 81 | data = {"ruleId": self.id} 82 | result = await self._rest_call_async("rule/getSimpleRule", data) 83 | js = result.json 84 | for errorRuleTriggerItem in js["errorRuleTriggerItems"]: 85 | pass 86 | -------------------------------------------------------------------------------- /src/homematicip/securityEvent.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from datetime import datetime 3 | 4 | from homematicip.base.homematicip_object import HomeMaticIPObject 5 | 6 | 7 | class SecurityEvent(HomeMaticIPObject): 8 | """this class represents a security event """ 9 | 10 | def __init__(self, connection): 11 | super().__init__(connection) 12 | self.eventTimestamp = None 13 | self.eventType = None 14 | self.label = None 15 | 16 | def from_json(self, js): 17 | super().from_json(js) 18 | self.label = js["label"] 19 | time = js["eventTimestamp"] 20 | if time > 0: 21 | self.eventTimestamp = datetime.fromtimestamp(time / 1000.0) 22 | else: 23 | self.eventTimestamp = None 24 | self.eventType = js["eventType"] 25 | 26 | def __str__(self): 27 | return "{} {} {}".format( 28 | self.eventType, 29 | self.label, 30 | self.eventTimestamp.strftime("%Y.%m.%d %H:%M:%S"), 31 | ) 32 | 33 | 34 | class SecurityZoneEvent(SecurityEvent): 35 | """ This class will be used by other events which are just adding "securityZoneValues" """ 36 | 37 | def __init__(self, connection): 38 | super().__init__(connection) 39 | self.external_zone = None 40 | self.internal_zone = None 41 | 42 | def from_json(self, js): 43 | super().from_json(js) 44 | self.external_zone = js["securityZoneValues"]["EXTERNAL"] 45 | self.internal_zone = js["securityZoneValues"]["INTERNAL"] 46 | 47 | def __str__(self): 48 | return "{} external_zone({}) internal_zone({}) ".format( 49 | super().__str__(), self.external_zone, self.internal_zone 50 | ) 51 | 52 | 53 | class SensorEvent(SecurityEvent): 54 | pass 55 | 56 | 57 | class AccessPointDisconnectedEvent(SecurityEvent): 58 | pass 59 | 60 | 61 | class AccessPointConnectedEvent(SecurityEvent): 62 | pass 63 | 64 | 65 | class ActivationChangedEvent(SecurityZoneEvent): 66 | pass 67 | 68 | 69 | class SilenceChangedEvent(SecurityZoneEvent): 70 | pass 71 | 72 | 73 | class SabotageEvent(SecurityEvent): 74 | pass 75 | 76 | 77 | class MoistureDetectionEvent(SecurityEvent): 78 | pass 79 | 80 | 81 | class SmokeAlarmEvent(SecurityEvent): 82 | pass 83 | 84 | 85 | class ExternalTriggeredEvent(SecurityEvent): 86 | pass 87 | 88 | 89 | class OfflineAlarmEvent(SecurityEvent): 90 | pass 91 | 92 | 93 | class WaterDetectionEvent(SecurityEvent): 94 | pass 95 | 96 | 97 | class MainsFailureEvent(SecurityEvent): 98 | pass 99 | 100 | 101 | class OfflineWaterDetectionEvent(SecurityEvent): 102 | pass 103 | -------------------------------------------------------------------------------- /src/homematicip/weather.py: -------------------------------------------------------------------------------- 1 | from homematicip.base.enums import WeatherCondition, WeatherDayTime 2 | from homematicip.base.homematicip_object import HomeMaticIPObject 3 | 4 | 5 | class Weather(HomeMaticIPObject): 6 | """this class represents the weather of the home location""" 7 | 8 | def __init__(self, connection): 9 | super().__init__(connection) 10 | #:float: the current temperature 11 | self.temperature = 0.0 12 | #:WeatherCondition: the current weather 13 | self.weatherCondition = WeatherCondition.UNKNOWN 14 | #:datetime: the current datime 15 | self.weatherDayTime = WeatherDayTime.DAY 16 | #:float: the minimum temperature of the day 17 | self.minTemperature = 0.0 18 | #:float: the maximum temperature of the day 19 | self.maxTemperature = 0.0 20 | #:float: the current humidity 21 | self.humidity = 0 22 | #:float: the current windspeed 23 | self.windSpeed = 0.0 24 | #:int: the current wind direction in 360° where 0° is north 25 | self.windDirection = 0 26 | #:float: the current vapor 27 | self.vaporAmount = 0.0 28 | 29 | def from_json(self, js): 30 | super().from_json(js) 31 | self.temperature = js["temperature"] 32 | self.weatherCondition = WeatherCondition.from_str(js["weatherCondition"]) 33 | self.weatherDayTime = WeatherDayTime.from_str(js["weatherDayTime"]) 34 | self.minTemperature = js["minTemperature"] 35 | self.maxTemperature = js["maxTemperature"] 36 | self.humidity = js["humidity"] 37 | self.windSpeed = js["windSpeed"] 38 | self.windDirection = js["windDirection"] 39 | self.vaporAmount = js["vaporAmount"] 40 | 41 | def __str__(self): 42 | return "temperature({}) weatherCondition({}) weatherDayTime({}) minTemperature({}) maxTemperature({}) humidity({}) vaporAmount({}) windSpeed({}) windDirection({})".format( 43 | self.temperature, 44 | self.weatherCondition, 45 | self.weatherDayTime, 46 | self.minTemperature, 47 | self.maxTemperature, 48 | self.humidity, 49 | self.vaporAmount, 50 | self.windSpeed, 51 | self.windDirection, 52 | ) 53 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import pytest 3 | from homematicip import __version__ 4 | 5 | print("HMIP Version ", __version__) 6 | pytest.main(["tests", "-vv"]) 7 | -------------------------------------------------------------------------------- /test_aio.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | import pytest 3 | from homematicip import __version__ 4 | 5 | print("HMIP Version ", __version__) 6 | pytest.main(["-vv", "tests/aio_tests/"]) 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import ssl 4 | import sys 5 | import time 6 | from datetime import datetime, timedelta, timezone 7 | from threading import Thread 8 | 9 | from homematicip.connection.connection_context import ConnectionContext, ConnectionContextBuilder 10 | 11 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 12 | 13 | import pytest 14 | from aiohttp import web 15 | from aiohttp.test_utils import TestServer 16 | 17 | from homematicip.async_home import AsyncHome 18 | from homematicip.home import Home 19 | from homematicip_demo.fake_cloud_server import AsyncFakeCloudServer 20 | from homematicip_demo.helper import * 21 | 22 | 23 | # content of conftest.py 24 | def pytest_configure(config): 25 | import sys 26 | 27 | sys._called_from_test = True 28 | 29 | 30 | def pytest_unconfigure(config): # pragma: no cover 31 | import sys # This was missing from the manual 32 | 33 | del sys._called_from_test 34 | 35 | 36 | @pytest.fixture 37 | def ssl_ctx(): 38 | ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 39 | ssl_ctx.load_cert_chain(get_full_path("server.pem"), get_full_path("server.key")) 40 | return ssl_ctx 41 | 42 | 43 | @pytest.fixture 44 | def ssl_ctx_client(): 45 | return str(get_full_path("client.pem")) 46 | 47 | 48 | def start_background_loop(stop_threads, loop: asyncio.AbstractEventLoop) -> None: 49 | async def wait_for_close(stop_threads): 50 | while True: 51 | await asyncio.sleep(1) 52 | if stop_threads(): 53 | break 54 | 55 | asyncio.set_event_loop(loop) 56 | loop.run_until_complete(wait_for_close(stop_threads)) 57 | loop.run_until_complete(asyncio.sleep(0)) 58 | 59 | 60 | @pytest.fixture(scope="session") 61 | def new_event_loop(request): 62 | loop = asyncio.get_event_loop_policy().new_event_loop() 63 | yield loop 64 | loop.close() 65 | 66 | 67 | test_server = None 68 | 69 | 70 | @pytest.fixture(scope="session") 71 | async def session_stop_threads(): 72 | loop = asyncio.new_event_loop() 73 | stop_threads = False 74 | t = Thread( 75 | name="aio_fake_cloud", 76 | target=start_background_loop, 77 | args=(lambda: stop_threads, loop), 78 | ) 79 | t.daemon = True 80 | t.start() 81 | yield loop 82 | 83 | stop_threads = True 84 | t.join() 85 | while loop.is_running(): # pragma: no cover 86 | await asyncio.sleep(0.1) 87 | loop.close() 88 | 89 | 90 | @pytest.fixture 91 | async def fake_cloud(aiohttp_server, ssl_ctx, session_stop_threads): 92 | """Defines the testserver funcarg""" 93 | global test_server 94 | if test_server is None: 95 | aio_server = AsyncFakeCloudServer() 96 | app = web.Application() 97 | app.router.add_route("GET", "/{tail:.*}", aio_server) 98 | app.router.add_route("POST", "/{tail:.*}", aio_server) 99 | 100 | test_server = TestServer(app) 101 | asyncio.run_coroutine_threadsafe( 102 | test_server.start_server(loop=session_stop_threads, ssl=ssl_ctx), 103 | session_stop_threads, 104 | ).result() 105 | aio_server.url = str(test_server._root) 106 | test_server.url = aio_server.url 107 | test_server.aio_server = aio_server 108 | test_server.aio_server.reset() 109 | return test_server 110 | 111 | 112 | @pytest.fixture 113 | def fake_connection_context_with_ssl(fake_cloud, ssl_ctx_client): 114 | access_point_id = "3014F711A000000BAD0C0DED" 115 | auth_token = "8A45BAA53BE37E3FCA58E9976EFA4C497DAFE55DB997DB9FD685236E5E63ED7DE" 116 | lookup_url = f"{fake_cloud.url}/getHost" 117 | 118 | return ConnectionContextBuilder.build_context(accesspoint_id=access_point_id, lookup_url=lookup_url, 119 | auth_token=auth_token, ssl_ctx=ssl_ctx_client) 120 | 121 | 122 | @pytest.fixture 123 | def fake_home(fake_cloud, fake_connection_context_with_ssl): 124 | home = Home() 125 | with no_ssl_verification(): 126 | # home.download_configuration = fake_home_download_configuration 127 | home._fake_cloud = fake_cloud 128 | home.init_with_context(fake_connection_context_with_ssl, use_rate_limiting=False) 129 | home.get_current_state() 130 | return home 131 | 132 | @pytest.fixture 133 | async def fake_home_async(fake_cloud, fake_connection_context_with_ssl): 134 | home = Home() 135 | with no_ssl_verification(): 136 | # home.download_configuration = fake_home_download_configuration 137 | home._fake_cloud = fake_cloud 138 | home.init_with_context(fake_connection_context_with_ssl, use_rate_limiting=False) 139 | await home.get_current_state_async() 140 | return home 141 | 142 | 143 | @pytest.fixture 144 | async def no_ssl_fake_async_home(fake_cloud, fake_connection_context_with_ssl, new_event_loop): 145 | home = AsyncHome(new_event_loop) 146 | 147 | lookup_url = f"{fake_cloud.url}/getHost" 148 | home._fake_cloud = fake_cloud 149 | home.init_with_context(fake_connection_context_with_ssl, use_rate_limiting=False) 150 | await home.get_current_state_async() 151 | 152 | yield home 153 | 154 | 155 | # 156 | # @pytest.fixture 157 | # async def no_ssl_fake_async_auth(event_loop): 158 | # auth = Auth() 159 | # auth._connection._websession.post = partial( 160 | # auth._connection._websession.post, ssl=False 161 | # ) 162 | # yield auth 163 | # 164 | # await auth._connection._websession.close() 165 | 166 | 167 | dt = datetime.now(timezone.utc).astimezone() 168 | utc_offset = dt.utcoffset() // timedelta(seconds=1) 169 | # the timestamp of the tests were written during DST so utc_offset is one hour less outside of DST 170 | # -> adding one hour extra 171 | if not time.localtime().tm_isdst: 172 | utc_offset = utc_offset + 3600 # pragma: no cover 173 | -------------------------------------------------------------------------------- /tests/connection/test_buckets.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from homematicip.connection.buckets import Buckets 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_get_bucket(): 10 | """Testing the get bucket method.""" 11 | bucket = Buckets(2, 10) 12 | 13 | got_1st_token = await bucket.take() 14 | got_2nd_token = await bucket.take() 15 | got_3rd_token = await bucket.take() 16 | 17 | assert got_1st_token is True 18 | assert got_2nd_token is True 19 | assert got_3rd_token is False 20 | 21 | 22 | async def test_get_bucket_with_timeout(): 23 | """Testing the get bucket method with timeout.""" 24 | bucket = Buckets(1, 100) 25 | 26 | got_1st_token = await bucket.take() 27 | with pytest.raises(asyncio.TimeoutError): 28 | await bucket.wait_and_take(timeout=1) 29 | 30 | 31 | async def test_get_bucket_and_wait_for_new(): 32 | """Testing the get bucket method and waiting for new tokens.""" 33 | bucket = Buckets(1, 1) 34 | 35 | got_1st_token = await bucket.take() 36 | got_2nd_token = await bucket.wait_and_take() 37 | 38 | assert got_1st_token is True 39 | assert got_2nd_token is True 40 | 41 | def test_initial_tokens(): 42 | """Testing the initial tokens of the bucket.""" 43 | bucket = Buckets(2, 10) 44 | assert bucket._tokens == 2 45 | assert bucket.capacity == 2 46 | assert bucket.fill_rate == 10 47 | -------------------------------------------------------------------------------- /tests/connection/test_client_characteristics_builder.py: -------------------------------------------------------------------------------- 1 | from homematicip.connection.client_characteristics_builder import (ClientCharacteristicsBuilder) 2 | 3 | 4 | def test_client_characteristics_builder(): 5 | access_point_id = "3014F711A000000000000355" 6 | cc = ClientCharacteristicsBuilder.get(access_point_id) 7 | assert cc['id'] == access_point_id 8 | -------------------------------------------------------------------------------- /tests/connection/test_connection_context.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock 2 | 3 | import httpx 4 | import pytest 5 | 6 | from homematicip.connection.connection_context import ConnectionContext, ConnectionContextBuilder 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_build_context_with_client_session(mocker): 11 | response = mocker.Mock(spec=httpx.Response) 12 | response.status_code = 200 13 | response.json.return_value = {"urlREST": "https://example.com/rest", "urlWebSocket": "wss://example.com/ws"} 14 | 15 | lookup_url = "https://example.com/lookup" 16 | mock_client = AsyncMock(spec=httpx.AsyncClient) 17 | mock_client.post.return_value = response 18 | 19 | context = await ConnectionContextBuilder.build_context_async( 20 | "access_point_id", 21 | lookup_url, 22 | httpx_client_session=mock_client 23 | ) 24 | 25 | mock_client.post.assert_called_once() 26 | assert context.rest_url == "https://example.com/rest" 27 | assert context.websocket_url == "wss://example.com/ws" 28 | assert context.accesspoint_id == "access_point_id" 29 | 30 | @pytest.mark.asyncio 31 | async def test_build_context_without_client_session(mocker): 32 | response = mocker.Mock(spec=httpx.Response) 33 | response.status_code = 200 34 | response.json.return_value = {"urlREST": "https://example.com/rest", "urlWebSocket": "wss://example.com/ws"} 35 | 36 | patched = mocker.patch("homematicip.connection.connection_context.httpx.AsyncClient.post") 37 | patched.return_value = response 38 | 39 | context = await ConnectionContextBuilder.build_context_async("access_point_id", "https://example.com/lookup") 40 | 41 | patched.assert_called_once() 42 | assert context.rest_url == "https://example.com/rest" 43 | assert context.websocket_url == "wss://example.com/ws" 44 | assert context.accesspoint_id == "access_point_id" -------------------------------------------------------------------------------- /tests/connection/test_connection_url_resolver.py: -------------------------------------------------------------------------------- 1 | from http.client import responses 2 | 3 | import pytest 4 | import httpx 5 | from unittest.mock import AsyncMock, patch 6 | from homematicip.connection.connection_url_resolver import ConnectionUrlResolver 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_lookup_urls_async_with_client_session(mocker): 11 | response = mocker.Mock(spec=httpx.Response) 12 | response.status_code = 200 13 | response.json.return_value = {"urlREST": "https://example.com/rest", "urlWebSocket": "wss://example.com/ws"} 14 | client_characteristics = { 15 | "clientCharacteristics": { 16 | "apiVersion": "10", 17 | "applicationIdentifier": "homematicip-python", 18 | "applicationVersion": "1.0", 19 | "deviceManufacturer": "none", 20 | "deviceType": "Computer", 21 | "language": "en_US", 22 | "osType": "Linux", 23 | "osVersion": "5.4.0-42-generic", 24 | }, 25 | "id": "access_point_id", 26 | } 27 | lookup_url = "https://example.com/lookup" 28 | mock_client = AsyncMock(spec=httpx.AsyncClient) 29 | mock_client.post.return_value = response 30 | 31 | rest_url, websocket_url = await ConnectionUrlResolver.lookup_urls_async( 32 | client_characteristics, 33 | lookup_url, 34 | httpx_client_session=mock_client 35 | ) 36 | 37 | mock_client.post.assert_called_once_with(lookup_url, json=client_characteristics) 38 | assert rest_url == "https://example.com/rest" 39 | assert websocket_url == "wss://example.com/ws" 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_lookup_urls_async_without_client_session(mocker): 44 | response = mocker.Mock(spec=httpx.Response) 45 | response.status_code = 200 46 | response.json.return_value = {"urlREST": "https://example.com/rest", "urlWebSocket": "wss://example.com/ws"} 47 | 48 | patched = mocker.patch("homematicip.connection.connection_url_resolver.httpx.AsyncClient.post") 49 | patched.return_value = response 50 | 51 | client_characteristics = { 52 | "clientCharacteristics": { 53 | "apiVersion": "10", 54 | "applicationIdentifier": "homematicip-python", 55 | "applicationVersion": "1.0", 56 | "deviceManufacturer": "none", 57 | "deviceType": "Computer", 58 | "language": "en_US", 59 | "osType": "Linux", 60 | "osVersion": "5.4.0-42-generic", 61 | }, 62 | "id": "access_point_id", 63 | } 64 | lookup_url = "https://example.com/lookup" 65 | 66 | rest_url, websocket_url = await ConnectionUrlResolver.lookup_urls_async( 67 | client_characteristics, 68 | lookup_url 69 | ) 70 | 71 | patched.assert_called_once_with(lookup_url, json=client_characteristics) 72 | assert rest_url == "https://example.com/rest" 73 | assert websocket_url == "wss://example.com/ws" 74 | -------------------------------------------------------------------------------- /tests/connection/test_rate_limited_rest_connection.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | from homematicip.connection.rate_limited_rest_connection import RateLimitedRestConnection 4 | from homematicip.connection.rest_connection import ConnectionContext 5 | 6 | 7 | async def test_send_single_request(mocker): 8 | response = mocker.Mock(spec=httpx.Response) 9 | response.status_code = 200 10 | patched = mocker.patch("homematicip.connection.rest_connection.httpx.AsyncClient.post") 11 | patched.return_value = response 12 | 13 | context = ConnectionContext(rest_url="http://asdf") 14 | conn = RateLimitedRestConnection(context, 1, 1) 15 | 16 | result = await conn.async_post("url", {"a": "b"}, {"c": "d"}) 17 | 18 | assert patched.called 19 | assert patched.call_args[0][0] == "http://asdf/hmip/url" 20 | assert patched.call_args[1] == {"json": {"a": "b"}, "headers": {"c": "d"}} 21 | assert result.status == 200 22 | 23 | 24 | async def test_send_and_wait_requests(mocker): 25 | response = mocker.Mock(spec=httpx.Response) 26 | response.status_code = 200 27 | patched = mocker.patch("homematicip.connection.rest_connection.httpx.AsyncClient.post") 28 | patched.return_value = response 29 | 30 | context = ConnectionContext(rest_url="http://asdf") 31 | conn = RateLimitedRestConnection(context, 1, 1) 32 | 33 | await conn.async_post("url", {"a": "b"}, {"c": "d"}) 34 | await conn.async_post("url", {"a": "b"}, {"c": "d"}) 35 | await conn.async_post("url", {"a": "b"}, {"c": "d"}) 36 | 37 | assert patched.call_count == 3 -------------------------------------------------------------------------------- /tests/connection/test_rest_connection.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock 2 | 3 | import httpx 4 | import pytest 5 | 6 | from homematicip.connection.rest_connection import RestResult, ConnectionContext, RestConnection 7 | 8 | 9 | def test_rest_result(): 10 | result = RestResult(status=200) 11 | assert result.status_text == "OK" 12 | 13 | result = RestResult(status=9999) 14 | assert result.status_text == "No status code" 15 | 16 | 17 | def test_conn_update_connection_context(mocker): 18 | patched = mocker.patch("homematicip.connection.rest_connection.RestConnection._get_header") 19 | patched.return_value = {"test_a": "a", "test_b": "b"} 20 | 21 | context = ConnectionContext() 22 | context2 = ConnectionContext(rest_url="asdf") 23 | conn = RestConnection(context) 24 | 25 | conn.update_connection_context(context2) 26 | 27 | assert patched.called 28 | assert conn._headers == {"test_a": "a", "test_b": "b"} 29 | assert conn._context == context2 30 | 31 | @pytest.mark.asyncio 32 | async def test_conn_async_post(mocker): 33 | response = mocker.Mock(spec=httpx.Response) 34 | response.status_code = 200 35 | patched = mocker.patch("homematicip.connection.rest_connection.httpx.AsyncClient.post") 36 | patched.return_value = response 37 | 38 | context = ConnectionContext(rest_url="http://asdf") 39 | conn = RestConnection(context) 40 | 41 | result = await conn.async_post("url", {"a": "b"}, {"c": "d"}) 42 | 43 | assert patched.called 44 | assert patched.call_args[0][0] == "http://asdf/hmip/url" 45 | assert patched.call_args[1] == {"json": {"a": "b"}, "headers": {"c": "d"}} 46 | assert result.status == 200 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_conn_async_post_throttle(mocker): 51 | response = mocker.Mock(spec=httpx.Response) 52 | response.status_code = 429 53 | patched = mocker.patch("homematicip.connection.rest_connection.httpx.AsyncClient.post") 54 | patched.return_value = response 55 | 56 | context = ConnectionContext(rest_url="http://asdf") 57 | conn = RestConnection(context) 58 | 59 | with pytest.raises(Exception): 60 | await conn.async_post("url", {"a": "b"}, {"c": "d"}) 61 | 62 | @pytest.mark.asyncio 63 | async def test_conn_async_post_with_httpx_client_session(mocker): 64 | response = mocker.Mock(spec=httpx.Response) 65 | response.status_code = 200 66 | 67 | mock_client = AsyncMock(spec=httpx.AsyncClient) 68 | mock_client.post.return_value = response 69 | 70 | context = ConnectionContext(rest_url="http://asdf") 71 | conn = RestConnection(context, httpx_client_session=mock_client) 72 | 73 | result = await conn.async_post("url", {"a": "b"}, {"c": "d"}) 74 | 75 | assert mock_client.post.called 76 | assert mock_client.post.call_args[0][0] == "http://asdf/hmip/url" 77 | assert mock_client.post.call_args[1] == {"json": {"a": "b"}, "headers": {"c": "d"}} 78 | assert result.status == 200 -------------------------------------------------------------------------------- /tests/fake_hmip_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import pathlib 4 | import socket 5 | import ssl 6 | 7 | import aiohttp 8 | from aiohttp import web 9 | from aiohttp.resolver import DefaultResolver 10 | from aiohttp.test_utils import unused_port 11 | 12 | from homematicip.connection import ATTR_CLIENT_AUTH, ATTR_AUTH_TOKEN 13 | from homematicip.connection.connection_context import ConnectionContext 14 | from homematicip.connection.rest_connection import RestConnection 15 | from homematicip.connection.websocket_handler import WebSocketHandler 16 | 17 | 18 | class FakeResolver: 19 | _LOCAL_HOST = {0: "127.0.0.1", socket.AF_INET: "127.0.0.1", socket.AF_INET6: "::1"} 20 | 21 | def __init__(self, fakes, *, loop): 22 | """fakes -- dns -> port dict""" 23 | self._fakes = fakes 24 | self._resolver = DefaultResolver(loop=loop) 25 | 26 | async def resolve(self, host, port=0, family=socket.AF_INET): 27 | fake_port = self._fakes.get(host) 28 | if fake_port is not None: 29 | return [ 30 | { 31 | "hostname": host, 32 | "host": self._LOCAL_HOST[family], 33 | "port": fake_port, 34 | "family": family, 35 | "proto": 0, 36 | "flags": socket.AI_NUMERICHOST, 37 | } 38 | ] 39 | else: 40 | return await self._resolver.resolve(host, port, family) 41 | 42 | 43 | class FakeServer: 44 | def __init__(self, loop, base_url=None, port=None): 45 | self.loop = loop 46 | self.app = web.Application(loop=loop) 47 | if base_url: 48 | self.base_url = base_url 49 | else: 50 | self.base_url = "http://127.0.0.1:8080" 51 | self.add_routes() 52 | 53 | def add_routes(self): 54 | self.app.router.add_get("/", self.websocket_handler) 55 | 56 | async def websocket_handler(self, request): 57 | self.ws = web.WebSocketResponse() 58 | await self.ws.prepare(request) 59 | async for msg in self.ws: 60 | await asyncio.sleep(2) 61 | 62 | return self.ws 63 | 64 | async def start_server(self): 65 | self.loop.create_task(self.app.startup()) 66 | 67 | 68 | class BaseFakeHmip: 69 | def __init__(self, *, loop, base_url, port=None): 70 | self.loop = loop 71 | self.app = web.Application(loop=loop) 72 | self.base_url = base_url 73 | self.port = port 74 | self.handler = None 75 | self.server = None 76 | here = pathlib.Path(__file__) 77 | ssl_cert = here.parent / "server.crt" 78 | ssl_key = here.parent / "server.key" 79 | self.ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 80 | self.ssl_context.load_cert_chain(str(ssl_cert), str(ssl_key)) 81 | self.add_routes() 82 | 83 | def add_routes(self): 84 | pass 85 | 86 | async def start(self): 87 | if self.port is None: 88 | self.port = unused_port() 89 | self.handler = self.app.make_handler() 90 | self.server = await self.loop.create_server( 91 | self.handler, "127.0.0.1", self.port, ssl=self.ssl_context 92 | ) 93 | # return the base url and port which need to be resolved/mocked. 94 | resolver = FakeResolver({self.base_url: self.port}, loop=self.loop) 95 | connector = aiohttp.TCPConnector( 96 | loop=self.loop, resolver=resolver, verify_ssl=False 97 | ) 98 | 99 | return connector 100 | 101 | async def stop(self): 102 | self.server.close() 103 | await self.server.wait_closed() 104 | await self.app.shutdown() 105 | await self.handler.shutdown() 106 | await self.app.cleanup() 107 | 108 | 109 | class FakeLookupHmip(BaseFakeHmip): 110 | host_response = {"urlREST": "abc", "urlWebSocket": "def"} 111 | 112 | def add_routes(self): 113 | self.app.router.add_routes([web.post("/getHost", self.get_host)]) 114 | 115 | async def get_host(self, request): 116 | return web.json_response(self.host_response) 117 | 118 | 119 | class FakeConnectionHmip(BaseFakeHmip): 120 | """Test various connection issues""" 121 | 122 | js_response = {"response": True} 123 | 124 | def add_routes(self): 125 | self.app.router.add_routes( 126 | [ 127 | web.post("/go_404", self.go_404), 128 | web.post("/go_200_no_json", self.go_200_no_json), 129 | web.post("/go_200_json", self.go_200_json), 130 | ] 131 | ) 132 | 133 | async def go_404(self, request): 134 | return web.Response(status=404) 135 | 136 | async def go_200_no_json(self, request): 137 | return web.Response(status=200) 138 | 139 | async def go_200_json(self, request): 140 | return web.json_response(self.js_response, status=200) 141 | 142 | 143 | class FakeWebsocketHmip(BaseFakeHmip): 144 | retry = 0 145 | 146 | def __init__(self, loop, base_url, port=None): 147 | super().__init__(loop=loop, base_url=base_url, port=port) 148 | self.connections = [] 149 | 150 | def add_routes(self): 151 | self.app.router.add_routes( 152 | [ 153 | web.get("/", self.websocket_handler), 154 | web.get("/nopong", self.no_pong_handler), 155 | web.get("/servershutdown", self.server_shutdown), 156 | web.get("/clientclose", self.client_shutdown), 157 | web.get("/recover", self.recover_handler), 158 | ] 159 | ) 160 | 161 | async def websocket_handler(self, request): 162 | ws = web.WebSocketResponse() 163 | self.connections.append(ws) 164 | await ws.prepare(request) 165 | ws.send_bytes(b"abc") 166 | await asyncio.sleep(2) 167 | print("websocket connection closed") 168 | 169 | return ws 170 | 171 | async def recover_handler(self, request): 172 | ws = web.WebSocketResponse() 173 | await ws.prepare(request) 174 | ws.send_bytes(b"abc") 175 | 176 | await asyncio.sleep(2) 177 | 178 | ws.send_bytes(b"resumed") 179 | 180 | async def no_pong_handler(self, request): 181 | ws = web.WebSocketResponse(autoping=False) 182 | self.connections.append(ws) 183 | await ws.prepare(request) 184 | await asyncio.sleep(20) 185 | return ws 186 | 187 | async def server_shutdown(self, request): 188 | ws = web.WebSocketResponse() 189 | self.connections.append(ws) 190 | await ws.prepare(request) 191 | self.loop.create_task(self.stop()) 192 | await asyncio.sleep(10) 193 | # await self.stop() 194 | return ws 195 | 196 | async def client_shutdown(self, request): 197 | ws = web.WebSocketResponse() 198 | self.connections.append(ws) 199 | await ws.prepare(request) 200 | await asyncio.sleep(10) 201 | 202 | async def stop(self): 203 | # for _ws in self.connections: 204 | # await _ws.close() 205 | await super().stop() 206 | 207 | 208 | async def main(loop): 209 | pass 210 | 211 | # TODO: Fix this 212 | # logging.basicConfig(level=logging.DEBUG) 213 | # fake_ws = FakeWebsocketHmip(loop=loop, base_url="ws.homematic.com") 214 | # connector = await fake_ws.start() 215 | # 216 | # incoming = {} 217 | # 218 | # def parser(*args, **kwargs): 219 | # incoming["test"] = None 220 | # 221 | # context = ConnectionContext(auth_token="auth_token", client_auth_token="client_auth", websocket_url="wss://ws.homematic.com/") 222 | # websocket = WebSocketHandler() 223 | # await websocket.listen(context) 224 | # 225 | # async with aiohttp.ClientSession(connector=connector, loop=loop) as session: 226 | # connection = RestConnection(loop, session) 227 | # 228 | # connection.headers[ATTR_AUTH_TOKEN] = "auth_token" 229 | # connection.headers[ATTR_CLIENT_AUTH] = "client_auth" 230 | # connection._urlWebSocket = "wss://ws.homematic.com/" 231 | # try: 232 | # ws_loop = await connection.ws_connect(parser) 233 | # await ws_loop 234 | # except Exception as err: 235 | # pass 236 | # print(incoming) 237 | # 238 | # await fake_ws.stop() 239 | 240 | 241 | if __name__ == "__main__": 242 | loop = asyncio.get_event_loop() 243 | loop.run_until_complete(main(loop)) 244 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import pytest 4 | 5 | from homematicip.auth import Auth 6 | from homematicip.connection.connection_context import ConnectionContext 7 | from homematicip.connection.rest_connection import RestConnection 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_async_auth_challenge_no_pin( 12 | fake_connection_context_with_ssl 13 | ): 14 | devicename = "auth_test" 15 | 16 | connection = RestConnection(fake_connection_context_with_ssl) 17 | 18 | auth = Auth(connection, fake_connection_context_with_ssl.client_auth_token, fake_connection_context_with_ssl.accesspoint_id) 19 | 20 | result = await auth.connection_request(devicename) 21 | assert result.status == 200 22 | 23 | assert (await auth.is_request_acknowledged()) is False 24 | assert (await auth.is_request_acknowledged()) is False 25 | 26 | await auth.connection.async_post("auth/simulateBlueButton") 27 | 28 | assert await auth.is_request_acknowledged() is True 29 | 30 | token = await auth.request_auth_token() 31 | assert token == hashlib.sha512(auth.client_id.encode("utf-8")).hexdigest().upper() 32 | 33 | result_id = await auth.confirm_auth_token(token) 34 | assert result_id == auth.client_id 35 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import platform 4 | from unittest.mock import patch, mock_open 5 | 6 | import homematicip 7 | 8 | 9 | def fake_windows(): 10 | return "Windows" 11 | 12 | 13 | def fake_linux(): 14 | return "Linux" 15 | 16 | 17 | def fake_mac(): 18 | return "Darwin" 19 | 20 | 21 | def fake_getenv(var): 22 | if var == "appdata": 23 | return "C:\\APPDATA" 24 | if var == "programdata": 25 | return "C:\\PROGRAMDATA" 26 | 27 | 28 | def test_find_and_load_config_file_success(): 29 | with io.open("./config.ini", mode="w") as f: 30 | f.write("[AUTH]\nauthtoken = TEMP_TOKEN\naccesspoint = TEMP_AP") 31 | config = homematicip.find_and_load_config_file() 32 | assert config.auth_token == "TEMP_TOKEN" 33 | assert config.access_point == "TEMP_AP" 34 | os.remove("./config.ini") 35 | 36 | 37 | def test_find_and_load_config_file_not_found(): 38 | with patch("builtins.open", mock_open(read_data="data")) as mock_file: 39 | mock_file.side_effect = FileNotFoundError 40 | assert homematicip.find_and_load_config_file() is None 41 | 42 | 43 | def test_get_config_file_locations_win(): 44 | platform.system = fake_windows 45 | os.getenv = fake_getenv 46 | locations = homematicip.get_config_file_locations() 47 | assert locations[0] == "./config.ini" 48 | assert ( 49 | locations[1].replace("/", "\\") 50 | == "C:\\APPDATA\\homematicip-rest-api\\config.ini" 51 | ) 52 | assert ( 53 | locations[2].replace("/", "\\") 54 | == "C:\\PROGRAMDATA\\homematicip-rest-api\\config.ini" 55 | ) 56 | 57 | 58 | def test_get_config_file_locations_linux(): 59 | platform.system = fake_linux 60 | locations = homematicip.get_config_file_locations() 61 | assert locations[0] == "./config.ini" 62 | assert locations[1] == "~/.homematicip-rest-api/config.ini" 63 | assert locations[2] == "/etc/homematicip-rest-api/config.ini" 64 | 65 | 66 | def test_get_config_file_locations_mac(): 67 | platform.system = fake_mac 68 | locations = homematicip.get_config_file_locations() 69 | assert locations[0] == "./config.ini" 70 | assert locations[1] == "~/Library/Preferences/homematicip-rest-api/config.ini" 71 | assert ( 72 | locations[2] == "/Library/Application Support/homematicip-rest-api/config.ini" 73 | ) 74 | -------------------------------------------------------------------------------- /tests/test_fake_cloud.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | 4 | import pytest 5 | import requests 6 | 7 | from conftest import no_ssl_verification 8 | from homematicip_demo.fake_cloud_server import AsyncFakeCloudServer 9 | 10 | 11 | def test_getHost(fake_cloud): 12 | with no_ssl_verification(): 13 | response = requests.post("{}/getHost".format(fake_cloud.url)) 14 | js = json.loads(response.text) 15 | assert js["urlREST"] == fake_cloud.url 16 | assert js["apiVersion"] == "12" 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_calling_internal_func(): 21 | with pytest.raises(NameError): 22 | await AsyncFakeCloudServer().call_method("__init__") 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_calling_invalid_func(): 27 | with pytest.raises(NameError): 28 | await AsyncFakeCloudServer().call_method("get_this_function_does_not_exist") 29 | 30 | 31 | def test_invlid_authorization(fake_cloud): 32 | with no_ssl_verification(): 33 | response = requests.post("{}/hmip/home/getCurrentState".format(fake_cloud.url)) 34 | js = json.loads(response.text) 35 | assert js["errorCode"] == "INVALID_AUTHORIZATION" 36 | assert response.status_code == 403 37 | 38 | 39 | def test_invalid_url(fake_cloud): 40 | with no_ssl_verification(): 41 | response = requests.post("{}/hmip/invalid/path".format(fake_cloud.url)) 42 | js = json.loads(response.text) 43 | assert js["errorCode"] == "Can't find method post_hmip_invalid_path" 44 | assert response.status_code == 404 45 | -------------------------------------------------------------------------------- /tests/test_hmip_cli.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | import pytest 5 | 6 | from homematicip.async_home import AsyncHome 7 | from homematicip.base.enums import CliActions, DoorState 8 | from homematicip.base.helpers import anonymizeConfig, handle_config 9 | from homematicip.cli.hmip_cli import ( 10 | _channel_supports_action, 11 | _execute_action_for_device, 12 | _get_target_channel_indices, 13 | _get_target_channels, 14 | get_rssi_bar_string, 15 | ) 16 | from homematicip.home import Home 17 | from homematicip_demo.helper import no_ssl_verification 18 | 19 | logger = logging.getLogger("test_hmip_cli") 20 | 21 | 22 | def test_getRssiBarString(): 23 | assert get_rssi_bar_string(-50) == "[**********]" 24 | assert get_rssi_bar_string(-55) == "[*********_]" 25 | assert get_rssi_bar_string(-60) == "[********__]" 26 | assert get_rssi_bar_string(-65) == "[*******___]" 27 | assert get_rssi_bar_string(-70) == "[******____]" 28 | assert get_rssi_bar_string(-75) == "[*****_____]" 29 | assert get_rssi_bar_string(-80) == "[****______]" 30 | assert get_rssi_bar_string(-85) == "[***_______]" 31 | assert get_rssi_bar_string(-90) == "[**________]" 32 | assert get_rssi_bar_string(-95) == "[*_________]" 33 | assert get_rssi_bar_string(-100) == "[__________]" 34 | 35 | 36 | def test_handle_config_error(): 37 | assert handle_config({"errorCode": "Dummy"}, False) is None 38 | 39 | 40 | def test_anonymizeConfig(): 41 | c = ( 42 | '{"id":"d0fea2b1-ef3b-44b1-ae96-f9b31f75de84",' 43 | '"id2":"d0fea2b1-ef3b-44b1-ae96-f9b31f75de84",' 44 | '"inboxGroup":"2dc54a8d-ceee-4626-8f27-b24e78dc05de",' 45 | '"availableFirmwareVersion": "0.0.0",' 46 | '"sgtin":"3014F71112345AB891234561", "sgtin_silvercrest" : "301503771234567891234567",' 47 | '"location":' 48 | '{"city": "Vatican City, Vatican","latitude":"41.9026011","longitude":"12.4533701"}}' 49 | ) 50 | c = handle_config(json.loads(c), True) 51 | 52 | js = json.loads(c) 53 | 54 | assert js["id"] == "00000000-0000-0000-0000-000000000000" 55 | assert js["id"] == js["id2"] 56 | assert js["inboxGroup"] == "00000000-0000-0000-0000-000000000001" 57 | assert js["sgtin"] == "3014F7110000000000000000" 58 | assert js["sgtin_silvercrest"] == "3014F7110000000000000001" 59 | assert js["availableFirmwareVersion"] == "0.0.0" 60 | 61 | location = js["location"] 62 | assert location["city"] == "1010, Vienna, Austria" 63 | assert location["latitude"] == "48.208088" 64 | assert location["longitude"] == "16.358608" 65 | 66 | c = '{"id":"test"}' 67 | c = anonymizeConfig(c, "original", "REPLACED") 68 | assert c == '{"id":"test"}' 69 | 70 | 71 | def test_get_target_channel_indices(fake_home: Home): 72 | d = fake_home.search_device_by_id("3014F71100000000000DRBL4") 73 | 74 | assert _get_target_channel_indices(d, [1]) == [1] 75 | assert _get_target_channel_indices(d, None) == [1] 76 | assert _get_target_channel_indices(d, [1, 2, 3]) == [1, 2, 3] 77 | 78 | 79 | def test_get_target_channels(fake_home: Home): 80 | d = fake_home.search_device_by_id("3014F71100000000000DRBL4") 81 | 82 | result = _get_target_channels(d, None) 83 | assert len(result) == 1 84 | assert result[0].index == 1 85 | 86 | result = _get_target_channels(d, [1, 3]) 87 | assert len(result) == 2 88 | assert result[0].index == 1 89 | assert result[1].index == 3 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_execute_action_for_device_shutter_level(fake_home_async: AsyncHome): 94 | class Args: 95 | def __init__(self) -> None: 96 | self.channels: list = [1, 2, 3] 97 | 98 | args = Args() 99 | d = fake_home_async.search_device_by_id("3014F71100000000000DRBL4") 100 | 101 | with no_ssl_verification(): 102 | await _execute_action_for_device( 103 | d, args, CliActions.SET_SHUTTER_LEVEL, "async_set_shutter_level", 0.5 104 | ) 105 | await fake_home_async.get_current_state_async() 106 | d = fake_home_async.search_device_by_id("3014F71100000000000DRBL4") 107 | assert d.functionalChannels[1].shutterLevel == 0.5 108 | assert d.functionalChannels[2].shutterLevel == 0.5 109 | assert d.functionalChannels[3].shutterLevel == 0.5 110 | 111 | 112 | @pytest.mark.asyncio 113 | async def test_execute_action_for_device_slats_level(fake_home_async: AsyncHome): 114 | class Args: 115 | def __init__(self) -> None: 116 | self.channels: list = [1, 2, 3] 117 | 118 | args = Args() 119 | d = fake_home_async.search_device_by_id("3014F71100000000000DRBL4") 120 | 121 | with no_ssl_verification(): 122 | await _execute_action_for_device( 123 | d, args, CliActions.SET_SLATS_LEVEL, "async_set_slats_level", 0.5 124 | ) 125 | await fake_home_async.get_current_state_async() 126 | d = fake_home_async.search_device_by_id("3014F71100000000000DRBL4") 127 | assert d.functionalChannels[1].slatsLevel == 0.5 128 | assert d.functionalChannels[2].slatsLevel == 0.5 129 | assert d.functionalChannels[3].slatsLevel == 0.5 130 | 131 | 132 | @pytest.mark.asyncio 133 | async def test_execute_action_for_device_send_door_command(fake_home_async: AsyncHome): 134 | class Args: 135 | def __init__(self) -> None: 136 | self.channels = None 137 | 138 | args = Args() 139 | d = fake_home_async.search_device_by_id("3014F0000000000000FAF9B4") 140 | 141 | with no_ssl_verification(): 142 | await _execute_action_for_device( 143 | d, args, CliActions.SEND_DOOR_COMMAND, "async_send_door_command", "OPEN" 144 | ) 145 | await fake_home_async.get_current_state_async() 146 | d = fake_home_async.search_device_by_id("3014F0000000000000FAF9B4") 147 | assert d.functionalChannels[1].doorState == DoorState.OPEN 148 | 149 | 150 | def test_channel_supports_action(fake_home: Home): 151 | d = fake_home.search_device_by_id("3014F71100000000000DRBL4") 152 | assert False is _channel_supports_action( 153 | d.functionalChannels[1], CliActions.SET_DIM_LEVEL 154 | ) 155 | assert True is _channel_supports_action( 156 | d.functionalChannels[1], CliActions.SET_SHUTTER_LEVEL 157 | ) 158 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from homematicip.base.enums import * 4 | from homematicip.base.helpers import bytes2str, detect_encoding 5 | from homematicip.base.homematicip_object import HomeMaticIPObject 6 | from homematicip.EventHook import EventHook 7 | 8 | 9 | def event_hook_handler2(mustBe2): 10 | assert mustBe2 == 2 11 | 12 | 13 | def event_hook_handler3(mustBe3): 14 | assert mustBe3 == 3 15 | 16 | 17 | def test_event_hook(): 18 | eh = EventHook() 19 | eh += event_hook_handler2 20 | eh.fire(2) 21 | eh += event_hook_handler3 22 | eh -= event_hook_handler2 23 | eh.fire(3) 24 | 25 | 26 | def test_detect_encoding(): 27 | testString = "This is my special string to test the encoding" 28 | assert detect_encoding(testString.encode("utf-8")) == "utf-8" 29 | assert detect_encoding(testString.encode("utf-8-sig")) == "utf-8-sig" 30 | assert detect_encoding(testString.encode("utf-16")) == "utf-16" 31 | assert detect_encoding(testString.encode("utf-32")) == "utf-32" 32 | assert detect_encoding(testString.encode("utf-16-be")) == "utf-16-be" 33 | assert detect_encoding(testString.encode("utf-32-be")) == "utf-32-be" 34 | assert detect_encoding(testString.encode("utf-16-le")) == "utf-16-le" 35 | assert detect_encoding(testString.encode("utf-32-le")) == "utf-32-le" 36 | 37 | 38 | def test_bytes2str(): 39 | testString = "This is my special string to test the encoding" 40 | assert bytes2str(testString.encode("utf-8")) == testString 41 | assert bytes2str(testString.encode("utf-8-sig")) == testString 42 | assert bytes2str(testString.encode("utf-16")) == testString 43 | assert bytes2str(testString.encode("utf-32")) == testString 44 | assert bytes2str(testString.encode("utf-16-be")) == testString 45 | assert bytes2str(testString.encode("utf-32-be")) == testString 46 | assert bytes2str(testString.encode("utf-16-le")) == testString 47 | assert bytes2str(testString.encode("utf-32-le")) == testString 48 | assert bytes2str(testString) == testString 49 | with pytest.raises(TypeError): 50 | assert bytes2str(44) == testString 51 | 52 | 53 | def test_auto_name_enum(): 54 | assert DeviceType.from_str("PUSH_BUTTON") == DeviceType.PUSH_BUTTON 55 | assert DeviceType.from_str(None) is None 56 | assert DeviceType.from_str("I_DONT_EXIST", DeviceType.DEVICE) == DeviceType.DEVICE 57 | assert DeviceType.from_str("I_DONT_EXIST_EITHER") is None 58 | -------------------------------------------------------------------------------- /tests/test_websocket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest.mock import AsyncMock, MagicMock, patch 3 | 4 | import aiohttp 5 | import pytest 6 | 7 | from homematicip.connection.websocket_handler import WebsocketHandler 8 | 9 | 10 | class DummyMsg: 11 | def __init__(self, data, type_): 12 | self.data = data 13 | self.type = type_ 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_add_on_message_handler(): 18 | client = WebsocketHandler() 19 | handler = MagicMock() 20 | client.add_on_message_handler(handler) 21 | assert handler in client._on_message_handlers 22 | 23 | 24 | @pytest.mark.asyncio 25 | async def test_is_connected_false_initial(): 26 | client = WebsocketHandler() 27 | assert not client.is_connected() 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_is_connected_true(monkeypatch): 32 | client = WebsocketHandler() 33 | ws_mock = MagicMock() 34 | ws_mock.closed = False 35 | client._ws = ws_mock 36 | assert client.is_connected() 37 | 38 | 39 | @pytest.mark.asyncio 40 | async def test_handle_task_result_logs_cancelled(caplog): 41 | client = WebsocketHandler() 42 | task = MagicMock() 43 | task.result.side_effect = asyncio.CancelledError() 44 | with caplog.at_level('INFO'): 45 | client._handle_task_result(task) 46 | assert any('cancelled' in m for m in caplog.text.splitlines()) 47 | 48 | 49 | @pytest.mark.asyncio 50 | async def test_handle_task_result_logs_exception(caplog): 51 | client = WebsocketHandler() 52 | task = MagicMock() 53 | task.result.side_effect = Exception('fail') 54 | with caplog.at_level('ERROR'): 55 | client._handle_task_result(task) 56 | assert any('Error in reconnect' in m for m in caplog.text.splitlines()) 57 | 58 | 59 | @pytest.mark.asyncio 60 | async def test_cleanup_closes_ws_and_session(monkeypatch): 61 | client = WebsocketHandler() 62 | ws_mock = AsyncMock() 63 | session_mock = AsyncMock() 64 | ws_mock.closed = False 65 | session_mock.closed = False 66 | client._ws = ws_mock 67 | client._session = session_mock 68 | await client._cleanup() 69 | ws_mock.close.assert_awaited() 70 | session_mock.close.assert_awaited() 71 | assert client._ws is None 72 | assert client._session is None 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_start_and_stop(monkeypatch): 77 | client = WebsocketHandler() 78 | context = MagicMock() 79 | monkeypatch.setattr(client, '_connect', AsyncMock()) 80 | await client.start(context) 81 | assert client._reconnect_task is not None 82 | await client.stop() 83 | assert client._reconnect_task is None 84 | 85 | 86 | @pytest.mark.asyncio 87 | async def test_listen_calls_handlers(monkeypatch): 88 | client = WebsocketHandler() 89 | handler = AsyncMock() 90 | client.add_on_message_handler(handler) 91 | ws_mock = MagicMock() 92 | ws_mock.__aiter__.return_value = [ 93 | DummyMsg('test', type_=aiohttp.WSMsgType.TEXT), 94 | DummyMsg('test2', type_=aiohttp.WSMsgType.BINARY), 95 | DummyMsg('err', type_=aiohttp.WSMsgType.ERROR) 96 | ] 97 | client._ws = ws_mock 98 | with patch('logging.Logger.debug'), patch('logging.Logger.error'): 99 | await client._listen() 100 | handler.assert_any_await('test') 101 | handler.assert_any_await('test2') 102 | --------------------------------------------------------------------------------