Compare commits
281 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35c8fefbd6 | |||
| 470baa782e | |||
| 6e607ffa01 | |||
| f5b51c6cf0 | |||
| bfccee17ef | |||
| b6b6de24ac | |||
| 70624f72b6 | |||
| c61cd422d1 | |||
| 0b2db2510f | |||
| bb1e263149 | |||
| 8e6b9c04f6 | |||
| cf931a75a7 | |||
| 3250a2fb46 | |||
| 6235adc69a | |||
| 5d258c2f82 | |||
| cc2aca2c2c | |||
| 04bd1967a7 | |||
| a046530eaf | |||
| 631523dfaf | |||
| dc20375506 | |||
| 811f085556 | |||
| fd86a43b28 | |||
| b7f830523e | |||
| 3f752e13ff | |||
| 783102f2f6 | |||
| 8ce30d9559 | |||
| cde17fc0ca | |||
| 83ae5f52da | |||
| 1cb9767bb8 | |||
| e98fe7dc9c | |||
| 40ec51c0a3 | |||
| 40fcc3b75b | |||
| 510fd09163 | |||
| e47bdc06a0 | |||
| b3d9908cd9 | |||
| 99d63c49bb | |||
| 4be2e84ce6 | |||
| 1fc624c7a7 | |||
| 8641a2141c | |||
| 04cc451c76 | |||
| a3b03caead | |||
| 49d1d781b8 | |||
| 11c75d7ef2 | |||
| 8ef6b62d9a | |||
| b410b414ec | |||
| e5f7421703 | |||
| 8330ae2d3a | |||
| 4b162f09bd | |||
| 9c558fabcd | |||
| 5f9cc0a5f6 | |||
| bc4a322e81 | |||
| b999c5906e | |||
| d2825e1c80 | |||
| 419e4f3b1d | |||
| 4a937d2452 | |||
| 01b4a5ceed | |||
| 4e71745c62 | |||
| 6a88ee7a8f | |||
| 3c4ecffa1b | |||
| 244e0f5ea8 | |||
| a656b6e26a | |||
| 691681a78a | |||
| 3bc00824e2 | |||
| 7d36a2e3a7 | |||
| b1e3561ead | |||
| bfc814c839 | |||
| 5008151688 | |||
| d738c0d6b1 | |||
| e42235285d | |||
| 04e69479f4 | |||
| b973916032 | |||
| 6f4757ef42 | |||
| a6962e9e1e | |||
| 142c10cccc | |||
| c137c96cfd | |||
| f0e0c954e7 | |||
| 681961d3a5 | |||
| 53d2f6b0c6 | |||
| 78c39f8a06 | |||
| a748525e03 | |||
| 8ca1fe83b7 | |||
| 8968cf704b | |||
| ebe04466f4 | |||
| e31470ba5b | |||
| 4bc2951f44 | |||
| 8334a0398c | |||
| 80a1e0e4cd | |||
| 3778f537d5 | |||
| adec157d43 | |||
| 8fc3fa51a8 | |||
| 4eb688b560 | |||
| 9472ff5d36 | |||
| 12e8b81ec7 | |||
| ec5e543c09 | |||
| 116c745872 | |||
| 1fdf152292 | |||
| b816f1a408 | |||
| eb351e6505 | |||
| 2f27d55495 | |||
| fa1bed1849 | |||
| b8c19f23f3 | |||
| b677ce6c90 | |||
| d6da686ffe | |||
| f50ef79c72 | |||
| 943fb9948b | |||
| 7447cf329b | |||
| 3d27c0ce52 | |||
| b7496be61f | |||
| 57a98240bd | |||
| ff76017ba6 | |||
| f10fcde6d8 | |||
| a7002e3a24 | |||
| bbe03dcab7 | |||
| f77e6cc8fc | |||
| cb8e076703 | |||
| 73251fbb1c | |||
| 7ff90ca49d | |||
| bab9ec9976 | |||
| 1051f85ac0 | |||
| 6c7da57af2 | |||
| 73e505d48d | |||
| ec65066f5e | |||
| 9c4951261c | |||
| 00dfc04b86 | |||
| bee07ad284 | |||
| b2108fdd40 | |||
| 3730a1a379 | |||
| 088c02d38a | |||
| afb247c907 | |||
| 0e6bbb30c1 | |||
| fdba791f18 | |||
| d4dec6c7a9 | |||
| f838e85a79 | |||
| 04ae966544 | |||
| 77dcba0984 | |||
| 48f9a12cca | |||
| bdd2ac9ae4 | |||
| 2e7113d881 | |||
| 6842bfae4c | |||
| 392cde20d9 | |||
| a6146fb5a9 | |||
| b2c393db72 | |||
| 6104731d53 | |||
| 3ed440a3af | |||
| 01e7efc7b4 | |||
| 60a930554a | |||
| 66308a848a | |||
| c71dbd9d4d | |||
| 1195c2ec10 | |||
| 78a9cd9201 | |||
| 639a749a0f | |||
| 058f3b8b6e | |||
| 926e9261ab | |||
| d6fb860889 | |||
| 5e03900e0a | |||
| 1e6e5ca1b6 | |||
| 60e3b38de1 | |||
| 852522219c | |||
| 23f1e8d1a3 | |||
| c707bf6264 | |||
| 655f009f07 | |||
| 3548ab70fd | |||
| e272ab1885 | |||
| d5d1b620d0 | |||
| 8b2f4f0f86 | |||
| 725269ecda | |||
| c42fc818bf | |||
| 5554e38171 | |||
| b25acfe823 | |||
| ff25948e37 | |||
| f85fc7173f | |||
| 748cc6386d | |||
| 47b232db49 | |||
| c61935fc41 | |||
| 414318f3fb | |||
| 08985d783f | |||
| e4bcde7d20 | |||
| 59bf39f4ed | |||
| 510e3977df | |||
| 922720576a | |||
| e10b581d4b | |||
| e38eac9415 | |||
| 11c9aa9280 | |||
| 52c86f8a6a | |||
| 6364a9ad98 | |||
| 651162b8e7 | |||
| 7deca35172 | |||
| 073a467fb2 | |||
| 3f9590b03b | |||
| b47f989c77 | |||
| 4ebffa8d23 | |||
| c5873c6dd0 | |||
| 2cb80e083e | |||
| 871296dff6 | |||
| db04c77e62 | |||
| e8204e5f8e | |||
| 66cf9c4ed5 | |||
| 1f6d28dcbf | |||
| 328e838351 | |||
| 62a1c8af11 | |||
| b50e599517 | |||
| 3c7c9176d2 | |||
| c771f5fe1e | |||
| 6dc464ad73 | |||
| ae48e3716e | |||
| 1543726095 | |||
| adbace95c3 | |||
| 578b43cf61 | |||
| a8b5d1511d | |||
| 5a0a1bbbf4 | |||
| cf2e69ed74 | |||
| c32b44b774 | |||
| 2f69ed4a8a | |||
| 4b3449fe0c | |||
| 33e1c6de68 | |||
| 81e712ea49 | |||
| d3c5684cd0 | |||
| 862b7460b5 | |||
| a65eb57539 | |||
| b537850f52 | |||
| 16c6bd08f8 | |||
| 18834849c2 | |||
| e4d820799f | |||
| 013a35176a | |||
| 8230557aef | |||
| 5451063714 | |||
| 8cdc7523a4 | |||
| 77ccfbd3a9 | |||
| 4977ee4998 | |||
| 5c0f2d37f0 | |||
| 0b5d2ab8e4 | |||
| 47f3bf29dd | |||
| 62f7cbb51e | |||
| b9e2c5d34c | |||
| 1829acd0e1 | |||
| 41b9a7a9a3 | |||
| 9782637ec8 | |||
| 6bd6fa65d2 | |||
| 85343a9f53 | |||
| bc607dd013 | |||
| c2c388e0cc | |||
| 3fc154e1d7 | |||
| efb29d024e | |||
| 263823c92c | |||
| e5e6ed601b | |||
| 28dfc997f3 | |||
| f93ab8d519 | |||
| cb359da79e | |||
| 6a7385590a | |||
| c0ec987b07 | |||
| 26521f8cc0 | |||
| 4df1f702bf | |||
| c8422c9fb8 | |||
| f8207a2e0e | |||
| 9cc75f3458 | |||
| a233b6b1e3 | |||
| c7677b91da | |||
| 1f57bba9cd | |||
| 4cc10ca2e2 | |||
| 153e1e43e8 | |||
| 398dd3ae46 | |||
| 17fd850fa6 | |||
| ae062b230c | |||
| d523f85404 | |||
| f28d6582c6 | |||
| 1e81e5990e | |||
| 5fe2e4b6ed | |||
| 914bb3aa76 | |||
| cfa6746115 | |||
| 03f9caf3eb | |||
| 6b2aaf3fdb | |||
| 2c4ea0d584 | |||
| e627811f7a | |||
| 150f41641b | |||
| b9a7371996 | |||
| 7d0e99da43 | |||
| 71f281cc14 | |||
| aec812a475 | |||
| d4b548b169 | |||
| a296324c30 | |||
| cff3d3d6ac |
@@ -0,0 +1,51 @@
|
||||
name: Task
|
||||
description: For staff only - Create a task
|
||||
type: Task
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## ⚠️ RESTRICTED ACCESS
|
||||
|
||||
**This form is restricted to Open Home Foundation staff, authorized contributors, and integration code owners only.**
|
||||
|
||||
If you are a community member wanting to contribute, please:
|
||||
- For bug reports: Use the [bug report form](https://github.com/home-assistant/core/issues/new?template=bug_report.yml)
|
||||
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
|
||||
|
||||
---
|
||||
|
||||
### For authorized contributors
|
||||
|
||||
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Task description
|
||||
description: |
|
||||
Provide a clear and detailed description of the task that needs to be accomplished.
|
||||
|
||||
Be specific about what needs to be done, why it's important, and any constraints or requirements.
|
||||
placeholder: |
|
||||
Describe the task, including:
|
||||
- What needs to be done
|
||||
- Why this task is needed
|
||||
- Expected outcome
|
||||
- Any constraints or requirements
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Any additional information, links, research, or context that would be helpful.
|
||||
|
||||
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
||||
placeholder: |
|
||||
- Roadmap opportunity: [links]
|
||||
- Feature request: [link]
|
||||
- Technical design documents: [link]
|
||||
- Prototype/mockup: [link]
|
||||
validations:
|
||||
required: false
|
||||
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
CACHE_VERSION: 4
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.8"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
name: Restrict task creation
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
check-authorization:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
// First check if user is an organization member
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: 'home-assistant',
|
||||
username: issueAuthor
|
||||
});
|
||||
console.log(`✅ ${issueAuthor} is an organization member`);
|
||||
return; // Authorized, no need to check further
|
||||
} catch (error) {
|
||||
console.log(`ℹ️ ${issueAuthor} is not an organization member, checking codeowners...`);
|
||||
}
|
||||
|
||||
// If not an org member, check if they're a codeowner
|
||||
try {
|
||||
// Fetch CODEOWNERS file from the repository
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: 'CODEOWNERS',
|
||||
ref: 'dev'
|
||||
});
|
||||
|
||||
// Decode the content (it's base64 encoded)
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf-8');
|
||||
|
||||
// Check if the issue author is mentioned in CODEOWNERS
|
||||
// GitHub usernames in CODEOWNERS are prefixed with @
|
||||
if (codeownersContent.includes(`@${issueAuthor}`)) {
|
||||
console.log(`✅ ${issueAuthor} is a integration code owner`);
|
||||
return; // Authorized
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking CODEOWNERS:', error);
|
||||
}
|
||||
|
||||
// If we reach here, user is not authorized
|
||||
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
||||
|
||||
// Close the issue with a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
||||
`Task issues are restricted to Open Home Foundation staff, authorized contributors, and integration code owners.\n\n` +
|
||||
`If you would like to:\n` +
|
||||
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/core/issues/new?template=bug_report.yml)\n` +
|
||||
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
||||
`If you believe you should have access to create Task issues, please contact the maintainers.`
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Add a label to indicate this was auto-closed
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['auto-closed']
|
||||
});
|
||||
@@ -381,6 +381,7 @@ homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
homeassistant.components.opower.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
|
||||
Generated
+2
-2
@@ -452,8 +452,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
|
||||
/homeassistant/components/escea/ @lazdavila
|
||||
/tests/components/escea/ @lazdavila
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/event/ @home-assistant/core
|
||||
|
||||
+10
-17
@@ -1,15 +1,7 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# Uninstall pre-installed formatting and linting tools
|
||||
# They would conflict with our pinned versions
|
||||
RUN \
|
||||
pipx uninstall pydocstyle \
|
||||
&& pipx uninstall pycodestyle \
|
||||
&& pipx uninstall mypy \
|
||||
&& pipx uninstall pylint
|
||||
|
||||
RUN \
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
@@ -32,21 +24,18 @@ RUN \
|
||||
libxml2 \
|
||||
git \
|
||||
cmake \
|
||||
autoconf \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
||||
&& uv pip install --system -e hass-release/ \
|
||||
&& chown -R vscode /usr/src/hass-release/data
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
RUN uv python install 3.13.2
|
||||
|
||||
USER vscode
|
||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||
@@ -55,6 +44,10 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Setup hass-release
|
||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||
&& uv pip install -e ~/hass-release/
|
||||
|
||||
# Install Python dependencies from requirements
|
||||
COPY requirements.txt ./
|
||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||
@@ -65,4 +58,4 @@ RUN uv pip install -r requirements_test.txt
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
ENV SHELL /bin/bash
|
||||
ENV SHELL=/bin/bash
|
||||
|
||||
@@ -76,6 +76,7 @@ from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
condition,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
@@ -452,6 +453,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Integration to offer AI tasks to Home Assistant."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
||||
from homeassistant.core import (
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
@@ -14,12 +15,14 @@ from homeassistant.core import (
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, storage
|
||||
from homeassistant.helpers import config_validation as cv, selector, storage
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
|
||||
from .const import (
|
||||
ATTR_INSTRUCTIONS,
|
||||
ATTR_REQUIRED,
|
||||
ATTR_STRUCTURE,
|
||||
ATTR_TASK_NAME,
|
||||
DATA_COMPONENT,
|
||||
DATA_PREFERENCES,
|
||||
@@ -47,6 +50,27 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
STRUCTURE_FIELD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DESCRIPTION): str,
|
||||
vol.Optional(ATTR_REQUIRED): bool,
|
||||
vol.Required(CONF_SELECTOR): selector.validate_selector,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_structure_fields(value: dict[str, Any]) -> vol.Schema:
|
||||
"""Validate the structure fields as a voluptuous Schema."""
|
||||
if not isinstance(value, dict):
|
||||
raise vol.Invalid("Structure must be a dictionary")
|
||||
fields = {}
|
||||
for k, v in value.items():
|
||||
field_class = vol.Required if v.get(ATTR_REQUIRED, False) else vol.Optional
|
||||
fields[field_class(k, description=v.get(CONF_DESCRIPTION))] = selector.selector(
|
||||
v[CONF_SELECTOR]
|
||||
)
|
||||
return vol.Schema(fields, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Register the process service."""
|
||||
@@ -64,6 +88,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||
vol.Optional(ATTR_STRUCTURE): vol.All(
|
||||
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
||||
_validate_structure_fields,
|
||||
),
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
|
||||
@@ -21,6 +21,8 @@ SERVICE_GENERATE_DATA = "generate_data"
|
||||
|
||||
ATTR_INSTRUCTIONS: Final = "instructions"
|
||||
ATTR_TASK_NAME: Final = "task_name"
|
||||
ATTR_STRUCTURE: Final = "structure"
|
||||
ATTR_REQUIRED: Final = "required"
|
||||
|
||||
DEFAULT_SYSTEM_PROMPT = (
|
||||
"You are a Home Assistant expert and help users with their tasks."
|
||||
|
||||
@@ -17,3 +17,9 @@ generate_data:
|
||||
domain: ai_task
|
||||
supported_features:
|
||||
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||
structure:
|
||||
advanced: true
|
||||
required: false
|
||||
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
|
||||
selector:
|
||||
object:
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
|
||||
},
|
||||
"structure": {
|
||||
"name": "Structured output",
|
||||
"description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
@@ -17,6 +19,7 @@ async def async_generate_data(
|
||||
task_name: str,
|
||||
entity_id: str | None = None,
|
||||
instructions: str,
|
||||
structure: vol.Schema | None = None,
|
||||
) -> GenDataTaskResult:
|
||||
"""Run a task in the AI Task integration."""
|
||||
if entity_id is None:
|
||||
@@ -38,6 +41,7 @@ async def async_generate_data(
|
||||
GenDataTask(
|
||||
name=task_name,
|
||||
instructions=instructions,
|
||||
structure=structure,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -52,6 +56,9 @@ class GenDataTask:
|
||||
instructions: str
|
||||
"""Instructions on what needs to be done."""
|
||||
|
||||
structure: vol.Schema | None = None
|
||||
"""Optional structure for the data to be generated."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return task as a string."""
|
||||
return f"<GenDataTask {self.name}: {id(self)}>"
|
||||
|
||||
@@ -2,19 +2,45 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
api = AmazonEchoApi(
|
||||
data[CONF_COUNTRY],
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
data = await api.login_mode_interactive(data[CONF_CODE])
|
||||
finally:
|
||||
await api.close()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
@@ -25,17 +51,14 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
client = AmazonEchoApi(
|
||||
user_input[CONF_COUNTRY],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
||||
data = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except WrongCountry:
|
||||
errors["base"] = "wrong_country"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
@@ -44,8 +67,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=user_input | {CONF_LOGIN_DATA: data},
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -61,3 +82,43 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth flow."""
|
||||
self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]}
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth confirm."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
entry_data = reauth_entry.data
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data={
|
||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||
CONF_CODE: user_input[CONF_CODE],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]},
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ from aioamazondevices.exceptions import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
@@ -55,4 +55,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryError("Could not authenticate") from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.22"]
|
||||
"requirements": ["aioamazondevices==3.2.3"]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: all tests missing
|
||||
|
||||
@@ -22,17 +22,29 @@
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ SERVICE_UPLOAD = "upload"
|
||||
ANDROIDTV_STATES = {
|
||||
"off": MediaPlayerState.OFF,
|
||||
"idle": MediaPlayerState.IDLE,
|
||||
"standby": MediaPlayerState.STANDBY,
|
||||
"standby": MediaPlayerState.IDLE,
|
||||
"playing": MediaPlayerState.PLAYING,
|
||||
"paused": MediaPlayerState.PAUSED,
|
||||
}
|
||||
|
||||
@@ -5,26 +5,18 @@ from __future__ import annotations
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
|
||||
from androidtvremote2 import (
|
||||
AndroidTVRemote,
|
||||
CannotConnect,
|
||||
ConnectionClosed,
|
||||
InvalidAuth,
|
||||
)
|
||||
from androidtvremote2 import CannotConnect, ConnectionClosed, InvalidAuth
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .helpers import create_api, get_enable_ime
|
||||
from .helpers import AndroidTVRemoteConfigEntry, create_api, get_enable_ime
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
|
||||
|
||||
AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
|
||||
@@ -82,13 +74,17 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
_LOGGER.debug("async_unload_entry: %s", entry.data)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
async def async_update_options(
|
||||
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
|
||||
) -> None:
|
||||
"""Handle options update."""
|
||||
_LOGGER.debug(
|
||||
"async_update_options: data: %s options: %s", entry.data, entry.options
|
||||
|
||||
@@ -16,7 +16,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigEntry,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
@@ -33,7 +33,7 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import CONF_APP_ICON, CONF_APP_NAME, CONF_APPS, CONF_ENABLE_IME, DOMAIN
|
||||
from .helpers import create_api, get_enable_ime
|
||||
from .helpers import AndroidTVRemoteConfigEntry, create_api, get_enable_ime
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,12 +41,6 @@ APPS_NEW_ID = "NewApp"
|
||||
CONF_APP_DELETE = "app_delete"
|
||||
CONF_APP_ID = "app_id"
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("host"): str,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_PAIR_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("pin"): str,
|
||||
@@ -67,7 +61,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
"""Handle the initial and reconfigure step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self.host = user_input[CONF_HOST]
|
||||
@@ -76,15 +70,32 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await api.async_generate_cert_if_missing()
|
||||
self.name, self.mac = await api.async_get_name_and_mac()
|
||||
await self.async_set_unique_id(format_mac(self.mac))
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data={
|
||||
CONF_HOST: self.host,
|
||||
CONF_NAME: self.name,
|
||||
CONF_MAC: self.mac,
|
||||
},
|
||||
)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
||||
return await self._async_start_pair()
|
||||
except (CannotConnect, ConnectionClosed):
|
||||
# Likely invalid IP address or device is network unreachable. Stay
|
||||
# in the user step allowing the user to enter a different host.
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
user_input = {}
|
||||
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
default_host = self._get_reconfigure_entry().data[CONF_HOST]
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
step_id="reconfigure" if self.source == SOURCE_RECONFIGURE else "user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_HOST, default=default_host): str}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -217,10 +228,16 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: AndroidTVRemoteConfigEntry,
|
||||
) -> AndroidTVRemoteOptionsFlowHandler:
|
||||
"""Create the options flow."""
|
||||
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
||||
@@ -229,7 +246,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
|
||||
"""Android TV Remote options flow."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self._apps: dict[str, Any] = dict(config_entry.options.get(CONF_APPS, {}))
|
||||
self._conf_app_id: str | None = None
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
from .helpers import AndroidTVRemoteConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_HOST, CONF_MAC}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -14,6 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import CONF_APPS, DOMAIN
|
||||
from .helpers import AndroidTVRemoteConfigEntry
|
||||
|
||||
|
||||
class AndroidTVRemoteBaseEntity(Entity):
|
||||
@@ -23,7 +23,9 @@ class AndroidTVRemoteBaseEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
|
||||
def __init__(
|
||||
self, api: AndroidTVRemote, config_entry: AndroidTVRemoteConfigEntry
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._api = api
|
||||
self._host = config_entry.data[CONF_HOST]
|
||||
|
||||
@@ -10,6 +10,8 @@ from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from .const import CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE
|
||||
|
||||
AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote]
|
||||
|
||||
|
||||
def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRemote:
|
||||
"""Create an AndroidTVRemote instance."""
|
||||
@@ -23,6 +25,6 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
|
||||
)
|
||||
|
||||
|
||||
def get_enable_ime(entry: ConfigEntry) -> bool:
|
||||
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
||||
"""Get value of enable_ime option or its default value."""
|
||||
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"requirements": ["androidtvremote2==0.2.2"],
|
||||
"requirements": ["androidtvremote2==0.2.3"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed, VolumeInfo
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
@@ -20,9 +20,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
from .helpers import AndroidTVRemoteConfigEntry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -75,13 +75,11 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
else current_app
|
||||
)
|
||||
|
||||
def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None:
|
||||
def _update_volume_info(self, volume_info: VolumeInfo) -> None:
|
||||
"""Update volume info."""
|
||||
if volume_info.get("max"):
|
||||
self._attr_volume_level = int(volume_info["level"]) / int(
|
||||
volume_info["max"]
|
||||
)
|
||||
self._attr_is_volume_muted = bool(volume_info["muted"])
|
||||
self._attr_volume_level = volume_info["level"] / volume_info["max"]
|
||||
self._attr_is_volume_muted = volume_info["muted"]
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
self._attr_is_volume_muted = None
|
||||
@@ -93,7 +91,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _volume_info_updated(self, volume_info: dict[str, str | bool]) -> None:
|
||||
def _volume_info_updated(self, volume_info: VolumeInfo) -> None:
|
||||
"""Update the state when the volume info changes."""
|
||||
self._update_volume_info(volume_info)
|
||||
self.async_write_ha_state()
|
||||
@@ -102,8 +100,10 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._update_current_app(self._api.current_app)
|
||||
self._update_volume_info(self._api.volume_info)
|
||||
if self._api.current_app is not None:
|
||||
self._update_current_app(self._api.current_app)
|
||||
if self._api.volume_info is not None:
|
||||
self._update_volume_info(self._api.volume_info)
|
||||
|
||||
self._api.add_current_app_updated_callback(self._current_app_updated)
|
||||
self._api.add_volume_info_updated_callback(self._volume_info_updated)
|
||||
|
||||
@@ -20,9 +20,9 @@ from homeassistant.components.remote import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
from .const import CONF_APP_NAME
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
from .helpers import AndroidTVRemoteConfigEntry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -63,7 +63,8 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
||||
self._attr_activity_list = [
|
||||
app.get(CONF_APP_NAME, "") for app in self._apps.values()
|
||||
]
|
||||
self._update_current_app(self._api.current_app)
|
||||
if self._api.current_app is not None:
|
||||
self._update_current_app(self._api.current_app)
|
||||
self._api.add_current_app_updated_callback(self._current_app_updated)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
"description": "Enter the IP address of the Android TV you want to add to Home Assistant. It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Android TV device."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Update the IP address of this previously configured Android TV device.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Android TV device."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
@@ -16,6 +28,9 @@
|
||||
"description": "Enter the pairing code displayed on the Android TV ({name}).",
|
||||
"data": {
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
},
|
||||
"data_description": {
|
||||
"pin": "Pairing code displayed on the Android TV device."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -32,7 +47,9 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"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%]",
|
||||
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -40,7 +57,11 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"apps": "Configure applications list",
|
||||
"enable_ime": "Enable IME. Needed for getting the current app. Disable for devices that show 'Use keyboard on mobile device screen' instead of the on screen keyboard."
|
||||
"enable_ime": "Enable IME"
|
||||
},
|
||||
"data_description": {
|
||||
"apps": "Here you can define the list of applications, specify names and icons that will be displayed in the UI.",
|
||||
"enable_ime": "Enable this option to be able to get the current app name and send text as keyboard input. Disable it for devices that show 'Use keyboard on mobile device screen' instead of the on-screen keyboard."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
@@ -53,8 +74,10 @@
|
||||
"app_delete": "Check to delete this application"
|
||||
},
|
||||
"data_description": {
|
||||
"app_name": "Name of the application as you would like it to be displayed in Home Assistant.",
|
||||
"app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android",
|
||||
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename"
|
||||
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename",
|
||||
"app_delete": "Check this box to delete the application from the list."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ class AppleTvMediaPlayer(
|
||||
self._is_feature_available(FeatureName.PowerState)
|
||||
and self.atv.power.power_state == PowerState.Off
|
||||
):
|
||||
return MediaPlayerState.STANDBY
|
||||
return MediaPlayerState.OFF
|
||||
if self._playing:
|
||||
state = self._playing.device_state
|
||||
if state in (DeviceState.Idle, DeviceState.Loading):
|
||||
@@ -200,7 +200,7 @@ class AppleTvMediaPlayer(
|
||||
return MediaPlayerState.PLAYING
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return MediaPlayerState.PAUSED
|
||||
return MediaPlayerState.STANDBY # Bad or unknown state?
|
||||
return MediaPlayerState.IDLE # Bad or unknown state?
|
||||
return None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce", default=True): bool,
|
||||
vol.Optional("preannounce_media_id"): _media_id_validator,
|
||||
}
|
||||
),
|
||||
@@ -89,7 +89,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("start_message"): str,
|
||||
vol.Optional("start_media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce", default=True): bool,
|
||||
vol.Optional("preannounce_media_id"): _media_id_validator,
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
@@ -114,7 +114,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
ask_question_args = {
|
||||
"question": call.data.get("question"),
|
||||
"question_media_id": call.data.get("question_media_id"),
|
||||
"preannounce": call.data.get("preannounce", False),
|
||||
"preannounce": call.data.get("preannounce", True),
|
||||
"answers": call.data.get("answers"),
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_domain(DOMAIN),
|
||||
vol.Optional("question"): str,
|
||||
vol.Optional("question_media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce", default=True): bool,
|
||||
vol.Optional("preannounce_media_id"): _media_id_validator,
|
||||
vol.Optional("answers"): [
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"bluetooth-data-tools==1.28.2",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
|
||||
@@ -107,7 +107,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
"""Return the state of the device."""
|
||||
media_state = self.client.play_state.state
|
||||
if media_state == "NETWORK":
|
||||
return MediaPlayerState.STANDBY
|
||||
return MediaPlayerState.OFF
|
||||
if self.client.state.power:
|
||||
if media_state == "play":
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.104.0"],
|
||||
"requirements": ["hass-nabucasa==0.105.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,11 +6,18 @@ from operator import itemgetter
|
||||
import numpy as np
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
STATE_CLASSES_SCHEMA as SENSOR_STATE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@@ -50,20 +57,23 @@ def datapoints_greater_than_degree(value: dict) -> dict:
|
||||
|
||||
COMPENSATION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Required(CONF_DATAPOINTS): [
|
||||
vol.ExactSequence([vol.Coerce(float), vol.Coerce(float)])
|
||||
],
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||
vol.Optional(CONF_DEGREE, default=DEFAULT_DEGREE): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=7),
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_LOWER_LIMIT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): cv.positive_int,
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_UPPER_LIMIT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,15 +7,23 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -59,24 +67,13 @@ async def async_setup_platform(
|
||||
|
||||
source: str = conf[CONF_SOURCE]
|
||||
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
if not (name := conf.get(CONF_NAME)):
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
CompensationSensor(
|
||||
conf.get(CONF_UNIQUE_ID),
|
||||
name,
|
||||
source,
|
||||
attribute,
|
||||
conf[CONF_PRECISION],
|
||||
conf[CONF_POLYNOMIAL],
|
||||
conf.get(CONF_UNIT_OF_MEASUREMENT),
|
||||
conf[CONF_MINIMUM],
|
||||
conf[CONF_MAXIMUM],
|
||||
)
|
||||
]
|
||||
[CompensationSensor(conf.get(CONF_UNIQUE_ID), name, source, attribute, conf)]
|
||||
)
|
||||
|
||||
|
||||
@@ -91,23 +88,27 @@ class CompensationSensor(SensorEntity):
|
||||
name: str,
|
||||
source: str,
|
||||
attribute: str | None,
|
||||
precision: int,
|
||||
polynomial: np.poly1d,
|
||||
unit_of_measurement: str | None,
|
||||
minimum: tuple[float, float] | None,
|
||||
maximum: tuple[float, float] | None,
|
||||
config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize the Compensation sensor."""
|
||||
|
||||
self._attr_name = name
|
||||
self._source_entity_id = source
|
||||
self._precision = precision
|
||||
self._source_attribute = attribute
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
|
||||
self._precision = config[CONF_PRECISION]
|
||||
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
|
||||
self._poly = polynomial
|
||||
self._coefficients = polynomial.coefficients.tolist()
|
||||
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_name = name
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._minimum = config[CONF_MINIMUM]
|
||||
self._maximum = config[CONF_MAXIMUM]
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_state_class = config.get(CONF_STATE_CLASS)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle added to Hass."""
|
||||
@@ -137,13 +138,40 @@ class CompensationSensor(SensorEntity):
|
||||
"""Handle sensor state changes."""
|
||||
new_state: State | None
|
||||
if (new_state := event.data["new_state"]) is None:
|
||||
_LOGGER.warning(
|
||||
"While updating compensation %s, the new_state is None", self.name
|
||||
)
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if new_state.state == STATE_UNAVAILABLE:
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_available = True
|
||||
|
||||
if self.native_unit_of_measurement is None and self._source_attribute is None:
|
||||
self._attr_native_unit_of_measurement = new_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
|
||||
if self._attr_device_class is None and (
|
||||
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
|
||||
):
|
||||
self._attr_device_class = device_class
|
||||
|
||||
if self._attr_state_class is None and (
|
||||
state_class := new_state.attributes.get(ATTR_STATE_CLASS)
|
||||
):
|
||||
self._attr_state_class = state_class
|
||||
|
||||
if self._source_attribute:
|
||||
value = new_state.attributes.get(self._source_attribute)
|
||||
else:
|
||||
|
||||
@@ -19,10 +19,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
SUPPORT_MINIMAL_SERVICES = VacuumEntityFeature.TURN_ON | VacuumEntityFeature.TURN_OFF
|
||||
|
||||
SUPPORT_BASIC_SERVICES = (
|
||||
VacuumEntityFeature.STATE
|
||||
| VacuumEntityFeature.START
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.BATTERY
|
||||
VacuumEntityFeature.STATE | VacuumEntityFeature.START | VacuumEntityFeature.STOP
|
||||
)
|
||||
|
||||
SUPPORT_MOST_SERVICES = (
|
||||
@@ -31,7 +28,6 @@ SUPPORT_MOST_SERVICES = (
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.FAN_SPEED
|
||||
)
|
||||
|
||||
@@ -46,7 +42,6 @@ SUPPORT_ALL_SERVICES = (
|
||||
| VacuumEntityFeature.SEND_COMMAND
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.STATUS
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.MAP
|
||||
| VacuumEntityFeature.CLEAN_SPOT
|
||||
@@ -90,12 +85,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
self._attr_activity = VacuumActivity.DOCKED
|
||||
self._fan_speed = FAN_SPEEDS[1]
|
||||
self._cleaned_area: float = 0
|
||||
self._battery_level = 100
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int:
|
||||
"""Return the current battery level of the vacuum."""
|
||||
return max(0, min(100, self._battery_level))
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str:
|
||||
@@ -117,7 +106,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
if self._attr_activity != VacuumActivity.CLEANING:
|
||||
self._attr_activity = VacuumActivity.CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def pause(self) -> None:
|
||||
@@ -142,7 +130,6 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||
"""Perform a spot clean-up."""
|
||||
self._attr_activity = VacuumActivity.CLEANING
|
||||
self._cleaned_area += 1.32
|
||||
self._battery_level -= 1
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
|
||||
@@ -94,6 +94,7 @@ async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict:
|
||||
max=6,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
unit_of_measurement="decimals",
|
||||
translation_key="round",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_TIME_WINDOW): selector.DurationSelector(),
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
"h": "Hours",
|
||||
"d": "Days"
|
||||
}
|
||||
},
|
||||
"round": {
|
||||
"unit_of_measurement": {
|
||||
"decimals": "decimals"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.2.1"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.hub import EheimDigitalHub
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -126,3 +131,52 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the config entry."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_RECONFIGURE, data_schema=CONFIG_SCHEMA
|
||||
)
|
||||
|
||||
self._async_abort_entries_match(user_input)
|
||||
errors: dict[str, str] = {}
|
||||
hub = EheimDigitalHub(
|
||||
host=user_input[CONF_HOST],
|
||||
session=async_get_clientsession(self.hass),
|
||||
loop=self.hass.loop,
|
||||
main_device_added_event=self.main_device_added_event,
|
||||
)
|
||||
|
||||
try:
|
||||
await hub.connect()
|
||||
|
||||
async with asyncio.timeout(2):
|
||||
# This event gets triggered when the first message is received from
|
||||
# the device, it contains the data necessary to create the main device.
|
||||
# This removes the race condition where the main device is accessed
|
||||
# before the response from the device is parsed.
|
||||
await self.main_device_added_event.wait()
|
||||
if TYPE_CHECKING:
|
||||
# At this point the main device is always set
|
||||
assert isinstance(hub.main, EheimDigitalDevice)
|
||||
await self.async_set_unique_id(hub.main.mac_address)
|
||||
await hub.close()
|
||||
except (ClientError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
LOGGER.exception("Unknown exception occurred")
|
||||
else:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_RECONFIGURE,
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["eheimdigital==1.2.0"],
|
||||
"requirements": ["eheimdigital==1.3.0"],
|
||||
"zeroconf": [
|
||||
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -60,7 +60,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"discovery_confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::eheimdigital::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
@@ -15,7 +23,9 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "The identifier does not match the previous identifier"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
|
||||
@@ -63,6 +63,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) ->
|
||||
coordinator = entry.runtime_data
|
||||
coordinator.async_cancel_token_refresh()
|
||||
coordinator.async_cancel_firmware_refresh()
|
||||
coordinator.async_cancel_mac_verification()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity):
|
||||
name=f"Encharge {serial_number}",
|
||||
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -158,6 +159,7 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity):
|
||||
name=f"Enpower {enpower.serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=enpower.serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.1.0"],
|
||||
"requirements": ["pyenphase==2.2.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -165,6 +165,7 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign numbers to Envoy itself
|
||||
|
||||
@@ -223,6 +223,7 @@ class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign selects to Envoy itself
|
||||
|
||||
@@ -1313,6 +1313,7 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||
manufacturer="Enphase",
|
||||
model="Inverter",
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1356,6 +1357,7 @@ class EnvoyEnchargeEntity(EnvoySensorBaseEntity):
|
||||
name=f"Encharge {serial_number}",
|
||||
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
|
||||
@@ -1420,6 +1422,7 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
|
||||
name=f"Enpower {enpower_data.serial_number}",
|
||||
sw_version=str(enpower_data.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=enpower_data.serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -138,6 +138,7 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -235,6 +236,7 @@ class EnvoyStorageSettingsSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign switches to Envoy itself
|
||||
|
||||
@@ -281,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
_has_state: bool = False
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "esphome",
|
||||
"name": "ESPHome",
|
||||
"after_dependencies": ["hassio", "zeroconf", "tag"],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz", "@kbx81", "@bdraco"],
|
||||
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["assist_pipeline", "bluetooth", "intent", "ffmpeg", "http"],
|
||||
"dhcp": [
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==33.1.1",
|
||||
"aioesphomeapi==34.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250627.0"]
|
||||
"requirements": ["home-assistant-frontend==20250702.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["av==13.1.0", "Pillow==11.2.1"]
|
||||
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
AUTH_CALLBACK_PATH,
|
||||
MY_AUTH_CALLBACK_PATH,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
@@ -14,12 +18,14 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
if "my" in hass.config.components:
|
||||
redirect_url = MY_AUTH_CALLBACK_PATH
|
||||
else:
|
||||
ha_host = hass.config.external_url or "https://YOUR_DOMAIN:PORT"
|
||||
redirect_url = f"{ha_host}{AUTH_CALLBACK_PATH}"
|
||||
return {
|
||||
"oauth_consent_url": (
|
||||
"https://console.cloud.google.com/apis/credentials/consent"
|
||||
),
|
||||
"more_info_url": (
|
||||
"https://www.home-assistant.io/integrations/google_assistant_sdk/"
|
||||
),
|
||||
"oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
|
||||
"more_info_url": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
|
||||
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
|
||||
"redirect_url": redirect_url,
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
},
|
||||
"application_credentials": {
|
||||
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type."
|
||||
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n1. Add `{redirect_url}` under *Authorized redirect URI*."
|
||||
},
|
||||
"services": {
|
||||
"send_text_command": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType
|
||||
@@ -37,11 +38,13 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_PROMPT,
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
DEFAULT_TITLE,
|
||||
DEFAULT_TTS_NAME,
|
||||
DOMAIN,
|
||||
FILE_POLLING_INTERVAL_SECONDS,
|
||||
LOGGER,
|
||||
RECOMMENDED_AI_TASK_OPTIONS,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
RECOMMENDED_TTS_OPTIONS,
|
||||
TIMEOUT_MILLIS,
|
||||
@@ -53,6 +56,7 @@ CONF_FILENAMES = "filenames"
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = (
|
||||
Platform.AI_TASK,
|
||||
Platform.CONVERSATION,
|
||||
Platform.TTS,
|
||||
)
|
||||
@@ -187,11 +191,9 @@ async def async_setup_entry(
|
||||
"""Set up Google Generative AI Conversation from a config entry."""
|
||||
|
||||
try:
|
||||
|
||||
def _init_client() -> Client:
|
||||
return Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
client = await hass.async_add_executor_job(_init_client)
|
||||
client = await hass.async_add_executor_job(
|
||||
partial(Client, api_key=entry.data[CONF_API_KEY])
|
||||
)
|
||||
await client.aio.models.get(
|
||||
model=RECOMMENDED_CHAT_MODEL,
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
@@ -350,6 +352,19 @@ async def async_migrate_entry(
|
||||
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
# Add AI Task subentry with default options
|
||||
hass.config_entries.async_add_subentry(
|
||||
entry,
|
||||
ConfigSubentry(
|
||||
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
|
||||
subentry_type="ai_task_data",
|
||||
title=DEFAULT_AI_TASK_NAME,
|
||||
unique_id=None,
|
||||
),
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""AI Task integration for Google Generative AI Conversation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import ai_task, conversation
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import LOGGER
|
||||
from .entity import ERROR_GETTING_RESPONSE, GoogleGenerativeAILLMBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up AI Task entities."""
|
||||
for subentry in config_entry.subentries.values():
|
||||
if subentry.subentry_type != "ai_task_data":
|
||||
continue
|
||||
|
||||
async_add_entities(
|
||||
[GoogleGenerativeAITaskEntity(config_entry, subentry)],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class GoogleGenerativeAITaskEntity(
|
||||
ai_task.AITaskEntity,
|
||||
GoogleGenerativeAILLMBaseEntity,
|
||||
):
|
||||
"""Google Generative AI AI Task entity."""
|
||||
|
||||
_attr_supported_features = ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||
|
||||
async def _async_generate_data(
|
||||
self,
|
||||
task: ai_task.GenDataTask,
|
||||
chat_log: conversation.ChatLog,
|
||||
) -> ai_task.GenDataTaskResult:
|
||||
"""Handle a generate data task."""
|
||||
await self._async_handle_chat_log(chat_log)
|
||||
|
||||
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
|
||||
LOGGER.error(
|
||||
"Last content in chat log is not an AssistantContent: %s. This could be due to the model not returning a valid response",
|
||||
chat_log.content[-1],
|
||||
)
|
||||
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||
|
||||
return ai_task.GenDataTaskResult(
|
||||
conversation_id=chat_log.conversation_id,
|
||||
data=chat_log.content[-1].content or "",
|
||||
)
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -46,10 +47,12 @@ from .const import (
|
||||
CONF_TOP_K,
|
||||
CONF_TOP_P,
|
||||
CONF_USE_GOOGLE_SEARCH_TOOL,
|
||||
DEFAULT_AI_TASK_NAME,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DEFAULT_TITLE,
|
||||
DEFAULT_TTS_NAME,
|
||||
DOMAIN,
|
||||
RECOMMENDED_AI_TASK_OPTIONS,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
RECOMMENDED_CONVERSATION_OPTIONS,
|
||||
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
@@ -72,12 +75,14 @@ STEP_API_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(data: dict[str, Any]) -> None:
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
client = genai.Client(api_key=data[CONF_API_KEY])
|
||||
client = await hass.async_add_executor_job(
|
||||
partial(genai.Client, api_key=data[CONF_API_KEY])
|
||||
)
|
||||
await client.aio.models.list(
|
||||
config={
|
||||
"http_options": {
|
||||
@@ -92,7 +97,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Google Generative AI Conversation."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
async def async_step_api(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -102,7 +107,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(user_input)
|
||||
await validate_input(self.hass, user_input)
|
||||
except (APIError, Timeout) as err:
|
||||
if isinstance(err, ClientError) and "API_KEY_INVALID" in str(err):
|
||||
errors["base"] = "invalid_auth"
|
||||
@@ -133,6 +138,12 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"title": DEFAULT_TTS_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
{
|
||||
"subentry_type": "ai_task_data",
|
||||
"data": RECOMMENDED_AI_TASK_OPTIONS,
|
||||
"title": DEFAULT_AI_TASK_NAME,
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
return self.async_show_form(
|
||||
@@ -181,6 +192,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return {
|
||||
"conversation": LLMSubentryFlowHandler,
|
||||
"tts": LLMSubentryFlowHandler,
|
||||
"ai_task_data": LLMSubentryFlowHandler,
|
||||
}
|
||||
|
||||
|
||||
@@ -214,6 +226,8 @@ class LLMSubentryFlowHandler(ConfigSubentryFlow):
|
||||
options: dict[str, Any]
|
||||
if self._subentry_type == "tts":
|
||||
options = RECOMMENDED_TTS_OPTIONS.copy()
|
||||
elif self._subentry_type == "ai_task_data":
|
||||
options = RECOMMENDED_AI_TASK_OPTIONS.copy()
|
||||
else:
|
||||
options = RECOMMENDED_CONVERSATION_OPTIONS.copy()
|
||||
else:
|
||||
@@ -288,6 +302,8 @@ async def google_generative_ai_config_option_schema(
|
||||
default_name = options[CONF_NAME]
|
||||
elif subentry_type == "tts":
|
||||
default_name = DEFAULT_TTS_NAME
|
||||
elif subentry_type == "ai_task_data":
|
||||
default_name = DEFAULT_AI_TASK_NAME
|
||||
else:
|
||||
default_name = DEFAULT_CONVERSATION_NAME
|
||||
schema: dict[vol.Required | vol.Optional, Any] = {
|
||||
@@ -315,6 +331,7 @@ async def google_generative_ai_config_option_schema(
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -330,13 +347,14 @@ async def google_generative_ai_config_option_schema(
|
||||
api_models = [api_model async for api_model in api_models_pager]
|
||||
models = [
|
||||
SelectOptionDict(
|
||||
label=api_model.display_name,
|
||||
label=api_model.name.lstrip("models/"),
|
||||
value=api_model.name,
|
||||
)
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
|
||||
for api_model in sorted(
|
||||
api_models, key=lambda x: x.name.lstrip("models/") or ""
|
||||
)
|
||||
if (
|
||||
api_model.display_name
|
||||
and api_model.name
|
||||
api_model.name
|
||||
and ("tts" in api_model.name) == (subentry_type == "tts")
|
||||
and "vision" not in api_model.name
|
||||
and api_model.supported_actions
|
||||
@@ -442,4 +460,5 @@ async def google_generative_ai_config_option_schema(
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
return schema
|
||||
|
||||
@@ -12,6 +12,7 @@ CONF_PROMPT = "prompt"
|
||||
|
||||
DEFAULT_CONVERSATION_NAME = "Google AI Conversation"
|
||||
DEFAULT_TTS_NAME = "Google AI TTS"
|
||||
DEFAULT_AI_TASK_NAME = "Google AI Task"
|
||||
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_CHAT_MODEL = "chat_model"
|
||||
@@ -35,6 +36,7 @@ RECOMMENDED_USE_GOOGLE_SEARCH_TOOL = False
|
||||
|
||||
TIMEOUT_MILLIS = 10000
|
||||
FILE_POLLING_INTERVAL_SECONDS = 0.05
|
||||
|
||||
RECOMMENDED_CONVERSATION_OPTIONS = {
|
||||
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
|
||||
CONF_LLM_HASS_API: [llm.LLM_API_ASSIST],
|
||||
@@ -44,3 +46,7 @@ RECOMMENDED_CONVERSATION_OPTIONS = {
|
||||
RECOMMENDED_TTS_OPTIONS = {
|
||||
CONF_RECOMMENDED: True,
|
||||
}
|
||||
|
||||
RECOMMENDED_AI_TASK_OPTIONS = {
|
||||
CONF_RECOMMENDED: True,
|
||||
}
|
||||
|
||||
@@ -88,6 +88,34 @@
|
||||
"entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
}
|
||||
},
|
||||
"ai_task_data": {
|
||||
"initiate_flow": {
|
||||
"user": "Add Generate data with AI service",
|
||||
"reconfigure": "Reconfigure Generate data with AI service"
|
||||
},
|
||||
"entry_type": "Generate data with AI service",
|
||||
"step": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"recommended": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::recommended%]",
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"temperature": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::temperature%]",
|
||||
"top_p": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::top_p%]",
|
||||
"top_k": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::top_k%]",
|
||||
"max_tokens": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::max_tokens%]",
|
||||
"harassment_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::harassment_block_threshold%]",
|
||||
"hate_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::hate_block_threshold%]",
|
||||
"sexual_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::sexual_block_threshold%]",
|
||||
"dangerous_block_threshold": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::step::set_options::data::dangerous_block_threshold%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"entry_not_loaded": "[%key:component::google_generative_ai_conversation::config_subentries::conversation::abort::entry_not_loaded%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -95,21 +95,16 @@ def get_recurrence_rule(recurrence: rrule) -> str:
|
||||
|
||||
'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
|
||||
|
||||
Parameters
|
||||
----------
|
||||
recurrence : rrule
|
||||
An RRULE object.
|
||||
Args:
|
||||
recurrence: An RRULE object.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Returns:
|
||||
The recurrence rule portion of the RRULE string, starting with 'FREQ='.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> rule = get_recurrence_rule(task)
|
||||
>>> print(rule)
|
||||
'FREQ=YEARLY;INTERVAL=2'
|
||||
Example:
|
||||
>>> rule = get_recurrence_rule(task)
|
||||
>>> print(rule)
|
||||
'FREQ=YEARLY;INTERVAL=2'
|
||||
|
||||
"""
|
||||
return str(recurrence).split("RRULE:")[1]
|
||||
|
||||
@@ -11,6 +11,7 @@ from urllib.parse import quote
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import ClientTimeout, ClientWebSocketResponse, hdrs, web
|
||||
from aiohttp.helpers import must_be_empty_body
|
||||
from aiohttp.web_exceptions import HTTPBadGateway, HTTPBadRequest
|
||||
from multidict import CIMultiDict
|
||||
from yarl import URL
|
||||
@@ -184,13 +185,16 @@ class HassIOIngress(HomeAssistantView):
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
# Simple request
|
||||
if result.status in (204, 304) or (
|
||||
if (empty_body := must_be_empty_body(result.method, result.status)) or (
|
||||
content_length is not UNDEFINED
|
||||
and (content_length_int := int(content_length))
|
||||
<= MAX_SIMPLE_RESPONSE_SIZE
|
||||
):
|
||||
# Return Response
|
||||
body = await result.read()
|
||||
if empty_body:
|
||||
body = None
|
||||
else:
|
||||
body = await result.read()
|
||||
simple_response = web.Response(
|
||||
headers=headers,
|
||||
status=result.status,
|
||||
|
||||
@@ -64,7 +64,7 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N
|
||||
"""Initialize bans when app starts up."""
|
||||
await app[KEY_BAN_MANAGER].async_load()
|
||||
|
||||
app.on_startup.append(ban_startup)
|
||||
app.on_startup.append(ban_startup) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@middleware
|
||||
|
||||
@@ -63,8 +63,8 @@ from .utils import get_device_macs, non_verifying_requests_session
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Huawei LTE config flow."""
|
||||
class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Huawei LTE config flow."""
|
||||
|
||||
VERSION = 3
|
||||
|
||||
@@ -75,9 +75,9 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> OptionsFlowHandler:
|
||||
) -> HuaweiLteOptionsFlow:
|
||||
"""Get options flow."""
|
||||
return OptionsFlowHandler()
|
||||
return HuaweiLteOptionsFlow()
|
||||
|
||||
async def _async_show_user_form(
|
||||
self,
|
||||
@@ -354,7 +354,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_update_reload_and_abort(entry, data=new_data)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
class HuaweiLteOptionsFlow(OptionsFlow):
|
||||
"""Huawei LTE options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -73,7 +73,6 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
|
||||
schedule = self.mower_attributes.calendar
|
||||
cursor = schedule.timeline.active_after(dt_util.now())
|
||||
program_event = next(cursor, None)
|
||||
_LOGGER.debug("program_event %s", program_event)
|
||||
if not program_event:
|
||||
return None
|
||||
work_area_name = None
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
"binary_sensor": {
|
||||
"leaving_dock": {
|
||||
"default": "mdi:debug-step-out"
|
||||
},
|
||||
"returning_to_dock": {
|
||||
"default": "mdi:debug-step-into"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -48,6 +45,26 @@
|
||||
"work_area_progress": {
|
||||
"default": "mdi:collage"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"my_lawn_work_area": {
|
||||
"default": "mdi:square-outline",
|
||||
"state": {
|
||||
"on": "mdi:square"
|
||||
}
|
||||
},
|
||||
"work_area_work_area": {
|
||||
"default": "mdi:square-outline",
|
||||
"state": {
|
||||
"on": "mdi:square"
|
||||
}
|
||||
},
|
||||
"stay_out_zones": {
|
||||
"default": "mdi:rhombus-outline",
|
||||
"state": {
|
||||
"on": "mdi:rhombus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==1.0.1"]
|
||||
"requirements": ["aioautomower==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.6.0"]
|
||||
"requirements": ["pydrawise==2025.7.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==11.2.1"]
|
||||
"requirements": ["Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -209,6 +209,12 @@
|
||||
"state": {
|
||||
"off": "mdi:card-bulleted-off-outline"
|
||||
}
|
||||
},
|
||||
"boost": {
|
||||
"default": "mdi:thermometer-high",
|
||||
"state": {
|
||||
"off": "mdi:thermometer-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,6 +464,16 @@ class IronOSTemperatureNumberEntity(IronOSNumberEntity):
|
||||
else super().native_max_value
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
if (
|
||||
self.entity_description.key is PinecilNumber.BOOST_TEMP
|
||||
and self.native_value == 0
|
||||
):
|
||||
return False
|
||||
return super().available
|
||||
|
||||
|
||||
class IronOSSetpointNumberEntity(IronOSTemperatureNumberEntity):
|
||||
"""IronOS setpoint temperature entity."""
|
||||
|
||||
@@ -278,6 +278,9 @@
|
||||
},
|
||||
"calibrate_cjc": {
|
||||
"name": "Calibrate CJC"
|
||||
},
|
||||
"boost": {
|
||||
"name": "Boost"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pynecil import CharSetting, SettingsDataResponse
|
||||
from pynecil import CharSetting, SettingsDataResponse, TempUnit
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import IronOSConfigEntry
|
||||
from .const import MIN_BOOST_TEMP, MIN_BOOST_TEMP_F
|
||||
from .coordinator import IronOSCoordinators
|
||||
from .entity import IronOSBaseEntity
|
||||
|
||||
@@ -39,6 +40,7 @@ class IronOSSwitch(StrEnum):
|
||||
INVERT_BUTTONS = "invert_buttons"
|
||||
DISPLAY_INVERT = "display_invert"
|
||||
CALIBRATE_CJC = "calibrate_cjc"
|
||||
BOOST = "boost"
|
||||
|
||||
|
||||
SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
@@ -94,6 +96,13 @@ SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
IronOSSwitchEntityDescription(
|
||||
key=IronOSSwitch.BOOST,
|
||||
translation_key=IronOSSwitch.BOOST,
|
||||
characteristic=CharSetting.BOOST_TEMP,
|
||||
is_on_fn=lambda x: bool(x.get("boost_temp")),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -136,7 +145,15 @@ class IronOSSwitchEntity(IronOSBaseEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
if self.entity_description.key is IronOSSwitch.BOOST:
|
||||
await self.settings.write(
|
||||
self.entity_description.characteristic,
|
||||
MIN_BOOST_TEMP_F
|
||||
if self.settings.data.get("temp_unit") is TempUnit.FAHRENHEIT
|
||||
else MIN_BOOST_TEMP,
|
||||
)
|
||||
else:
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
|
||||
@@ -39,7 +39,8 @@ from .const import (
|
||||
KNX_MODULE_KEY,
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SENSOR
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -146,17 +147,17 @@ class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxBinarySensor(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address_state=[
|
||||
config[DOMAIN][CONF_GA_SENSOR][CONF_GA_STATE],
|
||||
*config[DOMAIN][CONF_GA_SENSOR][CONF_GA_PASSIVE],
|
||||
],
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN].get(CONF_INVERT, False),
|
||||
ignore_internal_state=config[DOMAIN].get(CONF_IGNORE_INTERNAL_STATE, False),
|
||||
context_timeout=config[DOMAIN].get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=config[DOMAIN].get(CONF_RESET_AFTER),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SENSOR),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
invert=knx_conf.get(CONF_INVERT, default=False),
|
||||
ignore_internal_state=knx_conf.get(
|
||||
CONF_IGNORE_INTERNAL_STATE, default=False
|
||||
),
|
||||
context_timeout=knx_conf.get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=knx_conf.get(CONF_RESET_AFTER),
|
||||
)
|
||||
self._attr_force_update = self._device.ignore_internal_state
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import Cover as XknxCover
|
||||
@@ -35,15 +35,13 @@ from .schema import CoverSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_ANGLE,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_POSITION_SET,
|
||||
CONF_GA_POSITION_STATE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_STEP,
|
||||
CONF_GA_STOP,
|
||||
CONF_GA_UP_DOWN,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -230,38 +228,24 @@ class KnxYamlCover(_KnxCover, KnxYamlEntity):
|
||||
def _create_ui_cover(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxCover:
|
||||
"""Return a KNX Light device to be used within XKNX."""
|
||||
|
||||
def get_address(
|
||||
key: str, address_type: Literal["write", "state"] = CONF_GA_WRITE
|
||||
) -> str | None:
|
||||
"""Get a single group address for given key."""
|
||||
return knx_config[key][address_type] if key in knx_config else None
|
||||
|
||||
def get_addresses(
|
||||
key: str, address_type: Literal["write", "state"] = CONF_GA_STATE
|
||||
) -> list[Any] | None:
|
||||
"""Get group address including passive addresses as list."""
|
||||
return (
|
||||
[knx_config[key][address_type], *knx_config[key][CONF_GA_PASSIVE]]
|
||||
if key in knx_config
|
||||
else None
|
||||
)
|
||||
conf = ConfigExtractor(knx_config)
|
||||
|
||||
return XknxCover(
|
||||
xknx=xknx,
|
||||
name=name,
|
||||
group_address_long=get_addresses(CONF_GA_UP_DOWN, CONF_GA_WRITE),
|
||||
group_address_short=get_addresses(CONF_GA_STEP, CONF_GA_WRITE),
|
||||
group_address_stop=get_addresses(CONF_GA_STOP, CONF_GA_WRITE),
|
||||
group_address_position=get_addresses(CONF_GA_POSITION_SET, CONF_GA_WRITE),
|
||||
group_address_position_state=get_addresses(CONF_GA_POSITION_STATE),
|
||||
group_address_angle=get_address(CONF_GA_ANGLE),
|
||||
group_address_angle_state=get_addresses(CONF_GA_ANGLE),
|
||||
travel_time_down=knx_config[CoverConf.TRAVELLING_TIME_DOWN],
|
||||
travel_time_up=knx_config[CoverConf.TRAVELLING_TIME_UP],
|
||||
invert_updown=knx_config.get(CoverConf.INVERT_UPDOWN, False),
|
||||
invert_position=knx_config.get(CoverConf.INVERT_POSITION, False),
|
||||
invert_angle=knx_config.get(CoverConf.INVERT_ANGLE, False),
|
||||
sync_state=knx_config[CONF_SYNC_STATE],
|
||||
group_address_long=conf.get_write_and_passive(CONF_GA_UP_DOWN),
|
||||
group_address_short=conf.get_write_and_passive(CONF_GA_STEP),
|
||||
group_address_stop=conf.get_write_and_passive(CONF_GA_STOP),
|
||||
group_address_position=conf.get_write_and_passive(CONF_GA_POSITION_SET),
|
||||
group_address_position_state=conf.get_state_and_passive(CONF_GA_POSITION_STATE),
|
||||
group_address_angle=conf.get_write(CONF_GA_ANGLE),
|
||||
group_address_angle_state=conf.get_state_and_passive(CONF_GA_ANGLE),
|
||||
travel_time_down=conf.get(CoverConf.TRAVELLING_TIME_DOWN),
|
||||
travel_time_up=conf.get(CoverConf.TRAVELLING_TIME_UP),
|
||||
invert_updown=conf.get(CoverConf.INVERT_UPDOWN, default=False),
|
||||
invert_position=conf.get(CoverConf.INVERT_POSITION, default=False),
|
||||
invert_angle=conf.get(CoverConf.INVERT_ANGLE, default=False),
|
||||
sync_state=conf.get(CONF_SYNC_STATE),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ from .schema import LightSchema
|
||||
from .storage.const import (
|
||||
CONF_COLOR_TEMP_MAX,
|
||||
CONF_COLOR_TEMP_MIN,
|
||||
CONF_DPT,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_BLUE_BRIGHTNESS,
|
||||
CONF_GA_BLUE_SWITCH,
|
||||
@@ -45,17 +44,15 @@ from .storage.const import (
|
||||
CONF_GA_GREEN_BRIGHTNESS,
|
||||
CONF_GA_GREEN_SWITCH,
|
||||
CONF_GA_HUE,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_RED_BRIGHTNESS,
|
||||
CONF_GA_RED_SWITCH,
|
||||
CONF_GA_SATURATION,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WHITE_BRIGHTNESS,
|
||||
CONF_GA_WHITE_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.entity_store_schema import LightColorMode
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -203,94 +200,92 @@ def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight:
|
||||
def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight:
|
||||
"""Return a KNX Light device to be used within XKNX."""
|
||||
|
||||
def get_write(key: str) -> str | None:
|
||||
"""Get the write group address."""
|
||||
return knx_config[key][CONF_GA_WRITE] if key in knx_config else None
|
||||
|
||||
def get_state(key: str) -> list[Any] | None:
|
||||
"""Get the state group address."""
|
||||
return (
|
||||
[knx_config[key][CONF_GA_STATE], *knx_config[key][CONF_GA_PASSIVE]]
|
||||
if key in knx_config
|
||||
else None
|
||||
)
|
||||
|
||||
def get_dpt(key: str) -> str | None:
|
||||
"""Get the DPT."""
|
||||
return knx_config[key].get(CONF_DPT) if key in knx_config else None
|
||||
conf = ConfigExtractor(knx_config)
|
||||
|
||||
group_address_tunable_white = None
|
||||
group_address_tunable_white_state = None
|
||||
group_address_color_temp = None
|
||||
group_address_color_temp_state = None
|
||||
|
||||
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
|
||||
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_tunable_white_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if _color_temp_dpt := conf.get_dpt(CONF_GA_COLOR_TEMP):
|
||||
if _color_temp_dpt == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = conf.get_write(CONF_GA_COLOR_TEMP)
|
||||
group_address_tunable_white_state = conf.get_state_and_passive(
|
||||
CONF_GA_COLOR_TEMP
|
||||
)
|
||||
else:
|
||||
# absolute uint or float
|
||||
group_address_color_temp = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_color_temp_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
group_address_color_temp = conf.get_write(CONF_GA_COLOR_TEMP)
|
||||
group_address_color_temp_state = conf.get_state_and_passive(
|
||||
CONF_GA_COLOR_TEMP
|
||||
)
|
||||
if _color_temp_dpt == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
|
||||
|
||||
_color_dpt = get_dpt(CONF_GA_COLOR)
|
||||
color_dpt = conf.get_dpt(CONF_GA_COLOR)
|
||||
|
||||
return XknxLight(
|
||||
xknx,
|
||||
name=name,
|
||||
group_address_switch=get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=get_state(CONF_GA_SWITCH),
|
||||
group_address_brightness=get_write(CONF_GA_BRIGHTNESS),
|
||||
group_address_brightness_state=get_state(CONF_GA_BRIGHTNESS),
|
||||
group_address_color=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
group_address_switch=conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_switch_state=conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
group_address_brightness=conf.get_write(CONF_GA_BRIGHTNESS),
|
||||
group_address_brightness_state=conf.get_state_and_passive(CONF_GA_BRIGHTNESS),
|
||||
group_address_color=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_color_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
group_address_color_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_rgbw=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_rgbw_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_hue=get_write(CONF_GA_HUE),
|
||||
group_address_hue_state=get_state(CONF_GA_HUE),
|
||||
group_address_saturation=get_write(CONF_GA_SATURATION),
|
||||
group_address_saturation_state=get_state(CONF_GA_SATURATION),
|
||||
group_address_xyy_color=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.XYY
|
||||
group_address_hue=conf.get_write(CONF_GA_HUE),
|
||||
group_address_hue_state=conf.get_state_and_passive(CONF_GA_HUE),
|
||||
group_address_saturation=conf.get_write(CONF_GA_SATURATION),
|
||||
group_address_saturation_state=conf.get_state_and_passive(CONF_GA_SATURATION),
|
||||
group_address_xyy_color=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
else None,
|
||||
group_address_xyy_color_state=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.XYY
|
||||
group_address_xyy_color_state=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
else None,
|
||||
group_address_tunable_white=group_address_tunable_white,
|
||||
group_address_tunable_white_state=group_address_tunable_white_state,
|
||||
group_address_color_temperature=group_address_color_temp,
|
||||
group_address_color_temperature_state=group_address_color_temp_state,
|
||||
group_address_switch_red=get_write(CONF_GA_RED_SWITCH),
|
||||
group_address_switch_red_state=get_state(CONF_GA_RED_SWITCH),
|
||||
group_address_brightness_red=get_write(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_brightness_red_state=get_state(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_switch_green=get_write(CONF_GA_GREEN_SWITCH),
|
||||
group_address_switch_green_state=get_state(CONF_GA_GREEN_SWITCH),
|
||||
group_address_brightness_green=get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green_state=get_state(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_switch_blue=get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=get_state(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_brightness_blue_state=get_state(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_switch_white=get_write(CONF_GA_WHITE_SWITCH),
|
||||
group_address_switch_white_state=get_state(CONF_GA_WHITE_SWITCH),
|
||||
group_address_brightness_white=get_write(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_brightness_white_state=get_state(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_switch_red=conf.get_write(CONF_GA_RED_SWITCH),
|
||||
group_address_switch_red_state=conf.get_state_and_passive(CONF_GA_RED_SWITCH),
|
||||
group_address_brightness_red=conf.get_write(CONF_GA_RED_BRIGHTNESS),
|
||||
group_address_brightness_red_state=conf.get_state_and_passive(
|
||||
CONF_GA_RED_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_green=conf.get_write(CONF_GA_GREEN_SWITCH),
|
||||
group_address_switch_green_state=conf.get_state_and_passive(
|
||||
CONF_GA_GREEN_SWITCH
|
||||
),
|
||||
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
|
||||
group_address_brightness_green_state=conf.get_state_and_passive(
|
||||
CONF_GA_GREEN_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
|
||||
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
|
||||
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
|
||||
group_address_brightness_blue_state=conf.get_state_and_passive(
|
||||
CONF_GA_BLUE_BRIGHTNESS
|
||||
),
|
||||
group_address_switch_white=conf.get_write(CONF_GA_WHITE_SWITCH),
|
||||
group_address_switch_white_state=conf.get_state_and_passive(
|
||||
CONF_GA_WHITE_SWITCH
|
||||
),
|
||||
group_address_brightness_white=conf.get_write(CONF_GA_WHITE_BRIGHTNESS),
|
||||
group_address_brightness_white_state=conf.get_state_and_passive(
|
||||
CONF_GA_WHITE_BRIGHTNESS
|
||||
),
|
||||
color_temperature_type=color_temperature_type,
|
||||
min_kelvin=knx_config[CONF_COLOR_TEMP_MIN],
|
||||
max_kelvin=knx_config[CONF_COLOR_TEMP_MAX],
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Utility functions for the KNX integration."""
|
||||
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_DPT, CONF_GA_PASSIVE, CONF_GA_STATE, CONF_GA_WRITE
|
||||
|
||||
|
||||
def nested_get(dic: ConfigType, *keys: str, default: Any | None = None) -> Any:
|
||||
"""Get the value from a nested dictionary."""
|
||||
for key in keys:
|
||||
if key not in dic:
|
||||
return default
|
||||
dic = dic[key]
|
||||
return dic
|
||||
|
||||
|
||||
class ConfigExtractor:
|
||||
"""Helper class for extracting values from a knx config store dictionary."""
|
||||
|
||||
__slots__ = ("get",)
|
||||
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize the extractor."""
|
||||
self.get = partial(nested_get, config)
|
||||
|
||||
def get_write(self, *path: str) -> str | None:
|
||||
"""Get the write group address."""
|
||||
return self.get(*path, CONF_GA_WRITE) # type: ignore[no-any-return]
|
||||
|
||||
def get_state(self, *path: str) -> str | None:
|
||||
"""Get the state group address."""
|
||||
return self.get(*path, CONF_GA_STATE) # type: ignore[no-any-return]
|
||||
|
||||
def get_write_and_passive(self, *path: str) -> list[Any | None]:
|
||||
"""Get the group addresses of write and passive."""
|
||||
write = self.get(*path, CONF_GA_WRITE)
|
||||
passive = self.get(*path, CONF_GA_PASSIVE)
|
||||
return [write, *passive] if passive else [write]
|
||||
|
||||
def get_state_and_passive(self, *path: str) -> list[Any | None]:
|
||||
"""Get the group addresses of state and passive."""
|
||||
state = self.get(*path, CONF_GA_STATE)
|
||||
passive = self.get(*path, CONF_GA_PASSIVE)
|
||||
return [state, *passive] if passive else [state]
|
||||
|
||||
def get_dpt(self, *path: str) -> str | None:
|
||||
"""Get the data point type of a group address config key."""
|
||||
return self.get(*path, CONF_DPT) # type: ignore[no-any-return]
|
||||
@@ -36,13 +36,8 @@ from .const import (
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .schema import SwitchSchema
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SWITCH
|
||||
from .storage.util import ConfigExtractor
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -142,15 +137,13 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
knx_conf = ConfigExtractor(config[DOMAIN])
|
||||
self._device = XknxSwitch(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE],
|
||||
group_address_state=[
|
||||
config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE],
|
||||
*config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE],
|
||||
],
|
||||
respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ],
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN][CONF_INVERT],
|
||||
group_address=knx_conf.get_write(CONF_GA_SWITCH),
|
||||
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SWITCH),
|
||||
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
|
||||
sync_state=knx_conf.get(CONF_SYNC_STATE),
|
||||
invert=knx_conf.get(CONF_INVERT),
|
||||
)
|
||||
|
||||
@@ -104,7 +104,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -
|
||||
) as ex:
|
||||
await lcn_connection.async_close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to {config_entry.title}: {ex}"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"config_entry_title": config_entry.title,
|
||||
},
|
||||
) from ex
|
||||
|
||||
_LOGGER.info('LCN connected to "%s"', config_entry.title)
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
CONF_SWITCHES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -100,7 +101,11 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str:
|
||||
return cast(str, domain_data["setpoint"])
|
||||
if domain_name == "scene":
|
||||
return f"{domain_data['register']}{domain_data['scene']}"
|
||||
raise ValueError("Unknown domain")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_domain",
|
||||
translation_placeholders={CONF_DOMAIN: domain_name},
|
||||
)
|
||||
|
||||
|
||||
def generate_unique_id(
|
||||
@@ -304,6 +309,8 @@ def get_device_config(
|
||||
def is_states_string(states_string: str) -> list[str]:
|
||||
"""Validate the given states string and return states list."""
|
||||
if len(states_string) != 8:
|
||||
raise ValueError("Invalid length of states string")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_length_of_states_string"
|
||||
)
|
||||
states = {"1": "ON", "0": "OFF", "T": "TOGGLE", "-": "NOCHANGE"}
|
||||
return [states[state_string] for state_string in states_string]
|
||||
|
||||
@@ -19,7 +19,7 @@ rules:
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
|
||||
@@ -330,8 +330,9 @@ class SendKeys(LcnServiceCall):
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
hit = pypck.lcn_defs.SendKeyCommand.HIT
|
||||
if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit:
|
||||
raise ValueError(
|
||||
"Only hit command is allowed when sending deferred keys."
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_send_keys_action",
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit)
|
||||
@@ -368,8 +369,9 @@ class LockKeys(LcnServiceCall):
|
||||
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
if table_id != 0:
|
||||
raise ValueError(
|
||||
"Only table A is allowed when locking keys for a specific time."
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_lock_keys_table",
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.lock_keys_tab_a_temporary(
|
||||
|
||||
@@ -414,11 +414,23 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_address": {
|
||||
"message": "LCN device for given address has not been configured."
|
||||
"cannot_connect": {
|
||||
"message": "Unable to connect to {config_entry_title}."
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "LCN device for given device ID has not been configured."
|
||||
"message": "LCN device for given device ID {device_id} has not been configured."
|
||||
},
|
||||
"invalid_domain": {
|
||||
"message": "Invalid domain {domain}."
|
||||
},
|
||||
"invalid_send_keys_action": {
|
||||
"message": "Invalid state for sending keys. Only 'hit' allowed for deferred sending."
|
||||
},
|
||||
"invalid_lock_keys_table": {
|
||||
"message": "Invalid table for locking keys. Only table A allowed when locking for a specific time."
|
||||
},
|
||||
"invalid_length_of_states_string": {
|
||||
"message": "Invalid length of states string. Expected 8 characters."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.2", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "led-ble==1.1.7"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.2", "led-ble==1.1.7"]
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, entry: LookinConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove lookin config entry from a device."""
|
||||
data: LookinData = hass.data[DOMAIN][entry.entry_id]
|
||||
data = entry.runtime_data
|
||||
all_identifiers: set[tuple[str, str]] = {
|
||||
(DOMAIN, data.lookin_device.id),
|
||||
*((DOMAIN, remote["UUID"]) for remote in data.devices),
|
||||
|
||||
@@ -136,7 +136,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the media player off."""
|
||||
await self._async_send_command(self._power_off_command)
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
@@ -159,7 +159,5 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||
state = status[0]
|
||||
mute = status[2]
|
||||
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if state == "1" else MediaPlayerState.STANDBY
|
||||
)
|
||||
self._attr_state = MediaPlayerState.ON if state == "1" else MediaPlayerState.OFF
|
||||
self._attr_is_volume_muted = mute == "0"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.2.1"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity):
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value in (None, NullValue):
|
||||
value = None
|
||||
elif value_convert := self.entity_description.measurement_to_ha:
|
||||
elif value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
if TYPE_CHECKING:
|
||||
value = cast(bool | None, value)
|
||||
@@ -70,7 +70,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="HueMotionSensor",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
|
||||
@@ -83,7 +83,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="OccupancySensor",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
# The first bit = if occupied
|
||||
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
|
||||
@@ -94,7 +94,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="BatteryChargeLevel",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
measurement_to_ha=lambda x: x
|
||||
device_to_ha=lambda x: x
|
||||
!= clusters.PowerSource.Enums.BatChargeLevelEnum.kOk,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
@@ -109,7 +109,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ContactSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
# value is inverted on matter to what we expect
|
||||
measurement_to_ha=lambda x: not x,
|
||||
device_to_ha=lambda x: not x,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
|
||||
@@ -153,7 +153,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="LockDoorStateSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
|
||||
@@ -168,7 +168,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmDeviceMutedSensor",
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x == clusters.SmokeCoAlarm.Enums.MuteStateEnum.kMuted
|
||||
),
|
||||
translation_key="muted",
|
||||
@@ -181,7 +181,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmEndfOfServiceSensor",
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x == clusters.SmokeCoAlarm.Enums.EndOfServiceEnum.kExpired
|
||||
),
|
||||
translation_key="end_of_service",
|
||||
@@ -195,7 +195,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmBatteryAlertSensor",
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="battery_alert",
|
||||
@@ -232,7 +232,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmSmokeStateSensor",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
),
|
||||
@@ -244,7 +244,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmInterconnectSmokeAlarmSensor",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="interconnected_smoke_alarm",
|
||||
@@ -257,7 +257,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmInterconnectCOAlarmSensor",
|
||||
device_class=BinarySensorDeviceClass.CO,
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="interconnected_co_alarm",
|
||||
@@ -271,7 +271,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvseChargingStatusSensor",
|
||||
translation_key="evse_charging_status",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: False,
|
||||
@@ -291,7 +291,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvsePlugStateSensor",
|
||||
translation_key="evse_plug_state",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: True,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: True,
|
||||
@@ -311,7 +311,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvseSupplyStateSensor",
|
||||
translation_key="evse_supply_charging_state",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
|
||||
@@ -327,7 +327,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="WaterHeaterManagementBoostStateSensor",
|
||||
translation_key="boost_state",
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive
|
||||
),
|
||||
),
|
||||
@@ -342,7 +342,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
# DeviceFault or SupplyFault bit enabled
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kDeviceFault: True,
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSupplyFault: True,
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedLow: False,
|
||||
@@ -366,7 +366,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PumpStatusRunning",
|
||||
translation_key="pump_running",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x
|
||||
== clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning
|
||||
),
|
||||
@@ -384,7 +384,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="dishwasher_alarm_inflow",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x == clusters.DishwasherAlarm.Bitmaps.AlarmBitmap.kInflowError
|
||||
),
|
||||
),
|
||||
@@ -399,7 +399,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="dishwasher_alarm_door",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
measurement_to_ha=lambda x: (
|
||||
device_to_ha=lambda x: (
|
||||
x == clusters.DishwasherAlarm.Bitmaps.AlarmBitmap.kDoorError
|
||||
),
|
||||
),
|
||||
|
||||
@@ -59,8 +59,8 @@ class MatterEntityDescription(EntityDescription):
|
||||
"""Describe the Matter entity."""
|
||||
|
||||
# convert the value from the primary attribute to the value used by HA
|
||||
measurement_to_ha: Callable[[Any], Any] | None = None
|
||||
ha_to_native_value: Callable[[Any], Any] | None = None
|
||||
device_to_ha: Callable[[Any], Any] | None = None
|
||||
ha_to_device: Callable[[Any], Any] | None = None
|
||||
command_timeout: int | None = None
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==7.0.0"],
|
||||
"requirements": ["python-matter-server==8.0.0"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any, cast
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor, ClusterCommand
|
||||
from matter_server.client.models import device_types
|
||||
from matter_server.common import custom_clusters
|
||||
|
||||
from homeassistant.components.number import (
|
||||
@@ -18,6 +19,7 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfLength,
|
||||
@@ -53,7 +55,7 @@ class MatterRangeNumberEntityDescription(
|
||||
):
|
||||
"""Describe Matter Number Input entities with min and max values."""
|
||||
|
||||
ha_to_native_value: Callable[[Any], Any]
|
||||
ha_to_device: Callable[[Any], Any]
|
||||
|
||||
# attribute descriptors to get the min and max value
|
||||
min_attribute: type[ClusterAttributeDescriptor]
|
||||
@@ -72,7 +74,7 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
sendvalue = int(value)
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
sendvalue = value_convert(value)
|
||||
await self.write_attribute(
|
||||
value=sendvalue,
|
||||
@@ -82,7 +84,7 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -94,7 +96,7 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
send_value = self.entity_description.ha_to_native_value(value)
|
||||
send_value = self.entity_description.ha_to_device(value)
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(send_value),
|
||||
@@ -104,7 +106,7 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
self._attr_native_min_value = (
|
||||
@@ -123,6 +125,31 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
)
|
||||
|
||||
|
||||
class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
"""Representation of a Matter Attribute as a Number entity."""
|
||||
|
||||
entity_description: MatterNumberEntityDescription
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set level value."""
|
||||
send_value = int(value)
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
send_value = value_convert(value)
|
||||
await self.send_device_command(
|
||||
clusters.LevelControl.Commands.MoveToLevel(
|
||||
level=send_value,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
@@ -135,8 +162,8 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=0,
|
||||
mode=NumberMode.BOX,
|
||||
# use 255 to indicate that the value should revert to the default
|
||||
measurement_to_ha=lambda x: 255 if x is None else x,
|
||||
ha_to_native_value=lambda x: None if x == 255 else int(x),
|
||||
device_to_ha=lambda x: 255 if x is None else x,
|
||||
ha_to_device=lambda x: None if x == 255 else int(x),
|
||||
native_step=1,
|
||||
native_unit_of_measurement=None,
|
||||
),
|
||||
@@ -153,8 +180,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="on_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -172,8 +199,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="off_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -191,8 +218,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="on_off_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -229,8 +256,8 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=-50,
|
||||
native_step=0.5,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
@@ -239,6 +266,26 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
vendor_id=(4874,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="pump_setpoint",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
translation_key="pump_setpoint",
|
||||
native_max_value=100,
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
device_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
),
|
||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
entity_class=MatterLevelControlNumber,
|
||||
required_attributes=(clusters.LevelControl.Attributes.CurrentLevel,),
|
||||
device_type=(device_types.Pump,),
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
@@ -279,8 +326,8 @@ DISCOVERY_SCHEMAS = [
|
||||
targetTemperature=value
|
||||
),
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
measurement_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_native_value=lambda x: round(x * 100),
|
||||
device_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_device=lambda x: round(x * 100),
|
||||
min_attribute=clusters.TemperatureControl.Attributes.MinTemperature,
|
||||
max_attribute=clusters.TemperatureControl.Attributes.MaxTemperature,
|
||||
mode=NumberMode.SLIDER,
|
||||
|
||||
@@ -71,8 +71,8 @@ class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescrip
|
||||
class MatterMapSelectEntityDescription(MatterSelectEntityDescription):
|
||||
"""Describe Matter select entities for MatterMapSelectEntityDescription."""
|
||||
|
||||
measurement_to_ha: Callable[[int], str | None]
|
||||
ha_to_native_value: Callable[[str], int | None]
|
||||
device_to_ha: Callable[[int], str | None]
|
||||
ha_to_device: Callable[[str], int | None]
|
||||
|
||||
# list attribute: the attribute descriptor to get the list of values (= list of integers)
|
||||
list_attribute: type[ClusterAttributeDescriptor]
|
||||
@@ -97,7 +97,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected mode."""
|
||||
value_convert = self.entity_description.ha_to_native_value
|
||||
value_convert = self.entity_description.ha_to_device
|
||||
if TYPE_CHECKING:
|
||||
assert value_convert is not None
|
||||
await self.write_attribute(
|
||||
@@ -109,7 +109,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
"""Update from device."""
|
||||
value: Nullable | int | None
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
value_convert = self.entity_description.measurement_to_ha
|
||||
value_convert = self.entity_description.device_to_ha
|
||||
if TYPE_CHECKING:
|
||||
assert value_convert is not None
|
||||
self._attr_current_option = value_convert(value)
|
||||
@@ -132,7 +132,7 @@ class MatterMapSelectEntity(MatterAttributeSelectEntity):
|
||||
self._attr_options = [
|
||||
mapped_value
|
||||
for value in available_values
|
||||
if (mapped_value := self.entity_description.measurement_to_ha(value))
|
||||
if (mapped_value := self.entity_description.device_to_ha(value))
|
||||
]
|
||||
# use base implementation from MatterAttributeSelectEntity to set the current option
|
||||
super()._update_from_device()
|
||||
@@ -333,13 +333,13 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="startup_on_off",
|
||||
options=["on", "off", "toggle", "previous"],
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
0: "off",
|
||||
1: "on",
|
||||
2: "toggle",
|
||||
None: "previous",
|
||||
}.get,
|
||||
ha_to_native_value={
|
||||
ha_to_device={
|
||||
"off": 0,
|
||||
"on": 1,
|
||||
"toggle": 2,
|
||||
@@ -358,12 +358,12 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="sensitivity_level",
|
||||
options=["high", "standard", "low"],
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
0: "high",
|
||||
1: "standard",
|
||||
2: "low",
|
||||
}.get,
|
||||
ha_to_native_value={
|
||||
ha_to_device={
|
||||
"high": 0,
|
||||
"standard": 1,
|
||||
"low": 2,
|
||||
@@ -379,11 +379,11 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="temperature_display_mode",
|
||||
options=["Celsius", "Fahrenheit"],
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
0: "Celsius",
|
||||
1: "Fahrenheit",
|
||||
}.get,
|
||||
ha_to_native_value={
|
||||
ha_to_device={
|
||||
"Celsius": 0,
|
||||
"Fahrenheit": 1,
|
||||
}.get,
|
||||
@@ -432,8 +432,8 @@ DISCOVERY_SCHEMAS = [
|
||||
key="MatterLaundryWasherNumberOfRinses",
|
||||
translation_key="laundry_washer_number_of_rinses",
|
||||
list_attribute=clusters.LaundryWasherControls.Attributes.SupportedRinses,
|
||||
measurement_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
|
||||
ha_to_native_value=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
|
||||
device_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
|
||||
ha_to_device=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
|
||||
),
|
||||
entity_class=MatterMapSelectEntity,
|
||||
required_attributes=(
|
||||
@@ -450,13 +450,13 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="door_lock_sound_volume",
|
||||
options=["silent", "low", "medium", "high"],
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
0: "silent",
|
||||
1: "low",
|
||||
3: "medium",
|
||||
2: "high",
|
||||
}.get,
|
||||
ha_to_native_value={
|
||||
ha_to_device={
|
||||
"silent": 0,
|
||||
"low": 1,
|
||||
"medium": 3,
|
||||
@@ -472,8 +472,8 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PumpConfigurationAndControlOperationMode",
|
||||
translation_key="pump_operation_mode",
|
||||
options=list(PUMP_OPERATION_MODE_MAP.values()),
|
||||
measurement_to_ha=PUMP_OPERATION_MODE_MAP.get,
|
||||
ha_to_native_value=PUMP_OPERATION_MODE_MAP_REVERSE.get,
|
||||
device_to_ha=PUMP_OPERATION_MODE_MAP.get,
|
||||
ha_to_device=PUMP_OPERATION_MODE_MAP_REVERSE.get,
|
||||
),
|
||||
entity_class=MatterAttributeSelectEntity,
|
||||
required_attributes=(
|
||||
|
||||
@@ -194,7 +194,7 @@ class MatterSensor(MatterEntity, SensorEntity):
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value in (None, NullValue):
|
||||
value = None
|
||||
elif value_convert := self.entity_description.measurement_to_ha:
|
||||
elif value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -296,7 +296,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="TemperatureSensor",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -308,7 +308,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PressureSensor",
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -320,7 +320,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="FlowSensor",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
translation_key="flow",
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -332,7 +332,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="HumiditySensor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -346,7 +346,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="LightSensor",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
|
||||
device_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -360,7 +360,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
# value has double precision
|
||||
measurement_to_ha=lambda x: int(x / 2),
|
||||
device_to_ha=lambda x: int(x / 2),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -402,7 +402,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[state for state in CHARGE_STATE_MAP.values() if state is not None],
|
||||
measurement_to_ha=CHARGE_STATE_MAP.get,
|
||||
device_to_ha=CHARGE_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatChargeState,),
|
||||
@@ -589,7 +589,7 @@ DISCOVERY_SCHEMAS = [
|
||||
state_class=None,
|
||||
# convert to set first to remove the duplicate unknown value
|
||||
options=[x for x in AIR_QUALITY_MAP.values() if x is not None],
|
||||
measurement_to_ha=lambda x: AIR_QUALITY_MAP[x],
|
||||
device_to_ha=lambda x: AIR_QUALITY_MAP[x],
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.AirQuality.Attributes.AirQuality,),
|
||||
@@ -668,7 +668,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
device_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -685,7 +685,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
device_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -702,7 +702,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Watt,),
|
||||
@@ -731,7 +731,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=0,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Voltage,),
|
||||
@@ -823,7 +823,7 @@ DISCOVERY_SCHEMAS = [
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
# id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh)
|
||||
measurement_to_ha=lambda x: x.energy,
|
||||
device_to_ha=lambda x: x.energy,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -842,7 +842,7 @@ DISCOVERY_SCHEMAS = [
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
# id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh)
|
||||
measurement_to_ha=lambda x: x.energy,
|
||||
device_to_ha=lambda x: x.energy,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -910,7 +910,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="contamination_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(CONTAMINATION_STATE_MAP.values()),
|
||||
measurement_to_ha=CONTAMINATION_STATE_MAP.get,
|
||||
device_to_ha=CONTAMINATION_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.ContaminationState,),
|
||||
@@ -922,7 +922,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="expiry_date",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
# raw value is epoch seconds
|
||||
measurement_to_ha=datetime.fromtimestamp,
|
||||
device_to_ha=datetime.fromtimestamp,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.ExpiryDate,),
|
||||
@@ -993,7 +993,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ThermostatLocalTemperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -1044,7 +1044,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key="window_covering_target_position",
|
||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||
device_to_ha=lambda x: round((10000 - x) / 100),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -1060,7 +1060,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(EVSE_FAULT_STATE_MAP.values()),
|
||||
measurement_to_ha=EVSE_FAULT_STATE_MAP.get,
|
||||
device_to_ha=EVSE_FAULT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.EnergyEvse.Attributes.FaultState,),
|
||||
@@ -1173,7 +1173,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(ESA_STATE_MAP.values()),
|
||||
measurement_to_ha=ESA_STATE_MAP.get,
|
||||
device_to_ha=ESA_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,),
|
||||
@@ -1186,7 +1186,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(DEM_OPT_OUT_STATE_MAP.values()),
|
||||
measurement_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
device_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.OptOutState,),
|
||||
@@ -1200,7 +1200,7 @@ DISCOVERY_SCHEMAS = [
|
||||
options=[
|
||||
mode for mode in PUMP_CONTROL_MODE_MAP.values() if mode is not None
|
||||
],
|
||||
measurement_to_ha=PUMP_CONTROL_MODE_MAP.get,
|
||||
device_to_ha=PUMP_CONTROL_MODE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
|
||||
@@ -180,6 +180,9 @@
|
||||
"altitude": {
|
||||
"name": "Altitude above sea level"
|
||||
},
|
||||
"pump_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ class MatterGenericCommandSwitch(MatterSwitch):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_is_on = value
|
||||
|
||||
@@ -141,7 +141,7 @@ class MatterNumericSwitch(MatterSwitch):
|
||||
|
||||
async def _async_set_native_value(self, value: bool) -> None:
|
||||
"""Update the current value."""
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
send_value = value_convert(value)
|
||||
await self.write_attribute(
|
||||
value=send_value,
|
||||
@@ -159,7 +159,7 @@ class MatterNumericSwitch(MatterSwitch):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_is_on = value
|
||||
|
||||
@@ -248,11 +248,11 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EveTrvChildLock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="child_lock",
|
||||
measurement_to_ha={
|
||||
device_to_ha={
|
||||
0: False,
|
||||
1: True,
|
||||
}.get,
|
||||
ha_to_native_value={
|
||||
ha_to_device={
|
||||
False: 0,
|
||||
True: 1,
|
||||
}.get,
|
||||
@@ -275,7 +275,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
off_command=clusters.EnergyEvse.Commands.Disable,
|
||||
command_timeout=3000,
|
||||
measurement_to_ha=EVSE_SUPPLY_STATE_MAP.get,
|
||||
device_to_ha=EVSE_SUPPLY_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterGenericCommandSwitch,
|
||||
required_attributes=(
|
||||
|
||||
@@ -32,11 +32,18 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
llm_apis = {api.id: api.name for api in llm.async_get_apis(self.hass)}
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input
|
||||
)
|
||||
if not user_input[CONF_LLM_HASS_API]:
|
||||
errors[CONF_LLM_HASS_API] = "llm_api_required"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=", ".join(
|
||||
llm_apis[api_id] for api_id in user_input[CONF_LLM_HASS_API]
|
||||
),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -44,7 +51,7 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
default=llm.LLM_API_ASSIST,
|
||||
default=[llm.LLM_API_ASSIST],
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
@@ -53,10 +60,12 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
value=llm_api_id,
|
||||
)
|
||||
for llm_api_id, name in llm_apis.items()
|
||||
]
|
||||
],
|
||||
multiple=True,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders={"more_info_url": MORE_INFO_URL},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -42,7 +42,7 @@ def _format_tool(
|
||||
|
||||
|
||||
async def create_server(
|
||||
hass: HomeAssistant, llm_api_id: str, llm_context: llm.LLMContext
|
||||
hass: HomeAssistant, llm_api_id: str | list[str], llm_context: llm.LLMContext
|
||||
) -> Server:
|
||||
"""Create a new Model Context Protocol Server.
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"llm_api_required": "At least one LLM API must be configured."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user