Compare commits

..

4 commits

28 changed files with 2922 additions and 48 deletions

129
AGENTS.md Normal file
View file

@ -0,0 +1,129 @@
# SHinterface - Smart Home Interface
## Overview
SHinterface is a Qt6-based smart home control application that interfaces with microcontrollers and various sensors to manage home automation devices. It supports both primary (master) and secondary (client) modes, allowing for distributed control across multiple devices.
## Architecture
### Core Components
1. **Main Application** (`main.cpp`)
- Entry point with command-line argument parsing
- Supports three modes:
- `PROGRAM_MODE_UI_ONLY`: Secondary client mode
- `PROGRAM_MODE_PRIMARY`: Master mode with GUI
- `PROGRAM_MODE_HEADLESS_PRIMARY`: Master mode without GUI (server only)
2. **Main Object** (`mainobject.h`, `mainobject.cpp`)
- Base class: `MainObject`
- Primary mode: `PrimaryMainObject` - Manages microcontroller, sensors, and item sources
- Secondary mode: `SecondaryMainObject` - Connects to primary via TCP
3. **Microcontroller Interface** (`microcontroller.h`, `microcontroller.cpp`)
- Communicates with embedded devices via serial or TCP
- Handles relay control, sensor data, RGB lighting, and PWM outputs
- Implements a write queue to prevent buffer overflows
### Key Systems
#### Items System
- **Item** (`items/item.h`, `items/item.cpp`): Base class for all controllable items
- Supports different value types: boolean, unsigned integer, no value
- Tracks update sources (user, actor, remote, loaded, backend)
- Item Types:
- **Relay** (`items/relay.h`, `items/relay.cpp`): Switchable outputs
- **PowerItem**: Power measurement items
- **MessageItem**: Display messages
- **SystemItem**: System-related controls
- **AuxItem**: Auxiliary PWM outputs
- **RGBItem**: RGB LED control
- **ItemStore** (`items/itemstore.h`, `items/itemstore.cpp`): Manages collection of items
- **ItemSource** (`items/itemsource.h`): Interface for item providers
- **FixedItemSource**: Static predefined items
- **ItemLoaderSource**: Loads items from configuration
#### Actors System
Actors trigger actions based on sensor conditions or timers:
- **Actor** (`actors/actor.h`, `actors/actor.cpp`): Base actor class
- Can be active/inactive and exhausted (preventing repeated triggers)
- Actor Types:
- **FactorActor**: Triggers when a factor crosses a threshold
- **PolynomalActor**: Uses polynomial calculations for triggering
- **SensorActor**: Reacts to specific sensor states
- **TimerActor**: Time-based triggering
- **AlarmTime**: Alarm/clock functionality
- **Regulator**: PID-like regulation control
#### Sensors System
- **Sensor** (`sensors/sensor.h`, `sensors/sensor.cpp`): Represents physical sensors
- Sensor types: door, temperature, humidity, pressure, brightness, button, ADC, CO2, PM2.5, VOC, etc.
- Sensor Sources:
- **SunSensorSource** (`sensors/sunsensor.h`, `sensors/sunsensor.cpp`): Solar position calculations
- **MqttSensorSource** (`sensors/mqttsensorsource.h`, `sensors/mqttsensorsource.cpp`): MQTT-based sensor data
#### Networking Services
- **TcpServer** (`service/tcpserver.h`, `service/tcpserver.cpp`): TCP server for remote control
- **WebSocketServer** (`service/websocketserver.h`, `service/websocketserver.cpp`): WebSocket interface
- **TcpClient** (`service/tcpclient.h`, `service/tcpclient.cpp`): Client for secondary instances
### UI Components
- **MainWindow** (`ui/mainwindow.ui`, `ui/mainwindow.cpp`): Main application window
- **ItemWidget**: Visual representation of items
- **ItemScrollBox**: Scrollable container for items
- **SensorListWidget**: Displays sensor information
- **ItemCreationDialog**: Create new items
- **ItemSettingsDialog**: Configure item properties
- **ActorSettingsDialog**: Configure actor behavior
## Data Flow
### Primary Mode (Master)
```
Microcontroller ↔ Items ↔ Actors ↔ Sensors
↑ ↑ ↑ ↑
TCP/WebSocket │ │ │
└──────┬──────┘
UI (MainWindow)
```
### Secondary Mode (Client)
```
Secondary Client ↔ TCP Server (Primary) → Items → Microcontroller
```
## Configuration
- Settings stored in JSON format
- Default location: `~/.config/shinterface.json`
- Command-line options:
- `-m`, `--master`: Run in master mode
- `-H`, `--host`: Set server host IP
- `-p`, `--port`: Set server port (default: 38940)
- `-c`, `--config`: Specify config file path
- `-e`, `--headless`: Run without GUI (master mode only)
## Build Requirements
- CMake 4.0+
- Qt6 (Core, Gui, Widgets, Network, Multimedia, SerialPort, Mqtt, WebSockets)
- libpipewire-0.3
- libnl-3.0
## Communication Protocols
- **Microcontroller**: Text-based protocol over serial or TCP
- **Networking**: JSON messages over TCP/WebSocket
- **Sensors**: MQTT for remote sensors, calculated values for sun position
## Key Features
1. Distributed control with primary/secondary architecture
2. Actor-based automation system
3. Multi-protocol sensor support (MQTT, serial, calculated)
4. Relay and PWM control
5. RGB lighting control
6. WebSocket API for remote access
7. Configurable via JSON
8. Cross-platform Qt application

View file

@ -24,9 +24,19 @@ set(CMAKE_AUTOUIC ON)
# Add src to include path for relative includes # Add src to include path for relative includes
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
# Enable testing framework
enable_testing()
# Add subdirectory for tests
add_subdirectory(tests)
# Create executable # Create executable
add_executable(SHinterface add_executable(SHinterface
src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp) src/sensors/mqttsensorsource.h src/sensors/mqttsensorsource.cpp
src/items/mqttitem.h src/items/mqttitem.cpp
src/mqttclient.h src/mqttclient.cpp
)
# Add sources to executable # Add sources to executable
target_sources(SHinterface target_sources(SHinterface
@ -133,6 +143,8 @@ target_sources(SHinterface
src/ui/itemsettingswidgets/relayitemsettingswidget.cpp src/ui/itemsettingswidgets/relayitemsettingswidget.cpp
src/ui/itemsettingswidgets/systemitemsettingswidget.h src/ui/itemsettingswidgets/systemitemsettingswidget.h
src/ui/itemsettingswidgets/systemitemsettingswidget.cpp src/ui/itemsettingswidgets/systemitemsettingswidget.cpp
src/ui/itemsettingswidgets/mqttitemsettingswidget.h
src/ui/itemsettingswidgets/mqttitemsettingswidget.cpp
) )
# Add UI files # Add UI files
@ -153,6 +165,7 @@ target_sources(SHinterface
src/ui/itemsettingswidgets/messageitemsettingswidget.ui src/ui/itemsettingswidgets/messageitemsettingswidget.ui
src/ui/itemsettingswidgets/relayitemsettingswidget.ui src/ui/itemsettingswidgets/relayitemsettingswidget.ui
src/ui/itemsettingswidgets/systemitemsettingswidget.ui src/ui/itemsettingswidgets/systemitemsettingswidget.ui
src/ui/itemsettingswidgets/mqttitemsettingswidget.ui
) )
# Add resource file # Add resource file

90
README.md Normal file
View file

@ -0,0 +1,90 @@
# SHinterface - Smart Home Control Interface
A Qt6-based smart home control application that interfaces with microcontrollers and sensors to manage home automation devices.
## Quick Start
### Building
```bash
mkdir build
cd build
cmake ..
make
```
### Running
**Primary (Master) Mode:**
```bash
./SHinterface -m
```
**Secondary (Client) Mode:**
```bash
./SHinterface -H 192.168.1.100 -p 38940
```
**Headless Server Mode:**
```bash
./SHinterface -m -e
```
## Features
- ✅ Control relays and PWM outputs
- ✅ Monitor various sensors (temperature, humidity, doors, etc.)
- ✅ MQTT sensor integration
- ✅ Actor-based automation system
- ✅ RGB lighting control
- ✅ WebSocket API for remote access
- ✅ Primary/secondary architecture for distributed control
## Configuration
Settings are stored in JSON format. Default location:
```
~/.config/shinterface.json
```
You can specify a custom config file with:
```bash
./SHinterface -c /path/to/config.json
```
## Usage
### Primary Mode (Master)
The primary instance connects to your microcontroller and manages all devices. It can run with or without a GUI.
### Secondary Mode (Client)
Secondary instances connect to the primary via TCP and provide additional control points without needing their own microcontroller connection.
## Command Line Options
```
-m, --master Use in master mode
-H, --host <address> Set server host IP address (default: 0.0.0.0)
-p, --port <port> Set server port (default: 38940)
-c, --config <file> Set config file path
-e, --headless Don't start the GUI (master mode only)
--help Show help
--version Show version
```
## Architecture
- **Items**: Represent controllable devices (relays, lights, etc.)
- **Actors**: Automate actions based on sensors or time
- **Sensors**: Provide data from physical sensors and calculated sources
- **Services**: TCP and WebSocket interfaces for remote control
## Dependencies
- CMake 4.0+
- Qt6 (Core, Gui, Widgets, Network, Multimedia, SerialPort, Mqtt, WebSockets)
- libpipewire-0.3
- libnl-3.0
## License
See LICENSE file for details.

View file

@ -10,6 +10,7 @@
#include "auxitem.h" #include "auxitem.h"
#include "poweritem.h" #include "poweritem.h"
#include "rgbitem.h" #include "rgbitem.h"
#include "mqttitem.h"
#include <QJsonArray> #include <QJsonArray>
@ -338,6 +339,8 @@ std::shared_ptr<Item> Item::loadItem(const QJsonObject& json)
newItem = std::shared_ptr<PowerItem>(new PowerItem); newItem = std::shared_ptr<PowerItem>(new PowerItem);
else if(json["Type"].toString("") == "Rgb") else if(json["Type"].toString("") == "Rgb")
newItem = std::shared_ptr<RgbItem>(new RgbItem); newItem = std::shared_ptr<RgbItem>(new RgbItem);
else if(json["Type"].toString("") == "Mqtt")
newItem = std::shared_ptr<MqttItem>(new MqttItem);
else else
qWarning()<<"Unable to load unkown item type: "<<json["Type"].toString(); qWarning()<<"Unable to load unkown item type: "<<json["Type"].toString();
if(newItem) if(newItem)

164
src/items/mqttitem.cpp Normal file
View file

@ -0,0 +1,164 @@
#include "mqttitem.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QtMqtt/QMqttClient>
#include "mqttclient.h"
MqttItem::MqttItem(QString name, uint8_t value, QObject *parent)
: Item(0, name, value, parent),
topic_(""),
valueKey_("state"),
valueOn_("ON"),
valueOff_("OFF")
{
hashId();
std::shared_ptr<MqttClient> workClient = client.lock();
assert(workClient);
connect(workClient->getClient().get(), &QMqttClient::stateChanged, this, &MqttItem::onClientStateChanged);
}
MqttItem::~MqttItem()
{
qDebug()<<__func__;
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient || topic_.isEmpty() || !subscription)
return;
workClient->unsubscribe(subscription);
}
void MqttItem::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::Connected)
refreshSubscription();
}
void MqttItem::refreshSubscription()
{
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient || topic_.isEmpty())
return;
if(workClient->getClient()->state() != QMqttClient::Connected)
return;
if(subscription)
{
disconnect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
workClient->unsubscribe(subscription);
}
subscription = workClient->subscribe(workClient->getBaseTopic() + "/" + getTopic());
connect(subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttItem::onMessageReceived);
}
void MqttItem::onMessageReceived(const QMqttMessage& message)
{
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
if(doc.isObject())
{
QJsonObject obj = doc.object();
if(obj.contains(getValueKey()))
{
QString value = obj[getValueKey()].toString();
ItemUpdateRequest req = createValueUpdateRequest(ITEM_UPDATE_BACKEND);
req.changes.value = true;
if(value == getValueOn())
req.payload.setValueData(true);
else
req.payload.setValueData(false);
requestUpdate(req);
}
}
}
void MqttItem::hashId()
{
QString hashString = topic_ + "/" + valueKey_;
itemId_ = qHash(hashString.toLatin1());
}
void MqttItem::setTopic(const QString& topic)
{
topic_ = topic;
hashId();
refreshSubscription();
}
void MqttItem::setValueKey(const QString& valueKey)
{
valueKey_ = valueKey;
hashId();
}
void MqttItem::setValueOn(const QString& valueOn)
{
valueOn_ = valueOn;
}
void MqttItem::setValueOff(const QString& valueOff)
{
valueOff_ = valueOff;
}
QString MqttItem::getTopic() const
{
return topic_;
}
QString MqttItem::getValueKey() const
{
return valueKey_;
}
QString MqttItem::getValueOn() const
{
return valueOn_;
}
QString MqttItem::getValueOff() const
{
return valueOff_;
}
void MqttItem::store(QJsonObject& json)
{
Item::store(json);
json["Type"] = "Mqtt";
json["Topic"] = topic_;
json["ValueKey"] = valueKey_;
json["ValueOn"] = valueOn_;
json["ValueOff"] = valueOff_;
}
void MqttItem::load(const QJsonObject& json, const bool preserve)
{
Item::load(json, preserve);
topic_ = json["Topic"].toString();
valueKey_ = json["ValueKey"].toString("state");
valueOn_ = json["ValueOn"].toString("ON");
valueOff_ = json["ValueOff"].toString("OFF");
hashId();
refreshSubscription();
}
void MqttItem::enactValue(uint8_t value)
{
std::shared_ptr<MqttClient> workClient = client.lock();
if(!workClient || topic_.isEmpty())
return;
QString fullTopic = workClient->getBaseTopic() + "/" + topic_ + "/set";
QJsonObject payload;
payload[valueKey_] = value ? valueOn_ : valueOff_;
QJsonDocument doc(payload);
QByteArray data = doc.toJson(QJsonDocument::Compact);
qDebug() << "MqttItem publishing to" << fullTopic << ":" << data;
workClient->getClient()->publish(fullTopic, data);
}

53
src/items/mqttitem.h Normal file
View file

@ -0,0 +1,53 @@
#ifndef MQTTITEM_H
#define MQTTITEM_H
#include "item.h"
#include "mqttclient.h"
class QString;
class MqttItem : public Item
{
Q_OBJECT
public:
inline static std::weak_ptr<MqttClient> client;
private:
QString topic_;
QString valueKey_;
QString valueOn_;
QString valueOff_;
MqttClient::Subscription* subscription = nullptr;
void hashId();
void refreshSubscription();
void onMessageReceived(const QMqttMessage& message);
void onClientStateChanged(QMqttClient::ClientState state);
public:
explicit MqttItem(QString name = "MqttItem",
uint8_t value = 0,
QObject *parent = nullptr);
virtual ~MqttItem() override;
void setTopic(const QString& topic);
void setValueKey(const QString& valueKey);
void setBaseTopic(const QString& baseTopic);
void setValueOn(const QString& valueOn);
void setValueOff(const QString& valueOff);
QString getTopic() const;
QString getValueKey() const;
QString getBaseTopic() const;
QString getValueOn() const;
QString getValueOff() const;
virtual void store(QJsonObject& json) override;
virtual void load(const QJsonObject& json, const bool preserve = false) override;
protected:
virtual void enactValue(uint8_t value) override;
};
#endif // MQTTITEM_H

View file

@ -0,0 +1,205 @@
#include "mqttitemsource.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
MqttItemSource::MqttItemSource(QObject *parent)
: ItemSource(parent)
{
}
MqttItemSource::~MqttItemSource()
{
}
void MqttItemSource::start(const QJsonObject& settings)
{
baseTopicName_ = settings["BaseTopic"].toString("zigbee2mqtt");
connect(&client_, &QMqttClient::stateChanged, this, &MqttItemSource::onClientStateChanged);
connect(&client_, &QMqttClient::errorChanged, this, &MqttItemSource::onClientError);
client_.setHostname(settings["Host"].toString("127.0.0.1"));
client_.setPort(settings["Port"].toInt(1883));
if(settings.contains("User"))
client_.setUsername(settings["User"].toString());
if(settings.contains("Password"))
client_.setPassword(settings["Password"].toString());
client_.setProtocolVersion(QMqttClient::MQTT_5_0);
client_.connectToHost();
QJsonArray itemsArray = settings["Items"].toArray();
for(QJsonValueRef itemRef : itemsArray)
{
QJsonObject itemObject = itemRef.toObject();
if(!itemObject.contains("Topic"))
continue;
MqttItemConfig config;
config.topic = itemObject["Topic"].toString();
config.name = itemObject.contains("Name") ? itemObject["Name"].toString() : config.topic;
config.valueKey = itemObject["ValueKey"].toString("state");
config.valueOn = itemObject["ValueOn"].toString("ON");
config.valueOff = itemObject["ValueOff"].toString("OFF");
// Determine value type
QString valueTypeStr = itemObject["ValueType"].toString("bool");
if(valueTypeStr == "uint")
config.valueType = ITEM_VALUE_UINT;
else
config.valueType = ITEM_VALUE_BOOL;
config.id = qHash(baseTopicName_ + "/" + config.topic);
// Create the item
config.item = std::make_shared<MqttItem>(config.id, config.name, 0);
config.item->setTopic(config.topic);
config.item->setValueKey(config.valueKey);
config.item->setBaseTopic(baseTopicName_);
config.item->setValueOn(config.valueOn);
config.item->setValueOff(config.valueOff);
config.item->setMqttClient(&client_);
items_.push_back(config);
}
}
MqttItemSource::MqttItemConfig* MqttItemSource::findConfig(const QString& topic)
{
for(MqttItemConfig& config : items_)
{
if(baseTopicName_ + "/" + config.topic == topic)
return &config;
}
return nullptr;
}
void MqttItemSource::onClientError(QMqttClient::ClientError error)
{
qWarning() << "MqttItemSource Client error:" << error;
}
void MqttItemSource::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::ClientState::Connected)
{
qInfo() << "MqttItemSource connected to MQTT broker at " << client_.hostname() << client_.port();
for(MqttItemConfig& config : items_)
{
// Subscribe to state topic to receive updates from devices
QString stateTopic = baseTopicName_ + "/" + config.topic;
qDebug() << "MqttItemSource subscribing to" << stateTopic;
QMqttSubscription* subscription = client_.subscribe(stateTopic);
if(subscription)
{
connect(subscription, &QMqttSubscription::messageReceived, this, &MqttItemSource::onMessageReceived);
}
}
}
else if(state == QMqttClient::ClientState::Disconnected)
{
qWarning() << "MqttItemSource lost connection to MQTT broker";
}
else if(state == QMqttClient::ClientState::Connecting)
{
qInfo() << "MqttItemSource connecting to MQTT broker at " << client_.hostname() << client_.port();
}
}
void MqttItemSource::onMessageReceived(const QMqttMessage& message)
{
MqttItemConfig* config = findConfig(message.topic().name());
if(!config)
return;
QJsonDocument doc = QJsonDocument::fromJson(message.payload());
if(doc.isObject())
{
QJsonObject obj = doc.object();
QString valueKey = config->valueKey;
if(obj.contains(valueKey))
{
uint8_t newValue = 0;
// Handle different value types
if(config->valueType == ITEM_VALUE_UINT)
{
// Numeric value (brightness, etc.)
newValue = obj[valueKey].toInt(0);
}
else
{
// Binary value (state on/off, etc.)
QString value = obj[valueKey].toString();
if(value == config->valueOn || value == "ON" || value == "true")
newValue = 1;
else if(value == config->valueOff || value == "OFF" || value == "false")
newValue = 0;
}
// Only update if value changed
if(config->item->getValue() != newValue)
{
ItemUpdateRequest update;
update.type = ITEM_UPDATE_BACKEND;
update.payload = *config->item;
update.payload.setValueData(newValue);
update.changes.value = true;
config->item->requestUpdate(update);
}
}
}
}
void MqttItemSource::refresh()
{
std::vector<ItemAddRequest> requests;
for(MqttItemConfig& config : items_)
{
ItemAddRequest request;
request.type = ITEM_UPDATE_BACKEND;
request.payload = config.item;
request.changes = ItemFieldChanges(true);
requests.push_back(request);
}
gotItems(requests);
}
void MqttItemSource::store(QJsonObject& json)
{
json["Host"] = client_.hostname();
json["Port"] = client_.port();
json["BaseTopic"] = baseTopicName_;
if(client_.username() != "")
json["User"] = client_.username();
if(client_.password() != "")
json["Password"] = client_.password();
QJsonArray itemsArray;
for(const MqttItemConfig& config : items_)
{
QJsonObject itemObject;
itemObject["Name"] = config.name;
itemObject["Topic"] = config.topic;
itemObject["ValueKey"] = config.valueKey;
itemObject["ValueOn"] = config.valueOn;
itemObject["ValueOff"] = config.valueOff;
if(config.valueType == ITEM_VALUE_UINT)
itemObject["ValueType"] = "uint";
else
itemObject["ValueType"] = "bool";
itemsArray.append(itemObject);
}
json["Items"] = itemsArray;
}
QMqttClient* MqttItemSource::getClient()
{
return &client_;
}

View file

@ -0,0 +1,34 @@
#ifndef MQTTITEMSOURCE_H
#define MQTTITEMSOURCE_H
#include <QObject>
#include <QJsonObject>
#include <QtMqtt/QMqttClient>
#include <vector>
#include <memory>
#include "itemsource.h"
#include "mqttitem.h"
class MqttItemSource : public ItemSource
{
Q_OBJECT
QString baseTopicName_;
QMqttClient client_;
private slots:
void onClientStateChanged(QMqttClient::ClientState state);
void onMessageReceived(const QMqttMessage& message);
void onClientError(QMqttClient::ClientError error);
public:
explicit MqttItemSource(QObject *parent = nullptr);
virtual ~MqttItemSource() override;
virtual void refresh() override;
void start(const QJsonObject& settings);
void store(QJsonObject& json);
QMqttClient* getClient();
};
#endif // MQTTITEMSOURCE_H

View file

@ -4,6 +4,8 @@
#include<QJsonArray> #include<QJsonArray>
#include<QMessageBox> #include<QMessageBox>
#include "mqttclient.h"
#include "items/mqttitem.h"
#include "items/itemstore.h" #include "items/itemstore.h"
MainObject::MainObject(QObject *parent) : MainObject::MainObject(QObject *parent) :
@ -74,9 +76,12 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
micro(microDevice), micro(microDevice),
tcpServer(new TcpServer(this)), tcpServer(new TcpServer(this)),
webServer(new WebSocketServer("shinterface", this)), webServer(new WebSocketServer("shinterface", this)),
mqttClient(new MqttClient),
sunSensorSource(49.824972, 8.702194), sunSensorSource(49.824972, 8.702194),
fixedItems(&micro) fixedItems(&micro)
{ {
MqttItem::client = mqttClient;
//connect sensors subsystem //connect sensors subsystem
connect(&globalSensors, &SensorStore::sensorChangedState, tcpServer, &TcpServer::sensorEvent); connect(&globalSensors, &SensorStore::sensorChangedState, tcpServer, &TcpServer::sensorEvent);
connect(tcpServer, &TcpServer::gotSensor, &globalSensors, &SensorStore::sensorGotState); connect(tcpServer, &TcpServer::gotSensor, &globalSensors, &SensorStore::sensorGotState);
@ -98,7 +103,8 @@ PrimaryMainObject::PrimaryMainObject(QIODevice* microDevice, const QString& sett
loadFromDisk(settingsPath); loadFromDisk(settingsPath);
QJsonObject mqttJson = settings["Mqtt"].toObject(); QJsonObject mqttJson = settings["Mqtt"].toObject();
mqttSensorSource.start(mqttJson); mqttClient->start(mqttJson);
mqttSensorSource.start(mqttClient, mqttJson);
tcpServer->launch(QHostAddress(host), port); tcpServer->launch(QHostAddress(host), port);
webServer->launch(QHostAddress(host), port+1); webServer->launch(QHostAddress(host), port+1);
@ -115,6 +121,7 @@ void PrimaryMainObject::store(QJsonObject &json)
{ {
globalItems.store(json); globalItems.store(json);
QJsonObject mqttJson = json["Mqtt"].toObject(); QJsonObject mqttJson = json["Mqtt"].toObject();
mqttClient->store(mqttJson);
mqttSensorSource.store(mqttJson); mqttSensorSource.store(mqttJson);
json["Mqtt"] = mqttJson; json["Mqtt"] = mqttJson;
} }

View file

@ -46,6 +46,7 @@ public:
Microcontroller micro; Microcontroller micro;
TcpServer* tcpServer; TcpServer* tcpServer;
WebSocketServer* webServer; WebSocketServer* webServer;
std::shared_ptr<MqttClient> mqttClient;
//sensors //sensors
SunSensorSource sunSensorSource; SunSensorSource sunSensorSource;

110
src/mqttclient.cpp Normal file
View file

@ -0,0 +1,110 @@
#include "mqttclient.h"
MqttClient::MqttClient():
client(new QMqttClient)
{
}
void MqttClient::start(const QJsonObject& settings)
{
baseTopicName = settings["BaseTopic"].toString("zigbee2mqtt");
QMqttClient* cl = client.get();
connect(cl, &QMqttClient::stateChanged, this, &MqttClient::onClientStateChanged);
connect(cl, &QMqttClient::errorChanged, this, &MqttClient::onClientError);
client->setHostname(settings["Host"].toString("127.0.0.1"));
client->setPort(settings["Port"].toInt(1883));
if(settings.contains("User"))
client->setUsername(settings["User"].toString());
if(settings.contains("Password"))
client->setPassword(settings["Password"].toString());
client->setProtocolVersion(QMqttClient::MQTT_5_0);
client->connectToHost();
}
void MqttClient::onClientError(QMqttClient::ClientError error)
{
qWarning()<<"MQTT Client error:"<<error;
}
void MqttClient::onClientStateChanged(QMqttClient::ClientState state)
{
if(state == QMqttClient::ClientState::Connected)
qInfo()<<"Connected to MQTT broker at "<<client->hostname()<<client->port();
else if (state == QMqttClient::ClientState::Disconnected)
qWarning()<<"Lost connection to MQTT broker";
else if(state == QMqttClient::ClientState::Connecting)
qInfo()<<"Connecting to MQTT broker at "<<client->hostname()<<client->port();
}
void MqttClient::store(QJsonObject& json)
{
json["Host"] = client->hostname();
json["Port"] = client->port();
json["BaseTopic"] = baseTopicName;
if(client->username() != "")
json["User"] = client->username();
if(client->password() != "")
json["Password"] = client->password();
}
std::shared_ptr<QMqttClient> MqttClient::getClient()
{
return client;
}
MqttClient::Subscription* MqttClient::subscribe(QString topic)
{
if(subscriptions.contains(topic))
{
MqttClient::Subscription* sub = subscriptions[topic];
++sub->ref;
return sub;
}
else
{
qDebug()<<"MqttClient: subscibeing to"<<topic;
MqttClient::Subscription* sub = new Subscription;
sub->subscription = client->subscribe(topic);
sub->ref = 1;
subscriptions.insert({topic, sub});
return sub;
}
}
void MqttClient::unsubscribe(MqttClient::Subscription* subscription)
{
QString topic = subscription->subscription->topic().filter();
unsubscribe(topic);
}
void MqttClient::unsubscribe(QString topic)
{
assert(!subscriptions.contains(topic));
MqttClient::Subscription* sub = subscriptions[topic];
if(--sub->ref > 0)
return;
qDebug()<<"MqttClient: unsubscibeing"<<sub->subscription->topic();
client->unsubscribe(sub->subscription->topic());
subscriptions.erase(topic);
delete sub;
}
QString MqttClient::getBaseTopic()
{
return baseTopicName;
}
MqttClient::~MqttClient()
{
for(const std::pair<QString, Subscription*> sub : subscriptions)
{
qWarning()<<sub.first<<"not unregistered at exit!";
client->unsubscribe(sub.second->subscription->topic());
}
}

40
src/mqttclient.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef MQTTCLIENT_H
#define MQTTCLIENT_H
#include <QMqttClient>
#include <QObject>
#include <QJsonObject>
#include <map>
class MqttClient: public QObject
{
Q_OBJECT
public:
struct Subscription
{
int ref;
QMqttSubscription* subscription;
};
private:
QString baseTopicName;
std::shared_ptr<QMqttClient> client;
std::map<QString, Subscription*> subscriptions;
private slots:
void onClientStateChanged(QMqttClient::ClientState state);
void onClientError(QMqttClient::ClientError error);
public:
explicit MqttClient();
~MqttClient();
void start(const QJsonObject& settings);
void store(QJsonObject& json);
std::shared_ptr<QMqttClient> getClient();
Subscription* subscribe(QString topic);
void unsubscribe(Subscription* subscription);
void unsubscribe(QString topic);
QString getBaseTopic();
};
#endif // MQTTCLIENT_H

View file

@ -7,22 +7,11 @@ MqttSensorSource::MqttSensorSource(QObject *parent)
{ {
} }
void MqttSensorSource::start(const QJsonObject& settings) void MqttSensorSource::start(std::shared_ptr<MqttClient> client, const QJsonObject& settings)
{ {
baseTopicName = settings["BaseTopic"].toString("zigbee2mqtt"); this->client = client;
connect(&client, &QMqttClient::stateChanged, this, &MqttSensorSource::onClientStateChanged); connect(client->getClient().get(), &QMqttClient::stateChanged, this, &MqttSensorSource::onClientStateChanged);
connect(&client, &QMqttClient::errorChanged, this, &MqttSensorSource::onClientError);
client.setHostname(settings["Host"].toString("127.0.0.1"));
client.setPort(settings["Port"].toInt(1883));
if(settings.contains("User"))
client.setUsername(settings["User"].toString());
if(settings.contains("Password"))
client.setPassword(settings["Password"].toString());
client.setProtocolVersion(QMqttClient::MQTT_5_0);
client.connectToHost();
QJsonArray sensorsArray = settings["Sensors"].toArray(); QJsonArray sensorsArray = settings["Sensors"].toArray();
@ -37,21 +26,16 @@ void MqttSensorSource::start(const QJsonObject& settings)
sensor.name = sensor.topic; sensor.name = sensor.topic;
else else
sensor.name = sensorObject["Name"].toString(); sensor.name = sensorObject["Name"].toString();
sensor.id = qHash(baseTopicName + "/" + sensor.topic); sensor.id = qHash(client->getBaseTopic() + "/" + sensor.topic);
sensors.push_back(sensor); sensors.push_back(sensor);
} }
} }
void MqttSensorSource::onClientError(QMqttClient::ClientError error)
{
qWarning()<<"MQTT Client error:"<<error;
}
MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic) MqttSensorSource::SensorSubscription& MqttSensorSource::findSubscription(const QString& topic)
{ {
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
{ {
if(baseTopicName + "/" + sensor.topic == topic) if(client->getBaseTopic() + "/" + sensor.topic == topic)
return sensor; return sensor;
} }
assert(false); assert(false);
@ -61,30 +45,24 @@ void MqttSensorSource::onClientStateChanged(QMqttClient::ClientState state)
{ {
if(state == QMqttClient::ClientState::Connected) if(state == QMqttClient::ClientState::Connected)
{ {
qInfo()<<"Connected to MQTT broker at "<<client.hostname()<<client.port();
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
{ {
qDebug()<<"MQTT subscribeing to"<<baseTopicName + "/" + sensor.topic; qDebug()<<"MQTT subscribeing to"<<client->getBaseTopic() + "/" + sensor.topic;
sensor.subscription = client.subscribe(baseTopicName + "/" + sensor.topic); sensor.subscription = client->subscribe(client->getBaseTopic() + "/" + sensor.topic);
connect(sensor.subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived); connect(sensor.subscription->subscription, &QMqttSubscription::messageReceived, this, &MqttSensorSource::onMessageReceived);
} }
} }
else if (state == QMqttClient::ClientState::Disconnected) else if (state == QMqttClient::ClientState::Disconnected)
{ {
qWarning()<<"Lost connection to MQTT broker";
for(SensorSubscription& sensor : sensors) for(SensorSubscription& sensor : sensors)
{ {
if(sensor.subscription) if(sensor.subscription)
{ {
client.unsubscribe(sensor.topic); client->unsubscribe(sensor.topic);
sensor.subscription = nullptr; sensor.subscription = nullptr;
} }
} }
} }
else if(state == QMqttClient::ClientState::Connecting)
{
qInfo()<<"Connecting to MQTT broker at "<<client.hostname()<<client.port();
}
} }
void MqttSensorSource::onMessageReceived(const QMqttMessage& message) void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
@ -107,6 +85,14 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
stateChanged(sensor); stateChanged(sensor);
} }
if(obj.contains("local_temperature"))
{
sensor.name = baseName + " Temperature";
sensor.type = Sensor::TYPE_TEMPERATURE;
sensor.field = obj["local_temperature"].toDouble(0);
stateChanged(sensor);
}
if(obj.contains("humidity")) if(obj.contains("humidity"))
{ {
sensor.name = baseName + " Humidity"; sensor.name = baseName + " Humidity";
@ -167,13 +153,6 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
void MqttSensorSource::store(QJsonObject& json) void MqttSensorSource::store(QJsonObject& json)
{ {
json["Host"] = client.hostname();
json["Port"] = client.port();
json["BaseTopic"] = baseTopicName;
if(client.username() != "")
json["User"] = client.username();
if(client.password() != "")
json["Password"] = client.password();
QJsonArray sensorsArray; QJsonArray sensorsArray;
for(const SensorSubscription& sensor : sensors) for(const SensorSubscription& sensor : sensors)
{ {
@ -185,3 +164,9 @@ void MqttSensorSource::store(QJsonObject& json)
json["Sensors"] = sensorsArray; json["Sensors"] = sensorsArray;
} }
MqttSensorSource::~MqttSensorSource()
{
for(SensorSubscription& sub : sensors)
client->unsubscribe(sub.topic);
}

View file

@ -7,6 +7,7 @@
#include <vector> #include <vector>
#include "sensor.h" #include "sensor.h"
#include "mqttclient.h"
class MqttSensorSource : public QObject class MqttSensorSource : public QObject
{ {
@ -17,12 +18,11 @@ class MqttSensorSource : public QObject
uint64_t id; uint64_t id;
QString topic; QString topic;
QString name; QString name;
QMqttSubscription* subscription = nullptr; MqttClient::Subscription* subscription = nullptr;
}; };
QString baseTopicName;
std::vector<SensorSubscription> sensors; std::vector<SensorSubscription> sensors;
QMqttClient client; std::shared_ptr<MqttClient> client;
private: private:
SensorSubscription& findSubscription(const QString& topic); SensorSubscription& findSubscription(const QString& topic);
@ -30,11 +30,11 @@ private:
private slots: private slots:
void onClientStateChanged(QMqttClient::ClientState state); void onClientStateChanged(QMqttClient::ClientState state);
void onMessageReceived(const QMqttMessage& message); void onMessageReceived(const QMqttMessage& message);
void onClientError(QMqttClient::ClientError error);
public: public:
explicit MqttSensorSource(QObject *parent = nullptr); explicit MqttSensorSource(QObject *parent = nullptr);
void start(const QJsonObject& settings); ~MqttSensorSource();
void start(std::shared_ptr<MqttClient> client, const QJsonObject& settings);
void store(QJsonObject& json); void store(QJsonObject& json);
signals: signals:

View file

@ -16,6 +16,8 @@ public:
TcpServer(QObject* parent = nullptr); TcpServer(QObject* parent = nullptr);
virtual bool launch(const QHostAddress &address = QHostAddress::Any, quint16 port = 0) override; virtual bool launch(const QHostAddress &address = QHostAddress::Any, quint16 port = 0) override;
virtual void sendJson(const QJsonObject& json) override; virtual void sendJson(const QJsonObject& json) override;
quint16 getServerPort() const { return server.serverPort(); }
bool isListening() const { return server.isListening(); }
signals: signals:
void sigRequestSave(); void sigRequestSave();

View file

@ -3,6 +3,8 @@
#include "itemsettingswidgets/messageitemsettingswidget.h" #include "itemsettingswidgets/messageitemsettingswidget.h"
#include "itemsettingswidgets/systemitemsettingswidget.h" #include "itemsettingswidgets/systemitemsettingswidget.h"
#include "itemsettingswidgets/mqttitemsettingswidget.h"
#include "items/mqttitem.h"
ItemCreationDialog::ItemCreationDialog(QWidget *parent) : ItemCreationDialog::ItemCreationDialog(QWidget *parent) :
QDialog(parent), QDialog(parent),
@ -41,6 +43,13 @@ void ItemCreationDialog::itemTypeChanged(const QString& type)
widget = new SystemItemSettingsWidget(systemItem, this); widget = new SystemItemSettingsWidget(systemItem, this);
ui->verticalLayout->addWidget(widget); ui->verticalLayout->addWidget(widget);
} }
if(type == "Mqtt")
{
std::shared_ptr<MqttItem> mqttItem(new MqttItem);
item = mqttItem;
widget = new MqttItemSettingsWidget(mqttItem, this);
ui->verticalLayout->addWidget(widget);
}
} }
void ItemCreationDialog::itemNameChanged(const QString& name) void ItemCreationDialog::itemNameChanged(const QString& name)

View file

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>400</width> <width>400</width>
<height>140</height> <height>274</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -39,6 +39,11 @@
<string>System</string> <string>System</string>
</property> </property>
</item> </item>
<item>
<property name="text">
<string>Mqtt</string>
</property>
</item>
</widget> </widget>
</item> </item>
</layout> </layout>
@ -64,13 +69,26 @@
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout"/> <layout class="QVBoxLayout" name="verticalLayout"/>
</item> </item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="standardButtons"> <property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> <set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property> </property>
</widget> </widget>
</item> </item>

View file

@ -12,6 +12,8 @@
#include "itemsettingswidgets/messageitemsettingswidget.h" #include "itemsettingswidgets/messageitemsettingswidget.h"
#include "itemsettingswidgets/systemitemsettingswidget.h" #include "itemsettingswidgets/systemitemsettingswidget.h"
#include "itemsettingswidgets/relayitemsettingswidget.h" #include "itemsettingswidgets/relayitemsettingswidget.h"
#include "itemsettingswidgets/mqttitemsettingswidget.h"
#include "../items/mqttitem.h"
#include<memory> #include<memory>
ItemSettingsDialog::ItemSettingsDialog(std::shared_ptr<Item> item, bool noGroup, QWidget *parent) : ItemSettingsDialog::ItemSettingsDialog(std::shared_ptr<Item> item, bool noGroup, QWidget *parent) :
@ -47,6 +49,10 @@ ItemSettingsDialog::ItemSettingsDialog(std::shared_ptr<Item> item, bool noGroup,
{ {
itemSpecificWidget_ = new SystemItemSettingsWidget(sysItem); itemSpecificWidget_ = new SystemItemSettingsWidget(sysItem);
} }
else if(std::shared_ptr<MqttItem> mqttItem = std::dynamic_pointer_cast<MqttItem>(item_))
{
itemSpecificWidget_ = new MqttItemSettingsWidget(mqttItem);
}
if(itemSpecificWidget_) if(itemSpecificWidget_)
{ {

View file

@ -0,0 +1,62 @@
#include "mqttitemsettingswidget.h"
#include "ui_mqttitemsettingswidget.h"
#include <QDebug>
MqttItemSettingsWidget::MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent) :
QWidget(parent),
item_(item),
ui(new Ui::MqttItemSettingsWidget)
{
ui->setupUi(this);
if(auto workingItem = item_.lock())
{
ui->lineEdit_topic->setText(workingItem->getTopic());
ui->lineEdit_valueKey->setText(workingItem->getValueKey());
ui->lineEdit_valueOn->setText(workingItem->getValueOn());
ui->lineEdit_valueOff->setText(workingItem->getValueOff());
}
connect(ui->lineEdit_topic, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setTopic);
connect(ui->lineEdit_valueKey, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setValueKey);
connect(ui->lineEdit_valueOn, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setValueOn);
connect(ui->lineEdit_valueOff, &QLineEdit::textChanged, this, &MqttItemSettingsWidget::setValueOff);
}
void MqttItemSettingsWidget::setTopic(const QString& topic)
{
if(auto workingItem = item_.lock())
{
workingItem->setTopic(topic);
}
}
void MqttItemSettingsWidget::setValueKey(const QString& valueKey)
{
if(auto workingItem = item_.lock())
{
workingItem->setValueKey(valueKey);
}
}
void MqttItemSettingsWidget::setValueOn(const QString& valueOn)
{
if(auto workingItem = item_.lock())
{
workingItem->setValueOn(valueOn);
}
}
void MqttItemSettingsWidget::setValueOff(const QString& valueOff)
{
if(auto workingItem = item_.lock())
{
workingItem->setValueOff(valueOff);
}
}
MqttItemSettingsWidget::~MqttItemSettingsWidget()
{
delete ui;
}

View file

@ -0,0 +1,32 @@
#ifndef MQTTITEMSETTINGSWIDGET_H
#define MQTTITEMSETTINGSWIDGET_H
#include <QWidget>
#include <memory>
#include "../../items/mqttitem.h"
namespace Ui
{
class MqttItemSettingsWidget;
}
class MqttItemSettingsWidget : public QWidget
{
Q_OBJECT
std::weak_ptr<MqttItem> item_;
private slots:
void setTopic(const QString& topic);
void setValueKey(const QString& valueKey);
void setValueOn(const QString& valueOn);
void setValueOff(const QString& valueOff);
public:
explicit MqttItemSettingsWidget(std::weak_ptr<MqttItem> item, QWidget *parent = nullptr);
~MqttItemSettingsWidget();
private:
Ui::MqttItemSettingsWidget *ui;
};
#endif // MQTTITEMSETTINGSWIDGET_H

View file

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MqttItemSettingsWidget</class>
<widget class="QWidget" name="MqttItemSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>216</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_topic">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_topic">
<property name="text">
<string>Topic:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_topic">
<property name="placeholderText">
<string>e.g., kitchen/light</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_valueKey">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_valueKey">
<property name="text">
<string>Value Key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_valueKey">
<property name="text">
<string>state</string>
</property>
<property name="placeholderText">
<string>e.g., state, brightness</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_valueOn">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_valueOn">
<property name="text">
<string>Value On:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_valueOn">
<property name="text">
<string>ON</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_valueOff">
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_valueOff">
<property name="text">
<string>Value Off:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_valueOff">
<property name="text">
<string>OFF</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

177
tests/CMakeLists.txt Normal file
View file

@ -0,0 +1,177 @@
cmake_minimum_required(VERSION 4.0)
# Enable testing
enable_testing()
# Find Qt packages for tests
find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia Test REQUIRED)
# Define common sources needed by all tests - include only what's actually needed for basic testing
set(COMMON_TEST_SOURCES
../src/items/item.h
../src/items/item.cpp
../src/sensors/sensor.h
../src/sensors/sensor.cpp
../src/programmode.h
../src/programmode.cpp
../src/microcontroller.h
../src/microcontroller.cpp
../src/actors/actor.h
../src/actors/actor.cpp
../src/actors/factoractor.h
../src/actors/factoractor.cpp
../src/actors/polynomalactor.h
../src/actors/polynomalactor.cpp
../src/actors/sensoractor.h
../src/actors/sensoractor.cpp
../src/actors/timeractor.h
../src/actors/timeractor.cpp
../src/items/relay.h
../src/items/relay.cpp
../src/items/messageitem.h
../src/items/messageitem.cpp
../src/items/systemitem.h
../src/items/systemitem.cpp
../src/items/auxitem.h
../src/items/auxitem.cpp
../src/items/poweritem.h
../src/items/poweritem.cpp
../src/items/rgbitem.h
../src/items/rgbitem.cpp
../src/actors/alarmtime.h
../src/actors/alarmtime.cpp
../src/actors/regulator.h
../src/actors/regulator.cpp
../src/items/itemsource.h
../src/items/itemsource.cpp
../src/items/itemstore.h
../src/items/itemstore.cpp
../src/items/itemloadersource.h
../src/items/itemloadersource.cpp
../src/items/mqttitem.h
../src/items/mqttitem.cpp
../src/mqttclient.cpp
)
# Add test executables - compile all needed sources into each test
add_executable(test_item unit/items/test_item.cpp ${COMMON_TEST_SOURCES})
add_executable(test_sensor unit/sensors/test_sensor.cpp ${COMMON_TEST_SOURCES})
add_executable(test_actor unit/actors/test_actor.cpp ${COMMON_TEST_SOURCES})
add_executable(test_itemstore unit/items/test_itemstore.cpp ${COMMON_TEST_SOURCES})
add_executable(test_itemloadersource unit/items/test_itemloadersource.cpp ${COMMON_TEST_SOURCES})
add_executable(test_tcp unit/service/test_tcp.cpp ${COMMON_TEST_SOURCES}
../src/service/service.h
../src/service/service.cpp
../src/service/server.h
../src/service/server.cpp
../src/service/tcpserver.h
../src/service/tcpserver.cpp
../src/service/tcpclient.h
../src/service/tcpclient.cpp
)
# Link libraries for test_item
target_link_libraries(test_item
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
Qt6::Mqtt
Qt6::Test
)
# Include paths for source files
target_include_directories(test_item PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../src
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
)
# Link libraries for test_sensor
target_link_libraries(test_sensor
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
Qt6::Mqtt
Qt6::Test
)
# Include paths for source files
target_include_directories(test_sensor PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../src
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
)
# Link libraries for test_actor
target_link_libraries(test_actor
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
Qt6::Mqtt
Qt6::Test
)
# Include paths for source files
target_include_directories(test_actor PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../src
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
)
# Link libraries for test_itemstore
target_link_libraries(test_itemstore
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
Qt6::Mqtt
Qt6::Test
)
# Include paths for source files
target_include_directories(test_itemstore PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../src
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
)
# Link libraries for test_itemloadersource
target_link_libraries(test_itemloadersource
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
Qt6::Mqtt
Qt6::Test
)
# Include paths for source files
target_include_directories(test_itemloadersource PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../src
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
)
# Link libraries for test_tcp
target_link_libraries(test_tcp
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Multimedia
Qt6::Network
Qt6::WebSockets
Qt6::Mqtt
Qt6::Test
)
# Include paths for source files
target_include_directories(test_tcp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../src
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
)
# Add tests to CTest
add_test(NAME test_item COMMAND test_item)
add_test(NAME test_sensor COMMAND test_sensor)
add_test(NAME test_actor COMMAND test_actor)
add_test(NAME test_itemstore COMMAND test_itemstore)
add_test(NAME test_itemloadersource COMMAND test_itemloadersource)
add_test(NAME test_tcp COMMAND test_tcp)

View file

@ -0,0 +1,333 @@
#include <QtTest/QtTest>
#include "actors/actor.h"
#include "actors/timeractor.h"
#include "actors/sensoractor.h"
class TestActor : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
// Setup for all tests
}
void testActorCreation()
{
Actor actor;
// Actor should be active by default
QVERIFY(actor.isActive());
// Actor should not be exhausted by default
QVERIFY(!actor.isExausted());
}
void testActorActivation()
{
Actor actor;
// Test makeActive/makeInactive
actor.makeActive();
QVERIFY(actor.isActive());
actor.makeInactive();
QVERIFY(!actor.isActive());
}
void testActorSetActive()
{
Actor actor;
// Test setActive
actor.setActive(1);
QVERIFY(actor.isActive());
actor.setActive(0);
QVERIFY(!actor.isActive());
}
void testActorTriggerValue()
{
Actor actor;
// Default trigger value should be 0
QVERIFY(actor.getTriggerValue() == 0);
// Set trigger value
actor.setTriggerValue(1);
QVERIFY(actor.getTriggerValue() == 1);
actor.setTriggerValue(255);
QVERIFY(actor.getTriggerValue() == 255);
}
void testActorActionName()
{
Actor actor;
// Default trigger value is 0, so action name should be "off"
QVERIFY(actor.actionName() == "off");
// Set trigger value to 1
actor.setTriggerValue(1);
QVERIFY(actor.actionName() == "on");
// Set trigger value to something else
actor.setTriggerValue(5);
QVERIFY(actor.actionName() == "value to 5");
}
void testActorJsonSerialization()
{
Actor actor;
actor.setTriggerValue(1);
actor.makeInactive();
QJsonObject json;
actor.store(json);
// Verify JSON contents
QVERIFY(json.contains("Active"));
QVERIFY(json.contains("Exausted"));
QVERIFY(json.contains("TriggerValue"));
QVERIFY(json["Active"].toBool() == false);
QVERIFY(json["Exausted"].toBool() == false);
QVERIFY(json["TriggerValue"].toInt() == 1);
}
void testActorJsonDeserialization()
{
QJsonObject json;
json["Active"] = false;
json["Exausted"] = true;
json["TriggerValue"] = 5;
Actor actor;
actor.load(json);
QVERIFY(actor.isActive() == false);
QVERIFY(actor.isExausted() == true);
QVERIFY(actor.getTriggerValue() == 5);
}
void testActorCreateActor()
{
// Test creating different actor types
std::shared_ptr<Actor> alarmActor = Actor::createActor("Alarm");
QVERIFY(alarmActor != nullptr);
std::shared_ptr<Actor> sensorActor = Actor::createActor("Sensor");
QVERIFY(sensorActor != nullptr);
std::shared_ptr<Actor> timerActor = Actor::createActor("Timer");
QVERIFY(timerActor != nullptr);
std::shared_ptr<Actor> regulatorActor = Actor::createActor("Regulator");
QVERIFY(regulatorActor != nullptr);
std::shared_ptr<Actor> polynomalActor = Actor::createActor("Polynomal");
QVERIFY(polynomalActor != nullptr);
std::shared_ptr<Actor> factorActor = Actor::createActor("MultiFactor");
QVERIFY(factorActor != nullptr);
std::shared_ptr<Actor> genericActor = Actor::createActor("Actor");
QVERIFY(genericActor != nullptr);
// Test unknown type returns nullptr
std::shared_ptr<Actor> unknownActor = Actor::createActor("UnknownType");
QVERIFY(unknownActor == nullptr);
}
void testActorLoadActor()
{
QJsonObject json;
json["Type"] = "Timer";
json["Active"] = true;
json["Exausted"] = false;
json["TriggerValue"] = 1;
json["Timeout"] = 5000;
std::shared_ptr<Actor> actor = Actor::loadActor(json);
QVERIFY(actor != nullptr);
// Verify the actor was loaded with correct values
QVERIFY(actor->isActive());
QVERIFY(!actor->isExausted());
QVERIFY(actor->getTriggerValue() == 1);
}
void testTimerActorCreation()
{
TimerActor actor(60);
// Default timeout should be 60 seconds
QVERIFY(actor.getTimeout() == 60);
}
void testTimerActorSetTimeout()
{
TimerActor actor(60);
actor.setTimeout(120);
QVERIFY(actor.getTimeout() == 120);
actor.setTimeout(5);
QVERIFY(actor.getTimeout() == 5);
}
void testTimerActorJsonSerialization()
{
TimerActor actor(120);
QJsonObject json;
actor.store(json);
// Verify JSON contents
QVERIFY(json.contains("Type"));
QVERIFY(json.contains("Timeout"));
QVERIFY(json["Type"].toString() == "Timer");
QVERIFY(json["Timeout"].toInt() == 120000); // Converted to milliseconds
}
void testTimerActorJsonDeserialization()
{
QJsonObject json;
json["Type"] = "Timer";
json["Timeout"] = 30000; // 30 seconds in milliseconds
json["Active"] = true;
json["Exausted"] = false;
json["TriggerValue"] = 1;
TimerActor actor;
actor.load(json, false);
// Timeout should be 30 seconds
QVERIFY(actor.getTimeout() == 30);
}
void testTimerActorGetName()
{
TimerActor actor(60);
QString name = actor.getName();
QVERIFY(name.contains("60"));
}
void testSensorActorCreation()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "temp_sensor");
SensorActor actor(sensor);
// Verify sensor was set correctly
QVERIFY(actor.getSensor().type == Sensor::TYPE_TEMPERATURE);
QVERIFY(actor.getSensor().id == 1);
}
void testSensorActorDefaultCreation()
{
SensorActor actor;
// Default sensor should be dummy
QVERIFY(actor.getSensor().type == Sensor::TYPE_DUMMY);
}
void testSensorActorSetSensor()
{
SensorActor actor;
Sensor newSensor(Sensor::TYPE_HUMIDITY, 2, 60.0, "humidity_sensor");
actor.setSensor(newSensor);
QVERIFY(actor.getSensor().type == Sensor::TYPE_HUMIDITY);
QVERIFY(actor.getSensor().id == 2);
}
void testSensorActorSetThreshold()
{
SensorActor actor;
actor.setThreshold(25.0);
QVERIFY(actor.getThreshold() == 25.0);
}
void testSensorActorSetSlope()
{
SensorActor actor;
actor.setSloap(SensorActor::SLOPE_UP);
QVERIFY(actor.getSloap() == SensorActor::SLOPE_UP);
actor.setSloap(SensorActor::SLOPE_DOWN);
QVERIFY(actor.getSloap() == SensorActor::SLOPE_DOWN);
actor.setSloap(SensorActor::SLOPE_BOTH);
QVERIFY(actor.getSloap() == SensorActor::SLOPE_BOTH);
}
void testSensorActorJsonSerialization()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "temp_sensor");
SensorActor actor(sensor);
actor.setThreshold(30.0);
actor.setSloap(SensorActor::SLOPE_UP);
QJsonObject json;
actor.store(json);
// Verify JSON contents
QVERIFY(json.contains("Type"));
QVERIFY(json.contains("Threshold"));
QVERIFY(json.contains("Sloap"));
QVERIFY(json.contains("SensorType"));
QVERIFY(json["Type"].toString() == "Sensor");
QVERIFY(json["Threshold"].toDouble() == 30.0);
QVERIFY(json["Sloap"].toInt() == SensorActor::SLOPE_UP);
}
void testSensorActorJsonDeserialization()
{
QJsonObject json;
json["Type"] = "Sensor";
json["Threshold"] = 25.5;
json["Sloap"] = SensorActor::SLOPE_DOWN;
json["SensorType"] = Sensor::TYPE_HUMIDITY;
json["SensorId"] = 3;
json["SensorField"] = 65.0;
json["SensorName"] = "humidity";
json["Active"] = true;
json["Exausted"] = false;
json["TriggerValue"] = 1;
SensorActor actor;
actor.load(json, false);
QVERIFY(actor.getThreshold() == 25.5);
QVERIFY(actor.getSloap() == SensorActor::SLOPE_DOWN);
}
void testSensorActorGetName()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "temp_sensor");
SensorActor actor(sensor);
actor.setThreshold(30.0);
QString name = actor.getName();
QVERIFY(name.contains("temp_sensor"));
QVERIFY(name.contains("30"));
}
void cleanupTestCase()
{
// Cleanup after all tests
}
};
QTEST_APPLESS_MAIN(TestActor)
#include "test_actor.moc"

View file

@ -0,0 +1,225 @@
#include <QtTest/QtTest>
#include "items/item.h"
#include "items/itemstore.h"
#include "items/itemsource.h"
#include "items/itemloadersource.h"
class TestItem : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
// Setup for all tests
}
void testItemCreation()
{
Item item(0, "test_item", 0);
QCOMPARE(item.getName(), QString("test_item"));
}
void testOverrideFunctionality()
{
Item item(0, "test_item", 0);
// Test override on/off
item.setOverride(true);
QVERIFY(item.getOverride());
item.setOverride(false);
QVERIFY(!item.getOverride());
}
void testItemValueTypes()
{
// Test default value type is BOOL
Item item(1, "test_item", 0);
QCOMPARE(item.getValueType(), ITEM_VALUE_BOOL);
// Test setting and getting value
item.setValueData(1);
QCOMPARE(item.getValue(), static_cast<uint8_t>(1));
item.setValueData(0);
QCOMPARE(item.getValue(), static_cast<uint8_t>(0));
}
void testItemId()
{
Item item(42, "test_item", 0);
QCOMPARE(item.id(), static_cast<uint32_t>(42));
}
void testItemGroupName()
{
Item item(1, "test_item", 0);
QCOMPARE(item.getGroupName(), QString("All")); // Default group
item.setGroupName("Living Room");
QCOMPARE(item.getGroupName(), QString("Living Room"));
}
void testItemHidden()
{
Item item(1, "test_item", 0);
QVERIFY(!item.isHidden());
item.setHidden(true);
QVERIFY(item.isHidden());
item.setHidden(false);
QVERIFY(!item.isHidden());
}
void testItemLoaded()
{
Item item(1, "test_item", 0);
QVERIFY(!item.getLoaded());
item.setLoaded(true);
QVERIFY(item.getLoaded());
}
void testItemJsonSerialization()
{
Item item(42, "test_item", 1);
item.setGroupName("TestGroup");
QJsonObject json;
item.store(json);
// Verify JSON contents
QCOMPARE(json["ItemId"].toDouble(), 42.0);
// Note: Name is only stored if changes.name is true
// Value is only stored if changes.value is true
}
void testItemJsonDeserialization()
{
QJsonObject json;
json["ItemId"] = 100;
json["Name"] = "loaded_item";
json["Value"] = 1;
json["GroupName"] = "Bedroom";
Item item;
item.load(json);
// Note: load() uses loadWithChanges which requires preserve=false to apply changes
// The id should be set
// The name should be set (since preserve defaults to false)
}
void testItemUpdateRequest()
{
Item item(1, "test_item", 0);
// Test creating update request
ItemUpdateRequest request = item.createValueUpdateRequest(ITEM_UPDATE_USER, false);
QVERIFY(request.type == ITEM_UPDATE_USER);
// Note: changes is not automatically set by createValueUpdateRequest
// The caller is expected to set the changes they want
QVERIFY(request.newActors.empty());
}
void testItemUpdateRequestWithActors()
{
Item item(1, "test_item", 0);
// Test creating update request with actors
ItemUpdateRequest request = item.createValueUpdateRequest(ITEM_UPDATE_USER, true);
QVERIFY(request.type == ITEM_UPDATE_USER);
// With actors=true, newActors should be populated
// Note: changes.actors is set by the caller, not by createValueUpdateRequest
}
void testItemHasActors()
{
Item item(1, "test_item", 0);
QVERIFY(!item.hasActors());
// Note: Adding actors requires a valid Actor pointer
}
void testItemRemoveAllActors()
{
Item item(1, "test_item", 0);
item.removeAllActors();
QVERIFY(!item.hasActors());
}
void testItemSetActorsActive()
{
Item item(1, "test_item", 0);
// Should not crash when no actors
item.setActorsActive(true);
item.setActorsActive(false);
}
void testItemDataChangeDetection()
{
ItemData data1(1, "item1", 0);
ItemData data2(1, "item1", 1);
// Test value change detection
ItemFieldChanges changes(true);
QVERIFY(data1.hasChanged(data2, changes));
// Test no change
changes = ItemFieldChanges(false);
QVERIFY(!data1.hasChanged(data2, changes));
}
void testItemDataNameChange()
{
ItemData data1(1, "item1", 0);
ItemData data2(1, "item2", 0);
ItemFieldChanges changes;
changes.name = true;
QVERIFY(data1.hasChanged(data2, changes));
}
void testItemDataGroupNameChange()
{
ItemData data1(1, "item1", 0);
data1.setGroupName("Group1");
ItemData data2(1, "item1", 0);
data2.setGroupName("Group2");
ItemFieldChanges changes;
changes.groupName = true;
QVERIFY(data1.hasChanged(data2, changes));
}
void testItemFieldChanges()
{
ItemFieldChanges changes(false);
QVERIFY(changes.isNone());
changes.value = true;
QVERIFY(!changes.isNone());
ItemFieldChanges allChanges(true);
QVERIFY(!allChanges.isNone());
}
void cleanupTestCase()
{
// Cleanup after all tests
}
};
QTEST_APPLESS_MAIN(TestItem)
#include "test_item.moc"

View file

@ -0,0 +1,240 @@
#include <QtTest/QtTest>
#include "items/itemloadersource.h"
#include "items/item.h"
class TestItemLoaderSource : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
// Setup for all tests
}
void testItemLoaderSourceCreation()
{
ItemLoaderSource source;
// Should be created with empty JSON
QVERIFY(true); // No crash
}
void testItemLoaderSourceCreationWithJson()
{
QJsonObject json;
json["Items"] = QJsonArray();
ItemLoaderSource source(json);
// Should be created with JSON
QVERIFY(true); // No crash
}
void testItemLoaderSourceRefreshEmpty()
{
ItemLoaderSource source;
// Should not crash with empty JSON
source.refresh();
// No items should be emitted
QVERIFY(true);
}
void testItemLoaderSourceRefreshWithItems()
{
QJsonObject json;
QJsonArray itemsArray;
QJsonObject item1;
item1["Type"] = "Item";
item1["ItemId"] = 1;
item1["Name"] = "test_item1";
item1["Value"] = 0;
itemsArray.append(item1);
QJsonObject item2;
item2["Type"] = "Item";
item2["ItemId"] = 2;
item2["Name"] = "test_item2";
item2["Value"] = 1;
itemsArray.append(item2);
json["Items"] = itemsArray;
ItemLoaderSource source(json);
// Capture the gotItems signal
std::vector<ItemAddRequest> capturedItems;
connect(&source, &ItemLoaderSource::gotItems,
[&capturedItems](std::vector<ItemAddRequest> items) {
capturedItems = items;
});
source.refresh();
// Should have 2 items
QVERIFY(capturedItems.size() == 2);
}
void testItemLoaderSourceRefreshWithRelay()
{
QJsonObject json;
QJsonArray itemsArray;
QJsonObject relay;
relay["Type"] = "Relay";
relay["ItemId"] = 100;
relay["Name"] = "test_relay";
relay["Value"] = 0;
relay["Id"] = 1;
relay["Address"] = 0x2000;
itemsArray.append(relay);
json["Items"] = itemsArray;
ItemLoaderSource source(json);
// Capture the gotItems signal
std::vector<ItemAddRequest> capturedItems;
connect(&source, &ItemLoaderSource::gotItems,
[&capturedItems](std::vector<ItemAddRequest> items) {
capturedItems = items;
});
source.refresh();
// Should have 1 item
QVERIFY(capturedItems.size() == 1);
}
void testItemLoaderSourceUpdateJson()
{
ItemLoaderSource source;
// Initial JSON
QJsonObject json1;
QJsonArray items1;
QJsonObject item1;
item1["Type"] = "Item";
item1["ItemId"] = 1;
item1["Name"] = "item1";
items1.append(item1);
json1["Items"] = items1;
source.updateJson(json1);
// Update JSON
QJsonObject json2;
QJsonArray items2;
QJsonObject item2;
item2["Type"] = "Item";
item2["ItemId"] = 2;
item2["Name"] = "item2";
items2.append(item2);
json2["Items"] = items2;
source.updateJson(json2);
// Should not crash
QVERIFY(true);
}
void testItemLoaderSourceRefreshWithInvalidItems()
{
QJsonObject json;
QJsonArray itemsArray;
// Add invalid item (no Type)
QJsonObject item1;
item1["ItemId"] = 1;
itemsArray.append(item1);
json["Items"] = itemsArray;
ItemLoaderSource source(json);
// Capture the gotItems signal
std::vector<ItemAddRequest> capturedItems;
connect(&source, &ItemLoaderSource::gotItems,
[&capturedItems](std::vector<ItemAddRequest> items) {
capturedItems = items;
});
source.refresh();
// Should handle invalid item gracefully (may return 0 items)
QVERIFY(capturedItems.size() == 0 || capturedItems.size() == 1);
}
void testItemLoaderSourceRefreshWithMessageItem()
{
QJsonObject json;
QJsonArray itemsArray;
QJsonObject messageItem;
messageItem["Type"] = "Message";
messageItem["ItemId"] = 200;
messageItem["Name"] = "alert_item";
messageItem["Value"] = 0;
messageItem["Message"] = "Test message";
itemsArray.append(messageItem);
json["Items"] = itemsArray;
ItemLoaderSource source(json);
// Capture the gotItems signal
std::vector<ItemAddRequest> capturedItems;
connect(&source, &ItemLoaderSource::gotItems,
[&capturedItems](std::vector<ItemAddRequest> items) {
capturedItems = items;
});
source.refresh();
// Should have 1 item
QVERIFY(capturedItems.size() == 1);
}
void testItemLoaderSourceRefreshWithSystemItem()
{
QJsonObject json;
QJsonArray itemsArray;
QJsonObject systemItem;
systemItem["Type"] = "System";
systemItem["ItemId"] = 300;
systemItem["Name"] = "system_item";
systemItem["Value"] = 0;
systemItem["OnCommand"] = "echo on";
systemItem["OffCommand"] = "echo off";
itemsArray.append(systemItem);
json["Items"] = itemsArray;
ItemLoaderSource source(json);
// Capture the gotItems signal
std::vector<ItemAddRequest> capturedItems;
connect(&source, &ItemLoaderSource::gotItems,
[&capturedItems](std::vector<ItemAddRequest> items) {
capturedItems = items;
});
source.refresh();
// Should have 1 item
QVERIFY(capturedItems.size() == 1);
}
void cleanupTestCase()
{
// Cleanup after all tests
}
};
QTEST_APPLESS_MAIN(TestItemLoaderSource)
#include "test_itemloadersource.moc"

View file

@ -0,0 +1,255 @@
#include <QtTest/QtTest>
#include "items/itemstore.h"
#include "items/item.h"
class TestItemStore : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
// Setup for all tests
}
void testItemStoreCreation()
{
ItemStore store;
// Should start empty
QVERIFY(store.getItems()->empty());
}
void testItemStoreAddItem()
{
ItemStore store;
std::shared_ptr<Item> item(new Item(1, "test_item", 0));
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
// Item should be added
QVERIFY(store.getItems()->size() == 1);
}
void testItemStoreAddMultipleItems()
{
ItemStore store;
std::shared_ptr<Item> item1(new Item(1, "item1", 0));
std::shared_ptr<Item> item2(new Item(2, "item2", 1));
std::vector<ItemAddRequest> requests;
ItemAddRequest request1;
request1.type = ITEM_UPDATE_USER;
request1.payload = item1;
request1.changes = ItemFieldChanges(true);
requests.push_back(request1);
ItemAddRequest request2;
request2.type = ITEM_UPDATE_USER;
request2.payload = item2;
request2.changes = ItemFieldChanges(true);
requests.push_back(request2);
store.addItems(requests);
// Both items should be added
QVERIFY(store.getItems()->size() == 2);
}
void testItemStoreGetItem()
{
ItemStore store;
std::shared_ptr<Item> item(new Item(42, "test_item", 0));
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
// Get item by id
std::shared_ptr<Item> found = store.getItem(42);
QVERIFY(found != nullptr);
QVERIFY(found->getName() == "test_item");
// Get non-existent item
std::shared_ptr<Item> notFound = store.getItem(999);
QVERIFY(notFound == nullptr);
}
void testItemStoreRemoveItem()
{
ItemStore store;
std::shared_ptr<Item> item(new Item(1, "test_item", 0));
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
QVERIFY(store.getItems()->size() == 1);
// Remove item
ItemData itemData(1, "test_item", 0);
store.removeItem(itemData);
QVERIFY(store.getItems()->empty());
}
void testItemStoreClear()
{
ItemStore store;
// Add multiple items
for (int i = 0; i < 5; i++) {
std::shared_ptr<Item> item(new Item(i, "item" + QString::number(i), 0));
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
}
QVERIFY(store.getItems()->size() == 5);
// Clear all items
store.clear();
QVERIFY(store.getItems()->empty());
}
void testItemStoreUpdateItem()
{
ItemStore store;
std::shared_ptr<Item> item(new Item(1, "test_item", 0));
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
// Update item value
ItemUpdateRequest update;
update.type = ITEM_UPDATE_USER;
ItemData updatedData(1, "test_item", 1); // value = 1
update.payload = updatedData;
update.changes.value = true;
store.updateItem(update);
// Verify value was updated
std::shared_ptr<Item> found = store.getItem(1);
QVERIFY(found->getValue() == 1);
}
void testItemStoreUpdateMultipleItems()
{
ItemStore store;
// Add items
for (int i = 0; i < 3; i++) {
std::shared_ptr<Item> item(new Item(i, "item" + QString::number(i), 0));
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
}
// Update multiple items
std::vector<ItemUpdateRequest> updates;
for (int i = 0; i < 3; i++) {
ItemUpdateRequest update;
update.type = ITEM_UPDATE_USER;
ItemData data(i, "item" + QString::number(i), 1);
update.payload = data;
update.changes.value = true;
updates.push_back(update);
}
store.updateItems(updates);
// Verify all values were updated
for (int i = 0; i < 3; i++) {
std::shared_ptr<Item> found = store.getItem(i);
QVERIFY(found->getValue() == 1);
}
}
void testItemStoreJsonSerialization()
{
ItemStore store;
// Add items
for (int i = 0; i < 2; i++) {
std::shared_ptr<Item> item(new Item(i, "item" + QString::number(i), i));
item->setGroupName("TestGroup");
ItemAddRequest request;
request.type = ITEM_UPDATE_USER;
request.payload = item;
request.changes = ItemFieldChanges(true);
store.addItem(request);
}
// Serialize to JSON
QJsonObject json;
store.store(json);
// Verify JSON contains items array
QVERIFY(json.contains("Items"));
QVERIFY(json["Items"].toArray().size() == 2);
}
void testItemStoreReplaceItems()
{
ItemStore store;
// Add initial items
std::shared_ptr<Item> item1(new Item(1, "item1", 0));
ItemAddRequest request1;
request1.type = ITEM_UPDATE_USER;
request1.payload = item1;
request1.changes = ItemFieldChanges(true);
store.addItem(request1);
QVERIFY(store.getItems()->size() == 1);
// Replace with new items
std::vector<std::shared_ptr<Item>> newItems;
newItems.push_back(std::shared_ptr<Item>(new Item(2, "new_item1", 0)));
newItems.push_back(std::shared_ptr<Item>(new Item(3, "new_item2", 1)));
store.replaceItems(newItems);
// Should have new items
QVERIFY(store.getItems()->size() == 2);
// Old item should be removed
QVERIFY(store.getItem(1) == nullptr);
}
void cleanupTestCase()
{
// Cleanup after all tests
}
};
QTEST_APPLESS_MAIN(TestItemStore)
#include "test_itemstore.moc"

View file

@ -0,0 +1,230 @@
#include <QtTest/QtTest>
#include "sensors/sensor.h"
class TestSensor : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
// Setup for all tests
}
void testSensorCreation()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 0.0, "test_sensor");
QCOMPARE(sensor.type, Sensor::TYPE_TEMPERATURE);
QCOMPARE(sensor.name, QString("test_sensor"));
}
void testSensorData()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 0.0, "temp_sensor");
// Test setting and getting values
sensor.field = 25.0;
QCOMPARE(sensor.field, 25.0);
}
void testSensorTypes()
{
// Test different sensor types
Sensor doorSensor(Sensor::TYPE_DOOR, 1);
Sensor tempSensor(Sensor::TYPE_TEMPERATURE, 2);
Sensor humiditySensor(Sensor::TYPE_HUMIDITY, 3);
QCOMPARE(doorSensor.type, Sensor::TYPE_DOOR);
QCOMPARE(tempSensor.type, Sensor::TYPE_TEMPERATURE);
QCOMPARE(humiditySensor.type, Sensor::TYPE_HUMIDITY);
}
void testSensorStringParsing()
{
// Test parsing sensor from string (format: "SENSOR TYPE: X ID: Y FIELD: Z TIME: T")
// This tests the sensorFromString function
QString sensorStr = "SENSOR TYPE: 1 ID: 5 FIELD: 250 TIME: 1234567890";
Sensor sensor = Sensor::sensorFromString(sensorStr);
// The function should parse the type, id, and field
// Note: temperature and humidity values are divided by 10
QVERIFY(sensor.type != Sensor::TYPE_DUMMY || sensor.field != 0);
}
void testSensorStringParsingInvalid()
{
// Test parsing invalid string returns dummy sensor
QString invalidStr = "invalid data";
Sensor sensor = Sensor::sensorFromString(invalidStr);
// Should return a dummy sensor with hidden=true
QVERIFY(sensor.hidden);
}
void testSensorToString()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.5, "test_sensor");
QString str = sensor.toString();
// Should contain type, id, and field info
QVERIFY(str.contains("SENSOR TYPE"));
QVERIFY(str.contains("ID:"));
QVERIFY(str.contains("FIELD:"));
}
void testSensorJsonSerialization()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "test_sensor");
QJsonObject json;
sensor.store(json);
// Verify JSON contents
QVERIFY(json.contains("Type"));
QVERIFY(json.contains("SensorType"));
QVERIFY(json.contains("Id"));
QVERIFY(json.contains("Field"));
QVERIFY(json.contains("Name"));
QVERIFY(json.contains("Unit"));
// Check values
QVERIFY(json["Type"].toString() == "Sensor");
QVERIFY(json["SensorType"].toInt() == Sensor::TYPE_TEMPERATURE);
QVERIFY(json["Id"].toInt() == 1);
QVERIFY(json["Field"].toDouble() == 25.0);
QVERIFY(json["Name"].toString() == "test_sensor");
}
void testSensorJsonDeserialization()
{
QJsonObject json;
json["SensorType"] = Sensor::TYPE_HUMIDITY;
json["Id"] = 5;
json["Field"] = 60.5;
json["Name"] = "humidity_sensor";
json["Hidden"] = false;
Sensor sensor(json);
QVERIFY(sensor.type == Sensor::TYPE_HUMIDITY);
QVERIFY(sensor.id == 5);
QVERIFY(sensor.field == 60.5);
QVERIFY(sensor.name == "humidity_sensor");
QVERIFY(!sensor.hidden);
}
void testSensorEquality()
{
Sensor sensor1(Sensor::TYPE_TEMPERATURE, 1, 25.0);
Sensor sensor2(Sensor::TYPE_TEMPERATURE, 1, 30.0);
Sensor sensor3(Sensor::TYPE_TEMPERATURE, 2, 25.0);
// Same type and id means equal
QVERIFY(sensor1 == sensor2);
// Different id means not equal
QVERIFY(sensor1 != sensor3);
}
void testSensorUpdateSeen()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0);
QDateTime before = sensor.lastSeen;
// Wait a tiny bit and update
sensor.updateSeen();
QVERIFY(sensor.lastSeen >= before);
}
void testSensorGenerateName()
{
// Test auto-generated names for different sensor types
Sensor tempSensor(Sensor::TYPE_TEMPERATURE, 1);
QVERIFY(tempSensor.name.contains("Temperature"));
Sensor doorSensor(Sensor::TYPE_DOOR, 2);
QVERIFY(doorSensor.name.contains("Door"));
Sensor buttonSensor(Sensor::TYPE_BUTTON, 3);
QVERIFY(buttonSensor.name.contains("Button"));
Sensor humiditySensor(Sensor::TYPE_HUMIDITY, 4);
QVERIFY(humiditySensor.name.contains("Humidity"));
}
void testSensorGetUnit()
{
Sensor tempSensor(Sensor::TYPE_TEMPERATURE, 1);
QVERIFY(tempSensor.getUnit() == "°C");
Sensor humiditySensor(Sensor::TYPE_HUMIDITY, 1);
QVERIFY(humiditySensor.getUnit() == "%");
Sensor pressureSensor(Sensor::TYPE_PRESSURE, 1);
QVERIFY(pressureSensor.getUnit() == "hPa");
Sensor brightnessSensor(Sensor::TYPE_BRIGHTNESS, 1);
QVERIFY(brightnessSensor.getUnit() == "lx");
Sensor co2Sensor(Sensor::TYPE_CO2, 1);
QVERIFY(co2Sensor.getUnit() == "ppm");
Sensor vocSensor(Sensor::TYPE_TOTAL_VOC, 1);
QVERIFY(vocSensor.getUnit() == "ppb");
}
void testSensorHidden()
{
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "test", false);
QVERIFY(!sensor.hidden);
Sensor hiddenSensor(Sensor::TYPE_TEMPERATURE, 2, 25.0, "test", true);
QVERIFY(hiddenSensor.hidden);
}
void testSensorAllTypes()
{
// Test all sensor types can be created
Sensor door(Sensor::TYPE_DOOR, 1);
Sensor temp(Sensor::TYPE_TEMPERATURE, 1);
Sensor humidity(Sensor::TYPE_HUMIDITY, 1);
Sensor pressure(Sensor::TYPE_PRESSURE, 1);
Sensor brightness(Sensor::TYPE_BRIGHTNESS, 1);
Sensor button(Sensor::TYPE_BUTTON, 1);
Sensor adc(Sensor::TYPE_ADC, 1);
Sensor co2(Sensor::TYPE_CO2, 1);
Sensor pm25(Sensor::TYPE_PM25, 1);
Sensor voc(Sensor::TYPE_TOTAL_VOC, 1);
Sensor lowBattery(Sensor::TYPE_LOWBATTERY, 1);
Sensor occupancy(Sensor::TYPE_OCUPANCY, 1);
Sensor sunAlt(Sensor::TYPE_SUN_ALTITUDE, 1);
Sensor audio(Sensor::TYPE_AUDIO_OUTPUT, 1);
// All should be valid
QVERIFY(door.type == Sensor::TYPE_DOOR);
QVERIFY(temp.type == Sensor::TYPE_TEMPERATURE);
QVERIFY(humidity.type == Sensor::TYPE_HUMIDITY);
QVERIFY(pressure.type == Sensor::TYPE_PRESSURE);
QVERIFY(brightness.type == Sensor::TYPE_BRIGHTNESS);
QVERIFY(button.type == Sensor::TYPE_BUTTON);
QVERIFY(adc.type == Sensor::TYPE_ADC);
QVERIFY(co2.type == Sensor::TYPE_CO2);
QVERIFY(pm25.type == Sensor::TYPE_PM25);
QVERIFY(voc.type == Sensor::TYPE_TOTAL_VOC);
QVERIFY(lowBattery.type == Sensor::TYPE_LOWBATTERY);
QVERIFY(occupancy.type == Sensor::TYPE_OCUPANCY);
QVERIFY(sunAlt.type == Sensor::TYPE_SUN_ALTITUDE);
QVERIFY(audio.type == Sensor::TYPE_AUDIO_OUTPUT);
}
void cleanupTestCase()
{
// Cleanup after all tests
}
};
QTEST_APPLESS_MAIN(TestSensor)
#include "test_sensor.moc"

View file

@ -0,0 +1,343 @@
#include <QtTest/QtTest>
#include <QTcpServer>
#include <QTcpSocket>
#include <QJsonDocument>
#include <QJsonArray>
#include <QSignalSpy>
#include "service/tcpserver.h"
#include "service/tcpclient.h"
#include "service/server.h"
#include "service/service.h"
#include "items/item.h"
class TestTcp : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
// Setup for all tests
}
void testTcpServerCreation()
{
TcpServer server;
// Server should be created successfully
QVERIFY(true);
}
void testTcpServerLaunch()
{
TcpServer server;
// Launch server on any address, port 0 means dynamic port
bool result = server.launch(QHostAddress::Any, 0);
QVERIFY(result);
// Server should be listening after launch
QVERIFY(server.isListening());
}
void testTcpServerLaunchSpecificPort()
{
TcpServer server;
// Try to launch on a specific port
bool result = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(result);
// Server should be listening
QVERIFY(server.isListening());
}
void testTcpClientCreation()
{
TcpClient client;
// Client should be created successfully
QVERIFY(true);
}
void testTcpProtocolMessageFormat()
{
// Test the protocol message format: "MSG JSON LEN <size>\n<json>"
QJsonObject json;
json["MessageType"] = "TestMessage";
json["Data"] = QJsonArray();
QByteArray jsonData = QJsonDocument(json).toJson();
QString message = QString("MSG JSON LEN %1\n").arg(jsonData.size()) + QString::fromUtf8(jsonData);
// Verify message format
QVERIFY(message.startsWith("MSG JSON LEN "));
QVERIFY(message.contains("\n"));
// Parse the message size
QStringList parts = message.split("\n");
QVERIFY(parts.size() >= 1);
QString sizeStr = parts[0].mid(13); // Skip "MSG JSON LEN "
bool ok;
quint64 size = sizeStr.toUInt(&ok);
QVERIFY(ok);
QVERIFY(size == jsonData.size());
}
void testTcpProtocolParseCommand()
{
// Test parsing a command from the protocol
QByteArray command = "MSG JSON LEN 123\n";
QVERIFY(command.startsWith("MSG JSON LEN "));
QByteArray sizeStr = command.mid(13);
sizeStr.chop(1); // Remove newline
bool ok;
quint64 size = sizeStr.toLongLong(&ok);
QVERIFY(ok);
QVERIFY(size == 123);
}
void testTcpServerSendJson()
{
// Start server
TcpServer server;
bool serverResult = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(serverResult);
// Connect a client to trigger client list
QTcpSocket clientSocket;
clientSocket.connectToHost(QHostAddress::LocalHost, server.getServerPort());
QVERIFY(clientSocket.waitForConnected(1000));
// Give server time to accept connection
QTest::qWait(200);
// Send JSON from server
QJsonObject json;
json["MessageType"] = "TestMessage";
json["Data"] = QJsonArray();
server.sendJson(json);
// Give more time for data to be sent
QTest::qWait(200);
// Client should receive data (or at least the connection should work)
// Note: This may fail due to timing in test environment
QVERIFY(clientSocket.state() == QTcpSocket::ConnectedState);
// Cleanup
clientSocket.close();
}
void testTcpServerProcessIncomingJson()
{
// Start server
TcpServer server;
bool serverResult = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(serverResult);
// Connect client
QTcpSocket clientSocket;
clientSocket.connectToHost(QHostAddress::LocalHost, server.getServerPort());
QVERIFY(clientSocket.waitForConnected(1000));
// Give server time to accept connection
QTest::qWait(100);
// Send a message that the server can process
QJsonObject json;
json["MessageType"] = "GetItems";
json["Data"] = QJsonArray();
QByteArray jsonData = QJsonDocument(json).toJson();
QString message = QString("MSG JSON LEN %1\n").arg(jsonData.size()) + QString::fromUtf8(jsonData);
clientSocket.write(message.toUtf8());
clientSocket.flush();
// Give server time to process
QTest::qWait(100);
// Cleanup
clientSocket.close();
}
void testTcpServerMultipleClients()
{
// Start server
TcpServer server;
bool serverResult = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(serverResult);
// Connect multiple clients
QTcpSocket client1;
client1.connectToHost(QHostAddress::LocalHost, server.getServerPort());
QVERIFY(client1.waitForConnected(1000));
QTcpSocket client2;
client2.connectToHost(QHostAddress::LocalHost, server.getServerPort());
QVERIFY(client2.waitForConnected(1000));
// Give server time to accept connections
QTest::qWait(200);
// Send message to all clients
QJsonObject json;
json["MessageType"] = "TestMessage";
json["Data"] = QJsonArray();
server.sendJson(json);
// Give time for data to be sent
QTest::qWait(200);
// Both clients should be connected (timing may affect actual data receipt)
QVERIFY(client1.state() == QTcpSocket::ConnectedState);
QVERIFY(client2.state() == QTcpSocket::ConnectedState);
// Cleanup
client1.close();
client2.close();
}
void testItemUpdateMessageFormat()
{
// Test creating an item update message format
Item item(1, "test_item", 1);
QJsonObject itemJson;
item.store(itemJson);
QJsonArray items;
items.append(itemJson);
// Manually create the message (since createMessage is protected)
QJsonObject message;
message["MessageType"] = "ItemUpdate";
message["Data"] = items;
message["FullList"] = false;
QVERIFY(message["MessageType"].toString() == "ItemUpdate");
QVERIFY(message["Data"].toArray().size() == 1);
QVERIFY(message["FullList"].toBool() == false);
}
void testSensorUpdateMessageFormat()
{
// Test creating a sensor update message format
Sensor sensor(Sensor::TYPE_TEMPERATURE, 1, 25.0, "temp_sensor");
QJsonObject sensorJson;
sensor.store(sensorJson);
QJsonArray sensors;
sensors.append(sensorJson);
// Manually create the message (since createMessage is protected)
QJsonObject message;
message["MessageType"] = "SensorUpdate";
message["Data"] = sensors;
QVERIFY(message["MessageType"].toString() == "SensorUpdate");
QVERIFY(message["Data"].toArray().size() == 1);
}
void testTcpClientSocketState()
{
// Test that TcpClient creates a socket
TcpClient client;
// The client should have a valid socket (internal implementation detail)
// We can verify the client was created successfully
QVERIFY(true);
}
void testTcpServerClientConnection()
{
// Start server
TcpServer server;
bool serverResult = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(serverResult);
// Connect a client
QTcpSocket clientSocket;
clientSocket.connectToHost(QHostAddress::LocalHost, server.getServerPort());
bool waitResult = clientSocket.waitForConnected(1000);
QVERIFY(waitResult);
// Verify connection state
QVERIFY(clientSocket.state() == QTcpSocket::ConnectedState);
// Cleanup
clientSocket.close();
}
void testTcpServerDisconnection()
{
// Start server
TcpServer server;
bool serverResult = server.launch(QHostAddress::LocalHost, 0);
QVERIFY(serverResult);
// Connect a client
QTcpSocket* client = new QTcpSocket();
client->connectToHost(QHostAddress::LocalHost, server.getServerPort());
QVERIFY(client->waitForConnected(1000));
// Give server time to accept connection
QTest::qWait(100);
// Disconnect client
client->disconnectFromHost();
QTest::qWait(100);
// Cleanup
delete client;
}
void testTcpProtocolLargeMessage()
{
// Test the protocol with a larger message
QJsonObject json;
json["MessageType"] = "TestMessage";
// Create a large data array
QJsonArray data;
for (int i = 0; i < 100; i++) {
data.append(QJsonObject{{"id", i}, {"value", QString("item%1").arg(i)}});
}
json["Data"] = data;
QByteArray jsonData = QJsonDocument(json).toJson();
QString message = QString("MSG JSON LEN %1\n").arg(jsonData.size()) + QString::fromUtf8(jsonData);
// Verify message format
QVERIFY(message.startsWith("MSG JSON LEN "));
// Parse the message size
QStringList parts = message.split("\n");
QString sizeStr = parts[0].mid(13);
bool ok;
quint64 size = sizeStr.toUInt(&ok);
QVERIFY(ok);
QVERIFY(size == jsonData.size());
}
void cleanupTestCase()
{
// Cleanup after all tests
}
};
QTEST_APPLESS_MAIN(TestTcp)
#include "test_tcp.moc"