├── .dockerignore ├── .github └── workflows │ ├── build.yaml │ └── release.yml ├── Dockerfile ├── LICENSE ├── README.md ├── lirc_watcher.py └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENCE -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: docker build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - '*' 12 | schedule: 13 | - cron: '0 7 1 * *' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v1 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v1 27 | 28 | - name: Build 29 | uses: docker/build-push-action@v2 30 | with: 31 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Prepare 16 | id: prepare 17 | run: | 18 | git fetch --prune --unshallow --tags -f 19 | 20 | VERSION=$(git tag --points-at HEAD) 21 | VERSION=${VERSION//v} 22 | IMAGE_NAME=${GITHUB_REPOSITORY#*/} 23 | IMAGE_NAME="${{ secrets.DOCKERHUB_USER }}/${IMAGE_NAME//docker-}" 24 | IMAGE_TAGS="${IMAGE_NAME}:latest,${IMAGE_NAME}:${VERSION}" 25 | 26 | echo "## :bookmark_tabs: Changes" >>"CHANGELOG.md" 27 | git log --pretty=format:"- %s %H%n" $(git describe --abbrev=0 --tags $(git describe --tags --abbrev=0)^)...$(git describe --tags --abbrev=0) >>"CHANGELOG.md" 28 | 29 | echo ::set-output name=image_tags::${IMAGE_TAGS} 30 | echo ::set-output name=version::${VERSION} 31 | 32 | - name: Set up QEMU 33 | uses: docker/setup-qemu-action@v1 34 | 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@v1 37 | 38 | - name: Login to DockerHub 39 | uses: docker/login-action@v1 40 | with: 41 | username: ${{ secrets.DOCKERHUB_USER }} 42 | password: ${{ secrets.DOCKERHUB_TOKEN }} 43 | 44 | - name: Build and Push 45 | uses: docker/build-push-action@v2 46 | with: 47 | push: true 48 | tags: ${{ steps.prepare.outputs.image_tags }} 49 | platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 50 | 51 | - name: Create Release 52 | uses: actions/create-release@v1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | tag_name: ${{ github.ref }} 57 | release_name: Release ${{ steps.prepare.outputs.version }} 58 | body_path: CHANGELOG.md 59 | draft: false 60 | prerelease: false -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN pip3 install --trusted-host pypi.python.org -r requirements.txt 8 | 9 | CMD ["python3", "-u", "lirc_watcher.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pavel S 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LIRC watcher 2 | ![Docker Build](https://github.com/pilotak/docker-lirc-watcher/workflows/docker%20build/badge.svg) ![Docker Pulls](https://img.shields.io/docker/pulls/pilotak/lirc-watcher) ![Docker Size](https://img.shields.io/docker/image-size/pilotak/lirc-watcher?color=orange) 3 | 4 | Docker container that listens to LIRC daemon (running on the host) and sends received codes over MQTT with added benefit of short and long putton press. It can also send IR remote keys through lirc by publishing to MQTT topics. 5 | 6 | **LIRC must be install on the host system**. Following examples have been tested below but should work on other platforms with adjustments too. 7 | 8 | ### Debian Buster 9 | Please follow steps in wiki [Install-on-Debian-Buster](https://github.com/pilotak/docker-lirc-watcher/wiki/Install-on-Debian-Buster) 10 | 11 | ### Debian Stretch 12 | Please follow steps in wiki [Install-on-Debian-Stretch](https://github.com/pilotak/docker-lirc-watcher/wiki/Install-on-Debian-Stretch) 13 | 14 | ### Ubuntu 18.04 15 | Please follow steps in wiki [Install-on-Ubuntu-18.04](https://github.com/pilotak/docker-lirc-watcher/wiki/Install-on-Ubuntu-18.04) 16 | 17 | ## Recording codes 18 | Test receiver 19 | ```sh 20 | sudo systemctl stop lircd.service 21 | mode2 --driver default --device /dev/lirc0 22 | ``` 23 | Output should look similar to this: 24 | ``` 25 | space 4195 26 | pulse 551 27 | space 1621 28 | pulse 501 29 | space 529 30 | pulse 572 31 | space 1553 32 | pulse 547 33 | space 524 34 | pulse 551 35 | space 525 36 | pulse 549 37 | ``` 38 | 39 | If everything is ok, execute following and follow the commands in the script. Adjust name of the file should you wish. 40 | ```sh 41 | sudo irrecord --driver default --device /dev/lirc0 ~/pioneer.lircd.conf 42 | ``` 43 | 44 | You will end it up with file `~/pioneer.lircd.conf`. 45 | 46 | ``` 47 | begin remote 48 | 49 | name pioneer 50 | bits 32 51 | flags SPACE_ENC|CONST_LENGTH 52 | eps 30 53 | aeps 100 54 | 55 | header 8544 4180 56 | one 578 1545 57 | zero 578 497 58 | ptrail 575 59 | gap 91166 60 | min_repeat 3 61 | toggle_bit_mask 0xF0F08080 62 | frequency 38000 63 | 64 | begin codes 65 | KEY_POWER 0xA55A38C7 66 | KEY_MUTE 0xA55A48B7 67 | KEY_VOLUMEUP 0xA55A50AF 68 | KEY_VOLUMEDOWN 0xA55AD02F 69 | end codes 70 | 71 | end remote 72 | ``` 73 | 74 | Let's move this config over to LIRC daemon. 75 | ```sh 76 | sudo mv ~/pioneer.lircd.conf /etc/lirc/lircd.conf.d/ 77 | sudo systemctl start lircd.service 78 | ``` 79 | 80 | Test receiver again, if you see the names of your keys when button pressed, that's a win-win. 81 | ```sh 82 | irw 83 | ``` 84 | 85 | ## Docker-compose 86 | Now just start the docker container, alter the config to your needs and you ready to rock. 87 | ```yaml 88 | version: "3" 89 | services: 90 | lirc: 91 | container_name: lirc 92 | restart: always 93 | image: pilotak/lirc-watcher 94 | environment: 95 | - MQTT_BROKER=192.168.0.10 96 | - MQTT_USER=admin 97 | - MQTT_PASSWORD=my-secret-pw 98 | volumes: 99 | - /var/run/lirc/lircd:/var/run/lirc/lircd 100 | ``` 101 | 102 | ### Environmental variables 103 | Bellow are all available variables 104 | 105 | | Variable | Description | Default value | 106 | | --- | --- | :---:| 107 | | `LONG_PRESS` | How many messages is received to be considered as long press | 12 | 108 | | `READ_TIMEOUT` | How long to wait to process new data *seconds* | 0.2 | 109 | | `PAYLOAD_LONG_PRESS` | Payload on long press | "long" | 110 | | `PAYLOAD_SHORT_CLICK` | Payload on short press | "short" | 111 | | `MQTT_BROKER` | Broker address | localhost | 112 | | `MQTT_USER` | MQTT user | None | 113 | | `MQTT_PASSWORD` | MQTT password | None | 114 | | `MQTT_PORT` | MQTT broker port | 1883 | 115 | | `MQTT_ID` | MQTT client id | "lirc-watcher" | 116 | | `MQTT_PREFIX` | MQTT topic prefix | "lirc" | 117 | | `MQTT_QOS` | MQTT QOS | 1 | 118 | 119 | ### MQTT topics for receiving 120 | When button is pressed you will receive message in format 121 | `MQTT_PREFIX/REMOTE_NAME/KEY_NAME` with payload `short` / `long` ie. `lirc/pioneer/KEY_POWER` 122 | 123 | ### MQTT topics for sending 124 | To send a remote key press, publish to `MQTT_PREFIX/send/REMOTE_NAME/KEY_NAME` with the payload being the number of repeats ie. `lirc/send/pioneer/KEY_POWER` If the payload is empty, repeats will default to 1. 125 | -------------------------------------------------------------------------------- /lirc_watcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import paho.mqtt.client as paho 5 | from threading import Timer 6 | import sys 7 | import socket 8 | import fcntl 9 | import errno 10 | from time import sleep 11 | 12 | LONG_PRESS = os.getenv('LONG_PRESS', 12) 13 | READ_TIMEOUT = os.getenv('READ_TIMEOUT', 0.2) 14 | PAYLOAD_LONG_PRESS = os.getenv('PAYLOAD_LONG_PRESS', 'long') 15 | PAYLOAD_SHORT_CLICK = os.getenv('PAYLOAD_SHORT_CLICK', 'short') 16 | MQTT_BROKER = os.getenv('MQTT_BROKER', 'localhost') 17 | MQTT_USER = os.getenv('MQTT_USER', None) 18 | MQTT_PASSWORD = os.getenv('MQTT_PASSWORD', None) 19 | MQTT_QOS = os.getenv('MQTT_QOS', 1) 20 | MQTT_PORT = os.getenv('MQTT_PORT', 1883) 21 | MQTT_ID = os.getenv('MQTT_ID', 'lirc-watcher') 22 | MQTT_PREFIX = os.getenv('MQTT_PREFIX', 'lirc') 23 | 24 | MQTT_SEND_TOPIC = '%s/send/#' % MQTT_PREFIX 25 | MQTT_STATUS_TOPIC = '%s/alive' % MQTT_PREFIX 26 | MQTT_PAYLOAD_ONLINE = '1' 27 | MQTT_PAYLOAD_OFFLINE = '0' 28 | 29 | print("LIRC watcher started") 30 | 31 | 32 | def on_mqtt_connect(mqtt, userdata, flags, rc): 33 | if rc == 0: 34 | print('MQTT connected') 35 | 36 | mqtt.publish(MQTT_STATUS_TOPIC, payload=MQTT_PAYLOAD_ONLINE, 37 | qos=MQTT_QOS, retain=True) 38 | mqtt.subscribe(MQTT_SEND_TOPIC) 39 | else: 40 | print('MQTT connect failed:', rc) 41 | 42 | def on_mqtt_message(client, userdata, msg): 43 | try: 44 | (remote, key) = str(msg.topic).split("/")[-2:] 45 | try: 46 | repeat = int(msg.payload) 47 | except: 48 | repeat = 1 49 | command = "SEND_ONCE %s %s %d\n" % (remote, key, repeat) 50 | print(command) 51 | sock.sendall(command.encode("utf-8")) 52 | except: 53 | print(str(msg.topic)) 54 | 55 | prev_data = None 56 | timer = None 57 | 58 | mqtt = paho.Client(MQTT_ID) 59 | mqtt.on_connect = on_mqtt_connect 60 | mqtt.on_message = on_mqtt_message 61 | mqtt.will_set(MQTT_STATUS_TOPIC, payload=MQTT_PAYLOAD_OFFLINE, 62 | qos=MQTT_QOS, retain=True) 63 | mqtt.username_pw_set(MQTT_USER, MQTT_PASSWORD) 64 | mqtt.connect(MQTT_BROKER, MQTT_PORT) 65 | mqtt.loop_start() 66 | 67 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 68 | sock.connect("/var/run/lirc/lircd") 69 | fcntl.fcntl(sock, fcntl.F_SETFL, os.O_NONBLOCK) 70 | 71 | 72 | def send_code(priority_data=None): 73 | global prev_data, mqtt, timer 74 | 75 | if timer is not None and timer.is_alive(): 76 | timer.cancel() 77 | timer = None 78 | 79 | if priority_data is not None or prev_data is not None: 80 | to_send = priority_data if priority_data is not None else prev_data 81 | to_send = to_send.split() 82 | key_name = to_send[2] 83 | remote = to_send[3] 84 | counter = int(to_send[1], 16) 85 | 86 | if(counter >= LONG_PRESS): 87 | payload = PAYLOAD_LONG_PRESS 88 | else: 89 | payload = PAYLOAD_SHORT_CLICK 90 | 91 | topic = "%s/%s/%s" % (MQTT_PREFIX, remote, key_name) 92 | print("Sending message: '%s' to topic: '%s'" % 93 | (payload, topic)) 94 | mqtt.publish(topic, payload=payload, qos=MQTT_QOS) 95 | 96 | if priority_data is None: 97 | prev_data = None 98 | 99 | 100 | try: 101 | while True: 102 | """ 103 | Check for new data 104 | """ 105 | try: 106 | new_data = sock.recv(128) 107 | new_data = new_data.strip() 108 | 109 | except socket.error as e: 110 | err = e.args[0] 111 | 112 | """ 113 | Check for "real" error 114 | """ 115 | if err == errno.EAGAIN or err == errno.EWOULDBLOCK: 116 | sleep(0.01) 117 | continue 118 | else: 119 | print(e) 120 | mqtt.disconnect() 121 | mqtt.loop_stop() 122 | sys.exit(1) 123 | else: 124 | if new_data: 125 | new_data = new_data.decode("utf-8") 126 | print("new_data: ", new_data) 127 | counter_str = new_data.split() 128 | 129 | #ignore the status return from lirc sends 130 | if counter_str[0] != "BEGIN": 131 | """ 132 | If we received new_data and prev_data was not sent 133 | """ 134 | if prev_data is not None and int(counter_str[1], 16) == 0: 135 | send_code(prev_data) 136 | 137 | prev_data = new_data 138 | 139 | if timer is not None and timer.is_alive(): 140 | timer.cancel() 141 | 142 | timer = Timer(READ_TIMEOUT, send_code) 143 | timer.start() 144 | 145 | 146 | except KeyboardInterrupt: 147 | mqtt.disconnect() 148 | mqtt.loop_stop() 149 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | paho-mqtt 2 | --------------------------------------------------------------------------------