Restructure public config and deployment

This commit is contained in:
ajp_anton
2026-05-30 19:17:27 +00:00
parent cc4e0c2c0f
commit cdf1f4e3a1
24 changed files with 710 additions and 302 deletions
+70
View File
@@ -0,0 +1,70 @@
from ha_deconz_bridge.automation import ActionSpec, automation_rules
def kelvin_to_mireds(kelvin: int) -> int:
return int(round(1_000_000 / kelvin))
def scene(target: str, *, brightness: int, kelvin: int) -> ActionSpec:
return ActionSpec(
"set_scene",
target=target,
brightness=brightness,
color_temp=kelvin_to_mireds(kelvin),
)
def all_lights_scene(*, brightness: int, kelvin: int) -> tuple[ActionSpec, ...]:
return (
scene("Living Room", brightness=brightness, kelvin=kelvin),
scene("Hallway", brightness=brightness, kelvin=kelvin),
scene("Kitchen", brightness=brightness, kelvin=kelvin),
scene("Bathroom", brightness=brightness, kelvin=kelvin),
scene("Bedroom", brightness=brightness, kelvin=kelvin),
)
def all_lights_off() -> tuple[ActionSpec, ...]:
return (
ActionSpec("turn_off", target="Living Room"),
ActionSpec("turn_off", target="Hallway"),
ActionSpec("turn_off", target="Kitchen"),
ActionSpec("turn_off", target="Bathroom"),
ActionSpec("turn_off", target="Bedroom"),
)
def room_automation(room: str) -> dict[str, dict[str, tuple[ActionSpec, ...]]]:
return {
"top": {
"single": (scene(room, brightness=255, kelvin=4000),),
"double": (scene(room, brightness=255, kelvin=5500),),
"hold": all_lights_scene(brightness=255, kelvin=4000),
"double_hold": all_lights_scene(brightness=255, kelvin=5500),
},
"up": {
"single": (scene(room, brightness=153, kelvin=3000),),
"double": (scene(room, brightness=204, kelvin=3500),),
"hold": (ActionSpec("brightness_step", target=room, brightness_step=26),),
},
"down": {
"single": (scene(room, brightness=51, kelvin=2200),),
"double": (scene(room, brightness=102, kelvin=2500),),
"hold": (ActionSpec("brightness_step", target=room, brightness_step=-26),),
},
"bottom": {
"single": (ActionSpec("turn_off", target=room),),
"hold": all_lights_off(),
},
}
AUTOMATION_CONFIG = {
"example_living_remote": room_automation("Living Room"),
"example_kitchen_remote": room_automation("Kitchen"),
"example_hall_remote": room_automation("Hallway"),
"example_bathroom_remote": room_automation("Bathroom"),
"example_bedroom_remote": room_automation("Bedroom"),
}
AUTOMATIONS = automation_rules(AUTOMATION_CONFIG)
+130
View File
@@ -0,0 +1,130 @@
from collections.abc import Mapping
from typing import Any
from ha_deconz_bridge.color import Color
from ha_deconz_bridge.lights import ChannelSpec, ControllerModeSpec, ControllerSpec, VirtualLightSpec
RGB_RED = (0.68, 0.31)
RGB_GREEN = (0.18, 0.70)
RGB_BLUE = (0.14, 0.05)
CT_RANGE = (153, 500)
DAYLIGHT_MIREDS = 182
def define_lights(raw_lights: Mapping[str, Mapping[str, Any]]) -> dict[str, VirtualLightSpec]:
return {
light_name: _virtual_light(light_name, raw_spec)
for light_name, raw_spec in raw_lights.items()
}
def _virtual_light(light_name: str, raw_spec: Mapping[str, Any]) -> VirtualLightSpec:
controllers = {
controller_id: _controller_spec(
controller_id,
raw_controller,
default_name=light_name,
)
for controller_id, raw_controller in raw_spec["controllers"].items()
}
return VirtualLightSpec(
name=str(raw_spec.get("name", light_name)),
supported_color_modes=tuple(raw_spec["supported_color_modes"]),
controllers=controllers,
)
def _controller_spec(
controller_id: str,
raw_controller: Mapping[str, Any] | ControllerSpec,
*,
default_name: str,
) -> ControllerSpec:
if isinstance(raw_controller, ControllerSpec):
if raw_controller.zigbee_id != controller_id:
raise ValueError(
f"Controller key {controller_id!r} does not match spec zigbee_id {raw_controller.zigbee_id!r}."
)
return raw_controller
mireds = raw_controller.get("mireds")
if mireds is not None:
mireds = tuple(sorted(int(value) for value in mireds))
raw_modes = raw_controller.get("modes")
if raw_modes is None:
raw_modes = {
key: value
for key, value in raw_controller.items()
if key in {"onoff", "brightness", "rgb", "color_temp"}
}
modes = {
mode_name: _mode_spec(mode_name, raw_mode, mireds=mireds)
for mode_name, raw_mode in raw_modes.items()
}
return ControllerSpec(
zigbee_id=controller_id,
name=str(raw_controller.get("name", default_name)),
modes=modes,
mireds=mireds,
)
def _mode_spec(
mode_name: str,
raw_mode: Mapping[str, Any],
*,
mireds: tuple[int, int] | None,
) -> ControllerModeSpec:
channels = raw_mode.get("channels", raw_mode)
return ControllerModeSpec(
name=mode_name,
channels={
role: _channel_spec(role, raw_channel, mode_name=mode_name, mireds=mireds)
for role, raw_channel in channels.items()
if role not in {"name", "channels"}
},
)
def _channel_spec(
role: str,
raw_channel: Mapping[str, Any],
*,
mode_name: str,
mireds: tuple[int, int] | None,
) -> ChannelSpec:
label = raw_channel.get("label", raw_channel.get("semantic_label"))
return ChannelSpec(
role,
_parse_color(
raw_channel["color"],
brightness=raw_channel.get("brightness"),
temp_range=mireds or CT_RANGE,
),
label,
)
def _parse_color(
value: Color | int | float | tuple[float, ...] | list[float],
*,
brightness: float | int | None,
temp_range: tuple[int, int],
) -> Color:
output = 1.0 if brightness is None else float(brightness)
if isinstance(value, Color):
return value.with_brightness(output) if brightness is not None else value
if isinstance(value, int | float):
mireds = float(value)
if mireds >= 1000.0:
mireds = 1_000_000.0 / mireds
return Color.from_color_temp(mireds, brightness=output, temp_range=temp_range)
components = tuple(float(component) for component in value)
if len(components) == 2:
return Color.from_xy(components, brightness=output)
if len(components) == 3:
scale = 255.0 if any(component > 1.0 for component in components) else 1.0
return Color.from_rgb(tuple(component / scale for component in components), brightness=output)
raise ValueError(f"Unsupported color value {value!r}.")
+35
View File
@@ -0,0 +1,35 @@
# Copy this file to config/deploy.env and edit it for your target machine.
#
# The deploy script reads this file by default:
# python deploy/deploy.py
# SSH target for the Linux host that will run the service.
DEPLOY_HOST=pi@raspberrypi.local
# Install location on the target. "~" is expanded by the remote shell.
DEPLOY_REMOTE_DIR=~/ha-deconz-bridge
# Local config entry point and service env.
# All .py files next to DEPLOY_CONFIG are copied to the target config directory.
DEPLOY_CONFIG=config/lights_config.py
DEPLOY_SERVICE_ENV=config/service.env
# Optional behavior. Values: true/false.
DEPLOY_INSTALL_SPEED=false
DEPLOY_RESTART_DEBUG=false
# Runtime environment copied into the target service.env.
HA_DECONZ_BRIDGE_MQTT_HOST=homeassistant.local
HA_DECONZ_BRIDGE_MQTT_PORT=1883
HA_DECONZ_BRIDGE_MQTT_USER=
HA_DECONZ_BRIDGE_MQTT_PASSWORD=
HA_DECONZ_BRIDGE_DECONZ_HOST=localhost
HA_DECONZ_BRIDGE_DECONZ_HTTP_PORT=80
HA_DECONZ_BRIDGE_DECONZ_WS_PORT=443
HA_DECONZ_BRIDGE_DECONZ_API_KEY=replace-with-your-deconz-api-key
HA_DECONZ_BRIDGE_TARGET_CACHE_SCALE=10000
HA_DECONZ_BRIDGE_APPROXIMATE_CHROMA_BAND=0.025
HA_DECONZ_BRIDGE_WARMUP_ON_STARTUP=true
HA_DECONZ_BRIDGE_NUMERIC_BACKEND=scipy
+42
View File
@@ -0,0 +1,42 @@
from ha_deconz_bridge.groups import GroupMemberSpec, GroupSpec
from controllers import DAYLIGHT_MIREDS
GROUPS: dict[str, GroupSpec] = {
"Hallway": GroupSpec(
name="Hallway",
members=("Hallway Floor Light", "Hallway Corner Light", "Hallway Cabinet Lights"),
),
"Kitchen": GroupSpec(
name="Kitchen",
members=("Kitchen Counter Lights",),
),
"Bathroom": GroupSpec(
name="Bathroom",
members=("Bathroom Ceiling Light", "Bathroom Mirror Light"),
),
"Bedroom": GroupSpec(
name="Bedroom",
members=("Bedroom Left Lamp", "Bedroom Right Lamp"),
),
"Living Room": GroupSpec(
name="Living Room",
members=(
"Plant Light Left",
"Plant Light Right",
"Living Room Ceiling",
"Floor Lamp Plug",
"Desk Lamp Plug",
),
),
"Plant Lights": GroupSpec(
name="Plant Lights",
members=(
"Plant Light Left",
"Plant Light Right",
GroupMemberSpec("Living Room Ceiling", fixed_color=DAYLIGHT_MIREDS),
),
balance="relative_member",
),
}
+251
View File
@@ -0,0 +1,251 @@
from controllers import (
CT_RANGE,
DAYLIGHT_MIREDS,
RGB_BLUE,
RGB_GREEN,
RGB_RED,
define_lights,
)
LIGHTS = define_lights(
{
"Plant Light Left": {
"supported_color_modes": ("brightness",),
"controllers": {
"fixed_a": {
"name": "Plant Light Left",
"mireds": CT_RANGE,
"color_temp": {
"ww": {"color": DAYLIGHT_MIREDS, "brightness": 9000},
},
},
},
},
"Plant Light Right": {
"supported_color_modes": ("brightness",),
"controllers": {
"fixed_b": {
"name": "Plant Light Right",
"mireds": CT_RANGE,
"color_temp": {
"ww": {"color": DAYLIGHT_MIREDS, "brightness": 7000},
},
},
},
},
"Floor Lamp Plug": {
"supported_color_modes": ("onoff",),
"controllers": {
"plug_a": {
"name": "Floor Lamp Plug",
"onoff": {
"w": {"color": DAYLIGHT_MIREDS, "brightness": 8000, "label": "color_temp"},
},
},
},
},
"Desk Lamp Plug": {
"supported_color_modes": ("onoff",),
"controllers": {
"plug_b": {
"name": "Desk Lamp Plug",
"onoff": {
"w": {"color": DAYLIGHT_MIREDS, "brightness": 8000, "label": "color_temp"},
},
},
},
},
"Living Room Ceiling": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"mixed_rgb": {
"name": "Living Room Ceiling RGB",
"rgb": {
"r": {"color": RGB_RED, "brightness": 3000},
"g": {"color": RGB_GREEN, "brightness": 2480},
"b": {"color": RGB_BLUE, "brightness": 3000},
},
},
"mixed_ct": {
"name": "Living Room Ceiling CCT",
"mireds": CT_RANGE,
"color_temp": {
"ww": {"color": 500, "brightness": 25600},
"cw": {"color": 153, "brightness": 56000},
},
},
},
},
"Hallway Floor Light": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"hall_floor": {
"name": "Hallway Floor Light",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 2460},
"g": {"color": RGB_GREEN, "brightness": 2040},
"b": {"color": RGB_BLUE, "brightness": 1260},
},
"color_temp": {
"ww": {"color": 500, "brightness": 6000},
"cw": {"color": 153, "brightness": 16800},
},
},
},
},
"Hallway Corner Light": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"hall_corner": {
"name": "Hallway Corner Light",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 820},
"g": {"color": RGB_GREEN, "brightness": 680},
"b": {"color": RGB_BLUE, "brightness": 420},
},
"color_temp": {
"ww": {"color": 500, "brightness": 2000},
"cw": {"color": 153, "brightness": 5600},
},
},
},
},
"Hallway Cabinet Lights": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"hall_cabinets": {
"name": "Hallway Cabinet Lights",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 2460},
"g": {"color": RGB_GREEN, "brightness": 2040},
"b": {"color": RGB_BLUE, "brightness": 1260},
},
"color_temp": {
"ww": {"color": 500, "brightness": 6000},
"cw": {"color": 153, "brightness": 16800},
},
},
},
},
"Kitchen Counter Lights": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"kitchen_counter": {
"name": "Kitchen Counter Lights",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 1800},
"g": {"color": RGB_GREEN, "brightness": 2200},
"b": {"color": RGB_BLUE, "brightness": 1900},
},
"color_temp": {
"ww": {"color": 500, "brightness": 3800},
"cw": {"color": 153, "brightness": 3800},
},
},
},
},
"Bathroom Ceiling Light": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"bath_ceiling_1": {
"name": "Bathroom Ceiling Light 1",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 900},
"g": {"color": RGB_GREEN, "brightness": 420},
"b": {"color": RGB_BLUE, "brightness": 180},
},
"color_temp": {
"ww": {"color": 500, "brightness": 1700},
"cw": {"color": 153, "brightness": 2700},
},
},
"bath_ceiling_2": {
"name": "Bathroom Ceiling Light 2",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 900},
"g": {"color": RGB_GREEN, "brightness": 420},
"b": {"color": RGB_BLUE, "brightness": 180},
},
"color_temp": {
"ww": {"color": 500, "brightness": 1700},
"cw": {"color": 153, "brightness": 2700},
},
},
"bath_ceiling_3": {
"name": "Bathroom Ceiling Light 3",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 900},
"g": {"color": RGB_GREEN, "brightness": 420},
"b": {"color": RGB_BLUE, "brightness": 180},
},
"color_temp": {
"ww": {"color": 500, "brightness": 1700},
"cw": {"color": 153, "brightness": 2700},
},
},
},
},
"Bathroom Mirror Light": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"bath_mirror": {
"name": "Bathroom Mirror Light",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 4100},
"g": {"color": RGB_GREEN, "brightness": 3400},
"b": {"color": RGB_BLUE, "brightness": 2100},
},
"color_temp": {
"ww": {"color": 500, "brightness": 10000},
"cw": {"color": 153, "brightness": 28000},
},
},
},
},
"Bedroom Left Lamp": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"bedroom_w": {
"name": "Bedroom Left Lamp",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 1100},
"g": {"color": RGB_GREEN, "brightness": 810},
"b": {"color": RGB_BLUE, "brightness": 360},
},
"color_temp": {
"ww": {"color": 500, "brightness": 3200},
"cw": {"color": 153, "brightness": 4700},
},
},
},
},
"Bedroom Right Lamp": {
"supported_color_modes": ("rgb", "color_temp"),
"controllers": {
"bedroom_e": {
"name": "Bedroom Right Lamp",
"mireds": CT_RANGE,
"rgb": {
"r": {"color": RGB_RED, "brightness": 1100},
"g": {"color": RGB_GREEN, "brightness": 810},
"b": {"color": RGB_BLUE, "brightness": 360},
},
"color_temp": {
"ww": {"color": 500, "brightness": 3200},
"cw": {"color": 153, "brightness": 4700},
},
},
},
},
}
)
+9
View File
@@ -0,0 +1,9 @@
from ha_deconz_bridge.settings import Settings
from actions import AUTOMATIONS
from groups import GROUPS
from lights import LIGHTS
from remotes import REMOTES
SETTINGS = Settings.from_env()
+17
View File
@@ -0,0 +1,17 @@
from ha_deconz_bridge.automation import RemoteSpec
REMOTES = {
"example_sensor_1": RemoteSpec(
sensor_id="example_sensor_1",
remote_id="example_living_remote",
name="Living room remote",
model="RWL021",
),
"example_sensor_2": RemoteSpec(
sensor_id="example_sensor_2",
remote_id="example_kitchen_remote",
name="Kitchen remote",
model="RWL021",
),
}
+28
View File
@@ -0,0 +1,28 @@
# Copy or edit this file on the target host at:
# <install-dir>/config/service.env
HA_DECONZ_BRIDGE_MQTT_HOST=homeassistant
HA_DECONZ_BRIDGE_MQTT_PORT=1883
HA_DECONZ_BRIDGE_MQTT_USER=
HA_DECONZ_BRIDGE_MQTT_PASSWORD=
HA_DECONZ_BRIDGE_DECONZ_HOST=localhost
HA_DECONZ_BRIDGE_DECONZ_HTTP_PORT=80
HA_DECONZ_BRIDGE_DECONZ_WS_PORT=443
HA_DECONZ_BRIDGE_DECONZ_API_KEY=
# Lower values make nearby color/CT requests share solver cache entries.
# This improves repeated nearby solves, but can create visible color dead zones.
HA_DECONZ_BRIDGE_TARGET_CACHE_SCALE=10000
# CT-only uv slack for preferring CT-labeled emitters over exact RGB emulation.
HA_DECONZ_BRIDGE_APPROXIMATE_CHROMA_BAND=0.025
# Run one representative solver request per HA mode in the background so
# Numba-backed paths are compiled without blocking MQTT or debugger startup.
HA_DECONZ_BRIDGE_WARMUP_ON_STARTUP=true
# Numeric backend for color solves. Keep "scipy" unless optional speed
# dependencies were installed, for example with pip install ".[speed]".
# Allowed values: scipy, numba, auto.
HA_DECONZ_BRIDGE_NUMERIC_BACKEND=scipy