Skip to content

Commit 278204a

Browse files
authored
Merge pull request #31 from raman325/config_flow
Add config flow support
2 parents 4f790e9 + fe616d8 commit 278204a

File tree

12 files changed

+237
-35
lines changed

12 files changed

+237
-35
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ icon
33
venv
44
hass-custom-pyscript.zip
55
.coverage
6+
.vscode
67
.*.swp

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,11 @@ this [README](https://github.com/craigbarratt/hass-pyscript-jupyter/blob/master/
5959

6060
## Configuration
6161

62-
* Add `pyscript:` to `<config>/configuration.yaml`; pyscript has one optional
63-
configuration parameter that allows any python package to be imported if set, eg:
62+
* 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:
6463
```yaml
6564
pyscript:
6665
allow_all_imports: true
6766
```
68-
* Create the folder `<config>/pyscript`
6967
* Add files with a suffix of `.py` in the folder `<config>/pyscript`.
7068
* Restart HASS.
7169
* Whenever you change a script file, make a `reload` service call to `pyscript`.

custom_components/pyscript/__init__.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import voluptuous as vol
99

1010
from homeassistant.config import async_hass_config_yaml, async_process_component_config
11+
from homeassistant.config_entries import SOURCE_IMPORT
1112
from homeassistant.const import (
1213
EVENT_HOMEASSISTANT_STARTED,
1314
EVENT_HOMEASSISTANT_STOP,
@@ -18,7 +19,7 @@
1819
import homeassistant.helpers.config_validation as cv
1920
from homeassistant.loader import async_get_integration, bind_hass
2021

21-
from .const import DOMAIN, FOLDER, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START
22+
from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN, FOLDER, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START
2223
from .eval import AstEval
2324
from .event import Event
2425
from .function import Function
@@ -29,20 +30,27 @@
2930

3031
_LOGGER = logging.getLogger(LOGGER_PATH)
3132

32-
CONF_ALLOW_ALL_IMPORTS = "allow_all_imports"
33-
34-
CONFIG_SCHEMA = vol.Schema(
35-
{
36-
DOMAIN: vol.Schema(
37-
{vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): cv.boolean}, extra=vol.ALLOW_EXTRA,
38-
)
39-
},
40-
extra=vol.ALLOW_EXTRA,
33+
PYSCRIPT_SCHEMA = vol.Schema(
34+
{vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): cv.boolean}, extra=vol.ALLOW_EXTRA,
4135
)
4236

37+
CONFIG_SCHEMA = vol.Schema({DOMAIN: PYSCRIPT_SCHEMA}, extra=vol.ALLOW_EXTRA)
38+
4339

4440
async def async_setup(hass, config):
45-
"""Initialize the pyscript component."""
41+
"""Component setup, run import config flow for each entry in config."""
42+
if DOMAIN in config:
43+
hass.async_create_task(
44+
hass.config_entries.flow.async_init(
45+
DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN]
46+
)
47+
)
48+
49+
return True
50+
51+
52+
async def async_setup_entry(hass, config_entry):
53+
"""Initialize the pyscript config entry."""
4654
Function.init(hass)
4755
Event.init(hass)
4856
TrigTime.init(hass)
@@ -52,19 +60,16 @@ async def async_setup(hass, config):
5260

5361
pyscript_folder = hass.config.path(FOLDER)
5462

55-
def check_isdir(path):
56-
return os.path.isdir(path)
57-
58-
if not await hass.async_add_executor_job(check_isdir, pyscript_folder):
59-
_LOGGER.error("Folder %s not found in configuration folder", FOLDER)
60-
return False
63+
if not await hass.async_add_executor_job(os.path.isdir, pyscript_folder):
64+
_LOGGER.debug("Folder %s not found in configuration folder, creating it", FOLDER)
65+
await hass.async_add_executor_job(os.makedirs, pyscript_folder)
6166

6267
hass.data.setdefault(DOMAIN, {})
63-
hass.data[DOMAIN]["allow_all_imports"] = config[DOMAIN].get(CONF_ALLOW_ALL_IMPORTS)
68+
hass.data[DOMAIN][CONF_ALLOW_ALL_IMPORTS] = config_entry.data.get(CONF_ALLOW_ALL_IMPORTS)
6469

65-
State.set_pyscript_config(config.get(DOMAIN, {}))
70+
State.set_pyscript_config(config_entry.data)
6671

67-
await load_scripts(hass, config)
72+
await load_scripts(hass, config_entry.data)
6873

6974
async def reload_scripts_handler(call):
7075
"""Handle reload service calls."""
@@ -176,6 +181,12 @@ async def stop_triggers(event):
176181
return True
177182

178183

184+
async def async_unload_entry(hass, config_entry):
185+
"""Unload a config entry."""
186+
hass.data.pop(DOMAIN)
187+
return True
188+
189+
179190
@bind_hass
180191
async def load_scripts(hass, config):
181192
"""Load all python scripts in FOLDER."""
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Config flow for pyscript."""
2+
import voluptuous as vol
3+
4+
from homeassistant import config_entries
5+
6+
from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN
7+
8+
PYSCRIPT_SCHEMA = vol.Schema(
9+
{vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): bool}, extra=vol.ALLOW_EXTRA,
10+
)
11+
12+
13+
class PyscriptConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
14+
"""Handle a pyscript config flow."""
15+
16+
VERSION = 1
17+
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
18+
19+
async def async_step_user(self, user_input):
20+
"""Handle a flow initialized by the user."""
21+
if user_input is not None:
22+
if len(self.hass.config_entries.async_entries(DOMAIN)) > 0:
23+
return self.async_abort(reason="single_instance_allowed")
24+
25+
await self.async_set_unique_id(DOMAIN)
26+
return self.async_create_entry(title=DOMAIN, data=user_input)
27+
28+
return self.async_show_form(step_id="user", data_schema=PYSCRIPT_SCHEMA)
29+
30+
async def async_step_import(self, import_config):
31+
"""Import a config entry from configuration.yaml."""
32+
# Check if import config entry matches any existing config entries
33+
# so we can update it if necessary
34+
entries = self.hass.config_entries.async_entries(DOMAIN)
35+
if entries:
36+
entry = entries[0]
37+
if entry.data.get(CONF_ALLOW_ALL_IMPORTS, False) != import_config.get(
38+
CONF_ALLOW_ALL_IMPORTS, False
39+
):
40+
updated_data = entry.data.copy()
41+
updated_data[CONF_ALLOW_ALL_IMPORTS] = import_config.get(CONF_ALLOW_ALL_IMPORTS, False)
42+
self.hass.config_entries.async_update_entry(entry=entry, data=updated_data)
43+
await self.hass.config_entries.async_reload(entry.entry_id)
44+
return self.async_abort(reason="updated_entry")
45+
46+
return self.async_abort(reason="already_configured_service")
47+
48+
return await self.async_step_user(user_input=import_config)

custom_components/pyscript/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
FOLDER = "pyscript"
66

7+
CONF_ALLOW_ALL_IMPORTS = "allow_all_imports"
8+
79
SERVICE_JUPYTER_KERNEL_START = "jupyter_kernel_start"
810

911
LOGGER_PATH = "custom_components.pyscript"

custom_components/pyscript/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "pyscript",
33
"name": "Pyscript Python scripting",
4-
"config_flow": false,
4+
"config_flow": true,
55
"documentation": "https://github.com/custom-components/pyscript",
66
"issue_tracker": "https://github.com/custom-components/pyscript/issues",
77
"requirements": ["croniter==0.3.34"],
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"config": {
3+
"step": {
4+
"user": {
5+
"title": "pyscript",
6+
"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.",
7+
"data": {
8+
"allow_all_imports": "Allow All Imports?"
9+
}
10+
}
11+
},
12+
"abort": {
13+
"already_configured_service": "[%key:common::config_flow::abort::already_configured_service%]",
14+
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
15+
"updated_entry": "This entry has already been setup but the configuration has been updated."
16+
}
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"config": {
3+
"step": {
4+
"user": {
5+
"title": "pyscript",
6+
"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.",
7+
"data": {
8+
"allow_all_imports": "Allow All Imports?"
9+
}
10+
}
11+
},
12+
"abort": {
13+
"already_configured_service": "[%key:common::config_flow::abort::already_configured_service%]",
14+
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
15+
"updated_entry": "This entry has already been setup but the configuration has been updated."
16+
}
17+
}
18+
}

docs/configuration.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
Configuration
22
=============
33

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

89
.. code:: yaml
910
1011
pyscript:
1112
allow_all_imports: true
1213
13-
- Create the folder ``<config>/pyscript``
1414
- Add files with a suffix of ``.py`` in the folder ``<config>/pyscript``.
1515
- Restart HASS.
1616
- Whenever you change a script file, make a ``reload`` service call to ``pyscript``.

info.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ See the documentation if you want to install pyscript manually.
2323

2424
## Configuration
2525

26-
* Add `pyscript:` to `<config>/configuration.yaml`; there is one optional parameter (see docs)
27-
* Create the folder `<config>/pyscript`
26+
* 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)
2827
* Add files with a suffix of `.py` in the folder `<config>/pyscript`.
2928
* Whenever you change a script file, make a `reload` service call to `pyscript`.
3029
* Watch the HASS log for `pyscript` errors and logger output from your scripts.

tests/test_config_flow.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Tests for pyscript config flow."""
2+
import logging
3+
4+
from custom_components.pyscript import PYSCRIPT_SCHEMA
5+
from custom_components.pyscript.const import CONF_ALLOW_ALL_IMPORTS, DOMAIN
6+
import pytest
7+
from pytest_homeassistant.async_mock import patch
8+
9+
from homeassistant import data_entry_flow
10+
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
11+
12+
_LOGGER = logging.getLogger(__name__)
13+
14+
15+
@pytest.fixture(name="pyscript_bypass_setup", autouse=True)
16+
def pyscript_bypass_setup_fixture():
17+
"""Mock component setup."""
18+
with patch("custom_components.pyscript.async_setup_entry", return_value=True):
19+
yield
20+
21+
22+
async def test_user_flow_minimum_fields(hass):
23+
"""Test user config flow with minimum fields."""
24+
# test form shows
25+
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
26+
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
27+
assert result["step_id"] == "user"
28+
29+
result = await hass.config_entries.flow.async_configure(result["flow_id"], user_input={})
30+
31+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
32+
assert CONF_ALLOW_ALL_IMPORTS in result["data"]
33+
assert not result["data"][CONF_ALLOW_ALL_IMPORTS]
34+
35+
36+
async def test_user_flow_all_fields(hass):
37+
"""Test user config flow with all fields."""
38+
# test form shows
39+
result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_USER})
40+
41+
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
42+
assert result["step_id"] == "user"
43+
44+
result = await hass.config_entries.flow.async_configure(
45+
result["flow_id"], user_input={CONF_ALLOW_ALL_IMPORTS: True}
46+
)
47+
48+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
49+
assert CONF_ALLOW_ALL_IMPORTS in result["data"]
50+
assert result["data"][CONF_ALLOW_ALL_IMPORTS]
51+
52+
53+
async def test_user_already_configured(hass):
54+
"""Test service is already configured during user setup."""
55+
result = await hass.config_entries.flow.async_init(
56+
DOMAIN, context={"source": SOURCE_USER}, data={CONF_ALLOW_ALL_IMPORTS: True}
57+
)
58+
59+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
60+
61+
result = await hass.config_entries.flow.async_init(
62+
DOMAIN, context={"source": SOURCE_USER}, data={CONF_ALLOW_ALL_IMPORTS: True}
63+
)
64+
65+
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
66+
assert result["reason"] == "single_instance_allowed"
67+
68+
69+
async def test_import_flow(hass, pyscript_bypass_setup):
70+
"""Test import config flow works."""
71+
result = await hass.config_entries.flow.async_init(
72+
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
73+
)
74+
75+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
76+
77+
78+
async def test_import_flow_update_entry(hass):
79+
"""Test import config flow updates existing entry."""
80+
result = await hass.config_entries.flow.async_init(
81+
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
82+
)
83+
84+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
85+
86+
result = await hass.config_entries.flow.async_init(
87+
DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ALLOW_ALL_IMPORTS: True}
88+
)
89+
90+
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
91+
assert result["reason"] == "updated_entry"
92+
93+
94+
async def test_import_flow_no_update(hass):
95+
"""Test import config flow doesn't update existing entry when data is same."""
96+
result = await hass.config_entries.flow.async_init(
97+
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
98+
)
99+
100+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
101+
102+
result = await hass.config_entries.flow.async_init(
103+
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({})
104+
)
105+
106+
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
107+
assert result["reason"] == "already_configured_service"

tests/test_init.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ async def wait_until_done(notify_q):
6565
return await asyncio.wait_for(notify_q.get(), timeout=4)
6666

6767

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

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

82-
assert not res
83-
assert "Folder pyscript not found in configuration folder" in caplog.text
82+
assert res
83+
assert makedirs_call.called
8484

8585

8686
async def test_service_exists(hass, caplog):

0 commit comments

Comments
 (0)