├── .github └── workflows │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── MANIFEST.in ├── README.md ├── changes.md ├── demo.eodm ├── demo2.eodm ├── docs ├── README.md ├── getting-started │ ├── eo_man_HA_button.png │ ├── eo_man_connect.png │ ├── eo_man_connect_usb300.png │ ├── eo_man_connected.png │ ├── eo_man_detect_ports.png │ ├── eo_man_device_data.png │ ├── eo_man_device_scan.png │ ├── eo_man_write_to_devices.png │ └── readme.md ├── installation.md ├── load-pct14-data │ ├── added-gateway-into-eo-man.png │ ├── export-pct14-data.png │ ├── load-pct14-export-into-eo-man.png │ ├── loaded-pct14-export.png │ └── readme.md └── supported-devices.md ├── eo_man.bat ├── eo_man ├── __init__.py ├── __main__.py ├── controller │ ├── __init__.py │ ├── app_bus.py │ ├── gateway_registry.py │ ├── lan_service_detector.py │ ├── network_gateway_detector.py │ ├── serial_controller.py │ └── serial_port_detector.py ├── data │ ├── __init__.py │ ├── app_info.py │ ├── application_data.py │ ├── const.py │ ├── data_helper.py │ ├── data_manager.py │ ├── device.py │ ├── filter.py │ ├── ha_config_generator.py │ ├── homeassistant │ │ ├── __init__.py │ │ └── const.py │ ├── message_history.py │ ├── pct14_data_manager.py │ └── recorded_message.py ├── icons │ ├── Breathe-about.png │ ├── EUL_device.png │ ├── Faenza-system-search.bmp │ ├── Faenza-system-search.ico │ ├── Faenza-system-search.png │ ├── Gnome-help-faq.png │ ├── Home_Assistant_Logo.png │ ├── Oxygen480-actions-document-save-as.png │ ├── Oxygen480-actions-document-save.png │ ├── Oxygen480-actions-edit-clear.png │ ├── Oxygen480-actions-help.png │ ├── Oxygen480-status-folder-open.png │ ├── Software-update-available.png │ ├── __init__.py │ ├── blank.png │ ├── export_icon.png │ ├── fam-usb.png │ ├── fam-usb2.png │ ├── fam14.jpg │ ├── fam14.png │ ├── fgw14-usb.png │ ├── ftd14.png │ ├── github_icon.png │ ├── image_gallary.py │ ├── mail-forward.png │ ├── mail-send-receive.png │ ├── mgw_piotek.png │ ├── paypal_icon.png │ ├── paypal_me_badge.png │ ├── pct14.png │ ├── usb300.png │ ├── wireless-network-bw.png │ ├── wireless-network-colored.png │ └── wireless.png └── view │ ├── __init__.py │ ├── about_window.py │ ├── checklistcombobox.py │ ├── device_details.py │ ├── device_info_window.py │ ├── device_table.py │ ├── donation_button.py │ ├── eep_checker_window.py │ ├── filter_bar.py │ ├── log_output.py │ ├── main_panel.py │ ├── menu_presenter.py │ ├── message_window.py │ ├── send_message_window.py │ ├── serial_communication_bar.py │ ├── status_bar.py │ └── tool_bar.py ├── ha_config.yaml ├── install_from_official_releases.bat ├── install_from_officials_on_ubuntu.sh ├── install_from_repo.bat ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── screenshot.png ├── screenshot2.png └── tests ├── __init__.py ├── mocks.py ├── resources ├── 20240925_PCT14_export_test.xml ├── 20240925_PCT14_export_test_GENERATED.xml ├── 20240925_PCT14_export_test_extended.xml └── test_app_config_1.eodm ├── test_app_config.py ├── test_base_id_utils.py ├── test_detecting_usb.py ├── test_generate_docs.py └── test_pct14_import.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | pypi-publish: 12 | name: upload release to PyPI 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.x' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install pytest build setuptools wheel poetry 26 | pip install -r requirements.txt 27 | poetry lock 28 | 29 | - name: Run tests 30 | run: pytest tests 31 | 32 | - name: Build package 33 | run: poetry build 34 | 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | username: __token__ 39 | password: ${{ secrets.PYPI_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # logging 2 | *.log 3 | *.log.* 4 | 5 | # tests 6 | **test_app_config_1_temp* 7 | 8 | # dependencies 9 | src 10 | 11 | installed_official_eo_man 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | poetry.lock 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | share/python-wheels/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | *.py,cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | cover/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # poetry 111 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 112 | # This is especially recommended for binary packages to ensure reproducibility, and is more 113 | # commonly ignored for libraries. 114 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 115 | #poetry.lock 116 | 117 | # pdm 118 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 119 | #pdm.lock 120 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 121 | # in version control. 122 | # https://pdm.fming.dev/#use-with-ide 123 | .pdm.toml 124 | 125 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 126 | __pypackages__/ 127 | 128 | # Celery stuff 129 | celerybeat-schedule 130 | celerybeat.pid 131 | 132 | # SageMath parsed files 133 | *.sage.py 134 | 135 | # Environments 136 | .env 137 | .venv* 138 | .VSCodeCounter* 139 | env/ 140 | venv/ 141 | ENV/ 142 | env.bak/ 143 | venv.bak/ 144 | 145 | # Spyder project settings 146 | .spyderproject 147 | .spyproject 148 | 149 | # Rope project settings 150 | .ropeproject 151 | 152 | # mkdocs documentation 153 | /site 154 | 155 | # mypy 156 | .mypy_cache/ 157 | .dmypy.json 158 | dmypy.json 159 | 160 | # Pyre type checker 161 | .pyre/ 162 | 163 | # pytype static type analyzer 164 | .pytype/ 165 | 166 | # Cython debug symbols 167 | cython_debug/ 168 | 169 | # PyCharm 170 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 171 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 172 | # and can be added to the global gitignore or merged into this file. For a more nuclear 173 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 174 | #.idea/ -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: tests 5 | name: run tests 6 | #entry: python -m unittest discover 7 | entry: pytest tests 8 | language: python 9 | types: [python] 10 | stages: [commit] -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Main Window with Demo Data", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/eo_man", 12 | "console": "integratedTerminal", 13 | "args": ["-v", "--app_config", "${workspaceFolder}/demo2.eodm"], 14 | "justMyCode": false 15 | }, 16 | { 17 | "name": "Python: Main Window with PCT14 Data", 18 | "type": "debugpy", 19 | "request": "launch", 20 | "program": "${workspaceFolder}/eo_man", 21 | "console": "integratedTerminal", 22 | "args": ["-v", "--pct14_export", "${workspaceFolder}/tests/resources/20240925_PCT14_export_test.xml"], 23 | "justMyCode": false 24 | }, 25 | { 26 | "name": "Python: Main Window", 27 | "type": "debugpy", 28 | "request": "launch", 29 | "program": "${workspaceFolder}/eo_man", 30 | "args": ["-v"], 31 | "console": "integratedTerminal", 32 | "justMyCode": false 33 | }, 34 | { 35 | "name": "Python: CLI Help", 36 | "type": "debugpy", 37 | "request": "launch", 38 | "program": "${workspaceFolder}/eo_man", 39 | "console": "integratedTerminal", 40 | "args": ["-h"], 41 | "justMyCode": false 42 | }, 43 | { 44 | "name": "Python: CLI load demo and store ha config", 45 | "type": "debugpy", 46 | "request": "launch", 47 | "program": "${workspaceFolder}/eo_man", 48 | "console": "integratedTerminal", 49 | "args": ["-v", "--app_config", "${workspaceFolder}/demo.eodm", "--ha_config", "${workspaceFolder}/ha_config.yaml"], 50 | "justMyCode": true 51 | }, 52 | { 53 | "name": "Python: Debug Tests", 54 | "type": "debugpy", 55 | "request": "launch", 56 | "program": "${file}", 57 | "purpose": ["debug-test"], 58 | "console": "integratedTerminal", 59 | "justMyCode": false 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | "python.analysis.extraPaths": [ 8 | "./eo_man"//, "./eo_man/data" 9 | ] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Philipp Grimm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENS 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Generic badge](https://img.shields.io/github/commit-activity/y/grimmpp/home-assistant-eltako.svg?style=flat&color=3498db)](https://github.com/grimmpp/home-assistant-eltako/commits/main) 2 | [![Generic badge](https://img.shields.io/badge/Community-Forum-3498db.svg)](https://community.home-assistant.io/) 3 | [![Generic badge](https://img.shields.io/badge/Community_Forum-Eltako_Integration_Debugging-3498db.svg)](https://community.home-assistant.io/t/eltako-baureihe-14-rs485-enocean-debugging/49712) 4 | [![Generic badge](https://img.shields.io/badge/License-MIT-3498db.svg)](/LICENSE) 5 | 6 | # Enocean Device Manager and Home Assistant Configuration Exporter 7 | 8 | # WORK IN PROGRESS BUT ALREAD USABLE!!! 9 | 10 | This client application allows you to **inventory all EnOcean devices**. It can **automatically read and detect devices** from the RS485 bus or from wireless network. After the devices are listed in the EnOcean Device Manager **you can enricht device information** like changing the name, comment or adapt parameters like timeframes, thresholds, units, ... . 11 | Furthermore, it automatically can detect default settings for Home Assistant configuration which can be adjust as well and it allows you to **generate and export the configuration for Home Assistant**. 12 | (The exported Home Assistant configuration is intended for the [Eltako Home Assistant Integration](https://github.com/grimmpp/home-assistant-eltako/)) 13 | 14 | ## Preview 15 | 16 | What you can see here can automatically detected by reading the memory of the bus devices via FAM14. Telegrams of sensors and decentralized devices will be received and additionally added. 17 | Additional info for Home Assistant is automatically added. The configuration for Home Assistant can be generated by the detected information. 18 | For further steps it is planned to extend the support for changing the data which was collected so that a proper management of the devices can be supported. 19 | 20 | ## System Requirements / Where to install and how to use it? 21 | This tool is a desktop application (not browser based) and it runs independent of Home Assistant. Install it directly on a Windows, Linux or Max. (So far only Windows has been tested but all three operating systems should be supported.) Your PC requires Python pre-installed and you should be able to connect it to your EnOcean devices, either via USB cable (Eltako FAM14, FGW14-USB, ...) or wireless transceiver (Eltako FAM-USB). Support for the wireless transceiver 'EnOcean USB300' is planned for future releases. 22 | 23 | For the moment I recommend a laptop with Windows and Python installed. You should be able bring close to FAM14 in order to connect it. (Connection to FAM14 is only required for a short moment during the device scan process.) 24 | 25 | ## Install python package in virtual environment (Recommended) 26 | 1. Create virtual python environment: `python.exe -m venv .\.venv` 27 | 2. Install application: `.\.venv\Scripts\pip.exe install eo_man --force-reinstall` (Package available under pypi: [eo_man](https://pypi.org/project/eo-man/)) 28 | 3. Run application: `.\.venv\Scripts\python.exe -m eo_man` 29 | 30 | ## Install python package in gloabl environment 31 | 1. Install application: `pip.exe install eo_man` (Package available under pypi: [eo_man](https://pypi.org/project/eo-man/)) 32 | 2. Run application: `python.exe -m eo_man` 33 | 34 | ## Install source code from this repository and run the App (alternative) 35 | 1. Clone/Download the repo. 36 | 2. Change into the repo directory. 37 | 3. Create virtual environment for python: `python.exe -m venv .venv` 38 | 4. Install dependencies: `.\.venv\Scripts\python.exe setup.py install` 39 | 5. Start the app: `.\.venv\Scripts\python.exe -m eo_man` or `.\.venv\Scripts\python.exe -m eo_man demo.eodm` (Directly loads demo data) 40 | 41 | For update you can execute: 42 | 1. `git pull` (Gets newest state of the code) 43 | 2. Optionally change branch: `git checkout BRANCH_NAME` 44 | 3. Reinstall`.\.venv\Scripts\python.exe setup.py install --force` 45 | 4. Run app: `.\.venv\Scripts\python.exe -m eo_man` 46 | 47 | ## Bugs and Features 48 | Please open [issues](/issues) if you encounter bugs or if you have ideas for new features. Also quite a lot of devices are not yet supported. 49 | 50 | ## Run unittests 51 | `pytest tests` 52 | 53 | ## Install pre-commit hook to ensure unittests are executed before each commit 54 | 1. Install package `pip install pre-commit` 55 | 2. Config git: `pre-commit install` 56 | 57 | ## Build wheel package 58 | `python setup.py bdist_wheel` 59 | 60 | ## Install built wheel pacage 61 | `pip install dist/eo_man-VERSION-py3-none-any.whl` use `--force-reinstall` if you want to overwrite an existing version. 62 | 63 | # Use Command Line 64 | You can use command line only to generate Home Assistant Configuration based on an existing application configuration.
65 | Check out: `python -m eo_man -h` 66 | 67 | # [Chanagelog](https://github.com/grimmpp/enocean-device-manager/blob/main/changes.md) 68 | 69 | # Contribution and Support to this Project 70 | I'm really happy to provide a more and more growing Home Assistant Eltako Integration and tools like this which extend this automation corner even more. The size of this integration is getting much bigger than the use cases I've realized at home, the variety of supported devices is increasing and the stability of the integraiton is getting to a professional level. On the other side it is getting hard to keep this level of development speed and operational quality. I'm about to build up a professional development and testing environment so that the quality can even improved and futher features can still be delivered in a short time frame. 71 | 72 | In general, you can contribute to this project by: 73 | * Support users in the Home Assistant Community ([Eltako “Baureihe 14 – RS485” (Enocean) Debugging](https://community.home-assistant.io/t/eltako-baureihe-14-rs485-enocean-debugging)) 74 | * Reporting [Issues]([/issue](https://github.com/grimmpp/home-assistant-eltako/issues)) 75 | * Creating [Pull Requests](https://github.com/grimmpp/home-assistant-eltako/pulls) 76 | * Providing [Documentation](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs) 77 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v0.1.44 Fixed config generation for lan gateways 4 | 5 | ## v0.1.42 Fixed dependencies 6 | 7 | ## v0.1.41 HA Config Generation Fix for USB300 8 | * device type of USB300 was wrong 9 | * Getting started docs improved. 10 | 11 | ## v0.1.40 Dependency Fix 12 | * Updated to current lib of esp2-gateway-adapter 0.2.18 13 | 14 | ## v0.1.39 15 | * Support for EUL-2PJ3Q tcm515 added. https://busware.de/tiki-index.php?page=EUL 16 | * Clean up of device table implemented 17 | * Made ESP3 LAN gateway more reactive 18 | * Improved communication of remote connection to Home Assistant 19 | * Added binary output optionally to logout put 20 | * Improved installation script 21 | * Migration function/compatibility for loading older data added. 22 | 23 | ## v0.1.38 Remote detection of messages and gateways in Home Assistant 24 | 25 | ## v0.1.37 Fixed new packaging 26 | * Fixed missing files in 0.1.36 27 | * Green background removed from buttons in toolbar 28 | 29 | ## v0.1.36 Added Extending PCT14 Export 30 | * Changed package build description from setup.py to toml 31 | * Added install scripts for windows 32 | * Added install script for ubuntu (detecting of gateway devices is still not working under linux) 33 | * Added extending PCT14 export 34 | 35 | ## v0.1.35 Loading PCT14 Export 36 | * Loading exported data from PCT14 introduced. 37 | * eo_man can now take EEP from TeachIn telegram and pre-configure device. 38 | 39 | ## v0.1.34 Added more configuration templates for devices 40 | * Added more configuration templates for devices 41 | * Fixed configuration for FMZ 42 | * Fixed send message for fired gateways 43 | * Improved writing sender ids into actuators for EEP F6-02-01/02 44 | * Added List for supported devices. 45 | * Configuration Checks which are made before generating Home Assistant Configuration can be ignored. 46 | 47 | ## v0.1.33 Added Supported Device View 48 | * Improved template selection in Details View. 49 | * Improved GUI Details View 50 | 51 | ## v0.1.30 Extended device list 52 | * Extended device list with EEP mapping 53 | * Fixed sender id duplication check. 54 | 55 | ## v0.1.29 Added FGD14 and FD2G14 Support 56 | * Added FGD14 and FD2G14 Support 57 | * Fixed missing dependency for FSR14M-2x support. 58 | * Fixed configuration generation and sender id validation. 59 | 60 | ## v0.1.28 added EEP F6-01-01 61 | 62 | ## v0.1.27 Added Support for FSR14M-2x 63 | * Added support for FSR14M-2x. Switch/light and power meter are represented as separate entities. 64 | * Fixed sender ids for bus gateways 65 | * Introduced validation for auto-generated configuration before exporting it. 66 | * Prepared configuration option for Home Assistance base_id. 67 | 68 | ## v0.1.26 Fixed LAN Gateway Connection 69 | * For reconnecting lan gateway you still need to wait for 10sec 70 | 71 | ## v0.1.25 Fixed Gateway Detection 72 | * Bug-fix for gateway detection. 73 | 74 | ## v0.1.24 Added Support for FHK14, F4HK14 and FTD14 75 | * Added support for Eltako devices: FHK14, F4HK14 and FTD14 76 | * Config generation for MGW Gateway (LAN) extended 77 | 78 | ## v0.1.23 Support for MGW Gateway 79 | * Added support for [MGW Gateway](https://www.piotek.de/PioTek-MGW-POE) (ESP3 over LAN) 80 | 81 | ## v0.1.21 EEP representation bug fixes 82 | * Ignores unknown devices 83 | * Fixed EEP representation in dorp down 84 | * Preparation for FDG14 detection 85 | 86 | ## v0.1.20 Bug-fix for detection of FSR14_2x 87 | 88 | ## v0.1.19 Added EEP A5-30-01 and A5-30-03 89 | * Added EEPs for digital input 90 | 91 | ## v0.1.18 Bug-fix for USB300 detection 92 | * Bug-fix USB300 detection 93 | * Typos removed 94 | 95 | ## v0.1.17 Bug-Fix missing dependency in v0.1.16 96 | * added dependency `esp2_gateway_adapter` 97 | 98 | ## v0.1.16 Improved send message and program devices (DELETED) 99 | * Support for programming different baseId for HA sender into devices on bus. 100 | * Enabled FMZ14 and FAE14SSR to be programmed. 101 | * Added template list for messages to be sent 102 | * Added lib for ESP3 (USB300) support. (https://github.com/grimmpp/esp2_gateway_adapter) 103 | 104 | ## v0.1.15 Fixed Send Message 105 | * Send Message Window improved and fixed. 106 | * Added button in toolbar for send message window. 107 | 108 | ## v0.1.13 Send Message Window Improved 109 | * Improved send messages tool. 110 | * fixed log rotation 111 | * Improved ESP3. Sending is working but not for gateway commands like A5-38-08 which is needed to control lights. 112 | 113 | ## v0.1.12 Send Message Window added 114 | * Small tool to send messages added. 115 | 116 | ## v0.1.11 EEP Checker added 117 | * Small tool added to check EEP values of a data set. 118 | 119 | ## v0.1.10 Logs are sent into file 120 | * Logs are now written to log file in application folder. 121 | * Added Flag to see values from incomming telegrams. 122 | 123 | ## v0.1.9 Read Support for USB300 + Multi-Gateway Support for HA Config Export 124 | * Fixed compatibility of loading old application configs 125 | * Icons added 126 | * Remove Smart Home addresses as real buttons from HA export 127 | * Serial port detection for FAM-USB and USB300 improved. 128 | * TODO: Cleanup of ESP3 communication, move function into lib 129 | 130 | ## v0.1.8 Wireless Transceiver Support 131 | * Reset suggested HA settings added 132 | * Support for FAM-USB. Is now detected as gateway and contained in HA config 133 | * **Experimental** Support for USB300. CODE CLEANUP HEAVILY NEEDED!!! 134 | 135 | ## v0.1.7 F2SR14 Support 136 | * Support for F4SR14 added 137 | * Update Button added 138 | 139 | ## v0.1.6 Sensor Values are evaluated 140 | * Sensor values are displayed in command line 141 | * Sponsor button added 142 | * Docs updated with system requirements 143 | * Added links to documentation 144 | * Improved look and feel 145 | * About window improved 146 | * Icons to menu added 147 | * Unmapped devices are moved to FAM14 after connection. 148 | * Error handling added for serial connection. 149 | 150 | ## v0.1.5 Refactoring + Basic Features (GOAL: Stability) 151 | * Improved imports incl. homeassistant mock 152 | * Changed application output format to yaml. **=> Braking Change** 153 | * Refactored Home Assistant Configuration Exporter 154 | * Created start file for windows (eo-man.bat) which can be used to create a shortcut for e.g. the taskbar. 155 | * Changed folder structure (renamed 'eo-man' to 'eo_man' which allows using package name.) **=> Braking Change** 156 | * Introduced tests 157 | * Introduced cli commands 158 | * Added possibility to only use command line to generate Home Assistant Configuration 159 | * Application info added 160 | * Application info and description added to auto-generated Home Assistant configuration. 161 | * python pre-commits added to ensure unittests are executed successfully before every commit. 162 | 163 | ## v0.1.4 Bug fixed in python package 164 | * Bug in python package fixed 165 | 166 | ## v0.1.1 Bug Fix and values in log view 167 | * 🐞 Missing function added from refactoring 🐞 168 | * 💎 Added values for incoming messages which are displayed in log view. -------------------------------------------------------------------------------- /demo2.eodm: -------------------------------------------------------------------------------- 1 | !!python/object:eo_man.data.application_data.ApplicationData 2 | application_version: 0.1.37 3 | data_filters: {} 4 | devices: 5 | FE-D4-E9-48: !!python/object:eo_man.data.device.Device 6 | additional_fields: {} 7 | address: FE-D4-E9-48 8 | base_id: 00-00-00-00 9 | bus_device: false 10 | channel: 1 11 | comment: window and door contacts 12 | dev_size: 1 13 | device_type: FFTE 14 | eep: F6-10-00 15 | external_id: FE-D4-E9-48 16 | ha_platform: binary_sensor 17 | key_function: '' 18 | memory_entries: [] 19 | name: unknown 20 | static_info: {} 21 | use_in_ha: true 22 | version: unknown 23 | FF-CD-60-80: !!python/object:eo_man.data.device.Device 24 | additional_fields: {} 25 | address: 00-00-00-FF 26 | base_id: FF-CD-60-80 27 | bus_device: true 28 | channel: 1 29 | comment: Bus Gateway 30 | dev_size: 1 31 | device_type: fam14 32 | eep: null 33 | external_id: FF-CD-60-80 34 | ha_platform: null 35 | key_function: '' 36 | memory_entries: [] 37 | name: FAM14 (FF-CD-60-80) 38 | static_info: {} 39 | use_in_ha: true 40 | version: 1.5.0.0 41 | FF-CD-60-81: !!python/object:eo_man.data.device.Device 42 | additional_fields: 43 | sender: 44 | eep: F6-02-01 45 | id: '01' 46 | address: 00-00-00-01 47 | base_id: FF-CD-60-80 48 | bus_device: true 49 | channel: 1 50 | comment: Relay 51 | dev_size: 1 52 | device_type: FMZ14 53 | eep: M5-38-08 54 | external_id: FF-CD-60-81 55 | ha_platform: !!python/object/apply:homeassistant.const.Platform 56 | - light 57 | key_function: '' 58 | memory_entries: [] 59 | name: FMZ14 00-00-00-01 60 | static_info: {} 61 | use_in_ha: true 62 | version: 2.2.0.0 63 | FF-CD-60-85: !!python/object:eo_man.data.device.Device 64 | additional_fields: 65 | device_class: shutter 66 | sender: 67 | eep: H5-3F-7F 68 | id: '05' 69 | time_closes: 25 70 | time_opens: 25 71 | address: 00-00-00-05 72 | base_id: FF-CD-60-80 73 | bus_device: true 74 | channel: 1 75 | comment: Cover 76 | dev_size: 2 77 | device_type: FSB14 78 | eep: G5-3F-7F 79 | external_id: FF-CD-60-85 80 | ha_platform: !!python/object/apply:homeassistant.const.Platform 81 | - cover 82 | key_function: '' 83 | memory_entries: [] 84 | name: FSB14 00-00-00-05 (1/2) 85 | static_info: {} 86 | use_in_ha: true 87 | version: 2.5.0.0 88 | FF-CD-60-86: !!python/object:eo_man.data.device.Device 89 | additional_fields: 90 | device_class: shutter 91 | sender: 92 | eep: H5-3F-7F 93 | id: '06' 94 | time_closes: 25 95 | time_opens: 25 96 | address: 00-00-00-06 97 | base_id: FF-CD-60-80 98 | bus_device: true 99 | channel: 2 100 | comment: Cover 101 | dev_size: 2 102 | device_type: FSB14 103 | eep: G5-3F-7F 104 | external_id: FF-CD-60-86 105 | ha_platform: !!python/object/apply:homeassistant.const.Platform 106 | - cover 107 | key_function: '' 108 | memory_entries: [] 109 | name: FSB14 00-00-00-06 (2/2) 110 | static_info: {} 111 | use_in_ha: true 112 | version: 2.5.0.0 113 | FF-CD-60-87: !!python/object:eo_man.data.device.Device 114 | additional_fields: 115 | sender: 116 | eep: A5-38-08 117 | id: '07' 118 | address: 00-00-00-07 119 | base_id: FF-CD-60-80 120 | bus_device: true 121 | channel: 1 122 | comment: Relay 123 | dev_size: 4 124 | device_type: FSR14_4x 125 | eep: M5-38-08 126 | external_id: FF-CD-60-87 127 | ha_platform: !!python/object/apply:homeassistant.const.Platform 128 | - light 129 | key_function: '' 130 | memory_entries: [] 131 | name: FSR14_4X 00-00-00-07 (1/4) 132 | static_info: {} 133 | use_in_ha: true 134 | version: 4.1.0.0 135 | FF-CD-60-88: !!python/object:eo_man.data.device.Device 136 | additional_fields: 137 | sender: 138 | eep: A5-38-08 139 | id: 08 140 | address: 00-00-00-08 141 | base_id: FF-CD-60-80 142 | bus_device: true 143 | channel: 2 144 | comment: Relay 145 | dev_size: 4 146 | device_type: FSR14_4x 147 | eep: M5-38-08 148 | external_id: FF-CD-60-88 149 | ha_platform: !!python/object/apply:homeassistant.const.Platform 150 | - light 151 | key_function: '' 152 | memory_entries: [] 153 | name: FSR14_4X 00-00-00-08 (2/4) 154 | static_info: {} 155 | use_in_ha: true 156 | version: 4.1.0.0 157 | FF-CD-60-89: !!python/object:eo_man.data.device.Device 158 | additional_fields: 159 | sender: 160 | eep: A5-38-08 161 | id: 09 162 | address: 00-00-00-09 163 | base_id: FF-CD-60-80 164 | bus_device: true 165 | channel: 3 166 | comment: Relay 167 | dev_size: 4 168 | device_type: FSR14_4x 169 | eep: M5-38-08 170 | external_id: FF-CD-60-89 171 | ha_platform: !!python/object/apply:homeassistant.const.Platform 172 | - light 173 | key_function: '' 174 | memory_entries: [] 175 | name: FSR14_4X 00-00-00-09 (3/4) 176 | static_info: {} 177 | use_in_ha: true 178 | version: 4.1.0.0 179 | FF-CD-60-8A: !!python/object:eo_man.data.device.Device 180 | additional_fields: 181 | sender: 182 | eep: A5-38-08 183 | id: 0A 184 | address: 00-00-00-0A 185 | base_id: FF-CD-60-80 186 | bus_device: true 187 | channel: 4 188 | comment: Relay 189 | dev_size: 4 190 | device_type: FSR14_4x 191 | eep: M5-38-08 192 | external_id: FF-CD-60-8A 193 | ha_platform: !!python/object/apply:homeassistant.const.Platform 194 | - light 195 | key_function: '' 196 | memory_entries: [] 197 | name: FSR14_4X 00-00-00-0A (4/4) 198 | static_info: {} 199 | use_in_ha: true 200 | version: 4.1.0.0 201 | FF-D6-30-80: !!python/object:eo_man.data.device.Device 202 | additional_fields: {} 203 | address: FF-D6-30-80 204 | base_id: FF-D6-30-80 205 | bus_device: false 206 | channel: 1 207 | comment: USB Gateway 208 | dev_size: 1 209 | device_type: USB300 210 | eep: '' 211 | external_id: FF-D6-30-80 212 | ha_platform: '' 213 | key_function: '' 214 | memory_entries: [] 215 | name: esp3-gateway (FF-D6-30-80) 216 | static_info: {} 217 | use_in_ha: true 218 | version: '' 219 | recoreded_messages: 220 | - !!python/object:eo_man.data.recorded_message.RecordedMessage 221 | external_device_id: FE-D4-E9-48 222 | message: null 223 | received: 2024-11-12 15:40:10.709805 224 | received_via_gateway_id: FF-CD-61-7F 225 | - !!python/object:eo_man.data.recorded_message.RecordedMessage 226 | external_device_id: FE-D4-E9-48 227 | message: null 228 | received: 2024-11-12 15:40:10.853661 229 | received_via_gateway_id: FF-CD-61-7F 230 | selected_data_filter_name: null 231 | send_message_template_list: null 232 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # EnOcean Device Manager Documentation 2 | -------------------------------------------------------------------------------- /docs/getting-started/eo_man_HA_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_HA_button.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_connect.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_connect_usb300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_connect_usb300.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_connected.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_detect_ports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_detect_ports.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_device_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_device_data.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_device_scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_device_scan.png -------------------------------------------------------------------------------- /docs/getting-started/eo_man_write_to_devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/getting-started/eo_man_write_to_devices.png -------------------------------------------------------------------------------- /docs/getting-started/readme.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | ### Intro 4 | You can use eo_man to detect and manage your EnOcean devices. After you compiled a list of devices you want to use in Home Assistant you can auto-generate a configuration for Home Assistant. 5 | 6 | ### Installation 7 | Install eo_man on your PC (not in Home Assistant. This is a standalone application). Plugin all Gateways you want to use in conjunction with Home Assistant. 8 | 9 | 1. Create virtual python environment: `python.exe -m venv .\.venv` 10 | 2. Install application: `.\.venv\Scripts\pip.exe install eo_man --force-reinstall` (Package available under pypi: [eo_man](https://pypi.org/project/eo-man/)) 11 | 3. Run application: `.\.venv\Scripts\python.exe -m eo_man` 12 | 13 | ### Detect Gateway Ports 14 | Ensure gateways you want to use are plugged-in. If you use series 14 bus and if you want to read out the configuration plugin FAM14 via USB cable as well. 15 | 16 | Click on `Detect Serial Ports` eo_man will try to find all Gateways connected to your PC. 17 | 18 | 19 | 20 | ### Connect Gateway 21 | Choose the gateway you want to use. In your case FAM14 (controller on the 14 bus) and click connect. 22 | After being connected you can see all messages from bus and wireless network. 23 | 24 | 25 | 26 | ### Scan devices 27 | Messages from devices can now received but unfortunately the message does not say which type the devices is. 28 | You can change this data by clicking on the devices and changing the values on the right panel. 29 | For bus devices you can try to do a scan and read the memory of the bus devices by pushing `Scan for devices`. This button is only active if FAM14 is connected. Other gateway cannot read the memory of the bus devices. 30 | 31 | 32 | 33 | ### Detect devices during application runtime 34 | 35 | After having the devices scanned they should be properly listed with all their attributes: 36 | 37 | 38 | 39 | ### Change device values 40 | If you need to change device data manually ensure the following info are set so that it appears late in the Home Assistant configuration: 41 | * Device EEP (defines the message format) 42 | * Export to HA 43 | * HA Platform (device type in HA) 44 | * sender id (if the device can send out messages it needs an address) 45 | * sender EEP (if the device can send out messages the message format needs to be defined) 46 | 47 | 48 | 49 | 50 | ### Support of different Gateway than FAM14 or FGW14-USB 51 | If you want to use another gateway instead of FAM14 or FGW14-USB, you need to plug it into your PC as well and connect it once in eo_man so that it just knows it and can read its address. 52 | 53 | If you want to use e.g. USB300 for Home Assistant then: 54 | 1. Disconnect from FAM14 55 | 2. Plugin USB300 into your PC 56 | 3. Select `Gateway Type` `ESP3 Gateway` in drop down menu. 57 | 4. Run port scan and therefore push button `Detect Serial Port` 58 | 5. It should automatically propose a COM port. Connect to this port. 59 | 6. A new device named `esp3-gateway (BASE ID)` was inserted into the device table. 60 | 7. Change device type by clicking on the entry and select in the detail view on the right, `USB300` in the combobox `Device Type`. 61 | 8. After clicking on apply at the bottom of this area, `USB300` will appear in the column `Device Type` of the device table on the left. 62 | 63 | 64 | 65 | ### Write Sender Ids into Actuators 66 | In order to make your actuators react on switches in Home Assistant they need to be teached-in. For bus devices it is quite simple you just need to connect with FAM14 again and select the gateway you cant to use in conjunction with Home Assistant later on and push the button 'Write to devices'. 67 | Alternatively and for decentralized devices you need to push the teach-in button in Home Assistant. See docs [here](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/teach_in_buttons). 68 | 69 | In the following example an USB300 is used for Home Assistant and its addresses are teached-in automatically into the bus devices. Decentralized devices need to be teached-in manually like described in the liked docs above. 70 | 71 | 72 | 73 | ### Generate Home Assistant Configuration 74 | To generate the configuration push e.g. the HA shortcut button in the left upper bar or in the `File` menu you can find the button as well. 75 | 76 | It automatically creates for all known devices configurations. Don't be confused if you don't need them. You can actually copy all to Home Assistant and then just choose the gateway you want to use. All other configuration will be ignored. 77 | 78 | 79 | 80 | 81 | ### Import Configuration into Home Assistant 82 | 83 | Open the generated file and copy the content of it into Home Assistant ``/config/configuration.yaml``. 84 | You can also check out the [docs](https://github.com/grimmpp/home-assistant-eltako/blob/main/docs/update_home_assistant_configuration.md) in the repository of [Eltako Integration](https://github.com/grimmpp/home-assistant-eltako) -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/installation.md -------------------------------------------------------------------------------- /docs/load-pct14-data/added-gateway-into-eo-man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/load-pct14-data/added-gateway-into-eo-man.png -------------------------------------------------------------------------------- /docs/load-pct14-data/export-pct14-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/load-pct14-data/export-pct14-data.png -------------------------------------------------------------------------------- /docs/load-pct14-data/load-pct14-export-into-eo-man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/load-pct14-data/load-pct14-export-into-eo-man.png -------------------------------------------------------------------------------- /docs/load-pct14-data/loaded-pct14-export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/docs/load-pct14-data/loaded-pct14-export.png -------------------------------------------------------------------------------- /docs/load-pct14-data/readme.md: -------------------------------------------------------------------------------- 1 | # Load PCT14 Project and generate Home Assistant Configuration 2 | 3 | This section describes how to load/import data from PCT14 and how to generate automatically the configuration for Home Assistant. 4 | 5 | 6 | ## Export data from PCT14 7 | As preparation you need to have installed [PCT14](https://www.eltako.com/en/software-pct14/), FAM14 is connected via USB, and all devices are detected and complete memory is read and available in PCT14. 8 | 9 | Export the all devices including their memory data by exporting the whole list of devices. You have to store the file in XML format. See screenshot below. 10 | 11 | 12 | 13 | ## Load PCT14 Export/XML into eo-man 14 | Open eo-man. You can also extend existing data from an opened eo-man session. 15 | To import the data from PCT14 click on `File -> Load PCT14 Export...` . 16 | 17 | 18 | 19 | After importing the PCT14 export all the devices and their sensors should be visible in the application. 20 | 21 | 22 | 23 | 24 | ## Add Gateway and modify settings 25 | 26 | Most likely, your gateway you want to connect to Home Assistant is not present in the devices list. Now let's add the gateway by simply connecting to it. 27 | Ensure the gateway is plugged in via USB or is available in the same network. Let eo-man detect the gateways and choose the one to be used by Home Assistant later on. After that simply connect and the gateway should appear in the list automatically. 28 | 29 | 30 | 31 | 32 | ## Generate Home Assistant Configuration 33 | 34 | Before you generate the configuration for Home Assistant you can change some settings if you like and it is always worthful to store the settings (all devices) in eo-man to have some kind of backup. 35 | 36 | To generate the configuration for Home Assistant simply click on the Home Assistant icon in the toolbar. eo-man tries to generate as many settings as possible to give you most possibilities. If you want to reduce it you can simply remove the components from the exported file you don't want to use or just ignore/don't use them in Home Assistant. 37 | 38 | 39 | 40 | 41 | ### Import Configuration into Home Assistant 42 | 43 | Open the generated file and copy the content of it into Home Assistant ``/config/configuration.yaml``. 44 | You can also check out the [docs](https://github.com/grimmpp/home-assistant-eltako/blob/main/docs/update_home_assistant_configuration.md) in the repository of [Eltako Integration](https://github.com/grimmpp/home-assistant-eltako) -------------------------------------------------------------------------------- /docs/supported-devices.md: -------------------------------------------------------------------------------- 1 | # List of Supported Devices 2 | Devices not contained in this list are quite likely already supported by existing devices in this list. 3 |
4 | (This list is auto-generated by using template definitions in [data_helper.py](https://github.com/grimmpp/enocean-device-manager/blob/main/eo_man/data/data_helper.py).) 5 |
6 | | Brand | Name | Description | EEP | Sender EEP | HA Platform | 7 | |-----|-----|-----|-----|-----|-----| 8 | | Eltako | FAM14 | Bus Gateway | | | | 9 | | Eltako | FGW14_USB | Bus Gateway | | | | 10 | | Eltako | FTD14 | Bus Gateway | | | | 11 | | Eltako | FGW14 | Bus Gateway | | | | 12 | | Eltako | FAM-USB | USB Gateway | | | | 13 | | unknown | USB300 | USB Gateway | | | | 14 | | PioTek | MGW (LAN) | USB Gateway | | | | 15 | | PioTek | MGW (USB) | USB Gateway | | | | 16 | | Eltako | FTS14EM | Wired Rocker switch | F6-02-01 | | binary_sensor | 17 | | Eltako | FTS14EM | Wired Rocker switch | F6-02-02 | | binary_sensor | 18 | | Eltako | FTS14EM | Window handle | F6-10-00 | | binary_sensor | 19 | | Eltako | FTS14EM | Contact sensor | D5-00-01 | | binary_sensor | 20 | | Eltako | FTS14EM | Occupancy sensor | A5-08-01 | | binary_sensor | 21 | | Eltako | FT55 | Wireless 4-way pushbutton | F6-02-01 | | binary_sensor | 22 | | Eltako | F4T55E | Wireless 4-way pushbutton in E-Design55 | F6-02-01 | | binary_sensor | 23 | | Eltako | FFTE | window and door contacts | F6-10-00 | | binary_sensor | 24 | | Eltako | FTKE | window and door contacts | F6-10-00 | | binary_sensor | 25 | | Eltako | FTK | window and door contacts | F6-10-00 | | binary_sensor | 26 | | Eltako | FSDG14 | Electricity Meter | A5-12-01 | | sensor | 27 | | Eltako | F3Z14D | Electricity Meter | A5-12-01 | | sensor | 28 | | Eltako | FSVA-230V-10A-Power-Meter | Power Meter | A5-12-01 | | sensor | 29 | | Eltako | F3Z14D | Gas Meter | A5-12-02 | | sensor | 30 | | Eltako | F3Z14D | Water Meter | A5-12-03 | | sensor | 31 | | Eltako | FWZ14_65A | Electricity Meter | A5-12-01 | | sensor | 32 | | Eltako | FWG14MS | Weather Station Gateway | A5-13-01 | | sensor | 33 | | Eltako | MS | Weather Station | A5-13-01 | | sensor | 34 | | Eltako | WMS | Weather Station | A5-13-01 | | sensor | 35 | | Eltako | FWS61 | Weather Station | A5-13-01 | | sensor | 36 | | Eltako | FLGTF | Temperature and Humidity Sensor | A5-04-02 | | sensor | 37 | | Eltako | FLT58 | Temperature and Humidity Sensor | A5-04-02 | | sensor | 38 | | Eltako | FFT60 | Temperature and Humidity Sensor | A5-04-02 | | sensor | 39 | | Eltako | FTFSB | Temperature and Humidity Sensor | A5-04-02 | | sensor | 40 | | Eltako | FHD60SB | Light - Twilight and daylight Sensor | A5-06-01 | | sensor | 41 | | Eltako | FABH65S | Light-, Temperature-, Occupancy Sensor | A5-08-01 | | sensor | 42 | | Eltako | FBH65 | Light-, Temperature-, Occupancy Sensor | A5-08-01 | | sensor | 43 | | Eltako | FBH65S | Light-, Temperature-, Occupancy Sensor | A5-08-01 | | sensor | 44 | | Eltako | FBH65TF | Light-, Temperature-, Occupancy Sensor | A5-08-01 | | sensor | 45 | | Eltako | FLGTF | Air Quality, Temperature and Humidity Sensor | A5-09-0C | | sensor | 46 | | Eltako | FUTH | Temperature Sensor and Controller | A5-10-06 | | sensor | 47 | | Eltako | FUTH-feature | Temperature Sensor and Controller and Humidity Sensor | A5-10-12 | | sensor | 48 | | Eltako | FUD14 | Light dimmer | A5-38-08 | A5-38-08 | light | 49 | | Eltako | FUD14_800W | Light dimmer | A5-38-08 | A5-38-08 | light | 50 | | Eltako | FDG14 | Dali Gateway | A5-38-08 | A5-38-08 | light | 51 | | Eltako | FD2G14 | Dali Gateway | A5-38-08 | A5-38-08 | light | 52 | | Eltako | FMZ14 | Relay | M5-38-08 | F6-02-01 | light | 53 | | Eltako | FSR14 | Relay | M5-38-08 | A5-38-08 | light | 54 | | Eltako | FSR14_1x | Relay | M5-38-08 | A5-38-08 | light | 55 | | Eltako | FSR14_2x | Relay | M5-38-08 | A5-38-08 | light | 56 | | Eltako | FSR14_4x | Relay | M5-38-08 | A5-38-08 | light | 57 | | Eltako | FSR14M_2x | Relay | M5-38-08 | A5-38-08 | light | 58 | | Eltako | FSR14M_2x-feature | Electricity Meter | A5-12-01 | | sensor | 59 | | Eltako | F4SR14_LED | Relay | M5-38-08 | A5-38-08 | light | 60 | | Eltako | FSB14 | Cover | G5-3F-7F | H5-3F-7F | cover | 61 | | Eltako | FHK14 | Heating/Cooling | A5-10-06 | A5-10-06 | climate | 62 | | Eltako | F4HK14 | Heating/Cooling | A5-10-06 | A5-10-06 | climate | 63 | | Eltako | FAE14SSR | Heating/Cooling | A5-10-06 | A5-10-06 | climate | 64 | | Eltako | FMZ61 | Relay | M5-38-08 | F6-02-01 | light | 65 | | Eltako | FSR61-230V | Relay | M5-38-08 | A5-38-08 | light | 66 | | Eltako | FSR61NP-230V | Relay | M5-38-08 | A5-38-08 | light | 67 | | Eltako | FSR61/8-24V UC | Relay | M5-38-08 | A5-38-08 | light | 68 | | Eltako | FSR61-230V | Relay | M5-38-08 | A5-38-08 | light | 69 | | Eltako | FSR61G-230V | Relay | M5-38-08 | A5-38-08 | light | 70 | | Eltako | FSR61LN-230V | Relay | M5-38-08 | A5-38-08 | light | 71 | | Eltako | FLC61NP-230V | Relay | M5-38-08 | A5-38-08 | light | 72 | | Eltako | FR62-230V | Relay | M5-38-08 | A5-38-08 | light | 73 | | Eltako | FR62NP-230V | Relay | M5-38-08 | A5-38-08 | light | 74 | | Eltako | FL62-230V | Relay | M5-38-08 | A5-38-08 | light | 75 | | Eltako | FL62NP-230V | Relay | M5-38-08 | A5-38-08 | light | 76 | | Eltako | FSSA-230V | Socket Switch Actuator | M5-38-08 | A5-38-08 | light | 77 | | Eltako | FSVA-230V-10A | Socket Switch Actuator | M5-38-08 | A5-38-08 | light | 78 | | Eltako | FUD61NP-230V | Light dimmer | A5-38-08 | A5-38-08 | light | 79 | | Eltako | FUD61NPN-230V | Light dimmer | A5-38-08 | A5-38-08 | light | 80 | | Eltako | FD62NP-230V | Relay | A5-38-08 | A5-38-08 | light | 81 | | Eltako | FD62NPN-230V | Relay | A5-38-08 | A5-38-08 | light | 82 | | Eltako | FSB61-230V | Cover | G5-3F-7F | H5-3F-7F | cover | 83 | | Eltako | FSB61NP-230V | Cover | G5-3F-7F | H5-3F-7F | cover | 84 | | Eltako | FJ62/12-36V DC | Cover | G5-3F-7F | H5-3F-7F | cover | 85 | | Eltako | FJ62NP-230V | Cover | G5-3F-7F | H5-3F-7F | cover | 86 | | Eltako | FSUD-230V | Cover | G5-3F-7F | H5-3F-7F | cover | 87 | -------------------------------------------------------------------------------- /eo_man.bat: -------------------------------------------------------------------------------- 1 | REM Starts eo-man after installed globally on windows. (pip.exe install eo-man) 2 | REM This file can be used to e.g. create a shortcut on the taskbar 3 | @echo off 4 | START "eo-man" "python.exe" -m eo_man 5 | -------------------------------------------------------------------------------- /eo_man/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | LOGGER = logging.getLogger() 6 | 7 | def load_dep_homeassistant(): 8 | # import fake homeassistant package 9 | file_dir = os.path.join( os.path.dirname(__file__), 'data') 10 | sys.path.append(file_dir) 11 | __import__('homeassistant') -------------------------------------------------------------------------------- /eo_man/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import argparse 4 | import asyncio 5 | from typing import Final 6 | from logging.handlers import RotatingFileHandler 7 | 8 | PACKAGE_NAME: Final = 'eo_man' 9 | 10 | # load same path like calling the app via 'python -m eo-man' 11 | PROJECT_DIR = os.path.dirname(__file__) 12 | file_dir = os.path.join( PROJECT_DIR, '..') 13 | sys.path.append(file_dir) 14 | __import__(PACKAGE_NAME) 15 | __package__ = PACKAGE_NAME 16 | 17 | from eo_man import load_dep_homeassistant, LOGGER 18 | load_dep_homeassistant() 19 | 20 | from .data.app_info import ApplicationInfo 21 | from .data.data_manager import DataManager 22 | from .data.pct14_data_manager import PCT14DataManager 23 | from .data.ha_config_generator import HomeAssistantConfigurationGenerator 24 | from .view.main_panel import MainPanel 25 | from .controller.app_bus import AppBus, AppBusEventType 26 | 27 | import logging 28 | 29 | 30 | def cli_argument(): 31 | p = argparse.ArgumentParser( 32 | description= 33 | """EnOcean Device Manager (https://github.com/grimmpp/enocean-device-manager) allows you to managed your EnOcean devices and to generate 34 | Home Assistant Configurations for the Home Assistant Eltako Integration (https://github.com/grimmpp/home-assistant-eltako).""") 35 | p.add_argument('-v', '--verbose', help="Logs all messages.", action='count', default=0) 36 | p.add_argument('-c', "--app_config", help="Filename of stored application configuration. Filename must end with '.eodm'.", default=None) 37 | p.add_argument('-ha', "--ha_config", help="Filename for Home Assistant Configuration for Eltako Integration. By passing the filename it will disable the GUI and only generate the Home Assistant Configuration file.") 38 | p.add_argument('-pct14', '--pct14_export', help="Load PCT14 exported file. Filename must end with .xml") 39 | return p.parse_args() 40 | 41 | 42 | def init_logger(app_bus:AppBus, log_level:int=logging.INFO, verbose_level:int=0): 43 | file_handler = RotatingFileHandler(os.path.join(PROJECT_DIR, "enocean-device-manager.log"), 44 | mode='a', maxBytes=10*1024*1024, backupCount=2, encoding=None, delay=0) 45 | stream_handler = logging.StreamHandler() 46 | 47 | logging.basicConfig(format='%(asctime)s %(levelname)s %(name)s %(message)s ', 48 | level=log_level, 49 | handlers=[ file_handler, stream_handler ]) 50 | 51 | global LOGGER 52 | LOGGER = logging.getLogger(PACKAGE_NAME) 53 | LOGGER.setLevel(log_level) 54 | file_handler.setLevel(logging.DEBUG) 55 | stream_handler.setLevel(log_level) 56 | 57 | logging.getLogger('esp2_gateway_adapter').setLevel(logging.INFO) 58 | logging.getLogger('eltakobus.serial').setLevel(logging.INFO) 59 | if verbose_level > 0: 60 | logging.getLogger('esp2_gateway_adapter').setLevel(logging.DEBUG) 61 | elif verbose_level > 1: 62 | logging.getLogger('eltakobus.serial').setLevel(logging.DEBUG) 63 | 64 | LOGGER.info("Start Application eo_man") 65 | LOGGER.info(ApplicationInfo.get_app_info_as_str()) 66 | # add print log messages for log message view on command line as debug 67 | def print_log_event(e:dict): 68 | log_level = e.get('log-level', 'INFO') 69 | log_level_int = logging.getLevelName(log_level) 70 | if log_level_int >= LOGGER.getEffectiveLevel(): 71 | LOGGER.log(log_level_int, str(e['msg'])) 72 | app_bus.add_event_handler(AppBusEventType.LOG_MESSAGE, print_log_event) 73 | 74 | 75 | def main(): 76 | opts = cli_argument() 77 | 78 | if hasattr(opts, 'help') and opts.help: 79 | return 80 | 81 | # init application message BUS 82 | app_bus = AppBus() 83 | 84 | init_logger(app_bus, logging.DEBUG if opts.verbose > 0 else logging.INFO) 85 | 86 | # init DATA MANAGER 87 | data_manager = DataManager(app_bus) 88 | 89 | # initially load from file application data 90 | if opts.app_config and opts.app_config.endswith('.eodm'): 91 | e = {'msg': f"Initially load data from file {opts.app_config}", 'color': 'darkred'} 92 | app_bus.fire_event(AppBusEventType.LOG_MESSAGE, e) 93 | data_manager.load_application_data_from_file(opts.app_config) 94 | elif opts.app_config: 95 | e = {'msg': f"Invalid filename {opts.app_config}. It must end with '.eodm'", 'color': 'darkred'} 96 | app_bus.fire_event(AppBusEventType.LOG_MESSAGE, e) 97 | elif opts.pct14_export and opts.pct14_export.endswith('.xml'): 98 | e = {'msg': f"Initially load exported data from PCT14 {opts.pct14_export}", 'color': 'darkred'} 99 | devices = asyncio.run( PCT14DataManager.get_devices_from_pct14(opts.pct14_export) ) 100 | data_manager.load_devices(devices) 101 | 102 | # generate home assistant config instead of starting GUI 103 | if opts.app_config and opts.app_config.endswith('.eodm') and opts.ha_config: 104 | HomeAssistantConfigurationGenerator(app_bus, data_manager).save_as_yaml_to_file(opts.ha_config) 105 | else: 106 | MainPanel(app_bus, data_manager) 107 | 108 | if __name__ == "__main__": 109 | main() 110 | -------------------------------------------------------------------------------- /eo_man/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/controller/__init__.py -------------------------------------------------------------------------------- /eo_man/controller/app_bus.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import asyncio 3 | 4 | from enum import Enum 5 | from .. import LOGGER 6 | 7 | class AppBusEventType(Enum): 8 | LOG_MESSAGE = 0 # dict with keys: msg:str, color:str 9 | SERIAL_CALLBACK = 1 # dict: msg:EltakoMessage base_id:str 10 | CONNECTION_STATUS_CHANGE = 2 # dict with keys: serial_port:str, baudrate:int, connected:bool 11 | DEVICE_ITERATION_PROGRESS = 3 # percentage 0..100 in float 12 | DEVICE_SCAN_STATUS = 4 # str: STARTED, FINISHED, DEVICE_DETECTED 13 | ASYNC_DEVICE_DETECTED = 5 # BusObject 14 | UPDATE_DEVICE_REPRESENTATION = 6 # busdevice 15 | UPDATE_SENSOR_REPRESENTATION = 7 # esp2 eltakomessage 16 | WINDOW_CLOSED = 8 17 | WINDOW_LOADED = 9 18 | SELECTED_DEVICE = 10 # device 19 | LOAD_FILE = 11 20 | WRITE_SENDER_IDS_TO_DEVICES_STATUS = 12 21 | SET_DATA_TABLE_FILTER = 13 # applies data filter to data table 22 | ADDED_DATA_TABLE_FILTER = 14 # adds data filter to application data 23 | REMOVED_DATA_TABLE_FILTER = 15 # remove data filter from application data 24 | ASYNC_TRANSCEIVER_DETECTED = 17 # type:str (FAM-USB), base_id:str 00-00-00-00 25 | SEND_MESSAGE_TEMPLATE_LIST_UPDATED = 18 26 | REQUEST_SERVICE_ENDPOINT_DETECTION = 19 27 | SERVICE_ENDPOINTS_UPDATES = 20 # fired when new services are detected 28 | 29 | class AppBus(): 30 | 31 | 32 | 33 | def __init__(self) -> None: 34 | self.handler_count = 0 35 | 36 | for event_type in AppBusEventType: 37 | if event_type not in self._controller_event_handlers.keys(): 38 | self._controller_event_handlers[event_type] = {} 39 | 40 | 41 | _controller_event_handlers={} 42 | def add_event_handler(self, event:AppBusEventType, handler) -> int: 43 | self.handler_count += 1 44 | self._controller_event_handlers[event][self.handler_count] = handler 45 | return self.handler_count 46 | 47 | def remove_event_handler_by_id(self, handler_id:int) -> None: 48 | for et in AppBusEventType: 49 | if handler_id in self._controller_event_handlers[et]: 50 | del self._controller_event_handlers[et][handler_id] 51 | break 52 | 53 | def fire_event(self, event:AppBusEventType, data) -> None: 54 | # print(f"[Controller] Fire event {event}") 55 | for h in self._controller_event_handlers[event].values(): 56 | try: 57 | if inspect.iscoroutinefunction(h): 58 | asyncio.run(h(data)) 59 | else: 60 | h(data) 61 | except: 62 | LOGGER.exception(f"Error handling event {event}") 63 | 64 | 65 | async def async_fire_event(self, event:AppBusEventType, data) -> None: 66 | # print(f"[Controller] Fire async event {event}") 67 | for h in self._controller_event_handlers[event].values(): 68 | try: 69 | if inspect.iscoroutinefunction(h): 70 | await h(data) 71 | else: 72 | h(data) 73 | except: 74 | LOGGER.exception(f"Error handling event {event}") -------------------------------------------------------------------------------- /eo_man/controller/gateway_registry.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | import threading 3 | import asyncio 4 | 5 | from ..data.const import GatewayDeviceType as GDT, GATEWAY_DISPLAY_NAMES as GDN 6 | 7 | from .app_bus import AppBus, AppBusEventType 8 | 9 | from .serial_port_detector import SerialPortDetector 10 | from .lan_service_detector import LanServiceDetector 11 | 12 | class GatewayRegistry: 13 | 14 | def __init__(self, app_bus: AppBus) -> None: 15 | 16 | self.serial_port_detector = SerialPortDetector(app_bus) 17 | self.lan_service_detector = LanServiceDetector(app_bus, self._update_lan_gateway_entries) 18 | 19 | self.endpoint_list:Dict[str, List[str]] = {} 20 | 21 | self._is_running = threading.Event() 22 | self._is_running.clear() 23 | 24 | self.app_bus = app_bus 25 | self.app_bus.add_event_handler(AppBusEventType.REQUEST_SERVICE_ENDPOINT_DETECTION, self._process_update_service_endpoints) 26 | 27 | 28 | def find_mdns_service_by_ip_address(self, address:str): 29 | return self.lan_service_detector.find_mdns_service_by_ip_address(address) 30 | 31 | 32 | def _update_lan_gateway_entries(self): 33 | self.endpoint_list[GDT.LAN.value] = self.lan_service_detector.get_lan_gateway_endpoints() 34 | self.endpoint_list[GDT.LAN_ESP2.value] = self.lan_service_detector.get_virtual_network_gateway_service_endpoints() 35 | 36 | self.app_bus.fire_event(AppBusEventType.SERVICE_ENDPOINTS_UPDATES, self.endpoint_list) 37 | 38 | 39 | async def async_update_service_endpoint_list(self, force_reload:bool=True) -> None: 40 | 41 | if not force_reload and len(self.endpoint_list) > 0: 42 | await self.app_bus.async_fire_event(AppBusEventType.SERVICE_ENDPOINTS_UPDATES, self.endpoint_list) 43 | 44 | else: 45 | self.endpoint_list:Dict[str, List[str]] = await self.serial_port_detector.async_get_gateway2serial_port_mapping() 46 | self.endpoint_list[GDT.LAN.value] = self.lan_service_detector.get_lan_gateway_endpoints() 47 | self.endpoint_list[GDT.LAN_ESP2.value] = self.lan_service_detector.get_virtual_network_gateway_service_endpoints() 48 | 49 | # put all service together in section all as well 50 | self.endpoint_list['all'] = [] 51 | for k in self.endpoint_list: 52 | if k != 'all': 53 | self.endpoint_list['all'].extend(self.endpoint_list[k]) 54 | 55 | await self.app_bus.async_fire_event(AppBusEventType.SERVICE_ENDPOINTS_UPDATES, self.endpoint_list) 56 | 57 | 58 | async def _process_update_service_endpoints(self, force_update:bool=False): 59 | def process(force_update:bool=False): 60 | self._is_running.set() 61 | asyncio.run(self.async_update_service_endpoint_list(force_update)) 62 | self._is_running.clear() 63 | 64 | if not self._is_running.is_set(): 65 | t = threading.Thread(target=process, name="Thread-async_update_service_endpoint_list", args=(force_update,)) 66 | t.daemon = True 67 | t.start() 68 | -------------------------------------------------------------------------------- /eo_man/controller/lan_service_detector.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo 4 | 5 | import socket 6 | 7 | from .app_bus import AppBus, AppBusEventType 8 | from ..data.const import GatewayDeviceType, get_gateway_type_by_name 9 | from ..data import data_helper 10 | 11 | class LanServiceDetector: 12 | 13 | def __init__(self, app_bus: AppBus, update_callback) -> None: 14 | 15 | self.app_bus = app_bus 16 | self._update_callback = update_callback 17 | self.service_reg_lan_gw = {} 18 | self.service_reg_virt_lan_gw = {} 19 | self._start_service_discovery() 20 | 21 | 22 | def find_mdns_service_by_ip_address(self, address:str): 23 | service_list:List[Dict] = {} 24 | service_list.update(self.service_reg_lan_gw) 25 | service_list.update(self.service_reg_virt_lan_gw) 26 | 27 | for s in service_list.values(): 28 | dns_name = f"{s['hostname'][:-1]}:{s['port']}" 29 | ip_address = f"{s['address']}:{s['port']}" 30 | if dns_name == address or ip_address == address: 31 | return s['name'] 32 | 33 | return None 34 | 35 | def __del__(self): 36 | self.zeroconf.close() 37 | 38 | def add_service(self, zeroconf: Zeroconf, type, name): 39 | try: 40 | info:ServiceInfo = zeroconf.get_service_info(type, name) 41 | obj = {'name': name, 'type': type, 'address': socket.inet_ntoa(info.addresses[0]), 'port': info.port, 'hostname': info.server} 42 | msg = f"Detected Network Service: {name}, type: {type}, address: {obj['address']}, port: {obj['address']}, hostname: {obj['address']}" 43 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': msg, 'log-level': 'INFO', 'color': 'grey'}) 44 | 45 | for mdns_name in data_helper.MDNS_SERVICE_2_GW_TYPE_MAPPING: 46 | if mdns_name in name: 47 | gw_type: GatewayDeviceType = data_helper.MDNS_SERVICE_2_GW_TYPE_MAPPING[mdns_name] 48 | if gw_type == GatewayDeviceType.LAN: 49 | self.service_reg_lan_gw[name] = obj 50 | break 51 | elif gw_type == GatewayDeviceType.LAN_ESP2: 52 | self.service_reg_virt_lan_gw[name] = obj 53 | break 54 | 55 | except: 56 | pass 57 | 58 | self._update_callback() 59 | 60 | def remove_service(self, zeroconf, type, name): 61 | if name in self.service_reg_lan_gw: 62 | del self.service_reg_lan_gw[name] 63 | if name in self.service_reg_virt_lan_gw: 64 | del self.service_reg_virt_lan_gw[name] 65 | 66 | self._update_callback() 67 | 68 | def update_service(self, zeroconf, type, name): 69 | self._update_callback() 70 | 71 | def _start_service_discovery(self): 72 | self.zeroconf = Zeroconf() 73 | for mdns_type in data_helper.KNOWN_MDNS_SERVICES.values(): 74 | ServiceBrowser(self.zeroconf, mdns_type, self) 75 | 76 | def get_virtual_network_gateway_service_endpoints(self): 77 | return [f"{s['hostname'][:-1]}:{s['port']}" for s in self.service_reg_virt_lan_gw.values()] 78 | 79 | def get_lan_gateway_endpoints(self): 80 | return [f"{s['address']}:{s['port']}" for s in self.service_reg_lan_gw.values()] -------------------------------------------------------------------------------- /eo_man/controller/network_gateway_detector.py: -------------------------------------------------------------------------------- 1 | import time 2 | import threading 3 | from esp2_gateway_adapter.esp3_tcp_com import TCP2SerialCommunicator, detect_lan_gateways 4 | 5 | class NetworkGatewayDetector(threading.Thread): 6 | 7 | 8 | def __init__(self, callback_esp3_gw, callback_esp2_gw): 9 | self._running = False 10 | self.callback_esp3_gw = callback_esp3_gw 11 | self.callback_esp2_gw = callback_esp2_gw 12 | 13 | def stop(self): 14 | self._running = False 15 | self.join() 16 | 17 | def run(self): 18 | if not self._running: 19 | self._running = True 20 | 21 | while self._running: 22 | 23 | esp3_gw_list = detect_lan_gateways() 24 | for gw in esp3_gw_list: 25 | self.callback_esp3_gw(gw) 26 | 27 | 28 | time.sleep(5) 29 | -------------------------------------------------------------------------------- /eo_man/controller/serial_port_detector.py: -------------------------------------------------------------------------------- 1 | import serial 2 | from serial import rs485 3 | import serial.tools.list_ports 4 | import logging 5 | import sys 6 | 7 | from esp2_gateway_adapter.esp3_serial_com import ESP3SerialCommunicator 8 | from esp2_gateway_adapter.esp3_tcp_com import TCP2SerialCommunicator, detect_lan_gateways 9 | from esp2_gateway_adapter.esp2_tcp_com import ESP2TCP2SerialCommunicator 10 | 11 | from eltakobus.serial import RS485SerialInterfaceV2 12 | from eltakobus.message import ESP2Message 13 | from eltakobus.util import b2s 14 | 15 | from .app_bus import AppBusEventType, AppBus 16 | from ..data.data_helper import GatewayDeviceType 17 | from ..data.const import GatewayDeviceType as GDT, GATEWAY_DISPLAY_NAMES as GDN 18 | 19 | class SerialPortDetector: 20 | 21 | ## not used only for documentation 22 | # DATA = [ 23 | # {'USB VID': 'PID=0403:6001', 'Manufacturer': 'FTDI', 'Device_Type': GatewayDeviceType.EltakoFAM14}, 24 | # {'USB VID': 'PID=0403:6010', 'Manufacturer': 'FTDI', 'Device_Type': GatewayDeviceType.GatewayEltakoFGW14USB}, 25 | # {'USB VID': 'PID=0403:6001', 'Manufacturer': 'FTDI', 'Device_Type': GatewayDeviceType.USB300}, 26 | # ] 27 | 28 | def __init__(self, app_bus: AppBus): 29 | self.app_bus = app_bus 30 | 31 | @classmethod 32 | def print_device_info(cls): 33 | ports = serial.tools.list_ports.comports() 34 | 35 | for port in ports: 36 | logging.getLogger().info(f"Port: {port.device}") 37 | logging.getLogger().info(f"Description: {port.description}") 38 | logging.getLogger().info(f"HWID: {port.hwid}") 39 | logging.getLogger().info(f"Manufacturer: {port.manufacturer}") 40 | logging.getLogger().info(f"Interface: {port.interface}") 41 | logging.getLogger().info(f"Location: {port.location}") 42 | logging.getLogger().info(f"Name: {port.name}") 43 | logging.getLogger().info(f"PID: {port.pid}") 44 | logging.getLogger().info(f"Product: {port.product}") 45 | logging.getLogger().info(f"Serial Number: {port.serial_number}") 46 | 47 | 48 | ser = serial.Serial(port.device) 49 | logging.getLogger().info(f"Baud rate: {ser.baudrate}") 50 | ser.close() 51 | 52 | logging.getLogger().info("\n") 53 | 54 | 55 | async def async_get_gateway2serial_port_mapping(self) -> dict[str:list[str]]: 56 | 57 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"Start detecting serial ports", 'color':'grey'}) 58 | 59 | if sys.platform.startswith('win'): 60 | # ports = ['COM%s' % (i + 1) for i in range(256)] 61 | ports = [d.device for d in serial.tools.list_ports.comports()] 62 | else: 63 | raise NotImplementedError("Detection of devices under other systems than windows is not yet supported!") 64 | 65 | # ports = [p.device for p in _ports if p.vid == self.USB_VENDOR_ID] 66 | 67 | fam14 = GDT.EltakoFAM14.value 68 | esp3_gw = GDT.ESP3.value 69 | famusb = GDT.EltakoFAMUSB.value 70 | fgw14usb = GDT.EltakoFGW14USB.value 71 | result = { fam14: [], esp3_gw: [], famusb: [], fgw14usb: [], 'all': [] } 72 | 73 | count = 0 74 | for baud_rate in [9600, 57600]: 75 | for port in ports: 76 | count += 1 77 | # take in 10 as one step and start with 10 to see directly process is running 78 | progress = min(round((count/(2*256.0))*10)*10 + 10, 100) 79 | self.app_bus.fire_event(AppBusEventType.DEVICE_ITERATION_PROGRESS, progress) 80 | 81 | try: 82 | # is faster to precheck with serial 83 | s = serial.Serial(port, baudrate=baud_rate, timeout=0.2) 84 | s.rs485_mode = serial.rs485.RS485Settings() 85 | s.close() 86 | 87 | # test esp3 devices 88 | if baud_rate == 57600: 89 | s = ESP3SerialCommunicator(port, auto_reconnect=False) 90 | s.start() 91 | if not s.is_serial_connected.wait(1): 92 | break 93 | 94 | base_id = await s.async_base_id 95 | if base_id and isinstance(base_id, list) and port not in result['all']: 96 | result[esp3_gw].append(port) 97 | result['all'].append(port) 98 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"USB300 detected on serial port {port},(baudrate: {baud_rate})", 'color':'lightgreen'}) 99 | s.stop() 100 | continue 101 | 102 | s.stop() 103 | 104 | # test fam14, fgw14-usb and fam-usb 105 | s = RS485SerialInterfaceV2(port, baud_rate=baud_rate, delay_message=0.2, auto_reconnect=False) 106 | s.start() 107 | if not s.is_serial_connected.wait(1): 108 | break 109 | 110 | # test fam14 111 | if s.suppress_echo and port not in result['all']: 112 | result[fam14].append(port) 113 | result['all'].append(port) 114 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"FAM14 detected on serial port {port},(baudrate: {baud_rate})", 'color':'lightgreen'}) 115 | s.stop() 116 | continue 117 | 118 | # test fam-usb 119 | if baud_rate == 9600: 120 | # try to get base id of fam-usb to test if device is fam-usb 121 | base_id = await self.async_get_base_id_for_fam_usb(s, None) 122 | # fam14 can answer on both baud rates but fam-usb cannot echo 123 | if base_id is not None and base_id != '00-00-00-00' and not s.suppress_echo and port not in result['all']: 124 | result[famusb].append(port) 125 | result['all'].append(port) 126 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"FAM-USB detected on serial port {port},(baudrate: {baud_rate})", 'color':'lightgreen'}) 127 | s.stop() 128 | continue 129 | 130 | # fgw14-usb 131 | if baud_rate == 57600: 132 | if not s.suppress_echo and port not in result['all']: 133 | result[fgw14usb].append(port) 134 | result['all'].append(port) 135 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"FGW14-USB could be on serial port {port},(baudrate: {baud_rate})", 'color':'lightgreen'}) 136 | s.stop() 137 | continue 138 | 139 | s.stop() 140 | 141 | except Exception as e: 142 | pass 143 | 144 | self.app_bus.fire_event(AppBusEventType.DEVICE_ITERATION_PROGRESS, 0) 145 | return result 146 | 147 | 148 | async def async_get_base_id_for_fam_usb(self, fam_usb:RS485SerialInterfaceV2, callback) -> str: 149 | base_id:str = None 150 | try: 151 | fam_usb.set_callback( None ) 152 | 153 | # get base id 154 | data = b'\xAB\x58\x00\x00\x00\x00\x00\x00\x00\x00\x00' 155 | # timeout really requires for this command sometimes 1sec! 156 | response:ESP2Message = await fam_usb.exchange(ESP2Message(bytes(data)), ESP2Message, retries=3, timeout=1) 157 | base_id = b2s(response.body[2:6]) 158 | except: 159 | pass 160 | finally: 161 | fam_usb.set_callback( callback ) 162 | 163 | return base_id -------------------------------------------------------------------------------- /eo_man/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/data/__init__.py -------------------------------------------------------------------------------- /eo_man/data/app_info.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tomli 3 | import requests 4 | 5 | class ApplicationInfo(): 6 | 7 | app_info:dict[str:str]=None 8 | pypi_info_latest_versino:dict=None 9 | pypi_info_current_version:dict=None 10 | 11 | @classmethod 12 | def get_app_info(cls, filename:str=None): 13 | 14 | if not cls.app_info: 15 | 16 | app_info = {} 17 | parent_folder = os.path.join(os.path.dirname(__file__), '..') 18 | pyproject_file = os.path.join(parent_folder, 'pyproject.toml') 19 | if not os.path.isfile(pyproject_file): 20 | pyproject_file = os.path.join(parent_folder, '..', 'pyproject.toml') 21 | if not os.path.isfile(pyproject_file): 22 | pyproject_file = None 23 | 24 | if pyproject_file: 25 | with open(pyproject_file, "rb") as f: 26 | pyproject_data = tomli.load(f) 27 | app_info['version'] = pyproject_data["project"]["version"] 28 | app_info['name'] = pyproject_data["project"]["name"] 29 | app_info['author'] = ', '.join([ f"{a.get('name')} {a.get('email', '')}".strip() for a in pyproject_data["project"]["authors"]]) 30 | app_info['home-page'] = pyproject_data["project"]["urls"]["Homepage"] 31 | app_info['license'] = pyproject_data["project"]["license"]["text"] 32 | 33 | if not filename: 34 | parent_folder = os.path.join(os.path.dirname(__file__), '..', '..') 35 | for f in os.listdir(parent_folder): 36 | if os.path.isdir(os.path.join(parent_folder, f)): 37 | # for installed package => get info from package metadata folder 38 | if f.startswith('eo_man-') and f.endswith('.dist-info'): 39 | filename = os.path.join(parent_folder, f, 'METADATA') 40 | break 41 | # for development environment => get info from built package (needs to be built first) 42 | if 'eo_man.egg-info' in f: 43 | filename = os.path.join(parent_folder, f, 'PKG-INFO') 44 | break 45 | 46 | if filename and os.path.isfile(filename): 47 | with open(filename, 'r', encoding='utf-8') as file: 48 | for l in file.readlines(): 49 | if l.startswith('Name:'): 50 | app_info['name'] = l.split(':',1)[1].strip() 51 | elif l.startswith('Version:'): 52 | app_info['version'] = l.split(':',1)[1].strip() 53 | elif l.startswith('Summary:'): 54 | app_info['summary'] = l.split(':',1)[1].strip() 55 | elif l.startswith('Home-page:'): 56 | app_info['home-page'] = l.split(':',1)[1].strip() 57 | elif l.startswith('Author:'): 58 | app_info['author'] = l.split(':',1)[1].strip() 59 | elif l.startswith('License:'): 60 | app_info['license'] = l.split(':',1)[1].strip() 61 | elif l.startswith('Requires-Python:'): 62 | app_info['requires-python'] = l.split(':',1)[1].strip() 63 | 64 | 65 | # get latest version 66 | cls.pypi_info_latest_versino = cls._get_info_from_pypi() 67 | app_info['lastest_available_version'] = cls.pypi_info_latest_versino.get('info', {}).get('version', None) 68 | # get current/installed version 69 | if app_info['version'] is not None and app_info['version'] != '': 70 | cls.pypi_info_current_version = cls._get_info_from_pypi(app_info['version']) 71 | 72 | cls.app_info = app_info 73 | 74 | return cls.app_info 75 | 76 | @classmethod 77 | def _get_info_from_pypi(cls, version:str=''): 78 | if version is not None and version != '': 79 | if not version.startswith('/') and not version.endswith('/'): 80 | version = version + '/' 81 | 82 | url = f"https://pypi.org/pypi/eo-man/{version}json" 83 | 84 | response = requests.get(url) 85 | if response.status_code == 200: 86 | return response.json() 87 | 88 | return {} 89 | 90 | 91 | @classmethod 92 | def get_app_info_as_str(cls, separator:str='\n', prefix:str='') -> str: 93 | result = '' 94 | for k, v in cls.get_app_info().items(): 95 | result += f"{prefix}{k.title()}: {v}{separator}" 96 | return result 97 | 98 | @classmethod 99 | def get_package_name(cls) -> str: 100 | return cls.get_app_info().get('name', 'unknown') 101 | 102 | @classmethod 103 | def get_version(cls) -> str: 104 | return cls.get_app_info().get('version', 'unknown') 105 | 106 | @classmethod 107 | def get_summary(cls) -> str: 108 | return cls.get_app_info().get('summery', 'unknown') 109 | 110 | @classmethod 111 | def get_home_page(cls) -> str: 112 | return cls.get_app_info().get('home-page', 'unknown') 113 | 114 | @classmethod 115 | def get_author(cls) -> str: 116 | return cls.get_app_info().get('author', 'unknown') 117 | 118 | @classmethod 119 | def get_license(cls) -> str: 120 | return cls.get_app_info().get('license', 'unknown') 121 | 122 | @classmethod 123 | def get_requires_python(cls) -> str: 124 | return cls.get_app_info().get('requires-python', 'unknown') 125 | 126 | @classmethod 127 | def get_lastest_available_version(cls) -> str: 128 | return cls.get_app_info().get('lastest_available_version', 'unknown') 129 | 130 | @classmethod 131 | def is_version_up_to_date(cls) -> bool: 132 | cv = cls.get_app_info().get('version', '0.0.0') 133 | lv = cls.pypi_info_latest_versino['info']['version'] 134 | 135 | for i in range(0,3): 136 | l = int(lv.split('.')[i]) 137 | c = int(cv.split('.')[i]) 138 | if l > c: 139 | return False 140 | elif c > l: 141 | return True 142 | return True -------------------------------------------------------------------------------- /eo_man/data/application_data.py: -------------------------------------------------------------------------------- 1 | from typing import Final 2 | import yaml 3 | 4 | from .device import Device 5 | from .filter import DataFilter 6 | from .recorded_message import RecordedMessage 7 | 8 | import pickle 9 | 10 | class ApplicationData(): 11 | 12 | class_version:Final = '1.0.0' 13 | 14 | def __init__(self, 15 | version:str='unknown', 16 | selected_data_filter:str=None, data_filters:dict[str:DataFilter]={}, 17 | devices:dict[str:Device]={}, 18 | recoreded_messages:list[RecordedMessage]=[]): 19 | 20 | self.application_version:str = version 21 | 22 | self.selected_data_filter_name:str=selected_data_filter 23 | self.data_filters:dict[str:DataFilter] = data_filters 24 | 25 | self.devices:dict[str:Device] = devices 26 | 27 | self.recoreded_messages:list[RecordedMessage] = recoreded_messages 28 | 29 | self.send_message_template_list: list[str] = [] 30 | 31 | 32 | translations:dict[str:str] ={ 33 | 'name: HA Contoller': 'name: HA Controller', 34 | '(Wireless Tranceiver)': '(Wireless Transceiver)' 35 | } 36 | 37 | @classmethod 38 | def read_from_file(cls, filename:str): 39 | result = ApplicationData() 40 | 41 | file_content = None 42 | with open(filename, 'rb') as file: 43 | file_content = pickle.loads(file) 44 | 45 | if isinstance(file_content, ApplicationData): 46 | result = file_content 47 | return result 48 | 49 | # to be downwards compatible 50 | if isinstance(file_content, dict) and len(file_content) > 0 and isinstance(list(file_content.values())[0], Device): 51 | result.devices = file_content 52 | 53 | if hasattr(file_content, 'devices'): 54 | result.devices = file_content.devices 55 | 56 | if hasattr(file_content, 'data_filters'): 57 | result.data_filters = file_content.data_filters 58 | 59 | if hasattr(file_content, 'selected_data_filter_name'): 60 | result.selected_data_filter_name = file_content.selected_data_filter_name 61 | 62 | if hasattr(file_content, 'application_version'): 63 | result.application_version = file_content.application_version 64 | 65 | return result 66 | 67 | @classmethod 68 | def _migrate(cls, obj): 69 | """required to make different versions compatibel""" 70 | if not hasattr(obj, 'recoreded_messages'): 71 | setattr(obj, 'recoreded_messages', []) 72 | 73 | if not hasattr(obj, 'send_message_template_list'): 74 | setattr(obj, 'send_message_template_list', []) 75 | 76 | for d in obj.devices.values(): 77 | if not hasattr(d, 'additional_fields'): 78 | d.additional_fields = {} 79 | if not hasattr(d, 'ha_platform'): 80 | d.ha_platform = None 81 | if not hasattr(d, 'eep'): 82 | d.eep = None 83 | if not hasattr(d, 'key_function'): 84 | d.key_function = None 85 | if not hasattr(d, 'comment'): 86 | d.comment = None 87 | if not hasattr(d, 'device_type'): 88 | d.device_type = None 89 | 90 | if 'sender' in d.additional_fields and 'id' in d.additional_fields['sender']: 91 | try: 92 | sender_id = d.additional_fields['sender']['id'] 93 | if isinstance(sender_id, str) and '-' in sender_id: 94 | id = int( sender_id.split('-')[-1], 16 ) 95 | if id > 128: id -= 128 96 | hex_id = hex(id)[2:].upper() 97 | if len(hex_id) == 1: hex_id = "0"+hex_id 98 | d.additional_fields['sender']['id'] = hex_id 99 | except: 100 | pass 101 | 102 | @classmethod 103 | def read_from_yaml_file(cls, filename:str): 104 | with open(filename, 'r') as file: 105 | file_content = file.read() 106 | for k,v in cls.translations.items(): 107 | file_content.replace(k,v) 108 | app_data = yaml.load(file_content, Loader=yaml.Loader) 109 | cls._migrate(app_data) 110 | 111 | return app_data 112 | 113 | 114 | # @classmethod 115 | # def from_yaml(cls, constructor, node): 116 | # return cls(version=node.version, 117 | # selected_data_filter=node.selected_data_filter, 118 | # data_filters=node.data_filters, 119 | # devices=node.devices 120 | # ) 121 | 122 | @classmethod 123 | def write_to_file(cls, filename:str, application_data): 124 | with open(filename, 'wb') as file: 125 | pickle.dump(application_data, file) 126 | 127 | @classmethod 128 | def write_to_yaml_file(cls, filename:str, application_data): 129 | with open(filename, 'w') as file: 130 | yaml.dump(application_data, file) -------------------------------------------------------------------------------- /eo_man/data/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Eltako integration.""" 2 | from enum import Enum 3 | from strenum import StrEnum 4 | import logging 5 | 6 | from typing import Final 7 | 8 | from homeassistant.const import * 9 | 10 | DOMAIN: Final = "eltako" 11 | DATA_ELTAKO: Final = "eltako" 12 | DATA_ENTITIES: Final = "entities" 13 | ELTAKO_GATEWAY: Final = "gateway" 14 | CONF_GATEWAY_ADDRESS: Final ="address" 15 | ELTAKO_CONFIG: Final = "config" 16 | MANUFACTURER: Final = "Eltako" 17 | 18 | ERROR_INVALID_GATEWAY_PATH: Final = "Invalid gateway path" 19 | ERROR_NO_SERIAL_PATH_AVAILABLE: Final = "No serial path available. Try to reconnect your usb plug." 20 | ERROR_NO_GATEWAY_CONFIGURATION_AVAILABLE: Final = "No gateway configuration available. Enter gateway into '/homeassistant/configuration.yaml'." 21 | 22 | SIGNAL_RECEIVE_MESSAGE: Final = "receive_message" 23 | SIGNAL_SEND_MESSAGE: Final = "send_message" 24 | EVENT_BUTTON_PRESSED: Final = "btn_pressed" 25 | EVENT_CONTACT_CLOSED: Final = "contact_closed" 26 | 27 | LOGGER: Final = logging.getLogger(DOMAIN) 28 | 29 | CONF_UNKNOWN: Final = "unknown" 30 | CONF_REGISTERED_IN: Final = "registered_in" 31 | CONF_COMMENT: Final = "comment" 32 | CONF_EEP: Final = "eep" 33 | CONF_DEVICE_ID: Final = "device_id" 34 | CONF_SENSOR_KEY: Final = "sensor_key" 35 | CONF_SENSOR_KEY_FUNCTION: Final = "sensor_key_func" 36 | CONF_MEMORY_ENTRIES: Final = "memory_entries" 37 | CONF_EXTERNAL_ID: Final = "external_id" 38 | CONF_CONFIGURED_IN_DEVICES: Final = "configured_in_devices" 39 | CONF_MEMORY_LINE: Final = "memory_line" 40 | CONF_SWITCH_BUTTON: Final = "switch_button" 41 | CONF_SENDER: Final = "sender" 42 | CONF_SENSOR: Final = "sensor" 43 | CONF_GERNERAL_SETTINGS: Final = "general_settings" 44 | CONF_SHOW_DEV_ID_IN_DEV_NAME: Final = "show_dev_id_in_dev_name" 45 | CONF_ENABLE_TEACH_IN_BUTTONS: Final = "enable_teach_in_buttons" 46 | CONF_FAST_STATUS_CHANGE: Final = "fast_status_change" 47 | GATEWAY_DEFAULT_NAME: Final = "EnOcean ESP2 Gateway" 48 | CONF_GATEWAY: Final = "gateway" 49 | CONF_GATEWAY_ID: Final = "gateway_id" 50 | CONF_GATEWAY_DESCRIPTION: Final = "gateway_description" 51 | CONF_BASE_ID: Final = "base_id" 52 | CONF_DEVICE_TYPE: Final = "device_type" 53 | CONF_SERIAL_PATH: Final = "serial_path" 54 | CONF_CUSTOM_SERIAL_PATH: Final = "custom_serial_path" 55 | CONF_MAX_TARGET_TEMPERATURE: Final = "max_target_temperature" 56 | CONF_MIN_TARGET_TEMPERATURE: Final = "min_target_temperature" 57 | CONF_ROOM_THERMOSTAT: Final = "thermostat" 58 | CONF_COOLING_MODE: Final = "cooling_mode" 59 | 60 | CONF_ID_REGEX: Final = "^([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})-([0-9a-fA-F]{2})( (left|right))?$" 61 | CONF_METER_TARIFFS: Final = "meter_tariffs" 62 | CONF_TIME_CLOSES: Final = "time_closes" 63 | CONF_TIME_OPENS: Final = "time_opens" 64 | CONF_INVERT_SIGNAL: Final = "invert_signal" 65 | CONF_VOC_TYPE_INDEXES: Final = "voc_type_indexes" 66 | 67 | 68 | class LANGUAGE_ABBREVIATION(StrEnum): 69 | LANG_ENGLISH = 'en' 70 | LANG_GERMAN = 'de' 71 | 72 | 73 | PLATFORMS: Final = [ 74 | Platform.LIGHT, 75 | Platform.BINARY_SENSOR, 76 | Platform.SENSOR, 77 | Platform.SWITCH, 78 | Platform.COVER, 79 | Platform.CLIMATE, 80 | Platform.BUTTON, 81 | ] 82 | 83 | class GatewayDeviceType(str, Enum): 84 | GatewayEltakoFAM14 = 'fam14' 85 | GatewayEltakoFGW14USB = 'fgw14usb' 86 | GatewayEltakoFAMUSB = 'fam-usb' # ESP2 transceiver: https://www.eltako.com/en/product/professional-standard-en/three-phase-energy-meters-and-one-phase-energy-meters/fam-usb/ 87 | EltakoFTD14 = 'ftd14' 88 | EnOceanUSB300 = 'enocean-usb300' 89 | EltakoFAM14 = 'fam14' 90 | EltakoFGW14USB = 'fgw14usb' 91 | EltakoFAMUSB = 'fam-usb' 92 | USB300 = 'enocean-usb300' 93 | ESP3 = 'esp3-gateway' 94 | LAN = 'lan' 95 | MGW_LAN = 'mgw-lan' 96 | EUL_LAN = 'EUL_LAN' 97 | LAN_ESP2 = "lan-gw-esp2" 98 | VirtualNetworkAdapter = 'esp2-netowrk-reverse-bridge' # subtype of LAN_ESP2 99 | 100 | @classmethod 101 | def indexOf(cls, value): 102 | return list(cls).index(value) 103 | 104 | @classmethod 105 | def get_by_index(cls, index): 106 | return list(cls)[index] 107 | 108 | @classmethod 109 | def find(cls, value): 110 | for t in GatewayDeviceType: 111 | if t.value.lower() == value.lower(): 112 | return t 113 | return None 114 | 115 | @classmethod 116 | def getValueByKeyOrValue(cls, input, default_value = None): 117 | if input in GatewayDeviceType.__members__: 118 | return GatewayDeviceType.__members__[input].value 119 | if input in [m.value for m in GatewayDeviceType]: 120 | return input 121 | return default_value 122 | 123 | @classmethod 124 | def is_transceiver(cls, dev_type) -> bool: 125 | return dev_type in [GatewayDeviceType.GatewayEltakoFAMUSB, GatewayDeviceType.EnOceanUSB300, GatewayDeviceType.USB300, GatewayDeviceType.ESP3, GatewayDeviceType.LAN, 126 | GatewayDeviceType.LAN_ESP2, GatewayDeviceType.MGW_LAN, GatewayDeviceType.EUL_LAN] 127 | 128 | @classmethod 129 | def is_bus_gateway(cls, dev_type) -> bool: 130 | return dev_type in [GatewayDeviceType.GatewayEltakoFAM14, GatewayDeviceType.GatewayEltakoFGW14USB, 131 | GatewayDeviceType.EltakoFAM14, GatewayDeviceType.EltakoFAMUSB, GatewayDeviceType.EltakoFGW14USB] 132 | 133 | @classmethod 134 | def is_esp2_gateway(cls, dev_type) -> bool: 135 | return dev_type in [GatewayDeviceType.GatewayEltakoFAM14, GatewayDeviceType.GatewayEltakoFGW14USB, GatewayDeviceType.GatewayEltakoFAMUSB, 136 | GatewayDeviceType.EltakoFAM14, GatewayDeviceType.EltakoFAMUSB, GatewayDeviceType.EltakoFGW14USB, GatewayDeviceType.LAN_ESP2, 137 | GatewayDeviceType.VirtualNetworkAdapter] 138 | 139 | @classmethod 140 | def is_lan_gateway(cls, dev_type) -> bool: 141 | return dev_type in [GatewayDeviceType.LAN, GatewayDeviceType.LAN_ESP2, GatewayDeviceType.MGW_LAN, GatewayDeviceType.EUL_LAN, GatewayDeviceType.VirtualNetworkAdapter] 142 | 143 | 144 | BAUD_RATE_DEVICE_TYPE_MAPPING: dict = { 145 | GatewayDeviceType.EltakoFAM14: 57600, 146 | GatewayDeviceType.EltakoFGW14USB: 57600, 147 | GatewayDeviceType.EltakoFAMUSB: 9600, 148 | GatewayDeviceType.USB300: 57600, 149 | GatewayDeviceType.ESP3: 57600, 150 | } 151 | 152 | GATEWAY_DISPLAY_NAMES = { 153 | GatewayDeviceType.EltakoFAM14: "FAM14 (ESP2)", 154 | GatewayDeviceType.EltakoFGW14USB: 'FGW14-USB (ESP2)', 155 | GatewayDeviceType.EltakoFAMUSB: 'FAM-USB (ESP2)', 156 | # GatewayDeviceType.USB300: 'ESP3 Gateway', 157 | GatewayDeviceType.ESP3: 'ESP3 Gateway', 158 | GatewayDeviceType.LAN: 'LAN Gateway (ESP3)', 159 | GatewayDeviceType.LAN_ESP2: "Home Assistant - Virtual Gateway Adapter", 160 | } 161 | 162 | def get_display_names(): 163 | result = [] 164 | for v in GATEWAY_DISPLAY_NAMES.values(): 165 | if v not in result: 166 | result.append(v) 167 | return result 168 | 169 | def get_gateway_type_by_name(device_type) -> GatewayDeviceType: 170 | for t, dn in GATEWAY_DISPLAY_NAMES.items(): 171 | if device_type.lower() in dn.lower(): 172 | return t 173 | return None -------------------------------------------------------------------------------- /eo_man/data/filter.py: -------------------------------------------------------------------------------- 1 | from .device import Device 2 | 3 | class DataFilter(): 4 | 5 | def __init__(self, name:str, 6 | global_filter:list[str]=[], 7 | device_address_filter:list[str]=[], 8 | device_external_address_filter:list[str]=[], 9 | device_type_filter:list[str]=[], 10 | device_eep_filter:list[str]=[]): 11 | self.name = name 12 | self.global_filter = global_filter 13 | self.device_address_filter = device_address_filter 14 | self.device_external_address_filter = device_external_address_filter 15 | self.device_type_filter = device_type_filter 16 | self.device_eep_filter = device_eep_filter 17 | 18 | 19 | def filter_device(self, device:Device): 20 | # check address 21 | for f in self.device_address_filter + self.global_filter: 22 | if device.address and f.upper() in device.address.upper(): 23 | return True 24 | 25 | # check external id 26 | for f in self.device_external_address_filter + self.global_filter: 27 | if device.external_id and f.upper() in device.external_id.upper(): 28 | return True 29 | 30 | # check device type 31 | for f in self.device_type_filter + self.global_filter: 32 | if device.device_type and f.upper() in device.device_type.upper(): 33 | return True 34 | 35 | # check eep 36 | for f in self.device_eep_filter + self.global_filter: 37 | if device.eep and f.upper() in device.eep.upper(): 38 | return True 39 | 40 | for f in self.global_filter: 41 | # key function 42 | if device.key_function and f.upper() in device.key_function.upper(): 43 | return True 44 | 45 | # comment 46 | if device.comment and f.upper() in device.comment.upper(): 47 | return True 48 | 49 | # version 50 | if device.version and f.upper() in device.version.upper(): 51 | return True 52 | 53 | # ha platform 54 | if device.ha_platform and f.upper() in device.ha_platform.upper(): 55 | return True 56 | 57 | if self.find_in_dict(device.additional_fields, f.upper()): 58 | return True 59 | 60 | return False 61 | 62 | 63 | def find_in_dict(self, additional_fields:dict, filter:str) -> bool: 64 | for key, value in additional_fields.items(): 65 | if isinstance(value, dict): 66 | if self.find_in_dict(value, filter): 67 | return True 68 | elif filter in str(value).upper(): 69 | return True 70 | return False -------------------------------------------------------------------------------- /eo_man/data/ha_config_generator.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from homeassistant.const import CONF_ID, CONF_DEVICES, CONF_NAME 4 | 5 | from ..controller.app_bus import AppBus, AppBusEventType 6 | 7 | from .app_info import ApplicationInfo as AppInfo 8 | from .data_manager import DataManager 9 | from .device import Device 10 | from .const import * 11 | from . import data_helper 12 | 13 | class HomeAssistantConfigurationGenerator(): 14 | 15 | LOCAL_SENDER_OFFSET_ID = '00-00-B0-00' 16 | 17 | def __init__(self, app_bus:AppBus, data_manager:DataManager): 18 | self.app_bus = app_bus 19 | self.data_manager = data_manager 20 | 21 | def get_description(self) -> str: 22 | return f""" 23 | # DESCRIPTION: 24 | # 25 | # This is an automatically generated Home Assistant Configuration for the Eltako Integration (https://github.com/grimmpp/home-assistant-eltako) 26 | # Generated at {datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")} and by: 'EnOcean Device Manager': 27 | {AppInfo.get_app_info_as_str(prefix='# ')}# 28 | # Hints: 29 | # * The auto-generation considers all devices which are marked with 'Export to HA' = True. 30 | # * Decentralized devices are entered into the list under devices for every gateway. If you are using more than one gateway you probably want to have those only once listed. Please remove "dupplicated" entries. 31 | # * FAM14 gatways can be easily exchanged by FGW14-USB gateways. You just need to change the value of 'device_type' from 'fam14' to 'fgw14usb'. 32 | # * 'id' of the gateways are random and are just counted up. You can simply change them if needed. Just ensure that they are unique and not specified more than once. 33 | # 34 | """ 35 | 36 | def get_gateway_by(self, gw_d:Device) -> GatewayDeviceType: 37 | gw_type = None 38 | for t in GatewayDeviceType: 39 | if GATEWAY_DISPLAY_NAMES[t.value].lower() in gw_d.device_type.lower(): 40 | return t.value 41 | return None 42 | 43 | def perform_tests(self) -> str: 44 | device_list = [d for d in self.data_manager.devices.values() if not d.is_gateway() and d.use_in_ha] 45 | 46 | try: 47 | self.test_unique_sender_ids(device_list) 48 | return None 49 | except Exception as e: 50 | return str(e) 51 | 52 | def test_unique_sender_ids(self, device_list:list[Device]): 53 | sender_ids = {} 54 | for d in device_list: 55 | if 'sender' in d.additional_fields and 'id' in d.additional_fields['sender']: 56 | sender_id = d.additional_fields['sender']['id'] 57 | if int(sender_id, 16) < 1 or int(sender_id, 16) > 127: 58 | raise Exception(f"sender id '{sender_id}' of device '{d.external_id}' is no valid number between 1 and 127.") 59 | if sender_id in sender_ids: 60 | raise Exception(f"sender id '{sender_id}' is assigned more than once for at least device '{sender_ids[sender_id].external_id}' and '{d.external_id}'.") 61 | 62 | sender_ids[sender_id] = d 63 | 64 | def generate_ha_config(self, device_list:list[Device]) -> str: 65 | ha_platforms = set([str(d.ha_platform) for d in device_list if d.ha_platform is not None]) 66 | gateways = [d for d in device_list if d.is_gateway() and d.use_in_ha] 67 | devices = [d for d in device_list if not d.is_gateway() and d.use_in_ha] 68 | 69 | out = self.get_description() 70 | out += "\n" 71 | out += f"{DOMAIN}:\n" 72 | out += f" {CONF_GERNERAL_SETTINGS}:\n" 73 | out += f" {CONF_FAST_STATUS_CHANGE}: False\n" 74 | out += f" {CONF_SHOW_DEV_ID_IN_DEV_NAME}: False\n" 75 | out += f"\n" 76 | 77 | global_gw_id = 0 78 | # add fam14 gateways 79 | for gw_d in gateways: 80 | global_gw_id += 1 81 | out += f" {CONF_GATEWAY}:\n" 82 | out += f" - {CONF_ID}: {global_gw_id}\n" 83 | 84 | gw_fam14 = GatewayDeviceType.EltakoFAM14.value 85 | gw_fgw14usb = GatewayDeviceType.EltakoFGW14USB.value 86 | 87 | # if gw_d.is_fam14(): out += f" # you can simply change {gw_fam14} to {gw_fgw14usb}\n" 88 | out += f" {CONF_DEVICE_TYPE}: {GatewayDeviceType.getValueByKeyOrValue(gw_d.device_type)}\n" 89 | out += f" {CONF_BASE_ID}: {gw_d.base_id}\n" 90 | out += f" # {CONF_COMMENT}: {gw_d.comment}\n" 91 | if gw_d.device_type == GatewayDeviceType.LAN: 92 | out += f" {CONF_GATEWAY_ADDRESS}: {gw_d.additional_fields['address'].split(':')[0]}\n" 93 | if ':' in gw_d.additional_fields['address']: 94 | out += f" {CONF_PORT}: {gw_d.additional_fields['address'].split(':')[1]}\n" 95 | out += f" {CONF_DEVICES}:\n" 96 | 97 | for platform in ha_platforms: 98 | if platform != '': 99 | out += f" {platform}:\n" 100 | for device in devices: 101 | if device.ha_platform == platform: 102 | # add devices 103 | out += self.config_section_from_device_to_string(gw_d, device, True, 0) + "\n\n" 104 | # logs 105 | out += "\n" 106 | out += "logger:\n" 107 | out += " default: info\n" 108 | out += " logs:\n" 109 | out += f" {DOMAIN}: info\n" 110 | 111 | return out 112 | 113 | 114 | 115 | def config_section_from_device_to_string(self, gateway:Device, device:Device, is_list:bool, space_count:int=0) -> str: 116 | out = "" 117 | spaces = space_count*" " + " " 118 | 119 | # add user comment 120 | if device.comment: 121 | out += spaces + f"# {device.comment}\n" 122 | 123 | # list related devices for comment 124 | rel_devs = [] 125 | for d in self.data_manager.get_related_devices(device.external_id): 126 | rel_devs.append( f"{d.name} (Type: {d.device_type}, Adr: {d.address})" ) 127 | if len(rel_devs): 128 | out += spaces + f"# Related devices: {', '.join(rel_devs)}\n" 129 | 130 | info = data_helper.find_device_info_by_device_type(device.device_type) 131 | if info and 'PCT14-key-function' in info: 132 | kf = info['PCT14-key-function'] 133 | fg = info['PCT14-function-group'] 134 | out += spaces[:-2] + f" # Use 'Write HA senders to devices' button or enter manually sender id in PCT14 into function group {fg} with function {kf} \n" 135 | adr = device.address if gateway.is_wired_gateway() and gateway.base_id == device.base_id else device.external_id 136 | out += spaces[:-2] + f"- {CONF_ID}: {adr.strip()}\n" 137 | out += spaces[:-2] + f" {CONF_NAME}: {device.name}\n" 138 | out += spaces[:-2] + f" {CONF_EEP}: {device.eep}\n" 139 | 140 | out += self.export_additional_fields(gateway, device.additional_fields, space_count) 141 | 142 | return out 143 | 144 | 145 | def export_additional_fields(self, gateway:Device, additional_fields:dict, space_count:int=0, parent_key:str=None) -> str: 146 | out = "" 147 | spaces = space_count*" " + " " 148 | for key in additional_fields.keys(): 149 | value = additional_fields[key] 150 | if parent_key in ['sender'] and key == 'id': 151 | if gateway.is_wired_gateway(): sender_offset_id = self.LOCAL_SENDER_OFFSET_ID 152 | else: sender_offset_id = gateway.base_id 153 | value = data_helper.a2s( int("0x"+value[-2:], base=16) + data_helper.a2i(sender_offset_id) ) 154 | if isinstance(value, str) or isinstance(value, int): 155 | if key not in [CONF_COMMENT, CONF_REGISTERED_IN]: 156 | if isinstance(value, str) and '?' in value: 157 | value += " # <= NEED TO BE COMPLETED!!!" 158 | out += f"{spaces}{key}: {value}\n" 159 | elif isinstance(value, dict): 160 | out += f"{spaces}{key}: \n" 161 | out += self.export_additional_fields(gateway, value, space_count+2, key) 162 | return out 163 | 164 | 165 | def save_as_yaml_to_file(self, filename:str): 166 | 167 | devices = self.data_manager.devices.values() 168 | 169 | config_str = self.generate_ha_config(devices) 170 | 171 | with open(filename, 'w', encoding="utf-8") as f: 172 | print(config_str, file=f) 173 | -------------------------------------------------------------------------------- /eo_man/data/homeassistant/__init__.py: -------------------------------------------------------------------------------- 1 | # ist is a mock homeassistant package to not need to install the whole homeassistant for the const file. 2 | # this could easily be removed and replaced by installing the original package. -------------------------------------------------------------------------------- /eo_man/data/message_history.py: -------------------------------------------------------------------------------- 1 | 2 | class MessageHistoryEntry(): 3 | 4 | def __init__(self, name:str, msg: bytes): 5 | self.message = msg 6 | self.name = name -------------------------------------------------------------------------------- /eo_man/data/pct14_data_manager.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | import json 3 | 4 | from eltakobus.device import * 5 | from eltakobus.util import AddressExpression 6 | 7 | from .. import LOGGER 8 | from . import data_helper 9 | 10 | from .ha_config_generator import HomeAssistantConfigurationGenerator 11 | from .const import GatewayDeviceType 12 | from .device import Device 13 | from .data_helper import b2s, a2s, a2i, add_addresses, find_device_info_by_device_type 14 | 15 | class PCT14DataManager: 16 | 17 | @classmethod 18 | async def get_devices_from_pct14(cls, filename:str) -> dict: 19 | devices = {} 20 | 21 | pct14_import = {} 22 | with open(filename, 'r') as file: 23 | import_data = xmltodict.parse(file.read()) 24 | pct14_import = import_data['exchange'] 25 | 26 | # detect fam14 first 27 | fam14_device:Device = await cls._create_fam14_device( pct14_import['rootdevice'] ) 28 | devices[fam14_device.external_id] = fam14_device 29 | 30 | for d in pct14_import['devices']['device']: 31 | 32 | dev_size = int(d['header']['addressrange']['#text']) 33 | for i in range(1, dev_size+1): 34 | 35 | device = await cls._create_device(d, fam14_device, channel=i) 36 | devices[device.external_id] = device 37 | 38 | for si in device.memory_entries: 39 | s:Device = Device.get_decentralized_device_by_sensor_info(si) 40 | devices[s.external_id] = s 41 | 42 | if device.is_ftd14(): 43 | s2:Device = Device.get_decentralized_device_by_sensor_info(si, device.additional_fields['second base id']) 44 | devices[s2.external_id] = s2 45 | 46 | return devices 47 | 48 | 49 | @classmethod 50 | async def write_sender_ids_into_existing_pct14_export(cls, source_filename:str, target_filename:str, devices:dict[str,Device], base_id:str): 51 | 52 | with open(source_filename) as xml_file: 53 | data_dict = xmltodict.parse(xml_file.read()) 54 | 55 | fam14_device:Device = await cls._create_fam14_device( data_dict['exchange']['rootdevice'] ) 56 | 57 | # iterate through PCT14 devices 58 | for d in data_dict['exchange']['devices']['device']: 59 | 60 | # interate through device channels 61 | dev_size = int(d['header']['addressrange']['#text']) 62 | hw_info = find_device_info_by_device_type(d['name']['#text']) 63 | if hw_info == {} or 'PCT14-key-function' not in hw_info: 64 | print(f"No HW Type fond for: {d['name']['#text']}") 65 | continue 66 | function_id = hw_info['PCT14-key-function'] 67 | 68 | for i in range(1, dev_size+1): 69 | 70 | # check if device is in eo_man 71 | external_id = cls._get_external_id(fam14_device, i, d) 72 | if external_id in devices: 73 | _d = devices[external_id] 74 | if 'sender' in _d.additional_fields and not cls._is_device_registered(_d, d, i, base_id, function_id): 75 | # check if HA sender is registered 76 | cls._add_ha_sender_id_into_pct14_xml(base_id, d, d['data']['rangeofid']['entry'], _d) 77 | else: 78 | LOGGER.debug(f"PCT14 Export Extender: device {_d.name} ('{_d.external_id}' already registered in actuator {d['name']['#text']})") 79 | 80 | # Convert the modified dictionary back to XML 81 | new_xml = xmltodict.unparse(data_dict, pretty=True) 82 | 83 | # Write the updated XML back to a file 84 | with open(target_filename, 'w') as xml_file: 85 | xml_file.write(new_xml) 86 | LOGGER.debug(f"PCT14 Export Extender: process completed.") 87 | 88 | @classmethod 89 | def _add_ha_sender_id_into_pct14_xml(cls, base_id, xml_device, xml_entries, device: Device): 90 | index_range = data_helper.find_device_class_by_name(xml_device['name']['#text']).sensor_address_range 91 | free_index = -1 92 | for i in index_range: 93 | free_index = i 94 | for n in xml_entries: 95 | if int(n['entry_number']) == i: 96 | free_index = -1 97 | break 98 | if free_index > -1: 99 | break 100 | 101 | sender_id = device.additional_fields['sender']['id'] 102 | if free_index > -1: 103 | xml_entries.append({ 104 | 'entry_number': free_index, 105 | 'entry_id': cls._get_sender_id(base_id, sender_id), 106 | 'entry_function': device.key_function, 107 | 'entry_button': 0, 108 | 'entry_channel': device.channel, 109 | 'entry_value': 0 110 | }) 111 | LOGGER.debug(f"PCT14 Export Extender: Added sender id '{sender_id}' to PCT14 export for device {device.name} '{device.external_id}'.") 112 | else: 113 | LOGGER.warning(f"PCT14 Export Extender: Cannot add sender id '{sender_id}' entry into device {device.name} '{device.external_id}'.") 114 | 115 | @classmethod 116 | def _get_sender_id(cls, base_id: str, sender_id:str): 117 | sender_offset_id = base_id 118 | str_id = data_helper.a2s( int(sender_id, base=16) + data_helper.a2i(sender_offset_id) ) 119 | int_id = int(''.join(str_id.split('-')[::-1]), base=16) 120 | return int_id 121 | 122 | 123 | @classmethod 124 | def _is_device_registered(cls, device:Device, pct14_device, channel:int, base_id:str, function_id:int): 125 | if not isinstance(pct14_device['data']['rangeofid']['entry'], list): 126 | return False 127 | 128 | ha_id = add_addresses(base_id, '00-00-00-'+device.additional_fields['sender']['id']) 129 | 130 | for e in pct14_device['data']['rangeofid']['entry']: 131 | 132 | is_registered = int(e['entry_channel']) & (2**(channel-1)) 133 | is_registered = is_registered and (int(e['entry_function']) == function_id) 134 | is_registered = is_registered and (b2s(cls._convert_sensor_id_to_bytes(e['entry_id'])) == ha_id) 135 | if is_registered: 136 | return True 137 | return False 138 | 139 | @classmethod 140 | def _get_external_id(cls, fam14:Device, channel:int, xm_device:dict) -> str: 141 | id = int(xm_device['header']['address']['#text']) + channel - 1 142 | return add_addresses(fam14.base_id, a2s(id)) 143 | 144 | @classmethod 145 | async def _create_device(cls, import_obj, fam14:Device, channel:int=1): 146 | bd = Device() 147 | bd.additional_fields = {} 148 | id = int(import_obj['header']['address']['#text']) + channel - 1 149 | bd.address = a2s( id ) 150 | bd.channel = channel 151 | bd.dev_size = int(import_obj['header']['addressrange']['#text']) 152 | bd.use_in_ha = True 153 | bd.base_id = fam14.base_id 154 | bd.device_type = import_obj['name']['#text'] 155 | if 'FAM14' in bd.device_type: bd.device_type = GatewayDeviceType.EltakoFAM14.value 156 | if 'FGW14' in bd.device_type: bd.device_type = GatewayDeviceType.EltakoFGW14USB.value 157 | if 'FTD14' in bd.device_type: bd.device_type = GatewayDeviceType.EltakoFTD14.value 158 | int_version = int(import_obj['header']['versionofsoftware']['#text']) 159 | hex_version = format(int_version, 'X') 160 | bd.version = f"{hex_version[0]}.{hex_version[1]}" 161 | bd.key_function = '' 162 | bd.comment = import_obj['description'].get('#text', '') 163 | if isinstance(import_obj['channels']['channel'], list): 164 | for c in import_obj['channels']['channel']: 165 | if int(c['@channelnumber']) == channel and len(c['@description']) > 0: 166 | bd.comment += f" - {c['@description']}" 167 | else: 168 | c = import_obj['channels']['channel'] 169 | if int(c['@channelnumber']) == channel and len(c['@description']) > 0: 170 | bd.comment += f" - {c['@description']}" 171 | 172 | bd.bus_device = True 173 | bd.external_id = cls._get_external_id(fam14, channel, import_obj) 174 | 175 | if 'entry' in import_obj['data']['rangeofid']: 176 | bd.memory_entries = cls._get_sensors_from_xml(bd, import_obj['data']['rangeofid']['entry']) 177 | else: 178 | bd.memory_entries = [] 179 | 180 | # print(f"{bd.device_type} {bd.address}") 181 | # print_memory_entires( bd.memory_entries) 182 | # print("\n") 183 | bd.name = f"{bd.device_type.upper()} - {bd.address}" 184 | if bd.comment not in [None, '']: 185 | bd.name = f"{bd.device_type.upper()} - {bd.comment}" 186 | else: 187 | if bd.dev_size > 1: 188 | bd.name += f" ({bd.channel}/{bd.dev_size})" 189 | 190 | Device.set_suggest_ha_config(bd) 191 | 192 | return bd 193 | 194 | @classmethod 195 | def _convert_xml_baseid(cls, xml_baseid): 196 | numbers = [] 197 | for i in range(0,4): 198 | number = format(int(xml_baseid['baseid_byte_'+str(i)]),'X') 199 | if len(number) == 1: number = '0'+number 200 | numbers.append(number) 201 | return '-'.join(numbers) 202 | 203 | # return f"{}-{format(int(xml_baseid['baseid_byte_1']),'2X')}-{format(int(xml_baseid['baseid_byte_2']),'2X')}-{format(int(xml_baseid['baseid_byte_3']),'2X')}" 204 | 205 | @classmethod 206 | async def _create_fam14_device(cls, import_obj): 207 | bd = Device() 208 | bd.additional_fields = {} 209 | xml_baseid = import_obj['rootdevicedata']['baseid'] 210 | bd.address = cls._convert_xml_baseid(xml_baseid) 211 | bd.channel = 1 212 | bd.dev_size = int(import_obj['header']['addressrange']['#text']) 213 | bd.use_in_ha = True 214 | bd.base_id = bd.address 215 | bd.device_type = FAM14 216 | bd.device_type = GatewayDeviceType.EltakoFAM14.value 217 | 218 | bd.version = "" #TODO: '.'.join(map(str,device.version)) 219 | bd.key_function = '' 220 | bd.comment = import_obj['description']['#text'] 221 | bd.bus_device = True 222 | bd.external_id = bd.base_id 223 | 224 | bd.memory_entries = [] 225 | if bd.comment not in [None, '']: 226 | bd.name = f"{bd.device_type} - {bd.comment}" 227 | else: 228 | bd.name = f"{bd.device_type} - {bd.address}" 229 | 230 | Device.set_suggest_ha_config(bd) 231 | 232 | return bd 233 | 234 | 235 | @classmethod 236 | def _convert_sensor_id_to_bytes(cls, id:str): 237 | hex_rep = format(int(id), 'X') 238 | if len(hex_rep) % 2 == 1: 239 | hex_rep = "0"+hex_rep 240 | hex_rep = [hex_rep[i:i+2] for i in range(0, len(hex_rep), 2)] 241 | hex_rep = hex_rep[::-1] 242 | hex_string = '-'.join(hex_rep) 243 | return AddressExpression.parse(hex_string)[0] 244 | 245 | 246 | @classmethod 247 | def _get_sensors_from_xml(cls, device:Device, sensors) -> list[SensorInfo]: 248 | return [cls._get_sensor_from_xml(device, s) for s in sensors] 249 | 250 | @classmethod 251 | def _get_sensor_from_xml(cls, device:Device, sensor) -> list[SensorInfo]: 252 | 253 | hw_info = find_device_info_by_device_type(device.device_type) 254 | if hw_info: 255 | hw_info = hw_info['hw-type'] 256 | else: 257 | hw_info = None 258 | 259 | return SensorInfo( 260 | dev_type = hw_info, 261 | sensor_id = cls._convert_sensor_id_to_bytes(sensor['entry_id']), 262 | dev_adr = a2i(device.address).to_bytes(4, byteorder = 'big'), 263 | key = int(sensor['entry_button']), 264 | dev_id = device.address, 265 | key_func = int(sensor['entry_function']), 266 | channel = int(sensor['entry_channel']), 267 | in_func_group=None, 268 | memory_line = int(sensor['entry_number']) 269 | ) -------------------------------------------------------------------------------- /eo_man/data/recorded_message.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from eltakobus.message import ESP2Message 3 | 4 | class RecordedMessage(): 5 | 6 | def __init__(self, message:ESP2Message, external_id:str, gateway_id:str) -> None: 7 | self.message:ESP2Message = None 8 | self.external_device_id:str = external_id 9 | self.received_via_gateway_id: str = gateway_id 10 | self.received = datetime.now() -------------------------------------------------------------------------------- /eo_man/icons/Breathe-about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Breathe-about.png -------------------------------------------------------------------------------- /eo_man/icons/EUL_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/EUL_device.png -------------------------------------------------------------------------------- /eo_man/icons/Faenza-system-search.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Faenza-system-search.bmp -------------------------------------------------------------------------------- /eo_man/icons/Faenza-system-search.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Faenza-system-search.ico -------------------------------------------------------------------------------- /eo_man/icons/Faenza-system-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Faenza-system-search.png -------------------------------------------------------------------------------- /eo_man/icons/Gnome-help-faq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Gnome-help-faq.png -------------------------------------------------------------------------------- /eo_man/icons/Home_Assistant_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Home_Assistant_Logo.png -------------------------------------------------------------------------------- /eo_man/icons/Oxygen480-actions-document-save-as.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Oxygen480-actions-document-save-as.png -------------------------------------------------------------------------------- /eo_man/icons/Oxygen480-actions-document-save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Oxygen480-actions-document-save.png -------------------------------------------------------------------------------- /eo_man/icons/Oxygen480-actions-edit-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Oxygen480-actions-edit-clear.png -------------------------------------------------------------------------------- /eo_man/icons/Oxygen480-actions-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Oxygen480-actions-help.png -------------------------------------------------------------------------------- /eo_man/icons/Oxygen480-status-folder-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Oxygen480-status-folder-open.png -------------------------------------------------------------------------------- /eo_man/icons/Software-update-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/Software-update-available.png -------------------------------------------------------------------------------- /eo_man/icons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/__init__.py -------------------------------------------------------------------------------- /eo_man/icons/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/blank.png -------------------------------------------------------------------------------- /eo_man/icons/export_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/export_icon.png -------------------------------------------------------------------------------- /eo_man/icons/fam-usb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/fam-usb.png -------------------------------------------------------------------------------- /eo_man/icons/fam-usb2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/fam-usb2.png -------------------------------------------------------------------------------- /eo_man/icons/fam14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/fam14.jpg -------------------------------------------------------------------------------- /eo_man/icons/fam14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/fam14.png -------------------------------------------------------------------------------- /eo_man/icons/fgw14-usb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/fgw14-usb.png -------------------------------------------------------------------------------- /eo_man/icons/ftd14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/ftd14.png -------------------------------------------------------------------------------- /eo_man/icons/github_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/github_icon.png -------------------------------------------------------------------------------- /eo_man/icons/image_gallary.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image, ImageTk 3 | 4 | # icon list 5 | # https://commons.wikimedia.org/wiki/Comparison_of_icon_sets 6 | 7 | class ImageGallery(): 8 | 9 | # is needed to keep a reference to the images otherwise they will not displayed in the icons 10 | _images:dict[str:ImageTk.PhotoImage] = {} 11 | 12 | @classmethod 13 | def get_image(cls, filename, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 14 | id = f"{filename}_{size[0]}_{size[0]}" 15 | if id not in cls._images: 16 | with Image.open(os.path.join(os.path.dirname(__file__), filename)) as img: 17 | if size is not None: 18 | _img = img.resize(size, Image.LANCZOS) 19 | img = ImageTk.PhotoImage(_img) 20 | cls._images[id] = img 21 | 22 | return cls._images[id] 23 | 24 | @classmethod 25 | def get_eo_man_logo(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 26 | return ImageGallery.get_image("Faenza-system-search.png", size) 27 | 28 | @classmethod 29 | def get_blank(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 30 | return ImageGallery.get_image("blank.png", size) 31 | 32 | @classmethod 33 | def get_ha_logo(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 34 | return ImageGallery.get_image("Home_Assistant_Logo.png", size) 35 | 36 | @classmethod 37 | def get_open_folder_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 38 | return ImageGallery.get_image("Oxygen480-status-folder-open.png", size) 39 | 40 | @classmethod 41 | def get_save_file_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 42 | return ImageGallery.get_image("Oxygen480-actions-document-save.png", size) 43 | 44 | @classmethod 45 | def get_save_file_as_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 46 | return ImageGallery.get_image("Oxygen480-actions-document-save-as.png", size) 47 | 48 | @classmethod 49 | def get_help_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 50 | return ImageGallery.get_image("Oxygen480-actions-help.png", size) 51 | 52 | @classmethod 53 | def get_paypal_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 54 | return ImageGallery.get_image("paypal_icon.png", size) 55 | 56 | @classmethod 57 | def get_paypal_me_icon(cls, size:tuple[int:int]=(214,20)) -> ImageTk.PhotoImage: 58 | return ImageGallery.get_image("paypal_me_badge.png", size) 59 | 60 | @classmethod 61 | def get_about_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 62 | return ImageGallery.get_image("Breathe-about.png", size) 63 | 64 | @classmethod 65 | def get_info_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 66 | return cls.get_about_icon(size) 67 | 68 | @classmethod 69 | def get_github_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 70 | return ImageGallery.get_image("github_icon.png", size) 71 | 72 | @classmethod 73 | def get_software_update_available_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 74 | return ImageGallery.get_image("Software-update-available.png", size) 75 | 76 | @classmethod 77 | def get_fam14_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 78 | return ImageGallery.get_image("fam14.png", size) 79 | 80 | @classmethod 81 | def get_usb300_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 82 | return ImageGallery.get_image("usb300.png", size) 83 | 84 | @classmethod 85 | def get_fam_usb_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 86 | return ImageGallery.get_image("fam-usb2.png", size) 87 | 88 | @classmethod 89 | def get_ftd14_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 90 | return ImageGallery.get_image("ftd14.png", size) 91 | 92 | @classmethod 93 | def get_fgw14_usb_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 94 | return ImageGallery.get_image("fgw14-usb.png", size) 95 | 96 | @classmethod 97 | def get_wireless_network_in_color_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 98 | return ImageGallery.get_image("wireless-network-colored.png", size) 99 | 100 | @classmethod 101 | def get_wireless_network_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 102 | return ImageGallery.get_image("wireless-network-bw.png", size) 103 | 104 | @classmethod 105 | def get_wireless_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 106 | return ImageGallery.get_image("wireless.png", size) 107 | 108 | @classmethod 109 | def get_send_mail(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 110 | return ImageGallery.get_image("mail-send-receive.png", size) 111 | 112 | @classmethod 113 | def get_forward_mail(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 114 | return ImageGallery.get_image("mail-forward.png", size) 115 | 116 | @classmethod 117 | def get_pct14_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 118 | return ImageGallery.get_image("pct14.png", size) 119 | 120 | @classmethod 121 | def get_eul_gateway_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 122 | return ImageGallery.get_image("EUL_device.png", size) 123 | 124 | @classmethod 125 | def get_mgw_piotek_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 126 | return ImageGallery.get_image("mgw_piotek.png", size) 127 | 128 | @classmethod 129 | def get_clear_icon(cls, size:tuple[int:int]=(32,32)) -> ImageTk.PhotoImage: 130 | return ImageGallery.get_image("Oxygen480-actions-edit-clear.png", size) -------------------------------------------------------------------------------- /eo_man/icons/mail-forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/mail-forward.png -------------------------------------------------------------------------------- /eo_man/icons/mail-send-receive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/mail-send-receive.png -------------------------------------------------------------------------------- /eo_man/icons/mgw_piotek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/mgw_piotek.png -------------------------------------------------------------------------------- /eo_man/icons/paypal_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/paypal_icon.png -------------------------------------------------------------------------------- /eo_man/icons/paypal_me_badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/paypal_me_badge.png -------------------------------------------------------------------------------- /eo_man/icons/pct14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/pct14.png -------------------------------------------------------------------------------- /eo_man/icons/usb300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/usb300.png -------------------------------------------------------------------------------- /eo_man/icons/wireless-network-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/wireless-network-bw.png -------------------------------------------------------------------------------- /eo_man/icons/wireless-network-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/wireless-network-colored.png -------------------------------------------------------------------------------- /eo_man/icons/wireless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/icons/wireless.png -------------------------------------------------------------------------------- /eo_man/view/__init__.py: -------------------------------------------------------------------------------- 1 | DEFAULT_WINDOW_TITLE = "Device Manager" -------------------------------------------------------------------------------- /eo_man/view/about_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from tkinter import * 4 | from tkinter.font import * 5 | import webbrowser 6 | from tkinterhtml import HtmlFrame 7 | from idlelib.tooltip import Hovertip 8 | 9 | from .donation_button import DonationButton 10 | 11 | from ..data.app_info import ApplicationInfo as AppInfo 12 | 13 | 14 | class AboutWindow(): 15 | 16 | def __init__(self, main:Tk): 17 | popup = Toplevel(main) 18 | popup.wm_title("About") 19 | self.popup = popup 20 | 21 | self.label_font = Font(name="Arial", underline=True, size=10) 22 | 23 | l = tk.Label(popup, text="About EnOcean Device Manager", font='Arial 14 bold') 24 | l.pack(side=TOP, fill="x", pady=10) 25 | 26 | l = tk.Label(popup, text="Version: "+AppInfo.get_version(), font="Arial 10", anchor="w") 27 | l.pack(side=TOP, fill="x", pady=2, padx=5) 28 | 29 | self.get_label_link(text="PyPI: Package Name: "+__package__.split('.')[0], link=r"https://pypi.org/project/eo-man") 30 | 31 | self.get_label_link(text="GitHub: grimmpp/enocean-device-manager", link=AppInfo.get_home_page()) 32 | 33 | self.get_label_link(text="GitHub: EnOcean Device Manager Documentation", link=r"https://github.com/grimmpp/enocean-device-manager/tree/main/docs") 34 | 35 | self.get_label_link(text="GitHub: Report a bug or ask for features!", link=AppInfo.get_home_page()+"/issues") 36 | 37 | self.get_label_link(text="Author: "+AppInfo.get_author(), link=r"https://github.com/grimmpp") 38 | 39 | self.get_label_link(text="License - "+AppInfo.get_license(), link=r"https://github.com/grimmpp/enocean-device-manager/blob/main/LICENSE") 40 | 41 | b = DonationButton(popup) 42 | b.pack(side=TOP, pady=(8,0), padx=5) 43 | b.focus() 44 | 45 | b = ttk.Button(popup, text="OK", command=popup.destroy) 46 | b.bind('', lambda e: popup.destroy()) 47 | b.pack(side=TOP, fill="x", pady=(10,2), padx=10 ) 48 | # b.focus() 49 | 50 | popup.wm_attributes('-toolwindow', 'True') 51 | popup.resizable (width=False, height=False) 52 | popup.transient(main) 53 | popup.grab_set() 54 | 55 | # center 56 | w = 340 57 | h = 310 58 | x = main.winfo_x() + main.winfo_width()/2 - w/2 59 | y = main.winfo_y() + main.winfo_height()/2 - h/2 60 | popup.geometry('%dx%d+%d+%d' % (w, h, x, y)) 61 | 62 | main.wait_window(popup) 63 | 64 | def callback(self, url): 65 | webbrowser.open(url) 66 | 67 | def get_label_link(self, text:str, link:str, tooltip:str=None) -> Label: 68 | if tooltip is None: 69 | tooltip=link 70 | 71 | l = tk.Label(self.popup, text=text, fg="blue", font=self.label_font, cursor="hand2", anchor="w") 72 | l.pack(side=TOP, fill="x", pady=2, padx=5) 73 | l.bind("", lambda e: self.callback(link)) 74 | Hovertip(l,tooltip,300) 75 | -------------------------------------------------------------------------------- /eo_man/view/device_info_window.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from tkinter import * 4 | from tkinter.font import * 5 | import webbrowser 6 | from tkinterhtml import HtmlFrame 7 | from PIL import Image, ImageTk 8 | from idlelib.tooltip import Hovertip 9 | from tkscrolledframe import ScrolledFrame 10 | 11 | from ..data.const import * 12 | 13 | from .donation_button import DonationButton 14 | 15 | from ..data.app_info import ApplicationInfo as AppInfo 16 | from ..data.data_helper import EEP_MAPPING 17 | 18 | 19 | class DeviceInfoWindow(): 20 | 21 | def __init__(self, main:Tk): 22 | popup = Toplevel(main) 23 | popup.wm_title("Supported Devices") 24 | self.popup = popup 25 | 26 | self.label_font = Font(name="Arial", underline=True, size=10) 27 | 28 | l = tk.Label(popup, text="Device Information", font='Arial 14 bold') 29 | l.pack(side=TOP, fill="x", pady=10) 30 | 31 | scrolledFrame = ScrolledFrame(popup, use_ttk=True) 32 | scrolledFrame.pack(side=LEFT, fill=BOTH, expand=2) 33 | 34 | # Bind the arrow keys and scroll wheel 35 | scrolledFrame.bind_arrow_keys(popup) 36 | scrolledFrame.bind_scroll_wheel(popup) 37 | 38 | table = scrolledFrame.display_widget(ttk.Treeview) 39 | table.pack(expand=True, fill=BOTH) 40 | 41 | table['columns'] = ('#1','#2', '#3', '#4', '#5', '#6', '#7') 42 | table["show"] = "headings" 43 | table.heading('#1', text='Brand') 44 | table.heading('#2', text='Device') 45 | table.heading('#3', text='Description') 46 | table.heading('#4', text='EEP (Sensor)') 47 | table.heading('#5', text='Sender EEP (Controller)') 48 | table.heading('#6', text='HA Platform') 49 | table.heading('#7', text='Documentation') 50 | 51 | for m in EEP_MAPPING: 52 | table.insert('', tk.END, values=( 53 | m.get('brand', ''), 54 | m.get('hw-type', ''), 55 | m.get('description', ''), 56 | m.get(CONF_EEP, ''), 57 | m.get('sender_eep', ''), 58 | m.get(CONF_TYPE, ''), 59 | m.get('docs', '') 60 | )) 61 | 62 | 63 | 64 | popup.wm_attributes('-toolwindow', 'True') 65 | popup.resizable (width=True, height=True) 66 | popup.transient(main) 67 | 68 | popup.state('zoomed') 69 | 70 | main.wait_window(popup) 71 | -------------------------------------------------------------------------------- /eo_man/view/device_table.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from tkinter import * 4 | from tkinter import ttk 5 | 6 | from ..controller.app_bus import AppBus, AppBusEventType 7 | from ..data.const import * 8 | from ..data.homeassistant.const import CONF_ID, CONF_NAME 9 | from ..data.filter import DataFilter 10 | from ..data.data_manager import DataManager, Device 11 | from ..data import data_helper 12 | from ..icons.image_gallary import ImageGallery 13 | 14 | from eltakobus.util import b2s 15 | from eltakobus.message import EltakoMessage, RPSMessage, Regular1BSMessage, Regular4BSMessage, EltakoWrappedRPS 16 | 17 | 18 | class DeviceTable(): 19 | 20 | ICON_SIZE = (20,20) 21 | NON_BUS_DEVICE_LABEL:str="Distributed Devices" 22 | 23 | def __init__(self, main: Tk, app_bus:AppBus, data_manager:DataManager): 24 | 25 | self.blinking_enabled = True 26 | self.pane = ttk.Frame(main, padding=2) 27 | # self.pane.grid(row=0, column=0, sticky=W+E+N+S) 28 | self.root = self.pane 29 | 30 | # Scrollbar 31 | yscrollbar = ttk.Scrollbar(self.pane, orient=VERTICAL) 32 | yscrollbar.pack(side=RIGHT, fill=Y) 33 | 34 | xscrollbar = ttk.Scrollbar(self.pane, orient=HORIZONTAL) 35 | xscrollbar.pack(side=BOTTOM, fill=X) 36 | 37 | # Treeview 38 | columns = ("Address", "External Address", "Device Type", "Key Function", "Comment", "Export to HA Config", "HA Platform", "Device EEP", "Sender Address", "Sender EEP") 39 | self.treeview = ttk.Treeview( 40 | self.pane, 41 | show="tree headings", 42 | selectmode="browse", 43 | yscrollcommand=yscrollbar.set, 44 | xscrollcommand=xscrollbar.set, 45 | columns=(0,1,2,3,4,5,6,7,8,9), 46 | ) 47 | self.treeview.pack(expand=True, fill=BOTH) 48 | yscrollbar.config(command=self.treeview.yview) 49 | xscrollbar.config(command=self.treeview.xview) 50 | 51 | def sort_rows_in_treeview(tree:ttk.Treeview, col_i:int, descending:bool, partent:str=''): 52 | data = [(tree.set(item, col_i), item) for item in tree.get_children(partent)] 53 | data.sort(reverse=descending) 54 | for index, (val, item) in enumerate(data): 55 | tree.move(item, partent, index) 56 | 57 | for item in tree.get_children(partent): 58 | sort_rows_in_treeview(tree, col_i, descending, item) 59 | 60 | def sort_treeview(tree:ttk.Treeview, col:int, descending:bool): 61 | i = columns.index(col) 62 | for item in tree.get_children(''): 63 | sort_rows_in_treeview(tree, i, descending, item) 64 | tree.heading(i, command=lambda c=col, d=(not descending): sort_treeview(tree, c, d)) 65 | 66 | self.treeview.column('#0', anchor="w", width=250, minwidth=250)#, stretch=NO) 67 | for col in columns: 68 | # Treeview headings 69 | i = columns.index(col) 70 | if col in ['Key Function']: 71 | self.treeview.column(i, anchor="w", width=250, minwidth=250)#, stretch=NO) 72 | else: 73 | self.treeview.column(i, anchor="w", width=80, minwidth=80)#, stretch=NO) 74 | self.treeview.heading(i, text=col, anchor="center", command=lambda c=col, d=False: sort_treeview(self.treeview, c, d)) 75 | 76 | # self.menu = Menu(main, tearoff=0) 77 | # self.menu.add_command(label="Cut") 78 | # self.menu.add_command(label="Copy") 79 | # self.menu.add_command(label="Paste") 80 | # self.menu.add_command(label="Reload") 81 | # self.menu.add_separator() 82 | # self.menu.add_command(label="Rename") 83 | 84 | self.treeview.tag_configure('related_devices') 85 | self.treeview.tag_configure('blinking', background='lightblue') 86 | 87 | # self.treeview.bind('', self.on_selected) 88 | self.treeview.bind('<>', self.on_selected) 89 | # self.treeview.bind("", self.show_context_menu) 90 | 91 | self.check_if_wireless_network_exists() 92 | 93 | self.current_data_filter:DataFilter = None 94 | self.app_bus = app_bus 95 | self.app_bus.add_event_handler(AppBusEventType.DEVICE_SCAN_STATUS, self.device_scan_status_handler) 96 | self.app_bus.add_event_handler(AppBusEventType.UPDATE_DEVICE_REPRESENTATION, self.update_device_representation_handler) 97 | self.app_bus.add_event_handler(AppBusEventType.UPDATE_SENSOR_REPRESENTATION, self.update_sensor_representation_handler) 98 | self.app_bus.add_event_handler(AppBusEventType.LOAD_FILE, self._reset) 99 | self.app_bus.add_event_handler(AppBusEventType.SET_DATA_TABLE_FILTER, self._set_data_filter_handler) 100 | self.app_bus.add_event_handler(AppBusEventType.SERIAL_CALLBACK, self._serial_callback_handler) 101 | 102 | self.data_manager = data_manager 103 | 104 | # initial loading 105 | if self.data_manager.selected_data_filter_name is not None: 106 | self._set_data_filter_handler(self.data_manager.data_fitlers[self.data_manager.selected_data_filter_name]) 107 | for d in self.data_manager.devices.values(): 108 | parent = self.NON_BUS_DEVICE_LABEL if not d.is_bus_device() else None 109 | self.update_device_handler(d, parent) 110 | 111 | 112 | def _set_data_filter_handler(self, filter): 113 | self.current_data_filter = filter 114 | 115 | self._reset(None) 116 | for d in self.data_manager.devices.values(): 117 | if d.bus_device: 118 | self.update_device_handler(d) 119 | else: 120 | self.update_device_handler(d, parent=self.NON_BUS_DEVICE_LABEL) 121 | 122 | 123 | def _reset(self, data): 124 | for item in self.treeview.get_children(): 125 | self.treeview.delete(item) 126 | self.check_if_wireless_network_exists() 127 | 128 | 129 | def on_selected(self, event): 130 | device_external_id = self.treeview.focus() 131 | device = self.data_manager.get_device_by_id(device_external_id) 132 | if device is not None: 133 | self.app_bus.fire_event(AppBusEventType.SELECTED_DEVICE, device) 134 | 135 | self.mark_related_elements(device_external_id) 136 | 137 | 138 | def mark_related_elements(self, device_external_id:str) -> None: 139 | for iid in self.treeview.tag_has( 'related_devices' ): 140 | self.treeview.item( iid, tags=() ) 141 | 142 | devices = self.data_manager.get_related_devices(device_external_id) 143 | for d in devices: 144 | if self.treeview.exists(d.external_id): 145 | self.treeview.item(d.external_id, tags=('related_devices')) 146 | 147 | 148 | def show_context_menu(self, event): 149 | try: 150 | self.menu.tk_popup(event.x_root, event.y_root) 151 | finally: 152 | self.menu.grab_release() 153 | 154 | 155 | def insert_device(self, device:Device): 156 | v=("", b2s(device.address[0]), "", "") 157 | self.treeview.insert(parent="", index="end", text=device.id_string, values=v) 158 | 159 | 160 | def device_scan_status_handler(self, status:str): 161 | if status in ['STARTED']: 162 | #TODO: disable treeview or menue of it 163 | # self.treeview.config(state=DISABLED) 164 | pass 165 | elif status in ['FINISHED']: 166 | #TODO: enable treeview or menue of it 167 | # self.treeview.config(state=NORMAL) 168 | pass 169 | 170 | 171 | def add_fam14(self, d:Device): 172 | if d.is_fam14(): 173 | if not self.treeview.exists(d.base_id): 174 | text = "" 175 | comment = "" 176 | text = d.name 177 | comment = d.comment if d.comment is not None else "" 178 | in_ha = d.use_in_ha 179 | self.treeview.insert(parent="", 180 | index=0, 181 | iid=d.external_id, 182 | text=" " + text, 183 | values=("", "", "", "", comment, in_ha, "", "", ""), 184 | image=ImageGallery.get_fam14_icon(self.ICON_SIZE), 185 | open=True) 186 | else: 187 | self.treeview.item(d.base_id, 188 | text=" " + d.name, 189 | values=("", "", "", "", d.comment, d.use_in_ha, "", "", ""), 190 | image=ImageGallery.get_fam14_icon(self.ICON_SIZE), 191 | open=True) 192 | 193 | 194 | def check_if_wireless_network_exists(self): 195 | id = self.NON_BUS_DEVICE_LABEL 196 | if not self.treeview.exists(id): 197 | self.treeview.insert(parent="", 198 | index="end", 199 | iid=id, 200 | text=" " + self.NON_BUS_DEVICE_LABEL, 201 | values=("", "", "", "", "", "", "", "", ""), 202 | image=ImageGallery.get_wireless_icon(self.ICON_SIZE), 203 | open=True) 204 | 205 | 206 | def update_device_representation_handler(self, d:Device): 207 | self.update_device_handler(d) 208 | 209 | 210 | def update_device_handler(self, d:Device, parent:str=None): 211 | 212 | if self.current_data_filter is not None and not self.current_data_filter.filter_device(d): 213 | return 214 | 215 | if not d.is_fam14(): 216 | in_ha = d.use_in_ha 217 | ha_pl = "" if d.ha_platform is None else d.ha_platform 218 | eep = "" if d.eep is None else d.eep 219 | device_type = "" if d.device_type is None else d.device_type 220 | key_func = "" if d.key_function is None else d.key_function 221 | comment = "" if d.comment is None else d.comment 222 | sender_adr = "" if 'sender' not in d.additional_fields else d.additional_fields['sender'][CONF_ID] 223 | sender_eep = "" if 'sender' not in d.additional_fields else d.additional_fields['sender'][CONF_EEP] 224 | 225 | if d.is_usb300(): 226 | image = ImageGallery.get_usb300_icon(self.ICON_SIZE) 227 | elif d.is_fam_usb(): 228 | image = ImageGallery.get_fam_usb_icon(self.ICON_SIZE) 229 | elif d.is_fgw14_usb(): 230 | image = ImageGallery.get_fgw14_usb_icon(self.ICON_SIZE) 231 | elif d.is_ftd14(): 232 | image = ImageGallery.get_ftd14_icon(self.ICON_SIZE) 233 | elif d.is_EUL_Wifi_gw(): 234 | image = ImageGallery.get_eul_gateway_icon(self.ICON_SIZE) 235 | elif d.is_mgw(): 236 | image = ImageGallery.get_mgw_piotek_icon(self.ICON_SIZE) 237 | else: 238 | image = ImageGallery.get_blank(self.ICON_SIZE) 239 | 240 | _parent = d.base_id if parent is None else parent 241 | if not self.treeview.exists(_parent): self.add_fam14(self.data_manager.devices[_parent]) 242 | if not self.treeview.exists(d.external_id): 243 | self.treeview.insert(parent=_parent, 244 | index="end", 245 | iid=d.external_id, 246 | text=" " + d.name, 247 | values=(d.address, d.external_id, device_type, key_func, comment, in_ha, ha_pl, eep, sender_adr, sender_eep), 248 | open=True) 249 | self.treeview.item(d.external_id, image=image) 250 | else: 251 | # update device 252 | self.treeview.item(d.external_id, 253 | text=" " + d.name, 254 | values=(d.address, d.external_id, device_type, key_func, comment, in_ha, ha_pl, eep, sender_adr, sender_eep), 255 | image=image, 256 | open=True) 257 | if self.treeview.parent(d.external_id) != _parent: 258 | self.treeview.move(d.external_id, _parent, 0) 259 | else: 260 | self.add_fam14(d) 261 | # self.trigger_blinking(d.external_id) 262 | 263 | 264 | def _serial_callback_handler(self, data:dict): 265 | message:EltakoMessage = data['msg'] 266 | current_base_id:str = data['base_id'] 267 | 268 | if type(message) in [RPSMessage, Regular1BSMessage, Regular4BSMessage, EltakoWrappedRPS]: 269 | if isinstance(message.address, int): 270 | adr = data_helper.a2s(message.address) 271 | else: 272 | adr = b2s(message.address) 273 | 274 | if not adr.startswith('00-00-00-'): 275 | self.trigger_blinking(adr) 276 | elif current_base_id is not None: 277 | d:Device = self.data_manager.find_device_by_local_address(adr, current_base_id) 278 | if d is not None: 279 | self.trigger_blinking(d.external_id) 280 | 281 | 282 | def trigger_blinking(self, external_id:str): 283 | if not self.blinking_enabled: 284 | return 285 | 286 | def blink(ext_id:str): 287 | for i in range(0,2): 288 | if self.treeview.exists(ext_id): 289 | tags = self.treeview.item(ext_id)['tags'] 290 | if 'blinking' in tags: 291 | if isinstance(tags, str): 292 | self.treeview.item(ext_id, tags=() ) 293 | else: 294 | tags.remove('blinking') 295 | self.treeview.item(ext_id, tags=tags ) 296 | else: 297 | if isinstance(tags, str): 298 | self.treeview.item(ext_id, tags=('blinking') ) 299 | else: 300 | tags.append('blinking') 301 | self.treeview.item(ext_id, tags=tags ) 302 | time.sleep(.5) 303 | 304 | if self.treeview.exists(ext_id): 305 | tags = self.treeview.item(ext_id)['tags'] 306 | if 'blinking' in tags: 307 | if isinstance(tags, str): 308 | self.treeview.item(ext_id, tags=() ) 309 | else: 310 | tags.remove('blinking') 311 | self.treeview.item(ext_id, tags=tags ) 312 | 313 | t = threading.Thread(target=lambda ext_id=external_id: blink(ext_id)) 314 | t.start() 315 | 316 | 317 | def update_sensor_representation_handler(self, d:Device): 318 | self.update_device_handler(d, parent=self.NON_BUS_DEVICE_LABEL) -------------------------------------------------------------------------------- /eo_man/view/donation_button.py: -------------------------------------------------------------------------------- 1 | from tkinter import * 2 | from tkinter import ttk 3 | import webbrowser 4 | from idlelib.tooltip import Hovertip 5 | 6 | from ..icons.image_gallary import ImageGallery 7 | 8 | class DonationButton(ttk.Button): 9 | 10 | def __init__(self, master=None, cnf={}, **kw): 11 | if 'small_icon' in kw and kw['small_icon']: 12 | image = ImageGallery.get_paypal_icon() 13 | else: 14 | image = ImageGallery.get_paypal_me_icon() 15 | 16 | super().__init__(master, image=image, cursor="hand2", command=self.open_payapl_me) # , relief=kw.get('relief', FLAT)) 17 | self.image = image 18 | 19 | Hovertip(self,"Donate to support this project!",300) 20 | 21 | self.bind('', lambda e: self.open_payapl_me()) 22 | 23 | def open_payapl_me(self): 24 | webbrowser.open_new(r"https://paypal.me/grimmpp") -------------------------------------------------------------------------------- /eo_man/view/eep_checker_window.py: -------------------------------------------------------------------------------- 1 | from tkinter import * 2 | import tkinter as tk 3 | from tkinter import Tk, ttk 4 | 5 | from eltakobus.message import * 6 | from eltakobus.eep import A5_08_01 7 | 8 | from ..data import data_helper 9 | 10 | 11 | class EepCheckerWindow(): 12 | 13 | def __init__(self, main:Tk): 14 | popup = Toplevel(main, padx=4, pady=4) 15 | popup.wm_title("EEP Checker") 16 | self.popup = popup 17 | 18 | row = 0 19 | self.l_telegram = ttk.Label(popup, text="A5 5A") 20 | self.l_telegram.grid(row=row, column=0, sticky=EW, columnspan=2) 21 | 22 | row += 1 23 | l = ttk.Label(popup, text="EEP: ") 24 | l.grid(row=row, column=0, sticky=W) 25 | 26 | self.cb_eep = ttk.Combobox(popup, state="readonly", width="14") 27 | self.cb_eep['values'] = data_helper.get_all_eep_names() 28 | self.cb_eep.set(A5_08_01.eep_string) 29 | self.cb_eep.grid(row=row, column=1, sticky=W) 30 | self.cb_eep.bind('<>', lambda e: [self.set_message_type(), self.show_eep_values(e)]) 31 | 32 | 33 | row += 1 34 | l = ttk.Label(popup, text="Message Type: ") 35 | l.grid(row=row, column=0, sticky=W) 36 | 37 | self.l_msg_type = ttk.Label(popup, text='') 38 | self.l_msg_type.grid(row=row, column=1, sticky=W) 39 | # self.cb_msg_type = ttk.Combobox(popup, state="readonly", width="14") 40 | # self.cb_msg_type['values'] = ['RPS (Org = 0x05)', '1BS (Org = 0x06)', '4BS (Org = 0x7)'] 41 | # self.cb_msg_type.set('4BS (Org = 0x7)') 42 | # self.cb_msg_type.grid(row=row, column=1, sticky=W) 43 | # self.cb_msg_type.bind('<>', self.show_message) 44 | 45 | 46 | row += 1 47 | l = ttk.Label(popup, text="Data: ") 48 | l.grid(row=row, column=0, sticky=W) 49 | 50 | self.e_data = ttk.Entry(popup, width="12") 51 | self.e_data.insert(END, "aa 80 76 0f") 52 | self.e_data.bind_all('', self.show_eep_values) 53 | self.e_data.grid(row=row, column=1, sticky=W) 54 | 55 | 56 | row += 1 57 | l = ttk.Label(popup, text="EEP Values: ") 58 | l.grid(row=row, column=0, sticky=W, columnspan=2) 59 | 60 | 61 | row += 1 62 | self.l_result = ttk.Label(popup, text="", font=("Arial", 11)) 63 | self.l_result.grid(row=row, column=0, sticky=W, columnspan=2) 64 | 65 | 66 | row += 1 67 | l = ttk.Label(popup, text="") 68 | l.grid(row=row, column=0, sticky=W, columnspan=2) 69 | 70 | 71 | row += 1 72 | b = ttk.Button(popup, text="Close", command=popup.destroy) 73 | b.bind('', lambda e: popup.destroy()) 74 | b.grid(row=row, column=0, columnspan=2, sticky=E) 75 | 76 | self.show_eep_values(None) 77 | 78 | popup.wm_attributes('-toolwindow', 'True') 79 | 80 | def center(): 81 | w = popup.winfo_width() 82 | h = popup.winfo_height() 83 | x = main.winfo_x() + main.winfo_width()/2 - w/2 84 | y = main.winfo_y() + main.winfo_height()/2 - h/2 85 | popup.geometry('%dx%d+%d+%d' % (w, h, x, y)) 86 | 87 | popup.after(10, center) 88 | 89 | main.wait_window(popup) 90 | 91 | 92 | 93 | def set_message_type(self): 94 | try: 95 | msg = Regular4BSMessage(b'\x00\x00\x00\x00', 0x00, b'\x00\x00\x00\x00', False) 96 | eep = data_helper.find_eep_by_name(self.cb_eep.get()) 97 | eep_values = data_helper.get_values_for_eep(eep, msg) 98 | self.l_msg_type.config(text= '4BS (Org = 0x7)') 99 | return '4BS (Org = 0x7)' 100 | except: 101 | pass 102 | 103 | try: 104 | msg = Regular1BSMessage(b'\x00\x00\x00\x00', 0x00, b'\x00', False) 105 | eep = data_helper.find_eep_by_name(self.cb_eep.get()) 106 | eep_values = data_helper.get_values_for_eep(eep, msg) 107 | self.l_msg_type.config(text= '1BS (Org = 0x06)') 108 | return '1BS (Org = 0x06)' 109 | except: 110 | pass 111 | 112 | try: 113 | msg = RPSMessage(b'\x00\x00\x00\x00', 0x00, b'\x00', False) 114 | eep = data_helper.find_eep_by_name(self.cb_eep.get()) 115 | eep_values = data_helper.get_values_for_eep(eep, msg) 116 | self.l_msg_type.config(text= 'RPS (Org = 0x05)') 117 | return 'RPS (Org = 0x05)' 118 | except: 119 | pass 120 | 121 | 122 | def get_data_from_entry(self): 123 | text = self.e_data.get().upper() 124 | hex_string = '0x' 125 | for c in text: 126 | if c in ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F']: 127 | hex_string += c 128 | return int(hex_string, 16).to_bytes(4, 'big') 129 | 130 | 131 | def show_eep_values(self, event): 132 | msg_type = self.set_message_type() 133 | try: 134 | data = self.get_data_from_entry() 135 | 136 | if '4BS' in msg_type: 137 | msg = Regular4BSMessage(b'\x00\x00\x00\x00', 0x00, data, False) 138 | elif '1BS' in msg_type: 139 | data = data[0:1] 140 | msg = Regular1BSMessage(b'\x00\x00\x00\x00', 0x00, data, False) 141 | elif 'RPS' in msg_type: 142 | msg = RPSMessage(b'\x00\x00\x00\x00', 0x00, data, False) 143 | data = data[0:1] 144 | 145 | msg_text = b2a(msg.serialize()).upper() 146 | self.l_telegram.config(text=msg_text) 147 | except: 148 | self.l_telegram.config(text='') 149 | 150 | try: 151 | eep = data_helper.find_eep_by_name(self.cb_eep.get()) 152 | eep_values = data_helper.get_values_for_eep(eep, msg) 153 | self.l_result.config(text='\n'.join(eep_values).replace('_',' ')) 154 | 155 | except: 156 | self.l_result.config(text='') -------------------------------------------------------------------------------- /eo_man/view/filter_bar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | from tkinter import messagebox 5 | 6 | from .checklistcombobox import ChecklistCombobox 7 | 8 | from ..controller.app_bus import AppBus, AppBusEventType 9 | from ..data.data_manager import DataManager 10 | from ..data import data_helper 11 | from ..data.const import CONF_EEP 12 | from ..data.filter import DataFilter 13 | 14 | 15 | class FilterBar(): 16 | 17 | def __init__(self, main: Tk, app_bus:AppBus, data_manager:DataManager, row:int): 18 | self.app_bus = app_bus 19 | self.data_manager = data_manager 20 | 21 | f = LabelFrame(main, text= "Tabel Filter", bd=1, relief=SUNKEN) 22 | f.grid(row=row, column=0, columnspan=1, sticky=W+E+N+S, pady=(0,2), padx=2) 23 | self.root = f 24 | 25 | # filter name 26 | col = 0 27 | l = Label(f, text="Filter Name:") 28 | l.grid(row=0, column=col, padx=(3,3), sticky=W) 29 | 30 | self.cb_filtername = ttk.Combobox(f, width="14") 31 | self.cb_filtername.grid(row=1, column=col, padx=(0,3) ) 32 | self.cb_filtername.bind('', lambda e: [self.load_filter(), self.add_filter(False), self.apply_filter(e, True)] ) 33 | 34 | col += 1 35 | self.btn_save_filter = ttk.Button(f, text="Load", command=self.load_filter) 36 | self.btn_save_filter.grid(row=1, column=col, padx=(0,3)) 37 | 38 | col += 1 39 | self.btn_save_filter = ttk.Button(f, text="Remove", command=self.remove_filter) 40 | self.btn_save_filter.grid(row=0, column=col, padx=(0,3) ) 41 | 42 | self.btn_save_filter = ttk.Button(f, text="Add", command=self.add_filter) 43 | self.btn_save_filter.grid(row=1, column=col, padx=(0,3), sticky=W+E ) 44 | 45 | # global filter 46 | col += 1 47 | l = Label(f, text="Global Filter:") 48 | l.grid(row=0, column=col, padx=(0,3), sticky=W) 49 | 50 | self.global_filter = ttk.Entry(f, width="14") 51 | self.global_filter.grid(row=1, column=col, padx=(0,3) ) 52 | self.global_filter.bind('', self.apply_filter) 53 | self.global_filter.bind("", lambda e: self.cb_filtername.set('') ) 54 | 55 | # address 56 | col += 1 57 | l = Label(f, text="Address:") 58 | l.grid(row=0, column=col, padx=(0,3), sticky=W) 59 | 60 | self.cb_device_address = ttk.Entry(f, width="14") 61 | self.cb_device_address.grid(row=1, column=col, padx=(0,3) ) 62 | self.cb_device_address.bind('', self.apply_filter) 63 | self.cb_device_address.bind("", lambda e: self.cb_filtername.set('') ) 64 | 65 | # external address 66 | col += 1 67 | l = Label(f, text="External Address:") 68 | l.grid(row=0, column=col, padx=(0,3), sticky=W) 69 | 70 | self.cb_external_address = ttk.Entry(f, width="14") 71 | self.cb_external_address.grid(row=1, column=col, padx=(0,3) ) 72 | self.cb_external_address.bind('', self.apply_filter) 73 | self.cb_external_address.bind("", lambda e: self.cb_filtername.set('') ) 74 | 75 | # device type 76 | col += 1 77 | l = Label(f, text="Device Type:") 78 | l.grid(row=0, column=col, padx=(0,3), sticky=W) 79 | 80 | values = data_helper.get_known_device_types() 81 | self.cb_device_type = ChecklistCombobox(f, values=values, width="14") 82 | self.cb_device_type.grid(row=1, column=col, padx=(0,3) ) 83 | self.cb_device_type.bind('', self.apply_filter) 84 | self.cb_device_type.bind("", lambda e: self.cb_filtername.set('') ) 85 | 86 | # eep 87 | col += 1 88 | l = Label(f, text="Device EEP:") 89 | l.grid(row=0, column=col, padx=(0,3), sticky=W) 90 | 91 | values = data_helper.get_all_eep_names() 92 | self.cb_device_eep = ChecklistCombobox(f, values=values, width="14") 93 | self.cb_device_eep.grid(row=1, column=col, padx=(0,3)) 94 | self.cb_device_eep.bind('', self.apply_filter) 95 | self.cb_device_eep.bind("", lambda e: self.cb_filtername.set('') ) 96 | 97 | # used in ha 98 | col += 1 99 | self.var_in_ha = tk.IntVar() 100 | self.var_in_ha.set(1) 101 | self.cb_in_ha = ttk.Checkbutton(f, text="Used in HA", variable=self.var_in_ha) 102 | self.cb_in_ha.grid(row=1, column=col, padx=(0,3) ) 103 | self.cb_in_ha.bind('', self.apply_filter) 104 | self.cb_in_ha.bind('', lambda e: self.cb_filtername.set('') ) 105 | 106 | 107 | # button reset 108 | col += 1 109 | self.btn_clear_filter = ttk.Button(f, text="Reset", command=self.reset_filter) 110 | self.btn_clear_filter.grid(row=1, column=col, padx=(0,3) ) 111 | 112 | col += 1 113 | self.btn_apply_filter = ttk.Button(f, text="Apply", command=self.apply_filter) 114 | self.btn_apply_filter.grid(row=1, column=col, padx=(0,3) ) 115 | 116 | self.app_bus.add_event_handler(AppBusEventType.SET_DATA_TABLE_FILTER, self.on_set_filter_handler) 117 | self.app_bus.add_event_handler(AppBusEventType.ADDED_DATA_TABLE_FILTER, self.on_filter_added_handler) 118 | self.app_bus.add_event_handler(AppBusEventType.REMOVED_DATA_TABLE_FILTER, self.on_filter_removed_handler) 119 | 120 | ## initiall add all filters 121 | for d in self.data_manager.data_fitlers.values(): 122 | self.on_filter_added_handler(d) 123 | selected_fn = self.data_manager.selected_data_filter_name 124 | if selected_fn is not None and len(selected_fn) > 0 and selected_fn in self.data_manager.data_fitlers.keys(): 125 | self.on_set_filter_handler(self.data_manager.data_fitlers[selected_fn]) 126 | self.apply_filter() 127 | 128 | 129 | 130 | def on_set_filter_handler(self, filter:DataFilter): 131 | self.set_widget_values(filter) 132 | 133 | def on_filter_added_handler(self, filter:DataFilter): 134 | # always take entire list to be in sync 135 | self.cb_filtername['values'] = sorted([k for k in self.data_manager.data_fitlers.keys()]) 136 | 137 | def on_filter_removed_handler(self, filter:DataFilter): 138 | # always take entire list to be in sync 139 | self.cb_filtername['values'] = sorted([k for k in self.data_manager.data_fitlers.keys()]) 140 | self.reset_filter() 141 | 142 | def add_filter(self, show_error_message:bool=True): 143 | filter_obj = self.get_filter_object() 144 | if filter_obj and len(filter_obj.name) > 1: 145 | self.data_manager.add_filter(filter_obj) 146 | 147 | elif show_error_message: 148 | messagebox.showerror(title="Error: Cannot add filter", message="Please, provdied a valid filter name!") 149 | 150 | def remove_filter(self): 151 | filter_obj = self.get_filter_object() 152 | if len(filter_obj.name) > 1: 153 | self.data_manager.remove_filter(filter_obj) 154 | else: 155 | messagebox.showerror(title="Error: Cannot remove filter", message="Please, provdied a valid filter name!") 156 | 157 | def set_widget_values(self, filter:DataFilter): 158 | if filter is not None: 159 | self.cb_filtername.set(filter.name) 160 | self.global_filter.delete(0, END) 161 | self.global_filter.insert(END, ', '.join(filter.global_filter)) 162 | self.cb_device_address.delete(0, END) 163 | self.cb_device_address.insert(END, ', '.join(filter.device_address_filter)) 164 | self.cb_external_address.delete(0, END) 165 | self.cb_external_address.insert(END, ', '.join(filter.device_external_address_filter)) 166 | self.select_ChecklistCombobox(self.cb_device_type, filter.device_type_filter) 167 | self.select_ChecklistCombobox(self.cb_device_eep, filter.device_eep_filter) 168 | else: 169 | self.cb_filtername.set('') 170 | self.global_filter.delete(0, END) 171 | 172 | for cb in [self.cb_device_address, self.cb_external_address, self.cb_device_eep, self.cb_device_type]: 173 | cb.delete(0, END) 174 | if isinstance(cb, ChecklistCombobox): 175 | for var in cb.variables: 176 | var.set(0) 177 | 178 | def load_filter(self): 179 | filter_name = self.cb_filtername.get() 180 | if filter_name in self.data_manager.data_fitlers.keys(): 181 | df:DataFilter = self.data_manager.data_fitlers[filter_name] 182 | self.set_widget_values(df) 183 | 184 | # load shall only present the values and not apply the filter 185 | # self.apply_filter() 186 | 187 | 188 | def select_ChecklistCombobox(self, widget:ChecklistCombobox, values:list[str]): 189 | widget.set(', '.join(values)) 190 | for i in range(0,len(widget.checkbuttons)): 191 | widget.variables[i].set( 1 if widget.checkbuttons[i].cget('text') in values else 0 ) 192 | 193 | def apply_filter(self, event=None, reset_for_no_filter_name:bool=False): 194 | filter = self.get_filter_object() 195 | # trigger reset if filter name is empty and if enter was pushed on filter name field 196 | if reset_for_no_filter_name: 197 | if filter is None or filter.name is None or len(filter.name) == 0: 198 | filter = None 199 | 200 | self.app_bus.fire_event(AppBusEventType.SET_DATA_TABLE_FILTER, filter) 201 | 202 | 203 | def reset_filter(self): 204 | self.app_bus.fire_event(AppBusEventType.SET_DATA_TABLE_FILTER, None) 205 | 206 | def get_str_array(self, widget:Widget) -> list[str]: 207 | widget_value = '' 208 | try: 209 | widget_value = widget.get() 210 | except: 211 | pass 212 | if isinstance(widget_value, str): 213 | return [g.strip() for g in widget_value.replace(';',',').split(',') if len(g.strip()) > 0 ] 214 | elif isinstance(widget_value, list): 215 | return [g.strip() for g in widget_value if len(g.strip()) > 0 ] 216 | else: 217 | return [] 218 | 219 | 220 | def get_filter_object(self): 221 | 222 | filter_obj = DataFilter( 223 | name = self.cb_filtername.get(), 224 | global_filter=self.get_str_array(self.global_filter), 225 | device_address_filter=self.get_str_array(self.cb_device_address), 226 | device_external_address_filter=self.get_str_array(self.cb_external_address), 227 | device_type_filter=self.get_str_array(self.cb_device_type), 228 | device_eep_filter=self.get_str_array(self.cb_device_eep), 229 | ) 230 | 231 | reset = len(filter_obj.global_filter) == 0 232 | reset &= len(filter_obj.device_address_filter) == 0 233 | reset &= len(filter_obj.device_external_address_filter) == 0 234 | reset &= len(filter_obj.device_type_filter) == 0 235 | reset &= len(filter_obj.device_eep_filter) == 0 236 | 237 | if reset: 238 | return None 239 | else: 240 | return filter_obj 241 | 242 | -------------------------------------------------------------------------------- /eo_man/view/log_output.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import tkinter as tk 3 | from tkinter import * 4 | from tkinter import ttk 5 | import tkinter.scrolledtext as ScrolledText 6 | 7 | from eltakobus.message import EltakoPoll, EltakoDiscoveryReply, EltakoDiscoveryRequest, EltakoMessage, prettify, Regular1BSMessage, EltakoWrapped1BS 8 | from esp2_gateway_adapter.esp3_serial_com import ESP3SerialCommunicator 9 | 10 | from eo_man import LOGGER 11 | 12 | from ..data.data_helper import b2s, a2s 13 | from ..data.data_manager import DataManager 14 | from ..controller.app_bus import AppBus, AppBusEventType 15 | 16 | class LogOutputPanel(): 17 | 18 | def __init__(self, main: Tk, app_bus:AppBus, data_manager:DataManager): 19 | self.app_bus = app_bus 20 | self.data_manager = data_manager 21 | 22 | pane = ttk.Frame(main, padding=2, height=150) 23 | # pane.grid(row=2, column=0, sticky="nsew", columnspan=3) 24 | self.root = pane 25 | 26 | tool_bar = ttk.Frame(pane, height=24) 27 | tool_bar.pack(expand=False, fill=X, anchor=NW, pady=(0,4)) 28 | 29 | # l = Label(tool_bar, text="Search") 30 | # l.pack(side=LEFT, padx=(2,0)) 31 | 32 | # self.e_search = ttk.Entry(tool_bar, width="14") 33 | # self.e_search.pack(side=LEFT, padx=(2,0)) 34 | 35 | self.show_telegram_values = tk.BooleanVar(value=True) 36 | cb_show_values = ttk.Checkbutton(tool_bar, text="Show Telegram Values", variable=self.show_telegram_values) 37 | cb_show_values.pack(side=LEFT, padx=(2,0)) 38 | 39 | self.show_esp2_binary =tk.BooleanVar(value=False) 40 | cb_show_es2_binary = ttk.Checkbutton(tool_bar, text="Show ESP2 Binary", variable=self.show_esp2_binary) 41 | cb_show_es2_binary.pack(side=LEFT, padx=(2,0)) 42 | 43 | self.show_esp3_binary =tk.BooleanVar(value=False) 44 | cb_show_es3_binary = ttk.Checkbutton(tool_bar, text="Show ESP3 Binary", variable=self.show_esp3_binary) 45 | cb_show_es3_binary.pack(side=LEFT, padx=(2,0)) 46 | 47 | self.st = ScrolledText.ScrolledText(pane, border=3, height=150, 48 | state=DISABLED, bg='black', fg='lightgrey', 49 | font=('Arial', 14), padx=5, pady=5) 50 | self.st.configure(font='TkFixedFont') 51 | self.st.pack(expand=True, fill=BOTH) 52 | # self.st.grid(row=2, column=0, sticky="nsew", columnspan=3) 53 | 54 | app_bus.add_event_handler(AppBusEventType.SERIAL_CALLBACK, self.serial_callback) 55 | app_bus.add_event_handler(AppBusEventType.LOG_MESSAGE, self.receive_log_message) 56 | 57 | 58 | def serial_callback(self, data:dict): 59 | telegram:EltakoMessage = data['msg'] 60 | current_base_id:str = data['base_id'] 61 | 62 | # filter out poll messages 63 | filter = type(telegram) not in [EltakoPoll] 64 | # filter out empty telegrams (generated when sending telegrams with FAM-USB) 65 | try: 66 | filter &= (int.from_bytes(telegram.address, 'big') > 0 and int.from_bytes(telegram.payload, 'big')) 67 | except: 68 | pass 69 | 70 | if filter: 71 | tt = type(telegram).__name__ 72 | adr = str(telegram.address) if isinstance(telegram.address, int) else b2s(telegram.address) 73 | if hasattr(telegram, 'reported_address'): 74 | adr = telegram.reported_address 75 | payload = '' 76 | if hasattr(telegram, 'data'): 77 | payload += ', data: '+b2s(telegram.data) 78 | elif hasattr(telegram, 'payload'): 79 | payload += ', payload: '+b2s(telegram.payload) 80 | 81 | if hasattr(telegram, 'status'): 82 | payload += ', status: '+ a2s(telegram.status, 1) 83 | 84 | values = '' 85 | eep, values = self.data_manager.get_values_from_message_to_string(telegram, current_base_id) 86 | if eep is not None: 87 | if values is not None: 88 | values = f" => values for EEP {eep.__name__}: ({values})" 89 | else: 90 | values = f" => No matching value for EEP {eep.__name__}" 91 | else: 92 | values = '' 93 | 94 | display_values:str = values if self.show_telegram_values.get() else '' 95 | display_esp2:str = f", ESP2: {telegram.serialize().hex()}" if self.show_esp2_binary.get() else '' 96 | display_esp3:str = f", ESP3: { ''.join(f'{num:02x}' for num in ESP3SerialCommunicator.convert_esp2_to_esp3_message(telegram).build())}" if self.show_esp3_binary.get() else '' 97 | 98 | log_msg = f"Received Telegram: {tt} from {adr}{payload}{values}" 99 | LOGGER.info(log_msg) 100 | 101 | display_msg = f"Received Telegram: {tt} from {adr}{payload}{display_values}{display_esp2}{display_esp3}" 102 | self.receive_log_message({'msg': display_msg, 'color': 'darkgrey'}) 103 | 104 | 105 | def receive_log_message(self, data): 106 | msg = data.get('msg', False) 107 | if not msg: return 108 | 109 | # time_format = "%d.%b %Y %H:%M:%S" 110 | time_format = "%Y-%m-%d %H:%M:%S.%f" 111 | time_str = datetime.now().strftime(time_format) 112 | msg = f"{time_str}: {msg}" 113 | 114 | self.st.configure(state='normal') 115 | self.st.insert(tk.END, msg + '\n') 116 | self.st.configure(state='disabled') 117 | 118 | color = data.get('color', False) 119 | if color: 120 | final_index = str(self.st.index(tk.END)) 121 | num_of_lines = int(final_index.split('.')[0])-2 122 | self.st.tag_config('mark_'+color, foreground=color) 123 | self.st.tag_add('mark_'+color, f"{num_of_lines}.0", f"{num_of_lines}.{len(msg)}") 124 | 125 | self.st.yview(tk.END) -------------------------------------------------------------------------------- /eo_man/view/main_panel.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from tkinter import * 4 | from tkinter import ttk 5 | 6 | from ..icons.image_gallary import ImageGallery 7 | 8 | from ..controller.app_bus import AppBus, AppBusEventType 9 | from ..controller.serial_controller import SerialController 10 | from ..controller.gateway_registry import GatewayRegistry 11 | 12 | from ..data.data_manager import DataManager 13 | 14 | from ..view import DEFAULT_WINDOW_TITLE 15 | from ..view.device_details import DeviceDetails 16 | from ..view.device_table import DeviceTable 17 | from ..view.filter_bar import FilterBar 18 | from ..view.log_output import LogOutputPanel 19 | from ..view.menu_presenter import MenuPresenter 20 | from ..view.serial_communication_bar import SerialConnectionBar 21 | from ..view.status_bar import StatusBar 22 | from ..view.tool_bar import ToolBar 23 | 24 | class MainPanel(): 25 | 26 | def __init__(self, app_bus:AppBus, data_manager: DataManager): 27 | self.main = Tk() 28 | self.app_bus = app_bus 29 | ## init main window 30 | self._init_window() 31 | 32 | ## define grid 33 | row_button_bar = 0 34 | row_serial_con_bar = 1 35 | row_filter_bar = 2 36 | row_main_area = 3 37 | row_status_bar = 4 38 | self.main.rowconfigure(row_button_bar, weight=0, minsize=38) # button bar 39 | self.main.rowconfigure(row_serial_con_bar, weight=0, minsize=38) # serial connection bar 40 | self.main.rowconfigure(row_filter_bar, weight=0, minsize=38) # table filter bar 41 | self.main.rowconfigure(row_main_area, weight=5, minsize=100) # treeview 42 | # main.rowconfigure(2, weight=1, minsize=30) # logview 43 | self.main.rowconfigure(row_status_bar, weight=0, minsize=30) # status bar 44 | self.main.columnconfigure(0, weight=1, minsize=100) 45 | 46 | gateway_registry = GatewayRegistry(app_bus) 47 | serial_controller = SerialController(app_bus, gateway_registry) 48 | 49 | ## init presenters 50 | mp = MenuPresenter(self.main, app_bus, data_manager, serial_controller) 51 | 52 | ToolBar(self.main, mp, row=row_button_bar) 53 | SerialConnectionBar(self.main, app_bus, data_manager, serial_controller, row=row_serial_con_bar) 54 | FilterBar(self.main, app_bus, data_manager, row=row_filter_bar) 55 | # main area 56 | main_split_area = ttk.PanedWindow(self.main, orient=VERTICAL) 57 | main_split_area.grid(row=row_main_area, column=0, sticky=NSEW, columnspan=4) 58 | 59 | data_split_area = ttk.PanedWindow(main_split_area, orient=HORIZONTAL) 60 | # data_split_area = Frame(main_split_area) 61 | # data_split_area.columnconfigure(0, weight=5) 62 | # data_split_area.columnconfigure(0, weight=0, minsize=100) 63 | 64 | dt = DeviceTable(data_split_area, app_bus, data_manager) 65 | dd = DeviceDetails(self.main, data_split_area, app_bus, data_manager) 66 | lo = LogOutputPanel(main_split_area, app_bus, data_manager) 67 | 68 | main_split_area.add(data_split_area, weight=5) 69 | main_split_area.add(lo.root, weight=2) 70 | 71 | data_split_area.add(dt.root, weight=5) 72 | data_split_area.add(dd.root, weight=0) 73 | # dt.root.grid(row=0, column=0, sticky="nsew") 74 | # dd.root.grid(row=0, column=1, sticky="nsew") 75 | 76 | StatusBar(self.main, app_bus, data_manager, row=row_status_bar) 77 | 78 | self.main.after(1, lambda: self.main.focus_force()) 79 | 80 | ## start main loop 81 | self.main.mainloop() 82 | 83 | 84 | 85 | 86 | 87 | def _init_window(self): 88 | self.main.title(DEFAULT_WINDOW_TITLE) 89 | 90 | #style 91 | style = ttk.Style() 92 | style.configure("TButton", relief="sunken", background='green') 93 | style_theme = 'xpnative' # 'clam' 94 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"Available style themes: {ttk.Style().theme_names()}", 'log-level': 'DEBUG'}) 95 | try: 96 | style.theme_use(style_theme) 97 | except: 98 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, {'msg': f"Cannot load style theme {style_theme}!", 'log-level': 'WARNING'}) 99 | 100 | self.main.geometry("1400x600") # set starting size of window 101 | # self.main.attributes('-fullscreen', True) 102 | # self.main.state('zoomed') # opens window maximized 103 | 104 | self.main.config(bg="lightgrey") 105 | self.main.protocol("WM_DELETE_WINDOW", self.on_closing) 106 | 107 | # icon next to title in window frame 108 | self.main.wm_iconphoto(False, ImageGallery.get_eo_man_logo()) 109 | 110 | # icon in taskbar 111 | icon = ImageGallery.get_eo_man_logo() 112 | self.main.iconphoto(True, icon, icon) 113 | 114 | def on_loaded(self) -> None: 115 | self.app_bus.fire_event(AppBusEventType.WINDOW_LOADED, {}) 116 | 117 | def on_closing(self) -> None: 118 | self.app_bus.fire_event(AppBusEventType.WINDOW_CLOSED, {}) 119 | logging.info("Close Application eo-man") 120 | logging.info("========================\n") 121 | self.main.destroy() -------------------------------------------------------------------------------- /eo_man/view/message_window.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/eo_man/view/message_window.py -------------------------------------------------------------------------------- /eo_man/view/serial_communication_bar.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | import tkinter as tk 4 | from tkinter import * 5 | from tkinter import ttk 6 | from idlelib.tooltip import Hovertip 7 | import threading 8 | 9 | from eo_man import LOGGER 10 | 11 | from ..controller.app_bus import AppBus, AppBusEventType 12 | from ..controller.serial_controller import SerialController 13 | from ..data.data_manager import DataManager 14 | from ..data import data_helper 15 | from ..data.data_helper import get_gateway_type_by_name 16 | 17 | from eltakobus.message import * 18 | from eltakobus.eep import * 19 | from eltakobus.util import * 20 | 21 | from ..data.const import get_display_names, GATEWAY_DISPLAY_NAMES, GatewayDeviceType 22 | from ..data.const import GATEWAY_DISPLAY_NAMES as GDN 23 | 24 | class SerialConnectionBar(): 25 | 26 | def __init__(self, main: Tk, app_bus:AppBus, data_manager:DataManager, serial_controller:SerialController, row:int): 27 | self.main = main 28 | self.app_bus = app_bus 29 | self.data_manager = data_manager 30 | self.serial_cntr = serial_controller 31 | 32 | self.endpoint_list:Dict[str, List[str]]={} 33 | 34 | f = LabelFrame(main, text="Serial Connection", bd=1)#, relief=SUNKEN) 35 | f.grid(row=row, column=0, columnspan=1, sticky=W+E+N+S, pady=(0,2), padx=2) 36 | 37 | self.b_detect = ttk.Button(f, text="Detect Serial Ports") 38 | self.b_detect.pack(side=tk.LEFT, padx=(5, 5), pady=5) 39 | self.b_detect.config(command=lambda : self.detect_serial_ports_command(force_reload=True) ) 40 | Hovertip(self.b_detect,"Serial port detection is sometime unstable. Please try again if device was not detected.",300) 41 | 42 | l = Label(f, text="Gateway Type: ") 43 | l.pack(side=tk.LEFT, padx=(0, 5), pady=5) 44 | 45 | self.cb_device_type = ttk.Combobox(f, state="readonly", width="18") 46 | self.cb_device_type.pack(side=tk.LEFT, padx=(0, 5), pady=5) 47 | self.cb_device_type['values'] = get_display_names() # ['FAM14', 'FGW14-USB', 'FAM-USB', 'USB300', 'LAN Gateway'] 48 | self.cb_device_type.set(self.cb_device_type['values'][0]) 49 | self.cb_device_type.bind('<>', self.on_device_type_changed) 50 | 51 | l = ttk.Label(f, text="Serial Port: ") 52 | l.pack(side=tk.LEFT, padx=(0, 5), pady=5) 53 | 54 | self.cb_serial_ports = ttk.Combobox(f, state=NORMAL, width="14") 55 | self.cb_serial_ports.pack(side=tk.LEFT, padx=(0, 5), pady=5) 56 | 57 | self.b_connect = ttk.Button(f, text="Connect", state=NORMAL, command=self.toggle_serial_connection_command) 58 | self.b_connect.pack(side=tk.LEFT, padx=(0, 5), pady=5) 59 | 60 | s = ttk.Separator(f, orient=VERTICAL ) 61 | s.pack(side=tk.LEFT, padx=(0,5), pady=0, fill="y") 62 | 63 | self.b_scan = ttk.Button(f, text="Scan for devices", state=DISABLED, command=self.scan_for_devices) 64 | self.b_scan.pack(side=tk.LEFT, padx=(0, 5), pady=5) 65 | 66 | self.overwrite = tk.BooleanVar() 67 | self.cb_overwrite = ttk.Checkbutton(f, text="Overwrite existing values", variable=self.overwrite) 68 | self.cb_overwrite.pack(side=tk.LEFT, padx=(0, 5), pady=5) 69 | 70 | s = ttk.Separator(f, orient=VERTICAL ) 71 | s.pack(side=tk.LEFT, padx=(0,5), pady=0, fill="y") 72 | 73 | text = "Ensures sender configuration for Home Assistant is written into device memory.\n" 74 | text += "* Gateways will be added when being once connected.\n" 75 | text += "* Only devices connected to FAM14 via wire will be updated.\n" 76 | text += "* Button will be enabled when FAM14 is connected." 77 | 78 | l = ttk.Label(f, text="Program HA senders into devices: ") 79 | Hovertip(l, text, 300) 80 | l.pack(side=tk.LEFT, padx=(0, 5), pady=5) 81 | 82 | self.cb_gateways_for_HA = ttk.Combobox(f, state="readonly", width="24") 83 | Hovertip(self.cb_gateways_for_HA, text, 300) 84 | self.cb_gateways_for_HA.pack(side=tk.LEFT, padx=(0, 5), pady=5) 85 | 86 | self.b_sync_ha_sender = ttk.Button(f, text="Write to devices", state=DISABLED, command=self.write_ha_senders_to_devices) 87 | Hovertip(self.b_sync_ha_sender, text, 300) 88 | self.b_sync_ha_sender.pack(side=tk.LEFT, padx=(0, 5), pady=5) 89 | 90 | # # if connected via fam14 force to get status update message 91 | # b = ttk.Button(f, text="Send Poll", command=lambda: self.serial_cntr.send_message(EltakoPollForced(5))) 92 | # b.pack(side=tk.LEFT, padx=(0, 5), pady=5) 93 | 94 | self.app_bus.add_event_handler(AppBusEventType.CONNECTION_STATUS_CHANGE, self.is_connected_handler) 95 | self.app_bus.add_event_handler(AppBusEventType.DEVICE_SCAN_STATUS, self.device_scan_status_handler) 96 | self.app_bus.add_event_handler(AppBusEventType.WRITE_SENDER_IDS_TO_DEVICES_STATUS, self.device_scan_status_handler) 97 | self.app_bus.add_event_handler(AppBusEventType.WINDOW_LOADED, self.on_window_loaded) 98 | self.app_bus.add_event_handler(AppBusEventType.UPDATE_DEVICE_REPRESENTATION, self.update_cb_gateways_for_HA) 99 | self.app_bus.add_event_handler(AppBusEventType.UPDATE_SENSOR_REPRESENTATION, self.update_cb_gateways_for_HA) 100 | self.app_bus.add_event_handler(AppBusEventType.SERVICE_ENDPOINTS_UPDATES, self.update_service_endpoints) 101 | 102 | 103 | def update_cb_gateways_for_HA(self, event=None): 104 | gateways = [] 105 | for d in self.data_manager.devices.values(): 106 | if d.is_gateway(): 107 | gateways.append(f"{d.device_type.replace(' (Wireless Transceiver)', '')} ({d.external_id})") 108 | 109 | self.cb_gateways_for_HA['values'] = gateways 110 | if self.cb_gateways_for_HA.get() == '' and len(gateways) > 0: 111 | self.cb_gateways_for_HA.set(gateways[0]) 112 | elif len(gateways) == 0: 113 | self.cb_gateways_for_HA.set('') 114 | 115 | def on_device_type_changed(self, event): 116 | self.cb_serial_ports['values'] = [] 117 | self.cb_serial_ports.set('') 118 | 119 | self.update_combobox_serial_port() 120 | 121 | 122 | def write_ha_senders_to_devices(self): 123 | # get gateways and its base id 124 | baseId = self.cb_gateways_for_HA.get().split(' ')[-1].replace('(', '').replace(')', '') 125 | g = self.data_manager.devices.get(baseId, None) 126 | if g.is_wired_gateway(): 127 | sender_base_id = '00-00-B0-00' 128 | else: 129 | sender_base_id = g.base_id 130 | 131 | # get sender list to be entered 132 | sender_list = {} 133 | for d in self.data_manager.devices.values(): 134 | if d.base_id == self.serial_cntr.current_base_id and d.use_in_ha: 135 | if 'sender' in d.additional_fields: 136 | sender_list[d.external_id] = dict(d.additional_fields) 137 | sender_id = data_helper.a2i(sender_base_id) + data_helper.a2i(d.address) 138 | sender_list[d.external_id]['sender']['id'] = data_helper.a2s(sender_id) 139 | 140 | self.app_bus.fire_event(AppBusEventType.LOG_MESSAGE, { 141 | 'msg': f'Write sender ids to devices for gateway {self.cb_gateways_for_HA.get()} with sender base id {sender_base_id} on bus with base id {self.serial_cntr.current_base_id}', 142 | 'color': 'lightgreen'}) 143 | self.serial_cntr.write_sender_id_to_devices(sender_list) 144 | 145 | 146 | def on_window_loaded(self, data): 147 | self.detect_serial_ports_command() 148 | 149 | 150 | def scan_for_devices(self): 151 | self.serial_cntr.scan_for_devices( self.overwrite.get() ) 152 | 153 | 154 | def device_scan_status_handler(self, status:str): 155 | if status == 'FINISHED': 156 | self.is_connected_handler({'connected': self.serial_cntr.is_serial_connection_active()}) 157 | self.main.config(cursor="") 158 | self.b_scan.config(state=NORMAL) 159 | self.b_connect.config(state=NORMAL) 160 | self.b_sync_ha_sender.config(state=NORMAL) 161 | if status == 'STARTED': 162 | self.b_scan.config(state=DISABLED) 163 | self.b_connect.config(state=DISABLED) 164 | self.main.config(cursor="watch") #set cursor for waiting 165 | self.b_sync_ha_sender.config(state=DISABLED) 166 | 167 | 168 | def toggle_serial_connection_command(self): 169 | try: 170 | if not self.serial_cntr.is_serial_connection_active(): 171 | self.b_detect.config(state=DISABLED) 172 | self.b_connect.config(state=DISABLED) 173 | self.b_scan.config(state=DISABLED) 174 | self.serial_cntr.establish_serial_connection(self.cb_serial_ports.get(), self.cb_device_type.get()) 175 | else: 176 | self.serial_cntr.stop_serial_connection() 177 | except: 178 | # reset buttons 179 | self.is_connected_handler(data={'connected': self.serial_cntr.is_serial_connection_active()}) 180 | LOGGER.exception("Was not able to detect serial ports.") 181 | 182 | 183 | def is_connected_handler(self, data:dict, skipp_serial_port_detection:bool=False): 184 | status = data.get('connected') 185 | 186 | self.main.config(cursor="") 187 | 188 | if status: 189 | self.b_connect.config(text="Disconnect", state=NORMAL) 190 | self.cb_serial_ports.config(state=DISABLED) 191 | self.b_detect.config(state=DISABLED) 192 | self.cb_device_type.config(state=DISABLED) 193 | 194 | if self.cb_device_type.get() == GATEWAY_DISPLAY_NAMES[GatewayDeviceType.EltakoFAM14] and self.serial_cntr.is_fam14_connection_active(): 195 | self.b_scan.config(state=NORMAL) 196 | self.b_sync_ha_sender.config(state=NORMAL) 197 | else: 198 | self.b_scan.config(state=DISABLED) 199 | self.b_sync_ha_sender.config(state=DISABLED) 200 | 201 | else: 202 | self.b_connect.config(text="Connect", state=NORMAL) 203 | self.b_detect.config(state=NORMAL) 204 | if self.cb_device_type.get() in [GDN[GatewayDeviceType.LAN], GDN[GatewayDeviceType.LAN_ESP2]]: 205 | self.cb_serial_ports.config(state=NORMAL) 206 | else: 207 | self.cb_serial_ports.config(state='readonly') 208 | self.cb_device_type.config(state="readonly") 209 | self.b_scan.config(state=DISABLED) 210 | self.b_sync_ha_sender.config(state=DISABLED) 211 | if not skipp_serial_port_detection: self.detect_serial_ports_command() 212 | 213 | def update_service_endpoints(self, data:Dict[str, List[str]]=None): 214 | 215 | self.endpoint_list = data 216 | self.update_combobox_serial_port() 217 | 218 | def update_combobox_serial_port(self): 219 | 220 | self.b_detect.config(state=NORMAL) 221 | self.cb_device_type.config(state="readonly") 222 | try: 223 | self.cb_serial_ports['values'] = self.endpoint_list[get_gateway_type_by_name(self.cb_device_type.get())] 224 | if len(self.cb_serial_ports['values']) > 0: 225 | self.cb_serial_ports.set(self.cb_serial_ports['values'][0]) 226 | self.b_connect.config(state=NORMAL) 227 | self.cb_serial_ports.config(state=NORMAL) 228 | else: 229 | # self.b_connect.config(state=DISABLED) 230 | self.cb_serial_ports.config(state=NORMAL) 231 | self.cb_serial_ports.set('') 232 | except: 233 | # self.b_connect.config(state=DISABLED) 234 | self.cb_serial_ports.config(state=NORMAL) 235 | self.cb_serial_ports.set('') 236 | 237 | self.main.config(cursor="") 238 | 239 | def detect_serial_ports_command(self, force_reload:bool=False): 240 | 241 | self.main.config(cursor="watch") #set cursor for waiting 242 | self.b_detect.config(state=DISABLED) 243 | self.b_connect.config(state=DISABLED) 244 | self.cb_device_type.config(state=DISABLED) 245 | self.cb_serial_ports.config(state=DISABLED) 246 | self.app_bus.fire_event(AppBusEventType.REQUEST_SERVICE_ENDPOINT_DETECTION, force_reload) 247 | 248 | # def detect_serial_ports(): 249 | # try: 250 | # self.main.config(cursor="watch") #set cursor for waiting 251 | # self.b_detect.config(state=DISABLED) 252 | # self.b_connect.config(state=DISABLED) 253 | # self.cb_device_type.config(state=DISABLED) 254 | # self.cb_serial_ports.config(state=DISABLED) 255 | # self.app_bus.fire_event(AppBusEventType.REQUEST_SERVICE_ENDPOINT_DETECTION, None) 256 | # serial_ports = self.serial_cntr.get_serial_ports(self.cb_device_type.get(), force_reload) 257 | # self.b_detect.config(state=NORMAL) 258 | # self.cb_device_type.config(state=NORMAL) 259 | # self.cb_serial_ports['values'] = serial_ports 260 | # if len(self.cb_serial_ports['values']) > 0: 261 | # self.cb_serial_ports.set(self.cb_serial_ports['values'][0]) 262 | # self.b_connect.config(state=NORMAL) 263 | # else: 264 | # self.b_connect.config(state=DISABLED) 265 | # self.cb_serial_ports.set('') 266 | # except: 267 | # # reset buttons 268 | # LOGGER.exception("Was not able to detect serial ports.") 269 | # else: 270 | # self.is_connected_handler(data={'connected': False}, skipp_serial_port_detection=True) 271 | 272 | # t = threading.Thread(target=detect_serial_ports) 273 | # t.start() -------------------------------------------------------------------------------- /eo_man/view/status_bar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import * 3 | from tkinter import ttk 4 | 5 | from ..controller.app_bus import AppBus, AppBusEventType 6 | from ..data.data_manager import DataManager 7 | from ..data.filter import DataFilter 8 | 9 | class StatusBar(): 10 | 11 | 12 | def __init__(self, main: Tk, app_bus:AppBus, data_manager:DataManager, row:int): 13 | self.app_bus = app_bus 14 | self.data_manager = data_manager 15 | 16 | f = Frame(main, bd=1, relief=SUNKEN) 17 | f.grid(row=row, column=0, columnspan=1, sticky=W+E+N+S) 18 | 19 | self.l_connected = Label(f, borderwidth=1) 20 | self.l_connected.pack(side=tk.RIGHT, padx=(5, 5), pady=2) 21 | self.is_connected_handler({'connected':False}) 22 | 23 | self.pb = ttk.Progressbar(f, orient=tk.HORIZONTAL, length=160, maximum=100) 24 | self.pb.pack(side=tk.RIGHT, padx=(5, 0), pady=2) 25 | self.pb.step(0) 26 | 27 | l = Label(f, text="Device Scan:") 28 | l.pack(side=tk.RIGHT, padx=(5, 0), pady=2) 29 | 30 | self.l_filter_name = Label(f, text="No Filter") 31 | self.l_filter_name.pack(side=tk.RIGHT, padx=(0, 0), pady=2) 32 | self.l = Label(f, text="Active Filter: ") 33 | self.l.pack(side=tk.RIGHT, padx=(5, 0), pady=2) 34 | 35 | self.l_devices = Label(f, text=self.get_device_count_str()) 36 | self.l_devices.pack(side=tk.LEFT, padx=(5, 0), pady=2) 37 | 38 | self.app_bus.add_event_handler(AppBusEventType.CONNECTION_STATUS_CHANGE, self.is_connected_handler) 39 | self.app_bus.add_event_handler(AppBusEventType.DEVICE_ITERATION_PROGRESS, self.device_scan_progress_handler) 40 | self.app_bus.add_event_handler(AppBusEventType.UPDATE_DEVICE_REPRESENTATION, self.update_device_count) 41 | self.app_bus.add_event_handler(AppBusEventType.UPDATE_SENSOR_REPRESENTATION, self.update_device_count) 42 | self.app_bus.add_event_handler(AppBusEventType.DEVICE_SCAN_STATUS, self.device_scan_status_handler) 43 | self.app_bus.add_event_handler(AppBusEventType.SET_DATA_TABLE_FILTER, self.active_filter_name_handler) 44 | 45 | ## initialize 46 | if self.data_manager.selected_data_filter_name is not None: 47 | self.active_filter_name_handler(self.data_manager.data_fitlers[self.data_manager.selected_data_filter_name]) 48 | 49 | 50 | def active_filter_name_handler(self, filter:DataFilter): 51 | if filter is None: 52 | self.l_filter_name.config(text='No Filter', fg='SystemButtonText') 53 | elif filter.name is None or filter.name == '': 54 | self.l_filter_name.config(text='Custom Filter', fg='darkgreen') 55 | else: 56 | self.l_filter_name.config(text=filter.name, fg='darkgreen') 57 | 58 | def get_device_count_str(self) -> str: 59 | count:int = len(self.data_manager.devices) 60 | fam14s:int = len([d for d in self.data_manager.devices.values() if d.is_fam14()]) 61 | bus_device:int = len([d for d in self.data_manager.devices.values() if d.bus_device]) 62 | decentralized:int = count-bus_device 63 | 64 | return f"Devices: {count}, FAM14s: {fam14s}, Bus Devices: {bus_device}, Decentralized Devices: {decentralized}" 65 | 66 | def update_device_count(self, data): 67 | self.l_devices.config(text=self.get_device_count_str()) 68 | 69 | def device_scan_status_handler(self, status:str): 70 | if status in ['STARTED', 'FINISHED']: 71 | self.pb["value"] = 0 72 | 73 | def is_connected_handler(self, data:dict): 74 | status = data.get('connected') 75 | if status: 76 | self.l_connected.config(bg="lightgreen", fg="black", text="Connected") 77 | else: 78 | self.l_connected.config(bg="darkred", fg="white", text="Disconnected") 79 | 80 | def device_scan_progress_handler(self, progress:float): 81 | self.pb["value"] = progress -------------------------------------------------------------------------------- /eo_man/view/tool_bar.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import tkinter as tk 3 | from tkinter import ttk 4 | import os 5 | from tkinter import * 6 | from tkinter import messagebox 7 | from PIL import Image, ImageTk 8 | from idlelib.tooltip import Hovertip 9 | import webbrowser 10 | import subprocess 11 | 12 | from eo_man import LOGGER 13 | 14 | from .donation_button import DonationButton 15 | from .menu_presenter import MenuPresenter 16 | from ..icons.image_gallary import ImageGallery 17 | from ..data.app_info import ApplicationInfo as AppInfo 18 | 19 | 20 | class ToolBar(): 21 | # icon list 22 | # https://commons.wikimedia.org/wiki/Comparison_of_icon_sets 23 | def __init__(self, main: Tk, menu_presenter:MenuPresenter, row:int): 24 | self.main = main 25 | 26 | f = Frame(main, bd=1)#, relief=SUNKEN) 27 | f.grid(row=row, column=0, columnspan=1, sticky=W+E+N+S, pady=2, padx=2) 28 | 29 | b = self._create_img_button(f, "Save to current file", ImageGallery.get_save_file_icon(), menu_presenter.save_file ) 30 | b = self._create_img_button(f, "Save as file", ImageGallery.get_save_file_as_icon(), lambda: menu_presenter.save_file(save_as=True) ) 31 | b = self._create_img_button(f, "Open file", ImageGallery.get_open_folder_icon(), menu_presenter.load_file) 32 | 33 | self._add_separater(f) 34 | 35 | b = self._create_img_button(f, "Export Home Assistant Configuration", ImageGallery.get_ha_logo(), menu_presenter.export_ha_config) 36 | 37 | self._add_separater(f) 38 | 39 | b = self._create_img_button(f, "Send Message", ImageGallery.get_forward_mail(), menu_presenter.show_send_message_window) 40 | 41 | # placed at the right end 42 | b = DonationButton(f, relief=GROOVE, small_icon=True).pack(side=RIGHT, padx=(0,2), pady=2) 43 | b = self._create_img_button(f, "GitHub: EnOcean Device Manager Repository", ImageGallery.get_github_icon(), menu_presenter.open_eo_man_repo) 44 | b.pack(side=RIGHT, padx=(0,2), pady=2) 45 | b = self._create_img_button(f, "GitHub: EnOcean Device Manager Documentation", ImageGallery.get_help_icon(), menu_presenter.open_eo_man_documentation) 46 | b.pack(side=RIGHT, padx=(0,2), pady=2) 47 | 48 | if not AppInfo.is_version_up_to_date(): 49 | new_v = AppInfo.get_lastest_available_version() 50 | b = self._create_img_button(f, f"New Software Version 'v{new_v}' is available.", ImageGallery.get_software_update_available_icon(), self.show_how_to_update) 51 | b.pack(side=RIGHT, padx=(0,2), pady=2) 52 | 53 | def _add_separater(self, f:Frame): 54 | ttk.Separator(f, orient=VERTICAL).pack(side=LEFT, padx=4) 55 | 56 | 57 | def _create_img_button(self, f:Frame, tooltip:str, image:ImageTk.PhotoImage, command) -> Button: 58 | b = ttk.Button(f, image=image, cursor="hand2", command=command) #, relief=GROOVE) 59 | Hovertip(b,tooltip,300) 60 | b.image = image 61 | b.pack(side=LEFT, padx=(2,0), pady=2) 62 | return b 63 | 64 | def show_how_to_update(self): 65 | base_path = os.environ.get('VIRTUAL_ENV', '') 66 | if base_path != '': 67 | base_path = os.path.join(base_path, 'Scripts') 68 | 69 | new_version = AppInfo.get_lastest_available_version() 70 | 71 | msg = f"A new version 'v{new_version}' of 'EnOcean Device Manager' is available. \n\n" 72 | msg += f"You can update this application by entering \n" 73 | msg += f"'{os.path.join(base_path, 'pip')}' install eo_man --upgrade'\n" 74 | msg += f"into the command line." 75 | 76 | messagebox.showinfo("Update Available", msg) -------------------------------------------------------------------------------- /install_from_official_releases.bat: -------------------------------------------------------------------------------- 1 | 2 | echo Install EnOcean Device Manager (eo-man) from official releases 3 | 4 | echo Check if Python is installed. 5 | python --version >nul 2>&1 6 | if %errorlevel% equ 0 ( 7 | echo Python is installed. 8 | ) else ( 9 | echo Python is not installed or not working. 10 | pause 11 | exit 12 | ) 13 | 14 | :: Set directory 15 | set "directory=installed_official_eo_man" 16 | echo Use folder $directory to install eo-man 17 | 18 | :: Create a virtual environment 19 | echo Creating a virtual environment... 20 | python.exe -m venv %directory% 21 | 22 | :: Activate the virtual environment 23 | call .\%directory%\Scripts\activate.bat 24 | 25 | :: Upgrade pip 26 | echo Upgrading pip... 27 | python -m pip install --upgrade pip 28 | 29 | :: Install eo_man 30 | echo Install EnOcean Device Manager (eo-man) 31 | pip.exe install eo_man 32 | 33 | if %ERRORLEVEL% neq 0 ( 34 | echo Failed to install EnOcean Device Manager. 35 | exit /b 36 | ) 37 | 38 | set script_directory=%~dp0 39 | 40 | echo Generate execution file to start eo-man 41 | echo @echo off > .\%directory%\eo_man.bat 42 | echo START "eo-man" %script_directory%\%directory%\Scripts\pythonw.exe -m eo_man >> .\%directory%\eo_man.bat 43 | 44 | echo Generate execution file to update eo-man 45 | echo @echo off > %script_directory%\%directory%\update_eo_man.bat 46 | echo START "eo-man" %script_directory%\%directory%\Scripts\pip.exe install eo_man --upgrade >> .\%directory%\update_eo_man.bat 47 | 48 | echo Installation complete! 49 | 50 | echo Run %directory%\eo-man.bat to start eo-man 51 | cd %script_directory%\%directory% 52 | call .\eo_man.bat -------------------------------------------------------------------------------- /install_from_officials_on_ubuntu.sh: -------------------------------------------------------------------------------- 1 | sudo apt-get update 2 | sudo apt-get isntall python3-venv python3-tk idel3 3 | python3 -m venv eo_man 4 | ./eo_man/bin/pip3 install eo_man 5 | 6 | ## start application 7 | #./eo_man/bin/python3 -m eo_man -------------------------------------------------------------------------------- /install_from_repo.bat: -------------------------------------------------------------------------------- 1 | set "directory=installed_eo_man" 2 | 3 | python.exe -m venv %directory% 4 | .\%directory%\Scripts\pip.exe install build 5 | .\%directory%\Scripts\pip.exe install -r requirements.txt 6 | .\%directory%\Scripts\python.exe -m build 7 | .\%directory%\Scripts\pip.exe install .\dist\eo_man-0.1.35-py3-none-any.whl 8 | 9 | echo @echo off > .\%directory%\eo_man.bat 10 | echo START "eo-man" .\Scripts\pythonw.exe -m eo_man >> .\%directory%\eo_man.bat 11 | 12 | .\%directory%\eo_man.bat -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | # requires = ["hatchling"] 5 | # build-backend = "hatchling.build" 6 | 7 | [project] 8 | name = "eo_man" 9 | version = "0.1.44" 10 | description = "Tool to managed EnOcean Devices and to generate Home Assistant Configuration." 11 | readme = "README.md" 12 | requires-python = ">=3.12" 13 | license = {text = "MIT" } 14 | keywords = ["enocean", "device manager", "eltako"] 15 | authors = [ 16 | { name = "Philipp Grimm" } 17 | ] 18 | classifiers = [ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent" 22 | ] 23 | dependencies = [ 24 | "aiocoap==0.4.7", "eltako14bus==0.0.74", "numpy", "pillow>=11", "pyserial==3.5", "pyserial-asyncio==0.6", "PyYAML==6.0.1", "StrEnum==0.4.15", "termcolor==2.4.0", "tkinterhtml==0.7", "requests==2.31.0", "enocean==0.60.1", "esp2_gateway_adapter==0.2.20", "zeroconf", "tkScrolledFrame==1.0.4", "xmltodict", "tomli" 25 | ] 26 | 27 | [project.urls] 28 | Homepage = "https://github.com/grimmpp/enocean-device-manager" 29 | Documentation = "https://github.com/grimmpp/enocean-device-manager/tree/main/docs/getting-started" 30 | Repository = "https://github.com/grimmpp/enocean-device-manager.git" 31 | 32 | [tool.setuptools] 33 | include-package-data = true # Enables package data inclusion 34 | 35 | [tool.setuptools.package-dir] 36 | "eo_man" = "eo_man" 37 | 38 | [tool.setuptools.package-data] 39 | "eo_man" = ["icons/*.png", "icons/*.bmp", "icons/*.ico", "icons/*.jpg"] 40 | 41 | 42 | # [project.script] 43 | # eo_man_script = "eo_man.__main__:main" 44 | 45 | # [project.gui-script] 46 | # eo_man_gui = "eo_man.__main__:main" -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli = true 3 | log_cli_level = DEBUG 4 | log_cli_format = %(asctime)s [%(levelname)s] %(message)s 5 | log_cli_date_format = %Y-%m-%d %H:%M:%S -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiocoap==0.4.7 2 | eltako14bus==0.0.74 3 | numpy 4 | pillow>11 5 | pyserial==3.5 6 | pyserial-asyncio==0.6 7 | PyYAML==6.0.1 8 | StrEnum==0.4.15 9 | termcolor==2.4.0 10 | tkinterhtml==0.7 11 | requests==2.31.0 12 | enocean==0.60.1 13 | esp2_gateway_adapter==0.2.20 14 | zeroconf 15 | tkScrolledFrame==1.0.4 16 | xmltodict 17 | tomli 18 | tk 19 | pytest -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/screenshot.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/screenshot2.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grimmpp/enocean-device-manager/36b8dfeeb808c2083dbfcc551b61ff21308b0637/tests/__init__.py -------------------------------------------------------------------------------- /tests/mocks.py: -------------------------------------------------------------------------------- 1 | 2 | class AppBusMock(): 3 | 4 | def fire_event(self, event, data) -> None: 5 | pass 6 | 7 | async def fire_event(self, event, data) -> None: 8 | pass -------------------------------------------------------------------------------- /tests/test_app_config.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | import os 4 | import pickle 5 | 6 | from eo_man import load_dep_homeassistant 7 | load_dep_homeassistant() 8 | 9 | 10 | from eo_man.controller.app_bus import AppBus 11 | 12 | from eo_man.data.data_manager import DataManager 13 | from eo_man.data.application_data import ApplicationData 14 | 15 | 16 | class TestLoadAndStoreAppConfig(unittest.TestCase): 17 | 18 | def test_load(self): 19 | app_bus = AppBus() 20 | dm = DataManager(app_bus) 21 | 22 | filename = os.path.join( os.path.dirname(__file__), 'resources', 'test_app_config_1.eodm') 23 | self.assertTrue(os.path.isfile(filename), f"{filename} is no valid filename.") 24 | dm.load_application_data_from_file(filename) 25 | 26 | self.assertEqual(22, len(dm.devices), "Loaded device count does not match!") 27 | 28 | 29 | def test_load_save_load(self): 30 | app_bus = AppBus() 31 | dm = DataManager(app_bus) 32 | 33 | filename = os.path.join( os.path.dirname(__file__), 'resources', 'test_app_config_1.eodm') 34 | self.assertTrue(os.path.isfile(filename), f"{filename} is no valid filename.") 35 | app_data:ApplicationData = dm.load_application_data_from_file(filename) 36 | 37 | filename2 = os.path.join( os.path.dirname(__file__), 'resources', 'test_app_config_1_temp.eodm') 38 | dm.write_application_data_to_file(filename2) 39 | 40 | self.assertTrue(os.path.isfile(filename2), f"{filename2} is no valid filename.") 41 | app_data2:ApplicationData = dm.load_application_data_from_file(filename2) 42 | 43 | filename3 = os.path.join( os.path.dirname(__file__), 'resources', 'test_app_config_1_temp2.eodm') 44 | dm.write_application_data_to_file(filename3) 45 | 46 | self.assertTrue(os.path.isfile(filename3), f"{filename3} is no valid filename.") 47 | app_data3:ApplicationData = dm.load_application_data_from_file(filename3) 48 | 49 | # compare app_data 50 | self.assertEqual(len(app_data.devices), len(app_data2.devices)) 51 | self.assertEqual(len(app_data.data_filters), len(app_data2.data_filters)) 52 | self.assertEqual(len(app_data.selected_data_filter_name), len(app_data2.selected_data_filter_name)) 53 | # self.assertEqual(len(app_data.application_version), len(app_data2.application_version)) # will not match after version change 54 | 55 | self.assertEqual(len(app_data2.devices), len(app_data3.devices)) 56 | self.assertEqual(len(app_data2.data_filters), len(app_data3.data_filters)) 57 | self.assertEqual(len(app_data2.selected_data_filter_name), len(app_data3.selected_data_filter_name)) 58 | self.assertEqual(len(app_data2.application_version), len(app_data3.application_version)) 59 | 60 | # "binary" check 61 | self.assertEqual(pickle.dumps(app_data2), pickle.dumps(app_data3)) 62 | -------------------------------------------------------------------------------- /tests/test_base_id_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from eltakobus.message import ESP2Message, Regular4BSMessage 4 | 5 | class TestBaseIdUtils(unittest.TestCase): 6 | 7 | def test_add_addresses(self): 8 | a1 = b'\xff\x00\xff\x00' 9 | a2 = b'\x00\xff\x00\xff' 10 | 11 | r = self.add_two_addresses(a1, a2) 12 | self.assertEqual(r, b'\xff\xff\xff\xff') 13 | 14 | a1 = b'\xff\x00\x00\xff' 15 | r = self.add_two_addresses(a1, a2) 16 | self.assertEqual(r, b'\xff\xff\x01\xfe') 17 | 18 | def add_two_addresses(self, a1, a2): 19 | # return bytes((a + b) & 0xFF for a, b in zip(a1, a2)) 20 | return (int.from_bytes(a1, 'big') + int.from_bytes(a2, 'big')).to_bytes(4, byteorder='big') 21 | 22 | 23 | def test_add_base_id_to_local_message(self): 24 | base_id = b'\xff\x0C\x0b\x80' 25 | msg = Regular4BSMessage(b'\x00\x00\x00\x05', 50, b'\x08\x08\x08\x08', True) 26 | address = self.add_two_addresses(base_id, msg.address) 27 | msg2 = ESP2Message( msg.body[:8] + address + msg.body[12:] ) 28 | 29 | self.assertTrue(base_id not in msg.serialize()) 30 | self.assertTrue(base_id not in msg2.serialize()) 31 | 32 | self.assertTrue(address not in msg.serialize()) 33 | self.assertTrue(address in msg2.serialize()) 34 | 35 | 36 | def test_address_checks(self): 37 | address = b'\x00\x00\x00\x08' 38 | self.assertEqual( address[0], 0) 39 | self.assertEqual( address[0:2], b'\x00\x00') 40 | self.assertEqual( int.from_bytes(address[0:2]), 0) -------------------------------------------------------------------------------- /tests/test_detecting_usb.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | import os 4 | import asyncio 5 | 6 | file_dir = os.path.join( os.path.dirname(__file__), '..', 'eo_man', 'data') 7 | sys.path.append(file_dir) 8 | __import__('homeassistant') 9 | 10 | 11 | from tests.mocks import AppBusMock 12 | 13 | from eo_man.controller.serial_port_detector import SerialPortDetector 14 | 15 | 16 | 17 | class TestDetectingUsbDevices(unittest.TestCase): 18 | 19 | @unittest.skip("Not yet compatible with Linux") 20 | def test_print_device_info(self): 21 | SerialPortDetector.print_device_info() 22 | 23 | @unittest.skip("Not yet compatible with Linux") 24 | def test_port_detection(self): 25 | spd = SerialPortDetector(AppBusMock()) 26 | mapping = asyncio.run( spd.async_get_gateway2serial_port_mapping() ) 27 | pass -------------------------------------------------------------------------------- /tests/test_generate_docs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | file_dir = os.path.join( os.path.dirname(__file__), '..', 'eo_man', 'data') 6 | sys.path.append(file_dir) 7 | __import__('homeassistant') 8 | 9 | from eo_man.data.data_helper import EEP_MAPPING 10 | from eo_man.data.const import CONF_EEP, CONF_TYPE 11 | 12 | class TestGenerateDocs(unittest.TestCase): 13 | 14 | def test_generate_list_of_supported_devices(self): 15 | text = "# List of Supported Devices \n" 16 | text += "Devices not contained in this list are quite likely already supported by existing devices in this list. \n" 17 | 18 | text += "
\n" 19 | text += "(This list is auto-generated by using template definitions in [data_helper.py](https://github.com/grimmpp/enocean-device-manager/blob/main/eo_man/data/data_helper.py).) \n" 20 | text += "
\n" 21 | 22 | text += "| Brand | Name | Description | EEP | Sender EEP | HA Platform | \n" 23 | keys = ['brand', 'hw-type', 'description', CONF_EEP, 'sender_eep', CONF_TYPE] 24 | for k in keys: 25 | text += "|-----" 26 | text += "| \n" 27 | 28 | for e in EEP_MAPPING: 29 | if e.get('hw-type', '') != 'BusObject': 30 | for k in keys: 31 | text += f"| {e.get(k, '')} " 32 | text += "| \n" 33 | 34 | file='./docs/supported-devices.md' 35 | with open(file, 'w') as filetowrite: 36 | filetowrite.write(text) -------------------------------------------------------------------------------- /tests/test_pct14_import.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from eo_man import load_dep_homeassistant 4 | load_dep_homeassistant() 5 | 6 | from eo_man.data.pct14_data_manager import PCT14DataManager 7 | 8 | class TestPCT14Import(unittest.IsolatedAsyncioTestCase): 9 | 10 | async def test_import(self): 11 | devices = await PCT14DataManager.get_devices_from_pct14('./tests/resources/20240925_PCT14_export_test.xml') 12 | 13 | self.assertEqual(len(devices), 64) 14 | self.assertEqual(len([d for d in devices.values() if d.bus_device]), 15) 15 | 16 | 17 | async def test_write_sender_ids_into_existing_pct_export(self): 18 | devices = await PCT14DataManager.get_devices_from_pct14('./tests/resources/20240925_PCT14_export_test.xml') 19 | 20 | await PCT14DataManager.write_sender_ids_into_existing_pct14_export( 21 | source_filename='./tests/resources/20240925_PCT14_export_test.xml', 22 | target_filename='./tests/resources/20240925_PCT14_export_test_GENERATED.xml', 23 | devices=devices, 24 | base_id='00-00-B0-00') --------------------------------------------------------------------------------