Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Smarter shutters detection #297

Merged
merged 1 commit into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading