Skip to content

Add config flow support #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ icon
venv
hass-custom-pyscript.zip
.coverage
.vscode
.*.swp
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,11 @@ this [README](https://github.com/craigbarratt/hass-pyscript-jupyter/blob/master/

## Configuration

* Add `pyscript:` to `<config>/configuration.yaml`; pyscript has one optional
configuration parameter that allows any python package to be imported if set, eg:
* Go to the Integrations menu in the Home Assistant Configuration UI and add `Pyscript Python scripting` from there, or add `pyscript:` to `<config>/configuration.yaml`; pyscript has one optional configuration parameter that allows any python package to be imported if set, eg:
```yaml
pyscript:
allow_all_imports: true
```
* Create the folder `<config>/pyscript`
* Add files with a suffix of `.py` in the folder `<config>/pyscript`.
* Restart HASS.
* Whenever you change a script file, make a `reload` service call to `pyscript`.
Expand Down
51 changes: 31 additions & 20 deletions custom_components/pyscript/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import voluptuous as vol

from homeassistant.config import async_hass_config_yaml, async_process_component_config
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
Expand All @@ -18,7 +19,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import async_get_integration, bind_hass

from .const import DOMAIN, FOLDER, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START
from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN, FOLDER, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START
from .eval import AstEval
from .event import Event
from .function import Function
Expand All @@ -29,20 +30,27 @@

_LOGGER = logging.getLogger(LOGGER_PATH)

CONF_ALLOW_ALL_IMPORTS = "allow_all_imports"

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): cv.boolean}, extra=vol.ALLOW_EXTRA,
)
},
extra=vol.ALLOW_EXTRA,
PYSCRIPT_SCHEMA = vol.Schema(
{vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): cv.boolean}, extra=vol.ALLOW_EXTRA,
)

CONFIG_SCHEMA = vol.Schema({DOMAIN: PYSCRIPT_SCHEMA}, extra=vol.ALLOW_EXTRA)


async def async_setup(hass, config):
"""Initialize the pyscript component."""
"""Component setup, run import config flow for each entry in config."""
if DOMAIN in config:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
)
)

return True


async def async_setup_entry(hass, config_entry):
"""Initialize the pyscript config entry."""
Function.init(hass)
Event.init(hass)
TrigTime.init(hass)
Expand All @@ -52,19 +60,16 @@ async def async_setup(hass, config):

pyscript_folder = hass.config.path(FOLDER)

def check_isdir(path):
return os.path.isdir(path)

if not await hass.async_add_executor_job(check_isdir, pyscript_folder):
_LOGGER.error("Folder %s not found in configuration folder", FOLDER)
return False
if not await hass.async_add_executor_job(os.path.isdir, pyscript_folder):
_LOGGER.debug("Folder %s not found in configuration folder, creating it", FOLDER)
await hass.async_add_executor_job(os.makedirs, pyscript_folder)

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN]["allow_all_imports"] = config[DOMAIN].get(CONF_ALLOW_ALL_IMPORTS)
hass.data[DOMAIN][CONF_ALLOW_ALL_IMPORTS] = config_entry.data.get(CONF_ALLOW_ALL_IMPORTS)

State.set_pyscript_config(config.get(DOMAIN, {}))
State.set_pyscript_config(config_entry.data)

await load_scripts(hass, config)
await load_scripts(hass, config_entry.data)

async def reload_scripts_handler(call):
"""Handle reload service calls."""
Expand Down Expand Up @@ -176,6 +181,12 @@ async def stop_triggers(event):
return True


async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
hass.data.pop(DOMAIN)
return True


