diff --git a/light9/collector/collector.py b/light9/collector/collector.py --- a/light9/collector/collector.py +++ b/light9/collector/collector.py @@ -49,7 +49,7 @@ def outputMap(graph, outputs): class Collector(Generic[ClientType, ClientSessionType]): def __init__(self, graph, outputs, listeners=None, clientTimeoutSec=10): - # type: (Graph, List[Output], float) -> None + # type: (Graph, List[Output], List[Listener], float) -> None self.graph = graph self.outputs = outputs self.listeners = listeners diff --git a/light9/effect/settings.py b/light9/effect/settings.py --- a/light9/effect/settings.py +++ b/light9/effect/settings.py @@ -17,13 +17,51 @@ def getVal(graph, subj): return ret class _Settings(object): + """ + default values are 0. Internal rep must not store zeros or some + comparisons will break. + """ def __init__(self, graph, settingsList): self.graph = graph # for looking up all possible attrs self._compiled = {} # dev: { attr: val } for row in settingsList: self._compiled.setdefault(row[0], {})[row[1]] = row[2] # self._compiled may not be final yet- see _fromCompiled + self._delZeros() + + @classmethod + def _fromCompiled(cls, graph, compiled): + obj = cls(graph, []) + obj._compiled = compiled + obj._delZeros() + return obj + + @classmethod + def fromResource(cls, graph, subj): + settingsList = [] + with graph.currentState() as g: + for s in g.objects(subj, L9['setting']): + d = g.value(s, L9['device']) + da = g.value(s, L9['deviceAttr']) + v = getVal(g, s) + settingsList.append((d, da, v)) + return cls(graph, settingsList) + @classmethod + def fromVector(cls, graph, vector): + compiled = {} + for (d, a), v in zip(cls(graph, [])._vectorKeys(), vector): + compiled.setdefault(d, {})[a] = v + return cls._fromCompiled(graph, compiled) + + def _delZeros(self): + for dev, av in self._compiled.items(): + for attr, val in av.items(): + if val == 0: + del av[attr] + if not av: + del self._compiled[dev] + def __hash__(self): itemed = tuple([(d, tuple([(a, v) for a, v in sorted(av.items())])) for d, av in sorted(self._compiled.items())]) @@ -54,30 +92,6 @@ class _Settings(object): def getValue(self, dev, attr): return self._compiled.get(dev, {}).get(attr, 0) - - @classmethod - def _fromCompiled(cls, graph, compiled): - obj = cls(graph, []) - obj._compiled = compiled - return obj - - @classmethod - def fromResource(cls, graph, subj): - settingsList = [] - with graph.currentState() as g: - for s in g.objects(subj, L9['setting']): - d = g.value(s, L9['device']) - da = g.value(s, L9['deviceAttr']) - v = getVal(g, s) - settingsList.append((d, da, v)) - return cls(graph, settingsList) - - @classmethod - def fromVector(cls, graph, vector): - compiled = {} - for (d, a), v in zip(cls(graph, [])._vectorKeys(), vector): - compiled.setdefault(d, {})[a] = v - return cls._fromCompiled(graph, compiled) def _vectorKeys(self): """stable order of all the dev,attr pairs for this type of settings""" @@ -118,7 +132,7 @@ class _Settings(object): dist += abs(attrs1[key] - attrs2[key]) return dist - def addStatements(self, subj, ctx, settingRoot, settingsSubgraphCache): + def statements(self, subj, ctx, settingRoot, settingsSubgraphCache): """ settingRoot can be shared across images (or even wider if you want) """ @@ -141,7 +155,7 @@ class _Settings(object): ]) settingsSubgraphCache.add(setting) - self.graph.patch(Patch(addQuads=add)) + return add class DeviceSettings(_Settings): diff --git a/light9/effect/settings_test.py b/light9/effect/settings_test.py --- a/light9/effect/settings_test.py +++ b/light9/effect/settings_test.py @@ -1,4 +1,6 @@ import unittest +from rdflib import Literal +from light9.rdfdb.patch import Patch from light9.rdfdb.localsyncedgraph import LocalSyncedGraph from light9.namespaces import RDF, L9, DEV from light9.effect.settings import DeviceSettings @@ -11,3 +13,84 @@ class TestDeviceSettings(unittest.TestCa def testToVectorZero(self): ds = DeviceSettings(self.graph, []) self.assertEqual([0] * 20, ds.toVector()) + + def testEq(self): + s1 = DeviceSettings(self.graph, [ + (L9['light1'], L9['attr1'], 0.5), + (L9['light1'], L9['attr2'], 0.3), + ]) + s2 = DeviceSettings(self.graph, [ + (L9['light1'], L9['attr2'], 0.3), + (L9['light1'], L9['attr1'], 0.5), + ]) + self.assertTrue(s1 == s2) + self.assertFalse(s1 != s2) + + def testMissingFieldsEqZero(self): + self.assertEqual( + DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0),]), + DeviceSettings(self.graph, [])) + + def testFromResource(self): + ctx = L9[''] + self.graph.patch(Patch(addQuads=[ + (L9['foo'], L9['setting'], L9['foo_set0'], ctx), + (L9['foo_set0'], L9['device'], L9['light1'], ctx), + (L9['foo_set0'], L9['deviceAttr'], L9['brightness'], ctx), + (L9['foo_set0'], L9['value'], Literal(0.1), ctx), + (L9['foo'], L9['setting'], L9['foo_set1'], ctx), + (L9['foo_set1'], L9['device'], L9['light1'], ctx), + (L9['foo_set1'], L9['deviceAttr'], L9['speed'], ctx), + (L9['foo_set1'], L9['scaledValue'], Literal(0.2), ctx), + ])) + s = DeviceSettings.fromResource(self.graph, L9['foo']) + + self.assertEqual(DeviceSettings(self.graph, [ + (L9['light1'], L9['brightness'], 0.1), + (L9['light1'], L9['speed'], 0.2), + ]), s) + + def testToVector(self): + v = DeviceSettings(self.graph, [ + (DEV['aura1'], L9['rx'], 0.5), + ]).toVector() + self.assertEqual( + [0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], v) + + def testFromVector(self): + s = DeviceSettings.fromVector( + self.graph, + [0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + + self.assertEqual(DeviceSettings(self.graph, [ + (DEV['aura1'], L9['rx'], 0.5), + ]), s) + + def testAsList(self): + sets = [ + (L9['light1'], L9['attr2'], 0.3), + (L9['light1'], L9['attr1'], 0.5), + ] + self.assertItemsEqual(sets, DeviceSettings(self.graph, sets).asList()) + + def testDevices(self): + s = DeviceSettings(self.graph, [ + (L9['aura1'], L9['rx'], 0), + (L9['aura2'], L9['rx'], 0.1), + ]) + # aura1 is all defaults (zeros), so it doesn't get listed + self.assertItemsEqual([L9['aura2']], s.devices()) + + def testAddStatements(self): + s = DeviceSettings(self.graph, [ + (L9['aura2'], L9['rx'], 0.1), + ]) + stmts = s.statements(L9['foo'], L9['ctx1'], L9['s_'], set()) + self.maxDiff=None + self.assertItemsEqual([ + (L9['foo'], L9['setting'], L9['s_set8011962'], L9['ctx1']), + (L9['s_set8011962'], L9['device'], L9['aura2'], L9['ctx1']), + (L9['s_set8011962'], L9['deviceAttr'], L9['rx'], L9['ctx1']), + (L9['s_set8011962'], L9['value'], Literal(0.1), L9['ctx1']), + ], stmts) + diff --git a/light9/paint/capture.py b/light9/paint/capture.py --- a/light9/paint/capture.py +++ b/light9/paint/capture.py @@ -6,10 +6,10 @@ from light9.namespaces import L9 from light9.paint.solve import loadNumPy def writeCaptureDescription(graph, ctx, uri, dev, relOutPath, settingsSubgraphCache, settings): - settings.addStatements( + graph.patch(Patch(addQuads=settings.statements( uri, ctx=ctx, settingRoot=URIRef('/'.join([showconfig.showUri(), 'capture', dev.rsplit('/')[1]])), - settingsSubgraphCache=settingsSubgraphCache) + settingsSubgraphCache=settingsSubgraphCache))) graph.patch(Patch(addQuads=[ (dev, L9['capture'], uri, ctx), (uri, L9['imagePath'], URIRef('/'.join([showconfig.showUri(), relOutPath])), ctx), diff --git a/light9/paint/solve.py b/light9/paint/solve.py --- a/light9/paint/solve.py +++ b/light9/paint/solve.py @@ -149,7 +149,7 @@ class Solver(object): settings.append((dev, attr, toHex(rgb))) else: settings.append((dev, attr, xLeft.pop())) - return settings + return DeviceSettings(self.graph, settings) def drawError(x): diff --git a/light9/rdfdb/localsyncedgraph.py b/light9/rdfdb/localsyncedgraph.py --- a/light9/rdfdb/localsyncedgraph.py +++ b/light9/rdfdb/localsyncedgraph.py @@ -3,6 +3,7 @@ from rdflib import ConjunctiveGraph from light9.rdfdb.currentstategraphapi import CurrentStateGraphApi from light9.rdfdb.autodepgraphapi import AutoDepGraphApi from light9.rdfdb.grapheditapi import GraphEditApi +from light9.rdfdb.rdflibpatch import patchQuads class LocalSyncedGraph(CurrentStateGraphApi, AutoDepGraphApi, GraphEditApi): """for tests""" @@ -11,3 +12,10 @@ class LocalSyncedGraph(CurrentStateGraph for f in files or []: self._graph.parse(f, format='n3') + + def patch(self, p): + patchQuads(self._graph, + deleteQuads=p.delQuads, + addQuads=p.addQuads, + perfect=True) + # no deps