moving to device, trying to add api set for water setpoint

This commit is contained in:
Constantin Pascal
2025-03-12 12:13:39 +02:00
parent 1806fd51fb
commit 41c6fc9cdf
13 changed files with 386 additions and 42 deletions
+49
View File
@@ -0,0 +1,49 @@
{
"name": "ludeeus/integration_blueprint",
"image": "mcr.microsoft.com/devcontainers/python:3.12",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
"onAutoForward": "notify"
}
},
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ryanluker.vscode-coverage-gutters"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.defaultInterpreterPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": [
"ffmpeg",
"libturbojpeg0",
"libpcap-dev"
]
}
}
}
+12
View File
@@ -0,0 +1,12 @@
# https://www.home-assistant.io/integrations/default_config/
default_config:
# https://www.home-assistant.io/integrations/homeassistant/
homeassistant:
debug: true
# https://www.home-assistant.io/integrations/logger/
logger:
default: info
logs:
custom_components.integration_blueprint: debug
@@ -1,26 +1,12 @@
""" """
Custom integration for heat pump monitoring via web interface. Custom integration for heat pump monitoring and control via web interface.
Place this code in custom_components/heat_pump_monitor/__init__.py Place this code in custom_components/ha_rdz_pdc_config/__init__.py
""" """
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the Heat Pump Monitor component."""
hass.data.setdefault(DOMAIN, {})
return True
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)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
import asyncio import asyncio
import logging import logging
import aiohttp import aiohttp
from datetime import timedelta from datetime import timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_URL, CONF_URL,
@@ -32,12 +18,31 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator, DataUpdateCoordinator,
UpdateFailed, UpdateFailed,
) )
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "ha_rdz_pdc_config" DOMAIN = "ha_rdz_pdc_config"
DEFAULT_SCAN_INTERVAL = 3 DEFAULT_SCAN_INTERVAL = 3
PLATFORMS = ["sensor", "binary_sensor"] # Add climate to the platforms
PLATFORMS = ["sensor", "binary_sensor", "climate", "water_heater"]
# Service constants
SERVICE_SET_WATER_SETPOINT = "set_water_setpoint"
ATTR_TEMPERATURE = "temperature"
ATTR_DURATION = "duration"
# Service schema
SET_WATER_SETPOINT_SCHEMA = vol.Schema({
vol.Required(ATTR_TEMPERATURE): vol.All(vol.Coerce(float), vol.Range(min=35, max=60)),
vol.Optional(ATTR_DURATION, default="forever"): cv.string,
})
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the Heat Pump Monitor component."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up heat pump monitor from a config entry.""" """Set up heat pump monitor from a config entry."""
@@ -56,9 +61,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
# Register the service
async def set_water_setpoint(call: ServiceCall) -> None:
"""Set the water setpoint of the heat pump."""
temperature = call.data.get(ATTR_TEMPERATURE)
duration = call.data.get(ATTR_DURATION, "forever")
await coordinator.set_water_setpoint(temperature, duration)
hass.services.async_register(
DOMAIN,
SERVICE_SET_WATER_SETPOINT,
set_water_setpoint,
schema=SET_WATER_SETPOINT_SCHEMA,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
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)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class HeatPumpDataCoordinator(DataUpdateCoordinator): class HeatPumpDataCoordinator(DataUpdateCoordinator):
"""Class to manage fetching heat pump data.""" """Class to manage fetching heat pump data."""
@@ -77,6 +103,9 @@ class HeatPumpDataCoordinator(DataUpdateCoordinator):
update_interval=update_interval, update_interval=update_interval,
) )
self.url = url self.url = url
# Extract the base URL by removing the last path segment
parts = url.split('/')
self.base_url = '/'.join(parts[:-1])
async def _async_update_data(self): async def _async_update_data(self):
"""Fetch data from heat pump.""" """Fetch data from heat pump."""
@@ -129,3 +158,84 @@ class HeatPumpDataCoordinator(DataUpdateCoordinator):
} }
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
raise UpdateFailed(f"Error communicating with heat pump: {err}") raise UpdateFailed(f"Error communicating with heat pump: {err}")
async def set_water_setpoint(self, temperature: float, duration: str = "forever") -> bool:
"""Set water setpoint temperature."""
try:
setpoint_url = f"{self.base_url}/index.php?Action=SetParameter"
# Prepare form data
form_data = {
"Key": "watersetpoint",
"Value": str(temperature),
"manual": duration
}
_LOGGER.debug(f"Setting watersetpoint to {temperature} with duration {duration}")
async with aiohttp.ClientSession() as session:
async with session.post(
setpoint_url,
data=form_data,
headers={"Content-Type": "application/x-www-form-urlencoded"}
) as response:
if response.status != 200:
_LOGGER.error(f"Failed to set water setpoint: {response.status}")
return False
_LOGGER.info(f"Successfully set water setpoint to {temperature}")
# Force an immediate data refresh
await self.async_request_refresh()
return True
except Exception as err:
_LOGGER.error(f"Error setting water setpoint: {err}")
return False
async def set_dhw_setpoint(self, temperature: float, duration: str = "forever") -> bool:
"""Set water setpoint temperature."""
try:
setpoint_url = f"{self.base_url}/index.php?Action=SetParameter"
# Prepare form data
form_data = {
"Key": "watersetpoint",
"Value": str(temperature),
"manual": duration
}
_LOGGER.debug(f"Setting watersetpoint to {temperature} with duration {duration}")
async with aiohttp.ClientSession() as session:
async with session.post(
setpoint_url,
data=form_data,
headers={"Content-Type": "application/x-www-form-urlencoded"}
) as response:
if response.status != 200:
_LOGGER.error(f"Failed to set water setpoint: {response.status}")
return False
_LOGGER.info(f"Successfully set water setpoint to {temperature}")
# Force an immediate data refresh
await self.async_request_refresh()
return True
except Exception as err:
_LOGGER.error(f"Error setting water setpoint: {err}")
return False
class HeatPumpEntity(CoordinatorEntity):
"""Base class for heat pump entities."""
def __init__(self, coordinator, sensor_type=None):
"""Initialize the entity."""
super().__init__(coordinator)
self.sensor_type = sensor_type
@property
def device_info(self):
"""Return device information."""
return {
"identifiers": {(DOMAIN, "heat_pump")},
"name": "Heat Pump",
"manufacturer": "RDZ",
"model": "PDC",
"sw_version": self.coordinator.data.get("software_version", "Unknown"),
}
@@ -1,13 +1,12 @@
""" """
Place this code in custom_components/heat_pump_monitor/binary_sensor.py Place this code in custom_components/ha_rdz_pdc_config/binary_sensor.py
""" """
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorDeviceClass, BinarySensorDeviceClass,
) )
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, HeatPumpDataCoordinator from . import DOMAIN, HeatPumpEntity
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up binary sensors.""" """Set up binary sensors."""
@@ -27,13 +26,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
] ]
async_add_entities(entities) async_add_entities(entities)
class HeatPumpOperatingBinarySensor(CoordinatorEntity, BinarySensorEntity): class HeatPumpOperatingBinarySensor(HeatPumpEntity, BinarySensorEntity):
"""Binary sensor for heat pump operation states.""" """Binary sensor for heat pump operation states."""
def __init__(self, coordinator, sensor_type, name, device_class): def __init__(self, coordinator, sensor_type, name, device_class):
"""Initialize the binary sensor.""" """Initialize the binary sensor."""
super().__init__(coordinator) super().__init__(coordinator, sensor_type)
self.sensor_type = sensor_type
self._attr_name = f"Heat Pump {name}" self._attr_name = f"Heat Pump {name}"
self._attr_unique_id = f"{DOMAIN}_{sensor_type}" self._attr_unique_id = f"{DOMAIN}_{sensor_type}"
self._attr_device_class = device_class self._attr_device_class = device_class
@@ -0,0 +1,92 @@
"""
Place this code in custom_components/ha_rdz_pdc_config/climate.py
"""
import logging
from typing import Any, Dict, List, Optional
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
HVACAction,
)
from homeassistant.const import (
ATTR_TEMPERATURE,
UnitOfTemperature,
)
from . import DOMAIN, HeatPumpEntity
_LOGGER = logging.getLogger(__name__)
# Define available HVAC modes based on heat pump capabilities
HVAC_MODES = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up the heat pump climate."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([HeatPumpClimate(coordinator)])
class HeatPumpClimate(HeatPumpEntity, ClimateEntity):
"""Heat pump climate entity."""
def __init__(self, coordinator):
"""Initialize the heat pump climate entity."""
super().__init__(coordinator)
self._attr_name = "Heat Pump Water Temperature"
self._attr_unique_id = f"{DOMAIN}_climate"
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_hvac_modes = HVAC_MODES
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
self._attr_min_temp = 35
self._attr_max_temp = 60
self._attr_target_temperature_step = 0.5
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
# Use T1 (inflow temperature) as the current temperature
return self.coordinator.data.get("water_temp_t1")
@property
def target_temperature(self) -> Optional[float]:
"""Return the target temperature."""
return self.coordinator.data.get("water_setpoint")
@property
def hvac_mode(self) -> str:
"""Return the current HVAC mode."""
if self.coordinator.data.get("is_standby", False):
return HVACMode.OFF
elif self.coordinator.data.get("is_cooling", False):
return HVACMode.COOL
else:
return HVACMode.HEAT
@property
def hvac_action(self) -> Optional[str]:
"""Return the current HVAC action."""
if self.coordinator.data.get("is_standby", False):
return HVACAction.OFF
elif self.coordinator.data.get("is_cooling", False) and self.coordinator.data.get("is_working", False):
return HVACAction.COOLING
elif self.coordinator.data.get("is_heating", False) and self.coordinator.data.get("is_working", False):
return HVACAction.HEATING
else:
return HVACAction.IDLE
async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
if ATTR_TEMPERATURE not in kwargs:
return
temperature = kwargs[ATTR_TEMPERATURE]
await self.coordinator.set_water_setpoint(temperature)
self.async_write_ha_state()
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set HVAC mode."""
# Note: This would require additional endpoints to control the heat pump mode
# For now, we'll just log that this functionality isn't implemented
_LOGGER.warning("Setting HVAC mode is not currently supported by this integration")
# In the future, you could implement this if the heat pump API supports mode changes
@@ -1,5 +1,5 @@
""" """
Place this code in custom_components/heat_pump_monitor/sensor.py Place this code in custom_components/ha_rdz_pdc_config/sensor.py
""" """
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
@@ -7,9 +7,8 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.const import UnitOfTemperature, UnitOfTime from homeassistant.const import UnitOfTemperature, UnitOfTime
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, HeatPumpDataCoordinator from . import DOMAIN, HeatPumpEntity
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up sensors.""" """Set up sensors."""
@@ -31,13 +30,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
] ]
async_add_entities(entities) async_add_entities(entities)
class HeatPumpTemperatureSensor(CoordinatorEntity, SensorEntity): class HeatPumpTemperatureSensor(HeatPumpEntity, SensorEntity):
"""Temperature sensor for heat pump.""" """Temperature sensor for heat pump."""
def __init__(self, coordinator, sensor_type, name): def __init__(self, coordinator, sensor_type, name):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator, sensor_type)
self.sensor_type = sensor_type
self._attr_name = f"Heat Pump {name}" self._attr_name = f"Heat Pump {name}"
self._attr_unique_id = f"{DOMAIN}_{sensor_type}" self._attr_unique_id = f"{DOMAIN}_{sensor_type}"
self._attr_device_class = SensorDeviceClass.TEMPERATURE self._attr_device_class = SensorDeviceClass.TEMPERATURE
@@ -49,13 +47,12 @@ class HeatPumpTemperatureSensor(CoordinatorEntity, SensorEntity):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.coordinator.data.get(self.sensor_type) return self.coordinator.data.get(self.sensor_type)
class HeatPumpTimeSensor(CoordinatorEntity, SensorEntity): class HeatPumpTimeSensor(HeatPumpEntity, SensorEntity):
"""Time sensor for heat pump operation hours.""" """Time sensor for heat pump operation hours."""
def __init__(self, coordinator, sensor_type, name): def __init__(self, coordinator, sensor_type, name):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator, sensor_type)
self.sensor_type = sensor_type
self._attr_name = f"Heat Pump {name}" self._attr_name = f"Heat Pump {name}"
self._attr_unique_id = f"{DOMAIN}_{sensor_type}" self._attr_unique_id = f"{DOMAIN}_{sensor_type}"
self._attr_device_class = SensorDeviceClass.DURATION self._attr_device_class = SensorDeviceClass.DURATION
@@ -65,17 +62,15 @@ class HeatPumpTimeSensor(CoordinatorEntity, SensorEntity):
@property @property
def native_value(self): def native_value(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
# Convert the value from hours to seconds for Home Assistant # Return the hours value directly
hours = self.coordinator.data.get(self.sensor_type, 0) return self.coordinator.data.get(self.sensor_type, 0)
return hours
class HeatPumpVersionSensor(CoordinatorEntity, SensorEntity): class HeatPumpVersionSensor(HeatPumpEntity, SensorEntity):
"""Software version sensor for heat pump.""" """Software version sensor for heat pump."""
def __init__(self, coordinator, sensor_type, name): def __init__(self, coordinator, sensor_type, name):
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator, sensor_type)
self.sensor_type = sensor_type
self._attr_name = f"Heat Pump {name}" self._attr_name = f"Heat Pump {name}"
self._attr_unique_id = f"{DOMAIN}_{sensor_type}" self._attr_unique_id = f"{DOMAIN}_{sensor_type}"
@@ -0,0 +1,53 @@
"""
Place this code in custom_components/ha_rdz_pdc_config/water_heater.py
"""
from homeassistant.components.water_heater import (
WaterHeaterEntity,
WaterHeaterEntityFeature,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from . import DOMAIN, HeatPumpEntity
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up water heater."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([HeatPumpWaterHeater(coordinator)])
class HeatPumpWaterHeater(HeatPumpEntity, WaterHeaterEntity):
"""Heat pump water heater entity."""
def __init__(self, coordinator):
"""Initialize the water heater entity."""
super().__init__(coordinator)
self._attr_name = "Heat Pump DHW"
self._attr_unique_id = f"{DOMAIN}_water_heater"
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_min_temp = 35
self._attr_max_temp = 60
self._attr_target_temperature_step = 0.5
self._attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
@property
def current_temperature(self):
"""Return the current temperature."""
return self.coordinator.data.get("dhw_temp_t3")
@property
def target_temperature(self):
"""Return the target temperature."""
return self.coordinator.data.get("dhw_setpoint")
@property
def state(self):
"""Return the current state."""
if self.coordinator.data.get("is_dhw", False):
return STATE_ON
return STATE_OFF
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
await self.coordinator.set_dhw_setpoint(temperature)
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/integration_blueprint
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
hass --config "${PWD}/config" --debug
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
ruff format .
ruff check . --fix
Executable
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
python3 -m pip install --requirement requirements.txt