UPDATE!
This is now 99.9% solved!! The only remaining improvement I’d want is to change the mushroom card temperture slider to be a saturation slider for the active color of the light. I’ve tried a few card-mod style changes with no success. Anyone have a suggestion for better card for this?
A couple other notes:
I’m currently on HA Core 2025.12.1 and HA OS 16.3 so I believe I need to update this config before updating to the newer 2026 versions.
i
## ====================================
### RGB BEDROOM STRIP LIGHT ####
## ====================================
input_boolean:
bedroom_light_state_real:
name: Bedroom Light State
input_number:
bedroom_light_brightness_real:
name: Bedroom Brightness
min: 0
max: 255
step: 1
initial: 255
input_text:
bedroom_light_rgb_real:
name: Bedroom RGB
initial: "255,255,255"
bedroom_light_rgb_base: # NEW: Store the base color
name: Bedroom RGB Base Color
initial: "255,0,0" # Default to red
sensor:
- platform: rest
name: Bedroom Light Status
resource: http://192.168.2.120:23299/api/status
scan_interval: 2
value_template: "{{ value_json.state }}"
json_attributes:
- brightness
- rgb_color
- hex_color
light:
- platform: template
lights:
bedroom_python_light:
friendly_name: "Bedroom Python Light"
value_template: "{{ is_state('input_boolean.bedroom_light_state_real', 'on') }}"
level_template: "{{ states('input_number.bedroom_light_brightness_real') | int(255) }}"
rgb_template: >
{% set rgb = states('input_text.bedroom_light_rgb_real') %}
{% set parts = rgb.split(',') if rgb not in ['unknown','unavailable',''] else ['255','255','255'] %}
({{ parts[0] | int(255) }}, {{ parts[1] | int(255) }}, {{ parts[2] | int(255) }})
temperature_template: "{{ 153 }}" # Start at cool (least white)
turn_on:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.bedroom_light_state_real
- service: rest_command.bedroom_light_api
data:
payload: >
{
"state": "ON",
"brightness": {{ (states('input_number.bedroom_light_brightness_real') | int(255) / 255 * 100) | int }},
"rgb_color": [
{{ states('input_text.bedroom_light_rgb_real').split(',')[0] | int(255) }},
{{ states('input_text.bedroom_light_rgb_real').split(',')[1] | int(255) }},
{{ states('input_text.bedroom_light_rgb_real').split(',')[2] | int(255) }}
]
}
turn_off:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.bedroom_light_state_real
- service: rest_command.bedroom_light_api
data:
payload: '{"state": "OFF"}'
set_level:
- service: input_number.set_value
target:
entity_id: input_number.bedroom_light_brightness_real
data:
value: "{{ brightness }}"
- service: rest_command.bedroom_light_api
data:
payload: >
{
"state": "ON",
"brightness": {{ (brightness / 255 * 100) | int }},
"rgb_color": [
{{ states('input_text.bedroom_light_rgb_real').split(',')[0] | int(255) }},
{{ states('input_text.bedroom_light_rgb_real').split(',')[1] | int(255) }},
{{ states('input_text.bedroom_light_rgb_real').split(',')[2] | int(255) }}
]
}
# Set RGB color - ALSO UPDATE BASE COLOR
set_rgb:
- service: input_text.set_value
target:
entity_id: input_text.bedroom_light_rgb_base
data:
value: "{{ rgb[0] }},{{ rgb[1] }},{{ rgb[2] }}"
- service: input_text.set_value
target:
entity_id: input_text.bedroom_light_rgb_real
data:
value: "{{ rgb[0] }},{{ rgb[1] }},{{ rgb[2] }}"
- service: rest_command.bedroom_light_api
data:
payload: >
{
"state": "ON",
"brightness": {{ (states('input_number.bedroom_light_brightness_real') | int(255) / 255 * 100) | int }},
"rgb_color": [{{ rgb[0] }}, {{ rgb[1] }}, {{ rgb[2] }}]
}
# Set color temperature - MIX BASE COLOR WITH WHITE (INVERTED)
set_temperature:
- service: input_text.set_value
target:
entity_id: input_text.bedroom_light_rgb_real
data:
value: >
{% set base_rgb = states('input_text.bedroom_light_rgb_base').split(',') %}
{% set r = base_rgb[0] | int(255) %}
{% set g = base_rgb[1] | int(255) %}
{% set b = base_rgb[2] | int(255) %}
{% set white_amount = ((500 - color_temp) / (500 - 153)) | float %}
{% set final_r = (r + (255 - r) * white_amount) | int %}
{% set final_g = (g + (255 - g) * white_amount) | int %}
{% set final_b = (b + (255 - b) * white_amount) | int %}
{{ final_r }},{{ final_g }},{{ final_b }}
- service: rest_command.bedroom_light_api
data:
payload: >
{% set base_rgb = states('input_text.bedroom_light_rgb_base').split(',') %}
{% set r = base_rgb[0] | int(255) %}
{% set g = base_rgb[1] | int(255) %}
{% set b = base_rgb[2] | int(255) %}
{% set white_amount = ((500 - color_temp) / (500 - 153)) | float %}
{% set final_r = (r + (255 - r) * white_amount) | int %}
{% set final_g = (g + (255 - g) * white_amount) | int %}
{% set final_b = (b + (255 - b) * white_amount) | int %}
{
"state": "ON",
"brightness": {{ (states('input_number.bedroom_light_brightness_real') | int(255) / 255 * 100) | int }},
"rgb_color": [{{ final_r }}, {{ final_g }}, {{ final_b }}]
}
And in my automation.yaml:
- id: bedroom_light_sync_from_api
alias: "Sync Bedroom Python Light State"
mode: queued # Process updates in order
max: 5 # Maximum 5 queued updates
trigger:
- platform: state
entity_id: sensor.bedroom_light_status
action:
- service: input_boolean.turn_{{ 'on' if trigger.to_state.state == 'ON' else 'off' }}
target:
entity_id: input_boolean.bedroom_light_state_real
- service: input_number.set_value
target:
entity_id: input_number.bedroom_light_brightness_real
data:
value: "{{ (state_attr('sensor.bedroom_light_status', 'brightness') | int(100) * 2.55) | float }}"
- service: input_text.set_value
target:
entity_id: input_text.bedroom_light_rgb_real
data:
value: >
{% set rgb = state_attr('sensor.bedroom_light_status', 'rgb_color') %}
{% if rgb %}{{ rgb[0] }},{{ rgb[1] }},{{ rgb[2] }}{% else %}255,255,255{% endif %}
I don’t think I need help with the Python part but if anyone is interested:
This is my current python code (it still needs some work and it used to launch a webpage to control the RGB lights but it functions for creating an endpoint for the lights to be controlled)
It’s a Keepsmile brand RGB String/Strip light Python Bluetooth integration. It could definitely use some improvement but it’s been quite reliable for months when I had the webpage built for it.
import asyncio
import threading
from bleak import BleakClient
from bleak import BleakScanner
import time
import requests
from flask import redirect, url_for, Flask, request, jsonify
HA_URL = "http://192.168.1.200:8123/api/webhook/bedroom_light_update"
ble_client = None
ble_lock = threading.Lock()
loop = asyncio.new_event_loop()
app = Flask(__name__)
ADDRESS = "BE:27:79:00:F3:56"
CHARACTERISTIC_WRITE_UUID = "0000afd1-0000-1000-8000-00805f9b34fb"
LIGHTS_ON_STRING = "5BF000B5"
LIGHTS_OFF_STRING = "5B0F00B5"
# Store the last selected values
last_color = "#ffffff"
last_brightness = 100
lights_on = False # MOVED TO TOP - must be defined before functions use it
def push_state_to_ha():
"""Push current state to Home Assistant webhook"""
try:
r = int(last_color[1:3], 16)
g = int(last_color[3:5], 16)
b = int(last_color[5:7], 16)
payload = {
"state": "ON" if lights_on else "OFF",
"brightness": int(last_brightness * 2.55),
"rgb_color": [r, g, b]
}
requests.post(HA_URL, json=payload, timeout=1)
print(f"📤 Pushed to HA: {payload}")
except Exception as e:
print(f"❌ HA push failed: {e}")
async def send_command_to_connected_device(command_hex):
"""Send command using the persistent BLE connection"""
global ble_client
command_bytes = hex_str_to_bytes(command_hex)
try:
if ble_client and ble_client.is_connected: # <-- Remove await here
await ble_client.write_gatt_char(CHARACTERISTIC_WRITE_UUID, command_bytes)
print(f"✅ Sent command: {command_hex}")
else:
print("⚠️ BLE client not connected.")
except Exception as e:
print(f"❌ Failed to send BLE command: {e}")
import traceback
traceback.print_exc()
async def maintain_ble_connection():
"""Maintain persistent connection to BLE device"""
global ble_client
while True:
try:
print("🔄 Attempting BLE connection...")
devices = await BleakScanner.discover(timeout=3.0)
target = next((d for d in devices if d.name and d.name.startswith("KS03")), None)
if target:
print(f"✅ Found device: {target.name} @ {target.address}")
client = BleakClient(target.address)
await client.connect(timeout=5.0)
print("🔌 Connected and ready.")
ble_client = client
while await client.is_connected():
await asyncio.sleep(1)
print("⚠️ BLE disconnected.")
ble_client = None
else:
print("❌ BLE device not found. Retrying...")
except Exception as e:
print(f"❌ Connection error: {e}")
await asyncio.sleep(3)
def hex_str_to_bytes(hex_string):
"""Convert hex string to bytes"""
return bytearray.fromhex(hex_string)
def get_hex(number):
"""Convert number to 2-digit hex string"""
return hex(number)[2:].zfill(2).upper()
def get_color_command(red, green, blue, brightness):
"""Generate BLE color command"""
print(f"🎨 Building color command: RGB({red}, {green}, {blue}), Brightness: {brightness}")
return f"5A0001{get_hex(red)}{get_hex(green)}{get_hex(blue)}00{get_hex(brightness)}00A5"
def run_ble_command_async(command_hex):
"""Queue a BLE command to run in the async loop"""
asyncio.run_coroutine_threadsafe(
send_command_to_connected_device(command_hex),
loop
)
def build_html(current_color, current_brightness):
"""Build simple HTML interface"""
return f'''
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {{
font-family: sans-serif;
background: #121212;
color: #eee;
padding: 20px;
display: flex;
justify-content: center;
}}
form {{
width: 100%;
max-width: 400px;
background: #1e1e1e;
border-radius: 12px;
padding: 20px;
}}
button {{
width: 100%;
padding: 12px;
margin: 8px 0;
border: none;
border-radius: 8px;
font-size: 16px;
background: #333;
color: white;
cursor: pointer;
}}
button:hover {{
background: #555;
}}
label {{
display: block;
margin-top: 16px;
margin-bottom: 4px;
}}
input[type='color'], input[type='range'] {{
width: 100%;
margin-bottom: 12px;
}}
</style>
</head>
<body>
<form method="POST">
<h3>Presets</h3>
<button name="command" value="ON">Lights ON</button>
<button name="command" value="OFF">Lights OFF</button>
<button name="command" value="BRIGHT_WHITE">Bright White 🌞</button>
<button name="command" value="BABY_PURPLE">Baby Purple 💜</button>
<h3>Custom</h3>
<label for="color">Color:</label>
<input type="color" id="color" name="color" value="{current_color}">
<label for="brightness">Brightness: <span id="bval">{current_brightness}</span></label>
<input type="range" id="brightness" name="brightness" min="0" max="100" value="{current_brightness}"
oninput="document.getElementById('bval').innerText = this.value">
<button type="submit">Set Custom</button>
</form>
</body>
</html>
'''
# ==============================================
# API ENDPOINTS
# ==============================================
@app.route('/api/status', methods=['GET'])
def api_status():
"""Home Assistant checks this to see current state"""
try:
r = int(last_color[1:3], 16)
g = int(last_color[3:5], 16)
b = int(last_color[5:7], 16)
except:
r, g, b = 255, 255, 255
return jsonify({
"state": "ON" if lights_on else "OFF",
"brightness": int(last_brightness),
"rgb_color": [r, g, b],
"hex_color": last_color
})
@app.route('/api/update', methods=['POST'])
def api_update():
"""Home Assistant sends commands here"""
global lights_on, last_color, last_brightness
data = request.json
print(f"\n{'='*50}")
print(f"📥 HA REQUEST: {data}")
print(f"📊 BEFORE - lights_on: {lights_on}, color: {last_color}, brightness: {last_brightness}")
if not data:
return jsonify({"error": "No JSON data"}), 400
# Track if we're turning on from OFF state
was_off = not lights_on
# Handle OFF first (early return)
if data.get('state') == 'OFF':
lights_on = False
print(f"🔴 Turning OFF - sending simple command")
run_ble_command_async(LIGHTS_OFF_STRING)
push_state_to_ha()
print(f"{'='*50}\n")
return jsonify({"status": "turned_off"})
# Handle ON state
if data.get('state') == 'ON':
lights_on = True
print(f"🟢 State set to ON (was_off: {was_off})")
# Update color if provided
if 'rgb_color' in data:
r, g, b = data['rgb_color']
last_color = f"#{r:02x}{g:02x}{b:02x}"
print(f"🎨 Color updated to: {last_color}")
# Update brightness if provided
if 'brightness' in data:
last_brightness = int(data['brightness'])
print(f"💡 Brightness updated to: {last_brightness}")
# Send BLE command if light is ON
if lights_on:
# CRITICAL: If we just turned ON from OFF, send simple ON command
# Then send color command if color/brightness was also sent
if was_off:
print(f"📤 Sending SIMPLE ON command (was off): {LIGHTS_ON_STRING}")
run_ble_command_async(LIGHTS_ON_STRING)
# If color or brightness was also specified, send that too after a delay
if 'rgb_color' in data or 'brightness' in data:
import time
time.sleep(0.2) # Brief delay to let ON command process
r = int(last_color[1:3], 16)
g = int(last_color[3:5], 16)
b = int(last_color[5:7], 16)
command = get_color_command(r, g, b, last_brightness)
print(f"📤 Sending color command after ON: {command}")
run_ble_command_async(command)
else:
# Light was already on, just update color/brightness
r = int(last_color[1:3], 16)
g = int(last_color[3:5], 16)
b = int(last_color[5:7], 16)
command = get_color_command(r, g, b, last_brightness)
print(f"📤 Sending color update (already on): {command}")
run_ble_command_async(command)
print(f"📊 AFTER - lights_on: {lights_on}, color: {last_color}, brightness: {last_brightness}")
print(f"{'='*50}\n")
push_state_to_ha()
return jsonify({
"status": "ok",
"state": "ON" if lights_on else "OFF",
"color": last_color,
"brightness": last_brightness
})
# ==============================================
# WEB INTERFACE
# ==============================================
@app.route('/', methods=['GET'])
def index():
return build_html(last_color, last_brightness)
@app.route('/', methods=['POST'])
def control():
"""Handle web form submissions"""
global last_color, last_brightness, lights_on
cmd = request.form.get('command')
if cmd:
if cmd == 'ON':
lights_on = True
command = LIGHTS_ON_STRING
last_color = '#ffffff'
last_brightness = 100
elif cmd == 'OFF':
lights_on = False
command = LIGHTS_OFF_STRING
elif cmd == 'BRIGHT_WHITE':
lights_on = True
last_color = '#ffffff'
last_brightness = 100
command = get_color_command(255, 255, 255, 100)
elif cmd == 'BABY_PURPLE':
lights_on = True
last_color = '#7f00ff'
last_brightness = 100
command = get_color_command(127, 0, 255, 100)
else:
return "Unknown command", 400
else:
# Custom color/brightness from form
lights_on = True
try:
hex_color = request.form.get('color', '#ffffff')
last_color = hex_color
r = int(hex_color[1:3], 16)
g = int(hex_color[3:5], 16)
b = int(hex_color[5:7], 16)
last_brightness = int(request.form.get('brightness', 100))
command = get_color_command(r, g, b, last_brightness)
except Exception as e:
return f"Invalid input: {e}", 400
print(f"🌐 Web control: {cmd if cmd else 'custom'}")
run_ble_command_async(command)
push_state_to_ha()
return redirect(url_for('index'))
# ==============================================
# BACKGROUND TASKS
# ==============================================
def start_flask():
"""Start Flask web server"""
app.run(host='0.0.0.0', port=23299, debug=False)
async def keep_connection_alive():
"""Send periodic keep-alive commands"""
global ble_client
while True:
try:
if ble_client and await ble_client.is_connected():
r = int(last_color[1:3], 16)
g = int(last_color[3:5], 16)
b = int(last_color[5:7], 16)
command = get_color_command(r, g, b, last_brightness)
await ble_client.write_gatt_char(
CHARACTERISTIC_WRITE_UUID,
hex_str_to_bytes(command)
)
print("💓 Keep-alive ping sent")
except Exception as e:
print(f"⚠️ Keep-alive error: {e}")
await asyncio.sleep(2)
# ==============================================
# MAIN STARTUP
# ==============================================
if __name__ == '__main__':
print("🚀 Starting RGB Light Controller...")
# Start async event loop in background thread
threading.Thread(target=loop.run_forever, daemon=True).start()
# Start BLE connection manager
asyncio.run_coroutine_threadsafe(maintain_ble_connection(), loop)
# Start keep-alive pinger
asyncio.run_coroutine_threadsafe(keep_connection_alive(), loop)
# Start Flask web server (blocking)
print("🌐 Starting web server on http://0.0.0.0:23299")
start_flask()
Thank you all so much for HA and the amazing community <3