Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9ba478c34d
|
|||
|
15e132b187
|
|||
|
f40887ec37
|
|||
|
507f6f3854
|
|||
|
f163bb09bf
|
|||
|
54fdcc12e1
|
|||
|
9f725c4c70
|
|||
|
f1dbd9344d
|
|||
|
5a67d7b330
|
|||
|
cc342245f8
|
|||
|
50253d536d
|
|||
|
e0aa50c9d2
|
|||
|
dc20d9f4b2
|
|||
|
ffb35928b4
|
|||
|
ac84ff7103
|
|||
|
c185494da3
|
@@ -11,6 +11,7 @@ matrix:
|
||||
- abstraction
|
||||
- rules
|
||||
- static
|
||||
- pulsegen
|
||||
- homekit
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,6 +16,7 @@ matrix:
|
||||
- abstraction
|
||||
- rules
|
||||
- static
|
||||
- pulsegen
|
||||
|
||||
steps:
|
||||
deploy-${APP}:
|
||||
|
||||
@@ -14,7 +14,7 @@ class ContactAccessory(Accessory):
|
||||
|
||||
category = CATEGORY_SENSOR
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
"""
|
||||
Initialize the contact sensor accessory.
|
||||
|
||||
@@ -22,9 +22,8 @@ class ContactAccessory(Accessory):
|
||||
driver: HAP driver instance
|
||||
device: Device object from DeviceRegistry
|
||||
api_client: ApiClient for sending commands
|
||||
display_name: Optional display name (defaults to device.friendly_name)
|
||||
"""
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
super().__init__(driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
|
||||
@@ -16,7 +16,7 @@ class OnOffLightAccessory(Accessory):
|
||||
|
||||
category = CATEGORY_LIGHTBULB
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
"""
|
||||
Initialize the light accessory.
|
||||
|
||||
@@ -24,9 +24,8 @@ class OnOffLightAccessory(Accessory):
|
||||
driver: HAP driver instance
|
||||
device: Device object from DeviceRegistry
|
||||
api_client: ApiClient for sending commands
|
||||
display_name: Optional display name (defaults to device.friendly_name)
|
||||
"""
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
super().__init__(driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
@@ -57,9 +56,9 @@ class OnOffLightAccessory(Accessory):
|
||||
class DimmableLightAccessory(OnOffLightAccessory):
|
||||
"""Dimmable Light with brightness control."""
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
# Don't call super().__init__() yet - we need to set up service first
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
Accessory.__init__(self, driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
@@ -106,9 +105,9 @@ class DimmableLightAccessory(OnOffLightAccessory):
|
||||
class ColorLightAccessory(DimmableLightAccessory):
|
||||
"""RGB Light with full color control."""
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
# Don't call super().__init__() - build everything from scratch
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
Accessory.__init__(self, driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
|
||||
@@ -15,7 +15,7 @@ class OutletAccessory(Accessory):
|
||||
|
||||
category = CATEGORY_OUTLET
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
"""
|
||||
Initialize the outlet accessory.
|
||||
|
||||
@@ -23,9 +23,8 @@ class OutletAccessory(Accessory):
|
||||
driver: HAP driver instance
|
||||
device: Device object from DeviceRegistry
|
||||
api_client: ApiClient for sending commands
|
||||
display_name: Optional display name (defaults to device.friendly_name)
|
||||
"""
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
super().__init__(driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
|
||||
@@ -15,7 +15,7 @@ class TempHumidityAccessory(Accessory):
|
||||
|
||||
category = CATEGORY_SENSOR
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
"""
|
||||
Initialize the temp/humidity sensor accessory.
|
||||
|
||||
@@ -23,9 +23,8 @@ class TempHumidityAccessory(Accessory):
|
||||
driver: HAP driver instance
|
||||
device: Device object from DeviceRegistry
|
||||
api_client: ApiClient for sending commands
|
||||
display_name: Optional display name (defaults to device.friendly_name)
|
||||
"""
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
super().__init__(driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
|
||||
@@ -17,7 +17,7 @@ class ThermostatAccessory(Accessory):
|
||||
|
||||
category = CATEGORY_THERMOSTAT
|
||||
|
||||
def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs):
|
||||
def __init__(self, driver, device, api_client, *args, **kwargs):
|
||||
"""
|
||||
Initialize the thermostat accessory.
|
||||
|
||||
@@ -25,9 +25,8 @@ class ThermostatAccessory(Accessory):
|
||||
driver: HAP driver instance
|
||||
device: Device object from DeviceRegistry
|
||||
api_client: ApiClient for sending commands
|
||||
display_name: Optional display name (defaults to device.friendly_name)
|
||||
"""
|
||||
name = display_name or device.friendly_name or device.name
|
||||
name = device.name
|
||||
super().__init__(driver, name, *args, **kwargs)
|
||||
self.device = device
|
||||
self.api_client = api_client
|
||||
|
||||
@@ -50,26 +50,7 @@ class ApiClient:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get devices: {e}")
|
||||
raise
|
||||
|
||||
def get_layout(self) -> Dict:
|
||||
"""
|
||||
Get layout information (rooms and device assignments).
|
||||
|
||||
Returns:
|
||||
Layout dictionary with room structure
|
||||
"""
|
||||
try:
|
||||
response = httpx.get(
|
||||
f'{self.base_url}/layout',
|
||||
headers=self.headers,
|
||||
timeout=self.timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get layout: {e}")
|
||||
raise
|
||||
|
||||
def get_device_state(self, device_id: str) -> Dict:
|
||||
"""
|
||||
Get current state of a specific device.
|
||||
|
||||
@@ -18,8 +18,6 @@ class Device:
|
||||
device_id: str
|
||||
type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover"
|
||||
name: str # Short name from /devices
|
||||
friendly_name: str # Display title from /layout (fallback to name)
|
||||
room: Optional[str] # Room name from layout
|
||||
features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true})
|
||||
read_only: bool # True for sensors that don't accept commands
|
||||
|
||||
@@ -50,24 +48,7 @@ class DeviceRegistry:
|
||||
"""
|
||||
# Get devices and layout
|
||||
devices_data = api_client.get_devices()
|
||||
layout_data = api_client.get_layout()
|
||||
|
||||
# Build lookup: device_id -> (room_name, title)
|
||||
layout_map = {}
|
||||
if isinstance(layout_data, dict) and 'rooms' in layout_data:
|
||||
rooms_list = layout_data['rooms']
|
||||
if isinstance(rooms_list, list):
|
||||
for room in rooms_list:
|
||||
if isinstance(room, dict):
|
||||
room_name = room.get('name', 'Unknown')
|
||||
devices_in_room = room.get('devices', [])
|
||||
for device_info in devices_in_room:
|
||||
if isinstance(device_info, dict):
|
||||
device_id = device_info.get('device_id')
|
||||
title = device_info.get('title', '')
|
||||
if device_id:
|
||||
layout_map[device_id] = (room_name, title)
|
||||
|
||||
|
||||
# Create Device objects
|
||||
devices = []
|
||||
for dev_data in devices_data:
|
||||
@@ -76,9 +57,6 @@ class DeviceRegistry:
|
||||
logger.warning(f"Device without device_id: {dev_data}")
|
||||
continue
|
||||
|
||||
# Get layout info
|
||||
room_name, title = layout_map.get(device_id, (None, ''))
|
||||
|
||||
# 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']
|
||||
@@ -86,9 +64,7 @@ class DeviceRegistry:
|
||||
device = Device(
|
||||
device_id=device_id,
|
||||
type=device_type,
|
||||
name=dev_data.get('name', device_id),
|
||||
friendly_name=title or dev_data.get('name', device_id),
|
||||
room=room_name,
|
||||
name=device_id,
|
||||
features=dev_data.get('features', {}),
|
||||
read_only=read_only
|
||||
)
|
||||
|
||||
@@ -71,14 +71,9 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
|
||||
try:
|
||||
accessory = create_accessory_for_device(device, api_client, driver)
|
||||
if accessory:
|
||||
# Set room information in the accessory (HomeKit will use this for suggestions)
|
||||
if device.room:
|
||||
# Store room info for potential future use
|
||||
accessory._room_name = device.room
|
||||
|
||||
bridge.add_accessory(accessory)
|
||||
accessory_map[device.device_id] = accessory
|
||||
logger.info(f"Added accessory: {device.friendly_name} ({device.type}, {accessory.__class__.__name__}) in room: {device.room or 'Unknown'}")
|
||||
logger.info(f"Added accessory: {device.name} ({device.type}, {accessory.__class__.__name__})")
|
||||
else:
|
||||
logger.warning(f"No accessory mapping for device: {device.name} ({device.type})")
|
||||
except Exception as e:
|
||||
@@ -90,23 +85,6 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge:
|
||||
logger.info(f"Bridge built with {len(accessory_map)} accessories")
|
||||
return bridge
|
||||
|
||||
|
||||
def get_accessory_name(device) -> str:
|
||||
"""
|
||||
Build accessory name including room information.
|
||||
|
||||
Args:
|
||||
device: Device object from DeviceRegistry
|
||||
|
||||
Returns:
|
||||
Name string like "Device Name (Room)" or just "Device Name" if no room
|
||||
"""
|
||||
base_name = device.friendly_name or device.name
|
||||
if device.room:
|
||||
return f"{base_name} ({device.room})"
|
||||
return base_name
|
||||
|
||||
|
||||
def create_accessory_for_device(device, api_client: ApiClient, driver: AccessoryDriver):
|
||||
"""
|
||||
Create appropriate HomeKit accessory based on device type and features.
|
||||
@@ -115,32 +93,30 @@ def create_accessory_for_device(device, api_client: ApiClient, driver: Accessory
|
||||
"""
|
||||
device_type = device.type
|
||||
features = device.features
|
||||
display_name = get_accessory_name(device)
|
||||
|
||||
# Light accessories
|
||||
if device_type == "light":
|
||||
if features.get("color_hsb"):
|
||||
return ColorLightAccessory(driver, device, api_client, display_name=display_name)
|
||||
return ColorLightAccessory(driver, device, api_client)
|
||||
elif features.get("brightness"):
|
||||
return DimmableLightAccessory(driver, device, api_client, display_name=display_name)
|
||||
return DimmableLightAccessory(driver, device, api_client)
|
||||
else:
|
||||
return OnOffLightAccessory(driver, device, api_client, display_name=display_name)
|
||||
return OnOffLightAccessory(driver, device, api_client)
|
||||
|
||||
# Thermostat
|
||||
elif device_type == "thermostat":
|
||||
return ThermostatAccessory(driver, device, api_client, display_name=display_name)
|
||||
return ThermostatAccessory(driver, device, api_client)
|
||||
|
||||
# Contact sensor
|
||||
elif device_type == "contact":
|
||||
return ContactAccessory(driver, device, api_client, display_name=display_name)
|
||||
return ContactAccessory(driver, device, api_client)
|
||||
|
||||
# Temperature/Humidity sensor
|
||||
elif device_type == "temp_humidity_sensor":
|
||||
return TempHumidityAccessory(driver, device, api_client, display_name=display_name)
|
||||
|
||||
return TempHumidityAccessory(driver, device, api_client)
|
||||
# Relay/Outlet
|
||||
elif device_type == "relay":
|
||||
return OutletAccessory(driver, device, api_client, display_name=display_name)
|
||||
return OutletAccessory(driver, device, api_client)
|
||||
|
||||
# Cover/Blinds (optional)
|
||||
elif device_type == "cover":
|
||||
|
||||
35
apps/pulsegen/Dockerfile
Normal file
35
apps/pulsegen/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# Pulsegen Dockerfile
|
||||
# MQTT Pulse Generator Worker
|
||||
|
||||
FROM python:3.14-alpine
|
||||
|
||||
# Prevent Python from writing .pyc files and enable unbuffered output
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
MQTT_BROKER=172.16.2.16 \
|
||||
MQTT_PORT=1883
|
||||
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 10001 -S app && \
|
||||
adduser -u 10001 -S app -G app
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install Python dependencies
|
||||
COPY apps/pulsegen/requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY apps/__init__.py /app/apps/__init__.py
|
||||
COPY apps/pulsegen/ /app/apps/pulsegen/
|
||||
|
||||
# Change ownership to app user
|
||||
RUN chown -R app:app /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER app
|
||||
|
||||
# Run application
|
||||
CMD ["python", "-m", "apps.pulsegen.main"]
|
||||
53
apps/pulsegen/README.md
Normal file
53
apps/pulsegen/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Pulsegen
|
||||
|
||||
MQTT-basierte Pulse-Generator Applikation für Home Automation.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- MQTT-Kommunikation über `aiomqtt`
|
||||
- Automatische Reconnect-Logik
|
||||
- Graceful shutdown (SIGTERM/SIGINT)
|
||||
- JSON message parsing
|
||||
- Konfigurierbar über Umgebungsvariablen
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
- `MQTT_BROKER`: MQTT Broker Hostname (default: `localhost`)
|
||||
- `MQTT_PORT`: MQTT Broker Port (default: `1883`)
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Lokal starten:
|
||||
|
||||
```bash
|
||||
cd apps/pulsegen
|
||||
python -m venv venv
|
||||
source venv/bin/activate # oder venv\Scripts\activate auf Windows
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Build:
|
||||
|
||||
```bash
|
||||
docker build -f apps/pulsegen/Dockerfile -t pulsegen .
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker run -e MQTT_BROKER=172.16.2.16 -e MQTT_PORT=1883 pulsegen
|
||||
```
|
||||
|
||||
## MQTT Topics
|
||||
|
||||
### Subscribed
|
||||
|
||||
- `pulsegen/command/#` - Kommandos für pulsegen
|
||||
- `home/+/+/state` - Device state updates
|
||||
|
||||
### Published
|
||||
|
||||
- `pulsegen/status` - Status-Updates der Applikation
|
||||
1
apps/pulsegen/__init__.py
Normal file
1
apps/pulsegen/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Pulsegen - MQTT pulse generator application."""
|
||||
226
apps/pulsegen/main.py
Normal file
226
apps/pulsegen/main.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""Pulsegen - MQTT pulse generator application."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
from typing import Any
|
||||
|
||||
from aiomqtt import Client, Message
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
COIL_STATUS_PREFIX = "dt1/di"
|
||||
COIL_STATUS_TOPIC = f"{COIL_STATUS_PREFIX}/+"
|
||||
PULSEGEN_COMMAND_PREFIX = "pulsegen/command"
|
||||
PULSEGEN_COMMAND_TOPIC = f"{PULSEGEN_COMMAND_PREFIX}/+/+"
|
||||
COIL_COMMAND_PREFIX = "dt1/coil"
|
||||
PULSEGEN_STATUS_PREFIX = "pulsegen/status"
|
||||
|
||||
COIL_STATUS_CACHE: dict[int, bool] = {}
|
||||
|
||||
def get_mqtt_settings() -> tuple[str, int]:
|
||||
"""Get MQTT broker settings from environment variables.
|
||||
|
||||
Returns:
|
||||
tuple: (broker_host, broker_port)
|
||||
"""
|
||||
broker = os.getenv("MQTT_BROKER", "localhost")
|
||||
port = int(os.getenv("MQTT_PORT", "1883"))
|
||||
logger.info(f"MQTT settings: broker={broker}, port={port}")
|
||||
return broker, port
|
||||
|
||||
|
||||
async def handle_message(message: Message, client: Client) -> None:
|
||||
"""Handle incoming MQTT message.
|
||||
|
||||
Args:
|
||||
message: MQTT message object
|
||||
client: MQTT client instance
|
||||
"""
|
||||
try:
|
||||
payload = message.payload.decode()
|
||||
logger.info(f"Received message on {message.topic}: {payload}")
|
||||
|
||||
try:
|
||||
topic = str(message.topic)
|
||||
|
||||
match topic.split("/"):
|
||||
case [prefix, di, coil_id] if f"{prefix}/{di}" == COIL_STATUS_PREFIX:
|
||||
try:
|
||||
coil_num = int(coil_id)
|
||||
except ValueError:
|
||||
logger.debug(f"Invalid coil id in topic: {topic}")
|
||||
return
|
||||
|
||||
state = payload.lower() in ("1", "true", "on")
|
||||
COIL_STATUS_CACHE[coil_num] = state
|
||||
logger.info(f"Updated coil {coil_num} status to {state}")
|
||||
|
||||
logger.info(f"Publishing pulsegen status for coil {coil_num}: {state}")
|
||||
await client.publish(
|
||||
topic=f"{PULSEGEN_STATUS_PREFIX}/{coil_num}",
|
||||
payload="on" if state else "off",
|
||||
qos=1,
|
||||
retain=True,
|
||||
)
|
||||
|
||||
case [prefix, command, coil_in_id, coil_out_id] if f"{prefix}/{command}" == PULSEGEN_COMMAND_PREFIX:
|
||||
try:
|
||||
coil_in_id = int(coil_in_id)
|
||||
coil_out_id = int(coil_out_id)
|
||||
except ValueError:
|
||||
logger.debug(f"Invalid coil id in topic: {topic}")
|
||||
return
|
||||
|
||||
try:
|
||||
coil_state = COIL_STATUS_CACHE[coil_in_id]
|
||||
except KeyError:
|
||||
logger.debug(f"Coil {coil_in_id} status unknown, cannot process command")
|
||||
return
|
||||
|
||||
cmd = payload.lower() in ("1", "true", "on")
|
||||
|
||||
if cmd == coil_state:
|
||||
logger.info(f"Coil {coil_in_id} already in desired state {cmd}, ignoring command")
|
||||
return
|
||||
|
||||
logger.info(f"Received pulsegen command on {topic}: {coil_in_id=}, {coil_out_id=}, {cmd=}")
|
||||
|
||||
|
||||
coil_cmd_topic = f"{COIL_COMMAND_PREFIX}/{coil_out_id}"
|
||||
|
||||
logger.info(f"Sending raising edge command: topic={coil_cmd_topic}")
|
||||
await client.publish(
|
||||
topic=coil_cmd_topic,
|
||||
payload="1",
|
||||
qos=1,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
logger.info(f"Sending falling edge command: topic={coil_cmd_topic}")
|
||||
await client.publish(
|
||||
topic=coil_cmd_topic,
|
||||
payload="0",
|
||||
qos=1,
|
||||
retain=False,
|
||||
)
|
||||
|
||||
case _:
|
||||
logger.debug(f"Ignoring message on unrelated topic: {topic}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Exception when handling payload: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling message: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def publish_example(client: Client) -> None:
|
||||
"""Example function to publish MQTT messages.
|
||||
|
||||
Args:
|
||||
client: MQTT client instance
|
||||
"""
|
||||
topic = "pulsegen/status"
|
||||
payload = {
|
||||
"status": "running",
|
||||
"timestamp": asyncio.get_event_loop().time()
|
||||
}
|
||||
|
||||
await client.publish(
|
||||
topic=topic,
|
||||
payload=json.dumps(payload),
|
||||
qos=1
|
||||
)
|
||||
logger.info(f"Published to {topic}: {payload}")
|
||||
|
||||
|
||||
async def mqtt_worker(shutdown_event: asyncio.Event) -> None:
|
||||
"""Main MQTT worker loop.
|
||||
|
||||
Connects to MQTT broker, subscribes to topics, and processes messages.
|
||||
|
||||
Args:
|
||||
shutdown_event: Event to signal shutdown
|
||||
"""
|
||||
broker, port = get_mqtt_settings()
|
||||
|
||||
|
||||
reconnect_interval = 5 # seconds
|
||||
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
logger.info(f"Connecting to MQTT broker {broker}:{port}...")
|
||||
|
||||
async with Client(
|
||||
hostname=broker,
|
||||
port=port,
|
||||
identifier="pulsegen"
|
||||
) as client:
|
||||
logger.info("Connected to MQTT broker")
|
||||
|
||||
# Subscribe to topics
|
||||
for topic in [PULSEGEN_COMMAND_TOPIC, COIL_STATUS_TOPIC]:
|
||||
await client.subscribe(topic)
|
||||
logger.info(f"Subscribed to {topic}")
|
||||
|
||||
# Publish startup message
|
||||
await publish_example(client)
|
||||
|
||||
# Message loop
|
||||
async for message in client.messages:
|
||||
if shutdown_event.is_set():
|
||||
break
|
||||
try:
|
||||
await handle_message(message, client)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in message handler: {e}", exc_info=True)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("MQTT worker cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT error: {e}", exc_info=True)
|
||||
if not shutdown_event.is_set():
|
||||
logger.info(f"Reconnecting in {reconnect_interval} seconds...")
|
||||
await asyncio.sleep(reconnect_interval)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main application entry point."""
|
||||
logger.info("Starting pulsegen application...")
|
||||
|
||||
# Shutdown event for graceful shutdown
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
# Setup signal handlers
|
||||
def signal_handler(sig: int) -> None:
|
||||
logger.info(f"Received signal {sig}, initiating shutdown...")
|
||||
shutdown_event.set()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
loop.add_signal_handler(sig, lambda s=sig: signal_handler(s))
|
||||
|
||||
# Start MQTT worker
|
||||
worker_task = asyncio.create_task(mqtt_worker(shutdown_event))
|
||||
|
||||
# Wait for shutdown signal
|
||||
await shutdown_event.wait()
|
||||
|
||||
# Wait for worker to finish
|
||||
await worker_task
|
||||
|
||||
logger.info("Pulsegen application stopped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
1
apps/pulsegen/requirements.txt
Normal file
1
apps/pulsegen/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
aiomqtt==2.3.0
|
||||
@@ -241,22 +241,6 @@ devices:
|
||||
ieee_address: "0x0017880108a03e45"
|
||||
model: "929002241201"
|
||||
vendor: "Philips"
|
||||
- device_id: haustuer
|
||||
name: Haustür-Lampe
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: true
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xec1bbdfffea6a3da"
|
||||
set: "zigbee2mqtt/0xec1bbdfffea6a3da/set"
|
||||
metadata:
|
||||
friendly_name: "Haustür"
|
||||
ieee_address: "0xec1bbdfffea6a3da"
|
||||
model: "LED1842G3"
|
||||
vendor: "IKEA"
|
||||
- device_id: deckenlampe_flur_oben
|
||||
name: Deckenlampe oben
|
||||
type: light
|
||||
@@ -794,6 +778,17 @@ devices:
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xbc33acfffe21f547"
|
||||
set: "zigbee2mqtt/0xbc33acfffe21f547/set"
|
||||
- device_id: kueche_fensterbank_licht
|
||||
name: Fensterbank Küche
|
||||
type: light
|
||||
cap_version: "light@1.2.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: true
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b8000017515d"
|
||||
set: "zigbee2mqtt/0xf0d1b8000017515d/set"
|
||||
- device_id: licht_kommode_schlafzimmer
|
||||
name: Kommode Schlafzimmer
|
||||
type: relay
|
||||
@@ -804,6 +799,56 @@ devices:
|
||||
topics:
|
||||
set: "cmnd/tasmota/04/POWER"
|
||||
state: "stat/tasmota/04/POWER"
|
||||
- device_id: licht_fensterbank_esszimmer
|
||||
name: Fensterbank Esszimmer
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: tasmota
|
||||
features:
|
||||
power: true
|
||||
topics:
|
||||
set: "cmnd/tasmota/02/POWER"
|
||||
state: "stat/tasmota/02/POWER"
|
||||
- device_id: licht_schreibtisch_patty
|
||||
name: Schreibtisch Patty
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: tasmota
|
||||
features:
|
||||
power: true
|
||||
topics:
|
||||
set: "cmnd/tasmota/03/POWER"
|
||||
state: "stat/tasmota/03/POWER"
|
||||
- device_id: kugeln_regal_flur
|
||||
name: Kugeln Regal Flur
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: tasmota
|
||||
features:
|
||||
power: true
|
||||
topics:
|
||||
set: "cmnd/tasmota/01/POWER"
|
||||
state: "stat/tasmota/01/POWER"
|
||||
- device_id: schrank_flur_haustür
|
||||
name: Schrank Flur Haustür
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: tasmota
|
||||
features:
|
||||
power: true
|
||||
topics:
|
||||
set: "cmnd/tasmota/05/POWER"
|
||||
state: "stat/tasmota/05/POWER"
|
||||
- device_id: gartenlicht_vorne
|
||||
name: Gartenlicht vorne
|
||||
type: relay
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: tasmota
|
||||
features:
|
||||
power: true
|
||||
topics:
|
||||
set: "cmnd/tasmota/06/POWER"
|
||||
state: "stat/tasmota/06/POWER"
|
||||
|
||||
- device_id: power_relay_caroutlet
|
||||
name: Car Outlet
|
||||
@@ -824,4 +869,26 @@ devices:
|
||||
topics:
|
||||
state: "IoT/Car/Values"
|
||||
|
||||
- device_id: schranklicht_flur_vor_kueche
|
||||
name: Schranklicht Flur vor Küche
|
||||
type: light
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
topics:
|
||||
state: "zigbee2mqtt/0xf0d1b80000155a1f"
|
||||
set: "zigbee2mqtt/0xf0d1b80000155a1f/set"
|
||||
- device_id: deckenlampe_wohnzimmer
|
||||
name: Deckenlampe Wohnzimmer
|
||||
type: light
|
||||
cap_version: "relay@1.0.0"
|
||||
technology: zigbee2mqtt
|
||||
features:
|
||||
power: true
|
||||
brightness: true
|
||||
topics:
|
||||
state: "zigbee2mqtt/0x842e14fffea72027"
|
||||
set: "zigbee2mqtt/0x842e14fffea72027/set"
|
||||
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ rooms:
|
||||
title: Leselampe Esszimmer
|
||||
icon: 💡
|
||||
rank: 60
|
||||
# - device_id: standlampe_esszimmer
|
||||
# title: Standlampe Esszimmer
|
||||
# icon: 💡
|
||||
# rank: 70
|
||||
- device_id: licht_fensterbank_esszimmer
|
||||
title: Fensterbank Esszimmer
|
||||
icon: 💡
|
||||
rank: 70
|
||||
- device_id: kleine_lampe_links_esszimmer
|
||||
title: kleine Lampe links Esszimmer
|
||||
icon: 💡
|
||||
@@ -101,6 +101,10 @@ rooms:
|
||||
title: Regallicht Wohnzimmer
|
||||
icon: 💡
|
||||
rank: 132
|
||||
- device_id: deckenlampe_wohnzimmer
|
||||
title: Deckenlampe Wohnzimmer
|
||||
icon: 💡
|
||||
rank: 133
|
||||
- device_id: thermostat_wohnzimmer
|
||||
title: Thermostat Wohnzimmer
|
||||
icon: 🌡️
|
||||
@@ -131,6 +135,10 @@ rooms:
|
||||
title: Küche Putzlicht
|
||||
icon: 💡
|
||||
rank: 143
|
||||
- device_id: kueche_fensterbank_licht
|
||||
title: Küche Fensterbank
|
||||
icon: 💡
|
||||
rank: 144
|
||||
- device_id: thermostat_kueche
|
||||
title: Kueche
|
||||
icon: 🌡️
|
||||
@@ -173,6 +181,10 @@ rooms:
|
||||
title: Kugellampe Patty
|
||||
icon: 💡
|
||||
rank: 181
|
||||
- device_id: licht_schreibtisch_patty
|
||||
title: Licht Schreibtisch Patty
|
||||
icon: 💡
|
||||
rank: 182
|
||||
- device_id: thermostat_patty
|
||||
title: Thermostat Patty
|
||||
icon: 🌡️
|
||||
@@ -217,18 +229,22 @@ rooms:
|
||||
title: Deckenlampe Flur oben
|
||||
icon: 💡
|
||||
rank: 210
|
||||
- device_id: haustuer
|
||||
title: Haustür
|
||||
icon: 💡
|
||||
rank: 220
|
||||
- device_id: licht_flur_schrank
|
||||
title: Schranklicht Flur
|
||||
- device_id: kugeln_regal_flur
|
||||
title: Kugeln Regal
|
||||
icon: 💡
|
||||
rank: 222
|
||||
- device_id: licht_flur_oben_am_spiegel
|
||||
title: Licht Flur oben am Spiegel
|
||||
title: Licht oben am Spiegel
|
||||
icon: 💡
|
||||
rank: 230
|
||||
- device_id: schrank_flur_haustür
|
||||
title: Schranklicht an der Haustür
|
||||
icon: 💡
|
||||
rank: 231
|
||||
- device_id: schranklicht_flur_vor_kueche
|
||||
title: Schranklicht vor Küche
|
||||
icon: 💡
|
||||
rank: 232
|
||||
- device_id: sensor_flur
|
||||
title: Temperatur & Luftfeuchte
|
||||
icon: 🌡️
|
||||
@@ -291,6 +307,10 @@ rooms:
|
||||
title: Licht Terasse
|
||||
icon: 💡
|
||||
rank: 290
|
||||
- device_id: gartenlicht_vorne
|
||||
title: Gartenlicht vorne
|
||||
icon: 💡
|
||||
rank: 291
|
||||
- name: Garage
|
||||
devices:
|
||||
- device_id: power_relay_caroutlet
|
||||
|
||||
51
deployment/pulsegen-deployment.yaml
Normal file
51
deployment/pulsegen-deployment.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: pulsegen
|
||||
namespace: homea2
|
||||
labels:
|
||||
app: pulsegen
|
||||
component: home-automation
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: pulsegen
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
configmap.reloader.stakater.com/reload: "home-automation-environment"
|
||||
labels:
|
||||
app: pulsegen
|
||||
component: home-automation
|
||||
spec:
|
||||
containers:
|
||||
- name: pulsegen
|
||||
image: %IMAGE%
|
||||
env:
|
||||
- name: MQTT_BROKER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: home-automation-environment
|
||||
key: SHARED_MQTT_BROKER
|
||||
- name: MQTT_PORT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: home-automation-environment
|
||||
key: SHARED_MQTT_PORT
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- "ps aux | grep -v grep | grep python"
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
Reference in New Issue
Block a user