From 7ea32cee8c0da7e3f407573e7126eedc6ee94873 Mon Sep 17 00:00:00 2001 From: Mike Johnston Date: Mon, 30 Mar 2026 22:38:52 -0400 Subject: [PATCH] bugfix for rule fetch, feat: device info and trying to fix dimmer --- .gitea/workflows/rebuild-prod.yaml | 89 ----------------- apps/desktop/resources/web/index.html | 97 ++++++++++++++++++- docker/server.js | 15 ++- packages/homebridge-plugin/lib/wemo-client.js | 26 ++++- web-compose.yml | 9 -- 5 files changed, 129 insertions(+), 107 deletions(-) delete mode 100644 .gitea/workflows/rebuild-prod.yaml diff --git a/.gitea/workflows/rebuild-prod.yaml b/.gitea/workflows/rebuild-prod.yaml deleted file mode 100644 index c13fbcd..0000000 --- a/.gitea/workflows/rebuild-prod.yaml +++ /dev/null @@ -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 "$(GET /api/devicesList devices POST /api/devices/discoverScan network POST /api/devices/addAdd device manually {"host":"192.168.1.100","port":49153} + GET /api/devices/:ip/:port/infoGet device information POST /api/devices/:ip/:port/stateToggle power {"on":true} GET /api/devices/:ip/:port/brightnessGet brightness (dimmer only) POST /api/devices/:ip/:port/brightnessSet brightness {"brightness":50} + GET /api/devices/:ip/:port/rulesGet Wemo device rules + PUT /api/devices/:ip/:port/rules/:idUpdate Wemo rule GET /api/dwm-rulesList DWM rules POST /api/dwm-rulesCreate rule PUT /api/dwm-rules/:idUpdate rule @@ -525,6 +528,24 @@ + + +
@@ -674,11 +695,14 @@ function renderDevices() {
` : ''}
- +
+ + +
`; }).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 = '
Loading device information…
'; + + try { + const info = await api('GET', `/api/devices/${dev.host}/${dev.port}/info`); + renderDeviceInfo(info, dev); + } catch (err) { + content.innerHTML = ` +
+
⚠️
+
Failed to load device information
+
${err.message}
+
+ `; + } +} + +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 = ` +
+ ${fields.map(field => ` +
+ ${field.label} + ${field.value} +
+ `).join('')} +
+ `; +} + async function saveRule() { const errEl = document.getElementById('modal-rule-error'); errEl.style.display = 'none'; diff --git a/docker/server.js b/docker/server.js index 9712e20..37fe240 100644 --- a/docker/server.js +++ b/docker/server.js @@ -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+)$/); diff --git a/packages/homebridge-plugin/lib/wemo-client.js b/packages/homebridge-plugin/lib/wemo-client.js index d2aa707..e967033 100644 --- a/packages/homebridge-plugin/lib/wemo-client.js +++ b/packages/homebridge-plugin/lib/wemo-client.js @@ -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; } // --------------------------------------------------------------------------- diff --git a/web-compose.yml b/web-compose.yml index aede496..990165d 100644 --- a/web-compose.yml +++ b/web-compose.yml @@ -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: