Skip to content
1 change: 1 addition & 0 deletions homeassistant/components/eurotronic_cometblue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
]

Expand Down
11 changes: 11 additions & 0 deletions homeassistant/components/eurotronic_cometblue/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
"sync_time": {
"default": "mdi:calendar-clock"
}
},
"number": {
"comfort_setpoint": {
"default": "mdi:thermometer-chevron-up"
},
"eco_setpoint": {
"default": "mdi:thermometer-chevron-down"
},
"offset": {
"default": "mdi:thermometer-check"
}
}
}
}
136 changes: 136 additions & 0 deletions homeassistant/components/eurotronic_cometblue/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Comet Blue number integration."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from eurotronic_cometblue_ha import AsyncCometBlue

from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import PRECISION_HALVES, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .climate import MAX_TEMP, MIN_TEMP
from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
from .entity import CometBlueBluetoothEntity

PARALLEL_UPDATES = 1


@dataclass(frozen=True, kw_only=True)
class CometBlueRequiredKeysMixin:
"""Mixin for required keys."""

cometblue_key: str
set_fn: Callable[[AsyncCometBlue], Any]


@dataclass(frozen=True, kw_only=True)
class CometBlueNumberEntityDescription(
NumberEntityDescription, CometBlueRequiredKeysMixin
):
"""Describes a Comet Blue number entity."""


DESCRIPTIONS = [
CometBlueNumberEntityDescription(
key="offset",
cometblue_key="tempOffset",
translation_key="offset",
device_class=NumberDeviceClass.TEMPERATURE,
Comment thread
rikroe marked this conversation as resolved.
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
set_fn=lambda x: x.set_temperature_async,
native_min_value=-5.0,
native_max_value=5.0,
native_step=PRECISION_HALVES,
entity_registry_enabled_default=False,
),
Comment thread
rikroe marked this conversation as resolved.
CometBlueNumberEntityDescription(
key="eco_setpoint",
cometblue_key="targetTempLow",
translation_key="eco_setpoint",
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
set_fn=lambda x: x.set_temperature_async,
native_min_value=MIN_TEMP,
native_max_value=MAX_TEMP,
native_step=PRECISION_HALVES,
entity_registry_enabled_default=True,
),
CometBlueNumberEntityDescription(
key="comfort_setpoint",
cometblue_key="targetTempHigh",
translation_key="comfort_setpoint",
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
set_fn=lambda x: x.set_temperature_async,
native_min_value=MIN_TEMP,
native_max_value=MAX_TEMP,
native_step=PRECISION_HALVES,
entity_registry_enabled_default=True,
),
Comment on lines +56 to +81
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this part of the climate entity?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its part of the climate entity (as attribute). But it is not possible to set the values, as only the one target setpoint can be changed.

To adjust this, we need the number entity.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you set the hvac mode to heat cool you can set that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the device doesn't support cooling (only not heating). And hvacmode heat is mapped to a boost temperature/fully open valve because that is the only setting where the device doesn't regulate itself.

As the attributes in the climate platform dont really do anything, should they be removed? If so, in this or a separate PR (I guess the latter)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, interesting, I'd assume that the range would still work as there's a supported feature for it, so I think it's set correctly, it's now more how the frontend can update it. Is this something we should take to some frontenders? We could consider splitting it off from this PR so we don't have to deprecate it if we find a solution and this has been released already

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, based on your persistent feedback I double checked the actual service call (climate.set_temperature). If used on one of the CometBlue devices, both current target as well as high and low temperatures can be set - didn't realize that.

In this case, yes, I'll remove it from here (and open another PR to get it into the climate component).

I'll ask in the frontend discord if I'm missing something.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I know heat_cool has that double slider setup, but I'd expect that to happen to other ones as well when temperature_range is set as supported

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remembered why I implemented it like this. Its either possible to set the target temperature OR the high/low temperatures via UI, depending on the capabilities.

If high/low is set via UI, then the current target temperature cannot be set/overriden, as high/low only apply with the next schedule change. Therefore I added the number entities (same as the on-device schedule which would need an entity action to be set).

As target high/low temperatures can still be set via the device action (or rather, can be set after an additional, small PR), I'm indifferent if these should be added as separate number/configuration entity.

Please just let me know what you prefer.

More details:

If a device supports BOTH ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, then it displays like the following (only able to set the single target temperature).
image

If a device supports ONLY ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, then only high and low temperatures can be set via the UI.

image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked at other climate platforms that support ClimateEntityFeature.TARGET_TEMPERATURE_RANGE. Most of them have it guarded behind HVACMode.HEAT_COOL.

So probably the Eurotronic CometBlue climate platform should not expose it (i.e. set only ClimateEntityFeature.TARGET_TEMPERATURE), and keep it as separate number platform for config?

Copy link
Copy Markdown
Contributor Author

@rikroe rikroe Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With #169182, we should keep the number entities here.

Also renamed the temperatures to setpoints to make it clearer.

]


