bugfix for rule fetch, feat: device info and trying to fix dimmer
Build Images and Deploy / Update-PROD-Stack (push) Successful in 18s

This commit is contained in:
2026-03-30 22:38:52 -04:00
parent 5bffb1064d
commit 7ea32cee8c
5 changed files with 129 additions and 107 deletions
-89
View File
@@ -1,89 +0,0 @@
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions
name: Build Images and Deploy
run-name: ${{ gitea.actor }} is building new PROD images and redeploying the existing stack 🚀
on:
push:
# not working right now https://github.com/actions/runner/issues/2324
# paths-ignore:
# - **.yml
branches:
- main
env:
STACK_NAME: hashex
DOT_ENV: ${{ secrets.PROD_ENV }}
PORTAINER_TOKEN: ${{ vars.PORTAINER_TOKEN }}
PORTAINER_API_URL: https://portainer.dev.nervesocket.com/api
ENDPOINT_NAME: "mini" #sometimes "primary"
IMAGE_TAG: "reg.dev.nervesocket.com/hashex:latest"
jobs:
Update-PROD-Stack:
runs-on: ubuntu-latest
steps:
# if: contains(github.event.pull_request.head.ref, 'init-stack')
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push PROD Docker image
run: |
echo $DOT_ENV | base64 -d > .env
docker buildx build --push -f Dockerfile -t $IMAGE_TAG .
- name: Get the endpoint ID
# Usually ID is 1, but you can get it from the API. Only skip this if you are VERY sure.
run: |
ENDPOINT_ID=$(curl -s -H "X-API-Key: $PORTAINER_TOKEN" "$PORTAINER_API_URL/endpoints" | jq -r ".[] | select(.Name==\"$ENDPOINT_NAME\") | .Id")
echo "ENDPOINT_ID=$ENDPOINT_ID" >> $GITHUB_ENV
echo "Got stack Endpoint ID: $ENDPOINT_ID"
- name: Fetch stack ID from Portainer
run: |
STACK_ID=$(curl -s -H "X-API-Key: $PORTAINER_TOKEN" "$PORTAINER_API_URL/stacks" | jq -r ".[] | select(.Name==\"$STACK_NAME\" and .EndpointId==$ENDPOINT_ID) | .Id")
echo "STACK_ID=$STACK_ID" >> $GITHUB_ENV
echo "Got stack ID: $STACK_ID matched with Endpoint ID: $ENDPOINT_ID"
- name: Fetch Stack
run: |
# Get the stack details (including env vars)
STACK_DETAILS=$(curl -s -H "X-API-Key: $PORTAINER_TOKEN" "$PORTAINER_API_URL/stacks/$STACK_ID")
# Extract environment variables from the stack
echo "$STACK_DETAILS" | jq -r '.Env' > stack_env.json
echo "Existing stack environment variables:"
cat stack_env.json
- name: Redeploy stack in Portainer
run: |
# Read stack file content
STACK_FILE_CONTENT=$(echo "$(<prod-compose.yml )")
# Read existing environment variables from the fetched stack
ENV_VARS=$(cat stack_env.json)
# Prepare JSON payload with environment variables
JSON_PAYLOAD=$(jq -n --arg stackFileContent "$STACK_FILE_CONTENT" --argjson pullImage true --argjson env "$ENV_VARS" \
'{stackFileContent: $stackFileContent, pullImage: $pullImage, env: $env}')
echo "About to push the following JSON payload:"
echo $JSON_PAYLOAD
# Update stack in Portainer (this redeploys it)
DEPLOY_RESPONSE=$(curl -X PUT "$PORTAINER_API_URL/stacks/$STACK_ID?endpointId=$ENDPOINT_ID" \
-H "X-API-Key: $PORTAINER_TOKEN" \
-H "Content-Type: application/json" \
--data "$JSON_PAYLOAD")
echo "Redeployed stack in Portainer. Response:"
echo $DEPLOY_RESPONSE
- name: Status check
run: |
echo "📋 This job's status is ${{ job.status }}. Make sure you delete the init file to avoid issues."
+87
View File
@@ -316,9 +316,12 @@
<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/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;">GET /api/devices/:ip/:port/info</td><td>Get device information</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;">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;">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/devices/:ip/:port/rules</td><td>Get Wemo device rules</td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">PUT /api/devices/:ip/:port/rules/:id</td><td>Update Wemo rule</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>
@@ -525,6 +528,24 @@
</div> </div>
</div> </div>
<!-- ── Device Info Modal ── -->
<div class="modal-backdrop" id="modal-device-info" onclick="closeDeviceInfoModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-header">
<span class="modal-title">Device Information</span>
<button class="modal-close" onclick="closeDeviceInfoModal()">×</button>
</div>
<div id="device-info-content">
<div style="text-align:center;color:var(--text2);padding:20px;">Loading device information…</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeDeviceInfoModal()">Close</button>
</div>
</div>
</div>
<!-- ── Delete Confirm ── --> <!-- ── Delete Confirm ── -->
<div class="confirm-backdrop" id="confirm-delete"> <div class="confirm-backdrop" id="confirm-delete">
<div class="confirm-box"> <div class="confirm-box">
@@ -674,11 +695,14 @@ function renderDevices() {
</div> </div>
` : ''} ` : ''}
</div> </div>
<div class="rule-actions">
<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}">
<span class="track"></span> <span class="track"></span>
<span class="thumb"></span> <span class="thumb"></span>
</label> </label>
<button class="icon-btn" onclick="openDeviceInfoModal(${i})" title="Device Info">️</button>
</div>
</div>`; </div>`;
}).join(''); }).join('');
@@ -1056,6 +1080,69 @@ async function addDeviceManually() {
} }
} }
// ── Device Info Modal ───────────────────────────────────────────────────────
let currentDeviceInfo = null;
async function openDeviceInfoModal(i) {
const dev = devices[i];
if (!dev) return;
currentDeviceInfo = dev;
document.getElementById('modal-device-info').classList.add('open');
const content = document.getElementById('device-info-content');
content.innerHTML = '<div style="text-align:center;color:var(--text2);padding:20px;">Loading device information…</div>';
try {
const info = await api('GET', `/api/devices/${dev.host}/${dev.port}/info`);
renderDeviceInfo(info, dev);
} catch (err) {
content.innerHTML = `
<div style="text-align:center;color:var(--danger);padding:20px;">
<div style="font-size:24px;margin-bottom:10px;">⚠️</div>
<div>Failed to load device information</div>
<div style="font-size:12px;margin-top:8px;">${err.message}</div>
</div>
`;
}
}
function closeDeviceInfoModal(e) {
if (e && e.target !== document.getElementById('modal-device-info')) return;
document.getElementById('modal-device-info').classList.remove('open');
currentDeviceInfo = null;
}
function renderDeviceInfo(info, dev) {
const content = document.getElementById('device-info-content');
const fields = [
{ label: 'Device Name', value: info.friendlyName || dev.friendlyName || 'Unknown' },
{ label: 'IP Address', value: dev.host },
{ label: 'Port', value: dev.port },
{ label: 'Product Model', value: info.productModel || 'Unknown' },
{ label: 'Model Description', value: info.modelDescription || 'Unknown' },
{ label: 'Firmware Version', value: info.firmwareVersion || 'Unknown' },
{ label: 'UDN', value: info.udn || 'Unknown' },
{ label: 'Device Type', value: info.deviceType || 'Unknown' },
{ label: 'Manufacturer', value: info.manufacturer || 'Unknown' },
{ label: 'Is Dimmer', value: dev.isDimmer ? 'Yes' : 'No' },
{ label: 'Serial Number', value: info.serialNumber || 'Unknown' },
{ label: 'MAC Address', value: info.macAddress || 'Unknown' }
];
content.innerHTML = `
<div style="display:grid;gap:12px;">
${fields.map(field => `
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);">
<span style="font-size:13px;color:var(--text2);font-weight:600;">${field.label}</span>
<span style="font-size:13px;color:var(--text);font-family:monospace;word-break:break-all;">${field.value}</span>
</div>
`).join('')}
</div>
`;
}
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';
+14 -1
View File
@@ -246,6 +246,19 @@ async function handleRequest(req, res) {
} }
} }
// ── Device Information ───────────────────────────────────────────────────
const deviceInfoMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/info$/);
if (deviceInfoMatch && method === 'GET') {
const [, host, port] = deviceInfoMatch;
try {
const info = await wemo.getDeviceInfo(host, Number(port));
return json(res, info);
} catch (err) {
return jsonErr(res, `Failed to get device info: ${err.message}`, 500);
}
}
// ── Brightness control ─────────────────────────────────────────────────── // ── Brightness control ───────────────────────────────────────────────────
const brightnessMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/brightness$/); const brightnessMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/brightness$/);
@@ -292,7 +305,7 @@ async function handleRequest(req, res) {
const wemoRulesMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/rules$/); const wemoRulesMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/rules$/);
if (wemoRulesMatch && method === 'GET') { if (wemoRulesMatch && method === 'GET') {
const [, host, port] = wemoRulesMatch; const [, host, port] = wemoRulesMatch;
return json(res, await wemo.getRules(host, Number(port))); return json(res, await wemo.fetchRules(host, Number(port)));
} }
const wemoRuleMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/rules\/(\d+)$/); const wemoRuleMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/rules\/(\d+)$/);
+23 -3
View File
@@ -154,17 +154,23 @@ async function setBrightness(host, port, brightness) {
// Brightness should be 0-100 // Brightness should be 0-100
const level = Math.max(0, Math.min(100, Math.round(brightness))); const level = Math.max(0, Math.min(100, Math.round(brightness)));
console.log(`[DWM] Setting brightness for ${host}:${port} to ${level}%`);
if (level === 0) { if (level === 0) {
// Turn off the device // Turn off the device
console.log(`[DWM] Brightness 0, turning device off`);
await setBinaryState(host, port, false); await setBinaryState(host, port, false);
} else { } else {
// For dimmers, use the brightness format: "brightness|0" // For dimmers, use the brightness format: "brightness|0"
// For non-dimmers, just turn on // For non-dimmers, just turn on
try { try {
console.log(`[DWM] Trying dimmer brightness format: ${level}|0`);
await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', { await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', {
BinaryState: `${level}|0` BinaryState: `${level}|0`
}); });
console.log(`[DWM] Dimmer brightness set successfully`);
} catch (err) { } catch (err) {
console.log(`[DWM] Dimmer format failed: ${err.message}, falling back to on/off`);
// Fallback for non-dimmer devices - just turn on // Fallback for non-dimmer devices - just turn on
await setBinaryState(host, port, true); await setBinaryState(host, port, true);
} }
@@ -174,16 +180,30 @@ async function setBrightness(host, port, brightness) {
function isDimmerDevice(deviceInfo) { function isDimmerDevice(deviceInfo) {
if (!deviceInfo) return false; if (!deviceInfo) return false;
const { productModel, modelDescription, udn } = deviceInfo; const { productModel, modelDescription, udn, deviceType } = deviceInfo;
console.log(`[DWM] Checking if device is dimmer:`, {
productModel,
modelDescription,
udn,
deviceType
});
// Check various indicators that this is a dimmer // Check various indicators that this is a dimmer
return ( const isDimmer = (
(productModel && productModel.toLowerCase().includes('dimmer')) || (productModel && productModel.toLowerCase().includes('dimmer')) ||
(modelDescription && modelDescription.toLowerCase().includes('dimmer')) || (modelDescription && modelDescription.toLowerCase().includes('dimmer')) ||
(udn && udn.toLowerCase().includes('dimmer')) || (udn && udn.toLowerCase().includes('dimmer')) ||
(deviceType && deviceType.toLowerCase().includes('dimmer')) ||
(productModel && productModel.includes('WDS060')) || (productModel && productModel.includes('WDS060')) ||
(modelDescription && modelDescription.includes('Dimmer')) (modelDescription && modelDescription.includes('Dimmer')) ||
(udn && udn.includes('Dimmer-1_0')) ||
(productModel && productModel.includes('WDS')) ||
(modelDescription && modelDescription.toLowerCase().includes('light') && modelDescription.toLowerCase().includes('dim'))
); );
console.log(`[DWM] Dimmer detection result: ${isDimmer}`);
return isDimmer;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
-9
View File
@@ -14,15 +14,6 @@ services:
- PORT=3456 - PORT=3456
networks: networks:
- dibbly-network - dibbly-network
# Use host networking on Linux for Wemo SSDP discovery
# Comment out the network_mode line below if not on Linux
# network_mode: host
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3456/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes: volumes:
dibbly-data: dibbly-data: