This commit is contained in:
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Redeploy stack in Portainer
|
||||
run: |
|
||||
# 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
|
||||
ENV_VARS=$(cat stack_env.json)
|
||||
|
||||
@@ -134,6 +134,62 @@
|
||||
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.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-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55);
|
||||
z-index: 200; align-items: flex-end; justify-content: center; }
|
||||
@@ -196,8 +252,9 @@
|
||||
<div id="page-devices" class="page active">
|
||||
<div class="toolbar">
|
||||
<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-secondary btn-sm" id="btn-manual-add" onclick="openManualModal()" title="Add device manually" style="width:auto;padding:9px 12px;">+</button>
|
||||
</div>
|
||||
<div id="page-header-devices" class="page-header">
|
||||
<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;">
|
||||
<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/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;">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;">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>
|
||||
@@ -437,25 +497,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Manual Add Modal ── -->
|
||||
<div class="modal-backdrop" id="modal-manual" onclick="closeManualModal(event)">
|
||||
<!-- ── Add Device Modal ── -->
|
||||
<div class="modal-backdrop" id="modal-add-device" onclick="closeAddDeviceModal(event)">
|
||||
<div class="modal" onclick="event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">Add Device Manually</span>
|
||||
<button class="modal-close" onclick="closeManualModal()">✕</button>
|
||||
<span class="modal-title">Add Wemo Device Manually</span>
|
||||
<button class="modal-close" onclick="closeAddDeviceModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label class="form-label">Port</label>
|
||||
<input class="form-input" id="f-manual-port" type="number" placeholder="49153" value="49153" />
|
||||
<label class="form-label">Port (optional)</label>
|
||||
<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 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">
|
||||
<button class="btn btn-ghost" onclick="closeManualModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="btn-add-manual" onclick="addManualDevice()">Add</button>
|
||||
<button class="btn btn-ghost" onclick="closeAddDeviceModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="btn-add-device" onclick="addDeviceManually()">Add Device</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -585,14 +650,29 @@ function renderDevices() {
|
||||
}
|
||||
el.innerHTML = devices.map((d, i) => {
|
||||
const name = d.friendlyName || d.name || d.host;
|
||||
const isDimmer = d.isDimmer || false;
|
||||
const icon = isDimmer ? '🔆' : '💡';
|
||||
return `
|
||||
<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-name">${esc(name)}</div>
|
||||
<div class="card-meta">${esc(d.host)}:${d.port}
|
||||
${d.productModel ? ' · ' + esc(d.productModel) : ''}
|
||||
${isDimmer ? ' · Dimmer' : ''}
|
||||
</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>
|
||||
<label class="toggle" id="dtog-${i}" onclick="toggleDevice(${i},event)">
|
||||
<input type="checkbox" id="dchk-${i}">
|
||||
@@ -601,11 +681,29 @@ function renderDevices() {
|
||||
</label>
|
||||
</div>`;
|
||||
}).join('');
|
||||
// fetch current state for each
|
||||
|
||||
// fetch current state for each device
|
||||
devices.forEach((d, i) => {
|
||||
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(() => {});
|
||||
|
||||
// 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 ─────────────────────────────────────────────────────────────
|
||||
function openManualModal() {
|
||||
document.getElementById('f-manual-host').value = '';
|
||||
document.getElementById('f-manual-port').value = '49153';
|
||||
document.getElementById('modal-manual-error').style.display = 'none';
|
||||
document.getElementById('modal-manual').classList.add('open');
|
||||
setTimeout(() => document.getElementById('f-manual-host').focus(), 100);
|
||||
// ── Brightness control ───────────────────────────────────────────────────────
|
||||
function updateBrightnessPreview(i, value) {
|
||||
const valueEl = document.getElementById('bright-val-' + i);
|
||||
if (valueEl) {
|
||||
valueEl.textContent = Math.round(value) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
btn.disabled = true; btn.textContent = 'Adding…';
|
||||
async function setBrightness(i, value) {
|
||||
const dev = devices[i];
|
||||
const slider = document.getElementById('bright-' + i);
|
||||
if (!dev || !slider) return;
|
||||
|
||||
slider.disabled = true;
|
||||
try {
|
||||
const result = await api('POST', '/api/devices/discover', {
|
||||
manualEntries: [{ host, port }]
|
||||
});
|
||||
await api('POST', `/api/devices/${dev.host}/${dev.port}/brightness`, { brightness: Number(value) });
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
errEl.textContent = '⚠ No device found at that address';
|
||||
errEl.style.display = 'block';
|
||||
btn.disabled = false; btn.textContent = 'Add';
|
||||
return;
|
||||
// Update power toggle state based on brightness
|
||||
const chk = document.getElementById('dchk-' + i);
|
||||
if (chk) {
|
||||
chk.checked = Number(value) > 0;
|
||||
}
|
||||
|
||||
const device = result.find((d) => d.host === host);
|
||||
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';
|
||||
}
|
||||
toast(`Brightness set to ${Math.round(value)}%`, 'success');
|
||||
} catch (err) {
|
||||
errEl.textContent = `⚠ ${err.message}`;
|
||||
errEl.style.display = 'block';
|
||||
toast(`Failed to set brightness: ${err.message}`, 'error');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Add';
|
||||
slider.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,6 +995,67 @@ function closeRuleModal(e) {
|
||||
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() {
|
||||
const errEl = document.getElementById('modal-rule-error');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
dibbly-web:
|
||||
image: reg.dev.nervesocket.com/dibbly:latest
|
||||
@@ -12,6 +14,11 @@ services:
|
||||
- DATA_DIR=/data
|
||||
- PORT=3456
|
||||
- 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:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3456/"]
|
||||
interval: 30s
|
||||
@@ -30,8 +37,30 @@ services:
|
||||
reservations:
|
||||
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:
|
||||
dibbly-data:
|
||||
driver: local
|
||||
dibbly-logs:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
dibbly-network:
|
||||
driver: bridge
|
||||
|
||||
+49
-7
@@ -88,19 +88,46 @@ async function handleRequest(req, res) {
|
||||
// ── Devices ────────────────────────────────────────────────────────────
|
||||
|
||||
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') {
|
||||
const saved = store.getDevices();
|
||||
const manual = saved.map((d) => ({ host: d.host, port: d.port }));
|
||||
// Add any manual entries from the request body
|
||||
if (body.manualEntries && Array.isArray(body.manualEntries)) {
|
||||
manual.push(...body.manualEntries);
|
||||
const devices = await wemo.discoverDevices(8000, manual);
|
||||
store.saveDevices(devices);
|
||||
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$/);
|
||||
@@ -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 ──────────────────────────────────────────────────────────
|
||||
|
||||
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' });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -478,6 +542,9 @@ function _insertNewRule(db, ruleId, ruleData) {
|
||||
module.exports = {
|
||||
getBinaryState,
|
||||
setBinaryState,
|
||||
getBrightness,
|
||||
setBrightness,
|
||||
isDimmerDevice,
|
||||
getDeviceInfo,
|
||||
discoverDevices,
|
||||
fetchRules,
|
||||
|
||||
Reference in New Issue
Block a user