"""Topology module. Contains the topology classes for representing the physical and virtual topology of the lighting setup. """ import xml.etree.ElementTree as et from .constants import BXW from .interfaces import XMLSerializable from .exceptions import LoadError class Fixture(XMLSerializable): """Class representing a lighting fixture. Each lighting fixture has a number of channels, which are mapped to the physical topology. Channels must be added by changing the value of ``Fixture.channel_count``, which will create the necessary channel objects and handle deletion. """ def __init__(self, w: "Workspace", id_: int = None, name: str = None, channel_count: int = 0): self.w = w self.id = id_ if id_ is not None else w.next_fixture_id self.name = name if name else "Fixture %d" % self.id self.channels = () self.channel_count = channel_count self.w.register_fixture(self) class Channel: """Class representing a single channel on a Fixture. The physical address of the channel is stored in the ``Channel.address`` attribute and may be changed at will. This program takes a very lax approach to channel addressing: the address is not parsed in any way and duplicate addresses are not handled. For convenience with OLA, Channel is iterable and iteration returns an iterator over its ``address`` attribute. """ def __init__(self, f: "Fixture", id_: int, name: str = "Intensity", address = (-1,-1)): self.f = f self.id = id_ self.name = name self.address = address self._hash = hash((f.id, id_)) def __hash__(self): return self._hash def __iter__(self): return iter(self.address) def __repr__(self): return "Channel(fixture={c.f.id}, index={c.id}, name={c.name})".format(c=self) @property def channel_count(self): """Return the current number of channels on the fixture.""" return len(self.channels) @channel_count.setter def channel_count(self, value): """Change the number of channels on the fixture. This function handles deletion of removed channels from functions as well. """ if value < 0: raise ValueError("Number of channels must be nonnegative") elif value < len(self.channels): for i in range(value, len(self.channels)): self.w.delete_channel(self.channels[i]) self.channels = self.channels[:value] elif value > len(self.channels): self.channels += tuple((Fixture.Channel(self, i) for i in range(len(self.channels), value))) def __repr__(self): return "Fixture({f.name}, id={f.id}, channels={f.channel_count})".format(f=self) def serialize(self): e = et.Element(BXW+"fixture") e.set("name", self.name) e.set("id", str(self.id)) for c in self.channels: ce = et.SubElement(e, BXW+"channel") ce.set("name", c.name) if c.address is not None: ## TODO: Other addressing modes try: univ, addr = c.address ae = et.SubElement(ce, BXW+"ola") ae.set("universe", str(univ)) ae.set("address", str(addr)) except ValueError: pass return e @classmethod def deserialize(cls, w, e): if e.tag != BXW+"fixture": raise LoadError("Invalid fixture tag") id_ = cls.int_or_none(e.get("id")) if id_ is None: raise LoadError("Fixture tag has invalid/missing ID") name = e.get("name") f = cls(w, id_=id_, name=name, channel_count=len(e)) for n, channel in enumerate(e): if channel.tag != BXW+"channel": raise LoadError("Invalid channel tag") name = channel.get("name") if name is not None: f.channels[n].name = name if len(channel) > 1: raise LoadError("Channel can have at most one address") elif len(channel) == 1: address, = channel if address.tag == BXW+"ola": try: address = (int(address.get("universe")), int(address.get("address")),) except (ValueError, TypeError): raise LoadError("Invalid OLA address on channel") else: raise LoadError("Unknown address tag \"%s\"" % address.tag) f.channels[n].address = address return f