Quellcode durchsuchen

Add Home Assistant support (MQTT)

Includes total refactor of MQTT stack
master
Ursprung
Commit
1e57d31a32
1 geänderte Dateien mit 187 neuen und 79 gelöschten Zeilen
  1. +187
    -79
      blinky.py

+ 187
- 79
blinky.py Datei anzeigen

@@ -8,22 +8,17 @@ import os
import sys
import time
from pathlib import Path
import uuid
from threading import Lock, Condition

import pyinotify, threading
import paho.mqtt.subscribe as subscribe
import paho.mqtt.client as mqtt

from pattern import Pattern
from gradients import Gradients


lock_reload = threading.Lock()
cond_reload = threading.Condition(lock_reload)
reload = False
ignore_file = False
config = {}
new_config = None


# Monitor config from file
### Parameters ###

if len(sys.argv) > 1:
config_file = sys.argv[1]
@@ -33,19 +28,145 @@ else:
if not Path(config_file).is_file():
print("WARNING: Config file does not exist:", config_file)

def file_thread(file):

# Monitor config from MQTT

class Device:
id = "blinky" #f"{uuid.getnode():0>12x}"

state_topic = f"light/{id}/state"
command_topic = f"light/{id}/cmd"
fx_state_topic = f"light/{id}/fx"

ready = False
changed = False
should_publish = False

power_on = True
effect = None

def on_mqtt_connect(client, device, flags, rc):
if rc == 0:
with device.lock:
print("Connected to MQTT")
client.publish(
f"homeassistant/light/{device.id}/config",
json.dumps({
'unique_id': device.id,
'device': {
'name': "blinky",
'identifiers': [device.id],
},

'state_topic': device.state_topic,
'command_topic': device.command_topic,
'effect_state_topic': device.fx_state_topic,

'schema': "json",

'effect': True,
'effect_list': device.effects,
})
)
client.subscribe(device.command_topic)
device.ready = True
if device.should_publish:
device.publish_state_locked()
device.cond.notify_all()
else:
print(f"MQTT connection failed: {rc}")

def set_mqtt_config(self, data):
with self.lock:
changed = False
if 'state' in data:
device.power_on = data['state'] == "ON"
changed = True
if 'effect' in data:
device.effect = data['effect']
changed = True
if changed:
self.changed = True
self.update_state_locked()

def on_mqtt_message(client, device, msg):
if msg.topic == device.command_topic:
device.set_mqtt_config(json.loads(msg.payload))

def __init__(self, effects=[]):
print(f"Creating device with ID {self.id}")

self.effects = effects

self.lock = Lock()
self.cond = Condition(self.lock)

self.client = mqtt.Client(userdata=self)
self.client.on_connect = Device.on_mqtt_connect
self.client.on_message = Device.on_mqtt_message
self.client.tls_set("/etc/ssl/certs/ca-certificates.crt")
self.client.username_pw_set("blinky", "rainbow")
self.client.connect_async("mqtt.jrhoffa.com", 8883)
self.client.loop_start();

def is_ready(self): return self.ready
def is_changed(self): return self.changed

def wait_ready(self, timeout=None):
with self.lock: self.cond.wait_for(self.is_ready, timeout=timeout)
def wait_changed(self, timeout=None):
with self.lock: self.cond.wait_for(self.is_changed, timeout=timeout)

def publish_state_locked(self):
self.should_publish = not self.ready
if self.ready:
self.client.publish(
device.state_topic,
json.dumps({
'state': "ON" if self.power_on else "OFF",
'effect': self.effect
})
)

def update_state_locked(self):
self.changed = True
self.publish_state_locked()
self.cond.notify_all()

def set_power_state(self, power_on):
with self.lock:
self.power_on = power_on
self.update_state_locked()

def set_effect(self, effect):
with self.lock:
self.effect = effect
self.update_state_locked()

device = Device([gradient for gradient in dir(Gradients) if not gradient.startswith('__')])


# Monitor config from file

file_changed = True
ignore_file = False

config = {}
new_config = None

def file_thread(file, device):
class EventHandler(pyinotify.ProcessEvent):
def process_IN_CLOSE_WRITE(self, event):
global reload, new_config, ignore_file
with lock_reload:
global file_changed, new_config, ignore_file
with device.lock:
if ignore_file:
print("Ignoring file change")
ignore_file = False
else:
print("Triggering file reload")
new_config = None
reload = True
cond_reload.notify_all()
file_changed = True
device.cond.notify_all()

