Merge branch 'dev' into subscribe_config_flow_init_remove

This commit is contained in:
Erik Montnemery
2025-04-07 16:09:19 +02:00
committed by GitHub
327 changed files with 17760 additions and 3371 deletions

View File

@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Dependency review - name: Dependency review
uses: actions/dependency-review-action@v4.5.0 uses: actions/dependency-review-action@v4.6.0
with: with:
license-check: false # We use our own license audit checks license-check: false # We use our own license audit checks

View File

@@ -859,8 +859,14 @@ async def _async_set_up_integrations(
integrations, all_integrations = await _async_resolve_domains_and_preload( integrations, all_integrations = await _async_resolve_domains_and_preload(
hass, config hass, config
) )
all_domains = set(all_integrations) # Detect all cycles
domains = set(integrations) integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, all_integrations.values(), set(all_integrations)
)
)
all_domains = set(integrations_after_dependencies)
domains = set(integrations) & all_domains
_LOGGER.info( _LOGGER.info(
"Domains to be set up: %s | %s", "Domains to be set up: %s | %s",
@@ -868,6 +874,8 @@ async def _async_set_up_integrations(
all_domains - domains, all_domains - domains,
) )
async_set_domains_to_be_loaded(hass, all_domains)
# Initialize recorder # Initialize recorder
if "recorder" in all_domains: if "recorder" in all_domains:
recorder.async_initialize_recorder(hass) recorder.async_initialize_recorder(hass)
@@ -900,24 +908,12 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered = { stage_dep_domains_unfiltered = {
dep dep
for domain in stage_domains for domain in stage_domains
for dep in all_integrations[domain].all_dependencies for dep in integrations_after_dependencies[domain]
if dep not in stage_domains if dep not in stage_domains
} }
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
stage_all_domains = stage_domains | stage_dep_domains stage_all_domains = stage_domains | stage_dep_domains
stage_all_integrations = {
domain: all_integrations[domain] for domain in stage_all_domains
}
# Detect all cycles
stage_integrations_after_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, stage_all_integrations.values(), stage_all_domains
)
)
stage_all_domains = set(stage_integrations_after_dependencies)
stage_domains &= stage_all_domains
stage_dep_domains &= stage_all_domains
_LOGGER.info( _LOGGER.info(
"Setting up stage %s: %s | %s\nDependencies: %s | %s", "Setting up stage %s: %s | %s\nDependencies: %s | %s",
@@ -928,8 +924,6 @@ async def _async_set_up_integrations(
stage_dep_domains_unfiltered - stage_dep_domains, stage_dep_domains_unfiltered - stage_dep_domains,
) )
async_set_domains_to_be_loaded(hass, stage_all_domains)
if timeout is None: if timeout is None:
await _async_setup_multi_components(hass, stage_all_domains, config) await _async_setup_multi_components(hass, stage_all_domains, config)
continue continue

View File

@@ -0,0 +1,5 @@
{
"domain": "eve",
"name": "Eve",
"iot_standards": ["matter"]
}

View File

@@ -72,10 +72,10 @@
"level": { "level": {
"name": "Level", "name": "Level",
"state": { "state": {
"high": "High", "high": "[%key:common::state::high%]",
"low": "Low", "low": "[%key:common::state::low%]",
"moderate": "Moderate", "moderate": "Moderate",
"very_high": "Very high" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@@ -89,10 +89,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@@ -123,10 +123,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@@ -167,10 +167,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@@ -181,10 +181,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }
@@ -195,10 +195,10 @@
"level": { "level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]", "name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": { "state": {
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]", "high": "[%key:common::state::high%]",
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]", "low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]", "moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]" "very_high": "[%key:common::state::very_high%]"
} }
} }
} }

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.9"] "requirements": ["aioairzone==1.0.0"]
} }

View File

@@ -9,6 +9,8 @@ from aioairzone.const import (
AZD_HUMIDITY, AZD_HUMIDITY,
AZD_TEMP, AZD_TEMP,
AZD_TEMP_UNIT, AZD_TEMP_UNIT,
AZD_THERMOSTAT_BATTERY,
AZD_THERMOSTAT_SIGNAL,
AZD_WEBSERVER, AZD_WEBSERVER,
AZD_WIFI_RSSI, AZD_WIFI_RSSI,
AZD_ZONES, AZD_ZONES,
@@ -73,6 +75,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
), ),
SensorEntityDescription(
device_class=SensorDeviceClass.BATTERY,
key=AZD_THERMOSTAT_BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
key=AZD_THERMOSTAT_SIGNAL,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="thermostat_signal",
),
) )

View File

@@ -76,6 +76,9 @@
"sensor": { "sensor": {
"rssi": { "rssi": {
"name": "RSSI" "name": "RSSI"
},
"thermostat_signal": {
"name": "Signal strength"
} }
} }
} }

View File

@@ -20,6 +20,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ( from homeassistant.config_entries import (
SOURCE_IGNORE, SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_ZEROCONF, SOURCE_ZEROCONF,
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
@@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_IDENTIFIERS: list(combined_identifiers), CONF_IDENTIFIERS: list(combined_identifiers),
}, },
) )
if entry.source != SOURCE_IGNORE: # Don't reload ignored entries or in the middle of reauth,
# e.g. if the user is entering a new PIN
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
self.hass.config_entries.async_schedule_reload(entry.entry_id) self.hass.config_entries.async_schedule_reload(entry.entry_id)
if not allow_exist: if not allow_exist:
raise DeviceAlreadyConfigured raise DeviceAlreadyConfigured

View File

@@ -36,9 +36,9 @@
"wi_fi_strength": { "wi_fi_strength": {
"name": "Wi-Fi strength", "name": "Wi-Fi strength",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
} }
} }

View File

@@ -103,8 +103,8 @@
"temperature_range": { "temperature_range": {
"name": "Temperature range", "name": "Temperature range",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
} }
}, },

View File

@@ -124,8 +124,8 @@
"battery": { "battery": {
"name": "Battery", "name": "Battery",
"state": { "state": {
"off": "Normal", "off": "[%key:common::state::normal%]",
"on": "Low" "on": "[%key:common::state::low%]"
} }
}, },
"battery_charging": { "battery_charging": {
@@ -145,7 +145,7 @@
"cold": { "cold": {
"name": "Cold", "name": "Cold",
"state": { "state": {
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]", "off": "[%key:common::state::normal%]",
"on": "Cold" "on": "Cold"
} }
}, },
@@ -180,7 +180,7 @@
"heat": { "heat": {
"name": "Heat", "name": "Heat",
"state": { "state": {
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]", "off": "[%key:common::state::normal%]",
"on": "Hot" "on": "Hot"
} }
}, },

View File

@@ -19,7 +19,7 @@
"bleak-retry-connector==3.9.0", "bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4", "bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.4.5", "bluetooth-auto-recovery==1.4.5",
"bluetooth-data-tools==1.26.1", "bluetooth-data-tools==1.27.0",
"dbus-fast==2.43.0", "dbus-fast==2.43.0",
"habluetooth==3.37.0" "habluetooth==3.37.0"
] ]

View File

@@ -16,6 +16,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import DOMAIN from .const import DOMAIN
@@ -91,11 +92,22 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered[CONF_ACCESS_TOKEN] = token self._discovered[CONF_ACCESS_TOKEN] = token
try: try:
_, hub_name = await _validate_input(self.hass, self._discovered) bond_id, hub_name = await _validate_input(self.hass, self._discovered)
except InputValidationError: except InputValidationError:
return return
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._discovered[CONF_NAME] = hub_name self._discovered[CONF_NAME] = hub_name
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by dhcp discovery."""
host = discovery_info.ip
bond_id = discovery_info.hostname.partition("-")[2].upper()
await self.async_set_unique_id(bond_id)
return await self.async_step_any_discovery(bond_id, host)
async def async_step_zeroconf( async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@@ -104,11 +116,17 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
host: str = discovery_info.host host: str = discovery_info.host
bond_id = name.partition(".")[0] bond_id = name.partition(".")[0]
await self.async_set_unique_id(bond_id) await self.async_set_unique_id(bond_id)
return await self.async_step_any_discovery(bond_id, host)
async def async_step_any_discovery(
self, bond_id: str, host: str
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
for entry in self._async_current_entries(): for entry in self._async_current_entries():
if entry.unique_id != bond_id: if entry.unique_id != bond_id:
continue continue
updates = {CONF_HOST: host} updates = {CONF_HOST: host}
if entry.state == ConfigEntryState.SETUP_ERROR and ( if entry.state is ConfigEntryState.SETUP_ERROR and (
token := await async_get_token(self.hass, host) token := await async_get_token(self.hass, host)
): ):
updates[CONF_ACCESS_TOKEN] = token updates[CONF_ACCESS_TOKEN] = token
@@ -153,10 +171,14 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_HOST: self._discovered[CONF_HOST], CONF_HOST: self._discovered[CONF_HOST],
} }
try: try:
_, hub_name = await _validate_input(self.hass, data) bond_id, hub_name = await _validate_input(self.hass, data)
except InputValidationError as error: except InputValidationError as error:
errors["base"] = error.base errors["base"] = error.base
else: else:
await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self._discovered[CONF_HOST]}
)
return self.async_create_entry( return self.async_create_entry(
title=hub_name, title=hub_name,
data=data, data=data,
@@ -185,8 +207,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
except InputValidationError as error: except InputValidationError as error:
errors["base"] = error.base errors["base"] = error.base
else: else:
await self.async_set_unique_id(bond_id) await self.async_set_unique_id(bond_id, raise_on_progress=False)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured(
updates={CONF_HOST: user_input[CONF_HOST]}
)
return self.async_create_entry(title=hub_name, data=user_input) return self.async_create_entry(title=hub_name, data=user_input)
return self.async_show_form( return self.async_show_form(

View File

@@ -3,6 +3,16 @@
"name": "Bond", "name": "Bond",
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
"config_flow": true, "config_flow": true,
"dhcp": [
{
"hostname": "bond-*",
"macaddress": "3C6A2C1*"
},
{
"hostname": "bond-*",
"macaddress": "F44E38*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bond", "documentation": "https://www.home-assistant.io/integrations/bond",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["bond_async"], "loggers": ["bond_async"],

View File

@@ -9,7 +9,7 @@ from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
@@ -34,10 +34,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
await panel.connect() await panel.connect()
except (PermissionError, ValueError) as err: except (PermissionError, ValueError) as err:
await panel.disconnect() await panel.disconnect()
raise ConfigEntryNotReady from err raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from err
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err: except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
await panel.disconnect() await panel.disconnect()
raise ConfigEntryNotReady("Connection failed") from err raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
entry.runtime_data = panel entry.runtime_data = panel

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Mapping
import logging import logging
import ssl import ssl
from typing import Any from typing import Any
@@ -163,3 +164,55 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(schema, user_input), data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors, errors=errors,
) )
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an authentication error."""
self._data = dict(entry_data)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reauth step."""
errors: dict[str, str] = {}
# Each model variant requires a different authentication flow
if "Solution" in self._data[CONF_MODEL]:
schema = STEP_AUTH_DATA_SCHEMA_SOLUTION
elif "AMAX" in self._data[CONF_MODEL]:
schema = STEP_AUTH_DATA_SCHEMA_AMAX
else:
schema = STEP_AUTH_DATA_SCHEMA_BG
if user_input is not None:
reauth_entry = self._get_reauth_entry()
self._data.update(user_input)
try:
(_, _) = await try_connect(self._data, Panel.LOAD_EXTENDED_INFO)
except (PermissionError, ValueError) as e:
errors["base"] = "invalid_auth"
_LOGGER.error("Authentication Error: %s", e)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
TimeoutError,
) as e:
_LOGGER.error("Connection Error: %s", e)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)

View File

@@ -0,0 +1,73 @@
"""Diagnostics for bosch alarm."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from . import BoschAlarmConfigEntry
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: BoschAlarmConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": {
"model": entry.runtime_data.model,
"serial_number": entry.runtime_data.serial_number,
"protocol_version": entry.runtime_data.protocol_version,
"firmware_version": entry.runtime_data.firmware_version,
"areas": [
{
"id": area_id,
"name": area.name,
"all_ready": area.all_ready,
"part_ready": area.part_ready,
"faults": area.faults,
"alarms": area.alarms,
"disarmed": area.is_disarmed(),
"arming": area.is_arming(),
"pending": area.is_pending(),
"part_armed": area.is_part_armed(),
"all_armed": area.is_all_armed(),
"armed": area.is_armed(),
"triggered": area.is_triggered(),
}
for area_id, area in entry.runtime_data.areas.items()
],
"points": [
{
"id": point_id,
"name": point.name,
"open": point.is_open(),
"normal": point.is_normal(),
}
for point_id, point in entry.runtime_data.points.items()
],
"doors": [
{
"id": door_id,
"name": door.name,
"open": door.is_open(),
"locked": door.is_locked(),
}
for door_id, door in entry.runtime_data.doors.items()
],
"outputs": [
{
"id": output_id,
"name": output.name,
"active": output.is_active(),
}
for output_id, output in entry.runtime_data.outputs.items()
],
"history_events": entry.runtime_data.events,
},
}

View File

@@ -40,7 +40,7 @@ rules:
integration-owner: done integration-owner: done
log-when-unavailable: todo log-when-unavailable: todo
parallel-updates: todo parallel-updates: todo
reauthentication-flow: todo reauthentication-flow: done
test-coverage: done test-coverage: done
# Gold # Gold

View File

@@ -22,6 +22,18 @@
"installer_code": "The installer code from your panel", "installer_code": "The installer code from your panel",
"user_code": "The user code from your panel" "user_code": "The user code from your panel"
} }
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data::installer_code%]",
"user_code": "[%key:component::bosch_alarm::config::step::auth::data::user_code%]"
},
"data_description": {
"password": "[%key:component::bosch_alarm::config::step::auth::data_description::password%]",
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data_description::installer_code%]",
"user_code": "[%key:component::bosch_alarm::config::step::auth::data_description::user_code%]"
}
} }
}, },
"error": { "error": {
@@ -30,7 +42,16 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"exceptions": {
"cannot_connect": {
"message": "Could not connect to panel."
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
} }
} }
} }

View File

@@ -74,7 +74,7 @@
}, },
"get_events": { "get_events": {
"name": "Get events", "name": "Get events",
"description": "Get events on a calendar within a time range.", "description": "Retrieves events on a calendar within a time range.",
"fields": { "fields": {
"start_date_time": { "start_date_time": {
"name": "Start time", "name": "Start time",

View File

@@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
flow_id=flow_id, user_input=tokens flow_id=flow_id, user_input=tokens
) )
self.hass.async_create_task(await_tokens()) # It's a background task because it should be cancelled on shutdown and there's nothing else
# we can do in such case. There's also no need to wait for this during setup.
self.hass.async_create_background_task(
await_tokens(), name="Awaiting OAuth tokens"
)
return authorize_url return authorize_url

View File

@@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
async def async_set_humidity(self, humidity: int) -> None: async def async_set_humidity(self, humidity: int) -> None:
"""Set new target humidity.""" """Set new target humidity."""
if self.mode == HumidifierComelitMode.OFF: if not self._attr_is_on:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="humidity_while_off", translation_key="humidity_while_off",
@@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
await self.coordinator.api.set_humidity_status( await self.coordinator.api.set_humidity_status(
self._device.index, self._set_command self._device.index, self._set_command
) )
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off.""" """Turn off."""
await self.coordinator.api.set_humidity_status( await self.coordinator.api.set_humidity_status(
self._device.index, HumidifierComelitCommand.OFF self._device.index, HumidifierComelitCommand.OFF
) )
self._attr_is_on = False
self.async_write_ha_state()

View File

@@ -52,7 +52,9 @@
"rest": "Rest", "rest": "Rest",
"sabotated": "Sabotated" "sabotated": "Sabotated"
} }
}
}, },
"humidifier": {
"humidifier": { "humidifier": {
"name": "Humidifier" "name": "Humidifier"
}, },

View File

@@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.util.ssl import client_context_no_verify
from .const import KEY_MAC, TIMEOUT from .const import KEY_MAC, TIMEOUT
from .coordinator import DaikinConfigEntry, DaikinCoordinator from .coordinator import DaikinConfigEntry, DaikinCoordinator
@@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
key=entry.data.get(CONF_API_KEY), key=entry.data.get(CONF_API_KEY),
uuid=entry.data.get(CONF_UUID), uuid=entry.data.get(CONF_UUID),
password=entry.data.get(CONF_PASSWORD), password=entry.data.get(CONF_PASSWORD),
ssl_context=client_context_no_verify(),
) )
_LOGGER.debug("Connection to %s successful", host) _LOGGER.debug("Connection to %s successful", host)
except TimeoutError as err: except TimeoutError as err:

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin", "documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pydaikin"], "loggers": ["pydaikin"],
"requirements": ["pydaikin==2.14.1"], "requirements": ["pydaikin==2.15.0"],
"zeroconf": ["_dkapi._tcp.local."] "zeroconf": ["_dkapi._tcp.local."]
} }

View File

@@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["dsmr_parser"], "loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.4.2"] "requirements": ["dsmr-parser==1.4.3"]
} }

View File

@@ -51,8 +51,8 @@
"electricity_active_tariff": { "electricity_active_tariff": {
"name": "Active tariff", "name": "Active tariff",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"normal": "Normal" "normal": "[%key:common::state::normal%]"
} }
}, },
"electricity_delivered_tariff_1": { "electricity_delivered_tariff_1": {

View File

@@ -140,8 +140,8 @@
"electricity_tariff": { "electricity_tariff": {
"name": "Electricity tariff", "name": "Electricity tariff",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
}, },
"power_failure_count": { "power_failure_count": {

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"], "loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"] "requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
} }

View File

@@ -176,9 +176,9 @@
"water_amount": { "water_amount": {
"name": "Water flow level", "name": "Water flow level",
"state": { "state": {
"high": "High", "high": "[%key:common::state::high%]",
"low": "Low", "low": "[%key:common::state::low%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"ultrahigh": "Ultrahigh" "ultrahigh": "Ultrahigh"
} }
}, },

View File

@@ -9,7 +9,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN from .const import DOMAIN
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT] PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.SENSOR]
async def async_setup_entry( async def async_setup_entry(

View File

@@ -0,0 +1,18 @@
{
"entity": {
"sensor": {
"current_speed": {
"default": "mdi:pump"
},
"service_hours": {
"default": "mdi:wrench-clock"
},
"error_code": {
"default": "mdi:alert-octagon",
"state": {
"no_error": "mdi:check-circle"
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
"""EHEIM Digital sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterErrorCode
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
"""Class describing EHEIM Digital sensor entities."""
value_fn: Callable[[_DeviceT_co], float | str | None]
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSensorDescription[EheimDigitalClassicVario], ...
] = (
EheimDigitalSensorDescription[EheimDigitalClassicVario](
key="current_speed",
translation_key="current_speed",
value_fn=lambda device: device.current_speed,
native_unit_of_measurement=PERCENTAGE,
),
EheimDigitalSensorDescription[EheimDigitalClassicVario](
key="service_hours",
translation_key="service_hours",
value_fn=lambda device: device.service_hours,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
suggested_unit_of_measurement=UnitOfTime.DAYS,
entity_category=EntityCategory.DIAGNOSTIC,
),
EheimDigitalSensorDescription[EheimDigitalClassicVario](
key="error_code",
translation_key="error_code",
value_fn=(
lambda device: device.error_code.name.lower()
if device.error_code is not None
else None
),
device_class=SensorDeviceClass.ENUM,
options=[name.lower() for name in FilterErrorCode._member_names_],
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: EheimDigitalConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the callbacks for the coordinator so lights can be added as devices are found."""
coordinator = entry.runtime_data
def async_setup_device_entities(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the light entities for one or multiple devices."""
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities += [
EheimDigitalSensor[EheimDigitalClassicVario](
coordinator, device, description
)
for description in CLASSICVARIO_DESCRIPTIONS
]
async_add_entities(entities)
coordinator.add_platform_callback(async_setup_device_entities)
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalSensor(
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
):
"""Represent a EHEIM Digital sensor entity."""
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalSensorDescription[_DeviceT_co],
) -> None:
"""Initialize an EHEIM Digital number entity."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{self._device_address}_{description.key}"
@override
def _async_update_attrs(self) -> None:
self._attr_native_value = self.entity_description.value_fn(self._device)

View File

@@ -46,6 +46,22 @@
} }
} }
} }
},
"sensor": {
"current_speed": {
"name": "Current speed"
},
"service_hours": {
"name": "Remaining hours until service"
},
"error_code": {
"name": "Error code",
"state": {
"no_error": "No error",
"rotor_stuck": "Rotor stuck",
"air_in_filter": "Air in filter"
}
}
} }
} }
} }

View File

@@ -66,6 +66,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
] ]
for end_point in end_points: for end_point in end_points:
try:
response = await envoy.request(end_point) response = await envoy.request(end_point)
fixture_data[end_point] = response.text.replace("\n", "").replace( fixture_data[end_point] = response.text.replace("\n", "").replace(
serial, CLEAN_TEXT serial, CLEAN_TEXT
@@ -76,6 +77,8 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
"code": response.status_code, "code": response.status_code,
} }
) )
except EnvoyError as err:
fixture_data[f"{end_point}_log"] = {"Error": repr(err)}
return fixture_data return fixture_data
@@ -160,10 +163,7 @@ async def async_get_config_entry_diagnostics(
fixture_data: dict[str, Any] = {} fixture_data: dict[str, Any] = {}
if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False): if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False):
try:
fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial) fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial)
except EnvoyError as err:
fixture_data["Error"] = repr(err)
diagnostic_data: dict[str, Any] = { diagnostic_data: dict[str, Any] = {
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT), "config_entry": async_redact_data(entry.as_dict(), TO_REDACT),

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyenphase"], "loggers": ["pyenphase"],
"requirements": ["pyenphase==1.25.1"], "requirements": ["pyenphase==1.25.5"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_enphase-envoy._tcp.local." "type": "_enphase-envoy._tcp.local."

View File

@@ -22,5 +22,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["eq3btsmart"], "loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.12.0"] "requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.13.1"]
} }

View File

@@ -13,7 +13,7 @@ from aioesphomeapi import (
APIConnectionError, APIConnectionError,
APIVersion, APIVersion,
DeviceInfo as EsphomeDeviceInfo, DeviceInfo as EsphomeDeviceInfo,
EncryptionHelloAPIError, EncryptionPlaintextAPIError,
EntityInfo, EntityInfo,
HomeassistantServiceCall, HomeassistantServiceCall,
InvalidAuthAPIError, InvalidAuthAPIError,
@@ -571,7 +571,7 @@ class ESPHomeManager:
if isinstance( if isinstance(
err, err,
( (
EncryptionHelloAPIError, EncryptionPlaintextAPIError,
RequiresEncryptionAPIError, RequiresEncryptionAPIError,
InvalidEncryptionKeyAPIError, InvalidEncryptionKeyAPIError,
InvalidAuthAPIError, InvalidAuthAPIError,

View File

@@ -16,9 +16,9 @@
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"], "mqtt": ["esphome/discover/#"],
"requirements": [ "requirements": [
"aioesphomeapi==29.8.0", "aioesphomeapi==29.9.0",
"esphome-dashboard-api==1.2.3", "esphome-dashboard-api==1.2.3",
"bleak-esphome==2.12.0" "bleak-esphome==2.13.1"
], ],
"zeroconf": ["_esphomelib._tcp.local."] "zeroconf": ["_esphomelib._tcp.local."]
} }

View File

@@ -152,7 +152,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
super().__init__(coordinator, evo_device) super().__init__(coordinator, evo_device)
self._evo_id = evo_device.id self._evo_id = evo_device.id
if evo_device.model.startswith("VisionProWifi"): if evo_device.id == evo_device.tcs.id:
# this system does not have a distinct ID for the zone # this system does not have a distinct ID for the zone
self._attr_unique_id = f"{evo_device.id}z" self._attr_unique_id = f"{evo_device.id}z"
else: else:

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["evohome", "evohomeasync", "evohomeasync2"], "loggers": ["evohome", "evohomeasync", "evohomeasync2"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["evohome-async==1.0.4"] "requirements": ["evohome-async==1.0.5"]
} }

View File

@@ -301,6 +301,7 @@ class FibaroController:
device.ha_id = ( device.ha_id = (
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}" f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
) )
platform = None
if device.enabled and (not device.is_plugin or self._import_plugins): if device.enabled and (not device.is_plugin or self._import_plugins):
platform = self._map_device_to_platform(device) platform = self._map_device_to_platform(device)
if platform is None: if platform is None:

View File

@@ -53,5 +53,5 @@
"documentation": "https://www.home-assistant.io/integrations/flux_led", "documentation": "https://www.home-assistant.io/integrations/flux_led",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["flux_led"], "loggers": ["flux_led"],
"requirements": ["flux-led==1.1.3"] "requirements": ["flux-led==1.2.0"]
} }

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/forecast_solar", "documentation": "https://www.home-assistant.io/integrations/forecast_solar",
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["forecast-solar==4.0.0"] "requirements": ["forecast-solar==4.1.0"]
} }

View File

@@ -20,6 +20,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class FritzBinarySensorEntityDescription( class FritzBinarySensorEntityDescription(

View File

@@ -31,6 +31,9 @@ from .entity import FritzDeviceBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Set a sane value to avoid too many updates
PARALLEL_UPDATES = 5
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class FritzButtonDescription(ButtonEntityDescription): class FritzButtonDescription(ButtonEntityDescription):

View File

@@ -22,6 +22,9 @@ from .entity import FritzDeviceBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -18,6 +18,9 @@ from .entity import FritzBoxBaseEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -14,9 +14,7 @@ rules:
docs-actions: done docs-actions: done
docs-high-level-description: done docs-high-level-description: done
docs-installation-instructions: done docs-installation-instructions: done
docs-removal-instructions: docs-removal-instructions: done
status: todo
comment: include the proper docs snippet
entity-event-setup: done entity-event-setup: done
entity-unique-id: done entity-unique-id: done
has-entity-name: has-entity-name:
@@ -31,15 +29,11 @@ rules:
action-exceptions: done action-exceptions: done
config-entry-unloading: done config-entry-unloading: done
docs-configuration-parameters: done docs-configuration-parameters: done
docs-installation-parameters: docs-installation-parameters: done
status: todo
comment: add the proper configuration_basic block
entity-unavailable: done entity-unavailable: done
integration-owner: done integration-owner: done
log-when-unavailable: done log-when-unavailable: done
parallel-updates: parallel-updates: done
status: todo
comment: not set at the moment, we use a coordinator
reauthentication-flow: done reauthentication-flow: done
test-coverage: test-coverage:
status: todo status: todo
@@ -50,7 +44,7 @@ rules:
diagnostics: done diagnostics: done
discovery-update-info: todo discovery-update-info: todo
discovery: done discovery: done
docs-data-update: todo docs-data-update: done
docs-examples: done docs-examples: done
docs-known-limitations: docs-known-limitations:
status: exempt status: exempt

View File

@@ -32,6 +32,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime: def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime:
"""Calculate uptime with deviation.""" """Calculate uptime with deviation."""

View File

@@ -38,6 +38,9 @@ from .entity import FritzBoxBaseEntity, FritzDeviceBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Set a sane value to avoid too many updates
PARALLEL_UPDATES = 5
async def _async_deflection_entities_list( async def _async_deflection_entities_list(
avm_wrapper: AvmWrapper, device_friendly_name: str avm_wrapper: AvmWrapper, device_friendly_name: str

View File

@@ -20,6 +20,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Set a sane value to avoid too many updates
PARALLEL_UPDATES = 5
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription): class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription):

View File

@@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
key="battery", key="battery",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
suitable=lambda device: device.battery_level is not None, suitable=lambda device: device.battery_level is not None,
native_value=lambda device: device.battery_level, native_value=lambda device: device.battery_level,

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250401.0"] "requirements": ["home-assistant-frontend==20250404.0"]
} }

View File

@@ -79,9 +79,9 @@
"state": { "state": {
"no_data": "No data", "no_data": "No data",
"too_low": "Too low", "too_low": "Too low",
"low": "Low", "low": "[%key:common::state::low%]",
"perfect": "Perfect", "perfect": "Perfect",
"high": "High", "high": "[%key:common::state::high%]",
"too_high": "Too high" "too_high": "Too high"
} }
}, },
@@ -90,9 +90,9 @@
"state": { "state": {
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", "low": "[%key:common::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", "high": "[%key:common::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
} }
}, },
@@ -101,9 +101,9 @@
"state": { "state": {
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", "low": "[%key:common::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", "high": "[%key:common::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
} }
}, },
@@ -112,9 +112,9 @@
"state": { "state": {
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", "low": "[%key:common::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", "high": "[%key:common::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
} }
}, },
@@ -123,9 +123,9 @@
"state": { "state": {
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", "low": "[%key:common::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", "high": "[%key:common::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
} }
}, },

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google", "documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"], "loggers": ["googleapiclient"],
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"] "requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
} }

View File

@@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Manage the options.""" """Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended: if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if user_input[CONF_LLM_HASS_API] == "none": if user_input[CONF_LLM_HASS_API] == "none":
user_input.pop(CONF_LLM_HASS_API) user_input.pop(CONF_LLM_HASS_API)
if not (
user_input.get(CONF_LLM_HASS_API, "none") != "none"
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
):
# Don't allow to save options that enable the Google Seearch tool with an Assist API
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
# Re-render the options again, now with the recommended options shown/hidden # Re-render the options again, now with the recommended options shown/hidden
self.last_rendered_recommended = user_input[CONF_RECOMMENDED] self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
options = { options = user_input
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
}
schema = await google_generative_ai_config_option_schema( schema = await google_generative_ai_config_option_schema(
self.hass, options, self._genai_client self.hass, options, self._genai_client
) )
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="init", data_schema=vol.Schema(schema), errors=errors
data_schema=vol.Schema(schema),
) )

View File

@@ -40,9 +40,13 @@
"enable_google_search_tool": "Enable Google Search tool" "enable_google_search_tool": "Enable Google Search tool"
}, },
"data_description": { "data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template." "prompt": "Instruct how the LLM should respond. This can be a template.",
"enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
} }
} }
},
"error": {
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
} }
}, },
"services": { "services": {

View File

@@ -265,6 +265,11 @@
"version_latest": { "version_latest": {
"name": "Newest version" "name": "Newest version"
} }
},
"update": {
"update": {
"name": "[%key:component::update::title%]"
}
} }
}, },
"services": { "services": {

View File

@@ -39,7 +39,7 @@ from .entity import (
from .update_helper import update_addon, update_core from .update_helper import update_addon, update_core
ENTITY_DESCRIPTION = UpdateEntityDescription( ENTITY_DESCRIPTION = UpdateEntityDescription(
name="Update", translation_key="update",
key=ATTR_VERSION_LATEST, key=ATTR_VERSION_LATEST,
) )

View File

@@ -2,6 +2,7 @@
ATTR_PASSWORD = "password" ATTR_PASSWORD = "password"
ATTR_USERNAME = "username" ATTR_USERNAME = "username"
ATTR_DESTINATION_POSITION = "destination_position"
ATTR_QUEUE_IDS = "queue_ids" ATTR_QUEUE_IDS = "queue_ids"
DOMAIN = "heos" DOMAIN = "heos"
ENTRY_TITLE = "HEOS System" ENTRY_TITLE = "HEOS System"
@@ -9,6 +10,7 @@ SERVICE_GET_QUEUE = "get_queue"
SERVICE_GROUP_VOLUME_SET = "group_volume_set" SERVICE_GROUP_VOLUME_SET = "group_volume_set"
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down" SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
SERVICE_GROUP_VOLUME_UP = "group_volume_up" SERVICE_GROUP_VOLUME_UP = "group_volume_up"
SERVICE_MOVE_QUEUE_ITEM = "move_queue_item"
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue" SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
SERVICE_SIGN_IN = "sign_in" SERVICE_SIGN_IN = "sign_in"
SERVICE_SIGN_OUT = "sign_out" SERVICE_SIGN_OUT = "sign_out"

View File

@@ -6,6 +6,9 @@
"remove_from_queue": { "remove_from_queue": {
"service": "mdi:playlist-remove" "service": "mdi:playlist-remove"
}, },
"move_queue_item": {
"service": "mdi:playlist-edit"
},
"group_volume_set": { "group_volume_set": {
"service": "mdi:volume-medium" "service": "mdi:volume-medium"
}, },

View File

@@ -479,6 +479,13 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
"""Remove items from the queue.""" """Remove items from the queue."""
await self._player.remove_from_queue(queue_ids) await self._player.remove_from_queue(queue_ids)
@catch_action_error("move queue item")
async def async_move_queue_item(
self, queue_ids: list[int], destination_position: int
) -> None:
"""Move items in the queue."""
await self._player.move_queue_item(queue_ids, destination_position)
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return True if the device is available.""" """Return True if the device is available."""

View File

@@ -19,6 +19,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.typing import VolDictType, VolSchemaType from homeassistant.helpers.typing import VolDictType, VolSchemaType
from .const import ( from .const import (
ATTR_DESTINATION_POSITION,
ATTR_PASSWORD, ATTR_PASSWORD,
ATTR_QUEUE_IDS, ATTR_QUEUE_IDS,
ATTR_USERNAME, ATTR_USERNAME,
@@ -27,6 +28,7 @@ from .const import (
SERVICE_GROUP_VOLUME_DOWN, SERVICE_GROUP_VOLUME_DOWN,
SERVICE_GROUP_VOLUME_SET, SERVICE_GROUP_VOLUME_SET,
SERVICE_GROUP_VOLUME_UP, SERVICE_GROUP_VOLUME_UP,
SERVICE_MOVE_QUEUE_ITEM,
SERVICE_REMOVE_FROM_QUEUE, SERVICE_REMOVE_FROM_QUEUE,
SERVICE_SIGN_IN, SERVICE_SIGN_IN,
SERVICE_SIGN_OUT, SERVICE_SIGN_OUT,
@@ -87,6 +89,16 @@ REMOVE_FROM_QUEUE_SCHEMA: Final[VolDictType] = {
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = { GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
} }
MOVE_QEUEUE_ITEM_SCHEMA: Final[VolDictType] = {
vol.Required(ATTR_QUEUE_IDS): vol.All(
cv.ensure_list,
[vol.All(vol.Coerce(int), vol.Range(min=1, max=1000))],
vol.Unique(),
),
vol.Required(ATTR_DESTINATION_POSITION): vol.All(
vol.Coerce(int), vol.Range(min=1, max=1000)
),
}
MEDIA_PLAYER_ENTITY_SERVICES: Final = ( MEDIA_PLAYER_ENTITY_SERVICES: Final = (
# Player queue services # Player queue services
@@ -96,6 +108,9 @@ MEDIA_PLAYER_ENTITY_SERVICES: Final = (
EntityServiceDescription( EntityServiceDescription(
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
), ),
EntityServiceDescription(
SERVICE_MOVE_QUEUE_ITEM, "async_move_queue_item", MOVE_QEUEUE_ITEM_SCHEMA
),
# Group volume services # Group volume services
EntityServiceDescription( EntityServiceDescription(
SERVICE_GROUP_VOLUME_SET, SERVICE_GROUP_VOLUME_SET,

View File

@@ -17,6 +17,26 @@ remove_from_queue:
multiple: true multiple: true
type: number type: number
move_queue_item:
target:
entity:
integration: heos
domain: media_player
fields:
queue_ids:
required: true
selector:
text:
multiple: true
type: number
destination_position:
required: true
selector:
number:
min: 1
max: 1000
step: 1
group_volume_set: group_volume_set:
target: target:
entity: entity:

View File

@@ -100,6 +100,20 @@
} }
} }
}, },
"move_queue_item": {
"name": "Move queue item",
"description": "Move one or more items within the play queue.",
"fields": {
"queue_ids": {
"name": "Queue IDs",
"description": "The IDs (indexes) of the items in the queue to move."
},
"destination_position": {
"name": "Destination position",
"description": "The position index in the queue to move the items to."
}
}
},
"group_volume_down": { "group_volume_down": {
"name": "Turn down group volume", "name": "Turn down group volume",
"description": "Turns down the group volume." "description": "Turns down the group volume."

View File

@@ -34,9 +34,9 @@
} }
}, },
"error": { "error": {
"invalid_username": "Failed to sign into Hive. Your email address is not recognised.", "invalid_username": "Failed to sign in to Hive. Your email address is not recognised.",
"invalid_password": "Failed to sign into Hive. Incorrect password, please try again.", "invalid_password": "Failed to sign in to Hive. Incorrect password, please try again.",
"invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.", "invalid_code": "Failed to sign in to Hive. Your two-factor authentication code was incorrect.",
"no_internet_available": "An Internet connection is required to connect to Hive.", "no_internet_available": "An Internet connection is required to connect to Hive.",
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },

View File

@@ -74,6 +74,19 @@ class HomeConnectApplianceData:
self.settings.update(other.settings) self.settings.update(other.settings)
self.status.update(other.status) self.status.update(other.status)
@classmethod
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
"""Return empty data."""
return cls(
commands=set(),
events={},
info=appliance,
options={},
programs=[],
settings={},
status={},
)
class HomeConnectCoordinator( class HomeConnectCoordinator(
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]] DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
@@ -362,15 +375,7 @@ class HomeConnectCoordinator(
model=appliance.vib, model=appliance.vib,
) )
if appliance.ha_id not in self.data: if appliance.ha_id not in self.data:
self.data[appliance.ha_id] = HomeConnectApplianceData( self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
commands=set(),
events={},
info=appliance,
options={},
programs=[],
settings={},
status={},
)
else: else:
self.data[appliance.ha_id].info.connected = appliance.connected self.data[appliance.ha_id].info.connected = appliance.connected
old_appliances.remove(appliance.ha_id) old_appliances.remove(appliance.ha_id)
@@ -406,6 +411,15 @@ class HomeConnectCoordinator(
name=appliance.name, name=appliance.name,
model=appliance.vib, model=appliance.vib,
) )
if not appliance.connected:
_LOGGER.debug(
"Appliance %s is not connected, skipping data fetch",
appliance.ha_id,
)
if appliance_data_to_update:
appliance_data_to_update.info.connected = False
return appliance_data_to_update
return HomeConnectApplianceData.empty(appliance)
try: try:
settings = { settings = {
setting.key: setting setting.key: setting

View File

@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/home_connect", "documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aiohomeconnect"], "loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.16.3"], "requirements": ["aiohomeconnect==0.17.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@@ -487,9 +487,9 @@
}, },
"warming_level": { "warming_level": {
"options": { "options": {
"cooking_oven_enum_type_warming_level_low": "Low", "cooking_oven_enum_type_warming_level_low": "[%key:common::state::low%]",
"cooking_oven_enum_type_warming_level_medium": "Medium", "cooking_oven_enum_type_warming_level_medium": "[%key:common::state::medium%]",
"cooking_oven_enum_type_warming_level_high": "High" "cooking_oven_enum_type_warming_level_high": "[%key:common::state::high%]"
} }
}, },
"washer_temperature": { "washer_temperature": {
@@ -522,9 +522,9 @@
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]", "laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low", "laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:common::state::low%]",
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium", "laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:common::state::medium%]",
"laundry_care_washer_enum_type_spin_speed_ul_high": "High" "laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:common::state::high%]"
} }
}, },
"vario_perfect": { "vario_perfect": {
@@ -1468,9 +1468,9 @@
"warming_level": { "warming_level": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_warming_level::name%]", "name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_warming_level::name%]",
"state": { "state": {
"cooking_oven_enum_type_warming_level_low": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_low%]", "cooking_oven_enum_type_warming_level_low": "[%key:common::state::low%]",
"cooking_oven_enum_type_warming_level_medium": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_medium%]", "cooking_oven_enum_type_warming_level_medium": "[%key:common::state::medium%]",
"cooking_oven_enum_type_warming_level_high": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_high%]" "cooking_oven_enum_type_warming_level_high": "[%key:common::state::high%]"
} }
}, },
"washer_temperature": { "washer_temperature": {
@@ -1505,9 +1505,9 @@
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]", "laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]", "laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:common::state::low%]",
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]", "laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:common::state::medium%]",
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_high%]" "laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:common::state::high%]"
} }
}, },
"vario_perfect": { "vario_perfect": {

View File

@@ -71,7 +71,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Postpone loading the config entry if the device is missing # Postpone loading the config entry if the device is missing
device_path = entry.data[DEVICE] device_path = entry.data[DEVICE]
if not await hass.async_add_executor_job(os.path.exists, device_path): if not await hass.async_add_executor_job(os.path.exists, device_path):
raise ConfigEntryNotReady raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_disconnected",
)
await hass.config_entries.async_forward_entry_setups(entry, ["update"]) await hass.config_entries.async_forward_entry_setups(entry, ["update"])

View File

@@ -5,17 +5,21 @@ from __future__ import annotations
from homeassistant.components.hardware.models import HardwareInfo, USBInfo from homeassistant.components.hardware.models import HardwareInfo, USBInfo
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .config_flow import HomeAssistantSkyConnectConfigFlow
from .const import DOMAIN from .const import DOMAIN
from .util import get_hardware_variant from .util import get_hardware_variant
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/" DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
EXPECTED_ENTRY_VERSION = (
HomeAssistantSkyConnectConfigFlow.VERSION,
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
)
@callback @callback
def async_info(hass: HomeAssistant) -> list[HardwareInfo]: def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
"""Return board info.""" """Return board info."""
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
return [ return [
HardwareInfo( HardwareInfo(
board=None, board=None,
@@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
url=DOCUMENTATION_URL, url=DOCUMENTATION_URL,
) )
for entry in entries for entry in entries
# Ignore unmigrated config entries in the hardware page
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
] ]

View File

@@ -195,5 +195,10 @@
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]", "run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]" "uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
} }
},
"exceptions": {
"device_disconnected": {
"message": "The device is not plugged in"
}
} }
} }

View File

@@ -659,13 +659,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit. # e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
# Can be 0 - 2 (Off, Heat, Cool) # Can be 0 - 2 (Off, Heat, Cool)
# If the HVAC is switched off, it must be idle
# This works around a bug in some devices (like Eve radiator valves) that
# return they are heating when they are not.
target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
if target == HeatingCoolingTargetValues.OFF:
return HVACAction.IDLE
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value) current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
@@ -679,6 +673,12 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
): ):
return HVACAction.FAN return HVACAction.FAN
# If the HVAC is switched off, it must be idle
# This works around a bug in some devices (like Eve radiator valves) that
# return they are heating when they are not.
if target == HeatingCoolingTargetValues.OFF:
return HVACAction.IDLE
return current_hass_value return current_hass_value
@property @property

View File

@@ -2,7 +2,7 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"description": "Please enter the credentials used to log into mytotalconnectcomfort.com.", "description": "Please enter the credentials used to log in to mytotalconnectcomfort.com.",
"data": { "data": {
"username": "[%key:common::config_flow::data::username%]", "username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"

View File

@@ -3,25 +3,34 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Final
from aiohttp import hdrs
from aiohttp.web import Application, Request, StreamResponse, middleware from aiohttp.web import Application, Request, StreamResponse, middleware
from aiohttp.web_exceptions import HTTPException from aiohttp.web_exceptions import HTTPException
from multidict import CIMultiDict, istr
from homeassistant.core import callback from homeassistant.core import callback
REFERRER_POLICY: Final[istr] = istr("Referrer-Policy")
X_CONTENT_TYPE_OPTIONS: Final[istr] = istr("X-Content-Type-Options")
X_FRAME_OPTIONS: Final[istr] = istr("X-Frame-Options")
@callback @callback
def setup_headers(app: Application, use_x_frame_options: bool) -> None: def setup_headers(app: Application, use_x_frame_options: bool) -> None:
"""Create headers middleware for the app.""" """Create headers middleware for the app."""
added_headers = { added_headers = CIMultiDict(
"Referrer-Policy": "no-referrer", {
"X-Content-Type-Options": "nosniff", REFERRER_POLICY: "no-referrer",
"Server": "", # Empty server header, to prevent aiohttp of setting one. X_CONTENT_TYPE_OPTIONS: "nosniff",
hdrs.SERVER: "", # Empty server header, to prevent aiohttp of setting one.
} }
)
if use_x_frame_options: if use_x_frame_options:
added_headers["X-Frame-Options"] = "SAMEORIGIN" added_headers[X_FRAME_OPTIONS] = "SAMEORIGIN"
@middleware @middleware
async def headers_middleware( async def headers_middleware(

View File

@@ -9,7 +9,7 @@
"requirements": [ "requirements": [
"huawei-lte-api==1.10.0", "huawei-lte-api==1.10.0",
"stringcase==1.2.0", "stringcase==1.2.0",
"url-normalize==1.4.3" "url-normalize==2.2.0"
], ],
"ssdp": [ "ssdp": [
{ {

View File

@@ -197,5 +197,11 @@
} }
} }
} }
},
"issues": {
"deprecated_effect_none": {
"title": "Light turned on with deprecated effect",
"description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect."
}
} }
} }

View File

@@ -29,6 +29,7 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util import color as color_util from homeassistant.util import color as color_util
from ..bridge import HueBridge from ..bridge import HueBridge
@@ -44,6 +45,9 @@ FALLBACK_MIN_KELVIN = 6500
FALLBACK_MAX_KELVIN = 2000 FALLBACK_MAX_KELVIN = 2000
FALLBACK_KELVIN = 5800 # halfway FALLBACK_KELVIN = 5800 # halfway
# HA 2025.4 replaced the deprecated effect "None" with HA default "off"
DEPRECATED_EFFECT_NONE = "None"
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -233,6 +237,23 @@ class HueLight(HueBaseEntity, LightEntity):
self._color_temp_active = color_temp is not None self._color_temp_active = color_temp is not None
flash = kwargs.get(ATTR_FLASH) flash = kwargs.get(ATTR_FLASH)
effect = effect_str = kwargs.get(ATTR_EFFECT) effect = effect_str = kwargs.get(ATTR_EFFECT)
if effect_str == DEPRECATED_EFFECT_NONE:
# deprecated effect "None" is now "off"
effect_str = EFFECT_OFF
async_create_issue(
self.hass,
DOMAIN,
"deprecated_effect_none",
breaks_in_ha_version="2025.10.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_effect_none",
)
self.logger.warning(
"Detected deprecated effect 'None' in %s, use 'off' instead. "
"This will stop working in HA 2025.10",
self.entity_id,
)
if effect_str == EFFECT_OFF: if effect_str == EFFECT_OFF:
# ignore effect if set to "off" and we have no effect active # ignore effect if set to "off" and we have no effect active
# the special effect "off" is only used to stop an active effect # the special effect "off" is only used to stop an active effect

View File

@@ -5,5 +5,6 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.0.10"] "requirements": ["imgw_pib==1.0.10"]
} }

View File

@@ -0,0 +1,88 @@
rules:
# Bronze
action-setup:
status: exempt
comment: The integration does not register services.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: The integration does not register services.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: The integration does not register services.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: No options to configure.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: No authentication required.
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: The integration is a cloud service and thus does not support discovery.
discovery:
status: exempt
comment: The integration is a cloud service and thus does not support discovery.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices:
status: exempt
comment: This is a service, which doesn't integrate with any devices.
docs-supported-functions: todo
docs-troubleshooting:
status: exempt
comment: No known issues that could be resolved by the user.
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: This integration has a fixed single service.
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: exempt
comment: Only parameter that could be changed station_id would force a new config entry.
repair-issues:
status: exempt
comment: This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: This integration has a fixed single service.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@@ -10,8 +10,8 @@
}, },
"data_description": { "data_description": {
"host": "Hostname or IP-address of the Intergas gateway.", "host": "Hostname or IP-address of the Intergas gateway.",
"username": "The username to log into the gateway. This is `admin` in most cases.", "username": "The username to log in to the gateway. This is `admin` in most cases.",
"password": "The password to log into the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices." "password": "The password to log in to the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
} }
}, },
"dhcp_auth": { "dhcp_auth": {
@@ -22,8 +22,8 @@
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
}, },
"data_description": { "data_description": {
"username": "The username to log into the gateway. This is `admin` in most cases.", "username": "[%key:component::incomfort::config::step::user::data_description::username%]",
"password": "The password to log into the gateway, is printed at the bottom of the Gateway or is `intergas` for some older devices." "password": "[%key:component::incomfort::config::step::user::data_description::password%]"
} }
}, },
"dhcp_confirm": { "dhcp_confirm": {

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta
import logging import logging
from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
@@ -9,13 +10,16 @@ from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
from homeassistant.components.bluetooth import ( from homeassistant.components.bluetooth import (
BluetoothScanningMode, BluetoothScanningMode,
BluetoothServiceInfo, BluetoothServiceInfo,
BluetoothServiceInfoBleak,
async_ble_device_from_address,
) )
from homeassistant.components.bluetooth.passive_update_processor import ( from homeassistant.components.bluetooth.active_update_processor import (
PassiveBluetoothProcessorCoordinator, ActiveBluetoothProcessorCoordinator,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_track_time_interval
from .const import CONF_DEVICE_TYPE, DOMAIN from .const import CONF_DEVICE_TYPE, DOMAIN
@@ -23,34 +27,87 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
FALLBACK_POLL_INTERVAL = timedelta(seconds=180)
class INKBIRDActiveBluetoothProcessorCoordinator(ActiveBluetoothProcessorCoordinator):
"""Coordinator for INKBIRD Bluetooth devices."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
data: INKBIRDBluetoothDeviceData,
) -> None:
"""Initialize the INKBIRD Bluetooth processor coordinator."""
self._data = data
self._entry = entry
address = entry.unique_id
assert address is not None
entry.async_on_unload(
async_track_time_interval(
hass, self._async_schedule_poll, FALLBACK_POLL_INTERVAL
)
)
super().__init__(
hass=hass,
logger=_LOGGER,
address=address,
mode=BluetoothScanningMode.ACTIVE,
update_method=self._async_on_update,
needs_poll_method=self._async_needs_poll,
poll_method=self._async_poll_data,
)
async def _async_poll_data(
self, last_service_info: BluetoothServiceInfoBleak
) -> SensorUpdate:
"""Poll the device."""
return await self._data.async_poll(last_service_info.device)
@callback
def _async_needs_poll(
self, service_info: BluetoothServiceInfoBleak, last_poll: float | None
) -> bool:
return (
not self.hass.is_stopping
and self._data.poll_needed(service_info, last_poll)
and bool(
async_ble_device_from_address(
self.hass, service_info.device.address, connectable=True
)
)
)
@callback
def _async_on_update(self, service_info: BluetoothServiceInfo) -> SensorUpdate:
"""Handle update callback from the passive BLE processor."""
update = self._data.update(service_info)
if (
self._entry.data.get(CONF_DEVICE_TYPE) is None
and self._data.device_type is not None
):
device_type_str = str(self._data.device_type)
self.hass.config_entries.async_update_entry(
self._entry,
data={**self._entry.data, CONF_DEVICE_TYPE: device_type_str},
)
return update
@callback
def _async_schedule_poll(self, _: datetime) -> None:
"""Schedule a poll of the device."""
if self._last_service_info and self._async_needs_poll(
self._last_service_info, self._last_poll
):
self._debounced_poll.async_schedule_call()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up INKBIRD BLE device from a config entry.""" """Set up INKBIRD BLE device from a config entry."""
address = entry.unique_id
assert address is not None
device_type: str | None = entry.data.get(CONF_DEVICE_TYPE) device_type: str | None = entry.data.get(CONF_DEVICE_TYPE)
data = INKBIRDBluetoothDeviceData(device_type) data = INKBIRDBluetoothDeviceData(device_type)
coordinator = INKBIRDActiveBluetoothProcessorCoordinator(hass, entry, data)
@callback
def _async_on_update(service_info: BluetoothServiceInfo) -> SensorUpdate:
"""Handle update callback from the passive BLE processor."""
nonlocal device_type
update = data.update(service_info)
if device_type is None and data.device_type is not None:
device_type_str = str(data.device_type)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_DEVICE_TYPE: device_type_str}
)
device_type = device_type_str
return update
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.ACTIVE,
update_method=_async_on_update,
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# only start after all platforms have had a chance to subscribe # only start after all platforms have had a chance to subscribe

View File

@@ -40,5 +40,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird", "documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["inkbird-ble==0.9.0"] "requirements": ["inkbird-ble==0.10.1"]
} }

View File

@@ -123,7 +123,7 @@
"state": { "state": {
"off": "[%key:common::state::off%]", "off": "[%key:common::state::off%]",
"slow": "[%key:component::iron_os::common::slow%]", "slow": "[%key:component::iron_os::common::slow%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"fast": "[%key:component::iron_os::common::fast%]" "fast": "[%key:component::iron_os::common::fast%]"
} }
}, },

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/kulersky", "documentation": "https://www.home-assistant.io/integrations/kulersky",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["bleak", "pykulersky"], "loggers": ["bleak", "pykulersky"],
"requirements": ["pykulersky==0.5.2"] "requirements": ["pykulersky==0.5.8"]
} }

View File

@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
CONF_DOMAIN, CONF_DOMAIN,
CONF_ENTITIES, CONF_ENTITIES,
CONF_SOURCE, CONF_SOURCE,
@@ -49,6 +50,7 @@ DEVICE_CLASS_MAPPING = {
pypck.lcn_defs.VarUnit.METERPERSECOND: SensorDeviceClass.SPEED, pypck.lcn_defs.VarUnit.METERPERSECOND: SensorDeviceClass.SPEED,
pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE, pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE,
pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT, pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT,
pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2,
} }
UNIT_OF_MEASUREMENT_MAPPING = { UNIT_OF_MEASUREMENT_MAPPING = {
@@ -60,6 +62,7 @@ UNIT_OF_MEASUREMENT_MAPPING = {
pypck.lcn_defs.VarUnit.METERPERSECOND: UnitOfSpeed.METERS_PER_SECOND, pypck.lcn_defs.VarUnit.METERPERSECOND: UnitOfSpeed.METERS_PER_SECOND,
pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT, pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT,
pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE, pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE,
pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION,
} }

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble", "documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.26.1", "ld2410-ble==0.1.1"] "requirements": ["bluetooth-data-tools==1.27.0", "ld2410-ble==0.1.1"]
} }

View File

@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble", "documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.26.1", "led-ble==1.1.6"] "requirements": ["bluetooth-data-tools==1.27.0", "led-ble==1.1.6"]
} }

View File

@@ -119,9 +119,9 @@
"fan_mode": { "fan_mode": {
"state": { "state": {
"slow": "Slow", "slow": "Slow",
"low": "Low", "low": "[%key:common::state::low%]",
"mid": "Medium", "mid": "[%key:common::state::medium%]",
"high": "High", "high": "[%key:common::state::high%]",
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", "power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]" "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]"
} }
@@ -303,7 +303,7 @@
"state": { "state": {
"invalid": "Invalid", "invalid": "Invalid",
"weak": "Weak", "weak": "Weak",
"normal": "Normal", "normal": "[%key:common::state::normal%]",
"strong": "Strong", "strong": "Strong",
"very_strong": "Very strong" "very_strong": "Very strong"
} }
@@ -390,17 +390,17 @@
"temperature_state": { "temperature_state": {
"name": "[%key:component::sensor::entity_component::temperature::name%]", "name": "[%key:component::sensor::entity_component::temperature::name%]",
"state": { "state": {
"high": "High", "high": "[%key:common::state::high%]",
"normal": "Good", "normal": "Good",
"low": "Low" "low": "[%key:common::state::low%]"
} }
}, },
"temperature_state_for_location": { "temperature_state_for_location": {
"name": "[%key:component::lg_thinq::entity::number::target_temperature_for_location::name%]", "name": "[%key:component::lg_thinq::entity::number::target_temperature_for_location::name%]",
"state": { "state": {
"high": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::high%]", "high": "[%key:common::state::high%]",
"normal": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::normal%]", "normal": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::normal%]",
"low": "[%key:component::lg_thinq::entity::sensor::temperature_state::state::low%]" "low": "[%key:common::state::low%]"
} }
}, },
"current_state": { "current_state": {
@@ -607,7 +607,7 @@
"intensive_dry": "Spot", "intensive_dry": "Spot",
"macro": "Custom mode", "macro": "Custom mode",
"mop": "Mop", "mop": "Mop",
"normal": "Normal", "normal": "[%key:common::state::normal%]",
"off": "[%key:common::state::off%]", "off": "[%key:common::state::off%]",
"quiet_humidity": "Silent", "quiet_humidity": "Silent",
"rapid_humidity": "Jet", "rapid_humidity": "Jet",
@@ -626,7 +626,7 @@
"auto": "Low power", "auto": "Low power",
"high": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]", "high": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
"mop": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::mop%]", "mop": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::mop%]",
"normal": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::normal%]", "normal": "[%key:common::state::normal%]",
"off": "[%key:common::state::off%]", "off": "[%key:common::state::off%]",
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]" "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]"
} }
@@ -653,7 +653,7 @@
"heavy": "Intensive", "heavy": "Intensive",
"delicate": "Delicate", "delicate": "Delicate",
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
"normal": "Normal", "normal": "[%key:common::state::normal%]",
"rinse": "Rinse", "rinse": "Rinse",
"refresh": "Refresh", "refresh": "Refresh",
"express": "Express", "express": "Express",
@@ -781,8 +781,8 @@
"name": "Battery", "name": "Battery",
"state": { "state": {
"high": "Full", "high": "Full",
"mid": "Medium", "mid": "[%key:common::state::medium%]",
"low": "Low", "low": "[%key:common::state::low%]",
"warning": "Empty" "warning": "Empty"
} }
}, },
@@ -876,9 +876,9 @@
"name": "Speed", "name": "Speed",
"state": { "state": {
"slow": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::fan_mode::state::slow%]", "slow": "[%key:component::lg_thinq::entity::climate::climate_air_conditioner::state_attributes::fan_mode::state::slow%]",
"low": "Low", "low": "[%key:common::state::low%]",
"mid": "Medium", "mid": "[%key:common::state::medium%]",
"high": "High", "high": "[%key:common::state::high%]",
"power": "Turbo", "power": "Turbo",
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]", "turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]", "auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",

View File

@@ -118,9 +118,9 @@
"brightness_level": { "brightness_level": {
"name": "Panel brightness", "name": "Panel brightness",
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"high": "High" "high": "[%key:common::state::high%]"
} }
} }
}, },

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar", "documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["ical"], "loggers": ["ical"],
"requirements": ["ical==9.0.3"] "requirements": ["ical==9.1.0"]
} }

View File

@@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo", "documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["ical==9.0.3"] "requirements": ["ical==9.1.0"]
} }

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Final, NamedTuple, cast from typing import TYPE_CHECKING, Any, Final, NamedTuple, cast, final
from propcache.api import cached_property from propcache.api import cached_property
from sqlalchemy.engine.row import Row from sqlalchemy.engine.row import Row
@@ -114,6 +114,7 @@ DATA_POS: Final = 11
CONTEXT_POS: Final = 12 CONTEXT_POS: Final = 12
@final # Final to allow direct checking of the type instead of using isinstance
class EventAsRow(NamedTuple): class EventAsRow(NamedTuple):
"""Convert an event to a row. """Convert an event to a row.

View File

@@ -265,4 +265,61 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterBinarySensor, entity_class=MatterBinarySensor,
required_attributes=(clusters.SmokeCoAlarm.Attributes.InterconnectCOAlarm,), required_attributes=(clusters.SmokeCoAlarm.Attributes.InterconnectCOAlarm,),
), ),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="EnergyEvseChargingStatusSensor",
translation_key="evse_charging_status",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
measurement_to_ha={
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDischarging: False,
clusters.EnergyEvse.Enums.StateEnum.kSessionEnding: False,
clusters.EnergyEvse.Enums.StateEnum.kFault: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.EnergyEvse.Attributes.State,),
allow_multi=True, # also used for sensor entity
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="EnergyEvsePlugStateSensor",
translation_key="evse_plug_state",
device_class=BinarySensorDeviceClass.PLUG,
measurement_to_ha={
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging: True,
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDischarging: True,
clusters.EnergyEvse.Enums.StateEnum.kSessionEnding: False,
clusters.EnergyEvse.Enums.StateEnum.kFault: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.EnergyEvse.Attributes.State,),
allow_multi=True, # also used for sensor entity
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="EnergyEvseSupplyStateSensor",
translation_key="evse_supply_charging_state",
device_class=BinarySensorDeviceClass.RUNNING,
measurement_to_ha={
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledDiagnostics: False,
}.get,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.EnergyEvse.Attributes.SupplyState,),
allow_multi=True, # also used for sensor entity
),
] ]

View File

@@ -61,6 +61,7 @@ class MatterEntityDescription(EntityDescription):
# convert the value from the primary attribute to the value used by HA # convert the value from the primary attribute to the value used by HA
measurement_to_ha: Callable[[Any], Any] | None = None measurement_to_ha: Callable[[Any], Any] | None = None
ha_to_native_value: Callable[[Any], Any] | None = None ha_to_native_value: Callable[[Any], Any] | None = None
command_timeout: int | None = None
class MatterEntity(Entity): class MatterEntity(Entity):

View File

@@ -71,6 +71,15 @@
}, },
"battery_replacement_description": { "battery_replacement_description": {
"default": "mdi:battery-sync-outline" "default": "mdi:battery-sync-outline"
},
"evse_state": {
"default": "mdi:ev-station"
},
"evse_supply_state": {
"default": "mdi:ev-station"
},
"evse_fault_state": {
"default": "mdi:ev-station"
} }
}, },
"switch": { "switch": {
@@ -80,6 +89,9 @@
"on": "mdi:lock", "on": "mdi:lock",
"off": "mdi:lock-off" "off": "mdi:lock-off"
} }
},
"evse_charging_switch": {
"default": "mdi:ev-station"
} }
} }
} }

View File

