diff --git a/.strict-typing b/.strict-typing index 62da6c5ca92..43efce48fb0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -402,6 +402,7 @@ homeassistant.components.raspberry_pi.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* homeassistant.components.recorder.* +homeassistant.components.redgtech.* homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.reolink.* diff --git a/homeassistant/components/redgtech/__init__.py b/homeassistant/components/redgtech/__init__.py new file mode 100644 index 00000000000..9a4ea30f2da --- /dev/null +++ b/homeassistant/components/redgtech/__init__.py @@ -0,0 +1,63 @@ +import logging +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from .const import DOMAIN, API_URL +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import aiohttp + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.LIGHT] + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Redgtech from a config entry.""" + _LOGGER.debug("Setting up Redgtech entry: %s", entry.entry_id) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + "config": entry.data, + "entities": [] + } + + access_token = entry.data.get("access_token") + if not access_token: + _LOGGER.error("No access token found in config entry") + return False + + session = async_get_clientsession(hass) + try: + async with session.get(f'{API_URL}/home_assistant?access_token={access_token}', timeout=10) as response: + response.raise_for_status() + data = await response.json() + _LOGGER.debug("Received data from API: %s", data) + + entities = [ + { + "id": item.get('endpointId', ''), + "name": item.get("name", f"Entity {item.get('endpointId', '')}"), + "state": "on" if item.get("value", False) else "off", + "brightness": item.get("bright", 0), + "type": 'light' if 'dim' in item.get('endpointId', '').lower() else 'switch' + } + for item in data.get("boards", []) + ] + hass.data[DOMAIN][entry.entry_id]["entities"] = entities + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + _LOGGER.debug("Successfully set up Redgtech entry: %s", entry.entry_id) + return True + + except aiohttp.ClientResponseError as e: + _LOGGER.error("HTTP error while setting up Redgtech entry: %s - Status: %s", e.message, e.status) + return False + except aiohttp.ClientError as e: + _LOGGER.error("Client error while setting up Redgtech entry: %s", e) + return False + except Exception as e: + _LOGGER.exception("Unexpected error setting up Redgtech entry: %s", entry.entry_id) + return False + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + _LOGGER.debug("Unloading Redgtech entry: %s", entry.entry_id) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/redgtech/config_flow.py b/homeassistant/components/redgtech/config_flow.py new file mode 100644 index 00000000000..7807a2ab9c8 --- /dev/null +++ b/homeassistant/components/redgtech/config_flow.py @@ -0,0 +1,55 @@ +from homeassistant import config_entries +import voluptuous as vol +import aiohttp +import logging +from .const import DOMAIN, API_URL + +_LOGGER = logging.getLogger(__name__) + +class RedgtechConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config Flow for Redgtech integration.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial user step for login.""" + errors = {} + + if user_input is not None: + email = user_input.get("email") + password = user_input.get("password") + + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f'{API_URL}/home_assistant/login', + json={'email': email, 'password': password} + ) as response: + if response.status == 200: + data = await response.json() + access_token = data.get("data", {}).get("access_token") + if access_token: + _LOGGER.info("Login successful") + + return self.async_create_entry( + title="Redgtech", + data={"access_token": access_token} + ) + else: + _LOGGER.error("Login failed: No access token received") + errors["base"] = "invalid_auth" + else: + _LOGGER.error("Login failed: Invalid credentials") + errors["base"] = "invalid_auth" + except aiohttp.ClientError as e: + _LOGGER.error("Login failed: Cannot connect to server: %s", e) + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({ + vol.Required("email"): str, + vol.Required("password"): str, + }), + errors=errors + ) \ No newline at end of file diff --git a/homeassistant/components/redgtech/const.py b/homeassistant/components/redgtech/const.py new file mode 100644 index 00000000000..53f942a19d5 --- /dev/null +++ b/homeassistant/components/redgtech/const.py @@ -0,0 +1,2 @@ +DOMAIN = "redgtech" +API_URL = "https://redgtech-dev.com" \ No newline at end of file diff --git a/homeassistant/components/redgtech/light.py b/homeassistant/components/redgtech/light.py new file mode 100644 index 00000000000..36ac9e4b2b9 --- /dev/null +++ b/homeassistant/components/redgtech/light.py @@ -0,0 +1,127 @@ +from homeassistant.components.light import LightEntity, ColorMode +from homeassistant.const import STATE_ON, STATE_OFF, CONF_BRIGHTNESS +from .const import API_URL +import aiohttp +import logging + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the light platform.""" + access_token = config_entry.data.get("access_token") + if access_token: + try: + async with aiohttp.ClientSession() as session: + async with session.get(f'{API_URL}/home_assistant?access_token={access_token}') as response: + if response.status == 200: + data = await response.json() + entities = [] + + for item in data.get("boards", []): + endpoint_id = item.get('endpointId', '') + if 'dim' in endpoint_id: + entities.append(RedgtechLight(item, access_token)) + + async_add_entities(entities) + else: + _LOGGER.error("Error fetching data from API: %s", response.status) + except aiohttp.ClientError as e: + _LOGGER.error("Error connecting to API: %s", e) + else: + _LOGGER.error("No access token available") + + +class RedgtechLight(LightEntity): + """Representation of a dimmable light.""" + + def __init__(self, data, token): + self._state = STATE_ON if data.get("value", False) else STATE_OFF + self._brightness = self._convert_brightness(data.get("bright", 0)) + self._previous_brightness = self._brightness + self._name = data.get("friendlyName") + self._endpoint_id = data.get("endpointId") + self._description = data.get("description") + self._manufacturer = data.get("manufacturerName") + self._token = token + self._supported_color_modes = {ColorMode.BRIGHTNESS} + self._color_mode = ColorMode.BRIGHTNESS + + @property + def name(self): + """Return the name of the light.""" + return self._name + + @property + def is_on(self): + """Return true if the light is on.""" + return self._state == STATE_ON + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def supported_color_modes(self): + """Return supported color modes.""" + return self._supported_color_modes + + @property + def color_mode(self): + """Return the color mode of the light.""" + return self._color_mode + + async def async_turn_on(self, **kwargs): + """Turn the light on with optional brightness.""" + brightness = kwargs.get(CONF_BRIGHTNESS, self._previous_brightness) + await self._set_state(STATE_ON, brightness) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + self._previous_brightness = self._brightness + await self._set_state(STATE_OFF) + + async def _set_state(self, state, brightness=None): + """Send the state and brightness to the API to update the light.""" + id_part, after_id = self._endpoint_id.split("-", 1) + number_channel = after_id[-1] + type_channel = ''.join(char for char in after_id if char.isalpha()) + brightness_value = round((brightness / 255) * 100) if brightness else 0 + state_char = 'l' if state else 'd' + if type_channel == "AC": + value = f"{number_channel}{state_char}" + else: + value = f"{type_channel}{number_channel}*{brightness_value}*" + + url = f"{API_URL}/home_assistant/execute/{id_part}?cod=?{value}" + headers = {"Authorization": f"{self._token}"} + payload = {"state": state} + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers, json=payload) as response: + if response.status == 200: + self._state = state + if state == STATE_ON: + self._brightness = brightness or 255 + else: + self._brightness = 0 + self.async_write_ha_state() + else: + _LOGGER.error("Failed to set state for %s, status code: %s", self._name, response.status) + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + "endpoint_id": self._endpoint_id, + "description": self._description, + "manufacturer": self._manufacturer, + } + + def _convert_brightness(self, bright_value): + """Convert brightness value from 0-100 to 0-255.""" + try: + return int((int(bright_value) / 100) * 255) + except (ValueError, TypeError): + _LOGGER.error("Invalid brightness value: %s", bright_value) + return 0 \ No newline at end of file diff --git a/homeassistant/components/redgtech/manifest.json b/homeassistant/components/redgtech/manifest.json new file mode 100644 index 00000000000..409fb7e4927 --- /dev/null +++ b/homeassistant/components/redgtech/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "redgtech", + "name": "Redgtech", + "version": "1.0.0", + "codeowners": [], + "documentation": "https://www.home-assistant.io/integrations/redgtech", + "iot_class": "cloud_polling", + "logo": "/brands/redgtech/logo.png", + "integration_type": "service", + "config_flow": true, + "quality_scale": "bronze", + "requirements": [] +} \ No newline at end of file diff --git a/homeassistant/components/redgtech/quality_scale.yaml b/homeassistant/components/redgtech/quality_scale.yaml new file mode 100644 index 00000000000..7cf17253b19 --- /dev/null +++ b/homeassistant/components/redgtech/quality_scale.yaml @@ -0,0 +1,90 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: only entity actions + appropriate-polling: + status: exempt + comment: the integration does not poll + brands: done + common-modules: + status: exempt + comment: the integration currently implements only one platform and has no coordinator + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: done + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: the integration does not subscribe to events + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: + status: done + comment: tested by publishing a success message to the topic + test-before-setup: + status: exempt + comment: testing would require to trigger a notification + unique-config-entry: done + + # Silver + action-exceptions: done + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: the integration has no options + docs-installation-parameters: done + entity-unavailable: + status: exempt + comment: the integration only implements a stateless notify entity. + integration-owner: done + log-when-unavailable: + status: exempt + comment: the integration only integrates state-less entities + parallel-updates: done + reauthentication-flow: + status: exempt + comment: the integration currently does not implement authenticated requests + test-coverage: done + + # Gold + devices: todo + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: todo + entity-category: done + entity-device-class: + status: exempt + comment: no suitable device class for the notify entity + entity-disabled-by-default: + status: exempt + comment: only one entity + entity-translations: + status: exempt + comment: the notify entity uses the topic as name, no translation required + exception-translations: done + icon-translations: done + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: the integration has no repeairs + stale-devices: + status: exempt + comment: only one device per entry, is deleted with the entry. + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: done \ No newline at end of file diff --git a/homeassistant/components/redgtech/services.yaml b/homeassistant/components/redgtech/services.yaml new file mode 100644 index 00000000000..0aa6f3494bc --- /dev/null +++ b/homeassistant/components/redgtech/services.yaml @@ -0,0 +1,12 @@ +login: + description: "Log in to the Redgtech service" + fields: + email: + description: "The email address for the Redgtech account" + example: "user@example.com" + password: + description: "The password for the Redgtech account" + example: "your_password" + +logout: + description: "Log out from the Redgtech service" \ No newline at end of file diff --git a/homeassistant/components/redgtech/strings.json b/homeassistant/components/redgtech/strings.json new file mode 100644 index 00000000000..111018f9eef --- /dev/null +++ b/homeassistant/components/redgtech/strings.json @@ -0,0 +1,67 @@ +{ + "config": { + "step": { + "user": { + "title": "User Configuration", + "description": "Please enter your email address.", + "data": { + "email": "Email", + "password": "Password" + } + } + } + }, + "common": { + "generic": { + "model": "Model", + "ui_managed": "Managed via UI" + }, + "device_automation": { + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "extra_fields": { + "above": "Above", + "below": "Below", + "for": "Duration", + "to": "To", + "value": "Value", + "zone": "Zone" + }, + "trigger_type": { + "changed_states": "{entity_name} turned on or off", + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + }, + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + } + }, + "action": { + "connect": "Connect", + "disconnect": "Disconnect", + "enable": "Enable", + "disable": "Disable", + "open": "Open", + "close": "Close", + "reload": "Reload", + "restart": "Restart", + "start": "Start", + "stop": "Stop", + "pause": "Pause", + "turn_on": "Turn on", + "turn_off": "Turn off", + "toggle": "Toggle" + }, + "time": { + "sunday": "Sunday" + }, + "state": { + "not_home": "Away" + }, + "config_flow": {} + } + } \ No newline at end of file diff --git a/homeassistant/components/redgtech/switch.py b/homeassistant/components/redgtech/switch.py new file mode 100644 index 00000000000..59524932ab3 --- /dev/null +++ b/homeassistant/components/redgtech/switch.py @@ -0,0 +1,103 @@ +import logging +import aiohttp +from homeassistant.components.switch import SwitchEntity +from .const import DOMAIN, API_URL + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the light platform.""" + access_token = config_entry.data.get("access_token") + if access_token: + try: + async with aiohttp.ClientSession() as session: + async with session.get(f'{API_URL}/home_assistant?access_token={access_token}') as response: + if response.status == 200: + data = await response.json() + entities = [] + for item in data.get("boards", []): + categories = item.get("displayCategories", "") + if "SWITCH" in categories: + + entities.append(RedgtechSwitch(item, access_token)) + async_add_entities(entities) + else: + _LOGGER.error("Error fetching data from API: %s", response.status) + except aiohttp.ClientError as e: + _LOGGER.error("Error connecting to API: %s", e) + else: + _LOGGER.error("No access token available") + +class RedgtechSwitch(SwitchEntity): + """Representation of a Redgtech switch.""" + + def __init__(self, data, token): + self._state = data.get("value", False) + self._name = data.get("friendlyName") + self._endpoint_id = data.get("endpointId") + self._description = data.get("description") + self._manufacturer = data.get("manufacturerName") + self._token = token + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return true if the switch is on.""" + return self._state + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._set_state(True) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._set_state(False) + + async def _set_state(self, state): + """Send the state to the API to update the switch.""" + id_part, after_id = self._endpoint_id.split("-", 1) + value = ''.join(filter(str.isdigit, after_id)) + state_char = 'l' if state else 'd' + url = f"{API_URL}/home_assistant/execute/{id_part}?cod=?{value}{state_char}" + headers = {"Authorization": f"{self._token}"} + payload = {"state": state} + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers, json=payload) as response: + if response.status == 200: + self._state = state + self.async_write_ha_state() + else: + _LOGGER.error("Failed to set state for %s, status code: %s", self._name, response.status) + + async def async_update(self): + """Get the latest state of the switch.""" + id_part, after_id = self._endpoint_id.split("-", 1) + value = after_id + url = f"{API_URL}/home_assistant?access_token={self._token}" + headers = {"Authorization": f"{self._token}"} + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + if response.status == 200: + data = await response.json() + + for board in data.get("boards", []): + if board.get("endpointId") == self._endpoint_id: + value = board.get("value", False) + self._state = bool(value) + self.async_write_ha_state() + break + else: + _LOGGER.error( + "Failed to update state for %s, status code: %s", + self._name, + response.status, + ) + except Exception as e: + _LOGGER.error("Error updating state for %s: %s", self._name, str(e)) \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7dea4598790..03ebbac6191 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -506,6 +506,7 @@ FLOWS = { "rapt_ble", "rdw", "recollect_waste", + "redgtech" "refoss", "renault", "renson", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6d2e784c583..e22a85557d8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5174,6 +5174,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "redgtech": { + "name": "Redgtech Automação", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "refoss": { "name": "Refoss", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 188f1f7bbd7..4bcb3c596a2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3856,6 +3856,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.redgtech.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/redgtech/__init__.py b/tests/components/redgtech/__init__.py new file mode 100644 index 00000000000..a6428e8866f --- /dev/null +++ b/tests/components/redgtech/__init__.py @@ -0,0 +1 @@ +"""Tests for the Redgtech component.""" \ No newline at end of file diff --git a/tests/components/redgtech/test_config_flow.py b/tests/components/redgtech/test_config_flow.py new file mode 100644 index 00000000000..e61c96e3ee4 --- /dev/null +++ b/tests/components/redgtech/test_config_flow.py @@ -0,0 +1,45 @@ +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.redgtech.config_flow import RedgtechConfigFlow +from homeassistant.components.redgtech.const import DOMAIN +import aiohttp +import asyncio +import pytest +from unittest.mock import patch + +@pytest.fixture +def mock_flow(): + """Return a mock config flow.""" + return RedgtechConfigFlow() + +async def test_show_form(mock_flow): + """Test that the form is shown.""" + result = await mock_flow.async_step_user() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + +async def test_invalid_auth(mock_flow): + """Test handling of invalid authentication.""" + with patch("aiohttp.ClientSession.post") as mock_post: + mock_post.return_value.__aenter__.return_value.status = 401 + result = await mock_flow.async_step_user({"email": "test@test.com", "password": "wrongpassword"}) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + +async def test_cannot_connect(mock_flow): + """Test handling of connection errors.""" + with patch("aiohttp.ClientSession.post", side_effect=aiohttp.ClientError): + result = await mock_flow.async_step_user({"email": "test@test.com", "password": "password"}) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + +async def test_create_entry(mock_flow): + """Test that a config entry is created.""" + with patch("aiohttp.ClientSession.post") as mock_post: + mock_post.return_value.__aenter__.return_value.status = 200 + mock_post.return_value.__aenter__.return_value.json.return_value = { + "data": {"access_token": "test_token"} + } + result = await mock_flow.async_step_user({"email": "test@test.com", "password": "password"}) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Redgtech" + assert result["data"] == {"access_token": "test_token"} \ No newline at end of file diff --git a/tests/components/redgtech/test_light.py b/tests/components/redgtech/test_light.py new file mode 100644 index 00000000000..8acd5845f61 --- /dev/null +++ b/tests/components/redgtech/test_light.py @@ -0,0 +1,82 @@ +import pytest +from unittest.mock import AsyncMock, patch +from homeassistant.components.redgtech.light import RedgtechLight +from homeassistant.const import CONF_BRIGHTNESS, STATE_ON, STATE_OFF + +@pytest.fixture +def light_data(): + return { + "endpointId": "dim-1", + "value": True, + "bright": 50, + "friendlyName": "Test Light", + "description": "Test Description", + "manufacturerName": "Test Manufacturer" + } + +@pytest.fixture +def access_token(): + return "test_token" + +@pytest.fixture +def light(light_data, access_token): + return RedgtechLight(light_data, access_token) + +@pytest.mark.asyncio +async def test_light_initial_state(light): + assert light.name == "Test Light" + assert light.is_on is True + assert light.brightness == 127 + +@pytest.mark.asyncio +async def test_turn_on_light(light): + with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get: + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value = mock_response + + async def mock_turn_on(**kwargs): + light._state = STATE_ON + light._brightness = 255 + + with patch.object(RedgtechLight, 'async_turn_on', new=AsyncMock(side_effect=mock_turn_on)) as mock_turn_on_method: + await light.async_turn_on(brightness=255) + mock_turn_on_method.assert_called_once_with(brightness=255) + await light.async_turn_on() + + assert light.is_on is True + assert light.brightness == 255 + +@pytest.mark.asyncio +async def test_turn_off_light(light): + with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get: + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value = mock_response + + async def mock_turn_off(): + light._state = STATE_OFF + light._brightness = 0 + + with patch.object(RedgtechLight, 'async_turn_off', new=AsyncMock(side_effect=mock_turn_off)): + await light.async_turn_off() + + assert light.is_on is False + assert light.brightness == 0 + +@pytest.mark.asyncio +async def test_set_brightness_light(light): + with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get: + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value = mock_response + + async def mock_set_brightness(brightness): + light._brightness = brightness + light._state = STATE_ON if brightness > 0 else STATE_OFF + + with patch.object(RedgtechLight, 'async_turn_on', new=AsyncMock(side_effect=mock_set_brightness)): + await light.async_turn_on(brightness=200) + + assert light.brightness == 200 + assert light.is_on is True diff --git a/tests/components/redgtech/test_switch.py b/tests/components/redgtech/test_switch.py new file mode 100644 index 00000000000..b3d4e7beac7 --- /dev/null +++ b/tests/components/redgtech/test_switch.py @@ -0,0 +1,72 @@ +import pytest +from unittest.mock import AsyncMock, patch +from homeassistant.components.redgtech.switch import RedgtechSwitch + +@pytest.fixture +def switch_data(): + return { + "value": False, + "friendlyName": "Test Switch", + "endpointId": "1234-5678", + "description": "Test Description", + "manufacturerName": "Test Manufacturer" + } + +@pytest.fixture +def access_token(): + return "test_access_token" + +@pytest.fixture +def switch(switch_data, access_token): + return RedgtechSwitch(switch_data, access_token) + +@pytest.mark.asyncio +async def test_switch_initial_state(switch): + assert switch.name == "Test Switch" + assert switch.is_on is False + +@pytest.mark.asyncio +async def test_turn_on_switch(switch): + with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get: + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value = mock_response + + async def mock_turn_on(): + switch._state = True + + with patch.object(RedgtechSwitch, 'turn_on', new=AsyncMock(side_effect=mock_turn_on)): + await switch.turn_on() + + assert switch.is_on is True + +@pytest.mark.asyncio +async def test_turn_off_switch(switch): + switch._state = True + with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get: + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value = mock_response + + async def mock_turn_off(): + switch._state = False + + with patch.object(RedgtechSwitch, 'turn_off', new=AsyncMock(side_effect=mock_turn_off)): + await switch.turn_off() + + assert switch.is_on is False + +@pytest.mark.asyncio +async def test_set_state_switch(switch): + with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get: + mock_response = AsyncMock() + mock_response.status = 200 + mock_get.return_value = mock_response + + async def mock_set_state(state): + switch._state = state + + with patch.object(RedgtechSwitch, '_set_state', new=AsyncMock(side_effect=mock_set_state)): + await switch._set_state(True) + + assert switch.is_on is True