mirror of
https://github.com/home-assistant/core.git
synced 2025-09-29 06:49:31 +00:00
Compare commits
1 Commits
fix-radio-
...
state_temp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
be1ba31ff2 |
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
|
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 3
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.8"
|
||||
|
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']
|
||||
});
|
@@ -381,7 +381,6 @@ homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
homeassistant.components.opower.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
|
@@ -76,7 +76,6 @@ from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
condition,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
@@ -453,7 +452,6 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
|
@@ -1,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)}>"
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.2.3"]
|
||||
"requirements": ["aioamazondevices==3.2.2"]
|
||||
}
|
||||
|
@@ -56,7 +56,7 @@ SERVICE_UPLOAD = "upload"
|
||||
ANDROIDTV_STATES = {
|
||||
"off": MediaPlayerState.OFF,
|
||||
"idle": MediaPlayerState.IDLE,
|
||||
"standby": MediaPlayerState.IDLE,
|
||||
"standby": MediaPlayerState.STANDBY,
|
||||
"playing": MediaPlayerState.PLAYING,
|
||||
"paused": MediaPlayerState.PAUSED,
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -191,7 +191,7 @@ class AppleTvMediaPlayer(
|
||||
self._is_feature_available(FeatureName.PowerState)
|
||||
and self.atv.power.power_state == PowerState.Off
|
||||
):
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
if self._playing:
|
||||
state = self._playing.device_state
|
||||
if state in (DeviceState.Idle, DeviceState.Loading):
|
||||
@@ -200,7 +200,7 @@ class AppleTvMediaPlayer(
|
||||
return MediaPlayerState.PLAYING
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return MediaPlayerState.PAUSED
|
||||
return MediaPlayerState.IDLE # Bad or unknown state?
|
||||
return MediaPlayerState.STANDBY # Bad or unknown state?
|
||||
return None
|
||||
|
||||
@callback
|
||||
|
@@ -72,7 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): _media_id_validator,
|
||||
vol.Optional("preannounce", 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"): [
|
||||
{
|
||||
|
@@ -107,7 +107,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
"""Return the state of the device."""
|
||||
media_state = self.client.play_state.state
|
||||
if media_state == "NETWORK":
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
if self.client.state.power:
|
||||
if media_state == "play":
|
||||
return MediaPlayerState.PLAYING
|
||||
|
@@ -94,7 +94,6 @@ async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict:
|
||||
max=6,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
unit_of_measurement="decimals",
|
||||
translation_key="round",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_TIME_WINDOW): selector.DurationSelector(),
|
||||
|
@@ -52,11 +52,6 @@
|
||||
"h": "Hours",
|
||||
"d": "Days"
|
||||
}
|
||||
},
|
||||
"round": {
|
||||
"unit_of_measurement": {
|
||||
"decimals": "decimals"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,12 +10,7 @@ from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.hub import EheimDigitalHub
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -131,52 +126,3 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the config entry."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_RECONFIGURE, data_schema=CONFIG_SCHEMA
|
||||
)
|
||||
|
||||
self._async_abort_entries_match(user_input)
|
||||
errors: dict[str, str] = {}
|
||||
hub = EheimDigitalHub(
|
||||
host=user_input[CONF_HOST],
|
||||
session=async_get_clientsession(self.hass),
|
||||
loop=self.hass.loop,
|
||||
main_device_added_event=self.main_device_added_event,
|
||||
)
|
||||
|
||||
try:
|
||||
await hub.connect()
|
||||
|
||||
async with asyncio.timeout(2):
|
||||
# This event gets triggered when the first message is received from
|
||||
# the device, it contains the data necessary to create the main device.
|
||||
# This removes the race condition where the main device is accessed
|
||||
# before the response from the device is parsed.
|
||||
await self.main_device_added_event.wait()
|
||||
if TYPE_CHECKING:
|
||||
# At this point the main device is always set
|
||||
assert isinstance(hub.main, EheimDigitalDevice)
|
||||
await self.async_set_unique_id(hub.main.mac_address)
|
||||
await hub.close()
|
||||
except (ClientError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
LOGGER.exception("Unknown exception occurred")
|
||||
else:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_RECONFIGURE,
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
@@ -60,7 +60,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
|
@@ -4,14 +4,6 @@
|
||||
"discovery_confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::eheimdigital::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
@@ -23,9 +15,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "The identifier does not match the previous identifier"
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -126,7 +126,6 @@ class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity):
|
||||
name=f"Encharge {serial_number}",
|
||||
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -159,7 +158,6 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity):
|
||||
name=f"Enpower {enpower.serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=enpower.serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.2.0"],
|
||||
"requirements": ["pyenphase==2.1.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@@ -165,7 +165,6 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign numbers to Envoy itself
|
||||
|
@@ -223,7 +223,6 @@ class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign selects to Envoy itself
|
||||
|
@@ -1313,7 +1313,6 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||
manufacturer="Enphase",
|
||||
model="Inverter",
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1357,7 +1356,6 @@ class EnvoyEnchargeEntity(EnvoySensorBaseEntity):
|
||||
name=f"Encharge {serial_number}",
|
||||
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
|
||||
@@ -1422,7 +1420,6 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
|
||||
name=f"Enpower {enpower_data.serial_number}",
|
||||
sw_version=str(enpower_data.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=enpower_data.serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -138,7 +138,6 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -236,7 +235,6 @@ class EnvoyStorageSettingsSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign switches to Envoy itself
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250702.1"]
|
||||
"requirements": ["home-assistant-frontend==20250702.0"]
|
||||
}
|
||||
|
@@ -2,10 +2,6 @@
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
AUTH_CALLBACK_PATH,
|
||||
MY_AUTH_CALLBACK_PATH,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
@@ -18,14 +14,12 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
if "my" in hass.config.components:
|
||||
redirect_url = MY_AUTH_CALLBACK_PATH
|
||||
else:
|
||||
ha_host = hass.config.external_url or "https://YOUR_DOMAIN:PORT"
|
||||
redirect_url = f"{ha_host}{AUTH_CALLBACK_PATH}"
|
||||
return {
|
||||
"oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
|
||||
"more_info_url": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
|
||||
"oauth_consent_url": (
|
||||
"https://console.cloud.google.com/apis/credentials/consent"
|
||||
),
|
||||
"more_info_url": (
|
||||
"https://www.home-assistant.io/integrations/google_assistant_sdk/"
|
||||
),
|
||||
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
|
||||
"redirect_url": redirect_url,
|
||||
}
|
||||
|
@@ -46,7 +46,7 @@
|
||||
}
|
||||
},
|
||||
"application_credentials": {
|
||||
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n1. Add `{redirect_url}` under *Authorized redirect URI*."
|
||||
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type."
|
||||
},
|
||||
"services": {
|
||||
"send_text_command": {
|
||||
|
@@ -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(
|
||||
@@ -460,5 +443,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": {
|
||||
|
@@ -95,16 +95,21 @@ def get_recurrence_rule(recurrence: rrule) -> str:
|
||||
|
||||
'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
|
||||
|
||||
Args:
|
||||
recurrence: An RRULE object.
|
||||
Parameters
|
||||
----------
|
||||
recurrence : rrule
|
||||
An RRULE object.
|
||||
|
||||
Returns:
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The recurrence rule portion of the RRULE string, starting with 'FREQ='.
|
||||
|
||||
Example:
|
||||
>>> rule = get_recurrence_rule(task)
|
||||
>>> print(rule)
|
||||
'FREQ=YEARLY;INTERVAL=2'
|
||||
Example
|
||||
-------
|
||||
>>> rule = get_recurrence_rule(task)
|
||||
>>> print(rule)
|
||||
'FREQ=YEARLY;INTERVAL=2'
|
||||
|
||||
"""
|
||||
return str(recurrence).split("RRULE:")[1]
|
||||
|
@@ -64,7 +64,7 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N
|
||||
"""Initialize bans when app starts up."""
|
||||
await app[KEY_BAN_MANAGER].async_load()
|
||||
|
||||
app.on_startup.append(ban_startup) # type: ignore[arg-type]
|
||||
app.on_startup.append(ban_startup)
|
||||
|
||||
|
||||
@middleware
|
||||
|
@@ -3,6 +3,9 @@
|
||||
"binary_sensor": {
|
||||
"leaving_dock": {
|
||||
"default": "mdi:debug-step-out"
|
||||
},
|
||||
"returning_to_dock": {
|
||||
"default": "mdi:debug-step-into"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -45,26 +48,6 @@
|
||||
"work_area_progress": {
|
||||
"default": "mdi:collage"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"my_lawn_work_area": {
|
||||
"default": "mdi:square-outline",
|
||||
"state": {
|
||||
"on": "mdi:square"
|
||||
}
|
||||
},
|
||||
"work_area_work_area": {
|
||||
"default": "mdi:square-outline",
|
||||
"state": {
|
||||
"on": "mdi:square"
|
||||
}
|
||||
},
|
||||
"stay_out_zones": {
|
||||
"default": "mdi:rhombus-outline",
|
||||
"state": {
|
||||
"on": "mdi:rhombus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==1.2.0"]
|
||||
"requirements": ["aioautomower==1.0.1"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.7.0"]
|
||||
"requirements": ["pydrawise==2025.6.0"]
|
||||
}
|
||||
|
@@ -136,7 +136,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the media player off."""
|
||||
await self._async_send_command(self._power_off_command)
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
@@ -159,5 +159,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||
state = status[0]
|
||||
mute = status[2]
|
||||
|
||||
self._attr_state = MediaPlayerState.ON if state == "1" else MediaPlayerState.OFF
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if state == "1" else MediaPlayerState.STANDBY
|
||||
)
|
||||
self._attr_is_volume_muted = mute == "0"
|
||||
|
@@ -54,7 +54,7 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity):
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value in (None, NullValue):
|
||||
value = None
|
||||
elif value_convert := self.entity_description.device_to_ha:
|
||||
elif value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
if TYPE_CHECKING:
|
||||
value = cast(bool | None, value)
|
||||
@@ -70,7 +70,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="HueMotionSensor",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
|
||||
@@ -83,7 +83,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="OccupancySensor",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
# The first bit = if occupied
|
||||
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
|
||||
@@ -94,7 +94,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="BatteryChargeLevel",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: x
|
||||
measurement_to_ha=lambda x: x
|
||||
!= clusters.PowerSource.Enums.BatChargeLevelEnum.kOk,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
@@ -109,7 +109,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ContactSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
# value is inverted on matter to what we expect
|
||||
device_to_ha=lambda x: not x,
|
||||
measurement_to_ha=lambda x: not x,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
|
||||
@@ -153,7 +153,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="LockDoorStateSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
|
||||
@@ -168,7 +168,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmDeviceMutedSensor",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.SmokeCoAlarm.Enums.MuteStateEnum.kMuted
|
||||
),
|
||||
translation_key="muted",
|
||||
@@ -181,7 +181,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmEndfOfServiceSensor",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.SmokeCoAlarm.Enums.EndOfServiceEnum.kExpired
|
||||
),
|
||||
translation_key="end_of_service",
|
||||
@@ -195,7 +195,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmBatteryAlertSensor",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="battery_alert",
|
||||
@@ -232,7 +232,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmSmokeStateSensor",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
),
|
||||
@@ -244,7 +244,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmInterconnectSmokeAlarmSensor",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="interconnected_smoke_alarm",
|
||||
@@ -257,7 +257,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmInterconnectCOAlarmSensor",
|
||||
device_class=BinarySensorDeviceClass.CO,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="interconnected_co_alarm",
|
||||
@@ -271,7 +271,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvseChargingStatusSensor",
|
||||
translation_key="evse_charging_status",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: False,
|
||||
@@ -291,7 +291,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvsePlugStateSensor",
|
||||
translation_key="evse_plug_state",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: True,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: True,
|
||||
@@ -311,7 +311,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvseSupplyStateSensor",
|
||||
translation_key="evse_supply_charging_state",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
|
||||
@@ -327,7 +327,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="WaterHeaterManagementBoostStateSensor",
|
||||
translation_key="boost_state",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive
|
||||
),
|
||||
),
|
||||
@@ -342,7 +342,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
# DeviceFault or SupplyFault bit enabled
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kDeviceFault: True,
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSupplyFault: True,
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedLow: False,
|
||||
@@ -366,7 +366,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PumpStatusRunning",
|
||||
translation_key="pump_running",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x
|
||||
== clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning
|
||||
),
|
||||
@@ -384,7 +384,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="dishwasher_alarm_inflow",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.DishwasherAlarm.Bitmaps.AlarmBitmap.kInflowError
|
||||
),
|
||||
),
|
||||
@@ -399,7 +399,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="dishwasher_alarm_door",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.DishwasherAlarm.Bitmaps.AlarmBitmap.kDoorError
|
||||
),
|
||||
),
|
||||
|
@@ -59,8 +59,8 @@ class MatterEntityDescription(EntityDescription):
|
||||
"""Describe the Matter entity."""
|
||||
|
||||
# convert the value from the primary attribute to the value used by HA
|
||||
device_to_ha: Callable[[Any], Any] | None = None
|
||||
ha_to_device: Callable[[Any], Any] | None = None
|
||||
measurement_to_ha: Callable[[Any], Any] | None = None
|
||||
ha_to_native_value: Callable[[Any], Any] | None = None
|
||||
command_timeout: int | None = None
|
||||
|
||||
|
||||
|
@@ -55,7 +55,7 @@ class MatterRangeNumberEntityDescription(
|
||||
):
|
||||
"""Describe Matter Number Input entities with min and max values."""
|
||||
|
||||
ha_to_device: Callable[[Any], Any]
|
||||
ha_to_native_value: Callable[[Any], Any]
|
||||
|
||||
# attribute descriptors to get the min and max value
|
||||
min_attribute: type[ClusterAttributeDescriptor]
|
||||
@@ -74,7 +74,7 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
sendvalue = int(value)
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
sendvalue = value_convert(value)
|
||||
await self.write_attribute(
|
||||
value=sendvalue,
|
||||
@@ -84,7 +84,7 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -96,7 +96,7 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
send_value = self.entity_description.ha_to_device(value)
|
||||
send_value = self.entity_description.ha_to_native_value(value)
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(send_value),
|
||||
@@ -106,7 +106,7 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
self._attr_native_min_value = (
|
||||
@@ -133,7 +133,7 @@ class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set level value."""
|
||||
send_value = int(value)
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
send_value = value_convert(value)
|
||||
await self.send_device_command(
|
||||
clusters.LevelControl.Commands.MoveToLevel(
|
||||
@@ -145,7 +145,7 @@ class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -162,8 +162,8 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=0,
|
||||
mode=NumberMode.BOX,
|
||||
# use 255 to indicate that the value should revert to the default
|
||||
device_to_ha=lambda x: 255 if x is None else x,
|
||||
ha_to_device=lambda x: None if x == 255 else int(x),
|
||||
measurement_to_ha=lambda x: 255 if x is None else x,
|
||||
ha_to_native_value=lambda x: None if x == 255 else int(x),
|
||||
native_step=1,
|
||||
native_unit_of_measurement=None,
|
||||
),
|
||||
@@ -180,8 +180,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="on_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -199,8 +199,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="off_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -218,8 +218,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="on_off_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -256,8 +256,8 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=-50,
|
||||
native_step=0.5,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
@@ -275,10 +275,10 @@ DISCOVERY_SCHEMAS = [
|
||||
native_max_value=100,
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
device_to_ha=(
|
||||
measurement_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
),
|
||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
entity_class=MatterLevelControlNumber,
|
||||
@@ -326,8 +326,8 @@ DISCOVERY_SCHEMAS = [
|
||||
targetTemperature=value
|
||||
),
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_device=lambda x: round(x * 100),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_native_value=lambda x: round(x * 100),
|
||||
min_attribute=clusters.TemperatureControl.Attributes.MinTemperature,
|
||||
max_attribute=clusters.TemperatureControl.Attributes.MaxTemperature,
|
||||
mode=NumberMode.SLIDER,
|
||||
|
@@ -71,8 +71,8 @@ class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescrip
|
||||
class MatterMapSelectEntityDescription(MatterSelectEntityDescription):
|
||||
"""Describe Matter select entities for MatterMapSelectEntityDescription."""
|
||||
|
||||
device_to_ha: Callable[[int], str | None]
|
||||
ha_to_device: Callable[[str], int | None]
|
||||
measurement_to_ha: Callable[[int], str | None]
|
||||
ha_to_native_value: Callable[[str], int | None]
|
||||
|
||||
# list attribute: the attribute descriptor to get the list of values (= list of integers)
|
||||
list_attribute: type[ClusterAttributeDescriptor]
|
||||
@@ -97,7 +97,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected mode."""
|
||||
value_convert = self.entity_description.ha_to_device
|
||||
value_convert = self.entity_description.ha_to_native_value
|
||||
if TYPE_CHECKING:
|
||||
assert value_convert is not None
|
||||
await self.write_attribute(
|
||||
@@ -109,7 +109,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
"""Update from device."""
|
||||
value: Nullable | int | None
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
value_convert = self.entity_description.device_to_ha
|
||||
value_convert = self.entity_description.measurement_to_ha
|
||||
if TYPE_CHECKING:
|
||||
assert value_convert is not None
|
||||
self._attr_current_option = value_convert(value)
|
||||
@@ -132,7 +132,7 @@ class MatterMapSelectEntity(MatterAttributeSelectEntity):
|
||||
self._attr_options = [
|
||||
mapped_value
|
||||
for value in available_values
|
||||
if (mapped_value := self.entity_description.device_to_ha(value))
|
||||
if (mapped_value := self.entity_description.measurement_to_ha(value))
|
||||
]
|
||||
# use base implementation from MatterAttributeSelectEntity to set the current option
|
||||
super()._update_from_device()
|
||||
@@ -333,13 +333,13 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="startup_on_off",
|
||||
options=["on", "off", "toggle", "previous"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "off",
|
||||
1: "on",
|
||||
2: "toggle",
|
||||
None: "previous",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"off": 0,
|
||||
"on": 1,
|
||||
"toggle": 2,
|
||||
@@ -358,12 +358,12 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="sensitivity_level",
|
||||
options=["high", "standard", "low"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "high",
|
||||
1: "standard",
|
||||
2: "low",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"high": 0,
|
||||
"standard": 1,
|
||||
"low": 2,
|
||||
@@ -379,11 +379,11 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="temperature_display_mode",
|
||||
options=["Celsius", "Fahrenheit"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "Celsius",
|
||||
1: "Fahrenheit",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"Celsius": 0,
|
||||
"Fahrenheit": 1,
|
||||
}.get,
|
||||
@@ -432,8 +432,8 @@ DISCOVERY_SCHEMAS = [
|
||||
key="MatterLaundryWasherNumberOfRinses",
|
||||
translation_key="laundry_washer_number_of_rinses",
|
||||
list_attribute=clusters.LaundryWasherControls.Attributes.SupportedRinses,
|
||||
device_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
|
||||
ha_to_device=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
|
||||
measurement_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
|
||||
ha_to_native_value=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
|
||||
),
|
||||
entity_class=MatterMapSelectEntity,
|
||||
required_attributes=(
|
||||
@@ -450,13 +450,13 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="door_lock_sound_volume",
|
||||
options=["silent", "low", "medium", "high"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "silent",
|
||||
1: "low",
|
||||
3: "medium",
|
||||
2: "high",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"silent": 0,
|
||||
"low": 1,
|
||||
"medium": 3,
|
||||
@@ -472,8 +472,8 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PumpConfigurationAndControlOperationMode",
|
||||
translation_key="pump_operation_mode",
|
||||
options=list(PUMP_OPERATION_MODE_MAP.values()),
|
||||
device_to_ha=PUMP_OPERATION_MODE_MAP.get,
|
||||
ha_to_device=PUMP_OPERATION_MODE_MAP_REVERSE.get,
|
||||
measurement_to_ha=PUMP_OPERATION_MODE_MAP.get,
|
||||
ha_to_native_value=PUMP_OPERATION_MODE_MAP_REVERSE.get,
|
||||
),
|
||||
entity_class=MatterAttributeSelectEntity,
|
||||
required_attributes=(
|
||||
|
@@ -194,7 +194,7 @@ class MatterSensor(MatterEntity, SensorEntity):
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value in (None, NullValue):
|
||||
value = None
|
||||
elif value_convert := self.entity_description.device_to_ha:
|
||||
elif value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -296,7 +296,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="TemperatureSensor",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -308,7 +308,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PressureSensor",
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -320,7 +320,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="FlowSensor",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
translation_key="flow",
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -332,7 +332,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="HumiditySensor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -346,7 +346,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="LightSensor",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
device_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
|
||||
measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -360,7 +360,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
# value has double precision
|
||||
device_to_ha=lambda x: int(x / 2),
|
||||
measurement_to_ha=lambda x: int(x / 2),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -402,7 +402,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[state for state in CHARGE_STATE_MAP.values() if state is not None],
|
||||
device_to_ha=CHARGE_STATE_MAP.get,
|
||||
measurement_to_ha=CHARGE_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatChargeState,),
|
||||
@@ -589,7 +589,7 @@ DISCOVERY_SCHEMAS = [
|
||||
state_class=None,
|
||||
# convert to set first to remove the duplicate unknown value
|
||||
options=[x for x in AIR_QUALITY_MAP.values() if x is not None],
|
||||
device_to_ha=lambda x: AIR_QUALITY_MAP[x],
|
||||
measurement_to_ha=lambda x: AIR_QUALITY_MAP[x],
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.AirQuality.Attributes.AirQuality,),
|
||||
@@ -668,7 +668,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_to_ha=lambda x: x / 1000,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -685,7 +685,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_to_ha=lambda x: x / 1000,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -702,7 +702,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Watt,),
|
||||
@@ -731,7 +731,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=0,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Voltage,),
|
||||
@@ -823,7 +823,7 @@ DISCOVERY_SCHEMAS = [
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
# id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh)
|
||||
device_to_ha=lambda x: x.energy,
|
||||
measurement_to_ha=lambda x: x.energy,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -842,7 +842,7 @@ DISCOVERY_SCHEMAS = [
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
# id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh)
|
||||
device_to_ha=lambda x: x.energy,
|
||||
measurement_to_ha=lambda x: x.energy,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -910,7 +910,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="contamination_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(CONTAMINATION_STATE_MAP.values()),
|
||||
device_to_ha=CONTAMINATION_STATE_MAP.get,
|
||||
measurement_to_ha=CONTAMINATION_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.ContaminationState,),
|
||||
@@ -922,7 +922,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="expiry_date",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
# raw value is epoch seconds
|
||||
device_to_ha=datetime.fromtimestamp,
|
||||
measurement_to_ha=datetime.fromtimestamp,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.ExpiryDate,),
|
||||
@@ -993,7 +993,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ThermostatLocalTemperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -1044,7 +1044,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key="window_covering_target_position",
|
||||
device_to_ha=lambda x: round((10000 - x) / 100),
|
||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -1060,7 +1060,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(EVSE_FAULT_STATE_MAP.values()),
|
||||
device_to_ha=EVSE_FAULT_STATE_MAP.get,
|
||||
measurement_to_ha=EVSE_FAULT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.EnergyEvse.Attributes.FaultState,),
|
||||
@@ -1173,7 +1173,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(ESA_STATE_MAP.values()),
|
||||
device_to_ha=ESA_STATE_MAP.get,
|
||||
measurement_to_ha=ESA_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,),
|
||||
@@ -1186,7 +1186,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(DEM_OPT_OUT_STATE_MAP.values()),
|
||||
device_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
measurement_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.OptOutState,),
|
||||
@@ -1200,7 +1200,7 @@ DISCOVERY_SCHEMAS = [
|
||||
options=[
|
||||
mode for mode in PUMP_CONTROL_MODE_MAP.values() if mode is not None
|
||||
],
|
||||
device_to_ha=PUMP_CONTROL_MODE_MAP.get,
|
||||
measurement_to_ha=PUMP_CONTROL_MODE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
|
@@ -95,7 +95,7 @@ class MatterGenericCommandSwitch(MatterSwitch):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_is_on = value
|
||||
|
||||
@@ -141,7 +141,7 @@ class MatterNumericSwitch(MatterSwitch):
|
||||
|
||||
async def _async_set_native_value(self, value: bool) -> None:
|
||||
"""Update the current value."""
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
send_value = value_convert(value)
|
||||
await self.write_attribute(
|
||||
value=send_value,
|
||||
@@ -159,7 +159,7 @@ class MatterNumericSwitch(MatterSwitch):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_is_on = value
|
||||
|
||||
@@ -248,11 +248,11 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EveTrvChildLock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="child_lock",
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: False,
|
||||
1: True,
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
False: 0,
|
||||
True: 1,
|
||||
}.get,
|
||||
@@ -275,7 +275,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
off_command=clusters.EnergyEvse.Commands.Disable,
|
||||
command_timeout=3000,
|
||||
device_to_ha=EVSE_SUPPLY_STATE_MAP.get,
|
||||
measurement_to_ha=EVSE_SUPPLY_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterGenericCommandSwitch,
|
||||
required_attributes=(
|
||||
|
@@ -45,16 +45,6 @@ class MediaSourceItem:
|
||||
identifier: str
|
||||
target_media_player: str | None
|
||||
|
||||
@property
|
||||
def media_source_id(self) -> str:
|
||||
"""Return the media source ID."""
|
||||
uri = URI_SCHEME
|
||||
if self.domain:
|
||||
uri += self.domain
|
||||
if self.identifier:
|
||||
uri += f"/{self.identifier}"
|
||||
return uri
|
||||
|
||||
async def async_browse(self) -> BrowseMediaSource:
|
||||
"""Browse this item."""
|
||||
if self.domain is None:
|
||||
|
@@ -134,7 +134,7 @@ class MediaroomDevice(MediaPlayerEntity):
|
||||
|
||||
state_map = {
|
||||
State.OFF: MediaPlayerState.OFF,
|
||||
State.STANDBY: MediaPlayerState.IDLE,
|
||||
State.STANDBY: MediaPlayerState.STANDBY,
|
||||
State.PLAYING_LIVE_TV: MediaPlayerState.PLAYING,
|
||||
State.PLAYING_RECORDED_TV: MediaPlayerState.PLAYING,
|
||||
State.PLAYING_TIMESHIFT_TV: MediaPlayerState.PLAYING,
|
||||
@@ -155,7 +155,7 @@ class MediaroomDevice(MediaPlayerEntity):
|
||||
self._channel = None
|
||||
self._optimistic = optimistic
|
||||
self._attr_state = (
|
||||
MediaPlayerState.PLAYING if optimistic else MediaPlayerState.IDLE
|
||||
MediaPlayerState.PLAYING if optimistic else MediaPlayerState.STANDBY
|
||||
)
|
||||
self._name = f"Mediaroom {device_id if device_id else host}"
|
||||
self._available = True
|
||||
@@ -254,7 +254,7 @@ class MediaroomDevice(MediaPlayerEntity):
|
||||
try:
|
||||
self.set_state(await self.stb.turn_off())
|
||||
if self._optimistic:
|
||||
self._attr_state = MediaPlayerState.IDLE
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
self._available = True
|
||||
except PyMediaroomError:
|
||||
self._available = False
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from phone_modem import PhoneModem
|
||||
|
||||
from homeassistant.components.sensor import RestoreSensor
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
@@ -40,7 +40,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class ModemCalleridSensor(RestoreSensor):
|
||||
class ModemCalleridSensor(SensorEntity):
|
||||
"""Implementation of USB modem caller ID sensor."""
|
||||
|
||||
_attr_should_poll = False
|
||||
@@ -62,20 +62,8 @@ class ModemCalleridSensor(RestoreSensor):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when the modem sensor is added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if (last_state := await self.async_get_last_state()) is not None:
|
||||
self._attr_extra_state_attributes[CID.CID_NAME] = last_state.attributes.get(
|
||||
CID.CID_NAME, ""
|
||||
)
|
||||
self._attr_extra_state_attributes[CID.CID_NUMBER] = (
|
||||
last_state.attributes.get(CID.CID_NUMBER, "")
|
||||
)
|
||||
self._attr_extra_state_attributes[CID.CID_TIME] = last_state.attributes.get(
|
||||
CID.CID_TIME, 0
|
||||
)
|
||||
|
||||
self.api.registercallback(self._async_incoming_call)
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def _async_incoming_call(self, new_state: str) -> None:
|
||||
|
@@ -24,8 +24,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OpowerConfigEntry, OpowerCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class OpowerEntityDescription(SensorEntityDescription):
|
||||
@@ -40,7 +38,7 @@ class OpowerEntityDescription(SensorEntityDescription):
|
||||
ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
OpowerEntityDescription(
|
||||
key="elec_usage_to_date",
|
||||
translation_key="elec_usage_to_date",
|
||||
name="Current bill electric usage to date",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
# Not TOTAL_INCREASING because it can decrease for accounts with solar
|
||||
@@ -50,7 +48,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_forecasted_usage",
|
||||
translation_key="elec_forecasted_usage",
|
||||
name="Current bill electric forecasted usage",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -59,7 +57,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_typical_usage",
|
||||
translation_key="elec_typical_usage",
|
||||
name="Typical monthly electric usage",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -68,7 +66,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_cost_to_date",
|
||||
translation_key="elec_cost_to_date",
|
||||
name="Current bill electric cost to date",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
native_unit_of_measurement="USD",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -77,7 +75,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_forecasted_cost",
|
||||
translation_key="elec_forecasted_cost",
|
||||
name="Current bill electric forecasted cost",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
native_unit_of_measurement="USD",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -86,7 +84,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_typical_cost",
|
||||
translation_key="elec_typical_cost",
|
||||
name="Typical monthly electric cost",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
native_unit_of_measurement="USD",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -95,7 +93,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_start_date",
|
||||
translation_key="elec_start_date",
|
||||
name="Current bill electric start date",
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -103,7 +101,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="elec_end_date",
|
||||
translation_key="elec_end_date",
|
||||
name="Current bill electric end date",
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -113,7 +111,7 @@ ELEC_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
OpowerEntityDescription(
|
||||
key="gas_usage_to_date",
|
||||
translation_key="gas_usage_to_date",
|
||||
name="Current bill gas usage to date",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -122,7 +120,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_forecasted_usage",
|
||||
translation_key="gas_forecasted_usage",
|
||||
name="Current bill gas forecasted usage",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -131,7 +129,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_typical_usage",
|
||||
translation_key="gas_typical_usage",
|
||||
name="Typical monthly gas usage",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -140,7 +138,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_cost_to_date",
|
||||
translation_key="gas_cost_to_date",
|
||||
name="Current bill gas cost to date",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
native_unit_of_measurement="USD",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -149,7 +147,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_forecasted_cost",
|
||||
translation_key="gas_forecasted_cost",
|
||||
name="Current bill gas forecasted cost",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
native_unit_of_measurement="USD",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -158,7 +156,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_typical_cost",
|
||||
translation_key="gas_typical_cost",
|
||||
name="Typical monthly gas cost",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
native_unit_of_measurement="USD",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -167,7 +165,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_start_date",
|
||||
translation_key="gas_start_date",
|
||||
name="Current bill gas start date",
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -175,7 +173,7 @@ GAS_SENSORS: tuple[OpowerEntityDescription, ...] = (
|
||||
),
|
||||
OpowerEntityDescription(
|
||||
key="gas_end_date",
|
||||
translation_key="gas_end_date",
|
||||
name="Current bill gas end date",
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
@@ -231,7 +229,6 @@ async def async_setup_entry(
|
||||
class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||
"""Representation of an Opower sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: OpowerEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -252,6 +249,8 @@ class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType | date:
|
||||
"""Return the state."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self.utility_account_id]
|
||||
)
|
||||
if self.coordinator.data is not None:
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self.utility_account_id]
|
||||
)
|
||||
return None
|
||||
|
@@ -6,24 +6,12 @@
|
||||
"utility": "Utility name",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"utility": "The name of your utility provider",
|
||||
"username": "The username for your utility account",
|
||||
"password": "The password for your utility account"
|
||||
}
|
||||
},
|
||||
"mfa": {
|
||||
"description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"totp_secret": "TOTP secret"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -32,11 +20,6 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"totp_secret": "[%key:component::opower::config::step::mfa::data::totp_secret%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -54,57 +37,5 @@
|
||||
"title": "Return to grid statistics for account: {utility_account_id}",
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"elec_usage_to_date": {
|
||||
"name": "Current bill electric usage to date"
|
||||
},
|
||||
"elec_forecasted_usage": {
|
||||
"name": "Current bill electric forecasted usage"
|
||||
},
|
||||
"elec_typical_usage": {
|
||||
"name": "Typical monthly electric usage"
|
||||
},
|
||||
"elec_cost_to_date": {
|
||||
"name": "Current bill electric cost to date"
|
||||
},
|
||||
"elec_forecasted_cost": {
|
||||
"name": "Current bill electric forecasted cost"
|
||||
},
|
||||
"elec_typical_cost": {
|
||||
"name": "Typical monthly electric cost"
|
||||
},
|
||||
"elec_start_date": {
|
||||
"name": "Current bill electric start date"
|
||||
},
|
||||
"elec_end_date": {
|
||||
"name": "Current bill electric end date"
|
||||
},
|
||||
"gas_usage_to_date": {
|
||||
"name": "Current bill gas usage to date"
|
||||
},
|
||||
"gas_forecasted_usage": {
|
||||
"name": "Current bill gas forecasted usage"
|
||||
},
|
||||
"gas_typical_usage": {
|
||||
"name": "Typical monthly gas usage"
|
||||
},
|
||||
"gas_cost_to_date": {
|
||||
"name": "Current bill gas cost to date"
|
||||
},
|
||||
"gas_forecasted_cost": {
|
||||
"name": "Current bill gas forecasted cost"
|
||||
},
|
||||
"gas_typical_cost": {
|
||||
"name": "Typical monthly gas cost"
|
||||
},
|
||||
"gas_start_date": {
|
||||
"name": "Current bill gas start date"
|
||||
},
|
||||
"gas_end_date": {
|
||||
"name": "Current bill gas end date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,13 +6,7 @@ from collections.abc import Mapping
|
||||
import platform
|
||||
from typing import Any
|
||||
|
||||
from haphilipsjs import (
|
||||
DEFAULT_API_VERSION,
|
||||
ConnectionFailure,
|
||||
GeneralFailure,
|
||||
PairingFailure,
|
||||
PhilipsTV,
|
||||
)
|
||||
from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -24,18 +18,16 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import (
|
||||
CONF_API_VERSION,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PIN,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import LOGGER
|
||||
from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
|
||||
@@ -62,6 +54,21 @@ OPTIONS_FLOW = {
|
||||
}
|
||||
|
||||
|
||||
async def _validate_input(
|
||||
hass: HomeAssistant, host: str, api_version: int
|
||||
) -> PhilipsTV:
|
||||
"""Validate the user input allows us to connect."""
|
||||
hub = PhilipsTV(host, api_version)
|
||||
|
||||
await hub.getSystem()
|
||||
await hub.setTransport(hub.secured_transport)
|
||||
|
||||
if not hub.system:
|
||||
raise ConnectionFailure("System data is empty")
|
||||
|
||||
return hub
|
||||
|
||||
|
||||
class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Philips TV."""
|
||||
|
||||
@@ -74,38 +81,6 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._hub: PhilipsTV | None = None
|
||||
self._pair_state: Any = None
|
||||
|
||||
async def _async_attempt_prepare(
|
||||
self, host: str, api_version: int, secured_transport: bool
|
||||
) -> None:
|
||||
hub = PhilipsTV(
|
||||
host, api_version=api_version, secured_transport=secured_transport
|
||||
)
|
||||
|
||||
await hub.getSystem()
|
||||
await hub.setTransport(hub.secured_transport)
|
||||
|
||||
if not hub.system or not hub.name:
|
||||
raise ConnectionFailure("System data or name is empty")
|
||||
|
||||
self._hub = hub
|
||||
self._current[CONF_HOST] = host
|
||||
self._current[CONF_SYSTEM] = hub.system
|
||||
self._current[CONF_API_VERSION] = hub.api_version
|
||||
self.context.update({"title_placeholders": {CONF_NAME: hub.name}})
|
||||
|
||||
if serialnumber := hub.system.get("serialnumber"):
|
||||
await self.async_set_unique_id(serialnumber)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_configured(
|
||||
updates=self._current, reload_on_update=True
|
||||
)
|
||||
|
||||
async def _async_attempt_add(self) -> ConfigFlowResult:
|
||||
assert self._hub
|
||||
if self._hub.pairing_type == "digest_auth_pairing":
|
||||
return await self.async_step_pair()
|
||||
return await self._async_create_current()
|
||||
|
||||
async def _async_create_current(self) -> ConfigFlowResult:
|
||||
system = self._current[CONF_SYSTEM]
|
||||
if self.source == SOURCE_REAUTH:
|
||||
@@ -179,43 +154,6 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._current[CONF_API_VERSION] = entry_data[CONF_API_VERSION]
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
|
||||
LOGGER.debug(
|
||||
"Checking discovered device: {discovery_info.name} on {discovery_info.host}"
|
||||
)
|
||||
|
||||
secured_transport = discovery_info.type == "_philipstv_s_rpc._tcp.local."
|
||||
api_version = 6 if secured_transport else DEFAULT_API_VERSION
|
||||
|
||||
try:
|
||||
await self._async_attempt_prepare(
|
||||
discovery_info.host, api_version, secured_transport
|
||||
)
|
||||
except GeneralFailure:
|
||||
LOGGER.debug("Failed to get system info from discovery", exc_info=True)
|
||||
return self.async_abort(reason="discovery_failure")
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
if user_input is not None:
|
||||
return await self._async_attempt_add()
|
||||
|
||||
name = self.context.get("title_placeholders", {CONF_NAME: "Philips TV"})[
|
||||
CONF_NAME
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={CONF_NAME: name},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -224,14 +162,28 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input:
|
||||
self._current = user_input
|
||||
try:
|
||||
await self._async_attempt_prepare(
|
||||
user_input[CONF_HOST], user_input[CONF_API_VERSION], False
|
||||
hub = await _validate_input(
|
||||
self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION]
|
||||
)
|
||||
except GeneralFailure as exc:
|
||||
except ConnectionFailure as exc:
|
||||
LOGGER.error(exc)
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return await self._async_attempt_add()
|
||||
if serialnumber := hub.system.get("serialnumber"):
|
||||
await self.async_set_unique_id(serialnumber)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self._current[CONF_SYSTEM] = hub.system
|
||||
self._current[CONF_API_VERSION] = hub.api_version
|
||||
self._hub = hub
|
||||
|
||||
if hub.pairing_type == "digest_auth_pairing":
|
||||
return await self.async_step_pair()
|
||||
return await self._async_create_current()
|
||||
|
||||
schema = self.add_suggested_values_to_schema(USER_SCHEMA, self._current)
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
@@ -6,6 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["haphilipsjs"],
|
||||
"requirements": ["ha-philipsjs==3.2.2"],
|
||||
"zeroconf": ["_philipstv_s_rpc._tcp.local.", "_philipstv_rpc._tcp.local."]
|
||||
"requirements": ["ha-philipsjs==3.2.2"]
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -8,10 +7,6 @@
|
||||
"api_version": "API Version"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Discovered Philips TV",
|
||||
"description": "Do you want to add the TV ({name}) 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."
|
||||
},
|
||||
"pair": {
|
||||
"title": "Pair",
|
||||
"description": "Enter the PIN displayed on your TV",
|
||||
|
@@ -191,7 +191,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
)
|
||||
elif self.state != MediaPlayerState.IDLE:
|
||||
self.idle()
|
||||
elif self.state != MediaPlayerState.OFF:
|
||||
elif self.state != MediaPlayerState.STANDBY:
|
||||
self.state_standby()
|
||||
|
||||
elif self._retry > DEFAULT_RETRIES:
|
||||
@@ -223,7 +223,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
def state_standby(self) -> None:
|
||||
"""Set states for state standby."""
|
||||
self.reset_title()
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
|
||||
def state_unknown(self) -> None:
|
||||
"""Set states for state unknown."""
|
||||
|
@@ -50,8 +50,6 @@ class RadioMediaSource(MediaSource):
|
||||
@property
|
||||
def radios(self) -> RadioBrowser:
|
||||
"""Return the radio browser."""
|
||||
if not hasattr(self.entry, "runtime_data") or self.entry.runtime_data is None:
|
||||
raise Unresolvable("Radio Browser integration not properly loaded")
|
||||
return self.entry.runtime_data
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
|
@@ -1,12 +0,0 @@
|
||||
"""Specifies the parameter for the httpx download."""
|
||||
|
||||
from httpx import AsyncClient, Response, Timeout
|
||||
|
||||
|
||||
async def get_calendar(client: AsyncClient, url: str) -> Response:
|
||||
"""Make an HTTP GET request using Home Assistant's async HTTPX client with timeout."""
|
||||
return await client.get(
|
||||
url,
|
||||
follow_redirects=True,
|
||||
timeout=Timeout(5, read=30, write=5, pool=5),
|
||||
)
|
@@ -4,14 +4,13 @@ from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from httpx import HTTPError, InvalidURL, TimeoutException
|
||||
from httpx import HTTPError, InvalidURL
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .client import get_calendar
|
||||
from .const import CONF_CALENDAR_NAME, DOMAIN
|
||||
from .ics import InvalidIcsException, parse_calendar
|
||||
|
||||
@@ -50,7 +49,7 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
|
||||
client = get_async_client(self.hass)
|
||||
try:
|
||||
res = await get_calendar(client, user_input[CONF_URL])
|
||||
res = await client.get(user_input[CONF_URL], follow_redirects=True)
|
||||
if res.status_code == HTTPStatus.FORBIDDEN:
|
||||
errors["base"] = "forbidden"
|
||||
return self.async_show_form(
|
||||
@@ -59,14 +58,9 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
res.raise_for_status()
|
||||
except TimeoutException as err:
|
||||
errors["base"] = "timeout_connect"
|
||||
_LOGGER.debug(
|
||||
"A timeout error occurred: %s", str(err) or type(err).__name__
|
||||
)
|
||||
except (HTTPError, InvalidURL) as err:
|
||||
errors["base"] = "cannot_connect"
|
||||
_LOGGER.debug("An error occurred: %s", str(err) or type(err).__name__)
|
||||
_LOGGER.debug("An error occurred: %s", err)
|
||||
else:
|
||||
try:
|
||||
await parse_calendar(self.hass, res.text)
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from httpx import HTTPError, InvalidURL, TimeoutException
|
||||
from httpx import HTTPError, InvalidURL
|
||||
from ical.calendar import Calendar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -12,7 +12,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .client import get_calendar
|
||||
from .const import DOMAIN
|
||||
from .ics import InvalidIcsException, parse_calendar
|
||||
|
||||
@@ -37,7 +36,7 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"{DOMAIN}_{config_entry.title}",
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
always_update=True,
|
||||
)
|
||||
@@ -47,19 +46,13 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
||||
async def _async_update_data(self) -> Calendar:
|
||||
"""Update data from the url."""
|
||||
try:
|
||||
res = await get_calendar(self._client, self._url)
|
||||
res = await self._client.get(self._url, follow_redirects=True)
|
||||
res.raise_for_status()
|
||||
except TimeoutException as err:
|
||||
_LOGGER.debug("%s: %s", self._url, str(err) or type(err).__name__)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout",
|
||||
) from err
|
||||
except (HTTPError, InvalidURL) as err:
|
||||
_LOGGER.debug("%s: %s", self._url, str(err) or type(err).__name__)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unable_to_fetch",
|
||||
translation_placeholders={"err": str(err)},
|
||||
) from err
|
||||
try:
|
||||
self.ics = res.text
|
||||
|
@@ -18,18 +18,14 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"forbidden": "The server understood the request but refuses to authorize it.",
|
||||
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"timeout": {
|
||||
"message": "The connection timed out. See the debug log for additional details."
|
||||
},
|
||||
"unable_to_fetch": {
|
||||
"message": "Unable to fetch calendar data. See the debug log for additional details."
|
||||
"message": "Unable to fetch calendar data: {err}"
|
||||
},
|
||||
"unable_to_parse": {
|
||||
"message": "Unable to parse calendar data: {err}"
|
||||
|
@@ -142,7 +142,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the device."""
|
||||
if self.coordinator.data.state.standby:
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
|
||||
if self.coordinator.data.app is None:
|
||||
return None
|
||||
@@ -308,21 +308,21 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
@roku_exception_handler()
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
if self.state not in {MediaPlayerState.OFF, MediaPlayerState.PAUSED}:
|
||||
if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PAUSED}:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler()
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
if self.state not in {MediaPlayerState.OFF, MediaPlayerState.PLAYING}:
|
||||
if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PLAYING}:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler()
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send play/pause command."""
|
||||
if self.state != MediaPlayerState.OFF:
|
||||
if self.state != MediaPlayerState.STANDBY:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
@@ -31,7 +31,7 @@ async def async_setup_entry(
|
||||
if dev_id in event_entities:
|
||||
return
|
||||
# new player!
|
||||
event_entity = RoonEventEntity(roon_server, player_data, config_entry.entry_id)
|
||||
event_entity = RoonEventEntity(roon_server, player_data)
|
||||
event_entities.add(dev_id)
|
||||
async_add_entities([event_entity])
|
||||
|
||||
@@ -50,14 +50,13 @@ class RoonEventEntity(EventEntity):
|
||||
_attr_event_types = ["volume_up", "volume_down", "mute_toggle"]
|
||||
_attr_translation_key = "volume"
|
||||
|
||||
def __init__(self, server, player_data, entry_id):
|
||||
def __init__(self, server, player_data):
|
||||
"""Initialize the entity."""
|
||||
self._server = server
|
||||
self._player_data = player_data
|
||||
player_name = player_data["display_name"]
|
||||
self._attr_name = f"{player_name} roon volume"
|
||||
self._attr_unique_id = self._player_data["dev_id"]
|
||||
self._entry_id = entry_id
|
||||
|
||||
if self._player_data.get("source_controls"):
|
||||
dev_model = self._player_data["source_controls"][0].get("display_name")
|
||||
@@ -70,7 +69,7 @@ class RoonEventEntity(EventEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device=(DOMAIN, self._server.roon_id),
|
||||
)
|
||||
|
||||
def _roonapi_volume_callback(
|
||||
|
@@ -72,7 +72,7 @@ async def async_setup_entry(
|
||||
dev_id = player_data["dev_id"]
|
||||
if dev_id not in media_players:
|
||||
# new player!
|
||||
media_player = RoonDevice(roon_server, player_data, config_entry.entry_id)
|
||||
media_player = RoonDevice(roon_server, player_data)
|
||||
media_players.add(dev_id)
|
||||
async_add_entities([media_player])
|
||||
else:
|
||||
@@ -106,7 +106,7 @@ class RoonDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
)
|
||||
|
||||
def __init__(self, server, player_data, entry_id):
|
||||
def __init__(self, server, player_data):
|
||||
"""Initialize Roon device object."""
|
||||
self._remove_signal_status = None
|
||||
self._server = server
|
||||
@@ -125,7 +125,6 @@ class RoonDevice(MediaPlayerEntity):
|
||||
self._attr_volume_level = 0
|
||||
self._volume_fixed = True
|
||||
self._volume_incremental = False
|
||||
self._entry_id = entry_id
|
||||
self.update_data(player_data)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -167,7 +166,7 @@ class RoonDevice(MediaPlayerEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device=(DOMAIN, self._server.roon_id),
|
||||
)
|
||||
|
||||
def update_data(self, player_data=None):
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==4.7.0"],
|
||||
"requirements": ["aiorussound==4.6.1"],
|
||||
"zeroconf": ["_rio._tcp.local."]
|
||||
}
|
||||
|
@@ -343,7 +343,7 @@ class SnapcastClientDevice(SnapcastBaseDevice):
|
||||
if self.is_volume_muted or self._current_group.muted:
|
||||
return MediaPlayerState.IDLE
|
||||
return STREAM_STATUS.get(self._current_group.stream_status)
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any]:
|
||||
|
@@ -374,7 +374,9 @@ class TelegramNotificationService:
|
||||
}
|
||||
if data is not None:
|
||||
if ATTR_PARSER in data:
|
||||
params[ATTR_PARSER] = data[ATTR_PARSER]
|
||||
params[ATTR_PARSER] = self._parsers.get(
|
||||
data[ATTR_PARSER], self.parse_mode
|
||||
)
|
||||
if ATTR_TIMEOUT in data:
|
||||
params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
|
||||
if ATTR_DISABLE_NOTIF in data:
|
||||
@@ -406,8 +408,6 @@ class TelegramNotificationService:
|
||||
params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup(
|
||||
[_make_row_inline_keyboard(row) for row in keys]
|
||||
)
|
||||
if params[ATTR_PARSER] == PARSER_PLAIN_TEXT:
|
||||
params[ATTR_PARSER] = None
|
||||
return params
|
||||
|
||||
async def _send_msg(
|
||||
|
@@ -159,6 +159,8 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
"""Manage the options."""
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[ATTR_PARSER] == PARSER_PLAIN_TEXT:
|
||||
user_input[ATTR_PARSER] = None
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@@ -109,7 +109,6 @@ send_photo:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -262,7 +261,6 @@ send_animation:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -343,7 +341,6 @@ send_video:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -496,7 +493,6 @@ send_document:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -674,7 +670,6 @@ edit_message:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_web_page_preview:
|
||||
selector:
|
||||
boolean:
|
||||
|
@@ -31,7 +31,7 @@ from .const import (
|
||||
EVENTS,
|
||||
LOGGER,
|
||||
)
|
||||
from .helpers import get_device, get_first_geofence, get_geofence_ids
|
||||
from .helpers import get_device, get_first_geofence
|
||||
|
||||
|
||||
class TraccarServerCoordinatorDataDevice(TypedDict):
|
||||
@@ -131,7 +131,7 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
|
||||
"device": device,
|
||||
"geofence": get_first_geofence(
|
||||
geofences,
|
||||
get_geofence_ids(device, position),
|
||||
position["geofenceIds"] or [],
|
||||
),
|
||||
"position": position,
|
||||
"attributes": attr,
|
||||
@@ -187,7 +187,7 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
|
||||
self.data[device_id]["attributes"] = attr
|
||||
self.data[device_id]["geofence"] = get_first_geofence(
|
||||
self._geofences,
|
||||
get_geofence_ids(self.data[device_id]["device"], position),
|
||||
position["geofenceIds"] or [],
|
||||
)
|
||||
update_devices.add(device_id)
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pytraccar import DeviceModel, GeofenceModel, PositionModel
|
||||
from pytraccar import DeviceModel, GeofenceModel
|
||||
|
||||
|
||||
def get_device(device_id: int, devices: list[DeviceModel]) -> DeviceModel | None:
|
||||
@@ -22,17 +22,3 @@ def get_first_geofence(
|
||||
(geofence for geofence in geofences if geofence["id"] in target),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def get_geofence_ids(
|
||||
device: DeviceModel,
|
||||
position: PositionModel,
|
||||
) -> list[int]:
|
||||
"""Compatibility helper to return a list of geofence IDs."""
|
||||
# For Traccar >=5.8 https://github.com/traccar/traccar/commit/30bafaed42e74863c5ca68a33c87f39d1e2de93d
|
||||
if "geofenceIds" in position:
|
||||
return position["geofenceIds"] or []
|
||||
# For Traccar <5.8
|
||||
if "geofenceIds" in device:
|
||||
return device["geofenceIds"] or []
|
||||
return []
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["venstarcolortouch"],
|
||||
"requirements": ["venstarcolortouch==0.21"]
|
||||
"requirements": ["venstarcolortouch==0.19"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["weatherflow4py"],
|
||||
"requirements": ["weatherflow4py==1.4.1"]
|
||||
"requirements": ["weatherflow4py==1.3.1"]
|
||||
}
|
||||
|
@@ -35,10 +35,6 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity, template
|
||||
from homeassistant.helpers.condition import (
|
||||
async_get_all_descriptions as async_get_all_condition_descriptions,
|
||||
async_subscribe_platform_events as async_subscribe_condition_platform_events,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
||||
@@ -80,7 +76,6 @@ from . import const, decorators, messages
|
||||
from .connection import ActiveConnection
|
||||
from .messages import construct_event_message, construct_result_message
|
||||
|
||||
ALL_CONDITION_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_condition_descriptions_json"
|
||||
ALL_SERVICE_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_service_descriptions_json"
|
||||
ALL_TRIGGER_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_trigger_descriptions_json"
|
||||
|
||||
@@ -106,7 +101,6 @@ def async_register_commands(
|
||||
async_reg(hass, handle_ping)
|
||||
async_reg(hass, handle_render_template)
|
||||
async_reg(hass, handle_subscribe_bootstrap_integrations)
|
||||
async_reg(hass, handle_subscribe_condition_platforms)
|
||||
async_reg(hass, handle_subscribe_events)
|
||||
async_reg(hass, handle_subscribe_trigger)
|
||||
async_reg(hass, handle_subscribe_trigger_platforms)
|
||||
@@ -507,53 +501,6 @@ def _send_handle_entities_init_response(
|
||||
)
|
||||
|
||||
|
||||
async def _async_get_all_condition_descriptions_json(hass: HomeAssistant) -> bytes:
|
||||
"""Return JSON of descriptions (i.e. user documentation) for all condition."""
|
||||
descriptions = await async_get_all_condition_descriptions(hass)
|
||||
if ALL_CONDITION_DESCRIPTIONS_JSON_CACHE in hass.data:
|
||||
cached_descriptions, cached_json_payload = hass.data[
|
||||
ALL_CONDITION_DESCRIPTIONS_JSON_CACHE
|
||||
]
|
||||
# If the descriptions are the same, return the cached JSON payload
|
||||
if cached_descriptions is descriptions:
|
||||
return cast(bytes, cached_json_payload)
|
||||
json_payload = json_bytes(
|
||||
{
|
||||
condition: description
|
||||
for condition, description in descriptions.items()
|
||||
if description is not None
|
||||
}
|
||||
)
|
||||
hass.data[ALL_CONDITION_DESCRIPTIONS_JSON_CACHE] = (descriptions, json_payload)
|
||||
return json_payload
|
||||
|
||||
|
||||
@decorators.websocket_command({vol.Required("type"): "condition_platforms/subscribe"})
|
||||
@decorators.async_response
|
||||
async def handle_subscribe_condition_platforms(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle subscribe conditions command."""
|
||||
|
||||
async def on_new_conditions(new_conditions: set[str]) -> None:
|
||||
"""Forward new conditions to websocket."""
|
||||
descriptions = await async_get_all_condition_descriptions(hass)
|
||||
new_condition_descriptions = {}
|
||||
for condition in new_conditions:
|
||||
if (description := descriptions[condition]) is not None:
|
||||
new_condition_descriptions[condition] = description
|
||||
if not new_condition_descriptions:
|
||||
return
|
||||
connection.send_event(msg["id"], new_condition_descriptions)
|
||||
|
||||
connection.subscriptions[msg["id"]] = async_subscribe_condition_platform_events(
|
||||
hass, on_new_conditions
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
conditions_json = await _async_get_all_condition_descriptions_json(hass)
|
||||
connection.send_message(construct_event_message(msg["id"], conditions_json))
|
||||
|
||||
|
||||
async def _async_get_all_service_descriptions_json(hass: HomeAssistant) -> bytes:
|
||||
"""Return JSON of descriptions (i.e. user documentation) for all service calls."""
|
||||
descriptions = await async_get_all_service_descriptions(hass)
|
||||
|
10
homeassistant/generated/zeroconf.py
generated
10
homeassistant/generated/zeroconf.py
generated
@@ -771,16 +771,6 @@ ZEROCONF = {
|
||||
"domain": "onewire",
|
||||
},
|
||||
],
|
||||
"_philipstv_rpc._tcp.local.": [
|
||||
{
|
||||
"domain": "philips_js",
|
||||
},
|
||||
],
|
||||
"_philipstv_s_rpc._tcp.local.": [
|
||||
{
|
||||
"domain": "philips_js",
|
||||
},
|
||||
],
|
||||
"_plexmediasvr._tcp.local.": [
|
||||
{
|
||||
"domain": "plex",
|
||||
|
@@ -5,17 +5,19 @@ from __future__ import annotations
|
||||
import abc
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections.abc import Callable, Container, Coroutine, Generator, Iterable
|
||||
from collections.abc import Callable, Container, Generator
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, time as dt_time, timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zone as zone_cmp
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_GPS_ACCURACY,
|
||||
@@ -52,20 +54,11 @@ from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
TemplateError,
|
||||
)
|
||||
from homeassistant.loader import (
|
||||
Integration,
|
||||
IntegrationNotFound,
|
||||
async_get_integration,
|
||||
async_get_integrations,
|
||||
)
|
||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.yaml import load_yaml_dict
|
||||
from homeassistant.util.yaml.loader import JSON_TYPE
|
||||
|
||||
from . import config_validation as cv, entity_registry as er
|
||||
from .integration_platform import async_process_integration_platforms
|
||||
from .template import Template, render_complex
|
||||
from .trace import (
|
||||
TraceElement,
|
||||
@@ -83,8 +76,6 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
|
||||
FROM_CONFIG_FORMAT = "{}_from_config"
|
||||
VALIDATE_CONFIG_FORMAT = "{}_validate_config"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_PLATFORM_ALIASES: dict[str | None, str | None] = {
|
||||
"and": None,
|
||||
"device": "device_automation",
|
||||
@@ -103,99 +94,6 @@ INPUT_ENTITY_ID = re.compile(
|
||||
)
|
||||
|
||||
|
||||
CONDITION_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any] | None]] = HassKey(
|
||||
"condition_description_cache"
|
||||
)
|
||||
CONDITION_PLATFORM_SUBSCRIPTIONS: HassKey[
|
||||
list[Callable[[set[str]], Coroutine[Any, Any, None]]]
|
||||
] = HassKey("condition_platform_subscriptions")
|
||||
CONDITIONS: HassKey[dict[str, str]] = HassKey("conditions")
|
||||
|
||||
|
||||
# Basic schemas to sanity check the condition descriptions,
|
||||
# full validation is done by hassfest.conditions
|
||||
_FIELD_SCHEMA = vol.Schema(
|
||||
{},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def starts_with_dot(key: str) -> str:
|
||||
"""Check if key starts with dot."""
|
||||
if not key.startswith("."):
|
||||
raise vol.Invalid("Key does not start with .")
|
||||
return key
|
||||
|
||||
|
||||
_CONDITIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Remove(vol.All(str, starts_with_dot)): object,
|
||||
cv.slug: vol.Any(None, _CONDITION_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the condition helper."""
|
||||
hass.data[CONDITION_DESCRIPTION_CACHE] = {}
|
||||
hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS] = []
|
||||
hass.data[CONDITIONS] = {}
|
||||
await async_process_integration_platforms(
|
||||
hass, "condition", _register_condition_platform, wait_for_platforms=True
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_subscribe_platform_events(
|
||||
hass: HomeAssistant,
|
||||
on_event: Callable[[set[str]], Coroutine[Any, Any, None]],
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to condition platform events."""
|
||||
condition_platform_event_subscriptions = hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS]
|
||||
|
||||
def remove_subscription() -> None:
|
||||
condition_platform_event_subscriptions.remove(on_event)
|
||||
|
||||
condition_platform_event_subscriptions.append(on_event)
|
||||
return remove_subscription
|
||||
|
||||
|
||||
async def _register_condition_platform(
|
||||
hass: HomeAssistant, integration_domain: str, platform: ConditionProtocol
|
||||
) -> None:
|
||||
"""Register a condition platform."""
|
||||
|
||||
new_conditions: set[str] = set()
|
||||
|
||||
if hasattr(platform, "async_get_conditions"):
|
||||
for condition_key in await platform.async_get_conditions(hass):
|
||||
hass.data[CONDITIONS][condition_key] = integration_domain
|
||||
new_conditions.add(condition_key)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Integration %s does not provide condition support, skipping",
|
||||
integration_domain,
|
||||
)
|
||||
return
|
||||
|
||||
# We don't use gather here because gather adds additional overhead
|
||||
# when wrapping each coroutine in a task, and we expect our listeners
|
||||
# to call condition.async_get_all_descriptions which will only yield
|
||||
# the first time it's called, after that it returns cached data.
|
||||
for listener in hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS]:
|
||||
try:
|
||||
await listener(new_conditions)
|
||||
except Exception:
|
||||
_LOGGER.exception("Error while notifying condition platform listener")
|
||||
|
||||
|
||||
class Condition(abc.ABC):
|
||||
"""Condition class."""
|
||||
|
||||
@@ -819,8 +717,6 @@ def time(
|
||||
for the opposite. "(23:59 <= now < 00:01)" would be the same as
|
||||
"not (00:01 <= now < 23:59)".
|
||||
"""
|
||||
from homeassistant.components.sensor import SensorDeviceClass # noqa: PLC0415
|
||||
|
||||
now = dt_util.now()
|
||||
now_time = now.time()
|
||||
|
||||
@@ -928,8 +824,6 @@ def zone(
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
from homeassistant.components import zone as zone_cmp # noqa: PLC0415
|
||||
|
||||
if zone_ent is None:
|
||||
raise ConditionErrorMessage("zone", "no zone specified")
|
||||
|
||||
@@ -1186,109 +1080,3 @@ def async_extract_devices(config: ConfigType | Template) -> set[str]:
|
||||
referenced.add(device_id)
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
def _load_conditions_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
|
||||
"""Load conditions file for an integration."""
|
||||
try:
|
||||
return cast(
|
||||
JSON_TYPE,
|
||||
_CONDITIONS_SCHEMA(
|
||||
load_yaml_dict(str(integration.file_path / "conditions.yaml"))
|
||||
),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning(
|
||||
"Unable to find conditions.yaml for the %s integration", integration.domain
|
||||
)
|
||||
return {}
|
||||
except (HomeAssistantError, vol.Invalid) as ex:
|
||||
_LOGGER.warning(
|
||||
"Unable to parse conditions.yaml for the %s integration: %s",
|
||||
integration.domain,
|
||||
ex,
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
def _load_conditions_files(
|
||||
hass: HomeAssistant, integrations: Iterable[Integration]
|
||||
) -> dict[str, JSON_TYPE]:
|
||||
"""Load condition files for multiple integrations."""
|
||||
return {
|
||||
integration.domain: _load_conditions_file(hass, integration)
|
||||
for integration in integrations
|
||||
}
|
||||
|
||||
|
||||
async def async_get_all_descriptions(
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, dict[str, Any] | None]:
|
||||
"""Return descriptions (i.e. user documentation) for all conditions."""
|
||||
descriptions_cache = hass.data[CONDITION_DESCRIPTION_CACHE]
|
||||
|
||||
conditions = hass.data[CONDITIONS]
|
||||
# See if there are new conditions not seen before.
|
||||
# Any condition that we saw before already has an entry in description_cache.
|
||||
all_conditions = set(conditions)
|
||||
previous_all_conditions = set(descriptions_cache)
|
||||
# If the conditions are the same, we can return the cache
|
||||
if previous_all_conditions == all_conditions:
|
||||
return descriptions_cache
|
||||
|
||||
# Files we loaded for missing descriptions
|
||||
new_conditions_descriptions: dict[str, JSON_TYPE] = {}
|
||||
# We try to avoid making a copy in the event the cache is good,
|
||||
# but now we must make a copy in case new conditions get added
|
||||
# while we are loading the missing ones so we do not
|
||||
# add the new ones to the cache without their descriptions
|
||||
conditions = conditions.copy()
|
||||
|
||||
if missing_conditions := all_conditions.difference(descriptions_cache):
|
||||
domains_with_missing_conditions = {
|
||||
conditions[missing_condition] for missing_condition in missing_conditions
|
||||
}
|
||||
ints_or_excs = await async_get_integrations(
|
||||
hass, domains_with_missing_conditions
|
||||
)
|
||||
integrations: list[Integration] = []
|
||||
for domain, int_or_exc in ints_or_excs.items():
|
||||
if type(int_or_exc) is Integration and int_or_exc.has_conditions:
|
||||
integrations.append(int_or_exc)
|
||||
continue
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(int_or_exc, Exception)
|
||||
_LOGGER.debug(
|
||||
"Failed to load conditions.yaml for integration: %s",
|
||||
domain,
|
||||
exc_info=int_or_exc,
|
||||
)
|
||||
|
||||
if integrations:
|
||||
new_conditions_descriptions = await hass.async_add_executor_job(
|
||||
_load_conditions_files, hass, integrations
|
||||
)
|
||||
|
||||
# Make a copy of the old cache and add missing descriptions to it
|
||||
new_descriptions_cache = descriptions_cache.copy()
|
||||
for missing_condition in missing_conditions:
|
||||
domain = conditions[missing_condition]
|
||||
|
||||
if (
|
||||
yaml_description := new_conditions_descriptions.get(domain, {}).get( # type: ignore[union-attr]
|
||||
missing_condition
|
||||
)
|
||||
) is None:
|
||||
_LOGGER.debug(
|
||||
"No condition descriptions found for condition %s, skipping",
|
||||
missing_condition,
|
||||
)
|
||||
new_descriptions_cache[missing_condition] = None
|
||||
continue
|
||||
|
||||
description = {"fields": yaml_description.get("fields", {})}
|
||||
|
||||
new_descriptions_cache[missing_condition] = description
|
||||
|
||||
hass.data[CONDITION_DESCRIPTION_CACHE] = new_descriptions_cache
|
||||
return new_descriptions_cache
|
||||
|
@@ -1537,6 +1537,22 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict[str, Any]:
|
||||
return key_dependency("for", "state")(validated)
|
||||
|
||||
|
||||
SUN_CONDITION_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
**CONDITION_BASE_SCHEMA,
|
||||
vol.Required(CONF_CONDITION): "sun",
|
||||
vol.Optional("before"): sun_event,
|
||||
vol.Optional("before_offset"): time_period,
|
||||
vol.Optional("after"): vol.All(
|
||||
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
||||
),
|
||||
vol.Optional("after_offset"): time_period,
|
||||
}
|
||||
),
|
||||
has_at_least_one_key("before", "after"),
|
||||
)
|
||||
|
||||
TEMPLATE_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
**CONDITION_BASE_SCHEMA,
|
||||
|
@@ -866,17 +866,19 @@ def async_track_state_change_filtered(
|
||||
) -> _TrackStateChangeFiltered:
|
||||
"""Track state changes with a TrackStates filter that can be updated.
|
||||
|
||||
Args:
|
||||
hass:
|
||||
Home assistant object.
|
||||
track_states:
|
||||
A TrackStates data class.
|
||||
action:
|
||||
Callable to call with results.
|
||||
Parameters
|
||||
----------
|
||||
hass
|
||||
Home assistant object.
|
||||
track_states
|
||||
A TrackStates data class.
|
||||
action
|
||||
Callable to call with results.
|
||||
|
||||
Returns:
|
||||
Object used to update the listeners (async_update_listeners) with a new
|
||||
TrackStates or cancel the tracking (async_remove).
|
||||
Returns
|
||||
-------
|
||||
Object used to update the listeners (async_update_listeners) with a new
|
||||
TrackStates or cancel the tracking (async_remove).
|
||||
|
||||
"""
|
||||
tracker = _TrackStateChangeFiltered(hass, track_states, action)
|
||||
@@ -905,26 +907,29 @@ def async_track_template(
|
||||
exception, the listener will still be registered but will only
|
||||
fire if the template result becomes true without an exception.
|
||||
|
||||
Action args:
|
||||
entity_id:
|
||||
ID of the entity that triggered the state change.
|
||||
old_state:
|
||||
The old state of the entity that changed.
|
||||
new_state:
|
||||
New state of the entity that changed.
|
||||
Action arguments
|
||||
----------------
|
||||
entity_id
|
||||
ID of the entity that triggered the state change.
|
||||
old_state
|
||||
The old state of the entity that changed.
|
||||
new_state
|
||||
New state of the entity that changed.
|
||||
|
||||
Args:
|
||||
hass:
|
||||
Home assistant object.
|
||||
template:
|
||||
The template to calculate.
|
||||
action:
|
||||
Callable to call with results. See above for arguments.
|
||||
variables:
|
||||
Variables to pass to the template.
|
||||
Parameters
|
||||
----------
|
||||
hass
|
||||
Home assistant object.
|
||||
template
|
||||
The template to calculate.
|
||||
action
|
||||
Callable to call with results. See above for arguments.
|
||||
variables
|
||||
Variables to pass to the template.
|
||||
|
||||
Returns:
|
||||
Callable to unregister the listener.
|
||||
Returns
|
||||
-------
|
||||
Callable to unregister the listener.
|
||||
|
||||
"""
|
||||
job = HassJob(action, f"track template {template}")
|
||||
@@ -1356,24 +1361,26 @@ def async_track_template_result(
|
||||
Once the template returns to a non-error condition the result is sent
|
||||
to the action as usual.
|
||||
|
||||
Args:
|
||||
hass:
|
||||
Home assistant object.
|
||||
track_templates:
|
||||
An iterable of TrackTemplate.
|
||||
action:
|
||||
Callable to call with results.
|
||||
strict:
|
||||
When set to True, raise on undefined variables.
|
||||
log_fn:
|
||||
If not None, template error messages will logging by calling log_fn
|
||||
instead of the normal logging facility.
|
||||
has_super_template:
|
||||
When set to True, the first template will block rendering of other
|
||||
templates if it doesn't render as True.
|
||||
Parameters
|
||||
----------
|
||||
hass
|
||||
Home assistant object.
|
||||
track_templates
|
||||
An iterable of TrackTemplate.
|
||||
action
|
||||
Callable to call with results.
|
||||
strict
|
||||
When set to True, raise on undefined variables.
|
||||
log_fn
|
||||
If not None, template error messages will logging by calling log_fn
|
||||
instead of the normal logging facility.
|
||||
has_super_template
|
||||
When set to True, the first template will block rendering of other
|
||||
templates if it doesn't render as True.
|
||||
|
||||
Returns:
|
||||
Info object used to unregister the listener, and refresh the template.
|
||||
Returns
|
||||
-------
|
||||
Info object used to unregister the listener, and refresh the template.
|
||||
|
||||
"""
|
||||
tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
|
||||
|
@@ -185,21 +185,6 @@ def report_usage(
|
||||
"""
|
||||
if (hass := _hass.hass) is None:
|
||||
raise RuntimeError("Frame helper not set up")
|
||||
integration_frame: IntegrationFrame | None = None
|
||||
integration_frame_err: MissingIntegrationFrame | None = None
|
||||
if not integration_domain:
|
||||
try:
|
||||
integration_frame = get_integration_frame(
|
||||
exclude_integrations=exclude_integrations
|
||||
)
|
||||
except MissingIntegrationFrame as err:
|
||||
# We need to be careful with assigning the error here as it affects the
|
||||
# cleanup of objects referenced from the stack trace as seen in
|
||||
# https://github.com/home-assistant/core/pull/148021#discussion_r2182379834
|
||||
# When core_behavior is ReportBehavior.ERROR, we will re-raise the error,
|
||||
# so we can safely assign it to integration_frame_err.
|
||||
if core_behavior is ReportBehavior.ERROR:
|
||||
integration_frame_err = err
|
||||
_report_usage_partial = functools.partial(
|
||||
_report_usage,
|
||||
hass,
|
||||
@@ -208,9 +193,8 @@ def report_usage(
|
||||
core_behavior=core_behavior,
|
||||
core_integration_behavior=core_integration_behavior,
|
||||
custom_integration_behavior=custom_integration_behavior,
|
||||
exclude_integrations=exclude_integrations,
|
||||
integration_domain=integration_domain,
|
||||
integration_frame=integration_frame,
|
||||
integration_frame_err=integration_frame_err,
|
||||
level=level,
|
||||
)
|
||||
if hass.loop_thread_id != threading.get_ident():
|
||||
@@ -228,9 +212,8 @@ def _report_usage(
|
||||
core_behavior: ReportBehavior,
|
||||
core_integration_behavior: ReportBehavior,
|
||||
custom_integration_behavior: ReportBehavior,
|
||||
exclude_integrations: set[str] | None,
|
||||
integration_domain: str | None,
|
||||
integration_frame: IntegrationFrame | None,
|
||||
integration_frame_err: MissingIntegrationFrame | None,
|
||||
level: int,
|
||||
) -> None:
|
||||
"""Report incorrect code usage.
|
||||
@@ -252,10 +235,12 @@ def _report_usage(
|
||||
_report_usage_no_integration(what, core_behavior, breaks_in_ha_version, None)
|
||||
return
|
||||
|
||||
if not integration_frame:
|
||||
_report_usage_no_integration(
|
||||
what, core_behavior, breaks_in_ha_version, integration_frame_err
|
||||
try:
|
||||
integration_frame = get_integration_frame(
|
||||
exclude_integrations=exclude_integrations
|
||||
)
|
||||
except MissingIntegrationFrame as err:
|
||||
_report_usage_no_integration(what, core_behavior, breaks_in_ha_version, err)
|
||||
return
|
||||
|
||||
integration_behavior = core_integration_behavior
|
||||
|
@@ -777,23 +777,7 @@ def _selector_serializer(schema: Any) -> Any: # noqa: C901
|
||||
return result
|
||||
|
||||
if isinstance(schema, selector.ObjectSelector):
|
||||
result = {"type": "object"}
|
||||
if fields := schema.config.get("fields"):
|
||||
result["properties"] = {
|
||||
field: convert(
|
||||
selector.selector(field_schema["selector"]),
|
||||
custom_serializer=_selector_serializer,
|
||||
)
|
||||
for field, field_schema in fields.items()
|
||||
}
|
||||
else:
|
||||
result["additionalProperties"] = True
|
||||
if schema.config.get("multiple"):
|
||||
result = {
|
||||
"type": "array",
|
||||
"items": result,
|
||||
}
|
||||
return result
|
||||
return {"type": "object", "additionalProperties": True}
|
||||
|
||||
if isinstance(schema, selector.SelectSelector):
|
||||
options = [
|
||||
|
@@ -1066,7 +1066,6 @@ class NumberSelectorConfig(BaseSelectorConfig, total=False):
|
||||
step: float | Literal["any"]
|
||||
unit_of_measurement: str
|
||||
mode: NumberSelectorMode
|
||||
translation_key: str
|
||||
|
||||
|
||||
class NumberSelectorMode(StrEnum):
|
||||
@@ -1107,7 +1106,6 @@ class NumberSelector(Selector[NumberSelectorConfig]):
|
||||
vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.All(
|
||||
vol.Coerce(NumberSelectorMode), lambda val: val.value
|
||||
),
|
||||
vol.Optional("translation_key"): str,
|
||||
}
|
||||
),
|
||||
validate_slider,
|
||||
|
@@ -86,7 +86,6 @@ ALL_SERVICE_DESCRIPTIONS_CACHE: HassKey[
|
||||
def _base_components() -> dict[str, ModuleType]:
|
||||
"""Return a cached lookup of base components."""
|
||||
from homeassistant.components import ( # noqa: PLC0415
|
||||
ai_task,
|
||||
alarm_control_panel,
|
||||
assist_satellite,
|
||||
calendar,
|
||||
@@ -108,7 +107,6 @@ def _base_components() -> dict[str, ModuleType]:
|
||||
)
|
||||
|
||||
return {
|
||||
"ai_task": ai_task,
|
||||
"alarm_control_panel": alarm_control_panel,
|
||||
"assist_satellite": assist_satellite,
|
||||
"calendar": calendar,
|
||||
|
@@ -938,17 +938,15 @@ class AllStates:
|
||||
def __call__(
|
||||
self,
|
||||
entity_id: str,
|
||||
rounded: bool | object = _SENTINEL,
|
||||
rounded: bool = True,
|
||||
with_unit: bool = False,
|
||||
) -> str:
|
||||
"""Return the states."""
|
||||
state = _get_state(self._hass, entity_id)
|
||||
if state is None:
|
||||
return STATE_UNKNOWN
|
||||
if rounded is _SENTINEL:
|
||||
rounded = with_unit
|
||||
if rounded or with_unit:
|
||||
return state.format_state(rounded, with_unit) # type: ignore[arg-type]
|
||||
return state.format_state(rounded, with_unit)
|
||||
return state.state
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
@@ -147,15 +147,11 @@ async def _register_trigger_platform(
|
||||
)
|
||||
return
|
||||
|
||||
# We don't use gather here because gather adds additional overhead
|
||||
# when wrapping each coroutine in a task, and we expect our listeners
|
||||
# to call trigger.async_get_all_descriptions which will only yield
|
||||
# the first time it's called, after that it returns cached data.
|
||||
for listener in hass.data[TRIGGER_PLATFORM_SUBSCRIPTIONS]:
|
||||
try:
|
||||
await listener(new_triggers)
|
||||
except Exception:
|
||||
_LOGGER.exception("Error while notifying trigger platform listener")
|
||||
tasks: list[asyncio.Task[None]] = [
|
||||
create_eager_task(listener(new_triggers))
|
||||
for listener in hass.data[TRIGGER_PLATFORM_SUBSCRIPTIONS]
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
class Trigger(abc.ABC):
|
||||
|
@@ -67,7 +67,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
#
|
||||
BASE_PRELOAD_PLATFORMS = [
|
||||
"backup",
|
||||
"condition",
|
||||
"config",
|
||||
"config_flow",
|
||||
"diagnostics",
|
||||
@@ -858,11 +857,6 @@ class Integration:
|
||||
# True.
|
||||
return self.manifest.get("import_executor", True)
|
||||
|
||||
@cached_property
|
||||
def has_conditions(self) -> bool:
|
||||
"""Return if the integration has conditions."""
|
||||
return "conditions.yaml" in self._top_level_files
|
||||
|
||||
@cached_property
|
||||
def has_services(self) -> bool:
|
||||
"""Return if the integration has services."""
|
||||
@@ -1795,13 +1789,6 @@ def async_get_issue_tracker(
|
||||
# If we know nothing about the integration, suggest opening an issue on HA core
|
||||
return issue_tracker
|
||||
|
||||
if module and not integration_domain:
|
||||
# If we only have a module, we can try to get the integration domain from it
|
||||
if module.startswith("custom_components."):
|
||||
integration_domain = module.split(".")[1]
|
||||
elif module.startswith("homeassistant.components."):
|
||||
integration_domain = module.split(".")[2]
|
||||
|
||||
if not integration:
|
||||
integration = async_get_issue_integration(hass, integration_domain)
|
||||
|
||||
|
@@ -38,7 +38,7 @@ habluetooth==3.49.0
|
||||
hass-nabucasa==0.105.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250702.1
|
||||
home-assistant-frontend==20250702.0
|
||||
home-assistant-intents==2025.6.23
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@@ -3566,16 +3566,6 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.opower.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.oralb.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@@ -913,5 +913,4 @@ split-on-trailing-comma = false
|
||||
max-complexity = 25
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
property-decorators = ["propcache.api.cached_property"]
|
||||
|
18
requirements_all.txt
generated
18
requirements_all.txt
generated
@@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.2.3
|
||||
aioamazondevices==3.2.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -204,7 +204,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==1.2.0
|
||||
aioautomower==1.0.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -372,7 +372,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.42
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==4.7.0
|
||||
aiorussound==4.6.1
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -477,7 +477,7 @@ amcrest==1.9.8
|
||||
androidtv[async]==0.0.75
|
||||
|
||||
# homeassistant.components.androidtv_remote
|
||||
androidtvremote2==0.2.3
|
||||
androidtvremote2==0.2.2
|
||||
|
||||
# homeassistant.components.anel_pwrctrl
|
||||
anel-pwrctrl-homeassistant==0.0.1.dev2
|
||||
@@ -1168,7 +1168,7 @@ hole==0.8.0
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250702.1
|
||||
home-assistant-frontend==20250702.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.23
|
||||
@@ -1929,7 +1929,7 @@ pydiscovergy==3.0.2
|
||||
pydoods==1.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2025.7.0
|
||||
pydrawise==2025.6.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==3.0.0
|
||||
@@ -1962,7 +1962,7 @@ pyeiscp==0.0.7
|
||||
pyemoncms==0.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==2.2.0
|
||||
pyenphase==2.1.0
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==4.7
|
||||
@@ -3041,7 +3041,7 @@ vehicle==2.2.2
|
||||
velbus-aio==2025.5.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.21
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.5.0
|
||||
@@ -3084,7 +3084,7 @@ waterfurnace==1.1.0
|
||||
watergate-local-api==2024.4.1
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==1.4.1
|
||||
weatherflow4py==1.3.1
|
||||
|
||||
# homeassistant.components.cisco_webex_teams
|
||||
webexpythonsdk==2.0.1
|
||||
|
@@ -11,7 +11,7 @@ astroid==3.3.10
|
||||
coverage==7.9.1
|
||||
freezegun==1.5.2
|
||||
go2rtc-client==0.2.1
|
||||
license-expression==30.4.3
|
||||
license-expression==30.4.1
|
||||
mock-open==1.4.0
|
||||
mypy-dev==1.17.0a4
|
||||
pre-commit==4.2.0
|
||||
|
18
requirements_test_all.txt
generated
18
requirements_test_all.txt
generated
@@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.2.3
|
||||
aioamazondevices==3.2.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -192,7 +192,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==1.2.0
|
||||
aioautomower==1.0.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -354,7 +354,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.42
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==4.7.0
|
||||
aiorussound==4.6.1
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -453,7 +453,7 @@ amberelectric==2.0.12
|
||||
androidtv[async]==0.0.75
|
||||
|
||||
# homeassistant.components.androidtv_remote
|
||||
androidtvremote2==0.2.3
|
||||
androidtvremote2==0.2.2
|
||||
|
||||
# homeassistant.components.anova
|
||||
anova-wifi==0.17.0
|
||||
@@ -1017,7 +1017,7 @@ hole==0.8.0
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250702.1
|
||||
home-assistant-frontend==20250702.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.23
|
||||
@@ -1610,7 +1610,7 @@ pydexcom==0.2.3
|
||||
pydiscovergy==3.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2025.7.0
|
||||
pydrawise==2025.6.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==3.0.0
|
||||
@@ -1637,7 +1637,7 @@ pyeiscp==0.0.7
|
||||
pyemoncms==0.1.1
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==2.2.0
|
||||
pyenphase==2.1.0
|
||||
|
||||
# homeassistant.components.everlights
|
||||
pyeverlights==0.1.0
|
||||
@@ -2509,7 +2509,7 @@ vehicle==2.2.2
|
||||
velbus-aio==2025.5.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.21
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.5.0
|
||||
@@ -2543,7 +2543,7 @@ watchdog==6.0.0
|
||||
watergate-local-api==2024.4.1
|
||||
|
||||
# homeassistant.components.weatherflow_cloud
|
||||
weatherflow4py==1.4.1
|
||||
weatherflow4py==1.3.1
|
||||
|
||||
# homeassistant.components.nasweb
|
||||
webio-api==0.1.11
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user