@@ -77,6 +77,25 @@ OPERATIONAL_STATE_MAP = {
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked", clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
} }
EVSE_FAULT_STATE_MAP = {
clusters.EnergyEvse.Enums.FaultStateEnum.kNoError: "no_error",
clusters.EnergyEvse.Enums.FaultStateEnum.kMeterFailure: "meter_failure",
clusters.EnergyEvse.Enums.FaultStateEnum.kOverVoltage: "over_voltage",
clusters.EnergyEvse.Enums.FaultStateEnum.kUnderVoltage: "under_voltage",
clusters.EnergyEvse.Enums.FaultStateEnum.kOverCurrent: "over_current",
clusters.EnergyEvse.Enums.FaultStateEnum.kContactWetFailure: "contact_wet_failure",
clusters.EnergyEvse.Enums.FaultStateEnum.kContactDryFailure: "contact_dry_failure",
clusters.EnergyEvse.Enums.FaultStateEnum.kPowerLoss: "power_loss",
clusters.EnergyEvse.Enums.FaultStateEnum.kPowerQuality: "power_quality",
clusters.EnergyEvse.Enums.FaultStateEnum.kPilotShortCircuit: "pilot_short_circuit",
clusters.EnergyEvse.Enums.FaultStateEnum.kEmergencyStop: "emergency_stop",
clusters.EnergyEvse.Enums.FaultStateEnum.kEVDisconnected: "ev_disconnected",
clusters.EnergyEvse.Enums.FaultStateEnum.kWrongPowerSupply: "wrong_power_supply",
clusters.EnergyEvse.Enums.FaultStateEnum.kLiveNeutralSwap: "live_neutral_swap",
clusters.EnergyEvse.Enums.FaultStateEnum.kOverTemperature: "over_temperature",
clusters.EnergyEvse.Enums.FaultStateEnum.kOther: "other",
}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -904,4 +923,77 @@ DISCOVERY_SCHEMAS = [
# don't discover this entry if the supported state list is empty # don't discover this entry if the supported state list is empty
secondary_value_is_not=[], secondary_value_is_not=[],
), ),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseFaultState",
translation_key="evse_fault_state",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
options=list(EVSE_FAULT_STATE_MAP.values()),
measurement_to_ha=EVSE_FAULT_STATE_MAP.get,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.FaultState,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseCircuitCapacity",
translation_key="evse_circuit_capacity",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.CircuitCapacity,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseMinimumChargeCurrent",
translation_key="evse_min_charge_current",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.MinimumChargeCurrent,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseMaximumChargeCurrent",
translation_key="evse_max_charge_current",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.MaximumChargeCurrent,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="EnergyEvseUserMaximumChargeCurrent",
translation_key="evse_user_max_charge_current",
device_class=SensorDeviceClass.CURRENT,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=2,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent,),
),
] ]

View File

@@ -76,6 +76,15 @@
}, },
"muted": { "muted": {
"name": "Muted" "name": "Muted"
},
"evse_charging_status": {
"name": "Charging status"
},
"evse_plug": {
"name": "Plug state"
},
"evse_supply_charging_state": {
"name": "Supply charging state"
} }
}, },
"button": { "button": {
@@ -135,9 +144,9 @@
"state_attributes": { "state_attributes": {
"preset_mode": { "preset_mode": {
"state": { "state": {
"low": "Low", "low": "[%key:common::state::low%]",
"medium": "Medium", "medium": "[%key:common::state::medium%]",
"high": "High", "high": "[%key:common::state::high%]",
"auto": "Auto", "auto": "Auto",
"natural_wind": "Natural wind", "natural_wind": "Natural wind",
"sleep_wind": "Sleep wind" "sleep_wind": "Sleep wind"
@@ -189,9 +198,9 @@
"sensitivity_level": { "sensitivity_level": {
"name": "Sensitivity", "name": "Sensitivity",
"state": { "state": {
"low": "[%key:component::matter::entity::fan::fan::state_attributes::preset_mode::state::low%]", "low": "[%key:common::state::low%]",
"standard": "Standard", "standard": "Standard",
"high": "[%key:component::matter::entity::fan::fan::state_attributes::preset_mode::state::high%]" "high": "[%key:common::state::high%]"
} }
}, },
"startup_on_off": { "startup_on_off": {
@@ -213,7 +222,7 @@
"name": "Number of rinses", "name": "Number of rinses",
"state": { "state": {
"off": "[%key:common::state::off%]", "off": "[%key:common::state::off%]",
"normal": "Normal", "normal": "[%key:common::state::normal%]",
"extra": "Extra", "extra": "Extra",
"max": "Max" "max": "Max"
} }
@@ -229,8 +238,8 @@
"contamination_state": { "contamination_state": {
"name": "Contamination state", "name": "Contamination state",
"state": { "state": {
"normal": "Normal", "normal": "[%key:common::state::normal%]",
"low": "[%key:component::matter::entity::fan::fan::state_attributes::preset_mode::state::low%]", "low": "[%key:common::state::low%]",
"warning": "Warning", "warning": "Warning",
"critical": "Critical" "critical": "Critical"
} }
@@ -278,6 +287,42 @@
}, },
"current_phase": { "current_phase": {
"name": "Current phase" "name": "Current phase"
},
"evse_fault_state": {
"name": "Fault state",
"state": {
"no_error": "OK",
"meter_failure": "Meter failure",
"over_voltage": "Overvoltage",
"under_voltage": "Undervoltage",
"over_current": "Overcurrent",
"contact_wet_failure": "Contact wet failure",
"contact_dry_failure": "Contact dry failure",
"power_loss": "Power loss",
"power_quality": "Power quality",
"pilot_short_circuit": "Pilot short circuit",
"emergency_stop": "Emergency stop",
"ev_disconnected": "EV disconnected",
"wrong_power_supply": "Wrong power supply",
"live_neutral_swap": "Live/neutral swap",
"over_temperature": "Overtemperature",
"other": "Other fault"
}
},
"evse_circuit_capacity": {
"name": "Circuit capacity"
},
"evse_charge_current": {
"name": "Charge current"
},
"evse_min_charge_current": {
"name": "Min charge current"
},
"evse_max_charge_current": {
"name": "Max charge current"
},
"evse_user_max_charge_current": {
"name": "User max charge current"
} }
}, },
"switch": { "switch": {
@@ -289,6 +334,9 @@
}, },
"child_lock": { "child_lock": {
"name": "Child lock" "name": "Child lock"
},
"evse_charging_switch": {
"name": "Enable charging"
} }
}, },
"vacuum": { "vacuum": {

View File

@@ -2,10 +2,12 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from chip.clusters import Objects as clusters from chip.clusters import Objects as clusters
from chip.clusters.Objects import ClusterCommand, NullValue
from matter_server.client.models import device_types from matter_server.client.models import device_types
from homeassistant.components.switch import ( from homeassistant.components.switch import (
@@ -22,6 +24,13 @@ from .entity import MatterEntity, MatterEntityDescription
from .helpers import get_matter from .helpers import get_matter
from .models import MatterDiscoverySchema from .models import MatterDiscoverySchema
EVSE_SUPPLY_STATE_MAP = {
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledDiagnostics: False,
}
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -58,6 +67,66 @@ class MatterSwitch(MatterEntity, SwitchEntity):
) )
class MatterGenericCommandSwitch(MatterSwitch):
"""Representation of a Matter switch."""
entity_description: MatterGenericCommandSwitchEntityDescription
_platform_translation_key = "switch"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
if self.entity_description.on_command:
# custom command defined to set the new value
await self.send_device_command(
self.entity_description.on_command(),
self.entity_description.command_timeout,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
if self.entity_description.off_command:
await self.send_device_command(
self.entity_description.off_command(),
self.entity_description.command_timeout,
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
if value_convert := self.entity_description.measurement_to_ha:
value = value_convert(value)
self._attr_is_on = value
async def send_device_command(
self,
command: ClusterCommand,
command_timeout: int | None = None,
**kwargs: Any,
) -> None:
"""Send device command with timeout."""
await self.matter_client.send_device_command(
node_id=self._endpoint.node.node_id,
endpoint_id=self._endpoint.endpoint_id,
command=command,
timed_request_timeout_ms=command_timeout,
**kwargs,
)
@dataclass(frozen=True)
class MatterGenericCommandSwitchEntityDescription(
SwitchEntityDescription, MatterEntityDescription
):
"""Describe Matter Generic command Switch entities."""
# command: a custom callback to create the command to send to the device
on_command: Callable[[], Any] | None = None
off_command: Callable[[], Any] | None = None
command_timeout: int | None = None
@dataclass(frozen=True) @dataclass(frozen=True)
class MatterNumericSwitchEntityDescription( class MatterNumericSwitchEntityDescription(
SwitchEntityDescription, MatterEntityDescription SwitchEntityDescription, MatterEntityDescription
@@ -194,4 +263,26 @@ DISCOVERY_SCHEMAS = [
), ),
vendor_id=(4874,), vendor_id=(4874,),
), ),
MatterDiscoverySchema(
platform=Platform.SWITCH,
entity_description=MatterGenericCommandSwitchEntityDescription(
key="EnergyEvseChargingSwitch",
translation_key="evse_charging_switch",
on_command=lambda: clusters.EnergyEvse.Commands.EnableCharging(
chargingEnabledUntil=NullValue,
minimumChargeCurrent=0,
maximumChargeCurrent=0,
),
off_command=clusters.EnergyEvse.Commands.Disable,
command_timeout=3000,
measurement_to_ha=EVSE_SUPPLY_STATE_MAP.get,
),
entity_class=MatterGenericCommandSwitch,
required_attributes=(
clusters.EnergyEvse.Attributes.SupplyState,
clusters.EnergyEvse.Attributes.AcceptedCommandList,
),
value_contains=clusters.EnergyEvse.Commands.EnableCharging.command_id,
allow_multi=True,
),
] ]

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