fix action

Action commit

Created package file

minor updates

Add binary_sensor

Updated package file

Updated package file

Fix BinarySensor Class name

Add more descriptions

Updated package file

Move stuff

Updated package file

add binary_sensor to resources.json

Updated package file

Add configuration

Updated package file

Remove switch

Adds CONTRIBUTING

Fix broken link

Remove refrences

Fix links

fix broken actions

Update actions

Updated package file

Updated package file

Black format

Fix config issues

Updated package file

Fix icon for binary_sensor

Add switch platform

Update README.md (#1)

* Update README.md

* Update README.md

Update CONTRIBUTING.md (#2)

Update __init__.py (#3)

* Update __init__.py

* Set enabled default to `True`

Update const.py (#4)

cleanup badges/add style (#5)

* shield that supports styling

* badge cleanup

* Change discord ID

Correct typo

Create manifest.json (#12)

Fixes https://github.com/custom-components/blueprint/issues/10

Move manifest to correct dir

Fixes #9 by renaming the file

Updates, updates, more updates, worst commit messge ever!

revert

Update main.workflow

Delete resources.json

Create example.yaml

Update README.md

Summer update 😎

Adds info about devcontainer

Adds pylint

Add translation example

remove stalebot

Adds integrationhelper

adds requirements.txt

Adds "tabnine.tabnine-vscode" to devcontainer

Add support for config_flow configuration

Add config_flow "docs"

Update README.md

use https for pip install ha@dev (#15)

Typo on README.md (#18)

Fix various typos in comments (#19)

Add hassfest action (#22)

* Add hassfest action

* Fix manifest issue

Spring cleaning ☀️ (#23)

* Spring cleaning

* Actions

* Fix branches

* Changes for config_flow

Show how to only allow one instance

Adds HACS validation action (#24)

Update postCreateCommand

Update tasks

Minor updates (#26)

* link correction in README.md and info.md

* Add READMME.md for .devcontainer

* Add automation.yaml file in the configuration

* Complete CONTRIBUTING.md

Improve README for container dev and library update. (#27)

Feature/setup cfg (#28)

* Add setup.cfg

* Run black and isort.

* Add blueprint to first party.

* Make const import consistent.

Move translation files

Fixes #32

Use CoordinatorEntity (#33)

Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>

Fixed typos (#34)

Fix directory name for translations and link to the documentation (#36)

Fix info.md (#40)

Use debian based devcontainer (#44)

Remove sampleclient (#45)

Rename [blueprint|Blueprint] -> [integration_blueprint|Integration blueprint] (#47)

Change HACS action (#39)

Fix wrong path for link (#48)

For an unknown reason the link was pointing to a one of my repository.
Probably a too quick copy/paste.

Add french translation and strings improvements (#49)

Reusing work done on strings.json done in #37

Add example tests (#50)

Prepare and explain how to step by step debugging (#51)

Add version

Fix testing by bumping pytest-homeassistant-custom-component (#54)

Fix typo in api.py (#55)

passeword -> password

Fix a typo in the readme (#56)

Update .gitignore to include .idea (#57)

Update .gitignore to include .idea for those using Py Charm

Add iot_class to manifest

Use `enable_custom_integrations` fixture by default (#58)

Fix typo (#59)

retain user input after an error (#52)

Update README.md

Closes #61

remove async_timeout.timeout loop arg (#65)

Correct name "Blueprint" ->"Integration blueprint" (#64)

change entity.py to use extra_state_attributes (#66)
This commit is contained in:
ludeeus 2019-03-08 19:17:48 +01:00 committed by Alex Berry
commit d51d010901
Signed by: alex
GPG Key ID: D5BB9F05EB47A31A
43 changed files with 1678 additions and 0 deletions

60
.devcontainer/README.md Normal file
View File

@ -0,0 +1,60 @@
## 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.
### Step by Step debugging
With the development container,
you can test your custom component in Home Assistant with step by step debugging.
You need to modify the `configuration.yaml` file in `.devcontainer` folder
by uncommenting the line:
```yaml
# debugpy:
```
Then launch the task `Run Home Assistant on port 9123`, and launch the debugger
with the existing debugging configuration `Python: Attach Local`.
For more information, look at [the Remote Python Debugger integration documentation](https://www.home-assistant.io/integrations/debugpy/).

View File

@ -0,0 +1,9 @@
default_config:
logger:
default: info
logs:
custom_components.integration_blueprint: debug
# If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/)
# debugpy:

View File

@ -0,0 +1,30 @@
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"image": "ludeeus/container:integration-debian",
"name": "Blueprint integration development",
"context": "..",
"appPort": [
"9123:8123"
],
"postCreateCommand": "container install",
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance"
],
"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
}
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

42
.github/ISSUE_TEMPLATE/issue.md vendored Normal file
View File

@ -0,0 +1,42 @@
---
name: Issue
about: Create a report to help us improve
---
<!-- Before you open a new issue, search through the existing issues to see if others have had the same problem.
Issues not containing the minimum requirements will be closed:
- Issues without a description (using the header is not good enough) will be closed.
- Issues without debug logging will be closed.
- Issues without configuration will be closed
-->
## Version of the custom_component
<!-- If you are not using the newest version, download and try that before opening an issue
If you are unsure about the version check the const.py file.
-->
## Configuration
```yaml
Add your logs here.
```
## Describe the bug
A clear and concise description of what the bug is.
## Debug log
<!-- To enable debug logs check this https://www.home-assistant.io/components/logger/ -->
```text
Add your logs here.
```

21
.github/workflows/cron.yaml vendored Normal file
View File

@ -0,0 +1,21 @@
name: Cron actions
on:
schedule:
- cron: '0 0 * * *'
jobs:
validate:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
ignore: brands
- name: Hassfest validation
uses: "home-assistant/actions/hassfest@master"

55
.github/workflows/pull.yml vendored Normal file
View File

@ -0,0 +1,55 @@
name: Pull actions
on:
pull_request:
jobs:
validate:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
ignore: brands
- name: Hassfest validation
uses: "home-assistant/actions/hassfest@master"
style:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
with:
python-version: "3.x"
- run: python3 -m pip install black
- run: black .
tests:
runs-on: "ubuntu-latest"
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
- name: Setup Python
uses: "actions/setup-python@v1"
with:
python-version: "3.8"
- name: Install requirements
run: python3 -m pip install -r requirements_test.txt
- name: Run tests
run: |
pytest \
-qq \
--timeout=9 \
--durations=10 \
-n auto \
--cov custom_components.integration_blueprint \
-o console_output_style=count \
-p no:sugar \
tests

58
.github/workflows/push.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Push actions
on:
push:
branches:
- master
- dev
jobs:
validate:
runs-on: "ubuntu-latest"
name: Validate
steps:
- uses: "actions/checkout@v2"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
ignore: brands
- name: Hassfest validation
uses: "home-assistant/actions/hassfest@master"
style:
runs-on: "ubuntu-latest"
name: Check style formatting
steps:
- uses: "actions/checkout@v2"
- uses: "actions/setup-python@v1"
with:
python-version: "3.x"
- run: python3 -m pip install black
- run: black .
tests:
runs-on: "ubuntu-latest"
name: Run tests
steps:
- name: Check out code from GitHub
uses: "actions/checkout@v2"
- name: Setup Python
uses: "actions/setup-python@v1"
with:
python-version: "3.8"
- name: Install requirements
run: python3 -m pip install -r requirements_test.txt
- name: Run tests
run: |
pytest \
-qq \
--timeout=9 \
--durations=10 \
-n auto \
--cov custom_components.integration_blueprint \
-o console_output_style=count \
-p no:sugar \
tests

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__pycache__
pythonenv*
venv
.venv
.coverage
.idea

35
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
// 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"
}
]
}
]
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.pythonPath": "/usr/local/bin/python",
"files.associations": {
"*.yaml": "home-assistant"
}
}

29
.vscode/tasks.json vendored Normal file
View File

@ -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": []
}
]
}

61
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,61 @@
# Contribution guidelines
Contributing to this project should be as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
## Github is used for everything
Github is used to host code, to track issues and feature requests, as well as accept pull requests.
Pull requests are the best way to propose changes to the codebase.
1. Fork the repo and create your branch from `master`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using black).
4. Test you contribution.
5. Issue that pull request!
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
## Report bugs using Github's [issues](../../issues)
GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!
## Write bug reports with detail, background, and sample code
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
People *love* thorough bug reports. I'm not even kidding.
## Use a Consistent Coding Style
Use [black](https://github.com/ambv/black) to make sure the code follows the style.
## Test your code modification
This custom component is based on [integration_blueprint template](https://github.com/custom-components/integration_blueprint).
It comes with development environment in a container, easy to launch
if you use Visual Studio Code. With this container you will have a stand alone
Home Assistant instance running and already configured with the included
[`.devcontainer/configuration.yaml`](./.devcontainer/configuration.yaml)
file.
## License
By contributing, you agree that your contributions will be licensed under its MIT License.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Joakim Sørensen @ludeeus
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.

151
README.md Normal file
View File

@ -0,0 +1,151 @@
# Notice
The component and platforms in this repository are not meant to be used by a
user, but as a "blueprint" that custom component developers can build
upon, to make more awesome stuff.
HAVE FUN! 😎
## Why?
This is simple, by having custom_components look (README + structure) the same
it is easier for developers to help each other and for users to start using them.
If you are a developer and you want to add things to this "blueprint" that you think more
developers will have use for, please open a PR to add it :)
## What?
This repository contains multiple files, here is a overview:
File | Purpose
-- | --
`.devcontainer/*` | Used for development/testing with VSCODE, more info in the readme file in that dir.
`.github/ISSUE_TEMPLATE/feature_request.md` | Template for Feature Requests
`.github/ISSUE_TEMPLATE/issue.md` | Template for issues
`.vscode/tasks.json` | Tasks for the devcontainer.
`custom_components/integration_blueprint/translations/*` | [Translation files.](https://developers.home-assistant.io/docs/internationalization/custom_integration)
`custom_components/integration_blueprint/__init__.py` | The component file for the integration.
`custom_components/integration_blueprint/api.py` | This is a sample API client.
`custom_components/integration_blueprint/binary_sensor.py` | Binary sensor platform for the integration.
`custom_components/integration_blueprint/config_flow.py` | Config flow file, this adds the UI configuration possibilities.
`custom_components/integration_blueprint/const.py` | A file to hold shared variables/constants for the entire integration.
`custom_components/integration_blueprint/manifest.json` | A [manifest file](https://developers.home-assistant.io/docs/en/creating_integration_manifest.html) for Home Assistant.
`custom_components/integration_blueprint/sensor.py` | Sensor platform for the integration.
`custom_components/integration_blueprint/switch.py` | Switch sensor platform for the integration.
`tests/__init__.py` | Makes the `tests` folder a module.
`tests/conftest.py` | Global [fixtures](https://docs.pytest.org/en/stable/fixture.html) used in tests to [patch](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) functions.
`tests/test_api.py` | Tests for `custom_components/integration_blueprint/api.py`.
`tests/test_config_flow.py` | Tests for `custom_components/integration_blueprint/config_flow.py`.
`tests/test_init.py` | Tests for `custom_components/integration_blueprint/__init__.py`.
`tests/test_switch.py` | Tests for `custom_components/integration_blueprint/switch.py`.
`CONTRIBUTING.md` | Guidelines on how to contribute.
`example.png` | Screenshot that demonstrate how it might look in the UI.
`info.md` | An example on a info file (used by [hacs][hacs]).
`LICENSE` | The license file for the project.
`README.md` | The file you are reading now, should contain info about the integration, installation and configuration instructions.
`requirements.txt` | Python packages used by this integration.
`requirements_dev.txt` | Python packages used to provide [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense)/code hints during development of this integration, typically includes packages in `requirements.txt` but may include additional packages
`requirements_test.txt` | Python packages required to run the tests for this integration, typically includes packages in `requirements_dev.txt` but may include additional packages
## How?
If you want to use all the potential and features of this blueprint template you
should use Visual Studio Code to develop in a container. In this container you
will have all the tools to ease your python development and a dedicated Home
Assistant core instance to run your integration. See `.devcontainer/README.md` for more information.
If you need to work on the python library in parallel of this integration
(`sampleclient` in this example) there are different options. The following one seems
easy to implement:
- Create a dedicated branch for your python library on a public git repository (example: branch
`dev` on `https://github.com/ludeeus/sampleclient`)
- Update in the `manifest.json` file the `requirements` key to point on your development branch
( example: `"requirements": ["git+https://github.com/ludeeus/sampleclient.git@dev#devp==0.0.1beta1"]`)
- Each time you need to make a modification to your python library, push it to your
development branch and increase the number of the python library version in `manifest.json` file
to ensure Home Assistant update the code of the python library. (example `"requirements": ["git+https://...==0.0.1beta2"]`).
***
README content if this was a published component:
***
# integration_blueprint
[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]](LICENSE)
[![hacs][hacsbadge]][hacs]
![Project Maintenance][maintenance-shield]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]
_Component to integrate with [integration_blueprint][integration_blueprint]._
**This component will set up the following platforms.**
Platform | Description
-- | --
`binary_sensor` | Show something `True` or `False`.
`sensor` | Show info from blueprint API.
`switch` | Switch something `True` or `False`.
![example][exampleimg]
## Installation
1. Using the tool of choice open the directory (folder) for your HA configuration (where you find `configuration.yaml`).
2. If you do not have a `custom_components` directory (folder) there, you need to create it.
3. In the `custom_components` directory (folder) create a new folder called `integration_blueprint`.
4. Download _all_ the files from the `custom_components/integration_blueprint/` directory (folder) in this repository.
5. Place the files you downloaded in the new directory (folder) you created.
6. Restart Home Assistant
7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Integration blueprint"
Using your HA configuration directory (folder) as a starting point you should now also have this:
```text
custom_components/integration_blueprint/translations/en.json
custom_components/integration_blueprint/translations/nb.json
custom_components/integration_blueprint/translations/sensor.nb.json
custom_components/integration_blueprint/__init__.py
custom_components/integration_blueprint/api.py
custom_components/integration_blueprint/binary_sensor.py
custom_components/integration_blueprint/config_flow.py
custom_components/integration_blueprint/const.py
custom_components/integration_blueprint/manifest.json
custom_components/integration_blueprint/sensor.py
custom_components/integration_blueprint/switch.py
```
## Configuration is done in the UI
<!---->
## Contributions are welcome!
If you want to contribute to this please read the [Contribution guidelines](CONTRIBUTING.md)
***
[integration_blueprint]: https://github.com/custom-components/integration_blueprint
[buymecoffee]: https://www.buymeacoffee.com/ludeeus
[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-components/blueprint.svg?style=for-the-badge
[commits]: https://github.com/custom-components/integration_blueprint/commits/master
[hacs]: https://github.com/custom-components/hacs
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
[discord]: https://discord.gg/Qa5fW2R
[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge
[exampleimg]: example.png
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
[forum]: https://community.home-assistant.io/
[license-shield]: https://img.shields.io/github/license/custom-components/blueprint.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/custom-components/blueprint.svg?style=for-the-badge
[releases]: https://github.com/custom-components/integration_blueprint/releases

View File

@ -0,0 +1 @@
"""Custom components module."""

View File

@ -0,0 +1,109 @@
"""
Custom integration to integrate integration_blueprint with Home Assistant.
For more details about this integration, please refer to
https://github.com/custom-components/integration_blueprint
"""
import asyncio
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import IntegrationBlueprintApiClient
from .const import (
CONF_PASSWORD,
CONF_USERNAME,
DOMAIN,
PLATFORMS,
STARTUP_MESSAGE,
)
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER: logging.Logger = logging.getLogger(__package__)
async def async_setup(hass: HomeAssistant, config: Config):
"""Set up this integration using YAML is not supported."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up this integration using UI."""
if hass.data.get(DOMAIN) is None:
hass.data.setdefault(DOMAIN, {})
_LOGGER.info(STARTUP_MESSAGE)
username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)
session = async_get_clientsession(hass)
client = IntegrationBlueprintApiClient(username, password, session)
coordinator = BlueprintDataUpdateCoordinator(hass, client=client)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
hass.data[DOMAIN][entry.entry_id] = coordinator
for platform in PLATFORMS:
if entry.options.get(platform, True):
coordinator.platforms.append(platform)
hass.async_add_job(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
entry.add_update_listener(async_reload_entry)
return True
class BlueprintDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(
self, hass: HomeAssistant, client: IntegrationBlueprintApiClient
) -> None:
"""Initialize."""
self.api = client
self.platforms = []
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)
async def _async_update_data(self):
"""Update data via library."""
try:
return await self.api.async_get_data()
except Exception as exception:
raise UpdateFailed() from exception
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
unloaded = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
if platform in coordinator.platforms
]
)
)
if unloaded:
hass.data[DOMAIN].pop(entry.entry_id)
return unloaded
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)

View File

@ -0,0 +1,75 @@
"""Sample API Client."""
import logging
import asyncio
import socket
from typing import Optional
import aiohttp
import async_timeout
TIMEOUT = 10
_LOGGER: logging.Logger = logging.getLogger(__package__)
HEADERS = {"Content-type": "application/json; charset=UTF-8"}
class IntegrationBlueprintApiClient:
def __init__(
self, username: str, password: str, session: aiohttp.ClientSession
) -> None:
"""Sample API Client."""
self._username = username
self._password = password
self._session = session
async def async_get_data(self) -> dict:
"""Get data from the API."""
url = "https://jsonplaceholder.typicode.com/posts/1"
return await self.api_wrapper("get", url)
async def async_set_title(self, value: str) -> None:
"""Get data from the API."""
url = "https://jsonplaceholder.typicode.com/posts/1"
await self.api_wrapper("patch", url, data={"title": value}, headers=HEADERS)
async def api_wrapper(
self, method: str, url: str, data: dict = {}, headers: dict = {}
) -> dict:
"""Get information from the API."""
try:
async with async_timeout.timeout(TIMEOUT):
if method == "get":
response = await self._session.get(url, headers=headers)
return await response.json()
elif method == "put":
await self._session.put(url, headers=headers, json=data)
elif method == "patch":
await self._session.patch(url, headers=headers, json=data)
elif method == "post":
await self._session.post(url, headers=headers, json=data)
except asyncio.TimeoutError as exception:
_LOGGER.error(
"Timeout error fetching information from %s - %s",
url,
exception,
)
except (KeyError, TypeError) as exception:
_LOGGER.error(
"Error parsing information from %s - %s",
url,
exception,
)
except (aiohttp.ClientError, socket.gaierror) as exception:
_LOGGER.error(
"Error fetching information from %s - %s",
url,
exception,
)
except Exception as exception: # pylint: disable=broad-except
_LOGGER.error("Something really wrong happened! - %s", exception)

View File

@ -0,0 +1,35 @@
"""Binary sensor platform for integration_blueprint."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from .const import (
BINARY_SENSOR,
BINARY_SENSOR_DEVICE_CLASS,
DEFAULT_NAME,
DOMAIN,
)
from .entity import IntegrationBlueprintEntity
async def async_setup_entry(hass, entry, async_add_devices):
"""Setup binary_sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices([IntegrationBlueprintBinarySensor(coordinator, entry)])
class IntegrationBlueprintBinarySensor(IntegrationBlueprintEntity, BinarySensorEntity):
"""integration_blueprint binary_sensor class."""
@property
def name(self):
"""Return the name of the binary_sensor."""
return f"{DEFAULT_NAME}_{BINARY_SENSOR}"
@property
def device_class(self):
"""Return the class of this binary_sensor."""
return BINARY_SENSOR_DEVICE_CLASS
@property
def is_on(self):
"""Return true if the binary_sensor is on."""
return self.coordinator.data.get("title", "") == "foo"

View File

@ -0,0 +1,116 @@
"""Adds config flow for Blueprint."""
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession
import voluptuous as vol
from .api import IntegrationBlueprintApiClient
from .const import (
CONF_PASSWORD,
CONF_USERNAME,
DOMAIN,
PLATFORMS,
)
class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Blueprint."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self):
"""Initialize."""
self._errors = {}
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
self._errors = {}
# Uncomment the next 2 lines if only a single instance of the integration is allowed:
# if self._async_current_entries():
# return self.async_abort(reason="single_instance_allowed")
if user_input is not None:
valid = await self._test_credentials(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
if valid:
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
else:
self._errors["base"] = "auth"
return await self._show_config_form(user_input)
user_input = {}
# Provide defaults for form
user_input[CONF_USERNAME] = ""
user_input[CONF_PASSWORD] = ""
return await self._show_config_form(user_input)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return BlueprintOptionsFlowHandler(config_entry)
async def _show_config_form(self, user_input): # pylint: disable=unused-argument
"""Show the configuration form to edit location data."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): str,
vol.Required(CONF_PASSWORD, default=user_input[CONF_PASSWORD]): str,
}
),
errors=self._errors,
)
async def _test_credentials(self, username, password):
"""Return true if credentials is valid."""
try:
session = async_create_clientsession(self.hass)
client = IntegrationBlueprintApiClient(username, password, session)
await client.async_get_data()
return True
except Exception: # pylint: disable=broad-except
pass
return False
class BlueprintOptionsFlowHandler(config_entries.OptionsFlow):
"""Blueprint config flow options handler."""
def __init__(self, config_entry):
"""Initialize HACS options flow."""
self.config_entry = config_entry
self.options = dict(config_entry.options)
async def async_step_init(self, user_input=None): # pylint: disable=unused-argument
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
self.options.update(user_input)
return await self._update_options()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(x, default=self.options.get(x, True)): bool
for x in sorted(PLATFORMS)
}
),
)
async def _update_options(self):
"""Update config entry options."""
return self.async_create_entry(
title=self.config_entry.data.get(CONF_USERNAME), data=self.options
)

View File

@ -0,0 +1,40 @@
"""Constants for integration_blueprint."""
# Base component constants
NAME = "Integration blueprint"
DOMAIN = "integration_blueprint"
DOMAIN_DATA = f"{DOMAIN}_data"
VERSION = "0.0.1"
ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/"
ISSUE_URL = "https://github.com/custom-components/integration_blueprint/issues"
# Icons
ICON = "mdi:format-quote-close"
# Device classes
BINARY_SENSOR_DEVICE_CLASS = "connectivity"
# Platforms
BINARY_SENSOR = "binary_sensor"
SENSOR = "sensor"
SWITCH = "switch"
PLATFORMS = [BINARY_SENSOR, SENSOR, SWITCH]
# Configuration and options
CONF_ENABLED = "enabled"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
# Defaults
DEFAULT_NAME = DOMAIN
STARTUP_MESSAGE = f"""
-------------------------------------------------------------------
{NAME}
Version: {VERSION}
This is a custom integration!
If you have any issues with this you need to open an issue here:
{ISSUE_URL}
-------------------------------------------------------------------
"""

View File

@ -0,0 +1,33 @@
"""BlueprintEntity class"""
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, NAME, VERSION, ATTRIBUTION
class IntegrationBlueprintEntity(CoordinatorEntity):
def __init__(self, coordinator, config_entry):
super().__init__(coordinator)
self.config_entry = config_entry
@property
def unique_id(self):
"""Return a unique ID to use for this entity."""
return self.config_entry.entry_id
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": NAME,
"model": VERSION,
"manufacturer": NAME,
}
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {
"attribution": ATTRIBUTION,
"id": str(self.coordinator.data.get("id")),
"integration": DOMAIN,
}

View File

@ -0,0 +1,12 @@
{
"domain": "integration_blueprint",
"name": "Integration blueprint",
"documentation": "https://github.com/custom-components/integration_blueprint",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/custom-components/integration_blueprint/issues",
"version": "0.0.0",
"config_flow": true,
"codeowners": [
"@ludeeus"
]
}

View File

@ -0,0 +1,28 @@
"""Sensor platform for integration_blueprint."""
from .const import DEFAULT_NAME, DOMAIN, ICON, SENSOR
from .entity import IntegrationBlueprintEntity
async def async_setup_entry(hass, entry, async_add_devices):
"""Setup sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices([IntegrationBlueprintSensor(coordinator, entry)])
class IntegrationBlueprintSensor(IntegrationBlueprintEntity):
"""integration_blueprint Sensor class."""
@property
def name(self):
"""Return the name of the sensor."""
return f"{DEFAULT_NAME}_{SENSOR}"
@property
def state(self):
"""Return the state of the sensor."""
return self.coordinator.data.get("body")
@property
def icon(self):
"""Return the icon of the sensor."""
return ICON

View File

@ -0,0 +1,40 @@
"""Switch platform for integration_blueprint."""
from homeassistant.components.switch import SwitchEntity
from .const import DEFAULT_NAME, DOMAIN, ICON, SWITCH
from .entity import IntegrationBlueprintEntity
async def async_setup_entry(hass, entry, async_add_devices):
"""Setup sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_devices([IntegrationBlueprintBinarySwitch(coordinator, entry)])
class IntegrationBlueprintBinarySwitch(IntegrationBlueprintEntity, SwitchEntity):
"""integration_blueprint switch class."""
async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument
"""Turn on the switch."""
await self.coordinator.api.async_set_title("bar")
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument
"""Turn off the switch."""
await self.coordinator.api.async_set_title("foo")
await self.coordinator.async_request_refresh()
@property
def name(self):
"""Return the name of the switch."""
return f"{DEFAULT_NAME}_{SWITCH}"
@property
def icon(self):
"""Return the icon of this switch."""
return ICON
@property
def is_on(self):
"""Return true if the switch is on."""
return self.coordinator.data.get("title", "") == "foo"

View File

@ -0,0 +1,31 @@
{
"config": {
"step": {
"user": {
"title": "Blueprint",
"description": "If you need help with the configuration have a look here: https://github.com/custom-components/integration_blueprint",
"data": {
"username": "Username",
"password": "Password"
}
}
},
"error": {
"auth": "Username/Password is wrong."
},
"abort": {
"single_instance_allowed": "Only a single instance is allowed."
}
},
"options": {
"step": {
"user": {
"data": {
"binary_sensor": "Binary sensor enabled",
"sensor": "Sensor enabled",
"switch": "Switch enabled"
}
}
}
}
}

View File

@ -0,0 +1,31 @@
{
"config": {
"step": {
"user": {
"title": "Blueprint",
"description": "Si vous avez besoin d'aide pour la configuration, regardez ici: https://github.com/custom-components/integration_blueprint",
"data": {
"username": "Identifiant",
"password": "Mot de Passe"
}
}
},
"error": {
"auth": "Identifiant ou mot de passe erroné."
},
"abort": {
"single_instance_allowed": "Une seule instance est autorisée."
}
},
"options": {
"step": {
"user": {
"data": {
"binary_sensor": "Capteur binaire activé",
"sensor": "Capteur activé",
"switch": "Interrupteur activé"
}
}
}
}
}

View File

@ -0,0 +1,31 @@
{
"config": {
"step": {
"user": {
"title": "Blueprint",
"description": "Hvis du trenger hjep til konfigurasjon ta en titt her: https://github.com/custom-components/integration_blueprint",
"data": {
"username": "Brukernavn",
"password": "Passord"
}
}
},
"error": {
"auth": "Brukernavn/Passord er feil."
},
"abort": {
"single_instance_allowed": "Denne integrasjonen kan kun konfigureres en gang."
}
},
"options": {
"step": {
"user": {
"data": {
"binary_sensor": "Binær sensor aktivert",
"sensor": "Sensor aktivert",
"switch": "Bryter aktivert"
}
}
}
}
}

BIN
example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

10
hacs.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "Integration blueprint",
"hacs": "1.6.0",
"domains": [
"binary_sensor",
"sensor",
"switch"
],
"homeassistant": "0.118.0"
}

56
info.md Normal file
View File

@ -0,0 +1,56 @@
[![GitHub Release][releases-shield]][releases]
[![GitHub Activity][commits-shield]][commits]
[![License][license-shield]][license]
[![hacs][hacsbadge]][hacs]
[![Project Maintenance][maintenance-shield]][user_profile]
[![BuyMeCoffee][buymecoffeebadge]][buymecoffee]
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]
_Component to integrate with [integration_blueprint][integration_blueprint]._
**This component will set up the following platforms.**
Platform | Description
-- | --
`binary_sensor` | Show something `True` or `False`.
`sensor` | Show info from API.
`switch` | Switch something `True` or `False`.
![example][exampleimg]
{% if not installed %}
## Installation
1. Click install.
1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Blueprint".
{% endif %}
## Configuration is done in the UI
<!---->
***
[integration_blueprint]: https://github.com/custom-components/integration_blueprint
[buymecoffee]: https://www.buymeacoffee.com/ludeeus
[buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?style=for-the-badge
[commits-shield]: https://img.shields.io/github/commit-activity/y/custom-components/integration_blueprint.svg?style=for-the-badge
[commits]: https://github.com/custom-components/integration_blueprint/commits/master
[hacs]: https://hacs.xyz
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
[discord]: https://discord.gg/Qa5fW2R
[discord-shield]: https://img.shields.io/discord/330944238910963714.svg?style=for-the-badge
[exampleimg]: example.png
[forum-shield]: https://img.shields.io/badge/community-forum-brightgreen.svg?style=for-the-badge
[forum]: https://community.home-assistant.io/
[license]: https://github.com/custom-components/integration_blueprint/blob/main/LICENSE
[license-shield]: https://img.shields.io/github/license/custom-components/integration_blueprint.svg?style=for-the-badge
[maintenance-shield]: https://img.shields.io/badge/maintainer-Joakim%20Sørensen%20%40ludeeus-blue.svg?style=for-the-badge
[releases-shield]: https://img.shields.io/github/release/custom-components/integration_blueprint.svg?style=for-the-badge
[releases]: https://github.com/custom-components/integration_blueprint/releases
[user_profile]: https://github.com/ludeeus

1
requirements_dev.txt Normal file
View File

@ -0,0 +1 @@
homeassistant

1
requirements_test.txt Normal file
View File

@ -0,0 +1 @@
pytest-homeassistant-custom-component==0.4.0

35
setup.cfg Normal file
View File

@ -0,0 +1,35 @@
[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
known_first_party = custom_components.integration_blueprint, tests
combine_as_imports = true

24
tests/README.md Normal file
View File

@ -0,0 +1,24 @@
# Why?
While tests aren't required to publish a custom component for Home Assistant, they will generally make development easier because good tests will expose when changes you want to make to the component logic will break expected functionality. Home Assistant uses [`pytest`](https://docs.pytest.org/en/latest/) for its tests, and the tests that have been included are modeled after tests that are written for core Home Assistant integrations. These tests pass with 100% coverage (unless something has changed ;) ) and have comments to help you understand the purpose of different parts of the test.
# Getting Started
To begin, it is recommended to create a virtual environment to install dependencies:
```bash
python3 -m venv venv
source venv/bin/activate
```
You can then install the dependencies that will allow you to run tests:
`pip3 install -r requirements_test.txt.`
This will install `homeassistant`, `pytest`, and `pytest-homeassistant-custom-component`, a plugin which allows you to leverage helpers that are available in Home Assistant for core integration tests.
# Useful commands
Command | Description
------- | -----------
`pytest tests/` | This will run all tests in `tests/` and tell you how many passed/failed
`pytest --durations=10 --cov-report term-missing --cov=custom_components.integration_blueprint tests` | This tells `pytest` that your target module to test is `custom_components.integration_blueprint` so that it can give you a [code coverage](https://en.wikipedia.org/wiki/Code_coverage) summary, including % of code that was executed and the line numbers of missed executions.
`pytest tests/test_init.py -k test_setup_unload_and_reload_entry` | Runs the `test_setup_unload_and_reload_entry` test function located in `tests/test_init.py`

1
tests/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Tests for integration_blueprint integration."""

63
tests/conftest.py Normal file
View File

@ -0,0 +1,63 @@
"""Global fixtures for integration_blueprint integration."""
# Fixtures allow you to replace functions with a Mock object. You can perform
# many options via the Mock to reflect a particular behavior from the original
# function that you want to see without going through the function's actual logic.
# Fixtures can either be passed into tests as parameters, or if autouse=True, they
# will automatically be used across all tests.
#
# Fixtures that are defined in conftest.py are available across all tests. You can also
# define fixtures within a particular test file to scope them locally.
#
# pytest_homeassistant_custom_component provides some fixtures that are provided by
# Home Assistant core. You can find those fixture definitions here:
# https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/blob/master/pytest_homeassistant_custom_component/common.py
#
# See here for more info: https://docs.pytest.org/en/latest/fixture.html (note that
# pytest includes fixtures OOB which you can use as defined on this page)
from unittest.mock import patch
import pytest
pytest_plugins = "pytest_homeassistant_custom_component"
# This fixture enables loading custom integrations in all tests.
# Remove to enable selective use of this fixture
@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations):
yield
# This fixture is used to prevent HomeAssistant from attempting to create and dismiss persistent
# notifications. These calls would fail without this fixture since the persistent_notification
# integration is never loaded during a test.
@pytest.fixture(name="skip_notifications", autouse=True)
def skip_notifications_fixture():
"""Skip notification calls."""
with patch("homeassistant.components.persistent_notification.async_create"), patch(
"homeassistant.components.persistent_notification.async_dismiss"
):
yield
# This fixture, when used, will result in calls to async_get_data to return None. To have the call
# return a value, we would add the `return_value=<VALUE_TO_RETURN>` parameter to the patch call.
@pytest.fixture(name="bypass_get_data")
def bypass_get_data_fixture():
"""Skip calls to get data from API."""
with patch(
"custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_get_data"
):
yield
# In this fixture, we are forcing calls to async_get_data to raise an Exception. This is useful
# for exception handling.
@pytest.fixture(name="error_on_get_data")
def error_get_data_fixture():
"""Simulate error when retrieving data from API."""
with patch(
"custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_get_data",
side_effect=Exception,
):
yield

5
tests/const.py Normal file
View File

@ -0,0 +1,5 @@
"""Constants for integration_blueprint tests."""
from custom_components.integration_blueprint.const import CONF_PASSWORD, CONF_USERNAME
# Mock config data to be used across multiple tests
MOCK_CONFIG = {CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password"}

86
tests/test_api.py Normal file
View File

@ -0,0 +1,86 @@
"""Tests for integration_blueprint api."""
import asyncio
import aiohttp
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from custom_components.integration_blueprint.api import IntegrationBlueprintApiClient
async def test_api(hass, aioclient_mock, caplog):
"""Test API calls."""
# To test the api submodule, we first create an instance of our API client
api = IntegrationBlueprintApiClient("test", "test", async_get_clientsession(hass))
# Use aioclient_mock which is provided by `pytest_homeassistant_custom_components`
# to mock responses to aiohttp requests. In this case we are telling the mock to
# return {"test": "test"} when a `GET` call is made to the specified URL. We then
# call `async_get_data` which will make that `GET` request.
aioclient_mock.get(
"https://jsonplaceholder.typicode.com/posts/1", json={"test": "test"}
)
assert await api.async_get_data() == {"test": "test"}
# We do the same for `async_set_title`. Note the difference in the mock call
# between the previous step and this one. We use `patch` here instead of `get`
# because we know that `async_set_title` calls `api_wrapper` with `patch` as the
# first parameter
aioclient_mock.patch("https://jsonplaceholder.typicode.com/posts/1")
assert await api.async_set_title("test") is None
# In order to get 100% coverage, we need to test `api_wrapper` to test the code
# that isn't already called by `async_get_data` and `async_set_title`. Because the
# only logic that lives inside `api_wrapper` that is not being handled by a third
# party library (aiohttp) is the exception handling, we also want to simulate
# raising the exceptions to ensure that the function handles them as expected.
# The caplog fixture allows access to log messages in tests. This is particularly
# useful during exception handling testing since often the only action as part of
# exception handling is a logging statement
caplog.clear()
aioclient_mock.put(
"https://jsonplaceholder.typicode.com/posts/1", exc=asyncio.TimeoutError
)
assert (
await api.api_wrapper("put", "https://jsonplaceholder.typicode.com/posts/1")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Timeout error fetching information from" in caplog.record_tuples[0][2]
)
caplog.clear()
aioclient_mock.post(
"https://jsonplaceholder.typicode.com/posts/1", exc=aiohttp.ClientError
)
assert (
await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/1")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Error fetching information from" in caplog.record_tuples[0][2]
)
caplog.clear()
aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/2", exc=Exception)
assert (
await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/2")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Something really wrong happened!" in caplog.record_tuples[0][2]
)
caplog.clear()
aioclient_mock.post("https://jsonplaceholder.typicode.com/posts/3", exc=TypeError)
assert (
await api.api_wrapper("post", "https://jsonplaceholder.typicode.com/posts/3")
is None
)
assert (
len(caplog.record_tuples) == 1
and "Error parsing information from" in caplog.record_tuples[0][2]
)

110
tests/test_config_flow.py Normal file
View File

@ -0,0 +1,110 @@
"""Test integration_blueprint config flow."""
from unittest.mock import patch
from homeassistant import config_entries, data_entry_flow
import pytest
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.integration_blueprint.const import (
BINARY_SENSOR,
DOMAIN,
PLATFORMS,
SENSOR,
SWITCH,
)
from .const import MOCK_CONFIG
# This fixture bypasses the actual setup of the integration
# since we only want to test the config flow. We test the
# actual functionality of the integration in other test modules.
@pytest.fixture(autouse=True)
def bypass_setup_fixture():
"""Prevent setup."""
with patch(
"custom_components.integration_blueprint.async_setup",
return_value=True,
), patch(
"custom_components.integration_blueprint.async_setup_entry",
return_value=True,
):
yield
# Here we simiulate a successful config flow from the backend.
# Note that we use the `bypass_get_data` fixture here because
# we want the config flow validation to succeed during the test.
async def test_successful_config_flow(hass, bypass_get_data):
"""Test a successful config flow."""
# Initialize a config flow
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Check that the config flow shows the user form as the first step
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
# If a user were to enter `test_username` for username and `test_password`
# for password, it would result in this function call
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_CONFIG
)
# Check that the config flow is complete and a new entry is created with
# the input data
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "test_username"
assert result["data"] == MOCK_CONFIG
assert result["result"]
# In this case, we want to simulate a failure during the config flow.
# We use the `error_on_get_data` mock instead of `bypass_get_data`
# (note the function parameters) to raise an Exception during
# validation of the input config.
async def test_failed_config_flow(hass, error_on_get_data):
"""Test a failed config flow due to credential validation failure."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.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=MOCK_CONFIG
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "auth"}
# Our config flow also has an options flow, so we must test it as well.
async def test_options_flow(hass):
"""Test an options flow."""
# Create a new MockConfigEntry and add to HASS (we're bypassing config
# flow entirely)
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
entry.add_to_hass(hass)
# Initialize an options flow
result = await hass.config_entries.options.async_init(entry.entry_id)
# Verify that the first options step is a user form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
# Enter some fake data into the form
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={platform: platform != SENSOR for platform in PLATFORMS},
)
# Verify that the flow finishes
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "test_username"
# Verify that the options were updated
assert entry.options == {BINARY_SENSOR: True, SENSOR: False, SWITCH: True}

56
tests/test_init.py Normal file
View File

@ -0,0 +1,56 @@
"""Test integration_blueprint setup process."""
from homeassistant.exceptions import ConfigEntryNotReady
import pytest
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.integration_blueprint import (
BlueprintDataUpdateCoordinator,
async_reload_entry,
async_setup_entry,
async_unload_entry,
)
from custom_components.integration_blueprint.const import DOMAIN
from .const import MOCK_CONFIG
# We can pass fixtures as defined in conftest.py to tell pytest to use the fixture
# for a given test. We can also leverage fixtures and mocks that are available in
# Home Assistant using the pytest_homeassistant_custom_component plugin.
# Assertions allow you to verify that the return value of whatever is on the left
# side of the assertion matches with the right side.
async def test_setup_unload_and_reload_entry(hass, bypass_get_data):
"""Test entry setup and unload."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
# Set up the entry and assert that the values set during setup are where we expect
# them to be. Because we have patched the BlueprintDataUpdateCoordinator.async_get_data
# call, no code from custom_components/integration_blueprint/api.py actually runs.
assert await async_setup_entry(hass, config_entry)
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert (
type(hass.data[DOMAIN][config_entry.entry_id]) == BlueprintDataUpdateCoordinator
)
# Reload the entry and assert that the data from above is still there
assert await async_reload_entry(hass, config_entry) is None
assert DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]
assert (
type(hass.data[DOMAIN][config_entry.entry_id]) == BlueprintDataUpdateCoordinator
)
# Unload the entry and verify that the data has been removed
assert await async_unload_entry(hass, config_entry)
assert config_entry.entry_id not in hass.data[DOMAIN]
async def test_setup_entry_exception(hass, error_on_get_data):
"""Test ConfigEntryNotReady when API raises an exception during entry setup."""
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
# In this case we are testing the condition where async_setup_entry raises
# ConfigEntryNotReady using the `error_on_get_data` fixture which simulates
# an error.
with pytest.raises(ConfigEntryNotReady):
assert await async_setup_entry(hass, config_entry)

44
tests/test_switch.py Normal file
View File

@ -0,0 +1,44 @@
"""Test integration_blueprint switch."""
from unittest.mock import call, patch
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.const import ATTR_ENTITY_ID
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.integration_blueprint import async_setup_entry
from custom_components.integration_blueprint.const import DEFAULT_NAME, DOMAIN, SWITCH
from .const import MOCK_CONFIG
async def test_switch_services(hass):
"""Test switch services."""
# Create a mock entry so we don't have to go through config flow
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test")
assert await async_setup_entry(hass, config_entry)
await hass.async_block_till_done()
# Functions/objects can be patched directly in test code as well and can be used to test
# additional things, like whether a function was called or what arguments it was called with
with patch(
"custom_components.integration_blueprint.IntegrationBlueprintApiClient.async_set_title"
) as title_func:
await hass.services.async_call(
SWITCH,
SERVICE_TURN_OFF,
service_data={ATTR_ENTITY_ID: f"{SWITCH}.{DEFAULT_NAME}_{SWITCH}"},
blocking=True,
)
assert title_func.called
assert title_func.call_args == call("foo")
title_func.reset_mock()
await hass.services.async_call(
SWITCH,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: f"{SWITCH}.{DEFAULT_NAME}_{SWITCH}"},
blocking=True,
)
assert title_func.called
assert title_func.call_args == call("bar")