summaryrefslogtreecommitdiff
path: root/topology.py
blob: 331c4e233f1e0de56c093189a27b580ac16e20c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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