Skip to content

Commit

Permalink
Smarter shutters detection
Browse files Browse the repository at this point in the history
  • Loading branch information
jziolkowski committed Oct 4, 2024
1 parent 99d8390 commit c424705
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 32 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ celerybeat-schedule
.idea/

tdm.cfg
tdm.ini
devices.cfg
devices.ini
tdm*.log
__version__.py
4 changes: 3 additions & 1 deletion tdmgr/GUI/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ def __init__(self, devices):
self.lwCommands = QListWidget()

vl.addElements(
gbxDevice, self.lwCommands, QLabel("Double-click a command to use it, ESC to close.")
gbxDevice,
self.lwCommands,
QLabel("Double-click a command to use it, ESC to close."),
)
self.setLayout(vl)

Expand Down
12 changes: 10 additions & 2 deletions tdmgr/GUI/delegates/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,12 @@ def sizeHint(self, option, index):
return QStyledItemDelegate().sizeHint(option, index)

def get_used_width(self, option, index) -> int:
return sum([self.get_devicename_width(option, index), self.get_alerts_width(option, index)])
return sum(
[
self.get_devicename_width(option, index),
self.get_alerts_width(option, index),
]
)

@staticmethod
def get_devicename_width(option, index) -> int:
Expand Down Expand Up @@ -356,7 +361,10 @@ def paint(self, p: QPainter, option: QStyleOptionViewItem, index):
alerts_width = self.get_alerts_width(option, index)

exc_rect = QRect(
self.get_devicename_width(option, index), y, alerts_width, RECT_SIZE.height()
self.get_devicename_width(option, index),
y,
alerts_width,
RECT_SIZE.height(),
)

if selected:
Expand Down
13 changes: 10 additions & 3 deletions tdmgr/GUI/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,10 @@ def create_actions(self):
self.ctx_menu.addAction(QIcon(":/delete.png"), "Delete", self.ctx_menu_delete_device)

self.agAllPower = QActionGroup(self)
for label, shortcut, fill in [("ON", "Ctrl+F1", True), ("OFF", "Ctrl+F2", False)]:
for label, shortcut, fill in [
("ON", "Ctrl+F1", True),
("OFF", "Ctrl+F2", False),
]:
px = make_relay_pixmap(label, filled=fill)
act = self.agAllPower.addAction(QIcon(px), f"All relays {label}")
act.setShortcut(shortcut)
Expand Down Expand Up @@ -278,7 +281,11 @@ def ctx_menu_restart(self):
def ctx_menu_reset(self):
if self.device:
reset, ok = QInputDialog.getItem(
self, "Reset device and restart", "Select reset mode", resets, editable=False
self,
"Reset device and restart",
"Select reset mode",
resets,
editable=False,
)
if ok:
self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0])
Expand Down Expand Up @@ -366,7 +373,7 @@ def select_device(self, idx):
self.actColor.setEnabled(False)
self.actChannels.setEnabled(False)
if color := self.device.color():
self.actColor.setEnabled(bool(color.hsbcolor and color.SO68 == 1))
self.actColor.setEnabled(bool(color.hsbcolor) and color.SO68 == 0)
self.actChannels.setEnabled(True)

self.actChannels.menu().clear()
Expand Down
32 changes: 22 additions & 10 deletions tdmgr/GUI/dialogs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import re

from paho.mqtt import MQTTException
from PyQt5.QtCore import QDir, QFileInfo, QSettings, QSize, Qt, QTimer, QUrl, pyqtSlot
from PyQt5.QtGui import QDesktopServices, QFont, QIcon
from PyQt5.QtWidgets import (
Expand All @@ -15,7 +16,6 @@
QPushButton,
QStatusBar,
)
from paho.mqtt import MQTTException

