diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000..60458f6 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,43 @@ +## Developing with Visual Studio Code + devcontainer + +The easiest way to get started with custom integration development is to use Visual Studio Code with devcontainers. This approach will create a preconfigured development environment with all the tools you need. + +In the container you will have a dedicated Home Assistant core instance running with your custom component code. You can configure this instance by updating the `./devcontainer/configuration.yaml` file. + +**Prerequisites** + +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- Docker + - For Linux, macOS, or Windows 10 Pro/Enterprise/Education use the [current release version of Docker](https://docs.docker.com/install/) + - Windows 10 Home requires [WSL 2](https://docs.microsoft.com/windows/wsl/wsl2-install) and the current Edge version of Docker Desktop (see instructions [here](https://docs.docker.com/docker-for-windows/wsl-tech-preview/)). This can also be used for Windows Pro/Enterprise/Education. +- [Visual Studio code](https://code.visualstudio.com/) +- [Remote - Containers (VSC Extension)][extension-link] + +[More info about requirements and devcontainer in general](https://code.visualstudio.com/docs/remote/containers#_getting-started) + +[extension-link]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers + +**Getting started:** + +1. Fork the repository. +2. Clone the repository to your computer. +3. Open the repository using Visual Studio code. + +When you open this repository with Visual Studio code you are asked to "Reopen in Container", this will start the build of the container. + +_If you don't see this notification, open the command palette and select `Remote-Containers: Reopen Folder in Container`._ + +### Tasks + +The devcontainer comes with some useful tasks to help you with development, you can start these tasks by opening the command palette and select `Tasks: Run Task` then select the task you want to run. + +When a task is currently running (like `Run Home Assistant on port 9123` for the docs), it can be restarted by opening the command palette and selecting `Tasks: Restart Running Task`, then select the task you want to restart. + +The available tasks are: + +Task | Description +-- | -- +Run Home Assistant on port 9123 | Launch Home Assistant with your custom component code and the configuration defined in `.devcontainer/configuration.yaml`. +Run Home Assistant configuration against /config | Check the configuration. +Upgrade Home Assistant to latest dev | Upgrade the Home Assistant core version in the container to the latest version of the `dev` branch. +Install a specific version of Home Assistant | Install a specific version of Home Assistant core in the container. diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml new file mode 100644 index 0000000..7af7f06 --- /dev/null +++ b/.devcontainer/configuration.yaml @@ -0,0 +1,6 @@ +default_config: + +logger: + default: error + logs: + custom_components.sagemcom_fast: debug diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6065381 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,30 @@ +// See https://aka.ms/vscode-remote/devcontainer.json for format details. +{ + "image": "ludeeus/container:integration", + "context": "..", + "appPort": [ + "9123:8123" + ], + "postCreateCommand": "container install", + "runArgs": [ + "-v", + "${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh" + ], + "extensions": [ + "ms-python.vscode-pylance", + "github.vscode-pull-request-github", + ], + "settings": { + "files.eol": "\n", + "editor.tabSize": 4, + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/bin/python3", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true + } +} \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..9033f3e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @iMicknl diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml new file mode 100644 index 0000000..a14c555 --- /dev/null +++ b/.github/pr-labeler.yml @@ -0,0 +1,5 @@ +feature: ['feature/*', 'feat/*'] +enhancement: enhancement/* +bug: fix/* +breaking: breaking/* +documentation: doc/* diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..77328c8 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,21 @@ +name-template: 'v$NEXT_PATCH_VERSION' +tag-template: 'v$NEXT_PATCH_VERSION' +exclude-labels: + - 'exclude-from-changelog' +categories: + - title: '⚠️ Breaking changes' + label: 'breaking' + - title: '🚀 Features' + label: 'feature' + - title: '✨ Enhancement' + label: 'enhancement' + - title: '📘 Documentation' + label: 'documentation' + - title: '🐛 Bug Fixes' + label: 'bug' +template: | + ## What's changed + $CHANGES + + ## Contributors to this release + $CONTRIBUTORS diff --git a/.github/workflows/validate.yml b/.github/workflows/hacs.yml similarity index 70% rename from .github/workflows/validate.yml rename to .github/workflows/hacs.yml index 6f7bf6d..90d3a86 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/hacs.yml @@ -1,7 +1,6 @@ -name: Validate +name: HACS validation on: - push: pull_request: schedule: - cron: "0 0 * * *" @@ -12,7 +11,7 @@ jobs: steps: - uses: "actions/checkout@v2" - name: HACS validation - uses: "hacs/integration/action@master" + uses: "hacs/integration/action@main" with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CATEGORY: "integration" \ No newline at end of file + CATEGORY: "integration" diff --git a/.github/workflows/hassfest.yml b/.github/workflows/hassfest.yml index 99ef3fa..3421d47 100644 --- a/.github/workflows/hassfest.yml +++ b/.github/workflows/hassfest.yml @@ -4,11 +4,11 @@ on: push: pull_request: schedule: - - cron: '0 0 * * *' + - cron: '0 0 * * *' jobs: validate: runs-on: "ubuntu-latest" steps: - - uses: "actions/checkout@v2" - - uses: home-assistant/actions/hassfest@master \ No newline at end of file + - uses: "actions/checkout@v2" + - uses: home-assistant/actions/hassfest@master diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..f6d86ed --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,12 @@ +name: Linters (flake8, black, isort) + +on: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 0000000..1052a1c --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} \ No newline at end of file diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..70627d0 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,12 @@ +name: PR Labeler +on: + pull_request_target: + types: [opened] + +jobs: + pr-labeler: + runs-on: ubuntu-latest + steps: + - uses: TimonVS/pr-labeler-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..e260e45 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,15 @@ +name: Release Drafter + +on: + push: + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - name: Update release draft + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..b6e15be --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,28 @@ + # yamllint disable rule:line-length +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: > + 'There hasn't been any activity on this issue recently. Is this issue still present? + + Please make sure to update to the latest Home Assistant version and version of this integration to see + if that solves the issue. Let us know if that works for you by adding a + comment 👍. + + This issue now has been marked as stale and will be closed if no further + activity occurs. Thank you for your contributions.' + days-before-stale: 30 + days-before-close: 5 + stale-issue-label: 'no-issue-activity' + exempt-issue-labels: 'work-in-progress,blocked,help wanted,under investigation' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..79a018e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: Test (pytest) + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.test.txt + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + # - name: Install Pytest Annotation plugin + # run: | + # # Ideally this should be part of our dependencies + # # However this plugin is fairly new and doesn't run correctly + # # on a non-GitHub environment. + # pip install pytest-github-actions-annotate-failures + # - name: Test with pytest + # run: | + # pytest \ + # --cov custom_components/sagemcom_fast \ + # --cov-report=xml --cov-report=html \ + # -o console_output_style=count \ + # -p no:sugar \ + # tests + # - name: Upload coverage artifact + # uses: actions/upload-artifact@v2.1.3 + # with: + # name: coverage-${{ matrix.python-version }} + # path: htmlcov diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a725e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,137 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# PyCharm stuff: +.idea/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# HA Config directory for local testing +/Config/ + +**/.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..685e82b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +repos: + - repo: https://github.com/asottile/pyupgrade + rev: v2.7.2 + hooks: + - id: pyupgrade + args: [--py37-plus] + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((custom_components)/.+)?[^/]+\.py$ + - repo: https://github.com/codespell-project/codespell + rev: v1.17.1 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --skip="./.*,*.csv,*.json,*.md" + - --quiet-level=2 + exclude_types: [csv, json] + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.5.0 + - pydocstyle==5.1.1 + files: ^(custom_components)/.+\.py$ + - repo: https://github.com/PyCQA/isort + rev: 5.5.3 + hooks: + - id: isort + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.24.2 + hooks: + - id: yamllint diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7ab4ba8 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Home Assistant on port 9123", + "type": "shell", + "command": "container start", + "problemMatcher": [] + }, + { + "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": [] + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4548c11 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Mick Vleeshouwer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 734c3b6..cae7136 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,66 @@ -# Sagemcom F@st (work in progress) -Integration adding support for Sagemcom F@st routers to Home Assistant. [Supported devices](https://github.com/imicknl/python-sagemcom-api#supported-devices). +![screenshot of a device detail page in Home Assistant](https://raw.githubusercontent.com/iMicknl/ha-sagemcom-fast/master/media/sagemcom_fast_device_page.png) +[![GitHub release](https://img.shields.io/github/release/iMicknl/ha-sagemcom-fast.svg)](https://github.com/iMicknl/ha-sagemcom-fast/releases/) + +# Sagemcom F@st - Home Assistant (work in progress) + +This integration adds support for Sagemcom F@st routers to Home Assistant. Currently this is a work in progress where only a basic device_tracker is supported, however in the future sensors will be added as well. + +Sagemcom F@st routers are used by many providers worldwide, but many of them did rebrand the router. Examples are the b-box from Proximus, Home Hub from bell and the Smart Hub from BT. ## Installation ### Manual -Copy the `custom_components/sagemcom_fast` to your `custom_components` folder. Reboot home assistant and install the Sagemcom F@st integration via the integrations config flow. +Copy the `custom_components/sagemcom_fast` to your `custom_components` folder. Reboot Home Assistant and install the Sagemcom F@st integration via the integrations config flow. ### HACS -Add this repository to HACS, search for the `Sagemcom F@st` integration and choose install. Reboot home assistant and install the Sagemcom F@st integration via the integrations config flow. +Add this repository to HACS, search for the `Sagemcom F@st` integration and choose install. Reboot Home Assistant and install the Sagemcom F@st integration via the integrations config flow. ``` https://github.com/imicknl/ha-sagemcom-fast -``` \ No newline at end of file +``` + +## Usage + +This integration can only be confgured via the Config Flow. Go to `Configuration -> Integrations -> Add Integration` and choose Sagemcom F@st. The prompt will ask you for your credentials. Please note that some routers require authentication, where others can login with `guest` username and an empty password. + +The encryption method differs per device. Please refer to the table below to understand which option to select. If your device is not listed, please try both methods one by one. + +## Supported devices + +Have a look at the table below for more information about supported devices. + +| Router Model | Provider(s) | Authentication Method | Comments | +| --------------------- | -------------------- | --------------------- | ----------------------------- | +| Sagemcom F@st 3864 | Optus | sha512 | username: guest, password: "" | +| Sagemcom F@st 3865b | Proximus (b-box3) | md5 | | +| Sagemcom F@st 3890V3 | Delta / Zeelandnet | md5 | | +| Sagemcom F@st 4360Air | KPN | md5 | | +| Sagemcom F@st 5250 | Bell (Home Hub 2000) | md5 | username: guest, password: "" | +| Sagemcom F@st 5280 | | sha512 | | +| Sagemcom F@st 5364 | BT (Smart Hub) | md5 | username: guest, password: "" | +| SagemCom F@st 5366SD | Eir F3000 | md5 | | +| Sagemcom F@st 5370e | Telia | sha512 | | +| Sagemcom F@st 5566 | Bell (Home Hub 3000) | md5 | username: guest, password: "" | +| Sagemcom F@st 5655V2 | MásMóvil | md5 | | +| Speedport Pro | Telekom | md5 | | + +> Contributions welcome. If you router model is supported by this package, but not in the list above, please create [an issue](https://github.com/iMicknl/ha-sagemcom-fast/issues/new) or pull request. + +## Advanced + +### Enable debug logging + +The [logger](https://www.home-assistant.io/integrations/logger/) integration lets you define the level of logging activities in Home Assistant. Turning on debug mode will show more information to help us understand your issues. + +```yaml +logger: + default: critical + logs: + custom_components.sagemcom_fast: debug +``` + + +### Device not supported / working correctly + +If you are not able to use this integration with your Sagemcom F@st device, please create [an issue](https://github.com/iMicknl/ha-sagemcom-fast/issues/new) with as much information as possible. Turn on debug logging and share the logs in your issue description. diff --git a/custom_components/sagemcom_fast/__init__.py b/custom_components/sagemcom_fast/__init__.py index 4a7cdb6..1aa60c7 100644 --- a/custom_components/sagemcom_fast/__init__.py +++ b/custom_components/sagemcom_fast/__init__.py @@ -2,26 +2,30 @@ import asyncio import logging -import voluptuous as vol - -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST, HTTP_BAD_REQUEST -from homeassistant.config_entries import ConfigEntry +from aiohttp.client_exceptions import ClientError +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - discovery, +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, service +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from sagemcom_api.client import SagemcomClient +from sagemcom_api.enums import EncryptionMethod +from sagemcom_api.exceptions import ( + AccessRestrictionException, + AuthenticationException, + LoginTimeoutException, + UnauthorizedException, ) -from .const import DOMAIN, CONF_ENCRYPTION_METHOD - -from sagemcom_api import SagemcomClient, EncryptionMethod +from .const import CONF_ENCRYPTION_METHOD, DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) PLATFORMS = ["device_tracker"] +SERVICE_REBOOT = "reboot" + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Sagemcom component.""" @@ -39,28 +43,60 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): password = entry.data[CONF_PASSWORD] encryption_method = entry.data[CONF_ENCRYPTION_METHOD] - sagemcom = SagemcomClient(host, username, password, encryption_method) + session = aiohttp_client.async_get_clientsession(hass) + client = SagemcomClient( + host, username, password, EncryptionMethod(encryption_method), session + ) try: - device_info = await sagemcom.get_device_info() - _LOGGER.info(device_info) - except: - _LOGGER.error("Error retrieving DeviceInfo") + await client.login() + except AccessRestrictionException: + _LOGGER.error("access_restricted") + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data=entry.data, + ) + ) + return False + except (AuthenticationException, UnauthorizedException): + _LOGGER.error("invalid_auth") + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data=entry.data, + ) + ) + return False + except (TimeoutError, ClientError) as exception: + _LOGGER.error("cannot_connect") + raise ConfigEntryNotReady from exception + except LoginTimeoutException: + _LOGGER.error("login_timeout") + return False + except Exception as exception: # pylint: disable=broad-except + _LOGGER.exception(exception) return False - hass.data.setdefault(DOMAIN, {})[entry.entry_id]= sagemcom + hass.data[DOMAIN][entry.entry_id] = { + "client": client, + "devices": await client.get_hosts(only_active=True), + } - # Create router device - device_registry = await dr.async_get_registry(hass) + # Create gateway device in Home Assistant + gateway = await client.get_device_info() + device_registry = await hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, - identifiers={(DOMAIN, device_info.serial_number)}, - manufacturer=device_info.manufacturer, - name=f'{device_info.manufacturer} {device_info.model_number}', - model=device_info.model_name, - sw_version=device_info.software_version, + connections={(CONNECTION_NETWORK_MAC, gateway.mac_address)}, + identifiers={(DOMAIN, gateway.serial_number)}, + manufacturer=gateway.manufacturer, + name=f"{gateway.manufacturer} {gateway.model_number}", + model=gateway.model_name, + sw_version=gateway.software_version, ) # Register components @@ -69,22 +105,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, component) ) - return True - + # Handle gateway device services async def async_command_reboot(call): """Handle reboot service call.""" - await print("Reboot") + client.reboot() - hass.services.async_register(DOMAIN, "reboot", async_command_reboot) + service.async_register_admin_service( + hass, DOMAIN, SERVICE_REBOOT, async_command_reboot + ) + + 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) + hass.config_entries.async_forward_entry_unload(entry, component) for component in PLATFORMS ] ) diff --git a/custom_components/sagemcom_fast/__pycache__/__init__.cpython-37.pyc b/custom_components/sagemcom_fast/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 912d619..0000000 Binary files a/custom_components/sagemcom_fast/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/custom_components/sagemcom_fast/__pycache__/config_flow.cpython-37.pyc b/custom_components/sagemcom_fast/__pycache__/config_flow.cpython-37.pyc deleted file mode 100644 index 4483b3a..0000000 Binary files a/custom_components/sagemcom_fast/__pycache__/config_flow.cpython-37.pyc and /dev/null differ diff --git a/custom_components/sagemcom_fast/__pycache__/const.cpython-37.pyc b/custom_components/sagemcom_fast/__pycache__/const.cpython-37.pyc deleted file mode 100644 index a6fb113..0000000 Binary files a/custom_components/sagemcom_fast/__pycache__/const.cpython-37.pyc and /dev/null differ diff --git a/custom_components/sagemcom_fast/__pycache__/device_tracker.cpython-37.pyc b/custom_components/sagemcom_fast/__pycache__/device_tracker.cpython-37.pyc deleted file mode 100644 index 67a1623..0000000 Binary files a/custom_components/sagemcom_fast/__pycache__/device_tracker.cpython-37.pyc and /dev/null differ diff --git a/custom_components/sagemcom_fast/config_flow.py b/custom_components/sagemcom_fast/config_flow.py index e984a4a..a79ecb0 100644 --- a/custom_components/sagemcom_fast/config_flow.py +++ b/custom_components/sagemcom_fast/config_flow.py @@ -1,17 +1,34 @@ """Config flow for Sagemcom integration.""" import logging +from aiohttp import ClientError +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from sagemcom_api.client import SagemcomClient +from sagemcom_api.enums import EncryptionMethod +from sagemcom_api.exceptions import ( + AccessRestrictionException, + AuthenticationException, + LoginTimeoutException, +) import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST, HTTP_BAD_REQUEST -from homeassistant.core import callback -from homeassistant.components import ssdp -from .const import CONF_ENCRYPTION_METHOD, CONF_TRACK_WIRELESS_CLIENTS, CONF_TRACK_WIRED_CLIENTS, DOMAIN -from sagemcom_api import SagemcomClient, EncryptionMethod, UnauthorizedException +from .const import CONF_ENCRYPTION_METHOD +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) +ENCRYPTION_METHODS = [item.value for item in EncryptionMethod] + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_ENCRYPTION_METHOD): vol.In(ENCRYPTION_METHODS), + } +) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Sagemcom.""" @@ -19,166 +36,44 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return OptionsFlow(config_entry) - - def __init__(self): - """Initialize.""" - - encryption_methods = [str(item.value) for item in EncryptionMethod] - - self.data_schema = { - vol.Required(CONF_HOST): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_ENCRYPTION_METHOD): vol.In(encryption_methods) - } + async def async_validate_input(self, user_input): + """Validate user credentials.""" + username = user_input.get(CONF_USERNAME) or "" + password = user_input.get(CONF_PASSWORD) or "" + host = user_input.get(CONF_HOST) + encryption_method = user_input.get(CONF_ENCRYPTION_METHOD) + + async with SagemcomClient( + host, username, password, EncryptionMethod(encryption_method) + ) as client: + await client.login() + return self.async_create_entry( + title=host, + data=user_input, + ) async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} - self._abort_if_unique_id_configured() - - if user_input is not None: - await self.async_set_unique_id(user_input[CONF_HOST]) + if user_input: + await self.async_set_unique_id(user_input.get(CONF_HOST)) + self._abort_if_unique_id_configured() try: - validation = await validate_input(self.hass, user_input) - return self.async_create_entry(title=validation["title"], data=user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: + return await self.async_validate_input(user_input) + except AccessRestrictionException: + errors["base"] = "access_restricted" + except AuthenticationException: errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.error( - "Unknown error connecting with Sagemcom F@st at %s", - user_input[CONF_HOST], - ) + except (TimeoutError, ClientError): + errors["base"] = "cannot_connect" + except LoginTimeoutException: + errors["base"] = "login_timeout" + except Exception as exception: # pylint: disable=broad-except errors["base"] = "unknown" - return self.async_abort(reason="unknown") - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema(self.data_schema), - errors=errors or {} - ) - - async def async_step_unignore(self, user_input): - unique_id = user_input[CONF_HOST] - await self.async_set_unique_id(unique_id) + _LOGGER.exception(exception) return self.async_show_form( - step_id="user", - data_schema=vol.Schema(self.data_schema), - errors={} + step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - - async def async_step_ssdp(self, discovery_info): - """Handle SSDP initiated config flow.""" - _LOGGER.warning( - f'Found discovery {discovery_info[ssdp.ATTR_SSDP_LOCATION]}') - _LOGGER.warning(discovery_info) - - # if any( - # url == flow["context"].get(CONF_URL) for flow in self._async_in_progress() - # ): - # return self.async_abort(reason="already_in_progress") - - # user_input = {CONF_URL: url} - # if self._already_configured(user_input): - # return self.async_abort(reason="already_configured") - - return await self._async_show_user_form() - - -class OptionsFlow(config_entries.OptionsFlow): - """Handle Sagemcom F@st options.""" - - def __init__(self, config_entry): - """Initialize Sagemcom F@st options flow.""" - self.config_entry = config_entry - self.options = dict(config_entry.options) - - # Set default options, if not set - if self.options.get(CONF_TRACK_WIRELESS_CLIENTS) is None: - self.options[CONF_TRACK_WIRELESS_CLIENTS] = True - - if self.options.get(CONF_TRACK_WIRED_CLIENTS) is None: - self.options[CONF_TRACK_WIRED_CLIENTS] = True - - async def async_step_init(self, user_input=None): - """Manage the Sagemcom F@st options.""" - - # if self.show_advanced_options: - # return await self.async_step_device_tracker() - - return await self.async_step_simple_options() - - async def async_step_simple_options(self, user_input=None): - """For simple Jack.""" - if user_input is not None: - self.options.update(user_input) - return await self._update_options() - - return self.async_show_form( - step_id="simple_options", - data_schema=vol.Schema( - { - vol.Optional( - CONF_TRACK_WIRELESS_CLIENTS, - default=self.options[CONF_TRACK_WIRELESS_CLIENTS], - ): bool, - vol.Optional( - CONF_TRACK_WIRED_CLIENTS, - default=self.options[CONF_TRACK_WIRED_CLIENTS], - ): bool - } - ), - ) - - async def _update_options(self): - """Update config entry options.""" - return self.async_create_entry(title="", data=self.options) - - -async def validate_input(hass: core.HomeAssistant, data): - """Validate the user input allows us to connect. - - Data has the keys from DATA_SCHEMA with values provided by the user. - """ - - host = data[CONF_HOST] # TODO Validate if host is valid ip address - username = data[CONF_USERNAME] - password = data[CONF_PASSWORD] - encryption_method = data[CONF_ENCRYPTION_METHOD] - - print("VALIDATING") - - try: - sagemcom = SagemcomClient(host, username, password, encryption_method) - login = await sagemcom.login() - - if (login != True): - raise InvalidAuth - - except UnauthorizedException: - raise InvalidAuth - - except Exception as exception: - print(type(exception)) - raise CannotConnect - - # Return info that you want to store in the config entry. - return {"title": f"{host}"} - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/custom_components/sagemcom_fast/const.py b/custom_components/sagemcom_fast/const.py index c10da29..5524b56 100644 --- a/custom_components/sagemcom_fast/const.py +++ b/custom_components/sagemcom_fast/const.py @@ -1,16 +1,11 @@ """Constants for the Sagemcom integration.""" -import logging -from sagemcom_api import EncryptionMethod - -LOGGER = logging.getLogger(__package__) DOMAIN = "sagemcom_fast" -CONF_ENCRYPTION_METHOD = 'encryption_method' -CONF_TRACK_WIRELESS_CLIENTS = 'track_wireless_clients' -CONF_TRACK_WIRED_CLIENTS = 'track_wired_clients' +CONF_ENCRYPTION_METHOD = "encryption_method" +CONF_TRACK_WIRELESS_CLIENTS = "track_wireless_clients" +CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" DEFAULT_TRACK_WIRELESS_CLIENTS = True DEFAULT_TRACK_WIRED_CLIENTS = True ATTR_MANUFACTURER = "Sagemcom" - diff --git a/custom_components/sagemcom_fast/device_action.py b/custom_components/sagemcom_fast/device_action.py deleted file mode 100644 index 1db4e83..0000000 --- a/custom_components/sagemcom_fast/device_action.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Provides device automations for Sagemcom.""" -from typing import List, Optional - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_TYPE, - SERVICE_RELOAD, -) -from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import entity_registry -import homeassistant.helpers.config_validation as cv - -from . import DOMAIN - -# TODO specify your supported action types. -ACTION_TYPES = {"reboot"} - -ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), - vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), - } -) - - -async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: - """List device actions for Sagemcom devices.""" - registry = await entity_registry.async_get_registry(hass) - actions = [] - - # TODO Read this comment and remove it. - # This example shows how to iterate over the entities of this device - # that match this integration. If your actions instead rely on - # calling services, do something like: - # zha_device = await _async_get_zha_device(hass, device_id) - # return zha_device.device_actions - - # Get all the integrations entities for this device - for entry in entity_registry.async_entries_for_device(registry, device_id): - if entry.domain != DOMAIN: - continue - - print(entry) - - # Add actions for each entity that belongs to this integration - # TODO add your own actions. - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "reboot", - } - ) - - return actions - - -async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] -) -> None: - """Execute a device action.""" - config = ACTION_SCHEMA(config) - - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} - - if config[CONF_TYPE] == "reboot": - service = SERVICE_RELOAD - - await hass.services.async_call( - DOMAIN, service, service_data, blocking=True, context=context - ) diff --git a/custom_components/sagemcom_fast/device_tracker.py b/custom_components/sagemcom_fast/device_tracker.py index 9083670..fd077c2 100644 --- a/custom_components/sagemcom_fast/device_tracker.py +++ b/custom_components/sagemcom_fast/device_tracker.py @@ -1,90 +1,56 @@ -"""Support for device tracking of Sagemcom router.""" +"""Support for device tracking of client router.""" import logging -import re -from typing import Any, Dict, List, Optional, Set +from typing import Any, Dict -import attr - -from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, -) +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST, HTTP_BAD_REQUEST - -from homeassistant.core import callback -from homeassistant.helpers import entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from .const import CONF_ENCRYPTION_METHOD, CONF_TRACK_WIRELESS_CLIENTS, CONF_TRACK_WIRED_CLIENTS, DOMAIN +from homeassistant.helpers.restore_state import RestoreEntity -from sagemcom_api import SagemcomClient, EncryptionMethod +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -_DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up from config entry.""" - options = config_entry.options - - # Initialize already tracked entities - tracked: Set[str] = set() - registry = await entity_registry.async_get_registry(hass) - known_entities: List[SagemcomScannerEntity] = [] - - sagemcom = hass.data[DOMAIN][config_entry.entry_id] - - devices = await sagemcom.get_hosts() - - last_results = [] - - for device in devices: - - if options.get(CONF_TRACK_WIRELESS_CLIENTS) == False: - if device.interface == "WiFi": - continue + # TODO Handle status of disconnected devices + entities = [] + client = hass.data[DOMAIN][config_entry.entry_id]["client"] - if options.get(CONF_TRACK_WIRED_CLIENTS) == False: - if device.interface == "Ethernet": - continue - - print(device) + new_devices = await client.get_hosts(only_active=True) + for device in new_devices: entity = SagemcomScannerEntity(device, config_entry.entry_id) - last_results.append(entity) + entities.append(entity) - async_add_entities(last_results, update_before_add=True) + async_add_entities(entities, update_before_add=True) -class SagemcomScannerEntity(ScannerEntity): +class SagemcomScannerEntity(ScannerEntity, RestoreEntity): """Sagemcom router scanner entity.""" def __init__(self, device, parent): - """ Constructor """ - + """Initialize the device.""" self._device = device - self._device_state_attributes = { - "ip_address": self._device.ip_address, - "interface_type": self._device.interface, - "device_type": self._device.user_device_type or self._device.detected_device_type, - "address_source": self._device.address_source - } - self._via_device = parent super().__init__() @property def name(self) -> str: - return self._device.name or self._device.user_friendly_name or self._device.mac_address + """Return the name of the device.""" + return ( + self._device.name + or self._device.user_friendly_name + or self._device.mac_address + ) @property def unique_id(self) -> str: - return self._device.mac_address + """Return a unique ID.""" + return self._device.id @property def source_type(self) -> str: @@ -100,13 +66,29 @@ def is_connected(self) -> bool: def device_info(self): """Return the device info.""" return { - "name": self.name, + "default_name": self.name, "identifiers": {(DOMAIN, self.unique_id)}, - "via_device": (DOMAIN, self._via_device) + "via_device": (DOMAIN, self._via_device), } @property def device_state_attributes(self) -> Dict[str, Any]: - """Get additional attributes related to entity state.""" + """Return the state attributes of the device.""" + attr = {"interface_type": self._device.interface_type} + + return attr - return self._device_state_attributes or {} + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._device.ip_address or None + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._device.phys_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self._device.user_host_name or self._device.host_name diff --git a/custom_components/sagemcom_fast/manifest.json b/custom_components/sagemcom_fast/manifest.json index bf838d2..ab8a25d 100644 --- a/custom_components/sagemcom_fast/manifest.json +++ b/custom_components/sagemcom_fast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://github.com/imicknl/ha-sagemcom-fast", "requirements": [ - "git+https://github.com/iMicknl/python-sagemcom-api.git#sagemcom_api===0.3.0" + "sagemcom_api===1.0.1" ], "ssdp": [ { @@ -19,5 +19,7 @@ "zeroconf": [], "homekit": {}, "dependencies": [], - "codeowners": ["@imicknl"] -} + "codeowners": [ + "@imicknl" + ] +} \ No newline at end of file diff --git a/custom_components/sagemcom_fast/services.yaml b/custom_components/sagemcom_fast/services.yaml index d43610a..bfeed58 100644 --- a/custom_components/sagemcom_fast/services.yaml +++ b/custom_components/sagemcom_fast/services.yaml @@ -1,6 +1,2 @@ reboot: - description: Reboot router. - fields: - url: - description: URL of router to reboot; optional when only one is configured. - example: http://192.168.11.1/ \ No newline at end of file + description: Reboot Sagemcom F@st device. diff --git a/custom_components/sagemcom_fast/strings.json b/custom_components/sagemcom_fast/strings.json index 0c45f65..88b6261 100644 --- a/custom_components/sagemcom_fast/strings.json +++ b/custom_components/sagemcom_fast/strings.json @@ -4,9 +4,11 @@ "already_configured": "Device is already configured" }, "error": { + "access_restricted": "Access restricted", "cannot_connect": "Failed to connect, please try again", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "login_timeout": "Request timed-out. This is mainly caused by selection of the wrong encryption method." }, "step": { "user": { @@ -16,17 +18,10 @@ "username": "Username", "encryption_method": "Encryption Method" }, - "description": "Enter your credentials for accessing the routers web interface. Depending on the router model, Sagemcom is using different encryption methods for authentication, which can be found in the [supported devices](https://github.com/imicknl/python-sagemcom-api#supported-devices) list.", - "title": "Setup Sagemcom F@st device" + "description": "Enter your credentials for accessing the routers web interface. Depending on the router model, Sagemcom is using different encryption methods for authentication, which can be found in the [supported devices](https://github.com/iMicknl/ha-sagemcom-fast#supported-devices) list." } } }, - "device_automation": { - "action_type": { - "turn_off": "Turn off {entity_name}", - "turn_on": "Turn on {entity_name}" - } - }, "options": { "step": { "simple_options": { @@ -38,4 +33,4 @@ } } } -} +} \ No newline at end of file diff --git a/custom_components/sagemcom_fast/translations/en.json b/custom_components/sagemcom_fast/translations/en.json index 0c45f65..88b6261 100644 --- a/custom_components/sagemcom_fast/translations/en.json +++ b/custom_components/sagemcom_fast/translations/en.json @@ -4,9 +4,11 @@ "already_configured": "Device is already configured" }, "error": { + "access_restricted": "Access restricted", "cannot_connect": "Failed to connect, please try again", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "login_timeout": "Request timed-out. This is mainly caused by selection of the wrong encryption method." }, "step": { "user": { @@ -16,17 +18,10 @@ "username": "Username", "encryption_method": "Encryption Method" }, - "description": "Enter your credentials for accessing the routers web interface. Depending on the router model, Sagemcom is using different encryption methods for authentication, which can be found in the [supported devices](https://github.com/imicknl/python-sagemcom-api#supported-devices) list.", - "title": "Setup Sagemcom F@st device" + "description": "Enter your credentials for accessing the routers web interface. Depending on the router model, Sagemcom is using different encryption methods for authentication, which can be found in the [supported devices](https://github.com/iMicknl/ha-sagemcom-fast#supported-devices) list." } } }, - "device_automation": { - "action_type": { - "turn_off": "Turn off {entity_name}", - "turn_on": "Turn on {entity_name}" - } - }, "options": { "step": { "simple_options": { @@ -38,4 +33,4 @@ } } } -} +} \ No newline at end of file diff --git a/custom_components/sagemcom_fast/translations/nl.json b/custom_components/sagemcom_fast/translations/nl.json deleted file mode 100644 index 69bc590..0000000 --- a/custom_components/sagemcom_fast/translations/nl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host", - "username": "Gebruikersnaam", - "password": "Wachtwoord" - }, - "title": "Verbind met het apparaat" - } - }, - "title": "Sagemcom" - } -} \ No newline at end of file diff --git a/hacs.json b/hacs.json index cab40e7..bd0b719 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,7 @@ { "name": "Sagemcom F@st", "domains": ["device_tracker", "sensor"], - "homeassistant": "0.99.9", - "render_readme": "true" + "homeassistant": "0.115.0", + "render_readme": "true", + "iot_class": "Local Polling" } \ No newline at end of file diff --git a/media/sagemcom_fast_device_page.png b/media/sagemcom_fast_device_page.png new file mode 100644 index 0000000..f838686 Binary files /dev/null and b/media/sagemcom_fast_device_page.png differ diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..afeadb7 --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1,10 @@ +# Home Assistant test +# linters such as flake8 and pylint should be pinned, as new releases +# make new things fail. Manually update these pins when pulling in a +# new version +pytest<6.1 +pytest-cov<3.0.0 +pytest-homeassistant + +# from our manifest.json for our Custom Component +sagemcom_api==1.0.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a6af700 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pre-commit==2.5.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5d851e3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,34 @@ +[flake8] +exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +doctests = True +# To work with Black +max-line-length = 88 +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring +# W504 line break after binary operator +ignore = + E501, + W503, + E203, + D202, + W504 + +[isort] +# https://github.com/timothycrosley/isort +# https://github.com/timothycrosley/isort/wiki/isort-Settings +# splits long import on multiple lines indented by 4 spaces +multi_line_output = 3 +include_trailing_comma=True +force_grid_wrap=0 +use_parentheses=True +line_length=88 +indent = " " +# by default isort don't check module indexes +not_skip = __init__.py +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +default_section = THIRDPARTY +combine_as_imports = true diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..2708345 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the Sagemcom F@st integration."""