Merge branch 'dev' into prepare_protobuf6

This commit is contained in:
J. Nick Koston 2025-03-30 10:17:05 -10:00 committed by GitHub
commit ec0f808450
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
840 changed files with 29408 additions and 8346 deletions

View File

@ -32,7 +32,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -457,7 +457,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -40,7 +40,7 @@ env:
CACHE_VERSION: 12 CACHE_VERSION: 12
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9 MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.4" HA_SHORT_VERSION: "2025.5"
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']" ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -249,7 +249,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -294,7 +294,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: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -334,7 +334,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: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -374,7 +374,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: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
id: python id: python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
@ -484,7 +484,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -587,7 +587,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -620,7 +620,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -677,7 +677,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -720,7 +720,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -767,7 +767,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -812,7 +812,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -889,7 +889,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -949,7 +949,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1074,7 +1074,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1208,7 +1208,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true
@ -1359,7 +1359,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
check-latest: true check-latest: true

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.12 uses: github/codeql-action/init@v3.28.13
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.12 uses: github/codeql-action/analyze@v3.28.13
with: with:
category: "/language:python" category: "/language:python"

View File

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}

View File

@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.4.0 uses: actions/setup-python@v5.5.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true check-latest: true
@ -159,7 +159,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.02.0 uses: home-assistant/wheels@2025.03.0
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2
@ -219,7 +219,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt sed -i "/uv/d" requirements_diff.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2025.02.0 uses: home-assistant/wheels@2025.03.0
with: with:
abi: ${{ matrix.abi }} abi: ${{ matrix.abi }}
tag: musllinux_1_2 tag: musllinux_1_2

View File

@ -119,6 +119,7 @@ homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.* homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.* homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.* homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
homeassistant.components.braviatv.* homeassistant.components.braviatv.*
homeassistant.components.bring.* homeassistant.components.bring.*
homeassistant.components.brother.* homeassistant.components.brother.*

6
CODEOWNERS generated
View File

@ -216,6 +216,8 @@ build.json @home-assistant/supervisor
/tests/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe
/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto /tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto
/homeassistant/components/bosch_alarm/ @mag1024 @sanjay900
/tests/components/bosch_alarm/ @mag1024 @sanjay900
/homeassistant/components/bosch_shc/ @tschamm /homeassistant/components/bosch_shc/ @tschamm
/tests/components/bosch_shc/ @tschamm /tests/components/bosch_shc/ @tschamm
/homeassistant/components/braviatv/ @bieniu @Drafteed /homeassistant/components/braviatv/ @bieniu @Drafteed
@ -1183,6 +1185,8 @@ build.json @home-assistant/supervisor
/tests/components/prusalink/ @balloob /tests/components/prusalink/ @balloob
/homeassistant/components/ps4/ @ktnrg45 /homeassistant/components/ps4/ @ktnrg45
/tests/components/ps4/ @ktnrg45 /tests/components/ps4/ @ktnrg45
/homeassistant/components/pterodactyl/ @elmurato
/tests/components/pterodactyl/ @elmurato
/homeassistant/components/pure_energie/ @klaasnicolaas /homeassistant/components/pure_energie/ @klaasnicolaas
/tests/components/pure_energie/ @klaasnicolaas /tests/components/pure_energie/ @klaasnicolaas
/homeassistant/components/purpleair/ @bachya /homeassistant/components/purpleair/ @bachya
@ -1476,8 +1480,6 @@ build.json @home-assistant/supervisor
/tests/components/suez_water/ @ooii @jb101010-2 /tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @Swamp-Ig /homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig /tests/components/sun/ @Swamp-Ig
/homeassistant/components/sunweg/ @rokam
/tests/components/sunweg/ @rokam
/homeassistant/components/supla/ @mwegrzynek /homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen /homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen /tests/components/surepetcare/ @benleb @danielhiversen

2
Dockerfile generated
View File

@ -31,7 +31,7 @@ RUN \
&& go2rtc --version && go2rtc --version
# Install uv # Install uv
RUN pip3 install uv==0.6.8 RUN pip3 install uv==0.6.10
WORKDIR /usr/src WORKDIR /usr/src

View File

@ -19,4 +19,4 @@ labels:
org.opencontainers.image.authors: The Home Assistant Authors org.opencontainers.image.authors: The Home Assistant Authors
org.opencontainers.image.url: https://www.home-assistant.io/ org.opencontainers.image.url: https://www.home-assistant.io/
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/ org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
org.opencontainers.image.licenses: Apache License 2.0 org.opencontainers.image.licenses: Apache-2.0

View File

@ -0,0 +1,5 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}

View File

@ -1,5 +1,6 @@
{ {
"domain": "motionblinds", "domain": "motionblinds",
"name": "Motionblinds", "name": "Motionblinds",
"integrations": ["motion_blinds", "motionblinds_ble"] "integrations": ["motion_blinds", "motionblinds_ble"],
"iot_standards": ["matter"]
} }

View File

@ -68,8 +68,8 @@
"led_bar_mode": { "led_bar_mode": {
"name": "LED bar mode", "name": "LED bar mode",
"state": { "state": {
"off": "Off", "off": "[%key:common::state::off%]",
"co2": "Carbon dioxide", "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"pm": "Particulate matter" "pm": "Particulate matter"
} }
}, },
@ -143,8 +143,8 @@
"led_bar_mode": { "led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]", "name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
"state": { "state": {
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]", "off": "[%key:common::state::off%]",
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]", "co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]" "pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
} }
}, },

View File

@ -8,7 +8,7 @@ from aiohttp import ClientSession
from aiohttp.client_exceptions import ClientConnectorError from aiohttp.client_exceptions import ClientConnectorError
from pyairnow import WebServiceAPI from pyairnow import WebServiceAPI
from pyairnow.conv import aqi_to_concentration from pyairnow.conv import aqi_to_concentration
from pyairnow.errors import AirNowError from pyairnow.errors import AirNowError, InvalidJsonError
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
distance=self.distance, distance=self.distance,
) )
except (AirNowError, ClientConnectorError) as error: except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
raise UpdateFailed(error) from error raise UpdateFailed(error) from error
if not obs: if not obs:

View File

@ -102,7 +102,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._get_device_data(discovery_info) device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError: except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = get_name(device) name = get_name(device)
@ -160,7 +161,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
device = await self._get_device_data(discovery_info) device = await self._get_device_data(discovery_info)
except AirthingsDeviceUpdateError: except AirthingsDeviceUpdateError:
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unknown error occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
name = get_name(device) name = get_name(device)
self._discovered_devices[address] = Discovery(name, discovery_info, device) self._discovered_devices[address] = Discovery(name, discovery_info, device)

View File

@ -32,7 +32,8 @@ class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN):
client = Airtouch5SimpleClient(user_input[CONF_HOST]) client = Airtouch5SimpleClient(user_input[CONF_HOST])
try: try:
await client.test_connection() await client.test_connection()
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unexpected exception")
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
else: else:
await self.async_set_unique_id(user_input[CONF_HOST]) await self.async_set_unique_id(user_input[CONF_HOST])

View File

@ -2,7 +2,7 @@
"config": { "config": {
"step": { "step": {
"geography_by_coords": { "geography_by_coords": {
"title": "Configure a Geography", "title": "Configure a geography",
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.", "description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]", "api_key": "[%key:common::config_flow::data::api_key%]",
@ -56,12 +56,12 @@
"sensor": { "sensor": {
"pollutant_label": { "pollutant_label": {
"state": { "state": {
"co": "Carbon Monoxide", "co": "Carbon monoxide",
"n2": "Nitrogen Dioxide", "n2": "Nitrogen dioxide",
"o3": "Ozone", "o3": "Ozone",
"p1": "PM10", "p1": "PM10",
"p2": "PM2.5", "p2": "PM2.5",
"s2": "Sulfur Dioxide" "s2": "Sulfur dioxide"
} }
}, },
"pollutant_level": { "pollutant_level": {

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.10"] "requirements": ["aioairzone-cloud==0.6.11"]
} }

View File

@ -32,8 +32,8 @@
"air_quality": { "air_quality": {
"name": "Air Quality mode", "name": "Air Quality mode",
"state": { "state": {
"off": "Off", "off": "[%key:common::state::off%]",
"on": "On", "on": "[%key:common::state::on%]",
"auto": "Auto" "auto": "Auto"
} }
}, },

View File

@ -1438,7 +1438,7 @@ class AlexaModeController(AlexaCapability):
# Fan preset_mode # Fan preset_mode
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}": if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None) mode = self.entity.attributes.get(fan.ATTR_PRESET_MODE, None)
if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, None): if mode in self.entity.attributes.get(fan.ATTR_PRESET_MODES, ()):
return f"{fan.ATTR_PRESET_MODE}.{mode}" return f"{fan.ATTR_PRESET_MODE}.{mode}"
# Humidifier mode # Humidifier mode

View File

@ -6,5 +6,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"], "loggers": ["boto3", "botocore", "s3transfer"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["boto3==1.34.131"] "requirements": ["boto3==1.37.1"]
} }

View File

@ -240,6 +240,7 @@ SENSOR_DESCRIPTIONS = (
suggested_display_precision=0, suggested_display_precision=0,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDGUSTMPH, key=TYPE_WINDGUSTMPH,

View File

@ -609,6 +609,7 @@ SENSOR_DESCRIPTIONS = (
translation_key="wind_direction", translation_key="wind_direction",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
SensorEntityDescription( SensorEntityDescription(
key=TYPE_WINDDIR_AVG10M, key=TYPE_WINDDIR_AVG10M,

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["pydroid-ipcam==2.0.0"] "requirements": ["pydroid-ipcam==3.0.0"]
} }

View File

@ -7,6 +7,6 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["androidtvremote2"], "loggers": ["androidtvremote2"],
"requirements": ["androidtvremote2==0.2.0"], "requirements": ["androidtvremote2==0.2.1"],
"zeroconf": ["_androidtvremote2._tcp.local."] "zeroconf": ["_androidtvremote2._tcp.local."]
} }

View File

@ -2,6 +2,8 @@
from __future__ import annotations from __future__ import annotations
import logging
from anova_wifi import AnovaApi, InvalidLogin from anova_wifi import AnovaApi, InvalidLogin
import voluptuous as vol import voluptuous as vol
@ -11,8 +13,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
class AnovaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Sets up a config flow for Anova.""" """Sets up a config flow for Anova."""
VERSION = 1 VERSION = 1
@ -35,7 +39,8 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
await api.authenticate() await api.authenticate()
except InvalidLogin: except InvalidLogin:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
return self.async_create_entry( return self.async_create_entry(

View File

@ -43,6 +43,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
config_entry: ApSystemsConfigEntry config_entry: ApSystemsConfigEntry
device_version: str device_version: str
battery_system: bool
def __init__( def __init__(
self, self,
@ -68,6 +69,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
self.api.max_power = device_info.maxPower self.api.max_power = device_info.maxPower
self.api.min_power = device_info.minPower self.api.min_power = device_info.minPower
self.device_version = device_info.devVer self.device_version = device_info.devVer
self.battery_system = device_info.isBatterySystem
async def _async_update_data(self) -> ApSystemsSensorData: async def _async_update_data(self) -> ApSystemsSensorData:
try: try:

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems", "documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["apsystems-ez1==2.4.0"] "requirements": ["apsystems-ez1==2.5.0"]
} }

View File

@ -36,6 +36,8 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
super().__init__(data) super().__init__(data)
self._api = data.coordinator.api self._api = data.coordinator.api
self._attr_unique_id = f"{data.device_id}_inverter_status" self._attr_unique_id = f"{data.device_id}_inverter_status"
if data.coordinator.battery_system:
self._attr_available = False
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update switch status and availability.""" """Update switch status and availability."""

View File

@ -60,7 +60,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except AuthenticationFailed: except AuthenticationFailed:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except except Exception:
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:

View File

@ -6,7 +6,11 @@ import logging
from typing import Any from typing import Any
from homeassistant.components import mqtt from homeassistant.components import mqtt
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature from homeassistant.const import DEGREE, UnitOfPrecipitationDepth, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -98,6 +102,7 @@ def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] |
DEGREE, DEGREE,
"mdi:compass", "mdi:compass",
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
] ]
return None return None
@ -178,6 +183,7 @@ class ArwnSensor(SensorEntity):
units: str, units: str,
icon: str | None = None, icon: str | None = None,
device_class: SensorDeviceClass | None = None, device_class: SensorDeviceClass | None = None,
state_class: SensorStateClass | None = None,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
self.entity_id = _slug(name) self.entity_id = _slug(name)
@ -188,6 +194,7 @@ class ArwnSensor(SensorEntity):
self._attr_native_unit_of_measurement = units self._attr_native_unit_of_measurement = units
self._attr_icon = icon self._attr_icon = icon
self._attr_device_class = device_class self._attr_device_class = device_class
self._attr_state_class = state_class
def set_event(self, event: dict[str, Any]) -> None: def set_event(self, event: dict[str, Any]) -> None:
"""Update the sensor with the most recent event.""" """Update the sensor with the most recent event."""

View File

@ -125,7 +125,7 @@ SAVE_DELAY = 10
@callback @callback
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool: def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
"""Filter out intents that are not local fallback.""" """Filter out intents that are not local fallback."""
return result.intent.name in (intent.INTENT_GET_STATE, intent.INTENT_NEVERMIND) return result.intent.name in (intent.INTENT_GET_STATE)
@callback @callback

View File

@ -1,9 +1,11 @@
"""Base class for assist satellite entities.""" """Base class for assist satellite entities."""
import logging import logging
from pathlib import Path
import voluptuous as vol import voluptuous as vol
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -15,6 +17,8 @@ from .const import (
CONNECTION_TEST_DATA, CONNECTION_TEST_DATA,
DATA_COMPONENT, DATA_COMPONENT,
DOMAIN, DOMAIN,
PREANNOUNCE_FILENAME,
PREANNOUNCE_URL,
AssistSatelliteEntityFeature, AssistSatelliteEntityFeature,
) )
from .entity import ( from .entity import (
@ -56,6 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{ {
vol.Optional("message"): str, vol.Optional("message"): str,
vol.Optional("media_id"): str, vol.Optional("media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
} }
), ),
cv.has_at_least_one_key("message", "media_id"), cv.has_at_least_one_key("message", "media_id"),
@ -70,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{ {
vol.Optional("start_message"): str, vol.Optional("start_message"): str,
vol.Optional("start_media_id"): str, vol.Optional("start_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
vol.Optional("extra_system_prompt"): str, vol.Optional("extra_system_prompt"): str,
} }
), ),
@ -82,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_websocket_api(hass) async_register_websocket_api(hass)
hass.http.register_view(ConnectionTestView()) hass.http.register_view(ConnectionTestView())
# Default preannounce sound
await hass.http.async_register_static_paths(
[
StaticPathConfig(
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
)
]
)
return True return True

View File

@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
f"{DOMAIN}_connection_tests" f"{DOMAIN}_connection_tests"
) )
PREANNOUNCE_FILENAME = "preannounce.mp3"
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
class AssistSatelliteEntityFeature(IntFlag): class AssistSatelliteEntityFeature(IntFlag):
"""Supported features of Assist satellite entity.""" """Supported features of Assist satellite entity."""

View File

@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import chat_session, entity from homeassistant.helpers import chat_session, entity
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from .const import AssistSatelliteEntityFeature from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
from .errors import AssistSatelliteError, SatelliteBusyError from .errors import AssistSatelliteError, SatelliteBusyError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -101,6 +101,9 @@ class AssistSatelliteAnnouncement:
media_id_source: Literal["url", "media_id", "tts"] media_id_source: Literal["url", "media_id", "tts"]
"""Source of the media ID.""" """Source of the media ID."""
preannounce_media_id: str | None = None
"""Media ID to be played before announcement."""
class AssistSatelliteEntity(entity.Entity): class AssistSatelliteEntity(entity.Entity):
"""Entity encapsulating the state and functionality of an Assist satellite.""" """Entity encapsulating the state and functionality of an Assist satellite."""
@ -177,6 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
self, self,
message: str | None = None, message: str | None = None,
media_id: str | None = None, media_id: str | None = None,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None: ) -> None:
"""Play and show an announcement on the satellite. """Play and show an announcement on the satellite.
@ -186,6 +190,9 @@ class AssistSatelliteEntity(entity.Entity):
If media_id is provided, it is played directly. It is possible If media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text. to omit the message and the satellite will not show any text.
If preannounce_media_id is provided, it overrides the default sound.
If preannounce_media_id is None, no sound is played.
Calls async_announce with message and media id. Calls async_announce with message and media id.
""" """
await self._cancel_running_pipeline() await self._cancel_running_pipeline()
@ -193,7 +200,9 @@ class AssistSatelliteEntity(entity.Entity):
if message is None: if message is None:
message = "" message = ""
announcement = await self._resolve_announcement_media_id(message, media_id) announcement = await self._resolve_announcement_media_id(
message, media_id, preannounce_media_id
)
if self._is_announcing: if self._is_announcing:
raise SatelliteBusyError raise SatelliteBusyError
@ -220,6 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
start_message: str | None = None, start_message: str | None = None,
start_media_id: str | None = None, start_media_id: str | None = None,
extra_system_prompt: str | None = None, extra_system_prompt: str | None = None,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None: ) -> None:
"""Start a conversation from the satellite. """Start a conversation from the satellite.
@ -229,6 +239,9 @@ class AssistSatelliteEntity(entity.Entity):
If start_media_id is provided, it is played directly. It is possible If start_media_id is provided, it is played directly. It is possible
to omit the message and the satellite will not show any text. to omit the message and the satellite will not show any text.
If preannounce_media_id is provided, it is played before the announcement.
If preannounce_media_id is None, no sound is played.
Calls async_start_conversation. Calls async_start_conversation.
""" """
await self._cancel_running_pipeline() await self._cancel_running_pipeline()
@ -244,13 +257,15 @@ class AssistSatelliteEntity(entity.Entity):
start_message = "" start_message = ""
announcement = await self._resolve_announcement_media_id( announcement = await self._resolve_announcement_media_id(
start_message, start_media_id start_message, start_media_id, preannounce_media_id
) )
if self._is_announcing: if self._is_announcing:
raise SatelliteBusyError raise SatelliteBusyError
self._is_announcing = True self._is_announcing = True
self._set_state(AssistSatelliteState.RESPONDING)
# Provide our start info to the LLM so it understands context of incoming message # Provide our start info to the LLM so it understands context of incoming message
if extra_system_prompt is not None: if extra_system_prompt is not None:
self._extra_system_prompt = extra_system_prompt self._extra_system_prompt = extra_system_prompt
@ -280,6 +295,7 @@ class AssistSatelliteEntity(entity.Entity):
raise raise
finally: finally:
self._is_announcing = False self._is_announcing = False
self._set_state(AssistSatelliteState.IDLE)
async def async_start_conversation( async def async_start_conversation(
self, start_announcement: AssistSatelliteAnnouncement self, start_announcement: AssistSatelliteAnnouncement
@ -470,7 +486,10 @@ class AssistSatelliteEntity(entity.Entity):
return vad.VadSensitivity.to_seconds(vad_sensitivity) return vad.VadSensitivity.to_seconds(vad_sensitivity)
async def _resolve_announcement_media_id( async def _resolve_announcement_media_id(
self, message: str, media_id: str | None self,
message: str,
media_id: str | None,
preannounce_media_id: str | None = None,
) -> AssistSatelliteAnnouncement: ) -> AssistSatelliteAnnouncement:
"""Resolve the media ID.""" """Resolve the media ID."""
media_id_source: Literal["url", "media_id", "tts"] | None = None media_id_source: Literal["url", "media_id", "tts"] | None = None
@ -478,7 +497,6 @@ class AssistSatelliteEntity(entity.Entity):
if media_id: if media_id:
original_media_id = media_id original_media_id = media_id
else: else:
media_id_source = "tts" media_id_source = "tts"
# Synthesize audio and get URL # Synthesize audio and get URL
@ -530,10 +548,26 @@ class AssistSatelliteEntity(entity.Entity):
# Resolve to full URL # Resolve to full URL
media_id = async_process_play_media_url(self.hass, media_id) media_id = async_process_play_media_url(self.hass, media_id)
# Resolve preannounce media id
if preannounce_media_id:
if media_source.is_media_source_id(preannounce_media_id):
preannounce_media = await media_source.async_resolve_media(
self.hass,
preannounce_media_id,
None,
)
preannounce_media_id = preannounce_media.url
# Resolve to full URL
preannounce_media_id = async_process_play_media_url(
self.hass, preannounce_media_id
)
return AssistSatelliteAnnouncement( return AssistSatelliteAnnouncement(
message=message, message=message,
media_id=media_id, media_id=media_id,
original_media_id=original_media_id, original_media_id=original_media_id,
tts_token=tts_token, tts_token=tts_token,
media_id_source=media_id_source, media_id_source=media_id_source,
preannounce_media_id=preannounce_media_id,
) )

View File

@ -8,12 +8,17 @@ announce:
message: message:
required: false required: false
example: "Time to wake up!" example: "Time to wake up!"
default: ""
selector: selector:
text: text:
media_id: media_id:
required: false required: false
selector: selector:
text: text:
preannounce_media_id:
required: false
selector:
text:
start_conversation: start_conversation:
target: target:
entity: entity:
@ -24,6 +29,7 @@ start_conversation:
start_message: start_message:
required: false required: false
example: "You left the lights on in the living room. Turn them off?" example: "You left the lights on in the living room. Turn them off?"
default: ""
selector: selector:
text: text:
start_media_id: start_media_id:
@ -34,3 +40,7 @@ start_conversation:
required: false required: false
selector: selector:
text: text:
preannounce_media_id:
required: false
selector:
text:

View File

@ -23,6 +23,10 @@
"media_id": { "media_id": {
"name": "Media ID", "name": "Media ID",
"description": "The media ID to announce instead of using text-to-speech." "description": "The media ID to announce instead of using text-to-speech."
},
"preannounce_media_id": {
"name": "Preannounce Media ID",
"description": "The media ID to play before the announcement."
} }
} }
}, },
@ -41,6 +45,10 @@
"extra_system_prompt": { "extra_system_prompt": {
"name": "Extra system prompt", "name": "Extra system prompt",
"description": "Provide background information to the AI about the request." "description": "Provide background information to the AI about the request."
},
"preannounce_media_id": {
"name": "Preannounce Media ID",
"description": "The media ID to play before the start message or media."
} }
} }
} }

View File

@ -198,7 +198,8 @@ async def websocket_test_connection(
hass.async_create_background_task( hass.async_create_background_task(
satellite.async_internal_announce( satellite.async_internal_announce(
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}" media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
preannounce_media_id=None,
), ),
f"assist_satellite_connection_test_{msg['entity_id']}", f"assist_satellite_connection_test_{msg['entity_id']}",
) )

View File

@ -6,5 +6,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aiobotocore", "botocore"], "loggers": ["aiobotocore", "botocore"],
"quality_scale": "legacy", "quality_scale": "legacy",
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"] "requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
} }

View File

@ -1,7 +1,9 @@
"""The Backup integration.""" """The Backup integration."""
from homeassistant.config_entries import SOURCE_SYSTEM
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers.backup import DATA_BACKUP from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -18,10 +20,12 @@ from .agent import (
) )
from .config import BackupConfig, CreateBackupParametersDict from .config import BackupConfig, CreateBackupParametersDict
from .const import DATA_MANAGER, DOMAIN from .const import DATA_MANAGER, DOMAIN
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
from .http import async_register_http_views from .http import async_register_http_views
from .manager import ( from .manager import (
BackupManager, BackupManager,
BackupManagerError, BackupManagerError,
BackupPlatformEvent,
BackupPlatformProtocol, BackupPlatformProtocol,
BackupReaderWriter, BackupReaderWriter,
BackupReaderWriterError, BackupReaderWriterError,
@ -52,6 +56,7 @@ __all__ = [
"BackupConfig", "BackupConfig",
"BackupManagerError", "BackupManagerError",
"BackupNotFound", "BackupNotFound",
"BackupPlatformEvent",
"BackupPlatformProtocol", "BackupPlatformProtocol",
"BackupReaderWriter", "BackupReaderWriter",
"BackupReaderWriterError", "BackupReaderWriterError",
@ -74,6 +79,8 @@ __all__ = [
"suggested_filename_from_name_date", "suggested_filename_from_name_date",
] ]
PLATFORMS = [Platform.SENSOR]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@ -128,4 +135,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async_register_http_views(hass) async_register_http_views(hass)
discovery_flow.async_create_flow(
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
)
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
"""Set up a config entry."""
backup_manager: BackupManager = hass.data[DATA_MANAGER]
coordinator = BackupDataUpdateCoordinator(hass, entry, backup_manager)
await coordinator.async_config_entry_first_refresh()
entry.async_on_unload(coordinator.async_unsubscribe)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,21 @@
"""Config flow for Home Assistant Backup integration."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from .const import DOMAIN
class BackupConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Backup."""
VERSION = 1
async def async_step_system(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
return self.async_create_entry(title="Backup", data={})

View File

@ -16,8 +16,8 @@ DATA_MANAGER: HassKey[BackupManager] = HassKey(DOMAIN)
LOGGER = getLogger(__package__) LOGGER = getLogger(__package__)
EXCLUDE_FROM_BACKUP = [ EXCLUDE_FROM_BACKUP = [
"__pycache__/*", "**/__pycache__/*",
".DS_Store", "**/.DS_Store",
".HA_RESTORE", ".HA_RESTORE",
"*.db-shm", "*.db-shm",
"*.log.*", "*.log.*",

View File

@ -0,0 +1,81 @@
"""Coordinator for Home Assistant Backup integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import (
async_subscribe_events,
async_subscribe_platform_events,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER
from .manager import (
BackupManager,
BackupManagerState,
BackupPlatformEvent,
ManagerStateEvent,
)
type BackupConfigEntry = ConfigEntry[BackupDataUpdateCoordinator]
@dataclass
class BackupCoordinatorData:
"""Class to hold backup data."""
backup_manager_state: BackupManagerState
last_successful_automatic_backup: datetime | None
next_scheduled_automatic_backup: datetime | None
class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
"""Class to retrieve backup status."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
backup_manager: BackupManager,
) -> None:
"""Initialize coordinator."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=None,
)
self.unsubscribe: list[Callable[[], None]] = [
async_subscribe_events(hass, self._on_event),
async_subscribe_platform_events(hass, self._on_event),
]
self.backup_manager = backup_manager
@callback
def _on_event(self, event: ManagerStateEvent | BackupPlatformEvent) -> None:
"""Handle new event."""
LOGGER.debug("Received backup event: %s", event)
self.config_entry.async_create_task(self.hass, self.async_refresh())
async def _async_update_data(self) -> BackupCoordinatorData:
"""Update backup manager data."""
return BackupCoordinatorData(
self.backup_manager.state,
self.backup_manager.config.data.last_completed_automatic_backup,
self.backup_manager.config.data.schedule.next_automatic_backup,
)
@callback
def async_unsubscribe(self) -> None:
"""Unsubscribe from events."""
for unsub in self.unsubscribe:
unsub()

View File

@ -0,0 +1,27 @@
"""Diagnostics support for Home Assistant Backup integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from .coordinator import BackupConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: BackupConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
return {
"backup_agents": [
{"name": agent.name, "agent_id": agent.agent_id}
for agent in coordinator.backup_manager.backup_agents.values()
],
"backup_config": async_redact_data(
coordinator.backup_manager.config.data.to_dict(), [CONF_PASSWORD]
),
}

View File

@ -0,0 +1,36 @@
"""Base for backup entities."""
from __future__ import annotations
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import BackupDataUpdateCoordinator
class BackupManagerEntity(CoordinatorEntity[BackupDataUpdateCoordinator]):
"""Base entity for backup manager."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: BackupDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize base entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_unique_id = entity_description.key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, "backup_manager")},
manufacturer="Home Assistant",
model="Home Assistant Backup",
sw_version=HA_VERSION,
name="Backup",
entry_type=DeviceEntryType.SERVICE,
configuration_url="homeassistant://config/backup",
)

View File

@ -229,6 +229,13 @@ class RestoreBackupEvent(ManagerStateEvent):
state: RestoreBackupState state: RestoreBackupState
@dataclass(frozen=True, kw_only=True, slots=True)
class BackupPlatformEvent:
"""Backup platform class."""
domain: str
@dataclass(frozen=True, kw_only=True, slots=True) @dataclass(frozen=True, kw_only=True, slots=True)
class BlockedEvent(ManagerStateEvent): class BlockedEvent(ManagerStateEvent):
"""Backup manager blocked, Home Assistant is starting.""" """Backup manager blocked, Home Assistant is starting."""
@ -355,6 +362,9 @@ class BackupManager:
self._backup_event_subscriptions = hass.data[ self._backup_event_subscriptions = hass.data[
DATA_BACKUP DATA_BACKUP
].backup_event_subscriptions ].backup_event_subscriptions
self._backup_platform_event_subscriptions = hass.data[
DATA_BACKUP
].backup_platform_event_subscriptions
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Set up the backup manager.""" """Set up the backup manager."""
@ -465,6 +475,9 @@ class BackupManager:
LOGGER.debug("%s platforms loaded in total", len(self.platforms)) LOGGER.debug("%s platforms loaded in total", len(self.platforms))
LOGGER.debug("%s agents loaded in total", len(self.backup_agents)) LOGGER.debug("%s agents loaded in total", len(self.backup_agents))
LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents)) LOGGER.debug("%s local agents loaded in total", len(self.local_backup_agents))
event = BackupPlatformEvent(domain=integration_domain)
for subscription in self._backup_platform_event_subscriptions:
subscription(event)
async def async_pre_backup_actions(self) -> None: async def async_pre_backup_actions(self) -> None:
"""Perform pre backup actions.""" """Perform pre backup actions."""
@ -1713,7 +1726,9 @@ class CoreBackupReaderWriter(BackupReaderWriter):
"""Filter to filter excludes.""" """Filter to filter excludes."""
for exclude in excludes: for exclude in excludes:
if not path.match(exclude): # The home assistant core configuration directory is added as "data"
# in the tar file, so we need to prefix that path to the filters.
if not path.full_match(f"data/{exclude}"):
continue continue
LOGGER.debug("Ignoring %s because of %s", path, exclude) LOGGER.debug("Ignoring %s because of %s", path, exclude)
return True return True

View File

@ -5,8 +5,9 @@
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"dependencies": ["http", "websocket_api"], "dependencies": ["http", "websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/backup", "documentation": "https://www.home-assistant.io/integrations/backup",
"integration_type": "system", "integration_type": "service",
"iot_class": "calculated", "iot_class": "calculated",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["cronsim==2.6", "securetar==2025.2.1"] "requirements": ["cronsim==2.6", "securetar==2025.2.1"],
"single_config_entry": true
} }

View File

@ -0,0 +1,75 @@
"""Sensor platform for Home Assistant Backup integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import BackupConfigEntry, BackupCoordinatorData
from .entity import BackupManagerEntity
from .manager import BackupManagerState
@dataclass(kw_only=True, frozen=True)
class BackupSensorEntityDescription(SensorEntityDescription):
"""Description for Home Assistant Backup sensor entities."""
value_fn: Callable[[BackupCoordinatorData], str | datetime | None]
BACKUP_MANAGER_DESCRIPTIONS = (
BackupSensorEntityDescription(
key="backup_manager_state",
translation_key="backup_manager_state",
device_class=SensorDeviceClass.ENUM,
options=[state.value for state in BackupManagerState],
value_fn=lambda data: data.backup_manager_state,
),
BackupSensorEntityDescription(
key="next_scheduled_automatic_backup",
translation_key="next_scheduled_automatic_backup",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.next_scheduled_automatic_backup,
),
BackupSensorEntityDescription(
key="last_successful_automatic_backup",
translation_key="last_successful_automatic_backup",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.last_successful_automatic_backup,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BackupConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Sensor set up for backup config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
BackupManagerSensor(coordinator, description)
for description in BACKUP_MANAGER_DESCRIPTIONS
)
class BackupManagerSensor(BackupManagerEntity, SensorEntity):
"""Sensor to track backup manager state."""
entity_description: BackupSensorEntityDescription
@property
def native_value(self) -> str | datetime | None:
"""Return native value of entity."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -22,5 +22,24 @@
"name": "Create automatic backup", "name": "Create automatic backup",
"description": "Creates a new backup with automatic backup settings." "description": "Creates a new backup with automatic backup settings."
} }
},
"entity": {
"sensor": {
"backup_manager_state": {
"name": "Backup Manager state",
"state": {
"idle": "[%key:common::state::idle%]",
"create_backup": "Creating a backup",
"receive_backup": "Receiving a backup",
"restore_backup": "Restoring a backup"
}
},
"next_scheduled_automatic_backup": {
"name": "Next scheduled automatic backup"
},
"last_successful_automatic_backup": {
"name": "Last successful automatic backup"
}
}
} }
} }

