From 5d7f48d81372302dd40dbbc623f6f8ba80233e9e Mon Sep 17 00:00:00 2001 From: Franziska Kunsmann Date: Sat, 17 Aug 2024 12:19:01 +0200 Subject: [PATCH] update to support multiple universes per worker --- conf.py | 54 +++++++++-------- config.example.toml | 38 ++++-------- dmx_queue.py | 99 ++++++++++++++++++-------------- lights/__init__.py | 7 +++ lights/common.py | 10 ++-- lights/pulsar_chromaflood_200.py | 11 +++- lights/sheds_30w_cob_rgb.py | 6 +- lights/stairville_par_56.py | 4 +- lights/tsss_led_par_rgbw.py | 6 +- main.py | 71 +++++++---------------- mqtt_queue.py | 39 +++++-------- 11 files changed, 156 insertions(+), 189 deletions(-) diff --git a/conf.py b/conf.py index 83f722c..f9e8e73 100644 --- a/conf.py +++ b/conf.py @@ -7,7 +7,7 @@ except ImportError: import logging from sys import exit -LOG = logging.getLogger('Config') +LOG = logging.getLogger("Config") class ConfigWrapper: @@ -19,45 +19,43 @@ class ConfigWrapper: def load_and_validate_config(path): try: - with open(path, 'r') as cf: + with open(path, "r") as cf: config = toml_load(cf.read()) except Exception as e: - LOG.exception(f'{path} is no valid toml configuration file') + LOG.exception(f"{path} is no valid toml configuration file") exit(1) - if not config.get('mqtt', {}).get('host'): + if not config.get("mqtt", {}).get("host"): LOG.error( f'configuration option "mqtt" "host" is missing in config, but required to exist' ) exit(1) + universes = {} + used_universes = {} + for universe, uconfig in config.get("universes", {}).items(): + universes[universe] = ConfigWrapper( + alert_brightness=max(int(uconfig.get("alert_brightness", 255)), 10), + filters=uconfig.get("filters", []), + lights=uconfig.get("lights", {}), + multicast=bool(uconfig.get("multicast", False) is True), + rainbow_brightness=int(uconfig.get("rainbow_brightness", 150)), + target=uconfig.get("target", "127.0.0.1"), + universe=int(uconfig.get("universe", 1)), + ) + if universes[universe].universe in used_universes: + LOG.warning( + f"universe {universes[universe].universe} used by both {universe} and {used_universes[universes[universe]].universe}" + ) + conf = ConfigWrapper( mqtt=ConfigWrapper( - host=config['mqtt']['host'], - user=config['mqtt'].get('user'), - password=config['mqtt'].get('password'), - topic=config['mqtt'].get('topic', '/voc/alert'), + host=config["mqtt"]["host"], + user=config["mqtt"].get("user"), + password=config["mqtt"].get("password"), + topic=config["mqtt"].get("topic", "/voc/alert"), ), - sacn=ConfigWrapper( - multicast=bool(config.get('sacn', {}).get('multicast', False) is True), - target=config.get('sacn', {}).get('target', '127.0.0.1'), - universe=int(config.get('sacn', {}).get('universe', 1)), - ), - alerts=ConfigWrapper( - brightness=max(int(config.get('alerts', {}).get('brightness', 255)), 10), - filters=sorted(config.get('alerts', {}).get('filters', set())), - ), - rainbow=ConfigWrapper( - enable=bool(config.get('rainbow', {}).get('enable', True) is True), - intensity=max(int(config.get('rainbow', {}).get('intensity', 100)), 10), - brightness=max(int(config.get('rainbow', {}).get('brightness', 150)), 10), - speed=int(config.get('rainbow', {}).get('speed', 25)), - ), - lights=config.get('lights', {}), + universes=universes, ) - if conf.alerts.brightness < conf.rainbow.brightness: - LOG.error('alerts brightness must be equal or above rainbow brightness') - exit(1) - return conf diff --git a/config.example.toml b/config.example.toml index bced96c..3ba23ac 100644 --- a/config.example.toml +++ b/config.example.toml @@ -11,8 +11,8 @@ user = "" password = "" topic = "/voc/alert" - -[sacn] +# "demo" can be anything you like +[universes.demo] # Wether to enable sACN multicast. Default is off. multicast = false @@ -23,42 +23,24 @@ target = "127.0.0.1" # which universe to address universe = 1 - -[alerts] -# This specifies the maximum DMX dimmer value that's sent to your lights -# when alerts occur. This must be atleast the same or more as the -# rainbow brightness (see below). -brightness = 255 - # Filter by specific components. If this list is non-empty, the message # will get shown if atleast one of these filters match. The filters are # applied by using re.search() on the component part of the message. filters = [] - -[rainbow] -# Wether to enable the rainbow 'no alerts' loop. If false, all other -# options in here will be ignored. -enable = true - -# This directly controls the 'value' part of the HSV equation in the -# rainbow 'no alerts' break loop. Value must be between 10% and 100%. -intensity = 100 +# This specifies the maximum DMX dimmer value that's sent to your lights +# when alerts occur. This must be atleast the same or more as the +# rainbow brightness (see below). +alert_brightness = 255 # DMX dimmer value when displaying the rainbow pattern. Must be equal -# or below the generic 'brightness' value above. -brightness = 150 - -# Speed of the rainbow pattern. This is specified as "miliseconds -# between rotating the hue wheel by 1 degree". Minimum value is 25, -# because sACN does not support more than 40 fps. Setting it any lower -# will disable the animation altogehter, resulting in static lights. -speed = 25 - +# or below the generic 'brightness' value above. Set to 0 to disable +# the rainbow +rainbow_brightness = 150 # This contains the DMX start addresses of your light fixtures. You # have to add atleast one fixture for the software to work. -[lights] +[universes.demo.lights] ignition_wal_l710 = [] stairville_par_56 = [] tsss_led_par_rgbw = [] diff --git a/dmx_queue.py b/dmx_queue.py index 4cb892f..66ed1cf 100644 --- a/dmx_queue.py +++ b/dmx_queue.py @@ -5,14 +5,20 @@ from time import sleep from sacn import sACNsender -LOG = logging.getLogger('DMXQueue') +import lights class DMXQueue: - def __init__(self, config, queue, lights): - self.config = config + def __init__(self, config, universe, queue): + self.log = logging.getLogger(f"DMXQueue {universe}") + self.config = config.universes[universe] self.queue = queue - self.lights = lights + + self.lights = [] + for classname, addrs in self.config.lights.items(): + cls = getattr(lights, classname) + for addr in addrs: + self.lights.append(cls(addr)) self.worker = Thread(target=self._worker) self.worker_should_be_running = False @@ -23,11 +29,11 @@ class DMXQueue: def start(self): self.sacn.start() - self.sacn.activate_output(self.config.sacn.universe) + self.sacn.activate_output(self.config.universe) - self.sacn[self.config.sacn.universe].multicast = self.config.sacn.multicast - if not self.config.sacn.multicast: - self.sacn[self.config.sacn.universe].destination = self.config.sacn.target + self.sacn[self.config.universe].multicast = self.config.multicast + if not self.config.multicast: + self.sacn[self.config.universe].destination = self.config.target self.dmx_data = 512 * [0] @@ -35,14 +41,14 @@ class DMXQueue: self.worker.start() def stop(self): - LOG.info('Waiting for worker to terminate ...') + self.log.info("Waiting for worker to terminate ...") self.worker_should_be_running = False self.worker.join() self.sacn.stop() def _dmx(self, addr, data): self.dmx_data[addr - 1] = data - self.sacn[self.config.sacn.universe].dmx_data = tuple(self.dmx_data) + self.sacn[self.config.universe].dmx_data = tuple(self.dmx_data) def _bulk(self, start_addr, values): for idx, value in enumerate(values): @@ -58,29 +64,39 @@ class DMXQueue: self._bulk(*light.dump()) def _worker(self): - LOG.info('Worker startup') + self.log.info("Worker startup") rotation = 0 while self.worker_should_be_running: try: level, component, text = self.queue.get_nowait() - LOG.info(f'Got queue item: {level} {component} : {text}') + if self.config.filters: + filtered = True + for f in self.config.filters: + if re.search(f, component, re.IGNORECASE): + filtered = False + break # no point in searching further + if filtered: + # no alert for filtered messages + continue + + self.log.info(f"Got queue item: {level} {component} : {text}") # effect duration should be between 1s and 1.5s - if level == 'error': + if level == "error": self._update_all(0, 0, 0, 0, 0) sleep(0.2) # three instances of two flashes each for i in range(3): for j in range(2): self._update_all( - self.config.alerts.brightness, 255, 0, 0, 50 + self.config.alert_brightness, 255, 0, 0, 50 ) sleep(0.1) self._update_all(0, 255, 0, 0, 50) sleep(0.1) sleep(0.2) - elif level == 'warn': + elif level == "warn": self._update_all(0, 0, 0, 0, 0) sleep(0.2) # warning: blink alternate, but slow @@ -92,7 +108,7 @@ class DMXQueue: light.white = 50 if (idx + i) % 2: - light.intensity = self.config.alerts.brightness + light.intensity = self.config.alert_brightness else: light.intensity = 0 @@ -100,25 +116,25 @@ class DMXQueue: sleep(0.2) self._update_all(0, 0, 0, 0, 0) sleep(0.2) - elif level == 'info': + elif level == "info": forward = list(range(15)) reverse = list(range(15)) reverse.reverse() - if self.config.rainbow.enable: + if self.config.rainbow_brightness > 0: diff = ( - self.config.alerts.brightness - - self.config.rainbow.brightness + self.config.alert_brightness + - self.config.rainbow_brightness ) - LOG.debug(diff) + self.log.debug(diff) if diff >= 50: for idx in forward + reverse: - LOG.debug(idx) - LOG.debug(diff * idx) + self.log.debug(idx) + self.log.debug(diff * idx) self._update_all( int( - self.config.rainbow.brightness + self.config.rainbow_brightness + ((diff / len(forward)) * idx) ), 0, @@ -129,10 +145,10 @@ class DMXQueue: sleep(0.025) else: for idx in forward + reverse: - LOG.debug(idx) + self.log.debug(idx) self._update_all( int( - (self.config.alerts.brightness / len(forward)) * idx + (self.config.alert_brightness / len(forward)) * idx ), 0, 50, @@ -143,24 +159,23 @@ class DMXQueue: self._update_all(0, 0, 0, 0, 0) self.queue.task_done() except Empty: - if self.config.rainbow.enable: + if self.config.rainbow_brightness > 0: for idx, light in enumerate(self.lights): - self._bulk(*light.rainbow( - idx, - rotation, - len(self.lights), - self.config.rainbow.intensity, - self.config.rainbow.brightness, - )) + self._bulk( + *light.rainbow( + idx, + rotation, + len(self.lights), + 100, + self.config.rainbow_brightness, + ) + ) - if self.config.rainbow.speed >= 25: - rotation = rotation + 1 - if rotation >= 360: - rotation = 0 + rotation = rotation + 1 + if rotation >= 360: + rotation = 0 - sleep(self.config.rainbow.speed / 1000) - else: - sleep(0.2) + sleep(0.03) else: sleep(0.2) - LOG.info('Worker shutdown') + self.log.info("Worker shutdown") diff --git a/lights/__init__.py b/lights/__init__.py index e69de29..85713fb 100644 --- a/lights/__init__.py +++ b/lights/__init__.py @@ -0,0 +1,7 @@ +from .ignition_wal_l710 import IgnitionWALL710 +from .pulsar_chromaflood_200 import PulsarChromaflood200 +from .sheds_30w_cob_rgb import Sheds30WCOBRGB +from .stairville_par_56 import StairvillePar56 +from .tsss_led_par_rgbw import TSSS_LED_PAR_RGBW +from .varytec_hero_wash_zoom_712 import VarytecHeroWashZoom712 +from .wled import WLED diff --git a/lights/common.py b/lights/common.py index 2b98fe5..32c0c00 100644 --- a/lights/common.py +++ b/lights/common.py @@ -1,7 +1,7 @@ import logging from colorsys import hsv_to_rgb -LOG = logging.getLogger('DMX') +LOG = logging.getLogger("DMX") class BaseDMXLight: @@ -14,20 +14,18 @@ class BaseDMXLight: self.white = 0 def __str__(self): - return f'{self.name} ({self.address})' + return f"{self.name} ({self.address})" def _dump(self): raise NotImplementedError def dump(self): ret = self._dump() - LOG.debug(f'{str(self)} -> {ret[1]}') + LOG.debug(f"{str(self)} -> {ret[1]}") return ret def rainbow(self, idx, angle, number_of_lights, intensity, brightness): - my_degrees_dec = ( - (angle + (idx * (360 / number_of_lights))) % 360 / 360 - ) + my_degrees_dec = (angle + (idx * (360 / number_of_lights))) % 360 / 360 r, g, b = hsv_to_rgb( my_degrees_dec, 1, diff --git a/lights/pulsar_chromaflood_200.py b/lights/pulsar_chromaflood_200.py index 050b9fa..1036b79 100644 --- a/lights/pulsar_chromaflood_200.py +++ b/lights/pulsar_chromaflood_200.py @@ -1,4 +1,5 @@ from colorsys import hsv_to_rgb + from .common import BaseDMXLight @@ -10,7 +11,11 @@ class PulsarChromaflood200(BaseDMXLight): self.red, self.green, self.blue, - 0,0,0, # chase 1 - 0,0,0, # chase 2 - self.intensity + 0, + 0, + 0, # chase 1 + 0, + 0, + 0, # chase 2 + self.intensity, ] diff --git a/lights/sheds_30w_cob_rgb.py b/lights/sheds_30w_cob_rgb.py index 85518e1..369cbb0 100644 --- a/lights/sheds_30w_cob_rgb.py +++ b/lights/sheds_30w_cob_rgb.py @@ -10,7 +10,7 @@ class Sheds30WCOBRGB(BaseDMXLight): self.red, self.green, self.blue, - 0, # strobe - 0, # mode - 0, # speed + 0, # strobe + 0, # mode + 0, # speed ] diff --git a/lights/stairville_par_56.py b/lights/stairville_par_56.py index 6b7b923..808df47 100644 --- a/lights/stairville_par_56.py +++ b/lights/stairville_par_56.py @@ -7,9 +7,9 @@ class StairvillePar56(BaseDMXLight): def _dump(self): offset = self.intensity / 255 return self.address, [ - 0, # RGB mode + 0, # RGB mode int(self.red * offset), int(self.green * offset), int(self.blue * offset), - 0, # speed + 0, # speed ] diff --git a/lights/tsss_led_par_rgbw.py b/lights/tsss_led_par_rgbw.py index 4219455..bcfcf4a 100644 --- a/lights/tsss_led_par_rgbw.py +++ b/lights/tsss_led_par_rgbw.py @@ -6,9 +6,9 @@ class TSSS_LED_PAR_RGBW(BaseDMXLight): def _dump(self): return self.address, [ - 0, # function - 0, # mode - 0, # speed + 0, # function + 0, # mode + 0, # speed self.intensity, self.red, self.green, diff --git a/main.py b/main.py index 22a5622..220d477 100755 --- a/main.py +++ b/main.py @@ -8,82 +8,53 @@ from time import sleep from conf import load_and_validate_config from dmx_queue import DMXQueue -from lights.ignition_wal_l710 import IgnitionWALL710 -from lights.pulsar_chromaflood_200 import PulsarChromaflood200 -from lights.sheds_30w_cob_rgb import Sheds30WCOBRGB -from lights.stairville_par_56 import StairvillePar56 -from lights.varytec_hero_wash_zoom_712 import VarytecHeroWashZoom712 -from lights.tsss_led_par_rgbw import TSSS_LED_PAR_RGBW -from lights.wled import WLED from mqtt_queue import MQTTQueue logging.basicConfig( level=logging.INFO, - format='%(asctime)s %(name)20s [%(levelname)-8s] %(message)s', + format="%(asctime)s %(name)20s [%(levelname)-8s] %(message)s", ) -LOG = logging.getLogger('main') +LOG = logging.getLogger("main") def main(): parser = ArgumentParser() parser.add_argument( - '--config', - default='config.toml', + "--config", + default="config.toml", ) args = parser.parse_args() config = load_and_validate_config(args.config) - LOG.info('Welcome to Voc2DMX') + LOG.info("Welcome to Voc2DMX") - queue = Queue() + queues = {} + dmx_workers = {} - lights = [] - for addr in config.lights.get('ignition_wal_l710', []): - lights.append(IgnitionWALL710(addr)) - for addr in config.lights.get('pulsar_chromaflood_200', []): - lights.append(PulsarChromaflood200(addr)) - for addr in config.lights.get('sheds_30w_cob_rgb', []): - lights.append(Sheds30WCOBRGB(addr)) - for addr in config.lights.get('stairville_par_56', []): - lights.append(StairvillePar56(addr)) - for addr in config.lights.get('tsss_led_par_rgbw', []): - lights.append(TSSS_LED_PAR_RGBW(addr)) - for addr in config.lights.get('varytec_hero_wash_712_zoom', []): - lights.append(VarytecHeroWashZoom712(addr)) - for addr in config.lights.get('wled_multi_rgb', []): - lights.append(WLED(addr)) - - if not lights: - LOG.error('No lights configured, please add atleast one fixture') - exit(1) - - LOG.info('') - LOG.info('Configured lights:') - for light in lights: - LOG.info(light) - LOG.info('') - - LOG.info('Initializing worker queues ...') - - mqttq = MQTTQueue(config, queue) - dmxq = DMXQueue(config, queue, lights) + LOG.info("Initializing worker queues ...") + mqttq = MQTTQueue(config, queues) mqttq.start() - dmxq.start() - LOG.info('initialization done, now running. Press Ctrl-C to stop') + for universe in config.universes: + queues[universe] = Queue() + dmx_workers[universe] = DMXQueue(config, universe, queues[universe]) + dmx_workers[universe].start() + + LOG.info("initialization done, now running. Press Ctrl-C to stop") try: while True: sleep(1) - except KeyboardInterrupt: - LOG.warning('Got interrupt, stopping queues ...') + finally: + LOG.warning("Got interrupt, stopping queues ...") mqttq.stop() - dmxq.stop() + for universe in config.universes: + dmx_workers[universe].stop() - LOG.info('Bye!') + LOG.info("Bye!") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/mqtt_queue.py b/mqtt_queue.py index cc191c5..5f7fda0 100644 --- a/mqtt_queue.py +++ b/mqtt_queue.py @@ -5,14 +5,14 @@ from queue import Queue import paho.mqtt.client as mqtt -LOG = logging.getLogger('MQTTQueue') +LOG = logging.getLogger("MQTTQueue") class MQTTQueue: - def __init__(self, config, queue): + def __init__(self, config, queues): self.config = config self.client = mqtt.Client() - self.queue = queue + self.queues = queues self.client.on_connect = self.on_connect self.client.on_disconnect = self.on_disconnect @@ -32,38 +32,29 @@ class MQTTQueue: self.client.disconnect() def on_connect(self, client, userdata, flags, rc): - LOG.info(f'Connected to {self.config.mqtt.host} with code {rc}') + LOG.info(f"Connected to {self.config.mqtt.host} with code {rc}") self.client.subscribe(self.config.mqtt.topic) - LOG.info(f'Subscribed') + LOG.info(f"Subscribed") def on_disconnect(self, client, userdata, rc): - LOG.warning(f'Disconnected from {self.config.mqtt.host} with code {rc}') + LOG.warning(f"Disconnected from {self.config.mqtt.host} with code {rc}") def on_mqtt_message(self, client, userdata, msg): try: - data = json.loads(msg.payload.decode('utf-8')) + data = json.loads(msg.payload.decode("utf-8")) + text = re.sub(r"\<[a-z\/]+\>", "", data["msg"]) - text = re.sub(r'\<[a-z\/]+\>', '', data['msg']) - - add_to_queue = True - if self.config.alerts.filters: - add_to_queue = False - for f in self.config.alerts.filters: - if re.search(f, data['component'], re.IGNORECASE): - add_to_queue = True - break # no point in searching further - - if add_to_queue: - self.queue.put( + for queue in self.queues: + self.queues[queue].put( ( - data['level'].lower(), - data['component'], + data["level"].lower(), + data["component"], text, ) ) - LOG.info(f'Put queue item: {data["level"].lower()} {data["component"]} : {text}') - else: - LOG.info(f'Ignoring message for {data["component"]} because it was filtered') + LOG.info( + f'Put queue {queue} item: {data["level"].lower()} {data["component"]} : {text}' + ) except Exception as e: LOG.exception(msg.payload)