run the code formatter over all

This commit is contained in:
Jakob Ketterl 2021-01-20 17:01:46 +01:00
parent f0dc2f8ebe
commit 64b7b485b3
37 changed files with 268 additions and 203 deletions

View File

@ -198,10 +198,7 @@ class dsp(object):
"csdr limit_ff", "csdr limit_ff",
] ]
chain += last_decimation_block chain += last_decimation_block
chain += [ chain += ["csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}", "csdr convert_f_s16"]
"csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}",
"csdr convert_f_s16"
]
elif self.isDigitalVoice(which): elif self.isDigitalVoice(which):
chain += ["csdr fmdemod_quadri_cf"] chain += ["csdr fmdemod_quadri_cf"]
chain += last_decimation_block chain += last_decimation_block
@ -460,7 +457,9 @@ class dsp(object):
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value self.secondary_offset_freq = value
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"): if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.pipes["secondary_shift_pipe"].write(
"%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())
)
def stop_secondary_demodulator(self): def stop_secondary_demodulator(self):
if not self.secondary_processes_running: if not self.secondary_processes_running:
@ -581,7 +580,7 @@ class dsp(object):
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"] return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w"]
def isJs8(self, demodulator = None): def isJs8(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator == "js8" return demodulator == "js8"
@ -689,7 +688,11 @@ class dsp(object):
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level self.squelch_level = squelch_level
# no squelch required on digital voice modes # no squelch required on digital voice modes
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() else self.squelch_level actual_squelch = (
-150
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV()
else self.squelch_level
)
if self.running: if self.running:
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch))) self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
@ -842,6 +845,7 @@ class dsp(object):
self.start_secondary_demodulator() self.start_secondary_demodulator()
if self.has_pipe("smeter_pipe"): if self.has_pipe("smeter_pipe"):
def read_smeter(): def read_smeter():
raw = self.pipes["smeter_pipe"].readline() raw = self.pipes["smeter_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:
@ -851,6 +855,7 @@ class dsp(object):
self.output.send_output("smeter", read_smeter) self.output.send_output("smeter", read_smeter)
if self.has_pipe("meta_pipe"): if self.has_pipe("meta_pipe"):
def read_meta(): def read_meta():
raw = self.pipes["meta_pipe"].readline() raw = self.pipes["meta_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:

View File

@ -42,6 +42,7 @@ class Pipe(object):
immediately here), resulting in empty reads until data is available. This is handled specially in the immediately here), resulting in empty reads until data is available. This is handled specially in the
ReadingPipe class. ReadingPipe class.
""" """
def opener(path, flags): def opener(path, flags):
fd = os.open(path, flags | os.O_NONBLOCK) fd = os.open(path, flags | os.O_NONBLOCK)
os.set_blocking(fd, True) os.set_blocking(fd, True)
@ -88,7 +89,7 @@ class WritingPipe(Pipe):
except OSError as error: except OSError as error:
# ENXIO = FIFO has not been opened for reading # ENXIO = FIFO has not been opened for reading
if error.errno == 6: if error.errno == 6:
time.sleep(.1) time.sleep(0.1)
retries += 1 retries += 1
else: else:
raise raise

View File

@ -40,9 +40,7 @@ Support and info: https://groups.io/g/openwebrx
configErrors = Config.validateConfig() configErrors = Config.validateConfig()
if configErrors: if configErrors:
logger.error( logger.error("your configuration contains errors. please address the following errors:")
"your configuration contains errors. please address the following errors:"
)
for e in configErrors: for e in configErrors:
logger.error(e) logger.error(e)
return return

View File

@ -69,7 +69,9 @@ class DecoderQueue(Queue):
with DecoderQueue.creationLock: with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is None: if DecoderQueue.sharedInstance is None:
pm = Config.get() pm = Config.get()
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"]) DecoderQueue.sharedInstance = DecoderQueue(
maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"]
)
return DecoderQueue.sharedInstance return DecoderQueue.sharedInstance
@staticmethod @staticmethod

View File

@ -17,7 +17,7 @@ class Band(object):
for (mode, freqs) in dict["frequencies"].items(): for (mode, freqs) in dict["frequencies"].items():
if mode not in availableModes: if mode not in availableModes:
logger.info( logger.info(
"Modulation \"{mode}\" is not available, bandplan bookmark will not be displayed".format( 'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format(
mode=mode mode=mode
) )
) )

View File

@ -112,9 +112,7 @@ class Config:
@staticmethod @staticmethod
def validateConfig(): def validateConfig():
pm = Config.get() pm = Config.get()
errors = [ errors = [Config.checkTempDirectory(pm)]
Config.checkTempDirectory(pm)
]
return [e for e in errors if e is not None] return [e for e in errors if e is not None]

View File

@ -7,7 +7,9 @@ class Controller(object):
self.request = request self.request = request
self.options = options self.options = options
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None): def send_response(
self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None
):
self.handler.send_response(code) self.handler.send_response(code)
if headers is None: if headers is None:
headers = {} headers = {}
@ -27,7 +29,7 @@ class Controller(object):
def send_redirect(self, location, code=303, cookies=None): def send_redirect(self, location, code=303, cookies=None):
self.handler.send_response(code) self.handler.send_response(code)
if cookies is not None: if cookies is not None:
self.handler.send_header("Set-Cookie", cookies.output(header='')) self.handler.send_header("Set-Cookie", cookies.output(header=""))
self.handler.send_header("Location", location) self.handler.send_header("Location", location)
self.handler.end_headers() self.handler.end_headers()

View File

@ -13,9 +13,9 @@ logger = logging.getLogger(__name__)
class GzipMixin(object): class GzipMixin(object):
def send_response(self, content, headers=None, content_type="text/html", *args, **kwargs): def send_response(self, content, headers=None, content_type="text/html", *args, **kwargs):
if self.zipable(content_type) and "accept-encoding" in self.request.headers: if self.zipable(content_type) and "accept-encoding" in self.request.headers:
accepted = [s.strip().lower() for s in self.request.headers['accept-encoding'].split(",")] accepted = [s.strip().lower() for s in self.request.headers["accept-encoding"].split(",")]
if "gzip" in accepted: if "gzip" in accepted:
if type(content) == str: if type(content) == str:
content = content.encode() content = content.encode()
@ -26,11 +26,7 @@ class GzipMixin(object):
super().send_response(content, headers=headers, content_type=content_type, *args, **kwargs) super().send_response(content, headers=headers, content_type=content_type, *args, **kwargs)
def zipable(self, content_type): def zipable(self, content_type):
types = [ types = ["application/javascript", "text/css", "text/html"]
"application/javascript",
"text/css",
"text/html"
]
return content_type in types return content_type in types
def gzip(self, content): def gzip(self, content):
@ -41,11 +37,11 @@ class ModificationAwareController(Controller, metaclass=ABCMeta):
@abstractmethod @abstractmethod
def getModified(self, file): def getModified(self, file):
pass pass
def wasModified(self, file): def wasModified(self, file):
try: try:
modified = self.getModified(file).replace(microsecond=0) modified = self.getModified(file).replace(microsecond=0)
if modified is not None and "If-Modified-Since" in self.handler.headers: if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime( client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z" self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
@ -54,7 +50,7 @@ class ModificationAwareController(Controller, metaclass=ABCMeta):
return False return False
except FileNotFoundError: except FileNotFoundError:
pass pass
return True return True
@ -143,7 +139,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
"lib/settings/Input.js", "lib/settings/Input.js",
"lib/settings/SdrDevice.js", "lib/settings/SdrDevice.js",
"settings.js", "settings.js",
] ],
} }
def indexAction(self): def indexAction(self):

View File

@ -8,15 +8,19 @@ class ReceiverIdController(Controller):
super().__init__(handler, request, options) super().__init__(handler, request, options)
self.authHeader = None self.authHeader = None
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None): def send_response(
self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None
):
if self.authHeader is not None: if self.authHeader is not None:
if headers is None: if headers is None:
headers = {} headers = {}
headers['Authorization'] = self.authHeader headers["Authorization"] = self.authHeader
super().send_response(content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers) super().send_response(
content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers
)
pass pass
def handle_request(self): def handle_request(self):
if "Authorization" in self.request.headers: if "Authorization" in self.request.headers:
self.authHeader = ReceiverId.getResponseHeader(self.request.headers['Authorization']) self.authHeader = ReceiverId.getResponseHeader(self.request.headers["Authorization"])
super().handle_request() super().handle_request()

View File

@ -69,12 +69,16 @@ class SdrSettingsController(AdminController):
{form} {form}
</div> </div>
</div> </div>
""".format(device_name=config["name"], form=self.render_form(device_id, config)) """.format(
device_name=config["name"], form=self.render_form(device_id, config)
)
def render_form(self, device_id, config): def render_form(self, device_id, config):
return """ return """
<form class="sdrdevice" data-config="{formdata}"></form> <form class="sdrdevice" data-config="{formdata}"></form>
""".format(device_id=device_id, formdata=quote(json.dumps(config))) """.format(
device_id=device_id, formdata=quote(json.dumps(config))
)
def indexAction(self): def indexAction(self):
self.serve_template("sdrsettings.html", **self.template_variables()) self.serve_template("sdrsettings.html", **self.template_variables())
@ -119,12 +123,18 @@ class GeneralSettingsController(AdminController):
DropdownInput( DropdownInput(
"audio_compression", "audio_compression",
"Audio compression", "Audio compression",
options=[Option("adpcm", "ADPCM"), Option("none", "None"),], options=[
Option("adpcm", "ADPCM"),
Option("none", "None"),
],
), ),
DropdownInput( DropdownInput(
"fft_compression", "fft_compression",
"Waterfall compression", "Waterfall compression",
options=[Option("adpcm", "ADPCM"), Option("none", "None"),], options=[
Option("adpcm", "ADPCM"),
Option("none", "None"),
],
), ),
), ),
Section( Section(
@ -196,10 +206,7 @@ class GeneralSettingsController(AdminController):
"Js8Call decoding depth", "Js8Call decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu", infotext="A higher decoding depth will allow more results, but will also consume more cpu",
), ),
Js8ProfileCheckboxInput( Js8ProfileCheckboxInput("js8_enabled_profiles", "Js8Call enabled modes"),
"js8_enabled_profiles",
"Js8Call enabled modes"
),
), ),
Section( Section(
"Background decoding", "Background decoding",
@ -269,9 +276,7 @@ class GeneralSettingsController(AdminController):
def processFormData(self): def processFormData(self):
data = parse_qs(self.get_body().decode("utf-8")) data = parse_qs(self.get_body().decode("utf-8"))
data = { data = {k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()}
k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()
}
config = Config.get() config = Config.get()
for k, v in data.items(): for k, v in data.items():
config[k] = v config[k] = v

View File

@ -22,7 +22,7 @@ class StatusController(ReceiverIdController):
"name": receiver.getName(), "name": receiver.getName(),
# TODO would be better to have types from the config here # TODO would be better to have types from the config here
"type": type(receiver).__name__, "type": type(receiver).__name__,
"profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()] "profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()],
} }
return stats return stats
@ -38,6 +38,6 @@ class StatusController(ReceiverIdController):
}, },
"max_clients": pm["max_clients"], "max_clients": pm["max_clients"],
"version": openwebrx_version, "version": openwebrx_version,
"sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()] "sdrs": [self.getReceiverStats(r) for r in SdrService.getSources().values()],
} }
self.send_response(json.dumps(status), content_type="application/json") self.send_response(json.dumps(status), content_type="application/json")

View File

@ -152,7 +152,14 @@ class FeatureDetector(object):
# prevent X11 programs from opening windows if called from a GUI shell # prevent X11 programs from opening windows if called from a GUI shell
env.pop("DISPLAY", None) env.pop("DISPLAY", None)
try: try:
process = subprocess.Popen(cmd, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir, env=env) process = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=tmp_dir,
env=env,
)
rc = process.wait() rc = process.wait()
if expected_result is None: if expected_result is None:
return rc != 32512 return rc != 32512
@ -214,7 +221,6 @@ class FeatureDetector(object):
""" """
return self.command_is_runnable("perseustest -h") return self.command_is_runnable("perseustest -h")
def has_digiham(self): def has_digiham(self):
""" """
To use digital voice modes, the digiham package is required. You can find the package and installation To use digital voice modes, the digiham package is required. You can find the package and installation
@ -547,4 +553,4 @@ class FeatureDetector(object):
You can find more information [here](https://github.com/jketterl/eb200_connector). You can find more information [here](https://github.com/jketterl/eb200_connector).
""" """
return self._check_connector("eb200_connector") return self._check_connector("eb200_connector")

View File

@ -10,9 +10,7 @@ class Input(ABC):
self.infotext = infotext self.infotext = infotext
def bootstrap_decorate(self, input): def bootstrap_decorate(self, input):
infotext = ( infotext = "<small>{text}</small>".format(text=self.infotext) if self.infotext else ""
"<small>{text}</small>".format(text=self.infotext) if self.infotext else ""
)
return """ return """
<div class="form-group row"> <div class="form-group row">
<label class="col-form-label col-form-label-sm col-3" for="{id}">{label}</label> <label class="col-form-label col-form-label-sm col-3" for="{id}">{label}</label>
@ -108,9 +106,7 @@ class LocationInput(Input):
) )
def parse(self, data): def parse(self, data):
return { return {self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}}
self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}
}
class TextAreaInput(Input): class TextAreaInput(Input):
@ -195,9 +191,7 @@ class MultiCheckboxInput(Input):
class ServicesCheckboxInput(MultiCheckboxInput): class ServicesCheckboxInput(MultiCheckboxInput):
def __init__(self, id, label, infotext=None): def __init__(self, id, label, infotext=None):
services = [ services = [Option(s.modulation, s.name) for s in Modes.getAvailableServices()]
Option(s.modulation, s.name) for s in Modes.getAvailableServices()
]
super().__init__(id, label, services, infotext) super().__init__(id, label, services, infotext)

View File

@ -1,14 +1,6 @@
from owrx.controllers.status import StatusController from owrx.controllers.status import StatusController
from owrx.controllers.template import ( from owrx.controllers.template import IndexController, MapController, FeatureController
IndexController, from owrx.controllers.assets import OwrxAssetsController, AprsSymbolsController, CompiledAssetsController
MapController,
FeatureController
)
from owrx.controllers.assets import (
OwrxAssetsController,
AprsSymbolsController,
CompiledAssetsController
)
from owrx.controllers.websocket import WebSocketController from owrx.controllers.websocket import WebSocketController
from owrx.controllers.api import ApiController from owrx.controllers.api import ApiController
from owrx.controllers.metrics import MetricsController from owrx.controllers.metrics import MetricsController
@ -109,7 +101,9 @@ class Router(object):
StaticRoute("/metrics", MetricsController), StaticRoute("/metrics", MetricsController),
StaticRoute("/settings", SettingsController), StaticRoute("/settings", SettingsController),
StaticRoute("/generalsettings", GeneralSettingsController), StaticRoute("/generalsettings", GeneralSettingsController),
StaticRoute("/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}), StaticRoute(
"/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}
),
StaticRoute("/sdrsettings", SdrSettingsController), StaticRoute("/sdrsettings", SdrSettingsController),
StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, options={"action": "loginAction"}),
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),

View File

@ -102,15 +102,17 @@ class Js8Parser(Parser):
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
frame.callsign, LocatorLocation(frame.grid), "JS8", self.band frame.callsign, LocatorLocation(frame.grid), "JS8", self.band
) )
ReportingEngine.getSharedInstance().spot({ ReportingEngine.getSharedInstance().spot(
"callsign": frame.callsign, {
"mode": "JS8", "callsign": frame.callsign,
"locator": frame.grid, "mode": "JS8",
"freq": self.dial_freq + frame.freq, "locator": frame.grid,
"db": frame.db, "freq": self.dial_freq + frame.freq,
"timestamp": frame.timestamp, "db": frame.db,
"msg": str(frame) "timestamp": frame.timestamp,
}) "msg": str(frame),
}
)
except Exception: except Exception:
logger.exception("error while parsing js8 message") logger.exception("error while parsing js8 message")

View File

@ -13,6 +13,7 @@ TFESC = 0xDD
FEET_PER_METER = 3.28084 FEET_PER_METER = 3.28084
class DirewolfConfig(object): class DirewolfConfig(object):
def getConfig(self, port, is_service): def getConfig(self, port, is_service):
pm = Config.get() pm = Config.get()
@ -40,7 +41,7 @@ IGLOGIN {callsign} {password}
) )
if pm["aprs_igate_beacon"]: if pm["aprs_igate_beacon"]:
#Format beacon lat/lon # Format beacon lat/lon
lat = pm["receiver_gps"]["lat"] lat = pm["receiver_gps"]["lat"]
lon = pm["receiver_gps"]["lon"] lon = pm["receiver_gps"]["lon"]
direction_ns = "N" if lat > 0 else "S" direction_ns = "N" if lat > 0 else "S"
@ -50,13 +51,13 @@ IGLOGIN {callsign} {password}
lat = "{0:02d}^{1:05.2f}{2}".format(int(lat), (lat - int(lat)) * 60, direction_ns) lat = "{0:02d}^{1:05.2f}{2}".format(int(lat), (lat - int(lat)) * 60, direction_ns)
lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we) lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we)
#Format beacon details # Format beacon details
symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&" symbol = str(pm["aprs_igate_symbol"]) if "aprs_igate_symbol" in pm else "R&"
gain = "GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else "" gain = "GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else ""
adir = "DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else "" adir = "DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else ""
comment = str(pm["aprs_igate_comment"]) if "aprs_igate_comment" in pm else "\"OpenWebRX APRS gateway\"" comment = str(pm["aprs_igate_comment"]) if "aprs_igate_comment" in pm else '"OpenWebRX APRS gateway"'
#Convert height from meters to feet if specified # Convert height from meters to feet if specified
height = "" height = ""
if "aprs_igate_height" in pm: if "aprs_igate_height" in pm:
try: try:
@ -64,18 +65,21 @@ IGLOGIN {callsign} {password}
height_ft = round(height_m * FEET_PER_METER) height_ft = round(height_m * FEET_PER_METER)
height = "HEIGHT=" + str(height_ft) height = "HEIGHT=" + str(height_ft)
except: except:
logger.error("Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"])) logger.error(
"Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"])
)
if((len(comment) > 0) and ((comment[0] != '"') or (comment[len(comment)-1] != '"'))): if (len(comment) > 0) and ((comment[0] != '"') or (comment[len(comment) - 1] != '"')):
comment = "\"" + comment + "\"" comment = '"' + comment + '"'
elif(len(comment) == 0): elif len(comment) == 0:
comment = "\"\"" comment = '""'
pbeacon = "PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment={comment}".format( pbeacon = "PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment={comment}".format(
symbol=symbol, lat=lat, lon=lon, height=height, gain=gain, adir=adir, comment=comment ) symbol=symbol, lat=lat, lon=lon, height=height, gain=gain, adir=adir, comment=comment
)
logger.info("APRS PBEACON String: " + pbeacon) logger.info("APRS PBEACON String: " + pbeacon)
config += "\n" + pbeacon + "\n" config += "\n" + pbeacon + "\n"
return config return config
@ -98,7 +102,7 @@ class KissClient(object):
pass pass
def __init__(self, port): def __init__(self, port):
delay = .5 delay = 0.5
retries = 0 retries = 0
while True: while True:
try: try:

