diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ef68293..70416d5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,32 +1,30 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "image": "ghcr.io/ludeeus/devcontainer/integration:stable", "name": "TP-Link Deco integration development", - "context": "..", - "appPort": ["9123:8123"], - "postCreateCommand": ".devcontainer/setup.sh", - "mounts": [ - "source=${localEnv:HOME}${localEnv:USERPROFILE}/.gitconfig,target=/root/.gitconfig,type=bind,consistency=cached" - ], - "extensions": [ - "ms-python.python", - "github.vscode-pull-request-github", - "ryanluker.vscode-coverage-gutters", - "ms-python.vscode-pylance", - "esbenp.prettier-vscode" - ], - "settings": { - "files.eol": "\n", - "editor.tabSize": 4, - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/bin/python3", - "python.analysis.autoSearchPaths": false, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true + "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye", + "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", + "postAttachCommand": ".devcontainer/setup.sh", + "forwardPorts": [8123], + "portsAttributes": { + "8123": { + "label": "Home Assistant", + "onAutoForward": "notify" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "esbenp.prettier-vscode", + "github.vscode-pull-request-github", + "ms-vscode-remote.remote-containers", + "ms-python.black-formatter", + "ms-python.vscode-pylance", + "ryanluker.vscode-coverage-gutters" + ] + } + }, + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/rust:1": {} } } diff --git a/.devcontainer/develop.sh b/.devcontainer/develop.sh new file mode 100644 index 0000000..89eda50 --- /dev/null +++ b/.devcontainer/develop.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +# Create config dir if not present +if [[ ! -d "${PWD}/config" ]]; then + mkdir -p "${PWD}/config" + hass --config "${PWD}/config" --script ensure_config +fi + +# Set the path to custom_components +## This let's us have the structure we want /custom_components/integration_blueprint +## while at the same time have Home Assistant configuration inside /config +## without resulting to symlinks. +export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components" + +# Start Home Assistant +hass --config "${PWD}/config" --debug diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 63ddc49..593d5bd 100644 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -6,7 +6,5 @@ set -e cd "$(dirname "$0")/.." -pip3 install -r .github/workflows/constraints.txt +pip3 install --requirement requirements.txt pre-commit install - -container install diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 15c7513..eee5550 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,12 +3,11 @@ updates: - package-ecosystem: github-actions directory: "/" schedule: - interval: daily - - package-ecosystem: pip - directory: "/.github/workflows" - schedule: - interval: daily - - package-ecosystem: pip + interval: weekly + - package-ecosystem: "pip" directory: "/" schedule: - interval: daily + interval: weekly + ignore: + # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json + - dependency-name: "homeassistant" diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt deleted file mode 100644 index 1e0e504..0000000 --- a/.github/workflows/constraints.txt +++ /dev/null @@ -1,5 +0,0 @@ -pip==23.2.1 -pre-commit==3.4.0 -black==23.9.1 -flake8==6.1.0 -reorder-python-imports==3.11.0 diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index f807ee1..146d81f 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Dependabot metadata id: metadata - uses: dependabot/fetch-metadata@v1.5.1 + uses: dependabot/fetch-metadata@v1.6.0 with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Enable auto-merge for Dependabot PRs diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 18ccb8d..0410562 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -15,6 +15,6 @@ jobs: uses: actions/checkout@v4 - name: Run Labeler - uses: crazy-max/ghaction-github-labeler@v4.2.0 + uses: crazy-max/ghaction-github-labeler@v5.0.0 with: skip-delete: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 49f39a9..f8d9503 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -10,6 +10,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Run release-drafter - uses: release-drafter/release-drafter@v5.24.0 + uses: release-drafter/release-drafter@v5.25.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 105c759..1ea02e5 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -22,18 +22,18 @@ jobs: uses: actions/checkout@v4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.7.0 + uses: actions/setup-python@v4.7.1 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Upgrade pip run: | - pip install --constraint=.github/workflows/constraints.txt pip + pip install --constraint=requirements.txt pip pip --version - name: Install Python modules run: | - pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports + pip install --constraint=requirements.txt pre-commit black flake8 reorder-python-imports - name: Run pre-commit on all files run: | diff --git a/.gitignore b/.gitignore index a4a4d84..b66d45d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ +# artifacts __pycache__ -pythonenv* -.python-version +.pytest* +*.egg-info +*/build/* +*/dist/* + +# misc .coverage -venv -.venv +coverage.xml + +# Home Assistant configuration +config/* +!config/configuration.yaml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..260b188 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,48 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py310" + +select = [ + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D411", # Missing blank line before section + "E501", # line too long + "E731", # do not assign a lambda expression, use a def +] + +[flake8-pytest-style] +fixture-parentheses = false + +[pyupgrade] +keep-runtime-typing = true + +[mccabe] +max-complexity = 25 diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index cc5337a..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - // Example of attaching to local debug server - "name": "Python: Attach Local", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost", - "pathMappings": [ - { - "localRoot": "${workspaceFolder}", - "remoteRoot": "." - } - ] - }, - { - // Example of attaching to my production server - "name": "Python: Attach Remote", - "type": "python", - "request": "attach", - "port": 5678, - "host": "homeassistant.local", - "pathMappings": [ - { - "localRoot": "${workspaceFolder}", - "remoteRoot": "/usr/src/homeassistant" - } - ] - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 2be6282..48bfb2f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,17 @@ { - "python.linting.pylintArgs": ["--rcfile=setup.cfg"], - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.pythonPath": "venv/bin/python", - "files.associations": { - "*.yaml": "home-assistant" + "analysis.autoSearchPaths": false, + "editor.codeActionsOnSave": { + "source.organizeImports": true }, + "editor.formatOnSave": true, + "editor.tabSize": 4, + "files.eol": "\n", + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash", "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index db41b54..46a87c8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,32 +2,14 @@ "version": "2.0.0", "tasks": [ { - "label": "Run Home Assistant on port 9123", + "label": "Run Home Assistant on port 8123", "type": "shell", - "command": "container start", + "command": ".devcontainer/develop.sh", "problemMatcher": [], "group": { "kind": "build", "isDefault": true } - }, - { - "label": "Run Home Assistant configuration against /config", - "type": "shell", - "command": "container check", - "problemMatcher": [] - }, - { - "label": "Upgrade Home Assistant to latest dev", - "type": "shell", - "command": "container install", - "problemMatcher": [] - }, - { - "label": "Install a specific version of Home Assistant", - "type": "shell", - "command": "container set-version", - "problemMatcher": [] } ] } diff --git a/.devcontainer/configuration.yaml b/config/configuration.yaml similarity index 100% rename from .devcontainer/configuration.yaml rename to config/configuration.yaml diff --git a/custom_components/tplink_deco/__init__.py b/custom_components/tplink_deco/__init__.py index ec436b9..1627412 100644 --- a/custom_components/tplink_deco/__init__.py +++ b/custom_components/tplink_deco/__init__.py @@ -153,6 +153,8 @@ async def async_create_config_data(hass: HomeAssistant, config_entry: ConfigEntr async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up this integration using UI.""" + _LOGGER.debug("async_setup_entry: Config entry %s", config_entry.entry_id) + if hass.data.get(DOMAIN) is None: hass.data.setdefault(DOMAIN, {}) @@ -194,11 +196,13 @@ async def async_reboot_deco(service: ServiceCall) -> None: ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) + return True async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Handle removal of an entry.""" + _LOGGER.debug("async_unload_entry: Config entry %s", config_entry.entry_id) data = hass.data[DOMAIN][config_entry.entry_id] deco_coordinator = data.get(COORDINATOR_DECOS_KEY) clients_coordinator = data.get(COORDINATOR_CLIENTS_KEY) @@ -223,27 +227,14 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Reload config entry.""" + _LOGGER.debug("async_reload_entry: Config entry %s", config_entry) await async_unload_entry(hass, config_entry) await async_setup_entry(hass, config_entry) async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Update options.""" - if not config_entry.options or config_entry.data == config_entry.options: - _LOGGER.debug( - "update_listener: No changes in options for %s", config_entry.entry_id - ) - return - - _LOGGER.debug( - "update_listener: Updating options and reloading %s", config_entry.entry_id - ) - hass.config_entries.async_update_entry( - entry=config_entry, - title=config_entry.options.get(CONF_HOST), - data=config_entry.options, - options={}, - ) + _LOGGER.debug("update_listener: Reloading %s", config_entry.entry_id) await async_reload_entry(hass, config_entry) diff --git a/custom_components/tplink_deco/config_flow.py b/custom_components/tplink_deco/config_flow.py index 1082498..af69379 100644 --- a/custom_components/tplink_deco/config_flow.py +++ b/custom_components/tplink_deco/config_flow.py @@ -1,4 +1,5 @@ """Adds config flow for TP-Link Deco.""" +import asyncio import logging from typing import Any @@ -24,6 +25,8 @@ from .const import CONF_TIMEOUT_ERROR_RETRIES from .const import CONF_TIMEOUT_SECONDS from .const import CONF_VERIFY_SSL +from .const import COORDINATOR_CLIENTS_KEY +from .const import COORDINATOR_DECOS_KEY from .const import DEFAULT_CONSIDER_HOME from .const import DEFAULT_DECO_POSTFIX from .const import DEFAULT_SCAN_INTERVAL @@ -51,7 +54,7 @@ def _get_schema(data: dict[str:Any]): scan_interval = data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) schema.update( { - vol.Required(CONF_HOST, default=data.get(CONF_HOST, "192.168.0.1")): str, + vol.Required(CONF_HOST, default=data.get(CONF_HOST, "10.0.0.1")): str, vol.Required( CONF_SCAN_INTERVAL, default=scan_interval, @@ -112,7 +115,13 @@ def _ensure_user_input_optionals(data: dict[str:Any]) -> None: async def _async_test_credentials(hass: HomeAssistant, data: dict[str:Any]): """Return true if credentials is valid.""" try: - await async_create_and_refresh_coordinators(hass, data, consider_home_seconds=1) + coordinators = await async_create_and_refresh_coordinators( + hass, data, consider_home_seconds=1 + ) + await asyncio.gather( + coordinators[COORDINATOR_DECOS_KEY].async_shutdown(), + coordinators[COORDINATOR_CLIENTS_KEY].async_shutdown(), + ) return {} except TimeoutException: return {"base": "timeout_connect"} @@ -194,6 +203,7 @@ class TplinkDecoOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: ConfigEntry): """Initialize HACS options flow.""" + self.config_entry = config_entry self.data = dict(config_entry.data) self._errors = {} @@ -207,7 +217,13 @@ async def async_step_init(self, user_input: dict[str:Any] = None): self._errors = await _async_test_credentials(self.hass, self.data) if len(self._errors) == 0: - return self.async_create_entry(data=self.data) + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + title=self.data[CONF_HOST], + data=self.data, + options={}, + ) + return self.async_create_entry(data={}) return self.async_show_form( step_id="init", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bdbe451 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +black==23.10.1 +colorlog==6.7.0 +flake8==6.1.0 +homeassistant==2023.6.0 +pip>=21.0,<23.2 +pre-commit==3.5.0 +reorder-python-imports==3.12.0 +ruff==0.1.3