From 8589ebdf0f2215132078486d8ae88ecc6a1ffb66 Mon Sep 17 00:00:00 2001 From: atse Date: Mon, 4 Nov 2024 13:23:26 +0800 Subject: [PATCH] PyThermostat GUI: Implement ThermostatSettingsMenu Co-authored-by: linuswck Co-authored-by: Egor Savkin --- .../gui/view/net_settings_input_diag.py | 36 +++ .../gui/view/thermostat_settings_menu.py | 215 ++++++++++++++++++ pythermostat/pythermostat/thermostat_qt.py | 6 + 3 files changed, 257 insertions(+) create mode 100644 pythermostat/pythermostat/gui/view/net_settings_input_diag.py create mode 100644 pythermostat/pythermostat/gui/view/thermostat_settings_menu.py diff --git a/pythermostat/pythermostat/gui/view/net_settings_input_diag.py b/pythermostat/pythermostat/gui/view/net_settings_input_diag.py new file mode 100644 index 0000000..1ef4f61 --- /dev/null +++ b/pythermostat/pythermostat/gui/view/net_settings_input_diag.py @@ -0,0 +1,36 @@ +from PyQt6 import QtWidgets +from PyQt6.QtWidgets import QAbstractButton +from PyQt6.QtCore import pyqtSignal, pyqtSlot + + +class NetSettingsInputDiag(QtWidgets.QInputDialog): + set_ipv4_act = pyqtSignal(str) + + def __init__(self, current_ipv4_settings): + super().__init__() + self.setWindowTitle("Network Settings") + self.setLabelText( + "Set the Thermostat's IPv4 address, netmask and gateway (optional)" + ) + self.setTextValue(current_ipv4_settings) + self._new_ipv4 = "" + + @pyqtSlot(str) + def set_ipv4(ipv4_settings): + self._new_ipv4 = ipv4_settings + + sure = QtWidgets.QMessageBox(self) + sure.setWindowTitle("Set network?") + sure.setText( + f"Setting this as network and disconnecting:
{ipv4_settings}" + ) + + sure.buttonClicked.connect(self._emit_sig) + sure.show() + + self.textValueSelected.connect(set_ipv4) + self.show() + + @pyqtSlot(QAbstractButton) + def _emit_sig(self, _): + self.set_ipv4_act.emit(self._new_ipv4) diff --git a/pythermostat/pythermostat/gui/view/thermostat_settings_menu.py b/pythermostat/pythermostat/gui/view/thermostat_settings_menu.py new file mode 100644 index 0000000..09555e0 --- /dev/null +++ b/pythermostat/pythermostat/gui/view/thermostat_settings_menu.py @@ -0,0 +1,215 @@ +import logging +from PyQt6 import QtWidgets, QtGui, QtCore +from PyQt6.QtCore import pyqtSignal, pyqtSlot, QSignalBlocker +from qasync import asyncSlot +from pythermostat.gui.view.net_settings_input_diag import NetSettingsInputDiag +from pythermostat.gui.model.thermostat import ThermostatConnectionState + + +class ThermostatSettingsMenu(QtWidgets.QMenu): + def __init__(self, thermostat, info_box, style): + super().__init__() + self._thermostat = thermostat + self._info_box = info_box + self._style = style + self.setTitle("Thermostat settings") + + self.hw_rev_data = dict() + self._thermostat.hw_rev_update.connect(self.hw_rev) + self._thermostat.connection_state_update.connect( + self.thermostat_state_change_handler + ) + + self.fan_group = QtWidgets.QWidget() + self.fan_group.setEnabled(False) + self.fan_group.setMinimumSize(QtCore.QSize(40, 0)) + self.fan_layout = QtWidgets.QHBoxLayout(self.fan_group) + self.fan_layout.setSpacing(9) + self.fan_lbl = QtWidgets.QLabel(parent=self.fan_group) + self.fan_lbl.setMinimumSize(QtCore.QSize(40, 0)) + self.fan_lbl.setMaximumSize(QtCore.QSize(40, 16777215)) + self.fan_lbl.setBaseSize(QtCore.QSize(40, 0)) + self.fan_layout.addWidget(self.fan_lbl) + self.fan_power_slider = QtWidgets.QSlider(parent=self.fan_group) + self.fan_power_slider.setMinimumSize(QtCore.QSize(200, 0)) + self.fan_power_slider.setMaximumSize(QtCore.QSize(200, 16777215)) + self.fan_power_slider.setBaseSize(QtCore.QSize(200, 0)) + self.fan_power_slider.setRange(1, 100) + self.fan_power_slider.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.fan_layout.addWidget(self.fan_power_slider) + self.fan_auto_box = QtWidgets.QCheckBox(parent=self.fan_group) + self.fan_auto_box.setMinimumSize(QtCore.QSize(70, 0)) + self.fan_auto_box.setMaximumSize(QtCore.QSize(70, 16777215)) + self.fan_layout.addWidget(self.fan_auto_box) + self.fan_pwm_warning = QtWidgets.QLabel(parent=self.fan_group) + self.fan_pwm_warning.setMinimumSize(QtCore.QSize(16, 0)) + self.fan_layout.addWidget(self.fan_pwm_warning) + + self.fan_power_slider.valueChanged.connect(self.fan_set_request) + self.fan_auto_box.stateChanged.connect(self.fan_auto_set_request) + self._thermostat.fan_update.connect(self.fan_update) + + self.fan_lbl.setToolTip("Adjust the fan") + self.fan_lbl.setText("Fan:") + self.fan_auto_box.setText("Auto") + + fan = QtWidgets.QWidgetAction(self) + fan.setDefaultWidget(self.fan_group) + self.addAction(fan) + self.fan = fan + + self.actionReset = QtGui.QAction("Reset Thermostat", self) + self.actionReset.triggered.connect(self.reset_request) + self.addAction(self.actionReset) + + self.actionEnter_DFU_Mode = QtGui.QAction("Enter DFU Mode", self) + self.actionEnter_DFU_Mode.triggered.connect(self.dfu_request) + self.addAction(self.actionEnter_DFU_Mode) + + self.actionnet_settings_input_diag = QtGui.QAction("Set IPV4 Settings", self) + self.actionnet_settings_input_diag.triggered.connect(self.net_settings_request) + self.addAction(self.actionnet_settings_input_diag) + + @asyncSlot(bool) + async def load(_): + await self._thermostat.load_cfg() + + self._info_box.display_info_box( + "Config loaded", "All channel configs have been loaded from flash." + ) + + self.actionLoad_all_configs = QtGui.QAction("Load Config", self) + self.actionLoad_all_configs.triggered.connect(load) + self.addAction(self.actionLoad_all_configs) + + @asyncSlot(bool) + async def save(_): + await self._thermostat.save_cfg() + + self._info_box.display_info_box( + "Config saved", "All channel configs have been saved to flash." + ) + + self.actionSave_all_configs = QtGui.QAction("Save Config", self) + self.actionSave_all_configs.triggered.connect(save) + self.addAction(self.actionSave_all_configs) + + def about_thermostat(): + QtWidgets.QMessageBox.about( + self, + "About Thermostat", + f""" +

Sinara 8451 Thermostat v{self.hw_rev_data['rev']['major']}.{self.hw_rev_data['rev']['minor']}

+ +
+ +

Settings:

+ Default fan curve: + a = {self.hw_rev_data['settings']['fan_k_a']}, + b = {self.hw_rev_data['settings']['fan_k_b']}, + c = {self.hw_rev_data['settings']['fan_k_c']} +
+ Fan PWM range: + {self.hw_rev_data['settings']['min_fan_pwm']} \u2013 {self.hw_rev_data['settings']['max_fan_pwm']} +
+ Fan PWM frequency: {self.hw_rev_data['settings']['fan_pwm_freq_hz']} Hz +
+ Fan available: {self.hw_rev_data['settings']['fan_available']} +
+ Fan PWM recommended: {self.hw_rev_data['settings']['fan_pwm_recommended']} + """, + ) + + self.actionAbout_Thermostat = QtGui.QAction("About Thermostat", self) + self.actionAbout_Thermostat.triggered.connect(about_thermostat) + self.addAction(self.actionAbout_Thermostat) + + @pyqtSlot("QVariantMap") + def fan_update(self, fan_settings): + logging.debug(fan_settings) + if fan_settings is None: + return + with QSignalBlocker(self.fan_power_slider): + self.fan_power_slider.setValue( + fan_settings["fan_pwm"] or 100 # 0 = PWM off = full strength + ) + with QSignalBlocker(self.fan_auto_box): + self.fan_auto_box.setChecked(fan_settings["auto_mode"]) + + def set_fan_pwm_warning(self): + if self.fan_power_slider.value() != 100: + pixmapi = getattr(QtWidgets.QStyle.StandardPixmap, "SP_MessageBoxWarning") + icon = self._style.standardIcon(pixmapi) + self.fan_pwm_warning.setPixmap(icon.pixmap(16, 16)) + self.fan_pwm_warning.setToolTip( + "Throttling the fan (not recommended on this hardware rev)" + ) + else: + self.fan_pwm_warning.setPixmap(QtGui.QPixmap()) + self.fan_pwm_warning.setToolTip("") + + @pyqtSlot(ThermostatConnectionState) + def thermostat_state_change_handler(self, state): + if state == ThermostatConnectionState.DISCONNECTED: + self.fan_pwm_warning.setPixmap(QtGui.QPixmap()) + self.fan_pwm_warning.setToolTip("") + + @pyqtSlot("QVariantMap") + def hw_rev(self, hw_rev): + self.hw_rev_data = hw_rev + self.fan_group.setEnabled(self.hw_rev_data["settings"]["fan_available"]) + + @asyncSlot(int) + async def fan_set_request(self, value): + assert self._thermostat.connected() + + if self.fan_auto_box.isChecked(): + with QSignalBlocker(self.fan_auto_box): + self.fan_auto_box.setChecked(False) + await self._thermostat.set_fan(value) + if not self.hw_rev_data["settings"]["fan_pwm_recommended"]: + self.set_fan_pwm_warning() + + @asyncSlot(int) + async def fan_auto_set_request(self, enabled): + assert self._thermostat.connected() + + if enabled: + await self._thermostat.set_fan("auto") + self.fan_update(await self._thermostat.get_fan()) + else: + await self.thermostat.set_fan( + self.fan_power_slider.value() + ) + + @asyncSlot(bool) + async def reset_request(self, _): + assert self._thermostat.connected() + + await self._thermostat.reset() + await self._thermostat.end_session() + self._thermostat.connection_state = ThermostatConnectionState.DISCONNECTED + + @asyncSlot(bool) + async def dfu_request(self, _): + assert self._thermostat.connected() + + await self._thermostat.dfu() + await self._thermostat.end_session() + self._thermostat.connection_state = ThermostatConnectionState.DISCONNECTED + + @asyncSlot(bool) + async def net_settings_request(self, _): + assert self._thermostat.connected() + + ipv4 = await self._thermostat.get_ipv4() + self.net_settings_input_diag = NetSettingsInputDiag(ipv4["addr"]) + self.net_settings_input_diag.set_ipv4_act.connect(self.set_net_settings_request) + + @asyncSlot(str) + async def set_net_settings_request(self, ipv4_settings): + assert self._thermostat.connected() + + await self._thermostat.set_ipv4(ipv4_settings) + await self._thermostat.end_session() + self._thermostat.connection_state = ThermostatConnectionState.DISCONNECTED \ No newline at end of file diff --git a/pythermostat/pythermostat/thermostat_qt.py b/pythermostat/pythermostat/thermostat_qt.py index aa3d08a..a052362 100755 --- a/pythermostat/pythermostat/thermostat_qt.py +++ b/pythermostat/pythermostat/thermostat_qt.py @@ -11,6 +11,7 @@ from qasync import asyncSlot, asyncClose from pythermostat.gui.model.thermostat import Thermostat, ThermostatConnectionState from pythermostat.gui.view.connection_details_menu import ConnectionDetailsMenu from pythermostat.gui.view.info_box import InfoBox +from pythermostat.gui.view.thermostat_settings_menu import ThermostatSettingsMenu from pythermostat.gui.view.zero_limits_warning_view import ZeroLimitsWarningView @@ -68,6 +69,11 @@ class MainWindow(QtWidgets.QMainWindow): ) self.connect_btn.setMenu(self.connection_details_menu) + self._thermostat_settings_menu = ThermostatSettingsMenu( + self._thermostat, self._info_box, self.style() + ) + self.thermostat_settings.setMenu(self._thermostat_settings_menu) + # Status line self._zero_limits_warning_view = ZeroLimitsWarningView( self._thermostat, self.style(), self.limits_warning