Add switch platform to Ohme (#134347)

Co-authored-by: Joostlek <joostlek@outlook.com>
pull/134526/head
Dan Raper 2025-01-03 09:39:41 +00:00 committed by GitHub
parent 06580ce10f
commit cc0adcf47f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 370 additions and 15 deletions

View File

@ -12,7 +12,11 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, PLATFORMS
from .coordinator import OhmeAdvancedSettingsCoordinator, OhmeChargeSessionCoordinator
from .coordinator import (
OhmeAdvancedSettingsCoordinator,
OhmeChargeSessionCoordinator,
OhmeDeviceInfoCoordinator,
)
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@ -26,6 +30,7 @@ class OhmeRuntimeData:
charge_session_coordinator: OhmeChargeSessionCoordinator
advanced_settings_coordinator: OhmeAdvancedSettingsCoordinator
device_info_coordinator: OhmeDeviceInfoCoordinator
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@ -59,6 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OhmeConfigEntry) -> bool
coordinators = (
OhmeChargeSessionCoordinator(hass, client),
OhmeAdvancedSettingsCoordinator(hass, client),
OhmeDeviceInfoCoordinator(hass, client),
)
for coordinator in coordinators:

View File

@ -24,7 +24,6 @@ class OhmeButtonDescription(OhmeEntityDescription, ButtonEntityDescription):
"""Class describing Ohme button entities."""
press_fn: Callable[[OhmeApiClient], Awaitable[None]]
available_fn: Callable[[OhmeApiClient], bool]
BUTTON_DESCRIPTIONS = [
@ -67,11 +66,3 @@ class OhmeButton(OhmeEntity, ButtonEntity):
translation_key="api_failed", translation_domain=DOMAIN
) from e
await self.coordinator.async_request_refresh()
@property
def available(self) -> bool:
"""Is entity available."""
return super().available and self.entity_description.available_fn(
self.coordinator.client
)

View File

@ -3,4 +3,4 @@
from homeassistant.const import Platform
DOMAIN = "ohme"
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]

View File

@ -53,7 +53,7 @@ class OhmeChargeSessionCoordinator(OhmeBaseCoordinator):
coordinator_name = "Charge Sessions"
_default_update_interval = timedelta(seconds=30)
async def _internal_update_data(self):
async def _internal_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_get_charge_session()
@ -63,6 +63,17 @@ class OhmeAdvancedSettingsCoordinator(OhmeBaseCoordinator):
coordinator_name = "Advanced Settings"
async def _internal_update_data(self):
async def _internal_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_get_advanced_settings()
class OhmeDeviceInfoCoordinator(OhmeBaseCoordinator):
"""Coordinator to pull device info and charger settings from the API."""
coordinator_name = "Device Info"
_default_update_interval = timedelta(minutes=30)
async def _internal_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_update_device_info()

View File

