Initial commit

This commit is contained in:
Julian Pawlowski 2025-04-18 14:32:54 +02:00 committed by GitHub
commit 131a4eb148
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1049 additions and 0 deletions

49
.devcontainer.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "ludeeus/integration_blueprint",
"image": "mcr.microsoft.com/devcontainers/python:3.13",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
"onAutoForward": "notify"
}
},
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ryanluker.vscode-coverage-gutters"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.defaultInterpreterPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": [
"ffmpeg",
"libturbojpeg0",
"libpcap-dev"
]
}
}
}

1
.gitattributes vendored Normal file
View file

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

54
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View file

@ -0,0 +1,54 @@
---
name: "Bug report"
description: "Report a bug with the integration"
body:
- type: markdown
attributes:
value: Before you open a new issue, search through the existing issues to see if others have had the same problem.
- type: textarea
attributes:
label: "System Health details"
description: "Paste the data from the System Health card in Home Assistant (https://www.home-assistant.io/more-info/system-health#github-issues)"
validations:
required: true
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have enabled debug logging for my installation.
required: true
- label: I have filled out the issue template to the best of my ability.
required: true
- label: This issue only contains 1 issue (if you have multiple issues, open one issue for each issue).
required: true
- label: This issue is not a duplicate issue of any [previous issues](https://github.com/ludeeus/integration_blueprint/issues?q=is%3Aissue+label%3A%22Bug%22+)..
required: true
- type: textarea
attributes:
label: "Describe the issue"
description: "A clear and concise description of what the issue is."
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: "Without steps to reproduce, it will be hard to fix. It is very important that you fill out this part. Issues without it will be closed."
value: |
1.
2.
3.
...
validations:
required: true
- type: textarea
attributes:
label: "Debug logs"
description: "To enable debug logs check this https://www.home-assistant.io/integrations/logger/, this **needs** to include _everything_ from startup of Home Assistant to the point where you encounter the issue."
render: text
validations:
required: true
- type: textarea
attributes:
label: "Diagnostics dump"
description: "Drag the diagnostics dump file here. (see https://www.home-assistant.io/integrations/diagnostics/ for info)"

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

View file

@ -0,0 +1,46 @@
---
name: "Feature request"
description: "Suggest an idea for this project"
body:
- type: markdown
attributes:
value: Before you open a new feature request, search through the existing feature requests to see if others have had the same idea.
- type: checkboxes
attributes:
label: Checklist
options:
- label: I have filled out the template to the best of my ability.
required: true
- label: This only contains 1 feature request (if you have multiple feature requests, open one feature request for each feature request).
required: true
- label: This issue is not a duplicate feature request of [previous feature requests](https://github.com/ludeeus/integration_blueprint/issues?q=is%3Aissue+label%3A%22Feature+Request%22+).
required: true
- type: textarea
attributes:
label: "Is your feature request related to a problem? Please describe."
description: "A clear and concise description of what the problem is."
placeholder: "I'm always frustrated when [...]"
validations:
required: true
- type: textarea
attributes:
label: "Describe the solution you'd like"
description: "A clear and concise description of what you want to happen."
validations:
required: true
- type: textarea
attributes:
label: "Describe alternatives you've considered"
description: "A clear and concise description of any alternative solutions or features you've considered."
validations:
required: true
- type: textarea
attributes:
label: "Additional context"
description: "Add any other context or screenshots about the feature request here."
validations:
required: true

20
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,20 @@
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
ignore:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant"

34
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: Lint
on:
push:
branches:
- "main"
pull_request:
branches:
- "main"
permissions: {}
jobs:
ruff:
name: "Ruff"
runs-on: "ubuntu-latest"
steps:
- name: Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Python
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
with:
python-version: "3.13"
cache: "pip"
- name: Install requirements
run: python3 -m pip install -r requirements.txt
- name: Lint
run: python3 -m ruff check .
- name: Format
run: python3 -m ruff format . --check

36
.github/workflows/validate.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Validate
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
push:
branches:
- main
pull_request:
branches:
- main
permissions: {}
jobs:
hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest
name: Hassfest validation
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run hassfest validation
uses: home-assistant/actions/hassfest@a19f5f4e08ef2786e4604a948f62addd937a6bc9 # master
hacs: # https://github.com/hacs/action
name: HACS validation
runs-on: ubuntu-latest
steps:
- name: Run HACS validation
uses: hacs/action@d556e736723344f83838d08488c983a15381059a # 22.5.0
with:
category: integration
# Remove this 'ignore' key when you have added brand images for your integration to https://github.com/home-assistant/brands
ignore: brands

18
.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
# artifacts
__pycache__
.pytest*
*.egg-info
*/build/*
*/dist/*
# misc
.coverage
.vscode
coverage.xml
.ruff_cache
# Home Assistant configuration
config/*
!config/configuration.yaml

26
.ruff.toml Normal file
View file

@ -0,0 +1,26 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
target-version = "py313"
[lint]
select = [
"ALL",
]
ignore = [
"ANN101", # Missing type annotation for `self` in method
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
]
[lint.flake8-pytest-style]
fixture-parentheses = false
[lint.pyupgrade]
keep-runtime-typing = true
[lint.mccabe]
max-complexity = 25

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 `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using `scripts/lint`).
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/ludeeus/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
[`configuration.yaml`](./config/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) 2019 - 2025 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.

46
README.md Normal file
View file

@ -0,0 +1,46 @@
# 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 | Documentation
-- | -- | --
`.devcontainer.json` | Used for development/testing with Visual Studio Code. | [Documentation](https://code.visualstudio.com/docs/remote/containers)
`.github/ISSUE_TEMPLATE/*.yml` | Templates for the issue tracker | [Documentation](https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository)
`custom_components/integration_blueprint/*` | Integration files, this is where everything happens. | [Documentation](https://developers.home-assistant.io/docs/creating_component_index)
`CONTRIBUTING.md` | Guidelines on how to contribute. | [Documentation](https://help.github.com/en/github/building-a-strong-community/setting-guidelines-for-repository-contributors)
`LICENSE` | The license file for the project. | [Documentation](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository)
`README.md` | The file you are reading now, should contain info about the integration, installation and configuration instructions. | [Documentation](https://help.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax)
`requirements.txt` | Python packages used for development/lint/testing this integration. | [Documentation](https://pip.pypa.io/en/stable/user_guide/#requirements-files)
## How?
1. Create a new repository in GitHub, using this repository as a template by clicking the "Use this template" button in the GitHub UI.
1. Open your new repository in Visual Studio Code devcontainer (Preferably with the "`Dev Containers: Clone Repository in Named Container Volume...`" option).
1. Rename all instances of the `integration_blueprint` to `custom_components/<your_integration_domain>` (e.g. `custom_components/awesome_integration`).
1. Rename all instances of the `Integration Blueprint` to `<Your Integration Name>` (e.g. `Awesome Integration`).
1. Run the `scripts/develop` to start HA and test out your new integration.
## Next steps
These are some next steps you may want to look into:
- Add tests to your integration, [`pytest-homeassistant-custom-component`](https://github.com/MatthewFlamm/pytest-homeassistant-custom-component) can help you get started.
- Add brand images (logo/icon) to https://github.com/home-assistant/brands.
- Create your first release.
- Share your integration on the [Home Assistant Forum](https://community.home-assistant.io/).
- Submit your integration to [HACS](https://hacs.xyz/docs/publish/start).

12
config/configuration.yaml Normal file
View file

@ -0,0 +1,12 @@
# https://www.home-assistant.io/integrations/default_config/
default_config:
# https://www.home-assistant.io/integrations/homeassistant/
homeassistant:
debug: true
# https://www.home-assistant.io/integrations/logger/
logger:
default: info
logs:
custom_components.integration_blueprint: debug

View file

@ -0,0 +1,79 @@
"""
Custom integration to integrate integration_blueprint with Home Assistant.
For more details about this integration, please refer to
https://github.com/ludeeus/integration_blueprint
"""
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.loader import async_get_loaded_integration
from .api import IntegrationBlueprintApiClient
from .const import DOMAIN, LOGGER
from .coordinator import BlueprintDataUpdateCoordinator
from .data import IntegrationBlueprintData
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from .data import IntegrationBlueprintConfigEntry
PLATFORMS: list[Platform] = [
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.SWITCH,
]
# https://developers.home-assistant.io/docs/config_entries_index/#setting-up-an-entry
async def async_setup_entry(
hass: HomeAssistant,
entry: IntegrationBlueprintConfigEntry,
) -> bool:
"""Set up this integration using UI."""
coordinator = BlueprintDataUpdateCoordinator(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(hours=1),
)
entry.runtime_data = IntegrationBlueprintData(
client=IntegrationBlueprintApiClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
),
integration=async_get_loaded_integration(hass, entry.domain),
coordinator=coordinator,
)
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True
async def async_unload_entry(
hass: HomeAssistant,
entry: IntegrationBlueprintConfigEntry,
) -> bool:
"""Handle removal of an entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_reload_entry(
hass: HomeAssistant,
entry: IntegrationBlueprintConfigEntry,
) -> None:
"""Reload config entry."""
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)

View file

@ -0,0 +1,101 @@
"""Sample API Client."""
from __future__ import annotations
import socket
from typing import Any
import aiohttp
import async_timeout
class IntegrationBlueprintApiClientError(Exception):
"""Exception to indicate a general API error."""
class IntegrationBlueprintApiClientCommunicationError(
IntegrationBlueprintApiClientError,
):
"""Exception to indicate a communication error."""
class IntegrationBlueprintApiClientAuthenticationError(
IntegrationBlueprintApiClientError,
):
"""Exception to indicate an authentication error."""
def _verify_response_or_raise(response: aiohttp.ClientResponse) -> None:
"""Verify that the response is valid."""
if response.status in (401, 403):
msg = "Invalid credentials"
raise IntegrationBlueprintApiClientAuthenticationError(
msg,
)
response.raise_for_status()
class IntegrationBlueprintApiClient:
"""Sample API Client."""
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) -> Any:
"""Get data from the API."""
return await self._api_wrapper(
method="get",
url="https://jsonplaceholder.typicode.com/posts/1",
)
async def async_set_title(self, value: str) -> Any:
"""Get data from the API."""
return await self._api_wrapper(
method="patch",
url="https://jsonplaceholder.typicode.com/posts/1",
data={"title": value},
headers={"Content-type": "application/json; charset=UTF-8"},
)
async def _api_wrapper(
self,
method: str,
url: str,
data: dict | None = None,
headers: dict | None = None,
) -> Any:
"""Get information from the API."""
try:
async with async_timeout.timeout(10):
response = await self._session.request(
method=method,
url=url,
headers=headers,
json=data,
)
_verify_response_or_raise(response)
return await response.json()
except TimeoutError as exception:
msg = f"Timeout error fetching information - {exception}"
raise IntegrationBlueprintApiClientCommunicationError(
msg,
) from exception
except (aiohttp.ClientError, socket.gaierror) as exception:
msg = f"Error fetching information - {exception}"
raise IntegrationBlueprintApiClientCommunicationError(
msg,
) from exception
except Exception as exception: # pylint: disable=broad-except
msg = f"Something really wrong happened! - {exception}"
raise IntegrationBlueprintApiClientError(
msg,
) from exception

View file

@ -0,0 +1,61 @@
"""Binary sensor platform for integration_blueprint."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from .entity import IntegrationBlueprintEntity
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import BlueprintDataUpdateCoordinator
from .data import IntegrationBlueprintConfigEntry
ENTITY_DESCRIPTIONS = (
BinarySensorEntityDescription(
key="integration_blueprint",
name="Integration Blueprint Binary Sensor",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
),
)
async def async_setup_entry(
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
entry: IntegrationBlueprintConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the binary_sensor platform."""
async_add_entities(
IntegrationBlueprintBinarySensor(
coordinator=entry.runtime_data.coordinator,
entity_description=entity_description,
)
for entity_description in ENTITY_DESCRIPTIONS
)
class IntegrationBlueprintBinarySensor(IntegrationBlueprintEntity, BinarySensorEntity):
"""integration_blueprint binary_sensor class."""
def __init__(
self,
coordinator: BlueprintDataUpdateCoordinator,
entity_description: BinarySensorEntityDescription,
) -> None:
"""Initialize the binary_sensor class."""
super().__init__(coordinator)
self.entity_description = entity_description
@property
def is_on(self) -> bool:
"""Return true if the binary_sensor is on."""
return self.coordinator.data.get("title", "") == "foo"

View file

@ -0,0 +1,89 @@
"""Adds config flow for Blueprint."""
from __future__ import annotations
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from slugify import slugify
from .api import (
IntegrationBlueprintApiClient,
IntegrationBlueprintApiClientAuthenticationError,
IntegrationBlueprintApiClientCommunicationError,
IntegrationBlueprintApiClientError,
)
from .const import DOMAIN, LOGGER
class BlueprintFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Blueprint."""
VERSION = 1
async def async_step_user(
self,
user_input: dict | None = None,
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by the user."""
_errors = {}
if user_input is not None:
try:
await self._test_credentials(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
except IntegrationBlueprintApiClientAuthenticationError as exception:
LOGGER.warning(exception)
_errors["base"] = "auth"
except IntegrationBlueprintApiClientCommunicationError as exception:
LOGGER.error(exception)
_errors["base"] = "connection"
except IntegrationBlueprintApiClientError as exception:
LOGGER.exception(exception)
_errors["base"] = "unknown"
else:
await self.async_set_unique_id(
## Do NOT use this in production code
## The unique_id should never be something that can change
## https://developers.home-assistant.io/docs/config_entries_config_flow_handler#unique-ids
unique_id=slugify(user_input[CONF_USERNAME])
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME,
default=(user_input or {}).get(CONF_USERNAME, vol.UNDEFINED),
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT,
),
),
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.PASSWORD,
),
),
},
),
errors=_errors,
)
async def _test_credentials(self, username: str, password: str) -> None:
"""Validate credentials."""
client = IntegrationBlueprintApiClient(
username=username,
password=password,
session=async_create_clientsession(self.hass),
)
await client.async_get_data()

View file

@ -0,0 +1,8 @@
"""Constants for integration_blueprint."""
from logging import Logger, getLogger
LOGGER: Logger = getLogger(__package__)
DOMAIN = "integration_blueprint"
ATTRIBUTION = "Data provided by http://jsonplaceholder.typicode.com/"

View file

@ -0,0 +1,32 @@
"""DataUpdateCoordinator for integration_blueprint."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .api import (
IntegrationBlueprintApiClientAuthenticationError,
IntegrationBlueprintApiClientError,
)
if TYPE_CHECKING:
from .data import IntegrationBlueprintConfigEntry
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
class BlueprintDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
config_entry: IntegrationBlueprintConfigEntry
async def _async_update_data(self) -> Any:
"""Update data via library."""
try:
return await self.config_entry.runtime_data.client.async_get_data()
except IntegrationBlueprintApiClientAuthenticationError as exception:
raise ConfigEntryAuthFailed(exception) from exception
except IntegrationBlueprintApiClientError as exception:
raise UpdateFailed(exception) from exception

View file

@ -0,0 +1,25 @@
"""Custom types for integration_blueprint."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
from homeassistant.loader import Integration
from .api import IntegrationBlueprintApiClient
from .coordinator import BlueprintDataUpdateCoordinator
type IntegrationBlueprintConfigEntry = ConfigEntry[IntegrationBlueprintData]
@dataclass
class IntegrationBlueprintData:
"""Data for the Blueprint integration."""
client: IntegrationBlueprintApiClient
coordinator: BlueprintDataUpdateCoordinator
integration: Integration

View file

@ -0,0 +1,28 @@
"""BlueprintEntity class."""
from __future__ import annotations
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION
from .coordinator import BlueprintDataUpdateCoordinator
class IntegrationBlueprintEntity(CoordinatorEntity[BlueprintDataUpdateCoordinator]):
"""BlueprintEntity class."""
_attr_attribution = ATTRIBUTION
def __init__(self, coordinator: BlueprintDataUpdateCoordinator) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_unique_id = coordinator.config_entry.entry_id
self._attr_device_info = DeviceInfo(
identifiers={
(
coordinator.config_entry.domain,
coordinator.config_entry.entry_id,
),
},
)

View file

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

View file

@ -0,0 +1,57 @@
"""Sensor platform for integration_blueprint."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from .entity import IntegrationBlueprintEntity
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import BlueprintDataUpdateCoordinator
from .data import IntegrationBlueprintConfigEntry
ENTITY_DESCRIPTIONS = (
SensorEntityDescription(
key="integration_blueprint",
name="Integration Sensor",
icon="mdi:format-quote-close",
),
)
async def async_setup_entry(
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
entry: IntegrationBlueprintConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
async_add_entities(
IntegrationBlueprintSensor(
coordinator=entry.runtime_data.coordinator,
entity_description=entity_description,
)
for entity_description in ENTITY_DESCRIPTIONS
)
class IntegrationBlueprintSensor(IntegrationBlueprintEntity, SensorEntity):
"""integration_blueprint Sensor class."""
def __init__(
self,
coordinator: BlueprintDataUpdateCoordinator,
entity_description: SensorEntityDescription,
) -> None:
"""Initialize the sensor class."""
super().__init__(coordinator)
self.entity_description = entity_description
@property
def native_value(self) -> str | None:
"""Return the native value of the sensor."""
return self.coordinator.data.get("body")

View file

@ -0,0 +1,67 @@
"""Switch platform for integration_blueprint."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from .entity import IntegrationBlueprintEntity
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import BlueprintDataUpdateCoordinator
from .data import IntegrationBlueprintConfigEntry
ENTITY_DESCRIPTIONS = (
SwitchEntityDescription(
key="integration_blueprint",
name="Integration Switch",
icon="mdi:format-quote-close",
),
)
async def async_setup_entry(
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
entry: IntegrationBlueprintConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the switch platform."""
async_add_entities(
IntegrationBlueprintSwitch(
coordinator=entry.runtime_data.coordinator,
entity_description=entity_description,
)
for entity_description in ENTITY_DESCRIPTIONS
)
class IntegrationBlueprintSwitch(IntegrationBlueprintEntity, SwitchEntity):
"""integration_blueprint switch class."""
def __init__(
self,
coordinator: BlueprintDataUpdateCoordinator,
entity_description: SwitchEntityDescription,
) -> None:
"""Initialize the switch class."""
super().__init__(coordinator)
self.entity_description = entity_description
@property
def is_on(self) -> bool:
"""Return true if the switch is on."""
return self.coordinator.data.get("title", "") == "foo"
async def async_turn_on(self, **_: Any) -> None:
"""Turn on the switch."""
await self.coordinator.config_entry.runtime_data.client.async_set_title("bar")
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **_: Any) -> None:
"""Turn off the switch."""
await self.coordinator.config_entry.runtime_data.client.async_set_title("foo")
await self.coordinator.async_request_refresh()

View file

@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"description": "If you need help with the configuration have a look here: https://github.com/ludeeus/integration_blueprint",
"data": {
"username": "Username",
"password": "Password"
}
}
},
"error": {
"auth": "Username/Password is wrong.",
"connection": "Unable to connect to the server.",
"unknown": "Unknown error occurred."
},
"abort": {
"already_configured": "This entry is already configured."
}
}
}

5
hacs.json Normal file
View file

@ -0,0 +1,5 @@
{
"name": "Integration blueprint",
"homeassistant": "2025.2.4",
"hacs": "2.0.1"
}

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
colorlog==6.9.0
homeassistant==2025.2.4
pip>=21.3.1
ruff==0.11.5

20
scripts/develop Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/integration_blueprint
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
hass --config "${PWD}/config" --debug

8
scripts/lint Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
ruff format .
ruff check . --fix

7
scripts/setup Executable file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
python3 -m pip install --requirement requirements.txt