Mercurial > code > home > repos > homeauto
annotate lib/devices_shared/devices_shared.py @ 1754:92999dfbf321 default tip
add shelly support
author | drewp@bigasterisk.com |
---|---|
date | Tue, 04 Jun 2024 13:03:43 -0700 |
parents | 1ffe5ef470e5 |
children |
rev | line source |
---|---|
342 | 1 import time |
2 import numpy | |
3 import logging | |
4 import imageio | |
5 from rdflib import Namespace, RDF, URIRef, Literal | |
6 | |
634
1ffe5ef470e5
typo in xsd: namespace for led strips
drewp@bigasterisk.com
parents:
630
diff
changeset
|
7 XS = Namespace('http://www.w3.org/2001/XMLSchema#') |
342 | 8 ROOM = Namespace('http://projects.bigasterisk.com/room/') |
9 log = logging.getLogger() | |
10 | |
11 def _rgbFromHex(h): | |
12 rrggbb = h.lstrip('#') | |
13 return [int(x, 16) for x in [rrggbb[0:2], rrggbb[2:4], rrggbb[4:6]]] | |
14 | |
15 class PixelColumnsFromImages(object): | |
16 # could use this instead: | |
17 # https://github.com/OpenImageIO/oiio/blob/master/src/python/py_imagecache.cpp | |
18 def __init__(self): | |
19 self.lastImg = None, None | |
20 | |
21 def get(self, path, x, y, h): | |
22 if self.lastImg[0] != path: | |
369 | 23 fp = 'config/' + path |
24 self.lastImg = path, imageio.imread(fp) # or etcd or http | |
25 log.debug('read image from %r', fp) | |
342 | 26 img = self.lastImg[1] |
27 | |
28 y = numpy.clip(y, 0, img.shape[0] - 1) | |
29 h = numpy.clip(h, 1, img.shape[0] - y) | |
30 x = numpy.clip(x, 0, img.shape[1] - 1) | |
31 log.info('getcol y=%s h=%s img=%r ret=%r', y, h, img.shape, img[y:y + h,x,:].shape) | |
32 return img[y:y + h,x,:] | |
33 | |
34 getPixelColumn = PixelColumnsFromImages().get | |
35 | |
36 | |
37 class AnimChannel(object): | |
38 def __init__(self, x): | |
39 self.x = self.x2 = x | |
40 self.start = self.end = 0 | |
41 | |
42 def animTo(self, x2, rate): | |
43 self.get() # bring self.x to current time | |
44 log.info('animTo %s -> %s', self.x, x2) | |
45 if x2 == self.x: | |
46 return | |
47 self.start = time.time() | |
48 self.end = self.start + abs(x2 - self.x) / rate | |
49 self.x0 = self.x | |
50 self.x2 = x2 | |
51 | |
52 def get(self): | |
53 now = time.time() | |
54 if now > self.end: | |
55 self.x = self.x2 | |
56 else: | |
57 dur = self.end - self.start | |
58 self.x = (self.end - now) / dur * self.x0 + (now - self.start) / dur * self.x2 | |
59 return self.x | |
60 | |
630 | 61 def roundedLit(x): |
62 return Literal('%g' % x, datatype=XS['double']) | |
63 | |
342 | 64 class ScanGroup(object): |
65 | |
66 def __init__(self, uri, numLeds): | |
67 self.uri = uri | |
68 self.current = numpy.zeros((numLeds, 3), dtype=numpy.uint8) | |
69 | |
70 self.x = AnimChannel(0) | |
71 self.y = AnimChannel(0) | |
72 self.height = AnimChannel(numLeds) | |
431
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
73 self.src = "" |
342 | 74 |
75 def animateTo(self, x, y, height, src, rate=30, interpolate=ROOM['slide']): | |
76 log.info('anim to %s x=%s y=%s h=%s', src, x, y, height) | |
77 self.x.animTo(x, rate) | |
78 self.y.animTo(y, rate) # need separate y rate? | |
79 self.height.animTo(height, rate) | |
80 | |
81 self.src = src | |
82 | |
83 def updateCurrent(self): | |
84 try: | |
85 self.current = getPixelColumn(self.src, | |
86 int(self.x.get()), | |
87 int(self.y.get()), | |
88 int(self.height.get())) | |
369 | 89 except IOError as e: |
90 log.warn('getPixelColumn %r', e) | |
91 log.debug('current = %r', self.current) | |
630 | 92 |
342 | 93 def currentStatements(self): |
431
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
94 return [ |
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
95 (self.uri, RDF.type, ROOM['ScanGroup']), |
630 | 96 (self.uri, ROOM['xValue'], roundedLit(self.x.get())), |
97 (self.uri, ROOM['yValue'], roundedLit(self.y.get())), | |
431
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
98 (self.uri, ROOM['heightValue'], Literal(self.height.get())), |
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
99 (self.uri, ROOM['src'], Literal(self.src)), |
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
100 ] |
342 | 101 |
102 def colorForIndex(self, i): | |
103 return list(self.current[i,:]) | |
104 | |
105 args = {ROOM['src']: ('src', str), | |
630 | 106 ROOM['x']: ('x', float), |
107 ROOM['y']: ('y', float), | |
108 ROOM['height']: ('height', float), | |
342 | 109 ROOM['interpolate']: ('interpolate', lambda x: x), |
110 ROOM['rate']: ('rate', float), | |
111 } | |
112 | |
630 | 113 |
342 | 114 class RgbPixelsAnimation(object): |
115 | |
116 def __init__(self, graph, uri, updateOutput): | |
117 """we call updateOutput after any changes""" | |
118 self.graph = graph | |
119 self.uri = uri | |
120 self.updateOutput = updateOutput | |
121 self.setupGroups() | |
630 | 122 |
342 | 123 def setupGroups(self): |
124 self.groups = {} | |
125 self.groupWithIndex = {} | |
126 attrStatements = set() | |
127 for grp in self.graph.objects(self.uri, ROOM['pixelGroup']): | |
128 s = int(self.graph.value(grp, ROOM['startIndex'])) | |
129 e = int(self.graph.value(grp, ROOM['endIndex'])) | |
130 log.info('ScanGroup %s from %s to %s', grp, s, e) | |
131 sg = ScanGroup(grp, e - s + 1) | |
132 self.groups[grp] = [s, e, sg] | |
133 for i in range(s, e + 1): | |
134 self.groupWithIndex[i] = sg, i - s | |
135 attrStatements.update(self.graph.triples((grp, None, None))) | |
136 self.onStatements(attrStatements, _groups=False) | |
630 | 137 |
342 | 138 def maxIndex(self): |
630 | 139 return max(v[1] for v in self.groups.values()) |
342 | 140 |
141 def hostStatements(self): | |
142 return ( | |
143 [(self.uri, ROOM['pixelGroup'], grp) for grp in self.groups.keys()] + | |
144 sum([v[2].currentStatements() | |
630 | 145 for v in self.groups.values()], []) + |
431
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
146 [] # current |
be76e9677a99
animated rgb puts more data back into graph
drewp@bigasterisk.com
parents:
369
diff
changeset
|
147 ) |
342 | 148 |
149 def getColorOrder(self, graph, uri): | |
150 colorOrder = graph.value(uri, ROOM['colorOrder'], | |
151 default=ROOM['ledColorOrder/RGB']) | |
152 head, tail = str(colorOrder).rsplit('/', 1) | |
153 if head != str(ROOM['ledColorOrder']): | |
154 raise NotImplementedError('%r colorOrder %r' % (uri, colorOrder)) | |
155 stripType = None | |
156 return colorOrder, stripType | |
630 | 157 |
342 | 158 def step(self): |
159 # if animating... | |
160 self.updateOutput() | |
630 | 161 |
342 | 162 def onStatements(self, statements, _groups=True): |
163 needSetup = False | |
164 animateCalls = {} # group uri : kw for animateTo | |
165 for s, p, o in statements: | |
166 if s not in self.groups: | |
167 # missing the case that you just added a new group | |
168 continue | |
169 if p in args: | |
170 k, conv = args[p] | |
171 animateCalls.setdefault(s, {})[k] = conv(o.toPython()) | |
172 else: | |
173 needSetup = True | |
174 | |
175 if needSetup and _groups: | |
176 self.setupGroups() | |
177 for grp, kw in animateCalls.items(): | |
178 for pred, (k, conv) in args.items(): | |
179 if k not in kw: | |
180 v = self.graph.value(grp, pred) | |
181 if v is not None: | |
182 kw[k] = conv(v.toPython()) | |
183 | |
184 self.groups[grp][2].animateTo(**kw) | |
185 | |
186 def outputPatterns(self): | |
187 pats = [] | |
188 for grp in self.groups: | |
189 for attr in args: | |
190 pats.append((grp, attr, None)) | |
191 return pats | |
192 | |
193 def outputWidgets(self): | |
194 return [] | |
195 | |
196 def currentColors(self): | |
197 for _, _, sg in self.groups.values(): | |
198 sg.updateCurrent() | |
199 for idx in range(self.maxIndex() + 1): | |
200 sg, offset = self.groupWithIndex[idx] | |
201 r, g, b = sg.colorForIndex(offset) | |
202 yield idx, (r, g, b) |