mirror of
https://github.com/home-assistant/core.git
synced 2026-04-08 16:35:17 +00:00
Compare commits
77 Commits
infrared_r
...
adjust_dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4a117e6b1 | ||
|
|
1b4286381d | ||
|
|
524c2129eb | ||
|
|
8fe09e1837 | ||
|
|
99a186fad7 | ||
|
|
c1a9f293a7 | ||
|
|
783e2f0a00 | ||
|
|
36045c4bd3 | ||
|
|
18cd488622 | ||
|
|
ef66446a0d | ||
|
|
4870bb749c | ||
|
|
2e2ad0aaec | ||
|
|
fa7576dc5a | ||
|
|
c6ec90c871 | ||
|
|
c2065f1f14 | ||
|
|
5197722733 | ||
|
|
49d63892d1 | ||
|
|
713054f9f8 | ||
|
|
0b67644b97 | ||
|
|
f2001db68c | ||
|
|
df08d989f2 | ||
|
|
d5c7a04751 | ||
|
|
3369dfece1 | ||
|
|
7268571587 | ||
|
|
2341f8dd5a | ||
|
|
dd1722b5d6 | ||
|
|
c8667addd8 | ||
|
|
3b9a9ca6cb | ||
|
|
52050711a3 | ||
|
|
17abdd02d3 | ||
|
|
996f9fdca2 | ||
|
|
a434a0ab90 | ||
|
|
7bff0e2f3f | ||
|
|
9cf6911b7f | ||
|
|
b0201e893e | ||
|
|
df74d76ff2 | ||
|
|
6dc391e169 | ||
|
|
c7cf78952e | ||
|
|
2591cf2b3d | ||
|
|
2b1c93724f | ||
|
|
899b776e54 | ||
|
|
423b694a0d | ||
|
|
01324a84a8 | ||
|
|
b63ea35959 | ||
|
|
bb345dfd09 | ||
|
|
c05c2b7f70 | ||
|
|
3d07ec8696 | ||
|
|
3b396814ae | ||
|
|
b2047c1aca | ||
|
|
2b0cff2c93 | ||
|
|
fa7af34678 | ||
|
|
7563ea6217 | ||
|
|
08726af215 | ||
|
|
4fa1d6b0a1 | ||
|
|
3c86f1eee8 | ||
|
|
3a63f9fbb1 | ||
|
|
7b5408d20c | ||
|
|
058e8ba455 | ||
|
|
bba3c0e6bb | ||
|
|
a266976c33 | ||
|
|
f29c051c73 | ||
|
|
8842b4840e | ||
|
|
586d2ceff6 | ||
|
|
69a2284a00 | ||
|
|
19761a25da | ||
|
|
e4328fe34d | ||
|
|
e91b49e7cd | ||
|
|
7d145cd3b8 | ||
|
|
962d5386c7 | ||
|
|
3ba985f771 | ||
|
|
ef6718c242 | ||
|
|
02bcae00cf | ||
|
|
d6cd1dffa4 | ||
|
|
fc32f0dbd3 | ||
|
|
cda1974e40 | ||
|
|
5425e82fb4 | ||
|
|
84f36b0d4d |
228
.claude/agents/raise-pull-request.md
Normal file
228
.claude/agents/raise-pull-request.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: raise-pull-request
|
||||
description: |
|
||||
Use this agent when creating a pull request for the Home Assistant core repository after completing implementation work. This agent automates the PR creation process including running tests, formatting checks, and proper checkbox handling.
|
||||
model: inherit
|
||||
color: green
|
||||
tools: Read, Bash, Grep, Glob
|
||||
---
|
||||
|
||||
You are an expert at creating pull requests for the Home Assistant core repository. You will automate the PR creation process with proper verification, formatting, testing, and checkbox handling.
|
||||
|
||||
**Execute each step in order. Do not skip steps.**
|
||||
|
||||
## Step 1: Gather Information
|
||||
|
||||
Run these commands in parallel to analyze the changes:
|
||||
|
||||
```bash
|
||||
# Get current branch and remote
|
||||
git branch --show-current
|
||||
git remote -v | grep push
|
||||
|
||||
# Determine the best available dev reference
|
||||
if git rev-parse --verify --quiet upstream/dev >/dev/null; then
|
||||
BASE_REF="upstream/dev"
|
||||
elif git rev-parse --verify --quiet origin/dev >/dev/null; then
|
||||
BASE_REF="origin/dev"
|
||||
elif git rev-parse --verify --quiet dev >/dev/null; then
|
||||
BASE_REF="dev"
|
||||
else
|
||||
echo "Could not find upstream/dev, origin/dev, or local dev"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_SHA="$(git merge-base "$BASE_REF" HEAD)"
|
||||
echo "BASE_REF=$BASE_REF"
|
||||
echo "BASE_SHA=$BASE_SHA"
|
||||
|
||||
# Get commit info for this branch vs dev
|
||||
git log "${BASE_SHA}..HEAD" --oneline
|
||||
|
||||
# Check what files changed
|
||||
git diff "${BASE_SHA}..HEAD" --name-only
|
||||
|
||||
# Check if test files were added/modified
|
||||
git diff "${BASE_SHA}..HEAD" --name-only | grep -E "^tests/.*\.py$" || echo "NO_TESTS_CHANGED"
|
||||
|
||||
# Check if manifest.json changed
|
||||
git diff "${BASE_SHA}..HEAD" --name-only | grep "manifest.json" || echo "NO_MANIFEST_CHANGED"
|
||||
```
|
||||
|
||||
From the file paths, extract the **integration domain** from `homeassistant/components/{integration}/` or `tests/components/{integration}/`.
|
||||
|
||||
**Track results:**
|
||||
- `BASE_REF`: the dev reference used for comparison
|
||||
- `BASE_SHA`: the merge-base commit used for diff-based checks
|
||||
- `TESTS_CHANGED`: true if test files were added or modified
|
||||
- `MANIFEST_CHANGED`: true if manifest.json was modified
|
||||
|
||||
**If no suitable dev reference is available, STOP and tell the user to fetch `upstream/dev`, `origin/dev`, or a local `dev` branch before continuing.**
|
||||
|
||||
## Step 2: Run Code Quality Checks
|
||||
|
||||
Run `prek` to perform code quality checks (formatting, linting, hassfest, etc.) on the files changed since `BASE_SHA`:
|
||||
|
||||
```bash
|
||||
prek run --from-ref "$BASE_SHA" --to-ref HEAD
|
||||
```
|
||||
|
||||
**Track results:**
|
||||
- `PREK_PASSED`: true if `prek run` exits with code 0
|
||||
|
||||
**If `prek` fails or is not available, STOP and report the failure to the user. Do not proceed with PR creation. If the failure appears to be an environment setup issue (e.g., missing tools, command not found, venv not activated), also point the user to https://developers.home-assistant.io/docs/development_environment.**
|
||||
|
||||
## Step 3: Stage Any Changes from Checks
|
||||
|
||||
If `prek` made any formatting or generated file changes, stage and commit them as a separate commit:
|
||||
|
||||
```bash
|
||||
git status --porcelain
|
||||
# If changes exist:
|
||||
git add -A
|
||||
git commit -m "Apply prek formatting and generated file updates"
|
||||
```
|
||||
|
||||
## Step 4: Run Tests
|
||||
|
||||
Run pytest for the specific integration:
|
||||
|
||||
```bash
|
||||
pytest tests/components/{integration} \
|
||||
--timeout=60 \
|
||||
--durations-min=1 \
|
||||
--durations=0 \
|
||||
-q
|
||||
```
|
||||
|
||||
**Track results:**
|
||||
- `TESTS_PASSED`: true if pytest exits with code 0
|
||||
|
||||
**If tests fail, STOP and report the failures to the user. Do not proceed with PR creation.**
|
||||
|
||||
## Step 5: Identify PR Metadata
|
||||
|
||||
Write a release-note-style PR title summarizing the change. The title becomes the release notes entry, so it should be a complete sentence fragment describing what changed in imperative mood.
|
||||
|
||||
**PR Title Examples by Type:**
|
||||
| Type | Example titles |
|
||||
|------|----------------|
|
||||
| Bugfix | `Fix Hikvision NVR binary sensors not being detected` |
|
||||
| | `Fix JSON serialization of time objects in anthropic tool results` |
|
||||
| | `Fix config flow bug in Tesla Fleet` |
|
||||
| Dependency | `Bump eheimdigital to 1.5.0` |
|
||||
| | `Bump python-otbr-api to 2.7.1` |
|
||||
| New feature | `Add asyncio-level timeout to Backblaze B2 uploads` |
|
||||
| | `Add Nettleie optimization option` |
|
||||
| Code quality | `Add exception translations to Teslemetry` |
|
||||
| | `Improve test coverage of Tesla Fleet` |
|
||||
| | `Refactor adguard tests to use proper fixtures for mocking` |
|
||||
| | `Simplify entity init in Proxmox` |
|
||||
|
||||
## Step 6: Verify Development Checklist
|
||||
|
||||
Check each item from the [development checklist](https://developers.home-assistant.io/docs/development_checklist/):
|
||||
|
||||
| Item | How to verify |
|
||||
|------|---------------|
|
||||
| External libraries on PyPI | Check manifest.json requirements - all should be PyPI packages |
|
||||
| Dependencies in requirements_all.txt | Only if dependency declarations changed (the `requirements` field in `manifest.json` or `requirements_all.txt`), run `python -m script.gen_requirements_all` |
|
||||
| Codeowners updated | If this is a new integration, ensure its `manifest.json` includes a `codeowners` field with one or more GitHub usernames |
|
||||
| No commented out code | Visually scan the diff for blocks of commented-out code |
|
||||
|
||||
**Track results:**
|
||||
- `NO_COMMENTED_CODE`: true if no blocks of commented-out code found in the diff
|
||||
- `DEPENDENCIES_CHANGED`: true if the diff changes the `requirements` field in `manifest.json` or changes `requirements_all.txt`
|
||||
- `REQUIREMENTS_UPDATED`: true if `DEPENDENCIES_CHANGED` is true and requirements_all.txt was regenerated successfully; not applicable if `DEPENDENCIES_CHANGED` is false
|
||||
- `CHECKLIST_PASSED`: true if all items above pass
|
||||
|
||||
## Step 7: Determine Type of Change
|
||||
|
||||
Select exactly ONE based on the changes. Mark the selected type with `[x]` and all others with `[ ]` (space):
|
||||
|
||||
| Type | Condition |
|
||||
|------|-----------|
|
||||
| Dependency upgrade | Only manifest.json/requirements changes |
|
||||
| Bugfix | Fixes broken behavior, no new features |
|
||||
| New integration | New folder in components/ |
|
||||
| New feature | Adds capability to existing integration |
|
||||
| Deprecation | Adds deprecation warnings for future breaking change |
|
||||
| Breaking change | Removes or changes existing functionality |
|
||||
| Code quality | Only refactoring or test additions, no functional change |
|
||||
|
||||
**Track results:**
|
||||
- `CHANGE_TYPE`: the selected type (e.g., "Bugfix", "New feature", "Code quality", etc.)
|
||||
|
||||
**Important:** All seven type options must remain in the PR body. Only the selected type gets `[x]`, all others get `[ ]`.
|
||||
|
||||
## Step 8: Determine Checkbox States
|
||||
|
||||
Based on the verification steps above, determine checkbox states:
|
||||
|
||||
| Checkbox | Condition to tick |
|
||||
|----------|-------------------|
|
||||
| The code change is tested and works locally | Leave unchecked for the contributor to verify manually (this refers to manual testing, not unit tests) |
|
||||
| Local tests pass | Tick only if `TESTS_PASSED` is true |
|
||||
| I understand the code I am submitting and can explain how it works | Leave unchecked for the contributor to review and set manually |
|
||||
| There is no commented out code | Tick only if `NO_COMMENTED_CODE` is true |
|
||||
| Development checklist | Tick only if `CHECKLIST_PASSED` is true |
|
||||
| Perfect PR recommendations | Tick only if the PR affects a single integration or closely related modules, represents one primary type of change, and has a clear, self-contained scope |
|
||||
| Formatted using Ruff | Tick only if `PREK_PASSED` is true |
|
||||
| Tests have been added | Tick only if `TESTS_CHANGED` is true AND the changes exercise new or changed functionality (not only cosmetic test changes) |
|
||||
| Documentation added/updated | Tick if documentation PR created (or not applicable) |
|
||||
| Manifest file fields filled out | Tick if `PREK_PASSED` is true (or not applicable) |
|
||||
| Dependencies in requirements_all.txt | Tick only if `DEPENDENCIES_CHANGED` is false, or if `DEPENDENCIES_CHANGED` is true and `REQUIREMENTS_UPDATED` is true |
|
||||
| Dependency changelog linked | Tick if dependency changelog linked in PR description (or not applicable) |
|
||||
| Any generated code has been carefully reviewed | Leave unchecked for the contributor to review and set manually |
|
||||
|
||||
## Step 9: Breaking Change Section
|
||||
|
||||
**If `CHANGE_TYPE` is NOT "Breaking change" or "Deprecation": REMOVE the entire "## Breaking change" section from the PR body (including the heading).**
|
||||
|
||||
If `CHANGE_TYPE` IS "Breaking change" or "Deprecation", keep the `## Breaking change` section and describe:
|
||||
- What breaks
|
||||
- How users can fix it
|
||||
- Why it was necessary
|
||||
|
||||
## Step 10: Push Branch and Create PR
|
||||
|
||||
```bash
|
||||
# Get branch name and GitHub username
|
||||
BRANCH=$(git branch --show-current)
|
||||
PUSH_REMOTE=$(git config "branch.$BRANCH.remote" 2>/dev/null || git remote | head -1)
|
||||
GITHUB_USER=$(gh api user --jq .login 2>/dev/null || git remote get-url "$PUSH_REMOTE" | sed -E 's#.*[:/]([^/]+)/([^/]+)(\.git)?$#\1#')
|
||||
|
||||
# Create PR (gh pr create pushes the branch automatically)
|
||||
gh pr create --repo home-assistant/core --base dev \
|
||||
--head "$GITHUB_USER:$BRANCH" \
|
||||
--title "TITLE_HERE" \
|
||||
--body "$(cat <<'EOF'
|
||||
BODY_HERE
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### PR Body Template
|
||||
|
||||
Read the PR template from `.github/PULL_REQUEST_TEMPLATE.md` and use it as the basis for the PR body. **Do not hardcode the template — always read it from the file to stay in sync with upstream changes.**
|
||||
|
||||
Use any HTML comments (`<!-- ... -->`) in the template as guidance to understand what to fill in. For the final PR body sent to GitHub, keep the template text intact — do not delete any text from the template unless it explicitly instructs removal (e.g., the breaking change section when not applicable). Then fill in the sections:
|
||||
|
||||
1. **Breaking change section**: If the type is NOT "Breaking change" or "Deprecation", remove the entire `## Breaking change` section (heading and body). Otherwise, describe what breaks, how users can fix it, and why.
|
||||
2. **Proposed change section**: Fill in a description of the change extracted from commit messages.
|
||||
3. **Type of change**: Check exactly ONE checkbox matching the determined type from Step 7. Leave all others unchecked.
|
||||
4. **Additional information**: Fill in any related issue numbers if known.
|
||||
5. **Checklist**: Check boxes based on the conditions in Step 8. Leave manual-verification boxes unchecked for the contributor.
|
||||
|
||||
**Important:** Preserve all template structure, options, and link references exactly as they appear in the file — only modify checkbox states and fill in content sections.
|
||||
|
||||
## Step 11: Report Result
|
||||
|
||||
Provide the user with:
|
||||
1. **PR URL** - The created pull request link
|
||||
2. **Verification Summary** - Which checks passed/failed
|
||||
3. **Unchecked Items** - List any checkboxes left unchecked and why
|
||||
4. **User Action Required** - Remind user to:
|
||||
- Review and set manual-verification checkboxes ("I understand the code..." and "Any generated code...") as applicable
|
||||
- Consider reviewing two other open PRs
|
||||
- Add any related issue numbers if applicable
|
||||
@@ -174,7 +174,6 @@ homeassistant.components.dnsip.*
|
||||
homeassistant.components.doorbird.*
|
||||
homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.downloader.*
|
||||
homeassistant.components.dropbox.*
|
||||
homeassistant.components.droplet.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.duckdns.*
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -401,8 +401,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dropbox/ @bdr99
|
||||
/tests/components/dropbox/ @bdr99
|
||||
/homeassistant/components/droplet/ @sarahseidman
|
||||
/tests/components/droplet/ @sarahseidman
|
||||
/homeassistant/components/dsmr/ @Robbie1221
|
||||
|
||||
5
homeassistant/brands/bega.json
Normal file
5
homeassistant/brands/bega.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "bega",
|
||||
"name": "BEGA",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
"""The actiontec component."""
|
||||
"""The Actiontec integration."""
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from adext import AdExt
|
||||
from alarmdecoder.devices import SerialDevice, SocketDevice
|
||||
from alarmdecoder.devices import Device, SerialDevice, SocketDevice
|
||||
from alarmdecoder.util import NoDeviceError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -102,16 +102,21 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._async_current_entries(), user_input, self.protocol
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
connection = {}
|
||||
connection: dict[str, Any] = {}
|
||||
baud = None
|
||||
device: Device
|
||||
if self.protocol == PROTOCOL_SOCKET:
|
||||
host = connection[CONF_HOST] = user_input[CONF_HOST]
|
||||
port = connection[CONF_PORT] = user_input[CONF_PORT]
|
||||
title = f"{host}:{port}"
|
||||
host = connection[CONF_HOST] = cast(str, user_input[CONF_HOST])
|
||||
port = connection[CONF_PORT] = cast(int, user_input[CONF_PORT])
|
||||
title: str = f"{host}:{port}"
|
||||
device = SocketDevice(interface=(host, port))
|
||||
if self.protocol == PROTOCOL_SERIAL:
|
||||
path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
|
||||
baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
|
||||
path = connection[CONF_DEVICE_PATH] = cast(
|
||||
str, user_input[CONF_DEVICE_PATH]
|
||||
)
|
||||
baud = connection[CONF_DEVICE_BAUD] = cast(
|
||||
int, user_input[CONF_DEVICE_BAUD]
|
||||
)
|
||||
title = path
|
||||
device = SerialDevice(interface=path)
|
||||
|
||||
@@ -132,6 +137,7 @@ class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception during AlarmDecoder setup")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
schema: vol.Schema
|
||||
if self.protocol == PROTOCOL_SOCKET:
|
||||
schema = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.1"]
|
||||
"requirements": ["aioamazondevices==13.3.2"]
|
||||
}
|
||||
|
||||
64
homeassistant/components/anthropic/diagnostics.py
Normal file
64
homeassistant/components/anthropic/diagnostics.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Diagnostics support for Anthropic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from anthropic import __title__, __version__
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .const import (
|
||||
CONF_PROMPT,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AnthropicConfigEntry
|
||||
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_API_KEY,
|
||||
CONF_PROMPT,
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AnthropicConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return {
|
||||
"client": f"{__title__}=={__version__}",
|
||||
"title": entry.title,
|
||||
"entry_id": entry.entry_id,
|
||||
"entry_version": f"{entry.version}.{entry.minor_version}",
|
||||
"state": entry.state.value,
|
||||
"data": async_redact_data(entry.data, TO_REDACT),
|
||||
"options": async_redact_data(entry.options, TO_REDACT),
|
||||
"subentries": {
|
||||
subentry.subentry_id: {
|
||||
"title": subentry.title,
|
||||
"subentry_type": subentry.subentry_type,
|
||||
"data": async_redact_data(subentry.data, TO_REDACT),
|
||||
}
|
||||
for subentry in entry.subentries.values()
|
||||
},
|
||||
"entities": {
|
||||
entity_entry.entity_id: entity_entry.extended_dict
|
||||
for entity_entry in er.async_entries_for_config_entry(
|
||||
er.async_get(hass), entry.entry_id
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -46,7 +46,7 @@ rules:
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
@@ -61,10 +61,7 @@ rules:
|
||||
No data updates.
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: todo
|
||||
comment: |
|
||||
To write something about what models we support.
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
|
||||
@@ -30,9 +30,10 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_CREDENTIALS,
|
||||
@@ -42,9 +43,12 @@ from .const import (
|
||||
SIGNAL_CONNECTED,
|
||||
SIGNAL_DISCONNECTED,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
DEFAULT_NAME_TV = "Apple TV"
|
||||
DEFAULT_NAME_HP = "HomePod"
|
||||
|
||||
@@ -77,6 +81,12 @@ DEVICE_EXCEPTIONS = (
|
||||
type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Apple TV component."""
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
|
||||
"""Set up a config entry for Apple TV."""
|
||||
manager = AppleTVManager(hass, entry)
|
||||
|
||||
@@ -9,3 +9,5 @@ CONF_START_OFF = "start_off"
|
||||
|
||||
SIGNAL_CONNECTED = "apple_tv_connected"
|
||||
SIGNAL_DISCONNECTED = "apple_tv_disconnected"
|
||||
|
||||
ATTR_TEXT = "text"
|
||||
|
||||
@@ -8,5 +8,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"append_keyboard_text": {
|
||||
"service": "mdi:keyboard"
|
||||
},
|
||||
"clear_keyboard_text": {
|
||||
"service": "mdi:keyboard-off"
|
||||
},
|
||||
"set_keyboard_text": {
|
||||
"service": "mdi:keyboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
homeassistant/components/apple_tv/services.py
Normal file
130
homeassistant/components/apple_tv/services.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Define services for the Apple TV integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyatv.const import KeyboardFocusState
|
||||
from pyatv.exceptions import NotSupportedError, ProtocolError
|
||||
from pyatv.interface import AppleTV as AppleTVInterface
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
|
||||
from .const import ATTR_TEXT, DOMAIN
|
||||
|
||||
SERVICE_SET_KEYBOARD_TEXT = "set_keyboard_text"
|
||||
SERVICE_SET_KEYBOARD_TEXT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Required(ATTR_TEXT): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_APPEND_KEYBOARD_TEXT = "append_keyboard_text"
|
||||
SERVICE_APPEND_KEYBOARD_TEXT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Required(ATTR_TEXT): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_CLEAR_KEYBOARD_TEXT = "clear_keyboard_text"
|
||||
SERVICE_CLEAR_KEYBOARD_TEXT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_atv(call: ServiceCall) -> AppleTVInterface:
|
||||
"""Get the AppleTVInterface for a service call."""
|
||||
entry = service.async_get_config_entry(
|
||||
call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
|
||||
)
|
||||
atv: AppleTVInterface | None = entry.runtime_data.atv
|
||||
if atv is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_connected",
|
||||
)
|
||||
return atv
|
||||
|
||||
|
||||
def _check_keyboard_focus(atv: AppleTVInterface) -> None:
|
||||
"""Check that keyboard is focused on the device."""
|
||||
try:
|
||||
focus_state = atv.keyboard.text_focus_state
|
||||
except NotSupportedError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_available",
|
||||
) from err
|
||||
if focus_state != KeyboardFocusState.Focused:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_focused",
|
||||
)
|
||||
|
||||
|
||||
async def _async_set_keyboard_text(call: ServiceCall) -> None:
|
||||
"""Set text in the keyboard input field on an Apple TV."""
|
||||
atv = _get_atv(call)
|
||||
_check_keyboard_focus(atv)
|
||||
try:
|
||||
await atv.keyboard.text_set(call.data[ATTR_TEXT])
|
||||
except ProtocolError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_error",
|
||||
) from err
|
||||
|
||||
|
||||
async def _async_append_keyboard_text(call: ServiceCall) -> None:
|
||||
"""Append text to the keyboard input field on an Apple TV."""
|
||||
atv = _get_atv(call)
|
||||
_check_keyboard_focus(atv)
|
||||
try:
|
||||
await atv.keyboard.text_append(call.data[ATTR_TEXT])
|
||||
except ProtocolError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_error",
|
||||
) from err
|
||||
|
||||
|
||||
async def _async_clear_keyboard_text(call: ServiceCall) -> None:
|
||||
"""Clear text in the keyboard input field on an Apple TV."""
|
||||
atv = _get_atv(call)
|
||||
_check_keyboard_focus(atv)
|
||||
try:
|
||||
await atv.keyboard.text_clear()
|
||||
except ProtocolError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_error",
|
||||
) from err
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Apple TV integration."""
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_KEYBOARD_TEXT,
|
||||
_async_set_keyboard_text,
|
||||
schema=SERVICE_SET_KEYBOARD_TEXT_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_KEYBOARD_TEXT,
|
||||
_async_append_keyboard_text,
|
||||
schema=SERVICE_APPEND_KEYBOARD_TEXT_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_KEYBOARD_TEXT,
|
||||
_async_clear_keyboard_text,
|
||||
schema=SERVICE_CLEAR_KEYBOARD_TEXT_SCHEMA,
|
||||
)
|
||||
31
homeassistant/components/apple_tv/services.yaml
Normal file
31
homeassistant/components/apple_tv/services.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
set_keyboard_text:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: apple_tv
|
||||
text:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
|
||||
append_keyboard_text:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: apple_tv
|
||||
text:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
|
||||
clear_keyboard_text:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: apple_tv
|
||||
@@ -69,6 +69,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"keyboard_error": {
|
||||
"message": "An error occurred while sending text to the Apple TV"
|
||||
},
|
||||
"keyboard_not_available": {
|
||||
"message": "Keyboard input is not supported by this device"
|
||||
},
|
||||
"keyboard_not_focused": {
|
||||
"message": "No text input field is currently focused on the Apple TV"
|
||||
},
|
||||
"not_connected": {
|
||||
"message": "Apple TV is not connected"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
@@ -78,5 +92,45 @@
|
||||
"description": "Configure general device settings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"append_keyboard_text": {
|
||||
"description": "Appends text to the currently focused text input field on an Apple TV.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"description": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::description%]",
|
||||
"name": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::name%]"
|
||||
},
|
||||
"text": {
|
||||
"description": "The text to append.",
|
||||
"name": "[%key:component::apple_tv::services::set_keyboard_text::fields::text::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Append keyboard text"
|
||||
},
|
||||
"clear_keyboard_text": {
|
||||
"description": "Clears the text in the currently focused text input field on an Apple TV.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"description": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::description%]",
|
||||
"name": "[%key:component::apple_tv::services::set_keyboard_text::fields::config_entry_id::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Clear keyboard text"
|
||||
},
|
||||
"set_keyboard_text": {
|
||||
"description": "Sets the text in the currently focused text input field on an Apple TV.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"description": "The Apple TV to send text to.",
|
||||
"name": "Apple TV"
|
||||
},
|
||||
"text": {
|
||||
"description": "The text to set.",
|
||||
"name": "Text"
|
||||
}
|
||||
},
|
||||
"name": "Set keyboard text"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The aruba component."""
|
||||
"""The Aruba integration."""
|
||||
|
||||
@@ -12,7 +12,7 @@ import hashlib
|
||||
import io
|
||||
from itertools import chain
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path, PurePath, PureWindowsPath
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -1957,7 +1957,10 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
suggested_filename: str,
|
||||
) -> WrittenBackup:
|
||||
"""Receive a backup."""
|
||||
temp_file = Path(self.temp_backup_dir, suggested_filename)
|
||||
safe_filename = PureWindowsPath(suggested_filename).name
|
||||
if not safe_filename or safe_filename == "..":
|
||||
safe_filename = "backup.tar"
|
||||
temp_file = Path(self.temp_backup_dir, safe_filename)
|
||||
|
||||
async_add_executor_job = self._hass.async_add_executor_job
|
||||
await async_add_executor_job(make_backup_dir, self.temp_backup_dir)
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The bbox component."""
|
||||
"""The Bbox integration."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The blinksticklight component."""
|
||||
"""The BlinkStick integration."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Support for Blinkstick lights."""
|
||||
"""Support for BlinkStick lights."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
@@ -40,7 +40,7 @@ def setup_platform(
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up Blinkstick device specified by serial number."""
|
||||
"""Set up BlinkStick device specified by serial number."""
|
||||
|
||||
name = config[CONF_NAME]
|
||||
serial = config[CONF_SERIAL]
|
||||
|
||||
41
homeassistant/components/bmw_connected_drive/__init__.py
Normal file
41
homeassistant/components/bmw_connected_drive/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""The BMW Connected Drive integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
DOMAIN = "bmw_connected_drive"
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up BMW Connected Drive from a config entry."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/bmw_connected_drive",
|
||||
"custom_component_url": "https://github.com/kvanbiesen/bmw-cardata-ha",
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
@@ -0,0 +1,9 @@
|
||||
"""The BMW Connected Drive integration config flow."""
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class BMWConnectedDriveConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for BMW Connected Drive."""
|
||||
10
homeassistant/components/bmw_connected_drive/manifest.json
Normal file
10
homeassistant/components/bmw_connected_drive/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"description": "The BMW Connected Drive integration has been removed from Home Assistant.\n\nIn September 2025, BMW blocked third-party access to their servers by adding additional security measures. For EU-registered cars, a community-developed [custom component]({custom_component_url}) using BMW's CarData API is available as an alternative.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing BMW Connected Drive integration entries]({entries}).",
|
||||
"title": "The BMW Connected Drive integration has been removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
client = ChessComClient(session=session)
|
||||
try:
|
||||
user = await client.get_player(user_input[CONF_USERNAME])
|
||||
await client.get_player_stats(user_input[CONF_USERNAME])
|
||||
except NotFoundError:
|
||||
errors["base"] = "player_not_found"
|
||||
except Exception:
|
||||
|
||||
@@ -210,7 +210,7 @@ def websocket_update_entity(
|
||||
)
|
||||
return
|
||||
|
||||
changes = {}
|
||||
changes: dict[str, Any] = {}
|
||||
|
||||
for key in (
|
||||
"area_id",
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The denon component."""
|
||||
"""The Denon Network Receivers integration."""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The denonavr component."""
|
||||
"""The Denon AVR Network Receivers integration."""
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from devolo_home_control_api.devices.zwave import Zwave
|
||||
from devolo_home_control_api.homecontrol import HomeControl
|
||||
|
||||
@@ -188,6 +190,8 @@ class DevoloConsumptionEntity(DevoloMultiLevelDeviceEntity):
|
||||
def sync_callback(self, message: tuple) -> None:
|
||||
"""Update the consumption sensor state."""
|
||||
if message[0] == self._attr_unique_id:
|
||||
if TYPE_CHECKING:
|
||||
assert self._attr_unique_id is not None
|
||||
self._value = getattr(
|
||||
self._device_instance.consumption_property[self._attr_unique_id],
|
||||
self._sensor_type,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The dnsip component."""
|
||||
"""The DNS IP integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +17,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload dnsip config entry."""
|
||||
"""Unload DNS IP config entry."""
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""The Dropbox integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from python_dropbox_api import (
|
||||
DropboxAPIClient,
|
||||
DropboxAuthException,
|
||||
DropboxUnknownException,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .auth import DropboxConfigEntryAuth
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
type DropboxConfigEntry = ConfigEntry[DropboxAPIClient]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> bool:
|
||||
"""Set up Dropbox from a config entry."""
|
||||
try:
|
||||
oauth2_implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
oauth2_session = OAuth2Session(hass, entry, oauth2_implementation)
|
||||
|
||||
auth = DropboxConfigEntryAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), oauth2_session
|
||||
)
|
||||
|
||||
client = DropboxAPIClient(auth)
|
||||
|
||||
try:
|
||||
await client.get_account_info()
|
||||
except DropboxAuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (DropboxUnknownException, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
def async_notify_backup_listeners() -> None:
|
||||
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
|
||||
listener()
|
||||
|
||||
entry.async_on_unload(entry.async_on_state_change(async_notify_backup_listeners))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Application credentials platform for the Dropbox integration."""
|
||||
|
||||
from homeassistant.components.application_credentials import ClientCredential
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
AbstractOAuth2Implementation,
|
||||
LocalOAuth2ImplementationWithPkce,
|
||||
)
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_SCOPES, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_auth_implementation(
|
||||
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
|
||||
) -> AbstractOAuth2Implementation:
|
||||
"""Return custom auth implementation."""
|
||||
return DropboxOAuth2Implementation(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential.client_id,
|
||||
OAUTH2_AUTHORIZE,
|
||||
OAUTH2_TOKEN,
|
||||
credential.client_secret,
|
||||
)
|
||||
|
||||
|
||||
class DropboxOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
||||
"""Custom Dropbox OAuth2 implementation to add the necessary authorize url parameters."""
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data: dict = {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Authentication for Dropbox."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from python_dropbox_api import Auth
|
||||
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
|
||||
|
||||
class DropboxConfigEntryAuth(Auth):
|
||||
"""Provide Dropbox authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
oauth_session: OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize DropboxConfigEntryAuth."""
|
||||
super().__init__(websession)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
|
||||
|
||||
class DropboxConfigFlowAuth(Auth):
|
||||
"""Provide authentication tied to a fixed token for the config flow."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
token: str,
|
||||
) -> None:
|
||||
"""Initialize DropboxConfigFlowAuth."""
|
||||
super().__init__(websession)
|
||||
self._token = token
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return the fixed access token."""
|
||||
return self._token
|
||||
@@ -1,230 +0,0 @@
|
||||
"""Backup platform for the Dropbox integration."""
|
||||
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from python_dropbox_api import (
|
||||
DropboxAPIClient,
|
||||
DropboxAuthException,
|
||||
DropboxFileOrFolderNotFoundException,
|
||||
DropboxUnknownException,
|
||||
)
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
AgentBackup,
|
||||
BackupAgent,
|
||||
BackupAgentError,
|
||||
BackupNotFound,
|
||||
suggested_filename,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import DropboxConfigEntry
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata."""
|
||||
base_name = suggested_filename(backup).rsplit(".", 1)[0]
|
||||
return f"{base_name}.tar", f"{base_name}.metadata.json"
|
||||
|
||||
|
||||
async def _async_string_iterator(content: str) -> AsyncIterator[bytes]:
|
||||
"""Yield a string as a single bytes chunk."""
|
||||
yield content.encode()
|
||||
|
||||
|
||||
def handle_backup_errors[_R, **P](
|
||||
func: Callable[Concatenate[DropboxBackupAgent, P], Coroutine[Any, Any, _R]],
|
||||
) -> Callable[Concatenate[DropboxBackupAgent, P], Coroutine[Any, Any, _R]]:
|
||||
"""Handle backup errors."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(
|
||||
self: DropboxBackupAgent, *args: P.args, **kwargs: P.kwargs
|
||||
) -> _R:
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except DropboxFileOrFolderNotFoundException as err:
|
||||
raise BackupNotFound(
|
||||
f"Failed to {func.__name__.removeprefix('async_').replace('_', ' ')}"
|
||||
) from err
|
||||
except DropboxAuthException as err:
|
||||
self._entry.async_start_reauth(self._hass)
|
||||
raise BackupAgentError("Authentication error") from err
|
||||
except DropboxUnknownException as err:
|
||||
_LOGGER.error(
|
||||
"Error during %s: %s",
|
||||
func.__name__,
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Full error: %s", err, exc_info=True)
|
||||
raise BackupAgentError(
|
||||
f"Failed to {func.__name__.removeprefix('async_').replace('_', ' ')}"
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def async_get_backup_agents(
|
||||
hass: HomeAssistant,
|
||||
**kwargs: Any,
|
||||
) -> list[BackupAgent]:
|
||||
"""Return a list of backup agents."""
|
||||
entries = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
return [DropboxBackupAgent(hass, entry) for entry in entries]
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_backup_agents_listener(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
listener: Callable[[], None],
|
||||
**kwargs: Any,
|
||||
) -> Callable[[], None]:
|
||||
"""Register a listener to be called when agents are added or removed.
|
||||
|
||||
:return: A function to unregister the listener.
|
||||
"""
|
||||
hass.data.setdefault(DATA_BACKUP_AGENT_LISTENERS, []).append(listener)
|
||||
|
||||
@callback
|
||||
def remove_listener() -> None:
|
||||
"""Remove the listener."""
|
||||
hass.data[DATA_BACKUP_AGENT_LISTENERS].remove(listener)
|
||||
if not hass.data[DATA_BACKUP_AGENT_LISTENERS]:
|
||||
del hass.data[DATA_BACKUP_AGENT_LISTENERS]
|
||||
|
||||
return remove_listener
|
||||
|
||||
|
||||
class DropboxBackupAgent(BackupAgent):
|
||||
"""Backup agent for the Dropbox integration."""
|
||||
|
||||
domain = DOMAIN
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: DropboxConfigEntry) -> None:
|
||||
"""Initialize the backup agent."""
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
self._entry = entry
|
||||
self.name = entry.title
|
||||
assert entry.unique_id
|
||||
self.unique_id = entry.unique_id
|
||||
self._api: DropboxAPIClient = entry.runtime_data
|
||||
|
||||
async def _async_get_backups(self) -> list[tuple[AgentBackup, str]]:
|
||||
"""Get backups and their corresponding file names."""
|
||||
files = await self._api.list_folder("")
|
||||
|
||||
tar_files = {f.name for f in files if f.name.endswith(".tar")}
|
||||
metadata_files = [f for f in files if f.name.endswith(".metadata.json")]
|
||||
|
||||
backups: list[tuple[AgentBackup, str]] = []
|
||||
for metadata_file in metadata_files:
|
||||
tar_name = metadata_file.name.removesuffix(".metadata.json") + ".tar"
|
||||
if tar_name not in tar_files:
|
||||
_LOGGER.warning(
|
||||
"Found metadata file '%s' without matching backup file",
|
||||
metadata_file.name,
|
||||
)
|
||||
continue
|
||||
|
||||
metadata_stream = self._api.download_file(f"/{metadata_file.name}")
|
||||
raw = b"".join([chunk async for chunk in metadata_stream])
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
backup = AgentBackup.from_dict(data)
|
||||
except (json.JSONDecodeError, ValueError, TypeError, KeyError) as err:
|
||||
_LOGGER.warning(
|
||||
"Skipping invalid metadata file '%s': %s",
|
||||
metadata_file.name,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
backups.append((backup, tar_name))
|
||||
|
||||
return backups
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
backup: AgentBackup,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup."""
|
||||
backup_filename, metadata_filename = _suggested_filenames(backup)
|
||||
backup_path = f"/{backup_filename}"
|
||||
metadata_path = f"/{metadata_filename}"
|
||||
|
||||
file_stream = await open_stream()
|
||||
await self._api.upload_file(backup_path, file_stream)
|
||||
|
||||
metadata_stream = _async_string_iterator(json.dumps(backup.as_dict()))
|
||||
|
||||
try:
|
||||
await self._api.upload_file(metadata_path, metadata_stream)
|
||||
except (
|
||||
DropboxAuthException,
|
||||
DropboxUnknownException,
|
||||
):
|
||||
await self._api.delete_file(backup_path)
|
||||
raise
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||
"""List backups."""
|
||||
return [backup for backup, _ in await self._async_get_backups()]
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_download_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[bytes]:
|
||||
"""Download a backup file."""
|
||||
backups = await self._async_get_backups()
|
||||
for backup, filename in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
return self._api.download_file(f"/{filename}")
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_get_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> AgentBackup:
|
||||
"""Return a backup."""
|
||||
backups = await self._async_get_backups()
|
||||
|
||||
for backup, _ in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
return backup
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_delete_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Delete a backup file."""
|
||||
backups = await self._async_get_backups()
|
||||
for backup, tar_filename in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
metadata_filename = tar_filename.removesuffix(".tar") + ".metadata.json"
|
||||
await self._api.delete_file(f"/{tar_filename}")
|
||||
await self._api.delete_file(f"/{metadata_filename}")
|
||||
return
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Config flow for Dropbox."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from python_dropbox_api import DropboxAPIClient
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .auth import DropboxConfigFlowAuth
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Config flow to handle Dropbox OAuth2 authentication."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
access_token = data[CONF_TOKEN][CONF_ACCESS_TOKEN]
|
||||
|
||||
auth = DropboxConfigFlowAuth(async_get_clientsession(self.hass), access_token)
|
||||
|
||||
client = DropboxAPIClient(auth)
|
||||
account_info = await client.get_account_info()
|
||||
|
||||
await self.async_set_unique_id(account_info.account_id)
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=data
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=account_info.email, data=data)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Constants for the Dropbox integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN = "dropbox"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://www.dropbox.com/oauth2/authorize"
|
||||
OAUTH2_TOKEN = "https://api.dropboxapi.com/oauth2/token"
|
||||
OAUTH2_SCOPES = [
|
||||
"account_info.read",
|
||||
"files.content.read",
|
||||
"files.content.write",
|
||||
]
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
f"{DOMAIN}.backup_agent_listeners"
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"domain": "dropbox",
|
||||
"name": "Dropbox",
|
||||
"after_dependencies": ["backup"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dropbox",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-dropbox-api==0.1.3"]
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register any actions.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: Integration does not poll.
|
||||
brands: done
|
||||
common-modules:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities or coordinators.
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: Integration does not register any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-unique-id:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
has-entity-name:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: Integration does not register any actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration does not have any configuration parameters.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: Integration does not make any entity updates.
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
diagnostics:
|
||||
status: exempt
|
||||
comment: Integration does not have any data to diagnose.
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Integration is a service.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Integration is a service.
|
||||
docs-data-update:
|
||||
status: exempt
|
||||
comment: Integration does not update any data.
|
||||
docs-examples:
|
||||
status: exempt
|
||||
comment: Integration only provides backup functionality.
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: Integration does not support any devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Integration does not use any devices.
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: Integration does not have any repairs.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Integration does not have any devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"wrong_account": "Wrong account: Please authenticate with the correct account."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The Dropbox integration needs to re-authenticate your account.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ def _fix_device_registry_identifiers(
|
||||
if old_identifier not in device_entry.identifiers: # type: ignore[comparison-overlap]
|
||||
continue
|
||||
new_identifiers = device_entry.identifiers.copy()
|
||||
new_identifiers.discard(old_identifier) # type: ignore[arg-type]
|
||||
new_identifiers.discard(old_identifier)
|
||||
new_identifiers.add((DOMAIN, entry.data["station"]))
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, new_identifiers=new_identifiers
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The ebox component."""
|
||||
"""The EBox integration."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The edimax component."""
|
||||
"""The Edimax integration."""
|
||||
|
||||
@@ -273,7 +273,7 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
|
||||
continue
|
||||
|
||||
# Build kwargs common to both modes
|
||||
kwargs = base_stream_params | {
|
||||
kwargs: dict[str, Any] = base_stream_params | {
|
||||
"text": text,
|
||||
}
|
||||
|
||||
|
||||
@@ -293,7 +293,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> boo
|
||||
|
||||
elk_temp_unit = elk.panel.temperature_units
|
||||
if elk_temp_unit == "C":
|
||||
temperature_unit = UnitOfTemperature.CELSIUS
|
||||
temperature_unit = UnitOfTemperature.CELSIUS # type: ignore[unreachable]
|
||||
else:
|
||||
temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
config["temperature_unit"] = temperature_unit
|
||||
|
||||
@@ -361,7 +361,8 @@ class EvoController(EvoClimateEntity):
|
||||
async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None:
|
||||
"""Process a service request (system mode) for a controller.
|
||||
|
||||
Data validation is not required, it will have been done upstream.
|
||||
Data validation is not required here; it is performed upstream by the service
|
||||
handler (service schema plus runtime checks).
|
||||
"""
|
||||
|
||||
if service == EvoService.RESET_SYSTEM:
|
||||
@@ -387,9 +388,16 @@ class EvoController(EvoClimateEntity):
|
||||
) -> None:
|
||||
"""Set a Controller to any of its native operating modes."""
|
||||
until = dt_util.as_utc(until) if until else None
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_mode(mode, until=until)
|
||||
)
|
||||
try:
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_mode(mode, until=until)
|
||||
)
|
||||
except evo.InvalidSystemModeError as err:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_system_mode",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
|
||||
@@ -139,6 +139,9 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
try:
|
||||
result = await client_api
|
||||
|
||||
except ec2.InvalidSystemModeError:
|
||||
raise
|
||||
|
||||
except ec2.ApiRequestFailedError as err:
|
||||
self.logger.error(err)
|
||||
return None
|
||||
|
||||
@@ -5,17 +5,18 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from typing import Any, Final
|
||||
|
||||
from evohomeasync2 import ControlSystem
|
||||
from evohomeasync2.const import SZ_CAN_BE_TEMPORARY, SZ_SYSTEM_MODE, SZ_TIMING_MODE
|
||||
from evohomeasync2.schemas.const import (
|
||||
S2_DURATION as SZ_DURATION,
|
||||
S2_PERIOD as SZ_PERIOD,
|
||||
SystemMode as EvoSystemMode,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.const import ATTR_MODE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import verify_domain_control
|
||||
@@ -23,8 +24,19 @@ from homeassistant.helpers.service import verify_domain_control
|
||||
from .const import ATTR_DURATION, ATTR_PERIOD, ATTR_SETPOINT, DOMAIN, EvoService
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
|
||||
# system mode schemas are built dynamically when the services are registered
|
||||
# because supported modes can vary for edge-case systems
|
||||
# System service schemas (registered as domain services)
|
||||
SET_SYSTEM_MODE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
|
||||
# unsupported modes are rejected at runtime with ServiceValidationError
|
||||
vol.Required(ATTR_MODE): cv.string, # avoid vol.In(SystemMode)
|
||||
vol.Exclusive(ATTR_DURATION, "temporary"): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
|
||||
),
|
||||
vol.Exclusive(ATTR_PERIOD, "temporary"): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
|
||||
),
|
||||
}
|
||||
|
||||
# Zone service schemas (registered as entity services)
|
||||
SET_ZONE_OVERRIDE_SCHEMA: Final[dict[str | vol.Marker, Any]] = {
|
||||
@@ -59,16 +71,56 @@ def _register_zone_entity_services(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_set_system_mode_params(tcs: ControlSystem, data: dict[str, Any]) -> None:
|
||||
"""Validate that a set_system_mode service call is properly formed."""
|
||||
|
||||
mode = data[ATTR_MODE]
|
||||
tcs_modes = {m[SZ_SYSTEM_MODE]: m for m in tcs.allowed_system_modes}
|
||||
|
||||
# Validation occurs here, instead of in the library, because it uses a slightly
|
||||
# different schema (until instead of duration/period) for the method invoked
|
||||
# via this service call
|
||||
|
||||
if (mode_info := tcs_modes.get(mode)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mode_not_supported",
|
||||
translation_placeholders={ATTR_MODE: mode},
|
||||
)
|
||||
|
||||
# voluptuous schema ensures that duration and period are not both present
|
||||
|
||||
if not mode_info[SZ_CAN_BE_TEMPORARY]:
|
||||
if ATTR_DURATION in data or ATTR_PERIOD in data:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mode_cant_be_temporary",
|
||||
translation_placeholders={ATTR_MODE: mode},
|
||||
)
|
||||
return
|
||||
|
||||
timing_mode = mode_info.get(SZ_TIMING_MODE) # will not be None, as can_be_temporary
|
||||
|
||||
if timing_mode == SZ_DURATION and ATTR_PERIOD in data:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mode_cant_have_period",
|
||||
translation_placeholders={ATTR_MODE: mode},
|
||||
)
|
||||
|
||||
if timing_mode == SZ_PERIOD and ATTR_DURATION in data:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mode_cant_have_duration",
|
||||
translation_placeholders={ATTR_MODE: mode},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def setup_service_functions(
|
||||
hass: HomeAssistant, coordinator: EvoDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Set up the service handlers for the system/zone operating modes.
|
||||
|
||||
Not all Honeywell TCC-compatible systems support all operating modes. In addition,
|
||||
each mode will require any of four distinct service schemas. This has to be
|
||||
enumerated before registering the appropriate handlers.
|
||||
"""
|
||||
"""Set up the service handlers for Evohome systems."""
|
||||
|
||||
@verify_domain_control(DOMAIN)
|
||||
async def force_refresh(call: ServiceCall) -> None:
|
||||
@@ -77,7 +129,14 @@ def setup_service_functions(
|
||||
|
||||
@verify_domain_control(DOMAIN)
|
||||
async def set_system_mode(call: ServiceCall) -> None:
|
||||
"""Set the system mode."""
|
||||
"""Set the Evohome system mode or reset the system."""
|
||||
|
||||
# No additional validation for RESET_SYSTEM here, as the library method invoked
|
||||
# via that service call may be able to emulate the reset even if the system
|
||||
# doesn't support AutoWithReset natively
|
||||
|
||||
if call.service == EvoService.SET_SYSTEM_MODE:
|
||||
_validate_set_system_mode_params(coordinator.tcs, call.data)
|
||||
|
||||
payload = {
|
||||
"unique_id": coordinator.tcs.id,
|
||||
@@ -86,59 +145,14 @@ def setup_service_functions(
|
||||
}
|
||||
async_dispatcher_send(hass, DOMAIN, payload)
|
||||
|
||||
assert coordinator.tcs is not None # mypy
|
||||
|
||||
hass.services.async_register(DOMAIN, EvoService.REFRESH_SYSTEM, force_refresh)
|
||||
hass.services.async_register(DOMAIN, EvoService.RESET_SYSTEM, set_system_mode)
|
||||
|
||||
# Enumerate which operating modes are supported by this system
|
||||
modes = list(coordinator.tcs.allowed_system_modes)
|
||||
|
||||
system_mode_schemas = []
|
||||
modes = [m for m in modes if m[SZ_SYSTEM_MODE] != EvoSystemMode.AUTO_WITH_RESET]
|
||||
|
||||
# Permanent-only modes will use this schema
|
||||
perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]]
|
||||
if perm_modes: # any of: "Auto", "HeatingOff": permanent only
|
||||
schema = vol.Schema({vol.Required(ATTR_MODE): vol.In(perm_modes)})
|
||||
system_mode_schemas.append(schema)
|
||||
|
||||
modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]]
|
||||
|
||||
# These modes are set for a number of hours (or indefinitely): use this schema
|
||||
temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == SZ_DURATION]
|
||||
if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
|
||||
),
|
||||
}
|
||||
)
|
||||
system_mode_schemas.append(schema)
|
||||
|
||||
# These modes are set for a number of days (or indefinitely): use this schema
|
||||
temp_modes = [m[SZ_SYSTEM_MODE] for m in modes if m[SZ_TIMING_MODE] == SZ_PERIOD]
|
||||
if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_PERIOD): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
|
||||
),
|
||||
}
|
||||
)
|
||||
system_mode_schemas.append(schema)
|
||||
|
||||
if system_mode_schemas:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
EvoService.SET_SYSTEM_MODE,
|
||||
set_system_mode,
|
||||
schema=vol.Schema(vol.Any(*system_mode_schemas)),
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
EvoService.SET_SYSTEM_MODE,
|
||||
set_system_mode,
|
||||
schema=vol.Schema(SET_SYSTEM_MODE_SCHEMA),
|
||||
)
|
||||
|
||||
_register_zone_entity_services(hass)
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"invalid_system_mode": {
|
||||
"message": "The requested system mode is not supported: {error}"
|
||||
},
|
||||
"mode_cant_be_temporary": {
|
||||
"message": "The mode `{mode}` does not support `duration` or `period`"
|
||||
},
|
||||
"mode_cant_have_duration": {
|
||||
"message": "The mode `{mode}` does not support `duration`; use `period` instead"
|
||||
},
|
||||
"mode_cant_have_period": {
|
||||
"message": "The mode `{mode}` does not support `period`; use `duration` instead"
|
||||
},
|
||||
"mode_not_supported": {
|
||||
"message": "The mode `{mode}` is not supported by this controller"
|
||||
},
|
||||
"zone_only_service": {
|
||||
"message": "Only zones support the `{service}` action"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The fido component."""
|
||||
"""The Fido integration."""
|
||||
|
||||
@@ -30,22 +30,31 @@ class FreshrFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def _validate_input(self, username: str, password: str) -> str | None:
|
||||
"""Validate credentials, returning an error key or None on success."""
|
||||
client = FreshrClient(session=async_get_clientsession(self.hass))
|
||||
try:
|
||||
await client.login(username, password)
|
||||
except LoginError:
|
||||
return "invalid_auth"
|
||||
except ClientError:
|
||||
return "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return "unknown"
|
||||
return None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
client = FreshrClient(session=async_get_clientsession(self.hass))
|
||||
try:
|
||||
await client.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
|
||||
except LoginError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
error = await self._validate_input(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
if error:
|
||||
errors["base"] = error
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -58,6 +67,34 @@ class FreshrFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration."""
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
error = await self._validate_input(
|
||||
reconfigure_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
if error:
|
||||
errors["base"] = error
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||
description_placeholders={
|
||||
CONF_USERNAME: reconfigure_entry.data[CONF_USERNAME]
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, _user_input: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
@@ -72,18 +109,11 @@ class FreshrFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
client = FreshrClient(session=async_get_clientsession(self.hass))
|
||||
try:
|
||||
await client.login(
|
||||
reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except LoginError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
error = await self._validate_input(
|
||||
reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
if error:
|
||||
errors["base"] = error
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
|
||||
@@ -62,7 +62,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No actionable repair scenarios exist; authentication failures are handled via the reauthentication flow.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -19,6 +20,15 @@
|
||||
},
|
||||
"description": "Re-enter the password for your Fresh-r account `{username}`."
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::freshr::config::step::user::data_description::password%]"
|
||||
},
|
||||
"description": "Update the password for your Fresh-r account `{username}`."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260325.4"]
|
||||
"requirements": ["home-assistant-frontend==20260325.5"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The Glances component."""
|
||||
"""The Glances integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Constants for Glances component."""
|
||||
"""Constants for Glances integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import sys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Support gathering system information of hosts which are running glances."""
|
||||
"""Support gathering system information of hosts which are running Glances."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
from functools import partial
|
||||
|
||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||
from google.auth.exceptions import RefreshError
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import Resource, build
|
||||
|
||||
@@ -14,6 +13,8 @@ from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
@@ -37,24 +38,26 @@ class AsyncConfigEntryAuth:
|
||||
|
||||
async def check_and_refresh_token(self) -> str:
|
||||
"""Check the token."""
|
||||
setup_in_progress = (
|
||||
self.oauth_session.config_entry.state is ConfigEntryState.SETUP_IN_PROGRESS
|
||||
)
|
||||
|
||||
try:
|
||||
await self.oauth_session.async_ensure_token_valid()
|
||||
except (RefreshError, ClientResponseError, ClientError) as ex:
|
||||
if (
|
||||
self.oauth_session.config_entry.state
|
||||
is ConfigEntryState.SETUP_IN_PROGRESS
|
||||
):
|
||||
if isinstance(ex, ClientResponseError) and 400 <= ex.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from ex
|
||||
except OAuth2TokenRequestReauthError as ex:
|
||||
if setup_in_progress:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from ex
|
||||
self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass)
|
||||
raise
|
||||
except OAuth2TokenRequestError as ex:
|
||||
if setup_in_progress:
|
||||
raise ConfigEntryNotReady from ex
|
||||
raise
|
||||
except ClientError as ex:
|
||||
if setup_in_progress:
|
||||
raise ConfigEntryNotReady from ex
|
||||
if isinstance(ex, RefreshError) or (
|
||||
hasattr(ex, "status") and ex.status == 400
|
||||
):
|
||||
self.oauth_session.config_entry.async_start_reauth(
|
||||
self.oauth_session.hass
|
||||
)
|
||||
raise HomeAssistantError(ex) from ex
|
||||
return self.access_token
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The hikvisioncam component."""
|
||||
"""The Hikvision integration."""
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"serialx==0.6.2",
|
||||
"serialx==1.1.1",
|
||||
"universal-silabs-flasher==1.0.3",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
|
||||
@@ -979,7 +979,7 @@ class HomeKit:
|
||||
for entry in dev_reg.devices.get_devices_for_config_entry_id(self._entry_id)
|
||||
if (
|
||||
identifier not in entry.identifiers # type: ignore[comparison-overlap]
|
||||
or connection not in entry.connections
|
||||
or connection not in entry.connections # type: ignore[unreachable]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"message": "Honeywell could not stop hold mode"
|
||||
},
|
||||
"switch_failed_off": {
|
||||
"message": "Honeywell could turn off emergency heat mode."
|
||||
"message": "Honeywell could not turn off emergency heat mode."
|
||||
},
|
||||
"switch_failed_on": {
|
||||
"message": "Honeywell could not set system mode to emergency heat mode."
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
"dismiss": {
|
||||
"service": "mdi:bell-off"
|
||||
},
|
||||
"dismiss_message": {
|
||||
"service": "mdi:comment-remove"
|
||||
},
|
||||
"send_message": {
|
||||
"service": "mdi:message-arrow-right"
|
||||
"service": "mdi:comment-arrow-right"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,28 @@ def deprecated_notify_action_call(
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_notify_action",
|
||||
translation_placeholders={"action": action},
|
||||
translation_placeholders={
|
||||
"action": action,
|
||||
"new_action_1": "notify.send_message",
|
||||
"new_action_2": "html5.send_message",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def deprecated_dismiss_action_call(hass: HomeAssistant) -> None:
|
||||
"""Deprecated action call."""
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_dismiss_action",
|
||||
breaks_in_ha_version="2026.11.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_dismiss_action",
|
||||
translation_placeholders={
|
||||
"action": "html5.dismiss",
|
||||
"new_action": "html5.dismiss_message",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ from .const import (
|
||||
SERVICE_DISMISS,
|
||||
)
|
||||
from .entity import HTML5Entity, Registration
|
||||
from .issue import deprecated_notify_action_call
|
||||
from .issue import deprecated_dismiss_action_call, deprecated_notify_action_call
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -460,6 +460,9 @@ class HTML5NotificationService(BaseNotificationService):
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
|
||||
deprecated_dismiss_action_call(self.hass)
|
||||
|
||||
data: dict[str, Any] | None = kwargs.get(ATTR_DATA)
|
||||
tag: str = data.get(ATTR_TAG, "") if data else ""
|
||||
payload = {ATTR_TAG: tag, ATTR_DISMISS: True, ATTR_DATA: {}}
|
||||
@@ -624,6 +627,11 @@ class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
|
||||
await self._webpush(**kwargs)
|
||||
self._async_record_notification()
|
||||
|
||||
async def dismiss_notification(self, tag: str = "") -> None:
|
||||
"""Dismiss a message via html5.dismiss_message action."""
|
||||
await self._webpush(dismiss=True, tag=tag)
|
||||
self._async_record_notification()
|
||||
|
||||
async def _webpush(
|
||||
self,
|
||||
message: str | None = None,
|
||||
|
||||
@@ -32,6 +32,7 @@ from .const import (
|
||||
)
|
||||
|
||||
SERVICE_SEND_MESSAGE = "send_message"
|
||||
SERVICE_DISMISS_MESSAGE = "dismiss_message"
|
||||
|
||||
SERVICE_SEND_MESSAGE_SCHEMA = cv.make_entity_service_schema(
|
||||
{
|
||||
@@ -67,6 +68,10 @@ SERVICE_SEND_MESSAGE_SCHEMA = cv.make_entity_service_schema(
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_DISMISS_MESSAGE_SCHEMA = cv.make_entity_service_schema(
|
||||
{vol.Optional(ATTR_TAG): cv.string}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
@@ -80,3 +85,11 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
schema=SERVICE_SEND_MESSAGE_SCHEMA,
|
||||
func="send_push_notification",
|
||||
)
|
||||
service.async_register_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_DISMISS_MESSAGE,
|
||||
entity_domain=NOTIFY_DOMAIN,
|
||||
schema=SERVICE_DISMISS_MESSAGE_SCHEMA,
|
||||
func="dismiss_notification",
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ send_message:
|
||||
text:
|
||||
type: url
|
||||
example: /static/images/image.jpg
|
||||
tag:
|
||||
tag: &tag
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
@@ -142,3 +142,10 @@ send_message:
|
||||
selector:
|
||||
object:
|
||||
example: "{'customKey': 'customValue'}"
|
||||
dismiss_message:
|
||||
target:
|
||||
entity:
|
||||
domain: notify
|
||||
integration: html5
|
||||
fields:
|
||||
tag: *tag
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"received": "Received"
|
||||
}
|
||||
},
|
||||
"tag": { "name": "Tag" }
|
||||
"tag": {
|
||||
"name": "[%key:component::html5::services::send_message::fields::tag::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,8 +51,12 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_dismiss_action": {
|
||||
"description": "The action `{action}` is deprecated and will be removed in a future release.\n\nPlease update your automations and scripts to use the notify entities with the `{new_action}` action instead.",
|
||||
"title": "[%key:component::html5::issues::deprecated_notify_action::title%]"
|
||||
},
|
||||
"deprecated_notify_action": {
|
||||
"description": "The action `{action}` is deprecated and will be removed in a future release.\n\nPlease update your automations and scripts to use the notify entities with the `notify.send_message` or `html5.send_message` actions instead.",
|
||||
"description": "The action `{action}` is deprecated and will be removed in a future release.\n\nPlease update your automations and scripts to use the notify entities with the `{new_action_1}` or `{new_action_2}` actions instead.",
|
||||
"title": "Detected use of deprecated action {action}"
|
||||
}
|
||||
},
|
||||
@@ -101,6 +107,16 @@
|
||||
},
|
||||
"name": "Dismiss"
|
||||
},
|
||||
"dismiss_message": {
|
||||
"description": "Dismisses one or more HTML5 notifications.",
|
||||
"fields": {
|
||||
"tag": {
|
||||
"description": "The tag of the notifications to dismiss. If not specified, all notifications will be dismissed.",
|
||||
"name": "[%key:component::html5::services::send_message::fields::tag::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Dismiss message"
|
||||
},
|
||||
"send_message": {
|
||||
"description": "Sends a message via HTML5 Push Notifications",
|
||||
"fields": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The Hyperion component."""
|
||||
"""The Hyperion integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Switch platform for Hyperion."""
|
||||
"""Camera platform for Hyperion."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The kodi component."""
|
||||
"""The Kodi integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Constants for the Kodi platform."""
|
||||
"""Constants for the Kodi integration."""
|
||||
|
||||
DOMAIN = "kodi"
|
||||
|
||||
|
||||
@@ -36,5 +36,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.28.4", "led-ble==1.1.7"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.4", "led-ble==1.1.8"]
|
||||
}
|
||||
|
||||
@@ -97,7 +97,8 @@ SENSOR_TYPES: dict[str, LidarrSensorEntityDescription[Any]] = {
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
entity_registry_enabled_default=False,
|
||||
attributes_fn=lambda data: {
|
||||
album.title: album.artist.artistName for album in data.records
|
||||
album.title: album.artist.artistName # type: ignore[misc]
|
||||
for album in data.records
|
||||
},
|
||||
),
|
||||
"albums": LidarrSensorEntityDescription[int](
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""The mfi component."""
|
||||
"""The Ubiquiti mFI mPort integration."""
|
||||
|
||||
@@ -16,8 +16,10 @@ from .const import DOMAIN
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1
|
||||
|
||||
type NightscoutConfigEntry = ConfigEntry[NightscoutAPI]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: NightscoutConfigEntry) -> bool:
|
||||
"""Set up Nightscout from a config entry."""
|
||||
server_url = entry.data[CONF_URL]
|
||||
api_key = entry.data.get(CONF_API_KEY)
|
||||
@@ -28,8 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except (ClientError, TimeoutError, OSError) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = api
|
||||
entry.runtime_data = api
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
@@ -46,10 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: NightscoutConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -10,12 +10,12 @@ from aiohttp import ClientError
|
||||
from py_nightscout import Api as NightscoutAPI
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_DATE, UnitOfBloodGlucoseConcentration
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION, DOMAIN
|
||||
from . import NightscoutConfigEntry
|
||||
from .const import ATTR_DELTA, ATTR_DEVICE, ATTR_DIRECTION
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
@@ -26,11 +26,11 @@ DEFAULT_NAME = "Blood Glucose"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: NightscoutConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Glucose Sensor."""
|
||||
api = hass.data[DOMAIN][entry.entry_id]
|
||||
api = entry.runtime_data
|
||||
async_add_entities([NightscoutSensor(api, "Blood Sugar", entry.unique_id)], True)
|
||||
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ import logging
|
||||
import nuheat
|
||||
import requests
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS
|
||||
from .coordinator import NuHeatCoordinator
|
||||
from .const import CONF_SERIAL_NUMBER, PLATFORMS
|
||||
from .coordinator import NuHeatConfigEntry, NuHeatCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,7 +22,7 @@ def _get_thermostat(api: nuheat.NuHeat, serial_number: str) -> nuheat.NuHeatTher
|
||||
return api.get_thermostat(serial_number)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: NuHeatConfigEntry) -> bool:
|
||||
"""Set up NuHeat from a config entry."""
|
||||
|
||||
conf = entry.data
|
||||
@@ -52,20 +51,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.error("Failed to login to nuheat: %s", ex)
|
||||
return False
|
||||
|
||||
coordinator = NuHeatCoordinator(hass, entry, thermostat)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator)
|
||||
entry.runtime_data = NuHeatCoordinator(hass, entry, thermostat)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: NuHeatConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import event as event_helper
|
||||
@@ -27,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, NUHEAT_API_STATE_SHIFT_DELAY
|
||||
from .coordinator import NuHeatCoordinator
|
||||
from .coordinator import NuHeatConfigEntry, NuHeatCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,14 +54,15 @@ SCHEDULE_MODE_TO_PRESET_MODE_MAP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NuHeatConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the NuHeat thermostat(s)."""
|
||||
thermostat, coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
temperature_unit = hass.config.units.temperature_unit
|
||||
entity = NuHeatThermostat(coordinator, thermostat, temperature_unit)
|
||||
|
||||
entity = NuHeatThermostat(coordinator, coordinator.thermostat, temperature_unit)
|
||||
|
||||
# No longer need a service as set_hvac_mode to auto does this
|
||||
# since climate 1.0 has been implemented
|
||||
|
||||
@@ -16,15 +16,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
type NuHeatConfigEntry = ConfigEntry[NuHeatCoordinator]
|
||||
|
||||
|
||||
class NuHeatCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Coordinator for NuHeat thermostat data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: NuHeatConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: NuHeatConfigEntry,
|
||||
thermostat: nuheat.NuHeatThermostat,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
|
||||
@@ -6,10 +6,12 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .connectivity import ObihaiConnection
|
||||
from .const import DOMAIN, LOGGER, PLATFORMS
|
||||
from .const import LOGGER, PLATFORMS
|
||||
|
||||
type ObihaiConfigEntry = ConfigEntry[ObihaiConnection]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ObihaiConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
|
||||
requester = ObihaiConnection(
|
||||
@@ -18,20 +20,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
await hass.async_add_executor_job(requester.update)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = requester
|
||||
entry.runtime_data = requester
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ObihaiConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
version = entry.version
|
||||
|
||||
LOGGER.debug("Migrating from version %s", version)
|
||||
if version != 2:
|
||||
requester: ObihaiConnection = hass.data[DOMAIN][entry.entry_id]
|
||||
requester = entry.runtime_data
|
||||
|
||||
device_mac = await hass.async_add_executor_job(
|
||||
requester.pyobihai.get_device_mac
|
||||
@@ -45,6 +47,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ObihaiConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -7,14 +7,14 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_platform
|
||||
|
||||
from . import ObihaiConfigEntry
|
||||
from .connectivity import ObihaiConnection
|
||||
from .const import DOMAIN, OBIHAI
|
||||
from .const import OBIHAI
|
||||
|
||||
BUTTON_DESCRIPTION = ButtonEntityDescription(
|
||||
key="reboot",
|
||||
@@ -26,12 +26,12 @@ BUTTON_DESCRIPTION = ButtonEntityDescription(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ObihaiConfigEntry,
|
||||
async_add_entities: entity_platform.AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Obihai sensor entries."""
|
||||
"""Set up the Obihai button entries."""
|
||||
|
||||
requester: ObihaiConnection = hass.data[DOMAIN][entry.entry_id]
|
||||
requester = entry.runtime_data
|
||||
|
||||
buttons = [ObihaiButton(requester)]
|
||||
async_add_entities(buttons, update_before_add=True)
|
||||
|
||||
@@ -7,24 +7,24 @@ import datetime
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import ObihaiConfigEntry
|
||||
from .connectivity import ObihaiConnection
|
||||
from .const import DOMAIN, LOGGER, OBIHAI
|
||||
from .const import LOGGER, OBIHAI
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=5)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ObihaiConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Obihai sensor entries."""
|
||||
|
||||
requester: ObihaiConnection = hass.data[DOMAIN][entry.entry_id]
|
||||
requester = entry.runtime_data
|
||||
|
||||
sensors = [ObihaiServiceSensors(requester, key) for key in requester.services]
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import aiohttp
|
||||
from pyoctoprintapi import OctoprintClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_BINARY_SENSORS,
|
||||
@@ -34,7 +34,7 @@ from homeassistant.util import slugify as util_slugify
|
||||
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
|
||||
|
||||
from .const import CONF_BAUDRATE, DOMAIN, SERVICE_CONNECT
|
||||
from .coordinator import OctoprintDataUpdateCoordinator
|
||||
from .coordinator import OctoprintConfigEntry, OctoprintDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -168,12 +168,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OctoprintConfigEntry) -> bool:
|
||||
"""Set up OctoPrint from a config entry."""
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if CONF_VERIFY_SSL not in entry.data:
|
||||
data = {**entry.data, CONF_VERIFY_SSL: True}
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
@@ -210,10 +206,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"coordinator": coordinator,
|
||||
"client": client,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -237,14 +230,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: OctoprintConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def async_get_client_for_service_call(
|
||||
@@ -256,8 +244,9 @@ def async_get_client_for_service_call(
|
||||
|
||||
if device_entry := device_registry.async_get(device_id):
|
||||
for entry_id in device_entry.config_entries:
|
||||
if data := hass.data[DOMAIN].get(entry_id):
|
||||
return cast(OctoprintClient, data["client"])
|
||||
if entry := hass.config_entries.async_get_entry(entry_id):
|
||||
if entry.domain == DOMAIN and entry.state == ConfigEntryState.LOADED:
|
||||
return cast(OctoprintConfigEntry, entry).runtime_data.octoprint
|
||||
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -7,24 +7,20 @@ from abc import abstractmethod
|
||||
from pyoctoprintapi import OctoprintPrinterInfo
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import OctoprintDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OctoprintConfigEntry, OctoprintDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OctoprintConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the available OctoPrint binary sensors."""
|
||||
coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]["coordinator"]
|
||||
coordinator = config_entry.runtime_data
|
||||
device_id = config_entry.unique_id
|
||||
|
||||
assert device_id is not None
|
||||
|
||||
@@ -3,26 +3,22 @@
|
||||
from pyoctoprintapi import OctoprintClient, OctoprintPrinterInfo
|
||||
|
||||
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import OctoprintDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OctoprintConfigEntry, OctoprintDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OctoprintConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Octoprint control buttons."""
|
||||
coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]["coordinator"]
|
||||
client: OctoprintClient = hass.data[DOMAIN][config_entry.entry_id]["client"]
|
||||
coordinator = config_entry.runtime_data
|
||||
client = coordinator.octoprint
|
||||
device_id = config_entry.unique_id
|
||||
assert device_id is not None
|
||||
|
||||
|
||||
@@ -2,29 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyoctoprintapi import OctoprintClient, WebcamSettings
|
||||
from pyoctoprintapi import WebcamSettings
|
||||
|
||||
from homeassistant.components.mjpeg import MjpegCamera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import OctoprintDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OctoprintConfigEntry, OctoprintDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OctoprintConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the available OctoPrint camera."""
|
||||
coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]["coordinator"]
|
||||
client: OctoprintClient = hass.data[DOMAIN][config_entry.entry_id]["client"]
|
||||
coordinator = config_entry.runtime_data
|
||||
client = coordinator.octoprint
|
||||
device_id = config_entry.unique_id
|
||||
|
||||
assert device_id is not None
|
||||
|
||||
@@ -20,19 +20,21 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type OctoprintConfigEntry = ConfigEntry[OctoprintDataUpdateCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OctoprintDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Octoprint data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: OctoprintConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
octoprint: OctoprintClient,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OctoprintConfigEntry,
|
||||
interval: int,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
@@ -43,7 +45,7 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
name=f"octoprint-{config_entry.entry_id}",
|
||||
update_interval=timedelta(seconds=interval),
|
||||
)
|
||||
self._octoprint = octoprint
|
||||
self.octoprint = octoprint
|
||||
self._printer_offline = False
|
||||
self.data = {"printer": None, "job": None, "last_read_time": None}
|
||||
|
||||
@@ -51,7 +53,7 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Update data via API."""
|
||||
printer = None
|
||||
try:
|
||||
job = await self._octoprint.get_job_info()
|
||||
job = await self.octoprint.get_job_info()
|
||||
except UnauthorizedException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except ApiError as err:
|
||||
@@ -61,7 +63,7 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
# printer will return a 409, so continue using the last
|
||||
# reading if there is one
|
||||
try:
|
||||
printer = await self._octoprint.get_printer_info()
|
||||
printer = await self.octoprint.get_printer_info()
|
||||
except PrinterOffline:
|
||||
if not self._printer_offline:
|
||||
_LOGGER.debug("Unable to retrieve printer information: Printer offline")
|
||||
|
||||
@@ -7,15 +7,14 @@ import logging
|
||||
from pyoctoprintapi import OctoprintClient
|
||||
|
||||
from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import OctoprintDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OctoprintConfigEntry, OctoprintDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,14 +36,12 @@ def is_first_extruder(tool_name: str) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OctoprintConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the OctoPrint number entities."""
|
||||
coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]["coordinator"]
|
||||
client: OctoprintClient = hass.data[DOMAIN][config_entry.entry_id]["client"]
|
||||
coordinator = config_entry.runtime_data
|
||||
client = coordinator.octoprint
|
||||
device_id = config_entry.unique_id
|
||||
|
||||
assert device_id is not None
|
||||
|
||||
@@ -12,14 +12,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfInformation, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import OctoprintDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OctoprintConfigEntry, OctoprintDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,13 +33,11 @@ def _is_printer_printing(printer: OctoprintPrinterInfo) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OctoprintConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the available OctoPrint sensors."""
|
||||
coordinator: OctoprintDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]["coordinator"]
|
||||
coordinator = config_entry.runtime_data
|
||||
device_id = config_entry.unique_id
|
||||
|
||||
assert device_id is not None
|
||||
|
||||
@@ -4,27 +4,20 @@ import logging
|
||||
|
||||
from omnilogic import LoginException, OmniLogic, OmniLogicException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_SCAN_INTERVAL,
|
||||
COORDINATOR,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
OMNI_API,
|
||||
)
|
||||
from .coordinator import OmniLogicUpdateCoordinator
|
||||
from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||
from .coordinator import OmniLogicConfigEntry, OmniLogicUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OmniLogicConfigEntry) -> bool:
|
||||
"""Set up Omnilogic from a config entry."""
|
||||
|
||||
conf = entry.data
|
||||
@@ -56,21 +49,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
COORDINATOR: coordinator,
|
||||
OMNI_API: api,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: OmniLogicConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -15,17 +15,20 @@ from .const import ALL_ITEM_KINDS
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type OmniLogicConfigEntry = ConfigEntry[OmniLogicUpdateCoordinator]
|
||||
|
||||
|
||||
class OmniLogicUpdateCoordinator(DataUpdateCoordinator[dict[tuple, dict[str, Any]]]):
|
||||
"""Class to manage fetching update data from single endpoint."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: OmniLogicConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api: OmniLogic,
|
||||
name: str,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OmniLogicConfigEntry,
|
||||
polling_interval: int,
|
||||
) -> None:
|
||||
"""Initialize the global Omnilogic data updater."""
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
@@ -16,21 +15,19 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import check_guard
|
||||
from .const import COORDINATOR, DEFAULT_PH_OFFSET, DOMAIN, PUMP_TYPES
|
||||
from .coordinator import OmniLogicUpdateCoordinator
|
||||
from .const import DEFAULT_PH_OFFSET, PUMP_TYPES
|
||||
from .coordinator import OmniLogicConfigEntry, OmniLogicUpdateCoordinator
|
||||
from .entity import OmniLogicEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: OmniLogicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
|
||||
coordinator: OmniLogicUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||
COORDINATOR
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
entities = []
|
||||
|
||||
for item_id, item in coordinator.data.items():
|
||||
|
||||
@@ -7,14 +7,13 @@ from omnilogic import OmniLogicException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import check_guard
|
||||
from .const import COORDINATOR, DOMAIN, PUMP_TYPES
|
||||
from .coordinator import OmniLogicUpdateCoordinator
|
||||
from .const import PUMP_TYPES
|
||||
from .coordinator import OmniLogicConfigEntry, OmniLogicUpdateCoordinator
|
||||
from .entity import OmniLogicEntity
|
||||
|
||||
SERVICE_SET_SPEED = "set_pump_speed"
|
||||
@@ -23,14 +22,12 @@ OMNILOGIC_SWITCH_OFF = 7
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: OmniLogicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the light platform."""
|
||||
"""Set up the switch platform."""
|
||||
|
||||
coordinator: OmniLogicUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||
COORDINATOR
|
||||
]
|
||||
coordinator = entry.runtime_data
|
||||
entities = []
|
||||
|
||||
for item_id, item in coordinator.data.items():
|
||||
|
||||
@@ -4,7 +4,6 @@ from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -17,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import OndiloClient
|
||||
from .const import DOMAIN, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET
|
||||
from .coordinator import OndiloIcoPoolsCoordinator
|
||||
from .coordinator import OndiloIcoConfigEntry, OndiloIcoPoolsCoordinator
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@@ -35,7 +34,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OndiloIcoConfigEntry) -> bool:
|
||||
"""Set up Ondilo ICO from a config entry."""
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
@@ -51,17 +50,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: OndiloIcoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -16,8 +16,8 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import DOMAIN
|
||||
from .api import OndiloClient
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,13 +41,16 @@ class OndiloIcoMeasurementData:
|
||||
sensors: dict[str, Any]
|
||||
|
||||
|
||||
type OndiloIcoConfigEntry = ConfigEntry[OndiloIcoPoolsCoordinator]
|
||||
|
||||
|
||||
class OndiloIcoPoolsCoordinator(DataUpdateCoordinator[dict[str, OndiloIcoPoolData]]):
|
||||
"""Fetch Ondilo ICO pools data from API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: OndiloIcoConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, api: OndiloClient
|
||||
self, hass: HomeAssistant, config_entry: OndiloIcoConfigEntry, api: OndiloClient
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
|
||||
@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
@@ -24,6 +23,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
OndiloIcoConfigEntry,
|
||||
OndiloIcoMeasuresCoordinator,
|
||||
OndiloIcoPoolData,
|
||||
OndiloIcoPoolsCoordinator,
|
||||
@@ -78,11 +78,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: OndiloIcoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Ondilo ICO sensors."""
|
||||
pools_coordinator: OndiloIcoPoolsCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
pools_coordinator = entry.runtime_data
|
||||
known_entities: set[str] = set()
|
||||
|
||||
async_add_entities(get_new_entities(pools_coordinator, known_entities))
|
||||
|
||||
@@ -122,7 +122,7 @@ class ChannelMutingCoordinator(DataUpdateCoordinator[ChannelMutingData]):
|
||||
"""Send muting command for a channel."""
|
||||
self._desired[channel] = param
|
||||
message_data: ChannelMutingDesired = self.data | self._desired
|
||||
message = command.ChannelMuting(**message_data) # type: ignore[misc]
|
||||
message = command.ChannelMuting(**message_data)
|
||||
await self.manager.write(message)
|
||||
|
||||
async def _update_callback(self, message: Status) -> None:
|
||||
|
||||
@@ -4,18 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import opengarage
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_DEVICE_KEY, DOMAIN
|
||||
from .coordinator import OpenGarageDataUpdateCoordinator
|
||||
from .const import CONF_DEVICE_KEY
|
||||
from .coordinator import OpenGarageConfigEntry, OpenGarageDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OpenGarageConfigEntry) -> bool:
|
||||
"""Set up OpenGarage from a config entry."""
|
||||
open_garage_connection = opengarage.OpenGarage(
|
||||
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}",
|
||||
@@ -27,17 +26,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, entry, open_garage_connection
|
||||
)
|
||||
await open_garage_data_coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = open_garage_data_coordinator
|
||||
entry.runtime_data = open_garage_data_coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: OpenGarageConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -9,12 +9,10 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OpenGarageDataUpdateCoordinator
|
||||
from .coordinator import OpenGarageConfigEntry, OpenGarageDataUpdateCoordinator
|
||||
from .entity import OpenGarageEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -30,13 +28,11 @@ SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: OpenGarageConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the OpenGarage binary sensors."""
|
||||
open_garage_data_coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
open_garage_data_coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
OpenGarageBinarySensor(
|
||||
open_garage_data_coordinator,
|
||||
|
||||
@@ -13,13 +13,11 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OpenGarageDataUpdateCoordinator
|
||||
from .coordinator import OpenGarageConfigEntry, OpenGarageDataUpdateCoordinator
|
||||
from .entity import OpenGarageEntity
|
||||
|
||||
|
||||
@@ -42,13 +40,11 @@ BUTTONS: tuple[OpenGarageButtonEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenGarageConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the OpenGarage button entities."""
|
||||
coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
OpenGarageButtonEntity(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user