Skip to content
Open
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This has been tested using on :
- `Atlantic Naema 2 Duo 25` gas boiler using a `Ǹavilink Radio-Connect 128` thermostat
- `Atlantic Naia 2 Micro 25` gas boiler using a `Ǹavilink Radio-Connect 128` thermostat
- `Atlantic Loria Duo 6006 R32` heat pump using a `Navilink Radio-Connect 128` thermostat
- `Atlantic Alfea Excellia Duo` heat pump using a `Navilink Radio-Connect 228` thermostat
- `Takao M3` air conditionning
- `Kelud 1750W` towel rack
- `Sauter Asama Connecté II Ventilo 1750W` towel rack
Expand All @@ -25,7 +26,7 @@ More informations about HACS [here](https://hacs.xyz/).

#### Manually

Clone this repository and copy `custom_components/cozytouch` to your Home Assistant config durectory (ex : `config/custom_components/cozytouch`)
Clone this repository and copy `custom_components/cozytouch` to your Home Assistant config directory (ex : `config/custom_components/cozytouch`)

Restart Home Assistant.

Expand Down
77 changes: 65 additions & 12 deletions custom_components/cozytouch/capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
if (
capabilityId in (1, 2, 7, 8)
and capabilityId in modelInfos["HVACModesCapabilityId"]
and float(capabilityValue) < 3000
):
# Default Ids
capability["targetCapabilityId"] = 40
Expand Down Expand Up @@ -68,9 +69,21 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability.pop("lowestValueCapabilityId")
capability.pop("highestValueCapabilityId")
capability["icon"] = "mdi:heat-pump"
elif modelInfos["type"] == CozytouchDeviceType.THERMOSTAT:
capability["name"] = "heat"
capability["progOverrideCapabilityId"] = 157
capability["progOverrideTotalTimeCapabilityId"] = 158
capability["progOverrideTimeCapabilityId"] = 159
capability["lowestCoolValueCapabilityId"] = 162
capability["highestCoolValueCapabilityId"] = 163
capability["icon"] = "mdi:thermostat"
else:
capability["name"] = "heat"

# Add effective target temperature for model 557
if modelId == 557:
capability["setpointTemperatureId"] = 17

capability["type"] = "climate"
capability["category"] = "sensor"

Expand Down Expand Up @@ -147,21 +160,21 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["lowestValueCapabilityId"] = 160
capability["highestValueCapabilityId"] = 161

elif capabilityId == 44:
elif capabilityId == 44 and modelId not in (1693,):
capability["name"] = "ch_power_consumption"
capability["type"] = "energy"
capability["displayed_unit_of_measurement"] = UnitOfEnergy.KILO_WATT_HOUR
capability["category"] = "sensor"
capability["icon"] = "mdi:radiator"

elif capabilityId == 45:
elif capabilityId == 45 and modelId not in (1693,):
capability["name"] = "dhw_power_consumption"
capability["type"] = "energy"
capability["displayed_unit_of_measurement"] = UnitOfEnergy.KILO_WATT_HOUR
capability["category"] = "sensor"
capability["icon"] = "mdi:faucet"

elif capabilityId == 46:
elif capabilityId == 46 and modelId not in (1693,):
capability["name"] = "total_power_consumption"
capability["type"] = "energy"
capability["displayed_unit_of_measurement"] = UnitOfEnergy.KILO_WATT_HOUR
Expand All @@ -174,19 +187,32 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["displayed_unit_of_measurement"] = UnitOfEnergy.KILO_WATT_HOUR
capability["category"] = "sensor"

elif capabilityId == 60:
capability["name"] = "total_power_consumption"
capability["type"] = "energy"
capability["displayed_unit_of_measurement"] = UnitOfEnergy.KILO_WATT_HOUR
capability["category"] = "sensor"

elif capabilityId == 86:
capability["name"] = "domestic_hot_water"
capability["type"] = "switch"
capability["category"] = "sensor"
capability["icon"] = "mdi:faucet"

elif capabilityId == 87:
elif capabilityId == 87 and modelId not in (1376,):
capability["name"] = "heating_mode"
capability["type"] = "select"
capability["category"] = "sensor"
capability["icon"] = "mdi:water-boiler"
capability["modelList"] = "HeatingModes"

elif capabilityId == 87 and modelId in (1376,):
capability["name"] = "heating_mode"
capability["type"] = "string"
capability["category"] = "sensor"
capability["icon"] = "mdi:water-boiler"
capability["modelList"] = "HeatingModes"

elif capabilityId == 88:
capability["name"] = "model_name"
capability["type"] = "string"
Expand Down Expand Up @@ -302,9 +328,11 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["category"] = "diag"
capability["icon"] = "mdi:home-floor-2"

# elif capabilityId == 157:
# # Prog override flag
# return {}
elif capabilityId == 157:
capability["name"] = "override"
capability["type"] = "switch"
capability["category"] = "sensor"
capability["icon"] = "mdi:gesture-tap"

elif capabilityId == 158:
if modelInfos["type"] == CozytouchDeviceType.TOWEL_RACK:
Expand Down Expand Up @@ -345,6 +373,18 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["highest_value"] = 28
capability["step"] = 0.5

elif capabilityId == 162:
capability["name"] = "temperature_min"
capability["type"] = "temperature"
capability["category"] = "sensor"
capability["icon"] = "mdi:thermometer-chevron-down"

elif capabilityId == 163:
capability["name"] = "temperature_max"
capability["type"] = "temperature"
capability["category"] = "sensor"
capability["icon"] = "mdi:thermometer-chevron-up"

elif capabilityId == 165:
capability["name"] = "boost_mode"
capability["type"] = "switch"
Expand All @@ -361,15 +401,21 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["category"] = "diag"
capability["icon"] = "mdi:radio-tower"

elif capabilityId == 172:
elif capabilityId in (17, 172):
capability["name"] = "away_mode_temperature"
capability["type"] = "temperature_adjustment_number"
capability["category"] = "sensor"
capability["lowestValueCapabilityId"] = 160
capability["highestValueCapabilityId"] = 161
if modelId in (557,):
capability["name"] = "temperature_setpoint"
capability["type"] = "temperature"
else:
capability["name"] = "away_mode_temperature"
capability["type"] = "temperature_adjustment_number"
capability["lowestValueCapabilityId"] = 160
capability["highestValueCapabilityId"] = 161

elif capabilityId == 177:
if modelInfos["type"] == CozytouchDeviceType.GAZ_BOILER:
if modelInfos["type"] == CozytouchDeviceType.GAZ_BOILER or modelId in (557,):
return {}

capability["name"] = "target_cool_temperature"
Expand Down Expand Up @@ -470,7 +516,7 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["category"] = "diag"
capability["icon"] = "mdi:wifi"

elif capabilityId in (222, 226):
elif capabilityId in (222, 226) and modelId not in (1376,):
capability["name"] = "away_mode"
capability["name_0"] = "away_mode_start"
capability["name_1"] = "away_mode_stop"
Expand Down Expand Up @@ -740,6 +786,13 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s
capability["lowest_value"] = 5
capability["highest_value"] = 60
capability["step"] = 5

elif capabilityId == 105636:
capability["name"] = "heating_mode"
capability["type"] = "select"
capability["category"] = "sensor"
capability["icon"] = "mdi:water-boiler"
capability["modelList"] = "HeatingModes"

elif capabilityId == 105906:
capability["name"] = "Target 105906"
Expand Down
100 changes: 98 additions & 2 deletions custom_components/cozytouch/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

from __future__ import annotations

import asyncio
import logging

from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
HVACAction,
)
from homeassistant.components.climate.const import (
PRESET_ACTIVITY,
Expand All @@ -33,6 +35,9 @@
PRESET_PROG = "prog"
PRESET_OVERRIDE = "override"

PRESET_TRANSITION_TIMEOUT = 15.0
PRESET_TRANSITION_POLL_INTERVAL = 0.5


# config flow setup
async def async_setup_entry(
Expand Down Expand Up @@ -97,6 +102,7 @@ def __init__(

self._native_value = 0
self._current_value = None
self._current_boiler_temperature_value = None
self._attr_native_step = 0.5
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_min_temp = 0
Expand Down Expand Up @@ -181,6 +187,29 @@ def _configure_presets(self):
if PRESET_NONE not in self._attr_preset_modes :
self._attr_preset_mode = PRESET_BASIC

async def _wait_for_capability_value(
self,
capability_id,
expected_value,
*,
timeout: float = PRESET_TRANSITION_TIMEOUT,
interval: float = PRESET_TRANSITION_POLL_INTERVAL,
) -> bool:
"""Wait until a capability reaches the expected value."""
expected = str(expected_value)
deadline = asyncio.get_running_loop().time() + timeout

while True:
current_value = self.coordinator.get_capability_value(capability_id)
if current_value is not None and str(current_value) == expected:
return True

if asyncio.get_running_loop().time() >= deadline:
return False

await asyncio.sleep(interval)
await self.coordinator.async_request_refresh()

@callback
def _handle_coordinator_update(self) -> None:
"""Update the values from the hub."""
Expand Down Expand Up @@ -224,6 +253,13 @@ def _handle_coordinator_update(self) -> None:
self.coordinator.get_capability_value(currentValueId)
)

# Current boiler water temperature value
currentBoilerTemperatureValueId = 109
if currentBoilerTemperatureValueId:
self._current_boiler_temperature_value = float(
self.coordinator.get_capability_value(currentBoilerTemperatureValueId)
)

# Lowest adjustment value
if (
self._attr_hvac_mode in (
Expand Down Expand Up @@ -363,7 +399,46 @@ def current_temperature(self):
def target_temperature(self):
"""Return target temperature."""
return self._native_value


@property
def extra_state_attributes(self):
"""Return the computed target temperature."""
if "setpointTemperatureId" in self._capability:
effective_temp = self.coordinator.get_capability_value(
self._capability["setpointTemperatureId"]
)
if effective_temp is not None:
return {
"temperature_setpoint": float(effective_temp),
}
return None

@property
def hvac_action(self):
"""Return the current HVAC action."""
if self._attr_hvac_mode == HVACMode.OFF:
return HVACAction.OFF

# Get effective target temperature
setpoint_temp = None
if "setpointTemperatureId" in self._capability:
setpoint_temp_value = self.coordinator.get_capability_value(
self._capability["setpointTemperatureId"]
)
if setpoint_temp_value is not None:
setpoint_temp = float(setpoint_temp_value)

# If no setpoint temp, fall back to target temperature
if setpoint_temp is None:
setpoint_temp = self._native_value

# Determine action based on current vs target temperature
if self._current_boiler_temperature_value is not None and setpoint_temp is not None:
if self._current_boiler_temperature_value < setpoint_temp:
return HVACAction.HEATING

return HVACAction.IDLE

async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get("temperature")
Expand Down Expand Up @@ -489,9 +564,20 @@ async def async_set_preset_mode(self, preset_mode):
if preset_mode == PRESET_BASIC:
await self.coordinator.set_capability_value(progCapabilityId, "0")

elif preset_mode == PRESET_PROG:
elif preset_mode in (PRESET_PROG, PRESET_OVERRIDE):
await self.coordinator.set_capability_value(progCapabilityId, "1")

if preset_mode == PRESET_OVERRIDE:
await self.coordinator.async_request_refresh()
prog_ok = await self._wait_for_capability_value(
progCapabilityId,
"1",
)
if not prog_ok:
_LOGGER.warning(
"Timeout while waiting for prog mode before applying override"
)

if progOverrideCapabilityId:
progOverrideTimeCapabilityId = self._capability.get(
"progOverrideTimeCapabilityId", None
Expand All @@ -516,6 +602,16 @@ async def async_set_preset_mode(self, preset_mode):
progOverrideCapabilityId, "1"
)

await self.coordinator.async_request_refresh()
override_ok = await self._wait_for_capability_value(
progOverrideCapabilityId,
"1",
)
if not override_ok:
_LOGGER.warning(
"Timeout while waiting for override mode to be applied"
)

else:
if progOverrideTimeCapabilityId:
await self.coordinator.set_capability_value(
Expand Down
2 changes: 2 additions & 0 deletions custom_components/cozytouch/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ class CozytouchCapabilityVariableType(IntEnum):
HEATING_MODE_OFF = "off"
HEATING_MODE_MANUAL = "manual"
HEATING_MODE_ECO_PLUS = "eco_plus"
HEATING_MODE_ECO = "eco"
HEATING_MODE_PROG = "prog"
HEATING_MODE_COMFORT = "comfort"
17 changes: 14 additions & 3 deletions custom_components/cozytouch/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,20 @@
"state_attributes": {
"preset_mode": {
"state": {
"basic": "mdi:thermostat",
"prog": "mdi:thermostat-auto",
"override": "mdi:thermostat-cog"
"basic": "mdi:thermometer",
"prog": "mdi:clock-outline",
"override": "mdi:timer-sand-complete"
}
}
}
},
"heat_pump_z1": {
"state_attributes": {
"preset_mode": {
"state": {
"basic": "mdi:thermometer",
"prog": "mdi:clock-outline",
"override": "mdi:timer-sand-complete"
}
}
}
Expand Down
Loading