Add slot list service to ohme (#134170)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Dan Raper 2024-12-29 17:07:12 +00:00 committed by GitHub
parent 65147f8d4c
commit 88d366b0c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 202 additions and 8 deletions

View File

@ -8,9 +8,14 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, PLATFORMS from .const import DOMAIN, PLATFORMS
from .coordinator import OhmeAdvancedSettingsCoordinator, OhmeChargeSessionCoordinator from .coordinator import OhmeAdvancedSettingsCoordinator, OhmeChargeSessionCoordinator
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type OhmeConfigEntry = ConfigEntry[OhmeRuntimeData] type OhmeConfigEntry = ConfigEntry[OhmeRuntimeData]
@ -23,6 +28,13 @@ class OhmeRuntimeData:
advanced_settings_coordinator: OhmeAdvancedSettingsCoordinator advanced_settings_coordinator: OhmeAdvancedSettingsCoordinator
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Ohme integration."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: OhmeConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: OhmeConfigEntry) -> bool:
"""Set up Ohme from a config entry.""" """Set up Ohme from a config entry."""

View File

@ -19,5 +19,10 @@
"default": "mdi:gauge" "default": "mdi:gauge"
} }
} }
},
"services": {
"list_charge_slots": {
"service": "mdi:clock-start"
}
} }
} }

View File

@ -1,19 +1,13 @@
rules: rules:
# Bronze # Bronze
action-setup: action-setup: done
status: exempt
comment: |
This integration has no custom actions.
appropriate-polling: done appropriate-polling: done
brands: done brands: done
common-modules: done common-modules: done
config-flow-test-coverage: done config-flow-test-coverage: done
config-flow: done config-flow: done
dependency-transparency: done dependency-transparency: done
docs-actions: docs-actions: done
status: exempt
comment: |
This integration has no custom actions.
docs-high-level-description: done docs-high-level-description: done
docs-installation-instructions: done docs-installation-instructions: done
docs-removal-instructions: done docs-removal-instructions: done

View File

@ -0,0 +1,75 @@
"""Ohme services."""
from typing import Final
from ohme import OhmeApiClient
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import selector
from .const import DOMAIN
SERVICE_LIST_CHARGE_SLOTS = "list_charge_slots"
ATTR_CONFIG_ENTRY: Final = "config_entry"
SERVICE_SCHEMA: Final = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector(
{
"integration": DOMAIN,
}
),
}
)
def __get_client(call: ServiceCall) -> OhmeApiClient:
"""Get the client from the config entry."""
entry_id: str = call.data[ATTR_CONFIG_ENTRY]
entry: ConfigEntry | None = call.hass.config_entries.async_get_entry(entry_id)
if not entry:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_config_entry",
translation_placeholders={
"config_entry": entry_id,
},
)
if entry.state != ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unloaded_config_entry",
translation_placeholders={
"config_entry": entry.title,
},
)
return entry.runtime_data.charge_session_coordinator.client
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services."""
async def list_charge_slots(
service_call: ServiceCall,
) -> ServiceResponse:
"""List of charge slots."""
client = __get_client(service_call)
return {"slots": client.slots}
hass.services.async_register(
DOMAIN,
SERVICE_LIST_CHARGE_SLOTS,
list_charge_slots,
schema=SERVICE_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

View File

@ -0,0 +1,7 @@
list_charge_slots:
fields:
config_entry:
required: true
selector:
config_entry:
integration: ohme

View File

@ -32,6 +32,18 @@
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
}, },
"services": {
"list_charge_slots": {
"name": "List charge slots",
"description": "Return a list of charge slots.",
"fields": {
"config_entry": {
"name": "Ohme account",
"description": "The Ohme config entry for which to return charge slots."
}
}
}
},
"entity": { "entity": {
"button": { "button": {
"approve": { "approve": {
@ -62,6 +74,12 @@
}, },
"api_failed": { "api_failed": {
"message": "Error communicating with Ohme API" "message": "Error communicating with Ohme API"
},
"invalid_config_entry": {
"message": "Invalid config entry provided. Got {config_entry}"
},
"unloaded_config_entry": {
"message": "Invalid config entry provided. {config_entry} is not loaded."
} }
} }
} }

View File

@ -53,6 +53,7 @@ def mock_client():
client.async_login.return_value = True client.async_login.return_value = True
client.status = ChargerStatus.CHARGING client.status = ChargerStatus.CHARGING
client.power = ChargerPower(0, 0, 0, 0) client.power = ChargerPower(0, 0, 0, 0)
client.serial = "chargerid" client.serial = "chargerid"
client.ct_connected = True client.ct_connected = True
client.energy = 1000 client.energy = 1000

View File

@ -0,0 +1,12 @@
# serializer version: 1
# name: test_list_charge_slots
dict({
'slots': list([
dict({
'end': '2024-12-30T04:30:39+00:00',
'energy': 2.042,
'start': '2024-12-30T04:00:00+00:00',
}),
]),
})
# ---

View File

@ -0,0 +1,70 @@
"""Tests for services."""
from unittest.mock import MagicMock
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.ohme.const import DOMAIN
from homeassistant.components.ohme.services import (
ATTR_CONFIG_ENTRY,
SERVICE_LIST_CHARGE_SLOTS,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from . import setup_integration
from tests.common import MockConfigEntry
async def test_list_charge_slots(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test list charge slots service."""
await setup_integration(hass, mock_config_entry)
mock_client.slots = [
{
"start": "2024-12-30T04:00:00+00:00",
"end": "2024-12-30T04:30:39+00:00",
"energy": 2.042,
}
]
assert snapshot == await hass.services.async_call(
DOMAIN,
"list_charge_slots",
{
ATTR_CONFIG_ENTRY: mock_config_entry.entry_id,
},
blocking=True,
return_response=True,
)
async def test_list_charge_slots_exception(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test list charge slots service."""
await setup_integration(hass, mock_config_entry)
# Test error
with pytest.raises(
ServiceValidationError, match="Invalid config entry provided. Got invalid"
):
await hass.services.async_call(
DOMAIN,
SERVICE_LIST_CHARGE_SLOTS,
{ATTR_CONFIG_ENTRY: "invalid"},
blocking=True,
return_response=True,
)