529 lines
18 KiB
HTML
529 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SHInterface Control</title>
|
|
<link rel="icon" type="image/x-icon" href="UVOSicon.bmp">
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
margin: 0;
|
|
padding: 15px;
|
|
background-color: #32343d;
|
|
overflow-x: hidden;
|
|
}
|
|
.container {
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
color: #333;
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
.status {
|
|
padding: 10px 30px 10px 15px;
|
|
border-radius: 5px;
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
margin-top: 0px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
.status::after {
|
|
content: '↻';
|
|
position: absolute;
|
|
right: 15px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
.connected {
|
|
background-color: #d4edda;
|
|
color: #0F0F0F;
|
|
}
|
|
.disconnected {
|
|
background-color: #FF4733;
|
|
color: #0F0F0F;
|
|
}
|
|
.item-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.item-card {
|
|
background: #707177;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.item-info {
|
|
flex-grow: 1;
|
|
}
|
|
.item-name {
|
|
font-weight: normal;
|
|
margin-bottom: 5px;
|
|
color: white;
|
|
}
|
|
.item-id {
|
|
font-size: 0.8em;
|
|
color: #c9c9c9;
|
|
}
|
|
.toggle-switch {
|
|
position: relative;
|
|
width: 50px;
|
|
height: 25px;
|
|
background-color: #ccc;
|
|
border-radius: 25px;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
}
|
|
.toggle-switch.active {
|
|
background-color: #313665;
|
|
}
|
|
.toggle-knob {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 21px;
|
|
height: 21px;
|
|
background-color: white;
|
|
border-radius: 50%;
|
|
transition: transform 0.3s;
|
|
}
|
|
.toggle-switch.active .toggle-knob {
|
|
transform: translateX(25px);
|
|
}
|
|
.tab-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: calc(100vh - 85px);
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
background-color: #4a4c56;
|
|
border-radius: 8px 8px 0 0;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
padding: 12px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
background-color: #4a4c56;
|
|
color: #ccc;
|
|
transition: all 0.3s;
|
|
border-bottom: 3px solid transparent;
|
|
}
|
|
.tab.active {
|
|
background-color: #707177;
|
|
color: white;
|
|
border-bottom-color: #4CAF50;
|
|
}
|
|
.tab-content {
|
|
display: none;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
overflow-y: auto;
|
|
height: 100%;
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
.tab-content.active {
|
|
display: flex;
|
|
transform: translateX(0);
|
|
}
|
|
.item-list, .sensor-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
width: 100%;
|
|
}
|
|
.sensor-card {
|
|
background: #707177;
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.sensor-info {
|
|
font-size: 0.8em;
|
|
color: #c9c9c9;
|
|
}
|
|
.sensor-value-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
}
|
|
.sensor-name {
|
|
font-weight: normal;
|
|
margin-bottom: 3px;
|
|
color: white;
|
|
}
|
|
.sensor-value {
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
color: white;
|
|
}
|
|
.refresh-btn {
|
|
display: block;
|
|
margin: 20px auto;
|
|
padding: 10px 20px;
|
|
background-color: #4CAF50;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
}
|
|
.refresh-btn:hover {
|
|
background-color: #45a049;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="status disconnected" id="status" onclick="connectAndRefresh()">Disconnected</div>
|
|
|
|
<div class="tab-container">
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="switchTab(0)">Items</div>
|
|
<div class="tab" onclick="switchTab(1)">Sensors</div>
|
|
</div>
|
|
|
|
<div id="items-tab" class="tab-content active">
|
|
<div class="item-list" id="itemList"></div>
|
|
</div>
|
|
|
|
<div id="sensors-tab" class="tab-content">
|
|
<div class="sensor-list" id="sensorList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
<script>
|
|
let socket;
|
|
let items = {};
|
|
let sensors = {};
|
|
const wsUrl = 'https://' + 'local.uvos.xyz/shws';
|
|
|
|
function connectAndRefresh() {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
refreshItems();
|
|
refreshSensors();
|
|
return;
|
|
}
|
|
|
|
socket = new WebSocket(wsUrl);
|
|
|
|
socket.onopen = function(e) {
|
|
console.log('Connected to WebSocket');
|
|
document.getElementById('status').className = 'status connected';
|
|
document.getElementById('status').textContent = 'Connected';
|
|
refreshItems();
|
|
refreshSensors();
|
|
};
|
|
|
|
socket.onclose = function(e) {
|
|
console.log('Disconnected from WebSocket');
|
|
document.getElementById('status').className = 'status disconnected';
|
|
document.getElementById('status').textContent = 'Disconnected';
|
|
};
|
|
|
|
socket.onerror = function(e) {
|
|
console.error('WebSocket error:', e);
|
|
document.getElementById('status').className = 'status disconnected';
|
|
document.getElementById('status').textContent = 'Error: ' + e.message;
|
|
};
|
|
|
|
socket.onmessage = function(event) {
|
|
console.log('Message received:', event.data);
|
|
try {
|
|
const json = JSON.parse(event.data);
|
|
handleMessage(json);
|
|
} catch (e) {
|
|
console.error('Error parsing JSON:', e);
|
|
}
|
|
};
|
|
}
|
|
|
|
function refreshItems() {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
const message = {
|
|
MessageType: 'GetItems',
|
|
Data: []
|
|
};
|
|
socket.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
function refreshSensors() {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
const message = {
|
|
MessageType: 'GetSensors',
|
|
Data: []
|
|
};
|
|
socket.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
function handleMessage(json) {
|
|
if (json.MessageType === 'ItemUpdate') {
|
|
const fullList = json.FullList || false;
|
|
|
|
if (fullList) {
|
|
items = {}; // Clear existing items
|
|
json.Data.forEach(item => {
|
|
items[item.ItemId] = item;
|
|
});
|
|
}
|
|
else {
|
|
json.Data.forEach(item => {
|
|
items[item.ItemId].Value = item.Value;
|
|
});
|
|
}
|
|
|
|
renderItems();
|
|
} else if (json.MessageType === 'SensorUpdate') {
|
|
const fullList = json.FullList || false;
|
|
|
|
if (fullList) {
|
|
sensors = {}; // Clear existing sensors
|
|
}
|
|
|
|
json.Data.forEach(sensor => {
|
|
const key = `${sensor.SensorType}-${sensor.Id}`;
|
|
sensors[key] = sensor;
|
|
});
|
|
|
|
renderSensors();
|
|
}
|
|
}
|
|
|
|
function renderItems() {
|
|
const itemList = document.getElementById('itemList');
|
|
itemList.innerHTML = '';
|
|
|
|
if (Object.keys(items).length === 0) {
|
|
itemList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No items found</div>';
|
|
return;
|
|
}
|
|
|
|
Object.values(items).forEach(item => {
|
|
const itemCard = document.createElement('div');
|
|
itemCard.className = 'item-card';
|
|
|
|
const value = item.Value || 0;
|
|
const type = item.ValueType || 0; // Default to BOOL
|
|
|
|
let controlHtml = '';
|
|
|
|
switch (type) {
|
|
case 0: // ITEM_VALUE_BOOL
|
|
const isActive = value > 0;
|
|
controlHtml = `
|
|
<div class="toggle-switch ${isActive ? 'active' : ''}"
|
|
onclick="toggleItem(${item.ItemId}, ${!isActive})">
|
|
<div class="toggle-knob"></div>
|
|
</div>
|
|
`;
|
|
break;
|
|
case 1: // ITEM_VALUE_UINT
|
|
controlHtml = `
|
|
<input type="number"
|
|
value="${value}"
|
|
min="0" max="255"
|
|
onchange="setUintItem(${item.ItemId}, this.value)"
|
|
style="width: 80px; padding: 5px; border-radius: 5px; border: 1px solid #ddd;">
|
|
`;
|
|
break;
|
|
case 2: // ITEM_VALUE_NO_VALUE
|
|
controlHtml = '<div style="color: #b9b9b9;">No control</div>';
|
|
break;
|
|
}
|
|
|
|
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>
|
|
${controlHtml}
|
|
`;
|
|
|
|
itemList.appendChild(itemCard);
|
|
});
|
|
}
|
|
|
|
function toggleItem(itemId, newValue) {
|
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
alert('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
const message = {
|
|
MessageType: 'ItemUpdate',
|
|
Data: [{
|
|
ItemId: itemId,
|
|
Value: newValue ? 1 : 0
|
|
}],
|
|
FullList: false
|
|
};
|
|
|
|
socket.send(JSON.stringify(message));
|
|
}
|
|
|
|
function setUintItem(itemId, value) {
|
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
|
alert('Not connected to server');
|
|
return;
|
|
}
|
|
|
|
// Convert to integer and clamp to 0-255
|
|
const intValue = Math.min(255, Math.max(0, parseInt(value) || 0));
|
|
|
|
const message = {
|
|
MessageType: 'ItemUpdate',
|
|
Data: [{
|
|
ItemId: itemId,
|
|
Value: intValue
|
|
}],
|
|
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';
|
|
default: return 'UNKNOWN';
|
|
}
|
|
}
|
|
|
|
function switchTab(tabIndex) {
|
|
const tabs = document.querySelectorAll('.tab');
|
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
|
|
tabs.forEach((tab, index) => {
|
|
tab.classList.toggle('active', index === tabIndex);
|
|
});
|
|
|
|
tabContents.forEach((content, index) => {
|
|
content.classList.toggle('active', index === tabIndex);
|
|
});
|
|
}
|
|
|
|
function renderSensors() {
|
|
const sensorList = document.getElementById('sensorList');
|
|
sensorList.innerHTML = '';
|
|
|
|
if (Object.keys(sensors).length === 0) {
|
|
sensorList.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">No sensors found</div>';
|
|
return;
|
|
}
|
|
|
|
Object.values(sensors).forEach(sensor => {
|
|
const sensorCard = document.createElement('div');
|
|
sensorCard.className = 'sensor-card';
|
|
|
|
let typeName = '';
|
|
switch (sensor.SensorType) {
|
|
case 0: typeName = 'Door'; break;
|
|
case 1: typeName = 'Temperature'; break;
|
|
case 2: typeName = 'Humidity'; break;
|
|
case 3: typeName = 'Pressure'; break;
|
|
case 4: typeName = 'Brightness'; break;
|
|
case 5: typeName = 'Button'; break;
|
|
case 6: typeName = 'ADC'; break;
|
|
case 7: typeName = 'CO2'; break;
|
|
case 8: typeName = 'Formaldehyde'; break;
|
|
case 9: typeName = 'PM2.5'; break;
|
|
case 10: typeName = 'Total VOC'; break;
|
|
case 16: typeName = 'Occupancy'; break;
|
|
case 17: typeName = 'Sun Altitude'; break;
|
|
default: typeName = 'Unknown';
|
|
}
|
|
|
|
// Truncate to 2 decimal places
|
|
const fieldValue = Number(sensor.Field).toFixed(2);
|
|
|
|
sensorCard.innerHTML = `
|
|
<div>
|
|
<div class="sensor-name">${sensor.Name}</div>
|
|
<div class="sensor-info">Type: ${typeName} | ID: ${sensor.Id}</div>
|
|
</div>
|
|
<div class="sensor-value-container">
|
|
<div class="sensor-value">
|
|
${fieldValue} ${sensor.Unit || ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
sensorList.appendChild(sensorCard);
|
|
});
|
|
}
|
|
|
|
function getTypeName(type) {
|
|
switch (type) {
|
|
case 0: return 'BOOL';
|
|
case 1: return 'UINT';
|
|
case 2: return 'NO_VALUE';
|
|
default: return 'UNKNOWN';
|
|
}
|
|
}
|
|
|
|
// Touch/swipe support for mobile
|
|
let touchStartX = -1;
|
|
let touchEndX = -1;
|
|
|
|
function handleTouchStart(event) {
|
|
touchStartX = event.changedTouches[0].screenX;
|
|
}
|
|
|
|
function handleTouchMove(event) {
|
|
touchEndX = event.changedTouches[0].screenX;
|
|
}
|
|
|
|
function handleTouchEnd() {
|
|
const diff = touchStartX - touchEndX;
|
|
if (touchStartX > 0 && touchEndX > 0) {
|
|
if (diff > 100) { // Swipe left
|
|
const activeTab = document.querySelector('.tab.active');
|
|
const tabIndex = Array.from(document.querySelectorAll('.tab')).indexOf(activeTab);
|
|
if (tabIndex < 1) {
|
|
switchTab(tabIndex + 1);
|
|
}
|
|
} else if (diff < -100) { // Swipe right
|
|
const activeTab = document.querySelector('.tab.active');
|
|
const tabIndex = Array.from(document.querySelectorAll('.tab')).indexOf(activeTab);
|
|
if (tabIndex > 0) {
|
|
switchTab(tabIndex - 1);
|
|
}
|
|
}
|
|
}
|
|
touchStartX = -1
|
|
touchEndX = -1
|
|
}
|
|
|
|
// Add touch event listeners
|
|
document.addEventListener('touchstart', handleTouchStart, false);
|
|
document.addEventListener('touchmove', handleTouchMove, false);
|
|
document.addEventListener('touchend', handleTouchEnd, false);
|
|
|
|
// Auto-connect when page loads
|
|
window.onload = function() {
|
|
setTimeout(connectAndRefresh, 500);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|