1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-03 12:46:09 +01:00
Files

411 lines
15 KiB
Python

"""Fixtures for the nexia integration tests."""
from dataclasses import dataclass
from unittest.mock import NonCallableMock, patch
from nexia.automation import NexiaAutomation
from nexia.home import NexiaHome
from nexia.sensor import NexiaSensor
from nexia.thermostat import NexiaThermostat
from nexia.zone import NexiaThermostatZone
import pytest
from homeassistant.components.nexia.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
def create_mock_sensor(
sensor_id: int,
name: str,
weight: float = 1.0,
temperature: int = 72,
humidity: int = 45,
) -> NonCallableMock[NexiaSensor]:
"""Create a mock NexiaSensor."""
sensor = NonCallableMock(NexiaSensor)
sensor.id = sensor_id
sensor.name = name
sensor.weight = weight
sensor.temperature = temperature
sensor.humidity = humidity
sensor.has_online = False
sensor.has_battery = False
return sensor
def create_mock_zone(
zone_id: int,
name: str,
thermostat: NexiaThermostat | None = None,
*,
cooling_setpoint: int = 79,
current_mode: str = "AUTO",
heating_setpoint: int = 63,
is_calling: bool = False,
is_in_permanent_hold: bool = True,
preset: str = "None",
presets: list[str] | None = None,
requested_mode: str = "AUTO",
sensors: list[NexiaSensor] | None = None,
setpoint_status: str = "Permanent Hold",
status: str = "Idle",
temperature: int = 72,
) -> NonCallableMock[NexiaThermostatZone]:
"""Create a mock NexiaThermostatZone."""
zone = NonCallableMock(NexiaThermostatZone)
zone.zone_id = zone_id
zone.get_name.return_value = name
zone.thermostat = thermostat
zone.get_active_sensor_ids.return_value = set()
zone.get_cooling_setpoint.return_value = cooling_setpoint
zone.get_current_mode.return_value = current_mode
zone.get_heating_setpoint.return_value = heating_setpoint
zone.get_preset.return_value = preset
zone.get_presets.return_value = presets or ["None", "Home", "Away", "Sleep"]
zone.get_requested_mode.return_value = requested_mode
zone.get_sensors.return_value = sensors or []
zone.get_setpoint_status.return_value = setpoint_status
zone.get_status.return_value = status
zone.get_temperature.return_value = temperature
zone.is_calling.return_value = is_calling
zone.is_in_permanent_hold.return_value = is_in_permanent_hold
zone.is_native_zone.return_value = True
zone.load_current_sensor_state.return_value = True
zone.select_room_iq_sensors.return_value = True
return zone
@dataclass
class _ThermostatStateData:
"""Mutable class so thermostat getters and setters can share state."""
air_cleaner_mode: str
dehumidify_setpoint: float
emergency_heat_on: bool
fan_mode: str
fan_setpoint: float
humidify_setpoint: float
async def async_set_air_cleaner_mode(self, mode: str) -> None:
"""Set the air cleaner mode asynchronously."""
self.air_cleaner_mode = mode
async def async_set_dehumidify_setpoint(self, setpoint: float) -> None:
"""Set dehumidify setpoint asynchronously."""
self.dehumidify_setpoint = setpoint
async def async_set_emergency_heat_on(self, on: bool) -> None:
"""Set the emergency heat on asynchronously."""
self.emergency_heat_on = on
async def async_set_fan_mode(self, mode: str) -> None:
"""Set the fan mode asynchronously."""
self.fan_mode = mode
async def async_set_fan_setpoint(self, setpoint: float) -> None:
"""Set the fan setpoint asynchronously."""
self.fan_setpoint = setpoint
async def async_set_humidify_setpoint(self, setpoint: float) -> None:
"""Set humidify setpoint asynchronously."""
self.humidify_setpoint = setpoint
def create_mock_thermostat(
thermostat_id: int,
name: str,
zones: list[NexiaThermostatZone] | None = None,
*,
air_cleaner_mode: str = "auto",
current_compressor_speed: float = 0.0,
deadband: int = 3,
dehumidify_setpoint: float = 0.50,
dehumidify_setpoint_limits: tuple[float, float] = (0.35, 0.65),
fan_mode: str = "Auto",
fan_modes: list[str] | None = None,
fan_speed_setpoint: float = 0.35,
firmware: str = "5.9.1",
has_air_cleaner: bool = False,
has_dehumidify_support: bool = False,
has_emergency_heat: bool = False,
has_humidify_support: bool = False,
has_outdoor_temperature: bool = False,
has_relative_humidity: bool = False,
has_variable_fan_speed: bool = False,
has_variable_speed_compressor: bool = False,
has_zones: bool = False,
humidify_setpoint: float = 0.36,
humidify_setpoint_limits: tuple[float, float] = (0.10, 0.45),
is_blower_active: bool = False,
is_emergency_heat_active: bool = False,
is_online: bool = True,
model: str = "XL1050",
outdoor_temperature: float | None = 30.0,
relative_humidity: float | None = 0.52,
requested_compressor_speed: float = 0.0,
setpoint_limits: tuple[int, int] = (55, 99),
system_status: str = "Idle",
unit: str = "F",
variable_fan_speed_limits: tuple[float, float] = (0.35, 1.0),
) -> NonCallableMock[NexiaThermostat]:
"""Create a mock NexiaThermostat."""
# Mutable class so get_... and set_... share state
state_data = _ThermostatStateData(
air_cleaner_mode=air_cleaner_mode,
dehumidify_setpoint=dehumidify_setpoint,
emergency_heat_on=is_emergency_heat_active,
fan_mode=fan_mode,
fan_setpoint=fan_speed_setpoint,
humidify_setpoint=humidify_setpoint,
)
thermostat = NonCallableMock(NexiaThermostat)
thermostat.thermostat_id = thermostat_id
thermostat.get_name.return_value = name
if zones is not None:
zone_ids = [z.zone_id for z in zones]
thermostat.get_zone_ids.return_value = zone_ids
thermostat.get_zone_by_id.side_effect = lambda zid: next(
z for z in zones if z.zone_id == zid
)
else:
thermostat.get_zone_ids.return_value = []
thermostat.get_air_cleaner_mode.side_effect = lambda: state_data.air_cleaner_mode
thermostat.get_current_compressor_speed.return_value = current_compressor_speed
thermostat.get_deadband.return_value = deadband
thermostat.get_dehumidify_setpoint.side_effect = lambda: (
state_data.dehumidify_setpoint
)
thermostat.get_dehumidify_setpoint_limits.return_value = dehumidify_setpoint_limits
thermostat.get_fan_mode.side_effect = lambda: state_data.fan_mode
thermostat.get_fan_modes.return_value = fan_modes or ["Auto", "On", "Circulate"]
thermostat.get_fan_speed_setpoint.side_effect = lambda: state_data.fan_setpoint
thermostat.get_firmware.return_value = firmware
thermostat.get_humidify_setpoint.side_effect = lambda: state_data.humidify_setpoint
thermostat.get_humidify_setpoint_limits.return_value = humidify_setpoint_limits
thermostat.get_humidity_setpoint_limits.return_value = (
(humidify_setpoint_limits[0], dehumidify_setpoint_limits[1])
if has_humidify_support and has_dehumidify_support
else humidify_setpoint_limits
if has_humidify_support
else dehumidify_setpoint_limits
)
thermostat.get_model.return_value = model
thermostat.get_outdoor_temperature.return_value = outdoor_temperature
thermostat.get_relative_humidity.return_value = relative_humidity
thermostat.get_requested_compressor_speed.return_value = requested_compressor_speed
thermostat.get_setpoint_limits.return_value = setpoint_limits
thermostat.get_system_status.return_value = system_status
thermostat.get_unit.return_value = unit
thermostat.get_variable_fan_speed_limits.return_value = variable_fan_speed_limits
thermostat.has_air_cleaner.return_value = has_air_cleaner
thermostat.has_dehumidify_support.return_value = has_dehumidify_support
thermostat.has_emergency_heat.return_value = has_emergency_heat
thermostat.has_humidify_support.return_value = has_humidify_support
thermostat.has_outdoor_temperature.return_value = has_outdoor_temperature
thermostat.has_relative_humidity.return_value = has_relative_humidity
thermostat.has_variable_fan_speed.return_value = has_variable_fan_speed
thermostat.has_variable_speed_compressor.return_value = (
has_variable_speed_compressor
)
thermostat.has_zones.return_value = has_zones
thermostat.is_blower_active.return_value = is_blower_active
thermostat.is_emergency_heat_active.side_effect = lambda: (
state_data.emergency_heat_on
)
thermostat.is_online = is_online
thermostat.set_air_cleaner.side_effect = state_data.async_set_air_cleaner_mode
thermostat.set_dehumidify_setpoint.side_effect = (
state_data.async_set_dehumidify_setpoint
)
thermostat.set_emergency_heat.side_effect = state_data.async_set_emergency_heat_on
thermostat.set_fan_mode.side_effect = state_data.async_set_fan_mode
thermostat.set_fan_setpoint.side_effect = state_data.async_set_fan_setpoint
thermostat.set_humidify_setpoint.side_effect = (
state_data.async_set_humidify_setpoint
)
return thermostat
def create_mock_automation(
automation_id: int,
name: str,
description: str = "",
enabled: bool = True,
) -> NonCallableMock[NexiaAutomation]:
"""Create a mock NexiaAutomation."""
automation = NonCallableMock(NexiaAutomation)
automation.automation_id = automation_id
automation.name = name
automation.description = description
automation.enabled = enabled
return automation
def create_mock_nexia_home(
house_id: int = 123456,
thermostats: list[NexiaThermostat] | None = None,
automations: list[NexiaAutomation] | None = None,
root_url: str = "https://www.mynexia.com",
) -> NonCallableMock[NexiaHome]:
"""Create a mock NexiaHome."""
nexia_home = NonCallableMock(NexiaHome)
nexia_home.house_id = house_id
nexia_home.root_url = root_url
nexia_home.automations_json = [
{"name": "automation1", "data": 1},
{"name": "automation2", "data": 2},
]
nexia_home.devices_json = [
{"name": "device1", "data": 3},
{"name": "device2", "data": 4},
]
nexia_home.update.return_value = {}
_thermostats = thermostats or []
thermostat_ids = [t.thermostat_id for t in _thermostats]
nexia_home.get_thermostat_ids.return_value = thermostat_ids
nexia_home.get_thermostat_by_id.side_effect = lambda tid: next(
t for t in _thermostats if t.thermostat_id == tid
)
_automations = automations or []
automation_ids = [a.automation_id for a in _automations]
nexia_home.get_automation_ids.return_value = automation_ids
nexia_home.get_automation_by_id.side_effect = lambda aid: next(
a for a in _automations if a.automation_id == aid
)
return nexia_home
@pytest.fixture
def mock_nexia_home() -> NonCallableMock[NexiaHome]:
"""Return a default mock NexiaHome for use in tests that don't need custom setup."""
zone = create_mock_zone(
zone_id=100,
name="Nick Office",
is_calling=True,
status="Relieving Air",
temperature=73,
)
thermostat1 = create_mock_thermostat(
thermostat_id=2000000,
name="Nick Office",
zones=[zone],
has_dehumidify_support=True,
dehumidify_setpoint=0.45,
has_relative_humidity=True,
system_status="Cooling",
)
zone.thermostat = thermostat1
zone = create_mock_zone(zone_id=200, name="Main Zone")
thermostat2 = create_mock_thermostat(
thermostat_id=2000001,
name="Master Suite",
zones=[zone],
has_outdoor_temperature=True,
outdoor_temperature=87.0, # °F -> ~30.6 °C in the fixture data
has_relative_humidity=True,
relative_humidity=0.52,
has_variable_fan_speed=True,
fan_speed_setpoint=0.35,
variable_fan_speed_limits=(0.35, 1.0),
has_variable_speed_compressor=True,
current_compressor_speed=0.69,
requested_compressor_speed=0.69,
is_blower_active=True,
system_status="Cooling",
)
zone.thermostat = thermostat2
zone = create_mock_zone(zone_id=300, name="Zone B")
thermostat3 = create_mock_thermostat(
thermostat_id=2000002,
name="Downstairs East Wing",
zones=[zone],
has_variable_fan_speed=True,
fan_speed_setpoint=0.45,
)
zone.thermostat = thermostat3
zone = create_mock_zone(zone_id=400, name="Kitchen", temperature=77)
thermostat4 = create_mock_thermostat(
thermostat_id=2000003,
name="Kitchen",
zones=[zone],
has_dehumidify_support=True,
has_relative_humidity=True,
relative_humidity=0.36,
)
zone.thermostat = thermostat4
sensor1 = create_mock_sensor(sensor_id=1, name="Center", weight=0.5)
sensor2 = create_mock_sensor(sensor_id=2, name="Upstairs", weight=0.5)
zone = create_mock_zone(
zone_id=500, name="Center NativeZone", sensors=[sensor1, sensor2]
)
zone.get_active_sensor_ids.return_value = {1, 2}
zone.get_sensor_by_id.side_effect = lambda sid: {1: sensor1, 2: sensor2}[sid]
thermostat5 = create_mock_thermostat(
thermostat_id=2000004,
name="Center NativeZone",
zones=[zone],
)
zone.thermostat = thermostat5
automations = [
create_mock_automation(1001, "Away Short", "Sets all zones to away temps."),
create_mock_automation(1002, "Power Outage", "Hold zones at 55, 90 °F"),
create_mock_automation(1003, "Power Restored", "Return to Run Schedule"),
]
return create_mock_nexia_home(
thermostats=[thermostat1, thermostat2, thermostat3, thermostat4, thermostat5],
automations=automations,
)
@pytest.fixture
def patch_nexia_home(
mock_nexia_home: NonCallableMock[NexiaHome],
) -> NonCallableMock[NexiaHome]:
"""Patches NexiaHome to return a mock instance for the duration of a test."""
with patch(
"homeassistant.components.nexia.NexiaHome", return_value=mock_nexia_home
):
yield mock_nexia_home
async def setup_integration(
hass: HomeAssistant, patch_nexia_home: NexiaHome, unique_id: str = "123456"
) -> MockConfigEntry:
"""Set up the nexia integration with a pre-configured mock NexiaHome."""
entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"},
minor_version=2,
unique_id=unique_id,
)
entry.add_to_hass(hass)
# validate setup
for tid in patch_nexia_home.get_thermostat_ids():
thermostat = patch_nexia_home.get_thermostat_by_id(tid)
for zid in thermostat.get_zone_ids():
assert thermostat.get_zone_by_id(zid).thermostat is thermostat
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry