update to support multiple universes per worker
This commit is contained in:
parent
b4e6851e32
commit
5d7f48d813
11 changed files with 156 additions and 189 deletions
58
conf.py
58
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)
|
||||
|
||||
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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
87
dmx_queue.py
87
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(
|
||||
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")
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
71
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()
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue