comparison service/piNode/devices.py @ 1427:6bd36e5e109f

whitespace Ignore-this: 8abcbd0552c4356db0073b464fc3816a darcs-hash:2917f14b52fb149530a2369486ca9876cb0d334a
author drewp <drewp@bigasterisk.com>
date Thu, 08 Aug 2019 16:48:40 -0700
parents 83043957c809
children 69a84b3d1dfa
comparison
equal deleted inserted replaced
1426:c3c2418d138c 1427:6bd36e5e109f
64 def hostStateInit(self): 64 def hostStateInit(self):
65 """ 65 """
66 If you don't want to use __init__, you can use this to set up 66 If you don't want to use __init__, you can use this to set up
67 whatever storage you might need for hostStatements 67 whatever storage you might need for hostStatements
68 """ 68 """
69 69
70 def description(self): 70 def description(self):
71 return { 71 return {
72 'uri': self.uri, 72 'uri': self.uri,
73 'className': self.__class__.__name__, 73 'className': self.__class__.__name__,
74 'pinNumber': getattr(self, 'pinNumber', None), 74 'pinNumber': getattr(self, 'pinNumber', None),
84 (e.g. light brightness) if its master version is in this 84 (e.g. light brightness) if its master version is in this
85 object. This method is called on /graph requests so it should 85 object. This method is called on /graph requests so it should
86 be fast. 86 be fast.
87 """ 87 """
88 return [] 88 return []
89 89
90 def watchPrefixes(self): 90 def watchPrefixes(self):
91 """ 91 """
92 subj,pred pairs of the statements that might be returned from 92 subj,pred pairs of the statements that might be returned from
93 readFromPoll, so the dashboard knows what it should 93 readFromPoll, so the dashboard knows what it should
94 watch. This should be eliminated, as the dashboard should just 94 watch. This should be eliminated, as the dashboard should just
96 """ 96 """
97 return [] 97 return []
98 98
99 def poll(self): 99 def poll(self):
100 return [] # statements 100 return [] # statements
101 101
102 def outputPatterns(self): 102 def outputPatterns(self):
103 """ 103 """
104 Triple patterns, using None as a wildcard, that should be routed 104 Triple patterns, using None as a wildcard, that should be routed
105 to sendOutput 105 to sendOutput
106 """ 106 """
110 """ 110 """
111 structs to make output widgets on the dashboard. ~1 of these per 111 structs to make output widgets on the dashboard. ~1 of these per
112 handler you have in sendOutput 112 handler you have in sendOutput
113 """ 113 """
114 return [] 114 return []
115 115
116 def sendOutput(self, statements): 116 def sendOutput(self, statements):
117 """ 117 """
118 If we got statements that match this class's outputPatterns, this 118 If we got statements that match this class's outputPatterns, this
119 will be called with the statements that matched. 119 will be called with the statements that matched.
120 120
121 Todo: it would be fine to read back confirmations or 121 Todo: it would be fine to read back confirmations or
122 whatever. Just need a way to collect them into graph statements. 122 whatever. Just need a way to collect them into graph statements.
123 """ 123 """
124 raise NotImplementedError 124 raise NotImplementedError
125 125
126 _knownTypes = set() 126 _knownTypes = set()
127 def register(deviceType): 127 def register(deviceType):
128 _knownTypes.add(deviceType) 128 _knownTypes.add(deviceType)
129 return deviceType 129 return deviceType
130 130
131 @register 131 @register
132 class MotionSensorInput(DeviceType): 132 class MotionSensorInput(DeviceType):
133 """ 133 """
134 Triggering all the time? Try 5V VCC, per https://electronics.stackexchange.com/a/416295 134 Triggering all the time? Try 5V VCC, per https://electronics.stackexchange.com/a/416295
135 135
136 0 30s 60s 90s 10min 136 0 30s 60s 90s 10min
137 | | | | ... | 137 | | | | ... |
138 Sensor input ******** ** ******* **** 138 Sensor input ******** ** ******* ****
139 :sees output ........ .. ....... .... 139 :sees output ........ .. ....... ....
140 :seesRecently ............................................................. 140 :seesRecently .............................................................
141 :seesRecently30 .................................... 141 :seesRecently30 ....................................
142 :motionStart x x x x 142 :motionStart x x x x
158 self.lastMotionStart90 = 0 158 self.lastMotionStart90 = 0
159 159
160 def poll(self): 160 def poll(self):
161 motion = self.pi.read(self.pinNumber) 161 motion = self.pi.read(self.pinNumber)
162 now = time.time() 162 now = time.time()
163 163
164 oneshot = [] 164 oneshot = []
165 if self.lastRead is not None and motion != self.lastRead: 165 if self.lastRead is not None and motion != self.lastRead:
166 oneshot = [(self.uri, ROOM['sees'], ROOM['motionStart'])] 166 oneshot = [(self.uri, ROOM['sees'], ROOM['motionStart'])]
167 for v, t in [('lastMotionStart30', 30), ('lastMotionStart90', 90)]: 167 for v, t in [('lastMotionStart30', 30), ('lastMotionStart90', 90)]:
168 if now - getattr(self, v) > t: 168 if now - getattr(self, v) > t:
169 oneshot.append((self.uri, ROOM['sees'], ROOM['motionStart%s' % t])) 169 oneshot.append((self.uri, ROOM['sees'], ROOM['motionStart%s' % t]))
170 setattr(self, v, now) 170 setattr(self, v, now)
171 self.lastRead = motion 171 self.lastRead = motion
172 172
173 return {'latest': [ 173 return {'latest': [
174 (self.uri, ROOM['sees'], 174 (self.uri, ROOM['sees'],
175 ROOM['motion'] if motion else ROOM['noMotion']), 175 ROOM['motion'] if motion else ROOM['noMotion']),
176 ] + self.recentMotionStatements(now, motion), 176 ] + self.recentMotionStatements(now, motion),
177 'oneshot': oneshot} 177 'oneshot': oneshot}
184 dt = now - self.lastMotionTime 184 dt = now - self.lastMotionTime
185 return [(self.uri, ROOM['seesRecently'], 185 return [(self.uri, ROOM['seesRecently'],
186 ROOM['motion'] if (dt < 60 * 10) else ROOM['noMotion']), 186 ROOM['motion'] if (dt < 60 * 10) else ROOM['noMotion']),
187 (self.uri, ROOM['seesRecently30'], 187 (self.uri, ROOM['seesRecently30'],
188 ROOM['motion'] if (dt < 30) else ROOM['noMotion'])] 188 ROOM['motion'] if (dt < 30) else ROOM['noMotion'])]
189 189
190 def watchPrefixes(self): 190 def watchPrefixes(self):
191 return [ 191 return [
192 (self.uri, ROOM['sees']), 192 (self.uri, ROOM['sees']),
193 (self.uri, ROOM['seesRecently']), 193 (self.uri, ROOM['seesRecently']),
194 (self.uri, ROOM['seesRecently30']), 194 (self.uri, ROOM['seesRecently30']),
200 """3 PWMs for r/g/b on a strip""" 200 """3 PWMs for r/g/b on a strip"""
201 # pigpio daemon is working fine, but 201 # pigpio daemon is working fine, but
202 # https://github.com/RPi-Distro/python-gpiozero/blob/59ba7154c5918745ac894ea03503667d6473c760/gpiozero/output_devices.py#L213 202 # https://github.com/RPi-Distro/python-gpiozero/blob/59ba7154c5918745ac894ea03503667d6473c760/gpiozero/output_devices.py#L213
203 # can also apparently do PWM 203 # can also apparently do PWM
204 deviceType = ROOM['RgbStrip'] 204 deviceType = ROOM['RgbStrip']
205 205
206 @classmethod 206 @classmethod
207 def findInstances(cls, graph, board, pi): 207 def findInstances(cls, graph, board, pi):
208 for row in graph.query("""SELECT DISTINCT ?dev ?r ?g ?b WHERE { 208 for row in graph.query("""SELECT DISTINCT ?dev ?r ?g ?b WHERE {
209 ?board 209 ?board
210 :hasPin ?rpin; 210 :hasPin ?rpin;
225 225
226 def __init__(self, graph, uri, pi, r, g, b): 226 def __init__(self, graph, uri, pi, r, g, b):
227 self.graph, self.uri, self.pi = graph, uri, pi 227 self.graph, self.uri, self.pi = graph, uri, pi
228 self.rgb = map(int, [r, g, b]) 228 self.rgb = map(int, [r, g, b])
229 self.value = '#000000' 229 self.value = '#000000'
230 230
231 def setup(self): 231 def setup(self):
232 for i in self.rgb: 232 for i in self.rgb:
233 setupPwm(self.pi, i) 233 setupPwm(self.pi, i)
234 234
235 def hostStatements(self): 235 def hostStatements(self):
236 return [(self.uri, ROOM['color'], Literal(self.value))] 236 return [(self.uri, ROOM['color'], Literal(self.value))]
237 237
238 def outputPatterns(self): 238 def outputPatterns(self):
239 return [(self.uri, ROOM['color'], None)] 239 return [(self.uri, ROOM['color'], None)]
240 240
241 def _rgbFromHex(self, h): 241 def _rgbFromHex(self, h):
242 rrggbb = h.lstrip('#') 242 rrggbb = h.lstrip('#')
243 return [int(x, 16) for x in [rrggbb[0:2], rrggbb[2:4], rrggbb[4:6]]] 243 return [int(x, 16) for x in [rrggbb[0:2], rrggbb[2:4], rrggbb[4:6]]]
244 244
245 def sendOutput(self, statements): 245 def sendOutput(self, statements):
246 assert len(statements) == 1 246 assert len(statements) == 1
247 assert statements[0][:2] == (self.uri, ROOM['color']) 247 assert statements[0][:2] == (self.uri, ROOM['color'])
248 248
249 rgb = self._rgbFromHex(statements[0][2]) 249 rgb = self._rgbFromHex(statements[0][2])
250 self.value = statements[0][2] 250 self.value = statements[0][2]
251 251
252 for (i, v) in zip(self.rgb, rgb): 252 for (i, v) in zip(self.rgb, rgb):
253 self.pi.set_PWM_dutycycle(i, v) 253 self.pi.set_PWM_dutycycle(i, v)
254 254
255 def outputWidgets(self): 255 def outputWidgets(self):
256 return [{ 256 return [{
257 'element': 'output-rgb', 257 'element': 'output-rgb',
258 'subj': self.uri, 258 'subj': self.uri,
259 'pred': ROOM['color'], 259 'pred': ROOM['color'],
272 DeviceType.__init__(self, *a, **kw) 272 DeviceType.__init__(self, *a, **kw)
273 import DHT22 273 import DHT22
274 self.sens = DHT22.sensor(self.pi, self.pinNumber) 274 self.sens = DHT22.sensor(self.pi, self.pinNumber)
275 self.recentLowTemp = (0, None) # time, temp 275 self.recentLowTemp = (0, None) # time, temp
276 self.recentPeriodSec = 30 276 self.recentPeriodSec = 30
277 277
278 def poll(self): 278 def poll(self):
279 stmts = set() 279 stmts = set()
280 280
281 now = time.time() 281 now = time.time()
282 if self.recentLowTemp[0] < now - self.recentPeriodSec: 282 if self.recentLowTemp[0] < now - self.recentPeriodSec:
300 stmts.add((self.uri, RDFS['comment'], 300 stmts.add((self.uri, RDFS['comment'],
301 Literal('No recent DHT response (%.02f sec old)' % self.sens.staleness()))) 301 Literal('No recent DHT response (%.02f sec old)' % self.sens.staleness())))
302 302
303 if self.recentLowTemp[1] is not None: 303 if self.recentLowTemp[1] is not None:
304 stmts.add((self.uri, ROOM['recentLowTemperatureF'], Literal(self.recentLowTemp[1]))) 304 stmts.add((self.uri, ROOM['recentLowTemperatureF'], Literal(self.recentLowTemp[1])))
305 305
306 self.sens.trigger() 306 self.sens.trigger()
307 307
308 return stmts 308 return stmts
309 309
310 def watchPrefixes(self): 310 def watchPrefixes(self):
311 return [ 311 return [
312 (self.uri, ROOM['temperatureF']), 312 (self.uri, ROOM['temperatureF']),
313 (self.uri, ROOM['humidity']), 313 (self.uri, ROOM['humidity']),
314 ] 314 ]
339 ROOM['press'] if closed else ROOM['release']), 339 ROOM['press'] if closed else ROOM['release']),
340 ] 340 ]
341 else: 341 else:
342 oneshot = [] 342 oneshot = []
343 self.lastClosed = closed 343 self.lastClosed = closed
344 344
345 return {'latest': [ 345 return {'latest': [
346 (self.uri, ROOM['buttonState'], 346 (self.uri, ROOM['buttonState'],
347 ROOM['pressed'] if closed else ROOM['notPressed']), 347 ROOM['pressed'] if closed else ROOM['notPressed']),
348 ], 348 ],
349 'oneshot':oneshot} 349 'oneshot':oneshot}
350 350
351 def watchPrefixes(self): 351 def watchPrefixes(self):
352 return [ 352 return [
353 (self.uri, ROOM['buttonState']), 353 (self.uri, ROOM['buttonState']),
354 ] 354 ]
355 355
356 @register 356 @register
357 class OneWire(DeviceType): 357 class OneWire(DeviceType):
358 """ 358 """
359 Also see /my/proj/ansible/roles/raspi_io_node/tasks/main.yml for 359 Also see /my/proj/ansible/roles/raspi_io_node/tasks/main.yml for
360 some system config that contains the pin number that you want to 360 some system config that contains the pin number that you want to
394 394
395 returnValue(stmts) 395 returnValue(stmts)
396 except Exception as e: 396 except Exception as e:
397 log.error(e) 397 log.error(e)
398 os.abort() 398 os.abort()
399 399
400 def watchPrefixes(self): 400 def watchPrefixes(self):
401 return [(s.uri, ROOM['temperatureF']) for s in self._sensors] 401 return [(s.uri, ROOM['temperatureF']) for s in self._sensors]
402 402
403 class FilteredValue(object): 403 class FilteredValue(object):
404 def __init__(self, setter, 404 def __init__(self, setter,
405 slew=2.0, # step/sec max slew rate 405 slew=2.0, # step/sec max slew rate
406 accel=5, # step/sec^2 acceleration 406 accel=5, # step/sec^2 acceleration
407 ): 407 ):
408 self.setter = setter 408 self.setter = setter
409 self.slew, self.accel = slew, accel 409 self.slew, self.accel = slew, accel
410 410
411 self.x = None 411 self.x = None
412 self.dx = 0 412 self.dx = 0
413 self.goal = self.x 413 self.goal = self.x
414 self.lastStep = 0 414 self.lastStep = 0
415 415
438 self.x = nextX 438 self.x = nextX
439 reactor.callLater(.05, self.step) 439 reactor.callLater(.05, self.step)
440 440
441 #print "x= %(x)s dx= %(dx)s goal= %(goal)s" % self.__dict__ 441 #print "x= %(x)s dx= %(dx)s goal= %(goal)s" % self.__dict__
442 self.setter(self.x) 442 self.setter(self.x)
443 443
444 @register 444 @register
445 class LedOutput(DeviceType): 445 class LedOutput(DeviceType):
446 deviceType = ROOM['LedOutput'] 446 deviceType = ROOM['LedOutput']
447 447
448 def hostStateInit(self): 448 def hostStateInit(self):
455 class Instant(object): 455 class Instant(object):
456 def set(self, goal): 456 def set(self, goal):
457 _setPwm(goal) 457 _setPwm(goal)
458 self.fv = Instant() 458 self.fv = Instant()
459 self.gamma = float(self.graph.value(self.uri, ROOM['gamma'], default=1)) 459 self.gamma = float(self.graph.value(self.uri, ROOM['gamma'], default=1))
460 460
461 def setup(self): 461 def setup(self):
462 setupPwm(self.pi, self.pinNumber) 462 setupPwm(self.pi, self.pinNumber)
463 463
464 def outputPatterns(self): 464 def outputPatterns(self):
465 return [(self.uri, ROOM['brightness'], None)] 465 return [(self.uri, ROOM['brightness'], None)]
466 466
467 def sendOutput(self, statements): 467 def sendOutput(self, statements):
468 assert len(statements) == 1 468 assert len(statements) == 1
469 assert statements[0][:2] == (self.uri, ROOM['brightness']) 469 assert statements[0][:2] == (self.uri, ROOM['brightness'])
470 self.value = float(statements[0][2]) 470 self.value = float(statements[0][2])
471 self.fv.set(self.value) 471 self.fv.set(self.value)
473 def _setPwm(self, x): 473 def _setPwm(self, x):
474 v = max(0, min(255, int((x ** self.gamma)* 255))) 474 v = max(0, min(255, int((x ** self.gamma)* 255)))
475 self.pi.set_PWM_dutycycle(self.pinNumber, v) 475 self.pi.set_PWM_dutycycle(self.pinNumber, v)
476 476
477 def hostStatements(self): 477 def hostStatements(self):
478 return [(self.uri, ROOM['brightness'], Literal(self.value))] 478 return [(self.uri, ROOM['brightness'], Literal(self.value))]
479 479
480 def outputWidgets(self): 480 def outputWidgets(self):
481 return [{ 481 return [{
482 'element': 'output-slider', 482 'element': 'output-slider',
483 'min': 0, 483 'min': 0,
484 'max': 1, 484 'max': 1,
485 'step': 1 / 255, 485 'step': 1 / 255,
486 'subj': self.uri, 486 'subj': self.uri,
487 'pred': ROOM['brightness'], 487 'pred': ROOM['brightness'],
488 }] 488 }]
489 489
490 490
491 @register 491 @register
492 class OnboardTemperature(DeviceType): 492 class OnboardTemperature(DeviceType):
493 deviceType = ROOM['OnboardTemperature'] 493 deviceType = ROOM['OnboardTemperature']
494 pollPeriod = 10 494 pollPeriod = 10
495 @classmethod 495 @classmethod
496 def findInstances(cls, graph, board, pi): 496 def findInstances(cls, graph, board, pi):
497 log.debug('graph has any connected devices of type OnboardTemperature on %r?', board) 497 log.debug('graph has any connected devices of type OnboardTemperature on %r?', board)
498 for row in graph.query('''SELECT DISTINCT ?uri WHERE { 498 for row in graph.query('''SELECT DISTINCT ?uri WHERE {
499 ?board :onboardDevice ?uri . 499 ?board :onboardDevice ?uri .
500 ?uri a :OnboardTemperature . 500 ?uri a :OnboardTemperature .
501 }''', initBindings=dict(board=board)): 501 }''', initBindings=dict(board=board)):
502 log.debug(' found %s', row.uri) 502 log.debug(' found %s', row.uri)
503 yield cls(graph, row.uri, pi, pinNumber=None) 503 yield cls(graph, row.uri, pi, pinNumber=None)
504 504
505 def poll(self): 505 def poll(self):
506 milliC = open('/sys/class/thermal/thermal_zone0/temp').read().strip() 506 milliC = open('/sys/class/thermal/thermal_zone0/temp').read().strip()
507 c = float(milliC) / 1000. 507 c = float(milliC) / 1000.
508 f = c * 1.8 + 32 508 f = c * 1.8 + 32
509 return [ 509 return [
519 pixelStats = scales.collection('/rgbPixels', 519 pixelStats = scales.collection('/rgbPixels',
520 scales.PmfStat('updateOutput'), 520 scales.PmfStat('updateOutput'),
521 scales.PmfStat('currentColors'), 521 scales.PmfStat('currentColors'),
522 scales.PmfStat('poll'), 522 scales.PmfStat('poll'),
523 ) 523 )
524 524
525 @register 525 @register
526 class RgbPixels(DeviceType): 526 class RgbPixels(DeviceType):
527 """chain of ws2812 rgb pixels on pin GPIO18""" 527 """chain of ws2812 rgb pixels on pin GPIO18"""
528 deviceType = ROOM['RgbPixels'] 528 deviceType = ROOM['RgbPixels']
529 529
530 def hostStateInit(self): 530 def hostStateInit(self):
531 self.anim = RgbPixelsAnimation(self.graph, self.uri, self.updateOutput) 531 self.anim = RgbPixelsAnimation(self.graph, self.uri, self.updateOutput)
532 log.debug('%s maxIndex = %s', self.uri, self.anim.maxIndex()) 532 log.debug('%s maxIndex = %s', self.uri, self.anim.maxIndex())
533 self.neo = rpi_ws281x.Adafruit_NeoPixel(self.anim.maxIndex() + 1, pin=18) 533 self.neo = rpi_ws281x.Adafruit_NeoPixel(self.anim.maxIndex() + 1, pin=18)
534 self.neo.begin() 534 self.neo.begin()
535 535
536 colorOrder, stripType = self.anim.getColorOrder(self.graph, self.uri) 536 colorOrder, stripType = self.anim.getColorOrder(self.graph, self.uri)
537 537
538 def sendOutput(self, statements): 538 def sendOutput(self, statements):
539 self.anim.onStatements(statements) 539 self.anim.onStatements(statements)
540 540
541 @pixelStats.updateOutput.time() 541 @pixelStats.updateOutput.time()
542 def updateOutput(self): 542 def updateOutput(self):
546 print list(self.anim.currentColors()) 546 print list(self.anim.currentColors())
547 return 547 return
548 548
549 with pixelStats.currentColors.time(): 549 with pixelStats.currentColors.time():
550 colors = self.anim.currentColors() 550 colors = self.anim.currentColors()
551 551
552 for idx, (r, g, b) in colors: 552 for idx, (r, g, b) in colors:
553 if idx < 4: 553 if idx < 4:
554 log.debug('out color %s (%s,%s,%s)', idx, r, g, b) 554 log.debug('out color %s (%s,%s,%s)', idx, r, g, b)
555 self.neo.setPixelColorRGB(idx, r, g, b) 555 self.neo.setPixelColorRGB(idx, r, g, b)
556 self.neo.show() 556 self.neo.show()
560 self.anim.step() 560 self.anim.step()
561 return [] 561 return []
562 562
563 def hostStatements(self): 563 def hostStatements(self):
564 return self.anim.hostStatements() 564 return self.anim.hostStatements()
565 565
566 def outputPatterns(self): 566 def outputPatterns(self):
567 return self.anim.outputPatterns() 567 return self.anim.outputPatterns()
568 568
569 def outputWidgets(self): 569 def outputWidgets(self):
570 return self.anim.outputWidgets() 570 return self.anim.outputWidgets()
571 571
572 @register 572 @register
573 class Lcd8544(DeviceType): 573 class Lcd8544(DeviceType):
574 """PCD8544 lcd (nokia 5110)""" 574 """PCD8544 lcd (nokia 5110)"""
575 deviceType = ROOM['RgbStrip'] 575 deviceType = ROOM['RgbStrip']
576 576
577 @classmethod 577 @classmethod
578 def findInstances(cls, graph, board, pi): 578 def findInstances(cls, graph, board, pi):
579 for row in graph.query(""" 579 for row in graph.query("""
580 SELECT DISTINCT ?dev ?din ?clk ?dc ?rst WHERE { 580 SELECT DISTINCT ?dev ?din ?clk ?dc ?rst WHERE {
581 ?dev a :Lcd8544 . 581 ?dev a :Lcd8544 .
603 spi=Adafruit_GPIO.SPI.BitBang( 603 spi=Adafruit_GPIO.SPI.BitBang(
604 Adafruit_Nokia_LCD.GPIO.RPiGPIOAdapter(RPi.GPIO), 604 Adafruit_Nokia_LCD.GPIO.RPiGPIOAdapter(RPi.GPIO),
605 sclk=clk, 605 sclk=clk,
606 mosi=din)) 606 mosi=din))
607 self.lcd.begin(contrast=60) 607 self.lcd.begin(contrast=60)
608 608
609 def hostStatements(self): 609 def hostStatements(self):
610 return [] 610 return []
611 return [(self.uri, ROOM['color'], Literal(self.value))] 611 return [(self.uri, ROOM['color'], Literal(self.value))]
612 612
613 def outputPatterns(self): 613 def outputPatterns(self):
614 return [] 614 return []
615 return [(self.uri, ROOM['color'], None)] 615 return [(self.uri, ROOM['color'], None)]
616 616
617 def sendOutput(self, statements): 617 def sendOutput(self, statements):
622 rgb = self._rgbFromHex(statements[0][2]) 622 rgb = self._rgbFromHex(statements[0][2])
623 self.value = statements[0][2] 623 self.value = statements[0][2]
624 624
625 for (i, v) in zip(self.rgb, rgb): 625 for (i, v) in zip(self.rgb, rgb):
626 self.pi.set_PWM_dutycycle(i, v) 626 self.pi.set_PWM_dutycycle(i, v)
627 627
628 def outputWidgets(self): 628 def outputWidgets(self):
629 return [] 629 return []
630 return [{ 630 return [{
631 'element': 'output-rgb', 631 'element': 'output-rgb',
632 'subj': self.uri, 632 'subj': self.uri,
658 for out in graph.query("""SELECT DISTINCT ?area ?chan WHERE { 658 for out in graph.query("""SELECT DISTINCT ?area ?chan WHERE {
659 ?dev :output [:area ?area; :channel ?chan] . 659 ?dev :output [:area ?area; :channel ?chan] .
660 }""", initBindings=dict(dev=row.dev), initNs={'': ROOM}): 660 }""", initBindings=dict(dev=row.dev), initNs={'': ROOM}):
661 outs[out.area] = out.chan.toPython() 661 outs[out.area] = out.chan.toPython()
662 yield cls(graph, row.dev, pi, outs) 662 yield cls(graph, row.dev, pi, outs)
663 663
664 def __init__(self, graph, dev, pi, outs): 664 def __init__(self, graph, dev, pi, outs):
665 super(PwmBoard, self).__init__(graph, dev, pi, pinNumber=None) 665 super(PwmBoard, self).__init__(graph, dev, pi, pinNumber=None)
666 import PCA9685 666 import PCA9685
667 self.pwm = PCA9685.PWM(pi, bus=1, address=0x40) 667 self.pwm = PCA9685.PWM(pi, bus=1, address=0x40)
668 self.pwm.set_frequency(1200) 668 self.pwm.set_frequency(1200)
681 assert statements[0][1] == ROOM['brightness']; 681 assert statements[0][1] == ROOM['brightness'];
682 chan = self.outs[statements[0][0]] 682 chan = self.outs[statements[0][0]]
683 value = float(statements[0][2]) 683 value = float(statements[0][2])
684 self.values[statements[0][0]] = value 684 self.values[statements[0][0]] = value
685 self.pwm.set_duty_cycle(chan, value * 100) 685 self.pwm.set_duty_cycle(chan, value * 100)
686 686
687 def outputWidgets(self): 687 def outputWidgets(self):
688 return [{ 688 return [{
689 'element': 'output-slider', 689 'element': 'output-slider',
690 'min': 0, 690 'min': 0,
691 'max': 1, 691 'max': 1,
692 'step': 1 / 255, 692 'step': 1 / 255,
693 'subj': area, 693 'subj': area,
694 'pred': ROOM['brightness'], 694 'pred': ROOM['brightness'],
695 } for area in self.outs] 695 } for area in self.outs]
696 696
697 697
698 def makeDevices(graph, board, pi): 698 def makeDevices(graph, board, pi):
699 out = [] 699 out = []
700 for dt in sorted(_knownTypes, key=lambda cls: cls.__name__): 700 for dt in sorted(_knownTypes, key=lambda cls: cls.__name__):
701 out.extend(dt.findInstances(graph, board, pi)) 701 out.extend(dt.findInstances(graph, board, pi))
702 return out 702 return out
703