Skip to content

Commit

Permalink
✨ Expose climate components for zone 1 and zone 2 heating
Browse files Browse the repository at this point in the history
This also allow to introduce better management of operating mode and
zone state with nice enums.

⚠ Detection of heating mode (compensation curve or direct) does work but
does not succeed to update the climate component accordingly. Default is
set to direct mode at the moment.

Fixes #25

Change-Id: I9104290bd7c81ac253170969f1d34933da3a3ebb
  • Loading branch information
kamaradclimber committed Oct 31, 2022
1 parent 9882193 commit c1dc11b
Show file tree
Hide file tree
Showing 2 changed files with 340 additions and 51 deletions.
287 changes: 258 additions & 29 deletions custom_components/aquarea/climate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Support for HeishaMon controlled heatpumps through MQTT."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from enum import Enum, Flag, auto

from homeassistant.components import mqtt
from homeassistant.components.mqtt.client import async_publish
Expand All @@ -17,13 +19,44 @@
PRESET_COMFORT,
PRESET_NONE,
)
from .definitions import lookup_by_value
from .definitions import lookup_by_value, OperatingMode
from . import build_device_info
from .const import DeviceType

_LOGGER = logging.getLogger(__name__)


class ZoneState(Flag):
ZONE1 = auto()
ZONE2 = auto()

@staticmethod
def from_id(id: int) -> ZoneState:
if id == 1:
return ZoneState.ZONE1
elif id == 2:
return ZoneState.ZONE2
else:
raise Exception(f"No zone with id {id}")

def to_mqtt(self) -> str:
return str(
{
ZoneState.ZONE1: 0,
ZoneState.ZONE2: 1,
(ZoneState.ZONE1 | ZoneState.ZONE2): 2,
}[self]
)

@staticmethod
def from_mqtt(value: str) -> ZoneState:
return {
0: ZoneState.ZONE1,
1: ZoneState.ZONE2,
2: (ZoneState.ZONE1 | ZoneState.ZONE2),
}[int(value)]


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
Expand All @@ -35,6 +68,19 @@ async def async_setup_entry(
name="Aquarea Domestic Water Heater",
)
async_add_entities([HeishaMonDHWClimate(hass, description, config_entry)])
description_zone1 = ZoneClimateEntityDescription(
key="panasonic_heat_pump/main/Z1_Temp",
name="Aquarea Zone 1 climate",
zone_id=1,
)
zone1_climate = HeishaMonZoneClimate(hass, description_zone1, config_entry)
description_zone2 = ZoneClimateEntityDescription(
name="Aquarea Zone 2 climate",
key="panasonic_heat_pump/main/Z2_Temp",
zone_id=2,
)
zone2_climate = HeishaMonZoneClimate(hass, description_zone2, config_entry)
async_add_entities([zone1_climate, zone2_climate])


