1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add climate platform for niko_home_control (#138087)

Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: VandeurenGlenn <8685280+VandeurenGlenn@users.noreply.github.com>
This commit is contained in:
Glenn Vandeuren (aka Iondependent)
2025-11-10 16:27:59 +01:00
committed by GitHub
parent 2240d6b94c
commit b5ae04605a
8 changed files with 384 additions and 2 deletions

View File

@@ -12,7 +12,12 @@ from homeassistant.helpers import entity_registry as er
from .const import _LOGGER
PLATFORMS: list[Platform] = [Platform.COVER, Platform.LIGHT, Platform.SCENE]
PLATFORMS: list[Platform] = [
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,
Platform.SCENE,
]
type NikoHomeControlConfigEntry = ConfigEntry[NHCController]

View File

@@ -0,0 +1,100 @@
"""Support for Niko Home Control thermostats."""
from typing import Any
from nhc.const import THERMOSTAT_MODES, THERMOSTAT_MODES_REVERSE
from nhc.thermostat import NHCThermostat
from homeassistant.components.climate import (
PRESET_ECO,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.components.sensor import UnitOfTemperature
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import NikoHomeControlConfigEntry
from .const import (
NIKO_HOME_CONTROL_THERMOSTAT_MODES_MAP,
NikoHomeControlThermostatModes,
)
from .entity import NikoHomeControlEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: NikoHomeControlConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Niko Home Control thermostat entry."""
controller = entry.runtime_data
async_add_entities(
NikoHomeControlClimate(thermostat, controller, entry.entry_id)
for thermostat in controller.thermostats.values()
)
class NikoHomeControlClimate(NikoHomeControlEntity, ClimateEntity):
"""Representation of a Niko Home Control thermostat."""
_attr_supported_features: ClimateEntityFeature = (
ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_name = None
_action: NHCThermostat
_attr_translation_key = "nhc_thermostat"
_attr_hvac_modes = [HVACMode.OFF, HVACMode.COOL, HVACMode.AUTO]
_attr_preset_modes = [
"day",
"night",
PRESET_ECO,
"prog1",
"prog2",
"prog3",
]
def _get_niko_mode(self, mode: str) -> int:
"""Return the Niko mode."""
return THERMOSTAT_MODES_REVERSE.get(mode, NikoHomeControlThermostatModes.OFF)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if ATTR_TEMPERATURE in kwargs:
await self._action.set_temperature(kwargs.get(ATTR_TEMPERATURE))
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
await self._action.set_mode(self._get_niko_mode(preset_mode))
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
await self._action.set_mode(NIKO_HOME_CONTROL_THERMOSTAT_MODES_MAP[hvac_mode])
async def async_turn_off(self) -> None:
"""Turn thermostat off."""
await self._action.set_mode(NikoHomeControlThermostatModes.OFF)
def update_state(self) -> None:
"""Update the state of the entity."""
if self._action.state == NikoHomeControlThermostatModes.OFF:
self._attr_hvac_mode = HVACMode.OFF
self._attr_preset_mode = None
elif self._action.state == NikoHomeControlThermostatModes.COOL:
self._attr_hvac_mode = HVACMode.COOL
self._attr_preset_mode = None
else:
self._attr_hvac_mode = HVACMode.AUTO
self._attr_preset_mode = THERMOSTAT_MODES[self._action.state]
self._attr_target_temperature = self._action.setpoint
self._attr_current_temperature = self._action.measured

View File

@@ -1,6 +1,23 @@
"""Constants for niko_home_control integration."""
from enum import IntEnum
import logging
from homeassistant.components.climate import HVACMode
DOMAIN = "niko_home_control"
_LOGGER = logging.getLogger(__name__)
NIKO_HOME_CONTROL_THERMOSTAT_MODES_MAP = {
HVACMode.OFF: 3,
HVACMode.COOL: 4,
HVACMode.AUTO: 5,
}
class NikoHomeControlThermostatModes(IntEnum):
"""Enum for Niko Home Control thermostat modes."""
OFF = 3
COOL = 4
AUTO = 5

View File

@@ -0,0 +1,20 @@
{
"entity": {
"climate": {
"nhc_thermostat": {
"state_attributes": {
"preset_mode": {
"default": "mdi:calendar-clock",
"state": {
"day": "mdi:weather-sunny",
"night": "mdi:weather-night",
"prog1": "mdi:numeric-1",
"prog2": "mdi:numeric-2",
"prog3": "mdi:numeric-3"
}
}
}
}
}
}
}

View File

@@ -26,5 +26,23 @@
"description": "Set up your Niko Home Control instance."
}
}
},
"entity": {
"climate": {
"nhc_thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"day": "Day",
"eco": "Eco",
"night": "Night",
"prog1": "Program 1",
"prog2": "Program 2",
"prog3": "Program 3"
}
}
}
}
}
}
}

View File

@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, patch
from nhc.cover import NHCCover
from nhc.light import NHCLight
from nhc.scene import NHCScene
from nhc.thermostat import NHCThermostat
import pytest
from homeassistant.components.niko_home_control.const import DOMAIN
@@ -62,6 +63,22 @@ def cover() -> NHCCover:
return mock
@pytest.fixture
def climate() -> NHCThermostat:
"""Return a thermostat mock."""
mock = AsyncMock(spec=NHCThermostat)
mock.id = 5
mock.name = "thermostat"
mock.suggested_area = "room"
mock.state = 0
mock.measured = 180
mock.setpoint = 200
mock.overrule = 0
mock.overruletime = 0
mock.ecosave = 0
return mock
@pytest.fixture
def scene() -> NHCScene:
"""Return a scene mock."""
@@ -76,7 +93,11 @@ def scene() -> NHCScene:
@pytest.fixture
def mock_niko_home_control_connection(
light: NHCLight, dimmable_light: NHCLight, cover: NHCCover, scene: NHCScene
light: NHCLight,
dimmable_light: NHCLight,
cover: NHCCover,
climate: NHCThermostat,
scene: NHCScene,
) -> Generator[AsyncMock]:
"""Mock a NHC client."""
with (
@@ -92,6 +113,7 @@ def mock_niko_home_control_connection(
client = mock_client.return_value
client.lights = [light, dimmable_light]
client.covers = [cover]
client.thermostats = {"thermostat-5": climate}
client.scenes = [scene]
client.connect = AsyncMock(return_value=True)
yield client

View File

@@ -0,0 +1,84 @@
# serializer version: 1
# name: test_entities[climate.thermostat-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_modes': list([
'day',
'night',
'eco',
'prog1',
'prog2',
'prog3',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.thermostat',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'niko_home_control',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 145>,
'translation_key': 'nhc_thermostat',
'unique_id': '01JFN93M7KRA38V5AMPCJ2JYYV-5',
'unit_of_measurement': None,
})
# ---
# name: test_entities[climate.thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 180,
'friendly_name': 'thermostat',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.AUTO: 'auto'>,
]),
'max_temp': 35,
'min_temp': 7,
'preset_mode': 'day',
'preset_modes': list([
'day',
'night',
'eco',
'prog1',
'prog2',
'prog3',
]),
'supported_features': <ClimateEntityFeature: 145>,
'temperature': 200,
}),
'context': <ANY>,
'entity_id': 'climate.thermostat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'auto',
})
# ---

View File

@@ -0,0 +1,116 @@
"""Tests for the Niko Home Control Climate platform."""
from typing import Any
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import ATTR_HVAC_MODE, ATTR_PRESET_MODE, HVACMode
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import find_update_callback, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch(
"homeassistant.components.niko_home_control.PLATFORMS", [Platform.CLIMATE]
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("service", "service_parameters", "api_method", "api_parameters"),
[
("set_temperature", {"temperature": 25}, "set_temperature", (25,)),
("set_preset_mode", {ATTR_PRESET_MODE: "eco"}, "set_mode", (2,)),
("set_hvac_mode", {ATTR_HVAC_MODE: HVACMode.COOL}, "set_mode", (4,)),
("set_hvac_mode", {ATTR_HVAC_MODE: HVACMode.OFF}, "set_mode", (3,)),
("set_hvac_mode", {ATTR_HVAC_MODE: HVACMode.AUTO}, "set_mode", (5,)),
],
)
async def test_set(
hass: HomeAssistant,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
climate: AsyncMock,
service: str,
service_parameters: dict[str, Any],
api_method: str,
api_parameters: tuple[Any, ...],
) -> None:
"""Test setting a value on the climate entity."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
"climate",
service,
{ATTR_ENTITY_ID: "climate.thermostat"} | service_parameters,
blocking=True,
)
getattr(
mock_niko_home_control_connection.thermostats["thermostat-5"],
api_method,
).assert_called_once_with(*api_parameters)
async def test_updating(
hass: HomeAssistant,
mock_niko_home_control_connection: AsyncMock,
mock_config_entry: MockConfigEntry,
climate: AsyncMock,
) -> None:
"""Test updating the thermostat."""
await setup_integration(hass, mock_config_entry)
climate.state = 0
await find_update_callback(mock_niko_home_control_connection, 5)(0)
assert hass.states.get("climate.thermostat").attributes.get("preset_mode") == "day"
assert hass.states.get("climate.thermostat").state == "auto"
climate.state = 1
await find_update_callback(mock_niko_home_control_connection, 5)(1)
assert (
hass.states.get("climate.thermostat").attributes.get("preset_mode") == "night"
)
assert hass.states.get("climate.thermostat").state == "auto"
climate.state = 2
await find_update_callback(mock_niko_home_control_connection, 5)(2)
assert hass.states.get("climate.thermostat").state == "auto"
assert hass.states.get("climate.thermostat").attributes["preset_mode"] == "eco"
climate.state = 3
await find_update_callback(mock_niko_home_control_connection, 5)(3)
assert hass.states.get("climate.thermostat").state == "off"
climate.state = 4
await find_update_callback(mock_niko_home_control_connection, 5)(4)
assert hass.states.get("climate.thermostat").state == "cool"
climate.state = 5
await find_update_callback(mock_niko_home_control_connection, 5)(5)
assert hass.states.get("climate.thermostat").state == "auto"
assert hass.states.get("climate.thermostat").attributes["preset_mode"] == "prog1"
climate.state = 6
await find_update_callback(mock_niko_home_control_connection, 5)(6)
assert hass.states.get("climate.thermostat").state == "auto"
assert hass.states.get("climate.thermostat").attributes["preset_mode"] == "prog2"
climate.state = 7
await find_update_callback(mock_niko_home_control_connection, 5)(7)
assert hass.states.get("climate.thermostat").state == "auto"
assert hass.states.get("climate.thermostat").attributes["preset_mode"] == "prog3"