Add climate platform to Trane Local integration (#163571)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
J. Nick Koston
2026-02-23 20:03:08 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent dd78da929e
commit d732e3d5ae
9 changed files with 666 additions and 10 deletions
+4 -2
View File
@@ -8,14 +8,16 @@ from steamloop import (
ThermostatConnection,
)
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_SECRET_KEY, DOMAIN, MANUFACTURER, PLATFORMS
from .const import CONF_SECRET_KEY, DOMAIN, MANUFACTURER
from .types import TraneConfigEntry
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: TraneConfigEntry) -> bool:
"""Set up Trane Local from a config entry."""
+200
View File
@@ -0,0 +1,200 @@
"""Climate platform for the Trane Local integration."""
from __future__ import annotations
from typing import Any
from steamloop import FanMode, HoldType, ThermostatConnection, ZoneMode
from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import TraneZoneEntity
from .types import TraneConfigEntry
PARALLEL_UPDATES = 0
HA_TO_ZONE_MODE = {
HVACMode.OFF: ZoneMode.OFF,
HVACMode.HEAT: ZoneMode.HEAT,
HVACMode.COOL: ZoneMode.COOL,
HVACMode.HEAT_COOL: ZoneMode.AUTO,
HVACMode.AUTO: ZoneMode.AUTO,
}
ZONE_MODE_TO_HA = {
ZoneMode.OFF: HVACMode.OFF,
ZoneMode.HEAT: HVACMode.HEAT,
ZoneMode.COOL: HVACMode.COOL,
ZoneMode.AUTO: HVACMode.AUTO,
}
HA_TO_FAN_MODE = {
"auto": FanMode.AUTO,
"on": FanMode.ALWAYS_ON,
"circulate": FanMode.CIRCULATE,
}
FAN_MODE_TO_HA = {v: k for k, v in HA_TO_FAN_MODE.items()}
SINGLE_SETPOINT_MODES = frozenset({ZoneMode.COOL, ZoneMode.HEAT})
async def async_setup_entry(
hass: HomeAssistant,
config_entry: TraneConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Trane Local climate entities."""
conn = config_entry.runtime_data
async_add_entities(
TraneClimateEntity(conn, config_entry.entry_id, zone_id)
for zone_id in conn.state.zones
)
class TraneClimateEntity(TraneZoneEntity, ClimateEntity):
"""Climate entity for a Trane thermostat zone."""
_attr_name = None
_attr_translation_key = "zone"
_attr_fan_modes = list(HA_TO_FAN_MODE)
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
_attr_target_temperature_step = 1.0
def __init__(self, conn: ThermostatConnection, entry_id: str, zone_id: str) -> None:
"""Initialize the climate entity."""
super().__init__(conn, entry_id, zone_id, "zone")
modes: list[HVACMode] = []
for zone_mode in conn.state.supported_modes:
ha_mode = ZONE_MODE_TO_HA.get(zone_mode)
if ha_mode is None:
continue
modes.append(ha_mode)
# AUTO in steamloop maps to both AUTO (schedule) and HEAT_COOL (manual hold)
if zone_mode == ZoneMode.AUTO:
modes.append(HVACMode.HEAT_COOL)
self._attr_hvac_modes = modes
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
# indoor_temperature is a string from the protocol (e.g. "72.00")
# or empty string if not yet received
if temp := self._zone.indoor_temperature:
return float(temp)
return None
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
# relative_humidity is a string from the protocol (e.g. "45")
# or empty string if not yet received
if humidity := self._conn.state.relative_humidity:
return int(humidity)
return None
@property
def hvac_mode(self) -> HVACMode:
"""Return the current HVAC mode."""
zone = self._zone
if zone.mode == ZoneMode.AUTO and zone.hold_type == HoldType.MANUAL:
return HVACMode.HEAT_COOL
return ZONE_MODE_TO_HA.get(zone.mode, HVACMode.OFF)
@property
def hvac_action(self) -> HVACAction:
"""Return the current HVAC action."""
# heating_active and cooling_active are system-level strings from the
# protocol ("0"=off, "1"=idle, "2"=running); filter by zone mode so
# a zone in COOL never reports HEATING and vice versa
zone_mode = self._zone.mode
if zone_mode == ZoneMode.OFF:
return HVACAction.OFF
state = self._conn.state
if zone_mode != ZoneMode.HEAT and state.cooling_active == "2":
return HVACAction.COOLING
if zone_mode != ZoneMode.COOL and state.heating_active == "2":
return HVACAction.HEATING
return HVACAction.IDLE
@property
def target_temperature(self) -> float | None:
"""Return target temperature for single-setpoint modes."""
# Setpoints are strings from the protocol or empty string if not yet received
zone = self._zone
if zone.mode == ZoneMode.COOL:
return float(zone.cool_setpoint) if zone.cool_setpoint else None
if zone.mode == ZoneMode.HEAT:
return float(zone.heat_setpoint) if zone.heat_setpoint else None
return None
@property
def target_temperature_high(self) -> float | None:
"""Return the upper bound target temperature."""
zone = self._zone
if zone.mode in SINGLE_SETPOINT_MODES:
return None
return float(zone.cool_setpoint) if zone.cool_setpoint else None
@property
def target_temperature_low(self) -> float | None:
"""Return the lower bound target temperature."""
zone = self._zone
if zone.mode in SINGLE_SETPOINT_MODES:
return None
return float(zone.heat_setpoint) if zone.heat_setpoint else None
@property
def fan_mode(self) -> str:
"""Return the current fan mode."""
return FAN_MODE_TO_HA.get(self._conn.state.fan_mode, "auto")
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
if hvac_mode == HVACMode.OFF:
self._conn.set_zone_mode(self._zone_id, ZoneMode.OFF)
return
hold_type = HoldType.SCHEDULE if hvac_mode == HVACMode.AUTO else HoldType.MANUAL
self._conn.set_temperature_setpoint(self._zone_id, hold_type=hold_type)
self._conn.set_zone_mode(self._zone_id, HA_TO_ZONE_MODE[hvac_mode])
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set target temperature."""
heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
set_temp = kwargs.get(ATTR_TEMPERATURE)
if set_temp is not None:
if self._zone.mode == ZoneMode.COOL:
cool_temp = set_temp
elif self._zone.mode == ZoneMode.HEAT:
heat_temp = set_temp
self._conn.set_temperature_setpoint(
self._zone_id,
heat_setpoint=str(round(heat_temp)) if heat_temp is not None else None,
cool_setpoint=str(round(cool_temp)) if cool_temp is not None else None,
)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
self._conn.set_fan_mode(HA_TO_FAN_MODE[fan_mode])
@@ -25,8 +25,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class TraneConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Trane Local."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
-4
View File
@@ -1,11 +1,7 @@
"""Constants for the Trane Local integration."""
from homeassistant.const import Platform
DOMAIN = "trane"
PLATFORMS = [Platform.SWITCH]
CONF_SECRET_KEY = "secret_key"
MANUFACTURER = "Trane"
@@ -25,6 +25,19 @@
}
},
"entity": {
"climate": {
"zone": {
"state_attributes": {
"fan_mode": {
"state": {
"auto": "[%key:common::state::auto%]",
"circulate": "Circulate",
"on": "[%key:common::state::on%]"
}
}
}
}
},
"switch": {
"hold": {
"name": "Hold"
+18 -2
View File
@@ -1,13 +1,14 @@
"""Fixtures for the Trane Local integration tests."""
from collections.abc import Generator
from collections.abc import AsyncGenerator, Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from steamloop import FanMode, HoldType, ThermostatState, Zone, ZoneMode
from homeassistant.components.trane import PLATFORMS
from homeassistant.components.trane.const import CONF_SECRET_KEY, DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -31,6 +32,19 @@ def mock_config_entry() -> MockConfigEntry:
)
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return PLATFORMS
@pytest.fixture(autouse=True)
async def mock_patch_platforms(platforms: list[Platform]) -> AsyncGenerator[None]:
"""Fixture to set up platforms for tests."""
with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", platforms):
yield
def _make_state() -> ThermostatState:
"""Create a mock thermostat state."""
return ThermostatState(
@@ -49,6 +63,8 @@ def _make_state() -> ThermostatState:
supported_modes=[ZoneMode.OFF, ZoneMode.AUTO, ZoneMode.COOL, ZoneMode.HEAT],
fan_mode=FanMode.AUTO,
relative_humidity="45",
heating_active="0",
cooling_active="0",
)
@@ -0,0 +1,89 @@
# serializer version: 1
# name: test_climate_entities[climate.living_room-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'fan_modes': list([
'auto',
'on',
'circulate',
]),
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 95,
'min_temp': 45,
'target_temp_step': 1.0,
}),
'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.living_room',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'trane',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <ClimateEntityFeature: 395>,
'translation_key': 'zone',
'unique_id': 'test_entry_id_1_zone',
'unit_of_measurement': None,
})
# ---
# name: test_climate_entities[climate.living_room-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_humidity': 45,
'current_temperature': 72,
'fan_mode': 'auto',
'fan_modes': list([
'auto',
'on',
'circulate',
]),
'friendly_name': 'Living Room',
'hvac_action': <HVACAction.IDLE: 'idle'>,
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.AUTO: 'auto'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
]),
'max_temp': 95,
'min_temp': 45,
'supported_features': <ClimateEntityFeature: 395>,
'target_temp_high': 76,
'target_temp_low': 68,
'target_temp_step': 1.0,
'temperature': None,
}),
'context': <ANY>,
'entity_id': 'climate.living_room',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'heat_cool',
})
# ---
+335
View File
@@ -0,0 +1,335 @@
"""Tests for the Trane Local climate platform."""
from unittest.mock import MagicMock
import pytest
from steamloop import FanMode, HoldType, ZoneMode
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HVAC_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_TEMPERATURE,
HVACAction,
HVACMode,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return [Platform.CLIMATE]
@pytest.fixture(autouse=True)
def set_us_customary(hass: HomeAssistant) -> None:
"""Set US customary unit system for Trane (Fahrenheit thermostats)."""
hass.config.units = US_CUSTOMARY_SYSTEM
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_climate_entities(
hass: HomeAssistant,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Snapshot all climate entities."""
await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id)
async def test_hvac_mode_auto(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test HVAC mode is AUTO when following schedule."""
mock_connection.state.zones["1"].hold_type = HoldType.SCHEDULE
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
assert state is not None
assert state.state == HVACMode.AUTO
async def test_current_temperature_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test current temperature is None when not yet received."""
mock_connection.state.zones["1"].indoor_temperature = ""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
assert state is not None
assert state.attributes["current_temperature"] is None
async def test_current_humidity_not_available(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test current humidity is omitted when not yet received."""
mock_connection.state.relative_humidity = ""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
assert state is not None
assert "current_humidity" not in state.attributes
async def test_set_hvac_mode_off(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting HVAC mode to off."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVACMode.OFF},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_not_called()
mock_connection.set_zone_mode.assert_called_once_with("1", ZoneMode.OFF)
@pytest.mark.parametrize(
("hvac_mode", "expected_hold", "expected_zone_mode"),
[
(HVACMode.AUTO, HoldType.SCHEDULE, ZoneMode.AUTO),
(HVACMode.HEAT_COOL, HoldType.MANUAL, ZoneMode.AUTO),
(HVACMode.HEAT, HoldType.MANUAL, ZoneMode.HEAT),
(HVACMode.COOL, HoldType.MANUAL, ZoneMode.COOL),
],
)
async def test_set_hvac_mode(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
hvac_mode: HVACMode,
expected_hold: HoldType,
expected_zone_mode: ZoneMode,
) -> None:
"""Test setting HVAC mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: hvac_mode},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1", hold_type=expected_hold
)
mock_connection.set_zone_mode.assert_called_once_with("1", expected_zone_mode)
async def test_set_temperature_range(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting temperature range in heat_cool mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.living_room",
ATTR_TARGET_TEMP_LOW: 65,
ATTR_TARGET_TEMP_HIGH: 78,
},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1",
heat_setpoint="65",
cool_setpoint="78",
)
async def test_set_temperature_single_heat(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting single temperature in heat mode."""
mock_connection.state.zones["1"].mode = ZoneMode.HEAT
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.living_room",
ATTR_TEMPERATURE: 70,
},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1",
heat_setpoint="70",
cool_setpoint=None,
)
async def test_set_temperature_single_cool(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test setting single temperature in cool mode."""
mock_connection.state.zones["1"].mode = ZoneMode.COOL
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: "climate.living_room",
ATTR_TEMPERATURE: 78,
},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1",
heat_setpoint=None,
cool_setpoint="78",
)
@pytest.mark.parametrize(
("fan_mode", "expected_fan_mode"),
[
("auto", FanMode.AUTO),
("on", FanMode.ALWAYS_ON),
("circulate", FanMode.CIRCULATE),
],
)
async def test_set_fan_mode(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
fan_mode: str,
expected_fan_mode: FanMode,
) -> None:
"""Test setting fan mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: fan_mode},
blocking=True,
)
mock_connection.set_fan_mode.assert_called_once_with(expected_fan_mode)
@pytest.mark.parametrize(
("cooling_active", "heating_active", "zone_mode", "expected_action"),
[
("0", "0", ZoneMode.OFF, HVACAction.OFF),
("0", "2", ZoneMode.AUTO, HVACAction.HEATING),
("2", "0", ZoneMode.AUTO, HVACAction.COOLING),
("0", "0", ZoneMode.AUTO, HVACAction.IDLE),
("0", "1", ZoneMode.AUTO, HVACAction.IDLE),
("1", "0", ZoneMode.AUTO, HVACAction.IDLE),
("0", "2", ZoneMode.COOL, HVACAction.IDLE),
("2", "0", ZoneMode.HEAT, HVACAction.IDLE),
("2", "2", ZoneMode.AUTO, HVACAction.COOLING),
],
)
async def test_hvac_action(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_connection: MagicMock,
cooling_active: str,
heating_active: str,
zone_mode: ZoneMode,
expected_action: HVACAction,
) -> None:
"""Test HVAC action reflects thermostat state."""
mock_connection.state.cooling_active = cooling_active
mock_connection.state.heating_active = heating_active
mock_connection.state.zones["1"].mode = zone_mode
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("climate.living_room")
assert state is not None
assert state.attributes["hvac_action"] == expected_action
async def test_turn_on(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test turn on defaults to heat_cool mode."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "climate.living_room"},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_called_once_with(
"1", hold_type=HoldType.MANUAL
)
mock_connection.set_zone_mode.assert_called_once_with("1", ZoneMode.AUTO)
async def test_turn_off(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_connection: MagicMock,
) -> None:
"""Test turn off sets mode to off."""
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "climate.living_room"},
blocking=True,
)
mock_connection.set_temperature_setpoint.assert_not_called()
mock_connection.set_zone_mode.assert_called_once_with("1", ZoneMode.OFF)
+7
View File
@@ -12,6 +12,7 @@ from homeassistant.const import (
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@@ -19,6 +20,12 @@ from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture
def platforms() -> list[Platform]:
"""Platforms, which should be loaded during the test."""
return [Platform.SWITCH]
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_switch_entities(
hass: HomeAssistant,