Compare commits

..

1 Commits

Author SHA1 Message Date
Mike Degatano 696f0d15f2 Ensure Supervisor is up to date before hassio setup completes
During onboarding, Supervisor may be out of date relative to the core
version being installed. Attempt a Supervisor update at the start of
async_setup_entry when not yet onboarded:

- SupervisorBadRequestError: no update available, proceed normally.
- No exception: update was triggered, raise ConfigEntryNotReady to
  retry once Supervisor has restarted with the new version.
- Any other SupervisorError: unexpected failure communicating with
  Supervisor, raise ConfigEntryNotReady to retry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-20 20:10:04 +00:00
141 changed files with 3722 additions and 3258 deletions
+1
View File
@@ -19,6 +19,7 @@ machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true
requirements_test_all.txt linguist-generated=true
requirements_test_pre_commit.txt linguist-generated=true
script/hassfest/docker/Dockerfile linguist-generated=true
.github/workflows/*.lock.yml linguist-generated=true
+23 -55
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"75b8b624ba0c144fb4b28cba143d16a47c30de8afae568fa3256c6febe01a68a","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1ad29b8fb97f5df4466be54051779a3188f094d7efb041a8ed55211eab33c5f5","compiler_version":"v0.74.4","strict":true,"agent_id":"copilot"}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"d3abfe96a194bce3a523ed2093ddedd5704cdf62","version":"v0.74.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
# ___ _ _
# / _ \ | | (_)
@@ -65,9 +65,7 @@ run-name: "Check requirements (AW)"
jobs:
activation:
needs:
- extract_pr_number
- pre_activation
needs: pre_activation
# zizmor: ignore[dangerous-triggers] - workflow_run trigger is secured with role and fork validation
if: >
(needs.pre_activation.outputs.activated == 'true') && (github.event_name != 'workflow_run' || github.event.workflow_run.repository.id == github.repository_id &&
@@ -191,20 +189,20 @@ jobs:
run: |
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
{
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
<system>
GH_AW_PROMPT_198418d99edc7d5b_EOF
GH_AW_PROMPT_bb296919e461941b_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
<safe-output-tools>
Tools: add_comment, missing_tool, missing_data, noop
</safe-output-tools>
GH_AW_PROMPT_198418d99edc7d5b_EOF
GH_AW_PROMPT_bb296919e461941b_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
<github-context>
The following GitHub context information is available for this workflow:
{{#if github.actor}}
@@ -233,12 +231,12 @@ jobs:
{{/if}}
</github-context>
GH_AW_PROMPT_198418d99edc7d5b_EOF
GH_AW_PROMPT_bb296919e461941b_EOF
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
cat << 'GH_AW_PROMPT_198418d99edc7d5b_EOF'
cat << 'GH_AW_PROMPT_bb296919e461941b_EOF'
</system>
{{#runtime-import .github/workflows/check-requirements.md}}
GH_AW_PROMPT_198418d99edc7d5b_EOF
GH_AW_PROMPT_bb296919e461941b_EOF
} > "$GH_AW_PROMPT"
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -316,9 +314,7 @@ jobs:
retention-days: 1
agent:
needs:
- activation
- extract_pr_number
needs: activation
runs-on: ubuntu-latest
permissions:
actions: read
@@ -389,6 +385,11 @@ jobs:
name: check-requirements-deterministic
path: /tmp/gh-aw/deterministic
run-id: ${{ github.event.workflow_run.id }}
- if: github.event.workflow_run.conclusion == 'success'
name: Extract PR number from artifact
run: |-
PR=$(python3 -c 'import json,sys;print(json.load(open("/tmp/gh-aw/deterministic/results.json"))["pr_number"])')
echo "PR_NUMBER=${PR}" >> "${GITHUB_ENV}"
- name: Configure Git credentials
env:
@@ -453,15 +454,15 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_627e06df80c4e5ad_EOF'
{"add_comment":{"max":1,"target":"${{ needs.extract_pr_number.outputs.pr_number }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_627e06df80c4e5ad_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_c09f5151c817ddfc_EOF'
{"add_comment":{"max":1,"target":"${{ env.PR_NUMBER }}"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_c09f5151c817ddfc_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
{
"description_suffixes": {
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ needs.extract_pr_number.outputs.pr_number }}. Supports reply_to_id for discussion threading."
"add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Target: ${{ env.PR_NUMBER }}. Supports reply_to_id for discussion threading."
},
"repo_params": {},
"dynamic_tools": []
@@ -647,7 +648,7 @@ jobs:
mkdir -p /home/runner/.copilot
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
cat << GH_AW_MCP_CONFIG_175174907e5a28b4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
cat << GH_AW_MCP_CONFIG_d12799b4d7ffe5c2_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
{
"mcpServers": {
"github": {
@@ -691,7 +692,7 @@ jobs:
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
}
}
GH_AW_MCP_CONFIG_175174907e5a28b4_EOF
GH_AW_MCP_CONFIG_d12799b4d7ffe5c2_EOF
- name: Mount MCP servers as CLIs
id: mount-mcp-clis
continue-on-error: true
@@ -938,7 +939,6 @@ jobs:
- activation
- agent
- detection
- extract_pr_number
- safe_outputs
if: >
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
@@ -1283,37 +1283,6 @@ jobs:
}
}
extract_pr_number:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
- name: Extract PR number from artifact
id: extract
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
pre_activation:
runs-on: ubuntu-slim
outputs:
@@ -1352,7 +1321,6 @@ jobs:
- activation
- agent
- detection
- extract_pr_number
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
runs-on: ubuntu-slim
permissions:
@@ -1425,7 +1393,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,binstar.org,bootstrap.pypa.io,conda.anaconda.org,conda.binstar.org,files.pythonhosted.org,github.com,host.docker.internal,pip.pypa.io,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.npmjs.org,repo.anaconda.com,repo.continuum.io,telemetry.enterprise.githubcopilot.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ needs.extract_pr_number.outputs.pr_number }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"${{ env.PR_NUMBER }}\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
+13 -138
View File
@@ -19,30 +19,7 @@ tools:
safe-outputs:
add-comment:
max: 1
target: "${{ needs.extract_pr_number.outputs.pr_number }}"
needs:
- extract_pr_number
jobs:
extract_pr_number:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
pr_number: ${{ steps.extract.outputs.pr_number }}
steps:
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/deterministic
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract PR number from artifact
id: extract
run: |
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
target: "${{ env.PR_NUMBER }}"
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: true
@@ -55,6 +32,11 @@ steps:
path: /tmp/gh-aw/deterministic
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract PR number from artifact
if: github.event.workflow_run.conclusion == 'success'
run: |
PR=$(python3 -c 'import json,sys;print(json.load(open("/tmp/gh-aw/deterministic/results.json"))["pr_number"])')
echo "PR_NUMBER=${PR}" >> "${GITHUB_ENV}"
post-steps:
- name: Verify agent produced an add_comment safe-output
if: always() && github.event.workflow_run.conclusion == 'success'
@@ -98,11 +80,10 @@ The deterministic stage uploaded its results to the runner at
The JSON has this shape:
- `pr_number` — the PR being checked. The `add_comment` safe-output is
already targeted at this PR (a pre-job extracts `pr_number` from the
artifact and the workflow wires it into the safe-output config via
`needs.extract_pr_number.outputs.pr_number`), so **you do not need to
set `item_number` yourself** — just emit `add_comment` with the
rendered body.
already targeted at this PR (the workflow extracted `pr_number` from
the artifact and wired it into the safe-output config), so **you do
not need to set `item_number` yourself** — just emit `add_comment`
with the rendered body.
- `needs_agent``true` iff any package's check needs resolution.
- `packages[]` — one entry per changed package. Each entry has:
- `name`, `old_version` (`null` for a newly added package; otherwise the
@@ -180,10 +161,9 @@ Verify that the package's source repository is publicly reachable.
- Any other inconclusive result → ⚠️ with a one-line description.
If `repo_public` resolves to ❌ for a package, **also** mark that
package's `release_pipeline` and `async_blocking` cells/details as ``
(em dash) and explain `Skipped because the source repository is not
publicly accessible.` — neither check can be performed without a public
repo.
package's `release_pipeline` cell/detail as `` (em dash) and explain
`Skipped because the source repository is not publicly accessible.` —
because the release pipeline cannot be inspected without a public repo.
### Check kind: `pr_link`
@@ -259,111 +239,6 @@ host from `package.repo_url`, then apply the corresponding checklist.
3. If no CI config can be retrieved: ⚠️ `Release pipeline could not be
inspected; hosting provider is not GitHub or GitLab.`
### Check kind: `async_blocking`
Verify whether the dependency performs blocking I/O inside async code
paths. Home Assistant runs on a single asyncio event loop, so a library
that exposes an `async` surface must not call blocking APIs from inside
its `async def` functions — that stalls the whole loop. A purely sync
library is fine: Home Assistant integrations are expected to wrap such
calls in an executor.
**Two modes — pick by inspecting `package.old_version`:**
- `old_version` is `null` → **new package**: review the *entire current
source tree*. Nothing about this dependency has been vetted before.
- `old_version` is a string → **version bump**: review only the *diff
between `old_version` and `new_version`*. The previous version was
already accepted, so blocking calls that were present in
`old_version` are not regressions; report only what `new_version`
introduces.
#### Step 1 — Decide whether the library exposes an async surface
Use the `github` MCP tool (for `github.com` repos) or `web-fetch`
(other hosts) on `package.repo_url`. Always inspect the tag /
ref matching `new_version` (e.g. `v{new_version}` or `{new_version}`).
- Locate the top-level package directory (usually named after the
import name, often equal or close to `package.name`).
- Check `pyproject.toml` / `setup.py` / `setup.cfg` / `README*` for
async indicators (`Framework :: AsyncIO` trove classifier, `asyncio`
/ `aiohttp` / `httpx` / `anyio` in dependencies, an async usage
example in the README).
- Grep the package source for `async def`. A handful of `async def`
entries in the public modules is enough to treat the library as
having an async surface.
If the library is **sync-only** (no `async def` in its public modules
and no async framework dependency) → ✅
`Sync-only library; Home Assistant integrations must wrap calls in an
executor.` *This verdict is the same in both modes.*
#### Step 2a — Mode: new package (`old_version` is `null`)
Inspect **every `async def` in the public modules** for blocking
patterns. Walk transitively into helpers the async functions call.
#### Step 2b — Mode: version bump (`old_version` is a string)
Fetch the diff between the two tags and review **only changed lines**:
- GitHub: `GET /repos/{owner}/{repo}/compare/{old_tag}...{new_tag}` via
the `github` MCP tool, or
`https://github.com/{owner}/{repo}/compare/{old_tag}...{new_tag}.diff`
via `web-fetch`. Try the common tag formats in order until one
resolves: `v{version}`, `{version}`, `release-{version}`.
- GitLab: `https://gitlab.com/{namespace}/{project}/-/compare/{old_tag}...{new_tag}.diff`.
- Other hosts: use the project's equivalent compare URL via
`web-fetch`.
If neither tag format resolves on the host, fall back to a full review
(Step 2a) and mention in the detail that the diff was unavailable.
When reviewing the diff, only flag blocking patterns that appear in
**added lines** *inside or reachable from* an `async def`. A blocking
call that existed in `old_version` and is unchanged is not a regression
for this bump.
#### Step 3 — Blocking patterns to look for
In both modes, the patterns to flag inside `async def` bodies are:
- Sync HTTP: `requests.`, `urllib.request`, `urllib3.` direct use,
`http.client.`, sync `httpx.Client(` / `httpx.get(` (NOT the
`AsyncClient`), `pycurl`.
- `time.sleep(` (must be `await asyncio.sleep(`).
- Sync sockets: bare `socket.socket` reads/writes, `ssl.wrap_socket`,
blocking `select.select`.
- File I/O: `open(` / `pathlib.Path.read_*` / `.write_*` for
non-trivial sizes (small one-shot reads during import are
acceptable; reads/writes on the request path are not — prefer
`aiofiles` / executor).
- Sync DB drivers used directly: `sqlite3`, `psycopg2`, `pymysql`,
`pymongo` (sync client), `redis.Redis` (sync client).
- `subprocess.run` / `subprocess.call` / `os.system` (must be
`asyncio.create_subprocess_*`).
A call that is clearly dispatched to an executor
(`run_in_executor`, `asyncio.to_thread`, `anyio.to_thread.run_sync`)
does NOT count as blocking.
#### Step 4 — Verdict
- ✅ — no offending blocking pattern in the surface being reviewed
(whole tree for a new package, added lines for a bump). For a bump,
phrase the detail as `No new blocking calls introduced in
{old_version} → {new_version}.`.
- ⚠️ — blocking calls exist only in sync helpers that the async API
does not call, or only on a clearly non-hot path (e.g. one-shot
setup before the event loop is running). Cite at least one
`<file>:<line>` and explain why it is not on the hot path.
- ❌ — a blocking call is reachable from an `async def` that is part
of the public API on the request / polling path (for a bump: the
call was introduced or moved onto the hot path by this version).
Cite the offending `<file>:<line>` as a clickable link on the repo
host so the contributor can jump to it.
## Notes
- Be constructive and helpful. Reference the inspected workflow / CI
+3 -3
View File
@@ -132,7 +132,7 @@
"problemMatcher": []
},
{
"label": "Install all production Requirements",
"label": "Install all Requirements",
"type": "shell",
"command": "uv pip install -r requirements_all.txt",
"group": {
@@ -146,9 +146,9 @@
"problemMatcher": []
},
{
"label": "Install all (test & production) Requirements",
"label": "Install all Test Requirements",
"type": "shell",
"command": "uv pip install -r requirements_all.txt -r requirements_test.txt",
"command": "uv pip install -r requirements.txt -r requirements_test_all.txt",
"group": {
"kind": "build",
"isDefault": true
Generated
+4 -4
View File
@@ -1413,8 +1413,8 @@ CLAUDE.md @home-assistant/core
/tests/components/pushover/ @engrbm87
/homeassistant/components/pvoutput/ @frenck
/tests/components/pvoutput/ @frenck
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
/homeassistant/components/pvpc_hourly_pricing/ @azogue
/tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/pyload/ @tr4nt0r
/tests/components/pyload/ @tr4nt0r
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
@@ -1538,8 +1538,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/saj/ @fredericvl
/homeassistant/components/samsung_infrared/ @lmaertin
/tests/components/samsung_infrared/ @lmaertin
/homeassistant/components/samsungtv/ @chemelli74
/tests/components/samsungtv/ @chemelli74
/homeassistant/components/samsungtv/ @chemelli74 @epenet
/tests/components/samsungtv/ @chemelli74 @epenet
/homeassistant/components/sanix/ @tomaszsluszniak
/tests/components/sanix/ @tomaszsluszniak
/homeassistant/components/satel_integra/ @Tommatheussen
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aosmith",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.18"]
"requirements": ["py-aosmith==1.0.17"]
}
@@ -1,11 +1,9 @@
"""Config flow to configure the Arcam FMJ component."""
import socket
from typing import Any
from urllib.parse import urlparse
from arcam.fmj import ConnectionFailed
from arcam.fmj.client import Client
from arcam.fmj.client import Client, ConnectionFailed
from arcam.fmj.utils import get_uniqueid_from_host, get_uniqueid_from_udn
import voluptuous as vol
@@ -31,19 +29,26 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(uuid)
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})
async def _async_try_connect(self, host: str, port: int) -> None:
"""Verify the device is reachable."""
async def _async_check_and_create(self, host: str, port: int) -> ConfigFlowResult:
client = Client(host, port)
try:
await client.start()
except ConnectionFailed:
return self.async_abort(reason="cannot_connect")
finally:
await client.stop()
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({host})",
data={CONF_HOST: host, CONF_PORT: port},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a discovered device."""
errors: dict[str, str] = {}
if user_input is not None:
uuid = await get_uniqueid_from_host(
async_get_clientsession(self.hass), user_input[CONF_HOST]
@@ -53,36 +58,18 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
user_input[CONF_HOST], user_input[CONF_PORT], uuid
)
try:
await self._async_try_connect(
user_input[CONF_HOST], user_input[CONF_PORT]
)
except socket.gaierror:
errors["base"] = "invalid_host"
except TimeoutError:
errors["base"] = "timeout_connect"
except ConnectionRefusedError:
errors["base"] = "connection_refused"
except ConnectionFailed, OSError:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})",
data={
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
},
)
return await self._async_check_and_create(
user_input[CONF_HOST], user_input[CONF_PORT]
)
fields = {
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
schema = vol.Schema(fields)
if user_input is not None:
schema = self.add_suggested_values_to_schema(schema, user_input)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
return self.async_show_form(
step_id="user", data_schema=vol.Schema(fields), errors=errors
)
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
@@ -92,10 +79,7 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
self.context["title_placeholders"] = placeholders
if user_input is not None:
return self.async_create_entry(
title=f"{DEFAULT_NAME} ({self.host})",
data={CONF_HOST: self.host, CONF_PORT: self.port},
)
return await self._async_check_and_create(self.host, self.port)
return self.async_show_form(
step_id="confirm", description_placeholders=placeholders
@@ -113,11 +97,6 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
await self._async_set_unique_id_and_update(host, port, uuid)
try:
await self._async_try_connect(host, port)
except ConnectionFailed, OSError:
return self.async_abort(reason="cannot_connect")
self.host = host
self.port = port
self.port = DEFAULT_PORT
return await self.async_step_confirm()
@@ -5,12 +5,6 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"connection_refused": "Host refused connection",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
},
"flow_title": "{host}",
"step": {
"confirm": {
@@ -185,6 +185,7 @@ class BSBLANClimate(BSBLanCircuitEntity, ClimateEntity):
except BSBLANError as err:
# pylint: disable-next=home-assistant-exception-message-with-translation
raise HomeAssistantError(
"An error occurred while updating the BSBLAN device",
translation_domain=DOMAIN,
translation_key="set_data_error",
) from err
@@ -45,8 +45,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: CalDavConfigEntry) -> bo
# on some other unexpected server response.
_LOGGER.warning("Unexpected CalDAV server response: %s", err)
return False
except requests.Timeout as err:
raise ConfigEntryNotReady("Timeout connecting to CalDAV server") from err
except requests.ConnectionError as err:
raise ConfigEntryNotReady("Connection error from CalDAV server") from err
except DAVError as err:
+2 -8
View File
@@ -38,7 +38,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CalDavConfigEntry
from .api import async_get_calendars
from .const import TIMEOUT
from .coordinator import CalDavUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -92,12 +91,7 @@ async def async_setup_platform(
days = config[CONF_DAYS]
client = caldav.DAVClient(
url,
None,
username,
password,
ssl_verify_cert=config[CONF_VERIFY_SSL],
timeout=TIMEOUT,
url, None, username, password, ssl_verify_cert=config[CONF_VERIFY_SSL]
)
calendars = await async_get_calendars(hass, client, SUPPORTED_COMPONENT)
@@ -237,7 +231,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
await self.hass.async_add_executor_job(
partial(self.coordinator.calendar.add_event, **item_data),
)
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@callback
+5 -5
View File
@@ -138,7 +138,7 @@ class WebDavTodoListEntity(TodoListEntity):
)
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
async def async_update_todo_item(self, item: TodoItem) -> None:
@@ -150,7 +150,7 @@ class WebDavTodoListEntity(TodoListEntity):
)
except NotFoundError as err:
raise HomeAssistantError(f"Could not find To-do item {uid}") from err
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
vtodo = todo.icalendar_component # type: ignore[attr-defined]
vtodo["SUMMARY"] = item.summary or ""
@@ -174,7 +174,7 @@ class WebDavTodoListEntity(TodoListEntity):
)
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
async def async_delete_todo_items(self, uids: list[str]) -> None:
@@ -188,14 +188,14 @@ class WebDavTodoListEntity(TodoListEntity):
items = await asyncio.gather(*tasks)
except NotFoundError as err:
raise HomeAssistantError("Could not find To-do item") from err
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
# Run serially as some CalDAV servers do not support concurrent modifications
for item in items:
try:
await self.hass.async_add_executor_job(item.delete)
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV delete error: {err}") from err
# refreshing async otherwise it would take too much time
self.hass.async_create_task(self.async_update_ha_state(force_refresh=True))
@@ -1,18 +1,11 @@
"""Provide functionality to keep track of devices."""
import asyncio
from typing import Any
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from .config_entry import ( # noqa: F401
DATA_COMPONENT,
BaseScannerEntity,
BaseTrackerEntity,
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -40,8 +33,6 @@ from .const import ( # noqa: F401
DEFAULT_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
LOGGER,
PLATFORM_TYPE_LEGACY,
SCAN_INTERVAL,
SourceType,
)
@@ -53,9 +44,7 @@ from .legacy import ( # noqa: F401
SOURCE_TYPES,
AsyncSeeCallback,
DeviceScanner,
DeviceTracker,
SeeCallback,
async_create_platform_type,
async_setup_integration as async_setup_legacy_integration,
see,
)
@@ -68,43 +57,5 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker."""
component = hass.data[DATA_COMPONENT] = EntityComponent[BaseTrackerEntity](
LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
component.config = {}
component.register_shutdown()
# The tracker is loaded in the async_setup_legacy_integration task so
# we create a future to avoid waiting on it here so that only
# async_platform_discovered will have to wait in the rare event
# a custom component still uses the legacy device tracker discovery.
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {})
if platform is None:
return
if platform.type != PLATFORM_TYPE_LEGACY:
await component.async_setup_platform(p_type, {}, info)
return
tracker = await tracker_future
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
#
# Legacy and platforms load in a non-awaited tracked task
# to ensure device tracker setup can continue and config
# entry integrations are not waiting for legacy device
# tracker platforms to be set up.
#
hass.async_create_task(
async_setup_legacy_integration(hass, config, tracker_future),
eager_start=True,
)
async_setup_legacy_integration(hass, config)
return True
@@ -37,7 +37,11 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers import (
config_validation as cv,
discovery,
entity_registry as er,
)
from homeassistant.helpers.event import (
async_track_time_interval,
async_track_utc_time_change,
@@ -200,7 +204,40 @@ def see(
hass.services.call(DOMAIN, SERVICE_SEE, data)
async def async_setup_integration(
@callback
def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
"""Set up the legacy integration."""
# The tracker is loaded in the _async_setup_integration task so
# we create a future to avoid waiting on it here so that only
# async_platform_discovered will have to wait in the rare event
# a custom component still uses the legacy device tracker discovery.
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
async def async_platform_discovered(
p_type: str, info: dict[str, Any] | None
) -> None:
"""Load a platform."""
platform = await async_create_platform_type(hass, config, p_type, {})
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
return
tracker = await tracker_future
await platform.async_setup_legacy(hass, tracker, info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
#
# Legacy and platforms load in a non-awaited tracked task
# to ensure device tracker setup can continue and config
# entry integrations are not waiting for legacy device
# tracker platforms to be set up.
#
hass.async_create_task(
_async_setup_integration(hass, config, tracker_future), eager_start=True
)
async def _async_setup_integration(
hass: HomeAssistant,
config: ConfigType,
tracker_future: asyncio.Future[DeviceTracker],
@@ -31,7 +31,6 @@ class EkeyEvent(EventEntity):
_attr_device_class = EventDeviceClass.BUTTON
_attr_event_types = ["event happened"]
_attr_has_entity_name = True
def __init__(
self,
+2 -2
View File
@@ -509,12 +509,14 @@ FITBIT_RESOURCE_BATTERY = FitbitSensorEntityDescription(
icon="mdi:battery",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
)
FITBIT_RESOURCE_BATTERY_LEVEL = FitbitSensorEntityDescription(
key="devices/battery_level",
translation_key="battery_level",
scope=FitbitScope.DEVICE,
entity_category=EntityCategory.DIAGNOSTIC,
has_entity_name=True,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
)
@@ -652,7 +654,6 @@ class FitbitBatterySensor(CoordinatorEntity[FitbitDeviceCoordinator], SensorEnti
entity_description: FitbitSensorEntityDescription
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True
def __init__(
self,
@@ -712,7 +713,6 @@ class FitbitBatteryLevelSensor(
"""Implementation of a Fitbit battery level sensor."""
entity_description: FitbitSensorEntityDescription
_attr_has_entity_name = True
_attr_attribution = ATTRIBUTION
def __init__(
-2
View File
@@ -438,7 +438,6 @@ class FritzBoxBaseSwitch(FritzBoxBaseEntity, SwitchEntity):
self._attr_is_on = turn_on
# pylint: disable-next=home-assistant-missing-has-entity-name
class FritzBoxPortSwitch(FritzBoxBaseSwitch):
"""Defines a FRITZ!Box Tools PortForward switch."""
@@ -605,7 +604,6 @@ class FritzBoxProfileSwitch(FritzBoxBaseCoordinatorSwitch):
self.async_write_ha_state()
# pylint: disable-next=home-assistant-missing-has-entity-name
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
"""Defines a FRITZ!Box Tools Wifi switch."""
@@ -806,10 +806,10 @@ async def _create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa:
data["daysOfMonth"] = [start_date.day]
data["weeksOfMonth"] = []
if (interval := call.data.get(ATTR_INTERVAL)) is not None:
if interval := call.data.get(ATTR_INTERVAL):
data["everyX"] = interval
if (streak := call.data.get(ATTR_STREAK)) is not None:
if streak := call.data.get(ATTR_STREAK):
data["streak"] = streak
try:
+24 -1
View File
@@ -8,7 +8,7 @@ import os
import struct
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor import SupervisorBadRequestError, SupervisorError
from aiohasupervisor.models import (
GreenOptions,
HomeAssistantOptions,
@@ -25,6 +25,7 @@ from homeassistant.components.http import (
CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE,
)
from homeassistant.components.onboarding import async_is_onboarded
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
from homeassistant.const import (
EVENT_CORE_CONFIG_UPDATE,
@@ -301,6 +302,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
translation_key="supervisor_not_connected",
) from err
# During onboarding, Supervisor may be out of date. Attempt an update now
# so that core loads against an up-to-date Supervisor. A
# SupervisorBadRequestError means there is no update available, proceed
# normally. No exception means an update was triggered and we must wait for
# it to complete. Any other SupervisorError means something unexpected went
# wrong and we cannot proceed right now.
if not async_is_onboarded(hass):
try:
await supervisor_client.supervisor.update()
except SupervisorBadRequestError:
pass # No update available, proceed normally.
except SupervisorError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="supervisor_not_connected",
) from err
else:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="supervisor_update_pending",
)
# Get or create a refresh token for the Supervisor user
user = hass.data[DATA_HASSIO_SUPERVISOR_USER]
if user.refresh_tokens:
@@ -55,6 +55,9 @@
},
"supervisor_not_connected": {
"message": "Not connected with the supervisor / system too busy"
},
"supervisor_update_pending": {
"message": "Supervisor update in progress, will retry when complete"
}
},
"issues": {
@@ -266,12 +266,14 @@ class HomeConnectAirConditioningEntity(HomeConnectEntity, ClimateEntity):
value=BSH_POWER_ON,
)
except HomeConnectError as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="power_on",
translation_placeholders={
**get_dict_from_home_connect_error(err),
"appliance_name": self.appliance.info.name,
"value": BSH_POWER_ON,
},
) from err
@@ -180,24 +180,27 @@ async def async_setup_entry(
class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEntity):
"""Representation of the HomematicIP cloud connection sensor."""
_attr_translation_key = "cloud_connection"
def __init__(self, hap: HomematicipHAP) -> None:
"""Initialize the cloud connection sensor."""
super().__init__(hap, hap.home, feature_id="cloud_connection")
@property
def name(self) -> str:
"""Return the name cloud connection entity."""
name = "Cloud Connection"
# Add a prefix to the name if the homematic ip home has a name.
return name if not self._home.name else f"{self._home.name} {name}"
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
# Merges into the existing HAP device registered in __init__.py.
# Name must match __init__.py logic for has_entity_name to work.
label = self._home.label or ""
# Adds a sensor to the existing HAP device
return DeviceInfo(
identifiers={
# Serial numbers of Homematic IP device
(DOMAIN, self._home.id)
},
name=label,
}
)
@property
@@ -576,7 +579,6 @@ class HomematicipPluggableMainsFailureSurveillanceSensor(
class HomematicipSecurityZoneSensorGroup(HomematicipGenericEntity, BinarySensorEntity):
"""Representation of the HomematicIP security zone sensor group."""
_attr_has_entity_name = False
_attr_device_class = BinarySensorDeviceClass.SAFETY
def __init__(
@@ -74,7 +74,6 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
basically enabled in the hmip app.
"""
_attr_has_entity_name = False
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
)
@@ -320,7 +320,6 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity):
class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
"""Representation of the HomematicIP cover shutter group."""
_attr_has_entity_name = False
_attr_device_class = CoverDeviceClass.SHUTTER
def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None:
@@ -74,7 +74,6 @@ GROUP_ATTRIBUTES = {
class HomematicipGenericEntity(Entity):
"""Representation of the HomematicIP generic entity."""
_attr_has_entity_name = True
_attr_should_poll = False
def __init__(
@@ -113,14 +112,6 @@ class HomematicipGenericEntity(Entity):
# Marker showing that the HmIP device hase been removed.
self.hmip_device_removed = False
# Compute entity name based on has_entity_name mode.
if not self._attr_has_entity_name:
# Legacy mode (groups, special entities): compose the full name
# including device/group label and home prefix.
self._attr_name = self._compute_legacy_name()
else:
self._setup_entity_name()
@property
def device_info(self) -> DeviceInfo | None:
"""Return device specific attributes."""
@@ -129,14 +120,6 @@ class HomematicipGenericEntity(Entity):
device_id = str(self._device.id)
home_id = str(self._device.homeId)
# Include the home name in the device name so that the
# previous "{home} {device}" naming is preserved after
# switching to has_entity_name=True.
device_name = self._device.label
home_name = getattr(self._home, "name", None)
if device_name and home_name:
device_name = f"{home_name} {device_name}"
return DeviceInfo(
identifiers={
# Serial numbers of Homematic IP device
@@ -144,7 +127,7 @@ class HomematicipGenericEntity(Entity):
},
manufacturer=self._device.oem,
model=self._device.modelType,
name=device_name,
name=self._device.label,
sw_version=self._device.firmwareVersion,
# Link to the homematic ip access point.
via_device=(DOMAIN, home_id),
@@ -217,93 +200,38 @@ class HomematicipGenericEntity(Entity):
self.async_remove(force_remove=True), eager_start=False
)
def _compute_legacy_name(self) -> str:
"""Compute the full legacy name for entities without has_entity_name.
@property
def name(self) -> str:
"""Return the name of the generic entity."""
Used by group entities and other special cases where has_entity_name
is False. Includes device/group label, post suffix, and home prefix.
"""
name = self._device.label or ""
if self._post:
name = f"{name} {self._post}" if name else self._post
name = ""
# Try to get a label from a channel.
functional_channels = getattr(self._device, "functionalChannels", None)
if functional_channels and self.functional_channel:
if self._is_multi_channel:
label = getattr(self.functional_channel, "label", None)
if label:
name = str(label)
elif len(functional_channels) > 1:
label = getattr(functional_channels[1], "label", None)
if label:
name = str(label)
# Use device label, if name is not defined by channel label.
if not name:
name = self._device.label or ""
if self._post:
name = f"{name} {self._post}"
elif self._is_multi_channel:
name = f"{name} Channel{self.get_channel_index()}"
# Add a prefix to the name if the homematic ip home has a name.
home_name = getattr(self._home, "name", None)
if name and home_name:
name = f"{home_name} {name}"
return name
def _setup_entity_name(self) -> None:
"""Set up entity naming for has_entity_name mode.
With has_entity_name=True, HA composes the full friendly name as
"{device_name} {entity_name}". This method sets the appropriate
naming attributes.
For multi-channel entities, channel labels provide _attr_name (dynamic).
For entities with _post, _attr_name is derived from the post suffix,
with the first letter capitalized for display consistency.
For primary entities, HA uses device_class as the name.
"""
# Multi-channel entities: use channel label as entity name.
if self._is_multi_channel and self.functional_channel:
label = getattr(self.functional_channel, "label", None)
if label:
label_str = str(label)
device_label = self._device.label or ""
# Strip device name prefix from channel label to avoid
# duplication when HA composes "{device_name} {entity_name}".
# E.g., device "Licht Flur" + channel "Licht Flur 5" -> "5".
if device_label and label_str.startswith(device_label):
stripped = label_str[len(device_label) :].strip()
if stripped:
self._attr_name = stripped
# Otherwise channel label equals device label (modulo
# whitespace); leave _attr_name unset so HA composes just
# the device name without duplicating it.
return
self._attr_name = label_str
return
# Fallback: use post suffix or generic channel name.
if self._post:
self._attr_name = self._post[0].upper() + self._post[1:]
else:
self._attr_name = f"Channel{self.get_channel_index()}"
return
# Entities with a post suffix: use it as the entity name,
# capitalizing the first letter for display consistency.
if self._post:
self._attr_name = self._post[0].upper() + self._post[1:]
return
# Non-multi-channel entities on devices with multiple channels:
# use the first functional channel's label as name context.
# This preserves names like "Treppe CH" for single-function entities
# on multi-channel devices (e.g., HmIP-BSL switch channel).
functional_channels = getattr(self._device, "functionalChannels", None)
if functional_channels and len(functional_channels) > 1:
ch1 = (
functional_channels.get(1)
if isinstance(functional_channels, dict)
else functional_channels[1]
)
label = getattr(ch1, "label", None) if ch1 else None
if label:
label_str = str(label)
device_label = self._device.label or ""
# Strip device name prefix to avoid duplication.
if device_label and label_str.startswith(device_label):
stripped = label_str[len(device_label) :].strip()
if stripped:
self._attr_name = stripped
# Otherwise channel label equals device label (modulo
# whitespace); leave _attr_name unset.
return
self._attr_name = label_str
return
# Primary entity on device: leave unset so HA derives name from
# device_class or translation_key.
@property
def available(self) -> bool:
"""Return if entity is available."""
@@ -82,6 +82,7 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
super().__init__(
hap,
device,
post=description.key,
channel=channel,
is_multi_channel=False,
feature_id="doorbell",
@@ -1070,7 +1070,9 @@ class HmipSmokeDetectorSensor(HomematicipGenericEntity, SensorEntity):
description: HmipSmokeDetectorSensorDescription,
) -> None:
"""Initialize the smoke detector sensor."""
super().__init__(hap, device, feature_id="smoke_detector_sensor")
super().__init__(
hap, device, post=description.key, feature_id="smoke_detector_sensor"
)
self.entity_description = description
self._sensor_unique_id = f"{device.id}_{description.key}"
@@ -37,11 +37,6 @@
}
},
"entity": {
"binary_sensor": {
"cloud_connection": {
"name": "Cloud connection"
}
},
"light": {
"optical_signal_light": {
"state_attributes": {
@@ -142,8 +142,6 @@ class HomematicipSwitch(HomematicipMultiSwitch, SwitchEntity):
class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity):
"""Representation of the HomematicIP switching group."""
_attr_has_entity_name = False
def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None:
"""Initialize switching group."""
device.modelType = f"HmIP-{post}"
@@ -74,6 +74,11 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
"""Initialize the weather sensor."""
super().__init__(hap, device, feature_id="weather")
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._device.label
@property
def native_temperature(self) -> float:
"""Return the platform temperature."""
@@ -113,7 +118,6 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor):
class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity):
"""Representation of the HomematicIP home weather."""
_attr_has_entity_name = False
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
_attr_attribution = "Powered by Homematic IP"
@@ -16,7 +16,6 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_COMMAND,
CONF_HOST,
CONF_ID,
CONF_NAME,
@@ -40,6 +39,9 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
# pylint: disable-next=home-assistant-duplicate-const
CONF_COMMAND = "command"
EVENT_BUTTON_PRESS = "homeworks_button_press"
EVENT_BUTTON_RELEASE = "homeworks_button_release"
@@ -73,9 +73,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) ->
except InvalidHeaterList as exc:
raise NoHeaters from exc
except InvalidGateway as exc:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="incorrect_credentials"
) from exc
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed("Incorrect credentials") from exc
except ClientResponseError as exc:
if exc.status == 404:
raise NotFound from exc
@@ -15,12 +15,10 @@ from incomfortclient import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
type InComfortConfigEntry = ConfigEntry[InComfortDataCoordinator]
_LOGGER = logging.getLogger(__name__)
@@ -79,20 +77,16 @@ class InComfortDataCoordinator(DataUpdateCoordinator[InComfortData]):
try:
for heater in self.incomfort_data.heaters:
await heater.update()
except TimeoutError as exc:
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed("Timeout error") from exc
except ClientResponseError as exc:
if exc.status == 401:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="incorrect_credentials"
) from exc
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed_with_error_message",
translation_placeholders={"error": exc.message},
) from exc
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryError("Incorrect credentials") from exc
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(exc.message) from exc
except InvalidHeaterList as exc:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed_with_error_message",
translation_placeholders={"error": exc.message},
) from exc
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(exc.message) from exc
return self.incomfort_data
@@ -131,7 +131,6 @@
}
},
"exceptions": {
"incorrect_credentials": { "message": "Incorrect credentials." },
"no_heaters": {
"message": "[%key:component::incomfort::config::error::no_heaters%]"
},
@@ -143,9 +142,6 @@
},
"unknown": {
"message": "[%key:component::incomfort::config::error::unknown%]"
},
"update_failed_with_error_message": {
"message": "Update failed, got {error}."
}
},
"options": {
@@ -72,11 +72,7 @@ class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]):
try:
config_data = await self.api.get_config()
except (ClientError, OSError) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
translation_placeholders={"error": str(err)},
) from err
raise ConfigEntryNotReady(f"Device config retrieval failed: {err}") from err
# Cache device information
device_data = config_data.get("device", {})
@@ -91,11 +87,7 @@ class IndevoltCoordinator(DataUpdateCoordinator[dict[str, Any]]):
try:
return await self.api.fetch_data(sensor_keys)
except (ClientError, OSError) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
raise UpdateFailed(f"Device update failed: {err}") from err
async def async_push_data(self, sensor_key: str, value: Any) -> bool:
"""Push/write data values to given key on the device."""
@@ -12,6 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/indevolt",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"quality_scale": "silver",
"requirements": ["indevolt-api==1.8.1"]
}
@@ -56,7 +56,7 @@ rules:
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: done
repair-issues:
@@ -373,9 +373,6 @@
}
},
"exceptions": {
"config_entry_not_ready": {
"message": "Device config retrieval failed: {error}"
},
"energy_mode_change_unavailable_outdoor_portable": {
"message": "Energy mode cannot be changed when the device is in outdoor/portable mode"
},
@@ -403,9 +400,6 @@
"soc_below_minimum": {
"message": "Target SOC ({target}%) is below the device minimum ({minimum_soc}%)"
},
"update_failed": {
"message": "Device update failed: {error}"
},
"write_error": {
"message": "Cannot update value for {name}"
}
+5 -2
View File
@@ -556,8 +556,9 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required(
CONF_KNX_ROUTE_BACK, default=_route_back
): selector.BooleanSelector(),
vol.Optional(CONF_KNX_LOCAL_IP): _IP_SELECTOR,
}
if self.show_advanced_options:
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
if not self._found_tunnels and not errors.get("base"):
errors["base"] = "no_tunnel_discovered"
@@ -889,8 +890,10 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
): selector.BooleanSelector(),
vol.Required(CONF_KNX_MCAST_GRP, default=_multicast_group): _IP_SELECTOR,
vol.Required(CONF_KNX_MCAST_PORT, default=_multicast_port): _PORT_SELECTOR,
vol.Optional(CONF_KNX_LOCAL_IP): _IP_SELECTOR,
}
if self.show_advanced_options:
# Optional with default doesn't work properly in flow UI
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
@@ -77,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) ->
await coordinator_info.async_config_entry_first_refresh()
if info_api.data is None or info_api.serial_number is None:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="missing_device_info"
)
@@ -216,8 +216,6 @@ class LunatoneLineBroadcastLight(
_attr_assumed_state = True
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_has_entity_name = True
_attr_name = None
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(
@@ -35,10 +35,5 @@
"description": "Enter the URL of your Lunatone device.\nHome Assistant will use this address to connect to the device API."
}
}
},
"exceptions": {
"missing_device_info": {
"message": "Unable to read device information. Please verify the device's network connection."
}
}
}
@@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -22,6 +22,10 @@ _LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Information provided by MeteoAlarm"
# pylint: disable-next=home-assistant-duplicate-const
CONF_COUNTRY = "country"
# pylint: disable-next=home-assistant-duplicate-const
CONF_LANGUAGE = "language"
CONF_PROVINCE = "province"
DEFAULT_NAME = "meteoalarm"
@@ -10,6 +10,8 @@ DEFAULT_DETECTION_TIME: Final = 300
ATTR_MANUFACTURER: Final = "Mikrotik"
ATTR_SERIAL_NUMBER: Final = "serial-number"
ATTR_FIRMWARE: Final = "current-firmware"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODEL: Final = "model"
CONF_ARP_PING: Final = "arp_ping"
CONF_FORCE_DHCP: Final = "force_dhcp"
@@ -9,13 +9,7 @@ import librouteros
from librouteros.login import plain as login_plain, token as login_token
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_MODEL,
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -23,6 +17,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import (
ARP,
ATTR_FIRMWARE,
ATTR_MODEL,
ATTR_SERIAL_NUMBER,
CAPSMAN,
CONF_ARP_PING,
+21 -23
View File
@@ -11,29 +11,27 @@ publish:
example: "The temperature is {{ states('sensor.temperature') }}"
selector:
template:
publish_options:
collapsed: true
fields:
evaluate_payload:
default: false
selector:
boolean:
qos:
default: "0"
selector:
select:
options:
- "0"
- "1"
- "2"
retain:
default: false
selector:
boolean:
message_expiry_interval:
selector:
duration:
enable_day: true
evaluate_payload:
default: false
selector:
boolean:
qos:
default: 0
selector:
select:
options:
- "0"
- "1"
- "2"
retain:
default: false
selector:
boolean:
message_expiry_interval:
selector:
duration:
enable_day: true
dump:
fields:
topic:
+1 -6
View File
@@ -1578,12 +1578,7 @@
"name": "Topic"
}
},
"name": "Publish",
"sections": {
"publish_options": {
"name": "Publish options"
}
}
"name": "Publish"
},
"reload": {
"description": "Reloads MQTT entities from the YAML-configuration.",
@@ -23,7 +23,7 @@ from music_assistant_models.errors import (
from music_assistant_models.player import Player
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_TOKEN, CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
@@ -38,7 +38,7 @@ from homeassistant.helpers.issue_registry import (
async_delete_issue,
)
from .const import ATTR_CONF_EXPOSE_PLAYER_TO_HA, DOMAIN, LOGGER
from .const import ATTR_CONF_EXPOSE_PLAYER_TO_HA, CONF_TOKEN, DOMAIN, LOGGER
from .helpers import get_music_assistant_client
from .services import register_actions
@@ -21,7 +21,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_TOKEN, CONF_URL
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.config_entry_oauth2_flow import (
@@ -31,7 +31,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import AUTH_SCHEMA_VERSION, DOMAIN, HASSIO_DISCOVERY_SCHEMA_VERSION, LOGGER
from .const import (
AUTH_SCHEMA_VERSION,
CONF_TOKEN,
DOMAIN,
HASSIO_DISCOVERY_SCHEMA_VERSION,
LOGGER,
)
DEFAULT_TITLE = "Music Assistant"
DEFAULT_URL = "http://mass.local:8095"
@@ -12,6 +12,9 @@ AUTH_SCHEMA_VERSION = 28
# Schema version where hassio discovery support was added
HASSIO_DISCOVERY_SCHEMA_VERSION = 28
# pylint: disable-next=home-assistant-duplicate-const
CONF_TOKEN = "token"
ATTR_IS_GROUP = "is_group"
ATTR_GROUP_MEMBERS = "group_members"
ATTR_GROUP_PARENTS = "group_parents"
@@ -46,6 +46,7 @@ play_media:
- "add"
translation_key: enqueue
radio_mode:
advanced: true
selector:
boolean:
@@ -137,21 +138,20 @@ search:
example: "News of the world"
selector:
text:
search_options:
fields:
limit:
example: 25
default: 5
selector:
number:
min: 1
max: 100
step: 1
library_only:
example: "true"
default: false
selector:
boolean:
limit:
advanced: true
example: 25
default: 5
selector:
number:
min: 1
max: 100
step: 1
library_only:
example: "true"
default: false
selector:
boolean:
get_library:
fields:
@@ -183,24 +183,24 @@ get_library:
example: "We Are The Champions"
selector:
text:
pagination:
fields:
limit:
example: 25
default: 25
selector:
number:
min: 1
max: 500
step: 1
offset:
example: 25
default: 0
selector:
number:
min: 0
max: 1000000
step: 1
limit:
advanced: true
example: 25
default: 25
selector:
number:
min: 1
max: 500
step: 1
offset:
advanced: true
example: 25
default: 0
selector:
number:
min: 1
max: 1000000
step: 1
order_by:
example: "random"
selector:
@@ -360,12 +360,7 @@
"name": "Search"
}
},
"name": "Get library items",
"sections": {
"pagination": {
"name": "Pagination"
}
}
"name": "Get library items"
},
"get_queue": {
"description": "Retrieves the details of the currently active queue of a Music Assistant player.",
@@ -455,12 +450,7 @@
"name": "Search name"
}
},
"name": "Search Music Assistant",
"sections": {
"search_options": {
"name": "Search options"
}
}
"name": "Search Music Assistant"
},
"transfer_queue": {
"description": "Transfers a player's queue to another player.",
@@ -0,0 +1 @@
"""Virtual integration: National Grid US."""
@@ -0,0 +1,6 @@
{
"domain": "national_grid_us",
"name": "National Grid US",
"integration_type": "virtual",
"supported_by": "opower"
}
@@ -13,7 +13,7 @@ rules:
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: todo
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
+1 -1
View File
@@ -23,6 +23,7 @@ _ATTRIBUTION = "Data provided by OMIE.es"
SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
key: SensorEntityDescription(
key=key,
has_entity_name=True,
translation_key=key,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}",
@@ -35,7 +36,6 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
class OMIEPriceSensor(CoordinatorEntity[OMIECoordinator], SensorEntity):
"""OMIE price sensor."""
_attr_has_entity_name = True
_attr_should_poll = False
_attr_attribution = _ATTRIBUTION
@@ -11,7 +11,7 @@ upload_image:
media:
accept:
- image/*
additional_fields:
advanced_options:
collapsed: true
fields:
rotation:
@@ -151,8 +151,8 @@
},
"name": "Upload image",
"sections": {
"additional_fields": {
"name": "Additional options"
"advanced_options": {
"name": "Advanced options"
}
}
}
@@ -13,7 +13,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"],
"requirements": ["pyoverkiz==1.20.4"],
"requirements": ["pyoverkiz==1.20.3"],
"zeroconf": [
{
"name": "gateway*",
@@ -15,6 +15,8 @@ ATTR_REMOTE = "remote"
ATTR_DEVICE_INFO = "device_info"
ATTR_FRIENDLY_NAME = "friendlyName"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL_NUMBER = "modelNumber"
ATTR_UDN = "UDN"
@@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
MediaType,
async_process_play_media_url,
)
from homeassistant.const import ATTR_MANUFACTURER, CONF_NAME
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,6 +23,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PanasonicVieraConfigEntry
from .const import (
ATTR_DEVICE_INFO,
ATTR_MANUFACTURER,
ATTR_MODEL_NUMBER,
ATTR_UDN,
DEFAULT_MANUFACTURER,
@@ -4,7 +4,7 @@ from collections.abc import Iterable
from typing import Any
from homeassistant.components.remote import RemoteEntity
from homeassistant.const import ATTR_MANUFACTURER, CONF_NAME, STATE_ON
from homeassistant.const import CONF_NAME, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PanasonicVieraConfigEntry, Remote
from .const import (
ATTR_DEVICE_INFO,
ATTR_MANUFACTURER,
ATTR_MODEL_NUMBER,
ATTR_UDN,
DEFAULT_MANUFACTURER,
@@ -3,7 +3,7 @@
from collections.abc import Mapping
from typing import Any
from esios_api import DEFAULT_POWER_KW, PVPCData
from aiopvpc import DEFAULT_POWER_KW, PVPCData
import voluptuous as vol
from homeassistant.config_entries import (
@@ -63,10 +63,9 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
await self.async_set_unique_id(user_input[ATTR_TARIFF])
self._abort_if_unique_id_configured()
calc_name = f"{DEFAULT_NAME} - {user_input[ATTR_TARIFF]}"
if not user_input[CONF_USE_API_TOKEN]:
return self.async_create_entry(
title=calc_name,
title=DEFAULT_NAME,
data={
ATTR_TARIFF: user_input[ATTR_TARIFF],
ATTR_POWER: user_input[ATTR_POWER],
@@ -75,7 +74,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
self._name = calc_name
self._name = DEFAULT_NAME
self._tariff = user_input[ATTR_TARIFF]
self._power = user_input[ATTR_POWER]
self._power_p3 = user_input[ATTR_POWER_P3]
@@ -151,7 +150,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle re-authentication with ESIOS Token."""
self._api_token = entry_data.get(CONF_API_TOKEN)
self._use_api_token = self._api_token is not None
self._name = f"{DEFAULT_NAME} - {entry_data[ATTR_TARIFF]}"
self._name = DEFAULT_NAME
self._tariff = entry_data[ATTR_TARIFF]
self._power = entry_data[ATTR_POWER]
self._power_p3 = entry_data[ATTR_POWER_P3]
@@ -1,6 +1,6 @@
"""Constant values for pvpc_hourly_pricing."""
from esios_api.const import TARIFFS
from aiopvpc.const import TARIFFS
import voluptuous as vol
DOMAIN = "pvpc_hourly_pricing"
@@ -3,7 +3,7 @@
from datetime import timedelta
import logging
from esios_api import BadApiTokenAuthError, EsiosApiData, PVPCData
from aiopvpc import BadApiTokenAuthError, EsiosApiData, PVPCData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN
@@ -1,6 +1,6 @@
"""Helper functions to relate sensors keys and unique ids."""
from esios_api.const import (
from aiopvpc.const import (
ALL_SENSORS,
KEY_INJECTION,
KEY_MAG,
@@ -1,11 +1,11 @@
{
"domain": "pvpc_hourly_pricing",
"name": "Spain electricity hourly pricing (PVPC)",
"codeowners": ["@azogue", "@chiro79"],
"codeowners": ["@azogue"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["esios_api"],
"requirements": ["esios_api==4.4.0"]
"loggers": ["aiopvpc"],
"requirements": ["aiopvpc==4.3.1"]
}
@@ -5,7 +5,7 @@ from datetime import datetime
import logging
from typing import Any
from esios_api.const import KEY_INJECTION, KEY_MAG, KEY_OMIE, KEY_PVPC
from aiopvpc.const import KEY_INJECTION, KEY_MAG, KEY_OMIE, KEY_PVPC
from homeassistant.components.sensor import (
SensorEntity,
@@ -4,7 +4,7 @@ rules:
brands: done
dependency-transparency: done
common-modules: done
has-entity-name: todo
has-entity-name: done
action-setup:
status: done
comment: |
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.5.10"]
"requirements": ["renault-api==0.5.9"]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/risco",
"iot_class": "local_push",
"loggers": ["pyrisco"],
"requirements": ["pyrisco==0.7.0"]
"requirements": ["pyrisco==0.6.8"]
}
@@ -1,7 +1,7 @@
{
"domain": "samsungtv",
"name": "Samsung Smart TV",
"codeowners": ["@chemelli74"],
"codeowners": ["@chemelli74", "@epenet"],
"config_flow": true,
"dependencies": ["ssdp"],
"dhcp": [
@@ -401,6 +401,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
"""Fetch data."""
if self.sleep_period:
# Sleeping device, no point polling it, just mark it unavailable
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error_sleeping_device",
@@ -670,6 +671,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.sleep_period:
# Sleeping device, no point polling it, just mark it unavailable
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error_sleeping_device",
+10 -43
View File
@@ -2,7 +2,6 @@
import base64
import binascii
import contextlib
from dataclasses import dataclass
import datetime
import hashlib
@@ -39,15 +38,6 @@ from .utils import get_device_entry_gen
CONTENT_TYPE_AUDIO = "audio"
CONTENT_TYPE_RADIO = "radio"
ALLOWED_IMAGE_MIME_TYPES: Final = frozenset(
{
"image/gif",
"image/jpeg",
"image/png",
"image/webp",
}
)
PARALLEL_UPDATES = 0
@@ -112,9 +102,6 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
_last_media_position: int | None = None
_last_media_position_updated_at: datetime.datetime | None = None
_cached_thumb: str | None = None
_cached_thumb_result: tuple[bytes, str] | None = None
def __init__(
self,
coordinator: ShellyRpcCoordinator,
@@ -228,11 +215,9 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
@property
def media_image_hash(self) -> str | None:
"""Hash value for media image."""
thumb = self._media_meta.get("thumb")
if not thumb or self._decode_image_data(thumb) is None:
return super().media_image_hash
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
if (thumb := self._media_meta.get("thumb")) and thumb.startswith("data"):
return hashlib.sha256(thumb.encode("utf-8")).hexdigest()[:16]
return super().media_image_hash
def _get_updated_media_position(self) -> int | None:
"""Return the current playback position and update its timestamp."""
@@ -250,11 +235,15 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
"""Fetch media image of current playing track."""
thumb = self._media_meta.get("thumb")
if not thumb or (result := self._decode_image_data(thumb)) is None:
thumb = self._media_meta["thumb"]
try:
prefix, image_data = thumb.split(",", 1)
image = base64.b64decode(image_data, validate=True)
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
except binascii.Error, ValueError:
return await super().async_get_media_image()
return result
return image, mime
@rpc_call
async def async_media_play(self) -> None:
@@ -445,25 +434,3 @@ class ShellyRpcMediaPlayer(ShellyRpcAttributeEntity, MediaPlayerEntity):
translation_key="unsupported_media_type",
translation_placeholders={"media_type": str(media_type)},
)
def _decode_image_data(self, thumb: str) -> tuple[bytes, str] | None:
"""Return image_bytes and mime_type for a valid image data or None."""
if thumb == self._cached_thumb:
return self._cached_thumb_result
result: tuple[bytes, str] | None = None
if thumb.startswith("data"):
try:
prefix, image_data = thumb.split(",", 1)
mime = prefix.split(";", 1)[0].rsplit(":", 1)[-1]
except IndexError, ValueError:
pass
else:
if mime in ALLOWED_IMAGE_MIME_TYPES:
with contextlib.suppress(binascii.Error):
result = base64.b64decode(image_data, validate=True), mime
self._cached_thumb = thumb
self._cached_thumb_result = result
return result
+1 -1
View File
@@ -673,7 +673,7 @@
"message": "An error occurred while reconnecting to {device}"
},
"update_error_sleeping_device": {
"message": "Sleeping device {device} did not update within {period} seconds interval"
"message": "Sleeping device did not update within {period} seconds interval"
}
},
"issues": {
+5 -1
View File
@@ -344,10 +344,14 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
translation_placeholders={"device": self.coordinator.name},
) from err
except RpcCallError as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="ota_update_rpc_error",
translation_placeholders={"device": self.coordinator.name},
translation_placeholders={
"entity": self.entity_id,
"device": self.coordinator.name,
},
) from err
except InvalidAuthError:
await self.coordinator.async_shutdown_device_and_start_reauth()
@@ -69,14 +69,18 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
SmaConnectionException,
) as err:
await self.async_close_sma_session()
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"error": repr(err)},
) from err
except SmaAuthenticationException as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
async def _async_update_data(self) -> SMACoordinatorData:
@@ -87,14 +91,18 @@ class SMADataUpdateCoordinator(DataUpdateCoordinator[SMACoordinatorData]):
SmaReadException,
SmaConnectionException,
) as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"error": repr(err)},
) from err
except SmaAuthenticationException as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": repr(err)},
) from err
return SMACoordinatorData(
+2 -2
View File
@@ -69,10 +69,10 @@
},
"exceptions": {
"cannot_connect": {
"message": "Could not connect to SMA device"
"message": "Could not connect to SMA device - {error}"
},
"invalid_auth": {
"message": "Invalid authentication for SMA device"
"message": "Invalid authentication for SMA device - {error}"
}
},
"selector": {
@@ -25,8 +25,6 @@ async def async_setup_entry(
class SmartThingsScene(Scene):
"""Define a SmartThings scene."""
_attr_has_entity_name = True
def __init__(self, scene: STScene, client: SmartThings) -> None:
"""Init the scene class."""
self.client = client
@@ -394,7 +394,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
value_fn=dt_util.parse_datetime,
)
],
},
@@ -447,7 +447,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
value_fn=dt_util.parse_datetime,
)
],
},
@@ -565,7 +565,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.GAS_METER_TIME,
translation_key="gas_meter_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
value_fn=dt_util.parse_datetime,
)
],
Attribute.GAS_METER_VOLUME: [
@@ -726,7 +726,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
value_fn=dt_util.parse_datetime,
component_fn=lambda component: component == "cavity-01",
component_translation_key={
"cavity-01": "oven_completion_time_cavity_01",
@@ -1196,7 +1196,7 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.COMPLETION_TIME,
translation_key="completion_time",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda value: dt_util.parse_datetime(value) if value else None,
value_fn=dt_util.parse_datetime,
component_fn=lambda component: component == "sub",
component_translation_key={
"sub": "washer_sub_completion_time",
+2
View File
@@ -9,6 +9,8 @@ ATTR_HTML: Final = "html"
ATTR_SENDER_NAME: Final = "sender_name"
CONF_ENCRYPTION: Final = "encryption"
# pylint: disable-next=home-assistant-duplicate-const
CONF_DEBUG: Final = "debug"
CONF_SERVER: Final = "server"
CONF_SENDER_NAME: Final = "sender_name"
+1 -1
View File
@@ -24,7 +24,6 @@ from homeassistant.components.notify import (
BaseNotificationService,
)
from homeassistant.const import (
CONF_DEBUG,
CONF_PASSWORD,
CONF_PORT,
CONF_RECIPIENT,
@@ -45,6 +44,7 @@ from homeassistant.util.ssl import create_client_context
from .const import (
ATTR_HTML,
ATTR_IMAGES,
CONF_DEBUG,
CONF_ENCRYPTION,
CONF_SENDER_NAME,
CONF_SERVER,
-1
View File
@@ -195,7 +195,6 @@ class SonosFavoritesEntity(SensorEntity):
"""Representation of a Sonos favorites info entity."""
_attr_entity_registry_enabled_default = False
_attr_has_entity_name = True
_attr_name = "Sonos favorites"
_attr_translation_key = "favorites"
_attr_native_unit_of_measurement = "items"
+2 -1
View File
@@ -11,7 +11,7 @@ from propcache.api import cached_property
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_MODE, MAX_LENGTH_STATE_STATE
from homeassistant.const import MAX_LENGTH_STATE_STATE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity, EntityDescription
@@ -23,6 +23,7 @@ from homeassistant.util.hass_dict import HassKey
from .const import (
ATTR_MAX,
ATTR_MIN,
ATTR_MODE,
ATTR_PATTERN,
ATTR_VALUE,
DOMAIN,
+2
View File
@@ -4,6 +4,8 @@ DOMAIN = "text"
ATTR_MAX = "max"
ATTR_MIN = "min"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_MODE = "mode"
ATTR_PATTERN = "pattern"
ATTR_VALUE = "value"
@@ -43,7 +43,6 @@ class OmadaClientScannerEntity(
):
"""Entity for a client connected to the Omada network."""
_attr_has_entity_name = True
_client_details: OmadaWirelessClient | None = None
def __init__(
@@ -51,16 +51,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: UnifiAccessConfigEntry)
try:
await client.authenticate()
except ApiAuthError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_failed",
translation_placeholders={"host": entry.data[CONF_HOST]},
f"Authentication failed for UniFi Access at {entry.data[CONF_HOST]}"
) from err
except ApiConnectionError as err:
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"host": entry.data[CONF_HOST]},
f"Unable to connect to UniFi Access at {entry.data[CONF_HOST]}"
) from err
coordinator = UnifiAccessCoordinator(hass, entry, client)
@@ -197,25 +197,17 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
self.client.get_emergency_status(),
)
except ApiAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="update_failed_auth",
) from err
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(f"Authentication failed: {err}") from err
except ApiConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed_connection",
) from err
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(f"Error connecting to API: {err}") from err
except ApiError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed_api",
) from err
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed(f"Error communicating with API: {err}") from err
except TimeoutError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed_timeout",
) from err
# pylint: disable-next=home-assistant-exception-not-translated
raise UpdateFailed("Timeout communicating with UniFi Access API") from err
previous_lock_rules = self.data.door_lock_rules.copy() if self.data else {}
door_lock_rules: dict[str, DoorLockRuleStatus] = {}
@@ -133,12 +133,6 @@
}
},
"exceptions": {
"auth_failed": {
"message": "Authentication failed for UniFi Access at {host}."
},
"cannot_connect": {
"message": "Unable to connect to UniFi Access at {host}."
},
"emergency_failed": {
"message": "Failed to set emergency status."
},
@@ -156,18 +150,6 @@
},
"unlock_failed": {
"message": "Failed to unlock the door."
},
"update_failed_api": {
"message": "Error communicating with the UniFi Access API."
},
"update_failed_auth": {
"message": "Authentication failed while updating data."
},
"update_failed_connection": {
"message": "Error connecting to the UniFi Access API."
},
"update_failed_timeout": {
"message": "Timeout communicating with the UniFi Access API."
}
},
"selector": {
@@ -84,10 +84,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: UFPConfigEntry) -> bool:
except NotAuthorized as err:
data_service.auth_retries += 1
if data_service.auth_retries > AUTH_RETRIES:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="entry_auth_failed",
) from err
# pylint: disable-next=home-assistant-exception-not-translated
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady from err
except (TimeoutError, ClientError, ServerDisconnectedError) as err:
raise ConfigEntryNotReady from err
@@ -105,14 +105,12 @@ def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]:
@callback
def _bad_identifier(identifier: str, err: Exception | None = None) -> NoReturn:
exc = BrowseError(
translation_domain=DOMAIN,
translation_key="unexpected_identifier",
translation_placeholders={"identifier": identifier},
)
msg = f"Unexpected identifier: {identifier}"
if err is None:
raise exc
raise exc from err
# pylint: disable-next=home-assistant-exception-not-translated
raise BrowseError(msg)
# pylint: disable-next=home-assistant-exception-not-translated
raise BrowseError(msg) from err
@callback
@@ -381,10 +379,8 @@ class ProtectMediaSource(MediaSource):
_bad_identifier(f"{data.api.bootstrap.nvr.id}:{subtype}:{event_id}", err)
if event.start is None or event.end is None:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="event_ongoing",
)
# pylint: disable-next=home-assistant-exception-not-translated
raise BrowseError("Event is still ongoing")
return await self._build_event(data, event, thumbnail_only)
@@ -794,11 +790,8 @@ class ProtectMediaSource(MediaSource):
if camera_id != "all":
camera = data.api.bootstrap.cameras.get(camera_id)
if camera is None:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="unknown_camera_id",
translation_placeholders={"camera_id": camera_id},
)
# pylint: disable-next=home-assistant-exception-not-translated
raise BrowseError(f"Unknown Camera ID: {camera_id}")
name = camera.name or camera.market_name or camera.type
is_doorbell = camera.feature_flags.is_doorbell
has_smart = camera.feature_flags.has_smart_detect
@@ -679,12 +679,6 @@
"device_not_found": {
"message": "No device found for device id: {device_id}"
},
"entry_auth_failed": {
"message": "Authentication failed, please reauthenticate"
},
"event_ongoing": {
"message": "Event is still ongoing"
},
"global_alarm_manager": {
"message": "The alarm manager on this UniFi Protect NVR is set to Global mode and cannot be controlled locally."
},
@@ -723,12 +717,6 @@
},
"stream_error": {
"message": "Error playing audio, check the logs for more details"
},
"unexpected_identifier": {
"message": "Unexpected identifier: {identifier}"
},
"unknown_camera_id": {
"message": "Unknown camera ID: {camera_id}"
}
},
"issues": {
@@ -30,9 +30,6 @@
}
},
"exceptions": {
"cannot_access_or_create_backup_path": {
"message": "Cannot access or create backup path"
},
"cannot_connect": {
"message": "Cannot connect to WebDAV server"
},
+5 -1
View File
@@ -48,10 +48,14 @@ class LgWebOSNotificationService(BaseNotificationService):
icon_path = data.get(ATTR_ICON) if data else None
if not client.tv_state.is_on:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="notify_device_off",
translation_placeholders={"name": str(self._entry.title)},
translation_placeholders={
"name": str(self._entry.title),
"func": __name__,
},
)
try:
await client.send_message(message, icon_path=icon_path)
+1 -1
View File
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["wled==0.23.0"],
"requirements": ["wled==0.22.0"],
"zeroconf": ["_wled._tcp.local."]
}
+50 -124
View File
@@ -1,6 +1,5 @@
"""Support for Wyoming intent recognition services."""
import asyncio
import logging
from typing import Any, Literal
@@ -8,7 +7,7 @@ from wyoming.asr import Transcript
from wyoming.client import AsyncTcpClient
from wyoming.handle import Handled, NotHandled
from wyoming.info import HandleProgram, IntentProgram
from wyoming.intent import Intent, IntentsStart, IntentsStop, NotRecognized
from wyoming.intent import Intent, NotRecognized
from homeassistant.components import conversation
from homeassistant.const import MATCH_ALL
@@ -87,10 +86,6 @@ class WyomingConversationEntity(
model_languages.update(handle_model.languages)
self._attr_name = self._handle_service.name
if self._handle_service.supports_home_control:
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
)
self._supported_languages = list(model_languages)
self._attr_unique_id = f"{config_entry.entry_id}-conversation"
@@ -170,27 +165,62 @@ class WyomingConversationEntity(
intent_response: intent.IntentResponse,
) -> intent.IntentResponse:
"""Process a sentence into an intent response."""
has_intents_list = False
intents: list[Intent] = []
while True:
event = await client.read_event()
if event is None:
raise WyomingError("Connection lost")
if IntentsStart.is_type(event.type):
# Multiple intents may be present
has_intents_list = True
continue
if Intent.is_type(event.type):
intents.append(Intent.from_event(event))
if not has_intents_list:
# Only one intent, no need to wait
break
# Success
recognized_intent = Intent.from_event(event)
_LOGGER.debug("Recognized intent: %s", recognized_intent)
intent_type = recognized_intent.name
intent_slots = {
e.name: {"value": e.value} for e in recognized_intent.entities
}
# Add to trace and chat log
conversation.async_conversation_trace_append(
conversation.ConversationTraceEventType.TOOL_CALL,
{
"intent_name": intent_type,
"slots": intent_slots,
},
)
tool_input = llm.ToolInput(
tool_name=intent_type,
tool_args=intent_slots,
external=True,
)
chat_log.async_add_assistant_content_without_tools(
conversation.AssistantContent(
agent_id=user_input.agent_id,
content=None,
tool_calls=[tool_input],
)
)
intent_response = await intent.async_handle(
self.hass,
DOMAIN,
intent_type,
intent_slots,
text_input=user_input.text,
language=user_input.language,
satellite_id=user_input.satellite_id,
device_id=user_input.device_id,
)
if (not intent_response.speech) and recognized_intent.text:
response_text = recognized_intent.text
if template.is_template_string(response_text):
# Render text as a template
response_text = self._render_speech_template(
response_text, intent_response, intent_slots
)
intent_response.async_set_speech(response_text)
if IntentsStop.is_type(event.type):
# End of intents list
break
if NotRecognized.is_type(event.type):
@@ -200,9 +230,6 @@ class WyomingConversationEntity(
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
not_recognized.text or "",
)
# Don't process any intents if one was not recognized
intents.clear()
break
if Handled.is_type(event.type):
@@ -220,107 +247,6 @@ class WyomingConversationEntity(
)
break
if not intents:
return intent_response
# Process recognized intents with a task group.
# If any intent fails to be handled, the rest are cancelled.
intent_responses: list[intent.IntentResponse] = []
try:
async with asyncio.TaskGroup() as task_group:
intent_tasks: list[tuple[str, dict, str | None, asyncio.Task]] = []
for recognized_intent in intents:
_LOGGER.debug("Handling intent: %s", recognized_intent)
intent_type = recognized_intent.name
intent_slots = {
e.name: {"value": e.value} for e in recognized_intent.entities
}
# Add to trace
conversation.async_conversation_trace_append(
conversation.ConversationTraceEventType.TOOL_CALL,
{
"intent_name": intent_type,
"slots": intent_slots,
},
)
intent_tasks.append(
(
intent_type,
intent_slots,
recognized_intent.text,
task_group.create_task(
intent.async_handle(
self.hass,
DOMAIN,
intent_type,
intent_slots,
text_input=user_input.text,
language=user_input.language,
satellite_id=user_input.satellite_id,
device_id=user_input.device_id,
)
),
)
)
except* intent.IntentError as err_group:
# Bubble up first exception only.
# There's nothing the caller can do with multiple intent errors.
raise err_group.exceptions[0] from err_group
# Gather intent handling results
tool_calls: list[llm.ToolInput] = []
for intent_type, intent_slots, intent_text, intent_task in intent_tasks:
intent_task_response = await intent_task
intent_responses.append(intent_task_response)
# For the chat log
tool_calls.append(
llm.ToolInput(
tool_name=intent_type,
tool_args=intent_slots,
external=True,
)
)
# Process speech
if (not intent_task_response.speech) and intent_text:
if template.is_template_string(intent_text):
# Render text as a template
intent_text = self._render_speech_template(
intent_text, intent_task_response, intent_slots
)
intent_task_response.async_set_speech(intent_text)
# Add all tool calls to the chat log
chat_log.async_add_assistant_content_without_tools(
conversation.AssistantContent(
agent_id=user_input.agent_id,
content=None,
tool_calls=tool_calls,
)
)
# Must be the case because an exception would have been thrown otherwise
assert intent_responses
# Use the properties of the first intent (response_type, etc.) and
# combine the speech results.
intent_response = intent_responses[0]
speech_texts: list[str] = [
speech
for current_response in intent_responses
if (speech := current_response.speech.get("plain", {}).get("speech"))
]
if speech_texts:
# Combine response with newlines because punctuation would be
# language-dependent.
intent_response.async_set_speech("\n".join(speech_texts))
return intent_response
def _render_speech_template(
@@ -14,9 +14,10 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ZeversolarConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data: ZeverSolarData = config_entry.runtime_data.data
return {
payload: dict[str, Any] = {
"wifi_enabled": data.wifi_enabled,
"serial_or_registry_id": data.serial_or_registry_id,
"registry_key": data.registry_key,
@@ -32,6 +33,8 @@ async def async_get_config_entry_diagnostics(
"meter_status": data.meter_status.value,
}
return payload
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: ZeversolarConfigEntry, device: DeviceEntry
@@ -39,13 +42,15 @@ async def async_get_device_diagnostics(
"""Return diagnostics for a device entry."""
coordinator = entry.runtime_data
updateInterval = (
None
if coordinator.update_interval is None
else coordinator.update_interval.total_seconds()
)
return {
"name": coordinator.name,
"always_update": coordinator.always_update,
"last_update_success": coordinator.last_update_success,
"update_interval": (
None
if coordinator.update_interval is None
else coordinator.update_interval.total_seconds()
),
"update_interval": updateInterval,
}
-1
View File
@@ -91,7 +91,6 @@ CONF_COMMAND_ON: Final = "command_on"
CONF_COMMAND_OPEN: Final = "command_open"
CONF_COMMAND_STATE: Final = "command_state"
CONF_COMMAND_STOP: Final = "command_stop"
CONF_COMMENT: Final = "comment"
CONF_CONDITION: Final = "condition"
CONF_CONDITIONS: Final = "conditions"
CONF_CONTINUE_ON_ERROR: Final = "continue_on_error"
@@ -4613,6 +4613,11 @@
"config_flow": true,
"iot_class": "local_push"
},
"national_grid_us": {
"name": "National Grid US",
"integration_type": "virtual",
"supported_by": "opower"
},
"neato": {
"name": "Neato Botvac",
"integration_type": "hub",
@@ -38,7 +38,6 @@ from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_BELOW,
CONF_CHOOSE,
CONF_COMMENT,
CONF_CONDITION,
CONF_CONDITIONS,
CONF_CONTINUE_ON_ERROR,
@@ -1459,7 +1458,6 @@ SCRIPT_SCHEMA = vol.All(ensure_list, [script_action])
SCRIPT_ACTION_BASE_SCHEMA: VolDictType = {
vol.Optional(CONF_ALIAS): string,
vol.Remove(CONF_COMMENT): str, # Is only used in frontend
vol.Optional(CONF_CONTINUE_ON_ERROR): boolean,
vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
}
@@ -1527,7 +1525,6 @@ NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any(
CONDITION_BASE_SCHEMA: VolDictType = {
vol.Optional(CONF_ALIAS): string,
vol.Remove(CONF_COMMENT): str, # Is only used in frontend
vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
}
@@ -1862,7 +1859,6 @@ TRIGGER_BASE_SCHEMA = vol.Schema(
vol.Optional(CONF_ID): str,
vol.Optional(CONF_VARIABLES): SCRIPT_VARIABLES_SCHEMA,
vol.Optional(CONF_ENABLED): vol.Any(boolean, template),
vol.Remove(CONF_COMMENT): str, # Is only used in frontend
}
)

Some files were not shown because too many files have changed in this diff Show More