diff --git a/.coveragerc b/.coveragerc index 2f76fa78d0f..be3e31bf72f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1203,7 +1203,6 @@ omit = homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/sensor.py - homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py homeassistant/components/scsgate/* homeassistant/components/sendgrid/notify.py diff --git a/tests/components/screenlogic/__init__.py b/tests/components/screenlogic/__init__.py index e562b84ad14..9c8a21b1ba4 100644 --- a/tests/components/screenlogic/__init__.py +++ b/tests/components/screenlogic/__init__.py @@ -10,9 +10,13 @@ MOCK_ADAPTER_MAC = "aa:bb:cc:dd:ee:ff" MOCK_ADAPTER_IP = "127.0.0.1" MOCK_ADAPTER_PORT = 80 +MOCK_CONFIG_ENTRY_ID = "screenlogictest" +MOCK_DEVICE_AREA = "pool" + _LOGGER = logging.getLogger(__name__) +GATEWAY_IMPORT_PATH = "homeassistant.components.screenlogic.ScreenLogicGateway" GATEWAY_DISCOVERY_IMPORT_PATH = "homeassistant.components.screenlogic.coordinator.async_discover_gateways_by_unique_id" @@ -36,6 +40,9 @@ def num_key_string_to_int(data: dict) -> None: DATA_FULL_CHEM = num_key_string_to_int( load_json_object_fixture("screenlogic/data_full_chem.json") ) +DATA_FULL_CHEM_CHLOR = num_key_string_to_int( + load_json_object_fixture("screenlogic/data_full_chem_chlor.json") +) DATA_FULL_NO_GPM = num_key_string_to_int( load_json_object_fixture("screenlogic/data_full_no_gpm.json") ) diff --git a/tests/components/screenlogic/conftest.py b/tests/components/screenlogic/conftest.py index 7c4d6adf16b..b1c192f0022 100644 --- a/tests/components/screenlogic/conftest.py +++ b/tests/components/screenlogic/conftest.py @@ -5,7 +5,13 @@ import pytest from homeassistant.components.screenlogic import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL -from . import MOCK_ADAPTER_IP, MOCK_ADAPTER_MAC, MOCK_ADAPTER_NAME, MOCK_ADAPTER_PORT +from . import ( + MOCK_ADAPTER_IP, + MOCK_ADAPTER_MAC, + MOCK_ADAPTER_NAME, + MOCK_ADAPTER_PORT, + MOCK_CONFIG_ENTRY_ID, +) from tests.common import MockConfigEntry @@ -24,5 +30,5 @@ def mock_config_entry() -> MockConfigEntry: CONF_SCAN_INTERVAL: 30, }, unique_id=MOCK_ADAPTER_MAC, - entry_id="screenlogictest", + entry_id=MOCK_CONFIG_ENTRY_ID, ) diff --git a/tests/components/screenlogic/fixtures/data_full_chem_chlor.json b/tests/components/screenlogic/fixtures/data_full_chem_chlor.json new file mode 100644 index 00000000000..d80639add55 --- /dev/null +++ b/tests/components/screenlogic/fixtures/data_full_chem_chlor.json @@ -0,0 +1,909 @@ +{ + "adapter": { + "firmware": { + "name": "Protocol Adapter Firmware", + "value": "POOL: 5.2 Build 736.0 Rel", + "major": 5.2, + "minor": 736.0 + } + }, + "controller": { + "controller_id": 100, + "configuration": { + "body_type": { + "0": { + "min_setpoint": 40, + "max_setpoint": 104 + }, + "1": { + "min_setpoint": 40, + "max_setpoint": 104 + } + }, + "is_celsius": { + "name": "Is Celsius", + "value": 0 + }, + "controller_type": 13, + "hardware_type": 0, + "controller_data": 0, + "generic_circuit_name": "Water Features", + "circuit_count": 11, + "color_count": 8, + "color": [ + { + "name": "White", + "value": [255, 255, 255] + }, + { + "name": "Light Green", + "value": [160, 255, 160] + }, + { + "name": "Green", + "value": [0, 255, 80] + }, + { + "name": "Cyan", + "value": [0, 255, 200] + }, + { + "name": "Blue", + "value": [100, 140, 255] + }, + { + "name": "Lavender", + "value": [230, 130, 255] + }, + { + "name": "Magenta", + "value": [255, 0, 128] + }, + { + "name": "Light Magenta", + "value": [255, 180, 210] + } + ], + "interface_tab_flags": 127, + "show_alarms": 0, + "remotes": 0, + "unknown_at_offset_09": 0, + "unknown_at_offset_10": 0, + "unknown_at_offset_11": 0 + }, + "model": { + "name": "Model", + "value": "EasyTouch2 8" + }, + "equipment": { + "flags": 98364, + "list": [ + "CHLORINATOR", + "INTELLIBRITE", + "INTELLIFLO_0", + "INTELLIFLO_1", + "INTELLICHEM", + "HYBRID_HEATER" + ] + }, + "sensor": { + "state": { + "name": "Controller State", + "value": 1, + "device_type": "enum", + "enum_options": ["Unknown", "Ready", "Sync", "Service"] + }, + "freeze_mode": { + "name": "Freeze Mode", + "value": 0 + }, + "pool_delay": { + "name": "Pool Delay", + "value": 0 + }, + "spa_delay": { + "name": "Spa Delay", + "value": 0 + }, + "cleaner_delay": { + "name": "Cleaner Delay", + "value": 0 + }, + "air_temperature": { + "name": "Air Temperature", + "value": 69, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "ph": { + "name": "pH", + "value": 7.61, + "unit": "pH", + "state_type": "measurement" + }, + "orp": { + "name": "ORP", + "value": 728, + "unit": "mV", + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.06, + "unit": "lsi", + "state_type": "measurement" + }, + "salt_ppm": { + "name": "Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 2, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 3, + "state_type": "measurement" + }, + "active_alert": { + "name": "Active Alert", + "value": 0, + "device_type": "alarm" + } + }, + "date_time": { + "timestamp": 1700489169.0, + "timestamp_host": 1700517812.0, + "auto_dst": { + "name": "Automatic Daylight Saving Time", + "value": 1 + } + } + }, + "circuit": { + "500": { + "circuit_id": 500, + "name": "Spa", + "configuration": { + "name_index": 71, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_62": 0, + "unknown_at_offset_63": 0, + "delay": 0 + }, + "function": 1, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 1, + "value": 0 + }, + "501": { + "circuit_id": 501, + "name": "Waterfall", + "configuration": { + "name_index": 85, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_94": 0, + "unknown_at_offset_95": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 2, + "value": 0 + }, + "502": { + "circuit_id": 502, + "name": "Pool Light", + "configuration": { + "name_index": 62, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_126": 0, + "unknown_at_offset_127": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 2, + "color_position": 0, + "color_stagger": 2 + }, + "device_id": 3, + "value": 0 + }, + "503": { + "circuit_id": 503, + "name": "Spa Light", + "configuration": { + "name_index": 73, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_158": 0, + "unknown_at_offset_159": 0, + "delay": 0 + }, + "function": 16, + "interface": 3, + "color": { + "color_set": 6, + "color_position": 1, + "color_stagger": 10 + }, + "device_id": 4, + "value": 0 + }, + "504": { + "circuit_id": 504, + "name": "Cleaner", + "configuration": { + "name_index": 21, + "flags": 0, + "default_runtime": 240, + "unknown_at_offset_186": 0, + "unknown_at_offset_187": 0, + "delay": 0 + }, + "function": 5, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 5, + "value": 0 + }, + "505": { + "circuit_id": 505, + "name": "Pool Low", + "configuration": { + "name_index": 63, + "flags": 1, + "default_runtime": 720, + "unknown_at_offset_214": 0, + "unknown_at_offset_215": 0, + "delay": 0 + }, + "function": 2, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 6, + "value": 0 + }, + "506": { + "circuit_id": 506, + "name": "Yard Light", + "configuration": { + "name_index": 91, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_246": 0, + "unknown_at_offset_247": 0, + "delay": 0 + }, + "function": 7, + "interface": 4, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 7, + "value": 0 + }, + "507": { + "circuit_id": 507, + "name": "Cameras", + "configuration": { + "name_index": 101, + "flags": 0, + "default_runtime": 1620, + "unknown_at_offset_274": 0, + "unknown_at_offset_275": 0, + "delay": 0 + }, + "function": 0, + "interface": 2, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 8, + "value": 1 + }, + "508": { + "circuit_id": 508, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_306": 0, + "unknown_at_offset_307": 0, + "delay": 0 + }, + "function": 0, + "interface": 0, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 9, + "value": 0 + }, + "510": { + "circuit_id": 510, + "name": "Spillway", + "configuration": { + "name_index": 78, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_334": 0, + "unknown_at_offset_335": 0, + "delay": 0 + }, + "function": 14, + "interface": 1, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 11, + "value": 0 + }, + "511": { + "circuit_id": 511, + "name": "Pool High", + "configuration": { + "name_index": 61, + "flags": 0, + "default_runtime": 720, + "unknown_at_offset_366": 0, + "unknown_at_offset_367": 0, + "delay": 0 + }, + "function": 0, + "interface": 5, + "color": { + "color_set": 0, + "color_position": 0, + "color_stagger": 0 + }, + "device_id": 12, + "value": 0 + } + }, + "pump": { + "0": { + "data": 70, + "type": 3, + "state": { + "name": "Pool Low Pump", + "value": 0 + }, + "watts_now": { + "name": "Pool Low Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Pool Low Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Pool Low Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 6, + "setpoint": 63, + "is_rpm": 0 + }, + "1": { + "device_id": 9, + "setpoint": 72, + "is_rpm": 0 + }, + "2": { + "device_id": 1, + "setpoint": 3450, + "is_rpm": 1 + }, + "3": { + "device_id": 130, + "setpoint": 75, + "is_rpm": 0 + }, + "4": { + "device_id": 12, + "setpoint": 72, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "1": { + "data": 66, + "type": 3, + "state": { + "name": "Waterfall Pump", + "value": 0 + }, + "watts_now": { + "name": "Waterfall Pump Watts Now", + "value": 0, + "unit": "W", + "device_type": "power", + "state_type": "measurement" + }, + "rpm_now": { + "name": "Waterfall Pump RPM Now", + "value": 0, + "unit": "rpm", + "state_type": "measurement" + }, + "unknown_at_offset_16": 0, + "gpm_now": { + "name": "Waterfall Pump GPM Now", + "value": 0, + "unit": "gpm", + "state_type": "measurement" + }, + "unknown_at_offset_24": 255, + "preset": { + "0": { + "device_id": 2, + "setpoint": 2700, + "is_rpm": 1 + }, + "1": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "2": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "3": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "4": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "5": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "6": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + }, + "7": { + "device_id": 0, + "setpoint": 30, + "is_rpm": 0 + } + } + }, + "2": { + "data": 0 + }, + "3": { + "data": 0 + }, + "4": { + "data": 0 + }, + "5": { + "data": 0 + }, + "6": { + "data": 0 + }, + "7": { + "data": 0 + } + }, + "body": { + "0": { + "body_type": 0, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Pool", + "last_temperature": { + "name": "Last Pool Temperature", + "value": 81, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Pool Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Pool Heat Set Point", + "value": 83, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Pool Cool Set Point", + "value": 100, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Pool Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + }, + "1": { + "body_type": 1, + "min_setpoint": 40, + "max_setpoint": 104, + "name": "Spa", + "last_temperature": { + "name": "Last Spa Temperature", + "value": 84, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + }, + "heat_state": { + "name": "Spa Heat", + "value": 0, + "device_type": "enum", + "enum_options": ["Off", "Solar", "Heater", "Both"] + }, + "heat_setpoint": { + "name": "Spa Heat Set Point", + "value": 94, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "cool_setpoint": { + "name": "Spa Cool Set Point", + "value": 69, + "unit": "\u00b0F", + "device_type": "temperature" + }, + "heat_mode": { + "name": "Spa Heat Mode", + "value": 0, + "device_type": "enum", + "enum_options": [ + "Off", + "Solar", + "Solar Preferred", + "Heater", + "Don't Change" + ] + } + } + }, + "intellichem": { + "unknown_at_offset_00": 42, + "unknown_at_offset_04": 0, + "sensor": { + "ph_now": { + "name": "pH Now", + "value": 0.0, + "unit": "pH", + "state_type": "measurement" + }, + "orp_now": { + "name": "ORP Now", + "value": 0, + "unit": "mV", + "state_type": "measurement" + }, + "ph_supply_level": { + "name": "pH Supply Level", + "value": 2, + "state_type": "measurement" + }, + "orp_supply_level": { + "name": "ORP Supply Level", + "value": 3, + "state_type": "measurement" + }, + "saturation": { + "name": "Saturation Index", + "value": 0.06, + "unit": "lsi", + "state_type": "measurement" + }, + "ph_probe_water_temp": { + "name": "pH Probe Water Temperature", + "value": 81, + "unit": "\u00b0F", + "device_type": "temperature", + "state_type": "measurement" + } + }, + "configuration": { + "ph_setpoint": { + "name": "pH Setpoint", + "value": 7.6, + "unit": "pH", + "max_setpoint": 7.6, + "min_setpoint": 7.2 + }, + "orp_setpoint": { + "name": "ORP Setpoint", + "value": 720, + "unit": "mV", + "max_setpoint": 800, + "min_setpoint": 400 + }, + "calcium_harness": { + "name": "Calcium Hardness", + "value": 800, + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 + }, + "cya": { + "name": "Cyanuric Acid", + "value": 45, + "unit": "ppm", + "max_setpoint": 201, + "min_setpoint": 0 + }, + "total_alkalinity": { + "name": "Total Alkalinity", + "value": 45, + "unit": "ppm", + "max_setpoint": 800, + "min_setpoint": 25 + }, + "salt_tds_ppm": { + "name": "Salt/TDS", + "value": 1000, + "unit": "ppm", + "max_setpoint": 6500, + "min_setpoint": 500 + }, + "probe_is_celsius": 0, + "flags": 32 + }, + "dose_status": { + "ph_last_dose_time": { + "name": "Last pH Dose Time", + "value": 5, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "orp_last_dose_time": { + "name": "Last ORP Dose Time", + "value": 4, + "unit": "sec", + "device_type": "duration", + "state_type": "total_increasing" + }, + "ph_last_dose_volume": { + "name": "Last pH Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "orp_last_dose_volume": { + "name": "Last ORP Dose Volume", + "value": 8, + "unit": "mL", + "device_type": "volume", + "state_type": "total_increasing" + }, + "flags": 149, + "ph_dosing_state": { + "name": "pH Dosing State", + "value": 1, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + }, + "orp_dosing_state": { + "name": "ORP Dosing State", + "value": 2, + "device_type": "enum", + "enum_options": ["Dosing", "Mixing", "Monitoring"] + } + }, + "alarm": { + "flags": 1, + "flow_alarm": { + "name": "Flow Alarm", + "value": 1, + "device_type": "alarm" + }, + "ph_high_alarm": { + "name": "pH HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_low_alarm": { + "name": "pH LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_high_alarm": { + "name": "ORP HIGH Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_low_alarm": { + "name": "ORP LOW Alarm", + "value": 0, + "device_type": "alarm" + }, + "ph_supply_alarm": { + "name": "pH Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "orp_supply_alarm": { + "name": "ORP Supply Alarm", + "value": 0, + "device_type": "alarm" + }, + "probe_fault_alarm": { + "name": "Probe Fault", + "value": 0, + "device_type": "alarm" + } + }, + "alert": { + "flags": 0, + "ph_lockout": { + "name": "pH Lockout", + "value": 0 + }, + "ph_limit": { + "name": "pH Dose Limit Reached", + "value": 0 + }, + "orp_limit": { + "name": "ORP Dose Limit Reached", + "value": 0 + } + }, + "firmware": { + "name": "IntelliChem Firmware", + "value": "1.060", + "major": 1, + "minor": 60 + }, + "water_balance": { + "flags": 0, + "corrosive": { + "name": "SI Corrosive", + "value": 0, + "device_type": "alarm" + }, + "scaling": { + "name": "SI Scaling", + "value": 0, + "device_type": "alarm" + } + }, + "unknown_at_offset_44": 0, + "unknown_at_offset_45": 0, + "unknown_at_offset_46": 0 + }, + "scg": { + "scg_present": 1, + "sensor": { + "state": { + "name": "Chlorinator", + "value": 0 + }, + "salt_ppm": { + "name": "Chlorinator Salt", + "value": 0, + "unit": "ppm", + "state_type": "measurement" + } + }, + "configuration": { + "pool_setpoint": { + "name": "Pool Chlorinator Setpoint", + "value": 51, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 0 + }, + "spa_setpoint": { + "name": "Spa Chlorinator Setpoint", + "value": 0, + "unit": "%", + "min_setpoint": 0, + "max_setpoint": 100, + "step": 5, + "body_type": 1 + }, + "super_chlor_timer": { + "name": "Super Chlorination Timer", + "value": 0, + "unit": "hr", + "min_setpoint": 1, + "max_setpoint": 72, + "step": 1 + } + }, + "flags": 0, + "super_chlorinate": { + "name": "Super Chlorinate", + "value": 0 + } + } +} diff --git a/tests/components/screenlogic/test_services.py b/tests/components/screenlogic/test_services.py new file mode 100644 index 00000000000..cb6d4d9a687 --- /dev/null +++ b/tests/components/screenlogic/test_services.py @@ -0,0 +1,495 @@ +"""Tests for ScreenLogic integration service calls.""" + +from typing import Any +from unittest.mock import DEFAULT, AsyncMock, patch + +import pytest +from screenlogicpy import ScreenLogicGateway +from screenlogicpy.device_const.system import COLOR_MODE + +from homeassistant.components.screenlogic import DOMAIN +from homeassistant.components.screenlogic.const import ( + ATTR_COLOR_MODE, + ATTR_CONFIG_ENTRY, + ATTR_RUNTIME, + SERVICE_SET_COLOR_MODE, + SERVICE_START_SUPER_CHLORINATION, + SERVICE_STOP_SUPER_CHLORINATION, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import device_registry as dr +from homeassistant.util import slugify + +from . import ( + DATA_FULL_CHEM, + DATA_FULL_CHEM_CHLOR, + DATA_MIN_ENTITY_CLEANUP, + GATEWAY_DISCOVERY_IMPORT_PATH, + MOCK_ADAPTER_MAC, + MOCK_ADAPTER_NAME, + MOCK_CONFIG_ENTRY_ID, + MOCK_DEVICE_AREA, + stub_async_connect, +) + +from tests.common import MockConfigEntry + +NON_SL_CONFIG_ENTRY_ID = "test" + + +@pytest.fixture(name="dataset") +def dataset_fixture(): + """Define the default dataset for service tests.""" + return DATA_FULL_CHEM + + +@pytest.fixture(name="service_fixture") +async def setup_screenlogic_services_fixture( + hass: HomeAssistant, + request, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +): + """Define the setup for a patched screenlogic integration.""" + data = ( + marker.args[0] + if (marker := request.node.get_closest_marker("dataset")) is not None + else DATA_FULL_CHEM + ) + + def _service_connect(*args, **kwargs): + return stub_async_connect(data, *args, **kwargs) + + mock_config_entry.add_to_hass(hass) + + device: dr.DeviceEntry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)}, + suggested_area=MOCK_DEVICE_AREA, + ) + + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=_service_connect, + is_connected=True, + _async_connected_request=DEFAULT, + async_set_color_lights=DEFAULT, + async_set_scg_config=DEFAULT, + ) as gateway, + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + yield {"gateway": gateway, "device": device} + + +@pytest.mark.parametrize( + ("data", "target"), + [ + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_OFF.name.lower(), + ATTR_CONFIG_ENTRY: MOCK_CONFIG_ENTRY_ID, + }, + None, + ), + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_ON.name.lower(), + }, + { + ATTR_AREA_ID: MOCK_DEVICE_AREA, + }, + ), + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_ON.name.lower(), + }, + { + ATTR_ENTITY_ID: f"{Platform.SENSOR}.{slugify(f'{MOCK_ADAPTER_NAME} Air Temperature')}", + }, + ), + ], +) +async def test_service_set_color_mode( + hass: HomeAssistant, + service_fixture: dict[str, Any], + data: dict[str, Any], + target: dict[str, Any], +) -> None: + """Test set_color_mode service.""" + + mocked_async_set_color_lights: AsyncMock = service_fixture["gateway"][ + "async_set_color_lights" + ] + + assert hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE) + + non_screenlogic_entry = MockConfigEntry(entry_id="test") + non_screenlogic_entry.add_to_hass(hass) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COLOR_MODE, + service_data=data, + blocking=True, + target=target, + ) + + mocked_async_set_color_lights.assert_awaited_once() + + +async def test_service_set_color_mode_with_device( + hass: HomeAssistant, + service_fixture: dict[str, Any], +) -> None: + """Test set_color_mode service with a device target.""" + mocked_async_set_color_lights: AsyncMock = service_fixture["gateway"][ + "async_set_color_lights" + ] + + assert hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE) + + sl_device: dr.DeviceEntry = service_fixture["device"] + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COLOR_MODE, + service_data={ATTR_COLOR_MODE: COLOR_MODE.ALL_ON.name.lower()}, + blocking=True, + target={ATTR_DEVICE_ID: sl_device.id}, + ) + + mocked_async_set_color_lights.assert_awaited_once() + + +@pytest.mark.parametrize( + ("data", "target", "error_msg"), + [ + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_OFF.name.lower(), + ATTR_CONFIG_ENTRY: "invalidconfigentry", + }, + None, + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry " + "'invalidconfigentry' not found", + ), + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_OFF.name.lower(), + ATTR_CONFIG_ENTRY: NON_SL_CONFIG_ENTRY_ID, + }, + None, + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry " + "'test' is not a screenlogic config", + ), + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_ON.name.lower(), + }, + { + ATTR_AREA_ID: "invalidareaid", + }, + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry for " + "target not found", + ), + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_ON.name.lower(), + }, + { + ATTR_DEVICE_ID: "invaliddeviceid", + }, + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry for " + "target not found", + ), + ( + { + ATTR_COLOR_MODE: COLOR_MODE.ALL_ON.name.lower(), + }, + { + ATTR_ENTITY_ID: "sensor.invalidentityid", + }, + f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. Config entry for " + "target not found", + ), + ], +) +async def test_service_set_color_mode_error( + hass: HomeAssistant, + service_fixture: dict[str, Any], + data: dict[str, Any], + target: dict[str, Any], + error_msg: str, +) -> None: + """Test set_color_mode service error cases.""" + + mocked_async_set_color_lights: AsyncMock = service_fixture["gateway"][ + "async_set_color_lights" + ] + + non_screenlogic_entry = MockConfigEntry(entry_id=NON_SL_CONFIG_ENTRY_ID) + non_screenlogic_entry.add_to_hass(hass) + + assert hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE) + + with pytest.raises( + ServiceValidationError, + match=error_msg, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COLOR_MODE, + service_data=data, + blocking=True, + target=target, + ) + + mocked_async_set_color_lights.assert_not_awaited() + + +@pytest.mark.dataset(DATA_FULL_CHEM_CHLOR) +@pytest.mark.parametrize( + ("data", "target"), + [ + ( + { + ATTR_CONFIG_ENTRY: MOCK_CONFIG_ENTRY_ID, + ATTR_RUNTIME: 24, + }, + None, + ), + ], +) +async def test_service_start_super_chlorination( + hass: HomeAssistant, + service_fixture: dict[str, Any], + data: dict[str, Any], + target: dict[str, Any], +) -> None: + """Test start_super_chlorination service.""" + + mocked_async_set_scg_config: AsyncMock = service_fixture["gateway"][ + "async_set_scg_config" + ] + + assert hass.services.has_service(DOMAIN, SERVICE_START_SUPER_CHLORINATION) + + await hass.services.async_call( + DOMAIN, + SERVICE_START_SUPER_CHLORINATION, + service_data=data, + blocking=True, + target=target, + ) + + mocked_async_set_scg_config.assert_awaited_once() + + +@pytest.mark.parametrize( + ("data", "target", "error_msg"), + [ + ( + { + ATTR_CONFIG_ENTRY: "invalidconfigentry", + ATTR_RUNTIME: 24, + }, + None, + f"Failed to call service '{SERVICE_START_SUPER_CHLORINATION}'. " + "Config entry 'invalidconfigentry' not found", + ), + ( + { + ATTR_CONFIG_ENTRY: MOCK_CONFIG_ENTRY_ID, + ATTR_RUNTIME: 24, + }, + None, + f"Equipment configuration for {MOCK_ADAPTER_NAME} does not" + f" support {SERVICE_START_SUPER_CHLORINATION}", + ), + ], +) +async def test_service_start_super_chlorination_error( + hass: HomeAssistant, + service_fixture: dict[str, Any], + data: dict[str, Any], + target: dict[str, Any], + error_msg: str, +) -> None: + """Test start_super_chlorination service error cases.""" + + mocked_async_set_scg_config: AsyncMock = service_fixture["gateway"][ + "async_set_scg_config" + ] + + assert hass.services.has_service(DOMAIN, SERVICE_START_SUPER_CHLORINATION) + + with pytest.raises( + ServiceValidationError, + match=error_msg, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_START_SUPER_CHLORINATION, + service_data=data, + blocking=True, + target=target, + ) + + mocked_async_set_scg_config.assert_not_awaited() + + +@pytest.mark.dataset(DATA_FULL_CHEM_CHLOR) +@pytest.mark.parametrize( + ("data", "target"), + [ + ( + { + ATTR_CONFIG_ENTRY: MOCK_CONFIG_ENTRY_ID, + }, + None, + ), + ], +) +async def test_service_stop_super_chlorination( + hass: HomeAssistant, + service_fixture: dict[str, Any], + data: dict[str, Any], + target: dict[str, Any], +) -> None: + """Test stop_super_chlorination service.""" + + mocked_async_set_scg_config: AsyncMock = service_fixture["gateway"][ + "async_set_scg_config" + ] + + assert hass.services.has_service(DOMAIN, SERVICE_STOP_SUPER_CHLORINATION) + + await hass.services.async_call( + DOMAIN, + SERVICE_STOP_SUPER_CHLORINATION, + service_data=data, + blocking=True, + target=target, + ) + + mocked_async_set_scg_config.assert_awaited_once() + + +@pytest.mark.parametrize( + ("data", "target", "error_msg"), + [ + ( + { + ATTR_CONFIG_ENTRY: "invalidconfigentry", + }, + None, + f"Failed to call service '{SERVICE_STOP_SUPER_CHLORINATION}'. " + "Config entry 'invalidconfigentry' not found", + ), + ( + { + ATTR_CONFIG_ENTRY: MOCK_CONFIG_ENTRY_ID, + }, + None, + f"Equipment configuration for {MOCK_ADAPTER_NAME} does not" + f" support {SERVICE_STOP_SUPER_CHLORINATION}", + ), + ], +) +async def test_service_stop_super_chlorination_error( + hass: HomeAssistant, + service_fixture: dict[str, Any], + data: dict[str, Any], + target: dict[str, Any], + error_msg: str, +) -> None: + """Test stop_super_chlorination service error cases.""" + + mocked_async_set_scg_config: AsyncMock = service_fixture["gateway"][ + "async_set_scg_config" + ] + + assert hass.services.has_service(DOMAIN, SERVICE_STOP_SUPER_CHLORINATION) + + with pytest.raises( + ServiceValidationError, + match=error_msg, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_STOP_SUPER_CHLORINATION, + service_data=data, + blocking=True, + target=target, + ) + + mocked_async_set_scg_config.assert_not_awaited() + + +async def test_service_config_entry_not_loaded( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the error case of config not loaded.""" + mock_config_entry.add_to_hass(hass) + + _ = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, MOCK_ADAPTER_MAC)}, + ) + + mock_set_color_lights = AsyncMock() + + with ( + patch( + GATEWAY_DISCOVERY_IMPORT_PATH, + return_value={}, + ), + patch.multiple( + ScreenLogicGateway, + async_connect=lambda *args, **kwargs: stub_async_connect( + DATA_MIN_ENTITY_CLEANUP, *args, **kwargs + ), + async_disconnect=DEFAULT, + is_connected=True, + _async_connected_request=DEFAULT, + async_set_color_lights=mock_set_color_lights, + ), + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.services.has_service(DOMAIN, SERVICE_SET_COLOR_MODE) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await mock_config_entry.async_unload(hass) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + with pytest.raises( + ServiceValidationError, + match=f"Failed to call service '{SERVICE_SET_COLOR_MODE}'. " + f"Config entry '{MOCK_CONFIG_ENTRY_ID}' not loaded", + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COLOR_MODE, + service_data={ + ATTR_COLOR_MODE: COLOR_MODE.ALL_OFF.name.lower(), + ATTR_CONFIG_ENTRY: MOCK_CONFIG_ENTRY_ID, + }, + blocking=True, + ) + + mock_set_color_lights.assert_not_awaited()