diff --git a/owrx/property/__init__.py b/owrx/property/__init__.py index 41b922c..7e96440 100644 --- a/owrx/property/__init__.py +++ b/owrx/property/__init__.py @@ -183,7 +183,7 @@ class PropertyFilter(PropertyManager): class PropertyDelegator(PropertyManager): def __init__(self, pm: PropertyManager): self.pm = pm - self.pm.wire(self._fireCallbacks) + self.subscription = self.pm.wire(self._fireCallbacks) super().__init__() def __getitem__(self, item): @@ -340,3 +340,27 @@ class PropertyStack(PropertyManager): def keys(self): return set([key for l in self.layers for key in l["props"].keys()]) + + +class PropertyCarousel(PropertyDelegator): + def __init__(self): + # start with an empty dummy layer + super().__init__(PropertyLayer()) + self.layers = {} + + def addLayer(self, key, value): + self.layers[key] = value + + def switch(self, key): + before = self.pm + self.subscription.cancel() + self.pm = self.layers[key] + self.subscription = self.pm.wire(self._fireCallbacks) + changes = {} + for key in set(list(before.keys()) + list(self.keys())): + if key not in self: + changes[key] = PropertyDeleted + else: + if key not in before or before[key] != self[key]: + changes[key] = self[key] + self._fireCallbacks(changes) diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index e325c33..3992e1b 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -9,7 +9,7 @@ import signal from abc import ABC, abstractmethod from owrx.command import CommandMapper from owrx.socket import getAvailablePort -from owrx.property import PropertyStack, PropertyLayer, PropertyFilter +from owrx.property import PropertyStack, PropertyLayer, PropertyFilter, PropertyCarousel from owrx.property.filter import ByLambda from owrx.form import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput from owrx.form.converter import OptionalConverter @@ -67,7 +67,16 @@ class SdrSource(ABC): self.commandMapper = None self.props = PropertyStack() + # layer 0 reserved for profile properties + self.profileCarousel = PropertyCarousel() + if "profiles" in props: + for profile_id, profile in props["profiles"].items(): + profile_stack = PropertyStack() + profile_stack.addLayer(0, PropertyLayer(profile_id=profile_id).readonly()) + profile_stack.addLayer(1, profile) + self.profileCarousel.addLayer(profile_id, profile_stack) + self.props.addLayer(0, PropertyFilter(self.profileCarousel, ByLambda(lambda x: x != "name"))) # layer for runtime writeable properties # these may be set during runtime, but should not be persisted to disk with the configuration @@ -105,9 +114,6 @@ class SdrSource(ABC): if self.isAlwaysOn() and self.state is not SdrSourceState.DISABLED: self.start() - def _loadProfile(self, profile): - self.props.replaceLayer(0, PropertyFilter(profile, ByLambda(lambda x: x != "name"))) - def validateProfiles(self): props = PropertyStack() props.addLayer(1, self.props) @@ -154,20 +160,15 @@ class SdrSource(ABC): return [self.getCommandMapper().map(self.getCommandValues())] def activateProfile(self, profile_id=None): - profiles = self.props["profiles"] if profile_id is None: + profiles = self.props["profiles"] profile_id = list(profiles.keys())[0] - if profile_id not in profiles: - logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id) - return - if profile_id == self.profile_id: - return logger.debug("activating profile {0}".format(profile_id)) - self.props["profile_id"] = profile_id - profile = profiles[profile_id] - self.profile_id = profile_id - - self._loadProfile(profile) + try: + self.profileCarousel.switch(profile_id) + self.profile_id = profile_id + except KeyError: + logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.id) def getId(self): return self.id diff --git a/test/property/test_property_carousel.py b/test/property/test_property_carousel.py new file mode 100644 index 0000000..6c5ff9f --- /dev/null +++ b/test/property/test_property_carousel.py @@ -0,0 +1,78 @@ +from unittest import TestCase +from unittest.mock import Mock +from owrx.property import PropertyCarousel, PropertyLayer, PropertyDeleted + + +class PropertyCarouselTest(TestCase): + def testInitiallyEmpty(self): + pc = PropertyCarousel() + with self.assertRaises(KeyError): + x = pc["testkey"] + + def testPropertyAccess(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="testvalue") + pc.addLayer("test", pl) + pc.switch("test") + self.assertEqual(pc["testkey"], "testvalue") + + def testWriteAccess(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="old_value") + pc.addLayer("test", pl) + pc.switch("test") + pc["testkey"] = "new_value" + self.assertEqual(pc["testkey"], "new_value") + self.assertEqual(pl["testkey"], "new_value") + + def testForwardsEvents(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="old_value") + pc.addLayer("test", pl) + pc.switch("test") + mock = Mock() + pc.wire(mock.method) + pc["testkey"] = "new_value" + mock.method.assert_called_once_with({"testkey": "new_value"}) + + def testStopsForwardingAfterSwitch(self): + pc = PropertyCarousel() + pl_x = PropertyLayer(testkey="old_value") + pc.addLayer("x", pl_x) + pl_y = PropertyLayer(testkey="new_value") + pc.addLayer("y", pl_y) + pc.switch("x") + pc.switch("y") + mock = Mock() + pc.wire(mock.method) + pl_x["testkey"] = "new_value" + mock.method.assert_not_called() + + def testEventsOnSwitch(self): + pc = PropertyCarousel() + pl_x = PropertyLayer(old_key="old_value") + pc.addLayer("x", pl_x) + pl_y = PropertyLayer(new_key="new_value") + pc.addLayer("y", pl_y) + pc.switch("x") + mock = Mock() + pc.wire(mock.method) + pc.switch("y") + mock.method.assert_called_once_with({"old_key": PropertyDeleted, "new_key": "new_value"}) + + def testNoEventsIfKeysDontChange(self): + pc = PropertyCarousel() + pl_x = PropertyLayer(testkey="same_value") + pc.addLayer("x", pl_x) + pl_y = PropertyLayer(testkey="same_value") + pc.addLayer("y", pl_y) + pc.switch("x") + mock = Mock() + pc.wire(mock.method) + pc.switch("y") + mock.method.assert_not_called() + + def testKeyErrorOnInvalidSwitch(self): + pc = PropertyCarousel() + with self.assertRaises(KeyError): + pc.switch("doesntmatter")