View File

@ -0,0 +1 @@
"""Balay virtual integration."""

View File

@ -0,0 +1,6 @@
{
"domain": "balay",
"name": "Balay",
"integration_type": "virtual",
"supported_by": "home_connect"
}

View File

@ -132,7 +132,7 @@
"name": "Charging", "name": "Charging",
"state": { "state": {
"off": "Not charging", "off": "Not charging",
"on": "Charging" "on": "[%key:common::state::charging%]"
} }
}, },
"carbon_monoxide": { "carbon_monoxide": {

View File

@ -37,7 +37,7 @@
"vehicle_status": { "vehicle_status": {
"name": "Vehicle status", "name": "Vehicle status",
"state": { "state": {
"standby": "Standby", "standby": "[%key:common::state::standby%]",
"vehicle_detected": "Detected", "vehicle_detected": "Detected",
"ready": "Ready", "ready": "Ready",
"no_power": "No power", "no_power": "No power",

View File

@ -6,7 +6,7 @@
"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%]",
"region": "ConnectedDrive Region" "region": "ConnectedDrive region"
}, },
"data_description": { "data_description": {
"username": "The email address of your MyBMW/MINI Connected account.", "username": "The email address of your MyBMW/MINI Connected account.",
@ -113,10 +113,10 @@
}, },
"select": { "select": {
"ac_limit": { "ac_limit": {
"name": "AC Charging Limit" "name": "AC charging limit"
}, },
"charging_mode": { "charging_mode": {
"name": "Charging Mode", "name": "Charging mode",
"state": { "state": {
"immediate_charging": "Immediate charging", "immediate_charging": "Immediate charging",
"delayed_charging": "Delayed charging", "delayed_charging": "Delayed charging",
@ -181,7 +181,7 @@
"cooling": "Cooling", "cooling": "Cooling",
"heating": "Heating", "heating": "Heating",
"inactive": "Inactive", "inactive": "Inactive",
"standby": "Standby", "standby": "[%key:common::state::standby%]",
"ventilation": "Ventilation" "ventilation": "Ventilation"
} }
}, },

View File

@ -0,0 +1,62 @@
"""The Bosch Alarm integration."""
from __future__ import annotations
from ssl import SSLError
from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL]
type BoschAlarmConfigEntry = ConfigEntry[Panel]
async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
"""Set up Bosch Alarm from a config entry."""
panel = Panel(
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
automation_code=entry.data.get(CONF_PASSWORD),
installer_or_user_code=entry.data.get(
CONF_INSTALLER_CODE, entry.data.get(CONF_USER_CODE)
),
)
try:
await panel.connect()
except (PermissionError, ValueError) as err:
await panel.disconnect()
raise ConfigEntryNotReady from err
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
await panel.disconnect()
raise ConfigEntryNotReady("Connection failed") from err
entry.runtime_data = panel
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",
model=panel.model,
sw_version=panel.firmware_version,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.disconnect()
return unload_ok

View File

@ -0,0 +1,109 @@
"""Support for Bosch Alarm Panel."""
from __future__ import annotations
from bosch_alarm_mode2 import Panel
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up control panels for each area."""
panel = config_entry.runtime_data
async_add_entities(
AreaAlarmControlPanel(
panel,
area_id,
config_entry.unique_id or config_entry.entry_id,
)
for area_id in panel.areas
)
class AreaAlarmControlPanel(AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel."""
_attr_has_entity_name = True
_attr_supported_features = (
AlarmControlPanelEntityFeature.ARM_HOME
| AlarmControlPanelEntityFeature.ARM_AWAY
)
_attr_code_arm_required = False
_attr_name = None
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
"""Initialise a Bosch Alarm control panel entity."""
self.panel = panel
self._area = panel.areas[area_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
def alarm_state(self) -> AlarmControlPanelState | None:
"""Return the state of the alarm."""
if self._area.is_triggered():
return AlarmControlPanelState.TRIGGERED
if self._area.is_disarmed():
return AlarmControlPanelState.DISARMED
if self._area.is_arming():
return AlarmControlPanelState.ARMING
if self._area.is_pending():
return AlarmControlPanelState.PENDING
if self._area.is_part_armed():
return AlarmControlPanelState.ARMED_HOME
if self._area.is_all_armed():
return AlarmControlPanelState.ARMED_AWAY
return None
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Disarm this panel."""
await self.panel.area_disarm(self._area_id)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self.panel.area_arm_part(self._area_id)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
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

@ -0,0 +1,165 @@
"""Config flow for Bosch Alarm integration."""
from __future__ import annotations
import asyncio
import logging
import ssl
from typing import Any
from bosch_alarm_mode2 import Panel
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_CODE,
CONF_HOST,
CONF_MODEL,
CONF_PASSWORD,
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=7700): cv.positive_int,
}
)
STEP_AUTH_DATA_SCHEMA_SOLUTION = vol.Schema(
{
vol.Required(CONF_USER_CODE): str,
}
)
STEP_AUTH_DATA_SCHEMA_AMAX = vol.Schema(
{
vol.Required(CONF_INSTALLER_CODE): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_AUTH_DATA_SCHEMA_BG = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
STEP_INIT_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_CODE): str})
async def try_connect(
data: dict[str, Any], load_selector: int = 0
) -> tuple[str, int | None]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
panel = Panel(
host=data[CONF_HOST],
port=data[CONF_PORT],
automation_code=data.get(CONF_PASSWORD),
installer_or_user_code=data.get(CONF_INSTALLER_CODE, data.get(CONF_USER_CODE)),
)
try:
await panel.connect(load_selector)
finally:
await panel.disconnect()
return (panel.model, panel.serial_number)
class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Bosch Alarm."""
def __init__(self) -> None:
"""Init config flow."""
self._data: dict[str, Any] = {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, serial) = await try_connect(user_input, 0)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
asyncio.exceptions.TimeoutError,
) as e:
_LOGGER.error("Connection Error: %s", e)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
self._data = user_input
self._data[CONF_MODEL] = model
return await self.async_step_auth()
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
async def async_step_auth(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the auth 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:
self._data.update(user_input)
try:
(model, serial_number) = 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:
if serial_number:
await self.async_set_unique_id(str(serial_number))
self._abort_if_unique_id_configured()
else:
self._async_abort_entries_match({CONF_HOST: self._data[CONF_HOST]})
return self.async_create_entry(title=f"Bosch {model}", data=self._data)
return self.async_show_form(
step_id="auth",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)

View File

@ -0,0 +1,6 @@
"""Constants for the Bosch Alarm integration."""
DOMAIN = "bosch_alarm"
HISTORY_ATTR = "history"
CONF_INSTALLER_CODE = "installer_code"
CONF_USER_CODE = "user_code"

View File

@ -0,0 +1,11 @@
{
"domain": "bosch_alarm",
"name": "Bosch Alarm",
"codeowners": ["@mag1024", "@sanjay900"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["bosch-alarm-mode2==0.4.3"]
}

View File

@ -0,0 +1,84 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions defined
appropriate-polling:
status: exempt
comment: |
No polling
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
Device type integration
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
No repairs
stale-devices:
status: exempt
comment: |
Device type integration
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
Integration does not make any HTTP requests.
strict-typing: done

View File

@ -0,0 +1,36 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Bosch alarm panel",
"port": "The port used to connect to your Bosch alarm panel. This is usually 7700"
}
},
"auth": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"installer_code": "Installer code",
"user_code": "User code"
},
"data_description": {
"password": "The Mode 2 automation code from your panel",
"installer_code": "The installer code from your panel",
"user_code": "The user code from your panel"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -170,6 +170,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
icon="mdi:compass-outline", icon="mdi:compass-outline",
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
SensorEntityDescription( SensorEntityDescription(
key="pressure", key="pressure",

View File

@ -142,6 +142,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
@property @property
def media_artist(self) -> str | None: def media_artist(self) -> str | None:
"""Artist of current playing media, music track only.""" """Artist of current playing media, music track only."""
if (
not self.client.play_state.metadata.artist
and self.client.state.source == "IR"
):
# Return channel instead of artist when playing internet radio
return self.client.play_state.metadata.station
return self.client.play_state.metadata.artist return self.client.play_state.metadata.artist
@property @property
@ -169,6 +175,11 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
"""Last time the media position was updated.""" """Last time the media position was updated."""
return self.client.position_last_updated return self.client.position_last_updated
@property
def media_channel(self) -> str | None:
"""Channel currently playing."""
return self.client.play_state.metadata.station
@property @property
def is_volume_muted(self) -> bool | None: def is_volume_muted(self) -> bool | None:
"""Volume mute status.""" """Volume mute status."""

View File

@ -81,7 +81,7 @@ class ChromecastInfo:
"+label%3A%22integration%3A+cast%22" "+label%3A%22integration%3A+cast%22"
) )
_LOGGER.debug( _LOGGER.info(
( (
"Fetched cast details for unknown model '%s' manufacturer:" "Fetched cast details for unknown model '%s' manufacturer:"
" '%s', type: '%s'. Please %s" " '%s', type: '%s'. Please %s"

View File

@ -14,7 +14,7 @@
"documentation": "https://www.home-assistant.io/integrations/cast", "documentation": "https://www.home-assistant.io/integrations/cast",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"], "loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.6"], "requirements": ["PyChromecast==14.0.7"],
"single_config_entry": true, "single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."] "zeroconf": ["_googlecast._tcp.local."]
} }

View File

@ -44,7 +44,7 @@ class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except DIOChaconInvalidAuthError: except DIOChaconInvalidAuthError:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except except Exception:
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"

View File

@ -257,7 +257,7 @@
"selector": { "selector": {
"hvac_mode": { "hvac_mode": {
"options": { "options": {
"off": "Off", "off": "[%key:common::state::off%]",
"auto": "Auto", "auto": "Auto",
"cool": "Cool", "cool": "Cool",
"dry": "Dry", "dry": "Dry",

View File

@ -4,13 +4,14 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
from http import HTTPStatus
import logging import logging
import random import random
from typing import Any from typing import Any
from aiohttp import ClientError from aiohttp import ClientError, ClientResponseError
from hass_nabucasa import Cloud, CloudError from hass_nabucasa import Cloud, CloudError
from hass_nabucasa.api import CloudApiNonRetryableError from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
from hass_nabucasa.cloud_api import ( from hass_nabucasa.cloud_api import (
FilesHandlerListEntry, FilesHandlerListEntry,
async_files_delete_file, async_files_delete_file,
@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
""" """
if not backup.protected: if not backup.protected:
raise BackupAgentError("Cloud backups must be protected") raise BackupAgentError("Cloud backups must be protected")
if self._cloud.subscription_expired:
raise BackupAgentError("Cloud subscription has expired")
size = backup.size size = backup.size
try: try:
@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
) from err ) from err
raise BackupAgentError(f"Failed to upload backup {err}") from err raise BackupAgentError(f"Failed to upload backup {err}") from err
except CloudError as err: except CloudError as err:
if (
isinstance(err, CloudApiError)
and isinstance(err.orig_exc, ClientResponseError)
and err.orig_exc.status == HTTPStatus.FORBIDDEN
and self._cloud.subscription_expired
):
raise BackupAgentError("Cloud subscription has expired") from err
if tries == _RETRY_LIMIT: if tries == _RETRY_LIMIT:
raise BackupAgentError(f"Failed to upload backup {err}") from err raise BackupAgentError(f"Failed to upload backup {err}") from err
tries += 1 tries += 1

View File

@ -9,7 +9,6 @@ from typing import Any
import pycfdns import pycfdns
import voluptuous as vol import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -118,8 +117,6 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:

View File

@ -4,19 +4,19 @@
"step": { "step": {
"user": { "user": {
"title": "Connect to Cloudflare", "title": "Connect to Cloudflare",
"description": "This integration requires an API Token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.", "description": "This integration requires an API token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.",
"data": { "data": {
"api_token": "[%key:common::config_flow::data::api_token%]" "api_token": "[%key:common::config_flow::data::api_token%]"
} }
}, },
"zone": { "zone": {
"title": "Choose the Zone to Update", "title": "Choose the zone to update",
"data": { "data": {
"zone": "Zone" "zone": "Zone"
} }
}, },
"records": { "records": {
"title": "Choose the Records to Update", "title": "Choose the records to update",
"data": { "data": {
"records": "Records" "records": "Records"
} }
@ -40,7 +40,7 @@
"services": { "services": {
"update_records": { "update_records": {
"name": "Update records", "name": "Update records",
"description": "Manually trigger update to Cloudflare records." "description": "Manually triggers an update of Cloudflare records."
} }
} }
} }

View File

@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
ALARM_AREA_ARMED_STATUS: dict[str, int] = { ALARM_AREA_ARMED_STATUS: dict[str, int] = {
DISABLE: 0,
HOME_P1: 1, HOME_P1: 1,
HOME_P2: 2, HOME_P2: 2,
NIGHT: 3, NIGHT: 3,
@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED, AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
}.get(self._area.human_status) }.get(self._area.human_status)
async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None:
"""Update state after action."""
self._area.human_status = area_state
self._area.armed = armed
await self.async_update_ha_state()
async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
if code != str(self._api.device_pin): if code != str(self._api.device_pin):
return return
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE]) await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
await self._async_update_state(
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
)
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._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY]) await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
)
async def async_alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME]) await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
)
async def async_alarm_arm_night(self, code: str | None = None) -> None: async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """Send arm night command."""
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT]) await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
)

View File

@ -9,3 +9,5 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "comelit" DOMAIN = "comelit"
DEFAULT_PORT = 80 DEFAULT_PORT = 80
DEVICE_TYPE_LIST = [BRIDGE, VEDO] DEVICE_TYPE_LIST = [BRIDGE, VEDO]
SCAN_INTERVAL = 5

View File

@ -22,7 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import _LOGGER, DOMAIN from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator] type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
@ -53,7 +53,7 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
logger=_LOGGER, logger=_LOGGER,
config_entry=entry, config_entry=entry,
name=f"{DOMAIN}-{host}-coordinator", name=f"{DOMAIN}-{host}-coordinator",
update_interval=timedelta(seconds=5), update_interval=timedelta(seconds=SCAN_INTERVAL),
) )
device_registry = dr.async_get(self.hass) device_registry = dr.async_get(self.hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(

View File

@ -8,7 +8,7 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -98,13 +98,20 @@ class ComelitCoverEntity(
"""Return if the cover is opening.""" """Return if the cover is opening."""
return self._current_action("opening") return self._current_action("opening")
async def _cover_set_state(self, action: int, state: int) -> None:
"""Set desired cover state."""
self._last_state = self.state
await self._api.set_device_status(COVER, self._device.index, action)
self.coordinator.data[COVER][self._device.index].status = state
self.async_write_ha_state()
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover.""" """Close cover."""
await self._api.set_device_status(COVER, self._device.index, STATE_OFF) await self._cover_set_state(STATE_OFF, 2)
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open cover.""" """Open cover."""
await self._api.set_device_status(COVER, self._device.index, STATE_ON) await self._cover_set_state(STATE_ON, 1)
async def async_stop_cover(self, **_kwargs: Any) -> None: async def async_stop_cover(self, **_kwargs: Any) -> None:
"""Stop the cover.""" """Stop the cover."""
@ -112,13 +119,7 @@ class ComelitCoverEntity(
return return
action = STATE_ON if self.is_closing else STATE_OFF action = STATE_ON if self.is_closing else STATE_OFF
await self._api.set_device_status(COVER, self._device.index, action) await self._cover_set_state(action, 0)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle device update."""
self._last_state = self.state
self.async_write_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Handle entity which will be added.""" """Handle entity which will be added."""

View File

@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
async def _light_set_state(self, state: int) -> None: async def _light_set_state(self, state: int) -> None:
"""Set desired light state.""" """Set desired light state."""
await self.coordinator.api.set_device_status(LIGHT, self._device.index, state) await self.coordinator.api.set_device_status(LIGHT, self._device.index, state)
await self.coordinator.async_request_refresh() self.coordinator.data[LIGHT][self._device.index].status = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on.""" """Turn the light on."""

View File

@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiocomelit"], "loggers": ["aiocomelit"],
"requirements": ["aiocomelit==0.11.2"] "requirements": ["aiocomelit==0.11.3"]
} }

View File

@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
await self.coordinator.api.set_device_status( await self.coordinator.api.set_device_status(
self._device.type, self._device.index, state self._device.type, self._device.index, state
) )
await self.coordinator.async_request_refresh() self.coordinator.data[self._device.type][self._device.index].status = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on.""" """Turn the switch on."""

View File

@ -0,0 +1 @@
"""Constructa virtual integration."""

View File

@ -0,0 +1,6 @@
{
"domain": "constructa",
"name": "Constructa",
"integration_type": "virtual",
"supported_by": "home_connect"
}

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
import logging import logging
import re
from typing import Literal from typing import Literal
from hassil.recognize import RecognizeResult from hassil.recognize import RecognizeResult
@ -91,8 +90,6 @@ __all__ = [
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REGEX_TYPE = type(re.compile(""))
SERVICE_PROCESS_SCHEMA = vol.Schema( SERVICE_PROCESS_SCHEMA = vol.Schema(
{ {
vol.Required(ATTR_TEXT): cv.string, vol.Required(ATTR_TEXT): cv.string,

View File

@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
if ( if (
(maybe_result is None) # first result (maybe_result is None) # first result
or (num_matched_entities > best_num_matched_entities) or (
# More literal text matched
result.text_chunks_matched > maybe_result.text_chunks_matched
)
or (
# More entities matched
num_matched_entities > best_num_matched_entities
)
or ( or (
# Fewer unmatched entities # Fewer unmatched entities
(num_matched_entities == best_num_matched_entities) (num_matched_entities == best_num_matched_entities)
@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity):
and (num_unmatched_entities == best_num_unmatched_entities) and (num_unmatched_entities == best_num_unmatched_entities)
and (num_unmatched_ranges > best_num_unmatched_ranges) and (num_unmatched_ranges > best_num_unmatched_ranges)
) )
or (
# More literal text matched
(num_matched_entities == best_num_matched_entities)
and (num_unmatched_entities == best_num_unmatched_entities)
and (num_unmatched_ranges == best_num_unmatched_ranges)
and (
result.text_chunks_matched
> maybe_result.text_chunks_matched
)
)
or ( or (
# Prefer match failures with entities # Prefer match failures with entities
(result.text_chunks_matched == maybe_result.text_chunks_matched) (result.text_chunks_matched == maybe_result.text_chunks_matched)

View File

@ -3,11 +3,13 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import asdict
from typing import Any from typing import Any
from aiohttp import web from aiohttp import web
from hassil.recognize import MISSING_ENTITY, RecognizeResult from hassil.recognize import MISSING_ENTITY, RecognizeResult
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from home_assistant_intents import get_language_scores
import voluptuous as vol import voluptuous as vol
from homeassistant.components import http, websocket_api from homeassistant.components import http, websocket_api
@ -38,6 +40,7 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, websocket_list_agents) websocket_api.async_register_command(hass, websocket_list_agents)
websocket_api.async_register_command(hass, websocket_list_sentences) websocket_api.async_register_command(hass, websocket_list_sentences)
websocket_api.async_register_command(hass, websocket_hass_agent_debug) websocket_api.async_register_command(hass, websocket_hass_agent_debug)
websocket_api.async_register_command(hass, websocket_hass_agent_language_scores)
@websocket_api.websocket_command( @websocket_api.websocket_command(
@ -336,6 +339,36 @@ def _get_unmatched_slots(
return unmatched_slots return unmatched_slots
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/agent/homeassistant/language_scores",
vol.Optional("language"): str,
vol.Optional("country"): str,
}
)
@websocket_api.async_response
async def websocket_hass_agent_language_scores(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Get support scores per language."""
language = msg.get("language", hass.config.language)
country = msg.get("country", hass.config.country)
scores = await hass.async_add_executor_job(get_language_scores)
matching_langs = language_util.matches(language, scores.keys(), country=country)
preferred_lang = matching_langs[0] if matching_langs else language
result = {
"languages": {
lang_key: asdict(lang_scores) for lang_key, lang_scores in scores.items()
},
"preferred_language": preferred_lang,
}
connection.send_result(msg["id"], result)
class ConversationProcessView(http.HomeAssistantView): class ConversationProcessView(http.HomeAssistantView):
"""View to process text.""" """View to process text."""

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation", "documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.23"] "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
} }

View File

@ -1,37 +0,0 @@
"""Util for Conversation."""
from __future__ import annotations
import re
def create_matcher(utterance: str) -> re.Pattern[str]:
"""Create a regex that matches the utterance."""
# Split utterance into parts that are type: NORMAL, GROUP or OPTIONAL
# Pattern matches (GROUP|OPTIONAL): Change light to [the color] {name}
parts = re.split(r"({\w+}|\[[\w\s]+\] *)", utterance)
# Pattern to extract name from GROUP part. Matches {name}
group_matcher = re.compile(r"{(\w+)}")
# Pattern to extract text from OPTIONAL part. Matches [the color]
optional_matcher = re.compile(r"\[([\w ]+)\] *")
pattern = ["^"]
for part in parts:
group_match = group_matcher.match(part)
optional_match = optional_matcher.match(part)
# Normal part
if group_match is None and optional_match is None:
pattern.append(part)
continue
# Group part
if group_match is not None:
pattern.append(rf"(?P<{group_match.groups()[0]}>[\w ]+?)\s*")
# Optional part
elif optional_match is not None:
pattern.append(rf"(?:{optional_match.groups()[0]} *)?")
pattern.append("$")
return re.compile("".join(pattern), re.IGNORECASE)

View File

@ -6,7 +6,7 @@
"data": { "data": {
"email": "[%key:common::config_flow::data::email%]", "email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"country": "Country" "country": "[%key:common::config_flow::data::country%]"
}, },
"data_description": { "data_description": {
"email": "Email used to access your {cookidoo} account.", "email": "Email used to access your {cookidoo} account.",

View File

@ -38,10 +38,10 @@
"name": "[%key:component::cover::title%]", "name": "[%key:component::cover::title%]",
"state": { "state": {
"open": "[%key:common::state::open%]", "open": "[%key:common::state::open%]",
"opening": "Opening", "opening": "[%key:common::state::opening%]",
"closed": "[%key:common::state::closed%]", "closed": "[%key:common::state::closed%]",
"closing": "Closing", "closing": "[%key:common::state::closing%]",
"stopped": "Stopped" "stopped": "[%key:common::state::stopped%]"
}, },
"state_attributes": { "state_attributes": {
"current_position": { "current_position": {

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
import logging
from ssl import SSLError from ssl import SSLError
from typing import Any from typing import Any
@ -21,6 +22,8 @@ from .const import (
DOMAIN, DOMAIN,
) )
_LOGGER = logging.getLogger(__name__)
class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Deluge.""" """Handle a config flow for Deluge."""
@ -86,7 +89,8 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
await self.hass.async_add_executor_job(api.connect) await self.hass.async_add_executor_job(api.connect)
except (ConnectionRefusedError, TimeoutError, SSLError): except (ConnectionRefusedError, TimeoutError, SSLError):
return "cannot_connect" return "cannot_connect"
except Exception as ex: # noqa: BLE001 except Exception as ex:
_LOGGER.exception("Unexpected error")
if type(ex).__name__ == "BadLoginError": if type(ex).__name__ == "BadLoginError":
return "invalid_auth" return "invalid_auth"
return "unknown" return "unknown"

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging
from typing import Any from typing import Any
from pydexcom import AccountError, Dexcom, SessionError from pydexcom import AccountError, Dexcom, SessionError
@ -12,6 +13,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import CONF_SERVER, DOMAIN, SERVER_OUS, SERVER_US from .const import CONF_SERVER, DOMAIN, SERVER_OUS, SERVER_US
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema( DATA_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME): str,
@ -43,7 +46,8 @@ class DexcomConfigFlow(ConfigFlow, domain=DOMAIN):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
except AccountError: except AccountError:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001 except Exception:
_LOGGER.exception("Unexpected error")
errors["base"] = "unknown" errors["base"] = "unknown"
if "base" not in errors: if "base" not in errors:

View File

@ -9,7 +9,7 @@ import pydiscovergy.error as discovergyError
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import create_async_httpx_client
from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator from .coordinator import DiscovergyConfigEntry, DiscovergyUpdateCoordinator
@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -
client = Discovergy( client = Discovergy(
email=entry.data[CONF_EMAIL], email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD], password=entry.data[CONF_PASSWORD],
httpx_client=get_async_client(hass), httpx_client=create_async_httpx_client(hass),
authentication=BasicAuth(), authentication=BasicAuth(),
) )

View File

@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
username = auth["cdp_internal_user_id"].lower() username = auth["internalUserID"].lower()
await self.async_set_unique_id(username) await self.async_set_unique_id(username)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
email = auth["email"].lower() email = auth["loginEmailAddress"].lower()
data = { data = {
CONF_EMAIL: email, CONF_EMAIL: email,
CONF_USERNAME: username, CONF_USERNAME: username,

View File

@ -8,7 +8,11 @@ from aiodukeenergy import DukeEnergy
from aiohttp import ClientError from aiohttp import ClientError
from homeassistant.components.recorder import get_instance from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import ( from homeassistant.components.recorder.statistics import (
async_add_external_statistics, async_add_external_statistics,
get_last_statistics, get_last_statistics,
@ -137,7 +141,7 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}" f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
) )
consumption_metadata = StatisticMetaData( consumption_metadata = StatisticMetaData(
has_mean=False, mean_type=StatisticMeanType.NONE,
has_sum=True, has_sum=True,
name=f"{name_prefix} Consumption", name=f"{name_prefix} Consumption",
source=DOMAIN, source=DOMAIN,

View File

@ -6,5 +6,5 @@
"dependencies": ["recorder"], "dependencies": ["recorder"],
"documentation": "https://www.home-assistant.io/integrations/duke_energy", "documentation": "https://www.home-assistant.io/integrations/duke_energy",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["aiodukeenergy==0.2.2"] "requirements": ["aiodukeenergy==0.3.0"]
} }

View File

@ -132,7 +132,7 @@ SENSOR_TYPES: tuple[EcoforestSensorEntityDescription, ...] = (
), ),
EcoforestSensorEntityDescription( EcoforestSensorEntityDescription(
key="convecto_air_flow", key="convecto_air_flow",
translation_key="convecto_air_flow", translation_key="convector_air_flow",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda data: data.convecto_air_flow, value_fn=lambda data: data.convecto_air_flow,

View File

@ -78,8 +78,8 @@
"extractor": { "extractor": {
"name": "Extractor" "name": "Extractor"
}, },
"convecto_air_flow": { "convector_air_flow": {
"name": "Convecto air flow" "name": "Convector air flow"
} }
}, },
"number": { "number": {

View File

@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
def operation_list(self) -> list[str]: def operation_list(self) -> list[str]:
"""List of available operation modes.""" """List of available operation modes."""
econet_modes = self.water_heater.modes econet_modes = self.water_heater.modes
op_list = [] operation_modes = set()
for mode in econet_modes: for mode in econet_modes:
if ( if (
mode is not WaterHeaterOperationMode.UNKNOWN mode is not WaterHeaterOperationMode.UNKNOWN
and mode is not WaterHeaterOperationMode.VACATION and mode is not WaterHeaterOperationMode.VACATION
): ):
ha_mode = ECONET_STATE_TO_HA[mode] ha_mode = ECONET_STATE_TO_HA[mode]
op_list.append(ha_mode) operation_modes.add(ha_mode)
return op_list return list(operation_modes)
@property @property
def supported_features(self) -> WaterHeaterEntityFeature: def supported_features(self) -> WaterHeaterEntityFeature:

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.3.1"] "requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
} }

View File

@ -14,7 +14,7 @@
"step": { "step": {
"auth": { "auth": {
"data": { "data": {
"country": "Country", "country": "[%key:common::config_flow::data::country%]",
"override_rest_url": "REST URL", "override_rest_url": "REST URL",
"override_mqtt_url": "MQTT URL", "override_mqtt_url": "MQTT URL",
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",

View File

@ -68,6 +68,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
key="DEGREE", key="DEGREE",
native_unit_of_measurement=DEGREE, native_unit_of_measurement=DEGREE,
device_class=SensorDeviceClass.WIND_DIRECTION, device_class=SensorDeviceClass.WIND_DIRECTION,
state_class=SensorStateClass.MEASUREMENT_ANGLE,
), ),
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription( EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
key="WATT_METERS_SQUARED", key="WATT_METERS_SQUARED",

View File

@ -62,6 +62,7 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
except (ClientError, TimeoutError): except (ClientError, TimeoutError):
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
except Exception: # noqa: BLE001 except Exception: # noqa: BLE001
LOGGER.exception("Unknown exception occurred")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
await self.async_set_unique_id(hub.main.mac_address) await self.async_set_unique_id(hub.main.mac_address)
self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._abort_if_unique_id_configured(updates={CONF_HOST: host})

View File

@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, cast
from elvia import Elvia, error as ElviaError from elvia import Elvia, error as ElviaError
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import ( from homeassistant.components.recorder.statistics import (
async_add_external_statistics, async_add_external_statistics,
get_last_statistics, get_last_statistics,
@ -144,7 +148,7 @@ class ElviaImporter:
async_add_external_statistics( async_add_external_statistics(
hass=self.hass, hass=self.hass,
metadata=StatisticMetaData( metadata=StatisticMetaData(
has_mean=False, mean_type=StatisticMeanType.NONE,
has_sum=True, has_sum=True,
name=f"{self.metering_point_id} Consumption", name=f"{self.metering_point_id} Consumption",
source=DOMAIN, source=DOMAIN,

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