mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Add google sheet get service (#150133)
Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
"services": {
|
||||
"append_sheet": {
|
||||
"service": "mdi:google-spreadsheet"
|
||||
},
|
||||
"get_sheet": {
|
||||
"service": "mdi:google-spreadsheet"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,19 @@ from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -25,9 +33,11 @@ if TYPE_CHECKING:
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
ROWS = "rows"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
SERVICE_GET_SHEET = "get_sheet"
|
||||
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
@@ -37,6 +47,14 @@ SHEET_SERVICE_SCHEMA = vol.All(
|
||||
},
|
||||
)
|
||||
|
||||
get_SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(ROWS): cv.positive_int,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
@@ -65,6 +83,24 @@ def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
|
||||
def _get_from_sheet(
|
||||
call: ServiceCall, entry: GoogleSheetsConfigEntry
|
||||
) -> JsonObjectType:
|
||||
"""Run get in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to retrieve data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
all_values = worksheet.get_values()
|
||||
return {"range": all_values[-call.data[ROWS] :]}
|
||||
|
||||
|
||||
async def _async_append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
|
||||
@@ -76,6 +112,22 @@ async def _async_append_to_sheet(call: ServiceCall) -> None:
|
||||
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
|
||||
async def _async_get_from_sheet(call: ServiceCall) -> ServiceResponse:
|
||||
"""Get lines of data from a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if entry is None:
|
||||
raise ServiceValidationError(
|
||||
f"Invalid config entry id: {call.data[DATA_CONFIG_ENTRY]}"
|
||||
)
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"Config entry {entry.entry_id} is not loaded")
|
||||
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
return await call.hass.async_add_executor_job(_get_from_sheet, call, entry)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
@@ -86,3 +138,11 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
_async_append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_SHEET,
|
||||
_async_get_from_sheet,
|
||||
schema=get_SHEET_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
@@ -14,3 +14,19 @@ append_sheet:
|
||||
example: '{"hello": world, "cool": True, "count": 5}'
|
||||
selector:
|
||||
object:
|
||||
get_sheet:
|
||||
fields:
|
||||
config_entry:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: google_sheets
|
||||
worksheet:
|
||||
example: "Sheet1"
|
||||
selector:
|
||||
text:
|
||||
rows:
|
||||
required: true
|
||||
example: 2
|
||||
selector:
|
||||
number:
|
||||
|
||||
@@ -59,6 +59,24 @@
|
||||
}
|
||||
},
|
||||
"name": "Append to sheet"
|
||||
},
|
||||
"get_sheet": {
|
||||
"description": "Gets data from a worksheet in Google Sheets.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"description": "The sheet to get data from.",
|
||||
"name": "[%key:component::google_sheets::services::append_sheet::fields::config_entry::name%]"
|
||||
},
|
||||
"rows": {
|
||||
"description": "Maximum number of rows from the end of the worksheet to return.",
|
||||
"name": "Rows"
|
||||
},
|
||||
"worksheet": {
|
||||
"description": "[%key:component::google_sheets::services::append_sheet::fields::worksheet::description%]",
|
||||
"name": "[%key:component::google_sheets::services::append_sheet::fields::worksheet::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Get data from sheet"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
tests/components/google_sheets/snapshots/test_init.ambr
Normal file
15
tests/components/google_sheets/snapshots/test_init.ambr
Normal file
@@ -0,0 +1,15 @@
|
||||
# serializer version: 1
|
||||
# name: test_get_sheet
|
||||
dict({
|
||||
'range': list([
|
||||
list([
|
||||
'a',
|
||||
'b',
|
||||
]),
|
||||
list([
|
||||
'c',
|
||||
'd',
|
||||
]),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
@@ -9,15 +9,22 @@ from unittest.mock import patch
|
||||
from gspread.exceptions import APIError
|
||||
import pytest
|
||||
from requests.models import Response
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.google_sheets.const import DOMAIN
|
||||
from homeassistant.components.google_sheets.services import (
|
||||
DATA_CONFIG_ENTRY,
|
||||
ROWS,
|
||||
SERVICE_GET_SHEET,
|
||||
WORKSHEET,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -213,6 +220,40 @@ async def test_append_sheet(
|
||||
assert len(mock_client.mock_calls) == 8
|
||||
|
||||
|
||||
async def test_get_sheet(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test service call getting data from a sheet."""
|
||||
await setup_integration()
|
||||
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].state is ConfigEntryState.LOADED
|
||||
|
||||
with patch("homeassistant.components.google_sheets.services.Client") as mock_client:
|
||||
mock_client.return_value.open_by_key.return_value.worksheet.return_value.get_values.return_value = [
|
||||
["col1", "col2"],
|
||||
["a", "b"],
|
||||
["c", "d"],
|
||||
]
|
||||
response = await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_SHEET,
|
||||
{
|
||||
DATA_CONFIG_ENTRY: config_entry.entry_id,
|
||||
WORKSHEET: "Sheet1",
|
||||
ROWS: 2,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert len(mock_client.mock_calls) == 4
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
async def test_append_sheet_multiple_rows(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
@@ -330,3 +371,98 @@ async def test_append_sheet_invalid_config_entry(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_get_sheet_invalid_config_entry(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
config_entry: MockConfigEntry,
|
||||
expires_at: int,
|
||||
scopes: list[str],
|
||||
) -> None:
|
||||
"""Test service call get sheet with invalid config entries."""
|
||||
config_entry2 = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_SHEET_ID + "2",
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": expires_at,
|
||||
"scope": " ".join(scopes),
|
||||
},
|
||||
},
|
||||
)
|
||||
config_entry2.add_to_hass(hass)
|
||||
|
||||
await setup_integration()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry2.state is ConfigEntryState.LOADED
|
||||
|
||||
# Exercise service call on a config entry that does not exist
|
||||
with pytest.raises(ServiceValidationError, match="Invalid config entry"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_SHEET,
|
||||
{
|
||||
DATA_CONFIG_ENTRY: config_entry.entry_id + "XXX",
|
||||
WORKSHEET: "Sheet1",
|
||||
ROWS: 2,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
# Unload the config entry invoke the service on the unloaded entry id
|
||||
await hass.config_entries.async_unload(config_entry2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry2.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_get_sheet_invalid_worksheet(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: ComponentSetup,
|
||||
config_entry: MockConfigEntry,
|
||||
expires_at: int,
|
||||
scopes: list[str],
|
||||
) -> None:
|
||||
"""Test service call get sheet with invalid config entries."""
|
||||
config_entry2 = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=TEST_SHEET_ID + "2",
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
"token": {
|
||||
"access_token": "mock-access-token",
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"expires_at": expires_at,
|
||||
"scope": " ".join(scopes),
|
||||
},
|
||||
},
|
||||
)
|
||||
config_entry2.add_to_hass(hass)
|
||||
|
||||
await setup_integration()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry2.state is ConfigEntryState.LOADED
|
||||
|
||||
# Exercise service call on a worksheet that does not exist
|
||||
with patch("homeassistant.components.google_sheets.services.Client") as mock_client:
|
||||
mock_client.return_value.open_by_key.return_value.worksheet.side_effect = (
|
||||
APIError(Response())
|
||||
)
|
||||
with pytest.raises(APIError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_SHEET,
|
||||
{
|
||||
DATA_CONFIG_ENTRY: config_entry.entry_id,
|
||||
WORKSHEET: "DoesNotExist",
|
||||
ROWS: 2,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user