Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84dc68dd55 | ||
|
|
cfd302d3ca | ||
|
|
3c6658d19d | ||
|
|
5af0faaa65 | ||
|
|
ce2cfdc76a | ||
|
|
6e0a161b89 | ||
|
|
940c6a0d58 | ||
|
|
194b5c8fb8 | ||
|
|
4e1b3f24c5 | ||
|
|
eaeb51de65 | ||
|
|
995696f11a | ||
|
|
7503b6bb14 | ||
|
|
b867ca849d | ||
|
|
ab32fbbf98 | ||
|
|
bcd24cc035 | ||
|
|
5908110a43 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,4 @@
|
|||||||
__pycache__
|
__PYCache__
|
||||||
Data
|
Data
|
||||||
src/themes/__current_theme
|
src/themes/__current_theme
|
||||||
designer.bat
|
designer.bat
|
||||||
@@ -7,3 +7,5 @@ launch.bat
|
|||||||
.code-workspace
|
.code-workspace
|
||||||
spec_files/**/output
|
spec_files/**/output
|
||||||
*.txt
|
*.txt
|
||||||
|
*.json
|
||||||
|
info.log
|
||||||
|
|||||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -6,6 +6,26 @@ The first release is [3.0.0] because this is actually the third major version (c
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
...
|
...
|
||||||
|
|
||||||
|
## [3.2.1] - 2020-04-25
|
||||||
|
### Added
|
||||||
|
- Add some basic logging to the application. Also for severe errors, track them in info.log file in local folder.
|
||||||
|
- Add Raspberry PI support ([#18](https://github.com/AresValley/Artemis/pull/18), [#20](https://github.com/AresValley/Artemis/pull/20))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Support new `JSON` format for some forecast data ([#21](https://github.com/AresValley/Artemis/pull/14)).
|
||||||
|
- Fixed categorization for very low x-ray flux according to NOAA format.
|
||||||
|
- Remove the `exclusive` parameter in a PyQt function ([#16](https://github.com/AresValley/Artemis/pull/16)).
|
||||||
|
|
||||||
|
|
||||||
|
## [3.2.0] - 2019-12-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- The default font can be changed ([#14](https://github.com/AresValley/Artemis/pull/14)).
|
||||||
|
- Move `Themes` into `Settings`.
|
||||||
|
- Better settings management in `settings.json`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix a bug in the space weather. An inactive k-index caused a crash.
|
||||||
|
|
||||||
## [3.1.0] - 2019-10-21
|
## [3.1.0] - 2019-10-21
|
||||||
### Added
|
### Added
|
||||||
@@ -36,7 +56,9 @@ First release.
|
|||||||
|
|
||||||
|
|
||||||
<!-- Links definitions -->
|
<!-- Links definitions -->
|
||||||
[Unreleased]: https://github.com/AresValley/Artemis/compare/v3.1.0...HEAD
|
[Unreleased]: https://github.com/AresValley/Artemis/compare/v3.2.1...HEAD
|
||||||
|
[3.2.1]: https://github.com/AresValley/Artemis/compare/v3.2.0...v3.2.1
|
||||||
|
[3.2.0]: https://github.com/AresValley/Artemis/compare/v3.1.0...v3.2.0
|
||||||
[3.1.0]: https://github.com/AresValley/Artemis/compare/v3.0.1...v3.1.0
|
[3.1.0]: https://github.com/AresValley/Artemis/compare/v3.0.1...v3.1.0
|
||||||
[3.0.1]: https://github.com/AresValley/Artemis/compare/v3.0.0...v3.0.1
|
[3.0.1]: https://github.com/AresValley/Artemis/compare/v3.0.0...v3.0.1
|
||||||
[3.0.0]: https://github.com/AresValley/Artemis/releases/tag/v3.0.0
|
[3.0.0]: https://github.com/AresValley/Artemis/releases/tag/v3.0.0
|
||||||
@@ -147,7 +147,7 @@ The only folder with the pre-built package is the `themes` one. In this way the
|
|||||||
Some of the available themes were adapted from https://github.com/GTRONICK/QSS.
|
Some of the available themes were adapted from https://github.com/GTRONICK/QSS.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This program (ARTEMIS 3, 2014-2019) is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program (ARTEMIS 3, 2014-2020) is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
@@ -159,5 +159,6 @@ You should have received a copy of the GNU General Public License along with thi
|
|||||||
* **Paolo Romani (IZ1MLL)** - *Lead β Tester, RF specialist*
|
* **Paolo Romani (IZ1MLL)** - *Lead β Tester, RF specialist*
|
||||||
* **Carl Colena** - *Sigidwiki admin, β Tester, Signals expert*
|
* **Carl Colena** - *Sigidwiki admin, β Tester, Signals expert*
|
||||||
* [**Marco Bortoli**](https://github.com/marbort "GitHub profile") - *macOS deployment, β Tester*
|
* [**Marco Bortoli**](https://github.com/marbort "GitHub profile") - *macOS deployment, β Tester*
|
||||||
|
* [**Eric Wiessner (KI7POL)**](https://github.com/WheezyE "GitHub profile") - *ARM port (Raspberry Pi3B+ and Pi4B)*
|
||||||
* [**Pierpaolo Pravatto**](https://github.com/ppravatto "GitHub profile") - *Wiki page, β Tester*
|
* [**Pierpaolo Pravatto**](https://github.com/ppravatto "GitHub profile") - *Wiki page, β Tester*
|
||||||
* [**Francesco Capostagno**](https://github.com/fcapostagno "GitHub profile"), **Luca**, **Pietro** - *β Tester*
|
* [**Francesco Capostagno**](https://github.com/fcapostagno "GitHub profile"), **Luca**, **Pietro** - *β Tester*
|
||||||
|
|||||||
@@ -28,14 +28,13 @@ class ACFValue:
|
|||||||
self._description = ""
|
self._description = ""
|
||||||
self._value = value
|
self._value = value
|
||||||
self._string = self._value
|
self._string = self._value
|
||||||
try:
|
if self._value.isdigit():
|
||||||
self.numeric_value = float(self._value)
|
self.numeric_value = float(self._value)
|
||||||
except Exception:
|
|
||||||
self.is_numeric = False
|
|
||||||
self.numeric_value = 0.0
|
|
||||||
else:
|
|
||||||
self.is_numeric = True
|
self.is_numeric = True
|
||||||
self._string += " ms"
|
self._string += " ms"
|
||||||
|
else:
|
||||||
|
self.is_numeric = False
|
||||||
|
self.numeric_value = 0.0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list_from_series(cls, series):
|
def list_from_series(cls, series):
|
||||||
|
|||||||
112
src/artemis.py
112
src/artemis.py
@@ -1,9 +1,11 @@
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from itertools import chain
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from time import sleep, time
|
from time import sleep, time
|
||||||
|
import logging
|
||||||
|
|
||||||
from pandas import read_csv
|
from pandas import read_csv
|
||||||
|
|
||||||
@@ -15,8 +17,10 @@ from PyQt5.QtWidgets import (
|
|||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QSplashScreen,
|
QSplashScreen,
|
||||||
|
QFontDialog,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap, QFont
|
||||||
from PyQt5 import uic
|
from PyQt5 import uic
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
QFileInfo,
|
QFileInfo,
|
||||||
@@ -46,17 +50,21 @@ from utilities import (
|
|||||||
is_undef_band,
|
is_undef_band,
|
||||||
format_numbers,
|
format_numbers,
|
||||||
safe_cast,
|
safe_cast,
|
||||||
|
UniqueMessageBox,
|
||||||
)
|
)
|
||||||
from executable_utilities import IS_BINARY, resource_path
|
from executable_utilities import IS_BINARY, resource_path
|
||||||
from os_utilities import IS_MAC
|
from os_utilities import IS_MAC
|
||||||
from web_utilities import get_db_hash_code
|
from web_utilities import get_db_hash_code
|
||||||
from downloadtargetfactory import get_download_target
|
from downloadtargetfactory import get_download_target
|
||||||
|
from settings import Settings
|
||||||
from updatescontroller import UpdatesController
|
from updatescontroller import UpdatesController
|
||||||
|
from urlbutton import UrlButton
|
||||||
|
import loggingconf # noqa 401
|
||||||
|
|
||||||
# import default_imgs_rc
|
# import default_imgs_rc
|
||||||
|
|
||||||
|
|
||||||
__LATEST_VERSION__ = "3.1.0"
|
__LATEST_VERSION__ = "3.2.1"
|
||||||
|
|
||||||
if IS_BINARY:
|
if IS_BINARY:
|
||||||
__VERSION__ = __LATEST_VERSION__
|
__VERSION__ = __LATEST_VERSION__
|
||||||
@@ -101,6 +109,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
self.action_github.triggered.connect(
|
self.action_github.triggered.connect(
|
||||||
lambda: webbrowser.open(Constants.GITHUB_REPO)
|
lambda: webbrowser.open(Constants.GITHUB_REPO)
|
||||||
)
|
)
|
||||||
|
self.action_font.triggered.connect(self.start_font_selection)
|
||||||
self.db = None
|
self.db = None
|
||||||
self.current_signal_name = ''
|
self.current_signal_name = ''
|
||||||
self.signal_names = []
|
self.signal_names = []
|
||||||
@@ -124,8 +133,6 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
# #######################################################################################
|
# #######################################################################################
|
||||||
|
|
||||||
UrlColors = namedtuple("UrlColors", ["inactive", "active", "clicked"])
|
|
||||||
self.url_button.colors = UrlColors("#9f9f9f", "#4c75ff", "#942ccc")
|
|
||||||
self.category_labels = [
|
self.category_labels = [
|
||||||
self.cat_mil,
|
self.cat_mil,
|
||||||
self.cat_rad,
|
self.cat_rad,
|
||||||
@@ -205,10 +212,81 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Final operations.
|
# Final operations.
|
||||||
|
self.settings = Settings()
|
||||||
|
self.settings.load()
|
||||||
self.theme_manager.start()
|
self.theme_manager.start()
|
||||||
|
self.load_font()
|
||||||
self.load_db()
|
self.load_db()
|
||||||
self.display_signals()
|
self.display_signals()
|
||||||
|
|
||||||
|
def apply_font(self, font):
|
||||||
|
"""Apply a given QFont object to all the widgets."""
|
||||||
|
UniqueMessageBox.set_font(font)
|
||||||
|
# This is the smaller-text label. Not the most general strategy, but whatever..
|
||||||
|
smaller_point_size = self.forecast_today_0_lbl.font().pointSize()
|
||||||
|
min_reference_font = 4
|
||||||
|
for w in set(chain(
|
||||||
|
self.findChildren(QWidget),
|
||||||
|
self.download_window.findChildren(QWidget)
|
||||||
|
)):
|
||||||
|
old_font = w.font()
|
||||||
|
point_size = old_font.pointSize()
|
||||||
|
new_font = QFont(font)
|
||||||
|
new_font.setUnderline(old_font.underline())
|
||||||
|
new_font.setBold(old_font.bold())
|
||||||
|
new_font.setItalic(old_font.italic())
|
||||||
|
new_size = font.pointSize() + (point_size - smaller_point_size)
|
||||||
|
if new_size < min_reference_font:
|
||||||
|
new_size = min_reference_font
|
||||||
|
new_font.setPointSize(new_size)
|
||||||
|
w.setFont(new_font)
|
||||||
|
|
||||||
|
def load_font(self):
|
||||||
|
"""Apply a QFont object if present."""
|
||||||
|
if self.settings.font is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
font = QFont(
|
||||||
|
self.settings.font['family'],
|
||||||
|
self.settings.font['point_size'],
|
||||||
|
self.settings.font['weight'],
|
||||||
|
self.settings.font['italic']
|
||||||
|
)
|
||||||
|
font.setStyle(self.settings.font['style'])
|
||||||
|
font.setPointSize(self.settings.font['point_size'])
|
||||||
|
font.setStrikeOut(self.settings.font['strikeout'])
|
||||||
|
font.setUnderline(self.settings.font['underline'])
|
||||||
|
self.apply_font(font)
|
||||||
|
except Exception: # Invalid font
|
||||||
|
logging.warning("Invalid Font in settings.json")
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def start_font_selection(self):
|
||||||
|
"""Open a font selection widget and apply the selected font."""
|
||||||
|
initial_font = self.description_text.font()
|
||||||
|
dialog = QFontDialog()
|
||||||
|
dialog.setCurrentFont(initial_font)
|
||||||
|
font, ok = dialog.getFont(
|
||||||
|
initial_font,
|
||||||
|
self,
|
||||||
|
"Choose a font",
|
||||||
|
options=QFontDialog.DontUseNativeDialog
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
self.apply_font(font)
|
||||||
|
self.settings.save(
|
||||||
|
font={
|
||||||
|
'family': font.family(),
|
||||||
|
'style': font.style(),
|
||||||
|
'point_size': font.pointSize(),
|
||||||
|
'weight': font.weight(),
|
||||||
|
'italic': font.italic(),
|
||||||
|
'strikeout': font.strikeOut(),
|
||||||
|
'underline': font.underline(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def action_after_download(self):
|
def action_after_download(self):
|
||||||
"""Decide what to do after a successful download.
|
"""Decide what to do after a successful download.
|
||||||
|
|
||||||
@@ -282,6 +360,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
try:
|
try:
|
||||||
webbrowser.open(Constants.GFD_SITE + query.lower())
|
webbrowser.open(Constants.GFD_SITE + query.lower())
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.error("Cannot open browser")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_initial_size(self):
|
def set_initial_size(self):
|
||||||
@@ -370,7 +449,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
is_checksum_ok = checksum_ok(db, get_db_hash_code())
|
is_checksum_ok = checksum_ok(db, get_db_hash_code())
|
||||||
except Exception:
|
except ValueError as e:
|
||||||
|
logging.info(e)
|
||||||
pop_up(self, title=Messages.NO_CONNECTION,
|
pop_up(self, title=Messages.NO_CONNECTION,
|
||||||
text=Messages.NO_CONNECTION_MSG).show()
|
text=Messages.NO_CONNECTION_MSG).show()
|
||||||
else:
|
else:
|
||||||
@@ -410,7 +490,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
is_checksum_ok = checksum_ok(db, get_db_hash_code())
|
is_checksum_ok = checksum_ok(db, get_db_hash_code())
|
||||||
except Exception:
|
except ValueError as e:
|
||||||
|
logging.info(e)
|
||||||
pop_up(self, title=Messages.NO_CONNECTION,
|
pop_up(self, title=Messages.NO_CONNECTION,
|
||||||
text=Messages.NO_CONNECTION_MSG).show()
|
text=Messages.NO_CONNECTION_MSG).show()
|
||||||
else:
|
else:
|
||||||
@@ -544,15 +625,11 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
self.name_lab.setText(self.current_signal_name)
|
self.name_lab.setText(self.current_signal_name)
|
||||||
self.name_lab.setAlignment(Qt.AlignHCenter)
|
self.name_lab.setAlignment(Qt.AlignHCenter)
|
||||||
current_signal = self.db.loc[self.current_signal_name]
|
current_signal = self.db.loc[self.current_signal_name]
|
||||||
self.url_button.setEnabled(True)
|
|
||||||
if not current_signal.at[Signal.WIKI_CLICKED]:
|
if not current_signal.at[Signal.WIKI_CLICKED]:
|
||||||
self.url_button.setStyleSheet(
|
state = UrlButton.State.ACTIVE
|
||||||
f"color: {self.url_button.colors.active};"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.url_button.setStyleSheet(
|
state = UrlButton.State.CLICKED
|
||||||
f"color: {self.url_button.colors.clicked};"
|
self.url_button.set_enabled(state)
|
||||||
)
|
|
||||||
category_code = current_signal.at[Signal.CATEGORY_CODE]
|
category_code = current_signal.at[Signal.CATEGORY_CODE]
|
||||||
undef_freq = is_undef_freq(current_signal)
|
undef_freq = is_undef_freq(current_signal)
|
||||||
undef_band = is_undef_band(current_signal)
|
undef_band = is_undef_band(current_signal)
|
||||||
@@ -590,10 +667,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
self.set_band_range(current_signal)
|
self.set_band_range(current_signal)
|
||||||
self.audio_widget.set_audio_player(self.current_signal_name)
|
self.audio_widget.set_audio_player(self.current_signal_name)
|
||||||
else:
|
else:
|
||||||
self.url_button.setEnabled(False)
|
self.url_button.set_disabled()
|
||||||
self.url_button.setStyleSheet(
|
|
||||||
f"color: {self.url_button.colors.inactive};"
|
|
||||||
)
|
|
||||||
self.current_signal_name = ''
|
self.current_signal_name = ''
|
||||||
self.name_lab.setText("No Signal")
|
self.name_lab.setText("No Signal")
|
||||||
self.name_lab.setAlignment(Qt.AlignHCenter)
|
self.name_lab.setAlignment(Qt.AlignHCenter)
|
||||||
@@ -665,9 +739,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
Do nothing if no signal is selected.
|
Do nothing if no signal is selected.
|
||||||
"""
|
"""
|
||||||
if self.current_signal_name:
|
if self.current_signal_name:
|
||||||
self.url_button.setStyleSheet(
|
self.url_button.set_clicked()
|
||||||
f"color: {self.url_button.colors.clicked}"
|
|
||||||
)
|
|
||||||
webbrowser.open(self.db.at[self.current_signal_name, Signal.URL])
|
webbrowser.open(self.db.at[self.current_signal_name, Signal.URL])
|
||||||
self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True
|
self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True
|
||||||
|
|
||||||
|
|||||||
@@ -1255,7 +1255,7 @@
|
|||||||
</property>
|
</property>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_17">
|
<layout class="QHBoxLayout" name="horizontalLayout_17">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="url_button">
|
<widget class="UrlButton" name="url_button">
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
@@ -6949,7 +6949,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -6977,7 +6979,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7005,7 +7009,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7033,7 +7039,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7061,7 +7069,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7092,7 +7102,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7120,7 +7132,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7148,7 +7162,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -7176,7 +7192,9 @@ STORM</string>
|
|||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<pointsize>13</pointsize>
|
<pointsize>13</pointsize>
|
||||||
|
<weight>75</weight>
|
||||||
<italic>false</italic>
|
<italic>false</italic>
|
||||||
|
<bold>true</bold>
|
||||||
</font>
|
</font>
|
||||||
</property>
|
</property>
|
||||||
<property name="layoutDirection">
|
<property name="layoutDirection">
|
||||||
@@ -9516,11 +9534,6 @@ QSlider::handle:horizontal {
|
|||||||
<addaction name="action_update_database"/>
|
<addaction name="action_update_database"/>
|
||||||
<addaction name="action_check_software_version"/>
|
<addaction name="action_check_software_version"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menu_themes">
|
|
||||||
<property name="title">
|
|
||||||
<string>Themes</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
<widget class="QMenu" name="menuSigidwiki">
|
<widget class="QMenu" name="menuSigidwiki">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Sigidwiki</string>
|
<string>Sigidwiki</string>
|
||||||
@@ -9537,10 +9550,16 @@ QSlider::handle:horizontal {
|
|||||||
<addaction name="action_rtl_sdr_com"/>
|
<addaction name="action_rtl_sdr_com"/>
|
||||||
<addaction name="action_github"/>
|
<addaction name="action_github"/>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QMenu" name="settings_menu">
|
||||||
|
<property name="title">
|
||||||
|
<string>Settings</string>
|
||||||
|
</property>
|
||||||
|
<addaction name="action_font"/>
|
||||||
|
</widget>
|
||||||
<addaction name="menuFile"/>
|
<addaction name="menuFile"/>
|
||||||
<addaction name="menuUpdates"/>
|
<addaction name="menuUpdates"/>
|
||||||
<addaction name="menu_themes"/>
|
|
||||||
<addaction name="menuSigidwiki"/>
|
<addaction name="menuSigidwiki"/>
|
||||||
|
<addaction name="settings_menu"/>
|
||||||
<addaction name="menuAbout"/>
|
<addaction name="menuAbout"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QStatusBar" name="statusbar">
|
<widget class="QStatusBar" name="statusbar">
|
||||||
@@ -9598,6 +9617,21 @@ QSlider::handle:horizontal {
|
|||||||
<string>Check software version</string>
|
<string>Check software version</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_themes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Themes</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="action_font">
|
||||||
|
<property name="text">
|
||||||
|
<string>Font...</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
<action name="actionThemes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Themes</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
@@ -9636,6 +9670,11 @@ QSlider::handle:horizontal {
|
|||||||
<extends>QLabel</extends>
|
<extends>QLabel</extends>
|
||||||
<header>switchable_label.h</header>
|
<header>switchable_label.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>UrlButton</class>
|
||||||
|
<extends>QPushButton</extends>
|
||||||
|
<header>urlbutton.h</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="default_imgs.qrc"/>
|
<include location="default_imgs.qrc"/>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class SupportedOs:
|
|||||||
WINDOWS = "windows"
|
WINDOWS = "windows"
|
||||||
LINUX = "linux"
|
LINUX = "linux"
|
||||||
MAC = "mac"
|
MAC = "mac"
|
||||||
|
RASPBIAN = "raspberry"
|
||||||
|
|
||||||
|
|
||||||
class Ftype:
|
class Ftype:
|
||||||
@@ -113,8 +114,8 @@ class Constants:
|
|||||||
UPDATING_STR = "Updating..."
|
UPDATING_STR = "Updating..."
|
||||||
ACF_DOCS = "https://aresvalley.com/documentation/"
|
ACF_DOCS = "https://aresvalley.com/documentation/"
|
||||||
FORECAST_PROBABILITIES = "https://services.swpc.noaa.gov/text/sgarf.txt"
|
FORECAST_PROBABILITIES = "https://services.swpc.noaa.gov/text/sgarf.txt"
|
||||||
SPACE_WEATHER_XRAY = "https://services.swpc.noaa.gov/text/goes-xray-flux-primary.txt"
|
SPACE_WEATHER_XRAY = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json"
|
||||||
SPACE_WEATHER_PROT_EL = "https://services.swpc.noaa.gov/text/goes-particle-flux-primary.txt"
|
SPACE_WEATHER_PROT_EL = "https://services.swpc.noaa.gov/json/goes/primary/integral-protons-1-day.json"
|
||||||
SPACE_WEATHER_AK_INDEX = "https://services.swpc.noaa.gov/text/wwv.txt"
|
SPACE_WEATHER_AK_INDEX = "https://services.swpc.noaa.gov/text/wwv.txt"
|
||||||
SPACE_WEATHER_SGAS = "https://services.swpc.noaa.gov/text/sgas.txt"
|
SPACE_WEATHER_SGAS = "https://services.swpc.noaa.gov/text/sgas.txt"
|
||||||
SPACE_WEATHER_GEO_STORM = "https://services.swpc.noaa.gov/text/3-day-forecast.txt"
|
SPACE_WEATHER_GEO_STORM = "https://services.swpc.noaa.gov/text/3-day-forecast.txt"
|
||||||
@@ -181,6 +182,8 @@ class Constants:
|
|||||||
DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics")
|
DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics")
|
||||||
DEFAULT_NOT_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED)
|
DEFAULT_NOT_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED)
|
||||||
DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE)
|
DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE)
|
||||||
|
FONT_FILE = os.path.join(__BASE_FOLDER__, 'font.json')
|
||||||
|
SETTINGS_FILE = os.path.join(__BASE_FOLDER__, "settings.json")
|
||||||
|
|
||||||
|
|
||||||
class Messages:
|
class Messages:
|
||||||
@@ -207,6 +210,8 @@ class Messages:
|
|||||||
NEW_VERSION_AVAILABLE = "New software version"
|
NEW_VERSION_AVAILABLE = "New software version"
|
||||||
NEW_VERSION_MSG = lambda v: f"The software version {v} is available." # noqa: E731
|
NEW_VERSION_MSG = lambda v: f"The software version {v} is available." # noqa: E731
|
||||||
DOWNLOAD_SUGG_MSG = "Download new version now?"
|
DOWNLOAD_SUGG_MSG = "Download new version now?"
|
||||||
|
SCREEN_UPDATE_FAIL = "Unable to update the data"
|
||||||
|
SCREEN_UPDATE_FAIL_MSG = "Downloaded data currupted or invalid"
|
||||||
|
|
||||||
|
|
||||||
class ThemeConstants:
|
class ThemeConstants:
|
||||||
@@ -215,7 +220,6 @@ class ThemeConstants:
|
|||||||
EXTENSION = ".qss"
|
EXTENSION = ".qss"
|
||||||
ICONS_FOLDER = "icons"
|
ICONS_FOLDER = "icons"
|
||||||
DEFAULT = "dark"
|
DEFAULT = "dark"
|
||||||
CURRENT = "__current_theme"
|
|
||||||
COLORS = "colors.txt"
|
COLORS = "colors.txt"
|
||||||
COLOR_SEPARATOR = "="
|
COLOR_SEPARATOR = "="
|
||||||
DEFAULT_ACTIVE_COLOR = "#000000"
|
DEFAULT_ACTIVE_COLOR = "#000000"
|
||||||
@@ -231,5 +235,4 @@ class ThemeConstants:
|
|||||||
DEFAULT_ICONS_PATH = os.path.join(FOLDER, DEFAULT, ICONS_FOLDER)
|
DEFAULT_ICONS_PATH = os.path.join(FOLDER, DEFAULT, ICONS_FOLDER)
|
||||||
DEFAULT_SEARCH_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.SEARCH_LABEL_IMG)
|
DEFAULT_SEARCH_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.SEARCH_LABEL_IMG)
|
||||||
DEFAULT_VOLUME_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.VOLUME_LABEL_IMG)
|
DEFAULT_VOLUME_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.VOLUME_LABEL_IMG)
|
||||||
CURRENT_THEME_FILE = os.path.join(FOLDER, CURRENT)
|
|
||||||
DEFAULT_THEME_PATH = os.path.join(FOLDER, DEFAULT)
|
DEFAULT_THEME_PATH = os.path.join(FOLDER, DEFAULT)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class _TarExtractor:
|
|||||||
EXTRACTORS = {
|
EXTRACTORS = {
|
||||||
SupportedOs.WINDOWS: _ZipExtractor,
|
SupportedOs.WINDOWS: _ZipExtractor,
|
||||||
SupportedOs.LINUX: _TarExtractor,
|
SupportedOs.LINUX: _TarExtractor,
|
||||||
|
SupportedOs.RASPBIAN: _TarExtractor,
|
||||||
# No extractor for MacOs, just download the file through the browser.
|
# No extractor for MacOs, just download the file through the browser.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
src/loggingconf.py
Normal file
43
src/loggingconf.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from constants import __BASE_FOLDER__
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
"""Import the module to initialize the logging configuration.
|
||||||
|
|
||||||
|
It is imported only for its side effects."""
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGING_CONFIG = {
|
||||||
|
'version': 1,
|
||||||
|
'formatters': {
|
||||||
|
'general': {
|
||||||
|
'format': '%(asctime)s::%(levelname)s::%(module)s::%(funcName)s::%(message)s',
|
||||||
|
'datefmt': '%d/%m/%Y %I:%M:%S %p',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'formatter': 'general',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'stream': 'ext://sys.stdout',
|
||||||
|
},
|
||||||
|
'file': {
|
||||||
|
'class': 'logging.FileHandler',
|
||||||
|
'level': 'ERROR',
|
||||||
|
'filename': os.path.join(__BASE_FOLDER__, 'info.log'),
|
||||||
|
'mode': 'w',
|
||||||
|
'encoding': 'utf8',
|
||||||
|
'formatter': 'general',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'root': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
},
|
||||||
|
# Add loggers if required
|
||||||
|
# 'loggers': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.config.dictConfig(_LOGGING_CONFIG)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import sys
|
import sys
|
||||||
|
import platform
|
||||||
from constants import SupportedOs
|
from constants import SupportedOs
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ def _is_linux_os():
|
|||||||
IS_MAC = _is_mac_os()
|
IS_MAC = _is_mac_os()
|
||||||
IS_LINUX = _is_linux_os()
|
IS_LINUX = _is_linux_os()
|
||||||
IS_WINDOWS = _is_win_os()
|
IS_WINDOWS = _is_win_os()
|
||||||
|
IS_RASPBIAN = IS_LINUX and 'arm' in platform.machine().lower()
|
||||||
|
|
||||||
|
|
||||||
def get_os():
|
def get_os():
|
||||||
@@ -27,8 +29,10 @@ def get_os():
|
|||||||
if IS_WINDOWS:
|
if IS_WINDOWS:
|
||||||
return SupportedOs.WINDOWS
|
return SupportedOs.WINDOWS
|
||||||
elif IS_LINUX:
|
elif IS_LINUX:
|
||||||
|
if IS_RASPBIAN:
|
||||||
|
return SupportedOs.RASPBIAN
|
||||||
return SupportedOs.LINUX
|
return SupportedOs.LINUX
|
||||||
elif IS_MAC:
|
elif IS_MAC:
|
||||||
return SupportedOs.MAC
|
return SupportedOs.MAC
|
||||||
else:
|
else:
|
||||||
raise Exception("ERROR: OS not recognized.")
|
return None
|
||||||
|
|||||||
43
src/settings.py
Normal file
43
src/settings.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import os.path
|
||||||
|
from constants import Constants
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
"""Dynamically save and load the settings of the application."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._dct = {}
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""Load the setiings.json file."""
|
||||||
|
if not os.path.exists(Constants.SETTINGS_FILE):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(Constants.SETTINGS_FILE, 'r') as settings_file:
|
||||||
|
self._dct = json.load(settings_file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.info("No settings.json file")
|
||||||
|
pass # Invalid file.
|
||||||
|
|
||||||
|
def save(self, **kwargs):
|
||||||
|
"""Save the settings.json file.
|
||||||
|
|
||||||
|
Also update the current settings specified in kwargs.
|
||||||
|
New settings can be dynamically added via this method."""
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
self._dct[k] = v
|
||||||
|
with open(Constants.SETTINGS_FILE, mode='w') as settings_file:
|
||||||
|
json.dump(
|
||||||
|
self._dct,
|
||||||
|
settings_file,
|
||||||
|
sort_keys=True,
|
||||||
|
indent=4
|
||||||
|
)
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
"""Return the corresponding setting.
|
||||||
|
|
||||||
|
Return None if there is no such setting yet."""
|
||||||
|
return self._dct.get(attr, None)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import logging
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
from constants import Constants, Messages
|
from constants import Constants, Messages
|
||||||
from switchable_label import SwitchableLabelsIterable
|
from switchable_label import SwitchableLabelsIterable
|
||||||
from weatherdata import SpaceWeatherData
|
from weatherdata import SpaceWeatherData
|
||||||
from utilities import safe_cast, pop_up
|
from utilities import pop_up
|
||||||
|
|
||||||
|
|
||||||
class SpaceWeatherManager(QObject):
|
class SpaceWeatherManager(QObject):
|
||||||
@@ -136,13 +137,14 @@ class SpaceWeatherManager(QObject):
|
|||||||
"""
|
"""
|
||||||
self._owner.update_now_bar.set_idle()
|
self._owner.update_now_bar.set_idle()
|
||||||
if status_ok:
|
if status_ok:
|
||||||
xray_long = safe_cast(self._owner.space_weather_data.xray[-1][7], float)
|
try:
|
||||||
|
xray_long = float(self._owner.space_weather_data.xray)
|
||||||
|
|
||||||
def format_text(letter, power):
|
def format_text(letter, power):
|
||||||
return letter + f"{xray_long * 10**power:.1f}"
|
return letter + f"{xray_long * 10**power:.1f}"
|
||||||
|
|
||||||
if xray_long < 1e-8 and xray_long != -1.00e+05:
|
if xray_long < 1e-8 and xray_long != -1.00e+05:
|
||||||
self._owner.peak_flux_lbl.setText(format_text("<A", 8))
|
self._owner.peak_flux_lbl.setText("<A0.0")
|
||||||
elif xray_long >= 1e-8 and xray_long < 1e-7:
|
elif xray_long >= 1e-8 and xray_long < 1e-7:
|
||||||
self._owner.peak_flux_lbl.setText(format_text("A", 8))
|
self._owner.peak_flux_lbl.setText(format_text("A", 8))
|
||||||
elif xray_long >= 1e-7 and xray_long < 1e-6:
|
elif xray_long >= 1e-7 and xray_long < 1e-6:
|
||||||
@@ -171,7 +173,7 @@ class SpaceWeatherManager(QObject):
|
|||||||
elif xray_long == -1.00e+05:
|
elif xray_long == -1.00e+05:
|
||||||
self._switchable_r_labels.switch_off_all()
|
self._switchable_r_labels.switch_off_all()
|
||||||
|
|
||||||
pro10 = safe_cast(self._owner.space_weather_data.prot_el[-1][8], float)
|
pro10 = float(self._owner.space_weather_data.prot_el)
|
||||||
if pro10 < 10 and pro10 != -1.00e+05:
|
if pro10 < 10 and pro10 != -1.00e+05:
|
||||||
self._switchable_s_labels.switch_on(self._owner.s0_now_lbl)
|
self._switchable_s_labels.switch_on(self._owner.s0_now_lbl)
|
||||||
elif pro10 >= 10 and pro10 < 100:
|
elif pro10 >= 10 and pro10 < 100:
|
||||||
@@ -187,18 +189,14 @@ class SpaceWeatherManager(QObject):
|
|||||||
elif pro10 == -1.00e+05:
|
elif pro10 == -1.00e+05:
|
||||||
self._switchable_s_labels.switch_off_all()
|
self._switchable_s_labels.switch_off_all()
|
||||||
|
|
||||||
k_index = safe_cast(
|
k_index = int(self._owner.space_weather_data.ak_index[8][11].replace('.', ''))
|
||||||
self._owner.space_weather_data.ak_index[8][11].replace('.', ''), int
|
|
||||||
)
|
|
||||||
self._owner.k_index_lbl.setText(str(k_index))
|
self._owner.k_index_lbl.setText(str(k_index))
|
||||||
a_index = safe_cast(
|
a_index = int(self._owner.space_weather_data.ak_index[7][7].replace('.', ''))
|
||||||
self._owner.space_weather_data.ak_index[7][7].replace('.', ''), int
|
|
||||||
)
|
|
||||||
self._owner.a_index_lbl.setText(str(a_index))
|
self._owner.a_index_lbl.setText(str(a_index))
|
||||||
|
|
||||||
if k_index == 0:
|
if k_index == 0:
|
||||||
self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl)
|
self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl)
|
||||||
self._k_storm_labels.switch_on(self.k_inactive_lbl)
|
self._k_storm_labels.switch_on(self._owner.k_inactive_lbl)
|
||||||
self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ")
|
self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ")
|
||||||
elif k_index == 1:
|
elif k_index == 1:
|
||||||
self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl)
|
self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl)
|
||||||
@@ -252,9 +250,7 @@ class SpaceWeatherManager(QObject):
|
|||||||
self._a_storm_labels.switch_on(self._owner.a_sev_storm_lbl)
|
self._a_storm_labels.switch_on(self._owner.a_sev_storm_lbl)
|
||||||
|
|
||||||
index = self._owner.space_weather_data.geo_storm[6].index("was") + 1
|
index = self._owner.space_weather_data.geo_storm[6].index("was") + 1
|
||||||
k_index_24_hmax = safe_cast(
|
k_index_24_hmax = int(self._owner.space_weather_data.geo_storm[6][index])
|
||||||
self._owner.space_weather_data.geo_storm[6][index], int
|
|
||||||
)
|
|
||||||
if k_index_24_hmax == 0:
|
if k_index_24_hmax == 0:
|
||||||
self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl)
|
self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl)
|
||||||
elif k_index_24_hmax == 1:
|
elif k_index_24_hmax == 1:
|
||||||
@@ -276,13 +272,10 @@ class SpaceWeatherManager(QObject):
|
|||||||
elif k_index_24_hmax == 9:
|
elif k_index_24_hmax == 9:
|
||||||
self._switchable_g_today_labels.switch_on(self._owner.g5_today_lbl)
|
self._switchable_g_today_labels.switch_on(self._owner.g5_today_lbl)
|
||||||
|
|
||||||
val = safe_cast(
|
val = int(self._owner.space_weather_data.ak_index[7][2].replace('.', ''))
|
||||||
self._owner.space_weather_data.ak_index[7][2].replace('.', ''), int
|
|
||||||
)
|
|
||||||
self._owner.sfi_lbl.setText(f"{val}")
|
self._owner.sfi_lbl.setText(f"{val}")
|
||||||
val = safe_cast(
|
val = int(
|
||||||
[x[4] for x in self._owner.space_weather_data.sgas
|
[x[4] for x in self._owner.space_weather_data.sgas if "SSN" in x][0]
|
||||||
if "SSN" in x][0], int
|
|
||||||
)
|
)
|
||||||
self._owner.sn_lbl.setText(f"{val:d}")
|
self._owner.sn_lbl.setText(f"{val:d}")
|
||||||
|
|
||||||
@@ -291,6 +284,14 @@ class SpaceWeatherManager(QObject):
|
|||||||
label.pixmap = pixmap
|
label.pixmap = pixmap
|
||||||
label.make_transparent()
|
label.make_transparent()
|
||||||
label.apply_pixmap()
|
label.apply_pixmap()
|
||||||
|
except Exception as e: # This is a mess, so log an error and give up
|
||||||
|
logging.error(f"Forecast update failure: {e}")
|
||||||
|
pop_up(
|
||||||
|
self._owner,
|
||||||
|
title=Messages.SCREEN_UPDATE_FAIL,
|
||||||
|
text=Messages.SCREEN_UPDATE_FAIL_MSG
|
||||||
|
).show()
|
||||||
|
|
||||||
elif not self._owner.closing:
|
elif not self._owner.closing:
|
||||||
pop_up(self._owner, title=Messages.BAD_DOWNLOAD,
|
pop_up(self._owner, title=Messages.BAD_DOWNLOAD,
|
||||||
text=Messages.BAD_DOWNLOAD_MSG).show()
|
text=Messages.BAD_DOWNLOAD_MSG).show()
|
||||||
|
|||||||
@@ -260,8 +260,6 @@ QPushButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
border: 2px dashed #4545e5;
|
|
||||||
border-radius: 13px;
|
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,58 +113,60 @@ QSpinBox::down-button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
QPushButton{
|
QPushButton{
|
||||||
border-style: outset;
|
/*border-style: outset;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-bottom-color: rgb(58, 58, 58);
|
border-bottom-color: rgb(58, 58, 58);
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;*/
|
||||||
|
border: 0px;
|
||||||
color: rgb(255, 255, 255);
|
color: rgb(255, 255, 255);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
QPushButton:hover{
|
QPushButton:hover{
|
||||||
border-style: outset;
|
/*border-style: outset;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255));
|
||||||
border-bottom-color: rgb(115, 115, 115);
|
border-bottom-color: rgb(115, 115, 115);
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;*/
|
||||||
color: rgb(255, 255, 255);
|
border: 0px;
|
||||||
|
color: #AFAFAF;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(107, 107, 107, 255), stop:1 rgba(157, 157, 157, 255));
|
|
||||||
}
|
}
|
||||||
QPushButton:pressed{
|
QPushButton:pressed{
|
||||||
border-style: outset;
|
/*border-style: outset;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255));
|
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255));
|
||||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-bottom-color: rgb(58, 58, 58);
|
border-bottom-color: rgb(58, 58, 58);
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;*/
|
||||||
|
border: 0px;
|
||||||
color: rgb(255, 255, 255);
|
color: rgb(255, 255, 255);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255));
|
|
||||||
}
|
}
|
||||||
QPushButton:disabled{
|
QPushButton:disabled{
|
||||||
border-style: outset;
|
/*border-style: outset;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255));
|
||||||
border-bottom-color: rgb(58, 58, 58);
|
border-bottom-color: rgb(58, 58, 58);
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-style: solid;
|
border-style: solid;*/
|
||||||
|
border: 0px;
|
||||||
color: rgb(0, 0, 0);
|
color: rgb(0, 0, 0);
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(57, 57, 57, 255), stop:1 rgba(77, 77, 77, 255));
|
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QPushButton:checked {
|
||||||
|
border: 0px;
|
||||||
color: #00ff00;
|
color: #00ff00;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +189,10 @@ QRadioButton{
|
|||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QRadioButton:hover{
|
||||||
|
color: #AFAFAF;
|
||||||
|
}
|
||||||
|
|
||||||
QComboBox {
|
QComboBox {
|
||||||
border: 0px solid transparent;
|
border: 0px solid transparent;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class ThemeManager:
|
|||||||
self._theme_names = {}
|
self._theme_names = {}
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _apply(self, theme_path):
|
def _apply(self, theme_path, save=True):
|
||||||
"""Apply the selected theme.
|
"""Apply the selected theme.
|
||||||
|
|
||||||
Refresh all relevant widgets.
|
Refresh all relevant widgets.
|
||||||
@@ -110,7 +110,7 @@ class ThemeManager:
|
|||||||
self._theme_path = theme_path
|
self._theme_path = theme_path
|
||||||
if os.path.exists(theme_path):
|
if os.path.exists(theme_path):
|
||||||
if self._theme_path != self._current_theme:
|
if self._theme_path != self._current_theme:
|
||||||
self._change()
|
self._change(save)
|
||||||
self._owner.display_specs(
|
self._owner.display_specs(
|
||||||
item=self._owner.signals_list.currentItem(),
|
item=self._owner.signals_list.currentItem(),
|
||||||
previous_item=None
|
previous_item=None
|
||||||
@@ -140,8 +140,12 @@ class ThemeManager:
|
|||||||
Connect all the actions to change the theme.
|
Connect all the actions to change the theme.
|
||||||
Display a QMessageBox if the theme folder is not found."""
|
Display a QMessageBox if the theme folder is not found."""
|
||||||
themes = []
|
themes = []
|
||||||
ag = QActionGroup(self._owner, exclusive=True)
|
ag = QActionGroup(self._owner)
|
||||||
if os.path.exists(ThemeConstants.FOLDER):
|
themes_menu = self._owner.settings_menu.addMenu("Themes")
|
||||||
|
if not os.path.exists(ThemeConstants.FOLDER):
|
||||||
|
pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
|
||||||
|
text=ThemeConstants.MISSING_THEME_FOLDER).show()
|
||||||
|
return
|
||||||
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)):
|
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)):
|
||||||
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
|
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
|
||||||
if os.path.isdir(os.path.abspath(relative_folder)):
|
if os.path.isdir(os.path.abspath(relative_folder)):
|
||||||
@@ -156,14 +160,11 @@ class ThemeManager:
|
|||||||
checkable=True
|
checkable=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._owner.menu_themes.addAction(new_theme)
|
themes_menu.addAction(new_theme)
|
||||||
self._theme_names[theme_name.lstrip('&')] = new_theme
|
self._theme_names[theme_name.lstrip('&')] = new_theme
|
||||||
new_theme.triggered.connect(partial(self._apply, theme_path))
|
new_theme.triggered.connect(partial(self._apply, theme_path))
|
||||||
else:
|
|
||||||
pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
|
|
||||||
text=ThemeConstants.MISSING_THEME_FOLDER).show()
|
|
||||||
|
|
||||||
def _change(self):
|
def _change(self, save=True):
|
||||||
"""Change the current theme.
|
"""Change the current theme.
|
||||||
|
|
||||||
Apply the stylesheet and set active and inactive colors.
|
Apply the stylesheet and set active and inactive colors.
|
||||||
@@ -266,40 +267,33 @@ class ThemeManager:
|
|||||||
ThemeConstants.DEFAULT_TEXT_COLOR
|
ThemeConstants.DEFAULT_TEXT_COLOR
|
||||||
)
|
)
|
||||||
self._current_theme = self._theme_path
|
self._current_theme = self._theme_path
|
||||||
|
if save:
|
||||||
try:
|
self._owner.settings.save(theme=os.path.basename(self._theme_path))
|
||||||
with open(ThemeConstants.CURRENT_THEME_FILE, "w") as current_theme:
|
|
||||||
current_theme.write(os.path.basename(self._theme_path))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def apply_default_theme(self):
|
def apply_default_theme(self):
|
||||||
"""Apply the default theme if no theme is set or the theme name is invalid."""
|
"""Apply the default theme if no theme is set or the theme name is invalid."""
|
||||||
try:
|
pretty_name = self._theme_names.get(self._pretty_name(ThemeConstants.DEFAULT), None)
|
||||||
self._theme_names[
|
if pretty_name is None:
|
||||||
self._pretty_name(ThemeConstants.DEFAULT)
|
|
||||||
].setChecked(True)
|
|
||||||
except Exception:
|
|
||||||
pop_up(
|
pop_up(
|
||||||
self._owner,
|
self._owner,
|
||||||
title=ThemeConstants.THEME_NOT_FOUND,
|
title=ThemeConstants.THEME_NOT_FOUND,
|
||||||
text=ThemeConstants.MISSING_THEME
|
text=ThemeConstants.MISSING_THEME
|
||||||
).show()
|
).show()
|
||||||
else:
|
else:
|
||||||
|
pretty_name.setChecked(True)
|
||||||
self._apply(ThemeConstants.DEFAULT_THEME_PATH)
|
self._apply(ThemeConstants.DEFAULT_THEME_PATH)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Start the theme manager."""
|
"""Start the theme manager."""
|
||||||
self._detect_themes()
|
self._detect_themes()
|
||||||
if os.path.exists(ThemeConstants.CURRENT_THEME_FILE):
|
if self._owner.settings.theme is not None:
|
||||||
with open(ThemeConstants.CURRENT_THEME_FILE, "r") as current_theme_name:
|
theme_path = os.path.join(ThemeConstants.FOLDER, self._owner.settings.theme)
|
||||||
theme_path = os.path.join(ThemeConstants.FOLDER, current_theme_name.read())
|
|
||||||
theme_name = self._pretty_name(os.path.basename(theme_path))
|
theme_name = self._pretty_name(os.path.basename(theme_path))
|
||||||
try:
|
theme = self._theme_names.get(theme_name, None)
|
||||||
self._theme_names[theme_name].setChecked(True)
|
if theme is None:
|
||||||
except Exception:
|
|
||||||
self.apply_default_theme()
|
self.apply_default_theme()
|
||||||
else:
|
else:
|
||||||
self._apply(theme_path)
|
theme.setChecked(True)
|
||||||
|
self._apply(theme_path, save=False)
|
||||||
else:
|
else:
|
||||||
self.apply_default_theme()
|
self.apply_default_theme()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from time import perf_counter
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
|
||||||
from constants import Constants
|
from constants import Constants
|
||||||
from utilities import checksum_ok
|
from utilities import checksum_ok, get_file_extension
|
||||||
from web_utilities import (
|
from web_utilities import (
|
||||||
get_cacert_file,
|
get_cacert_file,
|
||||||
get_pool_manager,
|
get_pool_manager,
|
||||||
@@ -156,7 +156,7 @@ class DownloadThread(BaseDownloadThread):
|
|||||||
"""Verify the checksum of the downloaded data and set the status accordingly."""
|
"""Verify the checksum of the downloaded data and set the status accordingly."""
|
||||||
try:
|
try:
|
||||||
is_checksum_ok = checksum_ok(raw_data, self._target.hash_code)
|
is_checksum_ok = checksum_ok(raw_data, self._target.hash_code)
|
||||||
except Exception: # Invalid hash code.
|
except ValueError: # Invalid hash code.
|
||||||
self.status = ThreadStatus.NO_CONNECTION_ERR
|
self.status = ThreadStatus.NO_CONNECTION_ERR
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@@ -210,10 +210,7 @@ class UpdatesControllerThread(BaseDownloadThread):
|
|||||||
# self.status = ThreadStatus.OK
|
# self.status = ThreadStatus.OK
|
||||||
|
|
||||||
|
|
||||||
class _AsyncDownloader:
|
async def _download_resource(session, link):
|
||||||
"""Mixin class for asynchronous threads."""
|
|
||||||
|
|
||||||
async def _download_resource(self, session, link):
|
|
||||||
"""Return the content of 'link' as bytes."""
|
"""Return the content of 'link' as bytes."""
|
||||||
ssl_context = ssl.create_default_context(
|
ssl_context = ssl.create_default_context(
|
||||||
purpose=ssl.Purpose.SERVER_AUTH,
|
purpose=ssl.Purpose.SERVER_AUTH,
|
||||||
@@ -223,7 +220,7 @@ class _AsyncDownloader:
|
|||||||
return await resp.read()
|
return await resp.read()
|
||||||
|
|
||||||
|
|
||||||
class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
|
class UpdateSpaceWeatherThread(BaseDownloadThread):
|
||||||
"""Subclass BaseDownloadThread. Download the space weather data."""
|
"""Subclass BaseDownloadThread. Download the space weather data."""
|
||||||
|
|
||||||
_PROPERTIES = ("xray", "prot_el", "ak_index", "sgas", "geo_storm")
|
_PROPERTIES = ("xray", "prot_el", "ak_index", "sgas", "geo_storm")
|
||||||
@@ -236,14 +233,12 @@ class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
|
|||||||
async def _download_property(self, session, property_name):
|
async def _download_property(self, session, property_name):
|
||||||
"""Download the data conteining the information of a specific property."""
|
"""Download the data conteining the information of a specific property."""
|
||||||
link = getattr(Constants, "SPACE_WEATHER_" + property_name.upper())
|
link = getattr(Constants, "SPACE_WEATHER_" + property_name.upper())
|
||||||
data = await self._download_resource(session, link)
|
data = await _download_resource(session, link)
|
||||||
setattr(self._space_weather_data, property_name, str(data, 'utf-8'))
|
self._space_weather_data.set_property(property_name, data, get_file_extension(link))
|
||||||
|
|
||||||
async def _download_image(self, session, n):
|
async def _download_image(self, session, n):
|
||||||
"""Download the data corresponding the n-th image displayed in the screen."""
|
"""Download the data corresponding the n-th image displayed in the screen."""
|
||||||
im = await self._download_resource(
|
im = await _download_resource(session, Constants.SPACE_WEATHER_IMGS[n])
|
||||||
session, Constants.SPACE_WEATHER_IMGS[n]
|
|
||||||
)
|
|
||||||
self._space_weather_data.images[n].loadFromData(im)
|
self._space_weather_data.images[n].loadFromData(im)
|
||||||
|
|
||||||
async def _download_resources(self):
|
async def _download_resources(self):
|
||||||
@@ -278,7 +273,7 @@ class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
|
|||||||
asyncio.run(self._download_resources())
|
asyncio.run(self._download_resources())
|
||||||
|
|
||||||
|
|
||||||
class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader):
|
class UpdateForecastThread(BaseDownloadThread):
|
||||||
"""Subclass BaseDownloadThread. Download the forecast data."""
|
"""Subclass BaseDownloadThread. Download the forecast data."""
|
||||||
|
|
||||||
class _PropertyName(Enum):
|
class _PropertyName(Enum):
|
||||||
@@ -293,7 +288,7 @@ class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader):
|
|||||||
|
|
||||||
async def _download_property(self, session, link, prop_name):
|
async def _download_property(self, session, link, prop_name):
|
||||||
"""Download the data from 'link' and set the corresponding property of the owner."""
|
"""Download the data from 'link' and set the corresponding property of the owner."""
|
||||||
resp = await self._download_resource(session, link)
|
resp = await _download_resource(session, link)
|
||||||
resp = str(resp, 'utf-8')
|
resp = str(resp, 'utf-8')
|
||||||
if prop_name is self._PropertyName.FORECAST:
|
if prop_name is self._PropertyName.FORECAST:
|
||||||
self.owner.forecast = resp
|
self.owner.forecast = resp
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, QProcess
|
from PyQt5.QtCore import QObject, pyqtSlot, QProcess
|
||||||
@@ -99,6 +100,7 @@ class UpdatesController(QObject):
|
|||||||
try:
|
try:
|
||||||
updater.startDetached(command)
|
updater.startDetached(command)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
|
logging.error("Unable to start updater")
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
qApp.quit()
|
qApp.quit()
|
||||||
@@ -110,7 +112,7 @@ class UpdatesController(QObject):
|
|||||||
If so, ask to download the new version.
|
If so, ask to download the new version.
|
||||||
If the software is not a compiled version, the function is a NOP."""
|
If the software is not a compiled version, the function is a NOP."""
|
||||||
if not IS_BINARY or IS_MAC:
|
if not IS_BINARY or IS_MAC:
|
||||||
return
|
return None
|
||||||
latest_updater_version = self.version_controller.updater.version
|
latest_updater_version = self.version_controller.updater.version
|
||||||
try:
|
try:
|
||||||
with sp.Popen(
|
with sp.Popen(
|
||||||
@@ -122,9 +124,10 @@ class UpdatesController(QObject):
|
|||||||
) as proc:
|
) as proc:
|
||||||
updater_version = proc.stdout.read().rstrip("\r\n") # Strip any possible newline, to be sure.
|
updater_version = proc.stdout.read().rstrip("\r\n") # Strip any possible newline, to be sure.
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.error("Unable to query the updater")
|
||||||
updater_version = latest_updater_version
|
updater_version = latest_updater_version
|
||||||
if latest_updater_version is None:
|
if latest_updater_version is None:
|
||||||
return
|
return None
|
||||||
if updater_version != latest_updater_version:
|
if updater_version != latest_updater_version:
|
||||||
answer = pop_up(
|
answer = pop_up(
|
||||||
self._owner,
|
self._owner,
|
||||||
|
|||||||
80
src/urlbutton.py
Normal file
80
src/urlbutton.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from PyQt5.QtWidgets import QPushButton
|
||||||
|
from collections import namedtuple
|
||||||
|
from enum import Enum, auto
|
||||||
|
|
||||||
|
|
||||||
|
class UrlButton(QPushButton):
|
||||||
|
"""Define the behaviour of the wiki button."""
|
||||||
|
|
||||||
|
class State(Enum):
|
||||||
|
"""Possible states of the button."""
|
||||||
|
ACTIVE = auto()
|
||||||
|
INACTIVE = auto()
|
||||||
|
CLICKED = auto()
|
||||||
|
|
||||||
|
_UrlColors = namedtuple(
|
||||||
|
"UrlColors",
|
||||||
|
[
|
||||||
|
"INACTIVE",
|
||||||
|
"ACTIVE",
|
||||||
|
"CLICKED",
|
||||||
|
"ACTIVE_HOVER",
|
||||||
|
"CLICKED_HOVER",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
_COLORS = _UrlColors(
|
||||||
|
"#9f9f9f",
|
||||||
|
"#4c75ff",
|
||||||
|
"#942ccc",
|
||||||
|
"#808FFF",
|
||||||
|
"#DE78FF",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def set_enabled(self, state):
|
||||||
|
"""Enable the button and set the stylesheet."""
|
||||||
|
super().setEnabled(True)
|
||||||
|
if state is self.State.ACTIVE:
|
||||||
|
color = self._COLORS.ACTIVE
|
||||||
|
else:
|
||||||
|
color = self._COLORS.CLICKED
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
QPushButton {{
|
||||||
|
border: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: {color};
|
||||||
|
}}
|
||||||
|
QPushButton::hover {{
|
||||||
|
border: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: {self._COLORS.ACTIVE_HOVER};
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def set_disabled(self):
|
||||||
|
"""Disable the button and set the stylesheet."""
|
||||||
|
super().setEnabled(False)
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
QPushButton:disabled {{
|
||||||
|
border: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: {self._COLORS.INACTIVE};
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
|
def set_clicked(self):
|
||||||
|
"""Apply the stylesheet for the clicked state."""
|
||||||
|
self.setStyleSheet(f"""
|
||||||
|
QPushButton {{
|
||||||
|
border: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: {self._COLORS.CLICKED};
|
||||||
|
}}
|
||||||
|
QPushButton::hover {{
|
||||||
|
border: 0px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: {self._COLORS.CLICKED_HOVER};
|
||||||
|
}}
|
||||||
|
""")
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import hashlib
|
import hashlib
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
@@ -11,20 +12,40 @@ class UniqueMessageBox(QMessageBox):
|
|||||||
If another instance is the the exec loop, calling exec simply return None."""
|
If another instance is the the exec loop, calling exec simply return None."""
|
||||||
|
|
||||||
_open_message = False
|
_open_message = False
|
||||||
|
_font = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_font(cls, font):
|
||||||
|
"""Store the font for all UniqueMessageBox(es)."""
|
||||||
|
cls._font = font
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def setFont(self, font):
|
||||||
|
"""Extends QMessageBox.setFont. Apply the font only if it is not None."""
|
||||||
|
if font is not None:
|
||||||
|
super().setFont(font)
|
||||||
|
|
||||||
def exec(self):
|
def exec(self):
|
||||||
"""Overrides QMessageBox.exec. Call the parent method if there are no
|
"""Overrides QMessageBox.exec. Call the parent method if there are no
|
||||||
other instances executing exec. Otherwise return None,"""
|
other instances executing exec; also set the current font.
|
||||||
if UniqueMessageBox._open_message:
|
Otherwise return None,"""
|
||||||
|
if self.__class__._open_message:
|
||||||
return None
|
return None
|
||||||
UniqueMessageBox._open_message = True
|
self.setFont(self._font)
|
||||||
|
self.__class__._open_message = True
|
||||||
answer = super().exec()
|
answer = super().exec()
|
||||||
UniqueMessageBox._open_message = False
|
self.__class__._open_message = False
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""Extends QMessageBox.show().
|
||||||
|
|
||||||
|
Set the font before showing the message."""
|
||||||
|
self.setFont(self._font)
|
||||||
|
super().show()
|
||||||
|
|
||||||
|
|
||||||
def uncheck_and_emit(button):
|
def uncheck_and_emit(button):
|
||||||
"""Set the button to the unchecked state and emit the clicked signal."""
|
"""Set the button to the unchecked state and emit the clicked signal."""
|
||||||
@@ -87,7 +108,7 @@ def checksum_ok(data, reference_hash_code):
|
|||||||
|
|
||||||
Expects a sha256 code as argument."""
|
Expects a sha256 code as argument."""
|
||||||
if reference_hash_code is None:
|
if reference_hash_code is None:
|
||||||
raise Exception("ERROR: Invalid hash code.")
|
raise ValueError("ERROR: Invalid hash code.")
|
||||||
code = hashlib.sha256()
|
code = hashlib.sha256()
|
||||||
code.update(data)
|
code.update(data)
|
||||||
return code.hexdigest() == reference_hash_code
|
return code.hexdigest() == reference_hash_code
|
||||||
@@ -172,6 +193,25 @@ def safe_cast(value, cast_type, default=-1):
|
|||||||
try:
|
try:
|
||||||
r = cast_type(value)
|
r = cast_type(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.error("Cast type failure")
|
||||||
r = default
|
r = default
|
||||||
finally:
|
finally:
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_extension(file):
|
||||||
|
"""Return the extension of a file. Return None if there is not such property."""
|
||||||
|
components = file.split('.')
|
||||||
|
if len(components) > 1:
|
||||||
|
return components[-1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_value_from_list_of_dicts(iterable, callable_ok, key_value):
|
||||||
|
"""Return a value from a dict inside a list of dicts.
|
||||||
|
|
||||||
|
The iterable is reversed first, then the value corresponding to the key key_value
|
||||||
|
is returned from the first dict for which callable_ok(dict) returns True"""
|
||||||
|
for d in reversed(iterable):
|
||||||
|
if callable_ok(d):
|
||||||
|
return d[key_value]
|
||||||
|
|||||||
@@ -61,16 +61,25 @@ def _download_versions_file():
|
|||||||
"size": ...
|
"size": ...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"raspberry": {
|
||||||
|
"software": {
|
||||||
|
"version": "...",
|
||||||
|
"url": "...",
|
||||||
|
"hash_code": "...",
|
||||||
|
"size": ...
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"version": "...",
|
||||||
|
"url": "...",
|
||||||
|
"hash_code": "...",
|
||||||
|
"size": ...
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
return json.load(
|
||||||
version_dict = json.load(
|
|
||||||
BytesIO(download_file(Constants.VERSION_LINK))
|
BytesIO(download_file(Constants.VERSION_LINK))
|
||||||
)[get_os()]
|
).get(get_os(), None)
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return version_dict
|
|
||||||
|
|
||||||
|
|
||||||
class VersionController:
|
class VersionController:
|
||||||
@@ -80,7 +89,6 @@ class VersionController:
|
|||||||
|
|
||||||
def __init__(self, dct=None):
|
def __init__(self, dct=None):
|
||||||
"""Initialize the dictionary"""
|
"""Initialize the dictionary"""
|
||||||
super().__init__()
|
|
||||||
self._dct = dct
|
self._dct = dct
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
@@ -89,11 +97,9 @@ class VersionController:
|
|||||||
if self._dct is None:
|
if self._dct is None:
|
||||||
if not self.update():
|
if not self.update():
|
||||||
return None
|
return None
|
||||||
try:
|
dct_element = self._dct.get(attr, None)
|
||||||
dct_element = self._dct[attr]
|
if dct_element is None:
|
||||||
except Exception("ERROR: Invalid attribute!"):
|
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
if isinstance(dct_element, dict):
|
if isinstance(dct_element, dict):
|
||||||
setattr(self, attr, type(self)(dct_element))
|
setattr(self, attr, type(self)(dct_element))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
|
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
|
||||||
@@ -9,7 +11,7 @@ from threads import (
|
|||||||
)
|
)
|
||||||
from constants import Constants
|
from constants import Constants
|
||||||
from switchable_label import MultiColorSwitchableLabel
|
from switchable_label import MultiColorSwitchableLabel
|
||||||
from utilities import safe_cast
|
from utilities import safe_cast, get_value_from_list_of_dicts
|
||||||
|
|
||||||
|
|
||||||
class _BaseWeatherData(QObject):
|
class _BaseWeatherData(QObject):
|
||||||
@@ -89,12 +91,35 @@ class SpaceWeatherData(_BaseWeatherData):
|
|||||||
"""Override _BaseWeatherData._parse_data.
|
"""Override _BaseWeatherData._parse_data.
|
||||||
|
|
||||||
Set all the data."""
|
Set all the data."""
|
||||||
self.xray = self._double_split(self.xray)
|
if self.xray is not None:
|
||||||
self.prot_el = self._double_split(self.prot_el)
|
self.xray = get_value_from_list_of_dicts(
|
||||||
|
self.xray,
|
||||||
|
lambda d: d["energy"] == "0.1-0.8nm",
|
||||||
|
"flux"
|
||||||
|
)
|
||||||
|
if self.prot_el is not None:
|
||||||
|
self.prot_el = get_value_from_list_of_dicts(
|
||||||
|
self.prot_el,
|
||||||
|
lambda d: d["energy"] == ">=10 MeV",
|
||||||
|
"flux"
|
||||||
|
)
|
||||||
|
if self.ak_index is not None:
|
||||||
self.ak_index = self._double_split(self.ak_index)
|
self.ak_index = self._double_split(self.ak_index)
|
||||||
|
if self.sgas is not None:
|
||||||
self.sgas = self._double_split(self.sgas)
|
self.sgas = self._double_split(self.sgas)
|
||||||
|
if self.geo_storm is not None:
|
||||||
self.geo_storm = self._double_split(self.geo_storm)
|
self.geo_storm = self._double_split(self.geo_storm)
|
||||||
|
|
||||||
|
def set_property(self, property_name, data, extension):
|
||||||
|
"""Set a property to the object. Format the data based on the extension."""
|
||||||
|
if extension == 'txt':
|
||||||
|
setattr(self, property_name, str(data, 'utf-8'))
|
||||||
|
elif extension == 'json':
|
||||||
|
setattr(self, property_name, json.loads(data))
|
||||||
|
else:
|
||||||
|
logging.error("Invalid file extension")
|
||||||
|
setattr(self, property_name, None)
|
||||||
|
|
||||||
def remove_data(self):
|
def remove_data(self):
|
||||||
"""Remove the reference to all the data."""
|
"""Remove the reference to all the data."""
|
||||||
self.xray = ''
|
self.xray = ''
|
||||||
@@ -347,6 +372,7 @@ class ForecastData(_BaseWeatherData):
|
|||||||
self._set_dates(forecast, rows["solar_row"])
|
self._set_dates(forecast, rows["solar_row"])
|
||||||
self._set_labels_values(labels_table)
|
self._set_labels_values(labels_table)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.error("Update ForecastData failure")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_data(self):
|
def remove_data(self):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import urllib3
|
import urllib3
|
||||||
@@ -37,15 +38,21 @@ def _download_multiline_file_as_list(url=Database.LINK_REF):
|
|||||||
The downloaded file is a csv file with columns (last version == last line):
|
The downloaded file is a csv file with columns (last version == last line):
|
||||||
data.zip_SHA256 | db.csv_SHA256 | Version | Creation_date"""
|
data.zip_SHA256 | db.csv_SHA256 | Version | Creation_date"""
|
||||||
try:
|
try:
|
||||||
f = download_file(url, encoding="UTF-8").splitlines()[-1].split(Database.DELIMITER)
|
return download_file(url, encoding="UTF-8").splitlines()[-1].split(Database.DELIMITER)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.error("Database metadata download failure")
|
||||||
return None
|
return None
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def get_folder_hash_code():
|
def get_folder_hash_code():
|
||||||
return _download_multiline_file_as_list()[0]
|
f = _download_multiline_file_as_list()
|
||||||
|
if f is not None:
|
||||||
|
return f[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_db_hash_code():
|
def get_db_hash_code():
|
||||||
return _download_multiline_file_as_list()[1]
|
f = _download_multiline_file_as_list()
|
||||||
|
if f is not None:
|
||||||
|
return f[1]
|
||||||
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user