├── .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 | [](https://github.com/grimmpp/home-assistant-eltako/commits/main)
2 | [](https://community.home-assistant.io/)
3 | [](https://community.home-assistant.io/t/eltako-baureihe-14-rs485-enocean-debugging/49712)
4 | [](/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')
--------------------------------------------------------------------------------