update to support multiple universes per worker

This commit is contained in:
Franzi 2024-08-17 12:19:01 +02:00
parent b4e6851e32
commit 5d7f48d813
Signed by: kunsi
GPG key ID: 12E3D2136B818350
11 changed files with 156 additions and 189 deletions

58
conf.py
View file

@ -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)
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'),
),
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 = {}
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}"
)
if conf.alerts.brightness < conf.rainbow.brightness:
LOG.error('alerts brightness must be equal or above rainbow brightness')
exit(1)
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"),
),
universes=universes,
)
return conf

View file

@ -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 = []

View file

@ -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(
self._bulk(
*light.rainbow(
idx,
rotation,
len(self.lights),
self.config.rainbow.intensity,
self.config.rainbow.brightness,
))
100,
self.config.rainbow_brightness,
)
)
if self.config.rainbow.speed >= 25:
rotation = rotation + 1
if rotation >= 360:
rotation = 0
sleep(self.config.rainbow.speed / 1000)
sleep(0.03)
else:
sleep(0.2)
else:
sleep(0.2)
LOG.info('Worker shutdown')
self.log.info("Worker shutdown")

View file

@ -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

View file

@ -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,

View file

@ -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,
]

71
main.py
View file

@ -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()

View file

@ -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)