Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Markus Adrario
2026-05-26 22:09:42 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 27b0ba1a25
commit f6e8394771
6 changed files with 535 additions and 5 deletions
@@ -39,6 +39,9 @@
}
},
"switch": {
"homeegram": {
"default": "mdi:robot"
},
"manual_operation": {
"default": "mdi:hand-back-left"
},
@@ -499,6 +499,9 @@
"disarm_not_supported": {
"message": "Disarm is not supported by homee."
},
"homeegram_turn_off_not_supported": {
"message": "Turning off homeegrams is not supported."
},
"invalid_preset_mode": {
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
}
+80 -1
View File
@@ -6,6 +6,7 @@ from typing import Any
from pyHomee.const import AttributeType, NodeProfile
from pyHomee.model import HomeeAttribute, HomeeNode
from pyHomee.model_homeegram import HomeeGram
from homeassistant.components.switch import (
SwitchDeviceClass,
@@ -14,9 +15,11 @@ from homeassistant.components.switch import (
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import HomeeConfigEntry
from . import DOMAIN, HomeeConfigEntry
from .const import CLIMATE_PROFILES, LIGHT_PROFILES
from .entity import HomeeEntity
from .helpers import setup_homee_platform
@@ -95,6 +98,10 @@ async def async_setup_entry(
"""Set up the switch platform for the Homee component."""
await setup_homee_platform(add_switch_entities, async_add_entities, config_entry)
async_add_entities(
HomeegramSwitch(homeegram, config_entry)
for homeegram in config_entry.runtime_data.homeegrams
)
class HomeeSwitch(HomeeEntity, SwitchEntity):
@@ -137,3 +144,75 @@ class HomeeSwitch(HomeeEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.async_set_homee_value(0)
class HomeegramSwitch(SwitchEntity):
"""Representation of a Homeegram as switch."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(self, homeegram: HomeeGram, entry: HomeeConfigEntry) -> None:
"""Initialize a homee Homeegram switch entity."""
self._homeegram = homeegram
self._entry = entry
self._attr_unique_id = f"{entry.unique_id}-hg-{homeegram.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.unique_id}-homeegrams")},
name="Homeegrams",
model="Homeegram Switches",
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._attr_translation_key = "homeegram"
self._host_connected = entry.runtime_data.connected
self._attr_name = homeegram.name
self._attr_entity_registry_enabled_default = self._is_enabled_by_default(
homeegram
)
async def async_added_to_hass(self) -> None:
"""Add the Homeegram entity to home assistant."""
self.async_on_remove(
self._homeegram.add_on_changed_listener(self._on_homeegram_updated)
)
self.async_on_remove(
self._entry.runtime_data.add_connection_listener(
self._on_connection_changed
)
)
@property
def is_on(self) -> bool:
"""Return True if homeegram is executing."""
return bool(self._homeegram.play)
@property
def available(self) -> bool:
"""Return the availability of the homeegram based on host availability."""
return bool(self._homeegram.active) and self._host_connected
async def async_turn_on(self, **kwargs: Any) -> None:
"""Trigger Homeegram on switching on."""
await self._entry.runtime_data.play_homeegram(self._homeegram.id)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turning off homeegrams is not supported."""
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="homeegram_turn_off_not_supported",
)
def _on_homeegram_updated(self, homeegram: HomeeGram) -> None:
self.async_write_ha_state()
async def _on_connection_changed(self, connected: bool) -> None:
self._host_connected = connected
self.async_write_ha_state()
def _is_enabled_by_default(self, homeegram: HomeeGram) -> bool:
"""Return if the homeegram should be enabled by default."""
# Only enable homeegram switches by default if there is more than 1 homeegram action.
return (
sum(len(action_list) for action_list in homeegram.actions.data.values()) > 1
)
@@ -0,0 +1,158 @@
[
{
"id": 2,
"name": "Test HG 1",
"image": "homeegramicon_dimmablebulb_value_0",
"state": 1,
"visible": 1,
"favorite": 0,
"order": 1,
"active": 1,
"play": 0,
"added": 1772311311,
"phonetic_name": "",
"note": "",
"services": 0,
"last_triggered": 1775894540,
"owner": 2,
"triggers": {
"switch_triggers": [],
"time_triggers": [],
"attribute_triggers": [
{
"id": 2,
"homeegram_id": 2,
"node_id": 40,
"attribute_id": 73,
"operator": 3,
"operand": 1,
"value": 1.0
}
],
"webhook_triggers": [],
"homeegram_triggers": [],
"celestial_triggers": [],
"plan_triggers": [],
"group_triggers": [],
"user_triggers": []
},
"conditions": {
"time_conditions": [],
"attribute_conditions": [
{
"id": 1,
"homeegram_id": 2,
"node_id": 39,
"attribute_id": 65,
"operator": 1,
"check_moment": 1,
"operand": 1,
"value": 1.0
}
],
"homeegram_conditions": [],
"celestial_conditions": [],
"plan_conditions": [],
"group_conditions": [],
"user_conditions": []
},
"actions": {
"attribute_actions": [],
"group_actions": [],
"tts_actions": [],
"notification_actions": [
{
"id": 1,
"homeegram_id": 2,
"style": 1,
"delay": 0,
"critical": false,
"user_ids": [2],
"message": "Computer%20an%21"
}
],
"webhook_actions": [],
"homeegram_actions": [],
"plan_actions": [],
"user_actions": []
}
},
{
"id": 3,
"name": "Test HG 2",
"image": "homeegramicon_dimmablebulb_value_0",
"state": 1,
"visible": 1,
"favorite": 0,
"order": 2,
"active": 1,
"play": 0,
"added": 1775757779,
"phonetic_name": "",
"note": "",
"services": 0,
"last_triggered": 1775907320,
"owner": 2,
"triggers": {
"switch_triggers": [],
"time_triggers": [],
"attribute_triggers": [
{
"id": 3,
"homeegram_id": 3,
"node_id": 40,
"attribute_id": 73,
"operator": 3,
"operand": 1,
"value": 1.0
}
],
"webhook_triggers": [],
"homeegram_triggers": [],
"celestial_triggers": [],
"plan_triggers": [],
"group_triggers": [],
"user_triggers": []
},
"conditions": {
"time_conditions": [],
"attribute_conditions": [],
"homeegram_conditions": [],
"celestial_conditions": [],
"plan_conditions": [],
"group_conditions": [],
"user_conditions": []
},
"actions": {
"attribute_actions": [
{
"id": 3,
"homeegram_id": 3,
"delay": 0,
"node_id": 39,
"attribute_id": 65,
"source_attribute_id": 0,
"value": 0.0,
"command": 3
}
],
"group_actions": [],
"tts_actions": [],
"notification_actions": [
{
"id": 2,
"homeegram_id": 3,
"style": 1,
"delay": 0,
"critical": false,
"user_ids": [2],
"message": "Computer%20an%21"
}
],
"webhook_actions": [],
"homeegram_actions": [],
"plan_actions": [],
"user_actions": []
}
}
]
@@ -1,4 +1,104 @@
# serializer version: 1
# name: test_switch_snapshot[switch.homeegrams_test_hg_1-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.homeegrams_test_hg_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Test HG 1',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Test HG 1',
'platform': 'homee',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'homeegram',
'unique_id': '00055511EECC-hg-2',
'unit_of_measurement': None,
})
# ---
# name: test_switch_snapshot[switch.homeegrams_test_hg_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Homeegrams Test HG 1',
}),
'context': <ANY>,
'entity_id': 'switch.homeegrams_test_hg_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_snapshot[switch.homeegrams_test_hg_2-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'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.homeegrams_test_hg_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Test HG 2',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Test HG 2',
'platform': 'homee',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'homeegram',
'unique_id': '00055511EECC-hg-3',
'unit_of_measurement': None,
})
# ---
# name: test_switch_snapshot[switch.homeegrams_test_hg_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Homeegrams Test HG 2',
}),
'context': <ANY>,
'entity_id': 'switch.homeegrams_test_hg_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_switch_snapshot[switch.test_switch_child_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+191 -4
View File
@@ -1,8 +1,9 @@
"""Test Homee switches."""
from collections.abc import AsyncGenerator
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from pyHomee.model_homeegram import HomeeGram
import pytest
from syrupy.assertion import SnapshotAssertion
from websockets import frames
@@ -16,14 +17,14 @@ from homeassistant.components.switch import (
STATE_ON,
SwitchDeviceClass,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from . import build_mock_node, setup_integration
from tests.common import MockConfigEntry, snapshot_platform
from tests.common import MockConfigEntry, load_json_array_fixture, snapshot_platform
@pytest.fixture(autouse=True)
@@ -171,6 +172,166 @@ async def test_send_error(
assert exc_info.value.translation_key == "connection_closed"
# Homeegram buttons
async def test_homeegram_button_press(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test press homeegram button."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.homeegrams_test_hg_2"},
blocking=True,
)
mock_homee.play_homeegram.assert_awaited_once_with(3)
async def test_homeegram_turn_off_not_supported(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that turning off a homeegram raises an error."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.homeegrams_test_hg_2"},
blocking=True,
)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "homeegram_turn_off_not_supported"
async def test_homeegram_button_disabled_by_default(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that homeegram button is disabled by default if it has only one action."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
entry = entity_registry.async_get("switch.homeegrams_test_hg_1")
assert entry is not None
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
async def test_homeegram_connection_listener(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test if loss of connection is sensed correctly for homeegram buttons."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state is not None
await mock_homee.add_connection_listener.call_args_list[6][0][0](False)
await hass.async_block_till_done()
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state == STATE_UNAVAILABLE
await mock_homee.add_connection_listener.call_args_list[6][0][0](True)
await hass.async_block_till_done()
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state != STATE_UNAVAILABLE
async def test_homeegram_playing_in_homee(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test if homeegram playing in homee is sensed correctly for homeegram buttons."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state is not None
# Simulate homeegram playing in homee
mock_homee.homeegrams[1].play = True
mock_homee.homeegrams[1].add_on_changed_listener.call_args_list[0][0][0](
mock_homee.homeegrams[1]
)
await hass.async_block_till_done()
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state == STATE_ON
# Simulate homeegram stopped in homee
mock_homee.homeegrams[1].play = False
mock_homee.homeegrams[1].add_on_changed_listener.call_args_list[0][0][0](
mock_homee.homeegrams[1]
)
await hass.async_block_till_done()
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state != STATE_ON
async def test_homeegram_inactive(
hass: HomeAssistant,
mock_homee: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test if inactive homeegram is sensed correctly for homeegram buttons."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state is not None
# Simulate homeegram becoming inactive
mock_homee.homeegrams[1].active = False
mock_homee.homeegrams[1].add_on_changed_listener.call_args_list[0][0][0](
mock_homee.homeegrams[1]
)
await hass.async_block_till_done()
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state == STATE_UNAVAILABLE
# Simulate homeegram becoming active again
mock_homee.homeegrams[1].active = True
mock_homee.homeegrams[1].add_on_changed_listener.call_args_list[0][0][0](
mock_homee.homeegrams[1]
)
await hass.async_block_till_done()
states = hass.states.get("switch.homeegrams_test_hg_2")
assert states.state != STATE_UNAVAILABLE
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_switch_snapshot(
hass: HomeAssistant,
mock_homee: MagicMock,
@@ -181,6 +342,32 @@ async def test_switch_snapshot(
"""Test the multisensor snapshot."""
mock_homee.nodes = [build_mock_node("switches.json")]
mock_homee.get_node_by_id.return_value = mock_homee.nodes[0]
mock_homee.homeegrams = build_homeegrams()
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
def build_homeegrams() -> list[AsyncMock]:
"""Build a list of AsyncMock instances for homeegrams from fixtures."""
homeegrams_data = load_json_array_fixture("homeegrams.json", "homee")
homeegrams = []
for hg_data in homeegrams_data:
hg_mock = AsyncMock(spec=HomeeGram)
# Set basic properties
for key in ("id", "name", "active", "play"):
setattr(hg_mock, key, hg_data[key])
# Mock triggers with AsyncMock for subclasses
triggers_mock = MagicMock()
for trigger_type, trigger_list in hg_data["triggers"].items():
setattr(triggers_mock, trigger_type, [AsyncMock() for _ in trigger_list])
hg_mock.triggers = triggers_mock
# Mock actions with AsyncMock for subclasses
actions_mock = MagicMock()
actions_mock.data = {
action_type: [AsyncMock() for _ in action_list]
for action_type, action_list in hg_data["actions"].items()
}
hg_mock.actions = actions_mock
homeegrams.append(hg_mock)
return homeegrams