openwebrx-clone/owrx/controllers/settings/sdr.py

402 lines
15 KiB
Python
Raw Normal View History

from owrx.controllers.admin import AuthorizationMixin
from owrx.controllers.template import WebpageController
from owrx.controllers.settings import SettingsFormController
2021-02-27 19:48:37 +00:00
from owrx.source import SdrDeviceDescription, SdrDeviceDescriptionMissing, SdrClientClass
from owrx.config import Config
2021-02-27 19:48:37 +00:00
from owrx.connection import OpenWebRxReceiverClient
from owrx.controllers.settings import Section
from urllib.parse import quote, unquote
from owrx.sdr import SdrService
from owrx.form import TextInput, DropdownInput, Option
from owrx.form.validator import RequiredValidator
from owrx.property import PropertyLayer, PropertyStack
from abc import ABCMeta, abstractmethod
2021-02-19 17:18:25 +00:00
class SdrDeviceListController(AuthorizationMixin, WebpageController):
def template_variables(self):
variables = super().template_variables()
2021-02-18 22:05:43 +00:00
variables["content"] = self.render_devices()
2021-02-18 21:12:13 +00:00
variables["title"] = "SDR device settings"
2021-03-03 22:07:41 +00:00
variables["modal"] = ""
return variables
def render_devices(self):
2021-02-18 22:05:43 +00:00
def render_device(device_id, config):
sources = SdrService.getAllSources()
source = sources[device_id] if device_id in sources else None
2021-02-27 19:48:37 +00:00
additional_info = ""
state_info = "Unknown"
2021-02-27 19:48:37 +00:00
if source is not None:
profiles = source.getProfiles()
currentProfile = profiles[source.getProfileId()]
clients = {c: len(source.getClients(c)) for c in SdrClientClass}
clients = {c: v for c, v in clients.items() if v}
connections = len([c for c in source.getClients() if isinstance(c, OpenWebRxReceiverClient)])
additional_info = """
<div>{num_profiles} profile(s)</div>
2021-02-27 19:48:37 +00:00
<div>Current profile: {current_profile}</div>
<div>Clients: {clients}</div>
<div>Connections: {connections}</div>
""".format(
num_profiles=len(config["profiles"]),
2021-02-27 19:48:37 +00:00
current_profile=currentProfile["name"],
clients=", ".join("{cls}: {count}".format(cls=c.name, count=v) for c, v in clients.items()),
connections=connections,
)
state_info = ", ".join(
s for s in [
str(source.getState()),
None if source.isEnabled() else "Disabled",
"Failed" if source.isFailed() else None
]
if s is not None
)
2021-02-18 22:05:43 +00:00
return """
<li class="list-group-item">
2021-03-02 19:28:49 +00:00
<div class="row">
<div class="col-6">
<a href="{device_link}">
<h3>{device_name}</h3>
</a>
<div>State: {state}</div>
</div>
2021-03-03 21:33:37 +00:00
<div class="col-6">
{additional_info}
2021-03-03 21:33:37 +00:00
</div>
2021-03-02 19:28:49 +00:00
</div>
</li>
""".format(
device_name=config["name"],
device_link="{}/{}".format(self.request.path, quote(device_id)),
state=state_info,
2021-02-27 19:48:37 +00:00
additional_info=additional_info,
2021-03-03 21:33:37 +00:00
newprofile_link="{}settings/sdr/{}/newprofile".format(self.get_document_root(), quote(device_id)),
2021-02-18 22:05:43 +00:00
)
return """
2021-03-02 19:28:49 +00:00
<ul class="list-group list-group-flush sdr-device-list">
2021-02-18 22:05:43 +00:00
{devices}
</ul>
<div class="buttons container">
<a class="btn btn-success" href="newsdr">Add new device...</a>
</div>
2021-02-18 22:05:43 +00:00
""".format(
devices="".join(render_device(key, value) for key, value in Config.get()["sdrs"].items())
)
def indexAction(self):
2021-02-18 21:12:13 +00:00
self.serve_template("settings/general.html", **self.template_variables())
2021-02-23 17:32:23 +00:00
class SdrFormController(SettingsFormController, metaclass=ABCMeta):
def __init__(self, handler, request, options):
super().__init__(handler, request, options)
2021-02-19 17:45:29 +00:00
self.device_id, self.device = self._get_device()
def getTitle(self):
return self.device["name"]
def render_sections(self):
2021-03-05 16:43:15 +00:00
return """
{tabs}
<div class="tab-body">
{sections}
</div>
""".format(
tabs=self.render_tabs(),
sections=super().render_sections(),
)
def render_tabs(self):
return """
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link {device_active}" href="{device_link}">{device_name}</a>
</li>
{profile_tabs}
<li class="nav-item">
2021-03-05 16:51:19 +00:00
<a href="{new_profile_link}" class="nav-link {new_profile_active}">New profile</a>
</li>
</ul>
""".format(
device_link="{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id)),
device_name=self.device["name"],
device_active="active" if self.isDeviceActive() else "",
2021-03-05 16:51:19 +00:00
new_profile_active="active" if self.isNewProfileActive() else "",
new_profile_link="{}settings/sdr/{}/newprofile".format(self.get_document_root(), quote(self.device_id)),
profile_tabs="".join(
"""
<li class="nav-item">
<a class="nav-link {profile_active}" href="{profile_link}">{profile_name}</a>
</li>
""".format(
profile_link="{}settings/sdr/{}/profile/{}".format(
self.get_document_root(), quote(self.device_id), quote(profile_id)
),
profile_name=profile["name"],
profile_active="active" if self.isProfileActive(profile_id) else "",
)
for profile_id, profile in self.device["profiles"].items()
),
)
def isDeviceActive(self) -> bool:
return False
def isProfileActive(self, profile_id) -> bool:
return False
2021-03-05 16:51:19 +00:00
def isNewProfileActive(self) -> bool:
return False
2021-02-19 17:45:29 +00:00
def store(self):
# need to overwrite the existing key in the config since the layering won't capture the changes otherwise
config = Config.get()
sdrs = config["sdrs"]
2021-02-23 17:41:49 +00:00
sdrs[self.device_id] = self.device
2021-02-19 17:45:29 +00:00
config["sdrs"] = sdrs
super().store()
2021-02-19 17:18:25 +00:00
2021-02-23 17:32:23 +00:00
def _get_device(self):
2021-02-23 17:41:49 +00:00
config = Config.get()
2021-02-23 17:32:23 +00:00
device_id = unquote(self.request.matches.group(1))
2021-02-23 17:41:49 +00:00
if device_id not in config["sdrs"]:
return None, None
2021-02-23 17:41:49 +00:00
return device_id, config["sdrs"][device_id]
2021-02-23 17:32:23 +00:00
2021-03-03 21:33:37 +00:00
class SdrFormControllerWithModal(SdrFormController, metaclass=ABCMeta):
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
return ""
def render_buttons(self):
return self.render_remove_button() + super().render_buttons()
def buildModal(self):
return """
<div class="modal" id="deleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5>Please confirm</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Do you really want to delete this {object_type}?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<a type="button" class="btn btn-danger" href="{confirm_url}">Delete</a>
</div>
</div>
</div>
</div>
""".format(
object_type=self.getModalObjectType(),
confirm_url=self.getModalConfirmUrl(),
)
@abstractmethod
def getModalObjectType(self):
pass
@abstractmethod
def getModalConfirmUrl(self):
pass
2021-02-23 17:32:23 +00:00
2021-03-03 21:33:37 +00:00
class SdrDeviceController(SdrFormControllerWithModal):
2021-02-23 17:32:23 +00:00
def getData(self):
return self.device
def getSections(self):
try:
description = SdrDeviceDescription.getByType(self.device["type"])
2021-02-23 17:32:23 +00:00
return [description.getDeviceSection()]
except SdrDeviceDescriptionMissing:
# TODO provide a generic interface that allows to switch the type
return []
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
return """
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">Remove device...</button>
"""
def isDeviceActive(self) -> bool:
return True
def indexAction(self):
if self.device is None:
self.send_response("device not found", code=404)
return
super().indexAction()
2021-02-23 17:32:23 +00:00
def processFormData(self):
if self.device is None:
self.send_response("device not found", code=404)
return
return super().processFormData()
def getModalObjectType(self):
return "SDR device"
def getModalConfirmUrl(self):
return "{}settings/deletesdr/{}".format(self.get_document_root(), quote(self.device_id))
2021-03-03 22:07:41 +00:00
def deleteDevice(self):
if self.device_id is None:
return self.send_response("device not found", code=404)
config = Config.get()
sdrs = config["sdrs"]
del sdrs[self.device_id]
# need to overwrite the existing key in the config since the layering won't capture the changes otherwise
config["sdrs"] = sdrs
2021-03-03 22:07:41 +00:00
config.store()
return self.send_redirect("{}settings/sdr".format(self.get_document_root()))
class NewSdrDeviceController(SettingsFormController):
def __init__(self, handler, request, options):
super().__init__(handler, request, options)
id_layer = PropertyLayer(id="")
self.data_layer = PropertyLayer(name="", type="", profiles=PropertyLayer())
self.stack = PropertyStack()
self.stack.addLayer(0, id_layer)
self.stack.addLayer(1, self.data_layer)
def getSections(self):
return [
Section(
"New device settings",
TextInput("name", "Device name", validator=RequiredValidator()),
DropdownInput("type", "Device type", [Option(name, name) for name in SdrDeviceDescription.getTypes()]),
TextInput("id", "Device ID", validator=RequiredValidator()),
)
]
def getTitle(self):
return "New device"
def getData(self):
return self.stack
def store(self):
# need to overwrite the existing key in the config since the layering won't capture the changes otherwise
config = Config.get()
sdrs = config["sdrs"]
2021-03-03 21:33:37 +00:00
if self.stack["id"] in sdrs:
raise ValueError("device {} already exists!".format(self.stack["id"]))
sdrs[self.stack["id"]] = self.data_layer
config["sdrs"] = sdrs
super().store()
def getSuccessfulRedirect(self):
2021-03-03 21:33:37 +00:00
return "{}settings/sdr/{}".format(self.get_document_root(), quote(self.stack["id"]))
2021-02-23 17:32:23 +00:00
2021-03-03 21:33:37 +00:00
class SdrProfileController(SdrFormControllerWithModal):
2021-02-23 17:32:23 +00:00
def __init__(self, handler, request, options):
super().__init__(handler, request, options)
self.profile_id, self.profile = self._get_profile()
def getData(self):
return self.profile
def _get_profile(self):
2021-02-23 17:41:49 +00:00
if self.device is None:
2021-02-23 17:32:23 +00:00
return None
2021-02-23 17:41:49 +00:00
profile_id = unquote(self.request.matches.group(2))
if profile_id not in self.device["profiles"]:
2021-02-23 17:32:23 +00:00
return None
2021-02-23 17:41:49 +00:00
return profile_id, self.device["profiles"][profile_id]
2021-02-23 17:32:23 +00:00
def isProfileActive(self, profile_id) -> bool:
return profile_id == self.profile_id
2021-02-23 17:32:23 +00:00
def getSections(self):
try:
description = SdrDeviceDescription.getByType(self.device["type"])
return [description.getProfileSection()]
except SdrDeviceDescriptionMissing:
# TODO provide a generic interface that allows to switch the type
return []
def indexAction(self):
if self.profile is None:
self.send_response("profile not found", code=404)
return
super().indexAction()
def processFormData(self):
if self.profile is None:
self.send_response("profile not found", code=404)
return
return super().processFormData()
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
return """
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal">Remove profile...</button>
"""
def getModalObjectType(self):
return "profile"
def getModalConfirmUrl(self):
2021-03-03 22:07:41 +00:00
return "{}settings/sdr/{}/deleteprofile/{}".format(
self.get_document_root(), quote(self.device_id), quote(self.profile_id)
)
2021-03-03 21:33:37 +00:00
2021-03-03 22:07:41 +00:00
def deleteProfile(self):
if self.profile_id is None:
return self.send_response("profile not found", code=404)
config = Config.get()
del self.device["profiles"][self.profile_id]
config.store()
2021-03-05 17:32:16 +00:00
return self.send_redirect("{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id)))
2021-03-03 22:07:41 +00:00
2021-03-03 21:33:37 +00:00
2021-03-05 17:32:16 +00:00
class NewProfileController(SdrProfileController):
2021-03-03 21:33:37 +00:00
def __init__(self, handler, request, options):
id_layer = PropertyLayer(id="")
self.data_layer = PropertyLayer(name="")
self.stack = PropertyStack()
2021-03-05 17:32:16 +00:00
self.stack.addLayer(0, self.data_layer)
self.stack.addLayer(1, id_layer)
super().__init__(handler, request, options)
def _get_profile(self):
return "new", self.stack
2021-03-03 21:33:37 +00:00
def getSections(self):
return [
Section(
"New profile settings",
TextInput("id", "Profile ID", validator=RequiredValidator()),
2021-03-03 21:33:37 +00:00
)
2021-03-05 17:32:16 +00:00
] + super().getSections()
2021-03-03 21:33:37 +00:00
2021-03-05 16:51:19 +00:00
def isNewProfileActive(self) -> bool:
return True
2021-03-03 21:33:37 +00:00
def store(self):
if self.stack["id"] in self.device["profiles"]:
raise ValueError("Profile {} already exists!".format(self.stack["id"]))
self.device["profiles"][self.stack["id"]] = self.data_layer
super().store()
def getSuccessfulRedirect(self):
return "{}settings/sdr/{}/profile/{}".format(
self.get_document_root(), quote(self.device_id), quote(self.stack["id"])
)
2021-03-05 17:32:16 +00:00
def render_remove_button(self):
# new profile doesn't have a remove button
return ""