Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Google Sheets."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 
7 import aiohttp
8 from google.auth.exceptions import RefreshError
9 from google.oauth2.credentials import Credentials
10 from gspread import Client
11 from gspread.exceptions import APIError
12 from gspread.utils import ValueInputOption
13 import voluptuous as vol
14 
15 from homeassistant.config_entries import ConfigEntry, ConfigEntryState
16 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
17 from homeassistant.core import HomeAssistant, ServiceCall
18 from homeassistant.exceptions import (
19  ConfigEntryAuthFailed,
20  ConfigEntryNotReady,
21  HomeAssistantError,
22 )
24  OAuth2Session,
25  async_get_config_entry_implementation,
26 )
28 from homeassistant.helpers.selector import ConfigEntrySelector
29 
30 from .const import DEFAULT_ACCESS, DOMAIN
31 
32 type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session]
33 
34 DATA = "data"
35 DATA_CONFIG_ENTRY = "config_entry"
36 WORKSHEET = "worksheet"
37 
38 SERVICE_APPEND_SHEET = "append_sheet"
39 
40 SHEET_SERVICE_SCHEMA = vol.All(
41  {
42  vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector(),
43  vol.Optional(WORKSHEET): cv.string,
44  vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
45  },
46 )
47 
48 
50  hass: HomeAssistant, entry: GoogleSheetsConfigEntry
51 ) -> bool:
52  """Set up Google Sheets from a config entry."""
53  implementation = await async_get_config_entry_implementation(hass, entry)
54  session = OAuth2Session(hass, entry, implementation)
55  try:
56  await session.async_ensure_token_valid()
57  except aiohttp.ClientResponseError as err:
58  if 400 <= err.status < 500:
60  "OAuth session is not valid, reauth required"
61  ) from err
62  raise ConfigEntryNotReady from err
63  except aiohttp.ClientError as err:
64  raise ConfigEntryNotReady from err
65 
66  if not async_entry_has_scopes(hass, entry):
67  raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
68  entry.runtime_data = session
69 
70  await async_setup_service(hass)
71 
72  return True
73 
74 
75 def async_entry_has_scopes(hass: HomeAssistant, entry: GoogleSheetsConfigEntry) -> bool:
76  """Verify that the config entry desired scope is present in the oauth token."""
77  return DEFAULT_ACCESS in entry.data.get(CONF_TOKEN, {}).get("scope", "").split(" ")
78 
79 
81  hass: HomeAssistant, entry: GoogleSheetsConfigEntry
82 ) -> bool:
83  """Unload a config entry."""
84  loaded_entries = [
85  entry
86  for entry in hass.config_entries.async_entries(DOMAIN)
87  if entry.state == ConfigEntryState.LOADED
88  ]
89  if len(loaded_entries) == 1:
90  for service_name in hass.services.async_services_for_domain(DOMAIN):
91  hass.services.async_remove(DOMAIN, service_name)
92 
93  return True
94 
95 
96 async def async_setup_service(hass: HomeAssistant) -> None:
97  """Add the services for Google Sheets."""
98 
99  def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
100  """Run append in the executor."""
101  service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
102  try:
103  sheet = service.open_by_key(entry.unique_id)
104  except RefreshError:
105  entry.async_start_reauth(hass)
106  raise
107  except APIError as ex:
108  raise HomeAssistantError("Failed to write data") from ex
109 
110  worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
111  columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
112  now = str(datetime.now())
113  rows = []
114  for d in call.data[DATA]:
115  row_data = {"created": now} | d
116  row = [row_data.get(column, "") for column in columns]
117  for key, value in row_data.items():
118  if key not in columns:
119  columns.append(key)
120  worksheet.update_cell(1, len(columns), key)
121  row.append(value)
122  rows.append(row)
123  worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
124 
125  async def append_to_sheet(call: ServiceCall) -> None:
126  """Append new line of data to a Google Sheets document."""
127  entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry(
128  call.data[DATA_CONFIG_ENTRY]
129  )
130  if not entry or not hasattr(entry, "runtime_data"):
131  raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
132  await entry.runtime_data.async_ensure_token_valid()
133  await hass.async_add_executor_job(_append_to_sheet, call, entry)
134 
135  hass.services.async_register(
136  DOMAIN,
137  SERVICE_APPEND_SHEET,
138  append_to_sheet,
139  schema=SHEET_SERVICE_SCHEMA,
140  )
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_service(HomeAssistant hass)
Definition: __init__.py:96
bool async_entry_has_scopes(HomeAssistant hass, GoogleSheetsConfigEntry entry)
Definition: __init__.py:75
bool async_setup_entry(HomeAssistant hass, GoogleSheetsConfigEntry entry)
Definition: __init__.py:51
bool async_unload_entry(HomeAssistant hass, GoogleSheetsConfigEntry entry)
Definition: __init__.py:82
AbstractOAuth2Implementation async_get_config_entry_implementation(HomeAssistant hass, config_entries.ConfigEntry config_entry)