From 327f65c9910d4c14f06927ad90b555ef48edcd4f Mon Sep 17 00:00:00 2001 From: Geoffrey <85890024+Thulrus@users.noreply.github.com> Date: Tue, 30 Sep 2025 14:38:05 -0600 Subject: [PATCH] Add switch domain to VegeHub integration (#148436) Co-authored-by: GhoweVege <85890024+GhoweVege@users.noreply.github.com> --- homeassistant/components/vegehub/const.py | 2 +- homeassistant/components/vegehub/strings.json | 5 + homeassistant/components/vegehub/switch.py | 80 +++++++++++++ tests/components/vegehub/conftest.py | 4 +- .../vegehub/snapshots/test_sensor.ambr | 108 +++++++++++++++++- .../vegehub/snapshots/test_switch.ambr | 99 ++++++++++++++++ tests/components/vegehub/test_switch.py | 107 +++++++++++++++++ 7 files changed, 401 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/vegehub/switch.py create mode 100644 tests/components/vegehub/snapshots/test_switch.ambr create mode 100644 tests/components/vegehub/test_switch.py diff --git a/homeassistant/components/vegehub/const.py b/homeassistant/components/vegehub/const.py index 960ea4d3a91..ed9a115404a 100644 --- a/homeassistant/components/vegehub/const.py +++ b/homeassistant/components/vegehub/const.py @@ -4,6 +4,6 @@ from homeassistant.const import Platform DOMAIN = "vegehub" NAME = "VegeHub" -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH] MANUFACTURER = "vegetronix" MODEL = "VegeHub" diff --git a/homeassistant/components/vegehub/strings.json b/homeassistant/components/vegehub/strings.json index c35fe0d83c9..3566a9d6a8c 100644 --- a/homeassistant/components/vegehub/strings.json +++ b/homeassistant/components/vegehub/strings.json @@ -39,6 +39,11 @@ "battery_volts": { "name": "Battery voltage" } + }, + "switch": { + "switch": { + "name": "Actuator {index}" + } } } } diff --git a/homeassistant/components/vegehub/switch.py b/homeassistant/components/vegehub/switch.py new file mode 100644 index 00000000000..aacb7330a55 --- /dev/null +++ b/homeassistant/components/vegehub/switch.py @@ -0,0 +1,80 @@ +"""Switch configuration for VegeHub integration.""" + +from typing import Any + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import VegeHubConfigEntry, VegeHubCoordinator +from .entity import VegeHubEntity + +SWITCH_TYPES: dict[str, SwitchEntityDescription] = { + "switch": SwitchEntityDescription( + key="switch", + translation_key="switch", + device_class=SwitchDeviceClass.SWITCH, + ) +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: VegeHubConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up VegeHub switches from a config entry.""" + coordinator = config_entry.runtime_data + + async_add_entities( + VegeHubSwitch( + index=i, + duration=600, # Default duration of 10 minutes + coordinator=coordinator, + description=SWITCH_TYPES["switch"], + ) + for i in range(coordinator.vegehub.num_actuators) + ) + + +class VegeHubSwitch(VegeHubEntity, SwitchEntity): + """Class for VegeHub Switches.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__( + self, + index: int, + duration: int, + coordinator: VegeHubCoordinator, + description: SwitchEntityDescription, + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator) + self.entity_description = description + # Set unique ID for pulling data from the coordinator + self.data_key = f"actuator_{index}" + self._attr_unique_id = f"{self._mac_address}_{self.data_key}" + self._attr_translation_placeholders = {"index": str(index + 1)} + self._attr_available = False + self.index = index + self.duration = duration + + @property + def is_on(self) -> bool: + """Return True if the switch is on.""" + if self.coordinator.data is None or self._attr_unique_id is None: + return False + return self.coordinator.data.get(self.data_key, 0) > 0 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.coordinator.vegehub.set_actuator(1, self.index, self.duration) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.coordinator.vegehub.set_actuator(0, self.index, self.duration) diff --git a/tests/components/vegehub/conftest.py b/tests/components/vegehub/conftest.py index 6e48feb4271..feae5deccbe 100644 --- a/tests/components/vegehub/conftest.py +++ b/tests/components/vegehub/conftest.py @@ -28,7 +28,7 @@ HUB_DATA = { "first_boot": False, "page_updated": False, "error_message": 0, - "num_channels": 2, + "num_channels": 4, "num_actuators": 2, "version": "3.4.5", "agenda": 1, @@ -57,7 +57,7 @@ def mock_vegehub() -> Generator[Any, Any, Any]: mock_instance.unique_id = TEST_UNIQUE_ID mock_instance.url = f"http://{TEST_IP}" mock_instance.info = load_fixture("vegehub/info_hub.json") - mock_instance.num_sensors = 2 + mock_instance.num_sensors = 4 mock_instance.num_actuators = 2 mock_instance.sw_version = "3.4.5" diff --git a/tests/components/vegehub/snapshots/test_sensor.ambr b/tests/components/vegehub/snapshots/test_sensor.ambr index 3a9a93dc03b..6fb0ef67c50 100644 --- a/tests/components/vegehub/snapshots/test_sensor.ambr +++ b/tests/components/vegehub/snapshots/test_sensor.ambr @@ -49,7 +49,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1.330000043', + 'state': '9.314800262', }) # --- # name: test_sensor_entities[sensor.vegehub_input_1-entry] @@ -158,3 +158,109 @@ 'state': '1.45599997', }) # --- +# name: test_sensor_entities[sensor.vegehub_input_3-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.vegehub_input_3', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Input 3', + 'platform': 'vegehub', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'analog_sensor', + 'unique_id': 'A1B2C3D4E5F6_analog_2', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_entities[sensor.vegehub_input_3-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'VegeHub Input 3', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.vegehub_input_3', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.330000043', + }) +# --- +# name: test_sensor_entities[sensor.vegehub_input_4-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.vegehub_input_4', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Input 4', + 'platform': 'vegehub', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'analog_sensor', + 'unique_id': 'A1B2C3D4E5F6_analog_3', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_entities[sensor.vegehub_input_4-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'VegeHub Input 4', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.vegehub_input_4', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.075999998', + }) +# --- diff --git a/tests/components/vegehub/snapshots/test_switch.ambr b/tests/components/vegehub/snapshots/test_switch.ambr new file mode 100644 index 00000000000..ea6d0f81791 --- /dev/null +++ b/tests/components/vegehub/snapshots/test_switch.ambr @@ -0,0 +1,99 @@ +# serializer version: 1 +# name: test_switch_entities[switch.vegehub_actuator_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.vegehub_actuator_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Actuator 1', + 'platform': 'vegehub', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'A1B2C3D4E5F6_actuator_0', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[switch.vegehub_actuator_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'VegeHub Actuator 1', + }), + 'context': , + 'entity_id': 'switch.vegehub_actuator_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switch_entities[switch.vegehub_actuator_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.vegehub_actuator_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Actuator 2', + 'platform': 'vegehub', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'A1B2C3D4E5F6_actuator_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[switch.vegehub_actuator_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'VegeHub Actuator 2', + }), + 'context': , + 'entity_id': 'switch.vegehub_actuator_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/vegehub/test_switch.py b/tests/components/vegehub/test_switch.py new file mode 100644 index 00000000000..ab9768b8149 --- /dev/null +++ b/tests/components/vegehub/test_switch.py @@ -0,0 +1,107 @@ +"""Unit tests for the VegeHub integration's switch.py.""" + +from unittest.mock import patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration +from .conftest import TEST_SIMPLE_MAC, TEST_WEBHOOK_ID + +from tests.common import MockConfigEntry, snapshot_platform +from tests.typing import ClientSessionGenerator + +UPDATE_DATA = { + "api_key": "", + "mac": TEST_SIMPLE_MAC, + "error_code": 0, + "sensors": [ + {"slot": 1, "samples": [{"v": 1.5, "t": "2025-01-15T16:51:23Z"}]}, + {"slot": 2, "samples": [{"v": 1.45599997, "t": "2025-01-15T16:51:23Z"}]}, + {"slot": 3, "samples": [{"v": 1.330000043, "t": "2025-01-15T16:51:23Z"}]}, + {"slot": 4, "samples": [{"v": 0.075999998, "t": "2025-01-15T16:51:23Z"}]}, + {"slot": 5, "samples": [{"v": 9.314800262, "t": "2025-01-15T16:51:23Z"}]}, + {"slot": 6, "samples": [{"v": 1, "t": "2025-01-15T16:51:23Z"}]}, + {"slot": 7, "samples": [{"v": 0, "t": "2025-01-15T16:51:23Z"}]}, + ], + "send_time": 1736959883, + "wifi_str": -27, +} + + +async def test_switch_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + hass_client_no_auth: ClientSessionGenerator, + entity_registry: er.EntityRegistry, + mocked_config_entry: MockConfigEntry, +) -> None: + """Test all entities.""" + + with patch("homeassistant.components.vegehub.PLATFORMS", [Platform.SWITCH]): + await init_integration(hass, mocked_config_entry) + + assert TEST_WEBHOOK_ID in hass.data["webhook"], "Webhook was not registered" + + # Verify the webhook handler + webhook_info = hass.data["webhook"][TEST_WEBHOOK_ID] + assert webhook_info["handler"], "Webhook handler is not set" + + client = await hass_client_no_auth() + resp = await client.post(f"/api/webhook/{TEST_WEBHOOK_ID}", json=UPDATE_DATA) + + # Send the same update again so that the coordinator modifies existing data + # instead of creating new data. + resp = await client.post(f"/api/webhook/{TEST_WEBHOOK_ID}", json=UPDATE_DATA) + + # Wait for remaining tasks to complete. + await hass.async_block_till_done() + assert resp.status == 200, f"Unexpected status code: {resp.status}" + await snapshot_platform( + hass, entity_registry, snapshot, mocked_config_entry.entry_id + ) + + +async def test_switch_turn_on_off( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mocked_config_entry: MockConfigEntry, +) -> None: + """Test switch turn_on and turn_off methods.""" + with patch("homeassistant.components.vegehub.PLATFORMS", [Platform.SWITCH]): + await init_integration(hass, mocked_config_entry) + + # Send webhook data to initialize switches + client = await hass_client_no_auth() + resp = await client.post(f"/api/webhook/{TEST_WEBHOOK_ID}", json=UPDATE_DATA) + await hass.async_block_till_done() + assert resp.status == 200 + + # Get switch entity IDs + switch_entity_ids = hass.states.async_entity_ids("switch") + assert len(switch_entity_ids) > 0, "No switch entities found" + + # Test turn_on method + with patch( + "homeassistant.components.vegehub.VegeHub.set_actuator" + ) as mock_set_actuator: + await hass.services.async_call( + "switch", "turn_on", {"entity_id": switch_entity_ids[0]}, blocking=True + ) + mock_set_actuator.assert_called_once_with( + 1, 0, 600 + ) # on, index 0, duration 600 + + # Test turn_off method + with patch( + "homeassistant.components.vegehub.VegeHub.set_actuator" + ) as mock_set_actuator: + await hass.services.async_call( + "switch", "turn_off", {"entity_id": switch_entity_ids[0]}, blocking=True + ) + mock_set_actuator.assert_called_once_with( + 0, 0, 600 + ) # off, index 0, duration 600