16 Commits

18 changed files with 2231 additions and 262 deletions

41
DEVICES_BY_ROOM.md Normal file
View File

@@ -0,0 +1,41 @@
Schlafzimmer:
- Bettlicht Patty | 0x0017880108158b32
- Bettlicht Wolfgang | 0x00178801081570bf
- Deckenlampe Schlafzimmer | 0x0017880108a406a7
- Medusa-Lampe Schlafzimmer | 0xf0d1b80000154c7c
Esszimmer:
- Deckenlampe Esszimmer | 0x0017880108a03e45
- Leselampe Esszimmer | 0xec1bbdfffe7b84f2
- Standlampe Esszimmer | 0xbc33acfffe21f547
- kleine Lampe links Esszimmer | 0xf0d1b80000153099
- kleine Lampe rechts Esszimmer | 0xf0d1b80000156645
Wohnzimmer:
- Lampe Naehtischchen Wohnzimmer | 0x842e14fffee560ee
- Lampe Semeniere Wohnzimmer | 0xf0d1b8000015480b
- Sterne Wohnzimmer | 0xf0d1b80000155fc2
- grosse Lampe Wohnzimmer | 0xf0d1b80000151aca
Küche:
- Küche Deckenlampe | 0x001788010d2c40c4
- Kueche | 0x94deb8fffe2e5c06
Arbeitszimmer Patty:
- Leselampe Patty | 0x001788010600ec9d
- Schranklicht hinten Patty | 0x0017880106e29571
- Schranklicht vorne Patty | 0xf0d1b80000154cf5
Arbeitszimmer Wolfgang:
- Wolfgang | 0x540f57fffe7e3cfe
- ExperimentLabTest | 0xf0d1b80000195038
Flur:
- Deckenlampe Flur oben | 0x001788010d2123a7
- Haustür | 0xec1bbdfffea6a3da
- Licht Flur oben am Spiegel | 0x842e14fffefe4ba4
Sportzimmer:
- Sportlicht Regal | 0xf0d1b8be2409f569
- Sportlicht Tisch | 0xf0d1b8be2409f31b
- Sportlicht am Fernseher, Studierzimmer | 0x842e14fffe76a23a

223
MAX_INTEGRATION.md Normal file
View File

@@ -0,0 +1,223 @@
# MAX! (eQ-3) Thermostat Integration
## Overview
This document describes the integration of MAX! (eQ-3) thermostats via Homegear into the home automation system.
## Protocol Characteristics
MAX! thermostats use a **simple integer-based protocol** (not JSON):
- **SET messages**: Plain integer temperature value (e.g., `22`)
- **STATE messages**: Plain integer temperature value (e.g., `22`)
- **Topics**: Homegear MQTT format
### MQTT Topics
**SET Command:**
```
homegear/instance1/set/<peerId>/<channel>/SET_TEMPERATURE
Payload: "22" (plain integer as string)
```
**STATE Update:**
```
homegear/instance1/plain/<peerId>/<channel>/SET_TEMPERATURE
Payload: "22" (plain integer as string)
```
## Transformation Layer
The abstraction layer provides automatic transformation between the abstract home protocol and MAX! format.
### Abstract → MAX! (SET)
**Input (Abstract):**
```json
{
"mode": "heat",
"target": 22.5
}
```
**Output (MAX!):**
```
22
```
**Transformation Rules:**
- Extract `target` temperature
- Convert float → integer (round to nearest)
- Return as plain string (no JSON)
- Ignore `mode` field (MAX! always in heating mode)
### MAX! → Abstract (STATE)
**Input (MAX!):**
```
22
```
**Output (Abstract):**
```json
{
"target": 22.0,
"mode": "heat"
}
```
**Transformation Rules:**
- Parse plain string/integer value
- Convert to float
- Add default `mode: "heat"` (MAX! always heating)
- Wrap in abstract payload structure
## Device Configuration
### Example devices.yaml Entry
```yaml
- device_id: "thermostat_wolfgang"
type: "thermostat"
cap_version: "thermostat@1.0.0"
technology: "max"
features:
mode: true
target: true
current: false # SET_TEMPERATURE doesn't report current temp
topics:
set: "homegear/instance1/set/39/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/39/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Wolfgang"
location: "Arbeitszimmer Wolfgang"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "39"
channel: "1"
```
### Configuration Notes
1. **technology**: Must be set to `"max"` to activate MAX! transformations
2. **topics.set**: Use Homegear's `/set/` path with `/SET_TEMPERATURE` parameter
3. **topics.state**: Use Homegear's `/plain/` path with `/SET_TEMPERATURE` parameter
4. **features.current**: Set to `false` - SET_TEMPERATURE topic doesn't provide current temperature
5. **metadata**: Include `peer_id` and `channel` for reference
## Temperature Rounding
MAX! only supports **integer temperatures**. The system uses standard rounding:
| Abstract Input | MAX! Output |
|----------------|-------------|
| 20.4°C | 20 |
| 20.5°C | 20 |
| 20.6°C | 21 |
| 21.5°C | 22 |
| 22.5°C | 22 |
Python's `round()` function uses "banker's rounding" (round half to even).
## Limitations
1. **No current temperature**: SET_TEMPERATURE topic only reports target, not actual temperature
2. **No mode control**: MAX! thermostats are always in heating mode
3. **Integer only**: Temperature precision limited to 1°C steps
4. **No battery status**: Not available via SET_TEMPERATURE topic
5. **No window detection**: Not available via SET_TEMPERATURE topic
## Testing
Test the transformation functions:
```bash
poetry run python /tmp/test_max_transform.py
```
Expected output:
```
✅ PASS: Float 22.5 -> Integer string
✅ PASS: Integer string -> Abstract dict
✅ PASS: Integer -> Abstract dict
✅ PASS: Rounding works correctly
🎉 All MAX! transformation tests passed!
```
## Implementation Details
### Files Modified
1. **apps/abstraction/transformation.py**
- Added `_transform_thermostat_max_to_vendor()` - converts abstract → plain integer
- Added `_transform_thermostat_max_to_abstract()` - converts plain integer → abstract
- Registered handlers in `TRANSFORM_HANDLERS` registry
2. **apps/abstraction/main.py**
- Modified `handle_abstract_set()` to send plain string for MAX! devices (not JSON)
- Modified message processing to handle plain text payloads from MAX! STATE topics
### Transformation Functions
```python
def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
"""Convert {"target": 22.5} → "22" """
target_temp = payload.get("target", 21.0)
return str(int(round(target_temp)))
def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[str, Any]:
"""Convert "22" → {"target": 22.0, "mode": "heat"} """
target_temp = float(payload)
return {"target": target_temp, "mode": "heat"}
```
## Usage Example
### Setting Temperature via API
```bash
curl -X POST http://localhost:8001/devices/thermostat_wolfgang/set \
-H "Content-Type: application/json" \
-d '{
"type": "thermostat",
"payload": {
"mode": "heat",
"target": 22.5
}
}'
```
**Flow:**
1. API receives abstract payload: `{"mode": "heat", "target": 22.5}`
2. Abstraction transforms to MAX!: `"22"`
3. Publishes to: `homegear/instance1/set/39/1/SET_TEMPERATURE` with payload `22`
### Receiving State Updates
**Homegear publishes:**
```
Topic: homegear/instance1/plain/39/1/SET_TEMPERATURE
Payload: 22
```
**Flow:**
1. Abstraction receives plain text: `"22"`
2. Transforms to abstract: `{"target": 22.0, "mode": "heat"}`
3. Publishes to: `home/thermostat/thermostat_wolfgang/state`
4. Publishes to Redis: `ui:updates` channel for real-time UI updates
## Future Enhancements
Potential improvements for better MAX! integration:
1. **Current Temperature**: Subscribe to separate Homegear topic for actual temperature
2. **Battery Status**: Subscribe to LOWBAT or battery level topics
3. **Valve Position**: Monitor actual valve opening percentage
4. **Window Detection**: Subscribe to window open detection status
5. **Mode Control**: Support comfort/eco temperature presets
## Related Documentation
- [Homegear MAX! Documentation](https://doc.homegear.eu/data/homegear-max/)
- [Abstract Protocol Specification](docs/PROTOCOL.md)
- [Transformation Layer Design](apps/abstraction/README.md)

View File

@@ -0,0 +1,54 @@
# Nicht berücksichtigte Zigbee-Geräte
## Switches (0)
~~Gerät "Sterne Wohnzimmer" wurde als Light zu devices.yaml hinzugefügt~~
## Sensoren und andere Geräte (22)
### Tür-/Fenstersensoren (7)
- Wolfgang (MCCGQ11LM) - 0x00158d008b3328da
- Terassentür (MCCGQ11LM) - 0x00158d008b332788
- Garten Kueche (MCCGQ11LM) - 0x00158d008b332785
- Strasse rechts Kueche (MCCGQ11LM) - 0x00158d008b151803
- Strasse links Kueche (MCCGQ11LM) - 0x00158d008b331d0b
- Fenster Bad oben (MCCGQ11LM) - 0x00158d008b333aec
- Fenster Patty Strasse (MCCGQ11LM) - 0x00158d000af457cf
### Temperatur-/Feuchtigkeitssensoren (11)
- Kueche (WSDCGQ11LM) - 0x00158d00083299bb
- Wolfgang (WSDCGQ11LM) - 0x00158d000543fb99
- Patty (WSDCGQ11LM) - 0x00158d0003f052b7
- Schlafzimmer (WSDCGQ01LM) - 0x00158d00043292dc
- Bad oben (WSDCGQ11LM) - 0x00158d00093e8987
- Flur (WSDCGQ11LM) - 0x00158d000836ccc6
- Wohnzimmer (WSDCGQ11LM) - 0x00158d0008975707
- Bad unten (WSDCGQ11LM) - 0x00158d00093e662a
- Waschkueche (WSDCGQ11LM) - 0x00158d000449f3bc
- Studierzimmer (WSDCGQ11LM) - 0x00158d0009421422
- Wolfgang (SONOFF SNZB-02D) - 0x0ceff6fffe39a196
### Schalter (2)
- Schalter Schlafzimmer (Philips 929003017102) - 0x001788010cc490d4
- Schalter Bettlicht Patty (WXKG11LM) - 0x00158d000805d165
### Bewegungsmelder (1)
- Bewegungsmelder 8 (Philips 9290012607) - 0x001788010867d420
### Wasserleck-Sensor (1)
- unter Therme (SJCGQ11LM) - 0x00158d008b3a83a9
## Zusammenfassung
**Unterstützt in devices.yaml:**
- 24 Lampen (lights)
- 2 Thermostate
**Nicht unterstützt:**
- 0 Switches
- 7 Tür-/Fenstersensoren
- 11 Temperatur-/Feuchtigkeitssensoren
- 2 Schalter (Button-Devices)
- 1 Bewegungsmelder
- 1 Wasserleck-Sensor
Die nicht unterstützten Geräte könnten in Zukunft durch Erweiterung des Systems integriert werden.

View File

@@ -15,7 +15,7 @@ import uuid
from aiomqtt import Client from aiomqtt import Client
from pydantic import ValidationError from pydantic import ValidationError
from packages.home_capabilities import LightState, ThermostatState from packages.home_capabilities import LightState, ThermostatState, ContactState, TempHumidityState
from apps.abstraction.transformation import ( from apps.abstraction.transformation import (
transform_abstract_to_vendor, transform_abstract_to_vendor,
transform_vendor_to_abstract transform_vendor_to_abstract
@@ -89,12 +89,13 @@ def validate_devices(devices: list[dict[str, Any]]) -> None:
if "topics" not in device: if "topics" not in device:
raise ValueError(f"Device {device_id} missing 'topics'") raise ValueError(f"Device {device_id} missing 'topics'")
if "set" not in device["topics"]: # 'state' topic is required for all devices
raise ValueError(f"Device {device_id} missing 'topics.set'")
if "state" not in device["topics"]: if "state" not in device["topics"]:
raise ValueError(f"Device {device_id} missing 'topics.state'") raise ValueError(f"Device {device_id} missing 'topics.state'")
# 'set' topic is optional (read-only devices like contact sensors don't have it)
# No validation needed for topics.set
# Log loaded devices # Log loaded devices
device_ids = [d["device_id"] for d in devices] device_ids = [d["device_id"] for d in devices]
logger.info(f"Loaded {len(devices)} device(s): {', '.join(device_ids)}") logger.info(f"Loaded {len(devices)} device(s): {', '.join(device_ids)}")
@@ -166,6 +167,10 @@ async def handle_abstract_set(
# Validate against ThermostatState (current/battery/window_open are optional) # Validate against ThermostatState (current/battery/window_open are optional)
ThermostatState.model_validate(abstract_payload) ThermostatState.model_validate(abstract_payload)
elif device_type in {"contact", "contact_sensor"}:
# 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
except ValidationError as e: except ValidationError as e:
logger.error(f"Validation failed for {device_type} SET {device_id}: {e}") logger.error(f"Validation failed for {device_type} SET {device_id}: {e}")
return return
@@ -173,6 +178,11 @@ async def handle_abstract_set(
# Transform abstract payload to vendor-specific format # Transform abstract payload to vendor-specific format
vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload) vendor_payload = transform_abstract_to_vendor(device_type, device_technology, abstract_payload)
# For MAX! thermostats, vendor_payload is a plain string (integer temperature)
# For other devices, it's a dict that needs JSON encoding
if device_technology == "max" and device_type == "thermostat":
vendor_message = vendor_payload # Already a string
else:
vendor_message = json.dumps(vendor_payload) vendor_message = json.dumps(vendor_payload)
logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_message}") logger.info(f"→ vendor SET {device_id}: {vendor_topic}{vendor_message}")
@@ -209,12 +219,22 @@ async def handle_vendor_state(
elif device_type == "thermostat": elif device_type == "thermostat":
# Validate thermostat state: mode, target, current (required), battery, window_open # Validate thermostat state: mode, target, current (required), battery, window_open
ThermostatState.model_validate(abstract_payload) ThermostatState.model_validate(abstract_payload)
elif device_type in {"contact", "contact_sensor"}:
# Validate contact sensor state
ContactState.model_validate(abstract_payload)
elif device_type in {"temp_humidity", "temp_humidity_sensor"}:
# Validate temperature & humidity sensor state
TempHumidityState.model_validate(abstract_payload)
except ValidationError as e: except ValidationError as e:
logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}") logger.error(f"Validation failed for {device_type} STATE {device_id}: {e}")
return return
# Normalize device type for topic (use 'contact' for both 'contact' and 'contact_sensor')
topic_type = "contact" if device_type in {"contact", "contact_sensor"} else device_type
topic_type = "temp_humidity" if device_type in {"temp_humidity", "temp_humidity_sensor"} else topic_type
# Publish to abstract state topic (retained) # Publish to abstract state topic (retained)
abstract_topic = f"home/{device_type}/{device_id}/state" abstract_topic = f"home/{topic_type}/{device_id}/state"
abstract_message = json.dumps(abstract_payload) abstract_message = json.dumps(abstract_payload)
logger.info(f"← abstract STATE {device_id}: {abstract_topic}{abstract_message}") logger.info(f"← abstract STATE {device_id}: {abstract_topic}{abstract_message}")
@@ -268,15 +288,22 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
keepalive=keepalive, keepalive=keepalive,
timeout=10.0 # Add explicit timeout for operations timeout=10.0 # Add explicit timeout for operations
) as client: ) as client:
logger.info(f"Connected to MQTT broker as {client_id}") logger.info(f"Connected to MQTT broker as {unique_client_id}")
# Subscribe to abstract SET topics for all devices # Subscribe to topics for all devices
for device in devices.values(): for device in devices.values():
abstract_set_topic = f"home/{device['type']}/{device['device_id']}/set" device_id = device['device_id']
device_type = device['type']
# Subscribe to abstract SET topic only if device has a SET topic (not read-only)
if "set" in device["topics"]:
abstract_set_topic = f"home/{device_type}/{device_id}/set"
await client.subscribe(abstract_set_topic) await client.subscribe(abstract_set_topic)
logger.info(f"Subscribed to abstract SET: {abstract_set_topic}") logger.info(f"Subscribed to abstract SET: {abstract_set_topic}")
else:
logger.info(f"Skipping SET subscription for read-only device: {device_id}")
# Subscribe to vendor STATE topics # Subscribe to vendor STATE topics (all devices have state)
vendor_state_topic = device["topics"]["state"] vendor_state_topic = device["topics"]["state"]
await client.subscribe(vendor_state_topic) await client.subscribe(vendor_state_topic)
logger.info(f"Subscribed to vendor STATE: {vendor_state_topic}") logger.info(f"Subscribed to vendor STATE: {vendor_state_topic}")
@@ -294,6 +321,25 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
topic = str(message.topic) topic = str(message.topic)
payload_str = message.payload.decode() payload_str = message.payload.decode()
# Determine if message is from a MAX! device (requires plain text handling)
is_max_device = False
max_device_id = None
max_device_type = None
# Check if topic matches any MAX! device state topic
for device_id, device in devices.items():
if device.get("technology") == "max" and topic == device["topics"]["state"]:
is_max_device = True
max_device_id = device_id
max_device_type = device["type"]
break
# Parse payload based on device technology
if is_max_device:
# MAX! sends plain integer/string, not JSON
payload = payload_str.strip()
else:
# All other technologies use JSON
try: try:
payload = json.loads(payload_str) payload = json.loads(payload_str)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -318,7 +364,16 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
# Check if this is a vendor STATE message # Check if this is a vendor STATE message
else: else:
# Find device by vendor state topic # For MAX! devices, we already identified them above
if is_max_device:
device = devices[max_device_id]
device_technology = device.get("technology", "unknown")
await handle_vendor_state(
client, redis_client, max_device_id, max_device_type,
device_technology, payload, redis_channel
)
else:
# Find device by vendor state topic for other technologies
for device_id, device in devices.items(): for device_id, device in devices.items():
if topic == device["topics"]["state"]: if topic == device["topics"]["state"]:
device_technology = device.get("technology", "unknown") device_technology = device.get("technology", "unknown")

View File

@@ -111,19 +111,275 @@ def _transform_light_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[st
def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]: def _transform_thermostat_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract thermostat payload to zigbee2mqtt format. """Transform abstract thermostat payload to zigbee2mqtt format.
zigbee2mqtt uses same format as abstract protocol (no transformation needed). Transformations:
- target -> current_heating_setpoint (as string)
- mode is ignored (zigbee2mqtt thermostats use system_mode in state only)
Example:
- Abstract: {'target': 22.0}
- zigbee2mqtt: {'current_heating_setpoint': '22.0'}
""" """
return payload vendor_payload = {}
if "target" in payload:
# zigbee2mqtt expects current_heating_setpoint as string
vendor_payload["current_heating_setpoint"] = str(payload["target"])
return vendor_payload
def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]: def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform zigbee2mqtt thermostat payload to abstract format. """Transform zigbee2mqtt thermostat payload to abstract format.
zigbee2mqtt uses same format as abstract protocol (no transformation needed). Transformations:
- current_heating_setpoint -> target (as float)
- local_temperature -> current (as float)
- system_mode -> mode
Example:
- zigbee2mqtt: {'current_heating_setpoint': 15, 'local_temperature': 23, 'system_mode': 'heat'}
- Abstract: {'target': 15.0, 'current': 23.0, 'mode': 'heat'}
"""
abstract_payload = {}
# Extract target temperature
if "current_heating_setpoint" in payload:
setpoint = payload["current_heating_setpoint"]
abstract_payload["target"] = float(setpoint)
# Extract current temperature
if "local_temperature" in payload:
current = payload["local_temperature"]
abstract_payload["current"] = float(current)
# Extract mode
if "system_mode" in payload:
abstract_payload["mode"] = payload["system_mode"]
return abstract_payload
# ============================================================================
# HANDLER FUNCTIONS: contact_sensor - zigbee2mqtt technology
# ============================================================================
def _transform_contact_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract contact sensor payload to zigbee2mqtt format.
Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return payload
def _transform_contact_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform zigbee2mqtt contact sensor payload to abstract format.
Transformations:
- contact: bool -> "open" | "closed"
- zigbee2mqtt semantics: False = OPEN, True = CLOSED (inverted!)
- battery: pass through (already 0-100)
- linkquality: pass through
- device_temperature: pass through (if present)
- voltage: pass through (if present)
Example:
- zigbee2mqtt: {"contact": false, "battery": 100, "linkquality": 87}
- Abstract: {"contact": "open", "battery": 100, "linkquality": 87}
"""
abstract_payload = {}
# Transform contact state (inverted logic!)
if "contact" in payload:
contact_bool = payload["contact"]
# zigbee2mqtt: False = OPEN, True = CLOSED
abstract_payload["contact"] = "closed" if contact_bool else "open"
# Pass through optional fields
if "battery" in payload:
abstract_payload["battery"] = payload["battery"]
if "linkquality" in payload:
abstract_payload["linkquality"] = payload["linkquality"]
if "device_temperature" in payload:
abstract_payload["device_temperature"] = payload["device_temperature"]
if "voltage" in payload:
abstract_payload["voltage"] = payload["voltage"]
return abstract_payload
# ============================================================================
# HANDLER FUNCTIONS: contact_sensor - max technology (Homegear MAX!)
# ============================================================================
def _transform_contact_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract contact sensor payload to MAX! format.
Contact sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
logger.warning("Contact sensors are read-only - SET commands should not be used")
return payload
def _transform_contact_sensor_max_to_abstract(payload: str | bool | dict[str, Any]) -> dict[str, Any]:
"""Transform MAX! (Homegear) contact sensor payload to abstract format.
MAX! sends "true"/"false" (string or bool) on STATE topic.
Transformations:
- "true" or True -> "open" (window/door open)
- "false" or False -> "closed" (window/door closed)
Example:
- MAX!: "true" or True
- Abstract: {"contact": "open"}
"""
try:
# Handle string, bool, or dict input
if isinstance(payload, dict):
# If already a dict, extract contact field
contact_value = payload.get("contact", False)
elif isinstance(payload, str):
# Parse string to bool
contact_value = payload.strip().lower() == "true"
elif isinstance(payload, bool):
# Use bool directly
contact_value = payload
else:
logger.warning(f"MAX! contact sensor unexpected payload type: {type(payload)}, value: {payload}")
contact_value = False
# MAX! semantics: True = OPEN, False = CLOSED
return {
"contact": "open" if contact_value else "closed"
}
except (ValueError, TypeError) as e:
logger.error(f"MAX! contact sensor failed to parse: {payload}, error: {e}")
return {
"contact": "closed" # Default to closed on error
}
# ============================================================================
# HANDLER FUNCTIONS: temp_humidity_sensor - zigbee2mqtt technology
# ============================================================================
def _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract temp/humidity sensor payload to zigbee2mqtt format.
Temp/humidity sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
""" """
return payload return payload
def _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform zigbee2mqtt temp/humidity sensor payload to abstract format.
Passthrough - zigbee2mqtt provides temperature, humidity, battery, linkquality directly.
"""
return payload
# ============================================================================
# HANDLER FUNCTIONS: temp_humidity_sensor - MAX! technology
# ============================================================================
def _transform_temp_humidity_sensor_max_to_vendor(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform abstract temp/humidity sensor payload to MAX! format.
Temp/humidity sensors are read-only, so this should not be called for SET commands.
Returns payload as-is for compatibility.
"""
return payload
def _transform_temp_humidity_sensor_max_to_abstract(payload: dict[str, Any]) -> dict[str, Any]:
"""Transform MAX! temp/humidity sensor payload to abstract format.
Passthrough - MAX! provides temperature, humidity, battery directly.
"""
return payload
# ============================================================================
# HANDLER FUNCTIONS: max technology (Homegear MAX!)
# ============================================================================
def _transform_thermostat_max_to_vendor(payload: dict[str, Any]) -> str:
"""Transform abstract thermostat payload to MAX! (Homegear) format.
MAX! expects only the integer temperature value (no JSON).
Transformations:
- Extract 'target' temperature from payload
- Convert float to integer (MAX! only accepts integers)
- Return as plain string value
Example:
- Abstract: {'mode': 'heat', 'target': 22.5}
- MAX!: "22"
Note: MAX! ignores mode - it's always in heating mode
"""
if "target" not in payload:
logger.warning(f"MAX! thermostat payload missing 'target': {payload}")
return "21" # Default fallback
target_temp = payload["target"]
# Convert to integer (MAX! protocol requirement)
if isinstance(target_temp, (int, float)):
int_temp = int(round(target_temp))
return str(int_temp)
logger.warning(f"MAX! invalid target temperature type: {type(target_temp)}, value: {target_temp}")
return "21"
def _transform_thermostat_max_to_abstract(payload: str | int | float) -> dict[str, Any]:
"""Transform MAX! (Homegear) thermostat payload to abstract format.
MAX! sends only the integer temperature value (no JSON).
Transformations:
- Parse plain string/int value
- Convert to float for abstract protocol
- Wrap in abstract payload structure with mode='heat'
Example:
- MAX!: "22" or 22
- Abstract: {'target': 22.0, 'mode': 'heat'}
Note: MAX! doesn't send current temperature via SET_TEMPERATURE topic
"""
try:
# Handle both string and numeric input
if isinstance(payload, str):
target_temp = float(payload.strip())
elif isinstance(payload, (int, float)):
target_temp = float(payload)
else:
logger.warning(f"MAX! unexpected payload type: {type(payload)}, value: {payload}")
target_temp = 21.0
return {
"target": target_temp,
"mode": "heat" # MAX! is always in heating mode
}
except (ValueError, TypeError) as e:
logger.error(f"MAX! failed to parse temperature: {payload}, error: {e}")
return {
"target": 21.0,
"mode": "heat"
}
# ============================================================================ # ============================================================================
# REGISTRY: Maps (device_type, technology, direction) -> handler function # REGISTRY: Maps (device_type, technology, direction) -> handler function
# ============================================================================ # ============================================================================
@@ -142,6 +398,28 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
("thermostat", "simulator", "to_abstract"): _transform_thermostat_simulator_to_abstract, ("thermostat", "simulator", "to_abstract"): _transform_thermostat_simulator_to_abstract,
("thermostat", "zigbee2mqtt", "to_vendor"): _transform_thermostat_zigbee2mqtt_to_vendor, ("thermostat", "zigbee2mqtt", "to_vendor"): _transform_thermostat_zigbee2mqtt_to_vendor,
("thermostat", "zigbee2mqtt", "to_abstract"): _transform_thermostat_zigbee2mqtt_to_abstract, ("thermostat", "zigbee2mqtt", "to_abstract"): _transform_thermostat_zigbee2mqtt_to_abstract,
("thermostat", "max", "to_vendor"): _transform_thermostat_max_to_vendor,
("thermostat", "max", "to_abstract"): _transform_thermostat_max_to_abstract,
# Contact sensor transformations (support both 'contact' and 'contact_sensor' types)
("contact_sensor", "zigbee2mqtt", "to_vendor"): _transform_contact_sensor_zigbee2mqtt_to_vendor,
("contact_sensor", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
("contact_sensor", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
("contact_sensor", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
("contact", "zigbee2mqtt", "to_vendor"): _transform_contact_sensor_zigbee2mqtt_to_vendor,
("contact", "zigbee2mqtt", "to_abstract"): _transform_contact_sensor_zigbee2mqtt_to_abstract,
("contact", "max", "to_vendor"): _transform_contact_sensor_max_to_vendor,
("contact", "max", "to_abstract"): _transform_contact_sensor_max_to_abstract,
# Temperature & humidity sensor transformations (support both type aliases)
("temp_humidity_sensor", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity_sensor", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity_sensor", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity_sensor", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
("temp_humidity", "zigbee2mqtt", "to_vendor"): _transform_temp_humidity_sensor_zigbee2mqtt_to_vendor,
("temp_humidity", "zigbee2mqtt", "to_abstract"): _transform_temp_humidity_sensor_zigbee2mqtt_to_abstract,
("temp_humidity", "max", "to_vendor"): _transform_temp_humidity_sensor_max_to_vendor,
("temp_humidity", "max", "to_abstract"): _transform_temp_humidity_sensor_max_to_abstract,
} }

View File

@@ -15,7 +15,16 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from packages.home_capabilities import LIGHT_VERSION, THERMOSTAT_VERSION, LightState, ThermostatState from packages.home_capabilities import (
LIGHT_VERSION,
THERMOSTAT_VERSION,
CONTACT_SENSOR_VERSION,
TEMP_HUMIDITY_SENSOR_VERSION,
LightState,
ThermostatState,
ContactState,
TempHumidityState
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -137,7 +146,9 @@ async def spec() -> dict[str, dict[str, str]]:
return { return {
"capabilities": { "capabilities": {
"light": LIGHT_VERSION, "light": LIGHT_VERSION,
"thermostat": THERMOSTAT_VERSION "thermostat": THERMOSTAT_VERSION,
"contact": CONTACT_SENSOR_VERSION,
"temp_humidity": TEMP_HUMIDITY_SENSOR_VERSION
} }
} }
@@ -331,6 +342,13 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
detail=f"Device {device_id} not found" detail=f"Device {device_id} not found"
) )
# Check if device is read-only (contact sensors, etc.)
if "topics" in device and "set" not in device["topics"]:
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Device is read-only"
)
# Validate payload based on device type # Validate payload based on device type
if request.type == "light": if request.type == "light":
try: try:
@@ -356,6 +374,18 @@ async def set_device(device_id: str, request: SetDeviceRequest) -> dict[str, str
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Invalid payload for thermostat: {e}" detail=f"Invalid payload for thermostat: {e}"
) )
elif request.type in {"contact", "contact_sensor"}:
# Contact sensors are read-only
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Contact sensors are read-only devices"
)
elif request.type in {"temp_humidity", "temp_humidity_sensor"}:
# Temperature & humidity sensors are read-only
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail="Temperature & humidity sensors are read-only devices"
)
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,

View File

@@ -358,27 +358,6 @@
color: #999; color: #999;
} }
.mode-display {
background: #f8f9fa;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
margin-bottom: 1rem;
}
.mode-label {
font-size: 0.75rem;
color: #666;
text-transform: uppercase;
}
.mode-value {
font-size: 1rem;
font-weight: 600;
color: #667eea;
text-transform: uppercase;
}
.temp-controls { .temp-controls {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
@@ -407,39 +386,96 @@
transform: scale(0.95); transform: scale(0.95);
} }
.mode-controls { /* Contact Sensor Styles */
display: grid; .contact-status {
grid-template-columns: repeat(3, 1fr); display: flex;
gap: 0.5rem; align-items: center;
} gap: 0.75rem;
padding: 1rem;
.mode-button { background: #f8f9fa;
padding: 0.75rem;
border: 2px solid #ddd;
border-radius: 8px; border-radius: 8px;
font-size: 0.875rem; margin: 1rem 0;
}
.contact-badge {
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 600; font-weight: 600;
cursor: pointer; font-size: 0.875rem;
transition: all 0.2s;
background: white;
color: #666;
min-height: 44px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px;
} }
.mode-button:hover { .contact-badge.open {
border-color: #667eea; background: #dc3545;
color: #667eea;
}
.mode-button.active {
background: #667eea;
border-color: #667eea;
color: white; color: white;
} }
.mode-button:active { .contact-badge.closed {
transform: scale(0.95); background: #28a745;
color: white;
}
.contact-info {
font-size: 0.75rem;
color: #999;
margin-top: 1rem;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 4px;
text-align: center;
}
/* Temperature & Humidity Sensor Styles */
.temp-humidity-display {
padding: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
margin: 1rem 0;
}
.temp-humidity-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
}
.temp-humidity-row:not(:last-child) {
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.temp-humidity-label {
font-size: 0.875rem;
font-weight: 500;
opacity: 0.9;
}
.temp-humidity-value {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.5px;
}
.temp-humidity-battery {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.8);
text-align: center;
padding: 0.5rem;
margin-top: 0.5rem;
background: rgba(0, 0, 0, 0.1);
border-radius: 6px;
}
.temp-humidity-info {
font-size: 0.75rem;
color: #999;
margin-top: 1rem;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 4px;
text-align: center;
} }
.events { .events {
@@ -512,7 +548,7 @@
<span class="refresh-icon" id="refresh-icon"></span> <span class="refresh-icon" id="refresh-icon"></span>
</button> </button>
<button class="collapse-all-btn" onclick="toggleAllRooms()" title="Alle Räume ein-/ausklappen"> <button class="collapse-all-btn" onclick="toggleAllRooms()" title="Alle Räume ein-/ausklappen">
<span class="collapse-all-icon" id="collapse-all-icon"></span> <span class="collapse-all-icon collapsed" id="collapse-all-icon"></span>
</button> </button>
</div> </div>
</header> </header>
@@ -522,10 +558,10 @@
<section class="room"> <section class="room">
<div class="room-header" onclick="toggleRoom('room-{{ loop.index }}')"> <div class="room-header" onclick="toggleRoom('room-{{ loop.index }}')">
<h2 class="room-title">{{ room.name }}</h2> <h2 class="room-title">{{ room.name }}</h2>
<span class="room-toggle" id="toggle-room-{{ loop.index }}"></span> <span class="room-toggle collapsed" id="toggle-room-{{ loop.index }}"></span>
</div> </div>
<div class="room-content" id="room-{{ loop.index }}"> <div class="room-content collapsed" id="room-{{ loop.index }}">
<div class="devices"> <div class="devices">
{% for device in room.devices %} {% for device in room.devices %}
<div class="device-card" data-device-id="{{ device.device_id }}"> <div class="device-card" data-device-id="{{ device.device_id }}">
@@ -537,6 +573,8 @@
{% if device.features.brightness %}• Dimmbar{% endif %} {% if device.features.brightness %}• Dimmbar{% endif %}
{% elif device.type == "thermostat" %} {% elif device.type == "thermostat" %}
Thermostat Thermostat
{% elif device.type == "contact" or device.type == "contact_sensor" %}
Contact Sensor • Read-Only
{% else %} {% else %}
{{ device.type or "Unknown" }} {{ device.type or "Unknown" }}
{% endif %} {% endif %}
@@ -601,40 +639,48 @@
</div> </div>
</div> </div>
<div class="mode-display">
<div class="mode-label">Modus</div>
<div class="mode-value" id="state-{{ device.device_id }}-mode">OFF</div>
</div>
<div class="temp-controls"> <div class="temp-controls">
<button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', -0.5)"> <button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', -1.0)">
-0.5 -1.0
</button> </button>
<button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', 0.5)"> <button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', 1.0)">
+0.5 +1.0
</button> </button>
</div> </div>
<div class="mode-controls"> {% elif device.type == "contact" or device.type == "contact_sensor" %}
<button <div class="contact-status">
class="mode-button" <span class="contact-badge closed" id="state-{{ device.device_id }}">
id="mode-{{ device.device_id }}-off" Geschlossen
onclick="setMode('{{ device.device_id }}', 'off')"> </span>
Off
</button>
<button
class="mode-button"
id="mode-{{ device.device_id }}-heat"
onclick="setMode('{{ device.device_id }}', 'heat')">
Heat
</button>
<button
class="mode-button"
id="mode-{{ device.device_id }}-auto"
onclick="setMode('{{ device.device_id }}', 'auto')">
Auto
</button>
</div> </div>
<div class="contact-info">
🔒 Nur-Lesen Gerät • Keine Steuerung möglich
</div>
{% elif device.type == "temp_humidity" or device.type == "temp_humidity_sensor" %}
<div class="temp-humidity-display">
<div class="temp-humidity-row">
<span class="temp-humidity-label">🌡️ Temperatur:</span>
<span class="temp-humidity-value" id="th-{{ device.device_id }}-t">--</span>
<span style="font-size: 1.25rem; font-weight: 600;">°C</span>
</div>
<div class="temp-humidity-row">
<span class="temp-humidity-label">💧 Luftfeuchte:</span>
<span class="temp-humidity-value" id="th-{{ device.device_id }}-h">--</span>
<span style="font-size: 1.25rem; font-weight: 600;">%</span>
</div>
</div>
<div class="temp-humidity-battery" id="th-{{ device.device_id }}-battery" style="display: none;">
🔋 <span id="th-{{ device.device_id }}-battery-value">--</span>%
</div>
<div class="temp-humidity-info">
🔒 Nur-Lesen Gerät • Keine Steuerung möglich
</div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
@@ -761,7 +807,6 @@
let eventSource = null; let eventSource = null;
let currentState = {}; let currentState = {};
let thermostatTargets = {}; let thermostatTargets = {};
let thermostatModes = {};
// Initialize device states // Initialize device states
{% for room in rooms %} {% for room in rooms %}
@@ -770,7 +815,6 @@
currentState['{{ device.device_id }}'] = 'off'; currentState['{{ device.device_id }}'] = 'off';
{% elif device.type == "thermostat" %} {% elif device.type == "thermostat" %}
thermostatTargets['{{ device.device_id }}'] = 21.0; thermostatTargets['{{ device.device_id }}'] = 21.0;
thermostatModes['{{ device.device_id }}'] = 'off';
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
@@ -846,7 +890,6 @@
// Adjust thermostat target temperature // Adjust thermostat target temperature
async function adjustTarget(deviceId, delta) { async function adjustTarget(deviceId, delta) {
const currentTarget = thermostatTargets[deviceId] || 21.0; const currentTarget = thermostatTargets[deviceId] || 21.0;
const currentMode = thermostatModes[deviceId] || 'off';
const newTarget = Math.max(5.0, Math.min(30.0, currentTarget + delta)); const newTarget = Math.max(5.0, Math.min(30.0, currentTarget + delta));
try { try {
@@ -858,7 +901,6 @@
body: JSON.stringify({ body: JSON.stringify({
type: 'thermostat', type: 'thermostat',
payload: { payload: {
mode: currentMode,
target: newTarget target: newTarget
} }
}) })
@@ -877,38 +919,6 @@
} }
} }
// Set thermostat mode
async function setMode(deviceId, mode) {
const currentTarget = thermostatTargets[deviceId] || 21.0;
try {
const response = await fetch(api(`/devices/${deviceId}/set`), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'thermostat',
payload: {
mode: mode,
target: currentTarget
}
})
});
if (response.ok) {
console.log(`Sent mode ${mode} to ${deviceId}`);
addEvent({
action: 'mode_set',
device_id: deviceId,
mode: mode
});
}
} catch (error) {
console.error('Failed to set mode:', error);
}
}
// Update device UI // Update device UI
function updateDeviceUI(deviceId, power, brightness) { function updateDeviceUI(deviceId, power, brightness) {
currentState[deviceId] = power; currentState[deviceId] = power;
@@ -955,7 +965,6 @@
function updateThermostatUI(deviceId, current, target, mode) { function updateThermostatUI(deviceId, current, target, mode) {
const currentSpan = document.getElementById(`state-${deviceId}-current`); const currentSpan = document.getElementById(`state-${deviceId}-current`);
const targetSpan = document.getElementById(`state-${deviceId}-target`); const targetSpan = document.getElementById(`state-${deviceId}-target`);
const modeSpan = document.getElementById(`state-${deviceId}-mode`);
if (current !== undefined && currentSpan) { if (current !== undefined && currentSpan) {
currentSpan.textContent = current.toFixed(1); currentSpan.textContent = current.toFixed(1);
@@ -967,24 +976,56 @@
} }
thermostatTargets[deviceId] = target; thermostatTargets[deviceId] = target;
} }
}
if (mode !== undefined) { // Update contact sensor UI
if (modeSpan) { function updateContactUI(deviceId, contactState) {
modeSpan.textContent = mode.toUpperCase(); const badge = document.getElementById(`state-${deviceId}`);
if (!badge) {
console.warn(`No contact badge found for device ${deviceId}`);
return;
} }
thermostatModes[deviceId] = mode;
// Update mode button states // contactState is either "open" or "closed"
['off', 'heat', 'auto'].forEach(m => { if (contactState === "open") {
const btn = document.getElementById(`mode-${deviceId}-${m}`); badge.textContent = "Geöffnet";
if (btn) { badge.className = "contact-badge open";
if (m === mode.toLowerCase()) { } else if (contactState === "closed") {
btn.classList.add('active'); badge.textContent = "Geschlossen";
} else { badge.className = "contact-badge closed";
btn.classList.remove('active');
} }
} }
});
// Update temperature & humidity sensor UI
function updateTempHumidityUI(deviceId, payload) {
const tempSpan = document.getElementById(`th-${deviceId}-t`);
const humiditySpan = document.getElementById(`th-${deviceId}-h`);
const batteryDiv = document.getElementById(`th-${deviceId}-battery`);
const batteryValueSpan = document.getElementById(`th-${deviceId}-battery-value`);
if (!tempSpan || !humiditySpan) {
console.warn(`No temp/humidity elements found for device ${deviceId}`);
return;
}
// Update temperature (rounded to 1 decimal)
if (payload.temperature !== undefined && payload.temperature !== null) {
tempSpan.textContent = payload.temperature.toFixed(1);
}
// Update humidity (rounded to 0-1 decimals)
if (payload.humidity !== undefined && payload.humidity !== null) {
// Round to 1 decimal if has decimals, otherwise integer
const humidity = payload.humidity;
humiditySpan.textContent = (humidity % 1 === 0) ? humidity.toFixed(0) : humidity.toFixed(1);
}
// Update battery if present
if (payload.battery !== undefined && payload.battery !== null && batteryDiv && batteryValueSpan) {
batteryValueSpan.textContent = payload.battery;
batteryDiv.style.display = 'block';
} else if (batteryDiv) {
batteryDiv.style.display = 'none';
} }
} }
@@ -1043,20 +1084,26 @@
} }
// Check if it's a thermostat // Check if it's a thermostat
if (data.payload.mode !== undefined || data.payload.target !== undefined || data.payload.current !== undefined) { if (data.payload.target !== undefined || data.payload.current !== undefined) {
if (data.payload.mode !== undefined) {
thermostatModes[data.device_id] = data.payload.mode;
}
if (data.payload.target !== undefined) { if (data.payload.target !== undefined) {
thermostatTargets[data.device_id] = data.payload.target; thermostatTargets[data.device_id] = data.payload.target;
} }
updateThermostatUI( updateThermostatUI(
data.device_id, data.device_id,
data.payload.current, data.payload.current,
data.payload.target, data.payload.target
data.payload.mode
); );
} }
// Check if it's a contact sensor
if (data.payload.contact !== undefined) {
updateContactUI(data.device_id, data.payload.contact);
}
// Check if it's a temp/humidity sensor
if (data.payload.temperature !== undefined || data.payload.humidity !== undefined) {
updateTempHumidityUI(data.device_id, data.payload);
}
} }
}; };
@@ -1171,11 +1218,16 @@
// It's a light // It's a light
currentState[deviceId] = state.power; currentState[deviceId] = state.power;
updateDeviceUI(deviceId, state.power, state.brightness); updateDeviceUI(deviceId, state.power, state.brightness);
} else if (state.mode !== undefined || state.target !== undefined) { } else if (state.target !== undefined) {
// It's a thermostat // It's a thermostat
if (state.mode) thermostatModes[deviceId] = state.mode;
if (state.target) thermostatTargets[deviceId] = state.target; if (state.target) thermostatTargets[deviceId] = state.target;
updateThermostatUI(deviceId, state.current, state.target, state.mode); updateThermostatUI(deviceId, state.current, state.target);
} else if (state.contact !== undefined) {
// It's a contact sensor
updateContactUI(deviceId, state.contact);
} else if (state.temperature !== undefined || state.humidity !== undefined) {
// It's a temp/humidity sensor
updateTempHumidityUI(deviceId, state);
} }
} }
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,4 @@
version: 1 version: 1
mqtt: mqtt:
broker: "172.16.2.16" broker: "172.16.2.16"
port: 1883 port: 1883
@@ -7,54 +6,345 @@ mqtt:
username: null username: null
password: null password: null
keepalive: 60 keepalive: 60
redis: redis:
url: "redis://172.23.1.116:6379/8" url: "redis://172.23.1.116:6379/8"
channel: "ui:updates" channel: "ui:updates"
devices: devices:
- device_id: test_lampe_1 - device_id: lampe_semeniere_wohnzimmer
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: simulator technology: zigbee2mqtt
features: features:
power: true power: true
brightness: true brightness: false
topics: topics:
set: "vendor/test_lampe_1/set" state: "zigbee2mqtt/0xf0d1b8000015480b"
state: "vendor/test_lampe_1/state" set: "zigbee2mqtt/0xf0d1b8000015480b/set"
- device_id: test_lampe_2 metadata:
friendly_name: "Lampe Semeniere Wohnzimmer"
ieee_address: "0xf0d1b8000015480b"
model: "AC10691"
vendor: "OSRAM"
- device_id: grosse_lampe_wohnzimmer
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: simulator technology: zigbee2mqtt
features: features:
power: true power: true
brightness: false
topics: topics:
set: "vendor/test_lampe_2/set" state: "zigbee2mqtt/0xf0d1b80000151aca"
state: "vendor/test_lampe_2/state" set: "zigbee2mqtt/0xf0d1b80000151aca/set"
- device_id: test_lampe_3 metadata:
friendly_name: "grosse Lampe Wohnzimmer"
ieee_address: "0xf0d1b80000151aca"
model: "AC10691"
vendor: "OSRAM"
- device_id: lampe_naehtischchen_wohnzimmer
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: simulator technology: zigbee2mqtt
features: features:
power: true power: true
brightness: true brightness: false
topics: topics:
set: "vendor/test_lampe_3/set" state: "zigbee2mqtt/0x842e14fffee560ee"
state: "vendor/test_lampe_3/state" set: "zigbee2mqtt/0x842e14fffee560ee/set"
- device_id: test_thermo_1 metadata:
type: thermostat friendly_name: "Lampe Naehtischchen Wohnzimmer"
cap_version: "thermostat@2.0.0" ieee_address: "0x842e14fffee560ee"
technology: simulator model: "HG06337"
vendor: "Lidl"
- device_id: kleine_lampe_rechts_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features: features:
mode: false power: true
target: true brightness: false
current: true
battery: true
topics: topics:
set: "vendor/test_thermo_1/set" state: "zigbee2mqtt/0xf0d1b80000156645"
state: "vendor/test_thermo_1/state" set: "zigbee2mqtt/0xf0d1b80000156645/set"
- device_id: experiment_light_1 metadata:
friendly_name: "kleine Lampe rechts Esszimmer"
ieee_address: "0xf0d1b80000156645"
model: "AC10691"
vendor: "OSRAM"
- device_id: kleine_lampe_links_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: false
topics:
state: "zigbee2mqtt/0xf0d1b80000153099"
set: "zigbee2mqtt/0xf0d1b80000153099/set"
metadata:
friendly_name: "kleine Lampe links Esszimmer"
ieee_address: "0xf0d1b80000153099"
model: "AC10691"
vendor: "OSRAM"
- device_id: leselampe_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xec1bbdfffe7b84f2"
set: "zigbee2mqtt/0xec1bbdfffe7b84f2/set"
metadata:
friendly_name: "Leselampe Esszimmer"
ieee_address: "0xec1bbdfffe7b84f2"
model: "LED1842G3"
vendor: "IKEA"
- device_id: medusalampe_schlafzimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: false
topics:
state: "zigbee2mqtt/0xf0d1b80000154c7c"
set: "zigbee2mqtt/0xf0d1b80000154c7c/set"
metadata:
friendly_name: "Medusa-Lampe Schlafzimmer"
ieee_address: "0xf0d1b80000154c7c"
model: "AC10691"
vendor: "OSRAM"
- device_id: sportlicht_am_fernseher_studierzimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0x842e14fffe76a23a"
set: "zigbee2mqtt/0x842e14fffe76a23a/set"
metadata:
friendly_name: "Sportlicht am Fernseher, Studierzimmer"
ieee_address: "0x842e14fffe76a23a"
model: "LED1733G7"
vendor: "IKEA"
- device_id: deckenlampe_schlafzimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x0017880108a406a7"
set: "zigbee2mqtt/0x0017880108a406a7/set"
metadata:
friendly_name: "Deckenlampe Schlafzimmer"
ieee_address: "0x0017880108a406a7"
model: "8718699688882"
vendor: "Philips"
- device_id: bettlicht_wolfgang
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x00178801081570bf"
set: "zigbee2mqtt/0x00178801081570bf/set"
metadata:
friendly_name: "Bettlicht Wolfgang"
ieee_address: "0x00178801081570bf"
model: "9290020399"
vendor: "Philips"
- device_id: bettlicht_patty
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x0017880108158b32"
set: "zigbee2mqtt/0x0017880108158b32/set"
metadata:
friendly_name: "Bettlicht Patty"
ieee_address: "0x0017880108158b32"
model: "9290020399"
vendor: "Philips"
- device_id: schranklicht_hinten_patty
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x0017880106e29571"
set: "zigbee2mqtt/0x0017880106e29571/set"
metadata:
friendly_name: "Schranklicht hinten Patty"
ieee_address: "0x0017880106e29571"
model: "8718699673147"
vendor: "Philips"
- device_id: schranklicht_vorne_patty
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: false
topics:
state: "zigbee2mqtt/0xf0d1b80000154cf5"
set: "zigbee2mqtt/0xf0d1b80000154cf5/set"
metadata:
friendly_name: "Schranklicht vorne Patty"
ieee_address: "0xf0d1b80000154cf5"
model: "AC10691"
vendor: "OSRAM"
- device_id: leselampe_patty
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x001788010600ec9d"
set: "zigbee2mqtt/0x001788010600ec9d/set"
metadata:
friendly_name: "Leselampe Patty"
ieee_address: "0x001788010600ec9d"
model: "8718699673147"
vendor: "Philips"
- device_id: deckenlampe_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x0017880108a03e45"
set: "zigbee2mqtt/0x0017880108a03e45/set"
metadata:
friendly_name: "Deckenlampe Esszimmer"
ieee_address: "0x0017880108a03e45"
model: "929002241201"
vendor: "Philips"
- device_id: standlampe_esszimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0xbc33acfffe21f547"
set: "zigbee2mqtt/0xbc33acfffe21f547/set"
metadata:
friendly_name: "Standlampe Esszimmer"
ieee_address: "0xbc33acfffe21f547"
model: "LED1732G11"
vendor: "IKEA"
- device_id: haustuer
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
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
color_temperature: true
topics:
state: "zigbee2mqtt/0x001788010d2123a7"
set: "zigbee2mqtt/0x001788010d2123a7/set"
metadata:
friendly_name: "Deckenlampe Flur oben"
ieee_address: "0x001788010d2123a7"
model: "929003099001"
vendor: "Philips"
- device_id: kueche_deckenlampe
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0x001788010d2c40c4"
set: "zigbee2mqtt/0x001788010d2c40c4/set"
metadata:
friendly_name: "Küche Deckenlampe"
ieee_address: "0x001788010d2c40c4"
model: "929002469202"
vendor: "Philips"
- device_id: sportlicht_tisch
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b8be2409f31b"
set: "zigbee2mqtt/0xf0d1b8be2409f31b/set"
metadata:
friendly_name: "Sportlicht Tisch"
ieee_address: "0xf0d1b8be2409f31b"
model: "4058075729063"
vendor: "LEDVANCE"
- device_id: sportlicht_regal
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
state: "zigbee2mqtt/0xf0d1b8be2409f569"
set: "zigbee2mqtt/0xf0d1b8be2409f569/set"
metadata:
friendly_name: "Sportlicht Regal"
ieee_address: "0xf0d1b8be2409f569"
model: "4058075729063"
vendor: "LEDVANCE"
- device_id: licht_flur_oben_am_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
type: light type: light
cap_version: "light@1.2.0" cap_version: "light@1.2.0"
technology: zigbee2mqtt technology: zigbee2mqtt
@@ -62,5 +352,372 @@ devices:
power: true power: true
brightness: true brightness: true
topics: topics:
set: "zigbee2mqtt/0xf0d1b80000195038/set"
state: "zigbee2mqtt/0xf0d1b80000195038" state: "zigbee2mqtt/0xf0d1b80000195038"
set: "zigbee2mqtt/0xf0d1b80000195038/set"
metadata:
friendly_name: "ExperimentLabTest"
ieee_address: "0xf0d1b80000195038"
model: "4058075208421"
vendor: "LEDVANCE"
- device_id: thermostat_wolfgang
type: thermostat
cap_version: "thermostat@1.0.0"
technology: zigbee2mqtt
features:
heating: true
temperature_range:
- 5
- 30
temperature_step: 0.5
topics:
state: "zigbee2mqtt/0x540f57fffe7e3cfe"
set: "zigbee2mqtt/0x540f57fffe7e3cfe/set"
metadata:
friendly_name: "Wolfgang"
ieee_address: "0x540f57fffe7e3cfe"
model: "GS361A-H04"
vendor: "Siterwell"
- device_id: thermostat_kueche
type: thermostat
cap_version: "thermostat@1.0.0"
technology: zigbee2mqtt
features:
heating: true
temperature_range:
- 5
- 30
temperature_step: 0.5
topics:
state: "zigbee2mqtt/0x94deb8fffe2e5c06"
set: "zigbee2mqtt/0x94deb8fffe2e5c06/set"
metadata:
friendly_name: "Kueche"
ieee_address: "0x94deb8fffe2e5c06"
model: "GS361A-H04"
vendor: "Siterwell"
- device_id: thermostat_schlafzimmer
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
features:
mode: true
target: true
current: false
topics:
set: "homegear/instance1/set/42/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/42/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Schlafzimmer"
location: "Schlafzimmer"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "42"
channel: "1"
- device_id: thermostat_esszimmer
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
features:
mode: true
target: true
current: false
topics:
set: "homegear/instance1/set/45/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/45/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Esszimmer"
location: "Esszimmer"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "45"
channel: "1"
- device_id: thermostat_wohnzimmer
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
features:
mode: true
target: true
current: false
topics:
set: "homegear/instance1/set/46/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/46/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Wohnzimmer"
location: "Wohnzimmer"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "46"
channel: "1"
- device_id: thermostat_patty
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
features:
mode: true
target: true
current: false
topics:
set: "homegear/instance1/set/39/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/39/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Patty"
location: "Arbeitszimmer Patty"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "39"
channel: "1"
- device_id: thermostat_bad_oben
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
features:
mode: true
target: true
current: false
topics:
set: "homegear/instance1/set/41/1/SET_TEMPERATURE"
state: "homegear/instance1/plain/41/1/SET_TEMPERATURE"
metadata:
friendly_name: "Thermostat Bad Oben"
location: "Bad Oben"
vendor: "eQ-3"
model: "MAX! Thermostat"
peer_id: "41"
channel: "1"
- device_id: thermostat_bad_unten
type: thermostat
cap_version: "thermostat@1.0.0"
technology: max
features:
mode: true
target: true
current: false
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"
- device_id: sterne_wohnzimmer
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: false
topics:
state: "zigbee2mqtt/0xf0d1b80000155fc2"
set: "zigbee2mqtt/0xf0d1b80000155fc2/set"
metadata:
friendly_name: "Sterne Wohnzimmer"
ieee_address: "0xf0d1b80000155fc2"
model: "AC10691"
vendor: "OSRAM"
- device_id: kontakt_schlafzimmer_strasse
type: contact
name: Kontakt Schlafzimmer Straße
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/52/1/STATE
features: {}
- device_id: kontakt_esszimmer_strasse_rechts
type: contact
name: Kontakt Esszimmer Straße rechts
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/26/1/STATE
features: {}
- device_id: kontakt_esszimmer_strasse_links
type: contact
name: Kontakt Esszimmer Straße links
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/27/1/STATE
features: {}
- device_id: kontakt_wohnzimmer_garten_rechts
type: contact
name: Kontakt Wohnzimmer Garten rechts
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/28/1/STATE
features: {}
- device_id: kontakt_wohnzimmer_garten_links
type: contact
name: Kontakt Wohnzimmer Garten links
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/29/1/STATE
features: {}
- device_id: kontakt_kueche_garten_fenster
type: contact
name: Kontakt Küche Garten Fenster
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d008b332785
features: {}
- device_id: kontakt_kueche_garten_tuer
type: contact
name: Kontakt Küche Garten Tür
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d008b332788
features: {}
- device_id: kontakt_kueche_strasse_rechts
type: contact
name: Kontakt Küche Straße rechts
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d008b151803
features: {}
- device_id: kontakt_kueche_strasse_links
type: contact
name: Kontakt Küche Straße links
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d008b331d0b
features: {}
- device_id: kontakt_patty_garten_rechts
type: contact
name: Kontakt Patty Garten rechts
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/18/1/STATE
features: {}
- device_id: kontakt_patty_garten_links
type: contact
name: Kontakt Patty Garten links
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/22/1/STATE
features: {}
- device_id: kontakt_patty_strasse
type: contact
name: Kontakt Patty Straße
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000af457cf
features: {}
- device_id: kontakt_wolfgang_garten
type: contact
name: Kontakt Wolfgang Garten
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d008b3328da
features: {}
- device_id: kontakt_bad_oben_strasse
type: contact
name: Kontakt Bad Oben Straße
cap_version: contact_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d008b333aec
features: {}
- device_id: kontakt_bad_unten_strasse
type: contact
name: Kontakt Bad Unten Straße
cap_version: contact_sensor@1.0.0
technology: max
topics:
state: homegear/instance1/plain/44/1/STATE
features: {}
- device_id: sensor_schlafzimmer
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00043292dc
features: {}
- device_id: sensor_wohnzimmer
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d0008975707
features: {}
- device_id: sensor_kueche
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00083299bb
features: {}
- device_id: sensor_arbeitszimmer_patty
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d0003f052b7
features: {}
- device_id: sensor_arbeitszimmer_wolfgang
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000543fb99
features: {}
- device_id: sensor_bad_oben
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00093e8987
features: {}
- device_id: sensor_bad_unten
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d00093e662a
features: {}
- device_id: sensor_flur
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000836ccc6
features: {}
- device_id: sensor_waschkueche
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d000449f3bc
features: {}
- device_id: sensor_sportzimmer
type: temp_humidity_sensor
name: Temperatur & Luftfeuchte
cap_version: temp_humidity_sensor@1.0.0
technology: zigbee2mqtt
topics:
state: zigbee2mqtt/0x00158d0009421422
features: {}

