Skip to content
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This integration has been developed for and tested to be **fully functional** wi
This custom integration provides enhanced functionality compared to the core Home Assistant Touchline integration:

### Enhanced Features
- **Virtual Heat Mode**: Optional HVAC action monitoring with intelligent hysteresis control
- **Extended System Monitoring**: Access to controller datetime, error codes, and system status sensors
- **Time Synchronization**: Built-in button entity to sync controller time with Home Assistant
- **Controller Metadata**: Retrieves ownerKurzID and additional R0 parameters from the controller
Expand Down Expand Up @@ -76,13 +77,49 @@ The integration will automatically discover all heating zones configured on your

All configuration is done through the Home Assistant UI. No YAML configuration is required.

### Optional Features

#### Virtual Heat Mode

The integration includes an optional **Virtual Heat Mode** feature that provides HVAC action monitoring for your climate entities. This feature displays whether your heating system is actively heating or idle, giving you better visibility into your system's operation.

**Enabling Virtual Heat Mode:**
1. Go to **Settings** → **Devices & Services**
2. Find the "Roth Touchline" integration
3. Click **Configure**
4. Check the **"Virtual heat mode"** option
5. Click **Submit**

**How It Works:**

Virtual heat mode uses intelligent temperature-based logic with hysteresis control to determine the heating state:

- **Heating State**: Activated when the current temperature drops **0.2°C or more** below the target temperature
- **Idle State**: Activated when the current temperature rises **0.3°C or more** above the target temperature (with a 5-minute delay)
- **Hysteresis Band**: When temperature is within the -0.3°C to +0.2°C range relative to target, the system maintains its current state to prevent frequent switching

**Benefits:**
- **Better Visibility**: See at a glance whether your heating is actively working or idle
- **Energy Monitoring**: Track heating activity for better energy management
- **Automation Triggers**: Use HVAC action states in Home Assistant automations
- **Reduced Oscillation**: Asymmetric hysteresis prevents rapid on/off cycling

**Example Use Cases:**
- Create automations that notify you when heating starts or stops
- Track daily heating activity patterns
- Optimize heating schedules based on actual heating demand
- Monitor system efficiency

**Note**: This is a virtual/simulated feature based on temperature differences. The Roth Touchline controller itself does not provide direct heating state information.

## Usage

### Climate Entities

Each heating zone in your Roth Touchline system will be available as a climate entity. You can:
- **View current temperature**: Check the current temperature reading from each zone
- **Set target temperature**: Adjust the desired temperature for each zone
- **Monitor HVAC action** (when virtual heat mode is enabled): See whether the zone is actively heating, idle, or off
- **Change HVAC mode**:
- `Heat`: Normal heating operation
- `Off`: Holiday mode (disables heating)
Expand Down
7 changes: 7 additions & 0 deletions custom_components/touchline/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

entry.async_on_unload(entry.add_update_listener(async_reload_entry))

return True


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry when options change."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Expand Down
74 changes: 73 additions & 1 deletion custom_components/touchline/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from __future__ import annotations

import logging
from datetime import datetime, timedelta
from typing import Any, NamedTuple

from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
Expand All @@ -15,10 +17,15 @@
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util

from . import TouchlineDataUpdateCoordinator, ExtendedPyTouchline
from .const import (
CONF_VIRTUAL_HEAT_MODE,
DOMAIN,
HEAT_MODE_DELAY,
HEAT_MODE_HEATING_THRESHOLD,
HEAT_MODE_IDLE_THRESHOLD,
OPERATION_MODE_AUTO,
OPERATION_MODE_FROST,
OPERATION_MODE_HOLIDAY,
Expand Down Expand Up @@ -59,8 +66,10 @@ async def async_setup_entry(

await coordinator.async_refresh()

virtual_heat_mode = entry.options.get(CONF_VIRTUAL_HEAT_MODE, False)

async_add_entities(
TouchlineClimate(coordinator, idx)
TouchlineClimate(coordinator, idx, virtual_heat_mode)
for idx in range(len(coordinator.data))
)

Expand All @@ -79,10 +88,12 @@ def __init__(
self,
coordinator: TouchlineDataUpdateCoordinator,
idx: int,
virtual_heat_mode: bool,
) -> None:
"""Initialize the climate entity."""
super().__init__(coordinator)
self._idx = idx
self._virtual_heat_mode = virtual_heat_mode
self._attr_unique_id = f"{coordinator.host}_{idx}"
device_id = self._device.get_device_id()
self._attr_device_info = DeviceInfo(
Expand All @@ -93,6 +104,9 @@ def __init__(
serial_number=str(device_id) if device_id is not None else None,
via_device=(DOMAIN, f"{coordinator.host}_controller"),
)
# Track state for virtual heat mode
self._last_heating_time: datetime | None = None
self._is_heating = False

@property
def _device(self) -> ExtendedPyTouchline:
Expand Down Expand Up @@ -138,6 +152,64 @@ def hvac_mode(self) -> HVACMode:
return HVACMode.OFF
return HVACMode.HEAT

@property
def hvac_action(self) -> HVACAction | None:
"""Return the current HVAC action."""
# If not in HEAT mode, return OFF
if self.hvac_mode == HVACMode.OFF:
return HVACAction.OFF

# If virtual heat mode is not enabled, return None (no action reported)
if not self._virtual_heat_mode:
return None

# Get current and target temperatures
current_temp = self.current_temperature
target_temp = self.target_temperature

# If temperatures are unavailable, return None
if current_temp is None or target_temp is None:
return None

# Calculate temperature difference
temp_diff = target_temp - current_temp

# Logic as per requirements:
# - When temp drops 0.2°C below target -> immediately heating
# - When temp rises 0.3°C above target -> after 5 min delay, idle

if temp_diff >= HEAT_MODE_HEATING_THRESHOLD:
# Temperature is below target (needs heating)
self._is_heating = True
self._last_heating_time = dt_util.utcnow()
return HVACAction.HEATING
elif temp_diff <= -HEAT_MODE_IDLE_THRESHOLD:
# Temperature is above target
# Check if we should transition to idle after delay
if self._is_heating:
# We were heating, check if delay has passed
if self._last_heating_time is None:
self._last_heating_time = dt_util.utcnow()

time_since_heating = dt_util.utcnow() - self._last_heating_time
if time_since_heating.total_seconds() >= HEAT_MODE_DELAY:
# Delay passed, transition to idle
self._is_heating = False
return HVACAction.IDLE
else:
# Still within delay period, remain heating
return HVACAction.HEATING
else:
# Already idle
return HVACAction.IDLE
else:
# Within hysteresis band (between -0.3°C and +0.2°C)
# Maintain current state
if self._is_heating:
return HVACAction.HEATING
else:
return HVACAction.IDLE

@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
Expand Down
34 changes: 32 additions & 2 deletions custom_components/touchline/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
import voluptuous as vol
from pytouchline import PyTouchline

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.const import CONF_HOST
from homeassistant.core import callback

from .const import DOMAIN
from .const import DOMAIN, CONF_VIRTUAL_HEAT_MODE

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -62,3 +63,32 @@ def _get_number_of_devices(host: str) -> str:
"""Return number of devices from the controller."""
touchline = PyTouchline()
return touchline.get_number_of_devices(f"http://{host}")

@staticmethod
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return TouchlineOptionsFlow()


class TouchlineOptionsFlow(OptionsFlow):
"""Handle options flow for Roth Touchline."""

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_VIRTUAL_HEAT_MODE,
default=self.config_entry.options.get(CONF_VIRTUAL_HEAT_MODE, False),
): bool,
}
),
)

6 changes: 6 additions & 0 deletions custom_components/touchline/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
DOMAIN = "touchline"

CONF_HOST = "host"
CONF_VIRTUAL_HEAT_MODE = "virtual_heat_mode"

# Operation modes from pyTouchline
OPERATION_MODE_AUTO = 0
OPERATION_MODE_MANUAL = 1
OPERATION_MODE_HOLIDAY = 2
OPERATION_MODE_FROST = 3

# Virtual heat mode thresholds
HEAT_MODE_HEATING_THRESHOLD = 0.2 # Temperature difference to trigger heating (°C below target)
HEAT_MODE_IDLE_THRESHOLD = 0.3 # Temperature difference to trigger idle (°C above target)
HEAT_MODE_DELAY = 300 # Delay in seconds (5 minutes) before switching to idle
13 changes: 13 additions & 0 deletions custom_components/touchline/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@
"abort": {
"already_configured": "This Touchline controller is already configured."
}
},
"options": {
"step": {
"init": {
"title": "Touchline Options",
"data": {
"virtual_heat_mode": "Enable virtual heat mode (heating/idle based on temperature)"
},
"data_description": {
"virtual_heat_mode": "When enabled, climate entities will show 'heating' when temperature is below target and 'idle' when temperature is above target, with hysteresis and delay for stability."
}
}
}
}
}
13 changes: 13 additions & 0 deletions custom_components/touchline/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@
"abort": {
"already_configured": "This Touchline controller is already configured."
}
},
"options": {
"step": {
"init": {
"title": "Touchline Options",
"data": {
"virtual_heat_mode": "Enable virtual heat mode (heating/idle based on temperature)"
},
"data_description": {
"virtual_heat_mode": "When enabled, climate entities will show 'heating' when temperature is below target and 'idle' when temperature is above target, with hysteresis and delay for stability."
}
}
}
}
}
Loading