From cdf809926b01af7ae1c7595409be1e4c76ba9467 Mon Sep 17 00:00:00 2001 From: osohotwateriot <102795312+osohotwateriot@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:22:46 +0300 Subject: [PATCH] Add OSO Energy services (#118770) * Add OSO Energy services * Fixes after review * Add tests for OSO Energy water heater * Fixes after review * Revert changes for service schema in OSO Energy * Improve osoenergy unit tests --- homeassistant/components/osoenergy/icons.json | 17 ++ .../components/osoenergy/services.yaml | 261 +++++++++++++++++ .../components/osoenergy/strings.json | 138 +++++++++ .../components/osoenergy/water_heater.py | 144 ++++++++- tests/components/osoenergy/conftest.py | 90 ++++++ .../osoenergy/fixtures/water_heater.json | 20 ++ .../snapshots/test_water_heater.ambr | 57 ++++ .../components/osoenergy/test_water_heater.py | 276 ++++++++++++++++++ 8 files changed, 1001 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/osoenergy/services.yaml create mode 100644 tests/components/osoenergy/conftest.py create mode 100644 tests/components/osoenergy/fixtures/water_heater.json create mode 100644 tests/components/osoenergy/snapshots/test_water_heater.ambr create mode 100644 tests/components/osoenergy/test_water_heater.py diff --git a/homeassistant/components/osoenergy/icons.json b/homeassistant/components/osoenergy/icons.json index 60b2d257b8a..42d1f2cc480 100644 --- a/homeassistant/components/osoenergy/icons.json +++ b/homeassistant/components/osoenergy/icons.json @@ -11,5 +11,22 @@ "default": "mdi:water-boiler" } } + }, + "services": { + "get_profile": { + "service": "mdi:thermometer-lines" + }, + "set_profile": { + "service": "mdi:thermometer-lines" + }, + "set_v40_min": { + "service": "mdi:car-coolant-level" + }, + "turn_off": { + "service": "mdi:water-boiler-off" + }, + "turn_on": { + "service": "mdi:water-boiler" + } } } diff --git a/homeassistant/components/osoenergy/services.yaml b/homeassistant/components/osoenergy/services.yaml new file mode 100644 index 00000000000..6c8f5512215 --- /dev/null +++ b/homeassistant/components/osoenergy/services.yaml @@ -0,0 +1,261 @@ +get_profile: + target: + entity: + domain: water_heater +set_profile: + target: + entity: + domain: water_heater + fields: + hour_00: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_01: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_02: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_03: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_04: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_05: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_06: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_07: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_08: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_09: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_10: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_11: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_12: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_13: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_14: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_15: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_16: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_17: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_18: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_19: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_20: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_21: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_22: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C + hour_23: + required: false + example: 75 + selector: + number: + min: 10 + max: 75 + step: 1 + unit_of_measurement: °C +set_v40_min: + target: + entity: + domain: water_heater + fields: + v40_min: + required: true + example: 240 + selector: + number: + min: 200 + max: 550 + step: 1 + unit_of_measurement: L +turn_off: + target: + entity: + domain: water_heater + fields: + until_temp_limit: + required: true + default: false + example: false + selector: + boolean: +turn_on: + target: + entity: + domain: water_heater + fields: + until_temp_limit: + required: true + default: false + example: false + selector: + boolean: diff --git a/homeassistant/components/osoenergy/strings.json b/homeassistant/components/osoenergy/strings.json index a7963bfa436..b8f95c021fa 100644 --- a/homeassistant/components/osoenergy/strings.json +++ b/homeassistant/components/osoenergy/strings.json @@ -91,5 +91,143 @@ "name": "Temperature one" } } + }, + "services": { + "get_profile": { + "name": "Get heater profile", + "description": "Get the temperature profile of water heater" + }, + "set_profile": { + "name": "Set heater profile", + "description": "Set the temperature profile of water heater", + "fields": { + "hour_00": { + "name": "00:00", + "description": "00:00 hour" + }, + "hour_01": { + "name": "01:00", + "description": "01:00 hour" + }, + "hour_02": { + "name": "02:00", + "description": "02:00 hour" + }, + "hour_03": { + "name": "03:00", + "description": "03:00 hour" + }, + "hour_04": { + "name": "04:00", + "description": "04:00 hour" + }, + "hour_05": { + "name": "05:00", + "description": "05:00 hour" + }, + "hour_06": { + "name": "06:00", + "description": "06:00 hour" + }, + "hour_07": { + "name": "07:00", + "description": "07:00 hour" + }, + "hour_08": { + "name": "08:00", + "description": "08:00 hour" + }, + "hour_09": { + "name": "09:00", + "description": "09:00 hour" + }, + "hour_10": { + "name": "10:00", + "description": "10:00 hour" + }, + "hour_11": { + "name": "11:00", + "description": "11:00 hour" + }, + "hour_12": { + "name": "12:00", + "description": "12:00 hour" + }, + "hour_13": { + "name": "13:00", + "description": "13:00 hour" + }, + "hour_14": { + "name": "14:00", + "description": "14:00 hour" + }, + "hour_15": { + "name": "15:00", + "description": "15:00 hour" + }, + "hour_16": { + "name": "16:00", + "description": "16:00 hour" + }, + "hour_17": { + "name": "17:00", + "description": "17:00 hour" + }, + "hour_18": { + "name": "18:00", + "description": "18:00 hour" + }, + "hour_19": { + "name": "19:00", + "description": "19:00 hour" + }, + "hour_20": { + "name": "20:00", + "description": "20:00 hour" + }, + "hour_21": { + "name": "21:00", + "description": "21:00 hour" + }, + "hour_22": { + "name": "22:00", + "description": "22:00 hour" + }, + "hour_23": { + "name": "23:00", + "description": "23:00 hour" + } + } + }, + "set_v40_min": { + "name": "Set v40 min", + "description": "Set the minimum quantity of water at 40°C for a heater", + "fields": { + "v40_min": { + "name": "V40 Min", + "description": "Minimum quantity of water at 40°C (200-350 for SAGA S200, 300-550 for SAGA S300)" + } + } + }, + "turn_off": { + "name": "Turn off heating", + "description": "Turn off heating for one hour or until min temperature is reached", + "fields": { + "until_temp_limit": { + "name": "Until temperature limit", + "description": "Choose if heating should be off until min temperature (True) is reached or for one hour (False)" + } + } + }, + "turn_on": { + "name": "Turn on heating", + "description": "Turn on heating for one hour or until max temperature is reached", + "fields": { + "until_temp_limit": { + "name": "Until temperature limit", + "description": "Choose if heating should be on until max temperature (True) is reached or for one hour (False)" + } + } + } } } diff --git a/homeassistant/components/osoenergy/water_heater.py b/homeassistant/components/osoenergy/water_heater.py index 55229e42c2f..ff117d6577d 100644 --- a/homeassistant/components/osoenergy/water_heater.py +++ b/homeassistant/components/osoenergy/water_heater.py @@ -1,9 +1,11 @@ """Support for OSO Energy water heaters.""" +import datetime as dt from typing import Any from apyosoenergyapi import OSOEnergy from apyosoenergyapi.helper.const import OSOEnergyWaterHeaterData +import voluptuous as vol from homeassistant.components.water_heater import ( STATE_ECO, @@ -15,12 +17,17 @@ from homeassistant.components.water_heater import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util +from homeassistant.util.json import JsonValueType from .const import DOMAIN from .entity import OSOEnergyEntity +ATTR_UNTIL_TEMP_LIMIT = "until_temp_limit" +ATTR_V40MIN = "v40_min" CURRENT_OPERATION_MAP: dict[str, Any] = { "default": { "off": STATE_OFF, @@ -34,6 +41,11 @@ CURRENT_OPERATION_MAP: dict[str, Any] = { "extraenergy": STATE_HIGH_DEMAND, }, } +SERVICE_GET_PROFILE = "get_profile" +SERVICE_SET_PROFILE = "set_profile" +SERVICE_SET_V40MIN = "set_v40_min" +SERVICE_TURN_OFF = "turn_off" +SERVICE_TURN_ON = "turn_on" async def async_setup_entry( @@ -46,6 +58,102 @@ async def async_setup_entry( return async_add_entities((OSOEnergyWaterHeater(osoenergy, dev) for dev in devices), True) + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_GET_PROFILE, + {}, + OSOEnergyWaterHeater.async_get_profile.__name__, + supports_response=SupportsResponse.ONLY, + ) + + service_set_profile_schema = cv.make_entity_service_schema( + { + vol.Optional(f"hour_{hour:02d}"): vol.All( + vol.Coerce(int), vol.Range(min=10, max=75) + ) + for hour in range(24) + } + ) + + platform.async_register_entity_service( + SERVICE_SET_PROFILE, + service_set_profile_schema, + OSOEnergyWaterHeater.async_set_profile.__name__, + ) + + platform.async_register_entity_service( + SERVICE_SET_V40MIN, + { + vol.Required(ATTR_V40MIN): vol.All( + vol.Coerce(float), vol.Range(min=200, max=550) + ), + }, + OSOEnergyWaterHeater.async_set_v40_min.__name__, + ) + + platform.async_register_entity_service( + SERVICE_TURN_OFF, + {vol.Required(ATTR_UNTIL_TEMP_LIMIT): vol.All(cv.boolean)}, + OSOEnergyWaterHeater.async_oso_turn_off.__name__, + ) + + platform.async_register_entity_service( + SERVICE_TURN_ON, + {vol.Required(ATTR_UNTIL_TEMP_LIMIT): vol.All(cv.boolean)}, + OSOEnergyWaterHeater.async_oso_turn_on.__name__, + ) + + +def _get_utc_hour(local_hour: int) -> dt.datetime: + """Convert the requested local hour to a utc hour for the day. + + Args: + local_hour: the local hour (0-23) for the current day to be converted. + + Returns: + Datetime representation for the requested hour in utc time for the day. + + """ + now = dt_util.now() + local_time = now.replace(hour=local_hour, minute=0, second=0, microsecond=0) + return dt_util.as_utc(local_time) + + +def _get_local_hour(utc_hour: int) -> dt.datetime: + """Convert the requested utc hour to a local hour for the day. + + Args: + utc_hour: the utc hour (0-23) for the current day to be converted. + + Returns: + Datetime representation for the requested hour in local time for the day. + + """ + utc_now = dt_util.utcnow() + utc_time = utc_now.replace(hour=utc_hour, minute=0, second=0, microsecond=0) + return dt_util.as_local(utc_time) + + +def _convert_profile_to_local(values: list[float]) -> list[JsonValueType]: + """Convert UTC profile to local. + + Receives a device temperature schedule - 24 values for the day where the index represents the hour of the day in UTC. + Converts the schedule to local time. + + Args: + values: list of floats representing the 24 hour temperature schedule for the device + Returns: + The device temperature schedule in local time. + + """ + profile: list[JsonValueType] = [0.0] * 24 + for hour in range(24): + local_hour = _get_local_hour(hour) + profile[local_hour.hour] = float(values[hour]) + + return profile + class OSOEnergyWaterHeater( OSOEnergyEntity[OSOEnergyWaterHeaterData], WaterHeaterEntity @@ -53,7 +161,9 @@ class OSOEnergyWaterHeater( """OSO Energy Water Heater Device.""" _attr_name = None - _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE | WaterHeaterEntityFeature.ON_OFF + ) _attr_temperature_unit = UnitOfTemperature.CELSIUS def __init__( @@ -131,6 +241,36 @@ class OSOEnergyWaterHeater( await self.osoenergy.hotwater.set_profile(self.entity_data, profile) + async def async_get_profile(self) -> ServiceResponse: + """Return the current temperature profile of the device.""" + + profile = self.entity_data.profile + return {"profile": _convert_profile_to_local(profile)} + + async def async_set_profile(self, **kwargs: Any) -> None: + """Handle the service call.""" + profile = self.entity_data.profile + + for hour in range(24): + hour_key = f"hour_{hour:02d}" + + if hour_key in kwargs: + profile[_get_utc_hour(hour).hour] = kwargs[hour_key] + + await self.osoenergy.hotwater.set_profile(self.entity_data, profile) + + async def async_set_v40_min(self, v40_min) -> None: + """Handle the service call.""" + await self.osoenergy.hotwater.set_v40_min(self.entity_data, v40_min) + + async def async_oso_turn_off(self, until_temp_limit) -> None: + """Handle the service call.""" + await self.osoenergy.hotwater.turn_off(self.entity_data, until_temp_limit) + + async def async_oso_turn_on(self, until_temp_limit) -> None: + """Handle the service call.""" + await self.osoenergy.hotwater.turn_on(self.entity_data, until_temp_limit) + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.osoenergy.session.update_data() diff --git a/tests/components/osoenergy/conftest.py b/tests/components/osoenergy/conftest.py new file mode 100644 index 00000000000..bb14fec0241 --- /dev/null +++ b/tests/components/osoenergy/conftest.py @@ -0,0 +1,90 @@ +"""Common fixtures for the OSO Energy tests.""" + +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +from apyosoenergyapi.waterheater import OSOEnergyWaterHeaterData +import pytest + +from homeassistant.components.osoenergy.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.util.json import JsonObjectType + +from tests.common import MockConfigEntry, load_json_object_fixture + +MOCK_CONFIG = { + CONF_API_KEY: "secret_api_key", +} +TEST_USER_EMAIL = "test_user_email@domain.com" + + +@pytest.fixture +def water_heater_fixture() -> JsonObjectType: + """Load the water heater fixture.""" + return load_json_object_fixture("water_heater.json", DOMAIN) + + +@pytest.fixture +def mock_water_heater(water_heater_fixture) -> MagicMock: + """Water heater mock object.""" + mock_heater = MagicMock(OSOEnergyWaterHeaterData) + for key, value in water_heater_fixture.items(): + setattr(mock_heater, key, value) + return mock_heater + + +@pytest.fixture +def mock_entry_data() -> dict[str, Any]: + """Mock config entry data for fixture.""" + return MOCK_CONFIG + + +@pytest.fixture +def mock_config_entry( + hass: HomeAssistant, mock_entry_data: dict[str, Any] +) -> ConfigEntry: + """Mock a config entry setup for incomfort integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=mock_entry_data) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def mock_osoenergy_client(mock_water_heater) -> Generator[AsyncMock]: + """Mock a OSO Energy client.""" + + with ( + patch( + "homeassistant.components.osoenergy.OSOEnergy", MagicMock() + ) as mock_client, + patch( + "homeassistant.components.osoenergy.config_flow.OSOEnergy", new=mock_client + ), + ): + mock_session = MagicMock() + mock_session.device_list = {"water_heater": [mock_water_heater]} + mock_session.start_session = AsyncMock( + return_value={"water_heater": [mock_water_heater]} + ) + mock_session.update_data = AsyncMock(return_value=True) + + mock_client().session = mock_session + + mock_hotwater = MagicMock() + mock_hotwater.get_water_heater = AsyncMock(return_value=mock_water_heater) + mock_hotwater.set_profile = AsyncMock(return_value=True) + mock_hotwater.set_v40_min = AsyncMock(return_value=True) + mock_hotwater.turn_on = AsyncMock(return_value=True) + mock_hotwater.turn_off = AsyncMock(return_value=True) + + mock_client().hotwater = mock_hotwater + + mock_client().get_user_email = AsyncMock(return_value=TEST_USER_EMAIL) + mock_client().start_session = AsyncMock( + return_value={"water_heater": [mock_water_heater]} + ) + + yield mock_client diff --git a/tests/components/osoenergy/fixtures/water_heater.json b/tests/components/osoenergy/fixtures/water_heater.json new file mode 100644 index 00000000000..82bdafb5d8a --- /dev/null +++ b/tests/components/osoenergy/fixtures/water_heater.json @@ -0,0 +1,20 @@ +{ + "device_id": "osoenergy_water_heater", + "device_type": "SAGA S200", + "device_name": "TEST DEVICE", + "current_temperature": 60, + "min_temperature": 10, + "max_temperature": 75, + "target_temperature": 60, + "target_temperature_low": 57, + "target_temperature_high": 63, + "available": true, + "online": true, + "current_operation": "on", + "optimization_mode": "oso", + "heater_mode": "auto", + "profile": [ + 10, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, + 60, 60, 60, 60, 60 + ] +} diff --git a/tests/components/osoenergy/snapshots/test_water_heater.ambr b/tests/components/osoenergy/snapshots/test_water_heater.ambr new file mode 100644 index 00000000000..5ebac405144 --- /dev/null +++ b/tests/components/osoenergy/snapshots/test_water_heater.ambr @@ -0,0 +1,57 @@ +# serializer version: 1 +# name: test_water_heater[water_heater.test_device-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_temp': 75, + 'min_temp': 10, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'water_heater', + 'entity_category': None, + 'entity_id': 'water_heater.test_device', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'osoenergy', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'osoenergy_water_heater', + 'unit_of_measurement': None, + }) +# --- +# name: test_water_heater[water_heater.test_device-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 60, + 'friendly_name': 'TEST DEVICE', + 'max_temp': 75, + 'min_temp': 10, + 'supported_features': , + 'target_temp_high': 63, + 'target_temp_low': 57, + 'temperature': 60, + }), + 'context': , + 'entity_id': 'water_heater.test_device', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'eco', + }) +# --- diff --git a/tests/components/osoenergy/test_water_heater.py b/tests/components/osoenergy/test_water_heater.py new file mode 100644 index 00000000000..851e710fa1c --- /dev/null +++ b/tests/components/osoenergy/test_water_heater.py @@ -0,0 +1,276 @@ +"""The water heater tests for the OSO Energy platform.""" + +from unittest.mock import ANY, MagicMock, patch + +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.osoenergy.const import DOMAIN +from homeassistant.components.osoenergy.water_heater import ( + ATTR_UNTIL_TEMP_LIMIT, + ATTR_V40MIN, + SERVICE_GET_PROFILE, + SERVICE_SET_PROFILE, + SERVICE_SET_V40MIN, +) +from homeassistant.components.water_heater import ( + DOMAIN as WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +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 tests.common import snapshot_platform + + +@patch("homeassistant.components.osoenergy.PLATFORMS", [Platform.WATER_HEATER]) +async def test_water_heater( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_osoenergy_client: MagicMock, + snapshot: SnapshotAssertion, + mock_config_entry: ConfigEntry, +) -> None: + """Test states of the water heater.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.freeze_time("2024-10-10 00:00:00") +async def test_get_profile( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test getting the heater profile.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + profile = await hass.services.async_call( + DOMAIN, + SERVICE_GET_PROFILE, + {ATTR_ENTITY_ID: "water_heater.test_device"}, + blocking=True, + return_response=True, + ) + + # The profile is returned in UTC format from the server + # Each index represents an hour from the current day (0-23). For example index 2 - 02:00 UTC + # Depending on the time zone and the DST the UTC hour is converted to local time and the value is placed in the correct index + # Example: time zone 'US/Pacific' and DST (-7 hours difference) - index 9 (09:00 UTC) will be converted to index 2 (02:00 Local) + assert profile == { + "water_heater.test_device": { + "profile": [ + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 10, + 60, + 60, + 60, + 60, + 60, + 60, + ], + }, + } + + +@pytest.mark.freeze_time("2024-10-10 00:00:00") +async def test_set_profile( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test getting the heater profile.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PROFILE, + {ATTR_ENTITY_ID: "water_heater.test_device", "hour_01": 45}, + blocking=True, + ) + + # The server expects to receive the profile in UTC format + # Each field represents an hour from the current day (0-23). For example field hour_01 - 01:00 Local time + # Depending on the time zone and the DST the Local hour is converted to UTC time and the value is placed in the correct index + # Example: time zone 'US/Pacific' and DST (-7 hours difference) - index 1 (01:00 Local) will be converted to index 8 (08:00 Utc) + mock_osoenergy_client().hotwater.set_profile.assert_called_once_with( + ANY, + [ + 10, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 45, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + 60, + ], + ) + + +async def test_set_v40_min( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test getting the heater profile.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_V40MIN, + {ATTR_ENTITY_ID: "water_heater.test_device", ATTR_V40MIN: 300}, + blocking=True, + ) + + mock_osoenergy_client().hotwater.set_v40_min.assert_called_once_with(ANY, 300) + + +async def test_set_temperature( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test getting the heater profile.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: "water_heater.test_device", ATTR_TEMPERATURE: 45}, + blocking=True, + ) + + mock_osoenergy_client().hotwater.set_profile.assert_called_once_with( + ANY, + [ + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + 45, + ], + ) + + +async def test_turn_on( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test turning the heater on.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "water_heater.test_device"}, + blocking=True, + ) + + mock_osoenergy_client().hotwater.turn_on.assert_called_once_with(ANY, True) + + +async def test_turn_off( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test getting the heater profile.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + WATER_HEATER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "water_heater.test_device"}, + blocking=True, + ) + + mock_osoenergy_client().hotwater.turn_off.assert_called_once_with(ANY, True) + + +async def test_oso_turn_on( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test turning the heater on.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "water_heater.test_device", ATTR_UNTIL_TEMP_LIMIT: False}, + blocking=True, + ) + + mock_osoenergy_client().hotwater.turn_on.assert_called_once_with(ANY, False) + + +async def test_oso_turn_off( + hass: HomeAssistant, + mock_osoenergy_client: MagicMock, + mock_config_entry: ConfigEntry, +) -> None: + """Test getting the heater profile.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "water_heater.test_device", ATTR_UNTIL_TEMP_LIMIT: False}, + blocking=True, + ) + + mock_osoenergy_client().hotwater.turn_off.assert_called_once_with(ANY, False)