Add remote website
This commit is contained in:
parent
0e09b6f46c
commit
4a956d3484
1 changed files with 525 additions and 0 deletions
525
remote.html
Normal file
525
remote.html
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
<!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;
|
||||
});
|
||||
|
||||
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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue