mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
Initial commit
This commit is contained in:
commit
131a4eb148
31 changed files with 1049 additions and 0 deletions
49
.devcontainer.json
Normal file
49
.devcontainer.json
Normal 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
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
||||
54
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal 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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
||||
46
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
46
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
20
.github/dependabot.yml
vendored
Normal 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
34
.github/workflows/lint.yml
vendored
Normal 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
36
.github/workflows/validate.yml
vendored
Normal 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
18
.gitignore
vendored
Normal 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
26
.ruff.toml
Normal 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
61
CONTRIBUTING.md
Normal 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
21
LICENSE
Normal 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
46
README.md
Normal 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
12
config/configuration.yaml
Normal 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
|
||||
79
custom_components/integration_blueprint/__init__.py
Normal file
79
custom_components/integration_blueprint/__init__.py
Normal 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)
|
||||
101
custom_components/integration_blueprint/api.py
Normal file
101
custom_components/integration_blueprint/api.py
Normal 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
|
||||
61
custom_components/integration_blueprint/binary_sensor.py
Normal file
61
custom_components/integration_blueprint/binary_sensor.py
Normal 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"
|
||||
89
custom_components/integration_blueprint/config_flow.py
Normal file
89
custom_components/integration_blueprint/config_flow.py
Normal 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()
|
||||
8
custom_components/integration_blueprint/const.py
Normal file
8
custom_components/integration_blueprint/const.py
Normal 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/"
|
||||
32
custom_components/integration_blueprint/coordinator.py
Normal file
32
custom_components/integration_blueprint/coordinator.py
Normal 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
|
||||
25
custom_components/integration_blueprint/data.py
Normal file
25
custom_components/integration_blueprint/data.py
Normal 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
|
||||
28
custom_components/integration_blueprint/entity.py
Normal file
28
custom_components/integration_blueprint/entity.py
Normal 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,
|
||||
),
|
||||
},
|
||||
)
|
||||
12
custom_components/integration_blueprint/manifest.json
Normal file
12
custom_components/integration_blueprint/manifest.json
Normal 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"
|
||||
}
|
||||
57
custom_components/integration_blueprint/sensor.py
Normal file
57
custom_components/integration_blueprint/sensor.py
Normal 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")
|
||||
67
custom_components/integration_blueprint/switch.py
Normal file
67
custom_components/integration_blueprint/switch.py
Normal 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()
|
||||
21
custom_components/integration_blueprint/translations/en.json
Normal file
21
custom_components/integration_blueprint/translations/en.json
Normal 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
5
hacs.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "Integration blueprint",
|
||||
"homeassistant": "2025.2.4",
|
||||
"hacs": "2.0.1"
|
||||
}
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal 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
20
scripts/develop
Executable 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
8
scripts/lint
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
ruff format .
|
||||
ruff check . --fix
|
||||
7
scripts/setup
Executable file
7
scripts/setup
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
python3 -m pip install --requirement requirements.txt
|
||||
Loading…
Reference in a new issue