Compare commits
3 commits
32dd6bc2fe
...
ffe8b0095d
| Author | SHA1 | Date | |
|---|---|---|---|
| ffe8b0095d | |||
| e1195c6f5f | |||
| 037ef5aa09 |
4 changed files with 148 additions and 8 deletions
|
|
@ -1,7 +1,7 @@
|
|||
# SHinterface - Smart Home Interface
|
||||
# smartvos - Smart Home System
|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
142
remote.html
142
remote.html
|
|
@ -185,6 +185,25 @@
|
|||
.refresh-btn:hover {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -198,10 +217,22 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
|
@ -310,14 +341,24 @@
|
|||
|
||||
function renderItems() {
|
||||
const itemList = document.getElementById('itemList');
|
||||
const groupFilter = document.getElementById('itemGroupFilter').value;
|
||||
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>';
|
||||
return;
|
||||
}
|
||||
|
||||
Object.values(items).forEach(item => {
|
||||
filteredItems.forEach(item => {
|
||||
const itemCard = document.createElement('div');
|
||||
itemCard.className = 'item-card';
|
||||
|
||||
|
|
@ -348,12 +389,26 @@
|
|||
case 2: // ITEM_VALUE_NO_VALUE
|
||||
controlHtml = '<div style="color: #b9b9b9;">No control</div>';
|
||||
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 = `
|
||||
<div class="item-info">
|
||||
<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>
|
||||
${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) {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
alert('Not connected to server');
|
||||
|
|
@ -401,11 +478,30 @@
|
|||
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) {
|
||||
switch (type) {
|
||||
case 0: return 'BOOL';
|
||||
case 1: return 'UINT';
|
||||
case 2: return 'NO_VALUE';
|
||||
case 3: return 'ENUM';
|
||||
default: return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
|
@ -425,14 +521,24 @@
|
|||
|
||||
function renderSensors() {
|
||||
const sensorList = document.getElementById('sensorList');
|
||||
const groupFilter = document.getElementById('sensorGroupFilter').value;
|
||||
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>';
|
||||
return;
|
||||
}
|
||||
|
||||
Object.values(sensors).forEach(sensor => {
|
||||
filteredSensors.forEach(sensor => {
|
||||
const sensorCard = document.createElement('div');
|
||||
sensorCard.className = 'sensor-card';
|
||||
|
||||
|
|
@ -456,11 +562,12 @@
|
|||
|
||||
// Truncate to 2 decimal places
|
||||
const fieldValue = Number(sensor.Field).toFixed(2);
|
||||
const groupInfo = sensor.GroupName ? ` | Group: ${sensor.GroupName}` : '';
|
||||
|
||||
sensorCard.innerHTML = `
|
||||
<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 class="sensor-value-container">
|
||||
<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) {
|
||||
switch (type) {
|
||||
case 0: return 'BOOL';
|
||||
case 1: return 'UINT';
|
||||
case 2: return 'NO_VALUE';
|
||||
case 3: return 'ENUM';
|
||||
default: return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,14 @@ void MqttSensorSource::onMessageReceived(const QMqttMessage& message)
|
|||
sensor.field = obj["voltage"].toDouble(0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public:
|
|||
TYPE_ENERGY_USE,
|
||||
TYPE_POWER,
|
||||
TYPE_VOLTAGE,
|
||||
TYPE_SOIL_MOISTURE,
|
||||
TYPE_LOWBATTERY = 128,
|
||||
TYPE_SHUTDOWN_IMMINENT = 251,
|
||||
TYPE_OCUPANCY,
|
||||
|
|
@ -143,6 +144,7 @@ public:
|
|||
case TYPE_TEMPERATURE:
|
||||
return "°C";
|
||||
case TYPE_HUMIDITY:
|
||||
case TYPE_SOIL_MOISTURE:
|
||||
return "%";
|
||||
case TYPE_PRESSURE:
|
||||
return "hPa";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue