From cc0adcf47fb0eece811dba86ee366026faa5e78a Mon Sep 17 00:00:00 2001
From: Dan Raper <me@danr.uk>
Date: Fri, 3 Jan 2025 09:39:41 +0000
Subject: [PATCH] Add switch platform to Ohme (#134347)

Co-authored-by: Joostlek <joostlek@outlook.com>
---
 homeassistant/components/ohme/__init__.py     |   8 +-
 homeassistant/components/ohme/button.py       |   9 --
 homeassistant/components/ohme/const.py        |   2 +-
 homeassistant/components/ohme/coordinator.py  |  15 +-
 homeassistant/components/ohme/entity.py       |  10 +-
 homeassistant/components/ohme/icons.json      |  17 +++
 homeassistant/components/ohme/strings.json    |  11 ++
 homeassistant/components/ohme/switch.py       | 102 +++++++++++++
 .../ohme/snapshots/test_switch.ambr           | 139 ++++++++++++++++++
 tests/components/ohme/test_switch.py          |  72 +++++++++
 10 files changed, 370 insertions(+), 15 deletions(-)
 create mode 100644 homeassistant/components/ohme/switch.py
 create mode 100644 tests/components/ohme/snapshots/test_switch.ambr
 create mode 100644 tests/components/ohme/test_switch.py

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': <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',
+  })
+# ---
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
-- 
GitLab