summaryrefslogtreecommitdiff
path: root/topology.py
diff options
context:
space:
mode:
Diffstat (limited to 'topology.py')
-rw-r--r--topology.py135
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