├── dasshio ├── requirements.txt ├── Dockerfile ├── config.json ├── dasshio.py └── README.md ├── git_pull ├── build.json ├── logo.png ├── CHANGELOG.md ├── Dockerfile ├── config.json └── run.sh ├── create_all.sh ├── repository.json ├── README.md ├── local_audio_player ├── README.md ├── Dockerfile ├── config.json └── run.sh ├── broadlink_s1c ├── config.json ├── README.md ├── run.sh ├── Dockerfile ├── s1c.py └── broadlink.py ├── broadlink_s1c_2 ├── config.json ├── README.md ├── run.sh ├── Dockerfile ├── s1c.py └── broadlink.py ├── rpi-rf-receiver ├── README.md ├── config.json ├── run.sh ├── Dockerfile ├── rpi-rf_receive.py └── rpi-rf_receive2.py ├── rpi-rf-receiver3 ├── README.md ├── config.json ├── run.sh ├── Dockerfile └── rpi-rf_receive.py ├── test ├── config.json ├── run.sh └── Dockerfile ├── hc-sr04 ├── config.json ├── range_sensor.py ├── run.sh └── Dockerfile ├── .gitignore └── create_hassio_addon.sh /dasshio/requirements.txt: -------------------------------------------------------------------------------- 1 | netifaces 2 | requests 3 | scapy-python3 4 | -------------------------------------------------------------------------------- /git_pull/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "args": { 3 | "CLI_VERSION": "1.0.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /git_pull/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pantomax/hassio-addons/HEAD/git_pull/logo.png -------------------------------------------------------------------------------- /create_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | for arch in "armhf" "aarch64" "i386" "amd64" 5 | do 6 | ./create_hassio_addon.sh -a $arch "$@" & 7 | done 8 | wait 9 | -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Pantomax addons repository", 4 | "url": "https://github.com/pantomax/haxio", 5 | "maintainer": "Pantomax " 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hassio-addons 2 | 3 | Repository for hass.io addons. 4 | 5 | ## [Broadlink S1C] 6 | 7 | Broadlink S1C Addon : working 8 | 9 | ## [Rpi RF Receiver] 10 | 11 | RF Receiver Addon : working 12 | 13 | 14 | -------------------------------------------------------------------------------- /local_audio_player/README.md: -------------------------------------------------------------------------------- 1 | usage 2 | ```yaml 3 | - service: hassio.addon_stdin 4 | data: 5 | addon: local_audio_player 6 | input: "http://192.168.1.100:8123/local/song.mp3" 7 | ``` 8 | -------------------------------------------------------------------------------- /local_audio_player/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | 7 | # Setup base, install sox package 8 | RUN apk add --no-cache jq sox 9 | 10 | # Copy data 11 | COPY run.sh / 12 | RUN chmod a+x /run.sh 13 | 14 | CMD [ "/run.sh" ] 15 | -------------------------------------------------------------------------------- /local_audio_player/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Audio Player", 3 | "version": "1.55", 4 | "slug": "audio_player", 5 | "description": "Play audio files", 6 | "startup": "before", 7 | "boot": "auto", 8 | "stdin": true, 9 | "audio": true, 10 | "hassio_api": true, 11 | "options": {}, 12 | "schema": {}, 13 | "host_network": true 14 | } 15 | -------------------------------------------------------------------------------- /local_audio_player/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "starting!" 5 | 6 | # Read from STDIN aliases to play file 7 | while read -r input; do 8 | # removing JSON stuff 9 | input="$(echo "$input" | jq --raw-output '.')" 10 | echo "[Info] Read alias: $input" 11 | 12 | if ! msg="$(play $input)"; then 13 | echo "[Error] Playing failed -> $msg" 14 | fi 15 | done 16 | -------------------------------------------------------------------------------- /dasshio/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | ENV LANG C.UTF-8 5 | 6 | # Setup 7 | RUN apk add --no-cache python3 python3-dev gcc linux-headers musl-dev tcpdump \ 8 | && pip3 install --no-cache --upgrade pip 9 | COPY requirements.txt /tmp/ 10 | RUN pip3 install --requirement /tmp/requirements.txt 11 | 12 | # Copy data for add-on 13 | COPY dasshio.py / 14 | 15 | CMD ["python3", "dasshio.py"] -------------------------------------------------------------------------------- /broadlink_s1c/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Broadlink S1c", 3 | "version": "1.53", 4 | "slug": "s1c", 5 | "description": "Broadlink S1C", 6 | "startup": "application", 7 | "boot": "auto", 8 | "ports": {"5055/tcp": null}, 9 | "options": { 10 | "code": "/share/s1c.py", 11 | "requirements": [ 12 | "broadlink" 13 | ] 14 | }, 15 | "schema": {"code": "str", 16 | "requirements": ["str"], 17 | "clean": "bool?"}, 18 | "map": ["share"] 19 | } 20 | -------------------------------------------------------------------------------- /broadlink_s1c_2/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Broadlink S1c", 3 | "version": "2.01", 4 | "slug": "s1c2", 5 | "description": "Broadlink S1C", 6 | "startup": "application", 7 | "boot": "auto", 8 | "ports": {"5055/tcp": null}, 9 | "options": { 10 | "code": "/share/s1c.py", 11 | "requirements": [ 12 | "broadlink" 13 | ] 14 | }, 15 | "schema": {"code": "str", 16 | "requirements": ["str"], 17 | "clean": "bool?"}, 18 | "map": ["share"] 19 | } 20 | -------------------------------------------------------------------------------- /git_pull/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.0 4 | - Allow to use user/password authentication for GIT 5 | - New options `deployment_user` and `deployment_password` 6 | 7 | ## 3.0 8 | - New CLI 9 | - Update base image 10 | - Backup of files before clearing the /config folder 11 | - Copy back all non YAML files and the secrets.yaml after the git clone 12 | - More verbose error handling. Also logging the GIT exceptions. 13 | - Splitted code out into functions 14 | - Check SSH connection before setting the key 15 | -------------------------------------------------------------------------------- /broadlink_s1c/README.md: -------------------------------------------------------------------------------- 1 | # Broadlink S1C Addon 2 | 3 | This addon is for the Broadlink S1C Alarm System 4 | 5 | 1. Install the addon. 6 | 7 | 2. Copy "broadlink.py" and "s1c.py" in the "share" share of your hass.io. 8 | 9 | 3. Edit "s1c.py" adding your mosquitto address, port, user and password. Then add your S1C IP Address and Mac Address. 10 | 11 | 4. Start the addon (it takes also 10 minutes to start, don't worry) and eventually reboot the raspberry pi (hassio host reboot) if the addon starts giving you strange readings. 12 | -------------------------------------------------------------------------------- /broadlink_s1c_2/README.md: -------------------------------------------------------------------------------- 1 | # Broadlink S1C Addon 2 | 3 | This addon is for the Broadlink S1C Alarm System 4 | 5 | 1. Install the addon. 6 | 7 | 2. Copy "broadlink.py" and "s1c.py" in the "share" share of your hass.io. 8 | 9 | 3. Edit "s1c.py" adding your mosquitto address, port, user and password. Then add your S1C IP Address and Mac Address. 10 | 11 | 4. Start the addon (it takes also 10 minutes to start, don't worry) and eventually reboot the raspberry pi (hassio host reboot) if the addon starts giving you strange readings. 12 | -------------------------------------------------------------------------------- /git_pull/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | 7 | # Setup base 8 | RUN apk add --no-cache jq curl git openssh-client 9 | 10 | # Hass.io CLI 11 | ARG BUILD_ARCH 12 | ARG CLI_VERSION 13 | RUN apk add --no-cache curl \ 14 | && curl -Lso /usr/bin/hassio https://github.com/home-assistant/hassio-cli/releases/download/${CLI_VERSION}/hassio_${BUILD_ARCH} \ 15 | && chmod a+x /usr/bin/hassio 16 | 17 | # Copy data 18 | COPY run.sh / 19 | RUN chmod a+x /run.sh 20 | 21 | CMD [ "/run.sh" ] 22 | -------------------------------------------------------------------------------- /rpi-rf-receiver/README.md: -------------------------------------------------------------------------------- 1 | # RPI RF Receiver 2 | 3 | This addon is made to make the rpi-rf receiver script run in background of you hassio 4 | 5 | 1. Install the addon (it take 5-10 minutes to intall). 6 | 7 | 2. Copy "rpi-rf_receive.py" in the "share" share of your hass.io. 8 | 9 | 3. Edit "rpi-rf_receive.py" adding your mosquitto address, port, user and password. 10 | 11 | 4. Start the addon 12 | 13 | 5. Add this sensor to read the codes in your hassio: 14 | 15 | sensor: 16 | - platform: mqtt 17 | 18 | state_topic: "sensors/rf/receiver" 19 | 20 | name: "RF Receiver" 21 | -------------------------------------------------------------------------------- /rpi-rf-receiver3/README.md: -------------------------------------------------------------------------------- 1 | # RPI RF Receiver 3 2 | 3 | This addon is made to make the rpi-rf receiver script run in background of you hassio 4 | 5 | 1. Install the addon (it take 5-10 minutes to intall). 6 | 7 | 2. Copy "rpi-rf_receive.py" in the "share" share of your hass.io. 8 | 9 | 3. Edit "rpi-rf_receive.py" adding your mosquitto address, port, user and password. 10 | 11 | 4. Start the addon 12 | 13 | 5. Add this sensor to read the codes in your hassio: 14 | 15 | sensor: 16 | - platform: mqtt 17 | 18 | state_topic: "sensors/rf/receiver" 19 | 20 | name: "RF Receiver" 21 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0", 4 | "slug": "test", 5 | "description": "test", 6 | "startup": "application", 7 | "boot": "auto", 8 | "devices": ["/dev/mem:/dev/mem:rw"], 9 | "privileged": ["SYS_RAWIO"], 10 | "gpio": "true", 11 | "ports": {"5055/tcp": null}, 12 | "options": { 13 | "code": "/share/rpi-rf_receive.py", 14 | "requirements": [ 15 | "rpi-rf" 16 | ], 17 | "clean": true 18 | }, 19 | "schema": {"code": "str", 20 | "requirements": ["str"], 21 | "clean": "bool?"}, 22 | "map": ["share"] 23 | } 24 | -------------------------------------------------------------------------------- /hc-sr04/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ultrasound Receiver", 3 | "version": "1.0", 4 | "slug": "ultrasound_receiver", 5 | "description": "Ultrasound Receiver", 6 | "startup": "application", 7 | "boot": "auto", 8 | "devices": ["/dev/mem:/dev/mem:rw"], 9 | "privileged": ["SYS_RAWIO"], 10 | "gpio": "true", 11 | "ports": {"5055/tcp": null}, 12 | "options": { 13 | "code": "/share/range_sensor.py", 14 | "requirements": [ 15 | "RPi.GPIO" 16 | ], 17 | "clean": true 18 | }, 19 | "schema": {"code": "str", 20 | "requirements": ["str"], 21 | "clean": "bool?"}, 22 | "map": ["share"] 23 | } 24 | -------------------------------------------------------------------------------- /rpi-rf-receiver/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RPi RF Receiver", 3 | "version": "1.63", 4 | "slug": "rfreceiver", 5 | "description": "RPi RF Receiver Script", 6 | "startup": "application", 7 | "boot": "auto", 8 | "devices": ["/dev/mem:/dev/mem:rw"], 9 | "privileged": ["SYS_RAWIO"], 10 | "gpio": "true", 11 | "apparmor": "false", 12 | "ports": {"5055/tcp": null}, 13 | "options": { 14 | "code": "/share/rpi-rf_receive.py", 15 | "requirements": [ 16 | "rpi-rf" 17 | ], 18 | "clean": true 19 | }, 20 | "schema": {"code": "str", 21 | "requirements": ["str"], 22 | "clean": "bool?"}, 23 | "map": ["share"] 24 | } 25 | -------------------------------------------------------------------------------- /dasshio/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dasshio", 3 | "version": "0.0.9", 4 | "slug": "dasshio", 5 | "description": "Use Amazon Dash Buttons in Home Assistant", 6 | "url": "https://github.com/danimtb/dasshio", 7 | "startup": "services", 8 | "boot": "auto", 9 | "host_network": "True", 10 | "options": { 11 | "buttons": [ 12 | { 13 | "name": "null", 14 | "address": "null", 15 | "url": "null", 16 | "headers": "{}", 17 | "body": "{}" 18 | } 19 | ] 20 | }, 21 | "schema": { 22 | "buttons": [ 23 | { 24 | "name": "str", 25 | "address": "str", 26 | "url": "str", 27 | "headers": "str", 28 | "body": "str" 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /hc-sr04/range_sensor.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO 2 | import time 3 | GPIO.setmode(GPIO.BCM) 4 | 5 | TRIG = 21 6 | ECHO = 20 7 | 8 | print "Distance Measurement In Progress" 9 | 10 | GPIO.setup(TRIG,GPIO.OUT) 11 | GPIO.setup(ECHO,GPIO.IN) 12 | 13 | GPIO.output(TRIG, False) 14 | print "Waiting For Sensor To Settle" 15 | time.sleep(2) 16 | 17 | GPIO.output(TRIG, True) 18 | time.sleep(0.00001) 19 | GPIO.output(TRIG, False) 20 | 21 | while GPIO.input(ECHO)==0: 22 | pulse_start = time.time() 23 | 24 | while GPIO.input(ECHO)==1: 25 | pulse_end = time.time() 26 | 27 | pulse_duration = pulse_end - pulse_start 28 | 29 | distance = pulse_duration * 17150 30 | 31 | distance = round(distance, 2) 32 | 33 | print "Distance:",distance,"cm" 34 | 35 | GPIO.cleanup() 36 | -------------------------------------------------------------------------------- /rpi-rf-receiver3/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RPi RF Receiver 3", 3 | "version": "2.05", 4 | "slug": "rfreceiver3", 5 | "description": "RPi RF Receiver 2 Script", 6 | "startup": "application", 7 | "arch": [ 8 | "aarch64", 9 | "amd64", 10 | "armhf", 11 | "i386" 12 | ], 13 | "boot": "auto", 14 | "devices": ["/dev/mem:/dev/mem:rw"], 15 | "privileged": ["SYS_RAWIO"], 16 | "gpio": "true", 17 | "apparmor": "false", 18 | "ports": {"5055/tcp": null}, 19 | "options": { 20 | "code": "/share/rpi-rf_receive.py", 21 | "requirements": [ 22 | "rpi-rf" 23 | ], 24 | "clean": true 25 | }, 26 | "schema": {"code": "str", 27 | "requirements": ["str"], 28 | "clean": "bool?"}, 29 | "map": ["share"] 30 | } 31 | -------------------------------------------------------------------------------- /hc-sr04/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | requirements=$(cat /data/options.json | jq -r 'if .requirements then .requirements | join(" ") else "" end') 5 | code=$(cat /data/options.json | jq -r '.code') 6 | clean=$(cat /data/options.json | jq -r '.clean //empty') 7 | py2=$(cat /data/options.json | jq -r '.python2 // empty') 8 | 9 | PYTHON=$(which python3) 10 | 11 | if [ "${py2}" == "true" ]; 12 | then 13 | PYTHON=$(which python2) 14 | fi 15 | 16 | if [ -n "$requirements" ]; 17 | then 18 | if [ "$clean" == "true" ]; 19 | then 20 | rm -rf /data/venv/ 21 | fi 22 | if [ ! -f "/data/venv/bin/activate" ]; 23 | then 24 | mkdir -p /data/venv/ 25 | cd /data/venv 26 | virtualenv -p ${PYTHON} . 27 | . bin/activate 28 | fi 29 | pip install -U ${requirements} 30 | fi 31 | python ${code} 32 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | requirements=$(cat /data/options.json | jq -r 'if .requirements then .requirements | join(" ") else "" end') 5 | code=$(cat /data/options.json | jq -r '.code') 6 | clean=$(cat /data/options.json | jq -r '.clean //empty') 7 | py2=$(cat /data/options.json | jq -r '.python2 // empty') 8 | 9 | PYTHON=$(which python3) 10 | 11 | if [ "${py2}" == "true" ]; 12 | then 13 | PYTHON=$(which python2) 14 | fi 15 | 16 | if [ -n "$requirements" ]; 17 | then 18 | if [ "$clean" == "true" ]; 19 | then 20 | rm -rf /data/venv/ 21 | fi 22 | if [ ! -f "/data/venv/bin/activate" ]; 23 | then 24 | mkdir -p /data/venv/ 25 | cd /data/venv 26 | virtualenv -p ${PYTHON} . 27 | . bin/activate 28 | fi 29 | pip install -U ${requirements} 30 | fi 31 | python ${code} 32 | -------------------------------------------------------------------------------- /rpi-rf-receiver/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | requirements=$(cat /data/options.json | jq -r 'if .requirements then .requirements | join(" ") else "" end') 5 | code=$(cat /data/options.json | jq -r '.code') 6 | clean=$(cat /data/options.json | jq -r '.clean //empty') 7 | py2=$(cat /data/options.json | jq -r '.python2 // empty') 8 | 9 | PYTHON=$(which python3) 10 | 11 | if [ "${py2}" == "true" ]; 12 | then 13 | PYTHON=$(which python2) 14 | fi 15 | 16 | if [ -n "$requirements" ]; 17 | then 18 | if [ "$clean" == "true" ]; 19 | then 20 | rm -rf /data/venv/ 21 | fi 22 | if [ ! -f "/data/venv/bin/activate" ]; 23 | then 24 | mkdir -p /data/venv/ 25 | cd /data/venv 26 | virtualenv -p ${PYTHON} . 27 | . bin/activate 28 | fi 29 | pip install -U ${requirements} 30 | fi 31 | python ${code} 32 | -------------------------------------------------------------------------------- /rpi-rf-receiver3/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | requirements=$(cat /data/options.json | jq -r 'if .requirements then .requirements | join(" ") else "" end') 5 | code=$(cat /data/options.json | jq -r '.code') 6 | clean=$(cat /data/options.json | jq -r '.clean //empty') 7 | py2=$(cat /data/options.json | jq -r '.python2 // empty') 8 | 9 | PYTHON=$(which python3) 10 | 11 | if [ "${py2}" == "true" ]; 12 | then 13 | PYTHON=$(which python2) 14 | fi 15 | 16 | if [ -n "$requirements" ]; 17 | then 18 | if [ "$clean" == "true" ]; 19 | then 20 | rm -rf /data/venv/ 21 | fi 22 | if [ ! -f "/data/venv/bin/activate" ]; 23 | then 24 | mkdir -p /data/venv/ 25 | cd /data/venv 26 | virtualenv -p ${PYTHON} . 27 | . bin/activate 28 | fi 29 | pip install -U ${requirements} 30 | fi 31 | python ${code} 32 | -------------------------------------------------------------------------------- /broadlink_s1c/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CONFIG_PATH=/data/options.json 5 | requirements=$(cat /data/options.json | jq -r 'if .requirements then .requirements | join(" ") else "" end') 6 | code=$(cat /data/options.json | jq -r '.code') 7 | clean=$(cat /data/options.json | jq -r '.clean //empty') 8 | py2=$(cat /data/options.json | jq -r '.python2 // empty') 9 | 10 | PYTHON=$(which python3) 11 | 12 | if [ "${py2}" == "true" ]; 13 | then 14 | PYTHON=$(which python2) 15 | fi 16 | 17 | if [ -n "$requirements" ]; 18 | then 19 | if [ "$clean" == "true" ]; 20 | then 21 | rm -rf /data/venv/ 22 | fi 23 | if [ ! -f "/data/venv/bin/activate" ]; 24 | then 25 | mkdir -p /data/venv/ 26 | cd /data/venv 27 | virtualenv -p ${PYTHON} . 28 | . bin/activate 29 | fi 30 | pip install -U ${requirements} 31 | fi 32 | python ${code} 33 | -------------------------------------------------------------------------------- /broadlink_s1c_2/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CONFIG_PATH=/data/options.json 5 | requirements=$(cat /data/options.json | jq -r 'if .requirements then .requirements | join(" ") else "" end') 6 | code=$(cat /data/options.json | jq -r '.code') 7 | clean=$(cat /data/options.json | jq -r '.clean //empty') 8 | py2=$(cat /data/options.json | jq -r '.python2 // empty') 9 | 10 | PYTHON=$(which python3) 11 | 12 | if [ "${py2}" == "true" ]; 13 | then 14 | PYTHON=$(which python2) 15 | fi 16 | 17 | if [ -n "$requirements" ]; 18 | then 19 | if [ "$clean" == "true" ]; 20 | then 21 | rm -rf /data/venv/ 22 | fi 23 | if [ ! -f "/data/venv/bin/activate" ]; 24 | then 25 | mkdir -p /data/venv/ 26 | cd /data/venv 27 | virtualenv -p ${PYTHON} . 28 | . bin/activate 29 | fi 30 | pip install -U ${requirements} 31 | fi 32 | python ${code} 33 | -------------------------------------------------------------------------------- /rpi-rf-receiver3/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | RUN apk add --no-cache \ 7 | jq \ 8 | py2-pip \ 9 | clang \ 10 | libgcc \ 11 | gcc-gnat \ 12 | libgc++ \ 13 | g++ \ 14 | make \ 15 | libffi-dev \ 16 | openssl-dev \ 17 | python2-dev \ 18 | mosquitto \ 19 | mosquitto-dev \ 20 | mosquitto-libs \ 21 | mosquitto-clients \ 22 | py-pip \ 23 | python \ 24 | python-dev \ 25 | python3 \ 26 | python3-dev \ 27 | && pip install -U pip \ 28 | && pip3 install -U pip \ 29 | && pip install -U virtualenv 30 | 31 | RUN pip install pyaes 32 | RUN pip install broadlink 33 | RUN pip install pycrypto 34 | # Copy data for add-on 35 | COPY run.sh / 36 | RUN chmod a+x /run.sh 37 | 38 | CMD [ "/run.sh" ] -------------------------------------------------------------------------------- /broadlink_s1c_2/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | RUN apk add --no-cache \ 7 | jq \ 8 | py-pip \ 9 | py2-pip \ 10 | clang \ 11 | libgcc \ 12 | gcc-gnat \ 13 | libgc++ \ 14 | g++ \ 15 | make \ 16 | libffi-dev \ 17 | openssl-dev \ 18 | python2-dev \ 19 | mosquitto \ 20 | mosquitto-dev \ 21 | mosquitto-libs \ 22 | mosquitto-clients 23 | 24 | RUN pip install pyaes 25 | RUN pip install broadlink 26 | RUN pip install pycrypto 27 | 28 | RUN apk add --no-cache \ 29 | python \ 30 | python-dev \ 31 | python3 \ 32 | python3-dev \ 33 | && pip install -U pip \ 34 | && pip3 install -U pip \ 35 | && pip install -U virtualenv 36 | 37 | # Copy data for add-on 38 | COPY run.sh / 39 | RUN chmod a+x /run.sh 40 | 41 | CMD [ "/run.sh" ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /broadlink_s1c/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | RUN apk add --no-cache \ 7 | jq \ 8 | py2-pip \ 9 | clang \ 10 | libgcc \ 11 | gcc-gnat \ 12 | libgc++ \ 13 | g++ \ 14 | make \ 15 | libffi-dev \ 16 | openssl-dev \ 17 | python2-dev \ 18 | mosquitto \ 19 | mosquitto-dev \ 20 | mosquitto-libs \ 21 | mosquitto-clients 22 | 23 | RUN pip install pyaes 24 | RUN pip install broadlink 25 | RUN pip install pycrypto 26 | 27 | RUN apk add --no-cache \ 28 | jq \ 29 | py-pip \ 30 | python \ 31 | python-dev \ 32 | python3 \ 33 | mosquitto \ 34 | mosquitto-clients \ 35 | python3-dev \ 36 | && pip install -U pip \ 37 | && pip3 install -U pip \ 38 | && pip install -U virtualenv 39 | 40 | # Copy data for add-on 41 | COPY run.sh / 42 | RUN chmod a+x /run.sh 43 | 44 | CMD [ "/run.sh" ] -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | RUN apk add --no-cache jq 7 | RUN apk add --no-cache py2-pip 8 | RUN apk add --no-cache clang 9 | RUN apk add --no-cache libgcc 10 | RUN apk add --no-cache gcc-gnat 11 | RUN apk add --no-cache libgc++ 12 | RUN apk add --no-cache g++ 13 | RUN apk add --no-cache make 14 | RUN apk add --no-cache libffi-dev 15 | RUN apk add --no-cache openssl-dev 16 | RUN apk add --no-cache python2-dev 17 | RUN apk add --no-cache mosquitto 18 | RUN apk add --no-cache mosquitto-dev 19 | RUN apk add --no-cache mosquitto-clients 20 | RUN apk add --no-cache mosquitto-libs 21 | RUN pip install pyaes 22 | RUN pip install pycrypto 23 | RUN apk add --no-cache \ 24 | jq \ 25 | py-pip \ 26 | python \ 27 | python-dev \ 28 | python3 \ 29 | python3-dev\ 30 | && pip install -U pip \ 31 | && pip3 install -U pip \ 32 | && pip install -U virtualenv 33 | 34 | # Copy data for add-on 35 | COPY run.sh / 36 | RUN chmod a+x /run.sh 37 | 38 | CMD [ "/run.sh" ] 39 | -------------------------------------------------------------------------------- /rpi-rf-receiver/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | RUN apk add --no-cache jq 7 | RUN apk add --no-cache py2-pip 8 | RUN apk add --no-cache clang 9 | RUN apk add --no-cache libgcc 10 | RUN apk add --no-cache gcc-gnat 11 | RUN apk add --no-cache libgc++ 12 | RUN apk add --no-cache g++ 13 | RUN apk add --no-cache make 14 | RUN apk add --no-cache libffi-dev 15 | RUN apk add --no-cache openssl-dev 16 | RUN apk add --no-cache python2-dev 17 | RUN apk add --no-cache mosquitto 18 | RUN apk add --no-cache mosquitto-dev 19 | RUN apk add --no-cache mosquitto-clients 20 | RUN apk add --no-cache mosquitto-libs 21 | RUN pip install pyaes 22 | RUN pip install pycrypto 23 | RUN apk add --no-cache \ 24 | jq \ 25 | py-pip \ 26 | python \ 27 | python-dev \ 28 | python3 \ 29 | python3-dev\ 30 | && pip install -U pip \ 31 | && pip3 install -U pip \ 32 | && pip install -U virtualenv 33 | 34 | # Copy data for add-on 35 | COPY run.sh / 36 | RUN chmod a+x /run.sh 37 | 38 | CMD [ "/run.sh" ] 39 | -------------------------------------------------------------------------------- /hc-sr04/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BUILD_FROM 2 | FROM $BUILD_FROM 3 | 4 | # Add env 5 | ENV LANG C.UTF-8 6 | RUN apk add --no-cache jq 7 | RUN apk add --no-cache py2-pip 8 | RUN apk add --no-cache clang 9 | RUN apk add --no-cache libgcc 10 | RUN apk add --no-cache gcc-gnat 11 | RUN apk add --no-cache libgc++ 12 | RUN apk add --no-cache g++ 13 | RUN apk add --no-cache make 14 | RUN apk add --no-cache libffi-dev 15 | RUN apk add --no-cache openssl-dev 16 | RUN apk add --no-cache python2-dev 17 | RUN apk add --no-cache mosquitto 18 | RUN apk add --no-cache mosquitto-dev 19 | RUN apk add --no-cache mosquitto-clients 20 | RUN apk add --no-cache mosquitto-libs 21 | RUN apk add --no-cache RPi.GPIO 22 | RUN pip install pyaes 23 | RUN pip install pycrypto 24 | RUN apk add --no-cache \ 25 | jq \ 26 | py-pip \ 27 | python \ 28 | python-dev \ 29 | python3 \ 30 | python3-dev\ 31 | && pip install -U pip \ 32 | && pip3 install -U pip \ 33 | && pip install -U virtualenv 34 | 35 | # Copy data for add-on 36 | COPY run.sh / 37 | RUN chmod a+x /run.sh 38 | 39 | CMD [ "/run.sh" ] 40 | -------------------------------------------------------------------------------- /git_pull/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Git pull", 3 | "version": "4.0", 4 | "slug": "git_pull", 5 | "description": "Simple git pull to update the local configuration", 6 | "url": "https://home-assistant.io/addons/git_pull/", 7 | "startup": "services", 8 | "boot": "manual", 9 | "hassio_api": true, 10 | "map": ["config:rw"], 11 | "options": { 12 | "deployment_key": [], 13 | "deployment_key_protocol": "rsa", 14 | "deployment_user": "", 15 | "deployment_password": "", 16 | "repository": null, 17 | "auto_restart": false, 18 | "repeat": { 19 | "active": false, 20 | "interval": 300 21 | } 22 | }, 23 | "schema": { 24 | "deployment_key": ["str"], 25 | "deployment_key_protocol": "match(rsa|dsa|ecdsa|ed25519|rsa)", 26 | "deployment_user": "str", 27 | "deployment_password": "str", 28 | "repository": "match((?:.+):(\/\/)?(.*?)(\\.git)(\/?|\\#[-\\d\\w._]+?))", 29 | "auto_restart": "bool", 30 | "repeat": { 31 | "active": "bool", 32 | "interval": "int" 33 | } 34 | }, 35 | "image": "homeassistant/{arch}-addon-git_pull" 36 | } 37 | -------------------------------------------------------------------------------- /rpi-rf-receiver/rpi-rf_receive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import signal 5 | import sys 6 | import time 7 | import logging 8 | import io, os, datetime, shutil 9 | 10 | mosquitto_address = "192.168.1.100" 11 | mosquitto_port = "1883" 12 | mosquitto_user = "homeassistant" 13 | mosquitto_password = "REDACTED" 14 | 15 | from rpi_rf import RFDevice 16 | 17 | rfdevice = None 18 | 19 | # pylint: disable=unused-argument 20 | def exithandler(signal, frame): 21 | rfdevice.cleanup() 22 | sys.exit(0) 23 | 24 | logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', 25 | format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s', ) 26 | 27 | parser = argparse.ArgumentParser(description='Receives a decimal code via a 433/315MHz GPIO device') 28 | parser.add_argument('-g', dest='gpio', type=int, default=27, 29 | help="GPIO pin (Default: 27)") 30 | args = parser.parse_args() 31 | 32 | signal.signal(signal.SIGINT, exithandler) 33 | rfdevice = RFDevice(args.gpio) 34 | rfdevice.enable_rx() 35 | timestamp = None 36 | logging.info("Listening for codes on GPIO " + str(args.gpio)) 37 | while True: 38 | if rfdevice.rx_code_timestamp != timestamp: 39 | timestamp = rfdevice.rx_code_timestamp 40 | logging.info(str(rfdevice.rx_code) + 41 | " [pulselength " + str(rfdevice.rx_pulselength) + 42 | ", protocol " + str(rfdevice.rx_proto) + "]") 43 | os.system("mosquitto_pub -V mqttv311 -h " + mosquitto_address + " -p " + mosquitto_port + " -t 'sensors/rf/receiver' -u " + mosquitto_user + " -P " + mosquitto_password + " -m " + str(rfdevice.rx_code)) 44 | time.sleep(0.01) 45 | rfdevice.cleanup() 46 | 47 | -------------------------------------------------------------------------------- /rpi-rf-receiver3/rpi-rf_receive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import signal 5 | import sys 6 | import time 7 | import logging 8 | import io, os, datetime, shutil 9 | 10 | mosquitto_address = "192.168.1.100" 11 | mosquitto_port = "1883" 12 | mosquitto_user = "homeassistant" 13 | mosquitto_password = "REDACTED" 14 | 15 | from rpi_rf import RFDevice 16 | 17 | rfdevice = None 18 | 19 | # pylint: disable=unused-argument 20 | def exithandler(signal, frame): 21 | rfdevice.cleanup() 22 | sys.exit(0) 23 | 24 | logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', 25 | format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s', ) 26 | 27 | parser = argparse.ArgumentParser(description='Receives a decimal code via a 433/315MHz GPIO device') 28 | parser.add_argument('-g', dest='gpio', type=int, default=27, 29 | help="GPIO pin (Default: 27)") 30 | args = parser.parse_args() 31 | 32 | signal.signal(signal.SIGINT, exithandler) 33 | rfdevice = RFDevice(args.gpio) 34 | rfdevice.enable_rx() 35 | timestamp = None 36 | logging.info("Listening for codes on GPIO " + str(args.gpio)) 37 | while True: 38 | if rfdevice.rx_code_timestamp != timestamp: 39 | timestamp = rfdevice.rx_code_timestamp 40 | logging.info(str(rfdevice.rx_code) + 41 | " [pulselength " + str(rfdevice.rx_pulselength) + 42 | ", protocol " + str(rfdevice.rx_proto) + "]") 43 | os.system("mosquitto_pub -V mqttv311 -h " + mosquitto_address + " -p " + mosquitto_port + " -t 'sensors/rf/receiver' -u " + mosquitto_user + " -P " + mosquitto_password + " -m " + str(rfdevice.rx_code)) 44 | time.sleep(0.01) 45 | rfdevice.cleanup() 46 | 47 | -------------------------------------------------------------------------------- /rpi-rf-receiver/rpi-rf_receive2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import signal 5 | import sys 6 | import time 7 | import logging 8 | import io, os, datetime, shutil 9 | 10 | mosquitto_address = "192.168.1.100" 11 | mosquitto_port = "1883" 12 | mosquitto_user = "homeassistant" 13 | mosquitto_password = "REDACTED" 14 | 15 | from rpi_rf import RFDevice 16 | 17 | rfdevice = None 18 | rfdevice2 = None 19 | 20 | # pylint: disable=unused-argument 21 | def exithandler(signal, frame): 22 | rfdevice.cleanup() 23 | rfdevice2.cleanup() 24 | sys.exit(0) 25 | 26 | logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%d %H:%M:%S', 27 | format='%(asctime)-15s - [%(levelname)s] %(module)s: %(message)s', ) 28 | 29 | parser = argparse.ArgumentParser(description='Receives a decimal code via a 433 GPIO device') 30 | parser.add_argument('-g', dest='gpio', type=int, default=27, help="433 MHz GPIO pin (Default: 27)") 31 | args = parser.parse_args() 32 | parser2 = argparse.ArgumentParser(description='Receives a decimal code via a 434 GPIO device') 33 | parser2.add_argument('-g', dest='gpio', type=int, default=22, help="434 Mhz GPIO pin (Default: 22)") 34 | args2 = parser2.parse_args() 35 | 36 | signal.signal(signal.SIGINT, exithandler) 37 | 38 | rfdevice = RFDevice(args.gpio) 39 | rfdevice2 = RFDevice(args2.gpio) 40 | 41 | rfdevice.enable_rx() 42 | rfdevice2.enable_rx() 43 | 44 | timestamp = None 45 | timestamp2 = None 46 | 47 | logging.info("Listening for codes on GPIO " + str(args.gpio) + " and GPIO " + str(args2.gpio)) 48 | while True: 49 | if rfdevice.rx_code_timestamp != timestamp: 50 | timestamp = rfdevice.rx_code_timestamp 51 | logging.info(str(rfdevice.rx_code) + 52 | " [GPIO 27, pulselength " + str(rfdevice.rx_pulselength) + 53 | ", protocol " + str(rfdevice.rx_proto) + "]") 54 | os.system("mosquitto_pub -h " + mosquitto_address + " -p " + mosquitto_port + " -t 'sensors/rf/receiver' -u " + mosquitto_user + " -P " + mosquitto_password + " -m " + str(rfdevice.rx_code)) 55 | if rfdevice2.rx_code_timestamp != timestamp2: 56 | timestamp2 = rfdevice2.rx_code_timestamp 57 | logging.info(str(rfdevice2.rx_code) + 58 | " [GPIO 22, pulselength " + str(rfdevice2.rx_pulselength) + 59 | ", protocol " + str(rfdevice2.rx_proto) + "]") 60 | os.system("mosquitto_pub -V mqttv311 -h " + mosquitto_address + " -p " + mosquitto_port + " -t 'sensors/rf/receiver2' -u " + mosquitto_user + " -P " + mosquitto_password + " -m " + str(rfdevice2.rx_code)) 61 | time.sleep(0.01) 62 | rfdevice.cleanup() 63 | rfdevice2.cleanup() 64 | -------------------------------------------------------------------------------- /dasshio/dasshio.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | 4 | import json 5 | import logging 6 | import os 7 | import requests 8 | from scapy.all import sniff 9 | from scapy.all import ARP 10 | from scapy.all import UDP 11 | from scapy.all import IP 12 | from scapy.all import DHCP 13 | from scapy.all import Ether 14 | import sys 15 | import time 16 | import signal 17 | 18 | def signal_handler(signal, frame): 19 | sys.exit(0) 20 | 21 | def arp_display(pkt): 22 | mac = "" 23 | 24 | try: 25 | mac = pkt[ARP].hwsrc.lower() 26 | except: 27 | mac = pkt[Ether].src.lower() 28 | 29 | for button in config['buttons']: 30 | if mac == button['address'].lower(): 31 | 32 | idx = [button['address'].lower() for button in config['buttons']].index(mac) 33 | button = config['buttons'][idx] 34 | 35 | logging.info(button['name'] + " button pressed!") 36 | logging.info("Request: " + button['url']) 37 | 38 | try: 39 | request = requests.post(button['url'], json=json.loads(button['body']), headers=json.loads(button['headers'])) 40 | logging.info('Status Code: {}'.format(request.status_code)) 41 | 42 | if request.status_code == requests.codes.ok: 43 | logging.info("Successful request") 44 | else: 45 | logging.error("Bad request") 46 | except: 47 | logging.exception("Unable to perform request: Check url, body and headers format. Check API password") 48 | 49 | return True 50 | 51 | # Catch SIGINT/SIGTERM Signals 52 | signal.signal(signal.SIGINT, signal_handler) 53 | signal.signal(signal.SIGTERM, signal_handler) 54 | 55 | # Remove Scapy IPv6 warnings 56 | logging.getLogger("scapy.runtime").setLevel(logging.ERROR) 57 | 58 | # Create basepath 59 | path = os.path.dirname(os.path.realpath(__file__)) 60 | 61 | # Log events to stdout 62 | logger = logging.getLogger() 63 | logger.setLevel(logging.INFO) 64 | 65 | stdoutHandler = logging.StreamHandler(sys.stdout) 66 | stdoutHandler.setLevel(logging.INFO) 67 | 68 | formater = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") 69 | stdoutHandler.setFormatter(formater) 70 | 71 | logger.addHandler(stdoutHandler) 72 | 73 | # Read config file 74 | logging.info("Reading config file: /data/options.json") 75 | 76 | with open(path + '/data/options.json', mode='r') as data_file: 77 | config = json.load(data_file) 78 | 79 | while True: 80 | # Start sniffing 81 | logging.info("Starting sniffing...") 82 | sniff(stop_filter=arp_display, filter='arp or (udp and src port 68 and dst port 67 and src host 0.0.0.0)', store=0, count=0) 83 | logging.info("Packet captured, waiting 20s ...") 84 | time.sleep(20) -------------------------------------------------------------------------------- /broadlink_s1c_2/s1c.py: -------------------------------------------------------------------------------- 1 | import broadlink 2 | import time, os 3 | 4 | mosquitto_address = "x.x.x.x" # change the ip address, user name and password for your mosquitto server 5 | mosquitto_port = "xxxx" 6 | mosquitto_user = "x" 7 | mosquitto_password = "x" 8 | broadlink_s1c_ip = "x.x.x.x" # Change to your S1C IP Address and S1C Mac Address 9 | broadlink_s1c_mac = "xxxxxxxxxxxx" 10 | 11 | devices = broadlink.S1C(host=(broadlink_s1c_ip,80), mac=bytearray.fromhex(broadlink_s1c_mac)) # Change to your S1C IP Address and S1C Mac Address 12 | devices.auth() 13 | 14 | sens = devices.get_sensors_status() 15 | old = sens 16 | 17 | 18 | while 1: 19 | try: 20 | sens = devices.get_sensors_status() 21 | for i, se in enumerate(sens['sensors']): 22 | if se['status'] != old['sensors'][i]['status']: 23 | sName = se['name'] 24 | sType = se['type'] 25 | if sType == "Door Sensor" and str(se['status']) == "0" or sType == "Door Sensor" and str(se['status']) == "128": # Instead of sType you can test for sName in case you have multiple sensors 26 | print time.ctime() + ": Door closed: " + str(se['status']) 27 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/entrance_door' -u mosquitto_user -P mosquitto_password -m " + "Closed") 28 | elif sType == "Door Sensor" and str(se['status']) == "16" or sType == "Door Sensor" and str(se['status']) == "144": 29 | print time.ctime() +": Door opened: " + str(se['status']) 30 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/entrance_door' -u mosquitto_user -P mosquitto_password -m " + "Open") 31 | elif sType == "Door Sensor" and str(se['status']) == "48": 32 | print time.ctime() +": Door Sensor tampered: " + str(se['status']) 33 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/entrance_door' -u mosquitto_user -P mosquitto_password -m " + "Tampered") 34 | elif sType == "Motion Sensor" and str(se['status']) == "0" or sType == "Motion Sensor" and str(se['status']) == "128": 35 | print time.ctime() +": No Motion: " + str(se['status']) 36 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/motion_sensor' -u mosquitto_user -P mosquitto_password -m " + "No_motion") 37 | elif sType == "Motion Sensor" and str(se['status']) == "16": 38 | print time.ctime() +": Motion Detected: " + str(se['status']) 39 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/motion_sensor' -u mosquitto_user -P mosquitto_password -m " + "Motion_Detected") 40 | elif sType == "Motion Sensor" and str(se['status']) == "32": 41 | print time.ctime() +": Motion Sensor Tampered: " + str(se['status']) 42 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/motion_sensor' -u mosquitto_user -P mosquitto_password -m " + "Tampered") 43 | old = sens 44 | except: 45 | continue 46 | 47 | -------------------------------------------------------------------------------- /broadlink_s1c/s1c.py: -------------------------------------------------------------------------------- 1 | import broadlink 2 | import time, os 3 | 4 | mosquitto_address = "x.x.x.x" # change the ip address, user name and password for your mosquitto server 5 | mosquitto_port = "xxxx" 6 | mosquitto_user = "x" 7 | mosquitto_password = "x" 8 | broadlink_s1c_ip = "x.x.x.x" # Change to your S1C IP Address and S1C Mac Address 9 | broadlink_s1c_mac = "xxxxxxxxxxxx" 10 | 11 | devices = broadlink.S1C(host=(broadlink_s1c_ip,80), mac=bytearray.fromhex(broadlink_s1c_mac)) # Change to your S1C IP Address and S1C Mac Address 12 | devices.auth() 13 | 14 | sens = devices.get_sensors_status() 15 | old = sens 16 | 17 | 18 | while 1: 19 | try: 20 | sens = devices.get_sensors_status() 21 | for i, se in enumerate(sens['sensors']): 22 | if se['status'] != old['sensors'][i]['status']: 23 | sName = se['name'] 24 | sType = se['type'] 25 | if sType == "Door Sensor" and str(se['status']) == "0" or sType == "Door Sensor" and str(se['status']) == "128": # Instead of sType you can test for sName in case you have multiple sensors 26 | print time.ctime() + ": Door closed: " + str(se['status']) 27 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/entrance_door' -u mosquitto_user -P mosquitto_password -m " + "Closed") 28 | elif sType == "Door Sensor" and str(se['status']) == "16" or sType == "Door Sensor" and str(se['status']) == "144": 29 | print time.ctime() +": Door opened: " + str(se['status']) 30 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/entrance_door' -u mosquitto_user -P mosquitto_password -m " + "Open") 31 | elif sType == "Door Sensor" and str(se['status']) == "48": 32 | print time.ctime() +": Door Sensor tampered: " + str(se['status']) 33 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/entrance_door' -u mosquitto_user -P mosquitto_password -m " + "Tampered") 34 | elif sType == "Motion Sensor" and str(se['status']) == "0" or sType == "Motion Sensor" and str(se['status']) == "128": 35 | print time.ctime() +": No Motion: " + str(se['status']) 36 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/motion_sensor' -u mosquitto_user -P mosquitto_password -m " + "No_motion") 37 | elif sType == "Motion Sensor" and str(se['status']) == "16": 38 | print time.ctime() +": Motion Detected: " + str(se['status']) 39 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/motion_sensor' -u mosquitto_user -P mosquitto_password -m " + "Motion_Detected") 40 | elif sType == "Motion Sensor" and str(se['status']) == "32": 41 | print time.ctime() +": Motion Sensor Tampered: " + str(se['status']) 42 | os.system("mosquitto_pub -h mosquitto_address -p mosquitto_port -t 'sensors/s1c/motion_sensor' -u mosquitto_user -P mosquitto_password -m " + "Tampered") 43 | old = sens 44 | except: 45 | continue 46 | 47 | -------------------------------------------------------------------------------- /dasshio/README.md: -------------------------------------------------------------------------------- 1 | # Dasshio - Amazon Dash Buttons Hass.io add-on 2 | 3 | [Hass.io add-on](https://home-assistant.io/addons/) to use [Amazon Dash Buttons](https://en.wikipedia.org/wiki/Amazon_Dash) in [Home Assistant](https://home-assistant.io). 4 | 5 | ## Description: How dasshio works 6 | This is a python script used to scan wifi devices connected to your network (using ARP). If a device matches any MAC address of the options, it will perform a HTTP POST request to the url given with headers and body indicated. 7 | 8 | ## Usage 9 | You can use this add-on to do whatever you like following the description above. However, the purpose of Dasshio is to "integrate" [Amazon's Dash buttons](https://en.wikipedia.org/wiki/Amazon_Dash) in Home Assistant in an easy way with [Hass.io](https://home-assistant.io/hassio/). 10 | 11 | See [RESTful API Post Services](https://home-assistant.io/developers/rest_api/#post-apiservicesltdomainltservice) documentation to see what you can do. 12 | 13 | Examples: 14 | 15 | - Set a Dash Button to toggle **room_light** light: 16 | - url: *http://homeassistant:8123/api/services/light/toggle* 17 | - body: "*{\\"entity_id\\": \\"light.room_light\\"}*" 18 | - Set a Dash Button to activate a **welcome_home** script: 19 | - url: *http://homeassistant:8123/api/services/script/welcome_home* 20 | - body: "" 21 | 22 | Have a look at [Service calls](https://home-assistant.io/docs/scripts/service-calls/) to know what services you can use and what you can do with them. 23 | 24 | 25 | ## How to install this Hass.io add-on 26 | To install this add-on, please, follow Home Assistant documentation on how to [Install Third-party Add-ons](https://home-assistant.io/hassio/installing_third_party_addons/) 27 | 28 | ## Options example 29 | Here it is an example of a Dash Gillette button used to toggle a light. Note you can add as many buttons as you like inside the "buttons" array. 30 | 31 | - name: name of your device 32 | - address: MAC of your device 33 | - url: Url to perform the HTPP Post request (http or https). Check [Home Assistant RESTful API](https://home-assistant.io/developers/rest_api/). 34 | - headers: HTTP Post headers (Useful for Home Assistant API password -see example-). 35 | - body: HTTP Post Body (Normally the *entity_id* in Home Assistant) 36 | 37 | [*/data/options.json*](https://home-assistant.io/developers/hassio/addon_config/#options--schema) 38 | ``` 39 | { 40 | "buttons": [ 41 | { 42 | "name": "Gillette", 43 | "address": "AC:63:BE:77:C4:0D", 44 | "url": "http://homeassistant:8123/api/services/light/toggle", 45 | "headers": "{\"x-ha-access\": \"your_password\"}", 46 | "body": "{\"entity_id\": \"light.room_light\"}" 47 | }, 48 | { 49 | "name": "Bounty", 50 | "address": "AC:63:BE:77:C4:0C", 51 | "url": "https://example_ha_url.duckdns.org/api/services/script/welcome_home", 52 | "headers": "{}", 53 | "body": "{}" 54 | }] 55 | } 56 | ``` 57 | 58 | **Note**: You can use the hostname `homeassistant` to route requests over the Hassio local network between the containers. This is the prefered method as it means the requests don't have to leave the machine Hassio is running on. 59 | 60 | **WARNING**: As headers and body sections have to be strings, it is necessary to use backslashes ( *\\* ) before double quotes ( *"* ) to escape them. Like this: *\\"* 61 | 62 | ## How to find the MAC address of your Dash 63 | At the moment, the best way to do this is to hold down the button for 6 seconds, disconnect from the current WIFI and connect to the *Amazon ConfigureMe* SSID. If prompted, "stay connected" and open web page **192.168.0.1**. You will see your button’s ‘about’ page with the MAC and the additional information. 64 | 65 | 66 | Alternatively, you can access your Wifi Router and check the MAC addresses in the history of connected devices. Then, copy and paste the MAC in a service like [MA:CV:en:do:rs](https://macvendors.com/) to find the Vendor of that device. The Amazon Dash button vendor should be: *Amazon Technologies Inc.* 67 | 68 | --------------------- 69 | ### Credit 70 | - [amazon-dashbutton](https://github.com/JulianKahnert/amazon-dashbutton) (Thanks to [JulianKahnert](https://github.com/JulianKahnert) in [Issue#1](https://github.com/danimtb/dasshio/issues/1)) 71 | 72 | ### Sources & Inspiration: 73 | - [Raspberry Pi script](https://github.com/vancetran/amazon-dash-rpi) 74 | - [Maddox Dashbutton Repo](https://github.com/maddox/dasher) 75 | - [General Amazon Dash Hack](https://medium.com/@edwardbenson/how-i-hacked-amazon-s-5-wifi-button-to-track-baby-data-794214b0bdd8#.n6fhd3z40) 76 | -------------------------------------------------------------------------------- /create_hassio_addon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | BUILD_CONTAINER_NAME=hassio-addons-$$ 5 | DOCKER_PUSH="true" 6 | DOCKER_CACHE="true" 7 | DOCKER_WITH_LATEST="true" 8 | BRANCH=master 9 | REPOSITORY=https://github.com/home-assistant/hassio-addons 10 | 11 | cleanup() { 12 | echo "[INFO] Cleanup." 13 | 14 | # Stop docker container 15 | echo "[INFO] Cleaning up hassio-build container." 16 | docker stop $BUILD_CONTAINER_NAME 2> /dev/null || true 17 | docker rm --volumes $BUILD_CONTAINER_NAME 2> /dev/null || true 18 | 19 | if [ "$1" == "fail" ]; then 20 | exit 1 21 | fi 22 | } 23 | trap 'cleanup fail' SIGINT SIGTERM 24 | 25 | help () { 26 | cat << EOF 27 | Script for hassio addon docker build 28 | create_hassio_addon [options] 29 | Options: 30 | -h, --help 31 | Display this help and exit. 32 | -r, --repository https://.../addons 33 | Set git repository to load addon from. 34 | -b, --branch branch_name 35 | Set git branch to load addon from. 36 | -l, --local /path/to/repository 37 | Load addon from a local folder 38 | -s, --slug addon_slug 39 | Name of folder/slug 40 | -a, --arch armhf|aarch64|i386|amd64 41 | Arch for addon build. 42 | -t, --test 43 | Don't upload the build to docker hub. 44 | -n, --no-cache 45 | Disable build from cache 46 | EOF 47 | } 48 | 49 | # Parse arguments 50 | while [[ $# -gt 0 ]]; do 51 | key=$1 52 | case $key in 53 | -h|--help) 54 | help 55 | exit 0 56 | ;; 57 | -r|--repository) 58 | REPOSITORY=$2 59 | shift 60 | ;; 61 | -b|--branch) 62 | BRANCH=$2 63 | shift 64 | ;; 65 | -l|--local) 66 | LOCAL_REPOSITORY=$2 67 | shift 68 | ;; 69 | -s|--slug) 70 | SLUG=$2 71 | shift 72 | ;; 73 | -a|--arch) 74 | ARCH=$2 75 | shift 76 | ;; 77 | -t|--test) 78 | DOCKER_PUSH="false" 79 | ;; 80 | -n|--no-cache) 81 | DOCKER_CACHE="false" 82 | ;; 83 | *) 84 | echo "[WARNING] $0 : Argument '$1' unknown. Ignoring." 85 | ;; 86 | esac 87 | shift 88 | done 89 | 90 | # Sanity checks 91 | if [ "$ARCH" != 'armhf' ] && [ "$ARCH" != 'aarch64' ] && [ "$ARCH" != 'i386' ] && [ "$ARCH" != 'amd64' ]; then 92 | echo "Error: $ARCH is not a supported platform for hassio-supervisor!" 93 | help 94 | exit 1 95 | fi 96 | if [ -z "$SLUG" ]; then 97 | echo "[ERROR] please set a slug!" 98 | help 99 | exit 1 100 | fi 101 | 102 | # Get the absolute script location 103 | pushd "$(dirname "$0")" > /dev/null 2>&1 104 | SCRIPTPATH=$(pwd) 105 | popd > /dev/null 2>&1 106 | 107 | BASE_IMAGE="homeassistant\/$ARCH-base:latest" 108 | BUILD_DIR=${BUILD_DIR:=$SCRIPTPATH} 109 | WORKSPACE=${BUILD_DIR:=$SCRIPTPATH}/hassio-supervisor-$ARCH 110 | ADDON_WORKSPACE=$WORKSPACE/$SLUG 111 | 112 | # setup docker 113 | echo "[INFO] Setup docker for addon" 114 | mkdir -p "$BUILD_DIR" 115 | mkdir -p "$WORKSPACE" 116 | 117 | if [ -z "$LOCAL_REPOSITORY" ]; then 118 | git clone "$REPOSITORY" "$WORKSPACE" 119 | cd "$WORKSPACE"; git checkout "$BRANCH" 120 | 121 | if [ ! -d "$ADDON_WORKSPACE" ]; then 122 | echo "Error: $ADDON not found inside Repo!" 123 | exit 1 124 | fi 125 | else 126 | cp -r "$LOCAL_REPOSITORY/$SLUG" "$ADDON_WORKSPACE" 127 | fi 128 | 129 | # Init docker 130 | echo "[INFO] Setup dockerfile" 131 | 132 | sed -i "s/{arch}/${ARCH}/g" "$ADDON_WORKSPACE/config.json" 133 | DOCKER_TAG=$(jq --raw-output ".version" "$ADDON_WORKSPACE/config.json") 134 | UPSTREAM_VERSION=${DOCKER_TAG%-*} 135 | 136 | # If set custom image in file 137 | DOCKER_IMAGE=$(jq --raw-output ".image // empty" "$ADDON_WORKSPACE/config.json") 138 | 139 | # Replace hass.io vars 140 | sed -i "s/%%BASE_IMAGE%%/${BASE_IMAGE}/g" "$ADDON_WORKSPACE/Dockerfile" 141 | sed -i "s/#${ARCH}:FROM/FROM/g" "$ADDON_WORKSPACE/Dockerfile" 142 | sed -i "s/%%ARCH%%/${ARCH}/g" "$ADDON_WORKSPACE/Dockerfile" 143 | sed -i "s/%%UPSTREAM_VERSION%%/${UPSTREAM_VERSION}/g" "$ADDON_WORKSPACE/Dockerfile" 144 | echo "LABEL io.hass.version=\"$DOCKER_TAG\" io.hass.arch=\"$ARCH\" io.hass.type=\"addon\"" >> "$ADDON_WORKSPACE/Dockerfile" 145 | 146 | # Run build 147 | echo "[INFO] start docker build" 148 | docker stop $BUILD_CONTAINER_NAME 2> /dev/null || true 149 | docker rm --volumes $BUILD_CONTAINER_NAME 2> /dev/null || true 150 | docker run --rm \ 151 | -v "$ADDON_WORKSPACE":/docker \ 152 | -v ~/.docker:/root/.docker \ 153 | -e DOCKER_PUSH=$DOCKER_PUSH \ 154 | -e DOCKER_CACHE=$DOCKER_CACHE \ 155 | -e DOCKER_WITH_LATEST=$DOCKER_WITH_LATEST \ 156 | -e DOCKER_IMAGE="$DOCKER_IMAGE" \ 157 | -e DOCKER_TAG="$DOCKER_TAG" \ 158 | --name $BUILD_CONTAINER_NAME \ 159 | --privileged \ 160 | homeassistant/docker-build-env \ 161 | /run-docker.sh 162 | 163 | echo "[INFO] cleanup WORKSPACE" 164 | cd "$BUILD_DIR" 165 | rm -rf "$WORKSPACE" 166 | 167 | cleanup "okay" 168 | exit 0 169 | -------------------------------------------------------------------------------- /git_pull/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #### config #### 4 | 5 | CONFIG_PATH=/data/options.json 6 | 7 | DEPLOYMENT_KEY=$(jq --raw-output ".deployment_key[]" $CONFIG_PATH) 8 | DEPLOYMENT_KEY_PROTOCOL=$(jq --raw-output ".deployment_key_protocol" $CONFIG_PATH) 9 | DEPLOYMENT_USER=$(jq --raw-output ".deployment_user" $CONFIG_PATH) 10 | DEPLOYMENT_PASSWORD=$(jq --raw-output ".deployment_password" $CONFIG_PATH) 11 | REPOSITORY=$(jq --raw-output '.repository' $CONFIG_PATH) 12 | AUTO_RESTART=$(jq --raw-output '.auto_restart' $CONFIG_PATH) 13 | REPEAT_ACTIVE=$(jq --raw-output '.repeat.active' $CONFIG_PATH) 14 | REPEAT_INTERVAL=$(jq --raw-output '.repeat.interval' $CONFIG_PATH) 15 | ################ 16 | 17 | #### functions #### 18 | function add-ssh-key { 19 | echo "Start adding SSH key" 20 | mkdir -p ~/.ssh 21 | 22 | ( 23 | echo "Host *" 24 | echo " StrictHostKeyChecking no" 25 | ) > ~/.ssh/config 26 | 27 | echo "Setup deployment_key on id_${DEPLOYMENT_KEY_PROTOCOL}" 28 | while read -r line; do 29 | echo "$line" >> "${HOME}/.ssh/id_${DEPLOYMENT_KEY_PROTOCOL}" 30 | done <<< "$DEPLOYMENT_KEY" 31 | 32 | chmod 600 "${HOME}/.ssh/config" 33 | chmod 600 "${HOME}/.ssh/id_${DEPLOYMENT_KEY_PROTOCOL}" 34 | } 35 | 36 | function git-clone { 37 | # create backup 38 | BACKUP_LOCATION="/tmp/config-$(date +%Y-%m-%d_%H-%M-%S)" 39 | echo "Backup configuration to $BACKUP_LOCATION" 40 | 41 | mkdir "${BACKUP_LOCATION}" || { echo "[Error] Creation of backup directory failed"; exit 1; } 42 | cp -rf /config/* "${BACKUP_LOCATION}" || { echo "[Error] Copy files to backup directory failed"; exit 1; } 43 | 44 | # remove config folder content 45 | rm -rf /config/{,.[!.],..?}* || { echo "[Error] Clearing /config failed"; exit 1; } 46 | 47 | # git clone 48 | echo "Start git clone" 49 | git clone "$REPOSITORY" /config || { echo "[Error] Git clone failed"; exit 1; } 50 | 51 | # try to copy non yml files back 52 | cp "${BACKUP_LOCATION}" "!(*.yaml)" /config 2>/dev/null 53 | 54 | # try to copy secrets file back 55 | cp "${BACKUP_LOCATION}/secrets.yaml" /config 2>/dev/null 56 | } 57 | 58 | function check-ssh-key { 59 | if [ -n "$DEPLOYMENT_KEY" ]; then 60 | echo "Check SSH connection" 61 | IFS=':' read -ra GIT_URL_PARTS <<< "$REPOSITORY" 62 | # shellcheck disable=SC2029 63 | if ! ssh -T -o "BatchMode=yes" "${GIT_URL_PARTS[0]}" 64 | then 65 | echo "Valid SSH connection for ${GIT_URL_PARTS[0]}" 66 | else 67 | echo "No valid SSH connection for ${GIT_URL_PARTS[0]}" 68 | add-ssh-key 69 | fi 70 | fi 71 | } 72 | 73 | function setup-user-password { 74 | if [ ! -z "$DEPLOYMENT_USER" ]; then 75 | cd /config || return 76 | echo "[Info] setting up credential.helper for user: ${DEPLOYMENT_USER}" 77 | git config --system credential.helper 'store --file=/tmp/git-credentials' 78 | 79 | # Extract the hostname from repository 80 | h="$REPOSITORY" 81 | 82 | # Extract the protocol 83 | proto=${h%%://*} 84 | 85 | # Strip the protocol 86 | h="${h#*://}" 87 | 88 | # Strip username and password from URL 89 | h="${h#*:*@}" 90 | h="${h#*@}" 91 | 92 | # Strip the tail of the URL 93 | h=${h%%/*} 94 | 95 | # Format the input for git credential commands 96 | cred_data="\ 97 | protocol=${proto} 98 | host=${h} 99 | username=${DEPLOYMENT_USER} 100 | password=${DEPLOYMENT_PASSWORD} 101 | " 102 | 103 | # Use git commands to write the credentials to ~/.git-credentials 104 | echo "[Info] Saving git credentials to /tmp/git-credentials" 105 | git credential fill | git credential approve <<< "$cred_data" 106 | fi 107 | } 108 | 109 | function git-synchronize { 110 | if git rev-parse --is-inside-git-dir &>/dev/null 111 | then 112 | echo "git repository exists, start pulling" 113 | OLD_COMMIT=$(git rev-parse HEAD) 114 | git pull || { echo "[Error] Git pull failed"; exit 1; } 115 | else 116 | echo "git repostory doesn't exist" 117 | git-clone 118 | fi 119 | } 120 | 121 | function validate-config { 122 | echo "[Info] Check if something is changed" 123 | if [ "$AUTO_RESTART" == "true" ]; then 124 | # Compare commit ids & check config 125 | NEW_COMMIT=$(git rev-parse HEAD) 126 | if [ "$NEW_COMMIT" != "$OLD_COMMIT" ]; then 127 | echo "[Info] check Home-Assistant config" 128 | if hassio homeassistant check; then 129 | echo "[Info] restart Home-Assistant" 130 | hassio homeassistant restart 2&> /dev/null 131 | else 132 | echo "[Error] invalid config!" 133 | fi 134 | else 135 | echo "[Info] Nothing has changed." 136 | fi 137 | fi 138 | } 139 | 140 | ################### 141 | 142 | #### Main program #### 143 | cd /config || { echo "[Error] Failed to cd into /config"; exit 1; } 144 | while true; do 145 | check-ssh-key 146 | setup-user-password 147 | git-synchronize 148 | validate-config 149 | # do we repeat? 150 | if [ -z "$REPEAT_ACTIVE" ]; then 151 | exit 0 152 | fi 153 | sleep "$REPEAT_INTERVAL" 154 | done 155 | 156 | ################### 157 | -------------------------------------------------------------------------------- /broadlink_s1c/broadlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from datetime import datetime 4 | try: 5 | from Crypto.Cipher import AES 6 | except ImportError as e: 7 | import pyaes 8 | 9 | import time 10 | import random 11 | import socket 12 | import sys 13 | import threading 14 | import codecs 15 | 16 | def gendevice(devtype, host, mac): 17 | if devtype == 0: # SP1 18 | return sp1(host=host, mac=mac) 19 | if devtype == 0x2711: # SP2 20 | return sp2(host=host, mac=mac) 21 | if devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 22 | return sp2(host=host, mac=mac) 23 | if devtype == 0x2720: # SPMini 24 | return sp2(host=host, mac=mac) 25 | elif devtype == 0x753e: # SP3 26 | return sp2(host=host, mac=mac) 27 | elif devtype == 0x947a or devtype == 0x9479: # SP3S 28 | return sp2(host=host, mac=mac) 29 | elif devtype == 0x2728: # SPMini2 30 | return sp2(host=host, mac=mac) 31 | elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini 32 | return sp2(host=host, mac=mac) 33 | elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 34 | return sp2(host=host, mac=mac) 35 | elif devtype == 0x2736: # SPMiniPlus 36 | return sp2(host=host, mac=mac) 37 | elif devtype == 0x2712: # RM2 38 | return rm(host=host, mac=mac) 39 | elif devtype == 0x2737: # RM Mini 40 | return rm(host=host, mac=mac) 41 | elif devtype == 0x273d: # RM Pro Phicomm 42 | return rm(host=host, mac=mac) 43 | elif devtype == 0x2783: # RM2 Home Plus 44 | return rm(host=host, mac=mac) 45 | elif devtype == 0x277c: # RM2 Home Plus GDT 46 | return rm(host=host, mac=mac) 47 | elif devtype == 0x272a: # RM2 Pro Plus 48 | return rm(host=host, mac=mac) 49 | elif devtype == 0x2787: # RM2 Pro Plus2 50 | return rm(host=host, mac=mac) 51 | elif devtype == 0x278b: # RM2 Pro Plus BL 52 | return rm(host=host, mac=mac) 53 | elif devtype == 0x278f: # RM Mini Shate 54 | return rm(host=host, mac=mac) 55 | elif devtype == 0x2714: # A1 56 | return a1(host=host, mac=mac) 57 | elif devtype == 0x4EB5: # MP1 58 | return mp1(host=host, mac=mac) 59 | elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) 60 | return S1C(host=host, mac=mac) 61 | else: 62 | return device(host=host, mac=mac) 63 | 64 | def discover(timeout=None, local_ip_address=None): 65 | if local_ip_address is None: 66 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 67 | s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets 68 | local_ip_address = s.getsockname()[0] 69 | address = local_ip_address.split('.') 70 | cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 71 | cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 72 | cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 73 | cs.bind((local_ip_address,0)) 74 | port = cs.getsockname()[1] 75 | starttime = time.time() 76 | 77 | devices = [] 78 | 79 | timezone = int(time.timezone/-3600) 80 | packet = bytearray(0x30) 81 | 82 | year = datetime.now().year 83 | 84 | if timezone < 0: 85 | packet[0x08] = 0xff + timezone - 1 86 | packet[0x09] = 0xff 87 | packet[0x0a] = 0xff 88 | packet[0x0b] = 0xff 89 | else: 90 | packet[0x08] = timezone 91 | packet[0x09] = 0 92 | packet[0x0a] = 0 93 | packet[0x0b] = 0 94 | packet[0x0c] = year & 0xff 95 | packet[0x0d] = year >> 8 96 | packet[0x0e] = datetime.now().minute 97 | packet[0x0f] = datetime.now().hour 98 | subyear = str(year)[2:] 99 | packet[0x10] = int(subyear) 100 | packet[0x11] = datetime.now().isoweekday() 101 | packet[0x12] = datetime.now().day 102 | packet[0x13] = datetime.now().month 103 | packet[0x18] = int(address[0]) 104 | packet[0x19] = int(address[1]) 105 | packet[0x1a] = int(address[2]) 106 | packet[0x1b] = int(address[3]) 107 | packet[0x1c] = port & 0xff 108 | packet[0x1d] = port >> 8 109 | packet[0x26] = 6 110 | checksum = 0xbeaf 111 | 112 | for i in range(len(packet)): 113 | checksum += packet[i] 114 | checksum = checksum & 0xffff 115 | packet[0x20] = checksum & 0xff 116 | packet[0x21] = checksum >> 8 117 | 118 | cs.sendto(packet, ('255.255.255.255', 80)) 119 | if timeout is None: 120 | response = cs.recvfrom(1024) 121 | responsepacket = bytearray(response[0]) 122 | host = response[1] 123 | mac = responsepacket[0x3a:0x40] 124 | devtype = responsepacket[0x34] | responsepacket[0x35] << 8 125 | return gendevice(devtype, host, mac) 126 | else: 127 | while (time.time() - starttime) < timeout: 128 | cs.settimeout(timeout - (time.time() - starttime)) 129 | try: 130 | response = cs.recvfrom(1024) 131 | except socket.timeout: 132 | return devices 133 | responsepacket = bytearray(response[0]) 134 | host = response[1] 135 | devtype = responsepacket[0x34] | responsepacket[0x35] << 8 136 | mac = responsepacket[0x3a:0x40] 137 | dev = gendevice(devtype, host, mac) 138 | devices.append(dev) 139 | return devices 140 | 141 | 142 | class device: 143 | def __init__(self, host, mac, timeout=10): 144 | self.host = host 145 | self.mac = mac 146 | self.timeout = timeout 147 | self.count = random.randrange(0xffff) 148 | self.key = bytearray([0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) 149 | self.iv = bytearray([0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) 150 | self.id = bytearray([0, 0, 0, 0]) 151 | self.cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 152 | self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 153 | self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 154 | self.cs.bind(('',0)) 155 | self.type = "Unknown" 156 | self.lock = threading.Lock() 157 | 158 | if 'pyaes' in sys.modules: 159 | self.encrypt = self.encrypt_pyaes 160 | self.decrypt = self.decrypt_pyaes 161 | else: 162 | self.encrypt = self.encrypt_pycrypto 163 | self.decrypt = self.decrypt_pycrypto 164 | 165 | def encrypt_pyaes(self, payload): 166 | aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) 167 | return "".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) 168 | 169 | def decrypt_pyaes(self, payload): 170 | aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) 171 | return "".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) 172 | 173 | def encrypt_pycrypto(self, payload): 174 | aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) 175 | return aes.encrypt(bytes(payload)) 176 | 177 | def decrypt_pycrypto(self, payload): 178 | aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) 179 | return aes.decrypt(bytes(payload)) 180 | 181 | def auth(self): 182 | payload = bytearray(0x50) 183 | payload[0x04] = 0x31 184 | payload[0x05] = 0x31 185 | payload[0x06] = 0x31 186 | payload[0x07] = 0x31 187 | payload[0x08] = 0x31 188 | payload[0x09] = 0x31 189 | payload[0x0a] = 0x31 190 | payload[0x0b] = 0x31 191 | payload[0x0c] = 0x31 192 | payload[0x0d] = 0x31 193 | payload[0x0e] = 0x31 194 | payload[0x0f] = 0x31 195 | payload[0x10] = 0x31 196 | payload[0x11] = 0x31 197 | payload[0x12] = 0x31 198 | payload[0x1e] = 0x01 199 | payload[0x2d] = 0x01 200 | payload[0x30] = ord('T') 201 | payload[0x31] = ord('e') 202 | payload[0x32] = ord('s') 203 | payload[0x33] = ord('t') 204 | payload[0x34] = ord(' ') 205 | payload[0x35] = ord(' ') 206 | payload[0x36] = ord('1') 207 | 208 | response = self.send_packet(0x65, payload) 209 | 210 | payload = self.decrypt(response[0x38:]) 211 | 212 | if not payload: 213 | return False 214 | 215 | key = payload[0x04:0x14] 216 | if len(key) % 16 != 0: 217 | return False 218 | 219 | self.id = payload[0x00:0x04] 220 | self.key = key 221 | return True 222 | 223 | def get_type(self): 224 | return self.type 225 | 226 | def send_packet(self, command, payload): 227 | self.count = (self.count + 1) & 0xffff 228 | packet = bytearray(0x38) 229 | packet[0x00] = 0x5a 230 | packet[0x01] = 0xa5 231 | packet[0x02] = 0xaa 232 | packet[0x03] = 0x55 233 | packet[0x04] = 0x5a 234 | packet[0x05] = 0xa5 235 | packet[0x06] = 0xaa 236 | packet[0x07] = 0x55 237 | packet[0x24] = 0x2a 238 | packet[0x25] = 0x27 239 | packet[0x26] = command 240 | packet[0x28] = self.count & 0xff 241 | packet[0x29] = self.count >> 8 242 | packet[0x2a] = self.mac[0] 243 | packet[0x2b] = self.mac[1] 244 | packet[0x2c] = self.mac[2] 245 | packet[0x2d] = self.mac[3] 246 | packet[0x2e] = self.mac[4] 247 | packet[0x2f] = self.mac[5] 248 | packet[0x30] = self.id[0] 249 | packet[0x31] = self.id[1] 250 | packet[0x32] = self.id[2] 251 | packet[0x33] = self.id[3] 252 | 253 | # pad the payload for AES encryption 254 | if len(payload)>0: 255 | numpad=(len(payload)//16+1)*16 256 | payload=payload.ljust(numpad,b"\x00") 257 | 258 | checksum = 0xbeaf 259 | for i in range(len(payload)): 260 | checksum += payload[i] 261 | checksum = checksum & 0xffff 262 | 263 | payload = self.encrypt(payload) 264 | 265 | packet[0x34] = checksum & 0xff 266 | packet[0x35] = checksum >> 8 267 | 268 | for i in range(len(payload)): 269 | packet.append(payload[i]) 270 | 271 | checksum = 0xbeaf 272 | for i in range(len(packet)): 273 | checksum += packet[i] 274 | checksum = checksum & 0xffff 275 | packet[0x20] = checksum & 0xff 276 | packet[0x21] = checksum >> 8 277 | 278 | starttime = time.time() 279 | with self.lock: 280 | while True: 281 | try: 282 | self.cs.sendto(packet, self.host) 283 | self.cs.settimeout(1) 284 | response = self.cs.recvfrom(2048) 285 | break 286 | except socket.timeout: 287 | if (time.time() - starttime) > self.timeout: 288 | raise 289 | return bytearray(response[0]) 290 | 291 | 292 | class mp1(device): 293 | def __init__ (self, host, mac): 294 | device.__init__(self, host, mac) 295 | self.type = "MP1" 296 | 297 | def set_power_mask(self, sid_mask, state): 298 | """Sets the power state of the smart power strip.""" 299 | 300 | packet = bytearray(16) 301 | packet[0x00] = 0x0d 302 | packet[0x02] = 0xa5 303 | packet[0x03] = 0xa5 304 | packet[0x04] = 0x5a 305 | packet[0x05] = 0x5a 306 | packet[0x06] = 0xb2 + ((sid_mask<<1) if state else sid_mask) 307 | packet[0x07] = 0xc0 308 | packet[0x08] = 0x02 309 | packet[0x0a] = 0x03 310 | packet[0x0d] = sid_mask 311 | packet[0x0e] = sid_mask if state else 0 312 | 313 | response = self.send_packet(0x6a, packet) 314 | 315 | err = response[0x22] | (response[0x23] << 8) 316 | 317 | def set_power(self, sid, state): 318 | """Sets the power state of the smart power strip.""" 319 | sid_mask = 0x01 << (sid - 1) 320 | return self.set_power_mask(sid_mask, state) 321 | 322 | def check_power_raw(self): 323 | """Returns the power state of the smart power strip in raw format.""" 324 | packet = bytearray(16) 325 | packet[0x00] = 0x0a 326 | packet[0x02] = 0xa5 327 | packet[0x03] = 0xa5 328 | packet[0x04] = 0x5a 329 | packet[0x05] = 0x5a 330 | packet[0x06] = 0xae 331 | packet[0x07] = 0xc0 332 | packet[0x08] = 0x01 333 | 334 | response = self.send_packet(0x6a, packet) 335 | err = response[0x22] | (response[0x23] << 8) 336 | if err == 0: 337 | payload = self.decrypt(bytes(response[0x38:])) 338 | if type(payload[0x4]) == int: 339 | state = payload[0x0e] 340 | else: 341 | state = ord(payload[0x0e]) 342 | return state 343 | 344 | def check_power(self): 345 | """Returns the power state of the smart power strip.""" 346 | state = self.check_power_raw() 347 | data = {} 348 | data['s1'] = bool(state & 0x01) 349 | data['s2'] = bool(state & 0x02) 350 | data['s3'] = bool(state & 0x04) 351 | data['s4'] = bool(state & 0x08) 352 | return data 353 | 354 | 355 | class sp1(device): 356 | def __init__ (self, host, mac): 357 | device.__init__(self, host, mac) 358 | self.type = "SP1" 359 | 360 | def set_power(self, state): 361 | packet = bytearray(4) 362 | packet[0] = state 363 | self.send_packet(0x66, packet) 364 | 365 | 366 | class sp2(device): 367 | def __init__ (self, host, mac): 368 | device.__init__(self, host, mac) 369 | self.type = "SP2" 370 | 371 | def set_power(self, state): 372 | """Sets the power state of the smart plug.""" 373 | packet = bytearray(16) 374 | packet[0] = 2 375 | packet[4] = 1 if state else 0 376 | self.send_packet(0x6a, packet) 377 | 378 | def check_power(self): 379 | """Returns the power state of the smart plug.""" 380 | packet = bytearray(16) 381 | packet[0] = 1 382 | response = self.send_packet(0x6a, packet) 383 | err = response[0x22] | (response[0x23] << 8) 384 | if err == 0: 385 | payload = self.decrypt(bytes(response[0x38:])) 386 | if type(payload[0x4]) == int: 387 | state = bool(payload[0x4]) 388 | else: 389 | state = bool(ord(payload[0x4])) 390 | return state 391 | 392 | class a1(device): 393 | def __init__ (self, host, mac): 394 | device.__init__(self, host, mac) 395 | self.type = "A1" 396 | 397 | def check_sensors(self): 398 | packet = bytearray(16) 399 | packet[0] = 1 400 | response = self.send_packet(0x6a, packet) 401 | err = response[0x22] | (response[0x23] << 8) 402 | if err == 0: 403 | data = {} 404 | payload = self.decrypt(bytes(response[0x38:])) 405 | if type(payload[0x4]) == int: 406 | data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 407 | data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 408 | light = payload[0x8] 409 | air_quality = payload[0x0a] 410 | noise = payload[0xc] 411 | else: 412 | data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 413 | data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 414 | light = ord(payload[0x8]) 415 | air_quality = ord(payload[0x0a]) 416 | noise = ord(payload[0xc]) 417 | if light == 0: 418 | data['light'] = 'dark' 419 | elif light == 1: 420 | data['light'] = 'dim' 421 | elif light == 2: 422 | data['light'] = 'normal' 423 | elif light == 3: 424 | data['light'] = 'bright' 425 | else: 426 | data['light'] = 'unknown' 427 | if air_quality == 0: 428 | data['air_quality'] = 'excellent' 429 | elif air_quality == 1: 430 | data['air_quality'] = 'good' 431 | elif air_quality == 2: 432 | data['air_quality'] = 'normal' 433 | elif air_quality == 3: 434 | data['air_quality'] = 'bad' 435 | else: 436 | data['air_quality'] = 'unknown' 437 | if noise == 0: 438 | data['noise'] = 'quiet' 439 | elif noise == 1: 440 | data['noise'] = 'normal' 441 | elif noise == 2: 442 | data['noise'] = 'noisy' 443 | else: 444 | data['noise'] = 'unknown' 445 | return data 446 | 447 | def check_sensors_raw(self): 448 | packet = bytearray(16) 449 | packet[0] = 1 450 | response = self.send_packet(0x6a, packet) 451 | err = response[0x22] | (response[0x23] << 8) 452 | if err == 0: 453 | data = {} 454 | payload = self.decrypt(bytes(response[0x38:])) 455 | if type(payload[0x4]) == int: 456 | data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 457 | data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 458 | data['light'] = payload[0x8] 459 | data['air_quality'] = payload[0x0a] 460 | data['noise'] = payload[0xc] 461 | else: 462 | data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 463 | data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 464 | data['light'] = ord(payload[0x8]) 465 | data['air_quality'] = ord(payload[0x0a]) 466 | data['noise'] = ord(payload[0xc]) 467 | return data 468 | 469 | 470 | class rm(device): 471 | def __init__ (self, host, mac): 472 | device.__init__(self, host, mac) 473 | self.type = "RM2" 474 | 475 | def check_data(self): 476 | packet = bytearray(16) 477 | packet[0] = 4 478 | response = self.send_packet(0x6a, packet) 479 | err = response[0x22] | (response[0x23] << 8) 480 | if err == 0: 481 | payload = self.decrypt(bytes(response[0x38:])) 482 | return payload[0x04:] 483 | 484 | def send_data(self, data): 485 | packet = bytearray([0x02, 0x00, 0x00, 0x00]) 486 | packet += data 487 | self.send_packet(0x6a, packet) 488 | 489 | def enter_learning(self): 490 | packet = bytearray(16) 491 | packet[0] = 3 492 | self.send_packet(0x6a, packet) 493 | 494 | def check_temperature(self): 495 | packet = bytearray(16) 496 | packet[0] = 1 497 | response = self.send_packet(0x6a, packet) 498 | err = response[0x22] | (response[0x23] << 8) 499 | if err == 0: 500 | payload = self.decrypt(bytes(response[0x38:])) 501 | if type(payload[0x4]) == int: 502 | temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 503 | else: 504 | temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 505 | return temp 506 | 507 | # For legay compatibility - don't use this 508 | class rm2(rm): 509 | def __init__ (self): 510 | device.__init__(self, None, None) 511 | 512 | def discover(self): 513 | dev = discover() 514 | self.host = dev.host 515 | self.mac = dev.mac 516 | 517 | 518 | S1C_SENSORS_TYPES = { 519 | 0x31: 'Door Sensor', # 49 as hex 520 | 0x91: 'Key Fob', # 145 as hex, as serial on fob corpse 521 | 0x21: 'Motion Sensor' # 33 as hex 522 | } 523 | 524 | 525 | class S1C(device): 526 | """ 527 | Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C 528 | """ 529 | def __init__(self, *a, **kw): 530 | device.__init__(self, *a, **kw) 531 | self.type = 'S1C' 532 | 533 | def get_sensors_status(self): 534 | packet = bytearray(16) 535 | packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors 536 | response = self.send_packet(0x6a, packet) 537 | err = response[0x22] | (response[0x23] << 8) 538 | if err == 0: 539 | aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) 540 | 541 | payload = aes.decrypt(bytes(response[0x38:])) 542 | if payload: 543 | head = payload[:4] 544 | count = payload[0x4] #need to fix for python 2.x 545 | sensors = payload[0x6:] 546 | sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] 547 | 548 | sens_res = [] 549 | for sens in sensors_a: 550 | status = ord(chr(sens[0])) 551 | _name = str(bytes(sens[4:26]).decode()) 552 | _order = ord(chr(sens[1])) 553 | _type = ord(chr(sens[3])) 554 | _serial = bytes(codecs.encode(sens[26:30],"hex")).decode() 555 | 556 | type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') 557 | 558 | r = { 559 | 'status': status, 560 | 'name': _name.strip('\x00'), 561 | 'type': type_str, 562 | 'order': _order, 563 | 'serial': _serial, 564 | } 565 | if r['serial'] != '00000000': 566 | sens_res.append(r) 567 | result = { 568 | 'count': count, 569 | 'sensors': sens_res 570 | } 571 | return result 572 | 573 | 574 | # Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode. 575 | # Only tested with Broadlink RM3 Mini (Blackbean) 576 | def setup(ssid, password, security_mode): 577 | # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) 578 | payload = bytearray(0x88) 579 | payload[0x26] = 0x14 # This seems to always be set to 14 580 | # Add the SSID to the payload 581 | ssid_start = 68 582 | ssid_length = 0 583 | for letter in ssid: 584 | payload[(ssid_start + ssid_length)] = ord(letter) 585 | ssid_length += 1 586 | # Add the WiFi password to the payload 587 | pass_start = 100 588 | pass_length = 0 589 | for letter in password: 590 | payload[(pass_start + pass_length)] = ord(letter) 591 | pass_length += 1 592 | 593 | payload[0x84] = ssid_length # Character length of SSID 594 | payload[0x85] = pass_length # Character length of password 595 | payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) 596 | 597 | checksum = 0xbeaf 598 | for i in range(len(payload)): 599 | checksum += payload[i] 600 | checksum = checksum & 0xffff 601 | 602 | payload[0x20] = checksum & 0xff # Checksum 1 position 603 | payload[0x21] = checksum >> 8 # Checksum 2 position 604 | 605 | sock = socket.socket(socket.AF_INET, # Internet 606 | socket.SOCK_DGRAM) # UDP 607 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 608 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 609 | sock.sendto(payload, ('255.255.255.255', 80)) 610 | 611 | -------------------------------------------------------------------------------- /broadlink_s1c_2/broadlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from datetime import datetime 4 | try: 5 | from Crypto.Cipher import AES 6 | except ImportError as e: 7 | import pyaes 8 | 9 | import time 10 | import random 11 | import socket 12 | import sys 13 | import threading 14 | import codecs 15 | 16 | def gendevice(devtype, host, mac): 17 | if devtype == 0: # SP1 18 | return sp1(host=host, mac=mac) 19 | if devtype == 0x2711: # SP2 20 | return sp2(host=host, mac=mac) 21 | if devtype == 0x2719 or devtype == 0x7919 or devtype == 0x271a or devtype == 0x791a: # Honeywell SP2 22 | return sp2(host=host, mac=mac) 23 | if devtype == 0x2720: # SPMini 24 | return sp2(host=host, mac=mac) 25 | elif devtype == 0x753e: # SP3 26 | return sp2(host=host, mac=mac) 27 | elif devtype == 0x947a or devtype == 0x9479: # SP3S 28 | return sp2(host=host, mac=mac) 29 | elif devtype == 0x2728: # SPMini2 30 | return sp2(host=host, mac=mac) 31 | elif devtype == 0x2733 or devtype == 0x273e: # OEM branded SPMini 32 | return sp2(host=host, mac=mac) 33 | elif devtype >= 0x7530 and devtype <= 0x7918: # OEM branded SPMini2 34 | return sp2(host=host, mac=mac) 35 | elif devtype == 0x2736: # SPMiniPlus 36 | return sp2(host=host, mac=mac) 37 | elif devtype == 0x2712: # RM2 38 | return rm(host=host, mac=mac) 39 | elif devtype == 0x2737: # RM Mini 40 | return rm(host=host, mac=mac) 41 | elif devtype == 0x273d: # RM Pro Phicomm 42 | return rm(host=host, mac=mac) 43 | elif devtype == 0x2783: # RM2 Home Plus 44 | return rm(host=host, mac=mac) 45 | elif devtype == 0x277c: # RM2 Home Plus GDT 46 | return rm(host=host, mac=mac) 47 | elif devtype == 0x272a: # RM2 Pro Plus 48 | return rm(host=host, mac=mac) 49 | elif devtype == 0x2787: # RM2 Pro Plus2 50 | return rm(host=host, mac=mac) 51 | elif devtype == 0x278b: # RM2 Pro Plus BL 52 | return rm(host=host, mac=mac) 53 | elif devtype == 0x278f: # RM Mini Shate 54 | return rm(host=host, mac=mac) 55 | elif devtype == 0x2714: # A1 56 | return a1(host=host, mac=mac) 57 | elif devtype == 0x4EB5: # MP1 58 | return mp1(host=host, mac=mac) 59 | elif devtype == 0x2722: # S1 (SmartOne Alarm Kit) 60 | return S1C(host=host, mac=mac) 61 | else: 62 | return device(host=host, mac=mac) 63 | 64 | def discover(timeout=None, local_ip_address=None): 65 | if local_ip_address is None: 66 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 67 | s.connect(('8.8.8.8', 53)) # connecting to a UDP address doesn't send packets 68 | local_ip_address = s.getsockname()[0] 69 | address = local_ip_address.split('.') 70 | cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 71 | cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 72 | cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 73 | cs.bind((local_ip_address,0)) 74 | port = cs.getsockname()[1] 75 | starttime = time.time() 76 | 77 | devices = [] 78 | 79 | timezone = int(time.timezone/-3600) 80 | packet = bytearray(0x30) 81 | 82 | year = datetime.now().year 83 | 84 | if timezone < 0: 85 | packet[0x08] = 0xff + timezone - 1 86 | packet[0x09] = 0xff 87 | packet[0x0a] = 0xff 88 | packet[0x0b] = 0xff 89 | else: 90 | packet[0x08] = timezone 91 | packet[0x09] = 0 92 | packet[0x0a] = 0 93 | packet[0x0b] = 0 94 | packet[0x0c] = year & 0xff 95 | packet[0x0d] = year >> 8 96 | packet[0x0e] = datetime.now().minute 97 | packet[0x0f] = datetime.now().hour 98 | subyear = str(year)[2:] 99 | packet[0x10] = int(subyear) 100 | packet[0x11] = datetime.now().isoweekday() 101 | packet[0x12] = datetime.now().day 102 | packet[0x13] = datetime.now().month 103 | packet[0x18] = int(address[0]) 104 | packet[0x19] = int(address[1]) 105 | packet[0x1a] = int(address[2]) 106 | packet[0x1b] = int(address[3]) 107 | packet[0x1c] = port & 0xff 108 | packet[0x1d] = port >> 8 109 | packet[0x26] = 6 110 | checksum = 0xbeaf 111 | 112 | for i in range(len(packet)): 113 | checksum += packet[i] 114 | checksum = checksum & 0xffff 115 | packet[0x20] = checksum & 0xff 116 | packet[0x21] = checksum >> 8 117 | 118 | cs.sendto(packet, ('255.255.255.255', 80)) 119 | if timeout is None: 120 | response = cs.recvfrom(1024) 121 | responsepacket = bytearray(response[0]) 122 | host = response[1] 123 | mac = responsepacket[0x3a:0x40] 124 | devtype = responsepacket[0x34] | responsepacket[0x35] << 8 125 | return gendevice(devtype, host, mac) 126 | else: 127 | while (time.time() - starttime) < timeout: 128 | cs.settimeout(timeout - (time.time() - starttime)) 129 | try: 130 | response = cs.recvfrom(1024) 131 | except socket.timeout: 132 | return devices 133 | responsepacket = bytearray(response[0]) 134 | host = response[1] 135 | devtype = responsepacket[0x34] | responsepacket[0x35] << 8 136 | mac = responsepacket[0x3a:0x40] 137 | dev = gendevice(devtype, host, mac) 138 | devices.append(dev) 139 | return devices 140 | 141 | 142 | class device: 143 | def __init__(self, host, mac, timeout=10): 144 | self.host = host 145 | self.mac = mac 146 | self.timeout = timeout 147 | self.count = random.randrange(0xffff) 148 | self.key = bytearray([0x09, 0x76, 0x28, 0x34, 0x3f, 0xe9, 0x9e, 0x23, 0x76, 0x5c, 0x15, 0x13, 0xac, 0xcf, 0x8b, 0x02]) 149 | self.iv = bytearray([0x56, 0x2e, 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58]) 150 | self.id = bytearray([0, 0, 0, 0]) 151 | self.cs = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 152 | self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 153 | self.cs.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 154 | self.cs.bind(('',0)) 155 | self.type = "Unknown" 156 | self.lock = threading.Lock() 157 | 158 | if 'pyaes' in sys.modules: 159 | self.encrypt = self.encrypt_pyaes 160 | self.decrypt = self.decrypt_pyaes 161 | else: 162 | self.encrypt = self.encrypt_pycrypto 163 | self.decrypt = self.decrypt_pycrypto 164 | 165 | def encrypt_pyaes(self, payload): 166 | aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) 167 | return "".join([aes.encrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) 168 | 169 | def decrypt_pyaes(self, payload): 170 | aes = pyaes.AESModeOfOperationCBC(self.key, iv = bytes(self.iv)) 171 | return "".join([aes.decrypt(bytes(payload[i:i+16])) for i in range(0, len(payload), 16)]) 172 | 173 | def encrypt_pycrypto(self, payload): 174 | aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) 175 | return aes.encrypt(bytes(payload)) 176 | 177 | def decrypt_pycrypto(self, payload): 178 | aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) 179 | return aes.decrypt(bytes(payload)) 180 | 181 | def auth(self): 182 | payload = bytearray(0x50) 183 | payload[0x04] = 0x31 184 | payload[0x05] = 0x31 185 | payload[0x06] = 0x31 186 | payload[0x07] = 0x31 187 | payload[0x08] = 0x31 188 | payload[0x09] = 0x31 189 | payload[0x0a] = 0x31 190 | payload[0x0b] = 0x31 191 | payload[0x0c] = 0x31 192 | payload[0x0d] = 0x31 193 | payload[0x0e] = 0x31 194 | payload[0x0f] = 0x31 195 | payload[0x10] = 0x31 196 | payload[0x11] = 0x31 197 | payload[0x12] = 0x31 198 | payload[0x1e] = 0x01 199 | payload[0x2d] = 0x01 200 | payload[0x30] = ord('T') 201 | payload[0x31] = ord('e') 202 | payload[0x32] = ord('s') 203 | payload[0x33] = ord('t') 204 | payload[0x34] = ord(' ') 205 | payload[0x35] = ord(' ') 206 | payload[0x36] = ord('1') 207 | 208 | response = self.send_packet(0x65, payload) 209 | 210 | payload = self.decrypt(response[0x38:]) 211 | 212 | if not payload: 213 | return False 214 | 215 | key = payload[0x04:0x14] 216 | if len(key) % 16 != 0: 217 | return False 218 | 219 | self.id = payload[0x00:0x04] 220 | self.key = key 221 | return True 222 | 223 | def get_type(self): 224 | return self.type 225 | 226 | def send_packet(self, command, payload): 227 | self.count = (self.count + 1) & 0xffff 228 | packet = bytearray(0x38) 229 | packet[0x00] = 0x5a 230 | packet[0x01] = 0xa5 231 | packet[0x02] = 0xaa 232 | packet[0x03] = 0x55 233 | packet[0x04] = 0x5a 234 | packet[0x05] = 0xa5 235 | packet[0x06] = 0xaa 236 | packet[0x07] = 0x55 237 | packet[0x24] = 0x2a 238 | packet[0x25] = 0x27 239 | packet[0x26] = command 240 | packet[0x28] = self.count & 0xff 241 | packet[0x29] = self.count >> 8 242 | packet[0x2a] = self.mac[0] 243 | packet[0x2b] = self.mac[1] 244 | packet[0x2c] = self.mac[2] 245 | packet[0x2d] = self.mac[3] 246 | packet[0x2e] = self.mac[4] 247 | packet[0x2f] = self.mac[5] 248 | packet[0x30] = self.id[0] 249 | packet[0x31] = self.id[1] 250 | packet[0x32] = self.id[2] 251 | packet[0x33] = self.id[3] 252 | 253 | # pad the payload for AES encryption 254 | if len(payload)>0: 255 | numpad=(len(payload)//16+1)*16 256 | payload=payload.ljust(numpad,b"\x00") 257 | 258 | checksum = 0xbeaf 259 | for i in range(len(payload)): 260 | checksum += payload[i] 261 | checksum = checksum & 0xffff 262 | 263 | payload = self.encrypt(payload) 264 | 265 | packet[0x34] = checksum & 0xff 266 | packet[0x35] = checksum >> 8 267 | 268 | for i in range(len(payload)): 269 | packet.append(payload[i]) 270 | 271 | checksum = 0xbeaf 272 | for i in range(len(packet)): 273 | checksum += packet[i] 274 | checksum = checksum & 0xffff 275 | packet[0x20] = checksum & 0xff 276 | packet[0x21] = checksum >> 8 277 | 278 | starttime = time.time() 279 | with self.lock: 280 | while True: 281 | try: 282 | self.cs.sendto(packet, self.host) 283 | self.cs.settimeout(1) 284 | response = self.cs.recvfrom(2048) 285 | break 286 | except socket.timeout: 287 | if (time.time() - starttime) > self.timeout: 288 | raise 289 | return bytearray(response[0]) 290 | 291 | 292 | class mp1(device): 293 | def __init__ (self, host, mac): 294 | device.__init__(self, host, mac) 295 | self.type = "MP1" 296 | 297 | def set_power_mask(self, sid_mask, state): 298 | """Sets the power state of the smart power strip.""" 299 | 300 | packet = bytearray(16) 301 | packet[0x00] = 0x0d 302 | packet[0x02] = 0xa5 303 | packet[0x03] = 0xa5 304 | packet[0x04] = 0x5a 305 | packet[0x05] = 0x5a 306 | packet[0x06] = 0xb2 + ((sid_mask<<1) if state else sid_mask) 307 | packet[0x07] = 0xc0 308 | packet[0x08] = 0x02 309 | packet[0x0a] = 0x03 310 | packet[0x0d] = sid_mask 311 | packet[0x0e] = sid_mask if state else 0 312 | 313 | response = self.send_packet(0x6a, packet) 314 | 315 | err = response[0x22] | (response[0x23] << 8) 316 | 317 | def set_power(self, sid, state): 318 | """Sets the power state of the smart power strip.""" 319 | sid_mask = 0x01 << (sid - 1) 320 | return self.set_power_mask(sid_mask, state) 321 | 322 | def check_power_raw(self): 323 | """Returns the power state of the smart power strip in raw format.""" 324 | packet = bytearray(16) 325 | packet[0x00] = 0x0a 326 | packet[0x02] = 0xa5 327 | packet[0x03] = 0xa5 328 | packet[0x04] = 0x5a 329 | packet[0x05] = 0x5a 330 | packet[0x06] = 0xae 331 | packet[0x07] = 0xc0 332 | packet[0x08] = 0x01 333 | 334 | response = self.send_packet(0x6a, packet) 335 | err = response[0x22] | (response[0x23] << 8) 336 | if err == 0: 337 | payload = self.decrypt(bytes(response[0x38:])) 338 | if type(payload[0x4]) == int: 339 | state = payload[0x0e] 340 | else: 341 | state = ord(payload[0x0e]) 342 | return state 343 | 344 | def check_power(self): 345 | """Returns the power state of the smart power strip.""" 346 | state = self.check_power_raw() 347 | data = {} 348 | data['s1'] = bool(state & 0x01) 349 | data['s2'] = bool(state & 0x02) 350 | data['s3'] = bool(state & 0x04) 351 | data['s4'] = bool(state & 0x08) 352 | return data 353 | 354 | 355 | class sp1(device): 356 | def __init__ (self, host, mac): 357 | device.__init__(self, host, mac) 358 | self.type = "SP1" 359 | 360 | def set_power(self, state): 361 | packet = bytearray(4) 362 | packet[0] = state 363 | self.send_packet(0x66, packet) 364 | 365 | 366 | class sp2(device): 367 | def __init__ (self, host, mac): 368 | device.__init__(self, host, mac) 369 | self.type = "SP2" 370 | 371 | def set_power(self, state): 372 | """Sets the power state of the smart plug.""" 373 | packet = bytearray(16) 374 | packet[0] = 2 375 | packet[4] = 1 if state else 0 376 | self.send_packet(0x6a, packet) 377 | 378 | def check_power(self): 379 | """Returns the power state of the smart plug.""" 380 | packet = bytearray(16) 381 | packet[0] = 1 382 | response = self.send_packet(0x6a, packet) 383 | err = response[0x22] | (response[0x23] << 8) 384 | if err == 0: 385 | payload = self.decrypt(bytes(response[0x38:])) 386 | if type(payload[0x4]) == int: 387 | state = bool(payload[0x4]) 388 | else: 389 | state = bool(ord(payload[0x4])) 390 | return state 391 | 392 | class a1(device): 393 | def __init__ (self, host, mac): 394 | device.__init__(self, host, mac) 395 | self.type = "A1" 396 | 397 | def check_sensors(self): 398 | packet = bytearray(16) 399 | packet[0] = 1 400 | response = self.send_packet(0x6a, packet) 401 | err = response[0x22] | (response[0x23] << 8) 402 | if err == 0: 403 | data = {} 404 | payload = self.decrypt(bytes(response[0x38:])) 405 | if type(payload[0x4]) == int: 406 | data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 407 | data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 408 | light = payload[0x8] 409 | air_quality = payload[0x0a] 410 | noise = payload[0xc] 411 | else: 412 | data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 413 | data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 414 | light = ord(payload[0x8]) 415 | air_quality = ord(payload[0x0a]) 416 | noise = ord(payload[0xc]) 417 | if light == 0: 418 | data['light'] = 'dark' 419 | elif light == 1: 420 | data['light'] = 'dim' 421 | elif light == 2: 422 | data['light'] = 'normal' 423 | elif light == 3: 424 | data['light'] = 'bright' 425 | else: 426 | data['light'] = 'unknown' 427 | if air_quality == 0: 428 | data['air_quality'] = 'excellent' 429 | elif air_quality == 1: 430 | data['air_quality'] = 'good' 431 | elif air_quality == 2: 432 | data['air_quality'] = 'normal' 433 | elif air_quality == 3: 434 | data['air_quality'] = 'bad' 435 | else: 436 | data['air_quality'] = 'unknown' 437 | if noise == 0: 438 | data['noise'] = 'quiet' 439 | elif noise == 1: 440 | data['noise'] = 'normal' 441 | elif noise == 2: 442 | data['noise'] = 'noisy' 443 | else: 444 | data['noise'] = 'unknown' 445 | return data 446 | 447 | def check_sensors_raw(self): 448 | packet = bytearray(16) 449 | packet[0] = 1 450 | response = self.send_packet(0x6a, packet) 451 | err = response[0x22] | (response[0x23] << 8) 452 | if err == 0: 453 | data = {} 454 | payload = self.decrypt(bytes(response[0x38:])) 455 | if type(payload[0x4]) == int: 456 | data['temperature'] = (payload[0x4] * 10 + payload[0x5]) / 10.0 457 | data['humidity'] = (payload[0x6] * 10 + payload[0x7]) / 10.0 458 | data['light'] = payload[0x8] 459 | data['air_quality'] = payload[0x0a] 460 | data['noise'] = payload[0xc] 461 | else: 462 | data['temperature'] = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 463 | data['humidity'] = (ord(payload[0x6]) * 10 + ord(payload[0x7])) / 10.0 464 | data['light'] = ord(payload[0x8]) 465 | data['air_quality'] = ord(payload[0x0a]) 466 | data['noise'] = ord(payload[0xc]) 467 | return data 468 | 469 | 470 | class rm(device): 471 | def __init__ (self, host, mac): 472 | device.__init__(self, host, mac) 473 | self.type = "RM2" 474 | 475 | def check_data(self): 476 | packet = bytearray(16) 477 | packet[0] = 4 478 | response = self.send_packet(0x6a, packet) 479 | err = response[0x22] | (response[0x23] << 8) 480 | if err == 0: 481 | payload = self.decrypt(bytes(response[0x38:])) 482 | return payload[0x04:] 483 | 484 | def send_data(self, data): 485 | packet = bytearray([0x02, 0x00, 0x00, 0x00]) 486 | packet += data 487 | self.send_packet(0x6a, packet) 488 | 489 | def enter_learning(self): 490 | packet = bytearray(16) 491 | packet[0] = 3 492 | self.send_packet(0x6a, packet) 493 | 494 | def check_temperature(self): 495 | packet = bytearray(16) 496 | packet[0] = 1 497 | response = self.send_packet(0x6a, packet) 498 | err = response[0x22] | (response[0x23] << 8) 499 | if err == 0: 500 | payload = self.decrypt(bytes(response[0x38:])) 501 | if type(payload[0x4]) == int: 502 | temp = (payload[0x4] * 10 + payload[0x5]) / 10.0 503 | else: 504 | temp = (ord(payload[0x4]) * 10 + ord(payload[0x5])) / 10.0 505 | return temp 506 | 507 | # For legay compatibility - don't use this 508 | class rm2(rm): 509 | def __init__ (self): 510 | device.__init__(self, None, None) 511 | 512 | def discover(self): 513 | dev = discover() 514 | self.host = dev.host 515 | self.mac = dev.mac 516 | 517 | 518 | S1C_SENSORS_TYPES = { 519 | 0x31: 'Door Sensor', # 49 as hex 520 | 0x91: 'Key Fob', # 145 as hex, as serial on fob corpse 521 | 0x21: 'Motion Sensor' # 33 as hex 522 | } 523 | 524 | 525 | class S1C(device): 526 | """ 527 | Its VERY VERY VERY DIRTY IMPLEMENTATION of S1C 528 | """ 529 | def __init__(self, *a, **kw): 530 | device.__init__(self, *a, **kw) 531 | self.type = 'S1C' 532 | 533 | def get_sensors_status(self): 534 | packet = bytearray(16) 535 | packet[0] = 0x06 # 0x06 - get sensors info, 0x07 - probably add sensors 536 | response = self.send_packet(0x6a, packet) 537 | err = response[0x22] | (response[0x23] << 8) 538 | if err == 0: 539 | aes = AES.new(bytes(self.key), AES.MODE_CBC, bytes(self.iv)) 540 | 541 | payload = aes.decrypt(bytes(response[0x38:])) 542 | if payload: 543 | head = payload[:4] 544 | count = payload[0x4] #need to fix for python 2.x 545 | sensors = payload[0x6:] 546 | sensors_a = [bytearray(sensors[i * 83:(i + 1) * 83]) for i in range(len(sensors) // 83)] 547 | 548 | sens_res = [] 549 | for sens in sensors_a: 550 | status = ord(chr(sens[0])) 551 | _name = str(bytes(sens[4:26]).decode()) 552 | _order = ord(chr(sens[1])) 553 | _type = ord(chr(sens[3])) 554 | _serial = bytes(codecs.encode(sens[26:30],"hex")).decode() 555 | 556 | type_str = S1C_SENSORS_TYPES.get(_type, 'Unknown') 557 | 558 | r = { 559 | 'status': status, 560 | 'name': _name.strip('\x00'), 561 | 'type': type_str, 562 | 'order': _order, 563 | 'serial': _serial, 564 | } 565 | if r['serial'] != '00000000': 566 | sens_res.append(r) 567 | result = { 568 | 'count': count, 569 | 'sensors': sens_res 570 | } 571 | return result 572 | 573 | 574 | # Setup a new Broadlink device via AP Mode. Review the README to see how to enter AP Mode. 575 | # Only tested with Broadlink RM3 Mini (Blackbean) 576 | def setup(ssid, password, security_mode): 577 | # Security mode options are (0 - none, 1 = WEP, 2 = WPA1, 3 = WPA2, 4 = WPA1/2) 578 | payload = bytearray(0x88) 579 | payload[0x26] = 0x14 # This seems to always be set to 14 580 | # Add the SSID to the payload 581 | ssid_start = 68 582 | ssid_length = 0 583 | for letter in ssid: 584 | payload[(ssid_start + ssid_length)] = ord(letter) 585 | ssid_length += 1 586 | # Add the WiFi password to the payload 587 | pass_start = 100 588 | pass_length = 0 589 | for letter in password: 590 | payload[(pass_start + pass_length)] = ord(letter) 591 | pass_length += 1 592 | 593 | payload[0x84] = ssid_length # Character length of SSID 594 | payload[0x85] = pass_length # Character length of password 595 | payload[0x86] = security_mode # Type of encryption (00 - none, 01 = WEP, 02 = WPA1, 03 = WPA2, 04 = WPA1/2) 596 | 597 | checksum = 0xbeaf 598 | for i in range(len(payload)): 599 | checksum += payload[i] 600 | checksum = checksum & 0xffff 601 | 602 | payload[0x20] = checksum & 0xff # Checksum 1 position 603 | payload[0x21] = checksum >> 8 # Checksum 2 position 604 | 605 | sock = socket.socket(socket.AF_INET, # Internet 606 | socket.SOCK_DGRAM) # UDP 607 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 608 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 609 | sock.sendto(payload, ('255.255.255.255', 80)) 610 | 611 | --------------------------------------------------------------------------------