Esempio n. 1
0
class TimersDialog(QDialog):
    sendCommand = pyqtSignal(str, str)

    def __init__(self, device, *args, **kwargs):
        super(TimersDialog, self).__init__(*args, **kwargs)
        self.device = device
        self.timers = {}
        self.setWindowTitle("Timers [{}]".format(
            self.device.p['FriendlyName1']))

        vl = VLayout()

        self.gbTimers = GroupBoxV("Enabled", spacing=5)
        self.gbTimers.setCheckable(True)
        self.gbTimers.toggled.connect(self.toggleTimers)

        self.cbTimer = QComboBox()
        self.cbTimer.addItems(["Timer{}".format(nr + 1) for nr in range(16)])
        self.cbTimer.currentTextChanged.connect(self.loadTimer)

        hl_tmr_arm_rpt = HLayout(0)
        self.cbTimerArm = QCheckBox("Arm")
        self.cbTimerArm.clicked.connect(lambda x: self.describeTimer())
        self.cbTimerRpt = QCheckBox("Repeat")
        self.cbTimerRpt.clicked.connect(lambda x: self.describeTimer())
        hl_tmr_arm_rpt.addWidgets([self.cbTimerArm, self.cbTimerRpt])

        hl_tmr_out_act = HLayout(0)
        self.cbxTimerOut = QComboBox()
        self.cbxTimerOut.addItems(self.device.power().keys())
        self.cbxTimerOut.currentIndexChanged.connect(
            lambda x: self.describeTimer())
        self.cbxTimerAction = QComboBox()
        self.cbxTimerAction.addItems(["Off", "On", "Toggle", "Rule"])
        self.cbxTimerAction.currentIndexChanged.connect(
            lambda x: self.describeTimer())
        hl_tmr_out_act.addWidgets([self.cbxTimerOut, self.cbxTimerAction])

        self.TimerMode = QButtonGroup()
        rbTime = QRadioButton("Time")
        rbSunrise = QRadioButton("Sunrise ({})".format(
            self.device.p['Sunrise']))
        rbSunset = QRadioButton("Sunset ({})".format(self.device.p['Sunset']))
        self.TimerMode.addButton(rbTime, 0)
        self.TimerMode.addButton(rbSunrise, 1)
        self.TimerMode.addButton(rbSunset, 2)
        self.TimerMode.buttonClicked.connect(lambda x: self.describeTimer())
        gbTimerMode = GroupBoxH("Mode")
        gbTimerMode.addWidgets(self.TimerMode.buttons())

        hl_tmr_time = HLayout(0)
        self.cbxTimerPM = QComboBox()
        self.cbxTimerPM.addItems(["+", "-"])
        self.cbxTimerPM.currentIndexChanged.connect(
            lambda x: self.describeTimer())

        self.TimerMode.buttonClicked[int].connect(
            lambda x: self.cbxTimerPM.setEnabled(x != 0))
        self.teTimerTime = QTimeEdit()
        self.teTimerTime.setButtonSymbols(QTimeEdit.NoButtons)
        self.teTimerTime.setAlignment(Qt.AlignCenter)
        self.teTimerTime.timeChanged.connect(lambda x: self.describeTimer())

        lbWnd = QLabel("Window:")
        lbWnd.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
        self.cbxTimerWnd = QComboBox()
        self.cbxTimerWnd.addItems([str(x).zfill(2) for x in range(0, 16)])
        self.cbxTimerWnd.currentIndexChanged.connect(
            lambda x: self.describeTimer())

        hl_tmr_days = HLayout(0)
        self.TimerWeekday = QButtonGroup()
        self.TimerWeekday.setExclusive(False)
        for i, wd in enumerate(
            ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]):
            cb = QCheckBox(wd)
            cb.clicked.connect(lambda x: self.describeTimer())
            hl_tmr_days.addWidget(cb)
            self.TimerWeekday.addButton(cb, i)

        gbTimerDesc = GroupBoxV("Timer description", 5)
        gbTimerDesc.setMinimumHeight(200)
        self.lbTimerDesc = QLabel()
        self.lbTimerDesc.setAlignment(Qt.AlignCenter)
        self.lbTimerDesc.setWordWrap(True)
        gbTimerDesc.layout().addWidget(self.lbTimerDesc)

        hl_tmr_time.addWidgets(
            [self.cbxTimerPM, self.teTimerTime, lbWnd, self.cbxTimerWnd])

        self.gbTimers.layout().addWidget(self.cbTimer)
        self.gbTimers.layout().addLayout(hl_tmr_arm_rpt)
        self.gbTimers.layout().addLayout(hl_tmr_out_act)
        self.gbTimers.layout().addWidget(gbTimerMode)
        self.gbTimers.layout().addLayout(hl_tmr_time)
        self.gbTimers.layout().addLayout(hl_tmr_days)

        btns = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
        reload = btns.addButton("Reload", QDialogButtonBox.ResetRole)

        btns.accepted.connect(self.saveTimer)
        btns.rejected.connect(self.reject)
        reload.clicked.connect(
            lambda: self.loadTimer(self.cbTimer.currentText()))

        vl.addWidgets([self.gbTimers, gbTimerDesc, btns])
        self.setLayout(vl)

    def toggleTimers(self, state):
        self.sendCommand.emit(self.device.cmnd_topic('timers'),
                              "ON" if state else "OFF")

    def loadTimer(self, timer=""):
        if not timer:
            timer = self.cbTimer.currentText()
        payload = self.timers[timer]

        if payload:
            self.blockSignals(True)
            self.cbTimerArm.setChecked(payload['Arm'])
            self.cbTimerRpt.setChecked(payload['Repeat'])
            self.cbxTimerAction.setCurrentIndex(payload['Action'])

            output = payload.get('Output')
            if output:
                self.cbxTimerOut.setEnabled(True)
                self.cbxTimerOut.setCurrentIndex(output - 1)
            else:
                self.cbxTimerOut.setEnabled(False)

            mode = payload.get('Mode', 0)
            self.TimerMode.button(mode).setChecked(True)

            h, m = map(int, payload["Time"].split(":"))
            if h < 0:
                self.cbxTimerPM.setCurrentText("-")
                h *= -1
            self.teTimerTime.setTime(QTime(h, m))
            self.cbxTimerWnd.setCurrentText(str(payload['Window']).zfill(2))
            for wd, v in enumerate(payload['Days']):
                self.TimerWeekday.button(wd).setChecked(int(v))

            self.blockSignals(False)
            self.describeTimer()

    def describeTimer(self):
        if self.cbTimerArm.isChecked():
            desc = {
                'days': '',
                'repeat': '',
                'timer': self.cbTimer.currentText().upper()
            }
            repeat = self.cbTimerRpt.isChecked()
            out = self.cbxTimerOut.currentText()
            act = self.cbxTimerAction.currentText()
            mode = self.TimerMode.checkedId()
            pm = self.cbxTimerPM.currentText()
            time = self.teTimerTime.time()
            wnd = int(self.cbxTimerWnd.currentText()) * 60

            if mode == 0:
                if wnd == 0:
                    desc['time'] = "at {}".format(time.toString("hh:mm"))
                else:
                    desc['time'] = "somewhere between {} and {}".format(
                        time.addSecs(wnd * -1).toString("hh:mm"),
                        time.addSecs(wnd).toString("hh:mm"))
            else:
                prefix = "before" if pm == "-" else "after"
                mode_desc = "sunrise" if mode == 1 else "sunset"
                window = "somewhere in a {} minute window centered around ".format(
                    wnd // 30)
                desc['time'] = "{}h{}m {} {}".format(time.hour(),
                                                     time.minute(), prefix,
                                                     mode_desc)

                if wnd > 0:
                    desc['time'] = window + desc['time']

            if repeat:
                day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
                days = [cb.isChecked() for cb in self.TimerWeekday.buttons()]
                if days.count(True) == 7:
                    desc['days'] = "everyday"
                else:
                    days_list = [day_names[d] for d in range(7) if days[d]]
                    desc['days'] = "on every {}".format(", ".join(days_list))
            else:
                desc['repeat'] = "only ONCE"

            if act == "Rule":
                desc['action'] = "trigger clock#Timer={}".format(
                    self.cbTimer.currentIndex() + 1)
                text = "{timer} will {action} {time} {days} {repeat}".format(
                    **desc)

            elif self.cbxTimerOut.count() > 0:

                if act == "Toggle":
                    desc['action'] = "TOGGLE {}".format(out.upper())
                else:
                    desc['action'] = "set {} to {}".format(
                        out.upper(), act.upper())

                text = "{timer} will {action} {time} {days} {repeat}".format(
                    **desc)
            else:
                text = "{timer} will do nothing because there are no relays configured.".format(
                    **desc)

            self.lbTimerDesc.setText(text)

        else:
            self.lbTimerDesc.setText(
                "{} is not armed, it will do nothing.".format(
                    self.cbTimer.currentText().upper()))

    def saveTimer(self):
        payload = {
            "Arm":
            int(self.cbTimerArm.isChecked()),
            "Mode":
            self.TimerMode.checkedId(),
            "Time":
            self.teTimerTime.time().toString("hh:mm"),
            "Window":
            self.cbxTimerWnd.currentIndex(),
            "Days":
            "".join([
                str(int(cb.isChecked())) for cb in self.TimerWeekday.buttons()
            ]),
            "Repeat":
            int(self.cbTimerRpt.isChecked()),
            "Output":
            self.cbxTimerOut.currentIndex() + 1,
            "Action":
            self.cbxTimerAction.currentIndex()
        }
        self.sendCommand.emit(
            self.device.cmnd_topic(self.cbTimer.currentText()), dumps(payload))
        QMessageBox.information(
            self, "Timer saved",
            "{} data sent to device.".format(self.cbTimer.currentText()))

    @pyqtSlot(str, str)
    def parseMessage(self, topic, msg):
        if self.device.matches(topic):
            if self.device.reply == "RESULT" or self.device.reply == "TIMERS":
                try:
                    payload = loads(msg)
                    first = list(payload)[0]

                except JSONDecodeError as e:
                    error = "Timer loading error", "Can't load the timer from device.\n{}".format(
                        e)
                    logging.critical(error)
                    QMessageBox.critical(self, error)

                else:
                    if first == 'Timers':
                        self.gbTimers.setChecked(payload[first] == "ON")

                    elif first.startswith('Timers'):
                        self.timers.update(payload[first])

                    if first == 'Timers4':
                        self.loadTimer(self.cbTimer.currentText())
Esempio n. 2
0
class DevicesConfigWidget(QWidget):
    def __init__(self, parent, topic, *args, **kwargs):
        super(DevicesConfigWidget, self).__init__(*args, **kwargs)

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()),
                                  QSettings.IniFormat)
        self.topic = topic
        self.full_topic = self.settings.value(
            'Devices/{}/full_topic'.format(topic))
        self.friendly_name = self.settings.value(
            'Devices/{}/friendly_name'.format(topic))

        self.cmnd_topic = self.full_topic.replace("%topic%",
                                                  self.topic).replace(
                                                      "%prefix%", 'cmnd')
        self.tele_topic = self.full_topic.replace(
            "%topic%", self.topic).replace("%prefix%", 'tele') + "+"
        self.stat_topic = self.full_topic.replace(
            "%topic%", self.topic).replace("%prefix%", 'stat') + "+"

        self.setWindowTitle(self.friendly_name)

        self.mqtt = MqttClient()

        self.module = None
        self.modules = []

        self.gpios = []
        self.supported_gpios = []
        self.gpio_cb = []
        self.timers = False

        self.setLayout(VLayout(margin=[0, 6, 0, 0], spacing=3))

        self.lbModule = QLabel("Connecting...")
        fnt = self.lbModule.font()
        fnt.setPointSize(14)
        fnt.setBold(True)
        self.lbModule.setFont(fnt)
        self.lbModule.setAlignment(Qt.AlignCenter)
        self.lbModule.setMaximumHeight(25)
        self.layout().addWidget(self.lbModule)

        self.build_tabs()

        self.create_timers()

    def create_timers(self):
        self.auto_timer = QTimer()
        self.auto_timer.setInterval(1000)
        self.auto_timer.timeout.connect(self.auto)
        self.auto_timer.start()
        self.mqtt_timer = QTimer()
        self.mqtt_timer.setSingleShot(True)
        self.mqtt_timer.timeout.connect(self.setupMqtt)
        self.mqtt_timer.start(1000)

    def build_tabs(self):
        self.tabs = QTabWidget()

        tabInformation = self.tabInformation()
        self.tabs.addTab(tabInformation, "Information")

        tabModule = self.tabModule()
        self.tabs.addTab(tabModule, "Module and Firmware")

        self.rule_grps = []
        tabRules = self.tabRules()
        self.tabs.addTab(tabRules, "Rules and Timers")
        self.pbRTSet.clicked.connect(self.saveRuleTimers)
        self.pbVMSet.clicked.connect(self.saveVarMem)

        self.rg.pbSave.clicked.connect(self.saveRule)

        self.tabs.currentChanged.connect(self.tabChanged)
        self.tabs.setEnabled(False)
        self.layout().addWidget(self.tabs)

    def auto(self):
        if self.mqtt.state == self.mqtt.Connected:
            if self.pbRTPoll.isChecked():
                self.loadRuleTimers()

            if self.pbVMPoll.isChecked():
                self.loadVarMem()

    def tabChanged(self, tab):
        if tab == 2:
            self.loadRule(self.rg.cbRule.currentIndex())
            self.loadTimer(self.cbTimer.currentIndex())
            self.loadRuleTimers()
            self.loadVarMem()

    def setupMqtt(self):
        self.mqtt.hostname = self.settings.value('hostname', 'localhost')
        self.mqtt.port = self.settings.value('port', 1883, int)

        if self.settings.value('username'):
            self.mqtt.setAuth(self.settings.value('username'),
                              self.settings.value('password'))

        self.mqtt.connected.connect(self.mqtt_subscribe)
        self.mqtt.messageSignal.connect(self.mqtt_message)
        self.mqtt.connectToHost()

    def mqtt_subscribe(self):
        self.mqtt.subscribe(self.tele_topic)
        self.mqtt.subscribe(self.stat_topic)

        self.initial_query()

        self.tabs.setEnabled(True)

    def mqtt_message(self, topic, msg):
        match = match_topic(self.full_topic, topic)
        if match:
            match = match.groupdict()
            reply = match['reply']

            try:
                msg = loads(msg)

                if reply == "RESULT":
                    first = list(msg)[0]

                    if first.startswith('Rule'):
                        self.parseRule(msg)

                    elif first == "T1":
                        self.parseRuleTimer(msg)

                    elif first.startswith('Var'):
                        self.parseVarMem(msg, 0)

                    elif first.startswith('Mem'):
                        self.parseVarMem(msg, 1)

                    elif first == "Module":
                        self.module = msg[first]
                        self.updateCBModules()

                    elif first.startswith('Modules'):
                        self.parseModules(msg)

                    elif first.startswith(
                            'GPIO') and not first.startswith('GPIOs'):
                        self.parse_available_gpio(msg)

                    elif first.startswith('GPIOs'):
                        self.parse_supported_peripherals(msg)

                    elif not first.startswith("Timers") and first.startswith(
                            "Timer"):
                        self.parseTimer(msg[first])

                    elif first == "Timers":
                        self.gbTimers.setChecked(msg[first] == "ON")

                    elif first.startswith("Timers"):
                        pass

                    else:
                        print(msg)

                elif reply in ("STATE", "STATUS11"):
                    if reply == "STATUS11":
                        msg = msg['StatusSTS']

                    self.wifi_model.item(0, 1).setText("{} ({})".format(
                        msg['Wifi'].get('SSId', "n/a"),
                        msg['Wifi'].get('RSSI', "n/a")))
                    self.power = {
                        k: msg[k]
                        for k in msg.keys() if k.startswith("POWER")
                    }
                    self.cbxTimerOut.clear()
                    self.cbxTimerOut.addItems(self.power)

                elif reply == "STATUS":
                    msg = msg['Status']
                    self.lbModule.setText(modules.get(msg.get("Module")))

                    fname = msg['FriendlyName']
                    if isinstance(fname, str):
                        fname = [fname]
                    for i, fn in enumerate(fname):
                        self.program_model.item(6 + i, 1).setText(
                            msg['FriendlyName'][i])

                    self.mqtt_model.item(4, 1).setText("{}".format(
                        msg.get('Topic', "n/a")))

                elif reply == "STATUS1":
                    msg = msg['StatusPRM']
                    self.program_model.item(3, 1).setText("{} @{}".format(
                        msg.get('SaveCount', "n/a"),
                        msg.get('SaveAddress', "n/a")))
                    self.program_model.item(4, 1).setText("{}".format(
                        msg.get('BootCount', "n/a")))
                    self.program_model.item(5, 1).setText("{}".format(
                        msg.get('RestartReason', "n/a")))
                    self.mqtt_model.item(5, 1).setText("{}".format(
                        msg.get('GroupTopic', "n/a")))

                elif reply == "STATUS2":
                    msg = msg['StatusFWR']
                    self.program_model.item(0, 1).setText("{}".format(
                        msg.get('Version', "n/a")))
                    self.program_model.item(1, 1).setText("{}".format(
                        msg.get('BuildDateTime', "n/a")))
                    self.program_model.item(2, 1).setText("{} / {}".format(
                        msg.get('Core', "n/a"), msg.get('SDK', "n/a")))

                elif reply == "STATUS3":
                    msg = msg['StatusLOG']

                elif reply == "STATUS4":
                    msg = msg['StatusMEM']
                    self.esp_model.item(0, 1).setText("n/a")
                    self.esp_model.item(1, 1).setText("{}".format(
                        msg.get('FlashChipId', "n/a")))
                    self.esp_model.item(2, 1).setText("{}".format(
                        msg.get('FlashSize', "n/a")))
                    self.esp_model.item(3, 1).setText("{}".format(
                        msg.get('ProgramFlashSize', "n/a")))
                    self.esp_model.item(4, 1).setText("n/a")
                    self.esp_model.item(5, 1).setText("{}".format(
                        msg.get('Free', "n/a")))
                    self.esp_model.item(6, 1).setText("{}".format(
                        msg.get('Heap', "n/a")))

                elif reply == "STATUS5":
                    msg = msg['StatusNET']
                    self.wifi_model.item(1, 1).setText("{}".format(
                        msg.get('Hostname', "n/a")))
                    self.wifi_model.item(2, 1).setText("{}".format(
                        msg.get('IPAddress', "n/a")))
                    self.wifi_model.item(3, 1).setText("{}".format(
                        msg.get('Gateway', "n/a")))
                    self.wifi_model.item(4, 1).setText("{}".format(
                        msg.get('Subnetmask', "n/a")))
                    self.wifi_model.item(5, 1).setText("{}".format(
                        msg.get('DNSServer', "n/a")))
                    self.wifi_model.item(6, 1).setText("{}".format(
                        msg.get('Mac', "n/a")))

                elif reply == "STATUS6":
                    msg = msg['StatusMQT']
                    self.mqtt_model.item(0, 1).setText("{}".format(
                        msg.get('MqttHost', "n/a")))
                    self.mqtt_model.item(1, 1).setText("{}".format(
                        msg.get('MqttPort', "n/a")))
                    self.mqtt_model.item(2, 1).setText("{}".format(
                        msg.get('MqttUser', "n/a")))
                    self.mqtt_model.item(3, 1).setText("{}".format(
                        msg.get('MqttClient', "n/a")))
                    self.mqtt_model.item(6, 1).setText("{}".format(
                        self.full_topic))
                    self.mqtt_model.item(7, 1).setText("cmnd/{}_fb".format(
                        msg.get('MqttClient', "n/a")))

                elif reply == "STATUS7":
                    msg = msg['StatusTIM']
                    self._sunrise = msg.get('Sunrise', "")
                    self._sunset = msg.get('Sunset', "")
                    self.TimerMode.button(1).setText(
                        self.TimerMode.button(1).text().format(
                            msg.get('Sunrise', "")))
                    self.TimerMode.button(2).setText(
                        self.TimerMode.button(2).text().format(
                            msg.get('Sunset', "")))

            except JSONDecodeError:
                pass

    def initial_query(self):
        self.mqtt.publish(self.cmnd_topic + "status", 0)
        self.mqtt.publish(self.cmnd_topic + "timers")
        self.mqtt.publish(self.cmnd_topic + "modules")
        self.mqtt.publish(self.cmnd_topic + "module")
        self.mqtt.publish(self.cmnd_topic + "gpios")
        self.mqtt.publish(self.cmnd_topic + "gpio")

    def parseModules(self, msg):
        k = list(msg)[0]
        v = msg[k]

        if k == "Modules1":
            self.modules = v

        else:
            self.modules += v

    def updateCBModules(self):
        self.cbModule.clear()
        self.cbModule.addItems(self.modules)
        self.cbModule.setCurrentText(self.module)

    def saveModule(self):
        module = self.cbModule.currentText().split(" ")[0]
        self.mqtt.publish(self.cmnd_topic + "module", payload=module)
        self.parent().close()

    def parse_available_gpio(self, msg):
        self.gpios = list(msg)
        for i, g in enumerate(self.gpios):
            cb = QComboBox()
            cb.addItems(self.supported_gpios)
            cb.setCurrentText(msg[g])
            self.gpio_cb.append(cb)
            self.gbGPIO.layout().insertRow(0 + i, g, cb)

    def parse_supported_peripherals(self, msg):
        k = list(msg)[0]
        v = msg[k]

        if k == "GPIOs1":
            self.supported_gpios = v

        else:
            self.supported_gpios += v

    def saveGPIOs(self):
        payload = ""
        for i, g in enumerate(list(self.gpios)):
            gpio = self.gpio_cb[i].currentText().split(" ")[0]
            payload += "{} {}; ".format(g, gpio)
        self.mqtt.publish(self.cmnd_topic + "backlog", payload=payload)
        self.parent().close()

    def loadRule(self, idx):
        self.mqtt.publish(self.cmnd_topic + "Rule{}".format(idx + 1))

    def parseRule(self, msg):
        rule, once, stop, _, rules = list(msg)
        self.rg.cbEnabled.setChecked(msg[rule] == "ON")
        self.rg.cbOnce.setChecked(msg[once] == "ON")
        self.rg.cbStopOnError.setChecked(msg[stop] == "ON")
        self.rg.text.setPlainText(msg['Rules'].replace(
            " on ", "\non ").replace(" do ",
                                     " do\n\t").replace(" endon", "\nendon "))

    def saveRule(self):
        text = self.rg.text.toPlainText().replace("\n", " ").replace(
            "\t", " ").replace("  ", " ")
        backlog = {
            'rule_nr': "Rule{}".format(self.rg.cbRule.currentIndex() + 1),
            'text': text if len(text) > 0 else '""',
            'enabled': "1" if self.rg.cbEnabled.isChecked() else "0",
            'once': "5" if self.rg.cbOnce.isChecked() else "4",
            'stop': "9" if self.rg.cbStopOnError.isChecked() else "8"
        }
        self.mqtt.publish(
            self.cmnd_topic + "backlog",
            payload=
            '{rule_nr} {text}; {rule_nr} {once}; {rule_nr} {stop}; {rule_nr} {enabled}; '
            .format(**backlog))

    def loadRuleTimers(self):
        self.mqtt.publish(self.cmnd_topic + "ruletimer")

    def parseRuleTimer(self, msg):
        for c in range(8):
            itm = self.twRT.cellWidget(0, c)
            itm.setValue(int(msg["T{}".format(c + 1)]))

    def saveRuleTimers(self):
        for t in range(8):
            self.mqtt.publish(self.cmnd_topic + "ruletimer{}".format(t + 1),
                              payload=self.twRT.cellWidget(0, t).value())

    def loadVarMem(self):
        for x in range(5):
            self.mqtt.publish(self.cmnd_topic + "var{}".format(x + 1))
            self.mqtt.publish(self.cmnd_topic + "mem{}".format(x + 1))

    def parseVarMem(self, msg, row):
        k = list(msg)[0]
        nr = k[-1]
        v = msg[k]
        itm = self.twVM.cellWidget(row, int(nr) - 1)
        itm.setText(v)

    def saveVarMem(self):
        for r, cmd in enumerate(['Var', 'Mem']):
            for c in range(5):
                self.mqtt.publish(self.cmnd_topic + "{}{}".format(cmd, c + 1),
                                  payload="{}".format(
                                      self.twVM.cellWidget(r, c).text()))

    def toggleTimers(self, state):
        self.mqtt.publish(self.cmnd_topic + "timers",
                          payload="ON" if state else "OFF")

    def loadTimer(self, idx):
        self.mqtt.publish(self.cmnd_topic + "Timers")
        self.mqtt.publish(self.cmnd_topic + "Timer{}".format(idx + 1))

    def parseTimer(self, payload):
        self.blockSignals(True)
        self.cbTimerArm.setChecked(payload['Arm'])
        self.cbTimerRpt.setChecked(payload['Repeat'])
        self.cbxTimerAction.setCurrentIndex(payload['Action'])

        output = payload.get('Output')
        if output:
            self.cbxTimerOut.setEnabled(True)
            self.cbxTimerOut.setCurrentIndex(output - 1)
        else:
            self.cbxTimerOut.setEnabled(False)

        mode = payload.get('Mode')
        if not mode:
            mode = 0
            self.TimerMode.button(1).setEnabled(False)
            self.TimerMode.button(2).setEnabled(False)
        self.TimerMode.button(mode).setChecked(True)

        h, m = map(int, payload["Time"].split(":"))
        if h < 0:
            self.cbxTimerPM.setCurrentText("-")
            h *= -1
        self.teTimerTime.setTime(QTime(h, m))
        self.cbxTimerWnd.setCurrentText(str(payload['Window']).zfill(2))
        for wd, v in enumerate(payload['Days']):
            self.TimerWeekday.button(wd).setChecked(int(v))

        self.describeTimer()
        self.blockSignals(False)

    def saveTimer(self):
        payload = {
            "Arm":
            int(self.cbTimerArm.isChecked()),
            "Mode":
            self.TimerMode.checkedId(),
            "Time":
            self.teTimerTime.time().toString("hh:mm"),
            "Window":
            self.cbxTimerWnd.currentIndex(),
            "Days":
            "".join([
                str(int(cb.isChecked())) for cb in self.TimerWeekday.buttons()
            ]),
            "Repeat":
            int(self.cbTimerRpt.isChecked()),
            "Output":
            self.cbxTimerOut.currentIndex(),
            "Action":
            self.cbxTimerAction.currentIndex()
        }
        self.mqtt.publish(self.cmnd_topic +
                          "timer{}".format(self.cbTimer.currentIndex() + 1),
                          payload=dumps(payload))

    def copyTrigger(self):
        mode = self.cbxTimerAction.currentText()
        if mode == "Rule":
            trigger = "clock#Timer={}".format(self.cbTimer.currentIndex() + 1)
        else:
            trigger = "{}#state={}".format(self.cbxTimerOut.currentText(),
                                           self.cbxTimerAction.currentIndex())
        QApplication.clipboard().setText("on {} do\n\t\nendon".format(trigger))

    def describeTimer(self):
        if self.cbTimerArm.isChecked():
            desc = {'days': '', 'repeat': ''}
            desc['timer'] = self.cbTimer.currentText().upper()
            repeat = self.cbTimerRpt.isChecked()
            out = self.cbxTimerOut.currentText()
            act = self.cbxTimerAction.currentText()
            mode = self.TimerMode.checkedId()
            pm = self.cbxTimerPM.currentText()
            time = self.teTimerTime.time()
            wnd = int(self.cbxTimerWnd.currentText()) * 60

            if mode == 0:
                if wnd == 0:
                    desc['time'] = "at {}".format(time.toString("hh:mm"))
                else:
                    desc['time'] = "somewhere between {} and {}".format(
                        time.addSecs(wnd * -1).toString("hh:mm"),
                        time.addSecs(wnd).toString("hh:mm"))
            else:
                prefix = "before" if pm == "-" else "after"
                mode_desc = "sunrise" if mode == 1 else "sunset"
                window = "somewhere in a {} minute window centered around ".format(
                    wnd // 30)
                desc['time'] = "{}h{}m {} {}".format(time.hour(),
                                                     time.minute(), prefix,
                                                     mode_desc)

                if wnd > 0:
                    desc['time'] = window + desc['time']

            if repeat:
                day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
                days = [cb.isChecked() for cb in self.TimerWeekday.buttons()]
                if days.count(True) == 7:
                    desc['days'] = "everyday"
                else:
                    days_list = [day_names[d] for d in range(7) if days[d]]
                    desc['days'] = "on every {}".format(", ".join(days_list))
            else:
                desc['repeat'] = "only ONCE"

            if act == "Rule":
                desc['action'] = "trigger clock#Timer={}".format(
                    self.cbTimer.currentIndex() + 1)
                text = "{timer} will {action} {time} {days} {repeat}".format(
                    **desc)

            elif self.cbxTimerOut.count() > 0:

                if act == "Toggle":
                    desc['action'] = "TOGGLE {}".format(out.upper())
                else:
                    desc['action'] = "set {} to {}".format(
                        out.upper(), act.upper())

                text = "{timer} will {action} {time} {days} {repeat}".format(
                    **desc)
            else:
                text = "{timer} will do nothing because there are no relays configured.".format(
                    **desc)

            self.lbTimerDesc.setText(text)

        else:
            self.lbTimerDesc.setText(
                "{} is not armed, it will do nothing.".format(
                    self.cbTimer.currentText().upper()))

    def tabInformation(self):
        info = QWidget()
        vl = VLayout()

        self.program_model = QStandardItemModel()
        for d in [
                "Program version", "Build date & time", "Core/SDK version",
                "Flash write count", "Boot count", "Restart reason",
                "Friendly Name 1", "Friendly Name 2", "Friendly Name 3",
                "Friendly Name 4"
        ]:
            k = QStandardItem(d)
            k.setEditable(False)
            v = QStandardItem()
            v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
            v.setEditable(False)
            self.program_model.appendRow([k, v])

        gbPrgm = GroupBoxH("Program")
        gbPrgm.setFlat(True)
        tvPrgm = QTreeView()
        tvPrgm.setHeaderHidden(True)
        tvPrgm.setRootIsDecorated(False)
        tvPrgm.setModel(self.program_model)
        tvPrgm.resizeColumnToContents(0)
        gbPrgm.addWidget(tvPrgm)

        self.esp_model = QStandardItemModel()
        for d in [
                "ESP Chip Id", "Flash Chip Id", "Flash Size",
                "Program Flash Size", "Program Size", "Free Program Space",
                "Free Memory"
        ]:
            k = QStandardItem(d)
            k.setEditable(False)
            v = QStandardItem()
            v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
            v.setEditable(False)
            self.esp_model.appendRow([k, v])

        gbESP = GroupBoxH("ESP")
        gbESP.setFlat(True)
        tvESP = QTreeView()
        tvESP.setHeaderHidden(True)
        tvESP.setRootIsDecorated(False)
        tvESP.setModel(self.esp_model)
        tvESP.resizeColumnToContents(0)
        gbESP.addWidget(tvESP)

        # self.emul_model = QStandardItemModel()
        # for d in ["Emulation", "mDNS Discovery"]:
        #     k = QStandardItem(d)
        #     k.setEditable(False)
        #     v = QStandardItem()
        #     v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
        #     v.setEditable(False)
        #     self.emul_model.appendRow([k, v])
        #
        # gbEmul = GroupBoxH("Emulation")
        # gbEmul.setFlat(True)
        # tvEmul = QTreeView()
        # tvEmul.setHeaderHidden(True)
        # tvEmul.setRootIsDecorated(False)
        # tvEmul.setModel(self.emul_model)
        # tvEmul.resizeColumnToContents(0)
        # gbEmul.addWidget(tvEmul)

        self.wifi_model = QStandardItemModel()
        for d in [
                "AP1 SSId (RSSI)", "Hostname", "IP Address", "Gateway",
                "Subnet Mask", "DNS Server", "MAC Address"
        ]:
            k = QStandardItem(d)
            k.setEditable(False)
            v = QStandardItem()
            v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
            v.setEditable(False)
            self.wifi_model.appendRow([k, v])

        gbWifi = GroupBoxH("Wifi")
        gbWifi.setFlat(True)
        tvWifi = QTreeView()
        tvWifi.setHeaderHidden(True)
        tvWifi.setRootIsDecorated(False)
        tvWifi.setModel(self.wifi_model)
        tvWifi.resizeColumnToContents(0)
        gbWifi.addWidget(tvWifi)

        self.mqtt_model = QStandardItemModel()
        for d in [
                "MQTT Host", "MQTT Port", "MQTT User", "MQTT Client",
                "MQTT Topic", "MQTT Group Topic", "MQTT Full Topic",
                "MQTT Fallback Topic"
        ]:
            k = QStandardItem(d)
            k.setEditable(False)
            v = QStandardItem()
            v.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight)
            v.setEditable(False)
            self.mqtt_model.appendRow([k, v])

        gbMQTT = GroupBoxH("MQTT")
        gbMQTT.setFlat(True)
        tvMQTT = QTreeView()
        tvMQTT.setHeaderHidden(True)
        tvMQTT.setRootIsDecorated(False)
        tvMQTT.setModel(self.mqtt_model)
        tvMQTT.resizeColumnToContents(0)
        gbMQTT.addWidget(tvMQTT)

        hl = HLayout(0)
        vl_lc = VLayout(0, 3)
        vl_rc = VLayout(0, 3)

        vl_lc.addWidgets([gbPrgm, gbESP])
        vl_rc.addWidgets([gbWifi, gbMQTT])

        vl_rc.setStretch(0, 2)
        vl_rc.setStretch(1, 2)
        vl_rc.setStretch(2, 1)

        hl.addLayout(vl_lc)
        hl.addLayout(vl_rc)
        vl.addLayout(hl)
        info.setLayout(vl)
        return info

    def tabModule(self):
        module = QWidget()
        module.setLayout(HLayout())

        self.gbModule = GroupBoxH("Module")
        self.cbModule = QComboBox()
        self.pbModuleSet = QPushButton("Save and close (device will restart)")
        self.gbModule.addWidgets([self.cbModule, self.pbModuleSet])
        self.pbModuleSet.clicked.connect(self.saveModule)

        self.gbGPIO = QGroupBox("GPIO")
        fl_gpio = QFormLayout()
        pbGPIOSet = QPushButton("Save and close (device will restart)")
        fl_gpio.addWidget(pbGPIOSet)
        pbGPIOSet.clicked.connect(self.saveGPIOs)

        self.gbGPIO.setLayout(fl_gpio)

        mg_vl = VLayout([0, 0, 3, 0])
        mg_vl.addWidgets([self.gbModule, self.gbGPIO])
        mg_vl.setStretch(0, 1)
        mg_vl.setStretch(1, 3)

        self.gbFirmware = GroupBoxV("Firmware", margin=[3, 0, 0, 0])
        lb = QLabel("Feature under development.")
        lb.setAlignment(Qt.AlignCenter)
        lb.setEnabled(False)
        self.gbFirmware.addWidget(lb)

        module.layout().addLayout(mg_vl)
        module.layout().addWidget(self.gbFirmware)
        return module

    def tabRules(self):
        rules = QWidget()
        rules.setLayout(VLayout())
        hl = HLayout(0)
        vl_l = VLayout(0)
        self.rg = RuleGroupBox(rules, "Rule editor")
        self.rg.setFlat(True)
        self.rg.cbRule.currentIndexChanged.connect(self.loadRule)
        vl_l.addWidget(self.rg)

        gRT = GroupBoxH("Rule timers")
        vl_RT_func = VLayout(margin=[0, 0, 3, 0])
        self.pbRTPoll = QPushButton("Poll")
        self.pbRTPoll.setCheckable(True)
        self.pbRTSet = QPushButton("Set")

        vl_RT_func.addWidgets([self.pbRTPoll, self.pbRTSet])
        vl_RT_func.addStretch(1)
        gRT.layout().addLayout(vl_RT_func)

        self.twRT = QTableWidget(1, 8)
        self.twRT.setHorizontalHeaderLabels(
            ["T{}".format(i) for i in range(1, 9)])
        for c in range(8):
            self.twRT.horizontalHeader().setSectionResizeMode(
                c, QHeaderView.Stretch)
            self.twRT.setCellWidget(0, c, SpinBox(minimum=0, maximum=32766))
        self.twRT.verticalHeader().hide()

        self.twRT.verticalHeader().setDefaultSectionSize(
            self.twRT.horizontalHeader().height() * 2 + 1)
        self.twRT.setMaximumHeight(self.twRT.horizontalHeader().height() +
                                   self.twRT.rowHeight(0))
        gRT.layout().addWidget(self.twRT)

        gVM = GroupBoxH("VAR/MEM")
        vl_VM_func = VLayout(margin=[3, 0, 0, 0])
        self.pbVMPoll = QPushButton("Poll")
        self.pbVMPoll.setCheckable(True)
        self.pbVMSet = QPushButton("Set")

        vl_VM_func.addWidgets([self.pbVMPoll, self.pbVMSet])
        vl_VM_func.addStretch(1)
        gVM.layout().addLayout(vl_VM_func)

        self.twVM = QTableWidget(2, 5)
        self.twVM.setHorizontalHeaderLabels(
            ["{}".format(i) for i in range(1, 9)])
        self.twVM.setVerticalHeaderLabels(["VAR", "MEM"])
        self.twVM.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        for c in range(5):
            self.twVM.horizontalHeader().setSectionResizeMode(
                c, QHeaderView.Stretch)

        for r in range(2):
            for c in range(5):
                self.twVM.setCellWidget(r, c, QLineEdit())

        self.twVM.verticalHeader().setDefaultSectionSize(
            self.twVM.horizontalHeader().height())
        self.twVM.setMaximumHeight(self.twVM.horizontalHeader().height() +
                                   self.twVM.rowHeight(0) * 2)
        gVM.layout().addWidget(self.twVM)

        hl_rt_vm = HLayout(0)
        hl_rt_vm.addWidgets([gRT, gVM])

        hl.addLayout(vl_l)

        vl_r = VLayout(0)
        self.gbTimers = GroupBoxV("Timers", spacing=5)
        self.gbTimers.setCheckable(True)
        self.gbTimers.setChecked(False)
        self.gbTimers.toggled.connect(self.toggleTimers)

        self.cbTimer = QComboBox()
        self.cbTimer.addItems(["Timer{}".format(nr + 1) for nr in range(16)])
        self.cbTimer.currentIndexChanged.connect(self.loadTimer)

        hl_tmr_arm_rpt = HLayout(0)
        self.cbTimerArm = QCheckBox("Arm")
        self.cbTimerArm.clicked.connect(lambda x: self.describeTimer())
        self.cbTimerRpt = QCheckBox("Repeat")
        self.cbTimerRpt.clicked.connect(lambda x: self.describeTimer())
        hl_tmr_arm_rpt.addWidgets([self.cbTimerArm, self.cbTimerRpt])

        hl_tmr_out_act = HLayout(0)
        self.cbxTimerOut = QComboBox()
        self.cbxTimerOut.currentIndexChanged.connect(
            lambda x: self.describeTimer())
        self.cbxTimerAction = QComboBox()
        self.cbxTimerAction.addItems(["Off", "On", "Toggle", "Rule"])
        self.cbxTimerAction.currentIndexChanged.connect(
            lambda x: self.describeTimer())
        hl_tmr_out_act.addWidgets([self.cbxTimerOut, self.cbxTimerAction])

        self.TimerMode = QButtonGroup()
        rbTime = QRadioButton("Time")
        rbSunrise = QRadioButton("Sunrise ({})")
        rbSunset = QRadioButton("Sunset ({})")
        self.TimerMode.addButton(rbTime, 0)
        self.TimerMode.addButton(rbSunrise, 1)
        self.TimerMode.addButton(rbSunset, 2)
        self.TimerMode.buttonClicked.connect(lambda x: self.describeTimer())
        gbTimerMode = GroupBoxH("Mode")
        gbTimerMode.addWidgets(self.TimerMode.buttons())

        hl_tmr_time = HLayout(0)
        self.cbxTimerPM = QComboBox()
        self.cbxTimerPM.addItems(["+", "-"])
        self.cbxTimerPM.currentIndexChanged.connect(
            lambda x: self.describeTimer())

        self.TimerMode.buttonClicked[int].connect(
            lambda x: self.cbxTimerPM.setEnabled(x != 0))
        self.teTimerTime = QTimeEdit()
        self.teTimerTime.setButtonSymbols(QTimeEdit.NoButtons)
        self.teTimerTime.setAlignment(Qt.AlignCenter)
        self.teTimerTime.timeChanged.connect(lambda x: self.describeTimer())

        lbWnd = QLabel("Window:")
        lbWnd.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
        self.cbxTimerWnd = QComboBox()
        self.cbxTimerWnd.addItems([str(x).zfill(2) for x in range(0, 16)])
        self.cbxTimerWnd.currentIndexChanged.connect(
            lambda x: self.describeTimer())

        hl_tmr_days = HLayout(0)
        self.TimerWeekday = QButtonGroup()
        self.TimerWeekday.setExclusive(False)
        for i, wd in enumerate(
            ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]):
            cb = QCheckBox(wd)
            cb.clicked.connect(lambda x: self.describeTimer())
            hl_tmr_days.addWidget(cb)
            self.TimerWeekday.addButton(cb, i)

        gbTimerDesc = GroupBoxV("Timer description", 5)
        gbTimerDesc.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        self.lbTimerDesc = QLabel()
        self.lbTimerDesc.setAlignment(Qt.AlignCenter)
        self.lbTimerDesc.setWordWrap(True)
        gbTimerDesc.layout().addWidget(self.lbTimerDesc)
        hl_tmr_btns = HLayout(0)
        btnCopyTrigger = QPushButton("Copy trigger")
        btnTimerSave = QPushButton("Save")
        hl_tmr_btns.addWidgets([btnCopyTrigger, btnTimerSave])
        hl_tmr_btns.insertStretch(1)

        btnTimerSave.clicked.connect(self.saveTimer)
        btnCopyTrigger.clicked.connect(self.copyTrigger)

        hl_tmr_time.addWidgets(
            [self.cbxTimerPM, self.teTimerTime, lbWnd, self.cbxTimerWnd])

        self.gbTimers.layout().addWidget(self.cbTimer)
        self.gbTimers.layout().addLayout(hl_tmr_arm_rpt)
        self.gbTimers.layout().addLayout(hl_tmr_out_act)
        self.gbTimers.layout().addWidget(gbTimerMode)
        self.gbTimers.layout().addLayout(hl_tmr_time)
        self.gbTimers.layout().addLayout(hl_tmr_days)
        self.gbTimers.layout().addWidget(gbTimerDesc)
        self.gbTimers.layout().addLayout(hl_tmr_btns)

        vl_r.addWidget(self.gbTimers)

        hl.addLayout(vl_r)
        hl.setStretch(0, 2)
        hl.setStretch(1, 1)

        rules.layout().addLayout(hl)
        rules.layout().addLayout(hl_rt_vm)
        rules.layout().setStretch(0, 3)
        rules.layout().setStretch(1, 0)

        return rules

    def closeEvent(self, event):
        self.mqtt.disconnectFromHost()
        event.accept()