View File

@@ -0,0 +1,66 @@
version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
client_id: "home-automation-abstraction"
username: null
password: null
keepalive: 60
redis:
url: "redis://172.23.1.116:6379/8"
channel: "ui:updates"
devices:
- device_id: test_lampe_1
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_1/set"
state: "vendor/test_lampe_1/state"
- device_id: test_lampe_2
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
topics:
set: "vendor/test_lampe_2/set"
state: "vendor/test_lampe_2/state"
- device_id: test_lampe_3
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_3/set"
state: "vendor/test_lampe_3/state"
- device_id: test_thermo_1
type: thermostat
cap_version: "thermostat@2.0.0"
technology: simulator
features:
mode: false
target: true
current: true
battery: true
topics:
set: "vendor/test_thermo_1/set"
state: "vendor/test_thermo_1/state"
- device_id: experiment_light_1
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
set: "zigbee2mqtt/0xf0d1b80000195038/set"
state: "zigbee2mqtt/0xf0d1b80000195038"

View File

@@ -0,0 +1,66 @@
version: 1
mqtt:
broker: "172.16.2.16"
port: 1883
client_id: "home-automation-abstraction"
username: null
password: null
keepalive: 60
redis:
url: "redis://172.23.1.116:6379/8"
channel: "ui:updates"
devices:
- device_id: test_lampe_1
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_1/set"
state: "vendor/test_lampe_1/state"
- device_id: test_lampe_2
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
topics:
set: "vendor/test_lampe_2/set"
state: "vendor/test_lampe_2/state"
- device_id: test_lampe_3
type: light
cap_version: "light@1.2.0"
technology: simulator
features:
power: true
brightness: true
topics:
set: "vendor/test_lampe_3/set"
state: "vendor/test_lampe_3/state"
- device_id: test_thermo_1
type: thermostat
cap_version: "thermostat@2.0.0"
technology: simulator
features:
mode: false
target: true
current: true
battery: true
topics:
set: "vendor/test_thermo_1/set"
state: "vendor/test_thermo_1/state"
- device_id: experiment_light_1
type: light
cap_version: "light@1.2.0"
technology: zigbee2mqtt
features:
power: true
brightness: true
topics:
set: "zigbee2mqtt/0xf0d1b80000195038/set"
state: "zigbee2mqtt/0xf0d1b80000195038"

View File

@@ -1,35 +1,252 @@
# UI Layout Configuration
# Defines rooms and device tiles for the home automation UI
rooms: rooms:
- name: Wohnzimmer
devices:
- device_id: test_lampe_2
title: Deckenlampe
icon: "💡"
rank: 5
- device_id: test_lampe_1
title: Stehlampe
icon: "🔆"
rank: 10
- device_id: test_thermo_1
title: Thermostat
icon: "🌡️"
rank: 15
- name: Schlafzimmer - name: Schlafzimmer
devices: devices:
- device_id: test_lampe_3 - device_id: bettlicht_patty
title: Nachttischlampe title: Bettlicht Patty
icon: "🛏️" icon: 🛏️
rank: 10 rank: 10
- device_id: bettlicht_wolfgang
- name: Lab title: Bettlicht Wolfgang
icon: 🛏️
rank: 20
- device_id: deckenlampe_schlafzimmer
title: Deckenlampe Schlafzimmer
icon: 💡
rank: 30
- device_id: medusalampe_schlafzimmer
title: Medusa-Lampe Schlafzimmer
icon: 💡
rank: 40
- device_id: thermostat_schlafzimmer
title: Thermostat Schlafzimmer
icon: 🌡️
rank: 45
- device_id: kontakt_schlafzimmer_strasse
title: Kontakt Straße
icon: 🪟
rank: 46
- device_id: sensor_schlafzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 47
- name: Esszimmer
devices: devices:
- device_id: experiment_light_1 - device_id: deckenlampe_esszimmer
title: Experimentierlampe title: Deckenlampe Esszimmer
icon: "💡" icon: 💡
rank: 10 rank: 50
- device_id: leselampe_esszimmer
title: Leselampe Esszimmer
icon: 💡
rank: 60
- device_id: standlampe_esszimmer
title: Standlampe Esszimmer
icon: 💡
rank: 70
- device_id: kleine_lampe_links_esszimmer
title: kleine Lampe links Esszimmer
icon: 💡
rank: 80
- device_id: kleine_lampe_rechts_esszimmer
title: kleine Lampe rechts Esszimmer
icon: 💡
rank: 90
- device_id: thermostat_esszimmer
title: Thermostat Esszimmer
icon: 🌡️
rank: 95
- device_id: kontakt_esszimmer_strasse_rechts
title: Kontakt Straße rechts
icon: 🪟
rank: 96
- device_id: kontakt_esszimmer_strasse_links
title: Kontakt Straße links
icon: 🪟
rank: 97
- name: Wohnzimmer
devices:
- device_id: lampe_naehtischchen_wohnzimmer
title: Lampe Naehtischchen Wohnzimmer
icon: 💡
rank: 100
- device_id: lampe_semeniere_wohnzimmer
title: Lampe Semeniere Wohnzimmer
icon: 💡
rank: 110
- device_id: sterne_wohnzimmer
title: Sterne Wohnzimmer
icon: 💡
rank: 120
- device_id: grosse_lampe_wohnzimmer
title: grosse Lampe Wohnzimmer
icon: 💡
rank: 130
- device_id: thermostat_wohnzimmer
title: Thermostat Wohnzimmer
icon: 🌡️
rank: 135
- device_id: kontakt_wohnzimmer_garten_rechts
title: Kontakt Garten rechts
icon: 🪟
rank: 136
- device_id: kontakt_wohnzimmer_garten_links
title: Kontakt Garten links
icon: 🪟
rank: 137
- device_id: sensor_wohnzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 138
- name: Küche
devices:
- device_id: kueche_deckenlampe
title: Küche Deckenlampe
icon: 💡
rank: 140
- device_id: thermostat_kueche
title: Kueche
icon: 🌡️
rank: 150
- device_id: kontakt_kueche_garten_fenster
title: Kontakt Garten Fenster
icon: 🪟
rank: 151
- device_id: kontakt_kueche_garten_tuer
title: Kontakt Garten Tür
icon: 🪟
rank: 152
- device_id: kontakt_kueche_strasse_rechts
title: Kontakt Straße rechts
icon: 🪟
rank: 153
- device_id: kontakt_kueche_strasse_links
title: Kontakt Straße links
icon: 🪟
rank: 154
- device_id: sensor_kueche
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 155
- name: Arbeitszimmer Patty
devices:
- device_id: leselampe_patty
title: Leselampe Patty
icon: 💡
rank: 160
- device_id: schranklicht_hinten_patty
title: Schranklicht hinten Patty
icon: 💡
rank: 170
- device_id: schranklicht_vorne_patty
title: Schranklicht vorne Patty
icon: 💡
rank: 180
- device_id: thermostat_patty
title: Thermostat Patty
icon: 🌡️
rank: 185
- device_id: kontakt_patty_garten_rechts
title: Kontakt Garten rechts
icon: 🪟
rank: 186
- device_id: kontakt_patty_garten_links
title: Kontakt Garten links
icon: 🪟
rank: 187
- device_id: kontakt_patty_strasse
title: Kontakt Straße
icon: 🪟
rank: 188
- device_id: sensor_arbeitszimmer_patty
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 189
- name: Arbeitszimmer Wolfgang
devices:
- device_id: thermostat_wolfgang
title: Wolfgang
icon: 🌡️
rank: 190
- device_id: experimentlabtest
title: ExperimentLabTest
icon: 💡
rank: 200
- device_id: kontakt_wolfgang_garten
title: Kontakt Garten
icon: 🪟
rank: 201
- device_id: sensor_arbeitszimmer_wolfgang
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 202
- name: Flur
devices:
- device_id: deckenlampe_flur_oben
title: Deckenlampe Flur oben
icon: 💡
rank: 210
- device_id: haustuer
title: Haustür
icon: 💡
rank: 220
- device_id: licht_flur_oben_am_spiegel
title: Licht Flur oben am Spiegel
icon: 💡
rank: 230
- device_id: sensor_flur
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 235
- name: Sportzimmer
devices:
- device_id: sportlicht_regal
title: Sportlicht Regal
icon: 🏃
rank: 240
- device_id: sportlicht_tisch
title: Sportlicht Tisch
icon: 🏃
rank: 250
- device_id: sportlicht_am_fernseher_studierzimmer
title: Sportlicht am Fernseher, Studierzimmer
icon: 🏃
rank: 260
- device_id: sensor_sportzimmer
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 265
- name: Bad Oben
devices:
- device_id: thermostat_bad_oben
title: Thermostat Bad Oben
icon: 🌡️
rank: 270
- device_id: kontakt_bad_oben_strasse
title: Kontakt Straße
icon: 🪟
rank: 271
- device_id: sensor_bad_oben
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 272
- name: Bad Unten
devices:
- device_id: thermostat_bad_unten
title: Thermostat Bad Unten
icon: 🌡️
rank: 280
- device_id: kontakt_bad_unten_strasse
title: Kontakt Straße
icon: 🪟
rank: 281
- device_id: sensor_bad_unten
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 282
- name: Waschküche
devices:
- device_id: sensor_waschkueche
title: Temperatur & Luftfeuchte
icon: 🌡️
rank: 290

View File

@@ -0,0 +1,35 @@
# UI Layout Configuration
# Defines rooms and device tiles for the home automation UI
rooms:
- name: Wohnzimmer
devices:
- device_id: test_lampe_2
title: Deckenlampe
icon: "💡"
rank: 5
- device_id: test_lampe_1
title: Stehlampe
icon: "🔆"
rank: 10
- device_id: test_thermo_1
title: Thermostat
icon: "🌡️"
rank: 15
- name: Schlafzimmer
devices:
- device_id: test_lampe_3
title: Nachttischlampe
icon: "🛏️"
rank: 10
- name: Lab
devices:
- device_id: experiment_light_1
title: Experimentierlampe
icon: "💡"
rank: 10

View File

@@ -0,0 +1,23 @@
# MAX! Thermostats - Room Assignment
#
# Extracted from layout.yaml
# Format: Room Name | Device ID (if thermostat exists)
#
Schlafzimmer
42
Esszimmer
45
Wohnzimmer
46
Arbeitszimmer Patty
39
Bad Oben
41
Bad Unten
48

31
config/raeume.txt Normal file
View File

@@ -0,0 +1,31 @@
Schlafzimmer
0x00158d00043292dc
Esszimmer
Wohnzimmer
0x00158d0008975707
Küche
0x00158d00083299bb
Arbeitszimmer Patty
0x00158d0003f052b7
Arbeitszimmer Wolfgang
0x00158d000543fb99
Bad Oben
0x00158d00093e8987
Bad Unten
0x00158d00093e662a
Flur
0x00158d000836ccc6
Waschküche
0x00158d000449f3bc
Sportzimmer
0x00158d0009421422

