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;">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;">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;">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/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;">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>
|
||||
@@ -525,6 +528,24 @@
|
||||
</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 ── -->
|
||||
<div class="confirm-backdrop" id="confirm-delete">
|
||||
<div class="confirm-box">
|
||||
@@ -674,11 +695,14 @@ function renderDevices() {
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="rule-actions">
|
||||
<label class="toggle" id="dtog-${i}" onclick="toggleDevice(${i},event)">
|
||||
<input type="checkbox" id="dchk-${i}">
|
||||
<span class="track"></span>
|
||||
<span class="thumb"></span>
|
||||
</label>
|
||||
<button class="icon-btn" onclick="openDeviceInfoModal(${i})" title="Device Info">ℹ️</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).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() {
|
||||
const errEl = document.getElementById('modal-rule-error');
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
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$/);
|
||||
if (wemoRulesMatch && method === 'GET') {
|
||||
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+)$/);
|
||||
|
||||
@@ -154,17 +154,23 @@ async function setBrightness(host, port, brightness) {
|
||||
// Brightness should be 0-100
|
||||
const level = Math.max(0, Math.min(100, Math.round(brightness)));
|
||||
|
||||
console.log(`[DWM] Setting brightness for ${host}:${port} to ${level}%`);
|
||||
|
||||
if (level === 0) {
|
||||
// Turn off the device
|
||||
console.log(`[DWM] Brightness 0, turning device off`);
|
||||
await setBinaryState(host, port, false);
|
||||
} else {
|
||||
// For dimmers, use the brightness format: "brightness|0"
|
||||
// For non-dimmers, just turn on
|
||||
try {
|
||||
console.log(`[DWM] Trying dimmer brightness format: ${level}|0`);
|
||||
await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', {
|
||||
BinaryState: `${level}|0`
|
||||
});
|
||||
console.log(`[DWM] Dimmer brightness set successfully`);
|
||||
} catch (err) {
|
||||
console.log(`[DWM] Dimmer format failed: ${err.message}, falling back to on/off`);
|
||||
// Fallback for non-dimmer devices - just turn on
|
||||
await setBinaryState(host, port, true);
|
||||
}
|
||||
@@ -174,16 +180,30 @@ async function setBrightness(host, port, brightness) {
|
||||
function isDimmerDevice(deviceInfo) {
|
||||
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
|
||||
return (
|
||||
const isDimmer = (
|
||||
(productModel && productModel.toLowerCase().includes('dimmer')) ||
|
||||
(modelDescription && modelDescription.toLowerCase().includes('dimmer')) ||
|
||||
(udn && udn.toLowerCase().includes('dimmer')) ||
|
||||
(deviceType && deviceType.toLowerCase().includes('dimmer')) ||
|
||||
(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
|
||||
networks:
|
||||
- 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:
|
||||
dibbly-data:
|
||||
|
||||
Reference in New Issue
Block a user