bugfix for rule fetch, feat: device info and trying to fix dimmer
Build Images and Deploy / Update-PROD-Stack (push) Successful in 18s
Build Images and Deploy / Update-PROD-Stack (push) Successful in 18s
This commit is contained in:
@@ -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."
|
|
||||||
@@ -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
@@ -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+)$/);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user