Initial ComfyUI Hue node release
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
hue_config.json
|
||||
.DS_Store
|
||||
@@ -0,0 +1,314 @@
|
||||
# Niutonan_Comfyui_Hue
|
||||
|
||||
ComfyUI custom nodes that sample the edge colors of generated images and send
|
||||
matching colors to Philips Hue lights or Hue light strips.
|
||||
|
||||
Finally, ComfyUI is entering the physical world. Now you can control your home
|
||||
lights directly from a ComfyUI workflow.
|
||||
|
||||
It is easy: generate an image, let the node read the image edges, and have your
|
||||
lights match the mood of the result.
|
||||
|
||||
The package includes a full advanced node, a simpler everyday node, and a Hue
|
||||
setup node for bridge registration and light discovery.
|
||||
|
||||
## Nodes
|
||||
|
||||
### Niutonan: Hue Setup
|
||||
|
||||
Use this node to find your Hue bridge, register an API key, list lights, and
|
||||
test the connection.
|
||||
|
||||
Actions:
|
||||
|
||||
- `auto_scan`: finds Hue bridge IP addresses.
|
||||
- `register_new`: registers this ComfyUI node with a Hue bridge.
|
||||
- `check_connection`: checks the saved API key.
|
||||
- `list_lights`: lists numeric Hue light IDs.
|
||||
- `test_flash`: flashes all lights on the selected bridge.
|
||||
|
||||
### Niutonan: ComfyUI Hue Edge Bar simple
|
||||
|
||||
Recommended node for normal use.
|
||||
|
||||
Inputs:
|
||||
|
||||
- `image`: ComfyUI image input.
|
||||
- `bridge_ip`: Hue bridge IP, for example `192.168.0.57`.
|
||||
- `api_key`: optional. Leave blank after setup.
|
||||
- `light_id`: Hue light ID, `all`, or `group:<id>`.
|
||||
- `mode`: `send`, `preview_only`, or `turn_off`.
|
||||
- `brightness`: Hue brightness from `1` to `254`.
|
||||
- `edge_width`: how many pixels from the image border to sample.
|
||||
- `transitiontime`: fade time in 1/10 second units. `6` means 0.6 seconds.
|
||||
|
||||
The simple node uses good defaults internally:
|
||||
|
||||
- Hue `xy` color for better color matching.
|
||||
- Dominant edge color selection.
|
||||
- Mild border filtering.
|
||||
- Slight crop to avoid hard image borders.
|
||||
- Mild vibrance and saturation shaping.
|
||||
|
||||
### Niutonan: ComfyUI Hue Edge Bar
|
||||
|
||||
Advanced node with additional controls:
|
||||
|
||||
- `color_api`: `xy` or `hue_sat`.
|
||||
- `color_pick_mode`: `average`, `weighted_average`, `dominant`, `brightest`.
|
||||
- `brightness_mode`: `fixed`, `from_image`, `from_image_clamped`.
|
||||
- `crop_percent`
|
||||
- `ignore_dark_below`
|
||||
- `ignore_bright_above`
|
||||
- `saturation_boost`
|
||||
- `vibrance`
|
||||
- `min_saturation`
|
||||
- `max_saturation`
|
||||
- `top_weight`, `right_weight`, `bottom_weight`, `left_weight`
|
||||
- `transitiontime`
|
||||
|
||||
## Installation
|
||||
|
||||
Copy this folder into your ComfyUI custom nodes directory:
|
||||
|
||||
```text
|
||||
ComfyUI/custom_nodes/Niutonan_Comfyui_Hue
|
||||
```
|
||||
|
||||
For ComfyUI portable on Windows, that often looks like:
|
||||
|
||||
```text
|
||||
ComfyUI_windows_portable/ComfyUI/custom_nodes/Niutonan_Comfyui_Hue
|
||||
```
|
||||
|
||||
Restart ComfyUI after copying the folder.
|
||||
|
||||
No extra Python packages are required.
|
||||
|
||||
## Step-by-Step Hue Setup
|
||||
|
||||
### 1. Find Your Hue Bridge
|
||||
|
||||
Add the node:
|
||||
|
||||
```text
|
||||
Niutonan: Hue Setup
|
||||
```
|
||||
|
||||
Set:
|
||||
|
||||
```text
|
||||
bridge_ip = auto
|
||||
action = auto_scan
|
||||
```
|
||||
|
||||
Queue the prompt.
|
||||
|
||||
The output should look similar to:
|
||||
|
||||
```text
|
||||
Found Hue bridge(s):
|
||||
192.168.0.57 (meethue)
|
||||
```
|
||||
|
||||
If more than one bridge is found, choose the one that controls your Hue strip or
|
||||
room lights. Put that exact IP into `bridge_ip` for the next steps.
|
||||
|
||||
### 2. Register ComfyUI With the Hue Bridge
|
||||
|
||||
Press the physical button on top of the Philips Hue Bridge.
|
||||
|
||||
Then run `Niutonan: Hue Setup` with:
|
||||
|
||||
```text
|
||||
bridge_ip = 192.168.0.57
|
||||
action = register_new
|
||||
```
|
||||
|
||||
Replace `192.168.0.57` with your bridge IP.
|
||||
|
||||
If registration succeeds, the node saves an API key in:
|
||||
|
||||
```text
|
||||
Niutonan_Comfyui_Hue/hue_config.json
|
||||
```
|
||||
|
||||
Do not upload `hue_config.json` to GitHub. It is ignored by this repository's
|
||||
`.gitignore`.
|
||||
|
||||
### 3. List Your Hue Lights
|
||||
|
||||
Run `Niutonan: Hue Setup` with:
|
||||
|
||||
```text
|
||||
bridge_ip = 192.168.0.57
|
||||
action = list_lights
|
||||
```
|
||||
|
||||
The output will include light IDs:
|
||||
|
||||
```text
|
||||
1: Hue lightstrip plus [ON]
|
||||
2: Desk lamp [OFF]
|
||||
3: Hue play bar [ON]
|
||||
```
|
||||
|
||||
Write down the numeric ID for the light or strip you want to control.
|
||||
|
||||
### 4. Test the Bridge
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
bridge_ip = 192.168.0.57
|
||||
action = test_flash
|
||||
```
|
||||
|
||||
Your Hue lights should flash briefly.
|
||||
|
||||
### 5. Use the Simple Edge Bar Node
|
||||
|
||||
Add:
|
||||
|
||||
```text
|
||||
Niutonan: ComfyUI Hue Edge Bar simple
|
||||
```
|
||||
|
||||
Connect your generated image to the `image` input.
|
||||
|
||||
Recommended settings:
|
||||
|
||||
```text
|
||||
bridge_ip = 192.168.0.57
|
||||
api_key =
|
||||
light_id = 1
|
||||
mode = send
|
||||
brightness = 180
|
||||
edge_width = 32
|
||||
transitiontime = 6
|
||||
```
|
||||
|
||||
Leave `api_key` blank. The node loads the saved key from `hue_config.json`.
|
||||
|
||||
Use your actual `light_id` from the `list_lights` step.
|
||||
|
||||
## Controlling Multiple Lights
|
||||
|
||||
The `light_id` field accepts:
|
||||
|
||||
```text
|
||||
1
|
||||
```
|
||||
|
||||
Controls a specific light by ID.
|
||||
|
||||
```text
|
||||
all
|
||||
```
|
||||
|
||||
Controls all lights on the bridge.
|
||||
|
||||
```text
|
||||
group:3
|
||||
```
|
||||
|
||||
Controls Hue group ID `3`.
|
||||
|
||||
To find group IDs, use the Hue app or the Hue API. The setup node currently
|
||||
lists lights, not groups.
|
||||
|
||||
## Recommended Settings
|
||||
|
||||
For most users:
|
||||
|
||||
```text
|
||||
Node: Niutonan: ComfyUI Hue Edge Bar simple
|
||||
mode = send
|
||||
brightness = 160 to 210
|
||||
edge_width = 24 to 48
|
||||
transitiontime = 6 to 12
|
||||
```
|
||||
|
||||
If the light feels too jumpy, increase `transitiontime`.
|
||||
|
||||
If colors feel too influenced by black image borders, increase `edge_width` or
|
||||
use the advanced node with `ignore_dark_below`.
|
||||
|
||||
If colors feel too gray, use the advanced node and increase `vibrance` or
|
||||
`saturation_boost`.
|
||||
|
||||
## How Color Output Works
|
||||
|
||||
The node samples colors around the image perimeter and builds a virtual LED bar.
|
||||
Standard Philips Hue v1 light control accepts one active color per light or
|
||||
group, so the node sends one selected edge color to the selected Hue light.
|
||||
|
||||
The node also returns:
|
||||
|
||||
- `edge_bar_json`: the virtual LED bar and debug data.
|
||||
- `average_rgb`: the final RGB color sent through the Hue conversion.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Auto Scan Finds Multiple Bridges
|
||||
|
||||
Use the exact bridge IP instead of `auto`.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
bridge_ip = 192.168.0.57
|
||||
```
|
||||
|
||||
### Register Says to Press the Button
|
||||
|
||||
Press the physical Hue Bridge button and run `register_new` again within about
|
||||
30 seconds.
|
||||
|
||||
### No API Key Found
|
||||
|
||||
Run:
|
||||
|
||||
```text
|
||||
Niutonan: Hue Setup
|
||||
action = register_new
|
||||
```
|
||||
|
||||
Then leave `api_key` blank in the edge bar node.
|
||||
|
||||
### Light Does Not Change Color
|
||||
|
||||
Check:
|
||||
|
||||
- The bridge IP is correct.
|
||||
- The `light_id` matches the Hue strip or light.
|
||||
- `mode` is set to `send`.
|
||||
- The light is reachable in the Hue app.
|
||||
- `test_flash` works from the setup node.
|
||||
|
||||
### Colors Look Wrong
|
||||
|
||||
Try:
|
||||
|
||||
- Use `Niutonan: ComfyUI Hue Edge Bar simple` first.
|
||||
- Increase `transitiontime` for smoother fades.
|
||||
- Try the advanced node with `color_pick_mode = weighted_average`.
|
||||
- Use `color_api = xy` for better Hue color matching.
|
||||
|
||||
## Security Notes
|
||||
|
||||
The Hue API key allows local control of your Hue bridge. Keep this file private:
|
||||
|
||||
```text
|
||||
hue_config.json
|
||||
```
|
||||
|
||||
Do not commit it to GitHub.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Philips Hue Bridge local v1 API.
|
||||
- ComfyUI custom node system.
|
||||
- Windows portable ComfyUI and normal ComfyUI installs.
|
||||
|
||||
No third-party Python dependencies are required.
|
||||
+822
@@ -0,0 +1,822 @@
|
||||
"""Niutonan ComfyUI Hue nodes.
|
||||
|
||||
Extracts edge colors from generated images and sends the averaged edge color
|
||||
to a Philips Hue bridge using the local Hue v1 API.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import colorsys
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from ipaddress import ip_network
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
HUE_CONFIG_FILE = os.path.join(THIS_DIR, "hue_config.json")
|
||||
|
||||
|
||||
def load_hue_config() -> dict[str, Any]:
|
||||
if os.path.exists(HUE_CONFIG_FILE):
|
||||
try:
|
||||
with open(HUE_CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as exc:
|
||||
print(f"[Niutonan Hue] Failed to load config: {exc}")
|
||||
return {}
|
||||
|
||||
|
||||
def save_hue_config(config: dict[str, Any]) -> bool:
|
||||
try:
|
||||
with open(HUE_CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"[Niutonan Hue] Failed to save config: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def get_hue_api_key(bridge_ip: str) -> str:
|
||||
value = normalize_bridge_ip(bridge_ip)
|
||||
if value.lower() in {"auto", "scan", "discover"}:
|
||||
resolved_ip, _ = resolve_bridge_ip(value)
|
||||
value = resolved_ip or value
|
||||
return load_hue_config().get(value, {}).get("api_key", "")
|
||||
|
||||
|
||||
def normalize_bridge_ip(bridge_ip: str) -> str:
|
||||
return str(bridge_ip or "").strip()
|
||||
|
||||
|
||||
def discover_hue_bridges(timeout: float = 2.0) -> list[dict[str, str]]:
|
||||
bridges: dict[str, dict[str, str]] = {}
|
||||
|
||||
for bridge in discover_hue_bridges_cloud(timeout=timeout):
|
||||
bridges[bridge["ip"]] = bridge
|
||||
|
||||
for bridge in discover_hue_bridges_ssdp(timeout=timeout):
|
||||
bridges.setdefault(bridge["ip"], bridge)
|
||||
|
||||
if not bridges:
|
||||
for bridge in discover_hue_bridges_subnet(timeout=timeout):
|
||||
bridges.setdefault(bridge["ip"], bridge)
|
||||
|
||||
return sorted(bridges.values(), key=lambda item: item["ip"])
|
||||
|
||||
|
||||
def discover_hue_bridges_cloud(timeout: float = 2.0) -> list[dict[str, str]]:
|
||||
try:
|
||||
response = urllib.request.urlopen("https://discovery.meethue.com/", timeout=timeout)
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
bridges = []
|
||||
for item in result if isinstance(result, list) else []:
|
||||
ip = item.get("internalipaddress")
|
||||
if ip:
|
||||
bridges.append({"ip": ip, "id": item.get("id", ""), "source": "meethue"})
|
||||
return bridges
|
||||
except Exception as exc:
|
||||
print(f"[Niutonan Hue] Cloud discovery failed: {exc}")
|
||||
return []
|
||||
|
||||
|
||||
def discover_hue_bridges_ssdp(timeout: float = 2.0) -> list[dict[str, str]]:
|
||||
message = "\r\n".join(
|
||||
[
|
||||
"M-SEARCH * HTTP/1.1",
|
||||
"HOST: 239.255.255.250:1900",
|
||||
'MAN: "ssdp:discover"',
|
||||
"MX: 1",
|
||||
"ST: ssdp:all",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
).encode("ascii")
|
||||
bridges: dict[str, dict[str, str]] = {}
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
try:
|
||||
sock.settimeout(0.4)
|
||||
sock.sendto(message, ("239.255.255.250", 1900))
|
||||
end_time = time.time() + timeout
|
||||
while time.time() < end_time:
|
||||
try:
|
||||
data, addr = sock.recvfrom(4096)
|
||||
except socket.timeout:
|
||||
continue
|
||||
text = data.decode("utf-8", errors="ignore").lower()
|
||||
if "hue" in text or "ipbridge" in text or "philips" in text:
|
||||
bridges[addr[0]] = {"ip": addr[0], "id": "", "source": "ssdp"}
|
||||
except Exception as exc:
|
||||
print(f"[Niutonan Hue] SSDP discovery failed: {exc}")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
return list(bridges.values())
|
||||
|
||||
|
||||
def get_local_ipv4() -> str | None:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
sock.connect(("8.8.8.8", 80))
|
||||
return sock.getsockname()[0]
|
||||
except Exception:
|
||||
try:
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def is_hue_bridge_at(ip: str, timeout: float = 0.35) -> bool:
|
||||
try:
|
||||
response = urllib.request.urlopen(f"http://{ip}/description.xml", timeout=timeout)
|
||||
text = response.read(8192).decode("utf-8", errors="ignore").lower()
|
||||
return "philips hue" in text or "ipbridge" in text or "hue bridge" in text
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def discover_hue_bridges_subnet(timeout: float = 2.0) -> list[dict[str, str]]:
|
||||
local_ip = get_local_ipv4()
|
||||
if not local_ip:
|
||||
return []
|
||||
|
||||
network = ip_network(f"{local_ip}/24", strict=False)
|
||||
candidates = [str(ip) for ip in network.hosts()]
|
||||
found: list[dict[str, str]] = []
|
||||
deadline = time.time() + timeout
|
||||
|
||||
with ThreadPoolExecutor(max_workers=32) as executor:
|
||||
futures = {executor.submit(is_hue_bridge_at, ip): ip for ip in candidates}
|
||||
for future in as_completed(futures):
|
||||
if time.time() > deadline and found:
|
||||
break
|
||||
ip = futures[future]
|
||||
try:
|
||||
if future.result():
|
||||
found.append({"ip": ip, "id": "", "source": "subnet"})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def resolve_bridge_ip(bridge_ip: str) -> tuple[str | None, str | None]:
|
||||
value = normalize_bridge_ip(bridge_ip)
|
||||
if value and value.lower() not in {"auto", "scan", "discover"}:
|
||||
return value, None
|
||||
|
||||
bridges = discover_hue_bridges(timeout=2.0)
|
||||
if not bridges:
|
||||
return None, "No Hue bridge found. Enter bridge_ip manually or check that the bridge is on this network."
|
||||
if len(bridges) > 1:
|
||||
lines = [f"{bridge['ip']} ({bridge['source']})" for bridge in bridges]
|
||||
return None, "Multiple Hue bridges found. Put one of these in bridge_ip:\n" + "\n".join(lines)
|
||||
return bridges[0]["ip"], None
|
||||
|
||||
|
||||
def register_hue_user(bridge_ip: str) -> tuple[str | None, str | None]:
|
||||
bridge_ip, error = resolve_bridge_ip(bridge_ip)
|
||||
if not bridge_ip:
|
||||
return None, error
|
||||
|
||||
try:
|
||||
url = f"http://{bridge_ip}/api"
|
||||
data = json.dumps({"devicetype": "niutonan_comfyui_hue#comfyui"}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, method="POST")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
response = urllib.request.urlopen(req, timeout=10)
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
if result and isinstance(result, list):
|
||||
item = result[0]
|
||||
if "success" in item:
|
||||
api_key = item["success"]["username"]
|
||||
config = load_hue_config()
|
||||
config[bridge_ip] = {"api_key": api_key}
|
||||
save_hue_config(config)
|
||||
return api_key, None
|
||||
if "error" in item:
|
||||
error = item["error"]
|
||||
if error.get("type") == 101:
|
||||
return None, "Press the Hue bridge button, then run register again."
|
||||
return None, error.get("description", "Unknown Hue bridge error")
|
||||
return None, "Unexpected Hue bridge response"
|
||||
except Exception as exc:
|
||||
return None, str(exc)
|
||||
|
||||
|
||||
def get_hue_lights(bridge_ip: str, api_key: str) -> dict[str, Any]:
|
||||
bridge_ip, error = resolve_bridge_ip(bridge_ip)
|
||||
if not bridge_ip:
|
||||
print(f"[Niutonan Hue] {error}")
|
||||
return {}
|
||||
|
||||
try:
|
||||
url = f"http://{bridge_ip}/api/{api_key}/lights"
|
||||
response = urllib.request.urlopen(urllib.request.Request(url), timeout=5)
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
return result if isinstance(result, dict) else {}
|
||||
except Exception as exc:
|
||||
print(f"[Niutonan Hue] Failed to list lights: {exc}")
|
||||
return {}
|
||||
|
||||
|
||||
def send_hue_command(bridge_ip: str, api_key: str, light_id: str, command: dict[str, Any]) -> bool:
|
||||
bridge_ip, error = resolve_bridge_ip(bridge_ip)
|
||||
if not bridge_ip:
|
||||
print(f"[Niutonan Hue] {error}")
|
||||
return False
|
||||
|
||||
try:
|
||||
target = str(light_id).strip()
|
||||
if target.lower() == "all":
|
||||
url = f"http://{bridge_ip}/api/{api_key}/groups/0/action"
|
||||
elif target.lower().startswith("group:"):
|
||||
group_id = target.split(":", 1)[1]
|
||||
url = f"http://{bridge_ip}/api/{api_key}/groups/{group_id}/action"
|
||||
else:
|
||||
url = f"http://{bridge_ip}/api/{api_key}/lights/{target}/state"
|
||||
|
||||
data = json.dumps(command).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, method="PUT")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
response = urllib.request.urlopen(req, timeout=5)
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
print(f"[Niutonan Hue] Hue response: {result}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f"[Niutonan Hue] Hue command failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def tensor_to_rgb_uint8(image: Any, batch_index: int = 0) -> np.ndarray:
|
||||
img_tensor = image[min(max(batch_index, 0), image.shape[0] - 1)]
|
||||
img_np = (img_tensor.cpu().numpy() * 255.0).clip(0, 255).astype(np.uint8)
|
||||
|
||||
if img_np.ndim == 2:
|
||||
img_np = np.stack([img_np, img_np, img_np], axis=-1)
|
||||
elif img_np.shape[-1] == 1:
|
||||
img_np = np.repeat(img_np, 3, axis=-1)
|
||||
elif img_np.shape[-1] > 3:
|
||||
img_np = img_np[..., :3]
|
||||
return img_np
|
||||
|
||||
|
||||
def crop_image(img_np: np.ndarray, crop_percent: float) -> np.ndarray:
|
||||
crop_percent = max(0.0, min(float(crop_percent), 45.0))
|
||||
if crop_percent <= 0:
|
||||
return img_np
|
||||
|
||||
height, width, _ = img_np.shape
|
||||
crop_x = int(round(width * (crop_percent / 100.0)))
|
||||
crop_y = int(round(height * (crop_percent / 100.0)))
|
||||
if crop_x * 2 >= width or crop_y * 2 >= height:
|
||||
return img_np
|
||||
return img_np[crop_y : height - crop_y, crop_x : width - crop_x, :]
|
||||
|
||||
|
||||
def filter_pixels(samples: np.ndarray, ignore_dark_below: int, ignore_bright_above: int) -> np.ndarray:
|
||||
if samples.size == 0:
|
||||
return samples
|
||||
|
||||
pixels = samples.reshape(-1, 3)
|
||||
luminance = pixels[:, 0] * 0.2126 + pixels[:, 1] * 0.7152 + pixels[:, 2] * 0.0722
|
||||
dark_limit = max(0, min(int(ignore_dark_below), 255))
|
||||
bright_limit = max(0, min(int(ignore_bright_above), 255))
|
||||
if bright_limit <= 0:
|
||||
bright_limit = 255
|
||||
|
||||
mask = (luminance >= dark_limit) & (luminance <= bright_limit)
|
||||
filtered = pixels[mask]
|
||||
return filtered if filtered.size else pixels
|
||||
|
||||
|
||||
def edge_segments(img_np: np.ndarray, edge_width: int) -> dict[str, np.ndarray]:
|
||||
height, width, _ = img_np.shape
|
||||
edge_width = max(1, min(edge_width, max(1, min(width, height) // 2)))
|
||||
|
||||
return {
|
||||
"top": img_np[:edge_width, :, :].mean(axis=0),
|
||||
"right": img_np[:, width - edge_width :, :].mean(axis=1),
|
||||
"bottom": img_np[height - edge_width :, :, :].mean(axis=0)[::-1],
|
||||
"left": img_np[:, :edge_width, :].mean(axis=1)[::-1],
|
||||
}
|
||||
|
||||
|
||||
def perimeter_samples(
|
||||
img_np: np.ndarray,
|
||||
edge_width: int,
|
||||
crop_percent: float = 0.0,
|
||||
ignore_dark_below: int = 0,
|
||||
ignore_bright_above: int = 255,
|
||||
) -> np.ndarray:
|
||||
cropped = crop_image(img_np, crop_percent)
|
||||
segments = edge_segments(cropped, edge_width)
|
||||
samples = [
|
||||
filter_pixels(segment, ignore_dark_below, ignore_bright_above)
|
||||
for segment in (segments["top"], segments["right"], segments["bottom"], segments["left"])
|
||||
]
|
||||
return np.concatenate(samples, axis=0)
|
||||
|
||||
|
||||
def weighted_edge_average(
|
||||
img_np: np.ndarray,
|
||||
edge_width: int,
|
||||
crop_percent: float,
|
||||
ignore_dark_below: int,
|
||||
ignore_bright_above: int,
|
||||
top_weight: float,
|
||||
right_weight: float,
|
||||
bottom_weight: float,
|
||||
left_weight: float,
|
||||
) -> tuple[int, int, int]:
|
||||
cropped = crop_image(img_np, crop_percent)
|
||||
segments = edge_segments(cropped, edge_width)
|
||||
weighted_colors = []
|
||||
weights = []
|
||||
for name, weight in (
|
||||
("top", top_weight),
|
||||
("right", right_weight),
|
||||
("bottom", bottom_weight),
|
||||
("left", left_weight),
|
||||
):
|
||||
weight = max(0.0, float(weight))
|
||||
if weight <= 0:
|
||||
continue
|
||||
pixels = filter_pixels(segments[name], ignore_dark_below, ignore_bright_above)
|
||||
weighted_colors.append(pixels.mean(axis=0))
|
||||
weights.append(weight)
|
||||
|
||||
if not weighted_colors:
|
||||
return average_rgb(make_led_bar(cropped, edge_width=edge_width, led_count=1))
|
||||
|
||||
rgb = np.average(np.array(weighted_colors), axis=0, weights=np.array(weights))
|
||||
rgb = rgb.clip(0, 255).round().astype(np.uint8).tolist()
|
||||
return int(rgb[0]), int(rgb[1]), int(rgb[2])
|
||||
|
||||
|
||||
def make_led_bar(
|
||||
img_np: np.ndarray,
|
||||
edge_width: int,
|
||||
led_count: int,
|
||||
crop_percent: float = 0.0,
|
||||
ignore_dark_below: int = 0,
|
||||
ignore_bright_above: int = 255,
|
||||
) -> list[list[int]]:
|
||||
samples = perimeter_samples(img_np, edge_width, crop_percent, ignore_dark_below, ignore_bright_above)
|
||||
led_count = max(1, int(led_count))
|
||||
chunks = np.array_split(samples, led_count)
|
||||
colors = [chunk.mean(axis=0).clip(0, 255).round().astype(np.uint8).tolist() for chunk in chunks]
|
||||
return colors
|
||||
|
||||
|
||||
def average_rgb(colors: list[list[int]]) -> tuple[int, int, int]:
|
||||
arr = np.array(colors, dtype=np.float32)
|
||||
rgb = arr.mean(axis=0).clip(0, 255).round().astype(np.uint8).tolist()
|
||||
return int(rgb[0]), int(rgb[1]), int(rgb[2])
|
||||
|
||||
|
||||
def dominant_rgb(colors: list[list[int]]) -> tuple[int, int, int]:
|
||||
pixels = np.array(colors, dtype=np.uint8).reshape(-1, 3)
|
||||
if pixels.size == 0:
|
||||
return 0, 0, 0
|
||||
|
||||
bins = (pixels.astype(np.uint16) // 32).astype(np.uint16)
|
||||
keys, inverse, counts = np.unique(bins, axis=0, return_inverse=True, return_counts=True)
|
||||
best_index = int(np.argmax(counts))
|
||||
cluster = pixels[inverse == best_index]
|
||||
rgb = cluster.mean(axis=0).clip(0, 255).round().astype(np.uint8).tolist()
|
||||
return int(rgb[0]), int(rgb[1]), int(rgb[2])
|
||||
|
||||
|
||||
def brightest_rgb(colors: list[list[int]]) -> tuple[int, int, int]:
|
||||
pixels = np.array(colors, dtype=np.uint8).reshape(-1, 3)
|
||||
luminance = pixels[:, 0] * 0.2126 + pixels[:, 1] * 0.7152 + pixels[:, 2] * 0.0722
|
||||
rgb = pixels[int(np.argmax(luminance))].tolist()
|
||||
return int(rgb[0]), int(rgb[1]), int(rgb[2])
|
||||
|
||||
|
||||
def choose_rgb(
|
||||
led_bar: list[list[int]],
|
||||
color_pick_mode: str,
|
||||
weighted_rgb: tuple[int, int, int],
|
||||
) -> tuple[int, int, int]:
|
||||
if color_pick_mode == "dominant":
|
||||
return dominant_rgb(led_bar)
|
||||
if color_pick_mode == "brightest":
|
||||
return brightest_rgb(led_bar)
|
||||
if color_pick_mode == "weighted_average":
|
||||
return weighted_rgb
|
||||
return average_rgb(led_bar)
|
||||
|
||||
|
||||
def apply_color_shaping(
|
||||
rgb: tuple[int, int, int],
|
||||
saturation_boost: float,
|
||||
min_saturation: float,
|
||||
max_saturation: float,
|
||||
vibrance: float,
|
||||
) -> tuple[int, int, int]:
|
||||
r, g, b = [channel / 255.0 for channel in rgb]
|
||||
h, s, v = colorsys.rgb_to_hsv(r, g, b)
|
||||
shaped_boost = max(0.0, saturation_boost) + (max(0.0, vibrance) * (1.0 - s))
|
||||
s = s * shaped_boost
|
||||
s = max(0.0, min(1.0, s))
|
||||
s = max(max(0.0, min_saturation / 100.0), s)
|
||||
s = min(max(0.0, min(max_saturation / 100.0, 1.0)), s)
|
||||
boosted = colorsys.hsv_to_rgb(h, s, v)
|
||||
return tuple(int(round(channel * 255.0)) for channel in boosted)
|
||||
|
||||
|
||||
def rgb_luminance(rgb: tuple[int, int, int]) -> float:
|
||||
return rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722
|
||||
|
||||
|
||||
def choose_brightness(
|
||||
rgb: tuple[int, int, int],
|
||||
brightness_mode: str,
|
||||
brightness: int,
|
||||
min_brightness: int,
|
||||
max_brightness: int,
|
||||
) -> int:
|
||||
fixed = max(1, min(int(brightness), 254))
|
||||
low = max(1, min(int(min_brightness), 254))
|
||||
high = max(low, min(int(max_brightness), 254))
|
||||
if brightness_mode == "from_image":
|
||||
return max(1, min(int(round((rgb_luminance(rgb) / 255.0) * 254.0)), 254))
|
||||
if brightness_mode == "from_image_clamped":
|
||||
value = int(round(low + (rgb_luminance(rgb) / 255.0) * (high - low)))
|
||||
return max(low, min(value, high))
|
||||
return fixed
|
||||
|
||||
|
||||
def rgb_to_hue_sat_command(rgb: tuple[int, int, int], brightness: int) -> dict[str, Any]:
|
||||
r, g, b = [channel / 255.0 for channel in rgb]
|
||||
h, s, _ = colorsys.rgb_to_hsv(r, g, b)
|
||||
hue = int(round(h * 65535)) % 65536
|
||||
sat = int(round(s * 254))
|
||||
return {"on": True, "bri": int(brightness), "hue": hue, "sat": sat}
|
||||
|
||||
|
||||
def srgb_to_linear(channel: float) -> float:
|
||||
if channel <= 0.04045:
|
||||
return channel / 12.92
|
||||
return ((channel + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def rgb_to_xy(rgb: tuple[int, int, int]) -> tuple[float, float]:
|
||||
r, g, b = [srgb_to_linear(channel / 255.0) for channel in rgb]
|
||||
x_value = r * 0.664511 + g * 0.154324 + b * 0.162028
|
||||
y_value = r * 0.283881 + g * 0.668433 + b * 0.047685
|
||||
z_value = r * 0.000088 + g * 0.072310 + b * 0.986039
|
||||
total = x_value + y_value + z_value
|
||||
if total <= 0:
|
||||
return 0.3227, 0.3290
|
||||
return x_value / total, y_value / total
|
||||
|
||||
|
||||
def rgb_to_hue_command(
|
||||
rgb: tuple[int, int, int],
|
||||
brightness: int,
|
||||
color_api: str = "xy",
|
||||
transitiontime: int = 4,
|
||||
) -> dict[str, Any]:
|
||||
command: dict[str, Any] = {"on": True, "bri": int(brightness)}
|
||||
transitiontime = max(0, min(int(transitiontime), 6000))
|
||||
if transitiontime > 0:
|
||||
command["transitiontime"] = transitiontime
|
||||
|
||||
if color_api == "hue_sat":
|
||||
command.update(rgb_to_hue_sat_command(rgb, brightness))
|
||||
else:
|
||||
x_value, y_value = rgb_to_xy(rgb)
|
||||
command["xy"] = [round(x_value, 4), round(y_value, 4)]
|
||||
return command
|
||||
|
||||
|
||||
class Niutonan_Comfyui_Hue:
|
||||
"""Average image-edge colors into a virtual LED bar and push to Hue."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"bridge_ip": ("STRING", {"default": "auto", "multiline": False}),
|
||||
"api_key": ("STRING", {"default": "", "multiline": False}),
|
||||
"light_id": ("STRING", {"default": "1", "multiline": False}),
|
||||
"mode": (["send_average", "preview_only", "turn_off"], {"default": "send_average"}),
|
||||
},
|
||||
"optional": {
|
||||
"color_pick_mode": (["average", "weighted_average", "dominant", "brightest"], {"default": "average"}),
|
||||
"color_api": (["xy", "hue_sat"], {"default": "xy"}),
|
||||
"brightness_mode": (["fixed", "from_image", "from_image_clamped"], {"default": "fixed"}),
|
||||
"led_count": ("INT", {"default": 24, "min": 1, "max": 300}),
|
||||
"edge_width": ("INT", {"default": 32, "min": 1, "max": 512}),
|
||||
"crop_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 45.0, "step": 0.5}),
|
||||
"ignore_dark_below": ("INT", {"default": 0, "min": 0, "max": 255}),
|
||||
"ignore_bright_above": ("INT", {"default": 255, "min": 0, "max": 255}),
|
||||
"brightness": ("INT", {"default": 180, "min": 1, "max": 254}),
|
||||
"min_brightness": ("INT", {"default": 40, "min": 1, "max": 254}),
|
||||
"max_brightness": ("INT", {"default": 220, "min": 1, "max": 254}),
|
||||
"saturation_boost": ("FLOAT", {"default": 1.15, "min": 0.0, "max": 3.0, "step": 0.05}),
|
||||
"min_saturation": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 1.0}),
|
||||
"max_saturation": ("FLOAT", {"default": 100.0, "min": 0.0, "max": 100.0, "step": 1.0}),
|
||||
"vibrance": ("FLOAT", {"default": 0.20, "min": 0.0, "max": 2.0, "step": 0.05}),
|
||||
"transitiontime": ("INT", {"default": 6, "min": 0, "max": 6000}),
|
||||
"top_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
"right_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
"bottom_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
"left_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
"batch_index": ("INT", {"default": 0, "min": 0, "max": 999}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("image", "edge_bar_json", "average_rgb")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "Niutonan/Hue"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def execute(
|
||||
self,
|
||||
image,
|
||||
bridge_ip,
|
||||
api_key,
|
||||
light_id,
|
||||
mode,
|
||||
color_pick_mode="average",
|
||||
color_api="xy",
|
||||
brightness_mode="fixed",
|
||||
led_count=24,
|
||||
edge_width=32,
|
||||
crop_percent=0.0,
|
||||
ignore_dark_below=0,
|
||||
ignore_bright_above=255,
|
||||
brightness=180,
|
||||
min_brightness=40,
|
||||
max_brightness=220,
|
||||
saturation_boost=1.15,
|
||||
min_saturation=0.0,
|
||||
max_saturation=100.0,
|
||||
vibrance=0.20,
|
||||
transitiontime=6,
|
||||
top_weight=1.0,
|
||||
right_weight=1.0,
|
||||
bottom_weight=1.0,
|
||||
left_weight=1.0,
|
||||
batch_index=0,
|
||||
):
|
||||
img_np = tensor_to_rgb_uint8(image, batch_index=batch_index)
|
||||
led_bar = make_led_bar(
|
||||
img_np,
|
||||
edge_width=edge_width,
|
||||
led_count=led_count,
|
||||
crop_percent=crop_percent,
|
||||
ignore_dark_below=ignore_dark_below,
|
||||
ignore_bright_above=ignore_bright_above,
|
||||
)
|
||||
weighted_rgb = weighted_edge_average(
|
||||
img_np,
|
||||
edge_width=edge_width,
|
||||
crop_percent=crop_percent,
|
||||
ignore_dark_below=ignore_dark_below,
|
||||
ignore_bright_above=ignore_bright_above,
|
||||
top_weight=top_weight,
|
||||
right_weight=right_weight,
|
||||
bottom_weight=bottom_weight,
|
||||
left_weight=left_weight,
|
||||
)
|
||||
raw_rgb = choose_rgb(led_bar, color_pick_mode, weighted_rgb)
|
||||
rgb = apply_color_shaping(raw_rgb, saturation_boost, min_saturation, max_saturation, vibrance)
|
||||
final_brightness = choose_brightness(rgb, brightness_mode, brightness, min_brightness, max_brightness)
|
||||
|
||||
if mode == "turn_off":
|
||||
resolved_key = api_key or get_hue_api_key(bridge_ip)
|
||||
if resolved_key:
|
||||
send_hue_command(bridge_ip, resolved_key, light_id, {"on": False})
|
||||
elif mode == "send_average":
|
||||
resolved_key = api_key or get_hue_api_key(bridge_ip)
|
||||
if not resolved_key:
|
||||
print("[Niutonan Hue] No API key. Use Niutonan Hue Setup to register first.")
|
||||
else:
|
||||
command = rgb_to_hue_command(rgb, final_brightness, color_api, transitiontime)
|
||||
send_hue_command(bridge_ip, resolved_key, light_id, command)
|
||||
|
||||
bar_json = json.dumps(
|
||||
{
|
||||
"led_count": len(led_bar),
|
||||
"colors": led_bar,
|
||||
"raw_rgb": list(raw_rgb),
|
||||
"shaped_rgb": list(rgb),
|
||||
"brightness": final_brightness,
|
||||
"color_pick_mode": color_pick_mode,
|
||||
"color_api": color_api,
|
||||
}
|
||||
)
|
||||
rgb_text = f"{rgb[0]},{rgb[1]},{rgb[2]}"
|
||||
print(
|
||||
f"[Niutonan Hue] Edge RGB: {rgb_text}, brightness={final_brightness}, "
|
||||
f"mode={color_pick_mode}, api={color_api}, leds={len(led_bar)}"
|
||||
)
|
||||
return (image, bar_json, rgb_text)
|
||||
|
||||
|
||||
class Niutonan_Comfyui_Hue_Simple:
|
||||
"""Simplified edge-color Hue node with opinionated defaults."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"bridge_ip": ("STRING", {"default": "192.168.0.57", "multiline": False}),
|
||||
"api_key": ("STRING", {"default": "", "multiline": False}),
|
||||
"light_id": ("STRING", {"default": "1", "multiline": False}),
|
||||
"mode": (["send", "preview_only", "turn_off"], {"default": "send"}),
|
||||
},
|
||||
"optional": {
|
||||
"brightness": ("INT", {"default": 180, "min": 1, "max": 254}),
|
||||
"edge_width": ("INT", {"default": 32, "min": 1, "max": 512}),
|
||||
"transitiontime": ("INT", {"default": 6, "min": 0, "max": 6000}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
|
||||
RETURN_NAMES = ("image", "edge_bar_json", "average_rgb")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "Niutonan/Hue"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def execute(
|
||||
self,
|
||||
image,
|
||||
bridge_ip,
|
||||
api_key,
|
||||
light_id,
|
||||
mode,
|
||||
brightness=180,
|
||||
edge_width=32,
|
||||
transitiontime=6,
|
||||
):
|
||||
img_np = tensor_to_rgb_uint8(image, batch_index=0)
|
||||
led_bar = make_led_bar(
|
||||
img_np,
|
||||
edge_width=edge_width,
|
||||
led_count=24,
|
||||
crop_percent=1.0,
|
||||
ignore_dark_below=8,
|
||||
ignore_bright_above=250,
|
||||
)
|
||||
weighted_rgb = weighted_edge_average(
|
||||
img_np,
|
||||
edge_width=edge_width,
|
||||
crop_percent=1.0,
|
||||
ignore_dark_below=8,
|
||||
ignore_bright_above=250,
|
||||
top_weight=1.0,
|
||||
right_weight=1.0,
|
||||
bottom_weight=1.0,
|
||||
left_weight=1.0,
|
||||
)
|
||||
raw_rgb = choose_rgb(led_bar, "dominant", weighted_rgb)
|
||||
rgb = apply_color_shaping(
|
||||
raw_rgb,
|
||||
saturation_boost=1.20,
|
||||
min_saturation=8.0,
|
||||
max_saturation=100.0,
|
||||
vibrance=0.25,
|
||||
)
|
||||
|
||||
if mode == "turn_off":
|
||||
resolved_key = api_key or get_hue_api_key(bridge_ip)
|
||||
if resolved_key:
|
||||
send_hue_command(bridge_ip, resolved_key, light_id, {"on": False})
|
||||
elif mode == "send":
|
||||
resolved_key = api_key or get_hue_api_key(bridge_ip)
|
||||
if not resolved_key:
|
||||
print("[Niutonan Hue Simple] No API key. Use Niutonan Hue Setup to register first.")
|
||||
else:
|
||||
command = rgb_to_hue_command(rgb, brightness, color_api="xy", transitiontime=transitiontime)
|
||||
send_hue_command(bridge_ip, resolved_key, light_id, command)
|
||||
|
||||
bar_json = json.dumps(
|
||||
{
|
||||
"led_count": len(led_bar),
|
||||
"colors": led_bar,
|
||||
"raw_rgb": list(raw_rgb),
|
||||
"shaped_rgb": list(rgb),
|
||||
"brightness": int(brightness),
|
||||
"color_pick_mode": "dominant",
|
||||
"color_api": "xy",
|
||||
"simple": True,
|
||||
}
|
||||
)
|
||||
rgb_text = f"{rgb[0]},{rgb[1]},{rgb[2]}"
|
||||
print(f"[Niutonan Hue Simple] Edge RGB: {rgb_text}, brightness={brightness}, leds={len(led_bar)}")
|
||||
return (image, bar_json, rgb_text)
|
||||
|
||||
|
||||
class Niutonan_Comfyui_Hue_Setup:
|
||||
"""Register and inspect Hue bridge access for Niutonan_Comfyui_Hue."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"bridge_ip": ("STRING", {"default": "auto", "multiline": False}),
|
||||
"action": (["auto_scan", "check_connection", "register_new", "list_lights", "test_flash"],),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("result",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "Niutonan/Hue"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def execute(self, bridge_ip, action):
|
||||
result = ""
|
||||
|
||||
if action == "auto_scan":
|
||||
bridges = discover_hue_bridges(timeout=3.0)
|
||||
if bridges:
|
||||
lines = [f"{bridge['ip']} ({bridge['source']})" for bridge in bridges]
|
||||
result = "Found Hue bridge(s):\n" + "\n".join(lines)
|
||||
else:
|
||||
result = "No Hue bridge found. Check network access or enter bridge_ip manually."
|
||||
elif action == "register_new":
|
||||
resolved_ip, resolve_error = resolve_bridge_ip(bridge_ip)
|
||||
if not resolved_ip:
|
||||
result = f"ERROR: {resolve_error}"
|
||||
else:
|
||||
api_key, error = register_hue_user(resolved_ip)
|
||||
result = f"SUCCESS: API key saved for {resolved_ip}: {api_key}" if api_key else f"ERROR: {error}"
|
||||
elif action == "check_connection":
|
||||
resolved_ip, resolve_error = resolve_bridge_ip(bridge_ip)
|
||||
if not resolved_ip:
|
||||
result = f"ERROR: {resolve_error}"
|
||||
print(f"[Niutonan Hue Setup] {result}")
|
||||
return (result,)
|
||||
api_key = get_hue_api_key(resolved_ip)
|
||||
if not api_key:
|
||||
result = f"No saved API key for {resolved_ip}. Press the bridge button and run register_new."
|
||||
else:
|
||||
lights = get_hue_lights(resolved_ip, api_key)
|
||||
result = f"Connected to {resolved_ip}. Found {len(lights)} lights." if lights else "API key found, but no lights returned."
|
||||
elif action == "list_lights":
|
||||
resolved_ip, resolve_error = resolve_bridge_ip(bridge_ip)
|
||||
if not resolved_ip:
|
||||
result = f"ERROR: {resolve_error}"
|
||||
print(f"[Niutonan Hue Setup] {result}")
|
||||
return (result,)
|
||||
api_key = get_hue_api_key(resolved_ip)
|
||||
if not api_key:
|
||||
result = f"No saved API key for {resolved_ip}. Press the bridge button and run register_new."
|
||||
else:
|
||||
lights = get_hue_lights(resolved_ip, api_key)
|
||||
lines = []
|
||||
for light_id, light in lights.items():
|
||||
state = light.get("state", {})
|
||||
on_off = "ON" if state.get("on") else "OFF"
|
||||
lines.append(f"{light_id}: {light.get('name', 'Unknown')} [{on_off}]")
|
||||
result = "\n".join(lines) if lines else "No lights found."
|
||||
elif action == "test_flash":
|
||||
resolved_ip, resolve_error = resolve_bridge_ip(bridge_ip)
|
||||
if not resolved_ip:
|
||||
result = f"ERROR: {resolve_error}"
|
||||
print(f"[Niutonan Hue Setup] {result}")
|
||||
return (result,)
|
||||
api_key = get_hue_api_key(resolved_ip)
|
||||
if not api_key:
|
||||
result = f"No saved API key for {resolved_ip}. Press the bridge button and run register_new."
|
||||
else:
|
||||
ok = send_hue_command(resolved_ip, api_key, "all", {"alert": "select"})
|
||||
result = "Flashed all lights." if ok else "Hue test flash failed."
|
||||
|
||||
print(f"[Niutonan Hue Setup] {result}")
|
||||
return (result,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Niutonan_Comfyui_Hue": Niutonan_Comfyui_Hue,
|
||||
"Niutonan_Comfyui_Hue_Simple": Niutonan_Comfyui_Hue_Simple,
|
||||
"Niutonan_Comfyui_Hue_Setup": Niutonan_Comfyui_Hue_Setup,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Niutonan_Comfyui_Hue": "Niutonan: ComfyUI Hue Edge Bar",
|
||||
"Niutonan_Comfyui_Hue_Simple": "Niutonan: ComfyUI Hue Edge Bar simple",
|
||||
"Niutonan_Comfyui_Hue_Setup": "Niutonan: Hue Setup",
|
||||
}
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
Reference in New Issue
Block a user