async def async_setup_entry(
hass: HomeAssistant,
entry: CometBlueConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the client entities."""

coordinator = entry.runtime_data
entities: list[CometBlueNumberEntity] = [
CometBlueNumberEntity(coordinator, description) for description in DESCRIPTIONS
]

async_add_entities(entities)


class CometBlueNumberEntity(CometBlueBluetoothEntity, NumberEntity):
"""Representation of a number."""

entity_description: CometBlueNumberEntityDescription

def __init__(
self,
coordinator: CometBlueDataUpdateCoordinator,
description: CometBlueNumberEntityDescription,
) -> None:
"""Initialize CometBlueNumberEntity."""

super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.address}-{description.key}"

@property
def native_value(self) -> float | None:
"""Return the entity value to represent the entity state."""
return self.coordinator.data.temperatures.get(
self.entity_description.cometblue_key
)

async def async_set_native_value(self, value: float) -> None:
"""Update to the device."""

await self.coordinator.send_command(
self.entity_description.set_fn(self.coordinator.device),
{
"values": {
# manual temperature always needs to be set, otherwise TRV will turn OFF
"manualTemp": self.coordinator.data.temperatures["manualTemp"],
self.entity_description.cometblue_key: value,
}
Comment thread
rikroe marked this conversation as resolved.
},
Comment thread
rikroe marked this conversation as resolved.
)
Comment thread
rikroe marked this conversation as resolved.
Comment thread
rikroe marked this conversation as resolved.
await self.coordinator.async_request_refresh()
11 changes: 11 additions & 0 deletions homeassistant/components/eurotronic_cometblue/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@
"sync_time": {
"name": "Sync time"
}
},
"number": {
"comfort_setpoint": {
"name": "Comfort setpoint"
},
"eco_setpoint": {
"name": "Eco setpoint"
},
"offset": {
"name": "Setpoint offset"
}
}
}
}
184 changes: 184 additions & 0 deletions tests/components/eurotronic_cometblue/snapshots/test_number.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# serializer version: 1
# name: test_number_state[number.comet_blue_aa_bb_cc_dd_ee_ff_comfort_setpoint-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'max': 28.5,
'min': 7.5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.comet_blue_aa_bb_cc_dd_ee_ff_comfort_setpoint',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Comfort setpoint',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Comfort setpoint',
'platform': 'eurotronic_cometblue',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'comfort_setpoint',
'unique_id': 'aa:bb:cc:dd:ee:ff-comfort_setpoint',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_number_state[number.comet_blue_aa_bb_cc_dd_ee_ff_comfort_setpoint-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Comet Blue aa:bb:cc:dd:ee:ff Comfort setpoint',
'max': 28.5,
'min': 7.5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.comet_blue_aa_bb_cc_dd_ee_ff_comfort_setpoint',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '21.0',
})
# ---
# name: test_number_state[number.comet_blue_aa_bb_cc_dd_ee_ff_eco_setpoint-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'max': 28.5,
'min': 7.5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.comet_blue_aa_bb_cc_dd_ee_ff_eco_setpoint',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Eco setpoint',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Eco setpoint',
'platform': 'eurotronic_cometblue',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'eco_setpoint',
'unique_id': 'aa:bb:cc:dd:ee:ff-eco_setpoint',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_number_state[number.comet_blue_aa_bb_cc_dd_ee_ff_eco_setpoint-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Comet Blue aa:bb:cc:dd:ee:ff Eco setpoint',
'max': 28.5,
'min': 7.5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.comet_blue_aa_bb_cc_dd_ee_ff_eco_setpoint',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '17.0',
})
# ---
# name: test_number_state[number.comet_blue_aa_bb_cc_dd_ee_ff_setpoint_offset-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'max': 5.0,
'min': -5.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.comet_blue_aa_bb_cc_dd_ee_ff_setpoint_offset',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Setpoint offset',
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Setpoint offset',
'platform': 'eurotronic_cometblue',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'offset',
'unique_id': 'aa:bb:cc:dd:ee:ff-offset',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_number_state[number.comet_blue_aa_bb_cc_dd_ee_ff_setpoint_offset-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Comet Blue aa:bb:cc:dd:ee:ff Setpoint offset',
'max': 5.0,
'min': -5.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.comet_blue_aa_bb_cc_dd_ee_ff_setpoint_offset',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
Loading