@bind_hass
async def load_scripts(hass, config):
"""Load all python scripts in FOLDER."""
Expand Down
48 changes: 48 additions & 0 deletions custom_components/pyscript/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Config flow for pyscript."""
import voluptuous as vol

from homeassistant import config_entries

from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN

PYSCRIPT_SCHEMA = vol.Schema(
{vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): bool}, extra=vol.ALLOW_EXTRA,
)


class PyscriptConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a pyscript config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

async def async_step_user(self, user_input):
"""Handle a flow initialized by the user."""
if user_input is not None:
if len(self.hass.config_entries.async_entries(DOMAIN)) > 0:
return self.async_abort(reason="single_instance_allowed")

await self.async_set_unique_id(DOMAIN)
return self.async_create_entry(title=DOMAIN, data=user_input)

return self.async_show_form(step_id="user", data_schema=PYSCRIPT_SCHEMA)

async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
# Check if import config entry matches any existing config entries
# so we can update it if necessary
entries = self.hass.config_entries.async_entries(DOMAIN)
if entries:
entry = entries[0]
if entry.data.get(CONF_ALLOW_ALL_IMPORTS, False) != import_config.get(
CONF_ALLOW_ALL_IMPORTS, False
):
updated_data = entry.data.copy()
updated_data[CONF_ALLOW_ALL_IMPORTS] = import_config.get(CONF_ALLOW_ALL_IMPORTS, False)
self.hass.config_entries.async_update_entry(entry=entry, data=updated_data)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="updated_entry")

return self.async_abort(reason="already_configured_service")

return await self.async_step_user(user_input=import_config)
2 changes: 2 additions & 0 deletions custom_components/pyscript/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

FOLDER = "pyscript"

CONF_ALLOW_ALL_IMPORTS = "allow_all_imports"

SERVICE_JUPYTER_KERNEL_START = "jupyter_kernel_start"

LOGGER_PATH = "custom_components.pyscript"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/pyscript/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "pyscript",
"name": "Pyscript Python scripting",
"config_flow": false,
"config_flow": true,
"documentation": "https://github.com/custom-components/pyscript",
"issue_tracker": "https://github.com/custom-components/pyscript/issues",
"requirements": ["croniter==0.3.34"],
Expand Down
18 changes: 18 additions & 0 deletions custom_components/pyscript/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"step": {
"user": {
"title": "pyscript",
"description": "Once you have created an entry, refer to the [docs](https://hacs-pyscript.readthedocs.io/en/latest/) to learn how to create scripts and functions.",
"data": {
"allow_all_imports": "Allow All Imports?"
}
}
},
"abort": {
"already_configured_service": "[%key:common::config_flow::abort::already_configured_service%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"updated_entry": "This entry has already been setup but the configuration has been updated."
}
}
}
18 changes: 18 additions & 0 deletions custom_components/pyscript/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"step": {
"user": {
"title": "pyscript",
"description": "Once you have created an entry, refer to the [docs](https://hacs-pyscript.readthedocs.io/en/latest/) to learn how to create scripts and functions.",
"data": {
"allow_all_imports": "Allow All Imports?"
}
}
},
"abort": {
"already_configured_service": "[%key:common::config_flow::abort::already_configured_service%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"updated_entry": "This entry has already been setup but the configuration has been updated."
}
}
}
8 changes: 4 additions & 4 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
Configuration
=============

- Add ``pyscript:`` to ``<config>/configuration.yaml``; pyscript has
one optional configuration parameter that allows any python package
to be imported if set, eg:
- Go to the Integrations menu in the Home Assistant Configuration UI and add
``Pyscript Python scripting`` from there, or add ``pyscript:`` to
``<config>/configuration.yaml``; pyscript has one optional configuration
parameter that allows any python package to be imported if set, eg:

.. code:: yaml

pyscript:
allow_all_imports: true

- Create the folder ``<config>/pyscript``
- Add files with a suffix of ``.py`` in the folder ``<config>/pyscript``.
- Restart HASS.
- Whenever you change a script file, make a ``reload`` service call to ``pyscript``.
Expand Down
3 changes: 1 addition & 2 deletions info.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ See the documentation if you want to install pyscript manually.

## Configuration

* Add `pyscript:` to `<config>/configuration.yaml`; there is one optional parameter (see docs)
* Create the folder `<config>/pyscript`
* Go to the Integrations menu in the Home Assistant Configuration UI and add `Pyscript Python scripting` from there, or add `pyscript:` to `<config>/configuration.yaml`; there is one optional parameter (see docs)
* Add files with a suffix of `.py` in the folder `<config>/pyscript`.
* Whenever you change a script file, make a `reload` service call to `pyscript`.
* Watch the HASS log for `pyscript` errors and logger output from your scripts.
Expand Down
107 changes: 107 additions & 0 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Tests for pyscript config flow."""
import logging

from custom_components.pyscript import PYSCRIPT_SCHEMA
from custom_components.pyscript.const import CONF_ALLOW_ALL_IMPORTS, DOMAIN
import pytest
from pytest_homeassistant.async_mock import patch

from homeassistant import data_entry_flow
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER

_LOGGER = logging.getLogger(__name__)


@pytest.fixture(name="pyscript_bypass_setup", autouse=True)
def pyscript_bypass_setup_fixture():
"""Mock component setup."""
with patch("custom_components.pyscript.async_setup_entry", return_value=True):
yield


async def test_user_flow_minimum_fields(hass):
"""Test user config flow with minimum fields."""
# test form shows
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.flow.async_configure(result["flow_id"], user_input={})

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert CONF_ALLOW_ALL_IMPORTS in result["data"]
assert not result["data"][CONF_ALLOW_ALL_IMPORTS]


async def test_user_flow_all_fields(hass):
"""Test user config flow with all fields."""
# test form shows
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})

assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"

result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_ALLOW_ALL_IMPORTS: True}
)

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert CONF_ALLOW_ALL_IMPORTS in result["data"]
assert result["data"][CONF_ALLOW_ALL_IMPORTS]


async def test_user_already_configured(hass):
"""Test service is already configured during user setup."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_ALLOW_ALL_IMPORTS: True}
)

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_ALLOW_ALL_IMPORTS: True}
)

assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"


async def test_import_flow(hass, pyscript_bypass_setup):
"""Test import config flow works."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
)

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY


async def test_import_flow_update_entry(hass):
"""Test import config flow updates existing entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
)

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ALLOW_ALL_IMPORTS: True}
)

assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "updated_entry"


async def test_import_flow_no_update(hass):
"""Test import config flow doesn't update existing entry when data is same."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
)

assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY

result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
)

assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured_service"
10 changes: 5 additions & 5 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ async def wait_until_done(notify_q):
return await asyncio.wait_for(notify_q.get(), timeout=4)


async def test_setup_fails_on_no_dir(hass, caplog):
"""Test we fail setup when no dir found."""
async def test_setup_makedirs_on_no_dir(hass, caplog):
"""Test setup calls os.makedirs when no dir found."""
integration = loader.Integration(
hass,
"custom_components.pyscript",
Expand All @@ -76,11 +76,11 @@ async def test_setup_fails_on_no_dir(hass, caplog):

with patch("homeassistant.loader.async_get_integration", return_value=integration), patch(
"custom_components.pyscript.os.path.isdir", return_value=False
):
), patch("custom_components.pyscript.os.makedirs") as makedirs_call:
res = await async_setup_component(hass, "pyscript", {DOMAIN: {}})

assert not res
assert "Folder pyscript not found in configuration folder" in caplog.text
assert res
assert makedirs_call.called


async def test_service_exists(hass, caplog):
Expand Down