Compare commits

..

36 Commits

Author SHA1 Message Date
9431572008 fix 2026-01-12 09:13:48 +01:00
a4bfa265b9 merged 2026-01-12 09:12:07 +01:00
61b9437b71 no namespace on config change 2026-01-11 11:55:28 +01:00
d162664ac7 5.0 when window open
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2026-01-10 16:05:48 +01:00
474b41ffce deploy confguuration script 2026-01-06 14:02:11 +01:00
79081e7480 thermostat bad unten replaced 2 2026-01-06 13:48:29 +01:00
424f1d6743 thermostat bad unten replaced 2026-01-06 13:47:56 +01:00
a190ba208b switch added 2026-01-03 22:21:33 +01:00
7212a3bd5a Lampentausch
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2026-01-03 20:52:10 +01:00
7e0801d21a event_generator fix
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/4 Pipeline was successful
ci/woodpecker/tag/deploy/3 Pipeline was successful
ci/woodpecker/tag/deploy/5 Pipeline was successful
ci/woodpecker/tag/deploy/1 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-25 19:48:07 +01:00
49e555ce51 redis_state_listener fix 2025-12-25 19:36:19 +01:00
62f68fb513 Merge branch 'main' of gitea.hottis.de:wn/home-automation
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-22 19:18:32 +01:00
66f180755b heating rules 2025-12-22 19:18:23 +01:00
b9ba9cbd16 herdlicht again 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-18 19:04:26 +01:00
14c4c7c850 herdlicht again 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-18 19:01:33 +01:00
edb8b3313b herdlicht again
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-18 18:57:42 +01:00
68015905b0 herdlicht 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-17 16:22:11 +01:00
223c6e58b9 herdlicht
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-17 16:18:42 +01:00
0548996110 steckdose strandkorb 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-15 12:15:38 +01:00
35141f71a4 steckdose strandkorb 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-15 12:11:48 +01:00
eb5532739c steckdose strandkorb
All checks were successful
ci/woodpecker/tag/build/7 Pipeline was successful
ci/woodpecker/tag/deploy/4 Pipeline was successful
ci/woodpecker/tag/deploy/6 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
ci/woodpecker/tag/deploy/3 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/build/5 Pipeline was successful
ci/woodpecker/tag/build/6 Pipeline was successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/4 Pipeline was successful
ci/woodpecker/tag/build/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/2 Pipeline was successful
2025-12-15 11:52:57 +01:00
42411b1377 regallicht flur 3
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:17:44 +01:00
b99158fd25 regallicht flur 2
All checks were successful
ci/woodpecker/tag/namespace Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
2025-12-12 22:14:59 +01:00
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
bdb25e3550 config file loading
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/3 Pipeline was successful
ci/woodpecker/tag/config Pipeline was successful
ci/woodpecker/tag/build/1 Pipeline was successful
ci/woodpecker/tag/build/7 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/3 Pipeline was successful
ci/woodpecker/tag/deploy/2 Pipeline was successful
ci/woodpecker/tag/ingress Pipeline was successful
2025-12-11 11:37:00 +01:00
21 changed files with 673 additions and 228 deletions

View File

@@ -1,9 +1,6 @@
when:
event: [tag]
depends_on:
- namespace
steps:
apply_configuration:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4

View File

@@ -1,6 +1,13 @@
when:
event: [tag]
ref:
exclude:
- refs/tags/*-configchange
<<<<<<< HEAD
=======
>>>>>>> main
steps:
create_namespace:
image: quay.io/wollud1969/k8s-admin-helper:0.3.4

View File

@@ -15,7 +15,7 @@ import uuid
from aiomqtt import Client
from pydantic import ValidationError
from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState, ThreePhasePowerState
from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState, RelayState, ThreePhasePowerState, SwitchState
from apps.abstraction.transformation import (
transform_abstract_to_vendor,
transform_vendor_to_abstract
@@ -174,6 +174,10 @@ async def handle_abstract_set(
# Contact sensors are read-only - SET commands should not occur
logger.warning(f"Contact sensor {device_id} received SET command - ignoring (read-only device)")
return
elif device_type == "switch":
# Switches are read-only - SET commands should not occur
logger.warning(f"Switch {device_id} received SET command - ignoring (read-only device)")
return
except ValidationError as e:
logger.error(f"Validation failed for {device_type} SET {device_id}: {e}")
return
@@ -227,6 +231,9 @@ async def handle_vendor_state(
elif device_type == "three_phase_powermeter":
# Validate three-phase powermeter state
ThreePhasePowerState.model_validate(abstract_payload)
elif device_type == "switch":
# Validate switch state
SwitchState.model_validate(abstract_payload)
except ValidationError as e:
logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}")
return

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,
}

View File

@@ -161,6 +161,24 @@ def transform_temp_humidity_sensor_to_abstract(payload: str) -> dict[str, Any]:
return payload
def transform_switch_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract switch payload to zigbee2mqtt format.
Switches are read-only, so this should not be called for SET commands.
"""
logger.warning("Switches are read-only - SET commands should not be used")
return json.dumps(payload)
def transform_switch_to_abstract(payload: str) -> dict[str, Any]:
"""Transform zigbee2mqtt switch payload to abstract format.
Passthrough - zigbee2mqtt provides action, battery, linkquality directly.
"""
payload = json.loads(payload)
return payload
def transform_relay_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract relay payload to zigbee2mqtt format.
@@ -204,6 +222,8 @@ HANDLERS = {
("temp_humidity_sensor", "to_abstract"): transform_temp_humidity_sensor_to_abstract,
("temp_humidity", "to_vendor"): transform_temp_humidity_sensor_to_vendor,
("temp_humidity", "to_abstract"): transform_temp_humidity_sensor_to_abstract,
("switch", "to_vendor"): transform_switch_to_vendor,
("switch", "to_abstract"): transform_switch_to_abstract,
("relay", "to_vendor"): transform_relay_to_vendor,
("relay", "to_abstract"): transform_relay_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

@@ -24,9 +24,11 @@ from packages.home_capabilities import (
ContactState,
TempHumidityState,
RelayState,
load_layout,
)
# Import configuration management
from apps.api.config import initialize_config, load_devices, load_layout
# Import resolvers (must be before router imports to avoid circular dependency)
from apps.api.resolvers import (
DeviceDTO,
@@ -51,9 +53,6 @@ logger = logging.getLogger(__name__)
# Will be populated from Redis pub/sub messages
device_states: dict[str, dict[str, Any]] = {}
# Devices configuration cache (loaded once at startup)
devices_cache: list[dict[str, Any]] = []
# Background task reference
background_task: asyncio.Task | None = None
@@ -102,24 +101,6 @@ async def get_device_state(device_id: str):
except KeyError:
raise HTTPException(status_code=404, detail="Device state not found")
# --- Minimal-invasive: Einzelgerät-Layout-Endpunkt ---
@app.get("/devices/{device_id}/layout")
async def get_device_layout(device_id: str):
"""Gibt die layout-spezifischen Informationen für ein einzelnes Gerät zurück."""
layout = load_layout()
for room in layout.get("rooms", []):
for device in room.get("devices", []):
if device.get("device_id") == device_id:
# Rückgabe: Layout-Infos + Raumname
return {
"device_id": device_id,
"room": room.get("name"),
"title": device.get("title"),
"icon": device.get("icon"),
"rank": device.get("rank"),
}
raise HTTPException(status_code=404, detail="Device layout not found")
@app.get("/health")
async def health() -> dict[str, str]:
@@ -146,14 +127,9 @@ async def redis_state_listener():
logger.info("Redis state listener connected")
while True:
try:
message = await asyncio.wait_for(
pubsub.get_message(ignore_subscribe_messages=True),
timeout=1.0
)
if message and message["type"] == "message":
# listen() blocks async and waits for messages - prevents busy loop
async for message in pubsub.listen():
if message["type"] == "message":
data = message["data"]
try:
state_data = json.loads(data)
@@ -165,9 +141,6 @@ async def redis_state_listener():
except Exception as e:
logger.warning(f"Failed to parse state data: {e}")
except asyncio.TimeoutError:
pass # No message, continue
except asyncio.CancelledError:
logger.info("Redis state listener cancelled")
raise
@@ -184,7 +157,7 @@ async def redis_state_listener():
@app.on_event("startup")
async def startup_event():
"""Start background tasks on application startup."""
global background_task, devices_cache
global background_task
# Include routers
from apps.api.routes.groups_scenes import router as groups_scenes_router
@@ -193,11 +166,11 @@ async def startup_event():
app.include_router(groups_scenes_router, prefix="")
app.include_router(rooms_router, prefix="")
# Load and validate devices configuration
# Load and validate configuration (devices + layout)
try:
devices_cache = load_devices_from_file()
initialize_config()
except Exception as e:
logger.error(f"Failed to load devices configuration: {e}")
logger.error(f"Failed to initialize configuration: {e}")
raise # Fatal error - application will not start
background_task = asyncio.create_task(redis_state_listener())
@@ -252,57 +225,6 @@ class DeviceInfo(BaseModel):
# Configuration helpers
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_devices() -> list[dict[str, Any]]:
"""Get devices from in-memory cache.
Returns:
list: List of device configurations (loaded at startup)
"""
return devices_cache
def get_mqtt_settings() -> tuple[str, int]:
"""Get MQTT broker settings from environment.
@@ -637,25 +559,31 @@ async def event_generator(request: Request) -> AsyncGenerator[str, None]:
redis_client = None
pubsub = None
# Heartbeat tracking
last_heartbeat = asyncio.get_event_loop().time()
heartbeat_interval = 15 # Safari-friendly: shorter interval
# Use listen() iterator for blocking reads with heartbeat timeout
if pubsub:
listener = pubsub.listen()
else:
listener = None
while True:
# Check if client disconnected
if await request.is_disconnected():
logger.info("SSE client disconnected")
break
# Try to get message from Redis (if available)
if pubsub:
# Try to get message from Redis with timeout for heartbeat
if listener:
try:
# Wait for message with heartbeat timeout
# If no message arrives within timeout, send heartbeat
message = await asyncio.wait_for(
pubsub.get_message(ignore_subscribe_messages=True),
timeout=0.1
anext(listener),
timeout=heartbeat_interval
)
if message and message["type"] == "message":
if message["type"] == "message":
data = message["data"]
logger.debug(f"Sending SSE message: {data[:100]}...")
@@ -668,24 +596,21 @@ async def event_generator(request: Request) -> AsyncGenerator[str, None]:
logger.warning(f"Failed to parse state data for cache: {e}")
yield f"event: message\ndata: {data}\n\n"
last_heartbeat = asyncio.get_event_loop().time()
continue # Skip sleep, check for more messages immediately
except asyncio.TimeoutError:
pass # No message, continue to heartbeat check
# No message within heartbeat interval - send heartbeat
yield ": ping\n\n"
except StopAsyncIteration:
logger.warning("Redis listener stopped")
break
except Exception as e:
logger.error(f"Redis error: {e}")
# Continue with heartbeats even if Redis fails
# Sleep briefly to avoid busy loop
await asyncio.sleep(0.1)
# Send heartbeat if interval elapsed
current_time = asyncio.get_event_loop().time()
if current_time - last_heartbeat >= heartbeat_interval:
# Comment-style ping (Safari-compatible, no event type)
# Continue with heartbeat-only mode
listener = None
else:
# Heartbeat-only mode (no Redis)
await asyncio.sleep(heartbeat_interval)
yield ": ping\n\n"
last_heartbeat = current_time
except asyncio.CancelledError:
logger.info("SSE connection cancelled by client")

View File

@@ -4,12 +4,12 @@ import logging
from pathlib import Path
from typing import Any, TypedDict
from apps.api.config import load_layout
from packages.home_capabilities import (
GroupConfig,
GroupsConfigRoot,
SceneStep,
get_group_by_id,
load_layout,
)
logger = logging.getLogger(__name__)

View File

@@ -12,7 +12,7 @@ from typing import Any
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from packages.home_capabilities import load_layout
from apps.api.config import load_layout
logger = logging.getLogger(__name__)

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

@@ -326,41 +326,6 @@ devices:
ieee_address: "0xf0d1b8be2409f569"
model: "4058075729063"
vendor: "LEDVANCE"
- device_id: licht_flur_oben_am_spiegel
homekit_aid: 22
name: Spiegel
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0x842e14fffefe4ba4"
set: "zigbee2mqtt/0x842e14fffefe4ba4/set"
metadata:
friendly_name: "Licht Flur oben am Spiegel"
ieee_address: "0x842e14fffefe4ba4"
model: "LED1732G11"
vendor: "IKEA"
- device_id: experimentlabtest
homekit_aid: 23
name: Test Lampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b80000195038"
set: "zigbee2mqtt/0xf0d1b80000195038/set"
metadata:
friendly_name: "ExperimentLabTest"
ieee_address: "0xf0d1b80000195038"
model: "4058075208421"
vendor: "LEDVANCE"
- device_id: thermostat_wolfgang
homekit_aid: 24
name: Heizung
@@ -506,21 +471,16 @@ devices:
name: Heizung
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
technology: zigbee2mqtt
features:
mode: true
target: true
current: false
heating: true
temperature_range:
- 5
- 30
temperature_step: 0.5
topics:
set: "homegear/instance1/set/48/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/48/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Bad Unten"
location: "Bad Unten"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "48"
channel: "1"
state: "zigbee2mqtt/0x003c84fffebdcc28"
set: "zigbee2mqtt/0x003c84fffebdcc28/set"
- device_id: sterne_wohnzimmer
homekit_aid: 32
name: Sterne
@@ -843,17 +803,6 @@ devices:
topics:
state: "zigbee2mqtt/0xf0d1b8000017515d"
set: "zigbee2mqtt/0xf0d1b8000017515d/set"
- device_id: licht_kommode_schlafzimmer
homekit_aid: 65
name: Kommode Schlafzimmer
type: relay
cap_version: "relay@1.0.0"
technology: tasmota
features:
power: true
topics:
set: "cmnd/tasmota/04/POWER"
state: "stat/tasmota/04/POWER"
- device_id: licht_fensterbank_esszimmer
homekit_aid: 66
name: Fensterbank Esszimmer
@@ -1034,11 +983,172 @@ devices:
homekit_aid: 82
name: Herdlicht
type: light
cap_version: "relay@1.0.0"
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/herdlicht"
set: "zigbee2mqtt/herdlicht/set"
state: "zigbee2mqtt/0x64028ffffe50e79e"
set: "zigbee2mqtt/0x64028ffffe50e79e/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"
- device_id: steckdose_strandkorb
homekit_aid: 85
name: Steckdose Strandkorb
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/8"
state: "dt1/ci/8"
- device_id: steckdose_vor_waschkueche
homekit_aid: 86
name: Steckdose vor Waschküche
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/9"
state: "dt1/ci/9"
- device_id: wasser_vorne
homekit_aid: 87
name: Wasser Vorgarten
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/13"
state: "dt1/ci/13"
- device_id: wasser_hinten
homekit_aid: 88
name: Wasser Garten
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/12"
state: "dt1/ci/12"
- device_id: lampe_haustuer
homekit_aid: 89
name: Lampe Haustür
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/3"
state: "dt1/ci/3"
- device_id: power_relay_oven
homekit_aid: 90
name: Schütz Herd
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/1"
state: "dt1/di/1"
- device_id: power_relay_kitchen
homekit_aid: 91
name: Schütz Küche
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/0"
state: "dt1/di/0"
- device_id: power_relay_laundry
homekit_aid: 92
name: Schütz Waschküche
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/2"
state: "dt1/di/2"
- device_id: spot_garden
homekit_aid: 93
name: Spot Garten
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "dt1/coil/6"
state: "dt1/ci/6"
- device_id: licht_schuppen
homekit_aid: 94
name: Licht Schuppen
type: relay
cap_version: "relay@1.0.0"
technology: hottis_wago_modbus
features:
power: true
topics:
set: "pulsegen/command/5/18"
state: "pulsegen/status/5"
- device_id: licht_flur_oben_am_spiegel
homekit_aid: 95
name: Spiegel
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b80000195038"
set: "zigbee2mqtt/0xf0d1b80000195038/set"
- device_id: licht_kommode_schlafzimmer
homekit_aid: 96
name: Kommode Schlafzimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0x842e14fffefe4ba4"
set: "zigbee2mqtt/0x842e14fffefe4ba4/set"

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: 🌡️
@@ -261,6 +265,14 @@ rooms:
title: Schranklicht vor Küche
icon: 💡
rank: 232
- device_id: regallicht_flur
title: Regallicht Flur
icon: 💡
rank: 233
- device_id: lampe_haustuer
title: Lampe Haustür
icon: 💡
rank: 234
- device_id: sensor_flur
title: Temperatur & Luftfeuchte
icon: 🌡️
@@ -341,6 +353,30 @@ rooms:
title: Gartenlicht vorne
icon: 💡
rank: 291
- device_id: spot_garden
title: Spot Garten
icon: 💡
rank: 292
- device_id: licht_schuppen
title: Licht Schuppen
icon: 💡
rank: 293
- device_id: steckdose_strandkorb
title: Steckdose Strandkorb
icon: 🔌
rank: 294
- device_id: steckdose_vor_waschkueche
title: Steckdose vor Waschküche
icon: 🔌
rank: 295
- device_id: wasser_vorne
title: Wasser Vorgarten
icon: 💧
rank: 296
- device_id: wasser_hinten
title: Wasser Garten
icon: 💧
rank: 297
- id: garage
name: Garage
devices:
@@ -367,6 +403,21 @@ rooms:
title: Werkstatt Licht
icon: 💡
rank: 350
- id: devices
name: Devices
devices:
- device_id: power_relay_oven
title: Schütz Herd
icon:
rank: 400
- device_id: power_relay_kitchen
title: Schütz Küche
icon:
rank: 405
- device_id: power_relay_laundry
title: Schütz Waschküche
icon:
rank: 410

View File

@@ -1,9 +1,19 @@
# Rules Configuration
# Auto-generated from devices.yaml
rules:
- id: window_setback_bad_unten
enabled: true
name: Fensterabsenkung Bad Unten
type: window_setback@1.0
objects:
contacts:
- kontakt_bad_unten_strasse
thermostats:
- thermostat_bad_unten
params:
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
- id: window_setback_esszimmer
enabled: false
enabled: true
name: Fensterabsenkung Esszimmer
type: window_setback@1.0
objects:
@@ -13,12 +23,27 @@ rules:
thermostats:
- thermostat_esszimmer
params:
eco_target: 16.0
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_wohnzimmer
enabled: true
name: Fensterabsenkung Wohnzimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_wohnzimmer_garten_links
- kontakt_wohnzimmer_garten_rechts
thermostats:
- thermostat_wohnzimmer
params:
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_kueche
enabled: false
enabled: true
name: Fensterabsenkung Küche
type: window_setback@1.0
objects:
@@ -30,12 +55,12 @@ rules:
thermostats:
- thermostat_kueche
params:
eco_target: 16.0
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_patty
enabled: false
enabled: true
name: Fensterabsenkung Arbeitszimmer Patty
type: window_setback@1.0
objects:
@@ -46,12 +71,12 @@ rules:
thermostats:
- thermostat_patty
params:
eco_target: 16.0
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_schlafzimmer
enabled: false
enabled: true
name: Fensterabsenkung Schlafzimmer
type: window_setback@1.0
objects:
@@ -60,22 +85,7 @@ rules:
thermostats:
- thermostat_schlafzimmer
params:
eco_target: 16.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
- id: window_setback_wohnzimmer
enabled: false
name: Fensterabsenkung Wohnzimmer
type: window_setback@1.0
objects:
contacts:
- kontakt_wohnzimmer_garten_links
- kontakt_wohnzimmer_garten_rechts
thermostats:
- thermostat_wohnzimmer
params:
eco_target: 16.0
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
previous_target_ttl_secs: 86400
@@ -89,6 +99,19 @@ rules:
thermostats:
- thermostat_wolfgang
params:
eco_target: 16.0
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20
- id: window_setback_bad_oben
enabled: true
name: Fensterabsenkung Bad Oben
type: window_setback@1.0
objects:
contacts:
- kontakt_bad_oben_strasse
thermostats:
- thermostat_bad_oben
params:
eco_target: 5.0
open_min_secs: 20
close_min_secs: 20

View File

@@ -8,6 +8,8 @@ from packages.home_capabilities.contact_sensor import CAP_VERSION as CONTACT_SEN
from packages.home_capabilities.contact_sensor import ContactState
from packages.home_capabilities.temp_humidity_sensor import CAP_VERSION as TEMP_HUMIDITY_SENSOR_VERSION
from packages.home_capabilities.temp_humidity_sensor import TempHumidityState
from packages.home_capabilities.switch import CAP_VERSION as SWITCH_VERSION
from packages.home_capabilities.switch import SwitchState
from packages.home_capabilities.relay import CAP_VERSION as RELAY_VERSION
from packages.home_capabilities.relay import RelayState
from packages.home_capabilities.three_phase_powermeter import CAP_VERSION as THREE_PHASE_POWERMETER_VERSION
@@ -42,6 +44,8 @@ __all__ = [
"CONTACT_SENSOR_VERSION",
"TempHumidityState",
"TEMP_HUMIDITY_SENSOR_VERSION",
"SwitchState",
"SWITCH_VERSION",
"RelayState",
"RELAY_VERSION",
"DeviceTile",

View File

@@ -0,0 +1,69 @@
"""Switch Capability - Wireless Button/Switch (read-only).
This module defines the SwitchState model for wireless switches/buttons.
These devices report action events (e.g., button presses) and are read-only devices.
Capability Version: switch@1.0.0
"""
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, Field
# Capability metadata
CAP_VERSION = "switch@1.0.0"
DISPLAY_NAME = "Switch"
class SwitchState(BaseModel):
"""State model for wireless switches/buttons.
Wireless switches are read-only devices that report button actions such as
single press, double press, long press, etc. They typically also report
battery level and signal quality.
Attributes:
action: Action type (e.g., "single", "double", "long", "hold", etc.)
battery: Battery level percentage (0-100), optional
linkquality: MQTT link quality indicator, optional
voltage: Battery voltage in mV, optional
ts: Timestamp of the action event, optional
Examples:
>>> SwitchState(action="single")
SwitchState(action='single', battery=None, ...)
>>> SwitchState(action="double", battery=95, linkquality=87)
SwitchState(action='double', battery=95, linkquality=87, ...)
"""
action: str = Field(
...,
description="Action type: 'single', 'double', 'long', 'hold', etc."
)
battery: Annotated[int, Field(ge=0, le=100)] | None = Field(
None,
description="Battery level in percent (0-100)"
)
linkquality: int | None = Field(
None,
description="Link quality indicator (typically 0-255)"
)
voltage: int | None = Field(
None,
description="Battery voltage in millivolts"
)
ts: datetime | None = Field(
None,
description="Timestamp of the action event"
)
# Public API
__all__ = ["SwitchState", "CAP_VERSION", "DISPLAY_NAME"]

15
tools/deploy-configuration.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
NAMESPACE=homea2
kubectl create configmap home-automation-config \
--from-file=devices.yaml=config/devices.yaml \
--from-file=groups.yaml=config/groups.yaml \
--from-file=layout.yaml=config/layout.yaml \
--from-file=rules.yaml=config/rules.yaml \
--from-file=scenes.yaml=config/scenes.yaml \
--namespace=$NAMESPACE \
--dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f deployment/configmap.yaml -n $NAMESPACE