This commit is contained in:
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- name: Redeploy stack in Portainer
|
- name: Redeploy stack in Portainer
|
||||||
run: |
|
run: |
|
||||||
# Read stack file content
|
# Read stack file content
|
||||||
STACK_FILE_CONTENT=$(echo "$(<docker-compose.prod.yml )")
|
STACK_FILE_CONTENT=$(echo "$(<web-compose.yml )")
|
||||||
|
|
||||||
# Read existing environment variables from the fetched stack
|
# Read existing environment variables from the fetched stack
|
||||||
ENV_VARS=$(cat stack_env.json)
|
ENV_VARS=$(cat stack_env.json)
|
||||||
|
|||||||
@@ -134,6 +134,62 @@
|
|||||||
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
|
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
.icon-btn.del:hover { border-color: var(--danger); color: var(--danger); }
|
.icon-btn.del:hover { border-color: var(--danger); color: var(--danger); }
|
||||||
|
|
||||||
|
/* ── Brightness Slider ── */
|
||||||
|
.brightness-control {
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.brightness-slider {
|
||||||
|
flex: 1;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: linear-gradient(to right, var(--off) 0%, var(--on) 100%);
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.brightness-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--card);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.brightness-slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.brightness-slider::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--card);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.brightness-slider::-moz-range-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
.brightness-slider:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.brightness-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Modal ── */
|
/* ── Modal ── */
|
||||||
.modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
.modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
||||||
z-index: 200; align-items: flex-end; justify-content: center; }
|
z-index: 200; align-items: flex-end; justify-content: center; }
|
||||||
@@ -196,8 +252,9 @@
|
|||||||
<div id="page-devices" class="page active">
|
<div id="page-devices" class="page active">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h2>Devices</h2>
|
<h2>Devices</h2>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<button class="btn btn-ghost btn-sm" onclick="openAddDeviceModal()">+ Add IP</button>
|
||||||
<button class="btn btn-primary" id="btn-discover" onclick="discoverDevices()">⟳ Scan</button>
|
<button class="btn btn-primary" id="btn-discover" onclick="discoverDevices()">⟳ Scan</button>
|
||||||
<button class="btn btn-secondary btn-sm" id="btn-manual-add" onclick="openManualModal()" title="Add device manually" style="width:auto;padding:9px 12px;">+</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="page-header-devices" class="page-header">
|
<div id="page-header-devices" class="page-header">
|
||||||
<span class="ws-dot" id="ws-dot-d"></span>
|
<span class="ws-dot" id="ws-dot-d"></span>
|
||||||
@@ -258,7 +315,10 @@
|
|||||||
<table style="font-size:12px;color:var(--text2);width:100%;border-collapse:collapse;line-height:1.8;">
|
<table style="font-size:12px;color:var(--text2);width:100%;border-collapse:collapse;line-height:1.8;">
|
||||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices</td><td>List devices</td></tr>
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices</td><td>List devices</td></tr>
|
||||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/discover</td><td>Scan network</td></tr>
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/discover</td><td>Scan network</td></tr>
|
||||||
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/add</td><td>Add device manually <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"host":"192.168.1.100","port":49153}</code></td></tr>
|
||||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/state</td><td>Toggle power <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"on":true}</code></td></tr>
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/state</td><td>Toggle power <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"on":true}</code></td></tr>
|
||||||
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices/:ip/:port/brightness</td><td>Get brightness (dimmer only)</td></tr>
|
||||||
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/brightness</td><td>Set brightness <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"brightness":50}</code></td></tr>
|
||||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/dwm-rules</td><td>List DWM rules</td></tr>
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/dwm-rules</td><td>List DWM rules</td></tr>
|
||||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/dwm-rules</td><td>Create rule</td></tr>
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/dwm-rules</td><td>Create rule</td></tr>
|
||||||
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">PUT /api/dwm-rules/:id</td><td>Update rule</td></tr>
|
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">PUT /api/dwm-rules/:id</td><td>Update rule</td></tr>
|
||||||
@@ -437,25 +497,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Manual Add Modal ── -->
|
<!-- ── Add Device Modal ── -->
|
||||||
<div class="modal-backdrop" id="modal-manual" onclick="closeManualModal(event)">
|
<div class="modal-backdrop" id="modal-add-device" onclick="closeAddDeviceModal(event)">
|
||||||
<div class="modal" onclick="event.stopPropagation()">
|
<div class="modal" onclick="event.stopPropagation()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="modal-title">Add Device Manually</span>
|
<span class="modal-title">Add Wemo Device Manually</span>
|
||||||
<button class="modal-close" onclick="closeManualModal()">✕</button>
|
<button class="modal-close" onclick="closeAddDeviceModal()">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">IP Address</label>
|
<label class="form-label">IP Address</label>
|
||||||
<input class="form-input" id="f-manual-host" type="text" placeholder="192.168.1.100" autofocus />
|
<input class="form-input" id="add-device-host" type="text" placeholder="192.168.1.100" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Port</label>
|
<label class="form-label">Port (optional)</label>
|
||||||
<input class="form-input" id="f-manual-port" type="number" placeholder="49153" value="49153" />
|
<input class="form-input" id="add-device-port" type="number" placeholder="49153" min="1" max="65535" />
|
||||||
|
<div style="font-size:12px;color:var(--text2);margin-top:4px;">Default: 49153 (try 49152-49156 if needed)</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="modal-manual-error" style="display:none;color:var(--danger);font-size:13px;margin-bottom:10px;"></div>
|
|
||||||
|
<div id="add-device-error" style="display:none;color:var(--danger);font-size:13px;margin-bottom:10px;"></div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn btn-ghost" onclick="closeManualModal()">Cancel</button>
|
<button class="btn btn-ghost" onclick="closeAddDeviceModal()">Cancel</button>
|
||||||
<button class="btn btn-primary" id="btn-add-manual" onclick="addManualDevice()">Add</button>
|
<button class="btn btn-primary" id="btn-add-device" onclick="addDeviceManually()">Add Device</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,14 +650,29 @@ function renderDevices() {
|
|||||||
}
|
}
|
||||||
el.innerHTML = devices.map((d, i) => {
|
el.innerHTML = devices.map((d, i) => {
|
||||||
const name = d.friendlyName || d.name || d.host;
|
const name = d.friendlyName || d.name || d.host;
|
||||||
|
const isDimmer = d.isDimmer || false;
|
||||||
|
const icon = isDimmer ? '🔆' : '💡';
|
||||||
return `
|
return `
|
||||||
<div class="card" id="dev-${i}">
|
<div class="card" id="dev-${i}">
|
||||||
<div style="font-size:24px;flex-shrink:0">💡</div>
|
<div style="font-size:24px;flex-shrink:0">${icon}</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="card-name">${esc(name)}</div>
|
<div class="card-name">${esc(name)}</div>
|
||||||
<div class="card-meta">${esc(d.host)}:${d.port}
|
<div class="card-meta">${esc(d.host)}:${d.port}
|
||||||
${d.productModel ? ' · ' + esc(d.productModel) : ''}
|
${d.productModel ? ' · ' + esc(d.productModel) : ''}
|
||||||
|
${isDimmer ? ' · Dimmer' : ''}
|
||||||
</div>
|
</div>
|
||||||
|
${isDimmer ? `
|
||||||
|
<div class="brightness-control" style="margin-top:8px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
|
<span style="font-size:12px;color:var(--text2);">🔅</span>
|
||||||
|
<input type="range" class="brightness-slider" id="bright-${i}"
|
||||||
|
min="0" max="100" value="50"
|
||||||
|
oninput="updateBrightnessPreview(${i}, this.value)"
|
||||||
|
onchange="setBrightness(${i}, this.value)">
|
||||||
|
<span class="brightness-value" id="bright-val-${i}" style="font-size:12px;color:var(--text2);min-width:30px;">50%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
<label class="toggle" id="dtog-${i}" onclick="toggleDevice(${i},event)">
|
<label class="toggle" id="dtog-${i}" onclick="toggleDevice(${i},event)">
|
||||||
<input type="checkbox" id="dchk-${i}">
|
<input type="checkbox" id="dchk-${i}">
|
||||||
@@ -601,11 +681,29 @@ function renderDevices() {
|
|||||||
</label>
|
</label>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
// fetch current state for each
|
|
||||||
|
// fetch current state for each device
|
||||||
devices.forEach((d, i) => {
|
devices.forEach((d, i) => {
|
||||||
api('GET', `/api/devices/${d.host}/${d.port}/state`)
|
api('GET', `/api/devices/${d.host}/${d.port}/state`)
|
||||||
.then((on) => { const c = document.getElementById('dchk-'+i); if (c) c.checked = !!on; })
|
.then((on) => {
|
||||||
|
const c = document.getElementById('dchk-'+i);
|
||||||
|
if (c) c.checked = !!on;
|
||||||
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
|
// fetch brightness for dimmer devices
|
||||||
|
if (d.isDimmer) {
|
||||||
|
api('GET', `/api/devices/${d.host}/${d.port}/brightness`)
|
||||||
|
.then((data) => {
|
||||||
|
const slider = document.getElementById('bright-'+i);
|
||||||
|
const value = document.getElementById('bright-val-'+i);
|
||||||
|
if (slider && value && data.brightness !== undefined) {
|
||||||
|
slider.value = data.brightness;
|
||||||
|
value.textContent = Math.round(data.brightness) + '%';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,64 +725,34 @@ async function toggleDevice(i, e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Manual Add ─────────────────────────────────────────────────────────────
|
// ── Brightness control ───────────────────────────────────────────────────────
|
||||||
function openManualModal() {
|
function updateBrightnessPreview(i, value) {
|
||||||
document.getElementById('f-manual-host').value = '';
|
const valueEl = document.getElementById('bright-val-' + i);
|
||||||
document.getElementById('f-manual-port').value = '49153';
|
if (valueEl) {
|
||||||
document.getElementById('modal-manual-error').style.display = 'none';
|
valueEl.textContent = Math.round(value) + '%';
|
||||||
document.getElementById('modal-manual').classList.add('open');
|
|
||||||
setTimeout(() => document.getElementById('f-manual-host').focus(), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeManualModal(e) {
|
|
||||||
if (e && e.target !== document.getElementById('modal-manual')) return;
|
|
||||||
document.getElementById('modal-manual').classList.remove('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addManualDevice() {
|
|
||||||
const errEl = document.getElementById('modal-manual-error');
|
|
||||||
errEl.style.display = 'none';
|
|
||||||
|
|
||||||
const host = document.getElementById('f-manual-host').value.trim();
|
|
||||||
const port = parseInt(document.getElementById('f-manual-port').value, 10) || 49153;
|
|
||||||
|
|
||||||
if (!host) {
|
|
||||||
errEl.textContent = '⚠ Enter an IP address';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const btn = document.getElementById('btn-add-manual');
|
async function setBrightness(i, value) {
|
||||||
btn.disabled = true; btn.textContent = 'Adding…';
|
const dev = devices[i];
|
||||||
|
const slider = document.getElementById('bright-' + i);
|
||||||
|
if (!dev || !slider) return;
|
||||||
|
|
||||||
|
slider.disabled = true;
|
||||||
try {
|
try {
|
||||||
const result = await api('POST', '/api/devices/discover', {
|
await api('POST', `/api/devices/${dev.host}/${dev.port}/brightness`, { brightness: Number(value) });
|
||||||
manualEntries: [{ host, port }]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result || result.length === 0) {
|
// Update power toggle state based on brightness
|
||||||
errEl.textContent = '⚠ No device found at that address';
|
const chk = document.getElementById('dchk-' + i);
|
||||||
errEl.style.display = 'block';
|
if (chk) {
|
||||||
btn.disabled = false; btn.textContent = 'Add';
|
chk.checked = Number(value) > 0;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const device = result.find((d) => d.host === host);
|
toast(`Brightness set to ${Math.round(value)}%`, 'success');
|
||||||
if (device) {
|
|
||||||
const friendlyName = device.friendlyName || device.name || host;
|
|
||||||
toast(`Added ${friendlyName}`, 'success');
|
|
||||||
devices = result;
|
|
||||||
renderDevices();
|
|
||||||
closeManualModal();
|
|
||||||
} else {
|
|
||||||
errEl.textContent = '⚠ Device added but not found in results';
|
|
||||||
errEl.style.display = 'block';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errEl.textContent = `⚠ ${err.message}`;
|
toast(`Failed to set brightness: ${err.message}`, 'error');
|
||||||
errEl.style.display = 'block';
|
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false; btn.textContent = 'Add';
|
slider.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,6 +995,67 @@ function closeRuleModal(e) {
|
|||||||
document.getElementById('modal-rule').classList.remove('open');
|
document.getElementById('modal-rule').classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Add Device Modal ───────────────────────────────────────────────────────
|
||||||
|
function openAddDeviceModal() {
|
||||||
|
document.getElementById('modal-add-device').classList.add('open');
|
||||||
|
document.getElementById('add-device-error').style.display = 'none';
|
||||||
|
document.getElementById('add-device-host').value = '';
|
||||||
|
document.getElementById('add-device-port').value = '';
|
||||||
|
setTimeout(() => document.getElementById('add-device-host').focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAddDeviceModal(e) {
|
||||||
|
if (e && e.target !== document.getElementById('modal-add-device')) return;
|
||||||
|
document.getElementById('modal-add-device').classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addDeviceManually() {
|
||||||
|
const errEl = document.getElementById('add-device-error');
|
||||||
|
const btn = document.getElementById('btn-add-device');
|
||||||
|
|
||||||
|
errEl.style.display = 'none';
|
||||||
|
|
||||||
|
const host = document.getElementById('add-device-host').value.trim();
|
||||||
|
const port = document.getElementById('add-device-port').value.trim();
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
errEl.textContent = 'IP address is required';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic IP validation
|
||||||
|
const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
if (!ipPattern.test(host)) {
|
||||||
|
errEl.textContent = 'Please enter a valid IP address';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const portNum = port ? parseInt(port, 10) : 49153;
|
||||||
|
if (portNum < 1 || portNum > 65535) {
|
||||||
|
errEl.textContent = 'Port must be between 1 and 65535';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Adding…';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const device = await api('POST', '/api/devices/add', { host, port: portNum });
|
||||||
|
toast(`Successfully added ${device.friendlyName || device.host}`, 'success');
|
||||||
|
closeAddDeviceModal();
|
||||||
|
await loadDevices(); // Refresh device list
|
||||||
|
} catch (err) {
|
||||||
|
errEl.textContent = err.message || 'Failed to add device';
|
||||||
|
errEl.style.display = 'block';
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Add Device';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveRule() {
|
async function saveRule() {
|
||||||
const errEl = document.getElementById('modal-rule-error');
|
const errEl = document.getElementById('modal-rule-error');
|
||||||
errEl.style.display = 'none';
|
errEl.style.display = 'none';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dibbly-web:
|
dibbly-web:
|
||||||
image: reg.dev.nervesocket.com/dibbly:latest
|
image: reg.dev.nervesocket.com/dibbly:latest
|
||||||
@@ -12,6 +14,11 @@ services:
|
|||||||
- DATA_DIR=/data
|
- DATA_DIR=/data
|
||||||
- PORT=3456
|
- PORT=3456
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
networks:
|
||||||
|
- dibbly-network
|
||||||
|
# Use host networking on Linux for Wemo SSDP discovery
|
||||||
|
# Uncomment the line below if running on Linux
|
||||||
|
# network_mode: host
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3456/"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3456/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -30,8 +37,30 @@ services:
|
|||||||
reservations:
|
reservations:
|
||||||
memory: 128M
|
memory: 128M
|
||||||
|
|
||||||
|
# Optional: Reverse proxy for SSL termination
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: dibbly-nginx
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||||
|
networks:
|
||||||
|
- dibbly-network
|
||||||
|
depends_on:
|
||||||
|
- dibbly-web
|
||||||
|
profiles:
|
||||||
|
- with-nginx
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dibbly-data:
|
dibbly-data:
|
||||||
driver: local
|
driver: local
|
||||||
dibbly-logs:
|
dibbly-logs:
|
||||||
driver: local
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dibbly-network:
|
||||||
|
driver: bridge
|
||||||
|
|||||||
+49
-7
@@ -88,19 +88,46 @@ async function handleRequest(req, res) {
|
|||||||
// ── Devices ────────────────────────────────────────────────────────────
|
// ── Devices ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (url === '/api/devices' && method === 'GET') {
|
if (url === '/api/devices' && method === 'GET') {
|
||||||
return json(res, store.getDevices());
|
const devices = store.getDevices();
|
||||||
|
// Add dimmer detection to each device
|
||||||
|
const devicesWithDimmerInfo = devices.map(device => ({
|
||||||
|
...device,
|
||||||
|
isDimmer: wemo.isDimmerDevice ? wemo.isDimmerDevice(device) : false
|
||||||
|
}));
|
||||||
|
return json(res, devicesWithDimmerInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url === '/api/devices/discover' && method === 'POST') {
|
if (url === '/api/devices/discover' && method === 'POST') {
|
||||||
const saved = store.getDevices();
|
const saved = store.getDevices();
|
||||||
const manual = saved.map((d) => ({ host: d.host, port: d.port }));
|
const manual = saved.map((d) => ({ host: d.host, port: d.port }));
|
||||||
// Add any manual entries from the request body
|
const devices = await wemo.discoverDevices(8000, manual);
|
||||||
if (body.manualEntries && Array.isArray(body.manualEntries)) {
|
store.saveDevices(devices);
|
||||||
manual.push(...body.manualEntries);
|
return json(res, devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === '/api/devices/add' && method === 'POST') {
|
||||||
|
const { host, port } = body;
|
||||||
|
if (!host) {
|
||||||
|
return jsonErr(res, 'Host is required', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const devicePort = port ? parseInt(port, 10) : 49153;
|
||||||
|
const manualEntry = { host, port: devicePort };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to discover this specific device
|
||||||
|
const devices = await wemo.discoverDevices(5000, [manualEntry]);
|
||||||
|
if (devices.length > 0) {
|
||||||
|
// Add to existing devices
|
||||||
|
const allDevices = [...store.getDevices(), ...devices];
|
||||||
|
store.saveDevices(allDevices);
|
||||||
|
return json(res, devices[0], 201);
|
||||||
|
} else {
|
||||||
|
return jsonErr(res, 'No Wemo device found at this address', 404);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return jsonErr(res, `Failed to connect: ${err.message}`, 500);
|
||||||
}
|
}
|
||||||
const devs = await wemo.discoverDevices(8000, manual);
|
|
||||||
store.saveDevices(devs);
|
|
||||||
return json(res, devs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/state$/);
|
const stateMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/state$/);
|
||||||
@@ -116,6 +143,21 @@ async function handleRequest(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Brightness control ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const brightnessMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/brightness$/);
|
||||||
|
if (brightnessMatch) {
|
||||||
|
const [, host, port] = brightnessMatch;
|
||||||
|
if (method === 'GET') {
|
||||||
|
const brightness = await wemo.getBrightness(host, Number(port));
|
||||||
|
return json(res, { brightness });
|
||||||
|
}
|
||||||
|
if (method === 'POST') {
|
||||||
|
await wemo.setBrightness(host, Number(port), body.brightness);
|
||||||
|
return json(res, { ok: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── DWM Rules ──────────────────────────────────────────────────────────
|
// ── DWM Rules ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (url === '/api/dwm-rules') {
|
if (url === '/api/dwm-rules') {
|
||||||
|
|||||||
@@ -122,6 +122,70 @@ async function setBinaryState(host, port, on) {
|
|||||||
await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', { BinaryState: on ? '1' : '0' });
|
await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', { BinaryState: on ? '1' : '0' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Dimmer control
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function getBrightness(host, port) {
|
||||||
|
try {
|
||||||
|
const res = await soapWithFallback(host, port, BE_URL, BE_SVC, 'GetBinaryState');
|
||||||
|
const raw = String(res['BinaryState'] ?? '0');
|
||||||
|
|
||||||
|
// For dimmers, BinaryState contains brightness info in format: "1|brightness|..."
|
||||||
|
// Example: "1|50|0" where 50 is the brightness level (0-100)
|
||||||
|
if (raw.includes('|')) {
|
||||||
|
const parts = raw.split('|');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const brightness = parseInt(parts[1], 10);
|
||||||
|
return !isNaN(brightness) ? brightness : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If device is off, return 0
|
||||||
|
return raw === '1' || raw === '8' ? 100 : 0;
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for non-dimmer devices
|
||||||
|
const isOn = await getBinaryState(host, port);
|
||||||
|
return isOn ? 100 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setBrightness(host, port, brightness) {
|
||||||
|
// Brightness should be 0-100
|
||||||
|
const level = Math.max(0, Math.min(100, Math.round(brightness)));
|
||||||
|
|
||||||
|
if (level === 0) {
|
||||||
|
// Turn off the device
|
||||||
|
await setBinaryState(host, port, false);
|
||||||
|
} else {
|
||||||
|
// For dimmers, use the brightness format: "brightness|0"
|
||||||
|
// For non-dimmers, just turn on
|
||||||
|
try {
|
||||||
|
await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', {
|
||||||
|
BinaryState: `${level}|0`
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for non-dimmer devices - just turn on
|
||||||
|
await setBinaryState(host, port, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDimmerDevice(deviceInfo) {
|
||||||
|
if (!deviceInfo) return false;
|
||||||
|
|
||||||
|
const { productModel, modelDescription, udn } = deviceInfo;
|
||||||
|
|
||||||
|
// Check various indicators that this is a dimmer
|
||||||
|
return (
|
||||||
|
(productModel && productModel.toLowerCase().includes('dimmer')) ||
|
||||||
|
(modelDescription && modelDescription.toLowerCase().includes('dimmer')) ||
|
||||||
|
(udn && udn.toLowerCase().includes('dimmer')) ||
|
||||||
|
(productModel && productModel.includes('WDS060')) ||
|
||||||
|
(modelDescription && modelDescription.includes('Dimmer'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Device info
|
// Device info
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -478,6 +542,9 @@ function _insertNewRule(db, ruleId, ruleData) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getBinaryState,
|
getBinaryState,
|
||||||
setBinaryState,
|
setBinaryState,
|
||||||
|
getBrightness,
|
||||||
|
setBrightness,
|
||||||
|
isDimmerDevice,
|
||||||
getDeviceInfo,
|
getDeviceInfo,
|
||||||
discoverDevices,
|
discoverDevices,
|
||||||
fetchRules,
|
fetchRules,
|
||||||
|
|||||||
Reference in New Issue
Block a user