From 951743551ae5dce2c6d3ee0aa8672e4f385561e3 Mon Sep 17 00:00:00 2001 From: Niels Perfors Date: Mon, 26 Feb 2024 23:47:01 +0100 Subject: [PATCH] Add Tado add meter readings service (#111552) --- homeassistant/components/tado/__init__.py | 19 ++- homeassistant/components/tado/const.py | 6 + homeassistant/components/tado/services.py | 52 +++++++ homeassistant/components/tado/services.yaml | 15 ++ homeassistant/components/tado/strings.json | 14 ++ ...add_readings_duplicated_meter_reading.json | 4 + .../add_readings_invalid_meter_reading.json | 1 + .../tado/fixtures/add_readings_success.json | 6 + tests/components/tado/test_service.py | 140 ++++++++++++++++++ 9 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tado/services.py create mode 100644 tests/components/tado/fixtures/add_readings_duplicated_meter_reading.json create mode 100644 tests/components/tado/fixtures/add_readings_invalid_meter_reading.json create mode 100644 tests/components/tado/fixtures/add_readings_success.json create mode 100644 tests/components/tado/test_service.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 871d6c2e6b1..c7225caaff9 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -10,10 +10,11 @@ from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from .const import ( @@ -33,6 +34,7 @@ from .const import ( UPDATE_MOBILE_DEVICE_TRACK, UPDATE_TRACK, ) +from .services import setup_services _LOGGER = logging.getLogger(__name__) @@ -52,6 +54,14 @@ SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Tado.""" + + setup_services(hass) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tado from a config entry.""" @@ -425,3 +435,10 @@ class TadoConnector: self.tado.set_temp_offset(device_id, offset) except RequestException as exc: _LOGGER.error("Could not set temperature offset: %s", exc) + + def set_meter_reading(self, reading: int) -> dict[str, str]: + """Send meter reading to Tado.""" + try: + return self.tado.set_eiq_meter_readings(reading=reading) + except RequestException as exc: + raise HomeAssistantError("Could not set meter reading") from exc diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index ee24af29b9d..24123d3f2b8 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -204,3 +204,9 @@ TADO_TO_HA_OFFSET_MAP = { # Constants for Overlay Default settings HA_TERMINATION_TYPE = "default_overlay_type" HA_TERMINATION_DURATION = "default_overlay_seconds" + +# Constants for service calls +SERVICE_ADD_METER_READING = "add_meter_reading" +CONF_CONFIG_ENTRY = "config_entry" +CONF_READING = "reading" +ATTR_MESSAGE = "message" diff --git a/homeassistant/components/tado/services.py b/homeassistant/components/tado/services.py new file mode 100644 index 00000000000..a5c5387ce94 --- /dev/null +++ b/homeassistant/components/tado/services.py @@ -0,0 +1,52 @@ +"""Services for the Tado integration.""" +import logging + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import selector + +from .const import ( + ATTR_MESSAGE, + CONF_CONFIG_ENTRY, + CONF_READING, + DATA, + DOMAIN, + SERVICE_ADD_METER_READING, +) + +_LOGGER = logging.getLogger(__name__) +SCHEMA_ADD_METER_READING = vol.Schema( + { + vol.Required(CONF_CONFIG_ENTRY): selector.ConfigEntrySelector( + { + "integration": DOMAIN, + } + ), + vol.Required(CONF_READING): vol.Coerce(int), + } +) + + +@callback +def setup_services(hass: HomeAssistant) -> None: + """Set up the services for the Tado integration.""" + + async def add_meter_reading(call: ServiceCall) -> None: + """Send meter reading to Tado.""" + entry_id: str = call.data[CONF_CONFIG_ENTRY] + reading: int = call.data[CONF_READING] + _LOGGER.debug("Add meter reading %s", reading) + + tadoconnector = hass.data[DOMAIN][entry_id][DATA] + response: dict = await hass.async_add_executor_job( + tadoconnector.set_meter_reading, call.data[CONF_READING] + ) + + if ATTR_MESSAGE in response: + raise HomeAssistantError(response[ATTR_MESSAGE]) + + hass.services.async_register( + DOMAIN, SERVICE_ADD_METER_READING, add_meter_reading, SCHEMA_ADD_METER_READING + ) diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index 0f66798f864..a5cfb919a41 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -61,3 +61,18 @@ set_climate_temperature_offset: max: 10 step: 0.01 unit_of_measurement: "°" + +add_meter_reading: + fields: + config_entry: + required: true + selector: + config_entry: + integration: tado + reading: + required: true + selector: + number: + mode: box + min: 0 + step: 1 diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index d50d1490566..267cbbe6fee 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -122,6 +122,20 @@ "description": "Offset you would like (depending on your device)." } } + }, + "add_meter_reading": { + "name": "Add meter readings", + "description": "Add meter readings to Tado Energy IQ.", + "fields": { + "config_entry": { + "name": "Config Entry", + "description": "Config entry to add meter readings to." + }, + "reading": { + "name": "Reading", + "description": "Reading in m³ or kWh without decimals." + } + } } }, "issues": { diff --git a/tests/components/tado/fixtures/add_readings_duplicated_meter_reading.json b/tests/components/tado/fixtures/add_readings_duplicated_meter_reading.json new file mode 100644 index 00000000000..109519b36bc --- /dev/null +++ b/tests/components/tado/fixtures/add_readings_duplicated_meter_reading.json @@ -0,0 +1,4 @@ +{ + "code": "duplicated_meter_reading", + "message": "reading already exists for date [2024-01-01]" +} diff --git a/tests/components/tado/fixtures/add_readings_invalid_meter_reading.json b/tests/components/tado/fixtures/add_readings_invalid_meter_reading.json new file mode 100644 index 00000000000..b9f7d8051c9 --- /dev/null +++ b/tests/components/tado/fixtures/add_readings_invalid_meter_reading.json @@ -0,0 +1 @@ +{ "code": "invalid_meter_reading", "message": "invalid new reading" } diff --git a/tests/components/tado/fixtures/add_readings_success.json b/tests/components/tado/fixtures/add_readings_success.json new file mode 100644 index 00000000000..01cb427dc12 --- /dev/null +++ b/tests/components/tado/fixtures/add_readings_success.json @@ -0,0 +1,6 @@ +{ + "id": "12345a6b-7c8d-9e01-2fa3-4b5c67890def", + "homeId": 123456, + "date": "2024-01-01", + "reading": 1234 +} diff --git a/tests/components/tado/test_service.py b/tests/components/tado/test_service.py new file mode 100644 index 00000000000..fe7e78f4ba5 --- /dev/null +++ b/tests/components/tado/test_service.py @@ -0,0 +1,140 @@ +"""The serive tests for the tado platform.""" +import json +from unittest.mock import patch + +import pytest +from requests.exceptions import RequestException + +from homeassistant.components.tado.const import ( + CONF_CONFIG_ENTRY, + CONF_READING, + DOMAIN, + SERVICE_ADD_METER_READING, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from .util import async_init_integration + +from tests.common import MockConfigEntry, load_fixture + + +async def test_has_services( + hass: HomeAssistant, +) -> None: + """Test the existence of the Tado Service.""" + + await async_init_integration(hass) + + assert hass.services.has_service(DOMAIN, SERVICE_ADD_METER_READING) + + +async def test_add_meter_readings( + hass: HomeAssistant, +) -> None: + """Test the add_meter_readings service.""" + + await async_init_integration(hass) + + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + fixture: str = load_fixture("tado/add_readings_success.json") + with patch( + "PyTado.interface.Tado.set_eiq_meter_readings", + return_value=json.loads(fixture), + ): + response: None = await hass.services.async_call( + DOMAIN, + SERVICE_ADD_METER_READING, + service_data={ + CONF_CONFIG_ENTRY: config_entry.entry_id, + CONF_READING: 1234, + }, + blocking=True, + ) + assert response is None + + +async def test_add_meter_readings_exception( + hass: HomeAssistant, +) -> None: + """Test the add_meter_readings service with a RequestException.""" + + await async_init_integration(hass) + + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + with ( + patch( + "PyTado.interface.Tado.set_eiq_meter_readings", + side_effect=RequestException("Error"), + ), + pytest.raises(HomeAssistantError) as exc, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_METER_READING, + service_data={ + CONF_CONFIG_ENTRY: config_entry.entry_id, + CONF_READING: 1234, + }, + blocking=True, + ) + + assert "Could not set meter reading" in str(exc) + + +async def test_add_meter_readings_invalid( + hass: HomeAssistant, +) -> None: + """Test the add_meter_readings service with an invalid_meter_reading response.""" + + await async_init_integration(hass) + + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + fixture: str = load_fixture("tado/add_readings_invalid_meter_reading.json") + with ( + patch( + "PyTado.interface.Tado.set_eiq_meter_readings", + return_value=json.loads(fixture), + ), + pytest.raises(HomeAssistantError) as exc, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_METER_READING, + service_data={ + CONF_CONFIG_ENTRY: config_entry.entry_id, + CONF_READING: 1234, + }, + blocking=True, + ) + + assert "invalid new reading" in str(exc) + + +async def test_add_meter_readings_duplicate( + hass: HomeAssistant, +) -> None: + """Test the add_meter_readings service with a duplicated_meter_reading response.""" + + await async_init_integration(hass) + + config_entry: MockConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + fixture: str = load_fixture("tado/add_readings_duplicated_meter_reading.json") + with ( + patch( + "PyTado.interface.Tado.set_eiq_meter_readings", + return_value=json.loads(fixture), + ), + pytest.raises(HomeAssistantError) as exc, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_METER_READING, + service_data={ + CONF_CONFIG_ENTRY: config_entry.entry_id, + CONF_READING: 1234, + }, + blocking=True, + ) + + assert "reading already exists for date" in str(exc)