from tdmgr.GUI.console import ConsoleWidget
from tdmgr.GUI.devices import DevicesListWidget
Expand Down Expand Up @@ -63,9 +63,13 @@ def __init__(

self.menuBar().setNativeMenuBar(False)

self.mqtt = None
self.setup_mqtt()

self.unknown = []
self.custom_patterns = []
self.env = TasmotaEnvironment()
self.env.mqtt = self.mqtt
self.device = None

self.topics = []
Expand All @@ -88,8 +92,7 @@ def __init__(
)
device.debug = self.devices.value("debug", False, bool)
device.p["Mac"] = mac.replace("-", ":")
device.env = self.env
self.env.devices.append(device)
self.env.add_device(device)

# load device command history
self.devices.beginGroup("history")
Expand All @@ -101,7 +104,6 @@ def __init__(

self.device_model = TasmotaDevicesModel(self.settings, self.devices, self.env)

self.setup_mqtt()
self.setup_main_layout()
self.add_devices_tab()
self.build_mainmenu()
Expand Down Expand Up @@ -186,7 +188,9 @@ def build_mainmenu(self):

def build_toolbars(self):
main_toolbar = Toolbar(
orientation=Qt.Horizontal, iconsize=24, label_position=Qt.ToolButtonTextBesideIcon
orientation=Qt.Horizontal,
iconsize=24,
label_position=Qt.ToolButtonTextBesideIcon,
)
main_toolbar.setObjectName("main_toolbar")

Expand Down Expand Up @@ -304,7 +308,8 @@ def mqtt_subscribe(self):
# the custom patterns
for custom_pattern in self.custom_patterns:
custom_pattern_match = re.match(
custom_pattern.replace("+", f"({MQTT_PATH_REGEX})"), d.p["FullTopic"]
custom_pattern.replace("+", f"({MQTT_PATH_REGEX})"),
d.p["FullTopic"],
)
if not d.is_default() and not custom_pattern_match:
# if pattern is not found then add the device topics to subscription list.
Expand Down Expand Up @@ -428,7 +433,7 @@ def mqtt_message(self, msg: Message):
elif msg.endpoint in ("RESULT", "FULLTOPIC"):
# reply from an unknown device
if d := lwt_discovery_stage2(self.env, msg):
self.env.devices.append(d)
self.env.add_device(d)
self.device_model.addDevice(d)
log.debug("DISCOVERY: Sending initial query to topic %s", d.p["Topic"])
self.initial_query(d, True)
Expand All @@ -438,7 +443,10 @@ def mqtt_message(self, msg: Message):

def export(self):
fname, _ = QFileDialog.getSaveFileName(
self, "Export device list as...", directory=QDir.homePath(), filter="CSV files (*.csv)"
self,
"Export device list as...",
directory=QDir.homePath(),
filter="CSV files (*.csv)",
)
if fname:
if not fname.endswith(".csv"):
Expand Down Expand Up @@ -564,7 +572,9 @@ def openTelemetry(self):
self.mqtt_publish(self.device.cmnd_topic("STATUS"), "8")
self.tele_docks.append(tele_widget)
self.resizeDocks(
self.tele_docks, [100 // len(self.tele_docks) for _ in self.tele_docks], Qt.Vertical
self.tele_docks,
[100 // len(self.tele_docks) for _ in self.tele_docks],
Qt.Vertical,
)

@pyqtSlot()
Expand All @@ -577,7 +587,9 @@ def openConsole(self):
console_widget.command.setFocus()
self.consoles.append(console_widget)
self.resizeDocks(
self.consoles, [100 // len(self.consoles) for _ in self.consoles], Qt.Horizontal
self.consoles,
[100 // len(self.consoles) for _ in self.consoles],
Qt.Horizontal,
)

@pyqtSlot()
Expand Down
13 changes: 11 additions & 2 deletions tdmgr/GUI/dialogs/timers.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ def __init__(self, device, *args, **kwargs):
hl_tmr_time.addElements(self.cbxTimerPM, self.teTimerTime, lbWnd, self.cbxTimerWnd)

self.gbTimers.addElements(
self.cbTimer, hl_tmr_arm_rpt, hl_tmr_out_act, gbTimerMode, hl_tmr_time, hl_tmr_days
self.cbTimer,
hl_tmr_arm_rpt,
hl_tmr_out_act,
gbTimerMode,
hl_tmr_time,
hl_tmr_days,
)

btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
Expand Down Expand Up @@ -159,7 +164,11 @@ def loadTimer(self, timer=""):

def describeTimer(self):
if self.cbTimerArm.isChecked():
desc = {"days": "", "repeat": "", "timer": self.cbTimer.currentText().upper()}
desc = {
"days": "",
"repeat": "",
"timer": self.cbTimer.currentText().upper(),
}
repeat = self.cbTimerRpt.isChecked()
out = self.cbxTimerOut.currentText()
act = self.cbxTimerAction.currentText()
Expand Down
11 changes: 9 additions & 2 deletions tdmgr/GUI/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ def load_rule_from_file(self):
def save_to_file(self):
new_fname = f"{self.device.name} {self.cbRule.currentText()}.txt"
file, ok = QFileDialog.getSaveFileName(
self, "Save rule", os.path.join(QDir.homePath(), new_fname), "Text files | *.txt"
self,
"Save rule",
os.path.join(QDir.homePath(), new_fname),
"Text files | *.txt",
)
if ok:
with open(file, "w") as f:
Expand Down Expand Up @@ -262,7 +265,11 @@ def display_rule(self, payload, rule):
self.actStopOnError.setChecked(payload["StopOnError"] == "ON")

def unfold_rule(self, rules: str):
for pat, repl in [(r" on ", "\non "), (r" do ", " do\n\t"), (r" endon", "\nendon ")]:
for pat, repl in [
(r" on ", "\non "),
(r" do ", " do\n\t"),
(r" endon", "\nendon "),
]:
rules = re.sub(pat, repl, rules, flags=re.IGNORECASE)
return rules.rstrip(" ")

Expand Down
20 changes: 16 additions & 4 deletions tdmgr/GUI/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ def addElements(self, *elements):

class GroupBoxV(GroupBoxBase):
def __init__(
self, title: str, margin: Union[int, List[int]] = 3, spacing: int = 3, *args, **kwargs
self,
title: str,
margin: Union[int, List[int]] = 3,
spacing: int = 3,
*args,
**kwargs,
):
super(GroupBoxV, self).__init__(title, *args, **kwargs)

Expand All @@ -156,7 +161,12 @@ def __init__(

class GroupBoxH(GroupBoxBase):
def __init__(
self, title: str, margin: Union[int, List[int]] = 3, spacing: int = 3, *args, **kwargs
self,
title: str,
margin: Union[int, List[int]] = 3,
spacing: int = 3,
*args,
**kwargs,
):
super(GroupBoxH, self).__init__(title, *args, **kwargs)

Expand Down Expand Up @@ -387,7 +397,8 @@ def __init__(self, command, meta, value=None, *args, **kwargs):

elif meta["type"] == "value":
self.input = SpinBox(
minimum=int(meta["parameters"]["min"]), maximum=int(meta["parameters"]["max"])
minimum=int(meta["parameters"]["min"]),
maximum=int(meta["parameters"]["max"]),
)
self.input.setMinimumWidth(75)
if value:
Expand Down Expand Up @@ -538,7 +549,8 @@ def __init__(self, command: str, meta: dict, device: TasmotaDevice):

for idx, value in enumerate(values, start=1):
sb = SpinBox(
minimum=int(meta["parameters"]["min"]), maximum=int(meta["parameters"]["max"])
minimum=int(meta["parameters"]["min"]),
maximum=int(meta["parameters"]["max"]),
)
sb.setValue(value)
hl_group = HLayout(0)
Expand Down
4 changes: 0 additions & 4 deletions tdmgr/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ def initial_commands():
commands = [(command, "") for command in commands]
commands += [("status", "0"), ("gpios", "255")]

for sht in range(8):
commands.append([f"shutterrelay{sht + 1}", ""])
commands.append([f"shutterposition{sht + 1}", ""])

return commands


Expand Down
5 changes: 4 additions & 1 deletion tdmgr/tasmota/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@
"0": {"description": "Keep relay(s) OFF after power up"},
"1": {"description": "Turn relay(s) ON after power up"},
"2": {"description": "Toggle relay(s) from last saved state"},
"3": {"description": "Switch relay(s) to their last saved state", "default": "True"},
"3": {
"description": "Switch relay(s) to their last saved state",
"default": "True",
},
"4": {"description": "Turn relay(s) ON and disable further relay control"},
"5": {"description": "Turn relay(s) ON after a PulseTime period"},
},
Expand Down
18 changes: 16 additions & 2 deletions tdmgr/tasmota/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
ShutterResultSchema,
TemplateResultSchema,
)
from tdmgr.schemas.status import STATUS_SCHEMA_MAP
from tdmgr.schemas.status import STATUS_SCHEMA_MAP, Status13ResponseSchema
from tdmgr.tasmota.common import COMMAND_UNKNOWN, MAX_SHUTTERS, Color, DeviceProps, Relay, Shutter

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -245,6 +245,17 @@ def process_status(self, schema: BaseModel, payload: dict):
else:
self.update_property(k, v)

if schema == Status13ResponseSchema:
if self.version_above("12.2.0.6"): # Support for single-response for all shutters
command = self.cmnd_topic("ShutterRelay")
payload = []
else:
command = self.cmnd_topic("Backlog")
payload = [
f"shutterrelay{sht + 1}" for sht in range(len(payload["StatusSHT"].keys()))
]
self.env.mqtt.publish(command, ";".join(payload))

except ValidationError as e:
log.critical("MQTT: Cannot parse %s", e)

Expand Down Expand Up @@ -356,7 +367,10 @@ def color(self):

@property
def ip_address(self) -> str:
for ip in [self.p.get("IPAddress"), self.p.get("Ethernet", {}).get("IPAddress")]:
for ip in [
self.p.get("IPAddress"),
self.p.get("Ethernet", {}).get("IPAddress"),
]:
if ip != "0.0.0.0":
return ip
return "0.0.0.0"
Expand Down
5 changes: 5 additions & 0 deletions tdmgr/tasmota/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ def __init__(self):
self.devices: list[TasmotaDevice] = []
self.lwts = dict()
self.retained = set()
self.mqtt = None

def add_device(self, device: TasmotaDevice):
self.devices.append(device)
device.env = self

def find_device(self, msg: Message) -> TasmotaDevice:
for d in self.devices:
Expand Down
5 changes: 4 additions & 1 deletion tdmgr/tasmota/setoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
"description": "Allow immediate action on single button press",
"type": "select",
"parameters": {
"0": {"description": "Single, multi-press and hold button actions", "default": "True"},
"0": {
"description": "Single, multi-press and hold button actions",
"default": "True",
},
"1": {"description": "Only single press action for immediate response"},
},
},
Expand Down

0 comments on commit c424705

Please sign in to comment.