class HeishaMonDHWClimate(ClimateEntity):
Expand Down Expand Up @@ -68,7 +114,7 @@ def __init__(
self._attr_hvac_mode = HVACMode.OFF
self._attr_min_temp = 45
self._attr_max_temp = 65
self._operating_mode = -1
self._operating_mode = OperatingMode(0) # i.e None
self._attr_preset_modes = [PRESET_ECO, PRESET_COMFORT]
self._attr_preset_mode = PRESET_ECO

Expand Down Expand Up @@ -126,9 +172,8 @@ def target_temperature_message_received(message):

@callback
def operating_state_message_received(message):
value = int(message.payload)
self._operating_mode = value
if value in [3, 4, 5, 6, 8]:
self._operating_mode = OperatingMode.from_mqtt(message.payload)
if OperatingMode.DHW in self._operating_mode:
self._attr_hvac_mode = HVACMode.HEAT
else:
self._attr_hvac_mode = HVACMode.OFF
Expand All @@ -143,45 +188,229 @@ def operating_state_message_received(message):

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if hvac_mode == HVACMode.HEAT:
value = {
"0": "4",
"1": "5",
"2": "6",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "8",
"8": "8",
}[str(self._operating_mode)]
value = self._operating_mode | OperatingMode.DHW
elif hvac_mode == HVACMode.OFF:
value = {
"0": "0",
"1": "1",
"2": "2",
"3": "3", # we don't have a way to completely shut down DHW, so it should be different than 3
"4": "1",
"5": "2",
"6": "2",
"7": "7",
"8": "7",
}[str(self._operating_mode)]
if value == 3:
value = self._operating_mode & ~OperatingMode.DHW
if value == OperatingMode(0): # i.e "none"
_LOGGER.warn(
f"Impossible to set {hvac_mode} on this heatpump, we can't disable water heater when heating/cooling is already disabled"
)
raise NotImplemented(
f"Powering off heatpump entirely has not been implemented by this entity"
)
else:
raise NotImplemented(
f"Mode {hvac_mode} has not been implemented by this entity"
)
await async_publish(
self.hass,
"panasonic_heat_pump/commands/SetOperationMode",
value,
value.to_mqtt(),
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)


@dataclass
class ZoneClimateEntityDescription(ClimateEntityDescription):
zone_id: int = 1


class ZoneClimateMode(Enum):
COMPENSATION = 1
DIRECT = 2


class HeishaMonZoneClimate(ClimateEntity):
"""Representation of a HeishaMon climate entity that is updated via MQTT."""

def __init__(
self,
hass: HomeAssistant,
description: ZoneClimateEntityDescription,
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.zone_id = description.zone_id
slug = slugify(self.entity_description.key.replace("/", "_"))
self.entity_id = f"climate.{slug}"
self._attr_unique_id = f"{config_entry.entry_id}-{self.zone_id}"

self._mode = ZoneClimateMode.COMPENSATION
self.change_mode(ZoneClimateMode.COMPENSATION)

self._attr_temperature_unit = "°C"
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
self._attr_hvac_mode = HVACMode.OFF

self._attr_min_temp = -5
self._attr_max_temp = 5
self._zone_state = ZoneState(0) # i.e None
self._operating_mode = OperatingMode(0) # i.e None

def change_mode(self, mode: ZoneClimateMode):
_LOGGER.info(f"Changing mode to {mode} for zone {self.zone_id}")
self._mode = mode
if mode == ZoneClimateMode.COMPENSATION:
self._attr_min_temp = -5
self._attr_max_temp = 5
self._attr_target_temperature_step = 1
else:
self._attr_min_temp = 15
self._attr_max_temp = 25
self._attr_target_temperature_step = 1

async def async_set_temperature(self, **kwargs) -> None:
temperature = kwargs.get("temperature")

if self._mode == ZoneClimateMode.COMPENSATION:
_LOGGER.debug(
f"Changing {self.name} temperature offset to {temperature} for zone {self.zone_id}"
)
elif self._mode == ZoneClimateMode.DIRECT:
_LOGGER.debug(
f"Changing {self.name} target temperature to {temperature} for zone {self.zone_id}"
)
else:
raise Exception(f"Unknown climate mode: {self._mode}")
payload = str(temperature)

_LOGGER.debug(
f"sending {payload} as temperature command for zone {self.zone_id}"
)
await async_publish(
self.hass,
f"panasonic_heat_pump/commands/SetZ{self.zone_id}HeatRequestTemperature",
payload,
0,
False,
"utf-8",
)

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

@callback
def mode_received(message):
if message.payload == "0":
mode = ZoneClimateMode.COMPENSATION
elif message.payload == "1":
mode = ZoneClimateMode.DIRECT
else:
assert False, f"Mode received is not a known value"
if mode != self._mode:
self.change_mode(mode)

await mqtt.async_subscribe(
self.hass,
f"panasonic_heat_pump/main/Heating_mode",
mode_received,
1,
)

@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"panasonic_heat_pump/main/Z{self.zone_id}_Temp",
current_temperature_message_received,
1,
)

@callback
def target_temperature_message_received(message):
self._attr_target_temperature = float(message.payload)
self.async_write_ha_state()

await mqtt.async_subscribe(
self.hass,
f"panasonic_heat_pump/main/main/Z{self.zone_id}_Heat_Request_Temp",
target_temperature_message_received,
1,
)

def guess_hvac_mode() -> HVACMode:
global_heating = OperatingMode.HEAT in self._operating_mode
zone_heating = ZoneState.from_id(self.zone_id) in self._zone_state
if global_heating and zone_heating:
return HVACMode.HEAT
else:
return HVACMode.OFF

@callback
def heating_conf_message_received(message):
if message.topic == "panasonic_heat_pump/main/Zones_State":
self._zone_state = ZoneState.from_mqtt(message.payload)
elif message.topic == "panasonic_heat_pump/main/Operating_Mode_State":
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,
"panasonic_heat_pump/main/Zones_State",
heating_conf_message_received,
1,
)
await mqtt.async_subscribe(
self.hass,
"panasonic_heat_pump/main/Operating_Mode_State",
heating_conf_message_received,
1,
)

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
if hvac_mode == HVACMode.HEAT:
new_zone_state = self._zone_state | ZoneState.from_id(self.zone_id)
new_operating_mode = self._operating_mode | OperatingMode.HEAT
elif hvac_mode == HVACMode.OFF:
new_zone_state = self._zone_state & ~ZoneState.from_id(self.zone_id)
new_operating_mode = self._operating_mode
if new_zone_state == ZoneState(0):
new_operating_mode = self._operating_mode & ~OperatingMode.HEAT
else:
raise NotImplemented(
f"Mode {hvac_mode} has not been implemented by this entity"
)
if new_operating_mode != self._operating_mode:
_LOGGER.debug(
f"Setting operation mode {new_operating_mode} for zone {self.zone_id}"
)
await async_publish(
self.hass,
"panasonic_heat_pump/commands/SetOperationMode",
new_operating_mode.to_mqtt(),
0,
False,
"utf-8",
)
if new_zone_state not in [self._zone_state, ZoneState(0)]:
_LOGGER.debug(
f"Setting operation mode {new_zone_state} for zone {self.zone_id}"
)
await async_publish(
self.hass,
"panasonic_heat_pump/commands/SetZones",
new_zone_state.to_mqtt(),
0,
False,
"utf-8",
)
self._attr_hvac_mode = hvac_mode # let's be optimistic
self.async_write_ha_state()

Expand Down
Loading

0 comments on commit c1dc11b

Please sign in to comment.