mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Add OAuth2 config flow scaffold (#28220)
* Add OAuth2 scaffold * Generate integration if non-existing domain specified * Update URL
This commit is contained in:
parent
e700384cce
commit
24c29f9227
@ -40,6 +40,8 @@ CONF_BELOW = "below"
|
||||
CONF_BINARY_SENSORS = "binary_sensors"
|
||||
CONF_BLACKLIST = "blacklist"
|
||||
CONF_BRIGHTNESS = "brightness"
|
||||
CONF_CLIENT_ID = "client_id"
|
||||
CONF_CLIENT_SECRET = "client_secret"
|
||||
CONF_CODE = "code"
|
||||
CONF_COLOR_TEMP = "color_temp"
|
||||
CONF_COMMAND = "command"
|
||||
|
@ -48,34 +48,46 @@ def main():
|
||||
args = get_arguments()
|
||||
|
||||
info = gather_info.gather_info(args)
|
||||
print()
|
||||
|
||||
generate.generate(args.template, info)
|
||||
# If we are calling scaffold on a non-existing integration,
|
||||
# We're going to first make it. If we're making an integration,
|
||||
# we will also make a config flow to go with it.
|
||||
|
||||
# If creating new integration, create config flow too
|
||||
if args.template == "integration":
|
||||
if info.authentication or not info.discoverable:
|
||||
template = "config_flow"
|
||||
else:
|
||||
template = "config_flow_discovery"
|
||||
if info.is_new:
|
||||
generate.generate("integration", info)
|
||||
|
||||
generate.generate(template, info)
|
||||
# If it's a new integration and it's not a config flow,
|
||||
# create a config flow too.
|
||||
if not args.template.startswith("config_flow"):
|
||||
if info.oauth2:
|
||||
template = "config_flow_oauth2"
|
||||
elif info.authentication or not info.discoverable:
|
||||
template = "config_flow"
|
||||
else:
|
||||
template = "config_flow_discovery"
|
||||
|
||||
generate.generate(template, info)
|
||||
|
||||
# If we wanted a new integration, we've already done our work.
|
||||
if args.template != "integration":
|
||||
generate.generate(args.template, info)
|
||||
|
||||
pipe_null = "" if args.develop else "> /dev/null"
|
||||
|
||||
print("Running hassfest to pick up new information.")
|
||||
subprocess.run("python -m script.hassfest", shell=True)
|
||||
subprocess.run(f"python -m script.hassfest {pipe_null}", shell=True)
|
||||
print()
|
||||
|
||||
print("Running tests")
|
||||
print(f"$ pytest -vvv tests/components/{info.domain}")
|
||||
if (
|
||||
subprocess.run(
|
||||
f"pytest -vvv tests/components/{info.domain}", shell=True
|
||||
).returncode
|
||||
!= 0
|
||||
):
|
||||
return 1
|
||||
print("Running gen_requirements_all to pick up new information.")
|
||||
subprocess.run(f"python -m script.gen_requirements_all {pipe_null}", shell=True)
|
||||
print()
|
||||
|
||||
print(f"Done!")
|
||||
if args.develop:
|
||||
print("Running tests")
|
||||
print(f"$ pytest -vvv tests/components/{info.domain}")
|
||||
subprocess.run(f"pytest -vvv tests/components/{info.domain}", shell=True)
|
||||
print()
|
||||
|
||||
docs.print_relevant_docs(args.template, info)
|
||||
|
||||
|
@ -2,72 +2,76 @@
|
||||
from .model import Info
|
||||
|
||||
|
||||
DATA = {
|
||||
"config_flow": {
|
||||
"title": "Config Flow",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html",
|
||||
},
|
||||
"config_flow_discovery": {
|
||||
"title": "Discoverable Config Flow",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication",
|
||||
},
|
||||
"config_flow_oauth2": {
|
||||
"title": "OAuth2 Config Flow",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/next/config_entries_config_flow_handler.html#configuration-via-oauth2",
|
||||
},
|
||||
"device_action": {
|
||||
"title": "Device Action",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/device_automation_action.html",
|
||||
},
|
||||
"device_condition": {
|
||||
"title": "Device Condition",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/device_automation_condition.html",
|
||||
},
|
||||
"device_trigger": {
|
||||
"title": "Device Trigger",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/device_automation_trigger.html",
|
||||
},
|
||||
"integration": {
|
||||
"title": "Integration",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html",
|
||||
},
|
||||
"reproduce_state": {
|
||||
"title": "Reproduce State",
|
||||
"docs": "https://developers.home-assistant.io/docs/en/reproduce_state_index.html",
|
||||
"extra": "You will now need to update the code to make sure that every attribute that can occur in the state will cause the right service to be called.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def print_relevant_docs(template: str, info: Info) -> None:
|
||||
"""Print relevant docs."""
|
||||
if template == "integration":
|
||||
data = DATA[template]
|
||||
|
||||
print()
|
||||
print("**************************")
|
||||
print()
|
||||
print()
|
||||
print(f"{data['title']} code has been generated")
|
||||
print()
|
||||
if info.files_added:
|
||||
print("Added the following files:")
|
||||
for file in info.files_added:
|
||||
print(f"- {file}")
|
||||
print()
|
||||
|
||||
if info.tests_added:
|
||||
print("Added the following tests:")
|
||||
for file in info.tests_added:
|
||||
print(f"- {file}")
|
||||
print()
|
||||
|
||||
if info.examples_added:
|
||||
print(
|
||||
f"""
|
||||
Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO.
|
||||
|
||||
For a breakdown of each file, check the developer documentation at:
|
||||
https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html
|
||||
"""
|
||||
"Because some files already existed, we added the following example files. Please copy the relevant code to the existing files."
|
||||
)
|
||||
for file in info.examples_added:
|
||||
print(f"- {file}")
|
||||
print()
|
||||
|
||||
elif template == "config_flow":
|
||||
print(
|
||||
f"""
|
||||
The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO.
|
||||
"""
|
||||
)
|
||||
print(
|
||||
f"The next step is to look at the files and deal with all areas marked as TODO."
|
||||
)
|
||||
|
||||
elif template == "reproduce_state":
|
||||
print(
|
||||
f"""
|
||||
Reproduce state code has been added to the {info.domain} integration:
|
||||
- {info.integration_dir / "reproduce_state.py"}
|
||||
- {info.tests_dir / "test_reproduce_state.py"}
|
||||
|
||||
You will now need to update the code to make sure that every attribute
|
||||
that can occur in the state will cause the right service to be called.
|
||||
"""
|
||||
)
|
||||
|
||||
elif template == "device_trigger":
|
||||
print(
|
||||
f"""
|
||||
Device trigger base has been added to the {info.domain} integration:
|
||||
- {info.integration_dir / "device_trigger.py"}
|
||||
- {info.integration_dir / "strings.json"} (translations)
|
||||
- {info.tests_dir / "test_device_trigger.py"}
|
||||
|
||||
You will now need to update the code to make sure that relevant triggers
|
||||
are exposed.
|
||||
"""
|
||||
)
|
||||
|
||||
elif template == "device_condition":
|
||||
print(
|
||||
f"""
|
||||
Device condition base has been added to the {info.domain} integration:
|
||||
- {info.integration_dir / "device_condition.py"}
|
||||
- {info.integration_dir / "strings.json"} (translations)
|
||||
- {info.tests_dir / "test_device_condition.py"}
|
||||
|
||||
You will now need to update the code to make sure that relevant condtions
|
||||
are exposed.
|
||||
"""
|
||||
)
|
||||
|
||||
elif template == "device_action":
|
||||
print(
|
||||
f"""
|
||||
Device action base has been added to the {info.domain} integration:
|
||||
- {info.integration_dir / "device_action.py"}
|
||||
- {info.integration_dir / "strings.json"} (translations)
|
||||
- {info.tests_dir / "test_device_action.py"}
|
||||
|
||||
You will now need to update the code to make sure that relevant services
|
||||
are exposed as actions.
|
||||
"""
|
||||
)
|
||||
if "extra" in data:
|
||||
print(data["extra"])
|
||||
|
@ -13,36 +13,14 @@ CHECK_EMPTY = ["Cannot be empty", lambda value: value]
|
||||
|
||||
def gather_info(arguments) -> Info:
|
||||
"""Gather info."""
|
||||
existing = arguments.template != "integration"
|
||||
|
||||
if arguments.develop:
|
||||
if arguments.integration:
|
||||
info = {"domain": arguments.integration}
|
||||
elif arguments.develop:
|
||||
print("Running in developer mode. Automatically filling in info.")
|
||||
print()
|
||||
|
||||
if existing:
|
||||
if arguments.develop:
|
||||
return _load_existing_integration("develop")
|
||||
|
||||
if arguments.integration:
|
||||
return _load_existing_integration(arguments.integration)
|
||||
|
||||
return gather_existing_integration()
|
||||
|
||||
if arguments.develop:
|
||||
return Info(
|
||||
domain="develop",
|
||||
name="Develop Hub",
|
||||
codeowner="@developer",
|
||||
requirement="aiodevelop==1.2.3",
|
||||
)
|
||||
|
||||
return gather_new_integration()
|
||||
|
||||
|
||||
def gather_new_integration() -> Info:
|
||||
"""Gather info about new integration from user."""
|
||||
return Info(
|
||||
**_gather_info(
|
||||
info = {"domain": "develop"}
|
||||
else:
|
||||
info = _gather_info(
|
||||
{
|
||||
"domain": {
|
||||
"prompt": "What is the domain?",
|
||||
@ -52,84 +30,87 @@ def gather_new_integration() -> Info:
|
||||
"Domains cannot contain spaces or special characters.",
|
||||
lambda value: value == slugify(value),
|
||||
],
|
||||
[
|
||||
"There already is an integration with this domain.",
|
||||
lambda value: not (COMPONENT_DIR / value).exists(),
|
||||
],
|
||||
],
|
||||
},
|
||||
"name": {
|
||||
"prompt": "What is the name of your integration?",
|
||||
"validators": [CHECK_EMPTY],
|
||||
},
|
||||
"codeowner": {
|
||||
"prompt": "What is your GitHub handle?",
|
||||
"validators": [
|
||||
CHECK_EMPTY,
|
||||
[
|
||||
'GitHub handles need to start with an "@"',
|
||||
lambda value: value.startswith("@"),
|
||||
],
|
||||
],
|
||||
},
|
||||
"requirement": {
|
||||
"prompt": "What PyPI package and version do you depend on? Leave blank for none.",
|
||||
"validators": [
|
||||
[
|
||||
"Versions should be pinned using '=='.",
|
||||
lambda value: not value or "==" in value,
|
||||
]
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
info["is_new"] = not (COMPONENT_DIR / info["domain"] / "manifest.json").exists()
|
||||
|
||||
if not info["is_new"]:
|
||||
return _load_existing_integration(info["domain"])
|
||||
|
||||
if arguments.develop:
|
||||
info.update(
|
||||
{
|
||||
"name": "Develop Hub",
|
||||
"codeowner": "@developer",
|
||||
"requirement": "aiodevelop==1.2.3",
|
||||
"oauth2": True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
info.update(gather_new_integration(arguments.template == "integration"))
|
||||
|
||||
return Info(**info)
|
||||
|
||||
|
||||
YES_NO = {
|
||||
"validators": [["Type either 'yes' or 'no'", lambda value: value in ("yes", "no")]],
|
||||
"convertor": lambda value: value == "yes",
|
||||
}
|
||||
|
||||
|
||||
def gather_new_integration(determine_auth: bool) -> Info:
|
||||
"""Gather info about new integration from user."""
|
||||
fields = {
|
||||
"name": {
|
||||
"prompt": "What is the name of your integration?",
|
||||
"validators": [CHECK_EMPTY],
|
||||
},
|
||||
"codeowner": {
|
||||
"prompt": "What is your GitHub handle?",
|
||||
"validators": [
|
||||
CHECK_EMPTY,
|
||||
[
|
||||
'GitHub handles need to start with an "@"',
|
||||
lambda value: value.startswith("@"),
|
||||
],
|
||||
],
|
||||
},
|
||||
"requirement": {
|
||||
"prompt": "What PyPI package and version do you depend on? Leave blank for none.",
|
||||
"validators": [
|
||||
[
|
||||
"Versions should be pinned using '=='.",
|
||||
lambda value: not value or "==" in value,
|
||||
]
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
if determine_auth:
|
||||
fields.update(
|
||||
{
|
||||
"authentication": {
|
||||
"prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)",
|
||||
"default": "yes",
|
||||
"validators": [
|
||||
[
|
||||
"Type either 'yes' or 'no'",
|
||||
lambda value: value in ("yes", "no"),
|
||||
]
|
||||
],
|
||||
"convertor": lambda value: value == "yes",
|
||||
**YES_NO,
|
||||
},
|
||||
"discoverable": {
|
||||
"prompt": "Is the device/service discoverable on the local network? (yes/no)",
|
||||
"default": "no",
|
||||
"validators": [
|
||||
[
|
||||
"Type either 'yes' or 'no'",
|
||||
lambda value: value in ("yes", "no"),
|
||||
]
|
||||
],
|
||||
"convertor": lambda value: value == "yes",
|
||||
**YES_NO,
|
||||
},
|
||||
"oauth2": {
|
||||
"prompt": "Can the user authenticate the device using OAuth2? (yes/no)",
|
||||
"default": "no",
|
||||
**YES_NO,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def gather_existing_integration() -> Info:
|
||||
"""Gather info about existing integration from user."""
|
||||
answers = _gather_info(
|
||||
{
|
||||
"domain": {
|
||||
"prompt": "What is the domain?",
|
||||
"validators": [
|
||||
CHECK_EMPTY,
|
||||
[
|
||||
"Domains cannot contain spaces or special characters.",
|
||||
lambda value: value == slugify(value),
|
||||
],
|
||||
[
|
||||
"This integration does not exist.",
|
||||
lambda value: (COMPONENT_DIR / value).exists(),
|
||||
],
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return _load_existing_integration(answers["domain"])
|
||||
return _gather_info(fields)
|
||||
|
||||
|
||||
def _load_existing_integration(domain) -> Info:
|
||||
@ -179,5 +160,4 @@ def _gather_info(fields) -> dict:
|
||||
value = info["convertor"](value)
|
||||
answers[key] = value
|
||||
|
||||
print()
|
||||
return answers
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Generate an integration."""
|
||||
from pathlib import Path
|
||||
|
||||
from .error import ExitApp
|
||||
from .model import Info
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||
@ -11,8 +10,6 @@ TEMPLATE_TESTS = TEMPLATE_DIR / "tests"
|
||||
|
||||
def generate(template: str, info: Info) -> None:
|
||||
"""Generate a template."""
|
||||
_validate(template, info)
|
||||
|
||||
print(f"Scaffolding {template} for the {info.domain} integration...")
|
||||
_ensure_tests_dir_exists(info)
|
||||
_generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info)
|
||||
@ -21,13 +18,6 @@ def generate(template: str, info: Info) -> None:
|
||||
print()
|
||||
|
||||
|
||||
def _validate(template, info):
|
||||
"""Validate we can run this task."""
|
||||
if template == "config_flow":
|
||||
if (info.integration_dir / "config_flow.py").exists():
|
||||
raise ExitApp(f"Integration {info.domain} already has a config flow.")
|
||||
|
||||
|
||||
def _generate(src_dir, target_dir, info: Info) -> None:
|
||||
"""Generate an integration."""
|
||||
replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name}
|
||||
@ -42,6 +32,20 @@ def _generate(src_dir, target_dir, info: Info) -> None:
|
||||
content = content.replace(to_search, to_replace)
|
||||
|
||||
target_file = target_dir / source_file.relative_to(src_dir)
|
||||
|
||||
# If the target file exists, create our template as EXAMPLE_<filename>.
|
||||
# Exception: If we are creating a new integration, we can end up running integration base
|
||||
# and a config flows on top of one another. In that case, we want to override the files.
|
||||
if not info.is_new and target_file.exists():
|
||||
new_name = f"EXAMPLE_{target_file.name}"
|
||||
print(f"File {target_file} already exists, creating {new_name} instead.")
|
||||
target_file = target_file.parent / new_name
|
||||
info.examples_added.add(target_file)
|
||||
elif src_dir.name == "integration":
|
||||
info.files_added.add(target_file)
|
||||
else:
|
||||
info.tests_added.add(target_file)
|
||||
|
||||
print(f"Writing {target_file}")
|
||||
target_file.write_text(content)
|
||||
|
||||
@ -58,6 +62,11 @@ def _ensure_tests_dir_exists(info: Info) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _append(path: Path, text):
|
||||
"""Append some text to a path."""
|
||||
path.write_text(path.read_text() + text)
|
||||
|
||||
|
||||
def _custom_tasks(template, info) -> None:
|
||||
"""Handle custom tasks for templates."""
|
||||
if template == "integration":
|
||||
@ -68,7 +77,7 @@ def _custom_tasks(template, info) -> None:
|
||||
|
||||
info.update_manifest(**changes)
|
||||
|
||||
if template == "device_trigger":
|
||||
elif template == "device_trigger":
|
||||
info.update_strings(
|
||||
device_automation={
|
||||
**info.strings().get("device_automation", {}),
|
||||
@ -79,7 +88,7 @@ def _custom_tasks(template, info) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
if template == "device_condition":
|
||||
elif template == "device_condition":
|
||||
info.update_strings(
|
||||
device_automation={
|
||||
**info.strings().get("device_automation", {}),
|
||||
@ -90,7 +99,7 @@ def _custom_tasks(template, info) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
if template == "device_action":
|
||||
elif template == "device_action":
|
||||
info.update_strings(
|
||||
device_automation={
|
||||
**info.strings().get("device_automation", {}),
|
||||
@ -101,7 +110,7 @@ def _custom_tasks(template, info) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
if template == "config_flow":
|
||||
elif template == "config_flow":
|
||||
info.update_manifest(config_flow=True)
|
||||
info.update_strings(
|
||||
config={
|
||||
@ -118,7 +127,7 @@ def _custom_tasks(template, info) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
if template == "config_flow_discovery":
|
||||
elif template == "config_flow_discovery":
|
||||
info.update_manifest(config_flow=True)
|
||||
info.update_strings(
|
||||
config={
|
||||
@ -136,19 +145,28 @@ def _custom_tasks(template, info) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
if template in ("config_flow", "config_flow_discovery"):
|
||||
init_file = info.integration_dir / "__init__.py"
|
||||
init_file.write_text(
|
||||
init_file.read_text()
|
||||
+ """
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
\"\"\"Set up a config entry for NEW_NAME.\"\"\"
|
||||
# TODO forward the entry for each platform that you want to set up.
|
||||
# hass.async_create_task(
|
||||
# hass.config_entries.async_forward_entry_setup(entry, "media_player")
|
||||
# )
|
||||
|
||||
return True
|
||||
"""
|
||||
elif template == "config_flow_oauth2":
|
||||
info.update_manifest(config_flow=True)
|
||||
info.update_strings(
|
||||
config={
|
||||
"title": info.name,
|
||||
"step": {
|
||||
"pick_implementation": {"title": "Pick Authentication Method"}
|
||||
},
|
||||
"abort": {
|
||||
"missing_configuration": "The Somfy component is not configured. Please follow the documentation."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": f"Successfully authenticated with {info.name}."
|
||||
},
|
||||
}
|
||||
)
|
||||
_append(
|
||||
info.integration_dir / "const.py",
|
||||
"""
|
||||
|
||||
# TODO Update with your own urls
|
||||
OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize"
|
||||
OAUTH2_TOKEN = "https://www.example.com/auth/token"
|
||||
""",
|
||||
)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Models for scaffolding."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
import attr
|
||||
|
||||
@ -13,10 +14,16 @@ class Info:
|
||||
|
||||
domain: str = attr.ib()
|
||||
name: str = attr.ib()
|
||||
is_new: bool = attr.ib()
|
||||
codeowner: str = attr.ib(default=None)
|
||||
requirement: str = attr.ib(default=None)
|
||||
authentication: str = attr.ib(default=None)
|
||||
discoverable: str = attr.ib(default=None)
|
||||
oauth2: str = attr.ib(default=None)
|
||||
|
||||
files_added: Set[Path] = attr.ib(factory=set)
|
||||
tests_added: Set[Path] = attr.ib(factory=set)
|
||||
examples_added: Set[Path] = attr.ib(factory=set)
|
||||
|
||||
@property
|
||||
def integration_dir(self) -> Path:
|
||||
|
@ -0,0 +1,49 @@
|
||||
"""The NEW_NAME integration."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# TODO List the platforms that you want to support.
|
||||
# For your initial PR, limit it to 1 platform.
|
||||
PLATFORMS = ["light"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the NEW_NAME component."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Somfy from a config entry."""
|
||||
# TODO Store an API object for your platforms to access
|
||||
# hass.data[DOMAIN][entry.entry_id] = MyApi(…)
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
@ -0,0 +1,49 @@
|
||||
"""The NEW_NAME integration."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# TODO List the platforms that you want to support.
|
||||
# For your initial PR, limit it to 1 platform.
|
||||
PLATFORMS = ["light"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the NEW_NAME component."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Somfy from a config entry."""
|
||||
# TODO Store an API object for your platforms to access
|
||||
# hass.data[DOMAIN][entry.entry_id] = MyApi(…)
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
@ -0,0 +1,94 @@
|
||||
"""The NEW_NAME integration."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
config_entry_oauth2_flow,
|
||||
aiohttp_client,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
from . import api, config_flow
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
# TODO List the platforms that you want to support.
|
||||
# For your initial PR, limit it to 1 platform.
|
||||
PLATFORMS = ["light"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the NEW_NAME component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
config_flow.OAuth2FlowHandler.async_register_implementation(
|
||||
hass,
|
||||
config_entry_oauth2_flow.LocalOAuth2Implementation(
|
||||
hass,
|
||||
DOMAIN,
|
||||
config[DOMAIN][CONF_CLIENT_ID],
|
||||
config[DOMAIN][CONF_CLIENT_SECRET],
|
||||
OAUTH2_AUTHORIZE,
|
||||
OAUTH2_TOKEN,
|
||||
),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Somfy from a config entry."""
|
||||
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
# If using a requests-based API lib
|
||||
hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, entry, session)
|
||||
|
||||
# If using an aiohttp-based API lib
|
||||
hass.data[DOMAIN][entry.entry_id] = api.AsyncConfigEntryAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), session
|
||||
)
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
@ -0,0 +1,58 @@
|
||||
"""API for NEW_NAME bound to HASS OAuth."""
|
||||
from asyncio import run_coroutine_threadsafe
|
||||
|
||||
from aiohttp import ClientSession
|
||||
import my_pypi_package
|
||||
|
||||
from homeassistant import core, config_entries
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
# TODO the following two API examples are based on our suggested best practices
|
||||
# for libraries using OAuth2 with requests or aiohttp. Delete the one you won't use.
|
||||
# For more info see the docs at <insert url>.
|
||||
|
||||
|
||||
class ConfigEntryAuth(my_pypi_package.AbstractAuth):
|
||||
"""Provide NEW_NAME authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: core.HomeAssistant,
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
|
||||
):
|
||||
"""Initialize NEW_NAME Auth."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.session = config_entry_oauth2_flow.OAuth2Session(
|
||||
hass, config_entry, implementation
|
||||
)
|
||||
super().__init__(self.session.token)
|
||||
|
||||
def refresh_tokens(self) -> dict:
|
||||
"""Refresh and return new NEW_NAME tokens using Home Assistant OAuth2 session."""
|
||||
run_coroutine_threadsafe(
|
||||
self.session.async_ensure_token_valid(), self.hass.loop
|
||||
).result()
|
||||
|
||||
return self.session.token
|
||||
|
||||
|
||||
class AsyncConfigEntryAuth(my_pypi_package.AbstractAuth):
|
||||
"""Provide NEW_NAME authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
):
|
||||
"""Initialize NEW_NAME auth."""
|
||||
super().__init__(websession)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self):
|
||||
"""Return a valid access token."""
|
||||
if not self._oauth_session.is_valid:
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return self._oauth_session.token
|
@ -0,0 +1,23 @@
|
||||
"""Config flow for NEW_NAME."""
|
||||
import logging
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OAuth2FlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
):
|
||||
"""Config flow to handle NEW_NAME OAuth2 authentication."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
# TODO Pick one from config_entries.CONN_CLASS_*
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
@ -0,0 +1,60 @@
|
||||
"""Test the NEW_NAME config flow."""
|
||||
from homeassistant import config_entries, setup, data_entry_flow
|
||||
from homeassistant.components.NEW_DOMAIN.const import (
|
||||
DOMAIN,
|
||||
OAUTH2_AUTHORIZE,
|
||||
OAUTH2_TOKEN,
|
||||
)
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
|
||||
|
||||
async def test_full_flow(hass, aiohttp_client, aioclient_mock):
|
||||
"""Check full flow."""
|
||||
assert await setup.async_setup_component(
|
||||
hass,
|
||||
"NEW_DOMAIN",
|
||||
{
|
||||
"NEW_DOMAIN": {
|
||||
"type": "oauth2",
|
||||
"client_id": CLIENT_ID,
|
||||
"client_secret": CLIENT_SECRET,
|
||||
},
|
||||
"http": {"base_url": "https://example.com"},
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"NEW_DOMAIN", context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}"
|
||||
)
|
||||
|
||||
client = await aiohttp_client(hass.http.app)
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
assert resp.status == 200
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert entry.data["type"] == "oauth2"
|
@ -1,12 +1,14 @@
|
||||
"""The NEW_NAME integration."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}})
|
||||
CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the NEW_NAME integration."""
|
||||
return True
|
||||
|
Loading…
x
Reference in New Issue
Block a user