Compare commits
4 Commits
e69822719a
...
2eb4f3c376
| Author | SHA1 | Date | |
|---|---|---|---|
|
2eb4f3c376
|
|||
|
b57ddb1589
|
|||
|
a49d56df60
|
|||
|
5a7b16f7aa
|
223
MAX_INTEGRATION.md
Normal file
223
MAX_INTEGRATION.md
Normal 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)
|
||||
@@ -173,6 +173,11 @@ async def handle_abstract_set(
|
||||
# Transform abstract payload to vendor-specific format
|
||||
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)
|
||||
|
||||
logger.info(f"→ vendor SET {device_id}: {vendor_topic} ← {vendor_message}")
|
||||
@@ -294,6 +299,25 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
topic = str(message.topic)
|
||||
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:
|
||||
payload = json.loads(payload_str)
|
||||
except json.JSONDecodeError:
|
||||
@@ -318,7 +342,16 @@ async def mqtt_worker(config: dict[str, Any], redis_client: aioredis.Redis) -> N
|
||||
|
||||
# Check if this is a vendor STATE message
|
||||
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():
|
||||
if topic == device["topics"]["state"]:
|
||||
device_technology = device.get("technology", "unknown")
|
||||
|
||||
@@ -124,6 +124,79 @@ def _transform_thermostat_zigbee2mqtt_to_abstract(payload: dict[str, Any]) -> di
|
||||
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
|
||||
# ============================================================================
|
||||
@@ -142,6 +215,8 @@ TRANSFORM_HANDLERS: dict[tuple[str, str, str], TransformHandler] = {
|
||||
("thermostat", "simulator", "to_abstract"): _transform_thermostat_simulator_to_abstract,
|
||||
("thermostat", "zigbee2mqtt", "to_vendor"): _transform_thermostat_zigbee2mqtt_to_vendor,
|
||||
("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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -407,40 +407,7 @@
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.mode-controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
color: #666;
|
||||
min-height: 44px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mode-button:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.mode-button.active {
|
||||
background: #667eea;
|
||||
border-color: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mode-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.events {
|
||||
margin-top: 2rem;
|
||||
@@ -607,32 +574,11 @@
|
||||
</div>
|
||||
|
||||
<div class="temp-controls">
|
||||
<button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', -0.5)">
|
||||
-0.5
|
||||
<button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', -1.0)">
|
||||
-1.0
|
||||
</button>
|
||||
<button class="temp-button" onclick="adjustTarget('{{ device.device_id }}', 0.5)">
|
||||
+0.5
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mode-controls">
|
||||
<button
|
||||
class="mode-button"
|
||||
id="mode-{{ device.device_id }}-off"
|
||||
onclick="setMode('{{ device.device_id }}', 'off')">
|
||||
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 class="temp-button" onclick="adjustTarget('{{ device.device_id }}', 1.0)">
|
||||
+1.0
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -877,38 +823,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
|
||||
function updateDeviceUI(deviceId, power, brightness) {
|
||||
currentState[deviceId] = power;
|
||||
@@ -973,18 +887,6 @@
|
||||
modeSpan.textContent = mode.toUpperCase();
|
||||
}
|
||||
thermostatModes[deviceId] = mode;
|
||||
|
||||
// Update mode button states
|
||||
['off', 'heat', 'auto'].forEach(m => {
|
||||
const btn = document.getElementById(`mode-${deviceId}-${m}`);
|
||||
if (btn) {
|
||||
if (m === mode.toLowerCase()) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -395,6 +395,114 @@ devices:
|
||||
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"
|
||||
|
||||
@@ -17,6 +17,10 @@ rooms:
|
||||
title: Medusa-Lampe Schlafzimmer
|
||||
icon: 💡
|
||||
rank: 40
|
||||
- device_id: thermostat_schlafzimmer
|
||||
title: Thermostat Schlafzimmer
|
||||
icon: 🌡️
|
||||
rank: 45
|
||||
- name: Esszimmer
|
||||
devices:
|
||||
- device_id: deckenlampe_esszimmer
|
||||
@@ -39,6 +43,10 @@ rooms:
|
||||
title: kleine Lampe rechts Esszimmer
|
||||
icon: 💡
|
||||
rank: 90
|
||||
- device_id: thermostat_esszimmer
|
||||
title: Thermostat Esszimmer
|
||||
icon: 🌡️
|
||||
rank: 95
|
||||
- name: Wohnzimmer
|
||||
devices:
|
||||
- device_id: lampe_naehtischchen_wohnzimmer
|
||||
@@ -57,6 +65,10 @@ rooms:
|
||||
title: grosse Lampe Wohnzimmer
|
||||
icon: 💡
|
||||
rank: 130
|
||||
- device_id: thermostat_wohnzimmer
|
||||
title: Thermostat Wohnzimmer
|
||||
icon: 🌡️
|
||||
rank: 135
|
||||
- name: Küche
|
||||
devices:
|
||||
- device_id: kueche_deckenlampe
|
||||
@@ -81,6 +93,10 @@ rooms:
|
||||
title: Schranklicht vorne Patty
|
||||
icon: 💡
|
||||
rank: 180
|
||||
- device_id: thermostat_patty
|
||||
title: Thermostat Patty
|
||||
icon: 🌡️
|
||||
rank: 185
|
||||
- name: Arbeitszimmer Wolfgang
|
||||
devices:
|
||||
- device_id: thermostat_wolfgang
|
||||
@@ -119,3 +135,15 @@ rooms:
|
||||
title: Sportlicht am Fernseher, Studierzimmer
|
||||
icon: 🏃
|
||||
rank: 260
|
||||
- name: Bad Oben
|
||||
devices:
|
||||
- device_id: thermostat_bad_oben
|
||||
title: Thermostat Bad Oben
|
||||
icon: 🌡️
|
||||
rank: 270
|
||||
- name: Bad Unten
|
||||
devices:
|
||||
- device_id: thermostat_bad_unten
|
||||
title: Thermostat Bad Unten
|
||||
icon: 🌡️
|
||||
rank: 280
|
||||
|
||||
23
config/max-thermostats.txt
Normal file
23
config/max-thermostats.txt
Normal 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
|
||||
Reference in New Issue
Block a user