Compare commits

...

12 Commits

Author SHA1 Message Date
d86e7eecc9 regallicht flur
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:13:48 +01:00
8ab9db796c regallicht kueche 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-12 22:08:06 +01:00
a2ddcf7de2 regallicht kueche
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-12 22:01:29 +01:00
3cc3683e8c group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:53:18 +01:00
e0810c72ea group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:50:11 +01:00
3c1253da08 group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:40:02 +01:00
0efb6fab02 group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:30:50 +01:00
a48d189f85 group
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 20:14:20 +01:00
40c3faa128 loglevel
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 13:53:00 +01:00
5cca44638c aid in homekit 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 12:16:51 +01:00
fb2eef2a42 aid in homekit
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 11:51:54 +01:00
0a2007ee65 config file loading 2
All checks were successful
ci/woodpecker/tag/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 11:40:04 +01:00
10 changed files with 261 additions and 16 deletions

View File

@@ -20,6 +20,7 @@ from apps.abstraction.vendors import (
hottis_pv_modbus,
hottis_wago_modbus,
hottis_wifi_relay,
hottis_led_stripe
)
logger = logging.getLogger(__name__)
@@ -44,6 +45,7 @@ for vendor_name, vendor_module in [
("hottis_pv_modbus", hottis_pv_modbus),
("hottis_wago_modbus", hottis_wago_modbus),
("hottis_wifi_relay", hottis_wifi_relay),
("hottis_led_stripe", hottis_led_stripe),
]:
for (device_type, direction), handler in vendor_module.HANDLERS.items():
key = (device_type, vendor_name, direction)

View File

@@ -0,0 +1,46 @@
"""Hottis LED Stripe vendor transformations."""
import logging
from typing import Any
logger = logging.getLogger(__name__)
def transform_light_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to Hottis LED Stripe format.
Hottis LED Stripe expects plain text 'on' or 'off' (not JSON).
Example:
- Abstract: {'power': 'on'}
- Hottis LED Stripe: 'ON'
"""
bri = 89.0 / 254.0
r = int(255 * bri)
g = int(103 * bri)
b = int(25 * bri)
cmd = f"{r} {g} {b}" if payload.get("power", "off").lower() == "on" else "0 0 0"
return cmd
def transform_light_to_abstract(payload: str) -> dict[str, Any]:
"""Transform Hottis LED Stripe relay payload to abstract format.
Hottis LED Stripe sends plain text 'on' or 'off'.
Example:
- Hottis LED Stripe: 'ON'
- Abstract: {'power': 'on'}
"""
power = "on" if payload.strip() != "0 0 0" else "off"
return {"power": power}
# Registry of handlers for this vendor
HANDLERS = {
("light", "to_vendor"): transform_light_to_vendor,
("light", "to_abstract"): transform_light_to_abstract,
}

146
apps/api/config.py Normal file
View File

@@ -0,0 +1,146 @@
"""Configuration loading and caching for API application.
This module provides centralized configuration management for devices and layout,
with startup validation and in-memory caching for performance.
"""
import logging
from pathlib import Path
from typing import Any
import yaml
from packages.home_capabilities.layout import UiLayout
logger = logging.getLogger(__name__)
# Global caches (loaded once at startup)
devices_cache: list[dict[str, Any]] = []
layout_cache: UiLayout | None = None
def load_devices_from_file() -> list[dict[str, Any]]:
"""Load devices from configuration file and validate.
Returns:
list: List of device configurations
Raises:
FileNotFoundError: If devices.yaml doesn't exist
KeyError: If any device is missing required homekit_aid field
ValueError: If devices.yaml is invalid or contains duplicate homekit_aid values
"""
config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml"
if not config_path.exists():
raise FileNotFoundError(f"devices.yaml not found at {config_path}")
with open(config_path, "r") as f:
config = yaml.safe_load(f)
if not config or "devices" not in config:
raise ValueError("devices.yaml must contain 'devices' key")
# Normalize device entries: accept both 'id' and 'device_id', use 'device_id' internally
devices = config.get("devices", [])
for device in devices:
device["device_id"] = device.pop("device_id", device.pop("id", None))
# Validate required homekit_aid field
if "homekit_aid" not in device:
raise KeyError(f"Device {device.get('device_id', 'unknown')} is missing required 'homekit_aid' field")
# Validate unique homekit_aid values
aids = [d["homekit_aid"] for d in devices]
if len(aids) != len(set(aids)):
duplicates = [aid for aid in aids if aids.count(aid) > 1]
raise ValueError(f"Duplicate homekit_aid values found: {set(duplicates)}")
logger.info(f"Loaded {len(devices)} devices with unique homekit_aid values (range: {min(aids)}-{max(aids)})")
return devices
def load_layout_from_file() -> UiLayout:
"""Load UI layout from configuration file and validate.
Returns:
UiLayout: Parsed and validated layout configuration
Raises:
FileNotFoundError: If layout.yaml doesn't exist
ValueError: If layout validation fails
yaml.YAMLError: If YAML parsing fails
"""
config_path = Path(__file__).parent.parent.parent / "config" / "layout.yaml"
if not config_path.exists():
raise FileNotFoundError(
f"Layout configuration not found: {config_path}. "
f"Please create a layout.yaml file with room and device definitions."
)
try:
with open(config_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise yaml.YAMLError(f"Failed to parse YAML in {config_path}: {e}")
if data is None:
raise ValueError(f"Layout file is empty: {config_path}")
try:
layout = UiLayout(**data)
except Exception as e:
raise ValueError(f"Invalid layout configuration in {config_path}: {e}")
total_devices = layout.total_devices()
room_names = [room.name for room in layout.rooms]
logger.info(
f"Loaded layout: {len(layout.rooms)} rooms, "
f"{total_devices} total devices (Rooms: {', '.join(room_names)})"
)
return layout
def load_devices() -> list[dict[str, Any]]:
"""Get devices from in-memory cache.
Returns:
list: List of device configurations (loaded at startup)
"""
return devices_cache
def load_layout() -> UiLayout:
"""Get layout from in-memory cache.
Returns:
UiLayout: Layout configuration (loaded at startup)
Raises:
RuntimeError: If layout cache is not initialized
"""
if layout_cache is None:
raise RuntimeError("Layout cache not initialized. Application startup may have failed.")
return layout_cache
def initialize_config() -> None:
"""Initialize configuration by loading devices and layout.
This function should be called once during application startup.
Raises:
Exception: If configuration loading or validation fails
"""
global devices_cache, layout_cache
# Load devices with validation
devices_cache = load_devices_from_file()
# Load layout with validation
layout_cache = load_layout_from_file()
logger.info("Configuration initialization complete")

View File

@@ -2,6 +2,7 @@ FROM python:3.12-slim
# Environment defaults (can be overridden at runtime)
ENV PYTHONUNBUFFERED=1 \
LOG_LEVEL="INFO" \
HOMEKIT_NAME="Home Automation Bridge" \
HOMEKIT_PIN="031-45-154" \
HOMEKIT_PORT="51826" \

View File

@@ -18,6 +18,7 @@ class Device:
device_id: str
type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover"
name: str # Short name from /devices
homekit_aid: int # HomeKit Accessory ID
features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true})
read_only: bool # True for sensors that don't accept commands
@@ -57,6 +58,12 @@ class DeviceRegistry:
logger.warning(f"Device without device_id: {dev_data}")
continue
# Check for required homekit_aid field
homekit_aid = dev_data.get('homekit_aid')
if homekit_aid is None:
logger.error(f"Device {device_id} is missing required homekit_aid field - skipping")
continue
# Determine if read-only (sensors don't accept set commands)
device_type = dev_data.get('type', '')
read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke']
@@ -65,6 +72,7 @@ class DeviceRegistry:
device_id=device_id,
type=device_type,
name=device_id,
homekit_aid=homekit_aid,
features=dev_data.get('features', {}),
read_only=read_only
)

View File

@@ -1,12 +1,16 @@
services:
homekit-bridge:
image: gitea.hottis.de/wn/home-automation/homekit:0.5.0
build:
context: ../../
dockerfile: apps/homekit/Dockerfile
container_name: homekit-bridge
# Required for mDNS/Bonjour to work properly
network_mode: host
environment:
- LOG_LEVEL=INFO
- HOMEKIT_NAME=Hottis Home Automation Bridge
- HOMEKIT_PIN=031-45-154
- HOMEKIT_PORT=51826

View File

@@ -31,8 +31,9 @@ from .api_client import ApiClient
from .device_registry import DeviceRegistry
# Configure logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=logging.INFO,
level=getattr(logging, LOG_LEVEL, logging.INFO),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@@ -71,9 +72,11 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
try:
accessory = create_accessory_for_device(device, api_client, driver)
if accessory:
# Set AID from device configuration
accessory.aid = device.homekit_aid
bridge.add_accessory(accessory)
accessory_map[device.device_id] = accessory
logger.info(f"Added accessory: {device.name} ({device.type}, {accessory.__class__.__name__})")
logger.info(f"Added accessory: {device.name} ({device.type}, AID={device.homekit_aid}, {accessory.__class__.__name__})")
else:
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
except Exception as e:

View File

@@ -1041,4 +1041,28 @@ devices:
brightness: true
topics:
state: "zigbee2mqtt/herdlicht"
set: "zigbee2mqtt/herdlicht/set"
set: "zigbee2mqtt/herdlicht/set"
- device_id: regallicht_kueche
homekit_aid: 83
name: Regallicht
type: light
cap_version: "relay@1.0.0"
technology: hottis_led_stripe
features:
power: true
topics:
state: "IoT/RgbLedStripeKitchen/ColorCommand"
set: "IoT/RgbLedStripeKitchen/ColorCommand"
- device_id: regallicht_flur
homekit_aid: 84
name: Regallicht Flur
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wifi_relay
features:
power: true
topics:
set: "deconzhelper/flurregallist"
state: "deconzhelper/flurregallist"

View File

@@ -1,10 +1,13 @@
version: 1
groups:
- id: "kueche_lichter"
name: "Küche alle Lampen"
selector:
type: "light"
room: "Küche"
name: "Küche alle Lampen ausser Putzlicht"
device_ids:
- kueche_deckenlampe
- licht_spuele_kueche
- herdlicht
- kueche_fensterbank_licht
- regallicht_kueche
capabilities:
power: true
brightness: true
@@ -16,21 +19,25 @@ groups:
capabilities:
power: true
- id: "schlafzimmer_lichter"
name: "Schlafzimmer alle Lampen"
selector:
type: "light"
room: "Schlafzimmer"
capabilities:
power: true
brightness: true
- id: "schlafzimmer_schlummer_licht"
name: "Schlafzimmer Schlummerlicht"
device_ids:
- bettlicht_patty
- bettlicht_wolfgang
- medusalampe_schlafzimmer
- licht_kommode_schlafzimmer
capabilities:
power: true
brightness: true
- id: "arbeitslicht_patty"
name: "Patty Arbeitslicht"
device_ids:
- schranklicht_hinten_patty
- schranklicht_vorne_patty
- leselampe_patty
- kugellampe_patty
- licht_schreibtisch_patty
capabilities:
power: true
brightness: true

View File

@@ -148,6 +148,10 @@ rooms:
title: Herdlicht
icon: 💡
rank: 145
- device_id: regallicht_kueche
title: Regallicht Küche
icon: 💡
rank: 146
- device_id: thermostat_kueche
title: Kueche
icon: 🌡️