diff options
Diffstat (limited to 'topology.py')
-rw-r--r-- | topology.py | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/topology.py b/topology.py new file mode 100644 index 0000000..331c4e2 --- /dev/null +++ b/topology.py @@ -0,0 +1,135 @@ +"""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 |