Added a background thread for updating the light zones on the QSE and added MQTT support for Home Assistant.
This commit is contained in:
parent
f666e423c2
commit
979c865262
22 changed files with 543 additions and 49 deletions
32
Home Assistant/docker-compose.yml
Normal file
32
Home Assistant/docker-compose.yml
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
homeassistant:
|
||||||
|
container_name: home-assistant
|
||||||
|
image: homeassistant/home-assistant:stable
|
||||||
|
volumes:
|
||||||
|
- ./hass:/config
|
||||||
|
environment:
|
||||||
|
- TZ=America/Chicago
|
||||||
|
restart: always
|
||||||
|
devices:
|
||||||
|
- /dev/ttyUSB1:/dev/ttyUSB1
|
||||||
|
- /dev/serial/by-id/usb-Silicon_Labs_HubZ_Smart_Home_Controller_415007C7-if01-port0
|
||||||
|
network_mode: host
|
||||||
|
mqtt:
|
||||||
|
image: eclipse-mosquitto
|
||||||
|
volumes:
|
||||||
|
- ./mosquitto:/mosquitto/config
|
||||||
|
restart: always
|
||||||
|
network_mode: host
|
||||||
|
zwave-js:
|
||||||
|
container_name: zwavejs2mqtt
|
||||||
|
image: zwavejs/zwavejs2mqtt:latest
|
||||||
|
volumes:
|
||||||
|
- ./zwave-js:/usr/src/app/store
|
||||||
|
devices:
|
||||||
|
- /dev/ttyUSB0:/dev/ttyUSB0
|
||||||
|
- /dev/serial/by-id/usb-Silicon_Labs_HubZ_Smart_Home_Controller_415007C7-if00-port0
|
||||||
|
environment:
|
||||||
|
- TZ=America/Chicago
|
||||||
|
restart: always
|
||||||
|
network_mode: host
|
||||||
1
Home Assistant/hass/.HA_VERSION
Normal file
1
Home Assistant/hass/.HA_VERSION
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
2021.1.5
|
||||||
25
Home Assistant/hass/.storage/core.entity_registry
Normal file
25
Home Assistant/hass/.storage/core.entity_registry
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"key": "core.entity_registry",
|
||||||
|
"data": {
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"entity_id": "binary_sensor.updater",
|
||||||
|
"config_entry_id": null,
|
||||||
|
"device_id": null,
|
||||||
|
"area_id": null,
|
||||||
|
"unique_id": "updater",
|
||||||
|
"platform": "updater",
|
||||||
|
"name": null,
|
||||||
|
"icon": null,
|
||||||
|
"disabled_by": null,
|
||||||
|
"capabilities": null,
|
||||||
|
"supported_features": 0,
|
||||||
|
"device_class": null,
|
||||||
|
"unit_of_measurement": null,
|
||||||
|
"original_name": "Updater",
|
||||||
|
"original_icon": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Home Assistant/hass/.storage/core.uuid
Normal file
7
Home Assistant/hass/.storage/core.uuid
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"key": "core.uuid",
|
||||||
|
"data": {
|
||||||
|
"uuid": "6ae2a5eefe6741829c95a45064c93a0f"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Home Assistant/hass/.storage/http
Normal file
13
Home Assistant/hass/.storage/http
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"key": "http",
|
||||||
|
"data": {
|
||||||
|
"login_attempts_threshold": -1,
|
||||||
|
"server_port": 8123,
|
||||||
|
"cors_allowed_origins": [
|
||||||
|
"https://cast.home-assistant.io"
|
||||||
|
],
|
||||||
|
"ip_ban_enabled": true,
|
||||||
|
"ssl_profile": "modern"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Home Assistant/hass/automations.yaml
Normal file
1
Home Assistant/hass/automations.yaml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
blueprint:
|
||||||
|
name: Motion-activated Light
|
||||||
|
description: Turn on a light when motion is detected.
|
||||||
|
domain: automation
|
||||||
|
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml
|
||||||
|
input:
|
||||||
|
motion_entity:
|
||||||
|
name: Motion Sensor
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: binary_sensor
|
||||||
|
device_class: motion
|
||||||
|
light_target:
|
||||||
|
name: Light
|
||||||
|
selector:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: light
|
||||||
|
no_motion_wait:
|
||||||
|
name: Wait time
|
||||||
|
description: Time to leave the light on after last motion is detected.
|
||||||
|
default: 120
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 3600
|
||||||
|
unit_of_measurement: seconds
|
||||||
|
|
||||||
|
# If motion is detected within the delay,
|
||||||
|
# we restart the script.
|
||||||
|
mode: restart
|
||||||
|
max_exceeded: silent
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
platform: state
|
||||||
|
entity_id: !input motion_entity
|
||||||
|
from: "off"
|
||||||
|
to: "on"
|
||||||
|
|
||||||
|
action:
|
||||||
|
- service: light.turn_on
|
||||||
|
target: !input light_target
|
||||||
|
- wait_for_trigger:
|
||||||
|
platform: state
|
||||||
|
entity_id: !input motion_entity
|
||||||
|
from: "on"
|
||||||
|
to: "off"
|
||||||
|
- delay: !input no_motion_wait
|
||||||
|
- service: light.turn_off
|
||||||
|
target: !input light_target
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
blueprint:
|
||||||
|
name: Zone Notification
|
||||||
|
description: Send a notification to a device when a person leaves a specific zone.
|
||||||
|
domain: automation
|
||||||
|
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
|
||||||
|
input:
|
||||||
|
person_entity:
|
||||||
|
name: Person
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: person
|
||||||
|
zone_entity:
|
||||||
|
name: Zone
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: zone
|
||||||
|
notify_device:
|
||||||
|
name: Device to notify
|
||||||
|
description: Device needs to run the official Home Assistant app to receive notifications.
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: mobile_app
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
platform: state
|
||||||
|
entity_id: !input person_entity
|
||||||
|
|
||||||
|
variables:
|
||||||
|
zone_entity: !input zone_entity
|
||||||
|
# This is the state of the person when it's in this zone.
|
||||||
|
zone_state: "{{ states[zone_entity].name }}"
|
||||||
|
person_entity: !input person_entity
|
||||||
|
person_name: "{{ states[person_entity].name }}"
|
||||||
|
|
||||||
|
condition:
|
||||||
|
condition: template
|
||||||
|
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
|
||||||
|
|
||||||
|
action:
|
||||||
|
domain: mobile_app
|
||||||
|
type: notify
|
||||||
|
device_id: !input notify_device
|
||||||
|
message: "{{ person_name }} has left {{ zone_state }}"
|
||||||
49
Home Assistant/hass/configuration.yaml
Normal file
49
Home Assistant/hass/configuration.yaml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Configure a default setup of Home Assistant (frontend, api, etc)
|
||||||
|
default_config:
|
||||||
|
|
||||||
|
homeassistant:
|
||||||
|
name: Church
|
||||||
|
latitude: 34.719930
|
||||||
|
longitude: -86.704050
|
||||||
|
elevation: 470
|
||||||
|
unit_system: imperial
|
||||||
|
time_zone: "America/Chicago"
|
||||||
|
legacy_templates: false
|
||||||
|
|
||||||
|
logger:
|
||||||
|
default: info
|
||||||
|
|
||||||
|
# Text to speech
|
||||||
|
tts:
|
||||||
|
- platform: google_translate
|
||||||
|
|
||||||
|
group: !include groups.yaml
|
||||||
|
automation: !include automations.yaml
|
||||||
|
script: !include scripts.yaml
|
||||||
|
|
||||||
|
zha:
|
||||||
|
database_path: /config/zigbee.db
|
||||||
|
enable_quirks: true
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
discovery: true
|
||||||
|
broker: 127.0.0.1
|
||||||
|
port: 1883
|
||||||
|
username: !secret mqtt_username
|
||||||
|
password: !secret mqtt_password
|
||||||
|
birth_message:
|
||||||
|
topic: 'homeassistant/status'
|
||||||
|
payload: 'online'
|
||||||
|
will_message:
|
||||||
|
topic: 'homeassistant/status'
|
||||||
|
payload: 'offline'
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: mqtt
|
||||||
|
schema: json
|
||||||
|
name: lutron_qse_nwk
|
||||||
|
state_topic: "lutron/qse-nwk"
|
||||||
|
command_topic: "lutron/qse-nwk/set"
|
||||||
|
brightness: true
|
||||||
|
color_mode: true
|
||||||
|
supported_color_modes: ["brightness"]
|
||||||
BIN
Home Assistant/hass/core
Normal file
BIN
Home Assistant/hass/core
Normal file
Binary file not shown.
0
Home Assistant/hass/groups.yaml
Normal file
0
Home Assistant/hass/groups.yaml
Normal file
2
Home Assistant/hass/home-assistant.log
Normal file
2
Home Assistant/hass/home-assistant.log
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
2021-09-02 23:56:55 WARNING (MainThread) [homeassistant.components.http.ban] Login attempt or request with invalid authentication from 10.11.0.2 (10.11.0.2) (Mozilla/5.0 (X11; Linux x86_64; rv:90.0) Gecko/20100101 Firefox/90.0)
|
||||||
|
2021-09-02 23:56:55 WARNING (MainThread) [homeassistant.bootstrap] Support for the running Python version 3.7.3 is deprecated and will be removed in the first release after December 7, 2020. Please upgrade Python to 3.8.0 or higher.
|
||||||
BIN
Home Assistant/hass/home-assistant_v2.db
Normal file
BIN
Home Assistant/hass/home-assistant_v2.db
Normal file
Binary file not shown.
0
Home Assistant/hass/scenes.yaml
Normal file
0
Home Assistant/hass/scenes.yaml
Normal file
0
Home Assistant/hass/scripts.yaml
Normal file
0
Home Assistant/hass/scripts.yaml
Normal file
5
Home Assistant/hass/secrets.yaml
Normal file
5
Home Assistant/hass/secrets.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Use this file to store secrets like usernames and passwords.
|
||||||
|
# Learn more at https://home-assistant.io/docs/configuration/secrets/
|
||||||
|
mqtt_username: mqtt
|
||||||
|
mqtt_password: mqtt_password_placeholder
|
||||||
|
|
||||||
2
Home Assistant/mosquitto/aclfile
Normal file
2
Home Assistant/mosquitto/aclfile
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
user mqtt
|
||||||
|
topic readwrite #
|
||||||
6
Home Assistant/mosquitto/mosquitto.conf
Normal file
6
Home Assistant/mosquitto/mosquitto.conf
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
per_listener_settings true
|
||||||
|
allow_zero_length_clientid true
|
||||||
|
listener 1883 0.0.0.0
|
||||||
|
allow_anonymous false
|
||||||
|
password_file /mosquitto/config/pwfile
|
||||||
|
acl_file /mosquitto/config/aclfile
|
||||||
1
Home Assistant/mosquitto/pwfile
Normal file
1
Home Assistant/mosquitto/pwfile
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mqtt:mqtt_password_placeholder
|
||||||
45
README.md
45
README.md
|
|
@ -1,9 +1,23 @@
|
||||||
This project is designed to control the GRAFIK Eye QS Control panel via the QSE-CI-NWK-E with the serial interface. The project is designed to use the OLA (https://www.openlighting.org/) project to use either a DMX device to a network DMX protocol to control the 6 available zones.
|
This project is designed to control the GRAFIK Eye QS Control panel via the QSE-CI-NWK-E with the serial interface. The project is designed to use the OLA (https://www.openlighting.org/) project to use a DMX device to a network DMX protocol to control the 6 available zones. This project also supports MQTT messaging for Home Assistant support.
|
||||||
|
|
||||||
I designed this software for use on a Raspberry Pi using the 2019-07-10-raspbian-buster-lite release and OLA at https://github.com/OpenLightingProject/ola/tree/dc40569a7ef2512c7c9459a94c9dc4292d809262 compiled and installed using instructions at https://www.openlighting.org/ola/linuxinstall/
|
I designed this software for use on a Raspberry Pi using the 2019-07-10-raspbian-buster-lite release and OLA at https://github.com/OpenLightingProject/ola/tree/dc40569a7ef2512c7c9459a94c9dc4292d809262 compiled and installed using instructions at https://www.openlighting.org/ola/linuxinstall/
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
1. Decide if you want Home Assistant support, if you do not, you can skip to step 5.
|
||||||
|
2. If you do not already have an home assistant setup, you can view https://www.home-assistant.io/installation/ or use the base configurations in the `Home Assistant` directory to use my home assistant docker setup. The base configuration is designed for the HUSBZB-1 USB adapter, which you can use `ls -lah /dev/serial/by-id/` to see which ttyUSB interfaces are which, and get the correct serial number for your device.
|
||||||
|
3. If you don't have MQTT setup, you can follow guide at https://cyan-automation.medium.com/setting-up-mqtt-and-mosquitto-in-home-assistant-20eb810a91e6 or use my base configurations in the `Home Assistant` directory to configure mosquitto. Once you setup a username/password in the pwfile, use `mosquitto_passwd -U pwfile` to encrypt the password.
|
||||||
|
4. Edit the `lutron-dmx-control.py` file to make sure MQTT is enabled, pointed to the proper server, and has the correct password configured.
|
||||||
|
5. If you are not planning on using MQTT/Home Assistant, you can edit `lutron-dmx-control.py` to change `MQTT_ENABLED` from True to False.
|
||||||
|
6. Update the serial port for the QSE NWK in `lutron-dmx-control.py`, you can use `ls -lah /dev/serial/by-id/` to determine the device id.
|
||||||
|
7. Run the bash script install.sh to install services for olad and `lutron-dmx-control.py`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash ./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
Once OLA is installed, run it using `olad -l 3` and then edit the configuration files in `.ola/` to disable the modules which are not used as some of them will take the serial device. Once configured, run `olad -l 3` again and visit the raspberry pi's IP address at port 9090 in your browser to configure the DMX universe you are going to use. Once configured, you can then test this software by changing the configuration portion of the code.
|
Once services are installed we need to stop the olad service to edit configuration files with `sudo systemctl start olad@pi`. We can then configure ola by editing the configuration files in `.ola/` to disable the modules which are not used as some of them will take the serial device. Once configured, run `sudo systemctl start olad@pi` and visit the raspberry pi's IP address at port 9090 in your browser to configure the DMX universe you are going to use. Once configured, you can then test this software by changing the configuration portion of the code.
|
||||||
|
|
||||||
Trick to disable all modules except the one you are using.
|
Trick to disable all modules except the one you are using.
|
||||||
|
|
||||||
|
|
@ -12,23 +26,20 @@ sed -i '/enabled\s=/c\enabled = false' ~/.ola/*.conf
|
||||||
sed -i '/enabled\s=/c\enabled = true' ~/.ola/ola-e131.conf
|
sed -i '/enabled\s=/c\enabled = true' ~/.ola/ola-e131.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
# Installation
|
# Home Assistant MQTT config
|
||||||
|
|
||||||
Install Python/needed modules.
|
If you have your own Home Assistant install, the configuration for this project is below.
|
||||||
|
|
||||||
```bash
|
```yaml
|
||||||
apt install python3-pip python3-serial
|
light:
|
||||||
pip3 install ola
|
- platform: mqtt
|
||||||
```
|
schema: json
|
||||||
|
name: lutron_qse_nwk
|
||||||
Copy lutron-dmx-control@.service and olad@.service to /etc/systemd/system/ and run the following to enable/start.
|
state_topic: "lutron/qse-nwk"
|
||||||
|
command_topic: "lutron/qse-nwk/set"
|
||||||
```bash
|
brightness: true
|
||||||
systemctl daemon-reload
|
color_mode: true
|
||||||
systemctl enable olad@pi
|
supported_color_modes: ["brightness"]
|
||||||
systemctl start olad@pi
|
|
||||||
systemctl enable lutron-dmx-control@pi
|
|
||||||
systemctl start lutron-dmx-control@pi
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Recommend
|
# Recommend
|
||||||
|
|
|
||||||
29
install.sh
Normal file
29
install.sh
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
USER=$(whoami)
|
||||||
|
if [ "$USER" != "root" ]; then
|
||||||
|
echo "Please use sudo with this install script to ensure right permissions for installation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the script directory.
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
|
||||||
|
# Install Python/needed modules.
|
||||||
|
apt install -y python3-pip python3-serial python3
|
||||||
|
pip3 install ola
|
||||||
|
pip3 install paho-mqtt
|
||||||
|
|
||||||
|
cp lutron-dmx-control.py /home/pi/lutron-dmx-control.py
|
||||||
|
chown pi: /home/pi/lutron-dmx-control.py
|
||||||
|
|
||||||
|
# Copy lutron-dmx-control@.service and olad@.service to /etc/systemd/system/ and run the following to enable/start.
|
||||||
|
cp olad@.service /etc/systemd/system/
|
||||||
|
cp lutron-dmx-control@.service /etc/systemd/system/
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable olad@pi
|
||||||
|
systemctl start olad@pi
|
||||||
|
systemctl enable lutron-dmx-control@pi
|
||||||
|
systemctl start lutron-dmx-control@pi
|
||||||
|
|
@ -28,7 +28,10 @@ import io
|
||||||
import _thread
|
import _thread
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
|
import json
|
||||||
|
|
||||||
|
from paho.mqtt import client as mqtt_client
|
||||||
# Documentation
|
# Documentation
|
||||||
# This program is designed to use the Open Lighting Arcretechture (OLA) to receive a DMX signal
|
# This program is designed to use the Open Lighting Arcretechture (OLA) to receive a DMX signal
|
||||||
# and translate to commands to control the 6 dimiable zones on the Lutron GRAFIK Eye QS Control panel
|
# and translate to commands to control the 6 dimiable zones on the Lutron GRAFIK Eye QS Control panel
|
||||||
|
|
@ -36,9 +39,11 @@ import time
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
# Serial port device to use to communicate with Lutron's QSE NWK.
|
# Serial port device to use to communicate with Lutron's QSE NWK.
|
||||||
QSE_NWK_DEVICE = "/dev/ttyUSB0"
|
QSE_NWK_DEVICE = "/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0"
|
||||||
# Set baud rate on Lutron's QSE NWK.
|
# Set baud rate on Lutron's QSE NWK.
|
||||||
QSE_NWK_BAUD = 115200
|
QSE_NWK_BAUD = 115200
|
||||||
|
# Number of zones on GRAFIK Eye QS Control panel
|
||||||
|
QSE_ZONES = 6
|
||||||
# DMX Universe in OLA that is used.
|
# DMX Universe in OLA that is used.
|
||||||
DMX_UNIVERSE = 3
|
DMX_UNIVERSE = 3
|
||||||
# The starting address.
|
# The starting address.
|
||||||
|
|
@ -49,25 +54,45 @@ VERBOSE=1
|
||||||
|
|
||||||
# Variables used at run time, do not adjust.
|
# Variables used at run time, do not adjust.
|
||||||
serialSession = None
|
serialSession = None
|
||||||
currentValues = [0,0,0,0,0,0]
|
zoneValues = []
|
||||||
|
sentValues = []
|
||||||
sendAllDataThisTime = True
|
sendAllDataThisTime = True
|
||||||
controlDisabled = False
|
controlDisabled = False
|
||||||
|
lastDMXUniverseUpdate = 0
|
||||||
|
|
||||||
# To prevent data from overlapping which is known to crash the QSE NWK, we implement a thread lock which must be released before being obtained.
|
# To prevent data from overlapping which is known to crash the QSE NWK, we implement a thread lock which must be released before being obtained.
|
||||||
dataLock = threading.Lock()
|
dataLock = threading.Lock()
|
||||||
|
|
||||||
|
# To prevent data from overlapping which is known to crash the QSE NWK, we implement a thread lock which must be released before being obtained.
|
||||||
|
sendAllDataThisTImeLock = threading.Lock()
|
||||||
|
|
||||||
|
# MQTT Configurations
|
||||||
|
MQTT_ENABLED = True
|
||||||
|
MQTT_BROKER = '127.0.0.1'
|
||||||
|
MQTT_PORT = 1883
|
||||||
|
MQTT_TOPIC = "lutron/qse-nwk"
|
||||||
|
MQTT_TOPIC_SET = MQTT_TOPIC + "/set"
|
||||||
|
MQTT_CLIENT_ID = f'lutron-qse-nwk-{random.randint(0, 1000)}'
|
||||||
|
MQTT_USERNAME = 'mqtt'
|
||||||
|
MQTT_PASSWORD = 'mqtt_password_placeholder'
|
||||||
|
|
||||||
|
# MQTT light state
|
||||||
|
mqttLightState = "OFF"
|
||||||
|
mqttLightBrightness = 0
|
||||||
|
mqttSentLightState = ""
|
||||||
|
mqttSentLightBrightness = 0
|
||||||
|
|
||||||
|
# MQTT state values
|
||||||
|
MQTT_LIGHT_ON = "ON"
|
||||||
|
MQTT_LIGHT_OFF = "OFF"
|
||||||
|
|
||||||
|
# MQTT Connection
|
||||||
|
mqtt_conn = None
|
||||||
|
|
||||||
# This fucnction translates the 0-255 signal from DMX to 0.00 to 100.00 signal used by Lutron,
|
# This fucnction translates the 0-255 signal from DMX to 0.00 to 100.00 signal used by Lutron,
|
||||||
# and it sends the appropiate command to the QSE NWK to change the brightness level of a zone.
|
# and it sends the appropiate command to the QSE NWK to change the brightness level of a zone.
|
||||||
def SetZone(zone, value):
|
def qse_send_zone_value(zone, value):
|
||||||
global serialSession, currentValues, sendAllDataThisTime, controlDisabled
|
global serialSession, VERBOSE
|
||||||
# We only want to translate a level of it has not already been sent to the zone,
|
|
||||||
# or if we want to send all data this time. However we do not want to send the level
|
|
||||||
# if the controls has been disabled by the designated button on the control panel.
|
|
||||||
if (currentValues[zone-1]==value and not sendAllDataThisTime) or controlDisabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update the array of current values.
|
|
||||||
currentValues[zone-1] = value
|
|
||||||
|
|
||||||
# Translate to the command.
|
# Translate to the command.
|
||||||
command = "#DEVICE,1,%d,14,%.2f,00:00" % (zone,round((value/255.00)*100,2))
|
command = "#DEVICE,1,%d,14,%.2f,00:00" % (zone,round((value/255.00)*100,2))
|
||||||
|
|
@ -77,30 +102,73 @@ def SetZone(zone, value):
|
||||||
# Send to the QSE NWK.
|
# Send to the QSE NWK.
|
||||||
serialSession.write(bytes(command+"\n\r", 'utf-8'))
|
serialSession.write(bytes(command+"\n\r", 'utf-8'))
|
||||||
|
|
||||||
def NewData(data):
|
# This function receives data when a DMX update occurs.
|
||||||
global sendAllDataThisTime, dataLock
|
def dmx_universe_update(data):
|
||||||
|
global dataLock, zoneValues, lastDMXUniverseUpdate, QSE_ZONES, VERBOSE
|
||||||
# Acquire the lock for the thread to prevent data from overlapping.
|
# Acquire the lock for the thread to prevent data from overlapping.
|
||||||
dataLock.acquire()
|
dataLock.acquire()
|
||||||
if VERBOSE>=2:
|
if VERBOSE>=2:
|
||||||
print(data)
|
print(data)
|
||||||
|
|
||||||
# Send the new levels to each zone via the QSE NWK.
|
# Write the new levels to each zone.
|
||||||
SetZone(1,data[DMX_START_ADDRESS+0])
|
for zone in range(QSE_ZONES):
|
||||||
SetZone(2,data[DMX_START_ADDRESS+1])
|
zoneValues[zone] = data[DMX_START_ADDRESS+zone]
|
||||||
SetZone(3,data[DMX_START_ADDRESS+2])
|
|
||||||
SetZone(4,data[DMX_START_ADDRESS+3])
|
|
||||||
SetZone(5,data[DMX_START_ADDRESS+4])
|
|
||||||
SetZone(6,data[DMX_START_ADDRESS+5])
|
|
||||||
|
|
||||||
# Reset the flag of send all data to false as we would have sent all data this time.
|
# Keep up to date with the last update to determine rather or not to unlock mqtt support.
|
||||||
sendAllDataThisTime = False
|
lastDMXUniverseUpdate = time.time()
|
||||||
|
|
||||||
# Allow the next command call to follow through by releasing the lock.
|
# Allow the next command call to follow through by releasing the lock.
|
||||||
dataLock.release()
|
dataLock.release()
|
||||||
|
|
||||||
|
# This function is a thread that writes any changes to the QSE controller.
|
||||||
|
def qse_write_zone_values():
|
||||||
|
global sendAllDataThisTImeLock, dataLock, sendAllDataThisTime, zoneValues, sentValues, QSE_ZONES
|
||||||
|
while True:
|
||||||
|
|
||||||
|
# If control is disabled, we won't check this time.
|
||||||
|
if controlDisabled:
|
||||||
|
# Prevent CPU overload and wait half a second before continuing.
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
# Acquire lock to prevent conflict between threads.
|
||||||
|
dataLock.acquire()
|
||||||
|
|
||||||
|
# Copy zone values locally to allow changes by other threads while we send.
|
||||||
|
thisZoneValues = zoneValues.copy()
|
||||||
|
|
||||||
|
# Allow the next command call to follow through by releasing the lock.
|
||||||
|
dataLock.release()
|
||||||
|
|
||||||
|
# Acquire send all data this time lock.
|
||||||
|
sendAllDataThisTImeLock.acquire()
|
||||||
|
|
||||||
|
# Check for changes in zones values and send it.
|
||||||
|
for zone in range(QSE_ZONES):
|
||||||
|
# If zone value is the same and we're not sending all zone data this time, skip sending.
|
||||||
|
if thisZoneValues[zone]==sentValues[zone] and not sendAllDataThisTime:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update the array of sent values.
|
||||||
|
sentValues[zone] = thisZoneValues[zone]
|
||||||
|
|
||||||
|
# Send value via QSE NWK
|
||||||
|
qse_send_zone_value(zone+1, thisZoneValues[zone])
|
||||||
|
|
||||||
|
# Reset the flag of send all data to false as we would have sent all data this time.
|
||||||
|
sendAllDataThisTime = False
|
||||||
|
|
||||||
|
# Release the lock.
|
||||||
|
sendAllDataThisTImeLock.release()
|
||||||
|
|
||||||
|
# Lower CPU usage.
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
# This function reads the serial data from the QSE NWK line by line and performs a few functions based on response.
|
# This function reads the serial data from the QSE NWK line by line and performs a few functions based on response.
|
||||||
def QSE_Read():
|
def qse_read():
|
||||||
global serialSession, controlDisabled
|
global serialSession, controlDisabled, sendAllDataThisTime, mqttLightBrightness, mqttLightState, VERBOSE
|
||||||
# Creates a bufferred reader for the serial input.
|
# Creates a bufferred reader for the serial input.
|
||||||
sio = io.TextIOWrapper(io.BufferedReader(serialSession))
|
sio = io.TextIOWrapper(io.BufferedReader(serialSession))
|
||||||
|
|
||||||
|
|
@ -122,30 +190,172 @@ def QSE_Read():
|
||||||
serialSession.write(bytes("#RESET,0\n\r", 'utf-8'))
|
serialSession.write(bytes("#RESET,0\n\r", 'utf-8'))
|
||||||
|
|
||||||
# If the all zone up button is pressed, we disable control from the program to allow someone to manually control zones.
|
# If the all zone up button is pressed, we disable control from the program to allow someone to manually control zones.
|
||||||
if line=="~DEVICE,1,74,3":
|
elif line=="~DEVICE,1,74,3":
|
||||||
if VERBOSE>=2:
|
if VERBOSE>=2:
|
||||||
print("Received disable signal.")
|
print("Received disable signal.")
|
||||||
controlDisabled = True
|
controlDisabled = True
|
||||||
|
|
||||||
# If the all zone down button is pressed, we re-enable the programs control of the zones.
|
# If the all zone down button is pressed, we re-enable the programs control of the zones.
|
||||||
if line=="~DEVICE,1,75,3":
|
elif line=="~DEVICE,1,75,3":
|
||||||
if VERBOSE>=2:
|
if VERBOSE>=2:
|
||||||
print("Received enable signal.")
|
print("Received enable signal.")
|
||||||
controlDisabled = False
|
controlDisabled = False
|
||||||
sendAllDataThisTime = True
|
sendAllDataThisTime = True
|
||||||
|
|
||||||
|
# If none of the above, and is a device notification, we parse.
|
||||||
|
elif line.startswith("~DEVICE,1"):
|
||||||
|
data = line.split(",")
|
||||||
|
# If brightness notice and zone 1, let's update MQTT.
|
||||||
|
if data[3]=="14" and data[2]=="1":
|
||||||
|
# Acquire lock to prevent conflict between threads.
|
||||||
|
dataLock.acquire()
|
||||||
|
|
||||||
|
# Convert brightness to MQTT light state.
|
||||||
|
mqttLightBrightness = round((float(data[4])/100.00)*255,0)
|
||||||
|
if mqttLightBrightness==0:
|
||||||
|
# If control was disabled, and brightness is now 0, disable the control disablement.
|
||||||
|
if controlDisabled:
|
||||||
|
controlDisabled = False
|
||||||
|
mqttLightState = MQTT_LIGHT_OFF
|
||||||
|
else:
|
||||||
|
mqttLightState = MQTT_LIGHT_ON
|
||||||
|
|
||||||
|
# Publish current state to MQTT.
|
||||||
|
mqtt_publish_state()
|
||||||
|
|
||||||
|
# Allow the next command call to follow through by releasing the lock.
|
||||||
|
dataLock.release()
|
||||||
|
|
||||||
if VERBOSE>=1:
|
if VERBOSE>=1:
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
# Reset the send all data flag every 10 seconds to ensure all zones have the correct value set.
|
# Reset the send all data flag every 10 seconds to ensure all zones have the correct value set.
|
||||||
def sendAllDataReset():
|
def qse_reset_sendAllDataThisTime():
|
||||||
global sendAllDataThisTime
|
global sendAllDataThisTImeLock, sendAllDataThisTime, VERBOSE
|
||||||
while True:
|
while True:
|
||||||
|
# Wait 10 seconds before running.
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
|
# Acquire send all data this time lock.
|
||||||
|
sendAllDataThisTImeLock.acquire()
|
||||||
|
|
||||||
|
# Reset
|
||||||
if VERBOSE>=3:
|
if VERBOSE>=3:
|
||||||
print("Resetting flag to send all data")
|
print("Resetting flag to send all data")
|
||||||
sendAllDataThisTime = True
|
sendAllDataThisTime = True
|
||||||
|
|
||||||
|
# Release the lock.
|
||||||
|
sendAllDataThisTImeLock.release()
|
||||||
|
|
||||||
|
# Sends the current MQTT light state to MQTT.
|
||||||
|
def mqtt_publish_state():
|
||||||
|
global mqtt_conn, mqttLightState, mqttLightBrightness, mqttSentLightState, mqttSentLightBrightness, VERBOSE
|
||||||
|
# If we already sent this message, no duplicates.
|
||||||
|
if mqttLightState==mqttSentLightState and mqttLightBrightness==mqttSentLightBrightness:
|
||||||
|
return
|
||||||
|
mqttSentLightState = mqttLightState
|
||||||
|
mqttSentLightBrightness = mqttLightBrightness
|
||||||
|
|
||||||
|
# Generate json format of current state.
|
||||||
|
msg = json.dumps({"brightness": mqttLightBrightness,"state": mqttLightState})
|
||||||
|
# Send message.
|
||||||
|
result = mqtt_conn.publish(MQTT_TOPIC, msg)
|
||||||
|
# result: [0, 1]
|
||||||
|
status = result[0]
|
||||||
|
if status == 0 and VERBOSE>=2:
|
||||||
|
print(f"Send `{msg}` to topic `{MQTT_TOPIC}`")
|
||||||
|
if status != 0:
|
||||||
|
print(f"Failed to send message to topic {MQTT_TOPIC}")
|
||||||
|
|
||||||
|
# Receives MQTT messages from subscribed topics.
|
||||||
|
def mqtt_on_message(client, userdata, msg):
|
||||||
|
global mqttLightState, mqttLightBrightness, mqttSentLightState, mqttSentLightBrightness, lastDMXUniverseUpdate, VERBOSE
|
||||||
|
# If message received is to the JSON set topic, update light state.
|
||||||
|
if msg.topic==MQTT_TOPIC_SET:
|
||||||
|
# Decode JSON from the message.
|
||||||
|
decoded_message=str(msg.payload.decode("utf-8"))
|
||||||
|
data = json.loads(decoded_message)
|
||||||
|
if VERBOSE>=2:
|
||||||
|
print(f"Received `{data}` from `{msg.topic}` topic")
|
||||||
|
|
||||||
|
# Acquire lock to prevent conflict between threads.
|
||||||
|
dataLock.acquire()
|
||||||
|
|
||||||
|
# Check message for brightness and state values/update accordingly.
|
||||||
|
if "brightness" in data:
|
||||||
|
mqttLightBrightness = data["brightness"]
|
||||||
|
if "state" in data:
|
||||||
|
if mqttLightState!=data["state"]:
|
||||||
|
mqttLightState = data["state"]
|
||||||
|
# If light state is on, but brightness value is off, set brightness to 50%.
|
||||||
|
if mqttLightState==MQTT_LIGHT_ON and mqttLightBrightness==0:
|
||||||
|
mqttLightBrightness = 127
|
||||||
|
|
||||||
|
# Check to see if it has been more than 5 seconds since the last DMX universe update, if it has been, we're allowed to control the lights.
|
||||||
|
durationSinceLastDMXUniverseUpdate = time.time() - lastDMXUniverseUpdate
|
||||||
|
if durationSinceLastDMXUniverseUpdate>5:
|
||||||
|
# If state is on, set brightness levels to all zones.
|
||||||
|
if mqttLightState==MQTT_LIGHT_ON:
|
||||||
|
for zone in range(QSE_ZONES):
|
||||||
|
zoneValues[zone] = mqttLightBrightness
|
||||||
|
else: # If state is off, set brightness level of 0.
|
||||||
|
for zone in range(QSE_ZONES):
|
||||||
|
zoneValues[zone] = 0
|
||||||
|
else:
|
||||||
|
# If locked due to DMX control, force values to first zone value.
|
||||||
|
mqttLightBrightness = zoneValues[0]
|
||||||
|
if mqttLightBrightness==0:
|
||||||
|
mqttLightState = MQTT_LIGHT_OFF
|
||||||
|
else:
|
||||||
|
mqttLightState = MQTT_LIGHT_ON
|
||||||
|
|
||||||
|
mqttSentLightState = ""
|
||||||
|
mqttSentLightBrightness = 0
|
||||||
|
|
||||||
|
# Publish current state to MQTT.
|
||||||
|
mqtt_publish_state()
|
||||||
|
|
||||||
|
# Allow the next command call to follow through by releasing the lock.
|
||||||
|
dataLock.release()
|
||||||
|
elif msg.topic!=MQTT_TOPIC:
|
||||||
|
print(f"Received unknown message `{msg.payload.decode()}` from `{msg.topic}` topic")
|
||||||
|
|
||||||
|
# Subscribes to the MQTT topic for this light.
|
||||||
|
def mqtt_subscribe():
|
||||||
|
global mqtt_conn
|
||||||
|
mqtt_conn.subscribe(MQTT_TOPIC+"/#")
|
||||||
|
|
||||||
|
# When the MQTT broker is connected, this function is called.
|
||||||
|
def mqtt_on_connect(client, userdata, flags, rc):
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected to MQTT Broker!")
|
||||||
|
# New connection means we must publish state and subscribe to our topic.
|
||||||
|
mqtt_subscribe()
|
||||||
|
mqtt_publish_state()
|
||||||
|
else:
|
||||||
|
print("Failed to connect, return code %d\n", rc)
|
||||||
|
|
||||||
|
# The MQTT thread for connection to the MQTT broker.
|
||||||
|
def mqtt_connect():
|
||||||
|
global mqtt_conn
|
||||||
|
try:
|
||||||
|
mqtt_conn = mqtt_client.Client(MQTT_CLIENT_ID)
|
||||||
|
if MQTT_USERNAME!="":
|
||||||
|
mqtt_conn.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||||
|
mqtt_conn.on_connect = mqtt_on_connect
|
||||||
|
mqtt_conn.on_message = mqtt_on_message
|
||||||
|
mqtt_conn.connect(MQTT_BROKER, MQTT_PORT)
|
||||||
|
mqtt_conn.loop_forever()
|
||||||
|
except:
|
||||||
|
print("MQTT Connection Failed, trying again in 10 seconds.\n")
|
||||||
|
time.sleep(10.0)
|
||||||
|
mqtt_connect()
|
||||||
|
|
||||||
|
# Build array with 0% in each zone.
|
||||||
|
for zone in range(QSE_ZONES):
|
||||||
|
zoneValues.append(0)
|
||||||
|
sentValues.append(0)
|
||||||
|
|
||||||
# Connect to the QSE NWK by using the serial port.
|
# Connect to the QSE NWK by using the serial port.
|
||||||
print("Connecting to QSE NWK at: "+QSE_NWK_DEVICE)
|
print("Connecting to QSE NWK at: "+QSE_NWK_DEVICE)
|
||||||
with serial.Serial(QSE_NWK_DEVICE, QSE_NWK_BAUD, timeout=2) as ser:
|
with serial.Serial(QSE_NWK_DEVICE, QSE_NWK_BAUD, timeout=2) as ser:
|
||||||
|
|
@ -160,13 +370,20 @@ if serialSession == None:
|
||||||
serialSession.open()
|
serialSession.open()
|
||||||
|
|
||||||
# Now that we are ready to roll, we start the read thread.
|
# Now that we are ready to roll, we start the read thread.
|
||||||
_thread.start_new_thread(QSE_Read, ())
|
_thread.start_new_thread(qse_read, ())
|
||||||
|
|
||||||
|
# Start the write thread.
|
||||||
|
_thread.start_new_thread(qse_write_zone_values, ())
|
||||||
|
|
||||||
# Start the reset send all data thread.
|
# Start the reset send all data thread.
|
||||||
_thread.start_new_thread(sendAllDataReset, ())
|
_thread.start_new_thread(qse_reset_sendAllDataThisTime, ())
|
||||||
|
|
||||||
|
# Start the MQTT light thread.
|
||||||
|
if MQTT_ENABLED:
|
||||||
|
_thread.start_new_thread(mqtt_connect, ())
|
||||||
|
|
||||||
# Connect to the DMX universe with the OLA wrapper.
|
# Connect to the DMX universe with the OLA wrapper.
|
||||||
wrapper = ClientWrapper()
|
wrapper = ClientWrapper()
|
||||||
client = wrapper.Client()
|
client = wrapper.Client()
|
||||||
client.RegisterUniverse(DMX_UNIVERSE, client.REGISTER, NewData)
|
client.RegisterUniverse(DMX_UNIVERSE, client.REGISTER, dmx_universe_update)
|
||||||
wrapper.Run()
|
wrapper.Run()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue