Compare commits

..

3 commits

4 changed files with 148 additions and 8 deletions

View file

@ -1,7 +1,7 @@
# SHinterface - Smart Home Interface # smartvos - Smart Home System
## Overview ## 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. smartvos 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 ## Architecture

View file

@ -185,6 +185,25 @@
.refresh-btn:hover { .refresh-btn:hover {
background-color: #45a049; background-color: #45a049;
} }
.filter-container {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.filter-label {
color: #ccc;
font-size: 0.9em;
}
.filter-select {
flex: 1;
padding: 8px;
border-radius: 5px;
border: 1px solid #555;
background-color: #4a4c56;
color: white;
font-size: 0.9em;
}
</style> </style>
</head> </head>
<body> <body>
@ -198,10 +217,22 @@
</div> </div>
<div id="items-tab" class="tab-content active"> <div id="items-tab" class="tab-content active">
<div class="filter-container">
<span class="filter-label">Group:</span>
<select id="itemGroupFilter" class="filter-select" onchange="renderItems()">
<option value="">All</option>
</select>
</div>
<div class="item-list" id="itemList"></div> <div class="item-list" id="itemList"></div>
</div> </div>
<div id="sensors-tab" class="tab-content"> <div id="sensors-tab" class="tab-content">
<div class="filter-container">
<span class="filter-label">Group:</span>
<select id="sensorGroupFilter" class="filter-select" onchange="renderSensors()">
<option value="">All</option>
</select>
</div>
<div class="sensor-list" id="sensorList"></div> <div class="sensor-list" id="sensorList"></div>
</div> </div>
</div> </div>
@ -310,14 +341,24 @@
function renderItems() { function renderItems() {
const itemList = document.getElementById('itemList'); const itemList = document.getElementById('itemList');
const groupFilter = document.getElementById('itemGroupFilter').value;
itemList.innerHTML = ''; itemList.innerHTML = '';
if (Object.keys(items).length === 0) { // Update group filter options
updateItemGroupFilter();
// Filter items by group
let filteredItems = Object.values(items);
if (groupFilter) {
filteredItems = filteredItems.filter(item => item.GroupName === groupFilter);
}
if (filteredItems.length === 0) {
itemList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No items found</div>'; itemList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No items found</div>';
return; return;
} }
Object.values(items).forEach(item => { filteredItems.forEach(item => {
const itemCard = document.createElement('div'); const itemCard = document.createElement('div');
itemCard.className = 'item-card'; itemCard.className = 'item-card';
@ -348,12 +389,26 @@
case 2: // ITEM_VALUE_NO_VALUE case 2: // ITEM_VALUE_NO_VALUE
controlHtml = '<div style="color: #b9b9b9;">No control</div>'; controlHtml = '<div style="color: #b9b9b9;">No control</div>';
break; break;
case 3: // ITEM_VALUE_ENUM
const valueNames = item.ValueNames || [];
let optionsHtml = '';
valueNames.forEach((name, index) => {
optionsHtml += `<option value="${index}" ${value === index ? 'selected' : ''}>${name}</option>`;
});
controlHtml = `
<select onchange="setEnumItem(${item.ItemId}, this.value)"
style="padding: 5px; border-radius: 5px; border: 1px solid #ddd; min-width: 100px;">
${optionsHtml}
</select>
`;
break;
} }
const groupInfo = item.GroupName ? ` | Group: ${item.GroupName}` : '';
itemCard.innerHTML = ` itemCard.innerHTML = `
<div class="item-info"> <div class="item-info">
<div class="item-name">${item.Name}</div> <div class="item-name">${item.Name}</div>
<div class="item-id">ItemId: ${item.ItemId} | Type: ${getTypeName(type)}</div> <div class="item-id">ItemId: ${item.ItemId} | Type: ${getTypeName(type)}${groupInfo}</div>
</div> </div>
${controlHtml} ${controlHtml}
`; `;
@ -362,6 +417,28 @@
}); });
} }
function updateItemGroupFilter() {
const select = document.getElementById('itemGroupFilter');
const currentValue = select.value;
const groups = new Set();
Object.values(items).forEach(item => {
if (item.GroupName) {
groups.add(item.GroupName);
}
});
select.innerHTML = '<option value="">All</option>';
Array.from(groups).sort().forEach(group => {
const option = document.createElement('option');
option.value = group;
option.textContent = group;
select.appendChild(option);
});
select.value = currentValue;
}
function toggleItem(itemId, newValue) { function toggleItem(itemId, newValue) {
if (!socket || socket.readyState !== WebSocket.OPEN) { if (!socket || socket.readyState !== WebSocket.OPEN) {
alert('Not connected to server'); alert('Not connected to server');
@ -401,11 +478,30 @@
socket.send(JSON.stringify(message)); socket.send(JSON.stringify(message));
} }
function setEnumItem(itemId, value) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
alert('Not connected to server');
return;
}
const message = {
MessageType: 'ItemUpdate',
Data: [{
ItemId: itemId,
Value: parseInt(value)
}],
FullList: false
};
socket.send(JSON.stringify(message));
}
function getTypeName(type) { function getTypeName(type) {
switch (type) { switch (type) {
case 0: return 'BOOL'; case 0: return 'BOOL';
case 1: return 'UINT'; case 1: return 'UINT';
case 2: return 'NO_VALUE'; case 2: return 'NO_VALUE';
case 3: return 'ENUM';
default: return 'UNKNOWN'; default: return 'UNKNOWN';
} }
} }
@ -425,14 +521,24 @@
function renderSensors() { function renderSensors() {
const sensorList = document.getElementById('sensorList'); const sensorList = document.getElementById('sensorList');
const groupFilter = document.getElementById('sensorGroupFilter').value;
sensorList.innerHTML = ''; sensorList.innerHTML = '';
if (Object.keys(sensors).length === 0) { // Update group filter options
updateSensorGroupFilter();
// Filter sensors by group
let filteredSensors = Object.values(sensors);
if (groupFilter) {
filteredSensors = filteredSensors.filter(sensor => sensor.GroupName === groupFilter);
}
if (filteredSensors.length === 0) {
sensorList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No sensors found</div>'; sensorList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No sensors found</div>';
return; return;
} }
Object.values(sensors).forEach(sensor => { filteredSensors.forEach(sensor => {
const sensorCard = document.createElement('div'); const sensorCard = document.createElement('div');
sensorCard.className = 'sensor-card'; sensorCard.className = 'sensor-card';
@ -456,11 +562,12 @@
// Truncate to 2 decimal places // Truncate to 2 decimal places
const fieldValue = Number(sensor.Field).toFixed(2); const fieldValue = Number(sensor.Field).toFixed(2);
const groupInfo = sensor.GroupName ? ` | Group: ${sensor.GroupName}` : '';
sensorCard.innerHTML = ` sensorCard.innerHTML = `
<div> <div>
<div class="sensor-name">${sensor.Name}</div> <div class="sensor-name">${sensor.Name}</div>
<div class="sensor-info">Type: ${typeName} | ID: ${sensor.Id}</div> <div class="sensor-info">Type: ${typeName} | ID: ${sensor.Id}${groupInfo}</div>
</div> </div>
<div class="sensor-value-container"> <div class="sensor-value-container">
<div class="sensor-value"> <div class="sensor-value">
@ -473,11 +580,34 @@
}); });
} }
function updateSensorGroupFilter() {
const select = document.getElementById('sensorGroupFilter');
const currentValue = select.value;
const groups = new Set();
Object.values(sensors).forEach(sensor => {
if (sensor.GroupName) {
groups.add(sensor.GroupName);
}
});
select.innerHTML = '<option value="">All</option>';
Array.from(groups).sort().forEach(group => {
const option = document.createElement('option');
option.value = group;
option.textContent = group;
select.appendChild(option);
});
select.value = currentValue;
}
function getTypeName(type) { function getTypeName(type) {
switch (type) { switch (type) {
case 0: return 'BOOL'; case 0: return 'BOOL';
case 1: return 'UINT'; case 1: return 'UINT';
case 2: return 'NO_VALUE'; case 2: return 'NO_VALUE';
case 3: return 'ENUM';
default: return 'UNKNOWN'; default: return 'UNKNOWN';
} }
} }

View file

@ -191,6 +191,14 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
sensor.field = obj["voltage"].toDouble(0); sensor.field = obj["voltage"].toDouble(0);
stateChanged(sensor, SENSOR_UPDATE_BACKEND); stateChanged(sensor, SENSOR_UPDATE_BACKEND);
} }
if(obj.contains("soil_moisture"))
{
sensor.name = baseName + " Soil Moisture";
sensor.type = Sensor::TYPE_SOIL_MOISTURE;
sensor.field = obj["soil_moisture"].toDouble(0);
stateChanged(sensor, SENSOR_UPDATE_BACKEND);
}
} }
} }

View file

@ -25,6 +25,7 @@ public:
TYPE_ENERGY_USE, TYPE_ENERGY_USE,
TYPE_POWER, TYPE_POWER,
TYPE_VOLTAGE, TYPE_VOLTAGE,
TYPE_SOIL_MOISTURE,
TYPE_LOWBATTERY = 128, TYPE_LOWBATTERY = 128,
TYPE_SHUTDOWN_IMMINENT = 251, TYPE_SHUTDOWN_IMMINENT = 251,
TYPE_OCUPANCY, TYPE_OCUPANCY,
@ -143,6 +144,7 @@ public:
case TYPE_TEMPERATURE: case TYPE_TEMPERATURE:
return "°C"; return "°C";
case TYPE_HUMIDITY: case TYPE_HUMIDITY:
case TYPE_SOIL_MOISTURE:
return "%"; return "%";
case TYPE_PRESSURE: case TYPE_PRESSURE:
return "hPa"; return "hPa";