mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Merge branch 'dev' into prepare_protobuf6
This commit is contained in:
commit
ec0f808450
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@ -457,7 +457,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
34
.github/workflows/ci.yaml
vendored
34
.github/workflows/ci.yaml
vendored
@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 12
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
HA_SHORT_VERSION: "2025.5"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@ -249,7 +249,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -294,7 +294,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -334,7 +334,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -374,7 +374,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@ -484,7 +484,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@ -587,7 +587,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -620,7 +620,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -677,7 +677,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@ -720,7 +720,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -767,7 +767,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -812,7 +812,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -889,7 +889,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -949,7 +949,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@ -1074,7 +1074,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@ -1208,7 +1208,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@ -1359,7 +1359,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.12
|
||||
uses: github/codeql-action/init@v3.28.13
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.12
|
||||
uses: github/codeql-action/analyze@v3.28.13
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
6
.github/workflows/wheels.yml
vendored
6
.github/workflows/wheels.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.4.0
|
||||
uses: actions/setup-python@v5.5.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@ -159,7 +159,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.02.0
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@ -219,7 +219,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.02.0
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
@ -119,6 +119,7 @@ homeassistant.components.bluetooth_adapters.*
|
||||
homeassistant.components.bluetooth_tracker.*
|
||||
homeassistant.components.bmw_connected_drive.*
|
||||
homeassistant.components.bond.*
|
||||
homeassistant.components.bosch_alarm.*
|
||||
homeassistant.components.braviatv.*
|
||||
homeassistant.components.bring.*
|
||||
homeassistant.components.brother.*
|
||||
|
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@ -216,6 +216,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/bmw_connected_drive/ @gerard33 @rikroe
|
||||
/homeassistant/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
|
||||
/tests/components/bosch_shc/ @tschamm
|
||||
/homeassistant/components/braviatv/ @bieniu @Drafteed
|
||||
@ -1183,6 +1185,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/prusalink/ @balloob
|
||||
/homeassistant/components/ps4/ @ktnrg45
|
||||
/tests/components/ps4/ @ktnrg45
|
||||
/homeassistant/components/pterodactyl/ @elmurato
|
||||
/tests/components/pterodactyl/ @elmurato
|
||||
/homeassistant/components/pure_energie/ @klaasnicolaas
|
||||
/tests/components/pure_energie/ @klaasnicolaas
|
||||
/homeassistant/components/purpleair/ @bachya
|
||||
@ -1476,8 +1480,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sunweg/ @rokam
|
||||
/tests/components/sunweg/ @rokam
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
|
2
Dockerfile
generated
2
Dockerfile
generated
@ -31,7 +31,7 @@ RUN \
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.6.8
|
||||
RUN pip3 install uv==0.6.10
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
@ -19,4 +19,4 @@ labels:
|
||||
org.opencontainers.image.authors: The Home Assistant Authors
|
||||
org.opencontainers.image.url: https://www.home-assistant.io/
|
||||
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
|
||||
|
5
homeassistant/brands/bosch.json
Normal file
5
homeassistant/brands/bosch.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "bosch",
|
||||
"name": "Bosch",
|
||||
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "motionblinds",
|
||||
"name": "Motionblinds",
|
||||
"integrations": ["motion_blinds", "motionblinds_ble"]
|
||||
"integrations": ["motion_blinds", "motionblinds_ble"],
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
|
@ -68,8 +68,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "LED bar mode",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"co2": "Carbon dioxide",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"pm": "Particulate matter"
|
||||
}
|
||||
},
|
||||
@ -143,8 +143,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||
"state": {
|
||||
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
|
||||
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
||||
}
|
||||
},
|
||||
|
@ -8,7 +8,7 @@ from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from pyairnow import WebServiceAPI
|
||||
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.core import HomeAssistant
|
||||
@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
distance=self.distance,
|
||||
)
|
||||
|
||||
except (AirNowError, ClientConnectorError) as error:
|
||||
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
if not obs:
|
||||
|
@ -102,7 +102,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
device = await self._get_device_data(discovery_info)
|
||||
except AirthingsDeviceUpdateError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unknown error occurred")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
name = get_name(device)
|
||||
@ -160,7 +161,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
device = await self._get_device_data(discovery_info)
|
||||
except AirthingsDeviceUpdateError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unknown error occurred")
|
||||
return self.async_abort(reason="unknown")
|
||||
name = get_name(device)
|
||||
self._discovered_devices[address] = Discovery(name, discovery_info, device)
|
||||
|
@ -32,7 +32,8 @@ class AirTouch5ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
client = Airtouch5SimpleClient(user_input[CONF_HOST])
|
||||
try:
|
||||
await client.test_connection()
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors = {"base": "cannot_connect"}
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_HOST])
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"geography_by_coords": {
|
||||
"title": "Configure a Geography",
|
||||
"title": "Configure a geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "Carbon Monoxide",
|
||||
"n2": "Nitrogen Dioxide",
|
||||
"co": "Carbon monoxide",
|
||||
"n2": "Nitrogen dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur Dioxide"
|
||||
"s2": "Sulfur dioxide"
|
||||
}
|
||||
},
|
||||
"pollutant_level": {
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.10"]
|
||||
"requirements": ["aioairzone-cloud==0.6.11"]
|
||||
}
|
||||
|
@ -32,8 +32,8 @@
|
||||
"air_quality": {
|
||||
"name": "Air Quality mode",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"auto": "Auto"
|
||||
}
|
||||
},
|
||||
|
@ -1438,7 +1438,7 @@ class AlexaModeController(AlexaCapability):
|
||||
# Fan preset_mode
|
||||
if self.instance == f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}":
|
||||
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}"
|
||||
|
||||
# Humidifier mode
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["boto3==1.34.131"]
|
||||
"requirements": ["boto3==1.37.1"]
|
||||
}
|
||||
|
@ -240,6 +240,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDGUSTMPH,
|
||||
|
@ -609,6 +609,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
translation_key="wind_direction",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR_AVG10M,
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pydroid-ipcam==2.0.0"]
|
||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||
}
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"requirements": ["androidtvremote2==0.2.0"],
|
||||
"requirements": ["androidtvremote2==0.2.1"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from anova_wifi import AnovaApi, InvalidLogin
|
||||
import voluptuous as vol
|
||||
|
||||
@ -11,8 +13,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
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."""
|
||||
|
||||
VERSION = 1
|
||||
@ -35,7 +39,8 @@ class AnovaConfligFlow(ConfigFlow, domain=DOMAIN):
|
||||
await api.authenticate()
|
||||
except InvalidLogin:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
|
@ -43,6 +43,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
|
||||
config_entry: ApSystemsConfigEntry
|
||||
device_version: str
|
||||
battery_system: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -68,6 +69,7 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
self.device_version = device_info.devVer
|
||||
self.battery_system = device_info.isBatterySystem
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
try:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==2.4.0"]
|
||||
"requirements": ["apsystems-ez1==2.5.0"]
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
||||
super().__init__(data)
|
||||
self._api = data.coordinator.api
|
||||
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:
|
||||
"""Update switch status and availability."""
|
||||
|
@ -60,7 +60,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthenticationFailed:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
|
@ -6,7 +6,11 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
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.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@ -98,6 +102,7 @@ def discover_sensors(topic: str, payload: dict[str, Any]) -> list[ArwnSensor] |
|
||||
DEGREE,
|
||||
"mdi:compass",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
]
|
||||
return None
|
||||
@ -178,6 +183,7 @@ class ArwnSensor(SensorEntity):
|
||||
units: str,
|
||||
icon: str | None = None,
|
||||
device_class: SensorDeviceClass | None = None,
|
||||
state_class: SensorStateClass | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_id = _slug(name)
|
||||
@ -188,6 +194,7 @@ class ArwnSensor(SensorEntity):
|
||||
self._attr_native_unit_of_measurement = units
|
||||
self._attr_icon = icon
|
||||
self._attr_device_class = device_class
|
||||
self._attr_state_class = state_class
|
||||
|
||||
def set_event(self, event: dict[str, Any]) -> None:
|
||||
"""Update the sensor with the most recent event."""
|
||||
|
@ -125,7 +125,7 @@ SAVE_DELAY = 10
|
||||
@callback
|
||||
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
|
||||
"""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
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@ -15,6 +17,8 @@ from .const import (
|
||||
CONNECTION_TEST_DATA,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
PREANNOUNCE_FILENAME,
|
||||
PREANNOUNCE_URL,
|
||||
AssistSatelliteEntityFeature,
|
||||
)
|
||||
from .entity import (
|
||||
@ -56,6 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
}
|
||||
),
|
||||
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_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
),
|
||||
@ -82,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_register_websocket_api(hass)
|
||||
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
|
||||
|
||||
|
||||
|
@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
|
||||
f"{DOMAIN}_connection_tests"
|
||||
)
|
||||
|
||||
PREANNOUNCE_FILENAME = "preannounce.mp3"
|
||||
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
|
||||
|
||||
|
||||
class AssistSatelliteEntityFeature(IntFlag):
|
||||
"""Supported features of Assist satellite entity."""
|
||||
|
@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session, entity
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
|
||||
from .const import AssistSatelliteEntityFeature
|
||||
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
|
||||
from .errors import AssistSatelliteError, SatelliteBusyError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -101,6 +101,9 @@ class AssistSatelliteAnnouncement:
|
||||
media_id_source: Literal["url", "media_id", "tts"]
|
||||
"""Source of the media ID."""
|
||||
|
||||
preannounce_media_id: str | None = None
|
||||
"""Media ID to be played before announcement."""
|
||||
|
||||
|
||||
class AssistSatelliteEntity(entity.Entity):
|
||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||
@ -177,6 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""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
|
||||
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.
|
||||
"""
|
||||
await self._cancel_running_pipeline()
|
||||
@ -193,7 +200,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
if message is None:
|
||||
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:
|
||||
raise SatelliteBusyError
|
||||
@ -220,6 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message: str | None = None,
|
||||
start_media_id: str | None = None,
|
||||
extra_system_prompt: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""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
|
||||
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.
|
||||
"""
|
||||
await self._cancel_running_pipeline()
|
||||
@ -244,13 +257,15 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
start_message, start_media_id
|
||||
start_message, start_media_id, preannounce_media_id
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
raise SatelliteBusyError
|
||||
|
||||
self._is_announcing = True
|
||||
self._set_state(AssistSatelliteState.RESPONDING)
|
||||
|
||||
# Provide our start info to the LLM so it understands context of incoming message
|
||||
if extra_system_prompt is not None:
|
||||
self._extra_system_prompt = extra_system_prompt
|
||||
@ -280,6 +295,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
raise
|
||||
finally:
|
||||
self._is_announcing = False
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
|
||||
async def async_start_conversation(
|
||||
self, start_announcement: AssistSatelliteAnnouncement
|
||||
@ -470,7 +486,10 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
return vad.VadSensitivity.to_seconds(vad_sensitivity)
|
||||
|
||||
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:
|
||||
"""Resolve the media ID."""
|
||||
media_id_source: Literal["url", "media_id", "tts"] | None = None
|
||||
@ -478,7 +497,6 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
|
||||
if media_id:
|
||||
original_media_id = media_id
|
||||
|
||||
else:
|
||||
media_id_source = "tts"
|
||||
# Synthesize audio and get URL
|
||||
@ -530,10 +548,26 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
# Resolve to full URL
|
||||
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(
|
||||
message=message,
|
||||
media_id=media_id,
|
||||
original_media_id=original_media_id,
|
||||
tts_token=tts_token,
|
||||
media_id_source=media_id_source,
|
||||
preannounce_media_id=preannounce_media_id,
|
||||
)
|
||||
|
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
Binary file not shown.
@ -8,12 +8,17 @@ announce:
|
||||
message:
|
||||
required: false
|
||||
example: "Time to wake up!"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
start_conversation:
|
||||
target:
|
||||
entity:
|
||||
@ -24,6 +29,7 @@ start_conversation:
|
||||
start_message:
|
||||
required: false
|
||||
example: "You left the lights on in the living room. Turn them off?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
start_media_id:
|
||||
@ -34,3 +40,7 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
|
@ -23,6 +23,10 @@
|
||||
"media_id": {
|
||||
"name": "Media ID",
|
||||
"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": {
|
||||
"name": "Extra system prompt",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,7 +198,8 @@ async def websocket_test_connection(
|
||||
|
||||
hass.async_create_background_task(
|
||||
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']}",
|
||||
)
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore", "botocore"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
||||
"requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""The Backup integration."""
|
||||
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM
|
||||
from homeassistant.const import Platform
|
||||
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.hassio import is_hassio
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@ -18,10 +20,12 @@ from .agent import (
|
||||
)
|
||||
from .config import BackupConfig, CreateBackupParametersDict
|
||||
from .const import DATA_MANAGER, DOMAIN
|
||||
from .coordinator import BackupConfigEntry, BackupDataUpdateCoordinator
|
||||
from .http import async_register_http_views
|
||||
from .manager import (
|
||||
BackupManager,
|
||||
BackupManagerError,
|
||||
BackupPlatformEvent,
|
||||
BackupPlatformProtocol,
|
||||
BackupReaderWriter,
|
||||
BackupReaderWriterError,
|
||||
@ -52,6 +56,7 @@ __all__ = [
|
||||
"BackupConfig",
|
||||
"BackupManagerError",
|
||||
"BackupNotFound",
|
||||
"BackupPlatformEvent",
|
||||
"BackupPlatformProtocol",
|
||||
"BackupReaderWriter",
|
||||
"BackupReaderWriterError",
|
||||
@ -74,6 +79,8 @@ __all__ = [
|
||||
"suggested_filename_from_name_date",
|
||||
]
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
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)
|
||||
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
|
||||
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)
|
||||
|
21
homeassistant/components/backup/config_flow.py
Normal file
21
homeassistant/components/backup/config_flow.py
Normal 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={})
|
@ -16,8 +16,8 @@ DATA_MANAGER: HassKey[BackupManager] = HassKey(DOMAIN)
|
||||
LOGGER = getLogger(__package__)
|
||||
|
||||
EXCLUDE_FROM_BACKUP = [
|
||||
"__pycache__/*",
|
||||
".DS_Store",
|
||||
"**/__pycache__/*",
|
||||
"**/.DS_Store",
|
||||
".HA_RESTORE",
|
||||
"*.db-shm",
|
||||
"*.log.*",
|
||||
|
81
homeassistant/components/backup/coordinator.py
Normal file
81
homeassistant/components/backup/coordinator.py
Normal 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()
|
27
homeassistant/components/backup/diagnostics.py
Normal file
27
homeassistant/components/backup/diagnostics.py
Normal 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]
|
||||
),
|
||||
}
|
36
homeassistant/components/backup/entity.py
Normal file
36
homeassistant/components/backup/entity.py
Normal 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",
|
||||
)
|
@ -229,6 +229,13 @@ class RestoreBackupEvent(ManagerStateEvent):
|
||||
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)
|
||||
class BlockedEvent(ManagerStateEvent):
|
||||
"""Backup manager blocked, Home Assistant is starting."""
|
||||
@ -355,6 +362,9 @@ class BackupManager:
|
||||
self._backup_event_subscriptions = hass.data[
|
||||
DATA_BACKUP
|
||||
].backup_event_subscriptions
|
||||
self._backup_platform_event_subscriptions = hass.data[
|
||||
DATA_BACKUP
|
||||
].backup_platform_event_subscriptions
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the backup manager."""
|
||||
@ -465,6 +475,9 @@ class BackupManager:
|
||||
LOGGER.debug("%s platforms loaded in total", len(self.platforms))
|
||||
LOGGER.debug("%s agents loaded in total", len(self.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:
|
||||
"""Perform pre backup actions."""
|
||||
@ -1713,7 +1726,9 @@ class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
"""Filter to filter 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
|
||||
LOGGER.debug("Ignoring %s because of %s", path, exclude)
|
||||
return True
|
||||
|
@ -5,8 +5,9 @@
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["http", "websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/backup",
|
||||
"integration_type": "system",
|
||||
"integration_type": "service",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["cronsim==2.6", "securetar==2025.2.1"]
|
||||
"requirements": ["cronsim==2.6", "securetar==2025.2.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
75
homeassistant/components/backup/sensor.py
Normal file
75
homeassistant/components/backup/sensor.py
Normal 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)
|
@ -22,5 +22,24 @@
|
||||
"name": "Create automatic backup",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
homeassistant/components/balay/__init__.py
Normal file
1
homeassistant/components/balay/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Balay virtual integration."""
|
6
homeassistant/components/balay/manifest.json
Normal file
6
homeassistant/components/balay/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "balay",
|
||||
"name": "Balay",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
@ -132,7 +132,7 @@
|
||||
"name": "Charging",
|
||||
"state": {
|
||||
"off": "Not charging",
|
||||
"on": "Charging"
|
||||
"on": "[%key:common::state::charging%]"
|
||||
}
|
||||
},
|
||||
"carbon_monoxide": {
|
||||
|
@ -37,7 +37,7 @@
|
||||
"vehicle_status": {
|
||||
"name": "Vehicle status",
|
||||
"state": {
|
||||
"standby": "Standby",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"vehicle_detected": "Detected",
|
||||
"ready": "Ready",
|
||||
"no_power": "No power",
|
||||
|
@ -6,7 +6,7 @@
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"region": "ConnectedDrive Region"
|
||||
"region": "ConnectedDrive region"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "The email address of your MyBMW/MINI Connected account.",
|
||||
@ -113,10 +113,10 @@
|
||||
},
|
||||
"select": {
|
||||
"ac_limit": {
|
||||
"name": "AC Charging Limit"
|
||||
"name": "AC charging limit"
|
||||
},
|
||||
"charging_mode": {
|
||||
"name": "Charging Mode",
|
||||
"name": "Charging mode",
|
||||
"state": {
|
||||
"immediate_charging": "Immediate charging",
|
||||
"delayed_charging": "Delayed charging",
|
||||
@ -181,7 +181,7 @@
|
||||
"cooling": "Cooling",
|
||||
"heating": "Heating",
|
||||
"inactive": "Inactive",
|
||||
"standby": "Standby",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"ventilation": "Ventilation"
|
||||
}
|
||||
},
|
||||
|
62
homeassistant/components/bosch_alarm/__init__.py
Normal file
62
homeassistant/components/bosch_alarm/__init__.py
Normal 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
|
109
homeassistant/components/bosch_alarm/alarm_control_panel.py
Normal file
109
homeassistant/components/bosch_alarm/alarm_control_panel.py
Normal 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)
|
165
homeassistant/components/bosch_alarm/config_flow.py
Normal file
165
homeassistant/components/bosch_alarm/config_flow.py
Normal 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,
|
||||
)
|
6
homeassistant/components/bosch_alarm/const.py
Normal file
6
homeassistant/components/bosch_alarm/const.py
Normal 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"
|
11
homeassistant/components/bosch_alarm/manifest.json
Normal file
11
homeassistant/components/bosch_alarm/manifest.json
Normal 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"]
|
||||
}
|
84
homeassistant/components/bosch_alarm/quality_scale.yaml
Normal file
84
homeassistant/components/bosch_alarm/quality_scale.yaml
Normal 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
|
36
homeassistant/components/bosch_alarm/strings.json
Normal file
36
homeassistant/components/bosch_alarm/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -170,6 +170,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=DEGREE,
|
||||
icon="mdi:compass-outline",
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="pressure",
|
||||
|
@ -142,6 +142,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""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
|
||||
|
||||
@property
|
||||
@ -169,6 +175,11 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
"""Last time the media position was 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
|
||||
def is_volume_muted(self) -> bool | None:
|
||||
"""Volume mute status."""
|
||||
|
@ -81,7 +81,7 @@ class ChromecastInfo:
|
||||
"+label%3A%22integration%3A+cast%22"
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
_LOGGER.info(
|
||||
(
|
||||
"Fetched cast details for unknown model '%s' manufacturer:"
|
||||
" '%s', type: '%s'. Please %s"
|
||||
|
@ -14,7 +14,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.6"],
|
||||
"requirements": ["PyChromecast==14.0.7"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except DIOChaconInvalidAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
|
@ -257,7 +257,7 @@
|
||||
"selector": {
|
||||
"hvac_mode": {
|
||||
"options": {
|
||||
"off": "Off",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"auto": "Auto",
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
|
@ -4,13 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
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 (
|
||||
FilesHandlerListEntry,
|
||||
async_files_delete_file,
|
||||
@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
|
||||
"""
|
||||
if not backup.protected:
|
||||
raise BackupAgentError("Cloud backups must be protected")
|
||||
if self._cloud.subscription_expired:
|
||||
raise BackupAgentError("Cloud subscription has expired")
|
||||
|
||||
size = backup.size
|
||||
try:
|
||||
@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
|
||||
) from err
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from 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:
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
tries += 1
|
||||
|
@ -9,7 +9,6 @@ from typing import Any
|
||||
import pycfdns
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -118,8 +117,6 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
@ -4,19 +4,19 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"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": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"title": "Choose the Zone to Update",
|
||||
"title": "Choose the zone to update",
|
||||
"data": {
|
||||
"zone": "Zone"
|
||||
}
|
||||
},
|
||||
"records": {
|
||||
"title": "Choose the Records to Update",
|
||||
"title": "Choose the records to update",
|
||||
"data": {
|
||||
"records": "Records"
|
||||
}
|
||||
@ -40,7 +40,7 @@
|
||||
"services": {
|
||||
"update_records": {
|
||||
"name": "Update records",
|
||||
"description": "Manually trigger update to Cloudflare records."
|
||||
"description": "Manually triggers an update of Cloudflare records."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
|
||||
|
||||
|
||||
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
|
||||
DISABLE: 0,
|
||||
HOME_P1: 1,
|
||||
HOME_P2: 2,
|
||||
NIGHT: 3,
|
||||
@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
|
||||
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
||||
}.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:
|
||||
"""Send disarm command."""
|
||||
if code != str(self._api.device_pin):
|
||||
return
|
||||
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:
|
||||
"""Send arm away command."""
|
||||
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:
|
||||
"""Send arm home command."""
|
||||
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:
|
||||
"""Send arm night command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
)
|
||||
|
@ -9,3 +9,5 @@ _LOGGER = logging.getLogger(__package__)
|
||||
DOMAIN = "comelit"
|
||||
DEFAULT_PORT = 80
|
||||
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
|
||||
|
||||
SCAN_INTERVAL = 5
|
||||
|
@ -22,7 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
|
||||
|
||||
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
|
||||
|
||||
@ -53,7 +53,7 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
logger=_LOGGER,
|
||||
config_entry=entry,
|
||||
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.async_get_or_create(
|
||||
|
@ -8,7 +8,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
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.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -98,13 +98,20 @@ class ComelitCoverEntity(
|
||||
"""Return if the cover is 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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""Stop the cover."""
|
||||
@ -112,13 +119,7 @@ class ComelitCoverEntity(
|
||||
return
|
||||
|
||||
action = STATE_ON if self.is_closing else STATE_OFF
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle device update."""
|
||||
self._last_state = self.state
|
||||
self.async_write_ha_state()
|
||||
await self._cover_set_state(action, 0)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
|
||||
async def _light_set_state(self, state: int) -> None:
|
||||
"""Set desired light 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:
|
||||
"""Turn the light on."""
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.11.2"]
|
||||
"requirements": ["aiocomelit==0.11.3"]
|
||||
}
|
||||
|
@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
|
||||
await self.coordinator.api.set_device_status(
|
||||
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:
|
||||
"""Turn the switch on."""
|
||||
|
1
homeassistant/components/constructa/__init__.py
Normal file
1
homeassistant/components/constructa/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Constructa virtual integration."""
|
6
homeassistant/components/constructa/manifest.json
Normal file
6
homeassistant/components/constructa/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "constructa",
|
||||
"name": "Constructa",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
from hassil.recognize import RecognizeResult
|
||||
@ -91,8 +90,6 @@ __all__ = [
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REGEX_TYPE = type(re.compile(""))
|
||||
|
||||
SERVICE_PROCESS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_TEXT): cv.string,
|
||||
|
@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
if (
|
||||
(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 (
|
||||
# Fewer unmatched 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_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 (
|
||||
# Prefer match failures with entities
|
||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||
|
@ -3,11 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from hassil.recognize import MISSING_ENTITY, RecognizeResult
|
||||
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
||||
from home_assistant_intents import get_language_scores
|
||||
import voluptuous as vol
|
||||
|
||||
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_sentences)
|
||||
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(
|
||||
@ -336,6 +339,36 @@ def _get_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):
|
||||
"""View to process text."""
|
||||
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"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"]
|
||||
}
|
||||
|
@ -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)
|
@ -6,7 +6,7 @@
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"country": "Country"
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "Email used to access your {cookidoo} account.",
|
||||
|
@ -38,10 +38,10 @@
|
||||
"name": "[%key:component::cover::title%]",
|
||||
"state": {
|
||||
"open": "[%key:common::state::open%]",
|
||||
"opening": "Opening",
|
||||
"opening": "[%key:common::state::opening%]",
|
||||
"closed": "[%key:common::state::closed%]",
|
||||
"closing": "Closing",
|
||||
"stopped": "Stopped"
|
||||
"closing": "[%key:common::state::closing%]",
|
||||
"stopped": "[%key:common::state::stopped%]"
|
||||
},
|
||||
"state_attributes": {
|
||||
"current_position": {
|
||||
|
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from ssl import SSLError
|
||||
from typing import Any
|
||||
|
||||
@ -21,6 +22,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Deluge."""
|
||||
@ -86,7 +89,8 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.hass.async_add_executor_job(api.connect)
|
||||
except (ConnectionRefusedError, TimeoutError, SSLError):
|
||||
return "cannot_connect"
|
||||
except Exception as ex: # noqa: BLE001
|
||||
except Exception as ex:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
if type(ex).__name__ == "BadLoginError":
|
||||
return "invalid_auth"
|
||||
return "unknown"
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
@ -43,7 +46,8 @@ class DexcomConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except AccountError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if "base" not in errors:
|
||||
|
@ -9,7 +9,7 @@ import pydiscovergy.error as discovergyError
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
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
|
||||
|
||||
@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -
|
||||
client = Discovergy(
|
||||
email=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
httpx_client=get_async_client(hass),
|
||||
httpx_client=create_async_httpx_client(hass),
|
||||
authentication=BasicAuth(),
|
||||
)
|
||||
|
||||
|
@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
username = auth["cdp_internal_user_id"].lower()
|
||||
username = auth["internalUserID"].lower()
|
||||
await self.async_set_unique_id(username)
|
||||
self._abort_if_unique_id_configured()
|
||||
email = auth["email"].lower()
|
||||
email = auth["loginEmailAddress"].lower()
|
||||
data = {
|
||||
CONF_EMAIL: email,
|
||||
CONF_USERNAME: username,
|
||||
|
@ -8,7 +8,11 @@ from aiodukeenergy import DukeEnergy
|
||||
from aiohttp import ClientError
|
||||
|
||||
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 (
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
@ -137,7 +141,7 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
|
||||
f"Duke Energy {meter['serviceType'].capitalize()} {serial_number}"
|
||||
)
|
||||
consumption_metadata = StatisticMetaData(
|
||||
has_mean=False,
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"{name_prefix} Consumption",
|
||||
source=DOMAIN,
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodukeenergy==0.2.2"]
|
||||
"requirements": ["aiodukeenergy==0.3.0"]
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ SENSOR_TYPES: tuple[EcoforestSensorEntityDescription, ...] = (
|
||||
),
|
||||
EcoforestSensorEntityDescription(
|
||||
key="convecto_air_flow",
|
||||
translation_key="convecto_air_flow",
|
||||
translation_key="convector_air_flow",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: data.convecto_air_flow,
|
||||
|
@ -78,8 +78,8 @@
|
||||
"extractor": {
|
||||
"name": "Extractor"
|
||||
},
|
||||
"convecto_air_flow": {
|
||||
"name": "Convecto air flow"
|
||||
"convector_air_flow": {
|
||||
"name": "Convector air flow"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
|
||||
def operation_list(self) -> list[str]:
|
||||
"""List of available operation modes."""
|
||||
econet_modes = self.water_heater.modes
|
||||
op_list = []
|
||||
operation_modes = set()
|
||||
for mode in econet_modes:
|
||||
if (
|
||||
mode is not WaterHeaterOperationMode.UNKNOWN
|
||||
and mode is not WaterHeaterOperationMode.VACATION
|
||||
):
|
||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||
op_list.append(ha_mode)
|
||||
return op_list
|
||||
operation_modes.add(ha_mode)
|
||||
return list(operation_modes)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"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"]
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
"step": {
|
||||
"auth": {
|
||||
"data": {
|
||||
"country": "Country",
|
||||
"country": "[%key:common::config_flow::data::country%]",
|
||||
"override_rest_url": "REST URL",
|
||||
"override_mqtt_url": "MQTT URL",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
@ -68,6 +68,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
|
||||
key="DEGREE",
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
EcoWittSensorTypes.WATT_METERS_SQUARED: SensorEntityDescription(
|
||||
key="WATT_METERS_SQUARED",
|
||||
|
@ -62,6 +62,7 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
except (ClientError, TimeoutError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unknown exception occurred")
|
||||
return self.async_abort(reason="unknown")
|
||||
await self.async_set_unique_id(hub.main.mac_address)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
|
@ -7,7 +7,11 @@ from typing import TYPE_CHECKING, cast
|
||||
|
||||
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 (
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
@ -144,7 +148,7 @@ class ElviaImporter:
|
||||
async_add_external_statistics(
|
||||
hass=self.hass,
|
||||
metadata=StatisticMetaData(
|
||||
has_mean=False,
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"{self.metering_point_id} Consumption",
|
||||
source=DOMAIN,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user