diff --git a/homeassistant/components/ohme/__init__.py b/homeassistant/components/ohme/__init__.py index 90516835513..8518e55c0a3 100644 --- a/homeassistant/components/ohme/__init__.py +++ b/homeassistant/components/ohme/__init__.py @@ -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: diff --git a/homeassistant/components/ohme/button.py b/homeassistant/components/ohme/button.py index 21792770bb4..0b0590428ce 100644 --- a/homeassistant/components/ohme/button.py +++ b/homeassistant/components/ohme/button.py @@ -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 - ) diff --git a/homeassistant/components/ohme/const.py b/homeassistant/components/ohme/const.py index b44262ad509..770d18e823a 100644 --- a/homeassistant/components/ohme/const.py +++ b/homeassistant/components/ohme/const.py @@ -3,4 +3,4 @@ from homeassistant.const import Platform DOMAIN = "ohme" -PLATFORMS = [Platform.BUTTON, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] diff --git a/homeassistant/components/ohme/coordinator.py b/homeassistant/components/ohme/coordinator.py index 5de59b3d4b2..199eb7380a7 100644 --- a/homeassistant/components/ohme/coordinator.py +++ b/homeassistant/components/ohme/coordinator.py @@ -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() diff --git a/homeassistant/components/ohme/entity.py b/homeassistant/components/ohme/entity.py index 6a7d0ea16e4..38e281975a0 100644 --- a/homeassistant/components/ohme/entity.py +++ b/homeassistant/components/ohme/entity.py @@ -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) + ) diff --git a/homeassistant/components/ohme/icons.json b/homeassistant/components/ohme/icons.json index b6d4978682b..6fa7925aa02 100644 --- a/homeassistant/components/ohme/icons.json +++ b/homeassistant/components/ohme/icons.json @@ -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": { diff --git a/homeassistant/components/ohme/strings.json b/homeassistant/components/ohme/strings.json index f5905ce42eb..4c45f8eca8c 100644 --- a/homeassistant/components/ohme/strings.json +++ b/homeassistant/components/ohme/strings.json @@ -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": { diff --git a/homeassistant/components/ohme/switch.py b/homeassistant/components/ohme/switch.py new file mode 100644 index 00000000000..d1eb1a80b56 --- /dev/null +++ b/homeassistant/components/ohme/switch.py @@ -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() diff --git a/tests/components/ohme/snapshots/test_switch.ambr b/tests/components/ohme/snapshots/test_switch.ambr new file mode 100644 index 00000000000..76066b6e658 --- /dev/null +++ b/tests/components/ohme/snapshots/test_switch.ambr @@ -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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ohme_home_pro_lock_buttons', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'switch.ohme_home_pro_lock_buttons', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ohme_home_pro_require_approval-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ohme_home_pro_require_approval', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'switch.ohme_home_pro_require_approval', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_switches[switch.ohme_home_pro_sleep_when_inactive-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ohme_home_pro_sleep_when_inactive', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'entity_id': 'switch.ohme_home_pro_sleep_when_inactive', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/ohme/test_switch.py b/tests/components/ohme/test_switch.py new file mode 100644 index 00000000000..b16b70d67f8 --- /dev/null +++ b/tests/components/ohme/test_switch.py @@ -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