Compare commits

..

1 Commits

Author SHA1 Message Date
abmantis
be1ba31ff2 Set state() template round param default to True 2025-07-03 17:15:54 +01:00
157 changed files with 1599 additions and 3795 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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']
});

View File

@@ -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.*

View File

@@ -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)),
)

View File

@@ -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,

View File

@@ -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."

View File

@@ -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:

View File

@@ -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."
}
}
}

View File

@@ -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)}>"

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.2.3"]
"requirements": ["aioamazondevices==3.2.2"]
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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]

View File

@@ -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)

View File

@@ -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."]
}

View File

@@ -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)

View File

@@ -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:

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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"): [
{

View File

@@ -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

View File

@@ -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(),

View File

@@ -52,11 +52,6 @@
"h": "Hours",
"d": "Days"
}
},
"round": {
"unit_of_measurement": {
"decimals": "decimals"
}
}
}
}

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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%]",

View File

@@ -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)

View File

@@ -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

View File

@@ -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."

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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,
}

View File

@@ -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": {

View File

@@ -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
)

View File

@@ -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 "",
)

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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": {

View File

@@ -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]

View File

@@ -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

View File

@@ -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": {

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==1.2.0"]
"requirements": ["aioautomower==1.0.1"]
}

View File

@@ -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"]
}

View File

@@ -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"

View File

@@ -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
),
),

View File

@@ -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

View File

@@ -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.5100.0%
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5100.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,

View File

@@ -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=(

View File

@@ -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=(

View File

@@ -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=(

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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)

View File

@@ -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"]
}

View File

@@ -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",

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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),
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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}"

View File

@@ -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()

View File

@@ -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(

View File

@@ -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):

View File

@@ -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."]
}

View File

@@ -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]:

View File

@@ -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(

View File

@@ -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(

View File

@@ -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:

View File

@@ -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)

View File

@@ -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 []

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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",

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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 = [

View File

@@ -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,

View File

@@ -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,

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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
View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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