Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Use proper entity for water heater #101

Merged
merged 1 commit into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions custom_components/aquarea/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Platform.SELECT,
Platform.NUMBER,
Platform.CLIMATE,
Platform.WATER_HEATER,
]
_LOGGER = logging.getLogger(__name__)

Expand Down
178 changes: 1 addition & 177 deletions custom_components/aquarea/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@
from homeassistant.util import slugify

from homeassistant.components.climate import ClimateEntityDescription
from homeassistant.components.climate.const import (
PRESET_ECO,
PRESET_COMFORT,
PRESET_NONE,
)
from .definitions import lookup_by_value, OperatingMode
from .definitions import OperatingMode
from . import build_device_info
from .const import DeviceType

Expand Down Expand Up @@ -69,11 +64,6 @@ async def async_setup_entry(
f"Starting bootstrap of climate entities with prefix '{discovery_prefix}'"
)
"""Set up HeishaMon climates from config entry."""
description = ClimateEntityDescription(
key=f"{discovery_prefix}main/DHW_Target_Temp",
name="Aquarea Domestic Water Heater",
)
async_add_entities([HeishaMonDHWClimate(hass, description, config_entry)])
description_zone1 = ZoneClimateEntityDescription(
key=f"{discovery_prefix}main/Z1_Temp",
name="Aquarea Zone 1 climate",
Expand All @@ -89,172 +79,6 @@ async def async_setup_entry(
async_add_entities([zone1_climate, zone2_climate])


class HeishaMonDHWClimate(ClimateEntity):
"""Representation of a HeishaMon sensor that is updated via MQTT."""

preset_mode_temps = {
"52": PRESET_ECO,
"60": PRESET_COMFORT,
}

def __init__(
self,
hass: HomeAssistant,
description: ClimateEntityDescription,
config_entry: ConfigEntry,
) -> None:
"""Initialize the climate entity."""
self.config_entry_entry_id = config_entry.entry_id
self.entity_description = description
self.hass = hass
self.discovery_prefix = config_entry.data[
"discovery_prefix"
] # TODO: handle migration of entities

slug = slugify(self.entity_description.key.replace("/", "_"))
self.entity_id = f"climate.{slug}"
self._attr_unique_id = f"{config_entry.entry_id}"

self._attr_temperature_unit = "°C"
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
self._attr_hvac_mode = HVACMode.OFF
self._attr_min_temp = 40
self._attr_max_temp = 65
self._attr_target_temperature_step = 1
self._operating_mode = OperatingMode(0) # i.e None
self._attr_preset_modes = [PRESET_ECO, PRESET_COMFORT]
self._attr_preset_mode = PRESET_ECO

self._heatpump_state = False

async def async_set_temperature(self, **kwargs) -> None:
temperature = kwargs.get("temperature")
_LOGGER.debug(f"Changing {self.name} target temperature to {temperature})")
payload = str(temperature)
await async_publish(
self.hass,
f"{self.discovery_prefix}commands/SetDHWTemp",
payload,
0,
False,
"utf-8",
)

async def async_set_preset_mode(self, preset_mode: str):
temp = lookup_by_value(HeishaMonDHWClimate.preset_mode_temps, preset_mode)
if temp is None:
_LOGGER.warn(
f"No target temperature implemented for {preset_mode}, ignoring"
)
return
await self.async_set_temperature(temperature=float(temp))

async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events."""

@callback
def current_temperature_message_received(message):
self._attr_current_temperature = float(message.payload)
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/DHW_Temp",
current_temperature_message_received,
1,
)

@callback
def target_temperature_message_received(message):
self._attr_target_temperature = float(message.payload)
self._attr_preset_mode = HeishaMonDHWClimate.preset_mode_temps.get(
str(int(self._attr_target_temperature)), PRESET_NONE
)
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/DHW_Target_Temp",
target_temperature_message_received,
1,
)

def guess_hvac_mode() -> HVACMode:
if OperatingMode.DHW in self._operating_mode and self._heatpump_state:
return HVACMode.HEAT
else:
return HVACMode.OFF

@callback
def heatpump_state_message_received(message):
self._heatpump_state = bool(int(message.payload))
self._attr_hvac_mode = guess_hvac_mode()
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/Heatpump_State",
heatpump_state_message_received,
1,
)

@callback
def operating_state_message_received(message):
self._operating_mode = OperatingMode.from_mqtt(message.payload)
self._attr_hvac_mode = guess_hvac_mode()
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/Operating_Mode_State",
operating_state_message_received,
1,
)

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
new_heatpump_state = self._heatpump_state
if hvac_mode == HVACMode.HEAT:
new_operating_mode = self._operating_mode | OperatingMode.DHW
new_heatpump_state = True
elif hvac_mode == HVACMode.OFF:
new_operating_mode = self._operating_mode & ~OperatingMode.DHW
if new_operating_mode == OperatingMode(0): # i.e "none"
new_heatpump_state = False
else:
raise NotImplemented(
f"Mode {hvac_mode} has not been implemented by this entity"
)
if (
new_operating_mode != OperatingMode(0)
and new_operating_mode != self._operating_mode
):
await async_publish(
self.hass,
f"{self.discovery_prefix}commands/SetOperationMode",
new_operating_mode.to_mqtt(),
0,
False,
"utf-8",
)
if new_heatpump_state != self._heatpump_state:
await async_publish(
self.hass,
f"{self.discovery_prefix}commands/SetHeatpump",
str(int(new_heatpump_state)),
0,
False,
"utf-8",
)
self._attr_hvac_mode = hvac_mode # let's be optimistic
self.async_write_ha_state()

@property
def device_info(self):
return build_device_info(DeviceType.HEATPUMP, self.discovery_prefix)


@dataclass
class ZoneClimateEntityDescription(ClimateEntityDescription):
Expand Down
163 changes: 163 additions & 0 deletions custom_components/aquarea/water_heater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from __future__ import annotations
import logging

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import slugify
from homeassistant.components import mqtt
from homeassistant.components.mqtt.client import async_publish

from homeassistant.components.water_heater import (
WaterHeaterEntityEntityDescription,
WaterHeaterEntity,
WaterHeaterEntityFeature,
STATE_ECO,
STATE_HIGH_DEMAND,
)

from .definitions import lookup_by_value, OperatingMode
from . import build_device_info
from .const import DeviceType

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
discovery_prefix = config_entry.data[
"discovery_prefix"
] # TODO: handle migration of entities
_LOGGER.debug(
f"Starting bootstrap of water heater entities with prefix '{discovery_prefix}'"
)
"""Set up HeishaMon water heater from config entry."""
description = WaterHeaterEntityEntityDescription(
key=f"{discovery_prefix}main/DHW_Target_Temp",
name="Aquarea Domestic Water Heater",
)
async_add_entities([HeishaMonDHW(hass, description, config_entry)])


PRESET_COMFORT = "comfort"
PRESET_NONE = "none"


class HeishaMonDHW(WaterHeaterEntity):
"""Representation of a HeishaMon sensor that is updated via MQTT."""

operation_modes_temps = {
"52": STATE_ECO,
"60": STATE_HIGH_DEMAND,
}

def __init__(
self,
hass: HomeAssistant,
description: WaterHeaterEntityEntityDescription,
config_entry: ConfigEntry,
) -> None:
"""Initialize the water heater entity."""
self.config_entry_entry_id = config_entry.entry_id
self.entity_description = description
self.hass = hass
self.discovery_prefix = config_entry.data[
"discovery_prefix"
] # TODO: handle migration of entities

slug = slugify(self.entity_description.key.replace("/", "_"))
self.entity_id = f"climate.{slug}"
self._attr_unique_id = f"{config_entry.entry_id}.water_heater"

self._attr_temperature_unit = "°C"
self._attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.ON_OFF
| WaterHeaterEntityFeature.OPERATION_MODE
)
self._attr_current_operation = STATE_ECO
self._attr_min_temp = 40
self._attr_max_temp = 65
self._attr_precision = 1
self._attr_operation_list = [STATE_ECO, STATE_HIGH_DEMAND]
self._heat_delta = 0

async def async_set_temperature(self, **kwargs) -> None:
temperature = kwargs.get("temperature")
_LOGGER.debug(f"Changing {self.name} target temperature to {temperature})")
payload = str(temperature)
self.update_temperature_bounds() # optimistic update
await async_publish(
self.hass,
f"{self.discovery_prefix}commands/SetDHWTemp",
payload,
0,
False,
"utf-8",
)

async def async_set_operation_mode(self, operation_mode: str):
temp = lookup_by_value(HeishaMonDHW.operation_modes_temps, operation_mode)
if temp is None:
_LOGGER.warn(
f"No target temperature implemented for {operation_mode}, ignoring"
)
return
await self.async_set_temperature(temperature=float(temp))

def update_temperature_bounds(self) -> None:
self._attr_target_temperature_high = self._attr_target_temperature
self._attr_target_temperature_low = (
self._heat_delta + self._attr_target_temperature
)

async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events."""

@callback
def current_temperature_message_received(message):
self._attr_current_temperature = float(message.payload)
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/DHW_Temp",
current_temperature_message_received,
1,
)

@callback
def target_temperature_message_received(message):
self._attr_target_temperature = float(message.payload)
self.update_temperature_bounds() # optimistic update
self._attr_current_operation = HeishaMonDHW.operation_modes_temps.get(
str(int(self._attr_target_temperature)), PRESET_NONE
)
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/DHW_Target_Temp",
target_temperature_message_received,
1,
)

@callback
def heat_delta_received(message):
self._heat_delta = int(message.payload)
self.update_temperature_bounds()
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"{self.discovery_prefix}main/DHW_Heat_Delta",
heat_delta_received,
1,
)

@property
def device_info(self):
return build_device_info(DeviceType.HEATPUMP, self.discovery_prefix)
Loading