View File

@ -60,24 +60,47 @@ class Modes(object):
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)), AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)), AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)),
AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False), AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
AnalogMode("dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False), AnalogMode(
"dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False
),
AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False), AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False), AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
AnalogMode("m17", "M17", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_m17"], squelch=False), AnalogMode("m17", "M17", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_m17"], squelch=False),
AnalogMode("freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False), AnalogMode(
"freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False
),
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False), AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
DigitalMode("ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), DigitalMode(
DigitalMode("ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), "ft8", "FT8", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
DigitalMode("jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), ),
DigitalMode("jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True), DigitalMode(
"ft4", "FT4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode(
"jt65", "JT65", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode(
"jt9", "JT9", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x"], service=True
),
DigitalMode( DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True "wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True
), ),
DigitalMode("fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True), DigitalMode(
DigitalMode("fst4w", "FST4W", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"], service=True), "fst4", "FST4", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["wsjt-x-2-3"], service=True
DigitalMode("js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True), ),
DigitalMode(
"fst4w",
"FST4W",
underlying=["usb"],
bandpass=Bandpass(1350, 1650),
requirements=["wsjt-x-2-3"],
service=True,
),
DigitalMode(
"js8", "JS8Call", underlying=["usb"], bandpass=Bandpass(0, 3000), requirements=["js8call"], service=True
),
DigitalMode( DigitalMode(
"packet", "packet",
"Packet", "Packet",

View File

@ -150,10 +150,10 @@ class Uploader(object):
# id # id
[0x00, 0x03] [0x00, 0x03]
# length # length
+ list(length.to_bytes(2, 'big')) + list(length.to_bytes(2, "big"))
+ Uploader.receieverDelimiter + Uploader.receieverDelimiter
# number of fields # number of fields
+ list(num_fields.to_bytes(2, 'big')) + list(num_fields.to_bytes(2, "big"))
# padding # padding
+ [0x00, 0x00] + [0x00, 0x00]
# receiverCallsign # receiverCallsign
@ -163,9 +163,7 @@ class Uploader(object):
# decodingSoftware # decodingSoftware
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
# antennaInformation # antennaInformation
+ ( + ([0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else [])
[0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else []
)
# padding # padding
+ [0x00, 0x00] + [0x00, 0x00]
) )

View File

@ -77,6 +77,7 @@ class ReceiverId(object):
return Key(keyString) return Key(keyString)
except KeyException as e: except KeyException as e:
logger.error(e) logger.error(e)
config = Config.get() config = Config.get()
if "receiver_keys" not in config or config["receiver_keys"] is None: if "receiver_keys" not in config or config["receiver_keys"] is None:
return None return None

View File

@ -40,10 +40,12 @@ class ReportingEngine(object):
if "pskreporter_enabled" in config and config["pskreporter_enabled"]: if "pskreporter_enabled" in config and config["pskreporter_enabled"]:
# inline import due to circular dependencies # inline import due to circular dependencies
from owrx.pskreporter import PskReporter from owrx.pskreporter import PskReporter
self.reporters += [PskReporter()] self.reporters += [PskReporter()]
if "wsprnet_enabled" in config and config["wsprnet_enabled"]: if "wsprnet_enabled" in config and config["wsprnet_enabled"]:
# inline import due to circular dependencies # inline import due to circular dependencies
from owrx.wsprnet import WsprnetReporter from owrx.wsprnet import WsprnetReporter
self.reporters += [WsprnetReporter()] self.reporters += [WsprnetReporter()]
def stop(self): def stop(self):

View File

@ -137,9 +137,7 @@ class ServiceHandler(SdrSourceEventClient):
dials = [ dials = [
dial dial
for dial in Bandplan.getSharedInstance().collectDialFrequencies( for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range)
frequency_range
)
if self.isSupported(dial["mode"]) if self.isSupported(dial["mode"])
] ]
@ -150,16 +148,12 @@ class ServiceHandler(SdrSourceEventClient):
groups = self.optimizeResampling(dials, sr) groups = self.optimizeResampling(dials, sr)
if groups is None: if groups is None:
for dial in dials: for dial in dials:
self.services.append( self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
self.setupService(dial["mode"], dial["frequency"], self.source)
)
else: else:
for group in groups: for group in groups:
cf = self.get_center_frequency(group) cf = self.get_center_frequency(group)
bw = self.get_bandwidth(group) bw = self.get_bandwidth(group)
logger.debug( logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
"group center frequency: {0}, bandwidth: {1}".format(cf, bw)
)
resampler_props = PropertyLayer() resampler_props = PropertyLayer()
resampler_props["center_freq"] = cf resampler_props["center_freq"] = cf
resampler_props["samp_rate"] = bw resampler_props["samp_rate"] = bw
@ -167,11 +161,7 @@ class ServiceHandler(SdrSourceEventClient):
resampler.start() resampler.start()
for dial in group: for dial in group:
self.services.append( self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
self.setupService(
dial["mode"], dial["frequency"], resampler
)
)
# resampler goes in after the services since it must not be shutdown as long as the services are still running # resampler goes in after the services since it must not be shutdown as long as the services are still running
self.services.append(resampler) self.services.append(resampler)
@ -238,9 +228,7 @@ class ServiceHandler(SdrSourceEventClient):
results = sorted(usages, key=lambda f: f["total_bandwidth"]) results = sorted(usages, key=lambda f: f["total_bandwidth"])
for r in results: for r in results:
logger.debug( logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"]))
"splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])
)
best = results[0] best = results[0]
if best["num_splits"] is None: if best["num_splits"] is None:
@ -267,7 +255,7 @@ class ServiceHandler(SdrSourceEventClient):
d.set_secondary_demodulator(mode) d.set_secondary_demodulator(mode)
d.set_audio_compression("none") d.set_audio_compression("none")
d.set_samp_rate(source.getProps()["samp_rate"]) d.set_samp_rate(source.getProps()["samp_rate"])
d.set_temporary_directory(Config.get()['temporary_directory']) d.set_temporary_directory(Config.get()["temporary_directory"])
d.set_service() d.set_service()
d.start() d.start()
return d return d

View File

@ -68,6 +68,7 @@ class DatetimeScheduleEntry(ScheduleEntry):
def getNextActivation(self): def getNextActivation(self):
return self.startTime return self.startTime
class Schedule(ABC): class Schedule(ABC):
@staticmethod @staticmethod
def parse(props): def parse(props):
@ -140,7 +141,7 @@ class DaylightSchedule(TimerangeSchedule):
degtorad = math.pi / 180 degtorad = math.pi / 180
radtodeg = 180 / math.pi radtodeg = 180 / math.pi
#Number of days since 01/01 # Number of days since 01/01
days = date.timetuple().tm_yday days = date.timetuple().tm_yday
# Longitudinal correction # Longitudinal correction

View File

@ -82,17 +82,17 @@ class SdrSource(ABC):
for id, p in self.props["profiles"].items(): for id, p in self.props["profiles"].items():
props.replaceLayer(0, self._getProfilePropertyLayer(p)) props.replaceLayer(0, self._getProfilePropertyLayer(p))
if "center_freq" not in props: if "center_freq" not in props:
logger.warning("Profile \"%s\" does not specify a center_freq", id) logger.warning('Profile "%s" does not specify a center_freq', id)
continue continue
if "samp_rate" not in props: if "samp_rate" not in props:
logger.warning("Profile \"%s\" does not specify a samp_rate", id) logger.warning('Profile "%s" does not specify a samp_rate', id)
continue continue
if "start_freq" in props: if "start_freq" in props:
start_freq = props["start_freq"] start_freq = props["start_freq"]
srh = props["samp_rate"] / 2 srh = props["samp_rate"] / 2
center_freq = props["center_freq"] center_freq = props["center_freq"]
if start_freq < center_freq - srh or start_freq > center_freq + srh: if start_freq < center_freq - srh or start_freq > center_freq + srh:
logger.warning("start_freq for profile \"%s\" is out of range", id) logger.warning('start_freq for profile "%s" is out of range', id)
def _getProfilePropertyLayer(self, profile): def _getProfilePropertyLayer(self, profile):
layer = PropertyLayer() layer = PropertyLayer()

View File

@ -15,18 +15,22 @@ class ConnectorSource(SdrSource):
super().__init__(id, props) super().__init__(id, props)
def getCommandMapper(self): def getCommandMapper(self):
return super().getCommandMapper().setMappings( return (
{ super()
"samp_rate": Option("-s"), .getCommandMapper()
"tuner_freq": Option("-f"), .setMappings(
"port": Option("-p"), {
"controlPort": Option("-c"), "samp_rate": Option("-s"),
"device": Option("-d"), "tuner_freq": Option("-f"),
"iqswap": Flag("-i"), "port": Option("-p"),
"rtltcp_compat": Option("-r"), "controlPort": Option("-c"),
"ppm": Option("-P"), "device": Option("-d"),
"rf_gain": Option("-g"), "iqswap": Flag("-i"),
} "rtltcp_compat": Option("-r"),
"ppm": Option("-P"),
"rf_gain": Option("-g"),
}
)
) )
def sendControlMessage(self, changes): def sendControlMessage(self, changes):

View File

@ -32,11 +32,14 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
"These depend on nmux_memory and samp_rate options in config_webrx.py" "These depend on nmux_memory and samp_rate options in config_webrx.py"
) )
return ["nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % ( return [
nmux_bufsize, "nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1"
nmux_bufcnt, % (
self.port, nmux_bufsize,
)] nmux_bufcnt,
self.port,
)
]
def getCommand(self): def getCommand(self):
return super().getCommand() + self.getFormatConversion() + self.getNmuxCommand() return super().getCommand() + self.getFormatConversion() + self.getNmuxCommand()

View File

@ -8,8 +8,10 @@ class Eb200Source(ConnectorSource):
super() super()
.getCommandMapper() .getCommandMapper()
.setBase("eb200_connector") .setBase("eb200_connector")
.setMappings({ .setMappings(
"long": Flag("-l"), {
"remote": Argument(), "long": Flag("-l"),
}) "remote": Argument(),
}
)
) )

View File

@ -9,9 +9,13 @@ logger = logging.getLogger(__name__)
class FifiSdrSource(DirectSource): class FifiSdrSource(DirectSource):
def getCommandMapper(self): def getCommandMapper(self):
return super().getCommandMapper().setBase("arecord").setMappings( return (
{"device": Option("-D"), "samp_rate": Option("-r")} super()
).setStatic("-t raw -f S16_LE -c2 -") .getCommandMapper()
.setBase("arecord")
.setMappings({"device": Option("-D"), "samp_rate": Option("-r")})
.setStatic("-t raw -f S16_LE -c2 -")
)
def getEventNames(self): def getEventNames(self):
return super().getEventNames() + ["device"] return super().getEventNames() + ["device"]
@ -20,7 +24,7 @@ class FifiSdrSource(DirectSource):
return ["csdr convert_s16_f", "csdr gain_ff 5"] return ["csdr convert_s16_f", "csdr gain_ff 5"]
def sendRockProgFrequency(self, frequency): def sendRockProgFrequency(self, frequency):
process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1E6)]) process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)])
process.communicate() process.communicate()
rc = process.wait() rc = process.wait()
if rc != 0: if rc != 0:

View File

@ -8,4 +8,4 @@ class HackrfSource(SoapyConnectorSource):
return mappings return mappings
def getDriver(self): def getDriver(self):
return "hackrf" return "hackrf"

View File

@ -17,6 +17,7 @@ from owrx.command import Flag, Option
# If you omit `remote` from config_webrx.py, hpsdrconnector will use the HPSDR discovery protocol # If you omit `remote` from config_webrx.py, hpsdrconnector will use the HPSDR discovery protocol
# to find radios on your local network and will connect to the first radio it discovered. # to find radios on your local network and will connect to the first radio it discovered.
class HpsdrSource(ConnectorSource): class HpsdrSource(ConnectorSource):
def getCommandMapper(self): def getCommandMapper(self):
return ( return (
@ -24,10 +25,11 @@ class HpsdrSource(ConnectorSource):
.getCommandMapper() .getCommandMapper()
.setBase("hpsdrconnector") .setBase("hpsdrconnector")
.setMappings( .setMappings(
{ {
"tuner_freq": Option("--frequency"), "tuner_freq": Option("--frequency"),
"samp_rate": Option("--samplerate"), "samp_rate": Option("--samplerate"),
"remote": Option("--radio"), "remote": Option("--radio"),
"rf_gain": Option("--gain"), "rf_gain": Option("--gain"),
}) }
)
) )

