mirror of
https://github.com/home-assistant/core.git
synced 2026-07-03 12:46:09 +01:00
411 lines
15 KiB
Python
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
|