mirror of
https://github.com/home-assistant/core.git
synced 2025-12-25 09:18:27 +00:00
Compare commits
1 Commits
ai-task-st
...
enforce_en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df446f5240 |
51
.github/ISSUE_TEMPLATE/task.yml
vendored
51
.github/ISSUE_TEMPLATE/task.yml
vendored
@@ -1,51 +0,0 @@
|
||||
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
|
||||
84
.github/workflows/restrict-task-creation.yml
vendored
84
.github/workflows/restrict-task-creation.yml
vendored
@@ -1,84 +0,0 @@
|
||||
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']
|
||||
});
|
||||
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -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/ @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
||||
/homeassistant/components/eufylife_ble/ @bdr99
|
||||
/tests/components/eufylife_ble/ @bdr99
|
||||
/homeassistant/components/event/ @home-assistant/core
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
|
||||
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 \
|
||||
@@ -24,18 +32,21 @@ 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
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
|
||||
RUN uv python install 3.13.2
|
||||
# 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
|
||||
|
||||
USER vscode
|
||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||
@@ -44,10 +55,6 @@ 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
|
||||
@@ -58,4 +65,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
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""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, CONF_DESCRIPTION, CONF_SELECTOR
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import (
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
@@ -15,14 +14,12 @@ from homeassistant.core import (
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, selector, storage
|
||||
from homeassistant.helpers import config_validation as cv, 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,
|
||||
@@ -50,27 +47,6 @@ _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."""
|
||||
@@ -88,10 +64,6 @@ 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,8 +21,6 @@ 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,9 +17,3 @@ 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,10 +15,6 @@
|
||||
"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,8 +5,6 @@ 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
|
||||
|
||||
@@ -19,7 +17,6 @@ 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:
|
||||
@@ -41,7 +38,6 @@ async def async_generate_data(
|
||||
GenDataTask(
|
||||
name=task_name,
|
||||
instructions=instructions,
|
||||
structure=structure,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -56,9 +52,6 @@ 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,45 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
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."""
|
||||
@@ -51,14 +25,17 @@ 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 validate_input(self.hass, user_input)
|
||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
||||
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()
|
||||
@@ -67,6 +44,8 @@ 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",
|
||||
@@ -82,43 +61,3 @@ 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 ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
||||
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
@@ -55,8 +55,4 @@ 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 ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
raise ConfigEntryError("Could not authenticate") from err
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.2.2"]
|
||||
"requirements": ["aioamazondevices==3.1.22"]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: all tests missing
|
||||
|
||||
@@ -22,29 +22,17 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,18 +5,26 @@ from __future__ import annotations
|
||||
from asyncio import timeout
|
||||
import logging
|
||||
|
||||
from androidtvremote2 import CannotConnect, ConnectionClosed, InvalidAuth
|
||||
from androidtvremote2 import (
|
||||
AndroidTVRemote,
|
||||
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 AndroidTVRemoteConfigEntry, create_api, get_enable_ime
|
||||
from .helpers import 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
|
||||
@@ -74,17 +82,13 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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: AndroidTVRemoteConfigEntry
|
||||
) -> None:
|
||||
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> 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,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
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 AndroidTVRemoteConfigEntry, create_api, get_enable_ime
|
||||
from .helpers import create_api, get_enable_ime
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,6 +41,12 @@ 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,
|
||||
@@ -61,7 +67,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial and reconfigure step."""
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self.host = user_input[CONF_HOST]
|
||||
@@ -70,32 +76,15 @@ 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="reconfigure" if self.source == SOURCE_RECONFIGURE else "user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_HOST, default=default_host): str}
|
||||
),
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -228,16 +217,10 @@ 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: AndroidTVRemoteConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
) -> AndroidTVRemoteOptionsFlowHandler:
|
||||
"""Create the options flow."""
|
||||
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
||||
@@ -246,7 +229,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
|
||||
"""Android TV Remote options flow."""
|
||||
|
||||
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:
|
||||
def __init__(self, config_entry: ConfigEntry) -> 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 .helpers import AndroidTVRemoteConfigEntry
|
||||
from . import AndroidTVRemoteConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_HOST, CONF_MAC}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -13,7 +14,6 @@ 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,9 +23,7 @@ class AndroidTVRemoteBaseEntity(Entity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self, api: AndroidTVRemote, config_entry: AndroidTVRemoteConfigEntry
|
||||
) -> None:
|
||||
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._api = api
|
||||
self._host = config_entry.data[CONF_HOST]
|
||||
|
||||
@@ -10,8 +10,6 @@ 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."""
|
||||
@@ -25,6 +23,6 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
|
||||
)
|
||||
|
||||
|
||||
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
||||
def get_enable_ime(entry: ConfigEntry) -> 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.3"],
|
||||
"requirements": ["androidtvremote2==0.2.2"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed, VolumeInfo
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
|
||||
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,11 +75,13 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
else current_app
|
||||
)
|
||||
|
||||
def _update_volume_info(self, volume_info: VolumeInfo) -> None:
|
||||
def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None:
|
||||
"""Update volume info."""
|
||||
if volume_info.get("max"):
|
||||
self._attr_volume_level = volume_info["level"] / volume_info["max"]
|
||||
self._attr_is_volume_muted = volume_info["muted"]
|
||||
self._attr_volume_level = int(volume_info["level"]) / int(
|
||||
volume_info["max"]
|
||||
)
|
||||
self._attr_is_volume_muted = bool(volume_info["muted"])
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
self._attr_is_volume_muted = None
|
||||
@@ -91,7 +93,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _volume_info_updated(self, volume_info: VolumeInfo) -> None:
|
||||
def _volume_info_updated(self, volume_info: dict[str, str | bool]) -> None:
|
||||
"""Update the state when the volume info changes."""
|
||||
self._update_volume_info(volume_info)
|
||||
self.async_write_ha_state()
|
||||
@@ -100,10 +102,8 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
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._update_current_app(self._api.current_app)
|
||||
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,8 +63,7 @@ class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
||||
self._attr_activity_list = [
|
||||
app.get(CONF_APP_NAME, "") for app in self._apps.values()
|
||||
]
|
||||
if self._api.current_app is not None:
|
||||
self._update_current_app(self._api.current_app)
|
||||
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,18 +6,6 @@
|
||||
"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": {
|
||||
@@ -28,9 +16,6 @@
|
||||
"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": {
|
||||
@@ -47,9 +32,7 @@
|
||||
"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%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -57,11 +40,7 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"apps": "Configure applications list",
|
||||
"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."
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
@@ -74,10 +53,8 @@
|
||||
"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_delete": "Check this box to delete the application from the list."
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", default=True): bool,
|
||||
vol.Optional("preannounce"): 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", default=True): bool,
|
||||
vol.Optional("preannounce"): 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", True),
|
||||
"preannounce": call.data.get("preannounce", False),
|
||||
"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", default=True): bool,
|
||||
vol.Optional("preannounce"): 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.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.105.0"],
|
||||
"requirements": ["hass-nabucasa==0.104.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,18 +6,11 @@ from operator import itemgetter
|
||||
import numpy as np
|
||||
import voluptuous as vol
|
||||
|
||||
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.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_ATTRIBUTE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_MAXIMUM,
|
||||
CONF_MINIMUM,
|
||||
CONF_NAME,
|
||||
CONF_SOURCE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
@@ -57,23 +50,20 @@ def datapoints_greater_than_degree(value: dict) -> dict:
|
||||
|
||||
COMPENSATION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ATTRIBUTE): cv.string,
|
||||
vol.Required(CONF_SOURCE): cv.entity_id,
|
||||
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,23 +7,15 @@ from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.components.sensor import 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 (
|
||||
@@ -67,13 +59,24 @@ async def async_setup_platform(
|
||||
|
||||
source: str = conf[CONF_SOURCE]
|
||||
attribute: str | None = conf.get(CONF_ATTRIBUTE)
|
||||
if not (name := conf.get(CONF_NAME)):
|
||||
name = f"{DEFAULT_NAME} {source}"
|
||||
if attribute is not None:
|
||||
name = f"{name} {attribute}"
|
||||
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)]
|
||||
[
|
||||
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],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -88,27 +91,23 @@ class CompensationSensor(SensorEntity):
|
||||
name: str,
|
||||
source: str,
|
||||
attribute: str | None,
|
||||
config: dict[str, Any],
|
||||
precision: int,
|
||||
polynomial: np.poly1d,
|
||||
unit_of_measurement: str | None,
|
||||
minimum: tuple[float, float] | None,
|
||||
maximum: tuple[float, float] | None,
|
||||
) -> None:
|
||||
"""Initialize the Compensation sensor."""
|
||||
|
||||
self._attr_name = name
|
||||
self._source_entity_id = source
|
||||
self._precision = precision
|
||||
self._source_attribute = attribute
|
||||
|
||||
self._precision = config[CONF_PRECISION]
|
||||
self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
|
||||
polynomial: np.poly1d = config[CONF_POLYNOMIAL]
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._poly = polynomial
|
||||
self._coefficients = polynomial.coefficients.tolist()
|
||||
|
||||
self._attr_unique_id = unique_id
|
||||
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)
|
||||
self._attr_name = name
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle added to Hass."""
|
||||
@@ -138,40 +137,13 @@ 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,7 +19,10 @@ 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.STATE
|
||||
| VacuumEntityFeature.START
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.BATTERY
|
||||
)
|
||||
|
||||
SUPPORT_MOST_SERVICES = (
|
||||
@@ -28,6 +31,7 @@ SUPPORT_MOST_SERVICES = (
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.RETURN_HOME
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.FAN_SPEED
|
||||
)
|
||||
|
||||
@@ -42,6 +46,7 @@ SUPPORT_ALL_SERVICES = (
|
||||
| VacuumEntityFeature.SEND_COMMAND
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.STATUS
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.LOCATE
|
||||
| VacuumEntityFeature.MAP
|
||||
| VacuumEntityFeature.CLEAN_SPOT
|
||||
@@ -85,6 +90,12 @@ 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:
|
||||
@@ -106,6 +117,7 @@ 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:
|
||||
@@ -130,6 +142,7 @@ 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:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.2.1"]
|
||||
}
|
||||
|
||||
@@ -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.5.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.4.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eheimdigital"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["eheimdigital==1.3.0"],
|
||||
"requirements": ["eheimdigital==1.2.0"],
|
||||
"zeroconf": [
|
||||
{ "type": "_http._tcp.local.", "name": "eheimdigital._http._tcp.local." }
|
||||
]
|
||||
|
||||
@@ -63,7 +63,6 @@ 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)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.2.0"],
|
||||
"requirements": ["pyenphase==2.1.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -281,7 +281,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool = False
|
||||
_has_state: bool
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "esphome",
|
||||
"name": "ESPHome",
|
||||
"after_dependencies": ["hassio", "zeroconf", "tag"],
|
||||
"codeowners": ["@jesserockz", "@kbx81", "@bdraco"],
|
||||
"codeowners": ["@OttoWinter", "@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==34.1.0",
|
||||
"aioesphomeapi==33.1.1",
|
||||
"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==20250702.0"]
|
||||
"requirements": ["home-assistant-frontend==20250627.0"]
|
||||
}
|
||||
|
||||
@@ -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.3.0"]
|
||||
"requirements": ["av==13.1.0", "Pillow==11.2.1"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType
|
||||
@@ -38,13 +37,11 @@ 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,
|
||||
@@ -56,7 +53,6 @@ CONF_FILENAMES = "filenames"
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = (
|
||||
Platform.AI_TASK,
|
||||
Platform.CONVERSATION,
|
||||
Platform.TTS,
|
||||
)
|
||||
@@ -191,9 +187,11 @@ async def async_setup_entry(
|
||||
"""Set up Google Generative AI Conversation from a config entry."""
|
||||
|
||||
try:
|
||||
client = await hass.async_add_executor_job(
|
||||
partial(Client, api_key=entry.data[CONF_API_KEY])
|
||||
)
|
||||
|
||||
def _init_client() -> Client:
|
||||
return Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
client = await hass.async_add_executor_job(_init_client)
|
||||
await client.aio.models.get(
|
||||
model=RECOMMENDED_CHAT_MODEL,
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
@@ -352,19 +350,6 @@ 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
|
||||
)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"""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,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -47,12 +46,10 @@ 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,
|
||||
@@ -75,14 +72,12 @@ STEP_API_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
async def validate_input(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 = await hass.async_add_executor_job(
|
||||
partial(genai.Client, api_key=data[CONF_API_KEY])
|
||||
)
|
||||
client = genai.Client(api_key=data[CONF_API_KEY])
|
||||
await client.aio.models.list(
|
||||
config={
|
||||
"http_options": {
|
||||
@@ -97,7 +92,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Google Generative AI Conversation."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_api(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -107,7 +102,7 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
await validate_input(user_input)
|
||||
except (APIError, Timeout) as err:
|
||||
if isinstance(err, ClientError) and "API_KEY_INVALID" in str(err):
|
||||
errors["base"] = "invalid_auth"
|
||||
@@ -138,12 +133,6 @@ 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(
|
||||
@@ -192,7 +181,6 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return {
|
||||
"conversation": LLMSubentryFlowHandler,
|
||||
"tts": LLMSubentryFlowHandler,
|
||||
"ai_task_data": LLMSubentryFlowHandler,
|
||||
}
|
||||
|
||||
|
||||
@@ -226,8 +214,6 @@ 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:
|
||||
@@ -302,8 +288,6 @@ 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] = {
|
||||
@@ -331,7 +315,6 @@ async def google_generative_ai_config_option_schema(
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -347,14 +330,13 @@ 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.name.lstrip("models/"),
|
||||
label=api_model.display_name,
|
||||
value=api_model.name,
|
||||
)
|
||||
for api_model in sorted(
|
||||
api_models, key=lambda x: x.name.lstrip("models/") or ""
|
||||
)
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
|
||||
if (
|
||||
api_model.name
|
||||
api_model.display_name
|
||||
and api_model.name
|
||||
and ("tts" in api_model.name) == (subentry_type == "tts")
|
||||
and "vision" not in api_model.name
|
||||
and api_model.supported_actions
|
||||
@@ -460,5 +442,4 @@ async def google_generative_ai_config_option_schema(
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
return schema
|
||||
|
||||
@@ -12,7 +12,6 @@ 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"
|
||||
@@ -36,7 +35,6 @@ 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],
|
||||
@@ -46,7 +44,3 @@ RECOMMENDED_CONVERSATION_OPTIONS = {
|
||||
RECOMMENDED_TTS_OPTIONS = {
|
||||
CONF_RECOMMENDED: True,
|
||||
}
|
||||
|
||||
RECOMMENDED_AI_TASK_OPTIONS = {
|
||||
CONF_RECOMMENDED: True,
|
||||
}
|
||||
|
||||
@@ -88,34 +88,6 @@
|
||||
"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": {
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
@@ -185,16 +184,13 @@ class HassIOIngress(HomeAssistantView):
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
# Simple request
|
||||
if (empty_body := must_be_empty_body(result.method, result.status)) or (
|
||||
if result.status in (204, 304) or (
|
||||
content_length is not UNDEFINED
|
||||
and (content_length_int := int(content_length))
|
||||
<= MAX_SIMPLE_RESPONSE_SIZE
|
||||
):
|
||||
# Return Response
|
||||
if empty_body:
|
||||
body = None
|
||||
else:
|
||||
body = await result.read()
|
||||
body = await result.read()
|
||||
simple_response = web.Response(
|
||||
headers=headers,
|
||||
status=result.status,
|
||||
|
||||
@@ -63,8 +63,8 @@ from .utils import get_device_macs, non_verifying_requests_session
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Huawei LTE config flow."""
|
||||
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle Huawei LTE config flow."""
|
||||
|
||||
VERSION = 3
|
||||
|
||||
@@ -75,9 +75,9 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> HuaweiLteOptionsFlow:
|
||||
) -> OptionsFlowHandler:
|
||||
"""Get options flow."""
|
||||
return HuaweiLteOptionsFlow()
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def _async_show_user_form(
|
||||
self,
|
||||
@@ -354,7 +354,7 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_update_reload_and_abort(entry, data=new_data)
|
||||
|
||||
|
||||
class HuaweiLteOptionsFlow(OptionsFlow):
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Huawei LTE options flow."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -73,6 +73,7 @@ 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
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==11.3.0"]
|
||||
"requirements": ["Pillow==11.2.1"]
|
||||
}
|
||||
|
||||
@@ -209,12 +209,6 @@
|
||||
"state": {
|
||||
"off": "mdi:card-bulleted-off-outline"
|
||||
}
|
||||
},
|
||||
"boost": {
|
||||
"default": "mdi:thermometer-high",
|
||||
"state": {
|
||||
"off": "mdi:thermometer-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,16 +464,6 @@ 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,9 +278,6 @@
|
||||
},
|
||||
"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, TempUnit
|
||||
from pynecil import CharSetting, SettingsDataResponse
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
|
||||
@@ -40,7 +39,6 @@ class IronOSSwitch(StrEnum):
|
||||
INVERT_BUTTONS = "invert_buttons"
|
||||
DISPLAY_INVERT = "display_invert"
|
||||
CALIBRATE_CJC = "calibrate_cjc"
|
||||
BOOST = "boost"
|
||||
|
||||
|
||||
SWITCH_DESCRIPTIONS: tuple[IronOSSwitchEntityDescription, ...] = (
|
||||
@@ -96,13 +94,6 @@ 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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -145,15 +136,7 @@ class IronOSSwitchEntity(IronOSBaseEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
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)
|
||||
await self.settings.write(self.entity_description.characteristic, True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
|
||||
@@ -39,8 +39,7 @@ from .const import (
|
||||
KNX_MODULE_KEY,
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SENSOR
|
||||
from .storage.util import ConfigExtractor
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_PASSIVE, CONF_GA_SENSOR, CONF_GA_STATE
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -147,17 +146,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=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),
|
||||
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),
|
||||
)
|
||||
self._attr_force_update = self._device.ignore_internal_state
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import Cover as XknxCover
|
||||
@@ -35,13 +35,15 @@ 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(
|
||||
@@ -228,24 +230,38 @@ 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."""
|
||||
|
||||
conf = ConfigExtractor(knx_config)
|
||||
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
|
||||
)
|
||||
|
||||
return XknxCover(
|
||||
xknx=xknx,
|
||||
name=name,
|
||||
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),
|
||||
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],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ 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,
|
||||
@@ -44,15 +45,17 @@ 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(
|
||||
@@ -200,92 +203,94 @@ 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."""
|
||||
|
||||
conf = ConfigExtractor(knx_config)
|
||||
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
|
||||
|
||||
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 _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
|
||||
)
|
||||
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],
|
||||
]
|
||||
else:
|
||||
# absolute uint or float
|
||||
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:
|
||||
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:
|
||||
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
|
||||
|
||||
color_dpt = conf.get_dpt(CONF_GA_COLOR)
|
||||
|
||||
_color_dpt = get_dpt(CONF_GA_COLOR)
|
||||
return XknxLight(
|
||||
xknx,
|
||||
name=name,
|
||||
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
|
||||
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
|
||||
else None,
|
||||
group_address_color_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGB
|
||||
group_address_color_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGB
|
||||
else None,
|
||||
group_address_rgbw=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw=get_write(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
group_address_rgbw_state=conf.get_state_and_passive(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.RGBW
|
||||
group_address_rgbw_state=get_state(CONF_GA_COLOR)
|
||||
if _color_dpt == LightColorMode.RGBW
|
||||
else None,
|
||||
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
|
||||
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
|
||||
else None,
|
||||
group_address_xyy_color_state=conf.get_write(CONF_GA_COLOR)
|
||||
if color_dpt == LightColorMode.XYY
|
||||
group_address_xyy_color_state=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=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
|
||||
),
|
||||
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),
|
||||
color_temperature_type=color_temperature_type,
|
||||
min_kelvin=knx_config[CONF_COLOR_TEMP_MIN],
|
||||
max_kelvin=knx_config[CONF_COLOR_TEMP_MAX],
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"""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,8 +36,13 @@ from .const import (
|
||||
)
|
||||
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .schema import SwitchSchema
|
||||
from .storage.const import CONF_ENTITY, CONF_GA_SWITCH
|
||||
from .storage.util import ConfigExtractor
|
||||
from .storage.const import (
|
||||
CONF_ENTITY,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_STATE,
|
||||
CONF_GA_SWITCH,
|
||||
CONF_GA_WRITE,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -137,13 +142,15 @@ 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=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),
|
||||
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],
|
||||
)
|
||||
|
||||
@@ -104,11 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -
|
||||
) as ex:
|
||||
await lcn_connection.async_close()
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"config_entry_title": config_entry.title,
|
||||
},
|
||||
f"Unable to connect to {config_entry.title}: {ex}"
|
||||
) from ex
|
||||
|
||||
_LOGGER.info('LCN connected to "%s"', config_entry.title)
|
||||
|
||||
@@ -26,7 +26,6 @@ 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
|
||||
|
||||
@@ -101,11 +100,7 @@ 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 HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_domain",
|
||||
translation_placeholders={CONF_DOMAIN: domain_name},
|
||||
)
|
||||
raise ValueError("Unknown domain")
|
||||
|
||||
|
||||
def generate_unique_id(
|
||||
@@ -309,8 +304,6 @@ 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 HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_length_of_states_string"
|
||||
)
|
||||
raise ValueError("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: done
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
|
||||
@@ -330,9 +330,8 @@ 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 ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_send_keys_action",
|
||||
raise ValueError(
|
||||
"Only hit command is allowed when sending deferred keys."
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit)
|
||||
@@ -369,9 +368,8 @@ class LockKeys(LcnServiceCall):
|
||||
|
||||
if (delay_time := service.data[CONF_TIME]) != 0:
|
||||
if table_id != 0:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_lock_keys_table",
|
||||
raise ValueError(
|
||||
"Only table A is allowed when locking keys for a specific time."
|
||||
)
|
||||
delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT])
|
||||
await device_connection.lock_keys_tab_a_temporary(
|
||||
|
||||
@@ -414,23 +414,11 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Unable to connect to {config_entry_title}."
|
||||
"invalid_address": {
|
||||
"message": "LCN device for given address has not been configured."
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"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."
|
||||
"message": "LCN device for given device ID has not been configured."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.2", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "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.2", "led-ble==1.1.7"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.1", "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 = entry.runtime_data
|
||||
data: LookinData = hass.data[DOMAIN][entry.entry_id]
|
||||
all_identifiers: set[tuple[str, str]] = {
|
||||
(DOMAIN, data.lookin_device.id),
|
||||
*((DOMAIN, remote["UUID"]) for remote in data.devices),
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.2.1"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==8.0.0"],
|
||||
"requirements": ["python-matter-server==7.0.0"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 (
|
||||
@@ -19,7 +18,6 @@ from homeassistant.components.number import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfLength,
|
||||
@@ -125,31 +123,6 @@ 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_native_value:
|
||||
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.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
@@ -266,26 +239,6 @@ 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,
|
||||
measurement_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
),
|
||||
ha_to_native_value=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(
|
||||
|
||||
@@ -180,9 +180,6 @@
|
||||
"altitude": {
|
||||
"name": "Altitude above sea level"
|
||||
},
|
||||
"pump_setpoint": {
|
||||
"name": "Setpoint"
|
||||
},
|
||||
"temperature_offset": {
|
||||
"name": "Temperature offset"
|
||||
},
|
||||
|
||||
@@ -32,18 +32,11 @@ 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:
|
||||
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_create_entry(
|
||||
title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -51,7 +44,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=[
|
||||
@@ -60,12 +53,10 @@ 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 | list[str], llm_context: llm.LLMContext
|
||||
hass: HomeAssistant, llm_api_id: str, llm_context: llm.LLMContext
|
||||
) -> Server:
|
||||
"""Create a new Model Context Protocol Server.
|
||||
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"llm_api_required": "At least one LLM API must be configured."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
|
||||
@@ -2,23 +2,34 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from medcom_ble import MedcomBleDeviceData
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
# Supported platforms
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Medcom BLE radiation monitor from a config entry."""
|
||||
|
||||
address = entry.unique_id
|
||||
elevation = hass.config.elevation
|
||||
is_metric = hass.config.units is METRIC_SYSTEM
|
||||
assert address is not None
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
@@ -27,7 +38,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
f"Could not find Medcom BLE device with address {address}"
|
||||
)
|
||||
|
||||
coordinator = MedcomBleUpdateCoordinator(hass, entry, address)
|
||||
async def _async_update_method():
|
||||
"""Get data from Medcom BLE radiation monitor."""
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address)
|
||||
inspector = MedcomBleDeviceData(_LOGGER, elevation, is_metric)
|
||||
|
||||
try:
|
||||
data = await inspector.update_device(ble_device)
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_method,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
"""The Medcom BLE integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from medcom_ble import MedcomBleDevice, MedcomBleDeviceData
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]):
|
||||
"""Coordinator for Medcom BLE radiation monitor data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._address = address
|
||||
self._elevation = hass.config.elevation
|
||||
self._is_metric = hass.config.units is METRIC_SYSTEM
|
||||
|
||||
async def _async_update_data(self) -> MedcomBleDevice:
|
||||
"""Get data from Medcom BLE radiation monitor."""
|
||||
ble_device = bluetooth.async_ble_device_from_address(self.hass, self._address)
|
||||
inspector = MedcomBleDeviceData(_LOGGER, self._elevation, self._is_metric)
|
||||
|
||||
try:
|
||||
data = await inspector.update_device(ble_device)
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
|
||||
return data
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from medcom_ble import MedcomBleDevice
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -13,10 +15,12 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, UNIT_CPM
|
||||
from .coordinator import MedcomBleUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +41,9 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Medcom BLE radiation monitor sensors."""
|
||||
|
||||
coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: DataUpdateCoordinator[MedcomBleDevice] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
|
||||
entities = []
|
||||
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)
|
||||
@@ -56,14 +62,16 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MedcomSensor(CoordinatorEntity[MedcomBleUpdateCoordinator], SensorEntity):
|
||||
class MedcomSensor(
|
||||
CoordinatorEntity[DataUpdateCoordinator[MedcomBleDevice]], SensorEntity
|
||||
):
|
||||
"""Medcom BLE radiation monitor sensors for the device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MedcomBleUpdateCoordinator,
|
||||
coordinator: DataUpdateCoordinator[MedcomBleDevice],
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Populate the medcom entity with relevant data."""
|
||||
|
||||
@@ -30,7 +30,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
MEDIA_CLASS_MAP,
|
||||
MEDIA_MIME_TYPES,
|
||||
MEDIA_SOURCE_DATA,
|
||||
URI_SCHEME,
|
||||
URI_SCHEME_REGEX,
|
||||
)
|
||||
@@ -79,7 +78,7 @@ def generate_media_source_id(domain: str, identifier: str) -> str:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the media_source component."""
|
||||
hass.data[MEDIA_SOURCE_DATA] = {}
|
||||
hass.data[DOMAIN] = {}
|
||||
websocket_api.async_register_command(hass, websocket_browse_media)
|
||||
websocket_api.async_register_command(hass, websocket_resolve_media)
|
||||
frontend.async_register_built_in_panel(
|
||||
@@ -98,7 +97,7 @@ async def _process_media_source_platform(
|
||||
platform: MediaSourceProtocol,
|
||||
) -> None:
|
||||
"""Process a media source platform."""
|
||||
hass.data[MEDIA_SOURCE_DATA][domain] = await platform.async_get_media_source(hass)
|
||||
hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -110,10 +109,10 @@ def _get_media_item(
|
||||
item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player)
|
||||
else:
|
||||
# We default to our own domain if its only one registered
|
||||
domain = None if len(hass.data[MEDIA_SOURCE_DATA]) > 1 else DOMAIN
|
||||
domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN
|
||||
return MediaSourceItem(hass, domain, "", target_media_player)
|
||||
|
||||
if item.domain is not None and item.domain not in hass.data[MEDIA_SOURCE_DATA]:
|
||||
if item.domain is not None and item.domain not in hass.data[DOMAIN]:
|
||||
raise UnknownMediaSource(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unknown_media_source",
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
"""Constants for the media_source integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.media_player import MediaClass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .models import MediaSource
|
||||
|
||||
DOMAIN = "media_source"
|
||||
MEDIA_SOURCE_DATA: HassKey[dict[str, MediaSource]] = HassKey(DOMAIN)
|
||||
MEDIA_MIME_TYPES = ("audio", "video", "image")
|
||||
MEDIA_CLASS_MAP = {
|
||||
"audio": MediaClass.MUSIC,
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_request import FileField
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES, MEDIA_SOURCE_DATA
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
|
||||
from .error import Unresolvable
|
||||
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
||||
|
||||
@@ -30,7 +30,7 @@ LOGGER = logging.getLogger(__name__)
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up local media source."""
|
||||
source = LocalSource(hass)
|
||||
hass.data[MEDIA_SOURCE_DATA][DOMAIN] = source
|
||||
hass.data[DOMAIN][DOMAIN] = source
|
||||
hass.http.register_view(LocalMediaView(hass, source))
|
||||
hass.http.register_view(UploadMediaView(hass, source))
|
||||
websocket_api.async_register_command(hass, websocket_remove_media)
|
||||
@@ -352,7 +352,7 @@ async def websocket_remove_media(
|
||||
connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
|
||||
return
|
||||
|
||||
source = cast(LocalSource, hass.data[MEDIA_SOURCE_DATA][DOMAIN])
|
||||
source: LocalSource = hass.data[DOMAIN][DOMAIN]
|
||||
|
||||
try:
|
||||
source_dir_id, location = source.async_parse_identifier(item)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
|
||||
from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -70,7 +70,7 @@ class MediaSourceItem:
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for source in self.hass.data[MEDIA_SOURCE_DATA].values()
|
||||
for source in self.hass.data[DOMAIN].values()
|
||||
),
|
||||
key=lambda item: item.title,
|
||||
)
|
||||
@@ -85,9 +85,7 @@ class MediaSourceItem:
|
||||
@callback
|
||||
def async_media_source(self) -> MediaSource:
|
||||
"""Return media source that owns this item."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.domain is not None
|
||||
return self.hass.data[MEDIA_SOURCE_DATA][self.domain]
|
||||
return cast(MediaSource, self.hass.data[DOMAIN][self.domain])
|
||||
|
||||
@classmethod
|
||||
def from_uri(
|
||||
|
||||
@@ -27,11 +27,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
type MelCloudConfigEntry = ConfigEntry[dict[str, list[MelCloudDevice]]]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) -> bool:
|
||||
"""Establish connection with MELCloud."""
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Establish connection with MELClooud."""
|
||||
conf = entry.data
|
||||
try:
|
||||
mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN])
|
||||
@@ -42,14 +40,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) ->
|
||||
except (TimeoutError, ClientConnectionError) as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
entry.runtime_data = mel_devices
|
||||
hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices})
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
return unload_ok
|
||||
|
||||
|
||||
class MelCloudDevice:
|
||||
|
||||
@@ -24,12 +24,13 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from . import MelCloudDevice
|
||||
from .const import (
|
||||
ATTR_STATUS,
|
||||
ATTR_VANE_HORIZONTAL,
|
||||
@@ -37,6 +38,7 @@ from .const import (
|
||||
ATTR_VANE_VERTICAL,
|
||||
ATTR_VANE_VERTICAL_POSITIONS,
|
||||
CONF_POSITION,
|
||||
DOMAIN,
|
||||
SERVICE_SET_VANE_HORIZONTAL,
|
||||
SERVICE_SET_VANE_VERTICAL,
|
||||
)
|
||||
@@ -75,11 +77,11 @@ ATW_ZONE_HVAC_ACTION_LOOKUP = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MelCloudConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MelCloud device climate based on config_entry."""
|
||||
mel_devices = entry.runtime_data
|
||||
mel_devices = hass.data[DOMAIN][entry.entry_id]
|
||||
entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [
|
||||
AtaDeviceClimate(mel_device, mel_device.device)
|
||||
for mel_device in mel_devices[DEVICE_TYPE_ATA]
|
||||
|
||||
@@ -5,12 +5,11 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import MelCloudConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_USERNAME,
|
||||
CONF_TOKEN,
|
||||
@@ -18,7 +17,7 @@ TO_REDACT = {
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: MelCloudConfigEntry
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for the config entry."""
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
@@ -15,11 +15,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from . import MelCloudDevice
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||
@@ -103,11 +105,11 @@ ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MelCloudConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MELCloud device sensors based on config_entry."""
|
||||
mel_devices = entry.runtime_data
|
||||
mel_devices = hass.data[DOMAIN].get(entry.entry_id)
|
||||
|
||||
entities: list[MelDeviceSensor] = [
|
||||
MelDeviceSensor(mel_device, description)
|
||||
|
||||
@@ -17,21 +17,22 @@ from homeassistant.components.water_heater import (
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import MelCloudConfigEntry, MelCloudDevice
|
||||
from . import DOMAIN, MelCloudDevice
|
||||
from .const import ATTR_STATUS
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: MelCloudConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up MelCloud device climate based on config_entry."""
|
||||
mel_devices = entry.runtime_data
|
||||
mel_devices = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
[
|
||||
AtwWaterHeater(mel_device, mel_device.device)
|
||||
|
||||
@@ -6,11 +6,13 @@ from melnor_bluetooth.device import Device
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.NUMBER,
|
||||
@@ -20,8 +22,11 @@ PLATFORMS: list[Platform] = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up melnor from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {})
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, entry.data[CONF_ADDRESS])
|
||||
|
||||
if not ble_device:
|
||||
@@ -55,14 +60,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bo
|
||||
coordinator = MelnorDataUpdateCoordinator(hass, entry, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MelnorConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.data.disconnect()
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
device: Device = hass.data[DOMAIN][entry.entry_id].data
|
||||
|
||||
await device.disconnect()
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -11,17 +11,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MelnorConfigEntry = ConfigEntry[MelnorDataUpdateCoordinator]
|
||||
|
||||
|
||||
class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
||||
"""Melnor data update coordinator."""
|
||||
|
||||
config_entry: MelnorConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
_device: Device
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: MelnorConfigEntry, device: Device
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
|
||||
) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -13,11 +13,13 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -65,12 +67,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MelnorConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the number platform."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -25,7 +26,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorBluetoothEntity, MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -102,12 +104,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MelnorConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
# Device-level sensors
|
||||
async_add_entities(
|
||||
|
||||
@@ -13,10 +13,12 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -49,12 +51,12 @@ ZONE_ENTITY_DESCRIPTIONS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MelnorConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the switch platform."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
||||
@@ -10,11 +10,13 @@ from typing import Any
|
||||
from melnor_bluetooth.device import Valve
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import MelnorConfigEntry, MelnorDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MelnorDataUpdateCoordinator
|
||||
from .entity import MelnorZoneEntity, get_entities_for_valves
|
||||
|
||||
|
||||
@@ -39,12 +41,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneTimeEntityDescription] = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MelnorConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the number platform."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
get_entities_for_valves(
|
||||
|
||||
@@ -1,21 +1,59 @@
|
||||
"""The met_eireann component."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Self
|
||||
|
||||
import meteireann
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MetEireannUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||
|
||||
PLATFORMS = [Platform.WEATHER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Met Éireann as config entry."""
|
||||
coordinator = MetEireannUpdateCoordinator(hass, config_entry=config_entry)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
raw_weather_data = meteireann.WeatherData(
|
||||
async_get_clientsession(hass),
|
||||
latitude=config_entry.data[CONF_LATITUDE],
|
||||
longitude=config_entry.data[CONF_LONGITUDE],
|
||||
altitude=config_entry.data[CONF_ELEVATION],
|
||||
)
|
||||
|
||||
weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
|
||||
|
||||
async def _async_update_data() -> MetEireannWeatherData:
|
||||
"""Fetch data from Met Éireann."""
|
||||
try:
|
||||
return await weather_data.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_method=_async_update_data,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@@ -30,3 +68,26 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class MetEireannWeatherData:
|
||||
"""Keep data for Met Éireann weather entities."""
|
||||
|
||||
def __init__(
|
||||
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
|
||||
) -> None:
|
||||
"""Initialise the weather entity data."""
|
||||
self._config = config
|
||||
self._weather_data = weather_data
|
||||
self.current_weather_data: dict[str, Any] = {}
|
||||
self.daily_forecast: list[dict[str, Any]] = []
|
||||
self.hourly_forecast: list[dict[str, Any]] = []
|
||||
|
||||
async def fetch_data(self) -> Self:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
"""The met_eireann component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Self
|
||||
|
||||
import meteireann
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UPDATE_INTERVAL = timedelta(minutes=60)
|
||||
|
||||
|
||||
class MetEireannWeatherData:
|
||||
"""Keep data for Met Éireann weather entities."""
|
||||
|
||||
def __init__(
|
||||
self, config: Mapping[str, Any], weather_data: meteireann.WeatherData
|
||||
) -> None:
|
||||
"""Initialise the weather entity data."""
|
||||
self._config = config
|
||||
self._weather_data = weather_data
|
||||
self.current_weather_data: dict[str, Any] = {}
|
||||
self.daily_forecast: list[dict[str, Any]] = []
|
||||
self.hourly_forecast: list[dict[str, Any]] = []
|
||||
|
||||
async def fetch_data(self) -> Self:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.get_default_time_zone()
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
self.hourly_forecast = self._weather_data.get_forecast(time_zone, True)
|
||||
return self
|
||||
|
||||
|
||||
class MetEireannUpdateCoordinator(DataUpdateCoordinator[MetEireannWeatherData]):
|
||||
"""Coordinator for Met Éireann weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
raw_weather_data = meteireann.WeatherData(
|
||||
async_get_clientsession(hass),
|
||||
latitude=config_entry.data[CONF_LATITUDE],
|
||||
longitude=config_entry.data[CONF_LONGITUDE],
|
||||
altitude=config_entry.data[CONF_ELEVATION],
|
||||
)
|
||||
self._weather_data = MetEireannWeatherData(config_entry.data, raw_weather_data)
|
||||
|
||||
async def _async_update_data(self) -> MetEireannWeatherData:
|
||||
"""Fetch data from Met Éireann."""
|
||||
try:
|
||||
return await self._weather_data.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}") from err
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Met Éireann weather service."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
@@ -28,8 +29,10 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MetEireannWeatherData
|
||||
from .const import CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP
|
||||
from .coordinator import MetEireannWeatherData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_condition(condition: str | None) -> str | None:
|
||||
|
||||
@@ -23,6 +23,7 @@ from .const import (
|
||||
COORDINATOR_RAIN,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -129,9 +130,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.title,
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
COORDINATOR_FORECAST: coordinator_forecast,
|
||||
}
|
||||
if coordinator_rain and coordinator_rain.last_update_success:
|
||||
@@ -161,6 +163,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
@@ -26,6 +26,7 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
COORDINATOR_FORECAST = "coordinator_forecast"
|
||||
COORDINATOR_RAIN = "coordinator_rain"
|
||||
COORDINATOR_ALERT = "coordinator_alert"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
ATTRIBUTION = "Data provided by Météo-France"
|
||||
MODEL = "Météo-France mobile API"
|
||||
MANUFACTURER = "Météo-France"
|
||||
|
||||
@@ -6,8 +6,6 @@ import time
|
||||
from meteofrance_api.model.forecast import Forecast as MeteoFranceForecast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_HUMIDITY,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||
@@ -51,13 +49,9 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def format_condition(condition: str, force_day: bool = False) -> str:
|
||||
def format_condition(condition: str):
|
||||
"""Return condition from dict CONDITION_MAP."""
|
||||
mapped_condition = CONDITION_MAP.get(condition, condition)
|
||||
if force_day and mapped_condition == ATTR_CONDITION_CLEAR_NIGHT:
|
||||
# Meteo-France can return clear night condition instead of sunny for daily weather, so we map it to sunny
|
||||
return ATTR_CONDITION_SUNNY
|
||||
return mapped_condition
|
||||
return CONDITION_MAP.get(condition, condition)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -218,7 +212,7 @@ class MeteoFranceWeather(
|
||||
forecast["dt"]
|
||||
).isoformat(),
|
||||
ATTR_FORECAST_CONDITION: format_condition(
|
||||
forecast["weather12H"]["desc"], force_day=True
|
||||
forecast["weather12H"]["desc"]
|
||||
),
|
||||
ATTR_FORECAST_HUMIDITY: forecast["humidity"]["max"],
|
||||
ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"],
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
import logging
|
||||
|
||||
from meteoclimatic import MeteoclimaticClient
|
||||
from meteoclimatic.exceptions import MeteoclimaticError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
from .const import CONF_STATION_CODE, DOMAIN, PLATFORMS, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Meteoclimatic entry."""
|
||||
coordinator = MeteoclimaticUpdateCoordinator(hass, entry)
|
||||
station_code = entry.data[CONF_STATION_CODE]
|
||||
meteoclimatic_client = MeteoclimaticClient()
|
||||
|
||||
async def async_update_data():
|
||||
"""Obtain the latest data from Meteoclimatic."""
|
||||
try:
|
||||
data = await hass.async_add_executor_job(
|
||||
meteoclimatic_client.weather_at_station, station_code
|
||||
)
|
||||
except MeteoclimaticError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
return data.__dict__
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meteoclimatic weather for {entry.title} ({station_code})",
|
||||
update_method=async_update_data,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Support for Meteoclimatic weather data."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from meteoclimatic import MeteoclimaticClient
|
||||
from meteoclimatic.exceptions import MeteoclimaticError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_STATION_CODE, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for Meteoclimatic weather data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self._station_code = entry.data[CONF_STATION_CODE]
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meteoclimatic weather for {entry.title} ({self._station_code})",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self._meteoclimatic_client = MeteoclimaticClient()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Obtain the latest data from Meteoclimatic."""
|
||||
try:
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self._meteoclimatic_client.weather_at_station, self._station_code
|
||||
)
|
||||
except MeteoclimaticError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
return data.__dict__
|
||||
@@ -18,10 +18,12 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@@ -117,7 +119,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic sensor platform."""
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
[MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES],
|
||||
@@ -125,17 +127,13 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class MeteoclimaticSensor(
|
||||
CoordinatorEntity[MeteoclimaticUpdateCoordinator], SensorEntity
|
||||
):
|
||||
class MeteoclimaticSensor(CoordinatorEntity, SensorEntity):
|
||||
"""Representation of a Meteoclimatic sensor."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MeteoclimaticUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the Meteoclimatic sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -8,10 +8,12 @@ from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL
|
||||
from .coordinator import MeteoclimaticUpdateCoordinator
|
||||
|
||||
|
||||
def format_condition(condition):
|
||||
@@ -29,14 +31,12 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Meteoclimatic weather platform."""
|
||||
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities([MeteoclimaticWeather(coordinator)], False)
|
||||
|
||||
|
||||
class MeteoclimaticWeather(
|
||||
CoordinatorEntity[MeteoclimaticUpdateCoordinator], WeatherEntity
|
||||
):
|
||||
class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
@@ -44,7 +44,7 @@ class MeteoclimaticWeather(
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
|
||||
def __init__(self, coordinator: MeteoclimaticUpdateCoordinator) -> None:
|
||||
def __init__(self, coordinator: DataUpdateCoordinator) -> None:
|
||||
"""Initialise the weather platform."""
|
||||
super().__init__(coordinator)
|
||||
self._unique_id = self.coordinator.data["station"].code
|
||||
|
||||
@@ -9,7 +9,6 @@ from datapoint.Forecast import Forecast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
EntityCategory,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
@@ -60,7 +59,6 @@ SENSOR_TYPES: tuple[MetOfficeSensorEntityDescription, ...] = (
|
||||
native_attr_name="name",
|
||||
name="Station name",
|
||||
icon="mdi:label-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
MetOfficeSensorEntityDescription(
|
||||
@@ -237,13 +235,14 @@ class MetOfficeCurrentSensor(
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
native_attr = self.entity_description.native_attr_name
|
||||
value = get_attribute(
|
||||
self.coordinator.data.now(), self.entity_description.native_attr_name
|
||||
)
|
||||
|
||||
if native_attr == "name":
|
||||
return str(self.coordinator.data.name)
|
||||
|
||||
value = get_attribute(self.coordinator.data.now(), native_attr)
|
||||
if native_attr == "significantWeatherCode" and value is not None:
|
||||
if (
|
||||
self.entity_description.native_attr_name == "significantWeatherCode"
|
||||
and value is not None
|
||||
):
|
||||
value = CONDITION_MAP.get(value)
|
||||
|
||||
return value
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user