wm = pyinotify.WatchManager()
notifier = pyinotify.Notifier(wm, EventHandler())
@@ -53,101 +174,81 @@ def file_thread(file):

notifier.loop()

threading.Thread(target=file_thread, args=[config_file]).start()


# Monitor config from MQTT

class mqtt:
topic = "blinky/config"
hostname = "mqtt.jrhoffa.com"
auth = {'username':"blinky", 'password':"rainbow"}
tls = {'ca_certs':"/etc/ssl/certs/ca-certificates.crt"}

def mqtt_thread():
def on_message(client, userdata, message):
global reload, new_config
with lock_reload:
print("Triggering MQTT reload")
try:
new_config = json.loads(message.payload)
reload = True
cond_reload.notify_all()
except:
print("Invalid MQTT config")

global reload, new_config
while True:
print(f"Subscribing to MQTT topic {mqtt.topic}")
try:
message = subscribe.simple(mqtt.topic, hostname=mqtt.hostname, auth=mqtt.auth, port=8883, tls=mqtt.tls)
except Exception as e:
print("MQTT subscription error:", e)
message = None
if not message:
print("Subscription failed - retrying")
time.sleep(1)
else:
with lock_reload:
print("Triggering MQTT reload")
try:
new_config = json.loads(message.payload)
reload = True
cond_reload.notify_all()
except:
print("Invalid MQTT config")


threading.Thread(target=mqtt_thread).start()
threading.Thread(target=file_thread, args=[config_file, device]).start()


### Entry Point ##

framerate = 30

power_on = True
length = None
patterns = None
pixels = None

with lock_reload:
with device.lock:
while True:
print("Loading config")
reload = False

try:
# If it wasn't delivered, load from file
new_config = new_config or json.load(open(config_file, 'r'))
if file_changed:
print("Loading config file")
new_config = json.load(open(config_file, 'r'))
file_changed = False
elif device.changed:
print("Using new config from MQTT")
new_config = {'power_on': device.power_on}
if device.effect:
new_config['patterns'] = [{
'gradient': {'preset': device.effect},
'cycle_length': 20
}]
device.changed = False

if 'length' in new_config:
new_length = new_config['length']
if length != new_length:
config['length'] = new_length
length = new_length
config['length'] = length
pixels = neopixel.NeoPixel(board.D18, length, pixel_order=neopixel.RGB, auto_write=False)
#pixels = TermLEDs(length)
print("String configured")

patterns = [(pattern.get('length', length), Pattern.from_dict(pattern)) for pattern in new_config['patterns']]
config['patterns'] = new_config['patterns']
print("Pattern configured")
if 'power_on' in new_config:
config['power_on'] = new_config['power_on']
power_on = config['power_on']

if 'patterns' in new_config:
config['patterns'] = new_config['patterns']

# Don't regenerate the patterns if this is just a power command
if 'length' in new_config or 'patterns' in new_config:
patterns = [(pattern.get('length', length), Pattern.from_dict(pattern)) for pattern in config['patterns']]
if file_changed and len(patterns) > 0 and 'gradient' in config['patterns'][0]:
device.set_effect(config['patterns'][0]['gradient'].get('preset', None))
print("Pattern configured")

except Exception as e:
print("Failed to load config:", e)
print("Failed to set config:", e)

finally:
print("Saving config")
ignore_file = True
json.dump(config, open(config_file, 'w'))

if not length or not pixels or not patterns:
print("Waiting for valid config")
while not reload:
cond_reload.wait()
if not device.power_on or not length or not pixels or not patterns:
if not device.power_on:
pixels[:] = [0, 0, 0] * length
pixels.show()
print("Power off, waiting ...")
else:
print("Waiting for valid config")
while not file_changed and not device.is_changed():
device.cond.wait()

else:
print("Starting animation")
last = target = time.time()
while not reload:
lock_reload.release()
while not file_changed and not device.is_changed():
device.lock.release()

# Generate and show new light pattern
pixels[:] = [x for y in (pattern.step(length) for (length, pattern) in patterns) for x in y]
@@ -157,7 +258,14 @@ with lock_reload:
target += (1 / framerate)
now = time.time()
sleeptime = target - now

# Release before sleep because we're using the condition variable
# to wake up early in case there's an important message
#device.lock.acquire()

if sleeptime > 0:
time.sleep(sleeptime)
#while not file_changed and not device.is_changed():
# device.cond.wait(sleeptime)

lock_reload.acquire()
device.lock.acquire()

Laden…
Abbrechen
Speichern