View File

@ -17,15 +17,21 @@ from owrx.command import Flag, Option
# floating points (option -p),no need for further conversions, # floating points (option -p),no need for further conversions,
# so the method getFormatConversion(self) is not implemented at all. # so the method getFormatConversion(self) is not implemented at all.
class PerseussdrSource(DirectSource): class PerseussdrSource(DirectSource):
def getCommandMapper(self): def getCommandMapper(self):
return super().getCommandMapper().setBase("perseustest -p -d -1 -a -t 0 -o - ").setMappings( return (
{ super()
"samp_rate": Option("-s"), .getCommandMapper()
"tuner_freq": Option("-f"), .setBase("perseustest -p -d -1 -a -t 0 -o - ")
"attenuator": Option("-u"), .setMappings(
"adc_preamp": Option("-m"), {
"adc_dither": Option("-x"), "samp_rate": Option("-s"),
"wideband": Option("-w"), "tuner_freq": Option("-f"),
} "attenuator": Option("-u"),
"adc_preamp": Option("-m"),
"adc_dither": Option("-x"),
"wideband": Option("-w"),
}
)
) )

View File

@ -8,9 +8,11 @@ class RtlTcpSource(ConnectorSource):
super() super()
.getCommandMapper() .getCommandMapper()
.setBase("rtl_tcp_connector") .setBase("rtl_tcp_connector")
.setMappings({ .setMappings(
"bias_tee": Flag("-b"), {
"direct_sampling": Option("-e"), "bias_tee": Flag("-b"),
"remote": Argument(), "direct_sampling": Option("-e"),
}) "remote": Argument(),
}
)
) )

