Add switch platform to Ohme (#134347)
Co-authored-by: Joostlek <joostlek@outlook.com>pull/134526/head
parent
06580ce10f
commit
cc0adcf47f
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "ohme"
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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()
|
|
@ -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',
|
||||
})
|
||||
# ---
|
|
@ -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
|
Loading…
Reference in New Issue