forked from M-Labs/thermostat
Compare commits
10 Commits
6388aac023
...
0d7032b92f
Author | SHA1 | Date | |
---|---|---|---|
0d7032b92f | |||
155934da15 | |||
f76b414426 | |||
757fd1689a | |||
08925755a3 | |||
3d41254f43 | |||
744dabb868 | |||
9dd4138276 | |||
a64532eb23 | |||
759b9e57a1 |
@ -1,9 +1,9 @@
|
||||
import asyncio
|
||||
from pytec.aioclient import Client
|
||||
from pytec.aioclient import AsyncioClient
|
||||
|
||||
|
||||
async def main():
|
||||
tec = Client()
|
||||
tec = AsyncioClient()
|
||||
await tec.start_session() # (host="192.168.1.26", port=23)
|
||||
await tec.set_param("s-h", 1, "t0", 20)
|
||||
print(await tec.get_pwm())
|
||||
|
@ -7,56 +7,30 @@ class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class StoppedConnecting(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Client:
|
||||
class AsyncioClient:
|
||||
def __init__(self):
|
||||
self._reader = None
|
||||
self._writer = None
|
||||
self._connecting_task = None
|
||||
self._command_lock = asyncio.Lock()
|
||||
self._report_mode_on = False
|
||||
self.timeout = None
|
||||
|
||||
async def start_session(self, host="192.168.1.26", port=23, timeout=None):
|
||||
async def start_session(self, host="192.168.1.26", port=23):
|
||||
"""Start session to Thermostat at specified host and port.
|
||||
Throws StoppedConnecting if disconnect was called while connecting.
|
||||
Throws asyncio.TimeoutError if timeout was exceeded.
|
||||
|
||||
Example::
|
||||
client = Client()
|
||||
try:
|
||||
await client.start_session()
|
||||
except StoppedConnecting:
|
||||
print("Stopped connecting")
|
||||
client = AsyncioClient()
|
||||
await client.start_session()
|
||||
"""
|
||||
self._connecting_task = asyncio.create_task(
|
||||
asyncio.wait_for(asyncio.open_connection(host, port), timeout)
|
||||
)
|
||||
self.timeout = timeout
|
||||
try:
|
||||
self._reader, self._writer = await self._connecting_task
|
||||
except asyncio.CancelledError:
|
||||
raise StoppedConnecting
|
||||
finally:
|
||||
self._connecting_task = None
|
||||
|
||||
self._reader, self._writer = await asyncio.open_connection(host, port)
|
||||
await self._check_zero_limits()
|
||||
|
||||
def connecting(self):
|
||||
"""Returns True if client is connecting"""
|
||||
return self._connecting_task is not None
|
||||
|
||||
def connected(self):
|
||||
"""Returns True if client is connected"""
|
||||
return self._writer is not None
|
||||
|
||||
async def end_session(self):
|
||||
"""End session to Thermostat if connected, cancel connection if connecting"""
|
||||
if self._connecting_task is not None:
|
||||
self._connecting_task.cancel()
|
||||
"""End session to Thermostat"""
|
||||
|
||||
if self._writer is None:
|
||||
return
|
||||
@ -93,8 +67,7 @@ class Client:
|
||||
|
||||
async def _command(self, *command):
|
||||
async with self._command_lock:
|
||||
# protect the read-write process from being cancelled midway
|
||||
line = await asyncio.shield(self._read_write(command))
|
||||
line = await self._read_write(command)
|
||||
|
||||
response = json.loads(line)
|
||||
logging.debug(f"{command}: {response}")
|
||||
|
@ -4,10 +4,10 @@ from autotune import PIDAutotuneState, PIDAutotune
|
||||
|
||||
|
||||
class PIDAutoTuner(QObject):
|
||||
def __init__(self, parent, client, num_of_channel):
|
||||
def __init__(self, parent, thermostat, num_of_channel):
|
||||
super().__init__(parent)
|
||||
|
||||
self._client = client
|
||||
self._thermostat = thermostat
|
||||
self.autotuners = [PIDAutotune(25) for _ in range(num_of_channel)]
|
||||
self.target_temp = [20.0 for _ in range(num_of_channel)]
|
||||
self.test_current = [1.0 for _ in range(num_of_channel)]
|
||||
@ -37,7 +37,7 @@ class PIDAutoTuner(QObject):
|
||||
|
||||
async def stop_pid_from_running(self, ch):
|
||||
self.autotuners[ch].setOff()
|
||||
await self._client.set_param("pwm", ch, "i_set", 0)
|
||||
await self._thermostat.set_param("pwm", ch, "i_set", 0)
|
||||
|
||||
@asyncSlot(list)
|
||||
async def tick(self, report):
|
||||
@ -56,21 +56,21 @@ class PIDAutoTuner(QObject):
|
||||
self.autotuners[ch].run(
|
||||
channel_report["temperature"], channel_report["time"]
|
||||
)
|
||||
await self._client.set_param(
|
||||
await self._thermostat.set_param(
|
||||
"pwm", ch, "i_set", self.autotuners[ch].output()
|
||||
)
|
||||
case PIDAutotuneState.STATE_SUCCEEDED:
|
||||
kp, ki, kd = self.autotuners[ch].get_tec_pid()
|
||||
self.autotuners[ch].setOff()
|
||||
|
||||
await self._client.set_param("pid", ch, "kp", kp)
|
||||
await self._client.set_param("pid", ch, "ki", ki)
|
||||
await self._client.set_param("pid", ch, "kd", kd)
|
||||
await self._client.set_param("pwm", ch, "pid")
|
||||
await self._thermostat.set_param("pid", ch, "kp", kp)
|
||||
await self._thermostat.set_param("pid", ch, "ki", ki)
|
||||
await self._thermostat.set_param("pid", ch, "kd", kd)
|
||||
await self._thermostat.set_param("pwm", ch, "pid")
|
||||
|
||||
await self._client.set_param(
|
||||
await self._thermostat.set_param(
|
||||
"pid", ch, "target", self.target_temp[ch]
|
||||
)
|
||||
case PIDAutotuneState.STATE_FAILED:
|
||||
self.autotuners[ch].setOff()
|
||||
await self._client.set_param("pwm", ch, "i_set", 0)
|
||||
await self._thermostat.set_param("pwm", ch, "i_set", 0)
|
||||
|
@ -1,20 +1,9 @@
|
||||
from pytec.aioclient import Client
|
||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtSlot
|
||||
from qasync import asyncSlot
|
||||
from pytec.gui.model.property import Property, PropertyMeta
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
|
||||
class WrappedClient(QObject, Client):
|
||||
connection_error = pyqtSignal()
|
||||
|
||||
async def _read_line(self):
|
||||
try:
|
||||
return await super()._read_line()
|
||||
except (Exception, TimeoutError, asyncio.exceptions.TimeoutError):
|
||||
logging.error("Client connection error, disconnecting", exc_info=True)
|
||||
self.connection_error.emit()
|
||||
from pytec.aioclient import AsyncioClient
|
||||
|
||||
|
||||
class Thermostat(QObject, metaclass=PropertyMeta):
|
||||
@ -27,35 +16,40 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
||||
interval = Property(list)
|
||||
report = Property(list)
|
||||
info_box_trigger = pyqtSignal(str, str)
|
||||
connection_error = pyqtSignal()
|
||||
|
||||
def __init__(self, parent, client, update_s):
|
||||
def __init__(self, parent, update_s):
|
||||
self._update_s = update_s
|
||||
self._client = client
|
||||
self._client = AsyncioClient()
|
||||
self._watch_task = None
|
||||
self._report_mode_task = None
|
||||
self._poll_for_report = True
|
||||
self.connection_errored = False
|
||||
super().__init__(parent)
|
||||
|
||||
async def start_session(self, host, port):
|
||||
await self._client.start_session(host, port)
|
||||
|
||||
async def run(self):
|
||||
self.task = asyncio.create_task(self.update_params())
|
||||
while True:
|
||||
if self.task.done():
|
||||
if self.task.exception() is not None:
|
||||
try:
|
||||
raise self.task.exception()
|
||||
except (
|
||||
Exception,
|
||||
TimeoutError,
|
||||
asyncio.exceptions.TimeoutError,
|
||||
):
|
||||
logging.error(
|
||||
"Encountered an error while updating parameter tree.",
|
||||
exc_info=True,
|
||||
)
|
||||
_ = self.task.result()
|
||||
try:
|
||||
_ = self.task.result()
|
||||
except asyncio.TimeoutError:
|
||||
logging.error(
|
||||
"Encountered an error while updating parameter tree.",
|
||||
exc_info=True,
|
||||
)
|
||||
self.connection_error.emit()
|
||||
return
|
||||
self.task = asyncio.create_task(self.update_params())
|
||||
await asyncio.sleep(self._update_s)
|
||||
|
||||
@pyqtSlot()
|
||||
def timed_out(self):
|
||||
self.connection_errored = True
|
||||
|
||||
async def get_hw_rev(self):
|
||||
self.hw_rev = await self._client.hw_rev()
|
||||
return self.hw_rev
|
||||
@ -75,9 +69,6 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
||||
def connected(self):
|
||||
return self._client.connected()
|
||||
|
||||
def connecting(self):
|
||||
return self._client.connecting()
|
||||
|
||||
def start_watching(self):
|
||||
self._watch_task = asyncio.create_task(self.run())
|
||||
|
||||
@ -106,6 +97,7 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
||||
|
||||
async def end_session(self):
|
||||
await self._client.end_session()
|
||||
self.connection_errored = False
|
||||
|
||||
async def set_ipv4(self, ipv4):
|
||||
await self._client.set_param("ipv4", ipv4)
|
||||
@ -136,3 +128,12 @@ class Thermostat(QObject, metaclass=PropertyMeta):
|
||||
@pyqtSlot(float)
|
||||
def set_update_s(self, update_s):
|
||||
self._update_s = update_s
|
||||
|
||||
async def set_fan(self, power="auto"):
|
||||
await self._client.set_fan(power)
|
||||
|
||||
async def get_fan(self):
|
||||
return await self._client.get_fan()
|
||||
|
||||
async def set_param(self, topic, channel, field="", value=""):
|
||||
await self._client.set_param(topic, channel, field, value)
|
||||
|
@ -7,12 +7,11 @@ from pytec.gui.view.live_plot_view import LiveDataPlotter
|
||||
from pytec.gui.view.ctrl_panel import CtrlPanel
|
||||
from pytec.gui.view.info_box import InfoBox
|
||||
from pytec.gui.model.pid_autotuner import PIDAutoTuner
|
||||
from pytec.gui.model.thermostat import WrappedClient, Thermostat
|
||||
from pytec.gui.model.thermostat import Thermostat
|
||||
import json
|
||||
from autotune import PIDAutotuneState
|
||||
from qasync import asyncSlot, asyncClose
|
||||
import qasync
|
||||
from pytec.aioclient import StoppedConnecting
|
||||
import asyncio
|
||||
import logging
|
||||
import argparse
|
||||
@ -63,7 +62,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.hw_rev_data = None
|
||||
self.info_box = InfoBox()
|
||||
|
||||
self.client = WrappedClient(self)
|
||||
self.thermostat = Thermostat(
|
||||
self, self.report_refresh_spin.value()
|
||||
)
|
||||
|
||||
def handle_connection_error():
|
||||
self.info_box.display_info_box(
|
||||
@ -72,13 +73,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
self.bail()
|
||||
|
||||
self.client.connection_error.connect(handle_connection_error)
|
||||
self.thermostat.connection_error.connect(handle_connection_error)
|
||||
|
||||
self.thermostat = Thermostat(
|
||||
self, self.client, self.report_refresh_spin.value()
|
||||
)
|
||||
self.client.connection_error.connect(self.thermostat.timed_out)
|
||||
self.client.connection_error.connect(self.bail)
|
||||
|
||||
self.autotuners = PIDAutoTuner(self, self.client, 2)
|
||||
self.autotuners = PIDAutoTuner(self, self.thermostat, 2)
|
||||
|
||||
def get_ctrl_panel_config(args):
|
||||
with open(args.param_tree, "r") as f:
|
||||
@ -197,9 +197,12 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.thermostat_ctrl_menu.fan_pwm_warning.setToolTip("")
|
||||
self.clear_graphs()
|
||||
self.report_box.setChecked(False)
|
||||
if not Thermostat.connecting or Thermostat.connected:
|
||||
for ch in range(self.NUM_CHANNELS):
|
||||
if self.autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
|
||||
for ch in range(self.NUM_CHANNELS):
|
||||
if self.autotuners.get_state(ch) != PIDAutotuneState.STATE_OFF:
|
||||
if self.thermostat.connection_errored:
|
||||
# Don't send any commands, just reset local state
|
||||
self.autotuners.autotuners[ch].setOff()
|
||||
else:
|
||||
await self.autotuners.stop_pid_from_running(ch)
|
||||
await self.thermostat.set_report_mode(False)
|
||||
self.thermostat.stop_watching()
|
||||
@ -241,23 +244,30 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self.conn_menu.host_set_line.text(),
|
||||
self.conn_menu.port_set_spin.value(),
|
||||
)
|
||||
|
||||
self._connecting_task = None
|
||||
try:
|
||||
if not (self.client.connecting() or self.client.connected()):
|
||||
if (self._connecting_task is None) or (not self.thermostat.connected()):
|
||||
self.status_lbl.setText("Connecting...")
|
||||
self.connect_btn.setText("Stop")
|
||||
self.conn_menu.host_set_line.setEnabled(False)
|
||||
self.conn_menu.port_set_spin.setEnabled(False)
|
||||
|
||||
try:
|
||||
await self.client.start_session(host=host, port=port, timeout=5)
|
||||
except StoppedConnecting:
|
||||
self._connecting_task = asyncio.wait_for(
|
||||
self.thermostat.start_session(host=host, port=port), timeout=5
|
||||
)
|
||||
await self._connecting_task
|
||||
except asyncio.TimeoutError:
|
||||
return
|
||||
await self._on_connection_changed(True)
|
||||
else:
|
||||
if self._connecting_task is not None:
|
||||
self._connecting_task.cancel()
|
||||
await self.bail()
|
||||
|
||||
# TODO: Remove asyncio.TimeoutError in Python 3.11
|
||||
except (OSError, TimeoutError, asyncio.TimeoutError):
|
||||
except (OSError, asyncio.TimeoutError):
|
||||
try:
|
||||
await self.bail()
|
||||
except ConnectionResetError:
|
||||
@ -266,7 +276,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
@asyncSlot()
|
||||
async def bail(self):
|
||||
await self._on_connection_changed(False)
|
||||
await self.client.end_session()
|
||||
await self.thermostat.disconnect()
|
||||
|
||||
@asyncSlot(object, object)
|
||||
async def send_command(self, param, changes):
|
||||
@ -288,7 +298,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
set_param_args = (*thermostat_param, data)
|
||||
param.child(*param.childPath(inner_param)).setOpts(lock=True)
|
||||
await self.client.set_param(*set_param_args)
|
||||
await self.thermostat.set_param(*set_param_args)
|
||||
param.child(*param.childPath(inner_param)).setOpts(lock=False)
|
||||
|
||||
if inner_param.opts.get("pid_autotune", None) is not None:
|
||||
@ -312,7 +322,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
thermostat_param[1] = ch
|
||||
|
||||
inner_param.setOpts(lock=True)
|
||||
await self.client.set_param(*thermostat_param)
|
||||
await self.thermostat.set_param(*thermostat_param)
|
||||
inner_param.setOpts(lock=False)
|
||||
|
||||
@asyncSlot()
|
||||
@ -376,24 +386,24 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
|
||||
@asyncSlot(int)
|
||||
async def fan_set_request(self, value):
|
||||
assert self.client.connected()
|
||||
assert self.thermostat.connected()
|
||||
|
||||
if self.thermostat_ctrl_menu.fan_auto_box.isChecked():
|
||||
with QSignalBlocker(self.thermostat_ctrl_menu.fan_auto_box):
|
||||
self.thermostat_ctrl_menu.fan_auto_box.setChecked(False)
|
||||
await self.client.set_fan(value)
|
||||
await self.thermostat.set_fan(value)
|
||||
if not self.hw_rev_data["settings"]["fan_pwm_recommended"]:
|
||||
self.thermostat_ctrl_menu.set_fan_pwm_warning()
|
||||
|
||||
@asyncSlot(int)
|
||||
async def fan_auto_set_request(self, enabled):
|
||||
assert self.client.connected()
|
||||
assert self.thermostat.connected()
|
||||
|
||||
if enabled:
|
||||
await self.client.set_fan("auto")
|
||||
self.fan_update(await self.client.get_fan())
|
||||
await self.thermostat.set_fan("auto")
|
||||
self.fan_update(await self.thermostat.get_fan())
|
||||
else:
|
||||
await self.client.set_fan(
|
||||
await self.thermostat.set_fan(
|
||||
self.thermostat_ctrl_menu.fan_power_slider.value()
|
||||
)
|
||||
|
||||
@ -439,7 +449,7 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
assert self.thermostat.connected()
|
||||
|
||||
await self.thermostat.set_ipv4(ipv4_settings)
|
||||
await self.thermostat._client.end_session()
|
||||
await self.thermostat.end_session()
|
||||
await self._on_connection_changed(False)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user