@ -18,17 +18,19 @@ class OhmeEntityDescription(EntityDescription):
"""Class describing Ohme entities."""
is_supported_fn: Callable[[OhmeApiClient], bool] = lambda _: True
available_fn: Callable[[OhmeApiClient], bool] = lambda _: True
class OhmeEntity(CoordinatorEntity[OhmeBaseCoordinator]):
"""Base class for all Ohme entities."""
_attr_has_entity_name = True
entity_description: OhmeEntityDescription
def __init__(
self,
coordinator: OhmeBaseCoordinator,
entity_description: EntityDescription,
entity_description: OhmeEntityDescription,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
@ -51,4 +53,8 @@ class OhmeEntity(CoordinatorEntity[OhmeBaseCoordinator]):
@property
def available(self) -> bool:
"""Return if charger reporting as online."""
return super().available and self.coordinator.client.available
return (
super().available
and self.coordinator.client.available
and self.entity_description.available_fn(self.coordinator.client)
)

View File

@ -19,6 +19,23 @@
"ct_current": {
"default": "mdi:gauge"
}
},
"switch": {
"lock_buttons": {
"default": "mdi:lock",
"state": {
"off": "mdi:lock-open"
}
},
"require_approval": {
"default": "mdi:check-decagram"
},
"sleep_when_inactive": {
"default": "mdi:sleep",
"state": {
"off": "mdi:sleep-off"
}
}
}
},
"services": {

View File

@ -67,6 +67,17 @@
"vehicle_battery": {
"name": "Vehicle battery"
}
},
"switch": {
"lock_buttons": {
"name": "Lock buttons"
},
"require_approval": {
"name": "Require approval"
},
"sleep_when_inactive": {
"name": "Sleep when inactive"
}
}
},
"exceptions": {

View File

@ -0,0 +1,102 @@
"""Platform for switch."""
from dataclasses import dataclass
from typing import Any
from ohme import ApiException
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import OhmeConfigEntry
from .const import DOMAIN
from .entity import OhmeEntity, OhmeEntityDescription
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class OhmeSwitchDescription(OhmeEntityDescription, SwitchEntityDescription):
"""Class describing Ohme switch entities."""
configuration_key: str
SWITCH_DEVICE_INFO = [
OhmeSwitchDescription(
key="lock_buttons",
translation_key="lock_buttons",
entity_category=EntityCategory.CONFIG,
is_supported_fn=lambda client: client.is_capable("buttonsLockable"),
configuration_key="buttonsLocked",
),
OhmeSwitchDescription(
key="require_approval",
translation_key="require_approval",
entity_category=EntityCategory.CONFIG,
is_supported_fn=lambda client: client.is_capable("pluginsRequireApprovalMode"),
configuration_key="pluginsRequireApproval",
),
OhmeSwitchDescription(
key="sleep_when_inactive",
translation_key="sleep_when_inactive",
entity_category=EntityCategory.CONFIG,
is_supported_fn=lambda client: client.is_capable("stealth"),
configuration_key="stealthEnabled",
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: OhmeConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up switches."""
coordinators = config_entry.runtime_data
coordinator_map = [
(SWITCH_DEVICE_INFO, coordinators.device_info_coordinator),
]
async_add_entities(
OhmeSwitch(coordinator, description)
for entities, coordinator in coordinator_map
for description in entities
if description.is_supported_fn(coordinator.client)
)
class OhmeSwitch(OhmeEntity, SwitchEntity):
"""Generic switch for Ohme."""
entity_description: OhmeSwitchDescription
@property
def is_on(self) -> bool:
"""Return the entity value to represent the entity state."""
return self.coordinator.client.configuration_value(
self.entity_description.configuration_key
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self._toggle(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self._toggle(False)
async def _toggle(self, on: bool) -> None:
"""Toggle the switch."""
try:
await self.coordinator.client.async_set_configuration_value(
{self.entity_description.configuration_key: on}
)
except ApiException as e:
raise HomeAssistantError(
translation_key="api_failed", translation_domain=DOMAIN
) from e
await self.coordinator.async_request_refresh()

View File

@ -0,0 +1,139 @@
# serializer version: 1
# name: test_switches[switch.ohme_home_pro_lock_buttons-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.ohme_home_pro_lock_buttons',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Lock buttons',
'platform': 'ohme',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lock_buttons',
'unique_id': 'chargerid_lock_buttons',
'unit_of_measurement': None,
})
# ---
# name: test_switches[switch.ohme_home_pro_lock_buttons-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ohme Home Pro Lock buttons',
}),
'context': <ANY>,
'entity_id': 'switch.ohme_home_pro_lock_buttons',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches[switch.ohme_home_pro_require_approval-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.ohme_home_pro_require_approval',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Require approval',
'platform': 'ohme',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'require_approval',
'unique_id': 'chargerid_require_approval',
'unit_of_measurement': None,
})
# ---
# name: test_switches[switch.ohme_home_pro_require_approval-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ohme Home Pro Require approval',
}),
'context': <ANY>,
'entity_id': 'switch.ohme_home_pro_require_approval',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_switches[switch.ohme_home_pro_sleep_when_inactive-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.ohme_home_pro_sleep_when_inactive',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sleep when inactive',
'platform': 'ohme',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'sleep_when_inactive',
'unique_id': 'chargerid_sleep_when_inactive',
'unit_of_measurement': None,
})
# ---
# name: test_switches[switch.ohme_home_pro_sleep_when_inactive-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ohme Home Pro Sleep when inactive',
}),
'context': <ANY>,
'entity_id': 'switch.ohme_home_pro_sleep_when_inactive',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,72 @@
"""Tests for switches."""
from unittest.mock import MagicMock, patch
from syrupy import SnapshotAssertion
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_switches(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test the Ohme switches."""
with patch("homeassistant.components.ohme.PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_switch_on(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test the switch turn_on action."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "switch.ohme_home_pro_lock_buttons",
},
blocking=True,
)
assert len(mock_client.async_set_configuration_value.mock_calls) == 1
async def test_switch_off(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test the switch turn_off action."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: "switch.ohme_home_pro_lock_buttons",
},
blocking=True,
)
assert len(mock_client.async_set_configuration_value.mock_calls) == 1