View File

@@ -4,6 +4,10 @@ from packages.home_capabilities.light import CAP_VERSION as LIGHT_VERSION
from packages.home_capabilities.light import LightState from packages.home_capabilities.light import LightState
from packages.home_capabilities.thermostat import CAP_VERSION as THERMOSTAT_VERSION from packages.home_capabilities.thermostat import CAP_VERSION as THERMOSTAT_VERSION
from packages.home_capabilities.thermostat import ThermostatState from packages.home_capabilities.thermostat import ThermostatState
from packages.home_capabilities.contact_sensor import CAP_VERSION as CONTACT_SENSOR_VERSION
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.layout import DeviceTile, Room, UiLayout, load_layout from packages.home_capabilities.layout import DeviceTile, Room, UiLayout, load_layout
__all__ = [ __all__ = [
@@ -11,6 +15,10 @@ __all__ = [
"LIGHT_VERSION", "LIGHT_VERSION",
"ThermostatState", "ThermostatState",
"THERMOSTAT_VERSION", "THERMOSTAT_VERSION",
"ContactState",
"CONTACT_SENSOR_VERSION",
"TempHumidityState",
"TEMP_HUMIDITY_SENSOR_VERSION",
"DeviceTile", "DeviceTile",
"Room", "Room",
"UiLayout", "UiLayout",

View File

@@ -0,0 +1,96 @@
"""Contact Sensor Capability - Fensterkontakt (read-only).
This module defines the ContactState model for door/window contact sensors.
These sensors report their open/closed state and are read-only devices.
Capability Version: contact_sensor@1.0.0
"""
from datetime import datetime
from typing import Annotated, Literal
from pydantic import BaseModel, Field, field_validator
# Capability metadata
CAP_VERSION = "contact_sensor@1.0.0"
DISPLAY_NAME = "Contact Sensor"
class ContactState(BaseModel):
"""State model for contact sensors (door/window sensors).
Contact sensors are read-only devices that report whether a door or window
is open or closed. They typically also report battery level and signal quality.
Attributes:
contact: Current state of the contact ("open" or "closed")
battery: Battery level percentage (0-100), optional
linkquality: MQTT link quality indicator, optional
device_temperature: Internal device temperature in °C, optional
voltage: Battery voltage in mV, optional
ts: Timestamp of the state reading, optional
Examples:
>>> ContactState(contact="open")
ContactState(contact='open', battery=None, ...)
>>> ContactState(contact="closed", battery=95, linkquality=87)
ContactState(contact='closed', battery=95, linkquality=87, ...)
"""
contact: Literal["open", "closed"] = Field(
...,
description="Contact state: 'open' for open door/window, 'closed' for closed"
)
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)"
)
device_temperature: float | None = Field(
None,
description="Internal device temperature in degrees Celsius"
)
voltage: int | None = Field(
None,
description="Battery voltage in millivolts"
)
ts: datetime | None = Field(
None,
description="Timestamp of the state reading"
)
@staticmethod
def normalize_bool(is_open: bool) -> "ContactState":
"""Convert boolean to ContactState.
Helper method to convert a boolean value to a ContactState instance.
Useful when integrating with systems that use True/False for contact state.
Args:
is_open: True if contact is open, False if closed
Returns:
ContactState instance with appropriate contact value
Examples:
>>> ContactState.normalize_bool(True)
ContactState(contact='open', ...)
>>> ContactState.normalize_bool(False)
ContactState(contact='closed', ...)
"""
return ContactState(contact="open" if is_open else "closed")
# Public API
__all__ = ["ContactState", "CAP_VERSION", "DISPLAY_NAME"]

View File

@@ -0,0 +1,37 @@
"""
Temperature & Humidity Sensor Capability - temp_humidity_sensor@1.0.0
Read-only sensor for temperature and humidity measurements.
"""
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, Field
class TempHumidityState(BaseModel):
"""
State model for temperature & humidity sensors.
Required fields:
- temperature: Temperature in degrees Celsius
- humidity: Relative humidity in percent
Optional fields:
- battery: Battery level 0-100%
- linkquality: Signal quality indicator
- voltage: Battery voltage in mV
- ts: Timestamp of measurement
"""
temperature: float = Field(..., description="Temperature in degrees Celsius")
humidity: float = Field(..., description="Relative humidity in percent (0-100)")
battery: Annotated[int, Field(ge=0, le=100)] | None = None
linkquality: int | None = None
voltage: int | None = None
ts: datetime | None = None
# Capability metadata
CAP_VERSION = "temp_humidity_sensor@1.0.0"
DISPLAY_NAME = "Temperature & Humidity Sensor"

View File

@@ -16,16 +16,16 @@ class ThermostatState(BaseModel):
Thermostat state model with validation. Thermostat state model with validation.
Attributes: Attributes:
mode: Operating mode (off, heat, auto) mode: Operating mode (off, heat, auto) - optional for SET commands
target: Target temperature in °C [5.0..30.0] target: Target temperature in °C [5.0..30.0]
current: Current temperature in °C (optional in SET, required in STATE) current: Current temperature in °C (optional in SET, required in STATE)
battery: Battery level 0-100% (optional) battery: Battery level 0-100% (optional)
window_open: Window open detection (optional) window_open: Window open detection (optional)
""" """
mode: Literal["off", "heat", "auto"] = Field( mode: Literal["off", "heat", "auto"] | None = Field(
..., None,
description="Operating mode of the thermostat" description="Operating mode of the thermostat (optional for SET commands)"
) )
target: float | Decimal = Field( target: float | Decimal = Field(