// SPDX-License-Identifier: GPL-3.0-or-later
|
|
/*
|
|
* Qt mutizone MQTT thermostat
|
|
*
|
|
* Copyright (C) 2019 Richard Genoud
|
|
*
|
|
*/
|
|
|
|
#include <QtWidgets>
|
|
#include <QPushButton>
|
|
#include <QSizePolicy>
|
|
#include <QDateTime>
|
|
#include <QTime>
|
|
#include <QDate>
|
|
#include <QtMath>
|
|
#include <QLocale>
|
|
#include <QVector>
|
|
|
|
#include "boost_dlg.h"
|
|
#include "menu_dlg.h"
|
|
#include "edit_dlg.h"
|
|
#include "holiday_dlg.h"
|
|
#include "mqttclient.h"
|
|
#include "zoneitem.h"
|
|
#include "settings.h"
|
|
#include "mainwindow.h"
|
|
|
|
MainWindow::MainWindow(QWidget *parent) :
|
|
QMainWindow(parent)
|
|
{
|
|
ZoneItem *zone;
|
|
|
|
m_boost = NULL;
|
|
m_menu = NULL;
|
|
|
|
Settings *s = Settings::getInstance();
|
|
|
|
for (int i = 0; i < s->nbZones(); i++) {
|
|
zone = new ZoneItem(s->m_rooms.at(i).name, this);
|
|
zone->setProperty("idx", i);
|
|
zone->m_target_temperature = get_target_temperature(i);
|
|
connect(zone, SIGNAL(clicked()), this, SLOT(show_boost()));
|
|
connect(zone, SIGNAL(long_clicked()), this, SLOT(do_show_edit_dlg()));
|
|
m_zones << zone;
|
|
}
|
|
|
|
/*
|
|
* Sensors-related layout
|
|
*/
|
|
QGridLayout *mainLayout = new QGridLayout;
|
|
|
|
m_state_btn = new QPushButton("Auto");
|
|
QPushButton *menu_btn = new QPushButton(tr("Menu"));
|
|
|
|
if (s->nbZones() < MAX_NB_ZONES) {
|
|
// TODO
|
|
qDebug() << "bad configuration" ;
|
|
} else {
|
|
mainLayout->addWidget(m_zones.at(0), 0, 0);
|
|
mainLayout->addWidget(m_zones.at(1), 0, 1);
|
|
mainLayout->addWidget(menu_btn, 0, 2);
|
|
mainLayout->addWidget(m_zones.at(2), 1, 0);
|
|
mainLayout->addWidget(m_zones.at(3), 1, 1);
|
|
mainLayout->addWidget(m_state_btn, 1, 2);
|
|
}
|
|
update_state_btn(s->m_state);
|
|
|
|
QSizePolicy *szPolicy = new QSizePolicy(QSizePolicy::Minimum,
|
|
QSizePolicy::MinimumExpanding,
|
|
QSizePolicy::PushButton);
|
|
|
|
QFont font = menu_btn->font();
|
|
font.setPointSize(12);
|
|
font.setBold(true);
|
|
menu_btn->setFont(font);
|
|
m_state_btn->setFont(font);
|
|
|
|
menu_btn->setSizePolicy(*szPolicy);
|
|
m_state_btn->setSizePolicy(*szPolicy);
|
|
|
|
QWidget *mainWidget = new QWidget;
|
|
mainWidget->setLayout(mainLayout);
|
|
|
|
m_central_widget.addWidget(mainWidget);
|
|
|
|
/*
|
|
* Top widget
|
|
*/
|
|
setCentralWidget(&m_central_widget);
|
|
|
|
setWindowTitle(tr("Sorico's thermostat"));
|
|
|
|
/*
|
|
* MQTT client
|
|
*/
|
|
m_mqtt = new MQTTClient(s->m_broker.address, s->m_broker.port, NULL);
|
|
|
|
m_mqtt->connectToHost();
|
|
|
|
connect(m_mqtt, SIGNAL(new_temperature(int, double)),
|
|
this, SLOT(temperature_slot(int, double)));
|
|
connect(m_mqtt, SIGNAL(new_hygro(int, double)),
|
|
this, SLOT(hygro_slot(int, double)));
|
|
connect(m_mqtt, SIGNAL(new_availability(int, bool)),
|
|
this, SLOT(availability_slot(int, bool)));
|
|
|
|
connect(this, SIGNAL(setAllHeatersOn(bool)), m_mqtt, SLOT(allHeatersOn(bool)));
|
|
connect(m_mqtt, SIGNAL(connected(void)), this, SLOT(apply_order_to_heaters(void)));
|
|
|
|
connect(m_state_btn, SIGNAL(clicked()), this, SLOT(change_state()));
|
|
|
|
connect(menu_btn, SIGNAL(clicked()), this, SLOT(show_menu()));
|
|
/*
|
|
* Heater timer
|
|
*/
|
|
m_timer = new QTimer(this);
|
|
connect(m_timer, SIGNAL(timeout()), this, SLOT(apply_order_to_heaters()));
|
|
m_timer->start(60000);
|
|
}
|
|
|
|
MainWindow::~MainWindow()
|
|
{
|
|
}
|
|
|
|
void MainWindow::boost_slot(int idx, struct boost_data &data)
|
|
{
|
|
if (idx == -1) {
|
|
/*
|
|
* user has hit cancel
|
|
*/
|
|
goto out;
|
|
}
|
|
|
|
qDebug() << "room " << m_zones.at(idx)->m_name << " new BOOST " << data.temperature << " end: " << data.end_date;
|
|
|
|
m_zones.at(idx)->m_boost = data;
|
|
|
|
apply_order_to_heaters();
|
|
out:
|
|
show_main();
|
|
}
|
|
|
|
void MainWindow::show_boost(void)
|
|
{
|
|
QObject* obj = sender();
|
|
QVariant v;
|
|
|
|
v = obj->property("idx");
|
|
if (!v.isValid())
|
|
return;
|
|
|
|
int idx = v.toInt();
|
|
|
|
if ((idx < 0) || (idx >= MAX_NB_ZONES))
|
|
return;
|
|
|
|
m_boost = new BoostDlg(idx, m_zones.at(idx)->m_name, m_zones.at(idx)->m_target_temperature, NULL);
|
|
|
|
connect(m_boost, SIGNAL(boost_changed(int, struct boost_data &)), this, SLOT(boost_slot(int, struct boost_data &)));
|
|
m_central_widget.addWidget(m_boost);
|
|
m_central_widget.setCurrentWidget(m_boost);
|
|
}
|
|
|
|
void MainWindow::do_show_edit_dlg(void)
|
|
{
|
|
QObject* obj = sender();
|
|
QVariant v;
|
|
|
|
v = obj->property("idx");
|
|
if (!v.isValid())
|
|
return;
|
|
|
|
int idx = v.toInt();
|
|
|
|
if ((idx < 0) || (idx >= MAX_NB_ZONES))
|
|
return;
|
|
|
|
m_edit_dlg = new EditDlg(idx);
|
|
|
|
connect(m_edit_dlg, SIGNAL(close_edit_dlg(void)), this, SLOT(show_main(void)));
|
|
m_central_widget.addWidget(m_edit_dlg);
|
|
m_central_widget.setCurrentWidget(m_edit_dlg);
|
|
}
|
|
|
|
void MainWindow::do_show_holiday_dlg(void)
|
|
{
|
|
HolidayDlg *holiday_dlg = new HolidayDlg(this);
|
|
|
|
m_central_widget.addWidget(holiday_dlg);
|
|
m_central_widget.setCurrentWidget(holiday_dlg);
|
|
connect(holiday_dlg, SIGNAL(close_holiday_dlg(void)), this, SLOT(do_close_holiday_dlg(void)));
|
|
connect(holiday_dlg, SIGNAL(holiday_mode(QDateTime)), this, SLOT(set_holiday_mode(QDateTime)));
|
|
}
|
|
|
|
void MainWindow::do_close_holiday_dlg(void)
|
|
{
|
|
QWidget *current = m_central_widget.currentWidget();
|
|
|
|
if (current == NULL)
|
|
return;
|
|
|
|
m_central_widget.removeWidget(current);
|
|
delete current;
|
|
}
|
|
|
|
void MainWindow::set_holiday_mode(QDateTime end_date)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
|
|
s->m_end_holiday = end_date;
|
|
apply_order_to_heaters();
|
|
}
|
|
|
|
void MainWindow::show_menu(void)
|
|
{
|
|
m_menu = new MenuDlg(NULL);
|
|
|
|
connect(m_menu, SIGNAL(closed(void)), this, SLOT(show_main(void)));
|
|
connect(m_menu, SIGNAL(show_holiday_dlg(void)), this, SLOT(do_show_holiday_dlg(void)));
|
|
|
|
m_central_widget.addWidget(m_menu);
|
|
m_central_widget.setCurrentWidget(m_menu);
|
|
}
|
|
|
|
void MainWindow::show_main(void)
|
|
{
|
|
QWidget *current = m_central_widget.currentWidget();
|
|
|
|
if (current == NULL)
|
|
goto out;
|
|
|
|
if (m_central_widget.count() < 1) {
|
|
/* WTF ?! */
|
|
return;
|
|
}
|
|
|
|
if (m_central_widget.count() == 1) {
|
|
/* nothing to do */
|
|
return;
|
|
}
|
|
|
|
if (current == m_boost) {
|
|
m_boost = NULL;
|
|
}
|
|
|
|
if (current == m_menu) {
|
|
m_menu = NULL;
|
|
}
|
|
|
|
if (current == m_edit_dlg) {
|
|
m_edit_dlg = NULL;
|
|
}
|
|
|
|
m_central_widget.removeWidget(current);
|
|
delete current;
|
|
|
|
out:
|
|
m_central_widget.setCurrentIndex(0);
|
|
}
|
|
|
|
void MainWindow::update_state_btn(enum power_states st)
|
|
{
|
|
switch (st) {
|
|
case OFF:
|
|
m_state_btn->setText(tr("Force off"));
|
|
break;
|
|
case ON:
|
|
m_state_btn->setText(tr("Force on"));
|
|
break;
|
|
case AUTO:
|
|
m_state_btn->setText(tr("Auto"));
|
|
break;
|
|
default:
|
|
m_state_btn->setText(tr("Error"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
double MainWindow::get_target_temperature(int room_idx)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
const struct Room *r = NULL;
|
|
const struct Program *p = NULL;
|
|
const struct boost_data *b = NULL;
|
|
double target = FORCE_OFF;
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
uint8_t dow = QDate::currentDate().dayOfWeek();
|
|
|
|
if ((room_idx < 0) || (room_idx >= MAX_NB_ZONES)) {
|
|
goto out;
|
|
}
|
|
|
|
r = &(s->m_rooms.at(room_idx));
|
|
target = r->default_temperature;
|
|
if (!dow) {
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Check if there's a boost programmation
|
|
*/
|
|
if (room_idx < m_zones.count()) {
|
|
b = &(m_zones.at(room_idx)->m_boost);
|
|
if (b->end_date.isValid() && (b->end_date > now)) {
|
|
/* BOOST is active */
|
|
target = b->temperature;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* dayOfWeek() returns 1 for monday, 7 for sunday.
|
|
* so dow -= 1 gives 0 for monday, 6 for sunday.
|
|
* And (1 << dow) gives the day of week as a bit field
|
|
*/
|
|
dow -= 1;
|
|
for (int i = 0; i < r->progs.count(); i++) {
|
|
p = &(r->progs.at(i));
|
|
if ((1 << dow) & p->DoW) {
|
|
if (p->start_time == p->end_time) {
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Ex: from 10h to 18h
|
|
*/
|
|
if (p->start_time < p->end_time) {
|
|
if ((now.time() >= p->start_time) && (now.time() < p->end_time)) {
|
|
target = p->temperature;
|
|
}
|
|
} else {
|
|
/*
|
|
* Ex: from 22h to 6h
|
|
*/
|
|
if (now.time() >= p->start_time) {
|
|
target = p->temperature;
|
|
} else {
|
|
if (now.time() < p->end_time) {
|
|
target = p->temperature;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
out:
|
|
return target;
|
|
}
|
|
|
|
bool MainWindow::get_heater_order(int room_idx)
|
|
{
|
|
bool should_heat = false;
|
|
|
|
if ((room_idx < 0) || (room_idx >= MAX_NB_ZONES)) {
|
|
goto out;
|
|
}
|
|
|
|
if (m_zones.at(room_idx)->m_temperature_value == FORCE_OFF) {
|
|
goto out;
|
|
}
|
|
if (!m_zones.at(room_idx)->m_available) {
|
|
goto out;
|
|
}
|
|
|
|
/* TODO: make it smarter */
|
|
if (qFabs(get_target_temperature(room_idx)
|
|
- m_zones.at(room_idx)->m_temperature_value) < 0.1) {
|
|
/*
|
|
* Stay like before
|
|
*/
|
|
should_heat = m_zones.at(room_idx)->m_heating_on;
|
|
} else {
|
|
|
|
if (get_target_temperature(room_idx) >
|
|
m_zones.at(room_idx)->m_temperature_value) {
|
|
should_heat = true;
|
|
}
|
|
}
|
|
|
|
out:
|
|
return should_heat;
|
|
}
|
|
|
|
void MainWindow::apply_automatic_state(void)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
int i;
|
|
|
|
qDebug() << "apply auto state";
|
|
for (i = 0; i < s->m_rooms.count(); i++) {
|
|
const struct Room *r = &(s->m_rooms.at(i));
|
|
m_zones.at(i)->m_heating_on = get_heater_order(i);
|
|
m_zones.at(i)->m_target_temperature = get_target_temperature(i);
|
|
m_zones.at(i)->refresh();
|
|
qDebug() << "room " << r->name;
|
|
for (int j = 0; j < r->heaters.count(); j++) {
|
|
const struct Heater *h = &(r->heaters.at(j));
|
|
qDebug() << "heater " << h->ctrl_topic;
|
|
m_mqtt->publish_msg(h->ctrl_topic,
|
|
m_zones.at(i)->m_heating_on ? "1" : "0");
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::change_state(void)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
|
|
switch (s->m_state) {
|
|
case OFF:
|
|
s->m_state = ON;
|
|
break;
|
|
case ON:
|
|
s->m_state = AUTO;
|
|
break;
|
|
case AUTO:
|
|
/* fall through */
|
|
default:
|
|
s->m_state = OFF;
|
|
break;
|
|
}
|
|
|
|
update_state_btn(s->m_state);
|
|
|
|
apply_order_to_heaters();
|
|
}
|
|
|
|
void MainWindow::apply_order_to_heaters(void)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
bool heating_order = false;
|
|
|
|
if (s->m_end_holiday > QDateTime::currentDateTime()) {
|
|
qDebug() << "Holiday mode => emit ALL_OFF order";
|
|
emit setAllHeatersOn(false);
|
|
} else {
|
|
switch (s->m_state) {
|
|
case AUTO:
|
|
qDebug() << "apply AUTO state";
|
|
apply_automatic_state();
|
|
return;
|
|
case ON:
|
|
heating_order = true;
|
|
/* fall through */
|
|
case OFF:
|
|
/* fall through */
|
|
default:
|
|
emit setAllHeatersOn(heating_order);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < s->m_rooms.count(); i++) {
|
|
m_zones.at(i)->refresh();
|
|
}
|
|
}
|
|
|
|
void MainWindow::temperature_slot(int idx, double val)
|
|
{
|
|
static bool temp_received[MAX_NB_ZONES];
|
|
|
|
Settings *s = Settings::getInstance();
|
|
|
|
qDebug() << "temperature idx:" << idx << " val: " << val << endl;
|
|
if (idx < s->nbZones()) {
|
|
m_zones.at(idx)->m_temperature_value = val;
|
|
m_zones.at(idx)->refresh();
|
|
if (!temp_received[idx]) {
|
|
temp_received[idx] = true;
|
|
apply_order_to_heaters();
|
|
}
|
|
}
|
|
}
|
|
|
|
void MainWindow::hygro_slot(int idx, double val)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
|
|
qDebug() << "hygro idx:" << idx << " val: " << val << endl;
|
|
if (idx < s->nbZones()) {
|
|
m_zones.at(idx)-> m_hygro_value = val;
|
|
m_zones.at(idx)->refresh();
|
|
}
|
|
}
|
|
|
|
void MainWindow::availability_slot(int idx, bool ok)
|
|
{
|
|
Settings *s = Settings::getInstance();
|
|
|
|
if (idx < s->nbZones()) {
|
|
m_zones.at(idx)->m_available = ok;
|
|
if (!ok) {
|
|
m_zones.at(idx)->m_hygro_value = 0;
|
|
m_zones.at(idx)->m_temperature_value = 0;
|
|
m_zones.at(idx)->refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* vim: set tabstop=8 shiftwidth=8 softtabstop=0 noexpandtab: */
|