Add switch domain to VegeHub integration (#148436)

Co-authored-by: GhoweVege <85890024+GhoweVege@users.noreply.github.com>
This commit is contained in:
Geoffrey
2025-09-30 14:38:05 -06:00
committed by GitHub
parent 4ac89f6849
commit 327f65c991
7 changed files with 401 additions and 4 deletions

View File

@@ -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"

View File

@@ -39,6 +39,11 @@
"battery_volts": {
"name": "Battery voltage"
}
},
"switch": {
"switch": {
"name": "Actuator {index}"
}
}
}
}

View File

@@ -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)

View File

@@ -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"

View File

@@ -49,7 +49,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.vegehub_input_3',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'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': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_3-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'VegeHub Input 3',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.vegehub_input_3',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1.330000043',
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_4-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.vegehub_input_4',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'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': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_sensor_entities[sensor.vegehub_input_4-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': 'VegeHub Input 4',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.vegehub_input_4',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.075999998',
})
# ---

View File

@@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.vegehub_actuator_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'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': <ANY>,
'entity_id': 'switch.vegehub_actuator_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switch_entities[switch.vegehub_actuator_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.vegehub_actuator_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'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': <ANY>,
'entity_id': 'switch.vegehub_actuator_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -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