Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 905b54803d | |||
| b724e805dd | |||
| 93dc2952ec | |||
| cd0ca8cf7c | |||
| 0a4ff9bf76 | |||
| 5180b3bce1 | |||
| d7e9f0d3d3 | |||
| f7c952cec4 | |||
| 9e6101dc3c | |||
| 0bb3398097 | |||
| 7ea32cee8c | |||
| 5bffb1064d | |||
| da2693ae68 | |||
| 70c98af759 | |||
| 0977a610ff | |||
| ab17324f85 | |||
| f5e69fa9cd | |||
| fe48f9b465 | |||
| 3480c75f4c | |||
| 58f2724d17 | |||
| 05079ca545 | |||
| 129f22a785 | |||
| 020d43396f | |||
| fb688bdfd3 | |||
| 0aac2b60eb | |||
| d8c32b438f | |||
| 2e9dade24f | |||
| c5501c945f | |||
| e79ff02141 | |||
| 37f4a4ea25 | |||
| 7bd3a81bda | |||
| 3c155f7cfd | |||
| e8b365e5a7 | |||
| e52b3578dc | |||
| 5024996523 | |||
| 4c09fd0b66 | |||
| 2f9f68eca7 | |||
| 4f4d2a7b61 | |||
| b3af43120d | |||
| 817b91960c | |||
| c9ae546d4a | |||
| 9b1a81b968 | |||
| 2790f5a82d | |||
| a33a97cefc | |||
| 5a5155c216 | |||
| 3bcc427683 | |||
| 951d4c4eaa | |||
| b200c45385 | |||
| 3fa94ea6e3 | |||
| 3d98f684cf | |||
| 328a607e28 |
@@ -0,0 +1,81 @@
|
||||
name: Build Android APK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Version tag (e.g. 2.0.0)'
|
||||
required: false
|
||||
default: '2.0.0'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android platform tools
|
||||
run: |
|
||||
sdkmanager "platforms;android-34" "build-tools;34.0.0"
|
||||
|
||||
- name: Install Capacitor dependencies
|
||||
working-directory: apps/android
|
||||
run: npm install
|
||||
|
||||
- name: Add Android platform
|
||||
working-directory: apps/android
|
||||
run: npx cap add android || echo "Android platform already exists"
|
||||
|
||||
- name: Sync Capacitor
|
||||
working-directory: apps/android
|
||||
run: npx cap sync android
|
||||
|
||||
- name: Make gradlew executable
|
||||
working-directory: apps/android/android
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Build debug APK
|
||||
working-directory: apps/android/android
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: Rename APK
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.tag }}"
|
||||
cp apps/android/android/app/build/outputs/apk/debug/app-debug.apk \
|
||||
"dibby-wemo-$VERSION-android.apk"
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dibby-wemo-android-apk
|
||||
path: dibby-wemo-*.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
if: github.event.inputs.tag != ''
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ github.event.inputs.tag }}
|
||||
files: dibby-wemo-*.apk
|
||||
fail_on_unmatched_files: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Build & Push Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker image tag (e.g. 2.0.0)'
|
||||
required: true
|
||||
default: '2.0.0'
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/k0rb3nd4ll4s/dibby-wemo-manager:${{ github.event.inputs.tag }}
|
||||
ghcr.io/k0rb3nd4ll4s/dibby-wemo-manager:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Build & Upload Linux Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to upload assets to (e.g. v2.0.0)'
|
||||
required: true
|
||||
default: 'v2.0.0'
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Linux build tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq rpm fakeroot dpkg libarchive-tools
|
||||
|
||||
- name: Install workspace dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Install app-builder to system PATH
|
||||
run: |
|
||||
# The binary is bundled in the npm package for the current platform.
|
||||
# Copy it to /usr/local/bin so USE_SYSTEM_APP_BUILDER=true can find it.
|
||||
BINARY=$(node -e "console.log(require('./node_modules/app-builder-bin').appBuilderPath)")
|
||||
echo "app-builder binary: $BINARY"
|
||||
chmod +x "$BINARY"
|
||||
sudo cp "$BINARY" /usr/local/bin/app-builder
|
||||
app-builder --version
|
||||
|
||||
- name: Vite build
|
||||
run: npx electron-vite build
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Bundle standalone scheduler
|
||||
run: node scripts/bundle-standalone.js
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Build Linux x64 packages
|
||||
run: npx electron-builder --linux --x64 --publish never
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
USE_SYSTEM_APP_BUILDER: "true"
|
||||
|
||||
- name: Build Linux arm64 packages
|
||||
run: npx electron-builder --linux --arm64 --publish never
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
USE_SYSTEM_APP_BUILDER: "true"
|
||||
|
||||
- name: List build output
|
||||
run: ls -lh apps/desktop/dist/
|
||||
|
||||
- name: Upload Linux packages to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
apps/desktop/dist/*.AppImage
|
||||
apps/desktop/dist/*.deb
|
||||
apps/desktop/dist/*.rpm
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Build & Upload macOS Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to upload assets to (e.g. v2.0.0)'
|
||||
required: true
|
||||
default: 'v2.0.0'
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install workspace dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Vite build
|
||||
run: npx electron-vite build
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Bundle standalone scheduler
|
||||
run: node scripts/bundle-standalone.js
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Build macOS packages
|
||||
run: npx electron-builder --mac --publish never
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
|
||||
- name: List build output
|
||||
run: ls -lh apps/desktop/dist/
|
||||
|
||||
- name: Upload macOS packages to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
files: apps/desktop/dist/*.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,89 @@
|
||||
# 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: dibbly
|
||||
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/dibbly: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 "$(<web-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."
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Build Android APK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Version tag (e.g. 2.0.0)'
|
||||
required: false
|
||||
default: '2.0.0'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android platform tools
|
||||
run: |
|
||||
sdkmanager "platforms;android-34" "build-tools;34.0.0"
|
||||
|
||||
- name: Install Capacitor dependencies
|
||||
working-directory: apps/android
|
||||
run: npm install
|
||||
|
||||
- name: Add Android platform
|
||||
working-directory: apps/android
|
||||
run: npx cap add android || echo "Android platform already exists"
|
||||
|
||||
- name: Sync Capacitor
|
||||
working-directory: apps/android
|
||||
run: npx cap sync android
|
||||
|
||||
- name: Make gradlew executable
|
||||
working-directory: apps/android/android
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Build debug APK
|
||||
working-directory: apps/android/android
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: Rename APK
|
||||
run: |
|
||||
VERSION="${{ github.event.inputs.tag }}"
|
||||
cp apps/android/android/app/build/outputs/apk/debug/app-debug.apk \
|
||||
"dibby-wemo-$VERSION-android.apk"
|
||||
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dibby-wemo-android-apk
|
||||
path: dibby-wemo-*.apk
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
if: github.event.inputs.tag != ''
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ github.event.inputs.tag }}
|
||||
files: dibby-wemo-*.apk
|
||||
fail_on_unmatched_files: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Build & Push Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Docker image tag (e.g. 2.0.0)'
|
||||
required: true
|
||||
default: '2.0.0'
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/k0rb3nd4ll4s/dibby-wemo-manager:${{ github.event.inputs.tag }}
|
||||
ghcr.io/k0rb3nd4ll4s/dibby-wemo-manager:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Build & Upload Linux Packages
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to upload assets to (e.g. v2.0.0)'
|
||||
required: true
|
||||
default: 'v2.0.0'
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Linux build tools
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq rpm fakeroot dpkg libarchive-tools
|
||||
|
||||
- name: Install workspace dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Install app-builder to system PATH
|
||||
run: |
|
||||
# The binary is bundled in the npm package for the current platform.
|
||||
# Copy it to /usr/local/bin so USE_SYSTEM_APP_BUILDER=true can find it.
|
||||
BINARY=$(node -e "console.log(require('./node_modules/app-builder-bin').appBuilderPath)")
|
||||
echo "app-builder binary: $BINARY"
|
||||
chmod +x "$BINARY"
|
||||
sudo cp "$BINARY" /usr/local/bin/app-builder
|
||||
app-builder --version
|
||||
|
||||
- name: Vite build
|
||||
run: npx electron-vite build
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Bundle standalone scheduler
|
||||
run: node scripts/bundle-standalone.js
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Build Linux x64 packages
|
||||
run: npx electron-builder --linux --x64 --publish never
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
USE_SYSTEM_APP_BUILDER: "true"
|
||||
|
||||
- name: Build Linux arm64 packages
|
||||
run: npx electron-builder --linux --arm64 --publish never
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
USE_SYSTEM_APP_BUILDER: "true"
|
||||
|
||||
- name: List build output
|
||||
run: ls -lh apps/desktop/dist/
|
||||
|
||||
- name: Upload Linux packages to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
files: |
|
||||
apps/desktop/dist/*.AppImage
|
||||
apps/desktop/dist/*.deb
|
||||
apps/desktop/dist/*.rpm
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Build & Upload macOS Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to upload assets to (e.g. v2.0.0)'
|
||||
required: true
|
||||
default: 'v2.0.0'
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install workspace dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Vite build
|
||||
run: npx electron-vite build
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Bundle standalone scheduler
|
||||
run: node scripts/bundle-standalone.js
|
||||
working-directory: apps/desktop
|
||||
|
||||
- name: Build macOS packages
|
||||
run: npx electron-builder --mac --publish never
|
||||
working-directory: apps/desktop
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
|
||||
- name: List build output
|
||||
run: ls -lh apps/desktop/dist/
|
||||
|
||||
- name: Upload macOS packages to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
files: apps/desktop/dist/*.dmg
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,89 @@
|
||||
# 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: dibbly
|
||||
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/dibbly: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 "$(<web-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."
|
||||
@@ -8,6 +8,7 @@ dist/
|
||||
out/
|
||||
build/
|
||||
*.blockmap
|
||||
*.tgz
|
||||
builder-debug.yml
|
||||
builder-effective-config.yaml
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
LABEL maintainer="SRS IT" \
|
||||
org.opencontainers.image.title="Dibby Wemo Manager" \
|
||||
org.opencontainers.image.description="Headless Belkin Wemo scheduler + web remote — no cloud required" \
|
||||
org.opencontainers.image.source="https://github.com/K0rb3nD4ll4S/dibby-wemo-manager"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package manifest first for layer caching
|
||||
COPY docker/package.json ./package.json
|
||||
|
||||
# Install production dependencies
|
||||
RUN npm install --production
|
||||
|
||||
# Copy application code
|
||||
COPY packages/homebridge-plugin/lib ./lib
|
||||
COPY docker/server.js ./server.js
|
||||
|
||||
# Copy mobile web UI and icon
|
||||
COPY apps/desktop/resources/web ./web
|
||||
COPY apps/desktop/resources/icon.png ./icon.png
|
||||
|
||||
# Persistent data volume (stores dibby-wemo.json config + rules)
|
||||
VOLUME /data
|
||||
|
||||
ENV DATA_DIR=/data \
|
||||
PORT=3456
|
||||
|
||||
EXPOSE 3456
|
||||
|
||||
# NOTE: Wemo SSDP discovery requires --network host on Linux Docker hosts.
|
||||
# On macOS Docker Desktop, host networking is not supported — add devices
|
||||
# manually via the web UI or the REST API instead.
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
@@ -6,7 +6,7 @@ Dibby Wemo Manager gives you full local control of Belkin Wemo smart switches an
|
||||
|
||||
| Component | Description |
|
||||
|---|---|
|
||||
| 🖥️ **Desktop App** | Windows Electron app — device dashboard, power control, scheduling |
|
||||
| 🖥️ **Desktop App** | Cross-platform Electron app (Windows + Linux) — device dashboard, power control, scheduling |
|
||||
| 🏠 **Homebridge Plugin** | HomeKit integration with custom scheduling UI inside Homebridge |
|
||||
|
||||
Both share the same local-network Wemo protocol (UPnP/SOAP) and the same DWM scheduling engine. No Belkin account, no cloud dependency, no internet required.
|
||||
@@ -16,12 +16,12 @@ Both share the same local-network Wemo protocol (UPnP/SOAP) and the same DWM sch
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
wemo-manager/
|
||||
dibby-wemo-manager/
|
||||
├── apps/
|
||||
│ └── desktop/ # Electron desktop app (Windows)
|
||||
│ └── desktop/ # Electron desktop app (Windows + Linux)
|
||||
├── packages/
|
||||
│ ├── homebridge-plugin/ # homebridge-dibby-wemo Homebridge plugin
|
||||
│ └── wemo-core/ # Shared Wemo protocol helpers
|
||||
│ └── wemo-core/ # Shared Wemo protocol helpers (internal)
|
||||
└── package.json # npm workspaces root
|
||||
```
|
||||
|
||||
@@ -29,14 +29,24 @@ wemo-manager/
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Desktop App (Windows)
|
||||
### Desktop App
|
||||
|
||||
Download the latest installer from [Releases](../../releases):
|
||||
|
||||
**Windows:**
|
||||
- **`Dibby Wemo Manager Setup 2.0.0.exe`** — NSIS installer (recommended)
|
||||
- **`Dibby Wemo Manager 2.0.0.exe`** — Portable single-file executable
|
||||
|
||||
Run the installer, launch the app. Wemo devices are discovered automatically via SSDP on your local network.
|
||||
**Linux (x64):**
|
||||
- **`Dibby Wemo Manager-2.0.0.AppImage`** — Universal AppImage, runs anywhere
|
||||
- **`dibby-wemo-manager_2.0.0_amd64.deb`** — Debian / Ubuntu
|
||||
- **`dibby-wemo-manager-2.0.0.x86_64.rpm`** — Fedora / RHEL
|
||||
|
||||
**Linux (ARM64 — Raspberry Pi 4/5):**
|
||||
- **`Dibby Wemo Manager-2.0.0-arm64.AppImage`**
|
||||
- **`dibby-wemo-manager_2.0.0_arm64.deb`**
|
||||
|
||||
Run the installer (Windows) or AppImage (Linux). Wemo devices are discovered automatically via SSDP on your local network.
|
||||
|
||||
### Homebridge Plugin
|
||||
|
||||
@@ -74,10 +84,14 @@ Restart Homebridge. Devices appear in HomeKit automatically.
|
||||
- **Always On** — enforce a device stays on; auto-corrects within 10 seconds
|
||||
- **Trigger** — IFTTT-style: when device A changes state, control device B
|
||||
- **Native firmware rules** — read, toggle and delete rules stored on the Wemo device itself
|
||||
- **Standalone service** — Windows background service that enforces rules even when the GUI is closed
|
||||
- **Background scheduler** — keeps rules firing even when the GUI is closed
|
||||
- Windows: native Windows service (`DibbyWemoService`)
|
||||
- Linux: background process, runs while app is in system tray
|
||||
- **Web remote** — optional local web interface accessible from your phone
|
||||
- **Sunrise/sunset support** — location-aware scheduling via city search
|
||||
|
||||
**Platforms:** Windows 10+ (x64) · Linux x64 · Linux ARM64 (Raspberry Pi 4/5)
|
||||
|
||||
### 🏠 Homebridge Plugin
|
||||
|
||||
- All Wemo devices registered as **HomeKit switches**
|
||||
@@ -141,13 +155,17 @@ The Wemo device stores rules in a SQLite database inside a ZIP archive:
|
||||
|
||||
The DWM (Dibby Wemo Manager) scheduler is a Node.js process that:
|
||||
|
||||
- Loads rules from a JSON store (`dibby-wemo.json`)
|
||||
- Loads rules from a JSON store
|
||||
- Ticks every **30 seconds**, reloading rules on each tick (live edits take effect without restart)
|
||||
- Pre-schedules events within a **65-second look-ahead window**
|
||||
- On startup, catches up any rules missed within the last **10 minutes**
|
||||
- Runs a **health monitor** every 10 seconds for AlwaysOn and Trigger rules
|
||||
- Writes a **heartbeat** to the store on every tick so the UI can show scheduler status
|
||||
|
||||
### Shared Core
|
||||
|
||||
`packages/wemo-core` contains shared constants and utilities (day numbers, time conversions, sunrise/sunset calculator) used by both the desktop app and the Homebridge plugin without duplication. It is an internal npm workspace package, not published to npm.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
@@ -157,10 +175,10 @@ The DWM (Dibby Wemo Manager) scheduler is a Node.js process that:
|
||||
- Node.js ≥ 18
|
||||
- npm ≥ 9
|
||||
|
||||
### Install dependencies
|
||||
### Install all dependencies
|
||||
|
||||
```bash
|
||||
# From repo root
|
||||
# From repo root — installs all workspaces
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -171,16 +189,27 @@ cd apps/desktop
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Desktop App — build Windows installer
|
||||
### Desktop App — build
|
||||
|
||||
```bash
|
||||
# Windows installer + portable exe
|
||||
cd apps/desktop
|
||||
npm run build:win
|
||||
|
||||
# Linux AppImage + .deb + .rpm (x64)
|
||||
cd apps/desktop
|
||||
npm run build:linux
|
||||
|
||||
# Linux ARM64 (Raspberry Pi)
|
||||
cd apps/desktop
|
||||
npm run build:linux:arm64
|
||||
|
||||
# Windows x64 + Linux x64 in one command
|
||||
cd apps/desktop
|
||||
npm run build:all
|
||||
```
|
||||
|
||||
Output in `apps/desktop/dist/`:
|
||||
- `Dibby Wemo Manager Setup 2.0.0.exe` — NSIS installer
|
||||
- `Dibby Wemo Manager 2.0.0.exe` — portable EXE
|
||||
Output in `apps/desktop/dist/`.
|
||||
|
||||
### Homebridge Plugin — install locally
|
||||
|
||||
@@ -197,11 +226,16 @@ Then restart Homebridge.
|
||||
|
||||
Each [GitHub Release](../../releases) includes:
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `Dibby Wemo Manager Setup 2.0.0.exe` | Windows NSIS installer (recommended) |
|
||||
| `Dibby Wemo Manager 2.0.0.exe` | Windows portable executable |
|
||||
| `homebridge-dibby-wemo-1.0.0.tgz` | Homebridge plugin npm package |
|
||||
| File | OS | Description |
|
||||
|---|---|---|
|
||||
| `Dibby Wemo Manager Setup 2.0.0.exe` | Windows | NSIS installer (recommended) |
|
||||
| `Dibby Wemo Manager 2.0.0.exe` | Windows | Portable executable |
|
||||
| `Dibby Wemo Manager-2.0.0.AppImage` | Linux x64 | Universal AppImage |
|
||||
| `dibby-wemo-manager_2.0.0_amd64.deb` | Linux x64 | Debian / Ubuntu package |
|
||||
| `dibby-wemo-manager-2.0.0.x86_64.rpm` | Linux x64 | Fedora / RHEL package |
|
||||
| `Dibby Wemo Manager-2.0.0-arm64.AppImage` | Linux ARM64 | Raspberry Pi 4/5 AppImage |
|
||||
| `dibby-wemo-manager_2.0.0_arm64.deb` | Linux ARM64 | Raspberry Pi OS package |
|
||||
| `homebridge-dibby-wemo-1.0.0.tgz` | Any | Homebridge plugin npm package |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"appId": "com.dibby.wemo",
|
||||
"appName": "Dibby Wemo",
|
||||
"webDir": "www",
|
||||
"server": {
|
||||
"androidScheme": "https"
|
||||
},
|
||||
"android": {
|
||||
"allowMixedContent": true,
|
||||
"captureInput": true,
|
||||
"webContentsDebuggingEnabled": false
|
||||
},
|
||||
"plugins": {
|
||||
"SplashScreen": {
|
||||
"launchAutoHide": true,
|
||||
"launchShowDuration": 1000,
|
||||
"backgroundColor": "#0d1b27",
|
||||
"androidSplashResourceName": "splash",
|
||||
"showSpinner": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "dibby-wemo-android",
|
||||
"version": "2.0.0",
|
||||
"description": "Dibby Wemo Manager – Android companion app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"sync": "cap sync",
|
||||
"open": "cap open android",
|
||||
"build:apk": "cap sync && cd android && ./gradlew assembleDebug",
|
||||
"build:release": "cap sync && cd android && ./gradlew assembleRelease"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.0.0",
|
||||
"@capacitor/core": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^6.0.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+104
-27
@@ -1,8 +1,10 @@
|
||||
# Dibby Wemo Manager — Desktop App
|
||||
|
||||
**Windows desktop application for local Belkin Wemo control.**
|
||||
**Cross-platform desktop application for local Belkin Wemo control.**
|
||||
|
||||
Full device dashboard, power control, scheduling engine, Windows background service, and optional web remote — all communicating directly with your Wemo devices over your local network. No Belkin cloud account required.
|
||||
Full device dashboard, power control, scheduling engine, and optional web remote — all communicating directly with your Wemo devices over your local network. No Belkin cloud account required.
|
||||
|
||||
On Windows the scheduler can run as a background service so rules keep firing after the GUI is closed. Linux uses a background process instead.
|
||||
|
||||
---
|
||||
|
||||
@@ -10,6 +12,8 @@ Full device dashboard, power control, scheduling engine, Windows background serv
|
||||
|
||||
Download the latest release from [GitHub Releases](../../releases):
|
||||
|
||||
### Windows
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `Dibby Wemo Manager Setup 2.0.0.exe` | **NSIS installer** — recommended, installs to Program Files, adds Start Menu shortcut |
|
||||
@@ -17,6 +21,32 @@ Download the latest release from [GitHub Releases](../../releases):
|
||||
|
||||
Run the installer or portable exe. The app opens and immediately begins discovering Wemo devices on your network.
|
||||
|
||||
### Linux
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `Dibby Wemo Manager-2.0.0.AppImage` | **AppImage** — universal, runs on any modern Linux distro. No install needed. |
|
||||
| `dibby-wemo-manager_2.0.0_amd64.deb` | **Debian / Ubuntu** package |
|
||||
| `dibby-wemo-manager-2.0.0.x86_64.rpm` | **Fedora / RHEL / openSUSE** package |
|
||||
| `Dibby Wemo Manager-2.0.0-arm64.AppImage` | **AppImage (ARM64)** — Raspberry Pi 4/5, Apple Silicon VMs |
|
||||
| `dibby-wemo-manager_2.0.0_arm64.deb` | **Debian ARM64** — Raspberry Pi OS |
|
||||
|
||||
**AppImage:**
|
||||
```bash
|
||||
chmod +x "Dibby Wemo Manager-2.0.0.AppImage"
|
||||
./"Dibby Wemo Manager-2.0.0.AppImage"
|
||||
```
|
||||
|
||||
**Debian / Ubuntu (.deb):**
|
||||
```bash
|
||||
sudo dpkg -i dibby-wemo-manager_2.0.0_amd64.deb
|
||||
```
|
||||
|
||||
**Fedora / RHEL (.rpm):**
|
||||
```bash
|
||||
sudo rpm -i dibby-wemo-manager-2.0.0.x86_64.rpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
@@ -57,39 +87,48 @@ Read and manage rules stored directly on the Wemo device's own firmware:
|
||||
|
||||
> Wemo Dimmer V2 (WDS060) with newer RTOS firmware does not support firmware rule editing.
|
||||
|
||||
### 🛠️ Windows Background Service
|
||||
|
||||
The DWM scheduler can run as a **Windows service** (`DibbyWemoService`) so rules continue to fire even when the GUI is closed or the user logs out.
|
||||
|
||||
- Install/uninstall the service from the app's System tab
|
||||
- The service reads rules from the shared data directory and syncs automatically when rules are saved in the GUI
|
||||
- Service uses `node-windows` for reliable Windows service registration
|
||||
|
||||
### 🌐 Web Remote
|
||||
|
||||
Optional local web interface accessible from any device on your network (phone, tablet, another PC):
|
||||
|
||||
- View device status
|
||||
- Toggle devices on/off
|
||||
- Manage DWM rules
|
||||
- QR code for easy mobile access
|
||||
- Configurable port; firewall rule created automatically (UAC prompt)
|
||||
- Configurable port; firewall rule created automatically on Windows (UAC prompt)
|
||||
|
||||
### 📍 Sunrise/Sunset Scheduling
|
||||
|
||||
Set your city in the Settings tab. Schedule rules can then use local sunrise and sunset times as start/end points.
|
||||
|
||||
### 🛠️ Background Scheduler
|
||||
|
||||
The DWM scheduler continues running rules even when the main window is closed.
|
||||
|
||||
**Windows** — installs as a native **Windows Service** (`DibbyWemoService`) via `node-windows`:
|
||||
- Install/uninstall from the System tab
|
||||
- The service reads rules from `C:\ProgramData\DibbyWemoManager\dwm-rules.json`
|
||||
- Syncs automatically when rules are saved in the GUI
|
||||
|
||||
**Linux** — the scheduler runs as a background process spawned from the main Electron process. It continues running while the app is in the system tray.
|
||||
|
||||
---
|
||||
|
||||
## Data Storage
|
||||
|
||||
All app data is stored in `%APPDATA%\DibbyWemoManager\` (typically `C:\Users\<you>\AppData\Roaming\DibbyWemoManager\`):
|
||||
All app data is stored in the OS user-data directory:
|
||||
|
||||
| OS | Path |
|
||||
|---|---|
|
||||
| Windows | `%APPDATA%\DibbyWemoManager\` |
|
||||
| Linux | `~/.config/DibbyWemoManager/` |
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `wemo-manager.json` | App settings, discovered devices, DWM rules |
|
||||
| `dwm-rules.json` | DWM rules shared with the Windows background service |
|
||||
| `dwm-rules.json` | DWM rules shared with the background scheduler |
|
||||
|
||||
The standalone service reads `C:\ProgramData\DibbyWemoManager\dwm-rules.json`. The GUI syncs rules to this location after every create, update, or delete.
|
||||
On Windows the standalone service reads `C:\ProgramData\DibbyWemoManager\dwm-rules.json`. The GUI syncs rules to this location after every create, update, or delete.
|
||||
|
||||
---
|
||||
|
||||
@@ -102,7 +141,7 @@ Electron Main Process
|
||||
├── store.js — JSON persistence layer
|
||||
├── firewall.js — Windows Firewall rule management (elevated)
|
||||
├── web-server.js — Express web remote server
|
||||
├── service-manager.js— node-windows service install/uninstall
|
||||
├── service-manager.js— node-windows service install/uninstall (Windows)
|
||||
└── ipc/
|
||||
├── devices.ipc.js
|
||||
├── rules.ipc.js
|
||||
@@ -116,7 +155,7 @@ Electron Renderer (React 18 + Zustand)
|
||||
├── AllRulesTab — Native firmware rules per device
|
||||
└── Settings — location, service, web remote config
|
||||
|
||||
Standalone Service (scheduler-standalone.js)
|
||||
Standalone Scheduler (scheduler-standalone.js)
|
||||
└── Runs headless; reads dwm-rules.json; same scheduling logic
|
||||
```
|
||||
|
||||
@@ -141,7 +180,7 @@ Native firmware rules are stored in a SQLite database (`temppluginRules.db`) ins
|
||||
|
||||
- Node.js ≥ 18
|
||||
- npm ≥ 9
|
||||
- Windows (for Windows builds)
|
||||
- OS-specific toolchain (see below)
|
||||
|
||||
### Install dependencies
|
||||
|
||||
@@ -163,29 +202,67 @@ npm run dev
|
||||
|
||||
Opens the Electron app with hot-reload for the renderer.
|
||||
|
||||
### Production build
|
||||
### Production builds
|
||||
|
||||
#### Windows (NSIS installer + portable exe)
|
||||
|
||||
Run on a Windows machine:
|
||||
|
||||
```bash
|
||||
cd apps/desktop
|
||||
npm run build:win
|
||||
```
|
||||
|
||||
This:
|
||||
1. Compiles the renderer with `electron-vite`
|
||||
2. Bundles the standalone service script
|
||||
3. Runs `electron-builder` to produce the NSIS installer and portable exe
|
||||
Output in `apps/desktop/dist/`:
|
||||
- `Dibby Wemo Manager Setup 2.0.0.exe` — NSIS installer
|
||||
- `Dibby Wemo Manager 2.0.0.exe` — portable exe
|
||||
|
||||
Output appears in `apps/desktop/dist/`.
|
||||
> **Code signing:** The build config expects a PFX certificate at `resources/srsit-codesign.pfx`. Remove the `win.certificateFile` entry from `package.json` if you don't have a certificate.
|
||||
|
||||
> **Code signing:** The build configuration expects a PFX certificate at `resources/srsit-codesign.pfx`. Remove the `win.certificateFile` entry from `package.json` if you don't have a certificate.
|
||||
#### Linux x64 (AppImage + .deb + .rpm)
|
||||
|
||||
Run on a Linux machine or in WSL2 / CI:
|
||||
|
||||
```bash
|
||||
cd apps/desktop
|
||||
npm run build:linux
|
||||
```
|
||||
|
||||
Output in `apps/desktop/dist/`:
|
||||
- `Dibby Wemo Manager-2.0.0.AppImage`
|
||||
- `dibby-wemo-manager_2.0.0_amd64.deb`
|
||||
- `dibby-wemo-manager-2.0.0.x86_64.rpm`
|
||||
|
||||
#### Linux ARM64 (Raspberry Pi / Apple Silicon)
|
||||
|
||||
```bash
|
||||
cd apps/desktop
|
||||
npm run build:linux:arm64
|
||||
```
|
||||
|
||||
Output in `apps/desktop/dist/`:
|
||||
- `Dibby Wemo Manager-2.0.0-arm64.AppImage`
|
||||
- `dibby-wemo-manager_2.0.0_arm64.deb`
|
||||
|
||||
#### All targets at once
|
||||
|
||||
```bash
|
||||
cd apps/desktop
|
||||
npm run build:all
|
||||
```
|
||||
|
||||
Builds Windows x64 and Linux x64 targets in sequence. Requires the build host to have `wine` installed (for Windows cross-compilation on Linux), or run each command on its native OS.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows 10 or later (x64)
|
||||
- Node.js ≥ 18 (only needed for building from source)
|
||||
- Wemo devices on the same LAN
|
||||
| Component | Windows | Linux |
|
||||
|---|---|---|
|
||||
| OS | Windows 10 or later (x64) | Any modern distro (x64 or ARM64) |
|
||||
| Node.js | ≥ 18 (build only) | ≥ 18 (build only) |
|
||||
| Runtime deps | None — bundled | `libgtk-3`, `libnss3`, `libxss1` (auto via .deb) |
|
||||
| Wemo devices | Same LAN | Same LAN |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"private": true,
|
||||
"description": "Belkin Wemo device manager – local control, no cloud required",
|
||||
"author": "SRS IT",
|
||||
"homepage": "https://github.com/K0rb3nD4ll4S/dibby-wemo-manager",
|
||||
"main": "out/main/index.js",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
@@ -18,13 +19,15 @@
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.0",
|
||||
"node-windows": "^1.0.0-beta.8",
|
||||
"sql.js": "^1.12.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"ws": "^8.18.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^4.0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-windows": "^1.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"electron": "33.4.11",
|
||||
@@ -38,9 +41,20 @@
|
||||
"build": {
|
||||
"appId": "com.srsit.dibbywemomanager",
|
||||
"productName": "Dibby Wemo Manager",
|
||||
"npmRebuild": false,
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"mac": {
|
||||
"target": [
|
||||
{ "target": "dmg", "arch": ["x64", "arm64"] }
|
||||
],
|
||||
"icon": "resources/icon.png",
|
||||
"category": "public.app-category.utilities"
|
||||
},
|
||||
"dmg": {
|
||||
"title": "Dibby Wemo Manager"
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
@@ -69,11 +83,7 @@
|
||||
"createDesktopShortcut": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{ "target": "AppImage", "arch": ["x64"] },
|
||||
{ "target": "deb", "arch": ["x64"] },
|
||||
{ "target": "rpm", "arch": ["x64"] }
|
||||
],
|
||||
"target": ["AppImage", "deb", "rpm"],
|
||||
"icon": "resources/icon.png",
|
||||
"category": "Utility",
|
||||
"synopsis": "Belkin Wemo device manager — local control, no cloud required",
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<img src="icon.png" alt="Dibby Wemo Manager" />
|
||||
<div class="header-text">
|
||||
<h1>Dibby Wemo Manager</h1>
|
||||
<div class="version">Version 2.0</div>
|
||||
<div class="version">Version 2.0.0</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="desc">
|
||||
|
||||
@@ -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,6 +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="refreshDeviceStates()">⟳ Refresh</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="openAddDeviceModal()">+ Add IP</button>
|
||||
<button class="btn btn-primary" id="btn-discover" onclick="discoverDevices()">⟳ Scan</button>
|
||||
</div>
|
||||
<div id="page-header-devices" class="page-header">
|
||||
@@ -257,7 +316,13 @@
|
||||
<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;">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>
|
||||
@@ -436,6 +501,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 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 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="add-device-host" type="text" placeholder="192.168.1.100" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<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="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="closeAddDeviceModal()">Cancel</button>
|
||||
<button class="btn btn-primary" id="btn-add-device" onclick="addDeviceManually()">Add Device</button>
|
||||
</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 ── -->
|
||||
<div class="confirm-backdrop" id="confirm-delete">
|
||||
<div class="confirm-box">
|
||||
@@ -495,23 +606,54 @@ async function api(method, path, body) {
|
||||
// ── WebSocket ──────────────────────────────────────────────────────────────
|
||||
function connectWS() {
|
||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${proto}//${location.host}`);
|
||||
const wsUrl = `${proto}//${location.host}`;
|
||||
|
||||
console.log(`[DWM] Connecting WebSocket to: ${wsUrl}`);
|
||||
|
||||
try {
|
||||
ws = new WebSocket(wsUrl);
|
||||
} catch (err) {
|
||||
console.error('[DWM] WebSocket creation failed:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[DWM] WebSocket connected');
|
||||
document.querySelectorAll('.ws-dot').forEach((d) => d.classList.add('connected'));
|
||||
document.querySelectorAll('[id^="ws-label"]').forEach((l) => l.textContent = 'Connected');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[DWM] WebSocket disconnected, reconnecting...');
|
||||
document.querySelectorAll('.ws-dot').forEach((d) => d.classList.remove('connected'));
|
||||
document.querySelectorAll('[id^="ws-label"]').forEach((l) => l.textContent = 'Reconnecting…');
|
||||
setTimeout(connectWS, 4000);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[DWM] WebSocket error:', error);
|
||||
document.querySelectorAll('.ws-dot').forEach((d) => d.classList.remove('connected'));
|
||||
document.querySelectorAll('[id^="ws-label"]').forEach((l) => l.textContent = 'Connection Error');
|
||||
|
||||
// Show user-friendly error message
|
||||
if (error.type === 'error' && error.message.includes('SSL')) {
|
||||
toast('WebSocket SSL Error: Try accessing via domain name or check certificate', 'error');
|
||||
} else if (error.message) {
|
||||
toast(`WebSocket Error: ${error.message}`, 'error');
|
||||
} else {
|
||||
toast('WebSocket connection failed - try refreshing the page', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
const { type, data } = JSON.parse(e.data);
|
||||
if (type === 'scheduler-fired') appendLog(data);
|
||||
if (type === 'scheduler-status') applySchedStatus(data);
|
||||
if (type === 'scheduler-health') appendLog({ success: data.online, msg: `[health] ${data.msg}` });
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error('[DWM] WebSocket message parse error:', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -559,32 +701,125 @@ function renderDevices() {
|
||||
el.innerHTML = `<div class="empty"><div class="icon">📡</div><p>No devices found.<br>Tap <strong>Scan</strong> to search.</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are WebSocket connection issues
|
||||
const wsConnected = ws && ws.readyState === WebSocket.OPEN;
|
||||
if (!wsConnected) {
|
||||
el.innerHTML = `<div class="empty"><div class="icon">⚠️</div><p>WebSocket connection issue detected.<br>Device states may not be accurate.<br>Try refreshing the page.</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
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>
|
||||
<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('');
|
||||
// fetch current state for each
|
||||
devices.forEach((d, i) => {
|
||||
|
||||
// fetch current state for each device
|
||||
devices.forEach(async (d, i) => {
|
||||
// Skip fetching state if WebSocket is not connected
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.log(`[DWM] Skipping device state fetch due to WebSocket connection issues`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// For dimmer devices, fetch both brightness and state
|
||||
if (d.isDimmer) {
|
||||
// Fetch both brightness and state in parallel to avoid race conditions
|
||||
const [brightnessData, stateData] = await Promise.allSettled([
|
||||
api('GET', `/api/devices/${d.host}/${d.port}/brightness`),
|
||||
api('GET', `/api/devices/${d.host}/${d.port}/state`)
|
||||
.then((on) => { const c = document.getElementById('dchk-'+i); if (c) c.checked = !!on; })
|
||||
.catch(() => {});
|
||||
]);
|
||||
|
||||
// Handle brightness data
|
||||
if (brightnessData.status === 'fulfilled' && brightnessData.value.brightness !== undefined) {
|
||||
const slider = document.getElementById('bright-'+i);
|
||||
const value = document.getElementById('bright-val-'+i);
|
||||
if (slider && value) {
|
||||
slider.value = brightnessData.value.brightness;
|
||||
value.textContent = Math.round(brightnessData.value.brightness) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle state data (this should set the toggle correctly)
|
||||
if (stateData.status === 'fulfilled') {
|
||||
const checkbox = document.getElementById('dchk-'+i);
|
||||
if (checkbox) {
|
||||
// stateData.value is an object {on: false}, need to access the 'on' property
|
||||
const actualState = !!stateData.value.on;
|
||||
checkbox.checked = actualState;
|
||||
console.log(`[DWM] Device ${d.host}:${d.port} - BinaryState: ${actualState ? 'ON' : 'OFF'}, Last brightness: ${brightnessData.value?.brightness || 'N/A'}%`);
|
||||
console.log(`[DWM] Setting checkbox.checked to: ${actualState}`);
|
||||
console.log(`[DWM] Raw stateData.value:`, stateData.value);
|
||||
|
||||
// Force a DOM update to ensure the change takes effect
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For non-dimmer devices, just fetch binary state
|
||||
const stateResult = await api('GET', `/api/devices/${d.host}/${d.port}/state`);
|
||||
const checkbox = document.getElementById('dchk-'+i);
|
||||
if (checkbox) {
|
||||
const shouldBeChecked = !!stateResult;
|
||||
checkbox.checked = shouldBeChecked;
|
||||
console.log(`[DWM] Non-dimmer device ${d.host}:${d.port} - State: ${stateResult ? 'ON' : 'OFF'}, Setting checkbox to: ${shouldBeChecked}`);
|
||||
|
||||
// Force a DOM update to ensure the change takes effect
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[DWM] Error fetching state for device ${d.host}:${d.port}:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Refresh Device States ───────────────────────────────────────────────────
|
||||
async function refreshDeviceStates() {
|
||||
if (!devices.length) return;
|
||||
|
||||
// Show loading state
|
||||
toast('Refreshing device states...', 'info');
|
||||
|
||||
// Re-render devices to refresh their states
|
||||
renderDevices();
|
||||
|
||||
toast('Device states refreshed', 'success');
|
||||
}
|
||||
|
||||
async function toggleDevice(i, e) {
|
||||
e.preventDefault();
|
||||
const dev = devices[i];
|
||||
@@ -603,6 +838,37 @@ async function toggleDevice(i, e) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Brightness control ───────────────────────────────────────────────────────
|
||||
function updateBrightnessPreview(i, value) {
|
||||
const valueEl = document.getElementById('bright-val-' + i);
|
||||
if (valueEl) {
|
||||
valueEl.textContent = Math.round(value) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
async function setBrightness(i, value) {
|
||||
const dev = devices[i];
|
||||
const slider = document.getElementById('bright-' + i);
|
||||
if (!dev || !slider) return;
|
||||
|
||||
slider.disabled = true;
|
||||
try {
|
||||
await api('POST', `/api/devices/${dev.host}/${dev.port}/brightness`, { brightness: Number(value) });
|
||||
|
||||
// Update power toggle state based on brightness
|
||||
const chk = document.getElementById('dchk-' + i);
|
||||
if (chk) {
|
||||
chk.checked = Number(value) > 0;
|
||||
}
|
||||
|
||||
toast(`Brightness set to ${Math.round(value)}%`, 'success');
|
||||
} catch (err) {
|
||||
toast(`Failed to set brightness: ${err.message}`, 'error');
|
||||
} finally {
|
||||
slider.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rules list ─────────────────────────────────────────────────────────────
|
||||
async function loadRules() {
|
||||
try {
|
||||
@@ -842,6 +1108,130 @@ 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';
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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';
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
const wemo = require('./wemo');
|
||||
const store = require('./store');
|
||||
const { sunTimes: calcSunTimes } = require('./core/sun');
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -61,6 +62,8 @@ class LocalScheduler {
|
||||
this._onHealth = null; // ({host, port, name, online, msg}) health event callback
|
||||
this._deviceHealth = new Map(); // 'host:port' → true | false
|
||||
this._triggerStates = new Map(); // 'host:port' → last known boolean state (for Trigger rules)
|
||||
this._countdownStates = new Map(); // 'host:port' → last known boolean state (for Countdown rules)
|
||||
this._countdownTimers = new Map(); // 'deviceKey-ruleId' → {timer, wantOn}
|
||||
this._healthTimer = null;
|
||||
this._startedAt = null;
|
||||
}
|
||||
@@ -126,6 +129,29 @@ class LocalScheduler {
|
||||
const schedule = [];
|
||||
const rules = store.getDwmRules();
|
||||
|
||||
// Compute today's sunrise/sunset once — used to resolve sun-based start/end times
|
||||
const loc = store.getLocation();
|
||||
const todaySun = (loc?.lat != null && loc?.lng != null)
|
||||
? calcSunTimes(loc.lat, loc.lng)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* Convert a raw stored time value to seconds-from-midnight.
|
||||
* -2 = sunrise sentinel, -3 = sunset sentinel (set by RuleEditor.jsx saveDwm).
|
||||
* offsetMins is added/subtracted from the sun time (e.g. "30 min before sunset").
|
||||
* Returns null if the time cannot be resolved (no location, polar day/night, or no time set).
|
||||
*/
|
||||
const resolveSecs = (rawSecs, type, offsetMins) => {
|
||||
const offsetSecs = (offsetMins ?? 0) * 60;
|
||||
if (type === 'sunset' || rawSecs === -3) {
|
||||
return todaySun?.sunset != null ? todaySun.sunset + offsetSecs : null;
|
||||
}
|
||||
if (type === 'sunrise' || rawSecs === -2) {
|
||||
return todaySun?.sunrise != null ? todaySun.sunrise + offsetSecs : null;
|
||||
}
|
||||
return rawSecs >= 0 ? rawSecs : null;
|
||||
};
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.enabled) continue;
|
||||
|
||||
@@ -134,9 +160,12 @@ class LocalScheduler {
|
||||
|
||||
// ── Away Mode — handled by the randomisation loop, not pre-computed entries ──
|
||||
if (rule.type === 'Away') {
|
||||
const startSecs = Number(rule.startTime ?? -1);
|
||||
const endSecs = Number(rule.endTime ?? -1);
|
||||
if (startSecs < 0) continue; // sun-based — skip for now
|
||||
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset);
|
||||
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset);
|
||||
if (startSecs === null) continue; // no location set or polar day/night
|
||||
|
||||
const awayStartAction = Number(rule.startAction ?? 1);
|
||||
const awayEndAction = Number(rule.endAction ?? 0);
|
||||
|
||||
for (const dayId of (rule.days ?? [])) {
|
||||
const td0 = rule.targetDevices?.[0]; // for status display only
|
||||
@@ -148,11 +177,11 @@ class LocalScheduler {
|
||||
targetPort: td0?.port ?? 0,
|
||||
dayId: Number(dayId),
|
||||
targetSecs: startSecs,
|
||||
action: 1,
|
||||
action: awayStartAction,
|
||||
isAwayStart: true,
|
||||
});
|
||||
// Window-end entry: stops the away loop
|
||||
if (endSecs >= 0) {
|
||||
if (endSecs !== null && endSecs >= 0) {
|
||||
schedule.push({
|
||||
ruleId: rule.id + '-away-end',
|
||||
ruleName: rule.name,
|
||||
@@ -160,7 +189,7 @@ class LocalScheduler {
|
||||
targetPort: td0?.port ?? 0,
|
||||
dayId: Number(dayId),
|
||||
targetSecs: endSecs,
|
||||
action: 0,
|
||||
action: awayEndAction,
|
||||
isAwayEnd: true,
|
||||
awayRuleId: rule.id,
|
||||
});
|
||||
@@ -169,38 +198,15 @@ class LocalScheduler {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── Countdown with active window ─────────────────────────────────────
|
||||
if (rule.type === 'Countdown') {
|
||||
const windowStart = Number(rule.windowStart ?? -1);
|
||||
const windowEnd = Number(rule.windowEnd ?? -1);
|
||||
if (windowStart < 0 || !(rule.windowDays?.length)) continue;
|
||||
|
||||
const crossesMidnight = windowEnd >= 0 && windowEnd < windowStart;
|
||||
|
||||
for (const dayId of rule.windowDays) {
|
||||
for (const td of (rule.targetDevices ?? [])) {
|
||||
if (!td.host || !td.port) continue;
|
||||
schedule.push({ ruleId: rule.id, ruleName: rule.name,
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: Number(dayId), targetSecs: windowStart, action: 1 });
|
||||
|
||||
if (windowEnd >= 0) {
|
||||
const offDayId = crossesMidnight ? (Number(dayId) % 7) + 1 : Number(dayId);
|
||||
schedule.push({ ruleId: rule.id + '-wend', ruleName: rule.name,
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: offDayId, targetSecs: windowEnd, action: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// ── Countdown — handled by the health-monitor state-change poll ─────
|
||||
if (rule.type === 'Countdown') continue;
|
||||
|
||||
// ── Schedule / other time-based rules ────────────────────────────────
|
||||
const startSecs = Number(rule.startTime ?? -1);
|
||||
const endSecs = Number(rule.endTime ?? -1);
|
||||
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset);
|
||||
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset);
|
||||
const startAction = Number(rule.startAction ?? 1);
|
||||
const endAction = Number(rule.endAction ?? -1);
|
||||
if (startSecs < 0) continue;
|
||||
if (startSecs === null) continue; // no location set, polar day/night, or no time defined
|
||||
|
||||
for (const dayId of (rule.days ?? [])) {
|
||||
for (const td of (rule.targetDevices ?? [])) {
|
||||
@@ -210,7 +216,7 @@ class LocalScheduler {
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: Number(dayId), targetSecs: startSecs, action: startAction });
|
||||
}
|
||||
if (endSecs > 0 && endAction >= 0) {
|
||||
if (endSecs !== null && endSecs > 0 && endAction >= 0) {
|
||||
schedule.push({ ruleId: rule.id, ruleName: rule.name,
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: Number(dayId), targetSecs: endSecs, action: endAction });
|
||||
@@ -330,13 +336,15 @@ class LocalScheduler {
|
||||
this._awayLoops.delete(ruleId);
|
||||
|
||||
if (forceOff) {
|
||||
const endAction = Number(loop.rule.endAction ?? 0);
|
||||
const turnOn = endAction === 1;
|
||||
for (const td of loop.devices) {
|
||||
wemo.setBinaryState(td.host, td.port, false).catch(() => {});
|
||||
wemo.setBinaryState(td.host, td.port, turnOn).catch(() => {});
|
||||
}
|
||||
this._onFire?.({
|
||||
success: true,
|
||||
msg: `"${loop.rule.name}" Away Mode window ended — all devices OFF`,
|
||||
entry: { action: 0 },
|
||||
msg: `"${loop.rule.name}" Away Mode window ended — all devices ${turnOn ? 'ON' : 'OFF'}`,
|
||||
entry: { action: endAction },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -431,6 +439,7 @@ class LocalScheduler {
|
||||
const allRules = store.getDwmRules();
|
||||
const alwaysOnSet = new Set(); // keys with an active AlwaysOn rule
|
||||
const triggerSrcSet = new Set(); // keys that are trigger source devices
|
||||
const countdownDevMap = new Map(); // deviceKey → [{rule, td}]
|
||||
|
||||
const addDev = (td) => {
|
||||
if (!td?.host || !td?.port) return;
|
||||
@@ -450,7 +459,12 @@ class LocalScheduler {
|
||||
}
|
||||
for (const td of (rule.targetDevices ?? [])) {
|
||||
const k = addDev(td);
|
||||
if (k && rule.type === 'AlwaysOn') alwaysOnSet.add(k);
|
||||
if (!k) continue;
|
||||
if (rule.type === 'AlwaysOn') alwaysOnSet.add(k);
|
||||
if (rule.type === 'Countdown') {
|
||||
if (!countdownDevMap.has(k)) countdownDevMap.set(k, []);
|
||||
countdownDevMap.get(k).push({ rule, td });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +508,60 @@ class LocalScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Countdown — fire only when state matches condition and within window ──
|
||||
if (countdownDevMap.has(key)) {
|
||||
const prevState = this._countdownStates.get(key);
|
||||
this._countdownStates.set(key, isOn);
|
||||
if (prevState !== undefined && prevState !== isOn) {
|
||||
const nowSecs = secondsFromMidnight(new Date());
|
||||
for (const { rule, td } of countdownDevMap.get(key)) {
|
||||
const condition = rule.countdownAction ?? 'on_to_off';
|
||||
const triggered = condition === 'on_to_off' ? isOn : !isOn;
|
||||
if (!triggered) continue;
|
||||
|
||||
// Check active window (if defined)
|
||||
const winStart = Number(rule.windowStart ?? -1);
|
||||
const winEnd = Number(rule.windowEnd ?? -1);
|
||||
if (winStart >= 0 && winEnd >= 0) {
|
||||
const crossesMidnight = winEnd < winStart;
|
||||
const inWindow = crossesMidnight
|
||||
? (nowSecs >= winStart || nowSecs <= winEnd)
|
||||
: (nowSecs >= winStart && nowSecs <= winEnd);
|
||||
if (!inWindow) continue;
|
||||
} else if (winStart >= 0) {
|
||||
if (nowSecs < winStart) continue;
|
||||
}
|
||||
|
||||
const timerKey = `${key}-${rule.id}`;
|
||||
const existing = this._countdownTimers.get(timerKey);
|
||||
if (existing) { clearTimeout(existing.timer); this._countdownTimers.delete(timerKey); }
|
||||
|
||||
const wantOn = condition === 'off_to_on';
|
||||
const durationMs = (Number(rule.countdownTime) || 60) * 1000;
|
||||
const label = wantOn ? 'ON' : 'OFF';
|
||||
const mins = Math.round(durationMs / 60000);
|
||||
this._onFire?.({ success: true,
|
||||
msg: `"${rule.name}" countdown started — will turn ${label} in ${mins} min (${td.host})`,
|
||||
entry: { action: wantOn ? 1 : 0 } });
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
this._countdownTimers.delete(timerKey);
|
||||
try {
|
||||
await wemo.setBinaryState(td.host, td.port, wantOn);
|
||||
this._onFire?.({ success: true,
|
||||
msg: `"${rule.name}" countdown elapsed → ${label} (${td.host}) ✓`,
|
||||
entry: { action: wantOn ? 1 : 0 } });
|
||||
} catch (e2) {
|
||||
this._onFire?.({ success: false,
|
||||
msg: `"${rule.name}" countdown elapsed → ${label} FAILED: ${e2.message}`,
|
||||
entry: { action: wantOn ? 1 : 0 } });
|
||||
}
|
||||
}, durationMs);
|
||||
this._countdownTimers.set(timerKey, { timer, wantOn });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this._deviceHealth.set(key, false);
|
||||
if (wasOnline !== false) {
|
||||
@@ -602,6 +670,8 @@ class LocalScheduler {
|
||||
for (const t of this._timers) clearTimeout(t);
|
||||
this._timers = [];
|
||||
if (this._tickTimer) { clearTimeout(this._tickTimer); this._tickTimer = null; }
|
||||
for (const { timer } of this._countdownTimers.values()) clearTimeout(timer);
|
||||
this._countdownTimers.clear();
|
||||
}
|
||||
|
||||
_scheduleUpcoming() {
|
||||
|
||||
@@ -78,6 +78,7 @@ function dwmRuleToForm(rule, defaultDeviceUdn) {
|
||||
endAction: rule.endAction ?? -1,
|
||||
countdownMins: rule.countdownTime ? Math.round(rule.countdownTime / 60) : 60,
|
||||
countdownTime: rule.countdownTime ?? 3600,
|
||||
countdownAction: rule.countdownAction ?? 'on_to_off',
|
||||
// Countdown active window
|
||||
windowEnabled: rule.windowStart >= 0 && rule.windowStart != null,
|
||||
windowStartTime: rule.windowStart >= 0 ? secsToHHMM(rule.windowStart) : '',
|
||||
@@ -445,6 +446,7 @@ export default function RuleEditor({ rule, device, isDwm = false, onSave, onClos
|
||||
startOffset: form.startOffset ?? 0,
|
||||
endOffset: form.endOffset ?? 0,
|
||||
countdownTime: form.countdownTime ?? 3600,
|
||||
countdownAction: form.countdownAction ?? 'on_to_off',
|
||||
windowStart,
|
||||
windowEnd,
|
||||
windowDays,
|
||||
|
||||
@@ -126,6 +126,30 @@ export default function AwayModeEditor({ form, onChange, sunTimes }) {
|
||||
configured window. The DWM scheduler handles all randomisation while the app is running.
|
||||
</div>
|
||||
|
||||
{/* Start / End action */}
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 4 }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label>Window Start Action</label>
|
||||
<select
|
||||
value={form.startAction ?? 1}
|
||||
onChange={(e) => onChange({ ...form, startAction: Number(e.target.value) })}
|
||||
>
|
||||
<option value={1}>Turn ON</option>
|
||||
<option value={0}>Turn OFF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label>Window End Action</label>
|
||||
<select
|
||||
value={form.endAction ?? 0}
|
||||
onChange={(e) => onChange({ ...form, endAction: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0}>Turn OFF</option>
|
||||
<option value={1}>Turn ON</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active days */}
|
||||
<div className="form-group">
|
||||
<label>Active Days</label>
|
||||
|
||||
@@ -3,6 +3,7 @@ import DayPicker from '../DayPicker';
|
||||
|
||||
export default function CountdownEditor({ form, onChange }) {
|
||||
const mins = form.countdownMins ?? 60;
|
||||
const countdownAction = form.countdownAction ?? 'on_to_off';
|
||||
const windowEnabled = form.windowEnabled ?? false;
|
||||
const windowStartTime = form.windowStartTime ?? '';
|
||||
const windowEndTime = form.windowEndTime ?? '';
|
||||
@@ -18,14 +19,26 @@ export default function CountdownEditor({ form, onChange }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="notice notice-info">
|
||||
The device automatically turns off after the countdown completes.
|
||||
The countdown starts when the device is manually turned on (or at window start, if an active window is set below).
|
||||
{/* Condition */}
|
||||
<div className="form-group">
|
||||
<label>Condition</label>
|
||||
<select
|
||||
value={countdownAction}
|
||||
onChange={(e) => onChange({ ...form, countdownAction: e.target.value })}
|
||||
>
|
||||
<option value="on_to_off">If device turns ON → auto-OFF after duration</option>
|
||||
<option value="off_to_on">If device turns OFF → auto-ON after duration</option>
|
||||
</select>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)', marginTop: 4 }}>
|
||||
{countdownAction === 'on_to_off'
|
||||
? 'When the device is turned ON, it will automatically turn OFF after the countdown.'
|
||||
: 'When the device is turned OFF, it will automatically turn ON after the countdown.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Countdown duration */}
|
||||
<div className="form-group">
|
||||
<label>Turn off after (minutes)</label>
|
||||
<label>Duration (minutes)</label>
|
||||
<input
|
||||
type="number" min="1" max="1440"
|
||||
value={mins}
|
||||
@@ -59,9 +72,8 @@ export default function CountdownEditor({ form, onChange }) {
|
||||
{windowEnabled && (
|
||||
<>
|
||||
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12 }}>
|
||||
The scheduler will turn the device <strong>ON</strong> at the window start and
|
||||
<strong> OFF</strong> at the window end. The countdown auto-off fires in between.
|
||||
Use this to prevent the timer rule conflicting with other rules outside these hours.
|
||||
The countdown only activates when the device state changes within this time window.
|
||||
State changes outside the window are ignored.
|
||||
</p>
|
||||
|
||||
{/* Window times */}
|
||||
@@ -88,7 +100,6 @@ export default function CountdownEditor({ form, onChange }) {
|
||||
{windowStartTime && windowEndTime && crossesMidnight() && (
|
||||
<div className="notice notice-info" style={{ marginBottom: 12, fontSize: 12 }}>
|
||||
🌙 Window crosses midnight — ends at <strong>{windowEndTime}</strong> the <strong>next day</strong>.
|
||||
The OFF command fires on the following calendar day.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
dibbly-web:
|
||||
image: reg.dev.nervesocket.com/dibbly:latest
|
||||
container_name: dibbly-wemo-manager-prod
|
||||
restart: always
|
||||
ports:
|
||||
- "3456:3456"
|
||||
volumes:
|
||||
- dibbly-data:/data
|
||||
- dibbly-logs:/app/logs
|
||||
environment:
|
||||
- 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
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
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
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "dibby-wemo-docker",
|
||||
"version": "2.0.0",
|
||||
"description": "Dibby Wemo Manager — headless scheduler + web remote",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.0",
|
||||
"sql.js": "^1.12.0",
|
||||
"ws": "^8.18.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^4.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dibby Wemo Manager — Docker entry point
|
||||
*
|
||||
* Runs the DWM scheduler + REST/WebSocket API server without Electron.
|
||||
* Configure via environment variables:
|
||||
* DATA_DIR — path to persistent data directory (default: /data)
|
||||
* PORT — HTTP port (default: 3456)
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
const DwmStore = require('./lib/store');
|
||||
const DwmScheduler = require('./lib/scheduler');
|
||||
const wemo = require('./lib/wemo-client');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||
const PORT = parseInt(process.env.PORT || '3456', 10);
|
||||
const WEB_DIR = path.join(__dirname, 'web');
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const store = new DwmStore(DATA_DIR);
|
||||
const scheduler = new DwmScheduler({ store, wemoClient: wemo, log: console });
|
||||
|
||||
let _wss = null;
|
||||
|
||||
function broadcast(type, data) {
|
||||
if (!_wss) return;
|
||||
const msg = JSON.stringify({ type, data });
|
||||
for (const client of _wss.clients) {
|
||||
if (client.readyState === 1 /* OPEN */) client.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function getLocalIP() {
|
||||
for (const iface of Object.values(os.networkInterfaces())) {
|
||||
for (const addr of iface) {
|
||||
if (addr.family === 'IPv4' && !addr.internal) return addr.address;
|
||||
}
|
||||
}
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
function json(res, data, status = 200) {
|
||||
const body = JSON.stringify(data);
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function jsonErr(res, msg, status = 500) {
|
||||
json(res, { error: msg }, status);
|
||||
}
|
||||
|
||||
// ── Request router ────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
const url = req.url.split('?')[0];
|
||||
const method = req.method.toUpperCase();
|
||||
|
||||
if (method === 'OPTIONS') {
|
||||
res.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await new Promise((resolve) => {
|
||||
if (method !== 'POST' && method !== 'PUT') return resolve({});
|
||||
let raw = '';
|
||||
req.on('data', (c) => { raw += c; });
|
||||
req.on('end', () => { try { resolve(JSON.parse(raw)); } catch { resolve({}); } });
|
||||
});
|
||||
|
||||
try {
|
||||
// ── Debug endpoint ─────────────────────────────────────────────────────
|
||||
|
||||
if (url === '/api/debug/test-device' && method === 'POST') {
|
||||
const { host, port } = body;
|
||||
if (!host) {
|
||||
return jsonErr(res, 'Host is required', 400);
|
||||
}
|
||||
|
||||
const devicePort = port ? parseInt(port, 10) : 49153;
|
||||
|
||||
console.log(`[DWM] Debug test for ${host}:${devicePort}`);
|
||||
|
||||
const results = {};
|
||||
|
||||
// Test 1: HTTP connection to setup.xml
|
||||
try {
|
||||
const setupUrl = `http://${host}:${devicePort}/setup.xml`;
|
||||
console.log(`[DWM] Testing setup.xml URL: ${setupUrl}`);
|
||||
const response = await axios.get(setupUrl, {
|
||||
timeout: 5000,
|
||||
httpAgent: NO_KEEPALIVE
|
||||
});
|
||||
results.setupXml = {
|
||||
status: response.status,
|
||||
dataLength: response.data.length,
|
||||
success: response.status === 200
|
||||
};
|
||||
console.log(`[DWM] setup.xml response:`, results.setupXml);
|
||||
} catch (err) {
|
||||
results.setupXml = {
|
||||
success: false,
|
||||
error: err.message,
|
||||
code: err.code
|
||||
};
|
||||
console.log(`[DWM] setup.xml failed:`, err.message);
|
||||
}
|
||||
|
||||
// Test 2: Try to get device info
|
||||
try {
|
||||
console.log(`[DWM] Testing getDeviceInfo`);
|
||||
const deviceInfo = await wemo.getDeviceInfo(host, devicePort);
|
||||
results.deviceInfo = {
|
||||
success: true,
|
||||
data: deviceInfo
|
||||
};
|
||||
console.log(`[DWM] getDeviceInfo successful:`, deviceInfo);
|
||||
} catch (err) {
|
||||
results.deviceInfo = {
|
||||
success: false,
|
||||
error: err.message
|
||||
};
|
||||
console.log(`[DWM] getDeviceInfo failed:`, err.message);
|
||||
}
|
||||
|
||||
// Test 3: Try to get binary state
|
||||
try {
|
||||
console.log(`[DWM] Testing getBinaryState`);
|
||||
const state = await wemo.getBinaryState(host, devicePort);
|
||||
results.binaryState = {
|
||||
success: true,
|
||||
state: state
|
||||
};
|
||||
console.log(`[DWM] getBinaryState successful:`, state);
|
||||
} catch (err) {
|
||||
results.binaryState = {
|
||||
success: false,
|
||||
error: err.message
|
||||
};
|
||||
console.log(`[DWM] getBinaryState failed:`, err.message);
|
||||
}
|
||||
|
||||
return json(res, results);
|
||||
}
|
||||
|
||||
// ── Devices ────────────────────────────────────────────────────────────
|
||||
|
||||
if (url === '/api/devices' && method === 'GET') {
|
||||
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 }));
|
||||
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 };
|
||||
|
||||
console.log(`[DWM] Attempting to add device manually: ${host}:${devicePort}`);
|
||||
|
||||
try {
|
||||
// First try direct device info fetch
|
||||
let device = null;
|
||||
try {
|
||||
console.log(`[DWM] Trying direct device info fetch for ${host}:${devicePort}`);
|
||||
device = await wemo.getDeviceInfo(host, devicePort);
|
||||
if (device) {
|
||||
device.host = host;
|
||||
device.port = devicePort;
|
||||
device.manual = true; // Mark as manually added
|
||||
console.log(`[DWM] Direct fetch successful:`, device);
|
||||
}
|
||||
} catch (directErr) {
|
||||
console.log(`[DWM] Direct fetch failed:`, directErr.message);
|
||||
}
|
||||
|
||||
// If direct fetch failed, try discovery with manual entry
|
||||
if (!device) {
|
||||
console.log(`[DWM] Trying discovery with manual entry`);
|
||||
const devices = await wemo.discoverDevices(5000, [manualEntry]);
|
||||
if (devices.length > 0) {
|
||||
device = devices[0];
|
||||
device.manual = true; // Mark as manually added
|
||||
console.log(`[DWM] Discovery successful:`, device);
|
||||
}
|
||||
}
|
||||
|
||||
if (device) {
|
||||
// Add to existing devices
|
||||
const allDevices = [...store.getDevices(), ...[device]];
|
||||
store.saveDevices(allDevices);
|
||||
console.log(`[DWM] Device added successfully: ${device.friendlyName || device.host}`);
|
||||
return json(res, device, 201);
|
||||
} else {
|
||||
console.log(`[DWM] No device found at ${host}:${devicePort}`);
|
||||
return jsonErr(res, `No Wemo device found at ${host}:${devicePort}. Check IP address and port.`, 404);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[DWM] Error adding device:`, err);
|
||||
return jsonErr(res, `Failed to connect: ${err.message}`, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const stateMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/state$/);
|
||||
if (stateMatch) {
|
||||
const [, host, port] = stateMatch;
|
||||
if (method === 'GET') {
|
||||
const on = await wemo.getBinaryState(host, Number(port));
|
||||
return json(res, { on });
|
||||
}
|
||||
if (method === 'POST') {
|
||||
await wemo.setBinaryState(host, Number(port), !!body.on);
|
||||
return json(res, { ok: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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$/);
|
||||
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') {
|
||||
if (method === 'GET') return json(res, store.getDwmRules());
|
||||
if (method === 'POST') {
|
||||
const rule = store.createDwmRule(body);
|
||||
scheduler.reload();
|
||||
return json(res, rule, 201);
|
||||
}
|
||||
}
|
||||
|
||||
const ruleMatch = url.match(/^\/api\/dwm-rules\/(.+)$/);
|
||||
if (ruleMatch) {
|
||||
const id = ruleMatch[1];
|
||||
if (method === 'PUT') {
|
||||
const updated = store.updateDwmRule(id, body);
|
||||
scheduler.reload();
|
||||
return json(res, updated);
|
||||
}
|
||||
if (method === 'DELETE') {
|
||||
store.deleteDwmRule(id);
|
||||
scheduler.reload();
|
||||
return json(res, { ok: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wemo Device Rules ──────────────────────────────────────────────────
|
||||
|
||||
const wemoRulesMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/rules$/);
|
||||
if (wemoRulesMatch && method === 'GET') {
|
||||
const [, host, port] = wemoRulesMatch;
|
||||
return json(res, await wemo.fetchRules(host, Number(port)));
|
||||
}
|
||||
|
||||
const wemoRuleMatch = url.match(/^\/api\/devices\/([^/]+)\/(\d+)\/rules\/(\d+)$/);
|
||||
if (wemoRuleMatch && method === 'PUT') {
|
||||
const [, host, port, ruleId] = wemoRuleMatch;
|
||||
await wemo.updateRule(host, Number(port), Number(ruleId), body);
|
||||
return json(res, { ok: true });
|
||||
}
|
||||
|
||||
// ── Scheduler ──────────────────────────────────────────────────────────
|
||||
|
||||
if (url === '/api/scheduler/status' && method === 'GET') {
|
||||
return json(res, scheduler.getStatus());
|
||||
}
|
||||
|
||||
// ── Static assets ──────────────────────────────────────────────────────
|
||||
|
||||
if (url === '/icon.png' && method === 'GET') {
|
||||
const file = path.join(__dirname, 'icon.png');
|
||||
fs.readFile(file, (e, data) => {
|
||||
if (e) { res.writeHead(404); res.end(); return; }
|
||||
res.writeHead(200, { 'Content-Type': 'image/png' });
|
||||
res.end(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback → serve mobile web UI
|
||||
const file = path.join(WEB_DIR, 'index.html');
|
||||
fs.readFile(file, (e, data) => {
|
||||
if (e) { res.writeHead(404); res.end('Web UI not found'); return; }
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(data);
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
jsonErr(res, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
// Ensure data directory exists
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
await scheduler.start();
|
||||
console.log(`[DWM] Scheduler started — data: ${DATA_DIR}`);
|
||||
|
||||
const server = http.createServer(handleRequest);
|
||||
|
||||
let WebSocketServer;
|
||||
try { WebSocketServer = require('ws').WebSocketServer || require('ws').Server; } catch {}
|
||||
|
||||
if (WebSocketServer) {
|
||||
_wss = new WebSocketServer({ server });
|
||||
_wss.on('connection', (ws) => {
|
||||
ws.send(JSON.stringify({ type: 'scheduler-status', data: scheduler.getStatus() }));
|
||||
});
|
||||
scheduler.onFire = (event) => broadcast('scheduler-fired', event);
|
||||
scheduler.onStatus = (status) => broadcast('scheduler-status', status);
|
||||
}
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`[DWM] Web Remote: http://${getLocalIP()}:${PORT}`);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
||||
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error('[DWM] Fatal:', e); process.exit(1); });
|
||||
@@ -0,0 +1,189 @@
|
||||
# Web Deployment Guide
|
||||
|
||||
This guide covers deploying Dibby Wemo Manager as a web service using Docker containers.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
1. **Build and deploy with one command:**
|
||||
```bash
|
||||
# Linux/macOS
|
||||
./scripts/web-deploy.sh
|
||||
|
||||
# Windows
|
||||
powershell -ExecutionPolicy Bypass -File scripts/web-deploy.ps1
|
||||
```
|
||||
|
||||
2. **Or manually:**
|
||||
```bash
|
||||
# Build the Docker image
|
||||
docker build -t reg.dev.nervesocket.com/dibbly:latest .
|
||||
|
||||
# Start with Docker Compose
|
||||
docker-compose -f web-compose.yml up -d
|
||||
```
|
||||
|
||||
3. **Access the web interface:**
|
||||
- Local: http://localhost:3456
|
||||
- Mobile: http://YOUR_IP:3456
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DATA_DIR` | `/data` | Persistent data directory for device configs and rules |
|
||||
| `PORT` | `3456` | HTTP port for the web interface |
|
||||
|
||||
### Docker Compose Options
|
||||
|
||||
The `web-compose.yml` includes several production-ready features:
|
||||
|
||||
- **Persistent Data**: Uses Docker volumes to store device configurations and rules
|
||||
- **Health Checks**: Automatic monitoring of service health
|
||||
- **Restart Policy**: Automatically restarts if the service crashes
|
||||
- **Network Isolation**: Runs in a dedicated Docker network
|
||||
|
||||
### Networking Considerations
|
||||
|
||||
#### Wemo Device Discovery
|
||||
|
||||
Wemo devices use SSDP (Simple Service Discovery Protocol) which requires special networking:
|
||||
|
||||
- **Linux**: Use `network_mode: host` for automatic device discovery
|
||||
- **macOS/Windows**: Host networking isn't supported - add devices manually via the web UI
|
||||
|
||||
To enable host networking on Linux, uncomment this line in `web-compose.yml`:
|
||||
```yaml
|
||||
network_mode: host
|
||||
```
|
||||
|
||||
#### Port Configuration
|
||||
|
||||
The service runs on port 3456 by default. To change it:
|
||||
|
||||
1. Update the port mapping in `web-compose.yml`
|
||||
2. Set the `PORT` environment variable
|
||||
3. Restart the service
|
||||
|
||||
## Management
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
docker-compose -f web-compose.yml logs -f
|
||||
```
|
||||
|
||||
### Stop Service
|
||||
```bash
|
||||
docker-compose -f web-compose.yml down
|
||||
```
|
||||
|
||||
### Update Service
|
||||
```bash
|
||||
# Pull latest image and restart
|
||||
docker-compose -f web-compose.yml pull
|
||||
docker-compose -f web-compose.yml up -d
|
||||
```
|
||||
|
||||
### Backup Data
|
||||
```bash
|
||||
# Backup persistent data
|
||||
docker run --rm -v dibbly-data:/data -v $(pwd):/backup alpine tar czf /backup/dibbly-backup.tar.gz -C /data .
|
||||
|
||||
# Restore data
|
||||
docker run --rm -v dibbly-data:/data -v $(pwd):/backup alpine tar xzf /backup/dibbly-backup.tar.gz -C /data
|
||||
```
|
||||
|
||||
## Web Interface Features
|
||||
|
||||
The web interface provides full parity with the desktop application:
|
||||
|
||||
- **Device Management**: Discover, add, and control Wemo devices
|
||||
- **Scheduling**: Create and manage device schedules
|
||||
- **Real-time Updates**: WebSocket-based live status updates
|
||||
- **Mobile Optimized**: Responsive design for phones and tablets
|
||||
- **Dark Mode**: Automatic theme detection
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The service exposes a REST API for integration:
|
||||
|
||||
- `GET /api/devices` - List all devices
|
||||
- `POST /api/devices/discover` - Discover new devices
|
||||
- `GET/POST /api/devices/{host}/{port}/state` - Control device state
|
||||
- `GET/POST/PUT/DELETE /api/dwm-rules` - Manage scheduling rules
|
||||
- `GET /api/scheduler/status` - Get scheduler status
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Devices not discovered automatically**
|
||||
- On macOS/Windows, add devices manually via the web UI
|
||||
- On Linux, ensure host networking is enabled
|
||||
|
||||
2. **Service not accessible**
|
||||
- Check if port 3456 is available
|
||||
- Verify Docker is running
|
||||
- Check firewall settings
|
||||
|
||||
3. **Data persistence issues**
|
||||
- Ensure the Docker volume `dibbly-data` exists
|
||||
- Check permissions on the data directory
|
||||
|
||||
### Health Checks
|
||||
|
||||
The service includes built-in health checks. Monitor status:
|
||||
```bash
|
||||
docker-compose -f web-compose.yml ps
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
For optimal performance:
|
||||
- Use host networking on Linux for faster device discovery
|
||||
- Ensure adequate disk space for data persistence
|
||||
- Monitor memory usage with multiple devices
|
||||
|
||||
## Security
|
||||
|
||||
### Network Security
|
||||
|
||||
- The service binds to `0.0.0.0` by default
|
||||
- Consider using a reverse proxy (nginx/traefik) for production
|
||||
- Implement authentication if exposing to the internet
|
||||
|
||||
### Data Protection
|
||||
|
||||
- Device configurations are stored in `/data`
|
||||
- Regular backups are recommended
|
||||
- Consider encrypting the data volume in production
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For production environments, consider:
|
||||
|
||||
1. **Reverse Proxy**: Use nginx or Traefik for SSL termination
|
||||
2. **Authentication**: Add authentication layer
|
||||
3. **Monitoring**: Implement proper logging and monitoring
|
||||
4. **Backups**: Automated backup strategy
|
||||
5. **High Availability**: Multiple instances with load balancing
|
||||
|
||||
Example nginx configuration:
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name your-domain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3456;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream dibbly {
|
||||
server dibbly-web:3456;
|
||||
}
|
||||
|
||||
# HTTP redirect to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# SSL configuration
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
|
||||
|
||||
# Proxy to Dibby service
|
||||
location / {
|
||||
proxy_pass http://dibbly;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Timeout settings
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+7807
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "wemo-manager",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "npm run dev --workspace=apps/desktop",
|
||||
"build": "npm run build --workspace=apps/desktop",
|
||||
"install:all": "npm install"
|
||||
}
|
||||
}
|
||||
@@ -220,9 +220,32 @@
|
||||
<div class="flex-row" style="margin-bottom:12px">
|
||||
<h2 style="margin:0">DWM Automation Rules</h2>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-ghost btn-sm" id="btn-export-dwm" title="Export all rules to JSON file">⬇ Export</button>
|
||||
<button class="btn btn-ghost btn-sm" id="btn-import-dwm" title="Import rules from JSON file">⬆ Import</button>
|
||||
<input type="file" id="dwm-import-file" accept=".json" style="display:none" />
|
||||
<button class="btn btn-primary" id="btn-add-dwm">+ Add Rule</button>
|
||||
</div>
|
||||
|
||||
<!-- Import preview panel (hidden until file chosen) -->
|
||||
<div id="dwm-import-panel" style="display:none;margin-bottom:16px;padding:14px 16px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.18);border-radius:8px">
|
||||
<div class="flex-row" style="margin-bottom:10px">
|
||||
<strong id="dwm-import-title" style="font-size:0.92rem">Import Rules</strong>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-ghost btn-sm" id="btn-import-cancel">✕ Cancel</button>
|
||||
</div>
|
||||
<div id="dwm-import-list" style="max-height:180px;overflow-y:auto;margin-bottom:12px;font-size:0.82rem;color:#9ca3af"></div>
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:12px;font-size:0.85rem">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="dwm-import-mode" value="merge" checked /> Merge <span style="color:#9ca3af;font-size:0.78rem">(skip existing names)</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="dwm-import-mode" value="replace" /> Replace <span style="color:#fca5a5;font-size:0.78rem">(delete all current rules first)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="dwm-import-status" style="font-size:0.82rem;margin-bottom:8px;min-height:18px"></div>
|
||||
<button class="btn btn-primary" id="btn-import-confirm">⬆ Import Rules</button>
|
||||
</div>
|
||||
|
||||
<!-- Scheduler heartbeat bar -->
|
||||
<div id="dwm-heartbeat" style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:6px;margin-bottom:14px;font-size:0.8rem;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.18)">
|
||||
<span id="hb-dot" style="width:10px;height:10px;border-radius:50%;background:#6b7280;flex-shrink:0"></span>
|
||||
@@ -316,12 +339,40 @@
|
||||
<div class="flex-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Start Time</label>
|
||||
<select id="dwm-start-type" style="margin-bottom:6px">
|
||||
<option value="fixed">Fixed Time</option>
|
||||
<option value="sunrise">Sunrise</option>
|
||||
<option value="sunset">Sunset</option>
|
||||
</select>
|
||||
<div id="dwm-start-fixed">
|
||||
<input type="text" id="dwm-start-time" placeholder="e.g. 8:30 PM" />
|
||||
</div>
|
||||
<div id="dwm-start-sun" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<input type="number" id="dwm-start-offset" placeholder="0" style="width:70px" />
|
||||
<span style="font-size:0.8rem;color:#9ca3af">min (+ after, − before)</span>
|
||||
</div>
|
||||
<div id="dwm-start-preview" style="font-size:0.78rem;color:#4ade80;margin-top:4px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>End Time (optional)</label>
|
||||
<select id="dwm-end-type" style="margin-bottom:6px">
|
||||
<option value="fixed">Fixed Time</option>
|
||||
<option value="sunrise">Sunrise</option>
|
||||
<option value="sunset">Sunset</option>
|
||||
</select>
|
||||
<div id="dwm-end-fixed">
|
||||
<input type="text" id="dwm-end-time" placeholder="e.g. 11:00 PM" />
|
||||
</div>
|
||||
<div id="dwm-end-sun" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<input type="number" id="dwm-end-offset" placeholder="0" style="width:70px" />
|
||||
<span style="font-size:0.8rem;color:#9ca3af">min (+ after, − before)</span>
|
||||
</div>
|
||||
<div id="dwm-end-preview" style="font-size:0.78rem;color:#4ade80;margin-top:4px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
@@ -343,10 +394,29 @@
|
||||
</div>
|
||||
|
||||
<div id="dwm-countdown-fields" style="display:none">
|
||||
<div class="form-group">
|
||||
<label>Condition</label>
|
||||
<select id="dwm-countdown-action">
|
||||
<option value="on_to_off">If device turns ON → auto-OFF after duration</option>
|
||||
<option value="off_to_on">If device turns OFF → auto-ON after duration</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Countdown Duration (minutes)</label>
|
||||
<input type="number" id="dwm-countdown-mins" min="1" max="1440" placeholder="60" />
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Active Window Start</label>
|
||||
<input type="text" id="dwm-countdown-window-start" placeholder="e.g. 9:00 AM" />
|
||||
</div>
|
||||
<div class="form-group" style="flex:1">
|
||||
<label>Active Window End</label>
|
||||
<input type="text" id="dwm-countdown-window-end" placeholder="e.g. 4:00 AM" />
|
||||
<div style="font-size:0.75rem;color:var(--muted);margin-top:3px">End before start = next day</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:0.75rem;color:var(--muted);margin-bottom:8px">Leave window blank to run at any time.</div>
|
||||
</div>
|
||||
|
||||
<div id="dwm-alwayson-info" style="display:none;margin-bottom:12px;padding:10px;background:rgba(48,209,88,.1);border:1px solid rgba(48,209,88,.3);border-radius:6px;font-size:0.82rem;color:#4ade80">
|
||||
|
||||
@@ -12,6 +12,7 @@ let _wemoRules = null; // { rules, ruleDevices, targets } for selecte
|
||||
let _editingDwmId = null; // null = create, string = update
|
||||
let _selectedDwmDays = new Set();
|
||||
let _pendingLocation = null; // { lat, lng, label }
|
||||
let _todaySunTimes = null; // { sunrise, sunset } seconds from midnight
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tabs
|
||||
@@ -195,7 +196,11 @@ function dwmRuleSummary(r) {
|
||||
}
|
||||
if (r.type === 'Countdown') {
|
||||
const mins = r.countdownTime ? Math.round(r.countdownTime / 60) : null;
|
||||
return mins ? `⏱ ${mins} min auto-off` : '—';
|
||||
const cond = r.countdownAction === 'off_to_on' ? 'OFF→ON' : 'ON→OFF';
|
||||
const win = (r.windowStart >= 0 && r.windowEnd >= 0)
|
||||
? ` · ${secsToHHMM(r.windowStart)}–${secsToHHMM(r.windowEnd)}`
|
||||
: (r.windowStart >= 0 ? ` · from ${secsToHHMM(r.windowStart)}` : '');
|
||||
return mins ? `⏱ ${mins} min · ${cond}${win}` : '—';
|
||||
}
|
||||
const days = dayLabel(r.days);
|
||||
const devs = (r.targetDevices ?? []).map((td) => esc(td.name ?? td.host)).join(', ') || 'no targets';
|
||||
@@ -273,6 +278,139 @@ function deleteDwmRule(id) {
|
||||
setTimeout(() => row.remove(), 5000);
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────────────
|
||||
|
||||
document.getElementById('btn-export-dwm').addEventListener('click', async () => {
|
||||
try {
|
||||
const rules = await homebridge.request('/rules/export');
|
||||
if (!rules || !rules.length) { showStatus('dwm-rules-status', 'No rules to export.', 'warn'); return; }
|
||||
const blob = new Blob([JSON.stringify(rules, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
a.href = url;
|
||||
a.download = `dwm-rules-${date}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showStatus('dwm-rules-status', `Exported ${rules.length} rule${rules.length !== 1 ? 's' : ''}.`, 'success');
|
||||
} catch (e) {
|
||||
showStatus('dwm-rules-status', 'Export failed: ' + e.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Import ────────────────────────────────────────────────────────────────────
|
||||
|
||||
let _importRules = []; // parsed rules waiting for confirmation
|
||||
|
||||
document.getElementById('btn-import-dwm').addEventListener('click', () => {
|
||||
document.getElementById('dwm-import-file').value = ''; // reset so same file can be re-selected
|
||||
document.getElementById('dwm-import-file').click();
|
||||
});
|
||||
|
||||
document.getElementById('dwm-import-file').addEventListener('change', (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
try {
|
||||
const parsed = JSON.parse(ev.target.result);
|
||||
const rules = Array.isArray(parsed) ? parsed : parsed.rules ?? [];
|
||||
if (!rules.length) throw new Error('No rules found in file');
|
||||
|
||||
_importRules = rules;
|
||||
document.getElementById('dwm-import-title').textContent =
|
||||
`Import ${rules.length} rule${rules.length !== 1 ? 's' : ''} from "${file.name}"`;
|
||||
|
||||
// Build preview list
|
||||
const listEl = document.getElementById('dwm-import-list');
|
||||
listEl.innerHTML = rules.map((r) =>
|
||||
`<div style="padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.06)">` +
|
||||
`<span style="color:#e2e8f0">${esc(r.name ?? '(unnamed)')}</span> ` +
|
||||
`<span style="color:#6b7280;font-size:0.75rem">${esc(r.type ?? '')}</span></div>`
|
||||
).join('');
|
||||
|
||||
document.getElementById('dwm-import-status').textContent = '';
|
||||
document.getElementById('dwm-import-panel').style.display = '';
|
||||
document.getElementById('btn-import-confirm').disabled = false;
|
||||
} catch (err) {
|
||||
showStatus('dwm-rules-status', 'Import failed: ' + err.message, 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
document.getElementById('btn-import-cancel').addEventListener('click', () => {
|
||||
document.getElementById('dwm-import-panel').style.display = 'none';
|
||||
_importRules = [];
|
||||
});
|
||||
|
||||
document.getElementById('btn-import-confirm').addEventListener('click', async () => {
|
||||
if (!_importRules.length) return;
|
||||
const mode = document.querySelector('input[name="dwm-import-mode"]:checked')?.value ?? 'merge';
|
||||
const statusEl = document.getElementById('dwm-import-status');
|
||||
const btn = document.getElementById('btn-import-confirm');
|
||||
btn.disabled = true;
|
||||
statusEl.style.color = '#9ca3af';
|
||||
statusEl.textContent = 'Importing…';
|
||||
try {
|
||||
const res = await homebridge.request('/rules/import', { rules: _importRules, mode });
|
||||
document.getElementById('dwm-import-panel').style.display = 'none';
|
||||
_importRules = [];
|
||||
await loadDwmRules();
|
||||
const msg = mode === 'replace'
|
||||
? `Replaced all rules — imported ${res.imported}.`
|
||||
: `Imported ${res.imported} rule${res.imported !== 1 ? 's' : ''}${res.skipped ? `, skipped ${res.skipped} (name already exists)` : ''}.`;
|
||||
showStatus('dwm-rules-status', msg, 'success');
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
statusEl.style.color = '#fca5a5';
|
||||
statusEl.textContent = 'Import failed: ' + e.message;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Sun-time helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function secsToAmPm(secs) {
|
||||
if (secs == null || secs < 0) return '—';
|
||||
const h24 = Math.floor(secs / 3600) % 24;
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
const ap = h24 < 12 ? 'AM' : 'PM';
|
||||
const h12 = h24 % 12 || 12;
|
||||
return `${h12}:${String(m).padStart(2, '0')} ${ap}`;
|
||||
}
|
||||
|
||||
function updateSunTypeVisibility() {
|
||||
for (const side of ['start', 'end']) {
|
||||
const type = document.getElementById(`dwm-${side}-type`)?.value ?? 'fixed';
|
||||
const isSun = type === 'sunrise' || type === 'sunset';
|
||||
document.getElementById(`dwm-${side}-fixed`).style.display = isSun ? 'none' : '';
|
||||
document.getElementById(`dwm-${side}-sun`).style.display = isSun ? '' : 'none';
|
||||
updateSunPreview(side);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSunPreview(side) {
|
||||
const previewEl = document.getElementById(`dwm-${side}-preview`);
|
||||
if (!previewEl) return;
|
||||
const type = document.getElementById(`dwm-${side}-type`)?.value;
|
||||
const offset = parseInt(document.getElementById(`dwm-${side}-offset`)?.value ?? '0', 10) || 0;
|
||||
if (!_todaySunTimes || (type !== 'sunrise' && type !== 'sunset')) { previewEl.textContent = ''; return; }
|
||||
const baseSecs = type === 'sunrise' ? _todaySunTimes.sunrise : _todaySunTimes.sunset;
|
||||
if (baseSecs == null) { previewEl.textContent = 'No sun data for location'; return; }
|
||||
const fireSecs = baseSecs + offset * 60;
|
||||
const baseStr = secsToAmPm(baseSecs);
|
||||
const fireStr = secsToAmPm(fireSecs);
|
||||
const offStr = offset !== 0 ? ` (${offset > 0 ? '+' : ''}${offset} min)` : '';
|
||||
previewEl.textContent = `Today's ${type}: ${baseStr} → fires ${fireStr}${offStr}`;
|
||||
}
|
||||
|
||||
// Wire up type dropdowns and offset inputs for live preview
|
||||
['start', 'end'].forEach((side) => {
|
||||
document.getElementById(`dwm-${side}-type`)?.addEventListener('change', updateSunTypeVisibility);
|
||||
document.getElementById(`dwm-${side}-offset`)?.addEventListener('input', () => updateSunPreview(side));
|
||||
});
|
||||
|
||||
document.getElementById('btn-add-dwm').addEventListener('click', () => openDwmEdit(null));
|
||||
|
||||
// ── DWM Inline Form ───────────────────────────────────────────────────────────
|
||||
@@ -298,12 +436,19 @@ function openDwmEdit(id) {
|
||||
document.getElementById('dwm-name').value = r.name ?? '';
|
||||
document.getElementById('dwm-type').value = r.type ?? 'Schedule';
|
||||
document.getElementById('dwm-enabled').checked = r.enabled !== false;
|
||||
document.getElementById('dwm-start-time').value = secsToHHMM(r.startTime);
|
||||
document.getElementById('dwm-end-time').value = secsToHHMM(r.endTime);
|
||||
document.getElementById('dwm-start-type').value = r.startType || 'fixed';
|
||||
document.getElementById('dwm-start-offset').value = String(r.startOffset ?? 0);
|
||||
document.getElementById('dwm-start-time').value = (r.startType === 'fixed' && r.startTime >= 0) ? secsToHHMM(r.startTime) : '';
|
||||
document.getElementById('dwm-end-type').value = r.endType || 'fixed';
|
||||
document.getElementById('dwm-end-offset').value = String(r.endOffset ?? 0);
|
||||
document.getElementById('dwm-end-time').value = (r.endType === 'fixed' && r.endTime > 0) ? secsToHHMM(r.endTime) : '';
|
||||
document.getElementById('dwm-start-action').value = String(r.startAction ?? 1);
|
||||
document.getElementById('dwm-end-action').value = String(r.endAction ?? -1);
|
||||
document.getElementById('dwm-countdown-mins').value =
|
||||
r.countdownTime ? String(Math.round(r.countdownTime / 60)) : '';
|
||||
document.getElementById('dwm-countdown-action').value = r.countdownAction ?? 'on_to_off';
|
||||
document.getElementById('dwm-countdown-window-start').value = r.windowStart >= 0 ? secsToHHMM(r.windowStart) : '';
|
||||
document.getElementById('dwm-countdown-window-end').value = r.windowEnd >= 0 ? secsToHHMM(r.windowEnd) : '';
|
||||
|
||||
_selectedDwmDays = new Set((r.days ?? []).map(Number));
|
||||
|
||||
@@ -328,11 +473,18 @@ function openDwmEdit(id) {
|
||||
document.getElementById('dwm-name').value = '';
|
||||
document.getElementById('dwm-type').value = 'Schedule';
|
||||
document.getElementById('dwm-enabled').checked = true;
|
||||
document.getElementById('dwm-start-type').value = 'fixed';
|
||||
document.getElementById('dwm-start-offset').value = '0';
|
||||
document.getElementById('dwm-start-time').value = '';
|
||||
document.getElementById('dwm-end-type').value = 'fixed';
|
||||
document.getElementById('dwm-end-offset').value = '0';
|
||||
document.getElementById('dwm-end-time').value = '';
|
||||
document.getElementById('dwm-start-action').value = '1';
|
||||
document.getElementById('dwm-end-action').value = '-1';
|
||||
document.getElementById('dwm-countdown-mins').value = '';
|
||||
document.getElementById('dwm-countdown-action').value = 'on_to_off';
|
||||
document.getElementById('dwm-countdown-window-start').value = '';
|
||||
document.getElementById('dwm-countdown-window-end').value = '';
|
||||
document.getElementById('dwm-trigger-src').value = '';
|
||||
document.getElementById('dwm-trigger-event').value = 'any';
|
||||
document.getElementById('dwm-trigger-action').value = 'on';
|
||||
@@ -342,6 +494,7 @@ function openDwmEdit(id) {
|
||||
|
||||
updateDwmDayButtons();
|
||||
updateDwmTypeFields();
|
||||
updateSunTypeVisibility();
|
||||
document.getElementById('dwm-list-view').style.display = 'none';
|
||||
document.getElementById('dwm-form-panel').style.display = '';
|
||||
window.scrollTo(0, 0);
|
||||
@@ -462,11 +615,36 @@ document.getElementById('dwm-form-save-btn').addEventListener('click', async ()
|
||||
const mins = Number(document.getElementById('dwm-countdown-mins').value);
|
||||
if (!mins || mins < 1) { showModalError('Enter countdown duration in minutes'); return; }
|
||||
rule.countdownTime = mins * 60;
|
||||
rule.countdownAction = document.getElementById('dwm-countdown-action').value;
|
||||
const winStart = hhmmToSecs(document.getElementById('dwm-countdown-window-start').value);
|
||||
const winEnd = hhmmToSecs(document.getElementById('dwm-countdown-window-end').value);
|
||||
rule.windowStart = winStart >= 0 ? winStart : -1;
|
||||
rule.windowEnd = winEnd >= 0 ? winEnd : -1;
|
||||
} else {
|
||||
const startSecs = hhmmToSecs(document.getElementById('dwm-start-time').value);
|
||||
if (startSecs < 0) { showModalError('Enter a valid start time (HH:MM)'); return; }
|
||||
const startType = document.getElementById('dwm-start-type').value;
|
||||
const startOffset = parseInt(document.getElementById('dwm-start-offset').value ?? '0', 10) || 0;
|
||||
const endType = document.getElementById('dwm-end-type').value;
|
||||
const endOffset = parseInt(document.getElementById('dwm-end-offset').value ?? '0', 10) || 0;
|
||||
|
||||
let startSecs;
|
||||
if (startType === 'sunrise') { startSecs = -2; }
|
||||
else if (startType === 'sunset') { startSecs = -3; }
|
||||
else {
|
||||
startSecs = hhmmToSecs(document.getElementById('dwm-start-time').value);
|
||||
if (startSecs < 0) { showModalError('Enter a valid start time (e.g. 8:30 PM)'); return; }
|
||||
}
|
||||
|
||||
let endSecs;
|
||||
if (endType === 'sunrise') { endSecs = -2; }
|
||||
else if (endType === 'sunset') { endSecs = -3; }
|
||||
else { endSecs = hhmmToSecs(document.getElementById('dwm-end-time').value); }
|
||||
|
||||
rule.startTime = startSecs;
|
||||
rule.endTime = hhmmToSecs(document.getElementById('dwm-end-time').value);
|
||||
rule.startType = startType;
|
||||
rule.startOffset = startOffset;
|
||||
rule.endTime = endSecs;
|
||||
rule.endType = endType;
|
||||
rule.endOffset = endOffset;
|
||||
rule.startAction = Number(document.getElementById('dwm-start-action').value);
|
||||
rule.endAction = Number(document.getElementById('dwm-end-action').value);
|
||||
}
|
||||
@@ -764,5 +942,7 @@ document.querySelectorAll('.tab-btn').forEach((btn) => {
|
||||
await loadDwmRules();
|
||||
await loadLocation();
|
||||
refreshWemoDeviceSelect();
|
||||
startHeartbeatPolling(); // start immediately (DWM tab is not default, but still useful)
|
||||
startHeartbeatPolling();
|
||||
// Fetch today's sun times in background — used by rule editor previews
|
||||
homebridge.request('/sun-times').then((st) => { _todaySunTimes = st; }).catch(() => {});
|
||||
})();
|
||||
|
||||
@@ -28,6 +28,7 @@ const path = require('path');
|
||||
const DwmStore = require('../lib/store');
|
||||
const wemoClient = require('../lib/wemo-client');
|
||||
const axios = require('axios');
|
||||
const { sunTimes: calcSunTimes } = require('../lib/sun');
|
||||
|
||||
class DibbyWemoUiServer extends HomebridgePluginUiServer {
|
||||
constructor() {
|
||||
@@ -44,8 +45,8 @@ class DibbyWemoUiServer extends HomebridgePluginUiServer {
|
||||
this.onRequest('/devices/discover', async ({ timeout } = {}) => {
|
||||
const ms = typeof timeout === 'number' ? timeout : 10_000;
|
||||
const devices = await wemoClient.discoverDevices(ms);
|
||||
// Persist updated list
|
||||
this._store.saveDevices(devices.map((d) => ({
|
||||
// Merge into cached list — previously known devices stay even if not found this scan
|
||||
this._store.mergeDevices(devices.map((d) => ({
|
||||
host: d.host,
|
||||
port: d.port,
|
||||
udn: d.udn ?? `${d.host}:${d.port}`,
|
||||
@@ -53,7 +54,8 @@ class DibbyWemoUiServer extends HomebridgePluginUiServer {
|
||||
productModel: d.productModel ?? 'Wemo Device',
|
||||
firmwareVersion: d.firmwareVersion ?? null,
|
||||
})));
|
||||
return devices;
|
||||
// Return the full merged list so the UI shows all known devices
|
||||
return this._store.getDevices();
|
||||
});
|
||||
|
||||
this.onRequest('/devices/state', async ({ host, port }) => {
|
||||
@@ -83,6 +85,35 @@ class DibbyWemoUiServer extends HomebridgePluginUiServer {
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
this.onRequest('/rules/export', async () => {
|
||||
return this._store.getDwmRules();
|
||||
});
|
||||
|
||||
this.onRequest('/rules/import', async ({ rules, mode }) => {
|
||||
if (!Array.isArray(rules) || rules.length === 0) throw new Error('No valid rules found in import data');
|
||||
|
||||
if (mode === 'replace') {
|
||||
for (const r of this._store.getDwmRules()) this._store.deleteDwmRule(r.id);
|
||||
}
|
||||
|
||||
const existing = this._store.getDwmRules();
|
||||
const existingNames = new Set(existing.map((r) => (r.name ?? '').toLowerCase()));
|
||||
let imported = 0, skipped = 0;
|
||||
|
||||
for (const rule of rules) {
|
||||
// Strip old identity fields — store will assign fresh id + timestamps
|
||||
const { id: _id, createdAt: _ca, updatedAt: _ua, ...ruleData } = rule;
|
||||
if (mode === 'merge' && existingNames.has((ruleData.name ?? '').toLowerCase())) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
this._store.createDwmRule(ruleData);
|
||||
imported++;
|
||||
}
|
||||
|
||||
return { ok: true, imported, skipped };
|
||||
});
|
||||
|
||||
// ── Scheduler heartbeat ───────────────────────────────────────────────────
|
||||
this.onRequest('/scheduler/status', async () => {
|
||||
const hb = this._store.getHeartbeat();
|
||||
@@ -122,6 +153,13 @@ class DibbyWemoUiServer extends HomebridgePluginUiServer {
|
||||
return this._store.getLocation();
|
||||
});
|
||||
|
||||
this.onRequest('/sun-times', async () => {
|
||||
const loc = this._store.getLocation();
|
||||
if (!loc?.lat || !loc?.lng) return { sunrise: null, sunset: null };
|
||||
try { return calcSunTimes(loc.lat, loc.lng); }
|
||||
catch { return { sunrise: null, sunset: null }; }
|
||||
});
|
||||
|
||||
this.onRequest('/location/set', async (loc) => {
|
||||
this._store.setLocation(loc);
|
||||
return { ok: true };
|
||||
|
||||
@@ -114,25 +114,36 @@ class WemoPlatform {
|
||||
|
||||
this.log.info(`Found ${discovered.length} Wemo device(s)`);
|
||||
|
||||
// Save discovered device list for the custom UI
|
||||
this._store.saveDevices(discovered.map((d) => ({
|
||||
// Merge discovered devices into the cached list — keep offline devices too
|
||||
const freshForStore = discovered.map((d) => ({
|
||||
host: d.host,
|
||||
port: d.port,
|
||||
udn: d.udn ?? `${d.host}:${d.port}`,
|
||||
friendlyName: d.friendlyName ?? d.host,
|
||||
productModel: d.productModel ?? 'Wemo Device',
|
||||
firmwareVersion: d.firmwareVersion ?? null,
|
||||
})));
|
||||
}));
|
||||
const allKnown = this._store.mergeDevices(freshForStore);
|
||||
|
||||
// Register newly discovered devices in HomeKit
|
||||
for (const device of discovered) {
|
||||
this._registerDevice(device, pollInterval);
|
||||
}
|
||||
|
||||
// Remove stale accessories (devices no longer discovered)
|
||||
const activeUUIDs = new Set(discovered.map((d) => this._uuidForDevice(d)));
|
||||
// Register previously cached devices that weren't discovered (may be offline)
|
||||
// so HomeKit still knows about them
|
||||
const discoveredUDNs = new Set(discovered.map((d) => d.udn ?? `${d.host}:${d.port}`));
|
||||
for (const cached of allKnown) {
|
||||
if (!discoveredUDNs.has(cached.udn)) {
|
||||
this.log.info(`Device offline/not found during discovery, keeping cached: ${cached.friendlyName}`);
|
||||
this._registerDevice(cached, pollInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove orphaned accessories — those with no device context at all
|
||||
for (const [uuid, acc] of this._accessories) {
|
||||
if (!activeUUIDs.has(uuid)) {
|
||||
this.log.info('Removing stale accessory: ' + acc.displayName);
|
||||
if (!acc.context?.device?.host) {
|
||||
this.log.info('Removing orphaned accessory (no device context): ' + acc.displayName);
|
||||
this._handlers.get(uuid)?.stopPolling();
|
||||
this._handlers.delete(uuid);
|
||||
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [acc]);
|
||||
|
||||
@@ -19,11 +19,37 @@
|
||||
* await scheduler.start();
|
||||
*/
|
||||
|
||||
const { sunTimes: calcSunTimes } = require('./sun');
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Wemo DayID: 1=Mon … 7=Sun. JS getDay(): 0=Sun … 6=Sat. */
|
||||
function jsToWemoDayId(jsDay) { return jsDay === 0 ? 7 : jsDay; }
|
||||
|
||||
/**
|
||||
* Resolve a stored startTime/endTime value to actual seconds-from-midnight.
|
||||
* -2 = sunrise sentinel, -3 = sunset sentinel.
|
||||
* offsetMins is added to the sun time (negative = before).
|
||||
* Returns null if unresolvable (no location, polar day/night, or no time set).
|
||||
*/
|
||||
function resolveSecs(rawSecs, type, offsetMins, todaySun) {
|
||||
const offsetSecs = (offsetMins ?? 0) * 60;
|
||||
if (type === 'sunset' || rawSecs === -3) {
|
||||
return todaySun?.sunset != null ? todaySun.sunset + offsetSecs : null;
|
||||
}
|
||||
if (type === 'sunrise' || rawSecs === -2) {
|
||||
return todaySun?.sunrise != null ? todaySun.sunrise + offsetSecs : null;
|
||||
}
|
||||
return rawSecs >= 0 ? rawSecs : null;
|
||||
}
|
||||
|
||||
/** Compute today's sunrise/sunset from the store's saved location. Returns null if not set. */
|
||||
function getTodaySun(store) {
|
||||
const loc = store.getLocation?.();
|
||||
if (!loc?.lat || !loc?.lng) return null;
|
||||
try { return calcSunTimes(loc.lat, loc.lng); } catch { return null; }
|
||||
}
|
||||
|
||||
function secondsFromMidnight(date) {
|
||||
return date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds();
|
||||
}
|
||||
@@ -74,6 +100,8 @@ class DwmScheduler {
|
||||
this._onHealth = null; // ({host, port, name, online, msg}) health event callback
|
||||
this._deviceHealth = new Map(); // 'host:port' → true | false
|
||||
this._triggerStates = new Map(); // 'host:port' → last known boolean state (for Trigger rules)
|
||||
this._countdownStates = new Map(); // 'host:port' → last known boolean state (for Countdown rules)
|
||||
this._countdownTimers = new Map(); // 'deviceKey-ruleId' → {timer, wantOn}
|
||||
this._healthTimer = null;
|
||||
this._startedAt = null;
|
||||
}
|
||||
@@ -150,6 +178,7 @@ class DwmScheduler {
|
||||
_loadSchedule() {
|
||||
const schedule = [];
|
||||
const rules = this._store.getDwmRules();
|
||||
const todaySun = getTodaySun(this._store);
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.enabled) continue;
|
||||
@@ -159,9 +188,11 @@ class DwmScheduler {
|
||||
|
||||
// Away Mode
|
||||
if (rule.type === 'Away') {
|
||||
const startSecs = Number(rule.startTime ?? -1);
|
||||
const endSecs = Number(rule.endTime ?? -1);
|
||||
if (startSecs < 0) continue;
|
||||
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset, todaySun);
|
||||
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset, todaySun);
|
||||
if (startSecs === null) continue;
|
||||
const awayStartAction = Number(rule.startAction ?? 1);
|
||||
const awayEndAction = Number(rule.endAction ?? 0);
|
||||
|
||||
for (const dayId of (rule.days ?? [])) {
|
||||
const td0 = rule.targetDevices?.[0];
|
||||
@@ -169,50 +200,29 @@ class DwmScheduler {
|
||||
ruleId: rule.id, ruleName: rule.name,
|
||||
targetHost: td0?.host ?? '', targetPort: td0?.port ?? 0,
|
||||
dayId: Number(dayId), targetSecs: startSecs,
|
||||
action: 1, isAwayStart: true,
|
||||
action: awayStartAction, isAwayStart: true,
|
||||
});
|
||||
if (endSecs >= 0) {
|
||||
if (endSecs !== null && endSecs >= 0) {
|
||||
schedule.push({
|
||||
ruleId: rule.id + '-away-end', ruleName: rule.name,
|
||||
targetHost: td0?.host ?? '', targetPort: td0?.port ?? 0,
|
||||
dayId: Number(dayId), targetSecs: endSecs,
|
||||
action: 0, isAwayEnd: true, awayRuleId: rule.id,
|
||||
action: awayEndAction, isAwayEnd: true, awayRuleId: rule.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Countdown with active window
|
||||
if (rule.type === 'Countdown') {
|
||||
const windowStart = Number(rule.windowStart ?? -1);
|
||||
const windowEnd = Number(rule.windowEnd ?? -1);
|
||||
if (windowStart < 0 || !(rule.windowDays?.length)) continue;
|
||||
|
||||
const crossesMidnight = windowEnd >= 0 && windowEnd < windowStart;
|
||||
for (const dayId of rule.windowDays) {
|
||||
for (const td of (rule.targetDevices ?? [])) {
|
||||
if (!td.host || !td.port) continue;
|
||||
schedule.push({ ruleId: rule.id, ruleName: rule.name,
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: Number(dayId), targetSecs: windowStart, action: 1 });
|
||||
if (windowEnd >= 0) {
|
||||
const offDayId = crossesMidnight ? (Number(dayId) % 7) + 1 : Number(dayId);
|
||||
schedule.push({ ruleId: rule.id + '-wend', ruleName: rule.name,
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: offDayId, targetSecs: windowEnd, action: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Countdown — handled entirely by the health-monitor state-change poll
|
||||
if (rule.type === 'Countdown') continue;
|
||||
|
||||
// Schedule / time-based
|
||||
const startSecs = Number(rule.startTime ?? -1);
|
||||
const endSecs = Number(rule.endTime ?? -1);
|
||||
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset, todaySun);
|
||||
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset, todaySun);
|
||||
const startAction = Number(rule.startAction ?? 1);
|
||||
const endAction = Number(rule.endAction ?? -1);
|
||||
if (startSecs < 0) continue;
|
||||
if (startSecs === null) continue;
|
||||
|
||||
for (const dayId of (rule.days ?? [])) {
|
||||
for (const td of (rule.targetDevices ?? [])) {
|
||||
@@ -222,7 +232,7 @@ class DwmScheduler {
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: Number(dayId), targetSecs: startSecs, action: startAction });
|
||||
}
|
||||
if (endSecs > 0 && endAction >= 0) {
|
||||
if (endSecs !== null && endSecs > 0 && endAction >= 0) {
|
||||
schedule.push({ ruleId: rule.id, ruleName: rule.name,
|
||||
targetHost: td.host, targetPort: td.port,
|
||||
dayId: Number(dayId), targetSecs: endSecs, action: endAction });
|
||||
@@ -243,17 +253,18 @@ class DwmScheduler {
|
||||
const nowSecs = secondsFromMidnight(now);
|
||||
const todayId = jsToWemoDayId(now.getDay());
|
||||
const rules = this._store.getDwmRules();
|
||||
const todaySun = getTodaySun(this._store);
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.enabled || rule.type !== 'Away') continue;
|
||||
if (this._awayLoops.has(rule.id)) continue;
|
||||
|
||||
const startSecs = Number(rule.startTime ?? -1);
|
||||
const endSecs = Number(rule.endTime ?? -1);
|
||||
if (startSecs < 0) continue;
|
||||
const startSecs = resolveSecs(Number(rule.startTime ?? -1), rule.startType, rule.startOffset, todaySun);
|
||||
const endSecs = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset, todaySun);
|
||||
if (startSecs === null) continue;
|
||||
if (!(rule.days ?? []).includes(todayId)) continue;
|
||||
|
||||
const inWindow = endSecs >= 0
|
||||
const inWindow = endSecs !== null && endSecs >= 0
|
||||
? (startSecs <= endSecs ? (nowSecs >= startSecs && nowSecs < endSecs)
|
||||
: (nowSecs >= startSecs || nowSecs < endSecs))
|
||||
: nowSecs >= startSecs;
|
||||
@@ -269,7 +280,9 @@ class DwmScheduler {
|
||||
const devices = (rule.targetDevices ?? []).filter(td => td.host && td.port);
|
||||
if (!devices.length) return;
|
||||
|
||||
const loop = { rule, devices, endSecs: Number(rule.endTime ?? -1), timer: null, isOn: false };
|
||||
const todaySun = getTodaySun(this._store);
|
||||
const resolvedEnd = resolveSecs(Number(rule.endTime ?? -1), rule.endType, rule.endOffset, todaySun);
|
||||
const loop = { rule, devices, endSecs: resolvedEnd ?? -1, timer: null, isOn: false };
|
||||
this._awayLoops.set(rule.id, loop);
|
||||
this._awayStep(rule.id, true);
|
||||
}
|
||||
@@ -314,12 +327,14 @@ class DwmScheduler {
|
||||
if (loop.timer) clearTimeout(loop.timer);
|
||||
this._awayLoops.delete(ruleId);
|
||||
if (forceOff) {
|
||||
const endAction = Number(loop.rule.endAction ?? 0);
|
||||
const turnOn = endAction === 1;
|
||||
for (const td of loop.devices) {
|
||||
this._wemo.setBinaryState(td.host, td.port, false).catch(() => {});
|
||||
this._wemo.setBinaryState(td.host, td.port, turnOn).catch(() => {});
|
||||
}
|
||||
this._emit({ success: true,
|
||||
msg: `"${loop.rule.name}" Away Mode window ended — all devices OFF`,
|
||||
entry: { action: 0 } });
|
||||
msg: `"${loop.rule.name}" Away Mode window ended — all devices ${turnOn ? 'ON' : 'OFF'}`,
|
||||
entry: { action: endAction } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +380,8 @@ class DwmScheduler {
|
||||
for (const t of this._timers) clearTimeout(t);
|
||||
this._timers = [];
|
||||
if (this._tickTimer) { clearTimeout(this._tickTimer); this._tickTimer = null; }
|
||||
for (const { timer } of this._countdownTimers.values()) clearTimeout(timer);
|
||||
this._countdownTimers.clear();
|
||||
}
|
||||
|
||||
_scheduleUpcoming() {
|
||||
@@ -501,6 +518,7 @@ class DwmScheduler {
|
||||
const allRules = this._store.getDwmRules();
|
||||
const alwaysOnSet = new Set(); // keys with an active AlwaysOn rule
|
||||
const triggerSrcSet = new Set(); // keys that are trigger source devices
|
||||
const countdownDevMap = new Map(); // deviceKey → [{rule, td}]
|
||||
|
||||
const addDev = (td) => {
|
||||
if (!td?.host || !td?.port) return;
|
||||
@@ -520,7 +538,12 @@ class DwmScheduler {
|
||||
}
|
||||
for (const td of (rule.targetDevices ?? [])) {
|
||||
const k = addDev(td);
|
||||
if (k && rule.type === 'AlwaysOn') alwaysOnSet.add(k);
|
||||
if (!k) continue;
|
||||
if (rule.type === 'AlwaysOn') alwaysOnSet.add(k);
|
||||
if (rule.type === 'Countdown') {
|
||||
if (!countdownDevMap.has(k)) countdownDevMap.set(k, []);
|
||||
countdownDevMap.get(k).push({ rule, td });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +587,61 @@ class DwmScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Countdown — fire only when state matches condition and within window ──
|
||||
if (countdownDevMap.has(key)) {
|
||||
const prevState = this._countdownStates.get(key);
|
||||
this._countdownStates.set(key, isOn);
|
||||
if (prevState !== undefined && prevState !== isOn) {
|
||||
const nowSecs = secondsFromMidnight(new Date());
|
||||
for (const { rule, td } of countdownDevMap.get(key)) {
|
||||
const condition = rule.countdownAction ?? 'on_to_off';
|
||||
const triggered = condition === 'on_to_off' ? isOn : !isOn;
|
||||
if (!triggered) continue; // state doesn't match this rule's condition
|
||||
|
||||
// Check active window (if defined)
|
||||
const winStart = Number(rule.windowStart ?? -1);
|
||||
const winEnd = Number(rule.windowEnd ?? -1);
|
||||
if (winStart >= 0 && winEnd >= 0) {
|
||||
const crossesMidnight = winEnd < winStart;
|
||||
const inWindow = crossesMidnight
|
||||
? (nowSecs >= winStart || nowSecs <= winEnd)
|
||||
: (nowSecs >= winStart && nowSecs <= winEnd);
|
||||
if (!inWindow) continue; // outside active window
|
||||
} else if (winStart >= 0) {
|
||||
if (nowSecs < winStart) continue;
|
||||
}
|
||||
|
||||
const timerKey = `${key}-${rule.id}`;
|
||||
// Cancel any pending timer for this device+rule
|
||||
const existing = this._countdownTimers.get(timerKey);
|
||||
if (existing) { clearTimeout(existing.timer); this._countdownTimers.delete(timerKey); }
|
||||
|
||||
const wantOn = condition === 'off_to_on'; // on_to_off → turn OFF; off_to_on → turn ON
|
||||
const durationMs = (Number(rule.countdownTime) || 60) * 1000;
|
||||
const label = wantOn ? 'ON' : 'OFF';
|
||||
const mins = Math.round(durationMs / 60000);
|
||||
this._emit({ success: true,
|
||||
msg: `"${rule.name}" countdown started — will turn ${label} in ${mins} min (${td.host})`,
|
||||
entry: { action: wantOn ? 1 : 0 } });
|
||||
|
||||
const timer = setTimeout(async () => {
|
||||
this._countdownTimers.delete(timerKey);
|
||||
try {
|
||||
await this._wemo.setBinaryState(td.host, td.port, wantOn);
|
||||
this._emit({ success: true,
|
||||
msg: `"${rule.name}" countdown elapsed → ${label} (${td.host}) ✓`,
|
||||
entry: { action: wantOn ? 1 : 0 } });
|
||||
} catch (e2) {
|
||||
this._emit({ success: false,
|
||||
msg: `"${rule.name}" countdown elapsed → ${label} FAILED: ${e2.message}`,
|
||||
entry: { action: wantOn ? 1 : 0 } });
|
||||
}
|
||||
}, durationMs);
|
||||
this._countdownTimers.set(timerKey, { timer, wantOn });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this._deviceHealth.set(key, false);
|
||||
if (wasOnline !== false) {
|
||||
|
||||
@@ -56,6 +56,33 @@ class DwmStore {
|
||||
getDeviceGroups() { return this._load().deviceGroups ?? []; }
|
||||
saveDeviceGroups(groups) { const d = this._load(); d.deviceGroups = groups; this._save(d); }
|
||||
|
||||
/**
|
||||
* Merge freshly discovered devices into the cached list.
|
||||
* - Existing devices are updated with fresh data (host/port/name/firmware).
|
||||
* - Previously cached devices NOT in the new scan are kept as-is (offline ≠ removed).
|
||||
* - Newly found devices are appended.
|
||||
* Returns the merged list.
|
||||
*/
|
||||
mergeDevices(fresh) {
|
||||
const d = this._load();
|
||||
const cached = d.devices ?? [];
|
||||
const byUdn = new Map(cached.map((dev) => [dev.udn, dev]));
|
||||
|
||||
for (const f of fresh) {
|
||||
const udn = f.udn ?? `${f.host}:${f.port}`;
|
||||
if (byUdn.has(udn)) {
|
||||
// Update existing entry with latest network data
|
||||
byUdn.set(udn, { ...byUdn.get(udn), ...f, udn });
|
||||
} else {
|
||||
byUdn.set(udn, { ...f, udn });
|
||||
}
|
||||
}
|
||||
|
||||
d.devices = Array.from(byUdn.values());
|
||||
this._save(d);
|
||||
return d.devices;
|
||||
}
|
||||
|
||||
// ── Disabled-rule backups ─────────────────────────────────────────────────
|
||||
|
||||
getDisabledRules() { return this._load().disabledRules ?? {}; }
|
||||
|
||||
@@ -122,6 +122,108 @@ 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');
|
||||
|
||||
console.log(`[DWM] Full BinaryState response object:`, res);
|
||||
console.log(`[DWM] Raw BinaryState string: "${raw}"`);
|
||||
|
||||
// For dimmers, check if brightness is available as a separate parameter
|
||||
// This matches the Python pywemo implementation
|
||||
if (res.brightness !== undefined) {
|
||||
const brightness = parseInt(res.brightness, 10);
|
||||
console.log(`[DWM] Brightness from separate parameter: ${brightness}`);
|
||||
return !isNaN(brightness) ? brightness : null;
|
||||
}
|
||||
|
||||
// Fallback: Check if 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('|');
|
||||
console.log(`[DWM] BinaryState parts:`, parts);
|
||||
if (parts.length >= 2) {
|
||||
const brightness = parseInt(parts[1], 10);
|
||||
console.log(`[DWM] Brightness from pipe format: ${brightness}`);
|
||||
return !isNaN(brightness) ? brightness : null;
|
||||
}
|
||||
}
|
||||
|
||||
// If device is on but no brightness info, assume 100%
|
||||
// If device is off, return 0
|
||||
const isOn = raw === '1' || raw === '8';
|
||||
console.log(`[DWM] No brightness info, using binary state: ${isOn ? 100 : 0} (raw: "${raw}")`);
|
||||
return isOn ? 100 : 0;
|
||||
} catch (err) {
|
||||
console.log(`[DWM] getBrightness failed: ${err.message}, falling back to binary state`);
|
||||
// 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)));
|
||||
|
||||
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 correct format: BinaryState=1, brightness=level
|
||||
// This matches the Python pywemo implementation
|
||||
try {
|
||||
console.log(`[DWM] Trying dimmer format: BinaryState=1, brightness=${level}`);
|
||||
await soapWithFallback(host, port, BE_URL, BE_SVC, 'SetBinaryState', {
|
||||
BinaryState: '1',
|
||||
brightness: level.toString()
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDimmerDevice(deviceInfo) {
|
||||
if (!deviceInfo) return false;
|
||||
|
||||
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
|
||||
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')) ||
|
||||
(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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Device info
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -478,6 +580,9 @@ function _insertNewRule(db, ruleId, ruleData) {
|
||||
module.exports = {
|
||||
getBinaryState,
|
||||
setBinaryState,
|
||||
getBrightness,
|
||||
setBrightness,
|
||||
isDimmerDevice,
|
||||
getDeviceInfo,
|
||||
discoverDevices,
|
||||
fetchRules,
|
||||
|
||||
+996
@@ -0,0 +1,996 @@
|
||||
{
|
||||
"name": "homebridge-dibby-wemo",
|
||||
"version": "file:../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "homebridge-dibby-wemo",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@homebridge/plugin-ui-utils": "^2.2.0",
|
||||
"adm-zip": "^0.5.14",
|
||||
"axios": "^1.7.0",
|
||||
"sql.js": "^1.12.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"homebridge": "^1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"homebridge": ">=1.6.0",
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@homebridge/ciao": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.5.tgz",
|
||||
"integrity": "sha512-f7MAw7YuoEYgJEQ1VyRcLHGuVmCpmXi65GVR8CAtPWPqIZf/HFr4vHzVpOfQMpEQw9Pt5uh07guuLt5HE8ruog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"ciao-bcs": "lib/bonjour-conformance-testing.js"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@homebridge/dbus-native": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.7.3.tgz",
|
||||
"integrity": "sha512-21RywQjjJPssTNdz9gZtKquShzlIz8hDQ1SGAYmOWdoRwSsi+znCt10LAjDSXB8AVVMwothTm0KQx2KJSBhLAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-stream": "^4.0.1",
|
||||
"hexy": "^0.3.5",
|
||||
"long": "^5.3.2",
|
||||
"minimist": "^1.2.8",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"bin": {
|
||||
"dbus2js": "bin/dbus2js.js"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@homebridge/plugin-ui-utils": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@homebridge/plugin-ui-utils/-/plugin-ui-utils-2.2.2.tgz",
|
||||
"integrity": "sha512-qXHK3DLBE1/CC5ATxM1w7XJikUfg3cUReU3hcOGhNpelgsYlgJUp303NFZQnvOw6t5/shKm7DvD4Xb8pEPdj2Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
"integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@oozcitak/dom": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz",
|
||||
"integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "^2.0.2",
|
||||
"@oozcitak/url": "^3.0.0",
|
||||
"@oozcitak/util": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@oozcitak/infra": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz",
|
||||
"integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/util": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@oozcitak/url": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz",
|
||||
"integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": "^2.0.2",
|
||||
"@oozcitak/util": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/@oozcitak/util": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz",
|
||||
"integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/adm-zip": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
|
||||
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/axios": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
|
||||
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/bonjour-hap": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.10.0.tgz",
|
||||
"integrity": "sha512-2/jKd5X4WgvRQXIjwMtc26nT1cuD0DbAwmVJ9vq/IVvWUNxL7Tce3valQUXnkQ0yL38/jSmrXAUL2nPGZB9hGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"multicast-dns": "^7.2.5",
|
||||
"multicast-dns-service-types": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/commander": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/dns-packet": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
|
||||
"integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@leichtgewicht/ip-codec": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/event-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexer": "^0.1.1",
|
||||
"from": "^0.1.7",
|
||||
"map-stream": "0.0.7",
|
||||
"pause-stream": "^0.0.11",
|
||||
"split": "^1.0.1",
|
||||
"stream-combiner": "^0.2.2",
|
||||
"through": "^2.3.8"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/fast-srp-hap": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz",
|
||||
"integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.17.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/from": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
|
||||
"integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/fs-extra": {
|
||||
"version": "11.3.4",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",
|
||||
"integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/futoin-hkdf": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz",
|
||||
"integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/hap-nodejs": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.14.2.tgz",
|
||||
"integrity": "sha512-hjV6WkV8xDQS6khGt2aIRMyt5yp+VHBUwZmPZH+thkBQH41VqDLfxUaGqX4M0yqvKxTpVWSQbSdldBLCCwIztA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@homebridge/ciao": "^1.3.5",
|
||||
"@homebridge/dbus-native": "^0.7.3",
|
||||
"bonjour-hap": "^3.10.0",
|
||||
"debug": "^4.4.3",
|
||||
"fast-srp-hap": "^2.0.4",
|
||||
"futoin-hkdf": "^1.5.3",
|
||||
"node-persist": "^0.0.12",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tslib": "^2.8.1",
|
||||
"tweetnacl": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || ^22 || ^24"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/hexy": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/hexy/-/hexy-0.3.5.tgz",
|
||||
"integrity": "sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"hexy": "bin/hexy_cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/homebridge": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.11.3.tgz",
|
||||
"integrity": "sha512-r6Fy8CVDH2OrhaHjiBMoVTyxvuSaTIVnNRnM7HiFPx5AeWW7Q7hkIwuctz9Ls46iR4+elushD4YoZkvdBUwNgg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"commander": "13.1.0",
|
||||
"fs-extra": "11.3.4",
|
||||
"hap-nodejs": "0.14.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"semver": "7.7.4",
|
||||
"source-map-support": "0.5.21"
|
||||
},
|
||||
"bin": {
|
||||
"homebridge": "bin/homebridge"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.15.0 || ^20.7.0 || ^22 || ^24"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/map-stream": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
|
||||
"integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/multicast-dns": {
|
||||
"version": "7.2.5",
|
||||
"resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
|
||||
"integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dns-packet": "^5.2.2",
|
||||
"thunky": "^1.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"multicast-dns": "cli.js"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/multicast-dns-service-types": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
|
||||
"integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/node-persist": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.12.tgz",
|
||||
"integrity": "sha512-Fbia3FYnURzaql53wLu0t19dmAwQg/tXT6O7YPmdwNwysNKEyFmgoT2BQlPD3XXQnYeiQVNvR5lfvufGwKuxhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mkdirp": "~0.5.1",
|
||||
"q": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/pause-stream": {
|
||||
"version": "0.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
|
||||
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
|
||||
"dev": true,
|
||||
"license": [
|
||||
"MIT",
|
||||
"Apache2"
|
||||
],
|
||||
"dependencies": {
|
||||
"through": "~2.3"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/q": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz",
|
||||
"integrity": "sha512-ROtylwux7Vkc4C07oKE/ReigUmb33kVoLtcR4SJ1QVqwaZkBEDL3vX4/kwFzIERQ5PfCl0XafbU8u2YUhyGgVA==",
|
||||
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0",
|
||||
"teleport": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/qrcode-terminal": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
|
||||
"integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"qrcode-terminal": "bin/qrcode-terminal.js"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/sax": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/split": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
|
||||
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"through": "2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/sql.js": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
|
||||
"integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/stream-combiner": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
|
||||
"integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexer": "~0.1.1",
|
||||
"through": "~2.3.4"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/thunky": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"dev": true,
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"../../../../../../../Claude/Wemo/wemo-manager/packages/homebridge-plugin/node_modules/xmlbuilder2": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz",
|
||||
"integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oozcitak/dom": "^2.0.2",
|
||||
"@oozcitak/infra": "^2.0.2",
|
||||
"@oozcitak/util": "^10.0.0",
|
||||
"js-yaml": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# @wemo-manager/core
|
||||
|
||||
**Shared Wemo protocol constants and utilities used by both the Dibby Wemo Manager desktop app and the Homebridge plugin.**
|
||||
|
||||
This is an internal package within the `dibby-wemo-manager` monorepo. It is not published to npm — both packages reference it via npm workspaces.
|
||||
|
||||
---
|
||||
|
||||
## What's in here
|
||||
|
||||
### Constants
|
||||
|
||||
| Export | Description |
|
||||
|---|---|
|
||||
| `DAY_NUMBERS` | Map of day names → Wemo day numbers (Monday=1 … Sunday=7) |
|
||||
| `DAY_NAMES` | Map of Wemo day numbers → full names |
|
||||
| `DAY_SHORT` | Map of Wemo day numbers → abbreviated names (Mon, Tue…) |
|
||||
| `RULE_TYPES` | Wemo firmware rule type strings (Schedule, Away, Countdown, Long Press) |
|
||||
| `ACTIONS` | StartAction / EndAction numeric codes (ON=1, OFF=0, TOGGLE=2, NONE=-1) |
|
||||
| `NETWORK_STATUS` | GetNetworkStatus response codes |
|
||||
| `RESET_CODES` | ReSetup action codes (clear data, factory reset, clear WiFi) |
|
||||
| `RD_DEFAULTS` | Default field values for a new RULEDEVICES row |
|
||||
| `SUN_CODES` | Sentinel values for sunrise (−2) and sunset (−3) in StartTime/EndTime |
|
||||
|
||||
### Helper functions
|
||||
|
||||
| Function | Signature | Description |
|
||||
|---|---|---|
|
||||
| `namesToDayNumbers` | `(names: string[]) => number[]` | Convert day name array to sorted Wemo day numbers |
|
||||
| `dayNumbersToNames` | `(numbers: number[]) => string[]` | Convert day numbers to full name array |
|
||||
| `dayNumbersToShort` | `(numbers: number[]) => string[]` | Convert day numbers to abbreviated name array |
|
||||
| `timeToSecs` | `(hhmm: string) => number` | Parse `"HH:MM"` to seconds from midnight |
|
||||
| `secsToHHMM` | `(secs: number) => string` | Format seconds from midnight to `"HH:MM"` |
|
||||
| `sunTimes` | `(lat, lng, date?) => { sunrise, sunset }` | Calculate sunrise/sunset as seconds from midnight |
|
||||
|
||||
---
|
||||
|
||||
## Wemo day number convention
|
||||
|
||||
Wemo devices use **1-based day numbers**, not bitmasks:
|
||||
|
||||
| Number | Day |
|
||||
|---|---|
|
||||
| 1 | Monday |
|
||||
| 2 | Tuesday |
|
||||
| 3 | Wednesday |
|
||||
| 4 | Thursday |
|
||||
| 5 | Friday |
|
||||
| 6 | Saturday |
|
||||
| 7 | Sunday |
|
||||
|
||||
Multi-day rules have one `RULEDEVICES` row **per day** — not a single row with a bitmask.
|
||||
|
||||
---
|
||||
|
||||
## Sun time sentinel codes
|
||||
|
||||
When a rule's `StartTime` or `EndTime` is set to one of these values, the scheduler resolves it to the actual sunrise/sunset time for the configured location:
|
||||
|
||||
| Constant | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `SUN_CODES.SUNRISE` | `-2` | Use today's sunrise time |
|
||||
| `SUN_CODES.SUNSET` | `-3` | Use today's sunset time |
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const {
|
||||
DAY_NUMBERS,
|
||||
namesToDayNumbers,
|
||||
secsToHHMM,
|
||||
sunTimes,
|
||||
SUN_CODES,
|
||||
} = require('@wemo-manager/core');
|
||||
|
||||
// Convert user-selected days to Wemo day numbers
|
||||
const days = namesToDayNumbers(['Monday', 'Wednesday', 'Friday']);
|
||||
// → [1, 3, 5]
|
||||
|
||||
// Format a time stored as seconds from midnight
|
||||
console.log(secsToHHMM(75600)); // "21:00" (9 PM)
|
||||
|
||||
// Check if a StartTime is a sunrise/sunset sentinel
|
||||
if (startTime === SUN_CODES.SUNRISE) {
|
||||
const { sunrise } = sunTimes(lat, lng);
|
||||
startTime = sunrise;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
packages/wemo-core/
|
||||
├── package.json
|
||||
└── src/
|
||||
├── index.js — re-exports everything from sun.js and types.js
|
||||
├── types.js — constants + day/time helper functions
|
||||
└── sun.js — NOAA sunrise/sunset calculator (pure JS, no deps)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **No external dependencies** — pure JavaScript, no npm packages required.
|
||||
- `sun.js` implements the NOAA Solar Calculator algorithm (Jean Meeus, *Astronomical Algorithms*). It returns `null` for each value during polar day/polar night.
|
||||
- `secsToHHMM` returns `"00:00"` for negative input (used for no-time sentinel values like `−1`).
|
||||
- The DWM scheduler in both the desktop app and the Homebridge plugin uses `SUN_CODES` to resolve rule times at tick time, so sunrise/sunset-based rules automatically adjust every day.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,51 @@
|
||||
# Dibby Wemo Manager - Web Deployment Script (PowerShell)
|
||||
# This script builds and deploys the web version using Docker Compose
|
||||
|
||||
Write-Host "🚀 Deploying Dibby Wemo Manager (Web Version)..." -ForegroundColor Green
|
||||
|
||||
# Check if Docker is running
|
||||
try {
|
||||
docker info > $null 2>&1
|
||||
} catch {
|
||||
Write-Host "❌ Docker is not running. Please start Docker Desktop first." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build the Docker image
|
||||
Write-Host "📦 Building Docker image..." -ForegroundColor Blue
|
||||
docker build -t reg.dev.nervesocket.com/dibbly:latest .
|
||||
|
||||
# Stop existing container if running
|
||||
Write-Host "🛑 Stopping existing container..." -ForegroundColor Yellow
|
||||
docker-compose -f web-compose.yml down 2>$null
|
||||
|
||||
# Start the service
|
||||
Write-Host "🔄 Starting web service..." -ForegroundColor Blue
|
||||
docker-compose -f web-compose.yml up -d
|
||||
|
||||
# Wait for service to be ready
|
||||
Write-Host "⏳ Waiting for service to be ready..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 10
|
||||
|
||||
# Check if service is running
|
||||
$serviceStatus = docker-compose -f web-compose.yml ps
|
||||
if ($serviceStatus -match "Up") {
|
||||
Write-Host "✅ Dibby Wemo Manager is now running!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "🌐 Access the web interface at:" -ForegroundColor Cyan
|
||||
Write-Host " http://localhost:3456"
|
||||
Write-Host ""
|
||||
Write-Host "📱 Mobile-friendly URL:" -ForegroundColor Cyan
|
||||
$localIP = (Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias "Ethernet*","Wi-Fi*" | Where-Object { $_.IPAddress -notlike "169.*" -and $_.IPAddress -notlike "127.*" } | Select-Object -First 1).IPAddress
|
||||
Write-Host " http://$($localIP):3456"
|
||||
Write-Host ""
|
||||
Write-Host "📊 View logs:" -ForegroundColor Cyan
|
||||
Write-Host " docker-compose -f web-compose.yml logs -f"
|
||||
Write-Host ""
|
||||
Write-Host "🛑 Stop service:" -ForegroundColor Cyan
|
||||
Write-Host " docker-compose -f web-compose.yml down"
|
||||
} else {
|
||||
Write-Host "❌ Failed to start the service. Check logs:" -ForegroundColor Red
|
||||
docker-compose -f web-compose.yml logs
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Dibby Wemo Manager - Web Deployment Script
|
||||
# This script builds and deploys the web version using Docker Compose
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Deploying Dibby Wemo Manager (Web Version)..."
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "❌ Docker is not running. Please start Docker first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the Docker image (if needed)
|
||||
echo "📦 Building Docker image..."
|
||||
docker build -t reg.dev.nervesocket.com/dibbly:latest .
|
||||
|
||||
# Stop existing container if running
|
||||
echo "🛑 Stopping existing container..."
|
||||
docker-compose -f web-compose.yml down || true
|
||||
|
||||
# Start the service
|
||||
echo "🔄 Starting web service..."
|
||||
docker-compose -f web-compose.yml up -d
|
||||
|
||||
# Wait for service to be ready
|
||||
echo "⏳ Waiting for service to be ready..."
|
||||
sleep 10
|
||||
|
||||
# Check if service is running
|
||||
if docker-compose -f web-compose.yml ps | grep -q "Up"; then
|
||||
echo "✅ Dibby Wemo Manager is now running!"
|
||||
echo ""
|
||||
echo "🌐 Access the web interface at:"
|
||||
echo " http://localhost:3456"
|
||||
echo ""
|
||||
echo "📱 Mobile-friendly URL:"
|
||||
echo " http://$(hostname -I | awk '{print $1}'):3456"
|
||||
echo ""
|
||||
echo "📊 View logs:"
|
||||
echo " docker-compose -f web-compose.yml logs -f"
|
||||
echo ""
|
||||
echo "🛑 Stop service:"
|
||||
echo " docker-compose -f web-compose.yml down"
|
||||
else
|
||||
echo "❌ Failed to start the service. Check logs:"
|
||||
docker-compose -f web-compose.yml logs
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,24 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
dibbly-web:
|
||||
image: reg.dev.nervesocket.com/dibbly:latest
|
||||
container_name: dibbly-wemo-manager
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3456:3456"
|
||||
volumes:
|
||||
- dibbly-data:/data
|
||||
environment:
|
||||
- DATA_DIR=/data
|
||||
- PORT=3456
|
||||
networks:
|
||||
- dibbly-network
|
||||
|
||||
volumes:
|
||||
dibbly-data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
dibbly-network:
|
||||
driver: bridge
|
||||
Reference in New Issue
Block a user