├── .gitignore ├── .semver ├── .travis.yml ├── Dockerfile-armhf ├── Dockerfile-i386 ├── Dockerfile-x64 ├── LICENSE ├── Makefile ├── README.md ├── config └── config-sample.yaml ├── docker-compose.yaml └── src ├── main.py ├── mqtt.py ├── requirements.txt ├── xiaomihub.py └── yamlparser.py /.gitignore: -------------------------------------------------------------------------------- 1 | #docker-compose file, because it contains passwords 2 | config.yml 3 | config.yaml 4 | config/config.yaml 5 | config/config.yml 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # IPython Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | -------------------------------------------------------------------------------- /.semver: -------------------------------------------------------------------------------- 1 | 1.0.5 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: python 3 | services: 4 | - docker 5 | 6 | env: 7 | global: 8 | - PROJECT=aqara-mqtt 9 | - IMAGE_NAME=$DOCKER_USERNAME/$PROJECT 10 | - VERSION=$(cat .semver) 11 | - MAJOR=$(cut -d. -f1 .semver) 12 | - MINOR=$(cut -d. -f2 .semver) 13 | - PATCH=$(cut -d. -f3 .semver) 14 | 15 | jobs: 16 | include: 17 | - stage: build x64 image 18 | before_script: 19 | - ARCH=x64 20 | 21 | script: 22 | # build 23 | - docker build -t $PROJECT -f Dockerfile-$ARCH . 24 | 25 | # push image 26 | - > 27 | if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 28 | docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" 29 | 30 | #1.0.0-arch 31 | docker tag $PROJECT $IMAGE_NAME:$VERSION-$ARCH 32 | docker push $IMAGE_NAME:$VERSION-$ARCH 33 | 34 | #1.0-arch 35 | docker tag $PROJECT $IMAGE_NAME:$MAJOR.$MINOR-$ARCH 36 | docker push $IMAGE_NAME:$MAJOR.$MINOR-$ARCH 37 | 38 | #1-arch 39 | docker tag $PROJECT $IMAGE_NAME:$MAJOR-$ARCH 40 | docker push $IMAGE_NAME:$MAJOR-$ARCH 41 | 42 | #latest by arch 43 | docker tag $PROJECT $IMAGE_NAME:$ARCH 44 | docker push $IMAGE_NAME:$ARCH 45 | fi 46 | 47 | - stage: build i386 image 48 | before_script: 49 | - ARCH=i386 50 | 51 | script: 52 | # build 53 | - docker build -t $PROJECT -f Dockerfile-$ARCH . 54 | 55 | # push image 56 | - > 57 | if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 58 | docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" 59 | 60 | #1.0.0-arch 61 | docker tag $PROJECT $IMAGE_NAME:$VERSION-$ARCH 62 | docker push $IMAGE_NAME:$VERSION-$ARCH 63 | 64 | #1.0-arch 65 | docker tag $PROJECT $IMAGE_NAME:$MAJOR.$MINOR-$ARCH 66 | docker push $IMAGE_NAME:$MAJOR.$MINOR-$ARCH 67 | 68 | #1-arch 69 | docker tag $PROJECT $IMAGE_NAME:$MAJOR-$ARCH 70 | docker push $IMAGE_NAME:$MAJOR-$ARCH 71 | 72 | #latest by arch 73 | docker tag $PROJECT $IMAGE_NAME:$ARCH 74 | docker push $IMAGE_NAME:$ARCH 75 | fi 76 | 77 | - stage: build armhf image 78 | before_script: 79 | - ARCH=armhf 80 | 81 | script: 82 | # prepare qemu 83 | - docker run --rm --privileged multiarch/qemu-user-static:register --reset 84 | # get qemu-arm-static binary 85 | - > 86 | mkdir tmp && 87 | pushd tmp && 88 | curl -L -o qemu-arm-static.tar.gz https://github.com/multiarch/qemu-user-static/releases/download/v2.6.0/qemu-arm-static.tar.gz && 89 | tar xzf qemu-arm-static.tar.gz && 90 | popd 91 | #copy it to container 92 | - sed -i -e '2iCOPY tmp/qemu-arm-static /usr/bin/qemu-arm-static\' Dockerfile-$ARCH 93 | 94 | # build 95 | - docker build -t $PROJECT -f Dockerfile-$ARCH . 96 | 97 | # push image 98 | - > 99 | if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 100 | docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" 101 | 102 | #1.0.0-arch 103 | docker tag $PROJECT $IMAGE_NAME:$VERSION-$ARCH 104 | docker push $IMAGE_NAME:$VERSION-$ARCH 105 | 106 | #1.0-arch 107 | docker tag $PROJECT $IMAGE_NAME:$MAJOR.$MINOR-$ARCH 108 | docker push $IMAGE_NAME:$MAJOR.$MINOR-$ARCH 109 | 110 | #1-arch 111 | docker tag $PROJECT $IMAGE_NAME:$MAJOR-$ARCH 112 | docker push $IMAGE_NAME:$MAJOR-$ARCH 113 | 114 | #latest by arch 115 | docker tag $PROJECT $IMAGE_NAME:$ARCH 116 | docker push $IMAGE_NAME:$ARCH 117 | fi 118 | -------------------------------------------------------------------------------- /Dockerfile-armhf: -------------------------------------------------------------------------------- 1 | FROM arm32v7/python:slim 2 | 3 | ENV LIBRARY_PATH=/lib:/usr/lib 4 | 5 | ADD src/requirements.txt / 6 | RUN apt-get update && apt-get install -y build-essential autoconf \ 7 | && pip install --upgrade pip && pip install -r /requirements.txt \ 8 | && apt-get remove -y build-essential autoconf \ 9 | && apt-get autoremove -y \ 10 | && rm -rf /var/lib/apt/lists/* 11 | 12 | WORKDIR /app 13 | COPY src /app 14 | 15 | CMD ["python3", "-u", "/app/main.py"] -------------------------------------------------------------------------------- /Dockerfile-i386: -------------------------------------------------------------------------------- 1 | FROM monster1025/alpine86-python 2 | 3 | ENV LIBRARY_PATH=/lib:/usr/lib 4 | 5 | ADD src/requirements.txt / 6 | RUN pip install --upgrade pip && pip install -r /requirements.txt 7 | 8 | WORKDIR /app 9 | COPY src /app 10 | 11 | CMD ["python", "-u", "/app/main.py"] 12 | -------------------------------------------------------------------------------- /Dockerfile-x64: -------------------------------------------------------------------------------- 1 | FROM jfloff/alpine-python:3.4 2 | 3 | ENV LIBRARY_PATH=/lib:/usr/lib 4 | 5 | ADD src/requirements.txt / 6 | RUN pip install --upgrade pip && pip install -r /requirements.txt 7 | 8 | WORKDIR /app 9 | COPY src /app 10 | 11 | CMD ["python", "-u", "/app/main.py"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARGS = `arg="$(filter-out $@,$(MAKECMDGOALS))" && echo $${arg:-${1}}` 2 | 3 | lint: 4 | rm -rf "src/__pycache__" 5 | python3 -m compileall src 6 | rm -rf "src/__pycache__" 7 | 8 | commit: lint 9 | git add . 10 | git commit -m "$(call ARGS,\"updating to lastest local code\")" 11 | git push 12 | 13 | %: 14 | @: 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aqara-MQTT 2 | [![Build Status](https://travis-ci.org/monster1025/aqara-mqtt.svg?branch=master)](https://travis-ci.org/monster1025/aqara-mqtt) 3 | 4 | Aqara (Xiaomi) Gateway to MQTT bridge. 5 | I use it for home assistant integration and it works well now. 6 | 7 | You need to activate developer mode (described here: http://bbs.xiaomi.cn/t-13198850) 8 | 9 | Bridge accept following MQTT set: 10 | ``` 11 | "home/plug/heater/status/set" -> on 12 | ``` 13 | 14 | will turn on plug/heater and translate devices state from gateway: 15 | ``` 16 | "home/plug/heater/status" on 17 | ``` 18 | 19 | ## Architecture 20 | Docker image support following architectures (you must choose your architecture in docker-compose): 21 | - armhf (raspberry pi 3, arm32v7) 22 | - i386 (x86 pc) 23 | - x64 (x64 pc) 24 | 25 | ## Config 26 | Edit file config/config-sample.yaml and rename it to config/config.yaml 27 | 28 | ## Docker-Compose 29 | Sample docker-compose.yaml file for user: 30 | ``` 31 | aqara: 32 | image: monster1025/aqara-mqtt:1-armhf 33 | container_name: aqara 34 | volumes: 35 | - "./config:/app/config" 36 | net: host 37 | restart: always 38 | ``` 39 | 40 | ## Related projects 41 | - https://github.com/lazcad/homeassistant 42 | - https://github.com/fooxy/homeassistant-aqara/ 43 | 44 | General discussions: 45 | - https://community.home-assistant.io/t/beta-xiaomi-gateway-integration/8213/288 46 | 47 | ## Home assistant examples: 48 | - Gateway rgb light as home assistant bulb template 49 | ``` 50 | - platform: mqtt_template 51 | name: "Main Gateway" 52 | state_topic: "home/gateway/main/rgb" 53 | command_topic: "home/gateway/main/rgb/set" 54 | command_on_template: "{%- if red is defined and green is defined and blue is defined -%}{{ red }},{{ green }},{{ blue }}{%- else -%}255,179,0{%- endif -%},{%- if brightness is defined -%}{{ (float(brightness) / 255 * 100) | round(0) }}{%- else -%}100{%- endif -%}" 55 | command_off_template: "0,0,0,0" 56 | state_template: "{%- if value.split(',')[3]| float > 0 -%}on{%- else -%}off{%- endif -%}" # must return `on` or `off` 57 | brightness_template: "{{ (float(value.split(',')[3])/100*255) | round(0) }}" 58 | red_template: "{{ value.split(',')[0] | int }}" 59 | green_template: "{{ value.split(',')[1] | int }}" 60 | blue_template: "{{ value.split(',')[2] | int }}" 61 | ``` 62 | 63 | - Switch automation example: 64 | ``` 65 | trigger: 66 | platform: mqtt 67 | topic: home/switch/hall/status 68 | payload: 'click' 69 | action: 70 | service: script.hall_force_light_on 71 | ``` 72 | -------------------------------------------------------------------------------- /config/config-sample.yaml: -------------------------------------------------------------------------------- 1 | mqtt: 2 | server: 192.168.1.2 3 | port: 1883 4 | username: username 5 | password: passw0rd 6 | prefix: home 7 | #secure mqtt. uncomment to enable ssl: 8 | #ca: "config/roots.pem" 9 | #tls_version: "tlsv1.2" 10 | #send report as json 11 | json: False 12 | 13 | gateway: 14 | ip: 192.168.0.47 15 | password: passw0rd 16 | unwanted_data_fix: True #leave it True if it works for you. Otherwise (if you didn't recieve any data from sensors) - set it to False 17 | polling_interval: 2 18 | polling_models: 19 | - motion 20 | 21 | sids: 22 | # motion 23 | 158d0000e7c7ad: 24 | model: motion 25 | name: hall 26 | 27 | # temperature 28 | 158d0001149b3c: 29 | model: sensor_ht 30 | name: living 31 | 32 | # plugs 33 | 158d00010dd98d: 34 | model: plug 35 | name: heater 36 | 37 | # buttons 38 | 158d00012d5720: 39 | model: switch 40 | name: kitchen 41 | 42 | # cube 43 | 158d00011065e3: 44 | model: cube 45 | name: main 46 | 47 | # gateway 48 | f0b429aa1463: 49 | model: gateway 50 | name: main 51 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | aqara: 2 | #image: monster1025/aqara-mqtt:1-armhf 3 | build: . 4 | dockerfile: Dockerfile-armhf 5 | container_name: aqara 6 | volumes: 7 | - "./config:/app/config" 8 | net: host 9 | restart: always 10 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import threading 4 | import json 5 | import signal 6 | 7 | # mine 8 | import mqtt 9 | import yamlparser 10 | from xiaomihub import XiaomiHub 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | def process_gateway_messages(gateway, client, stop_event): 17 | while not stop_event.is_set(): 18 | try: 19 | packet = gateway._queue.get() 20 | if packet is None: 21 | continue 22 | _LOGGER.debug("data from queuee: " + format(packet)) 23 | 24 | sid = packet.get("sid", None) 25 | model = packet.get("model", "") 26 | data = packet.get("data", "") 27 | 28 | if (sid != None and data != ""): 29 | data_decoded = json.loads(data) 30 | client.publish(model, sid, data_decoded) 31 | gateway._queue.task_done() 32 | except Exception as e: 33 | _LOGGER.error('Error while sending from gateway to mqtt: ', str(e)) 34 | _LOGGER.info("Stopping Gateway Thread ...") 35 | 36 | 37 | def read_motion_data(gateway, client, polling_interval, polling_models, stop_event): 38 | first = True 39 | while not stop_event.is_set(): 40 | try: 41 | for device_type in gateway.XIAOMI_DEVICES: 42 | devices = gateway.XIAOMI_DEVICES[device_type] 43 | for device in devices: 44 | model = device.get("model", "") 45 | if model not in polling_models: 46 | continue 47 | sid = device['sid'] 48 | 49 | sensor_resp = gateway.get_from_hub(sid) 50 | if sensor_resp is None: 51 | continue 52 | if sensor_resp['sid'] != sid: 53 | _LOGGER.error("Error: Response sid(" + sensor_resp['sid'] + ") differs from requested(" + sid + "). Skipping.") 54 | continue 55 | 56 | data = json.loads(sensor_resp['data']) 57 | if device['data'] != data or first: 58 | device['data'] = data 59 | _LOGGER.debug("Polling result differs for " + str(model) + " with sid(First: " + str(first) + "): " + str(sid) + "; " + str(data)) 60 | client.publish(model, sid, data) 61 | first = False 62 | except Exception as e: 63 | _LOGGER.error('Error while sending from mqtt to gateway: ', str(e)) 64 | time.sleep(polling_interval) 65 | _LOGGER.info("Stopping Polling Thread ...") 66 | 67 | 68 | def process_mqtt_messages(gateway, client, stop_event): 69 | while not stop_event.is_set(): 70 | try: 71 | data = client._queue.get() 72 | if data is None: 73 | continue 74 | _LOGGER.debug("data from mqtt: " + format(data)) 75 | 76 | sid = data.get("sid", None) 77 | values = data.get("values", dict()) 78 | 79 | gateway.write_to_hub(sid, **values) 80 | client._queue.task_done() 81 | except Exception as e: 82 | _LOGGER.error('Error while sending from mqtt to gateway: ', str(e)) 83 | _LOGGER.info("Stopping MQTT Thread ...") 84 | 85 | 86 | def exit_handler(signal, frame): 87 | print('Exiting') 88 | stop_event.set() 89 | t3.join() 90 | client.disconnect() 91 | t2.join() 92 | gateway.stop() 93 | t1.join() 94 | 95 | if __name__ == "__main__": 96 | _LOGGER.info("Loading config file...") 97 | config = yamlparser.load_yaml('config/config.yaml') 98 | gateway_pass = yamlparser.get_gateway_password(config) 99 | polling_interval = config['gateway'].get("polling_interval", 2) 100 | polling_models = config['gateway'].get("polling_models", ['motion']) 101 | gateway_ip = config['gateway'].get("ip", None) 102 | 103 | signal.signal(signal.SIGINT, exit_handler) 104 | signal.signal(signal.SIGTERM, exit_handler) 105 | 106 | _LOGGER.info("Init mqtt client.") 107 | client = mqtt.Mqtt(config) 108 | client.connect() 109 | # only this devices can be controlled from MQTT 110 | client.subscribe("gateway", "+", "+", "set") 111 | client.subscribe("gateway", "+", "write", None) 112 | client.subscribe("plug", "+", "status", "set") 113 | 114 | gateway = XiaomiHub(gateway_pass, gateway_ip, config) 115 | stop_event = threading.Event() 116 | t1 = threading.Thread(target=process_gateway_messages, args=[gateway, client, stop_event]) 117 | t1.daemon = True 118 | t1.start() 119 | 120 | t2 = threading.Thread(target=process_mqtt_messages, args=[gateway, client, stop_event]) 121 | t2.daemon = True 122 | t2.start() 123 | 124 | t3 = threading.Thread(target=read_motion_data, args=[gateway, client, polling_interval, polling_models, stop_event]) 125 | t3.daemon = True 126 | t3.start() 127 | 128 | while True: 129 | if stop_event.is_set(): 130 | break 131 | time.sleep(10) 132 | -------------------------------------------------------------------------------- /src/mqtt.py: -------------------------------------------------------------------------------- 1 | import paho.mqtt.client as mqtt 2 | import logging 3 | import os 4 | import ssl 5 | from queue import Queue 6 | from threading import Thread 7 | import json 8 | 9 | _LOGGER = logging.getLogger(__name__) 10 | 11 | 12 | class Mqtt: 13 | event_based_sensors = ["switch", "cube"] 14 | motion_sensors = ["motion", "sensor_motion.aq2"] 15 | magnet_sensors = ["magnet"] 16 | username = "" 17 | password = "" 18 | server = "localhost" 19 | port = 1883 20 | ca = None 21 | tlsvers = None 22 | prefix = "home" 23 | 24 | _client = None 25 | _sids = None 26 | _queue = None 27 | _threads = None 28 | 29 | def __init__(self, config): 30 | if (config == None): 31 | raise "Config is null" 32 | 33 | # load sids dictionary 34 | self._sids = config.get("sids", None) 35 | if (self._sids == None): 36 | self._sids = dict({}) 37 | 38 | # load mqtt settings 39 | mqttConfig = config.get("mqtt", None) 40 | if (mqttConfig == None): 41 | raise "Config mqtt section is null" 42 | 43 | self.username = mqttConfig.get("username", "") 44 | self.password = mqttConfig.get("password", "") 45 | self.server = mqttConfig.get("server", "localhost") 46 | self.port = mqttConfig.get("port", 1883) 47 | self.prefix = mqttConfig.get("prefix", "home") 48 | self.ca = mqttConfig.get("ca",None) 49 | self.tlsvers = self._get_tls_version( 50 | mqttConfig.get("tls_version","tlsv1.2") 51 | ) 52 | self.json = mqttConfig.get("json", False) 53 | self._queue = Queue() 54 | self._threads = [] 55 | 56 | def connect(self): 57 | _LOGGER.info("Connecting to MQTT server " + self.server + ":" + str(self.port) + " with username (" + self.username + ":" + self.password + ")") 58 | self._client = mqtt.Client() 59 | if (self.username != "" and self.password != ""): 60 | self._client.username_pw_set(self.username, self.password) 61 | self._client.on_message = self._mqtt_process_message 62 | self._client.on_connect = self._mqtt_on_connect 63 | if (self.ca != None): 64 | self._client.tls_set( 65 | ca_certs=self.ca, 66 | cert_reqs=ssl.CERT_REQUIRED, 67 | tls_version=self.tlsvers 68 | ) 69 | 70 | self._client.tls_insecure_set(False) 71 | 72 | self._client.connect(self.server, self.port, 60) 73 | # run message processing loop 74 | t1 = Thread(target=self._mqtt_loop) 75 | t1.start() 76 | self._threads.append(t1) 77 | 78 | def disconnect(self): 79 | self._client.disconnect() 80 | self._queue.put(None) 81 | 82 | def subscribe(self, model="+", name="+", prop="+", command=None): 83 | topic = self.prefix + "/" + model + "/" + name + "/" + prop 84 | if command is not None: 85 | topic += "/" + command 86 | _LOGGER.info("Subscribing to " + topic + ".") 87 | self._client.subscribe(topic) 88 | 89 | def publish(self, model, sid, data, retain=True): 90 | sidprops = self._sids.get(sid, None) 91 | if (sidprops != None): 92 | model = sidprops.get("model", model) 93 | sid = sidprops.get("name", sid) 94 | 95 | items = {} 96 | for key, value in data.items(): 97 | # fix for latest motion value 98 | if (model in self.motion_sensors and key == "status"): 99 | items["no_motion"] = 0 100 | if (model in self.motion_sensors and key == "no_motion"): 101 | items[key] = value 102 | key = "status" 103 | value = "no_motion" 104 | if (model in self.magnet_sensors and key == "no_close"): 105 | key = "status" 106 | value = "open" 107 | # do not retain event-based sensors (like switches and cubes). 108 | if (model in self.event_based_sensors): 109 | retain = False 110 | # fix for rgb format 111 | if (key == "rgb" and str(value).isdigit()): 112 | value = self._color_xiaomi_to_rgb(str(value)) 113 | items[key] = value 114 | 115 | if self.json == True: 116 | PATH_FMT = self.prefix + "/{model}/{sid}/json" 117 | topic = PATH_FMT.format(model=model, sid=sid) 118 | values = {} 119 | values['sid'] = sid 120 | for key in items: 121 | values[key] = items[key] 122 | jsondata = json.dumps(values) 123 | _LOGGER.info("Publishing message to topic " + topic + ": " + str(jsondata) + ".") 124 | self._client.publish(topic, payload=jsondata, qos=0, retain=retain) 125 | else: 126 | for key in items: 127 | PATH_FMT = self.prefix + "/{model}/{sid}/{prop}" 128 | topic = PATH_FMT.format(model=model, sid=sid, prop=key) 129 | _LOGGER.info("Publishing message to topic " + topic + ": " + str(items[key]) + ".") 130 | self._client.publish(topic, payload=items[key], qos=0, retain=retain) 131 | 132 | def _mqtt_on_connect(self, client, userdata, rc, unk): 133 | _LOGGER.info("Connected to mqtt server.") 134 | 135 | def _mqtt_process_message(self, client, userdata, msg): 136 | _LOGGER.info("Processing message in " + str(msg.topic) + ": " + str(msg.payload) + ".") 137 | 138 | # need to strip prefix to make parts assignment reliable 139 | parts = msg.topic.replace(self.prefix+"/","").split("/") 140 | partlen = len(parts) 141 | if len(parts) < 3: 142 | # should we return an error message ? 143 | return 144 | 145 | model = parts[0] 146 | query_sid = parts[1] # sid or name part 147 | param = parts[2] # param part 148 | method = None 149 | if len(parts) > 3: 150 | method = parts[3] 151 | else: 152 | method = parts[2] 153 | 154 | name = "" # we will find it next 155 | sid = query_sid 156 | isFound = False 157 | for current_sid in self._sids: 158 | if (current_sid == None): 159 | continue 160 | sidprops = self._sids.get(current_sid, None) 161 | if sidprops == None: 162 | continue 163 | sidname = sidprops.get("name", current_sid) 164 | sidmodel = sidprops.get("model", "") 165 | if (sidname == query_sid and sidmodel == model): 166 | sid = current_sid 167 | name = sidname 168 | isFound = True 169 | _LOGGER.debug("Found " + sid + " = " + name) 170 | break 171 | else: 172 | _LOGGER.debug(sidmodel + "-" + sidname + " is not " + model + "-" + query_sid + ".") 173 | continue 174 | 175 | if isFound == False: 176 | # should we return an error message ? 177 | return 178 | 179 | if method == "set": 180 | # use single value set method 181 | 182 | value = (msg.payload).decode('utf-8') 183 | if value.isdigit(): 184 | value = int(value) 185 | 186 | # fix for rgb format 187 | if (param == "rgb" and "," in str(value)): 188 | value = self._color_rgb_to_xiaomi(value) 189 | 190 | # prepare values dict 191 | data = {'sid': sid, 'model': model, 'name': name, 192 | 'values': {param: value}} 193 | # put in process queuee 194 | self._queue.put(data) 195 | 196 | elif method == "write": 197 | # use raw write method to the sensor, we expect a jsonified dict here. 198 | values = json.loads((msg.payload).decode('utf-8')) 199 | data = {'sid': sid, 'model': model, 'name': name, 200 | 'values': values} 201 | # put in process queuee 202 | self._queue.put(data) 203 | 204 | def _mqtt_loop(self): 205 | _LOGGER.info("Starting mqtt loop.") 206 | self._client.loop_forever() 207 | 208 | def _color_xiaomi_to_rgb(self, xiaomi_color): 209 | intval = int(xiaomi_color) 210 | blue = (intval) & 255 211 | green = (intval >> 8) & 255 212 | red = (intval >> 16) & 255 213 | bright = (intval >> 24) & 255 214 | value = str(red)+","+str(green)+","+str(blue)+","+str(bright) 215 | return value 216 | 217 | def _color_rgb_to_xiaomi(self, rgb_string): 218 | arr = rgb_string.split(",") 219 | r = int(arr[0]) 220 | g = int(arr[1]) 221 | b = int(arr[2]) 222 | if len(arr) > 3: 223 | bright = int(arr[3]) 224 | else: 225 | bright = 255 226 | value = int('%02x%02x%02x%02x' % (bright, r, g, b), 16) 227 | return value 228 | 229 | def _get_tls_version(self,tlsString): 230 | switcher = { 231 | "tlsv1": ssl.PROTOCOL_TLSv1, 232 | "tlsv1.1": ssl.PROTOCOL_TLSv1_1, 233 | "tlsv1.2": ssl.PROTOCOL_TLSv1_2 234 | } 235 | return switcher.get(tlsString,ssl.PROTOCOL_TLSv1_2) 236 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt 2 | pyyaml 3 | pycrypto 4 | -------------------------------------------------------------------------------- /src/xiaomihub.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import struct 3 | import json 4 | import logging 5 | import sys 6 | import select 7 | from collections import defaultdict 8 | from queue import Queue 9 | from threading import Thread 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | # MANDATORY!!!! NEED TO TURN OFF "_process_report" THREAD IF CODE IS UPDATED!!! 14 | 15 | 16 | class XiaomiHub: 17 | GATEWAY_KEY = None 18 | GATEWAY_IP = None 19 | GATEWAY_PORT = None 20 | GATEWAY_SID = None 21 | GATEWAY_TOKEN = None 22 | 23 | XIAOMI_DEVICES = defaultdict(list) 24 | XIAOMI_HA_DEVICES = defaultdict(list) 25 | 26 | MULTICAST_ADDRESS = '224.0.0.50' 27 | MULTICAST_PORT = 9898 28 | GATEWAY_DISCOVERY_ADDRESS = '224.0.0.50' 29 | GATEWAY_DISCOVERY_PORT = 4321 30 | SOCKET_BUFSIZE = 1024 31 | 32 | def __init__(self, key, gateway_ip=None, config=None): 33 | self.GATEWAY_KEY = key 34 | self._listening = False 35 | self._queue = None 36 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 37 | self._mcastsocket = None 38 | self._deviceCallbacks = defaultdict(list) 39 | self._threads = [] 40 | self._read_unwanted_data_enabled = True 41 | 42 | if gateway_ip is not None: 43 | self.GATEWAY_DISCOVERY_ADDRESS = gateway_ip 44 | 45 | if config is not None and 'gateway' in config and 'unwanted_data_fix' in config['gateway']: 46 | self._read_unwanted_data_enabled = (config['gateway']['unwanted_data_fix'] == True) 47 | _LOGGER.info('"Read unwanted data" fix is {0}'.format(self._read_unwanted_data_enabled)) 48 | 49 | try: 50 | _LOGGER.info('Discovering Xiaomi Gateways using address {0}'.format(self.GATEWAY_DISCOVERY_ADDRESS)) 51 | data = self._send_socket('{"cmd":"whois"}', "iam", self.GATEWAY_DISCOVERY_ADDRESS, self.GATEWAY_DISCOVERY_PORT) 52 | if data["model"] == "gateway": 53 | self.GATEWAY_IP = data["ip"] 54 | self.GATEWAY_PORT = int(data["port"]) 55 | self.GATEWAY_SID = data["sid"] 56 | _LOGGER.info('Gateway found on IP {0}'.format(self.GATEWAY_IP)) 57 | else: 58 | _LOGGER.error('Error with gateway response : {0}'.format(data)) 59 | except Exception as e: 60 | raise 61 | _LOGGER.error("Cannot discover hub using whois: {0}".format(e)) 62 | 63 | self._socket.close() 64 | 65 | if self.GATEWAY_IP is None: 66 | _LOGGER.error('No Gateway found. Cannot continue') 67 | return None 68 | 69 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 70 | 71 | _LOGGER.info('Creating Multicast Socket') 72 | self._mcastsocket = self._create_mcast_socket() 73 | if self._listen() is True: 74 | _LOGGER.info("Listening") 75 | 76 | _LOGGER.info('Discovering Xiaomi Devices') 77 | self._discover_devices() 78 | 79 | def _discover_devices(self): 80 | 81 | cmd = '{"cmd" : "get_id_list"}' 82 | resp = self._send_cmd(cmd, "get_id_list_ack") 83 | self.GATEWAY_TOKEN = resp["token"] 84 | sids = json.loads(resp["data"]) 85 | 86 | _LOGGER.info('Found {0} devices'.format(len(sids))) 87 | 88 | sensors = ['sensor_ht', 'weather.v1', 'sensor_wleak.aq1'] 89 | binary_sensors = ['magnet', 'sensor_magnet.aq2', 'motion', 90 | 'sensor_motion.aq2', 'switch', 'sensor_switch.aq2', 91 | '86sw1', '86sw2', 'cube'] 92 | switches = ['plug', 'ctrl_neutral1', 'ctrl_neutral2'] 93 | 94 | for sid in sids: 95 | cmd = '{"cmd":"read","sid":"' + sid + '"}' 96 | resp = self._send_cmd(cmd, "read_ack") 97 | model = resp["model"] 98 | 99 | xiaomi_device = { 100 | "model": model, 101 | "sid": resp["sid"], 102 | "short_id": resp["short_id"], 103 | "data": json.loads(resp["data"])} 104 | 105 | device_type = None 106 | if model in sensors: 107 | device_type = 'sensor' 108 | elif model in binary_sensors: 109 | device_type = 'binary_sensor' 110 | elif model in switches: 111 | device_type = 'switch' 112 | else: 113 | device_type = 'sensor' # not really matters 114 | 115 | self.XIAOMI_DEVICES[device_type].append(xiaomi_device) 116 | 117 | def _send_cmd(self, cmd, rtnCmd): 118 | return self._send_socket(cmd, rtnCmd, self.GATEWAY_IP, self.GATEWAY_PORT) 119 | 120 | def _read_unwanted_data(self): 121 | if not self._read_unwanted_data_enabled: 122 | return 123 | 124 | try: 125 | socket = self._socket 126 | socket_list = [sys.stdin, socket] 127 | read_sockets, write_sockets, error_sockets = select.select(socket_list, [], []) 128 | for sock in read_sockets: 129 | if sock == socket: 130 | data = sock.recv(4096) 131 | _LOGGER.error("Unwanted data recieved: " + str(data)) 132 | except Exception as e: 133 | _LOGGER.error("Cannot read unwanted data: " + str(e)) 134 | 135 | def _send_socket(self, cmd, rtnCmd, ip, port): 136 | socket = self._socket 137 | try: 138 | _LOGGER.debug('Sending to GW {0}'.format(cmd)) 139 | self._read_unwanted_data() 140 | 141 | socket.settimeout(30.0) 142 | socket.sendto(cmd.encode(), (ip, port)) 143 | socket.settimeout(30.0) 144 | data, addr = socket.recvfrom(1024) 145 | if len(data) is not None: 146 | resp = json.loads(data.decode()) 147 | _LOGGER.debug('Recieved from GW {0}'.format(resp)) 148 | if resp["cmd"] == rtnCmd: 149 | return resp 150 | else: 151 | _LOGGER.error("Response from {0} does not match return cmd".format(ip)) 152 | _LOGGER.error(data) 153 | else: 154 | _LOGGER.error("No response from Gateway") 155 | except socket.timeout: 156 | _LOGGER.error("Cannot connect to Gateway") 157 | socket.close() 158 | 159 | def write_to_hub(self, sid, **values): 160 | key = self._get_key() 161 | cmd = { 162 | "cmd": "write", 163 | "sid": sid, 164 | "data": dict(key=key, **values) 165 | } 166 | return self._send_cmd(json.dumps(cmd), "write_ack") 167 | 168 | def get_from_hub(self, sid): 169 | cmd = '{ "cmd":"read","sid":"' + sid + '"}' 170 | return self._send_cmd(cmd, "read_ack") 171 | 172 | def _get_key(self): 173 | from Crypto.Cipher import AES 174 | IV = bytes(bytearray.fromhex('17996d093d28ddb3ba695a2e6f58562e')) 175 | encryptor = AES.new(self.GATEWAY_KEY, AES.MODE_CBC, IV=IV) 176 | ciphertext = encryptor.encrypt(self.GATEWAY_TOKEN) 177 | return ''.join('{:02x}'.format(x) for x in ciphertext) 178 | 179 | def _create_mcast_socket(self): 180 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 181 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 182 | sock.bind((self.MULTICAST_ADDRESS, self.MULTICAST_PORT)) 183 | mreq = struct.pack("4sl", socket.inet_aton(self.MULTICAST_ADDRESS), socket.INADDR_ANY) 184 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) 185 | return sock 186 | 187 | def _listen(self): 188 | """Start listening.""" 189 | self._queue = Queue() 190 | self._listening = True 191 | 192 | t1 = Thread(target=self._listen_to_msg, args=()) 193 | self._threads.append(t1) 194 | t1.daemon = True 195 | t1.start() 196 | 197 | # t2 = Thread(target=self._process_report, args=()) 198 | # self._threads.append(t2) 199 | # t2.da = True 200 | # t2.start() 201 | 202 | return True 203 | 204 | def stop(self): 205 | """Stop listening.""" 206 | self._listening = False 207 | self._queue.put(None) 208 | 209 | for t in self._threads: 210 | t.join() 211 | 212 | if self._mcastsocket is not None: 213 | self._mcastsocket.close() 214 | self._mcastsocket = None 215 | 216 | def _listen_to_msg(self): 217 | while self._listening: 218 | if self._mcastsocket is not None: 219 | data, addr = self._mcastsocket.recvfrom(self.SOCKET_BUFSIZE) 220 | try: 221 | data = json.loads(data.decode("ascii")) 222 | cmd = data['cmd'] 223 | _LOGGER.debug(format(data)) 224 | if cmd == 'heartbeat' and data['model'] == 'gateway': 225 | self.GATEWAY_TOKEN = data['token'] 226 | elif cmd == 'report' or cmd == 'heartbeat': 227 | self._queue.put(data) 228 | else: 229 | _LOGGER.error('Unknown multicast data : {0}'.format(data)) 230 | except Exception as e: 231 | raise 232 | _LOGGER.error('Cannot process multicast message : {0}'.format(data)) 233 | 234 | def _process_report(self): 235 | while self._listening: 236 | packet = self._queue.get(True) 237 | if isinstance(packet, dict): 238 | try: 239 | sid = packet['sid'] 240 | # model = packet['model'] 241 | data = json.loads(packet['data']) 242 | 243 | for device in self.XIAOMI_HA_DEVICES[sid]: 244 | device.push_data(data) 245 | 246 | except Exception as e: 247 | _LOGGER.error("Cannot process Report: {0}".format(e)) 248 | 249 | self._queue.task_done() 250 | 251 | 252 | class XiaomiDevice(): 253 | """Representation a base Xiaomi device.""" 254 | 255 | def __init__(self, device, name, xiaomi_hub): 256 | """Initialize the xiaomi device.""" 257 | self._sid = device['sid'] 258 | self._name = '{}_{}'.format(name, self._sid) 259 | self.parse_data(device['data']) 260 | 261 | self.xiaomi_hub = xiaomi_hub 262 | xiaomi_hub.XIAOMI_HA_DEVICES[self._sid].append(self) 263 | 264 | @property 265 | def name(self): 266 | """Return the name of the device.""" 267 | return self._name 268 | 269 | @property 270 | def should_poll(self): 271 | return False 272 | 273 | def push_data(self, data): 274 | return True 275 | 276 | def parse_data(self, data): 277 | return True 278 | -------------------------------------------------------------------------------- /src/yamlparser.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from os import environ 3 | import logging 4 | 5 | _LOGGER = logging.getLogger(__name__) 6 | 7 | 8 | def load_yaml(file): 9 | try: 10 | if environ.get("AQARA_MQTT_CONFIG") is not None: 11 | stram = environ.get("AQARA_MQTT_CONFIG") 12 | else: 13 | stram = open(file, "r") 14 | yaml_data = yaml.load(stram) 15 | return yaml_data 16 | except Exception as e: 17 | raise 18 | _LOGGER.error("Can't load yaml with sids %r (%r)" % (file, e)) 19 | 20 | 21 | def get_gateway_password(config, ip=""): 22 | if (config == None): 23 | raise "Config is null" 24 | configGateway = config.get("gateway", None) 25 | if (configGateway == None): 26 | raise "Config gateway is null" 27 | password = configGateway.get("password", None) 28 | if (password == None): 29 | raise "Config gateway passowrd is null" 30 | return password 31 | --------------------------------------------------------------------------------