Merge branch 'dev' into websocket_api_wait_integration

This commit is contained in:
Erik Montnemery
2025-04-09 15:29:01 +02:00
committed by GitHub
416 changed files with 20297 additions and 4446 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

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.13 uses: github/codeql-action/init@v3.28.15
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.13 uses: github/codeql-action/analyze@v3.28.15
with: with:
category: "/language:python" category: "/language:python"

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

@@ -73,7 +73,7 @@ class AndroidTVRemoteBaseEntity(Entity):
self._api.send_key_command(key_code, direction) self._api.send_key_command(key_code, direction)
except ConnectionClosed as exc: except ConnectionClosed as exc:
raise HomeAssistantError( raise HomeAssistantError(
"Connection to Android TV device is closed" translation_domain=DOMAIN, translation_key="connection_closed"
) from exc ) from exc
def _send_launch_app_command(self, app_link: str) -> None: def _send_launch_app_command(self, app_link: str) -> None:
@@ -85,5 +85,5 @@ class AndroidTVRemoteBaseEntity(Entity):
self._api.send_launch_app_command(app_link) self._api.send_launch_app_command(app_link)
except ConnectionClosed as exc: except ConnectionClosed as exc:
raise HomeAssistantError( raise HomeAssistantError(
"Connection to Android TV device is closed" translation_domain=DOMAIN, translation_key="connection_closed"
) from exc ) from exc

View File

@@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AndroidTVRemoteConfigEntry from . import AndroidTVRemoteConfigEntry
from .const import CONF_APP_ICON, CONF_APP_NAME from .const import CONF_APP_ICON, CONF_APP_NAME, DOMAIN
from .entity import AndroidTVRemoteBaseEntity from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -233,5 +233,5 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
await asyncio.sleep(delay_secs) await asyncio.sleep(delay_secs)
except ConnectionClosed as exc: except ConnectionClosed as exc:
raise HomeAssistantError( raise HomeAssistantError(
"Connection to Android TV device is closed" translation_domain=DOMAIN, translation_key="connection_closed"
) from exc ) from exc

View File

@@ -54,5 +54,10 @@
} }
} }
} }
},
"exceptions": {
"connection_closed": {
"message": "Connection to the Android TV device is closed"
}
} }
} }

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,12 +9,12 @@ 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
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL] PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR]
type BoschAlarmConfigEntry = ConfigEntry[Panel] type BoschAlarmConfigEntry = ConfigEntry[Panel]
@@ -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

@@ -10,11 +10,10 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelState, AlarmControlPanelState,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry from . import BoschAlarmConfigEntry
from .const import DOMAIN from .entity import BoschAlarmAreaEntity
async def async_setup_entry( async def async_setup_entry(
@@ -35,7 +34,7 @@ async def async_setup_entry(
) )
class AreaAlarmControlPanel(AlarmControlPanelEntity): class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel.""" """An alarm control panel entity for a bosch alarm panel."""
_attr_has_entity_name = True _attr_has_entity_name = True
@@ -48,19 +47,8 @@ class AreaAlarmControlPanel(AlarmControlPanelEntity):
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None: def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
"""Initialise a Bosch Alarm control panel entity.""" """Initialise a Bosch Alarm control panel entity."""
self.panel = panel super().__init__(panel, area_id, unique_id, False, False, True)
self._area = panel.areas[area_id] self._attr_unique_id = self._area_unique_id
self._area_id = area_id
self._attr_unique_id = f"{unique_id}_area_{area_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=self._area.name,
manufacturer="Bosch Security Systems",
via_device=(
DOMAIN,
unique_id,
),
)
@property @property
def alarm_state(self) -> AlarmControlPanelState | None: def alarm_state(self) -> AlarmControlPanelState | None:
@@ -90,20 +78,3 @@ class AreaAlarmControlPanel(AlarmControlPanelEntity):
async def async_alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
await self.panel.area_arm_all(self._area_id) await self.panel.area_arm_all(self._area_id)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.panel.connection_status()
async def async_added_to_hass(self) -> None:
"""Run when entity attached to hass."""
await super().async_added_to_hass()
self._area.status_observer.attach(self.schedule_update_ha_state)
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Run when entity removed from hass."""
await super().async_will_remove_from_hass()
self._area.status_observer.detach(self.schedule_update_ha_state)
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)

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
@@ -10,7 +11,12 @@ from typing import Any
from bosch_alarm_mode2 import Panel from bosch_alarm_mode2 import Panel
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_CODE, CONF_CODE,
CONF_HOST, CONF_HOST,
@@ -107,6 +113,13 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
else: else:
self._data = user_input self._data = user_input
self._data[CONF_MODEL] = model self._data[CONF_MODEL] = model
if self.source == SOURCE_RECONFIGURE:
if (
self._get_reconfigure_entry().data[CONF_MODEL]
!= self._data[CONF_MODEL]
):
return self.async_abort(reason="device_mismatch")
return await self.async_step_auth() return await self.async_step_auth()
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@@ -116,6 +129,12 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reconfigure step."""
return await self.async_step_user()
async def async_step_auth( async def async_step_auth(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@@ -153,13 +172,77 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
else: else:
if serial_number: if serial_number:
await self.async_set_unique_id(str(serial_number)) await self.async_set_unique_id(str(serial_number))
self._abort_if_unique_id_configured() if self.source == SOURCE_USER:
else: if serial_number:
self._async_abort_entries_match({CONF_HOST: self._data[CONF_HOST]}) self._abort_if_unique_id_configured()
return self.async_create_entry(title=f"Bosch {model}", data=self._data) else:
self._async_abort_entries_match(
{CONF_HOST: self._data[CONF_HOST]}
)
return self.async_create_entry(
title=f"Bosch {model}", data=self._data
)
if serial_number:
self._abort_if_unique_id_mismatch(reason="device_mismatch")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data=self._data,
)
return self.async_show_form( return self.async_show_form(
step_id="auth", step_id="auth",
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

@@ -0,0 +1,88 @@
"""Support for Bosch Alarm Panel History as a sensor."""
from __future__ import annotations
from bosch_alarm_mode2 import Panel
from homeassistant.components.sensor import Entity
from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN
PARALLEL_UPDATES = 0
class BoschAlarmEntity(Entity):
"""A base entity for a bosch alarm panel."""
_attr_has_entity_name = True
def __init__(self, panel: Panel, unique_id: str) -> None:
"""Set up a entity for a bosch alarm panel."""
self.panel = panel
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.panel.connection_status()
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmAreaEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(
self,
panel: Panel,
area_id: int,
unique_id: str,
observe_alarms: bool,
observe_ready: bool,
observe_status: bool,
) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._area_id = area_id
self._area_unique_id = f"{unique_id}_area_{area_id}"
self._observe_alarms = observe_alarms
self._observe_ready = observe_ready
self._observe_status = observe_status
self._area = panel.areas[area_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._area_unique_id)},
name=self._area.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
if self._observe_alarms:
self._area.alarm_observer.attach(self.schedule_update_ha_state)
if self._observe_ready:
self._area.ready_observer.attach(self.schedule_update_ha_state)
if self._observe_status:
self._area.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
if self._observe_alarms:
self._area.alarm_observer.detach(self.schedule_update_ha_state)
if self._observe_ready:
self._area.ready_observer.detach(self.schedule_update_ha_state)
if self._observe_status:
self._area.status_observer.detach(self.schedule_update_ha_state)

View File

@@ -0,0 +1,9 @@
{
"entity": {
"sensor": {
"faulting_points": {
"default": "mdi:alert-circle-outline"
}
}
}
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["bosch-alarm-mode2==0.4.3"] "requirements": ["bosch-alarm-mode2==0.4.6"]
} }

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
@@ -62,9 +62,9 @@ rules:
entity-category: todo entity-category: todo
entity-device-class: todo entity-device-class: todo
entity-disabled-by-default: todo entity-disabled-by-default: todo
entity-translations: todo entity-translations: done
exception-translations: todo exception-translations: todo
icon-translations: todo icon-translations: done
reconfiguration-flow: todo reconfiguration-flow: todo
repair-issues: repair-issues:
status: exempt status: exempt

View File

@@ -0,0 +1,86 @@
"""Support for Bosch Alarm Panel History as a sensor."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.panel import Area
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
value_fn: Callable[[Area], int]
observe_alarms: bool = False
observe_ready: bool = False
observe_status: bool = False
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
BoschAlarmSensorEntityDescription(
key="faulting_points",
translation_key="faulting_points",
value_fn=lambda area: area.faults,
observe_ready=True,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up bosch alarm sensors."""
panel = config_entry.runtime_data
unique_id = config_entry.unique_id or config_entry.entry_id
async_add_entities(
BoschAreaSensor(panel, area_id, unique_id, template)
for area_id in panel.areas
for template in SENSOR_TYPES
)
PARALLEL_UPDATES = 0
class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
"""An area sensor entity for a bosch alarm panel."""
entity_description: BoschAlarmSensorEntityDescription
def __init__(
self,
panel: Panel,
area_id: int,
unique_id: str,
entity_description: BoschAlarmSensorEntityDescription,
) -> None:
"""Set up an area sensor entity for a bosch alarm panel."""
super().__init__(
panel,
area_id,
unique_id,
entity_description.observe_alarms,
entity_description.observe_ready,
entity_description.observe_status,
)
self.entity_description = entity_description
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
@property
def native_value(self) -> int:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._area)

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,26 @@
"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%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"device_mismatch": "Please ensure you reconfigure against the same device."
}
},
"exceptions": {
"cannot_connect": {
"message": "Could not connect to panel."
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
}
},
"entity": {
"sensor": {
"faulting_points": {
"name": "Faulting points",
"unit_of_measurement": "points"
}
} }
} }
} }

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

@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.util.ssl import client_context_no_verify
from .const import DOMAIN, KEY_MAC, TIMEOUT from .const import DOMAIN, KEY_MAC, TIMEOUT
@@ -90,6 +91,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
key=key, key=key,
uuid=uuid, uuid=uuid,
password=password, password=password,
ssl_context=client_context_no_verify(),
) )
except (TimeoutError, ClientError): except (TimeoutError, ClientError):
self.host = None self.host = None

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

@@ -55,7 +55,7 @@
"fields": { "fields": {
"entity_id": { "entity_id": {
"name": "Entity", "name": "Entity",
"description": "Ecobee thermostat on which to create the vacation." "description": "ecobee thermostat on which to create the vacation."
}, },
"vacation_name": { "vacation_name": {
"name": "Vacation name", "name": "Vacation name",
@@ -101,7 +101,7 @@
"fields": { "fields": {
"entity_id": { "entity_id": {
"name": "Entity", "name": "Entity",
"description": "Ecobee thermostat on which to delete the vacation." "description": "ecobee thermostat on which to delete the vacation."
}, },
"vacation_name": { "vacation_name": {
"name": "[%key:component::ecobee::services::create_vacation::fields::vacation_name::name%]", "name": "[%key:component::ecobee::services::create_vacation::fields::vacation_name::name%]",
@@ -149,7 +149,7 @@
}, },
"set_mic_mode": { "set_mic_mode": {
"name": "Set mic mode", "name": "Set mic mode",
"description": "Enables/disables Alexa microphone (only for Ecobee 4).", "description": "Enables/disables Alexa microphone (only for ecobee 4).",
"fields": { "fields": {
"mic_enabled": { "mic_enabled": {
"name": "Mic enabled", "name": "Mic enabled",
@@ -177,7 +177,7 @@
"fields": { "fields": {
"entity_id": { "entity_id": {
"name": "Entity", "name": "Entity",
"description": "Ecobee thermostat on which to set active sensors." "description": "ecobee thermostat on which to set active sensors."
}, },
"preset_mode": { "preset_mode": {
"name": "Climate Name", "name": "Climate Name",
@@ -203,12 +203,12 @@
}, },
"issues": { "issues": {
"migrate_aux_heat": { "migrate_aux_heat": {
"title": "Migration of Ecobee set_aux_heat action", "title": "Migration of ecobee set_aux_heat action",
"fix_flow": { "fix_flow": {
"step": { "step": {
"confirm": { "confirm": {
"description": "The Ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.", "description": "The ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
"title": "Disable legacy Ecobee set_aux_heat action" "title": "Disable legacy ecobee set_aux_heat action"
} }
} }
} }

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

@@ -16,7 +16,13 @@ from homeassistant.config_entries import (
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlow,
) )
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
CONF_TOKEN,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
@@ -40,6 +46,13 @@ CONF_SERIAL = "serial"
INSTALLER_AUTH_USERNAME = "installer" INSTALLER_AUTH_USERNAME = "installer"
AVOID_REFLECT_KEYS = {CONF_PASSWORD, CONF_TOKEN}
def without_avoid_reflect_keys(dictionary: Mapping[str, Any]) -> dict[str, Any]:
"""Return a dictionary without AVOID_REFLECT_KEYS."""
return {k: v for k, v in dictionary.items() if k not in AVOID_REFLECT_KEYS}
async def validate_input( async def validate_input(
hass: HomeAssistant, hass: HomeAssistant,
@@ -205,7 +218,10 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders["serial"] = serial description_placeholders["serial"] = serial
return self.async_show_form( return self.async_show_form(
step_id="reauth_confirm", step_id="reauth_confirm",
data_schema=self._async_generate_schema(), data_schema=self.add_suggested_values_to_schema(
self._async_generate_schema(),
without_avoid_reflect_keys(user_input or reauth_entry.data),
),
description_placeholders=description_placeholders, description_placeholders=description_placeholders,
errors=errors, errors=errors,
) )
@@ -259,10 +275,12 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_SERIAL: self.unique_id, CONF_SERIAL: self.unique_id,
CONF_HOST: host, CONF_HOST: host,
} }
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=self._async_generate_schema(), data_schema=self.add_suggested_values_to_schema(
self._async_generate_schema(),
without_avoid_reflect_keys(user_input or {}),
),
description_placeholders=description_placeholders, description_placeholders=description_placeholders,
errors=errors, errors=errors,
) )
@@ -306,11 +324,11 @@ class EnphaseConfigFlow(ConfigFlow, domain=DOMAIN):
} }
description_placeholders["serial"] = serial description_placeholders["serial"] = serial
suggested_values: Mapping[str, Any] = user_input or reconfigure_entry.data
return self.async_show_form( return self.async_show_form(
step_id="reconfigure", step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema( data_schema=self.add_suggested_values_to_schema(
self._async_generate_schema(), suggested_values self._async_generate_schema(),
without_avoid_reflect_keys(user_input or reconfigure_entry.data),
), ),
description_placeholders=description_placeholders, description_placeholders=description_placeholders,
errors=errors, errors=errors,

View File

@@ -66,16 +66,19 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
] ]
for end_point in end_points: for end_point in end_points:
response = await envoy.request(end_point) try:
fixture_data[end_point] = response.text.replace("\n", "").replace( response = await envoy.request(end_point)
serial, CLEAN_TEXT fixture_data[end_point] = response.text.replace("\n", "").replace(
) serial, CLEAN_TEXT
fixture_data[f"{end_point}_log"] = json_dumps( )
{ fixture_data[f"{end_point}_log"] = json_dumps(
"headers": dict(response.headers.items()), {
"code": response.status_code, "headers": dict(response.headers.items()),
} "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

@@ -14,9 +14,10 @@ from pyfibaro.fibaro_client import (
) )
from pyfibaro.fibaro_data_helper import read_rooms from pyfibaro.fibaro_data_helper import read_rooms
from pyfibaro.fibaro_device import DeviceModel from pyfibaro.fibaro_device import DeviceModel
from pyfibaro.fibaro_device_manager import FibaroDeviceManager
from pyfibaro.fibaro_info import InfoModel from pyfibaro.fibaro_info import InfoModel
from pyfibaro.fibaro_scene import SceneModel from pyfibaro.fibaro_scene import SceneModel
from pyfibaro.fibaro_state_resolver import FibaroEvent, FibaroStateResolver from pyfibaro.fibaro_state_resolver import FibaroEvent
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform
@@ -81,8 +82,8 @@ class FibaroController:
self._client = fibaro_client self._client = fibaro_client
self._fibaro_info = info self._fibaro_info = info
# Whether to import devices from plugins # The fibaro device manager exposes higher level API to access fibaro devices
self._import_plugins = import_plugins self._fibaro_device_manager = FibaroDeviceManager(fibaro_client, import_plugins)
# Mapping roomId to room object # Mapping roomId to room object
self._room_map = read_rooms(fibaro_client) self._room_map = read_rooms(fibaro_client)
self._device_map: dict[int, DeviceModel] # Mapping deviceId to device object self._device_map: dict[int, DeviceModel] # Mapping deviceId to device object
@@ -91,79 +92,30 @@ class FibaroController:
) # List of devices by entity platform ) # List of devices by entity platform
# All scenes # All scenes
self._scenes = self._client.read_scenes() self._scenes = self._client.read_scenes()
self._callbacks: dict[int, list[Any]] = {} # Update value callbacks by deviceId
# Event callbacks by device id
self._event_callbacks: dict[int, list[Callable[[FibaroEvent], None]]] = {}
# Unique serial number of the hub # Unique serial number of the hub
self.hub_serial = info.serial_number self.hub_serial = info.serial_number
# Device infos by fibaro device id # Device infos by fibaro device id
self._device_infos: dict[int, DeviceInfo] = {} self._device_infos: dict[int, DeviceInfo] = {}
self._read_devices() self._read_devices()
def enable_state_handler(self) -> None: def disconnect(self) -> None:
"""Start StateHandler thread for monitoring updates.""" """Close push channel."""
self._client.register_update_handler(self._on_state_change) self._fibaro_device_manager.close()
def disable_state_handler(self) -> None: def register(
"""Stop StateHandler thread used for monitoring updates.""" self, device_id: int, callback: Callable[[DeviceModel], None]
self._client.unregister_update_handler() ) -> Callable[[], None]:
def _on_state_change(self, state: Any) -> None:
"""Handle change report received from the HomeCenter."""
callback_set = set()
for change in state.get("changes", []):
try:
dev_id = change.pop("id")
if dev_id not in self._device_map:
continue
device = self._device_map[dev_id]
for property_name, value in change.items():
if property_name == "log":
if value and value != "transfer OK":
_LOGGER.debug("LOG %s: %s", device.friendly_name, value)
continue
if property_name == "logTemp":
continue
if property_name in device.properties:
device.properties[property_name] = value
_LOGGER.debug(
"<- %s.%s = %s", device.ha_id, property_name, str(value)
)
else:
_LOGGER.warning("%s.%s not found", device.ha_id, property_name)
if dev_id in self._callbacks:
callback_set.add(dev_id)
except (ValueError, KeyError):
pass
for item in callback_set:
for callback in self._callbacks[item]:
callback()
resolver = FibaroStateResolver(state)
for event in resolver.get_events():
# event does not always have a fibaro id, therefore it is
# essential that we first check for relevant event type
if (
event.event_type.lower() == "centralsceneevent"
and event.fibaro_id in self._event_callbacks
):
for callback in self._event_callbacks[event.fibaro_id]:
callback(event)
def register(self, device_id: int, callback: Any) -> None:
"""Register device with a callback for updates.""" """Register device with a callback for updates."""
device_callbacks = self._callbacks.setdefault(device_id, []) return self._fibaro_device_manager.add_change_listener(device_id, callback)
device_callbacks.append(callback)
def register_event( def register_event(
self, device_id: int, callback: Callable[[FibaroEvent], None] self, device_id: int, callback: Callable[[FibaroEvent], None]
) -> None: ) -> Callable[[], None]:
"""Register device with a callback for central scene events. """Register device with a callback for central scene events.
The callback receives one parameter with the event. The callback receives one parameter with the event.
""" """
device_callbacks = self._event_callbacks.setdefault(device_id, []) return self._fibaro_device_manager.add_event_listener(device_id, callback)
device_callbacks.append(callback)
def get_children(self, device_id: int) -> list[DeviceModel]: def get_children(self, device_id: int) -> list[DeviceModel]:
"""Get a list of child devices.""" """Get a list of child devices."""
@@ -286,7 +238,7 @@ class FibaroController:
def _read_devices(self) -> None: def _read_devices(self) -> None:
"""Read and process the device list.""" """Read and process the device list."""
devices = self._client.read_devices() devices = self._fibaro_device_manager.get_devices()
self._device_map = {} self._device_map = {}
last_climate_parent = None last_climate_parent = None
last_endpoint = None last_endpoint = None
@@ -301,8 +253,8 @@ 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}"
) )
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:
continue continue
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}" device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
@@ -392,8 +344,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bo
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
controller.enable_state_handler()
return True return True
@@ -402,8 +352,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> b
_LOGGER.debug("Shutting down Fibaro connection") _LOGGER.debug("Shutting down Fibaro connection")
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
entry.runtime_data.disable_state_handler() entry.runtime_data.disconnect()
return unload_ok return unload_ok

View File

@@ -36,9 +36,13 @@ class FibaroEntity(Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass.""" """Call when entity is added to hass."""
self.controller.register(self.fibaro_device.fibaro_id, self._update_callback) self.async_on_remove(
self.controller.register(
self.fibaro_device.fibaro_id, self._update_callback
)
)
def _update_callback(self) -> None: def _update_callback(self, fibaro_device: DeviceModel) -> None:
"""Update the state.""" """Update the state."""
self.schedule_update_ha_state(True) self.schedule_update_ha_state(True)

View File

@@ -60,11 +60,16 @@ class FibaroEventEntity(FibaroEntity, EventEntity):
await super().async_added_to_hass() await super().async_added_to_hass()
# Register event callback # Register event callback
self.controller.register_event( self.async_on_remove(
self.fibaro_device.fibaro_id, self._event_callback self.controller.register_event(
self.fibaro_device.fibaro_id, self._event_callback
)
) )
def _event_callback(self, event: FibaroEvent) -> None: def _event_callback(self, event: FibaroEvent) -> None:
if event.key_id == self._button: if (
event.event_type.lower() == "centralsceneevent"
and event.key_id == self._button
):
self._trigger_event(event.key_event_type) self._trigger_event(event.key_event_type)
self.schedule_update_ha_state() self.schedule_update_ha_state()

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)
return self.async_create_entry(title="", data=user_input) 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)
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),
) )
@@ -301,7 +303,7 @@ async def google_generative_ai_config_option_schema(
CONF_TEMPERATURE, CONF_TEMPERATURE,
description={"suggested_value": options.get(CONF_TEMPERATURE)}, description={"suggested_value": options.get(CONF_TEMPERATURE)},
default=RECOMMENDED_TEMPERATURE, default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)), ): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
vol.Optional( vol.Optional(
CONF_TOP_P, CONF_TOP_P,
description={"suggested_value": options.get(CONF_TOP_P)}, description={"suggested_value": options.get(CONF_TOP_P)},

View File

@@ -55,6 +55,10 @@ from .const import (
# Max number of back and forth with the LLM to generate a response # Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10 MAX_TOOL_ITERATIONS = 10
ERROR_GETTING_RESPONSE = (
"Sorry, I had a problem getting a response from Google Generative AI."
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -429,6 +433,12 @@ class GoogleGenerativeAIConversationEntity(
raise HomeAssistantError( raise HomeAssistantError(
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}" f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
) )
if not chat_response.candidates:
LOGGER.error(
"No candidates found in the response: %s",
chat_response,
)
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
except ( except (
APIError, APIError,
@@ -452,9 +462,7 @@ class GoogleGenerativeAIConversationEntity(
response_parts = chat_response.candidates[0].content.parts response_parts = chat_response.candidates[0].content.parts
if not response_parts: if not response_parts:
raise HomeAssistantError( raise HomeAssistantError(ERROR_GETTING_RESPONSE)
"Sorry, I had a problem getting a response from Google Generative AI."
)
content = " ".join( content = " ".join(
[part.text.strip() for part in response_parts if part.text] [part.text.strip() for part in response_parts if part.text]
) )

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

@@ -182,6 +182,6 @@ async def websocket_update_addon(
async def websocket_update_core( async def websocket_update_core(
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
) -> None: ) -> None:
"""Websocket handler to update an addon.""" """Websocket handler to update Home Assistant Core."""
await update_core(hass, None, msg["backup"]) await update_core(hass, None, msg["backup"])
connection.send_result(msg[WS_ID]) connection.send_result(msg[WS_ID])

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

@@ -8,7 +8,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyheos"], "loggers": ["pyheos"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pyheos==1.0.4"], "requirements": ["pyheos==1.0.5"],
"ssdp": [ "ssdp": [
{ {
"st": "urn:schemas-denon-com:device:ACT-Denon:1" "st": "urn:schemas-denon-com:device:ACT-Denon:1"

View File

@@ -71,6 +71,7 @@ BASE_SUPPORTED_FEATURES = (
PLAY_STATE_TO_STATE = { PLAY_STATE_TO_STATE = {
None: MediaPlayerState.IDLE, None: MediaPlayerState.IDLE,
PlayState.UNKNOWN: MediaPlayerState.IDLE,
PlayState.PLAY: MediaPlayerState.PLAYING, PlayState.PLAY: MediaPlayerState.PLAYING,
PlayState.STOP: MediaPlayerState.IDLE, PlayState.STOP: MediaPlayerState.IDLE,
PlayState.PAUSE: MediaPlayerState.PAUSED, PlayState.PAUSE: MediaPlayerState.PAUSED,
@@ -479,6 +480,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

@@ -5,37 +5,18 @@ from typing import cast
from aiohomeconnect.model import EventKey, StatusKey from aiohomeconnect.model import EventKey, StatusKey
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.components.script import scripts_with_entity
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from .common import setup_home_connect_entry from .common import setup_home_connect_entry
from .const import ( from .const import REFRIGERATION_STATUS_DOOR_CLOSED, REFRIGERATION_STATUS_DOOR_OPEN
BSH_DOOR_STATE_CLOSED, from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
BSH_DOOR_STATE_LOCKED,
BSH_DOOR_STATE_OPEN,
DOMAIN,
REFRIGERATION_STATUS_DOOR_CLOSED,
REFRIGERATION_STATUS_DOOR_OPEN,
)
from .coordinator import (
HomeConnectApplianceData,
HomeConnectConfigEntry,
HomeConnectCoordinator,
)
from .entity import HomeConnectEntity from .entity import HomeConnectEntity
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -173,8 +154,6 @@ def _get_entities_for_appliance(
for description in BINARY_SENSORS for description in BINARY_SENSORS
if description.key in appliance.status if description.key in appliance.status
) )
if StatusKey.BSH_COMMON_DOOR_STATE in appliance.status:
entities.append(HomeConnectDoorBinarySensor(entry.runtime_data, appliance))
return entities return entities
@@ -220,83 +199,3 @@ class HomeConnectConnectivityBinarySensor(HomeConnectEntity, BinarySensorEntity)
def available(self) -> bool: def available(self) -> bool:
"""Return the availability.""" """Return the availability."""
return self.coordinator.last_update_success return self.coordinator.last_update_success
class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
"""Binary sensor for Home Connect Generic Door."""
_attr_has_entity_name = False
def __init__(
self,
coordinator: HomeConnectCoordinator,
appliance: HomeConnectApplianceData,
) -> None:
"""Initialize the entity."""
super().__init__(
coordinator,
appliance,
HomeConnectBinarySensorEntityDescription(
key=StatusKey.BSH_COMMON_DOOR_STATE,
device_class=BinarySensorDeviceClass.DOOR,
boolean_map={
BSH_DOOR_STATE_CLOSED: False,
BSH_DOOR_STATE_LOCKED: False,
BSH_DOOR_STATE_OPEN: True,
},
entity_registry_enabled_default=False,
),
)
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
self._attr_name = f"{appliance.info.name} Door"
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
items = automations + scripts
if not items:
return
entity_reg: er.EntityRegistry = er.async_get(self.hass)
entity_automations = [
automation_entity
for automation_id in automations
if (automation_entity := entity_reg.async_get(automation_id))
]
entity_scripts = [
script_entity
for script_id in scripts
if (script_entity := entity_reg.async_get(script_id))
]
items_list = [
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
for item in entity_automations
] + [
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
for item in entity_scripts
]
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_binary_common_door_sensor_{self.entity_id}",
breaks_in_ha_version="2025.5.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_binary_common_door_sensor",
translation_placeholders={
"entity": self.entity_id,
"items": "\n".join(items_list),
},
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
await super().async_will_remove_from_hass()
async_delete_issue(
self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
)

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

@@ -132,17 +132,6 @@
} }
} }
}, },
"deprecated_binary_common_door_sensor": {
"title": "Deprecated binary door sensor detected in some automations or scripts",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
}
}
}
},
"deprecated_command_actions": { "deprecated_command_actions": {
"title": "The command related actions are deprecated in favor of the new buttons", "title": "The command related actions are deprecated in favor of the new buttons",
"fix_flow": { "fix_flow": {
@@ -487,9 +476,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 +511,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 +1457,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 +1494,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

@@ -1,6 +1,6 @@
{ {
"config": { "config": {
"flow_title": "Homee {name} ({host})", "flow_title": "homee {name} ({host})",
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}, },
@@ -18,9 +18,9 @@
"username": "[%key:common::config_flow::data::username%]" "username": "[%key:common::config_flow::data::username%]"
}, },
"data_description": { "data_description": {
"host": "The IP address of your Homee.", "host": "The IP address of your homee.",
"username": "The username for your Homee.", "username": "The username for your homee.",
"password": "The password for your Homee." "password": "The password for your homee."
} }
} }
} }
@@ -45,7 +45,7 @@
"load_alarm": { "load_alarm": {
"name": "Load", "name": "Load",
"state": { "state": {
"off": "Normal", "off": "[%key:common::state::normal%]",
"on": "Overload" "on": "Overload"
} }
}, },
@@ -352,7 +352,7 @@
}, },
"exceptions": { "exceptions": {
"connection_closed": { "connection_closed": {
"message": "Could not connect to Homee while setting attribute." "message": "Could not connect to homee while setting attribute."
} }
} }
} }

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

@@ -88,7 +88,7 @@ from .utils import get_device_macs, non_verifying_requests_session
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=30)
NOTIFY_SCHEMA = vol.Any( NOTIFY_SCHEMA = vol.Any(
None, None,

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

@@ -62,7 +62,7 @@
"mode": { "mode": {
"name": "Mode", "name": "Mode",
"state": { "state": {
"normal": "Normal", "normal": "[%key:common::state::normal%]",
"home": "[%key:common::state::home%]", "home": "[%key:common::state::home%]",
"away": "[%key:common::state::not_home%]", "away": "[%key:common::state::not_home%]",
"auto": "Auto", "auto": "Auto",

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