View File

@ -5,11 +5,16 @@ from .connector import ConnectorSource
class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta): class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta):
def getCommandMapper(self): def getCommandMapper(self):
return super().getCommandMapper().setBase("soapy_connector").setMappings( return (
{ super()
"antenna": Option("-a"), .getCommandMapper()
"soapy_settings": Option("-t"), .setBase("soapy_connector")
} .setMappings(
{
"antenna": Option("-a"),
"soapy_settings": Option("-t"),
}
)
) )
""" """

View File

@ -29,7 +29,7 @@ class Password(ABC):
class CleartextPassword(Password): class CleartextPassword(Password):
def is_valid(self, inp: str): def is_valid(self, inp: str):
return self.pwinfo['value'] == inp return self.pwinfo["value"] == inp
class User(object): class User(object):

View File

@ -99,7 +99,7 @@ class Ft4Profile(WsjtProfile):
class Fst4Profile(WsjtProfile): class Fst4Profile(WsjtProfile):
availableIntervals = [15, 30, 60, 120, 300, 900, 1800] availableIntervals = [15, 30, 60, 120, 300, 900, 1800]
def __init__(self, interval): def __init__(self, interval):
self.interval = interval self.interval = interval
@ -208,7 +208,7 @@ class Decoder(ABC):
dateformat = self.profile.getTimestampFormat() dateformat = self.profile.getTimestampFormat()
remain = instring[len(dateformat) + 1:] remain = instring[len(dateformat) + 1:]
try: try:
ts = datetime.strptime(instring[0:len(dateformat)], dateformat) ts = datetime.strptime(instring[0: len(dateformat)], dateformat)
return remain, int( return remain, int(
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000 datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
) )

View File

@ -43,24 +43,26 @@ class Worker(threading.Thread):
# function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2 # function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2
# {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'} # {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'}
date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc) date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc)
data = parse.urlencode({ data = parse.urlencode(
"function": "wspr", {
"date": date.strftime("%y%m%d"), "function": "wspr",
"time": date.strftime("%H%M"), "date": date.strftime("%y%m%d"),
"sig": spot["db"], "time": date.strftime("%H%M"),
"dt": spot["dt"], "sig": spot["db"],
# FST4W does not have drift "dt": spot["dt"],
"drift": spot["drift"] if "drift" in spot else 0, # FST4W does not have drift
"tqrg": spot["freq"] / 1E6, "drift": spot["drift"] if "drift" in spot else 0,
"tcall": spot["callsign"], "tqrg": spot["freq"] / 1e6,
"tgrid": spot["locator"], "tcall": spot["callsign"],
"dbm": spot["dbm"], "tgrid": spot["locator"],
"version": openwebrx_version, "dbm": spot["dbm"],
"rcall": self.callsign, "version": openwebrx_version,
"rgrid": self.locator, "rcall": self.callsign,
# mode 2 = WSPR 2 minutes "rgrid": self.locator,
"mode": self._getMode(spot) # mode 2 = WSPR 2 minutes
}).encode() "mode": self._getMode(spot),
}
).encode()
request.urlopen("http://wsprnet.org/post/", data) request.urlopen("http://wsprnet.org/post/", data)

View File

@ -6,12 +6,24 @@ try:
from setuptools import find_namespace_packages from setuptools import find_namespace_packages
except ImportError: except ImportError:
from setuptools import PEP420PackageFinder from setuptools import PEP420PackageFinder
find_namespace_packages = PEP420PackageFinder.find find_namespace_packages = PEP420PackageFinder.find
setup( setup(
name="OpenWebRX", name="OpenWebRX",
version=str(looseversion), version=str(looseversion),
packages=find_namespace_packages(include=["owrx", "owrx.source", "owrx.service", "owrx.controllers", "owrx.property", "owrx.form", "csdr", "htdocs"]), packages=find_namespace_packages(
include=[
"owrx",
"owrx.source",
"owrx.service",
"owrx.controllers",
"owrx.property",
"owrx.form",
"csdr",
"htdocs",
]
),
package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]},
entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]},
url="https://www.openwebrx.de/", url="https://www.openwebrx.de/",

View File

@ -4,7 +4,6 @@ from owrx.property import PropertyLayer, PropertyFilter
class PropertyFilterTest(TestCase): class PropertyFilterTest(TestCase):
def testPassesProperty(self): def testPassesProperty(self):
pm = PropertyLayer() pm = PropertyLayer()
pm["testkey"] = "testvalue" pm["testkey"] = "testvalue"