UvosSmartHomeInterface/remote.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>